diff --git a/.github/workflows/build_server.yml b/.github/workflows/build_server.yml index a104b31..1060de2 100644 --- a/.github/workflows/build_server.yml +++ b/.github/workflows/build_server.yml @@ -45,11 +45,11 @@ jobs: env: POSTGRES_PASSWORD: ${{ env.PGPASSWORD }} POSTGRES_USER: ${{ env.PGUSER }} - POSTGRES_DATABASE: ${{ env.PGDATABASE }} + POSTGRES_DB: ${{ env.PGDATABASE }} options: >- --health-cmd pg_isready --health-interval 10s - --health-timeout 5s + --health-timeout 10s --health-retries 5 ports: - 5432:5432 @@ -68,10 +68,14 @@ jobs: - name: Install Nix Cache uses: DeterminateSystems/magic-nix-cache-action@main - - name: "[Server] build" + - name: "[Server] Build" run: | nix build .#server - - name: "[Server] tests" + - name: "[Server] Tests" run: | nix develop .#ci --impure -c just test + + - name: "[Server] Check Migrations" + run: | + nix develop .#ci --impure -c just db-up diff --git a/README.org b/README.org index 773d806..0036669 100644 --- a/README.org +++ b/README.org @@ -31,6 +31,8 @@ Then simply interact with ~/localhost:8080/~. For more commands, make sure to ch just #+END_SRC +to migrate the local dababase, you can use ~just db-up~ or ~db-up~. + *** Nix Builds You can also build the server and related OCI images with Nix. diff --git a/database/main.input.sql b/database/main.input.sql index 5125099..d8a63e1 100644 --- a/database/main.input.sql +++ b/database/main.input.sql @@ -13,10 +13,10 @@ VALUES ('Huneric', 'mmagueta@example.com', 'mmagueta'), ('Gaiseric', 'mmagueta@example.com', 'mmagueta'), ('Legion', 'lambdu@example.com', 'lambdu'); -INSERT INTO lyceum.character_stats (name, e_mail, username, constitution, wisdom, strength, endurance, intelligence, faith) -VALUES ('Huneric', 'mmagueta@example.com', 'mmagueta', 100, 110, 95, 120, 105, 100), - ('Gaiseric', 'mmagueta@example.com', 'mmagueta', 60, 110, 55, 150, 150, 50), - ('Legion', 'lambdu@example.com', 'lambdu', 60, 110, 55, 150, 150, 50); +INSERT INTO lyceum.character_stats (name, e_mail, username, constitution, wisdom, strength, endurance, intelligence, faith, mana, health) +VALUES ('Huneric', 'mmagueta@example.com', 'mmagueta', 100, 110, 95, 120, 105, 100, 100, 100), + ('Gaiseric', 'mmagueta@example.com', 'mmagueta', 60, 110, 55, 150, 150, 50, 100, 100), + ('Legion', 'lambdu@example.com', 'lambdu', 60, 110, 55, 150, 150, 50, 100, 100); INSERT INTO lyceum.character_position (name, e_mail, username, x_position, y_position, map_name, face_direction) VALUES ('Huneric', 'mmagueta@example.com', 'mmagueta', 10, 20, 'CASTLE_HALL', 270), @@ -35,3 +35,9 @@ VALUES ('Huneric', 'mmagueta@example.com', 'mmagueta', true, 'Vandal''s Prima', ('Huneric', 'mmagueta@example.com', 'mmagueta', true, 'Blood-Reaver''s Gauntlet', 'ARMS'::lyceum.equipment_use, 'ARMS'::lyceum.equipment_kind), ('Gaiseric', 'mmagueta@example.com', 'mmagueta', true, 'Vandal''s Prima', 'RIGHT_ARM', 'ARMS'::lyceum.equipment_kind), ('Legion', 'lambdu@example.com', 'lambdu', true, 'Vandal''s Prima', 'RIGHT_ARM', 'ARMS'::lyceum.equipment_kind); + +-- Insert into lyceum.view_spell_destruction +INSERT INTO lyceum.view_spell_destruction (name, description, cost, duration, cast_time, kind, target, base_damage, damage_kind, destruction_kind) +VALUES ('Fire Ball', 'Something cool', 10, 50, 1, 'PROJECTILE'::lyceum.spell_type, 'SINGULAR'::lyceum.spell_target, 100, 'FIRE'::lyceum.spell_damage_type, 'MAGIC'::lyceum.spell_destruction_type), + ('Ice Ball', 'Another cool thing', 10, 50, 1, 'PROJECTILE'::lyceum.spell_type, 'SINGULAR'::lyceum.spell_target, 100, 'FIRE'::lyceum.spell_damage_type, 'MAGIC'::lyceum.spell_destruction_type); + diff --git a/database/migrations/000001_schemas.sql b/database/migrations/000001_schemas.sql new file mode 100644 index 0000000..0de3aa7 --- /dev/null +++ b/database/migrations/000001_schemas.sql @@ -0,0 +1 @@ +CREATE SCHEMA IF NOT EXISTS lyceum; \ No newline at end of file diff --git a/database/migrations/000001_setup.sql b/database/migrations/000001_setup.sql deleted file mode 100644 index d81ab9e..0000000 --- a/database/migrations/000001_setup.sql +++ /dev/null @@ -1,228 +0,0 @@ -CREATE SCHEMA IF NOT EXISTS lyceum; - -CREATE TABLE lyceum.user( - username VARCHAR(32) NOT NULL, - password TEXT NOT NULL, - e_mail TEXT NOT NULL CHECK (e_mail ~* '^[A-Za-z0-9.+%-]+@[A-Za-z0-9.-]+[.][A-Za-z]+$'), - PRIMARY KEY(username, e_mail) -); - -CREATE TABLE lyceum.map( - name VARCHAR(16) NOT NULL, - PRIMARY KEY(name) -); - -CREATE TYPE lyceum.TILE_TYPE AS ENUM( - 'WATER', - 'GRASS', - 'SAND', - 'ROCK' -); - -CREATE TABLE lyceum.tile( - map_name VARCHAR(16) NOT NULL, - kind lyceum.TILE_TYPE NOT NULL, - x_position SMALLINT NOT NULL, - y_position SMALLINT NOT NULL, - PRIMARY KEY(map_name, kind, x_position, y_position), - FOREIGN KEY (map_name) REFERENCES lyceum.map(name) -); - -CREATE TABLE lyceum.object( - map_name VARCHAR(16) NOT NULL, - name VARCHAR(16) NOT NULL, - x_position SMALLINT NOT NULL, - y_position SMALLINT NOT NULL, - PRIMARY KEY(map_name, x_position, y_position), - FOREIGN KEY (map_name) REFERENCES lyceum.map(name) -); - -CREATE OR REPLACE FUNCTION map_object_overlap() RETURNS trigger AS $map_object_overlap$ -DECLARE - kind TEXT; -BEGIN - SELECT kind INTO kind FROM tile WHERE x_position = NEW.x_position AND y_position = NEW.y_position; - - IF NEW.name = 'TREE' THEN - IF kind <> 'GRASS' AND kind <> 'SAND' THEN - RAISE EXCEPTION '''TREE'' cannot be defined in tiles that are not ''GRASS'' or ''SAND''.'; - END IF; - END IF; - - RETURN NEW; -END; -$map_object_overlap$ LANGUAGE plpgsql; - -CREATE TRIGGER map_object_overlap BEFORE INSERT OR UPDATE ON lyceum.object -FOR EACH ROW EXECUTE FUNCTION map_object_overlap(); - -CREATE TABLE lyceum.character( - name VARCHAR(18) NOT NULL, - e_mail TEXT NOT NULL CHECK (e_mail ~* '^[A-Za-z0-9.+%-]+@[A-Za-z0-9.-]+[.][A-Za-z]+$'), - username VARCHAR(32) NOT NULL, - FOREIGN KEY (e_mail, username) REFERENCES lyceum.user(e_mail, username), - PRIMARY KEY(name, username, e_mail) -); - -CREATE TABLE lyceum.character_stats( - name VARCHAR(18) NOT NULL, - e_mail TEXT NOT NULL CHECK (e_mail ~* '^[A-Za-z0-9.+%-]+@[A-Za-z0-9.-]+[.][A-Za-z]+$'), - username VARCHAR(32) NOT NULL, - constitution SMALLINT NOT NULL CHECK (constitution > 0 AND constitution <= 150), - wisdom SMALLINT NOT NULL CHECK (wisdom > 0 AND wisdom <= 150), - strength SMALLINT NOT NULL CHECK (strength > 0 AND strength <= 150), - endurance SMALLINT NOT NULL CHECK (endurance > 0 AND endurance <= 150), - intelligence SMALLINT NOT NULL CHECK (intelligence > 0 AND intelligence <= 150), - faith SMALLINT NOT NULL CHECK (faith > 0 AND faith <= 150), - FOREIGN KEY (name, username, e_mail) REFERENCES lyceum.character(name, username, e_mail), - PRIMARY KEY(name, username, e_mail) -); - -CREATE TABLE lyceum.character_position( - name VARCHAR(18) NOT NULL, - e_mail TEXT NOT NULL CHECK (e_mail ~* '^[A-Za-z0-9.+%-]+@[A-Za-z0-9.-]+[.][A-Za-z]+$'), - username VARCHAR(32) NOT NULL, - x_position SMALLINT NOT NULL, - y_position SMALLINT NOT NULL, - face_direction SMALLINT NOT NULL CHECK (face_direction >= 0 AND face_direction < 360), - map_name VARCHAR(64) NOT NULL, - FOREIGN KEY (name, username, e_mail) REFERENCES lyceum.character(name, username, e_mail), - FOREIGN KEY (map_name) REFERENCES lyceum.map(name), - PRIMARY KEY(name, username, e_mail) -); - -CREATE TABLE lyceum.active_characters( - name VARCHAR(18) NOT NULL, - e_mail TEXT NOT NULL CHECK (e_mail ~* '^[A-Za-z0-9.+%-]+@[A-Za-z0-9.-]+[.][A-Za-z]+$'), - username VARCHAR(32) NOT NULL, - user_pid VARCHAR(50) NOT NULL UNIQUE, - FOREIGN KEY (name, username, e_mail) REFERENCES lyceum.character(name, username, e_mail), - PRIMARY KEY(name, username, e_mail) -); - -CREATE OR REPLACE VIEW lyceum.view_character AS -SELECT * FROM lyceum.character -NATURAL JOIN lyceum.character_stats -NATURAL JOIN lyceum.character_position; - -CREATE OR REPLACE FUNCTION lyceum.view_character_upsert() RETURNS trigger LANGUAGE plpgsql AS $$ -BEGIN - INSERT INTO lyceum.character(name, e_mail, username) - VALUES (NEW.name, NEW.e_mail, NEW.username) - ON CONFLICT DO NOTHING; - - INSERT INTO lyceum.character_stats(name, e_mail, username, constitution, wisdom, strength, endurance, intelligence, faith) - VALUES (NEW.name, NEW.e_mail, NEW.username, NEW.constitution, NEW.wisdom, NEW.strength, NEW.endurance, NEW.intelligence, NEW.faith) - ON CONFLICT (name, e_mail, username) DO UPDATE SET - name = NEW.name, - e_mail = NEW.e_mail, - username = NEW.username, - constitution = NEW.constitution, - wisdom = NEW.wisdom, - strength = NEW.strength, - endurance = NEW.endurance, - intelligence = NEW.intelligence, - faith = NEW.faith, - face_direction = NEW.face_direction; - -- Is this really necessary? The on conflict already catches this! - -- WHERE name = NEW.name AND e_mail = NEW.e_mail AND username = NEW.username; - - INSERT INTO lyceum.character_position(name, e_mail, username, x_position, y_position, map_name) - VALUES (NEW.name, NEW.e_mail, NEW.username, NEW.x_position, NEW.y_position, NEW.map_name) - ON CONFLICT (name, username, e_mail) DO UPDATE SET - name = NEW.name, - e_mail = NEW.e_mail, - username = NEW.username, - x_position = NEW.x_position, - y_position = NEW.y_position, - map_name = NEW.map_name; - - -- Same issue here. - -- WHERE name = NEW.name AND e_mail = NEW.e_mail AND username = NEW.username; - - RETURN NEW; -END -$$; - -CREATE TABLE lyceum.item( - name VARCHAR(32) NOT NULL, - description TEXT NOT NULL, - PRIMARY KEY(name) -); - -CREATE TABLE lyceum.character_inventory( - name VARCHAR(18) NOT NULL, - e_mail TEXT NOT NULL CHECK (e_mail ~* '^[A-Za-z0-9.+%-]+@[A-Za-z0-9.-]+[.][A-Za-z]+$'), - username VARCHAR(32) NOT NULL, - quantity SMALLINT NOT NULL, - item_name VARCHAR(32) NOT NULL, - FOREIGN KEY (name, username, e_mail) REFERENCES lyceum.character(name, username, e_mail), - FOREIGN KEY (item_name) REFERENCES lyceum.item(name), - PRIMARY KEY(name, username, e_mail, item_name, quantity) -); - -CREATE TYPE lyceum.EQUIPMENT_KIND AS ENUM( - 'HEAD', - 'TOP', - 'BOTTOM', - 'FEET', - 'ARMS', - 'FINGER' -); - -CREATE TYPE lyceum.EQUIPMENT_USE AS ENUM( - 'HEAD', - 'TOP', - 'BOTTOM', - 'FEET', - 'ARMS', - 'LEFT_ARM', - 'RIGHT_ARM', - 'FINGER' -); - -CREATE TABLE lyceum.equipment( - name VARCHAR(32) NOT NULL, - description TEXT NOT NULL, - kind lyceum.EQUIPMENT_KIND NOT NULL, - PRIMARY KEY(name, kind) -); - -CREATE OR REPLACE FUNCTION lyceum.check_equipment_position_compatibility(use lyceum.EQUIPMENT_USE, kind lyceum.EQUIPMENT_KIND) RETURNS BOOL AS $$ -BEGIN - RETURN CASE - WHEN use::TEXT = kind::TEXT THEN true - WHEN use = 'RIGHT_ARM' AND kind = 'ARMS' THEN true - WHEN use = 'LEFT_ARM' AND kind = 'ARMS' THEN true - ELSE false - END; -END; -$$ LANGUAGE plpgsql; - -CREATE TABLE lyceum.character_equipment( - name VARCHAR(18) NOT NULL, - e_mail TEXT NOT NULL CHECK (e_mail ~* '^[A-Za-z0-9.+%-]+@[A-Za-z0-9.-]+[.][A-Za-z]+$'), - username VARCHAR(32) NOT NULL, - is_equiped BOOL NOT NULL, - equipment_name VARCHAR(32) NOT NULL, - use lyceum.EQUIPMENT_USE NOT NULL, - kind lyceum.EQUIPMENT_KIND NOT NULL, - CHECK (lyceum.check_equipment_position_compatibility(use, kind)), - FOREIGN KEY (name, username, e_mail) REFERENCES lyceum.character(name, username, e_mail), - FOREIGN KEY (equipment_name, kind) REFERENCES lyceum.equipment(name, kind), - PRIMARY KEY(name, username, e_mail, equipment_name) -); - -CREATE OR REPLACE TRIGGER trigger_character_upsert -INSTEAD OF INSERT ON lyceum.view_character -FOR EACH ROW EXECUTE FUNCTION lyceum.view_character_upsert(); - --- INSERT INTO lyceum.user(username, e_mail, password) --- VALUES ('test', 'test@email.com', '123'); - --- INSERT INTO lyceum.view_character(name, e_mail, username, constitution, wisdom, strength, endurance, intelligence, faith, x_position, y_position, map_name) --- VALUES ('knight', 'test@email.com', 'test', 10, 12, 13, 14, 15, 16, 0, 0, 'arda'); - --- With map stuff --- INSERT INTO lyceum.view_character(name, e_mail, username, constitution, wisdom, strength, endurance, intelligence, faith, x_position, y_position, map_name) --- VALUES ('knight', 'test@email.com', 'test', 10, 12, 13, 14, 15, 16, 0, 0, 'CASTLE_HALL'); diff --git a/database/migrations/000002_lyceum_world.sql b/database/migrations/000002_lyceum_world.sql new file mode 100644 index 0000000..c79b42b --- /dev/null +++ b/database/migrations/000002_lyceum_world.sql @@ -0,0 +1,53 @@ +-- Defines: +-- Tile Types +-- Map +-- Objects + +CREATE TYPE lyceum.TILE_TYPE AS ENUM( + 'WATER', + 'GRASS', + 'SAND', + 'ROCK' +); + +CREATE TABLE lyceum.map( + name VARCHAR(16) NOT NULL, + PRIMARY KEY(name) +); + +CREATE TABLE lyceum.tile( + map_name VARCHAR(16) NOT NULL, + kind lyceum.TILE_TYPE NOT NULL, + x_position SMALLINT NOT NULL, + y_position SMALLINT NOT NULL, + PRIMARY KEY(map_name, kind, x_position, y_position), + FOREIGN KEY (map_name) REFERENCES lyceum.map(name) +); + +CREATE TABLE lyceum.object( + map_name VARCHAR(16) NOT NULL, + name VARCHAR(16) NOT NULL, + x_position SMALLINT NOT NULL, + y_position SMALLINT NOT NULL, + PRIMARY KEY(map_name, x_position, y_position), + FOREIGN KEY (map_name) REFERENCES lyceum.map(name) +); + +CREATE OR REPLACE FUNCTION map_object_overlap() RETURNS trigger AS $map_object_overlap$ +DECLARE + kind TEXT; +BEGIN + SELECT kind INTO kind FROM tile WHERE x_position = NEW.x_position AND y_position = NEW.y_position; + + IF NEW.name = 'TREE' THEN + IF kind <> 'GRASS' AND kind <> 'SAND' THEN + RAISE EXCEPTION '''TREE'' cannot be defined in tiles that are not ''GRASS'' or ''SAND''.'; + END IF; + END IF; + + RETURN NEW; +END; +$map_object_overlap$ LANGUAGE plpgsql; + +CREATE TRIGGER map_object_overlap BEFORE INSERT OR UPDATE ON lyceum.object +FOR EACH ROW EXECUTE FUNCTION map_object_overlap(); diff --git a/database/migrations/000003_lyceum_character.sql b/database/migrations/000003_lyceum_character.sql new file mode 100644 index 0000000..d7d1ce3 --- /dev/null +++ b/database/migrations/000003_lyceum_character.sql @@ -0,0 +1,131 @@ +CREATE TABLE lyceum.user( + username VARCHAR(32) NOT NULL, + password TEXT NOT NULL, + e_mail TEXT NOT NULL CHECK (e_mail ~* '^[A-Za-z0-9.+%-]+@[A-Za-z0-9.-]+[.][A-Za-z]+$') UNIQUE, + PRIMARY KEY(e_mail) +); + +-- TODO: I'm getting a bunch of migration conflicts because +-- this crappy name is not UNIQUE. +CREATE TABLE lyceum.character( + id SERIAL NOT NULL UNIQUE, + name VARCHAR(18) NOT NULL UNIQUE, + level SMALLINT NOT NULL DEFAULT 1 CHECK (level >= 1), + mana SMALLINT NOT NULL DEFAULT 100, + health SMALLINT NOT NULL DEFAULT 100, + PRIMARY KEY (id) +); + +-- CREATE TABLE lyceum.npc( +-- name VARCHAR(18) NOT NULL, +-- level SMALLINT NOT NULL DEFAULT 1 CHECK (level >= 1), +-- mana SMALLINT NOT NULL DEFAULT 100, +-- health SMALLINT NOT NULL DEFAULT 100, +-- ); + +-- TODO: this is getting annoying af, I don't have the time to +-- deal with these annoying uniqueness constraints everywhere!!! +--CREATE TABLE lyceum.player( +-- id SERIAL NOT NULL, +-- name VARCHAR(18) NOT NULL, +-- level SMALLINT NOT NULL DEFAULT 1 CHECK (level >= 1), +-- mana SMALLINT NOT NULL DEFAULT 100, +-- health SMALLINT NOT NULL DEFAULT 100, +-- e_mail TEXT NOT NULL CHECK (e_mail ~* '^[A-Za-z0-9.+%-]+@[A-Za-z0-9.-]+[.][A-Za-z]+$') UNIQUE, +-- username VARCHAR(32) NOT NULL, +-- FOREIGN KEY (e_mail, username) REFERENCES lyceum.user(e_mail, username), +-- FOREIGN KEY (id, name, level, mana, health) REFERENCES lyceum.character(id, name, level, mana, health), +-- PRIMARY KEY (name, username, e_mail) +--); + +-- HEALTH = CONSTITUTION + (99 - CONSTITUTION) / (1 + e^(50 - ENDURANCE) * 0.1) + (99 - CONSTITUTION) / (1 + e^(50 - STRENGTH) * 0.1) +-- MANA = SOMETHING SIMILAR HERE + +CREATE TABLE lyceum.character_stats( + name VARCHAR(18) NOT NULL, + e_mail TEXT NOT NULL CHECK (e_mail ~* '^[A-Za-z0-9.+%-]+@[A-Za-z0-9.-]+[.][A-Za-z]+$') UNIQUE, + username VARCHAR(32) NOT NULL, + constitution SMALLINT NOT NULL CHECK (constitution > 0 AND constitution <= 150), + wisdom SMALLINT NOT NULL CHECK (wisdom > 0 AND wisdom <= 150), + strength SMALLINT NOT NULL CHECK (strength > 0 AND strength <= 150), + endurance SMALLINT NOT NULL CHECK (endurance > 0 AND endurance <= 150), + intelligence SMALLINT NOT NULL CHECK (intelligence > 0 AND intelligence <= 150), + faith SMALLINT NOT NULL CHECK (faith > 0 AND faith <= 150), + mana SMALLINT NOT NULL CHECK (mana > 0), + health SMALLINT NOT NULL CHECK (health > 0), + FOREIGN KEY (name) REFERENCES lyceum.character(name), + PRIMARY KEY(name, username, e_mail) +); + +CREATE TABLE lyceum.character_position( + name VARCHAR(18) NOT NULL UNIQUE, + --TODO: Someone removed email from the character table + --e_mail TEXT NOT NULL CHECK (e_mail ~* '^[A-Za-z0-9.+%-]+@[A-Za-z0-9.-]+[.][A-Za-z]+$') UNIQUE, + username VARCHAR(32) NOT NULL, + x_position SMALLINT NOT NULL, + y_position SMALLINT NOT NULL, + face_direction SMALLINT NOT NULL CHECK (face_direction >= 0 AND face_direction < 360), + map_name VARCHAR(64) NOT NULL, + FOREIGN KEY (name) REFERENCES lyceum.character(name), + FOREIGN KEY (map_name) REFERENCES lyceum.map(name), + PRIMARY KEY(name, map_name) +); + +CREATE TABLE lyceum.active_characters( + name VARCHAR(18) NOT NULL, + --TODO: Someone removed email from the character table + --e_mail TEXT NOT NULL CHECK (e_mail ~* '^[A-Za-z0-9.+%-]+@[A-Za-z0-9.-]+[.][A-Za-z]+$') UNIQUE, + --TODO: Someone also removed the username as well + --username VARCHAR(32) NOT NULL, + user_pid VARCHAR(50) NOT NULL UNIQUE, + FOREIGN KEY (name) REFERENCES lyceum.character(name), + PRIMARY KEY(name) +); + +CREATE OR REPLACE VIEW lyceum.view_character AS +SELECT * FROM lyceum.character +NATURAL JOIN lyceum.character_stats +NATURAL JOIN lyceum.character_position; + +CREATE OR REPLACE FUNCTION lyceum.view_character_upsert() RETURNS trigger LANGUAGE plpgsql AS $$ +BEGIN + INSERT INTO lyceum.character(name, e_mail, username) + VALUES (NEW.name, NEW.e_mail, NEW.username) + ON CONFLICT DO NOTHING; + + INSERT INTO lyceum.character_stats(name, e_mail, username, constitution, wisdom, strength, endurance, intelligence, faith) + VALUES (NEW.name, NEW.e_mail, NEW.username, NEW.constitution, NEW.wisdom, NEW.strength, NEW.endurance, NEW.intelligence, NEW.faith) + ON CONFLICT (name, e_mail, username) DO UPDATE SET + name = NEW.name, + e_mail = NEW.e_mail, + username = NEW.username, + constitution = NEW.constitution, + wisdom = NEW.wisdom, + strength = NEW.strength, + endurance = NEW.endurance, + intelligence = NEW.intelligence, + faith = NEW.faith, + face_direction = NEW.face_direction; + -- Is this really necessary? The on conflict already catches this! + -- WHERE name = NEW.name AND e_mail = NEW.e_mail AND username = NEW.username; + + INSERT INTO lyceum.character_position(name, e_mail, username, x_position, y_position, map_name) + VALUES (NEW.name, NEW.e_mail, NEW.username, NEW.x_position, NEW.y_position, NEW.map_name) + ON CONFLICT (name, username, e_mail) DO UPDATE SET + name = NEW.name, + e_mail = NEW.e_mail, + username = NEW.username, + x_position = NEW.x_position, + y_position = NEW.y_position, + map_name = NEW.map_name; + + -- Same issue here. + -- WHERE name = NEW.name AND e_mail = NEW.e_mail AND username = NEW.username; + + RETURN NEW; +END +$$; + +CREATE OR REPLACE TRIGGER trigger_character_upsert +INSTEAD OF INSERT ON lyceum.view_character +FOR EACH ROW EXECUTE FUNCTION lyceum.view_character_upsert(); diff --git a/database/migrations/000004_lyceum_equipment.sql b/database/migrations/000004_lyceum_equipment.sql new file mode 100644 index 0000000..a0c1869 --- /dev/null +++ b/database/migrations/000004_lyceum_equipment.sql @@ -0,0 +1,71 @@ +CREATE TYPE lyceum.EQUIPMENT_KIND AS ENUM( + 'HEAD', + 'TOP', + 'BOTTOM', + 'FEET', + 'ARMS', + 'FINGER' +); + +CREATE TYPE lyceum.EQUIPMENT_USE AS ENUM( + 'HEAD', + 'TOP', + 'BOTTOM', + 'FEET', + 'ARMS', + 'LEFT_ARM', + 'RIGHT_ARM', + 'FINGER' +); + +CREATE TABLE lyceum.item( + name VARCHAR(32) NOT NULL, + description TEXT NOT NULL, + PRIMARY KEY(name) +); + +CREATE TABLE lyceum.character_inventory( + name VARCHAR(18) NOT NULL, + --e_mail TEXT NOT NULL CHECK (e_mail ~* '^[A-Za-z0-9.+%-]+@[A-Za-z0-9.-]+[.][A-Za-z]+$'), + --username VARCHAR(32) NOT NULL, + quantity SMALLINT NOT NULL, + item_name VARCHAR(32) NOT NULL, + --FOREIGN KEY (name, username, e_mail) REFERENCES lyceum.character(name, username, e_mail), + FOREIGN KEY (name) REFERENCES lyceum.character(name), + FOREIGN KEY (item_name) REFERENCES lyceum.item(name), + --PRIMARY KEY(name, username, e_mail, item_name, quantity) + PRIMARY KEY(name, item_name, quantity) +); + +CREATE TABLE lyceum.equipment( + name VARCHAR(32) NOT NULL, + description TEXT NOT NULL, + kind lyceum.EQUIPMENT_KIND NOT NULL, + PRIMARY KEY(name, kind) +); + +CREATE OR REPLACE FUNCTION lyceum.check_equipment_position_compatibility(use lyceum.EQUIPMENT_USE, kind lyceum.EQUIPMENT_KIND) RETURNS BOOL AS $$ +BEGIN + RETURN CASE + WHEN use::TEXT = kind::TEXT THEN true + WHEN use = 'RIGHT_ARM' AND kind = 'ARMS' THEN true + WHEN use = 'LEFT_ARM' AND kind = 'ARMS' THEN true + ELSE false + END; +END; +$$ LANGUAGE plpgsql; + +CREATE TABLE lyceum.character_equipment( + name VARCHAR(18) NOT NULL, + --e_mail TEXT NOT NULL CHECK (e_mail ~* '^[A-Za-z0-9.+%-]+@[A-Za-z0-9.-]+[.][A-Za-z]+$'), + --username VARCHAR(32) NOT NULL, + is_equiped BOOL NOT NULL, + equipment_name VARCHAR(32) NOT NULL, + use lyceum.EQUIPMENT_USE NOT NULL, + kind lyceum.EQUIPMENT_KIND NOT NULL, + CHECK (lyceum.check_equipment_position_compatibility(use, kind)), + --FOREIGN KEY (name, username, e_mail) REFERENCES lyceum.character(name, username, e_mail), + FOREIGN KEY (name) REFERENCES lyceum.character(name), + FOREIGN KEY (equipment_name, kind) REFERENCES lyceum.equipment(name, kind), + PRIMARY KEY(name, equipment_name) +); diff --git a/database/migrations/000005_lyceum_spells.sql b/database/migrations/000005_lyceum_spells.sql new file mode 100644 index 0000000..04740c7 --- /dev/null +++ b/database/migrations/000005_lyceum_spells.sql @@ -0,0 +1,209 @@ +CREATE TYPE lyceum.SPELL_DESTRUCTION_TYPE AS ENUM( + 'MAGIC' +); + +CREATE TYPE lyceum.SPELL_DAMAGE_TYPE AS ENUM( + 'FIRE', + 'PHYSICAL' +); + +CREATE TYPE lyceum.SPELL_CONJURATION_TYPE AS ENUM( + 'FAKE WEAPONS', + 'INVOCATION' +); + +CREATE TYPE lyceum.SPELL_HEAL_TYPE AS ENUM( + 'BLESSING' +); + +CREATE TYPE lyceum.SPELL_TYPE AS ENUM( + 'PROJECTILE', + 'AREA', + 'BUFF' +); + +CREATE TYPE lyceum.SPELL_TARGET AS ENUM( + 'SINGULAR', + 'VICINITY', + 'SELF' +); + +CREATE TABLE lyceum.spell( + name VARCHAR(16) NOT NULL, + description VARCHAR(32) NOT NULL, + cost SMALLINT NOT NULL CHECK (cost > 0), + duration SMALLINT NOT NULL CHECK (duration >= 0), + cast_time SMALLINT NOT NULL CHECK (cast_time >= 0), + kind lyceum.SPELL_TYPE NOT NULL, + target lyceum.SPELL_TARGET NOT NULL, + PRIMARY KEY (name) +); + +CREATE TABLE lyceum.spell_destruction( + name VARCHAR(16) NOT NULL, + base_damage SMALLINT NOT NULL CHECK (base_damage > 0), + damage_kind lyceum.SPELL_DAMAGE_TYPE NOT NULL, + destruction_kind lyceum.SPELL_DESTRUCTION_TYPE NOT NULL, + FOREIGN KEY (name) REFERENCES lyceum.spell(name) +); + +CREATE TABLE lyceum.spell_conjuration( + name VARCHAR(16) NOT NULL, + conjuration_kind lyceum.SPELL_CONJURATION_TYPE NOT NULL, + FOREIGN KEY (name) REFERENCES lyceum.spell(name) +); + +CREATE TABLE lyceum.spell_restoration( + name VARCHAR(16) NOT NULL, + base_heal SMALLINT NOT NULL CHECK (base_heal > 0), + restoration_kind lyceum.SPELL_HEAL_TYPE NOT NULL, + FOREIGN KEY (name) REFERENCES lyceum.spell(name) +); + +-- Get rid of this table for instantenous spells, and do instead: +-- Make procedures for each name of spell, which will handle the entirety +-- of the side effects of that specific spell. +-- Things like target information come as arguments to the procedure +-- We gotta check if we can store procedures in tables. If yes, we need tagged unions, +-- If no, we may have to do the switching on Erlang. + +-- Instance that are not instanteneous +CREATE TABLE lyceum.effect_instance( + id SERIAL NOT NULL, + name VARCHAR(16) NOT NULL, + duration SMALLINT NOT NULL CHECK (duration >= 0), + map_name VARCHAR(16) NOT NULL, + owner_id SERIAL NOT NULL, + -- TODO: Targets can be something else, not only points + x_position SMALLINT NOT NULL, + y_position SMALLINT NOT NULL, + FOREIGN KEY (name) REFERENCES lyceum.spell(name), + FOREIGN KEY (map_name) REFERENCES lyceum.map(name), + --FOREIGN KEY (owner_id) REFERENCES lyceum.map(name), + PRIMARY KEY (id) +); + +CREATE OR REPLACE FUNCTION effect_to_projectile() RETURNS trigger AS $effect_to_projectile$ +DECLARE + spell_type TEXT; + x_position SMALLINT; + y_position SMALLINT; +BEGIN + SELECT kind INTO spell_type FROM lyceum.spell WHERE name = NEW.name; + + IF spell_type = 'PROJECTILE' THEN + SELECT x_position, y_position INTO x_position, y_position + FROM lyceum.character + WHERE id = NEW.owner_id; + + INSERT INTO lyceum.projectile_instance (map_name, x_position, y_position, owner_id) + VALUES (NEW.map_name, x_position, y_position, NEW.owner_id); + END IF; + + RETURN NEW; +END; +$effect_to_projectile$ LANGUAGE plpgsql; + +CREATE TRIGGER effect_to_projectile BEFORE INSERT OR UPDATE ON lyceum.effect_instance +FOR EACH ROW EXECUTE FUNCTION effect_to_projectile(); + +-- - Do we need to record casted spells? +-- * Depends on duration (are we making `owner VARCHAR(32) NULL`?) +-- * Lifetime of spells may differ from their projectiles (if they are present) +-- - How will a casted spell interact with a projectile? +-- - How will we bind a projectile to an owner? +-- - How (and should we) will bind spells to an owner? +-- - How will NPCs cast spells? Yes, done below. + +CREATE TYPE lyceum.PROJECTILE_PATHS AS ENUM( + 'LINEAR', + 'CURVE' +); + +CREATE TABLE lyceum.projectile( + spell_name VARCHAR(16) NOT NULL, + initial_velocity FLOAT(4) NOT NULL, + duration SMALLINT NOT NULL CHECK (duration > 0), + path_function lyceum.PROJECTILE_PATHS NOT NULL, + PRIMARY KEY (spell_name), + FOREIGN KEY (spell_name) REFERENCES lyceum.spell(name) +); + +CREATE TABLE lyceum.projectile_instance( + id SERIAL NOT NULL, + map_name VARCHAR(16) NOT NULL, + duration SMALLINT NOT NULL CHECK (duration > 0), + x_position SMALLINT NOT NULL, + y_position SMALLINT NOT NULL, + owner_id SERIAL NOT NULL, + PRIMARY KEY (id) +); + +CREATE OR REPLACE VIEW lyceum.view_spell_destruction AS +SELECT * FROM lyceum.spell +NATURAL JOIN lyceum.spell_destruction; + +CREATE OR REPLACE VIEW lyceum.view_spell_conjuration AS +SELECT * FROM lyceum.spell +NATURAL JOIN lyceum.spell_conjuration; + +CREATE OR REPLACE VIEW lyceum.view_spell_restoration AS +SELECT * FROM lyceum.spell +NATURAL JOIN lyceum.spell_restoration; + +CREATE OR REPLACE FUNCTION lyceum.view_spell_destruction_upsert() RETURNS trigger LANGUAGE plpgsql AS $$ +BEGIN + + INSERT INTO lyceum.spell(name, description, cost, duration, cast_time, kind, target) + VALUES (NEW.name, NEW.description, NEW.cost, NEW.duration, NEW.cast_time, NEW.kind, NEW.target) + ON CONFLICT DO NOTHING; + + INSERT INTO lyceum.spell_destruction(name, base_damage, damage_kind, destruction_kind) + VALUES (NEW.name, NEW.base_damage, NEW.damage_kind, NEW.destruction_kind) + ON CONFLICT DO NOTHING; + + RETURN NEW; +END +$$; + +CREATE OR REPLACE FUNCTION lyceum.view_spell_restoration_upsert() RETURNS trigger LANGUAGE plpgsql AS $$ +BEGIN + + INSERT INTO lyceum.spell(name, description, cost, duration, cast_time, kind, target) + VALUES (NEW.name, NEW.description, NEW.cost, NEW.duration, NEW.cast_time, NEW.kind, NEW.target) + ON CONFLICT DO NOTHING; + + INSERT INTO lyceum.spell_restoration(name, base_heal, restoration_kind) + VALUES (NEW.name, NEW.base_heal, NEW.restoration_kind) + ON CONFLICT DO NOTHING; + + RETURN NEW; +END +$$; + +CREATE OR REPLACE FUNCTION lyceum.view_spell_conjuration_upsert() RETURNS trigger LANGUAGE plpgsql AS $$ +BEGIN + + INSERT INTO lyceum.spell(name, description, cost, duration, cast_time, kind, target) + VALUES (NEW.name, NEW.description, NEW.cost, NEW.duration, NEW.cast_time, NEW.kind, NEW.target) + ON CONFLICT DO NOTHING; + + INSERT INTO lyceum.spell_conjuration(name, conjuration_kind) + VALUES (NEW.name, NEW.conjuration_kind) + ON CONFLICT DO NOTHING; + + RETURN NEW; +END +$$; + +CREATE OR REPLACE TRIGGER trigger_spell_destruction_upsert +INSTEAD OF INSERT ON lyceum.view_spell_destruction +FOR EACH ROW EXECUTE FUNCTION lyceum.view_spell_destruction_upsert(); + +CREATE OR REPLACE TRIGGER trigger_spell_conjuration_upsert +INSTEAD OF INSERT ON lyceum.view_spell_conjuration +FOR EACH ROW EXECUTE FUNCTION lyceum.view_spell_conjuration_upsert(); + +CREATE OR REPLACE TRIGGER trigger_spell_restoration_upsert +INSTEAD OF INSERT ON lyceum.view_spell_restoration +FOR EACH ROW EXECUTE FUNCTION lyceum.view_spell_restoration_upsert(); diff --git a/flake.nix b/flake.nix index 085e566..5518ac6 100644 --- a/flake.nix +++ b/flake.nix @@ -262,6 +262,8 @@ scripts = { build.exec = "just build"; server.exec = "just server"; + db-up.exec = "just db-up"; + db-down.exec = "just db-down"; }; enterShell = '' diff --git a/justfile b/justfile index 30cf9f5..2cc30b8 100644 --- a/justfile +++ b/justfile @@ -84,6 +84,14 @@ server: build test: rebar3 do eunit, ct +# Migrates the DB (up) +db-up: + ./migrate_up.sh + +# Nukes the DB +db-down: + ./migrate_down.sh + # -------- # Releases # -------- diff --git a/migrate_down.sh b/migrate_down.sh new file mode 100755 index 0000000..fe96954 --- /dev/null +++ b/migrate_down.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -euxo pipefail + +PG_URL=${PG_URL:-"postgresql://admin:admin@127.0.0.1:5432/mmo"} + +echo "Setting PG_URL=${PG_URL}" + +psql $PG_URL -1 -v ON_ERROR_STOP=1 -f ./database/main.down.sql diff --git a/migrate_up.sh b/migrate_up.sh new file mode 100755 index 0000000..f105964 --- /dev/null +++ b/migrate_up.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -euxo pipefail + +PG_URL=${PG_URL:-"postgresql://admin:admin@127.0.0.1:5432/mmo"} + +echo "Setting PG_URL=${PG_URL}" + +find ./database/migrations -iname "*.sql" | sort | xargs printf -- '-f %s\n' | xargs psql $PG_URL -1 -v ON_ERROR_STOP=1; diff --git a/src/database.erl b/src/database.erl index 984e90b..d23fd28 100644 --- a/src/database.erl +++ b/src/database.erl @@ -80,7 +80,7 @@ epgsql_query_fun(Conn) -> end. database_connect() -> - io:format("Connecting to: ~p~n", [?PGHOST]), + io:format("Connecting to ~p at ~p~n", [?PGHOST, ?PGPORT]), {ok, Connection} = epgsql:connect(#{host => ?PGHOST, username => ?PGUSER,