From 8714891c2ecd1a38cb6eb565801164bacb900a9f Mon Sep 17 00:00:00 2001 From: Mari Date: Sat, 5 Nov 2022 17:03:35 -0400 Subject: [PATCH] Refactor database, further progress on managers --- migrations/0001-characters.sql | 426 ++++++++++++++----------- src/bin/bot.ts | 7 +- src/commands/bot/index.ts | 4 +- src/commands/bot/rebuild.ts | 8 +- src/commands/bot/restart.ts | 8 +- src/commands/bot/shutdown.ts | 8 +- src/commands/character/create.ts | 8 + src/database/battle_types.ts | 66 +++- src/database/character_creation.ts | 64 ---- src/database/gender.ts | 58 ++++ src/database/{database.ts => index.ts} | 24 +- src/database/inmemory/database.ts | 10 - src/database/inmemory/users.ts | 46 --- src/database/pronouns.ts | 22 -- src/database/users.ts | 88 +---- src/managers/battle_types.ts | 115 +++++++ src/managers/character.ts | 13 + src/managers/gender.ts | 168 ++++++++++ src/managers/index.ts | 16 + src/managers/users.ts | 87 +++++ 20 files changed, 797 insertions(+), 449 deletions(-) delete mode 100644 src/database/character_creation.ts create mode 100644 src/database/gender.ts rename src/database/{database.ts => index.ts} (61%) delete mode 100644 src/database/inmemory/database.ts delete mode 100644 src/database/inmemory/users.ts delete mode 100644 src/database/pronouns.ts create mode 100644 src/managers/battle_types.ts create mode 100644 src/managers/character.ts create mode 100644 src/managers/gender.ts create mode 100644 src/managers/index.ts create mode 100644 src/managers/users.ts diff --git a/migrations/0001-characters.sql b/migrations/0001-characters.sql index f24d31c..c51fed5 100644 --- a/migrations/0001-characters.sql +++ b/migrations/0001-characters.sql @@ -16,75 +16,80 @@ CREATE TABLE IF NOT EXISTS users --changeset reya:battle_types_table runInTransaction:false CREATE TABLE IF NOT EXISTS battle_types ( - id INT NOT NULL PRIMARY KEY, - name STRING NOT NULL UNIQUE, - emoji STRING NOT NULL UNIQUE, - color STRING NOT NULL, - display_order INT NOT NULL, - immunities INT[] NOT NULL DEFAULT '{}', - resistances INT[] NOT NULL DEFAULT '{}', - weaknesses INT[] NOT NULL DEFAULT '{}' + id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(), + name STRING NOT NULL UNIQUE, + emoji STRING NOT NULL UNIQUE, + display_order SMALLINT NOT NULL ); --rollback DROP TABLE IF EXISTS battle_types; +--changeset reya:battle_types_relationships runInTransaction:false +CREATE TABLE IF NOT EXISTS battle_types_relationships +( + defender UUID NOT NULL REFERENCES battle_types (id) ON DELETE CASCADE, + attacker UUID NOT NULL REFERENCES battle_types (id) ON DELETE CASCADE, + damage_percentage SMALLINT NOT NULL DEFAULT 100, + PRIMARY KEY (defender, attacker) +); +--rollback DROP TABLE IF EXISTS battle_types_relationships + --changeset reya:battle_types_values runInTransaction:true -INSERT INTO battle_types (id, name, emoji, color, display_order) -VALUES (0, 'Basic', '🔲', '', 0), - (1, 'Sassy', '🔥', '', 1), - (2, 'Gentle', '💙', '', 2), - (3, 'Sexy', '💋', '', 3), - (4, 'Muscle', '💪', '', 4), - (5, 'Glam', '✨', '', 5), - (6, 'Punk', '🎸', '', 6), - (7, 'Glutton', '🍗', '', 7), - (8, 'Dumb', '🪨', '', 8), - (9, 'Drone', '🤖', '', 9), - (10, 'Spooky', '👻', '', 10), - (11, 'Lively', '🎉', '', 11), - (12, 'Smart', '🧠', '', 12), - (13, 'Cool', '🧊', '', 13), - (14, 'Bully', '😈', '', 14), - (15, 'Mythic', '🔱', '', 15), - (16, 'Toy', '🧸', '', 16), - (17, 'Cute', '🌺', '', 17) +INSERT INTO battle_types (name, emoji, display_order) +VALUES ('Basic', '🔲', 0), + ('Sassy', '🔥', 1), + ('Gentle', '💙', 2), + ('Sexy', '💋', 3), + ('Muscle', '💪', 4), + ('Glam', '✨', 5), + ('Punk', '🎸', 6), + ('Glutton', '🍗', 7), + ('Dumb', '🪨', 8), + ('Drone', '🤖', 9), + ('Spooky', '👻', 10), + ('Lively', '🎉', 11), + ('Smart', '🧠', 12), + ('Cool', '🧊', 13), + ('Bully', '😈', 14), + ('Mythic', '🔱', 15), + ('Toy', '🧸', 16), + ('Cute', '🌺', 17) ON CONFLICT DO NOTHING; ---rollback DELETE FROM battle_types WHERE id in (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17) +--rollback TRUNCATE battle_types; --changeset reya:difficulties_table runInTransaction:false CREATE TABLE IF NOT EXISTS difficulties ( - id INT NOT NULL PRIMARY KEY, - name STRING NOT NULL UNIQUE, - emoji STRING NOT NULL UNIQUE, - short_description STRING NOT NULL, - long_description STRING NOT NULL, - display_order INT NOT NULL, - allow_digestion BOOLEAN NOT NULL DEFAULT true, - regenerate_talent BOOLEAN NOT NULL DEFAULT false, - reformation_health_percent FLOAT NOT NULL DEFAULT 100, - reformation_stamina_percent FLOAT NOT NULL DEFAULT 100, - lose_proficiency_percent FLOAT NOT NULL DEFAULT 0, - lose_exp_percent FLOAT NOT NULL DEFAULT 0, - lose_money_percent FLOAT NOT NULL DEFAULT 0 + id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(), + name STRING NOT NULL UNIQUE, + emoji STRING NOT NULL UNIQUE, + short_description STRING NOT NULL, + long_description STRING NOT NULL, + display_order SMALLINT NOT NULL, + allow_digestion BOOLEAN NOT NULL DEFAULT true, + regenerate_talent BOOLEAN NOT NULL DEFAULT false, + reformation_health_percent FLOAT NOT NULL DEFAULT 100, + reformation_stamina_percent FLOAT NOT NULL DEFAULT 100, + lose_proficiency_percent FLOAT NOT NULL DEFAULT 0, + lose_exp_percent FLOAT NOT NULL DEFAULT 0, + lose_money_percent FLOAT NOT NULL DEFAULT 0 ); --rollback DROP TABLE IF EXISTS difficulties; --changeset reya:difficulties_values runInTransaction:true INSERT INTO difficulties -(id, name, emoji, short_description, long_description, display_order, allow_digestion, regenerate_talent, +(name, emoji, short_description, long_description, display_order, allow_digestion, regenerate_talent, reformation_health_percent, reformation_stamina_percent, lose_proficiency_percent, lose_exp_percent, lose_money_percent) -VALUES (0, 'Indigestible', '🔰', +VALUES ('Indigestible', '🌞', 'You can''t be digested. Stomachs just make you sleepy, and you always come out none the worse for wear.', 'Stomachs have no effect on you. When your health is reduced to 0, you simply become tired and drift off to ' - || - 'sleep. You don''t need to be reformed after battles when this happens, and you won''t lose anything for ' - || 'falling asleep this way. You''ll be ready to go right away as soon as you''re released!' + || 'sleep. You don''t need to be reformed after battles when this happens, and you won''t lose anything ' + || 'for falling asleep this way. You''ll be ready to go right away as soon as you''re released!' || e'\n\n' || 'Suitable for players who play with the Pred Only or Spectator preferences, or who don''t want their ' || 'character to die even temporarily.', 0, false, false, 100, 100, 0, 0, 0), - (1, 'Very Safe', '☀', + ('Very Safe', '☀', 'You''ll be completely safe. The only downsides to being digested? Inconvenience. And smug predators.', 'Reformation is a cakewalk for you. You can be digested, but you can reform after battle with no penalties ' || 'of any kind. Your body and mind are completely unaffected by the visit to someone else''s gut.' @@ -92,11 +97,11 @@ VALUES (0, 'Indigestible', '🔰', || 'Reformation leaves you a little bit weary, but in good health.' || e'\n\n' || 'Suitable for players who prefer a digestive end, but don''t want to lose any progress.', - 1, true, false, 100, 90, 0, 0, 0), - (2, 'Safe', '🌤', + 1, true, false, 95, 90, 0, 0, 0), + ('Safe', '🌤', 'There''s a small price to being digested, but you didn''t think cheating death would be free, right?', 'Digestion takes a little bit of a toll on your body and mind. And while you do come back, you don''t come ' - || 'back _quite_ the way you left.' + || 'back _quite_ the way you left. ' || 'Your proficiencies will drop by 10% as a little of what you''ve taken from your past exploits fades ' || 'from you. You''ll also lose 10% of your money as a cost for the reformation process.' || e'\n\n' @@ -104,8 +109,8 @@ VALUES (0, 'Indigestible', '🔰', || e'\n\n' || 'Suitable for players who want to add a little risk to being digested, but don''t want to lose levels.', 2, true, false, 90, 75, 10, 0, 10), - (3, 'Risky', '🌥', - 'Reformation is a painful experience. It''s no laughing matter to be digested, so watch your back.', + ('Risky', '🌥', + 'Reformation is an unpleasant experience, but you can handle it if you have to.', 'Digestion is no walk in the park for you. When you come back, you leave some of yourself in the predator who ' || 'turned you from a person to a snack. Your proficiencies drop by 25% as some of what you''ve taken from ' || 'your past conquests fades from you, and your talents will be re-randomized, as the process of ' @@ -113,152 +118,194 @@ VALUES (0, 'Indigestible', '🔰', || 'lose 25% of your money as the cost of returning from being belly paunch. Worst of all, you''ll lose ' || '10% of your experience points as your memories are clouded by your brush with fatality.' || e'\n\n' - || '' + || 'After reforming, you''ll need some time to rest before you''re back at your peak.' || e'\n\n' || 'Suitable for players who want a balance between risk and maintaining their progress.', - 3, true, true, 25, 10, 25), - (4, 'Dangerous', '🌦', - 'Digestion pushes your', - 'Digestion is something to be avoided at all costs. You''ll lose half your money, your stats will have atrophied, and you''ll lose some of your memories.', - 4, true, true, 50, 25, 50), - (5, 'Very Dangerous', '🌧', '', - 'Being a meal is not just humiliating - it''s a nightmare. You''re lucky you get to hold on to anything.', - 5, true, true, 90, 50, 90), - (6, 'Extremely Dangerous', '⛈', '', - 'If you get devoured and you don''t get out, you''ll lose just about everything. Be very, very careful...', - 6, true, true, 100, 100, 100) + 3, true, true, 75, 60, 25, 10, 25), + ('Dangerous', '🌦', + 'You need to be careful around predators, as you''ll be in real trouble if you melt away.', + 'Digestion is something to be avoided at all costs. The process of being revived is expensive, costing a full ' + || '50% of your available money. You''ll also lose 50% of your proficiencies and 25% of your experience ' + || 'points as your mind and body are imperfectly restored. Your talents will be regenerated, as well.' + || e'\n\n' + || 'After reforming, you will be tired and still vulnerable to other predators. Take some time to rest ' + || 'after a defeat, or you''ll find yourself back in another stomach before you know it.' + || e'\n\n' + || 'Suitable for players who want digestion to be particularly painful, but not devastating.', + 4, true, true, 50, 30, 50, 25, 50), + ('Very Dangerous', '🌧', + 'The world is cruel. Once you start getting digested, it''s hard to stop.', + 'Being a meal is not just humiliating - it''s a nightmare. You''re lucky you still remember who you are when ' + || 'you stumble out of the reformation chamber. You''ll lose 90% of your money to the reformation fee, ' + || 'and 90% of your proficiency points will be drained away as the reformation process leaves you ' + || 'seriously addled and barely feeling like yourself. Your experience point total will also drop by 50%.' + || e'\n\n' + || 'After reforming, you will be very weak and exhausted. Make sure to rest and recover before you get ' + || 'into another fight, or you''ll lose another large chunk of progress.' + || e'\n\n' + || 'Suitable for players who want digestion to be a devastating loss.', + 5, true, true, 25, 15, 90, 50, 90), + ('Extremely Dangerous', '⛈', + 'Be very, very careful. There''s no coming back from digestion. Not really. You''ll lose everything...', + 'After being digested, your experience points, proficiencies, and money will all be set to 0, as if you ' + || 'had just stumbled blinking into this world for the first time. You''ll be able to keep your items, ' + || 'but that''s little comfort as your body and mind are returned to life as a blank slate...' + || e'\n\n' + || 'After reforming, you will have 1 Health and Energy. You definitely need to spend some time resting - ' + || 'or you won''t be able to break out of the cycle of digestion.' + || e'\n\n' + || 'Suitable for players who want a soft-perma type of experience.', + 6, true, true, 0, 0, 100, 100, 100) ON CONFLICT DO NOTHING; ---rollback DELETE FROM difficulties WHERE id IN (0, 1, 2, 3, 4, 5, 6, 7); +--rollback TRUNCATE difficulties; --changeset reya:preferences_table runInTransaction:false CREATE TABLE IF NOT EXISTS preferences ( - id INT NOT NULL PRIMARY KEY, - name STRING NOT NULL UNIQUE, - emoji STRING NOT NULL UNIQUE, - short_description STRING NOT NULL, - long_description STRING NOT NULL, - display_order INT NOT NULL, - can_use_vore BOOLEAN NOT NULL, - can_receive_vore BOOLEAN NOT NULL + id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(), + name STRING NOT NULL UNIQUE, + emoji STRING NOT NULL UNIQUE, + short_description STRING NOT NULL, + display_order SMALLINT NOT NULL, + can_use_vore BOOLEAN NOT NULL, + can_receive_vore BOOLEAN NOT NULL ); --rollback DROP TABLE IF EXISTS preferences; --changeset reya:preferences_values runInTransaction:true -INSERT INTO preferences (id, name, emoji, short_description, long_description, display_order, can_use_vore, +INSERT INTO preferences (name, emoji, short_description, display_order, can_use_vore, can_receive_vore) -VALUES (0, 'Observer', '', 'You can neither eat nor be eaten.', '', 0, false, false), - (1, 'Prey Only', '', 'You can only be eaten, not eat.', '', 1, false, true), - (2, 'Pred Only', '', 'You can only eat, not be eaten.', '', 2, true, false), - (3, 'Switch', '', 'You can both eat and be eaten.', '', 3, true, true) +VALUES ('Switch', '😃', 'You can both eat and be eaten.', 0, true, true), + ('Pred Only', '🤤', 'You can only eat, not be eaten.', 1, true, false), + ('Prey Only', '🥩', 'You can only be eaten, not eat.', 2, false, true), + ('Observer', '👀', 'You can neither eat nor be eaten.', 3, false, false) +ON CONFLICT DO NOTHING; +--rollback TRUNCATE preferences; + +--changeset reya:gender_markers_table runInTransaction:false +CREATE TABLE IF NOT EXISTS gender_markers +( + emoji STRING NOT NULL PRIMARY KEY, + default_name STRING NOT NULL UNIQUE, + display_order SMALLINT NOT NULL +); +--rollback DROP TABLE IF EXISTS gender_markers; + +--changeset reya:gender_markers runInTransaction:true +INSERT INTO gender_markers (default_name, emoji, display_order) +VALUES ('Non-binary', '⚧', 0), + ('Female', '♀️', 1), + ('Male', '♂️', 2), + ('Non*binary', '✳️', 3), + ('Genderless', '⬜', 4) ON CONFLICT DO NOTHING; ---rollback DELETE FROM preferences WHERE id IN (0, 1, 2, 3); +--rollback TRUNCATE gender_markers; --changeset reya:pronouns_table runInTransaction:false CREATE TABLE IF NOT EXISTS pronouns ( - id INT NOT NULL PRIMARY KEY, - default_gender STRING NOT NULL, - pronouns STRING NOT NULL UNIQUE, - display_order INT NOT NULL, - use_plural BOOLEAN NOT NULL, - subjective STRING NOT NULL, - adjective STRING NOT NULL, - possessive STRING NOT NULL, - reflexive STRING NOT NULL, - objective STRING NOT NULL + id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(), + default_marker STRING NOT NULL REFERENCES gender_markers (emoji) ON DELETE RESTRICT, + name STRING NOT NULL UNIQUE, + display_order SMALLINT NOT NULL, + use_plural BOOLEAN NOT NULL, + subjective STRING NOT NULL, + adjective STRING NOT NULL, + possessive STRING NOT NULL, + reflexive STRING NOT NULL, + objective STRING NOT NULL, + UNIQUE (subjective, adjective, possessive, reflexive, objective) ); --rollback DROP TABLE IF EXISTS pronouns; --changeset reya:pronouns_values runInTransaction:true -INSERT INTO pronouns (id, default_gender, pronouns, display_order, use_plural, subjective, adjective, possessive, +INSERT INTO pronouns (default_marker, name, display_order, use_plural, subjective, adjective, possessive, reflexive, objective) -VALUES (0, 'Genderless', 'none', 0, false, '@@', '@@''s', '@@''s', '@@''s self', '@@'), - (1, 'Female', 'she/her', 1, false, 'she', 'her', 'hers', 'herself', 'her'), - (2, 'Non-binary', 'they/them', 2, true, 'they', 'their', 'theirs', 'themself', 'them'), - (3, 'Male', 'he/him', 3, false, 'he', 'his', 'his', 'himself', 'him'), - (4, 'Genderless', 'it/its', 4, false, 'it', 'its', 'its', 'itself', 'it'), - (5, 'Herm', 'shi/hir', 5, false, 'shi', 'hir', 'hirs', 'hirself', 'hir'), - (6, 'Non-binary', 'ae/aer', 6, false, 'ae', 'aer', 'aers', 'aerself', 'aer'), - (7, 'Non-binary', 'fae/faer', 7, false, 'fae', 'faer', 'faers', 'faerself', 'faer'), - (8, 'Non-binary', 'e/em', 8, false, 'e', 'eir', 'eirs', 'emself', 'em'), - (9, 'Non-binary', 'ey/em', 9, false, 'ey', 'eir', 'eirs', 'emself', 'em'), - (10, 'Non-binary', 'per/per', 10, false, 'per', 'pers', 'pers', 'perself', 'per'), - (11, 'Non-binary', 've/ver', 11, false, 've', 'vis', 'vis', 'verself', 'ver'), - (12, 'Non-binary', 'xe/xem', 12, false, 'xe', 'xyr', 'xyrs', 'xemself', 'xem'), - (13, 'Non-binary', 'ze/hir', 13, false, 'ze', 'hir', 'hirs', 'hirself', 'hir'), - (14, 'Non-binary', 'zie/hir', 14, false, 'zie', 'hir', 'hirs', 'hirself', 'hir'), - (15, 'Non-binary', 'zie/zim', 15, false, 'zie', 'zir', 'zis', 'zieself', 'zim'), - (16, 'Non-binary', 'sie/sie', 16, false, 'sie', 'hir', 'hirs', 'hirself', 'sie'), - (17, 'Non-binary', 'te/ter', 17, false, 'te', 'tem', 'ters', 'terself', 'ter') +VALUES ('⬜', 'none', 0, false, '@@', '@@''s', '@@''s', '@@''s self', '@@'), + ('♀', 'she/her', 1, false, 'she', 'her', 'hers', 'herself', 'her'), + ('⚧', 'they/them', 2, true, 'they', 'their', 'theirs', 'themself', 'them'), + ('♂️', 'he/him', 3, false, 'he', 'his', 'his', 'himself', 'him'), + ('⬜', 'it/its', 4, false, 'it', 'its', 'its', 'itself', 'it'), + ('⚧', 'shi/hir', 5, false, 'shi', 'hir', 'hirs', 'hirself', 'hir'), + ('✳️', 'fae/faer', 6, false, 'fae', 'faer', 'faers', 'faerself', 'faer'), + ('✳️', 'ae/aer', 7, false, 'ae', 'aer', 'aers', 'aerself', 'aer'), + ('✳️', 'e/em', 8, false, 'e', 'eir', 'eirs', 'emself', 'em'), + ('✳️', 'ey/em', 9, false, 'ey', 'eir', 'eirs', 'emself', 'em'), + ('✳️', 'per/per', 10, false, 'per', 'pers', 'pers', 'perself', 'per'), + ('✳️', 've/ver', 11, false, 've', 'vis', 'vis', 'verself', 'ver'), + ('✳️', 'xe/xem', 12, false, 'xe', 'xyr', 'xyrs', 'xemself', 'xem'), + ('✳️', 'ze/hir', 13, false, 'ze', 'hir', 'hirs', 'hirself', 'hir'), + ('✳️', 'zie/hir', 14, false, 'zie', 'hir', 'hirs', 'hirself', 'hir'), + ('✳️', 'zie/zim', 15, false, 'zie', 'zir', 'zis', 'zieself', 'zim'), + ('✳️', 'sie/sie', 16, false, 'sie', 'hir', 'hirs', 'hirself', 'sie'), + ('✳️', 'te/ter', 17, false, 'te', 'tem', 'ters', 'terself', 'ter') ON CONFLICT DO NOTHING; ---rollback DELETE FROM pronouns WHERE id IN (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17); +--rollback DELETE FROM pronouns; --changeset reya:characters_table runInTransaction:false CREATE TABLE IF NOT EXISTS characters ( - id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID NOT NULL REFERENCES users (id) ON DELETE CASCADE, - name STRING NOT NULL, - discriminator INT NULL DEFAULT NULL, - title STRING NOT NULL, - profile STRING NOT NULL, - pronouns_id INT NOT NULL REFERENCES pronouns (id) ON DELETE RESTRICT, - gender_name STRING NOT NULL, - difficulty_id INT NOT NULL REFERENCES difficulties (id) ON DELETE RESTRICT, - preference_id INT NOT NULL REFERENCES preferences (id) ON DELETE RESTRICT, - battle_type1_id INT NOT NULL REFERENCES battle_types (id) ON DELETE RESTRICT, - battle_type2_id INT NOT NULL REFERENCES battle_types (id) ON DELETE RESTRICT, - experience INT NOT NULL DEFAULT 0, - money INT NOT NULL DEFAULT 0, + id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users (id) ON DELETE CASCADE, + archived BOOLEAN NOT NULL DEFAULT FALSE, + name STRING NOT NULL, + title STRING NOT NULL, + profile STRING NOT NULL, + pronouns_id SMALLINT NOT NULL REFERENCES pronouns (id) ON DELETE RESTRICT, + gender_marker STRING NOT NULL REFERENCES gender_markers (emoji) ON DELETE RESTRICT, + gender_name STRING NOT NULL, + difficulty_id SMALLINT NOT NULL REFERENCES difficulties (id) ON DELETE RESTRICT, + preference_id SMALLINT NOT NULL REFERENCES preferences (id) ON DELETE RESTRICT, + battle_type1_id SMALLINT NOT NULL REFERENCES battle_types (id) ON DELETE RESTRICT, + battle_type2_id SMALLINT NOT NULL REFERENCES battle_types (id) ON DELETE RESTRICT, + experience SMALLINT NOT NULL DEFAULT 0, + money SMALLINT NOT NULL DEFAULT 0, -- stats: base - base_confidence INT NOT NULL DEFAULT 70, - base_health INT NOT NULL DEFAULT 70, - base_stamina INT NOT NULL DEFAULT 70, - base_brawn INT NOT NULL DEFAULT 70, - base_durability INT NOT NULL DEFAULT 70, - base_intensity INT NOT NULL DEFAULT 70, - base_resilience INT NOT NULL DEFAULT 70, - base_speed INT NOT NULL DEFAULT 70, + base_confidence SMALLINT NOT NULL DEFAULT 70, + base_health SMALLINT NOT NULL DEFAULT 70, + base_stamina SMALLINT NOT NULL DEFAULT 70, + base_brawn SMALLINT NOT NULL DEFAULT 70, + base_durability SMALLINT NOT NULL DEFAULT 70, + base_intensity SMALLINT NOT NULL DEFAULT 70, + base_resilience SMALLINT NOT NULL DEFAULT 70, + base_speed SMALLINT NOT NULL DEFAULT 70, -- stats: banked talent points - min_confidence_talent INT NOT NULL DEFAULT 0, - min_health_talent INT NOT NULL DEFAULT 0, - min_stamina_talent INT NOT NULL DEFAULT 0, - min_brawn_talent INT NOT NULL DEFAULT 0, - min_durability_talent INT NOT NULL DEFAULT 0, - min_intensity_talent INT NOT NULL DEFAULT 0, - min_resilience_talent INT NOT NULL DEFAULT 0, - min_speed_talent INT NOT NULL DEFAULT 0, + min_confidence_talent SMALLINT NOT NULL DEFAULT 0, + min_health_talent SMALLINT NOT NULL DEFAULT 0, + min_stamina_talent SMALLINT NOT NULL DEFAULT 0, + min_brawn_talent SMALLINT NOT NULL DEFAULT 0, + min_durability_talent SMALLINT NOT NULL DEFAULT 0, + min_intensity_talent SMALLINT NOT NULL DEFAULT 0, + min_resilience_talent SMALLINT NOT NULL DEFAULT 0, + min_speed_talent SMALLINT NOT NULL DEFAULT 0, -- stats: current talent points - confidence_talent INT NOT NULL DEFAULT floor(random() * 32)::INT, --min_confidence_talent + floor(random() * (32 - min_confidence_talent)), - health_talent INT NOT NULL DEFAULT floor(random() * 32)::INT, --min_health_talent + floor(random() * (32 - min_health_talent)), - stamina_talent INT NOT NULL DEFAULT floor(random() * 32)::INT, --min_stamina_talent + floor(random() * (32 - min_stamina_talent)), - brawn_talent INT NOT NULL DEFAULT floor(random() * 32)::INT, --min_brawn_talent + floor(random() * (32 - min_brawn_talent)), - durability_talent INT NOT NULL DEFAULT floor(random() * 32)::INT, --min_durability_talent + floor(random() * (32 - min_durability_talent)), - intensity_talent INT NOT NULL DEFAULT floor(random() * 32)::INT, --min_intensity_talent + floor(random() * (32 - min_intensity_talent)), - resilience_talent INT NOT NULL DEFAULT floor(random() * 32)::INT, --min_resilience_talent + floor(random() * (32 - min_resilience_talent)), - speed_talent INT NOT NULL DEFAULT floor(random() * 32)::INT, --min_speed_talent + floor(random() * (32 - min_speed_talent)), + confidence_talent SMALLINT NOT NULL DEFAULT floor(random() * 32)::SMALLINT, --min_confidence_talent + floor(random() * (32 - min_confidence_talent)), + health_talent SMALLINT NOT NULL DEFAULT floor(random() * 32)::SMALLINT, --min_health_talent + floor(random() * (32 - min_health_talent)), + stamina_talent SMALLINT NOT NULL DEFAULT floor(random() * 32)::SMALLINT, --min_stamina_talent + floor(random() * (32 - min_stamina_talent)), + brawn_talent SMALLINT NOT NULL DEFAULT floor(random() * 32)::SMALLINT, --min_brawn_talent + floor(random() * (32 - min_brawn_talent)), + durability_talent SMALLINT NOT NULL DEFAULT floor(random() * 32)::SMALLINT, --min_durability_talent + floor(random() * (32 - min_durability_talent)), + intensity_talent SMALLINT NOT NULL DEFAULT floor(random() * 32)::SMALLINT, --min_intensity_talent + floor(random() * (32 - min_intensity_talent)), + resilience_talent SMALLINT NOT NULL DEFAULT floor(random() * 32)::SMALLINT, --min_resilience_talent + floor(random() * (32 - min_resilience_talent)), + speed_talent SMALLINT NOT NULL DEFAULT floor(random() * 32)::SMALLINT, --min_speed_talent + floor(random() * (32 - min_speed_talent)), -- stats: current proficiency points - confidence_proficiency INT NOT NULL DEFAULT 0, - health_proficiency INT NOT NULL DEFAULT 0, - stamina_proficiency INT NOT NULL DEFAULT 0, - brawn_proficiency INT NOT NULL DEFAULT 0, - durability_proficiency INT NOT NULL DEFAULT 0, - intensity_proficiency INT NOT NULL DEFAULT 0, - resilience_proficiency INT NOT NULL DEFAULT 0, - speed_proficiency INT NOT NULL DEFAULT 0, + confidence_proficiency SMALLINT NOT NULL DEFAULT 0, + health_proficiency SMALLINT NOT NULL DEFAULT 0, + stamina_proficiency SMALLINT NOT NULL DEFAULT 0, + brawn_proficiency SMALLINT NOT NULL DEFAULT 0, + durability_proficiency SMALLINT NOT NULL DEFAULT 0, + intensity_proficiency SMALLINT NOT NULL DEFAULT 0, + resilience_proficiency SMALLINT NOT NULL DEFAULT 0, + speed_proficiency SMALLINT NOT NULL DEFAULT 0, - UNIQUE (user_id, name, discriminator), - FAMILY character_base (id, user_id, name, title, profile, pronouns_id, battle_type1_id, battle_type2_id, - base_confidence, - base_health, - base_stamina, base_brawn, base_durability, base_intensity, base_resilience, base_speed), + FAMILY character_base (id, user_id, name, title, profile, pronouns_id, gender_marker, gender_name, + battle_type1_id, battle_type2_id, base_confidence, base_health, base_stamina, + base_brawn, base_durability, base_intensity, base_resilience, base_speed), FAMILY character_reformation_stats (min_confidence_talent, min_health_talent, min_stamina_talent, min_brawn_talent, min_durability_talent, min_intensity_talent, min_resilience_talent, - min_speed_talent, - confidence_talent, health_talent, stamina_talent, brawn_talent, - durability_talent, intensity_talent, resilience_talent, speed_talent), + min_speed_talent, confidence_talent, health_talent, stamina_talent, + brawn_talent, durability_talent, intensity_talent, resilience_talent, + speed_talent), FAMILY character_live_stats (difficulty_id, preference_id, experience, money, confidence_proficiency, health_proficiency, stamina_proficiency, brawn_proficiency, durability_proficiency, intensity_proficiency, resilience_proficiency, speed_proficiency) @@ -268,38 +315,39 @@ CREATE TABLE IF NOT EXISTS characters --changeset reya:character_creation_table runInTransaction:false CREATE TABLE IF NOT EXISTS character_creation ( - user_id UUID NOT NULL REFERENCES users (id) ON DELETE CASCADE, - character_id UUID NULL REFERENCES characters (id) ON DELETE CASCADE, - name STRING NULL, - title STRING NULL, - profile STRING NULL, - pronouns_id INT NULL REFERENCES pronouns (id) ON DELETE RESTRICT, - gender_name STRING NULL, - difficulty_id INT NULL REFERENCES difficulties (id) ON DELETE RESTRICT, - preference_id INT NULL REFERENCES preferences (id) ON DELETE RESTRICT, - battle_type1_id INT NULL REFERENCES battle_types (id) ON DELETE RESTRICT, - battle_type2_id INT NULL REFERENCES battle_types (id) ON DELETE RESTRICT, - base_confidence INT NULL DEFAULT 70, - base_health INT NULL DEFAULT 70, - base_stamina INT NULL DEFAULT 70, - base_brawn INT NULL DEFAULT 70, - base_durability INT NULL DEFAULT 70, - base_intensity INT NULL DEFAULT 70, - base_resilience INT NULL DEFAULT 70, - base_speed INT NULL DEFAULT 70, - - PRIMARY KEY (user_id, character_id) + user_id UUID NOT NULL REFERENCES users (id) ON DELETE CASCADE PRIMARY KEY, + character_id UUID NULL REFERENCES characters (id) ON DELETE CASCADE, + name STRING NULL, + title STRING NULL, + profile STRING NULL, + pronouns_id UUID NULL REFERENCES pronouns (id) ON DELETE SET NULL, + gender_marker STRING NULL REFERENCES gender_markers (emoji) ON DELETE SET NULL, + gender_name STRING NULL, + difficulty_id UUID NULL REFERENCES difficulties (id) ON DELETE SET NULL, + preference_id UUID NULL REFERENCES preferences (id) ON DELETE SET NULL, + battle_type1_id UUID NULL REFERENCES battle_types (id) ON DELETE SET NULL, + battle_type2_id UUID NULL REFERENCES battle_types (id) ON DELETE SET NULL, + base_confidence SMALLINT NULL DEFAULT 70, + base_health SMALLINT NULL DEFAULT 70, + base_stamina SMALLINT NULL DEFAULT 70, + base_brawn SMALLINT NULL DEFAULT 70, + base_durability SMALLINT NULL DEFAULT 70, + base_intensity SMALLINT NULL DEFAULT 70, + base_resilience SMALLINT NULL DEFAULT 70, + base_speed SMALLINT NULL DEFAULT 70 ); --rollback DROP TABLE IF EXISTS character_creation; --changeset reya:userDefaultDifficultyPreferenceGender ALTER TABLE users - ADD COLUMN default_pronouns_id INT NULL REFERENCES pronouns (id) ON DELETE RESTRICT DEFAULT NULL + ADD COLUMN default_pronouns_id UUID NULL REFERENCES pronouns (id) ON DELETE RESTRICT DEFAULT NULL CREATE IF NOT EXISTS FAMILY character_defaults, - ADD COLUMN default_gender_name STRING NULL DEFAULT NULL + ADD COLUMN default_gender_marker STRING NULL REFERENCES gender_markers (emoji) ON DELETE RESTRICT DEFAULT NULL + FAMILY character_defaults, + ADD COLUMN default_gender_name STRING NULL DEFAULT NULL FAMILY character_defaults, - ADD COLUMN default_difficulty_id INT NULL REFERENCES difficulties (id) ON DELETE RESTRICT DEFAULT NULL + ADD COLUMN default_difficulty_id UUID NULL REFERENCES difficulties (id) ON DELETE RESTRICT DEFAULT NULL FAMILY character_defaults, - ADD COLUMN default_preference_id INT NULL REFERENCES preferences (id) ON DELETE RESTRICT DEFAULT NULL + ADD COLUMN default_preference_id UUID NULL REFERENCES preferences (id) ON DELETE RESTRICT DEFAULT NULL FAMILY character_defaults; ---rollback ALTER \ No newline at end of file +--rollback ALTER TABLE users DROP COLUMN default_pronouns_id, DROP COLUMN default_gender_marker, DROP COLUMN default_gender_name, DROP COLUMN default_difficulty_id, DROP COLUMN default_preference_id \ No newline at end of file diff --git a/src/bin/bot.ts b/src/bin/bot.ts index 60a6bd2..6ada802 100644 --- a/src/bin/bot.ts +++ b/src/bin/bot.ts @@ -5,7 +5,7 @@ import {Commands} from "../commands/index.js" import {checkIsRestart, reportFailed, reportReady, reportStarted} from "../ipc/restart.js" import {defaultPresence} from "../defaultPresence.js" import {Pool} from "pg" -import {Database, DatabaseImpl, makeTransactable} from "../database/database.js" +import {Database, DatabaseImpl, makeTransact} from "../database" async function bot() { await checkIsRestart() @@ -16,6 +16,9 @@ async function bot() { const p = new Pool({ application_name: "VoreRPG Bot", }) + p.on("error", (err) => { + console.log(err) + }) async function cleanUp() { await p.end() @@ -24,7 +27,7 @@ async function bot() { const db: Database = new DatabaseImpl({ query: p.query.bind(p), - transactable: makeTransactable(p), + transact: makeTransact(p), }) const cmd = new Commands({users: db.users, cleanUp}) diff --git a/src/commands/bot/index.ts b/src/commands/bot/index.ts index ba8e2c6..9131d33 100644 --- a/src/commands/bot/index.ts +++ b/src/commands/bot/index.ts @@ -3,7 +3,7 @@ import {ApplicationCommandType} from "discord.js" import {BotRestartCommand} from "./restart" import {BotShutdownCommand} from "./shutdown" import {BotRebuildCommand} from "./rebuild" -import {UsersTable} from "../../database/users.js" +import {UsersManager} from "../../managers/users"; export class BotCommand extends CommandWithSubcommandsData { readonly rebuild: BotRebuildCommand @@ -11,7 +11,7 @@ export class BotCommand extends CommandWithSubcommandsData { readonly shutdown: BotShutdownCommand readonly subcommands: SubcommandData[] - constructor({users, cleanUp}: { users: UsersTable, cleanUp: () => Promise }) { + constructor({users, cleanUp}: { users: UsersManager, cleanUp: () => Promise }) { super() this.rebuild = new BotRebuildCommand({users, cleanUp}) this.restart = new BotRestartCommand({users, cleanUp}) diff --git a/src/commands/bot/rebuild.ts b/src/commands/bot/rebuild.ts index b207c1a..646e239 100644 --- a/src/commands/bot/rebuild.ts +++ b/src/commands/bot/rebuild.ts @@ -9,13 +9,13 @@ import {wrappedRestart} from "../../ipc/restart" import {fork} from "child_process" import {defaultPresence} from "../../defaultPresence" import {resolve as resolvePath} from "path" -import {UsersTable} from "../../database/users.js" +import {UsersManager} from "../../managers/users"; export class BotRebuildCommand extends SubcommandData { - private readonly _users: UsersTable + private readonly _users: UsersManager private readonly _cleanUp: () => Promise - constructor({users, cleanUp}: { users: UsersTable, cleanUp(): Promise }) { + constructor({users, cleanUp}: { users: UsersManager, cleanUp(): Promise }) { super() this._users = users this._cleanUp = cleanUp @@ -28,7 +28,7 @@ export class BotRebuildCommand extends SubcommandData { } async execute(b: ChatInputCommandInteraction): Promise { - if (await this._users.getActiveSnowflakeIsAdmin(b.user.id)) { + if (await this._users.isSnowflakeAdmin(b.user.id)) { await b.reply({ content: "I dunno... Let's check if this will work...", ephemeral: true, diff --git a/src/commands/bot/restart.ts b/src/commands/bot/restart.ts index 35fa51b..0570065 100644 --- a/src/commands/bot/restart.ts +++ b/src/commands/bot/restart.ts @@ -6,13 +6,13 @@ import { ChatInputCommandInteraction, } from "discord.js" import {wrappedRestart} from "../../ipc/restart" -import {UsersTable} from "../../database/users.js" +import {UsersManager} from "../../managers/users"; export class BotRestartCommand extends SubcommandData { - private readonly _users: UsersTable + private readonly _users: UsersManager private readonly _cleanUp: () => Promise - constructor({users, cleanUp}: { users: UsersTable, cleanUp(): Promise }) { + constructor({users, cleanUp}: { users: UsersManager, cleanUp(): Promise }) { super() this._users = users this._cleanUp = cleanUp @@ -25,7 +25,7 @@ export class BotRestartCommand extends SubcommandData { } async execute(b: ChatInputCommandInteraction): Promise { - if (await this._users.getActiveSnowflakeIsAdmin(b.user.id)) { + if (await this._users.isSnowflakeAdmin(b.user.id)) { await b.reply({ ephemeral: true, content: "Yaaaawwn... Okay... Just a quick nap then...", diff --git a/src/commands/bot/shutdown.ts b/src/commands/bot/shutdown.ts index f71a841..80b7ed1 100644 --- a/src/commands/bot/shutdown.ts +++ b/src/commands/bot/shutdown.ts @@ -1,12 +1,12 @@ import {SubcommandData} from "../types" import {ApplicationCommandOptionType, ApplicationCommandSubCommandData, ChatInputCommandInteraction} from "discord.js" -import {UsersTable} from "../../database/users.js" +import {UsersManager} from "../../managers/users"; export class BotShutdownCommand extends SubcommandData { - private readonly _users: UsersTable + private readonly _users: UsersManager private readonly _cleanUp: () => Promise - constructor({users, cleanUp}: { users: UsersTable, cleanUp(): Promise }) { + constructor({users, cleanUp}: { users: UsersManager, cleanUp(): Promise }) { super() this._users = users this._cleanUp = cleanUp @@ -19,7 +19,7 @@ export class BotShutdownCommand extends SubcommandData { } async execute(b: ChatInputCommandInteraction): Promise { - if (await this._users.getActiveSnowflakeIsAdmin(b.user.id)) { + if (await this._users.isSnowflakeAdmin(b.user.id)) { await b.reply({ ephemeral: true, content: "Good night =w=", diff --git a/src/commands/character/create.ts b/src/commands/character/create.ts index 5d40312..273969e 100644 --- a/src/commands/character/create.ts +++ b/src/commands/character/create.ts @@ -31,6 +31,14 @@ export class CharacterCreateCommand extends SubcommandData { required: false, autocomplete: true, }, + { + name: "gender", + type: ApplicationCommandOptionType.String, + description: "Your character's pronouns. Don't worry, you can change this later.", + required: false, + maxLength: 20, + minLength: 1, + }, { name: "difficulty", type: ApplicationCommandOptionType.String, diff --git a/src/database/battle_types.ts b/src/database/battle_types.ts index 3170810..1bc15a1 100644 --- a/src/database/battle_types.ts +++ b/src/database/battle_types.ts @@ -1,18 +1,54 @@ -export interface BattleType { - id: number, - name: string, - emoji: string, - color: string, - displayOrder: number, - immmunities: BattleTypeStub[] - resistances: BattleTypeStub[] - weaknesses: BattleTypeStub[] +import {TransactType} from "./index"; + +export interface BattleTypeData { + id: string + name: string + emoji: string + display_order: number +} + +export interface BattleTypeRelationshipData { + attacker: string + defender: string + damage_percentage: number +} + +export interface BattleTypeTablesData { + types: BattleTypeData[] + relationships: BattleTypeRelationshipData[] } -export interface BattleTypeStub { - id: number, - name: string, - emoji: string, - color: string, - displayOrder: number +export interface BattleTypeTables { + getTypesAndRelationships(): Promise +} + +export class BattleTypeTablesImpl implements BattleTypeTables { + private readonly _transact: TransactType + + constructor({transact}: { transact: TransactType }) { + this._transact = transact + } + + async getTypesAndRelationships(): Promise { + const result: BattleTypeTablesData = { + types: [], + relationships: [], + } + await this._transact({ + readonly: true, + async transaction(query) { + const types = await query<{ id: string, name: string, emoji: string, display_order: number }>( + `SELECT id, name, emoji, display_order + FROM battle_types` + ) + const relationships = await query<{ attacker: string, defender: string, damage_percentage: number }>( + `SELECT attacker, defender, damage_percentage + FROM battle_types_relationships` + ) + result.types = types.rows + result.relationships = relationships.rows + } + }) + return result + } } \ No newline at end of file diff --git a/src/database/character_creation.ts b/src/database/character_creation.ts deleted file mode 100644 index 544facf..0000000 --- a/src/database/character_creation.ts +++ /dev/null @@ -1,64 +0,0 @@ -import {QueryType, Transactable} from "./database.js" -import {Snowflake} from "discord-api-types/globals.js" -import {Pronouns} from "./pronouns.js" - -export interface CharacterCreationState { - name: string | null - title: string | null - profile: string | null - gender: string | null - pronouns: Pronouns | null - difficulty: Difficulty | null - preference: Preference | null - type1: BattleType | null, - type2: BattleType | null, - baseConfidence: number | null, - baseHealth: number | null, - baseStamina: number | null, - baseBrawn: number | null, - baseDurability: number | null, - baseIntensity: number | null, - baseResilience: number | null, - baseSpeed: number | null, -} - -export interface CharacterCreationInput { - name?: string - title?: string - profile?: string - gender?: string - pronouns?: string - difficultyName?: string - preferenceName?: string - type1Name?: string - type2Name?: string - baseConfidence?: number - baseHealth?: number - baseStamina?: number - baseBrawn?: number - baseDurability?: number - baseIntensity?: number - baseResilience?: number - baseSpeed?: number -} - -export interface CharacterCreationTable { - startOrContinueCreatingCharacter(snowflake: Snowflake, - input: CharacterCreationInput): Promise -} - -export class CharacterCreationTableImpl implements CharacterCreationTable { - private readonly _query: QueryType - private readonly _transactable: Transactable - - constructor({query, transactable}: { query: QueryType, transactable: Transactable }) { - this._query = query - this._transactable = transactable - } - - async startOrContinueCreatingCharacter( - owner: Snowflake, - input: CharacterCreationInput): Promise { - - } -} \ No newline at end of file diff --git a/src/database/gender.ts b/src/database/gender.ts new file mode 100644 index 0000000..ea4410d --- /dev/null +++ b/src/database/gender.ts @@ -0,0 +1,58 @@ +import {TransactType} from "./index"; + +export interface PronounData { + id: string + default_marker: string + name: string + display_order: number + use_plural: boolean + subjective: string + adjective: string + possessive: string + reflexive: string + objective: string +} + +export interface GenderMarkerData { + emoji: string + default_name: string + display_order: number +} + +export interface GenderTablesData { + pronouns: PronounData[] + markers: GenderMarkerData[] +} + +export interface GenderTables { + getPronounsAndMarkers(): Promise +} + +export class GenderTablesImpl implements GenderTables { + private readonly _transact: TransactType + + constructor({transact}: { transact: TransactType }) { + this._transact = transact + } + + async getPronounsAndMarkers(): Promise { + const result: GenderTablesData = { + pronouns: [], + markers: [], + } + await this._transact({ + readonly: true, + async transaction(query) { + const pronouns = await query<{id: string, default_marker: string, name: string, display_order: number, use_plural: boolean, subjective: string, adjective: string, possessive: string, reflexive: string, objective: string}>( + `SELECT id, default_marker, name, display_order, use_plural, subjective, adjective, possessive, reflexive, objective FROM pronouns` + ) + const markers = await query<{emoji: string, default_name: string, display_order: number}>( + `SELECT emoji, default_name, display_order FROM gender_markers` + ) + result.pronouns = pronouns.rows + result.markers = markers.rows + } + }) + return result + } +} \ No newline at end of file diff --git a/src/database/database.ts b/src/database/index.ts similarity index 61% rename from src/database/database.ts rename to src/database/index.ts index 2e2ef14..19365ac 100644 --- a/src/database/database.ts +++ b/src/database/index.ts @@ -1,6 +1,7 @@ import {QueryResult, QueryResultRow} from "pg" import {UsersTable, UsersTableImpl} from "./users.js" -import {CharacterCreationTable, CharacterCreationTableImpl} from "./character_creation.js" +import {BattleTypeTables, BattleTypeTablesImpl} from "./battle_types"; +import {GenderTables, GenderTablesImpl} from "./gender"; export interface PoolLike { connect(): Promise @@ -10,21 +11,21 @@ export interface PoolClientLike extends Queryable { release(err?: boolean): void } -export function makeTransactable(p: PoolLike): Transactable { - return async function transactable(callback: (q: QueryType, attempts: number) => Promise): Promise { +export function makeTransact(p: PoolLike): TransactType { + return async function transact({readonly=false, transaction}: {readonly?: boolean, transaction: (q: QueryType, attempts: number) => Promise}): Promise { const client = await p.connect() const query = client.query.bind(client) let committed = false let attempts = 0 while (!committed) { try { - await client.query("BEGIN") + await client.query("BEGIN READ " + (readonly ? "ONLY" : "WRITE")) } catch (err) { client.release() throw err } try { - await callback(query, attempts) + await transaction(query, attempts) } catch (err) { try { await client.query("ROLLBACK") @@ -47,7 +48,8 @@ export function makeTransactable(p: PoolLike): Transactable { export interface Database { readonly users: UsersTable - readonly characterCreation: CharacterCreationTable + readonly gender: GenderTables + readonly battleTypes: BattleTypeTables } export interface Queryable { @@ -58,14 +60,16 @@ export interface Queryable { } export type QueryType = Queryable["query"] -export type Transactable = (transaction: (client: QueryType, attempts: number) => Promise) => Promise +export type TransactType = (config: {readonly?: boolean, transaction: (client: QueryType, attempts: number) => Promise}) => Promise export class DatabaseImpl implements Database { readonly users: UsersTableImpl - readonly characterCreation: CharacterCreationTableImpl + readonly gender: GenderTables + readonly battleTypes: BattleTypeTablesImpl - constructor({query, transactable}: { query: QueryType, transactable: Transactable }) { + constructor({query, transact}: { query: QueryType, transact: TransactType }) { this.users = new UsersTableImpl({query}) - this.characterCreation = new CharacterCreationTableImpl({query, transactable}) + this.gender = new GenderTablesImpl({transact}) + this.battleTypes = new BattleTypeTablesImpl({transact}) } } \ No newline at end of file diff --git a/src/database/inmemory/database.ts b/src/database/inmemory/database.ts deleted file mode 100644 index e73a3b0..0000000 --- a/src/database/inmemory/database.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {Database} from "../database.js" -import {InMemoryUsersTable} from "./users.js" - -export class InMemoryDatabase implements Database { - readonly users: InMemoryUsersTable - - constructor() { - this.users = new InMemoryUsersTable() - } -} \ No newline at end of file diff --git a/src/database/inmemory/users.ts b/src/database/inmemory/users.ts deleted file mode 100644 index 341b689..0000000 --- a/src/database/inmemory/users.ts +++ /dev/null @@ -1,46 +0,0 @@ -import {userSnowflakeToUuid, UsersTable} from "../users.js" -import {Snowflake} from "discord-api-types/globals.js" - -interface InMemoryUserData { - id: string, - is_admin: boolean - active_at: Date | null -} - -export class InMemoryUsersTable implements UsersTable { - private readonly _users: Record = {} - - async createBotOwnerAsAdmin(snowflake: Snowflake): Promise { - const uuid = userSnowflakeToUuid(snowflake) - this._users[uuid] = { - id: uuid, - is_admin: true, - active_at: null, - } - } - - async getActiveSnowflakeIsAdmin(snowflake: Snowflake): Promise { - const uuid = userSnowflakeToUuid(snowflake) - if (!this._users.hasOwnProperty(uuid)) { - this._users[uuid] = { - id: uuid, - is_admin: false, - active_at: new Date(), - } - } - const user = this._users[uuid] - return user.is_admin - } - - async getUuidForActiveSnowflake(snowflake: Snowflake): Promise { - const uuid = userSnowflakeToUuid(snowflake) - if (!this._users.hasOwnProperty(uuid)) { - this._users[uuid] = { - id: uuid, - is_admin: false, - active_at: new Date(), - } - } - return uuid - } -} \ No newline at end of file diff --git a/src/database/pronouns.ts b/src/database/pronouns.ts deleted file mode 100644 index 9c0a2ac..0000000 --- a/src/database/pronouns.ts +++ /dev/null @@ -1,22 +0,0 @@ -export interface Pronouns { - id: number - defaultGender: string - pronouns: string - displayOrder: number - usePlural: boolean - subjective: string - adjective: string - possessive: string - reflexive: string - objective: string -} - -export interface PronounsAutocomplete { - pronouns: string - usePlural: boolean - subjective: string - adjective: string - possessive: string - reflexive: string - objective: string -} \ No newline at end of file diff --git a/src/database/users.ts b/src/database/users.ts index c8b40ca..b90c706 100644 --- a/src/database/users.ts +++ b/src/database/users.ts @@ -1,15 +1,11 @@ -import {Snowflake} from "discord-api-types/globals.js" -import {fast1a52 as fnvFast1a52} from "fnv-plus" -import {parse as uuidParse, v1 as uuidV1, validate as uuidValidate, version as uuidVersion} from "uuid" -import {SnowflakeUtil} from "discord.js" -import {QueryType} from "./database.js" +import {QueryType} from "./index.js" export interface UsersTable { - getUuidForActiveSnowflake(snowflake: Snowflake): Promise + createUserOrUpdateActiveTime(id: string): Promise - getActiveSnowflakeIsAdmin(snowflake: Snowflake): Promise + getUserExistsAndIsAdmin(id: string): Promise - createBotOwnerAsAdmin(snowflake: Snowflake): Promise + createOrSetUserAsAdmin(id: string): Promise } export class UsersTableImpl implements UsersTable { @@ -19,95 +15,33 @@ export class UsersTableImpl implements UsersTable { this._query = query } - async getUuidForActiveSnowflake(snowflake: Snowflake): Promise { - const result = await this._query<{ id: string }, [string]>( + async createUserOrUpdateActiveTime(id: string): Promise { + await this._query<{ id: string }, [string]>( `INSERT INTO users (id, is_admin, created_at, updated_at, active_at) VALUES ($1, FALSE, now(), now(), now()) ON CONFLICT (id) DO UPDATE SET active_at = now() - RETURNING id;`, [userSnowflakeToUuid(snowflake)]) - return result.rows[0].id + RETURNING id;`, [id]) } - async getActiveSnowflakeIsAdmin(snowflake: Snowflake): Promise { + async getUserExistsAndIsAdmin(id: string): Promise { const result = await this._query<{ is_admin: boolean }, [string]>( `UPDATE users SET active_at = NOW() WHERE id = $1 - RETURNING is_admin;`, [userSnowflakeToUuid(snowflake)]) + RETURNING is_admin;`, [id]) if (result.rowCount === 0) { return false } return result.rows[0].is_admin } - async createBotOwnerAsAdmin(snowflake: Snowflake): Promise { + async createOrSetUserAsAdmin(id: string): Promise { await this._query<{}, [string]>( `INSERT INTO users (id, is_admin, created_at, updated_at) VALUES ($1, TRUE, now(), now()) ON CONFLICT (id) DO UPDATE SET is_admin = TRUE, updated_at = NOW() - WHERE users.is_admin = FALSE;`, [userSnowflakeToUuid(snowflake)]) + WHERE users.is_admin = FALSE;`, [id]) } } -export function userUuidToSnowflake(uuid: string): Snowflake | null { - if (!uuidValidate(uuid) || uuidVersion(uuid) !== 1) { - return null - } - const bytes = uuidParse(uuid) - const time_low = bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3] - const time_mid = bytes[4] << 8 | bytes[5] - const time_high = (bytes[6] & 0x0F) << 8 | bytes[7] - const timestamp = ((BigInt(time_high) << 48n) | (BigInt(time_mid) << 32n) | BigInt(time_low)) / 10000n - const increment = BigInt((bytes[8] & 0x0F) << 8 | bytes[9]) - const workerId = BigInt((bytes[14] & 0x03) << 3 | (bytes[15] & 0x07) >> 5) - const processId = BigInt(bytes[15] & 0x1F) - - return SnowflakeUtil.generate({ - timestamp, - increment, - workerId, - processId, - }).toString(10) -} - -export function userSnowflakeToUuid(snowflake: Snowflake): string { - const { - timestamp, - increment, - workerId, - processId, - } = SnowflakeUtil.deconstruct(snowflake) - - // Grab 52 hash bits for filling the empty spaces in the UUID. - const hashCode = fnvFast1a52(snowflake) - // We get 10000 possibilities for number of 100-nanosecond periods. - const nanoseconds = (hashCode % 10000) - // The node ID is 48 bits, and we have 10 from the process and worker IDs. - // The first bit must be 1 to indicate a made up node name, but the remaining 37 are open. - // Grab 37 bits after removing the 10000 variants of the 100-nanosecond periods. - const nodeIdPadding = Math.floor(hashCode / 10000) & 0x1FFFFFFFFF - const nodeId = new Uint8Array(6) - // Set the highest bit, indicating a multicast address... - // ... indicating an invalid address, indicating a made up node name. - // Then insert the hash padding and worker/process IDs. - // It doesn't matter what order they're in, as long as it's consistent. - // In this case, we choose the padding to be most significant, then worker, then process. - nodeId[0] = 0x80 | ((nodeIdPadding >> 30) & 0x7F) - nodeId[1] = (nodeIdPadding >> 22) & 0xFF - nodeId[2] = (nodeIdPadding >> 14) & 0xFF - nodeId[3] = (nodeIdPadding >> 6) & 0xFF - nodeId[4] = ((nodeIdPadding & 0x3F) << 2) | Number((workerId >> 3n) & 0x03n) - nodeId[5] = Number(((workerId & 0x07n) << 5n) | (processId & 0x1Fn)) - return uuidV1({ - rng: () => { - throw Error("rng is not supposed to be invoked") - }, - // The snowflake increment is 12 bits. - // UUIDs use a 12 bit clock sequence number. Perfect fit! - clockseq: Number(increment & 0xFFFn), - nsecs: nanoseconds, - msecs: Number(timestamp & 0x3FFFFFFFFFFn), - node: nodeId, - }) -} \ No newline at end of file diff --git a/src/managers/battle_types.ts b/src/managers/battle_types.ts new file mode 100644 index 0000000..e1538fc --- /dev/null +++ b/src/managers/battle_types.ts @@ -0,0 +1,115 @@ +import {BattleTypeTables} from "../database/battle_types"; + +export interface BattleType { + id: string, + name: string, + emoji: string, + displayOrder: number, + offensiveRelationships: {defender: BattleType, damagePercentage: number}[] + defensiveRelationships: {attacker: BattleType, damagePercentage: number}[] +} + +export class BattleTypeManager { + private cachedBattleTypes: {[key: string]: BattleType} = {} + private cachedTypeList: BattleType[] = [] + private refreshPromise: Promise|null = null + + private readonly _table: BattleTypeTables + + constructor({battleTypes}: { battleTypes: BattleTypeTables }) { + this._table = battleTypes + } + + async refreshCache(): Promise { + if (this.refreshPromise !== null) { + await this.refreshPromise + return + } + + this.refreshPromise = (async () => { + const {types, relationships} = await this._table.getTypesAndRelationships() + + const existing = new Set() + this.cachedTypeList = [] + + for (const typeData of types) { + const battleType: BattleType = this.cachedBattleTypes[typeData.id] ?? {} + battleType.id = typeData.id + battleType.name = typeData.name + battleType.emoji = typeData.emoji + battleType.displayOrder = typeData.display_order + battleType.offensiveRelationships = [] + battleType.defensiveRelationships = [] + this.cachedBattleTypes[battleType.id] = battleType + existing.add(battleType.id) + this.cachedTypeList.push(battleType) + } + + for (const key of Object.keys(this.cachedBattleTypes)) { + if (!existing.has(key)) { + delete this.cachedBattleTypes[key] + } + } + + for (const relationshipData of relationships) { + const attacker: BattleType|null = this.cachedBattleTypes[relationshipData.attacker] ?? null + const defender: BattleType|null = this.cachedBattleTypes[relationshipData.defender] ?? null + if (attacker === null || defender === null) { + throw Error("Battle types named in relationship not found: " + relationshipData.attacker + " vs. " + relationshipData.defender) + } + attacker.offensiveRelationships.push({defender: defender, damagePercentage: relationshipData.damage_percentage}) + defender.defensiveRelationships.push({attacker: attacker, damagePercentage: relationshipData.damage_percentage}) + } + + this.cachedTypeList.sort((a, b) => a.displayOrder - b.displayOrder) + })() + + try { + await this.refreshPromise + } finally { + this.refreshPromise = null + } + } + + async getBattleTypes(): Promise { + if (this.refreshPromise !== null) { + await this.refreshPromise + } + return this.cachedTypeList + } + + async getBattleTypeById(id: string): Promise { + if (this.refreshPromise !== null) { + await this.refreshPromise + } + const result = this.cachedBattleTypes[id] ?? null + if (result === null) { + throw Error("No battle type found with id: " + id) + } + return result + } + + async getBattleTypeByName(name: string): Promise { + if (this.refreshPromise !== null) { + await this.refreshPromise + } + const result = this.cachedTypeList.find(t => t.name === name) ?? null + if (result === null) { + throw Error("No battle type found with name: " + name) + } + return result + } + + async getDamagePercentageByIds(attacker: string, defender: string): Promise { + if (this.refreshPromise !== null) { + await this.refreshPromise + } + if (Object.hasOwn(this.cachedBattleTypes, attacker) && Object.hasOwn(this.cachedBattleTypes, defender)) { + return this.cachedBattleTypes[attacker] + .offensiveRelationships + .find(({defender: d}) => d.id === defender)?.damagePercentage ?? 100 + } else { + throw Error("Battle types not found by ID: " + attacker + " vs. " + defender) + } + } +} \ No newline at end of file diff --git a/src/managers/character.ts b/src/managers/character.ts new file mode 100644 index 0000000..edbd959 --- /dev/null +++ b/src/managers/character.ts @@ -0,0 +1,13 @@ +import {Snowflake} from "discord-api-types/globals"; + +export class CharacterManager { + async startNewCharacter(user: Snowflake, forceErase: boolean): Promise<{alreadyEditing: boolean}> { + } + + async abandonEditedCharacter(user: Snowflake): Promise<{abandoned: boolean}> { + } + + async saveEditedCharacter(user: Snowflake, asNew?: boolean): Promise<{ id: string, name: string, title: string }|{ id: null }> { + + } +} diff --git a/src/managers/gender.ts b/src/managers/gender.ts new file mode 100644 index 0000000..5ae926a --- /dev/null +++ b/src/managers/gender.ts @@ -0,0 +1,168 @@ +import {GenderTables} from "../database/gender"; + +export interface Pronouns { + id: string + defaultGenderMarker: GenderMarker + name: string + displayOrder: number + usePlural: boolean + subjective: string + adjective: string + possessive: string + reflexive: string + objective: string +} + +export interface GenderMarker { + emoji: string + defaultGenderName: string + displayOrder: number + defaultPronouns: Pronouns|null +} + +export class GenderManager { + private cachedPronouns: {[key: string]: Pronouns} = {} + private cachedGenderMarkers: {[key: string]: GenderMarker} = {} + private cachedPronounList: Pronouns[] = [] + private cachedMarkerList: GenderMarker[] = [] + private refreshPromise: Promise|null = null + + private readonly _table: GenderTables + + constructor({gender}: { gender: GenderTables }) { + this._table = gender + } + + async refreshCache(): Promise { + if (this.refreshPromise !== null) { + await this.refreshPromise + return + } + + this.refreshPromise = (async () => { + const {pronouns, markers} = await this._table.getPronounsAndMarkers() + + const pronounSet = new Set() + const markerSet = new Set() + this.cachedPronounList = [] + this.cachedMarkerList = [] + + for (const pronounData of pronouns) { + const pronoun: Pronouns = this.cachedPronouns[pronounData.id] ?? {} + pronoun.id = pronounData.id + pronoun.name = pronounData.name + pronoun.displayOrder = pronounData.display_order + pronoun.subjective = pronounData.subjective + pronoun.adjective = pronounData.adjective + pronoun.possessive = pronounData.possessive + pronoun.reflexive = pronounData.reflexive + pronoun.objective = pronounData.objective + this.cachedPronouns[pronoun.id] = pronoun + pronounSet.add(pronoun.id) + this.cachedPronounList.push(pronoun) + } + + for (const key of Object.keys(this.cachedPronouns)) { + if (!pronounSet.has(key)) { + delete this.cachedPronouns[key] + } + } + + for (const markerData of markers) { + const marker: GenderMarker = this.cachedGenderMarkers[markerData.emoji] ?? {} + marker.emoji = markerData.emoji + marker.displayOrder = markerData.display_order + marker.defaultGenderName = markerData.default_name + this.cachedGenderMarkers[marker.emoji] = marker + markerSet.add(marker.emoji) + this.cachedMarkerList.push(marker) + } + + for (const key of Object.keys(this.cachedGenderMarkers)) { + if (!markerSet.has(key)) { + delete this.cachedGenderMarkers[key] + } + } + + this.cachedPronounList.sort((a, b) => a.displayOrder - b.displayOrder) + this.cachedMarkerList.sort((a, b) => a.displayOrder - b.displayOrder) + + for (const pronounData of pronouns) { + if (!Object.hasOwn(this.cachedPronouns, pronounData.id) + || !Object.hasOwn(this.cachedGenderMarkers, pronounData.default_marker)) { + throw Error("invalid marker: can't find " + pronounData.id + " and " + pronounData.default_marker) + } + this.cachedPronouns[pronounData.id].defaultGenderMarker = this.cachedGenderMarkers[pronounData.default_marker] + } + + for (const markerData of markers) { + this.cachedGenderMarkers[markerData.emoji].defaultPronouns = + this.cachedPronounList.find((p) => p.defaultGenderMarker.emoji === markerData.emoji) ?? null + } + })() + + try { + await this.refreshPromise + } finally { + this.refreshPromise = null + } + } + + async getPronouns(): Promise { + if (this.refreshPromise !== null) { + await this.refreshPromise + } + return this.cachedPronounList + } + + async getPronounsById(id: string): Promise { + if (this.refreshPromise !== null) { + await this.refreshPromise + } + const result = this.cachedPronouns[id] ?? null + if (result === null) { + throw Error("No battle type found with id: " + id) + } + return result + } + + async getPronounsByName(name: string): Promise { + if (this.refreshPromise !== null) { + await this.refreshPromise + } + const result = this.cachedPronounList.find(t => t.name === name) ?? null + if (result === null) { + throw Error("No pronouns found with name: " + name) + } + return result + } + + async getGenderMarkers(): Promise { + if (this.refreshPromise !== null) { + await this.refreshPromise + } + return this.cachedMarkerList + } + + async getGenderMarkerByEmoji(emoji: string): Promise { + if (this.refreshPromise !== null) { + await this.refreshPromise + } + const result = this.cachedGenderMarkers[emoji] ?? null + if (result === null) { + throw Error("No marker found with emoji: " + emoji) + } + return result + } + + async getDefaultGenderMarkerForGender(gender: string): Promise { + if (this.refreshPromise !== null) { + await this.refreshPromise + } + const result = this.cachedMarkerList.find(t => t.defaultGenderName === gender) ?? this.cachedMarkerList[0] ?? null + if (result === null) { + throw Error("No markers found") + } + return result + } +} \ No newline at end of file diff --git a/src/managers/index.ts b/src/managers/index.ts new file mode 100644 index 0000000..2860378 --- /dev/null +++ b/src/managers/index.ts @@ -0,0 +1,16 @@ +import {BattleTypeManager} from "./battle_types"; +import {UsersManager} from "./users"; +import {GenderManager} from "./gender"; +import {Database} from "../database"; + +export class Managers { + readonly battleTypes: BattleTypeManager + readonly gender: GenderManager + readonly users: UsersManager + + constructor(db: Database) { + this.battleTypes = new BattleTypeManager(db) + this.gender = new GenderManager(db) + this.users = new UsersManager(db) + } +} \ No newline at end of file diff --git a/src/managers/users.ts b/src/managers/users.ts new file mode 100644 index 0000000..dcc9ad4 --- /dev/null +++ b/src/managers/users.ts @@ -0,0 +1,87 @@ +import {Snowflake} from "discord-api-types/globals"; +import {parse as uuidParse, v1 as uuidV1, validate as uuidValidate, version as uuidVersion} from "uuid"; +import {SnowflakeUtil} from "discord.js"; +import {fast1a52 as fnvFast1a52} from "fnv-plus"; +import {UsersTable} from "../database/users"; + +export class UsersManager { + private readonly _table: UsersTable + + constructor({users}: { users: UsersTable }) { + this._table = users + } + + async isSnowflakeAdmin(snowflake: Snowflake): Promise { + return this._table.getUserExistsAndIsAdmin(userSnowflakeToUuid(snowflake)) + } + + async updateSnowflakeActivity(snowflake: Snowflake): Promise { + return this._table.createUserOrUpdateActiveTime(userSnowflakeToUuid(snowflake)) + } + + async setSnowflakeAdmin(snowflake: Snowflake): Promise { + return this._table.createOrSetUserAsAdmin(userSnowflakeToUuid(snowflake)) + } +} + +export function userUuidToSnowflake(uuid: string): Snowflake | null { + if (!uuidValidate(uuid) || uuidVersion(uuid) !== 1) { + return null + } + const bytes = uuidParse(uuid) + const time_low = bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3] + const time_mid = bytes[4] << 8 | bytes[5] + const time_high = (bytes[6] & 0x0F) << 8 | bytes[7] + const timestamp = ((BigInt(time_high) << 48n) | (BigInt(time_mid) << 32n) | BigInt(time_low)) / 10000n + const increment = BigInt((bytes[8] & 0x0F) << 8 | bytes[9]) + const workerId = BigInt((bytes[14] & 0x03) << 3 | (bytes[15] & 0x07) >> 5) + const processId = BigInt(bytes[15] & 0x1F) + + return SnowflakeUtil.generate({ + timestamp, + increment, + workerId, + processId, + }).toString(10) +} + +export function userSnowflakeToUuid(snowflake: Snowflake): string { + const { + timestamp, + increment, + workerId, + processId, + } = SnowflakeUtil.deconstruct(snowflake) + + // Grab 52 hash bits for filling the empty spaces in the UUID. + const hashCode = fnvFast1a52(snowflake) + // We get 10000 possibilities for number of 100-nanosecond periods. + const nanoseconds = (hashCode % 10000) + // The node ID is 48 bits, and we have 10 from the process and worker IDs. + // The first bit must be 1 to indicate a made up node name, but the remaining 37 are open. + // Grab 37 bits after removing the 10000 variants of the 100-nanosecond periods. + const nodeIdPadding = Math.floor(hashCode / 10000) & 0x1FFFFFFFFF + const nodeId = new Uint8Array(6) + // Set the highest bit, indicating a multicast address... + // ... indicating an invalid address, indicating a made up node name. + // Then insert the hash padding and worker/process IDs. + // It doesn't matter what order they're in, as long as it's consistent. + // In this case, we choose the padding to be most significant, then worker, then process. + nodeId[0] = 0x80 | ((nodeIdPadding >> 30) & 0x7F) + nodeId[1] = (nodeIdPadding >> 22) & 0xFF + nodeId[2] = (nodeIdPadding >> 14) & 0xFF + nodeId[3] = (nodeIdPadding >> 6) & 0xFF + nodeId[4] = ((nodeIdPadding & 0x3F) << 2) | Number((workerId >> 3n) & 0x03n) + nodeId[5] = Number(((workerId & 0x07n) << 5n) | (processId & 0x1Fn)) + return uuidV1({ + rng: () => { + throw Error("rng is not supposed to be invoked") + }, + // The snowflake increment is 12 bits. + // UUIDs use a 12 bit clock sequence number. Perfect fit! + clockseq: Number(increment & 0xFFFn), + nsecs: nanoseconds, + msecs: Number(timestamp & 0x3FFFFFFFFFFn), + node: nodeId, + }) +} \ No newline at end of file