feat(loot): add loot and reward migration

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Bastian Wagner
2026-08-19 23:39:17 +02:00
parent af9d422e8b
commit fe130c597e

View File

@@ -0,0 +1,174 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateLootAndRewards1788600000000 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
// Existing characters keep their progression; silver simply starts at 0.
await queryRunner.query(
'ALTER TABLE "characters" ADD COLUMN "silver" integer NOT NULL DEFAULT 0',
);
await queryRunner.query(
"CREATE TYPE \"item_type_enum\" AS ENUM ('WEAPON', 'ARMOR', 'MATERIAL', 'CONSUMABLE')",
);
await queryRunner.query(
"CREATE TYPE \"equipment_slot_enum\" AS ENUM ('WEAPON', 'HEAD', 'CHEST', 'HANDS', 'LEGS', 'FEET', 'AMULET')",
);
await queryRunner.query(
"CREATE TYPE \"item_rarity_enum\" AS ENUM ('COMMON', 'RARE', 'EPIC')",
);
await queryRunner.query(`CREATE TABLE "item_definitions" (
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
"key" character varying(100) NOT NULL,
"name" character varying(150) NOT NULL,
"description" text NOT NULL,
"type" "item_type_enum" NOT NULL,
"equipment_slot" "equipment_slot_enum",
"rarity" "item_rarity_enum" NOT NULL,
"tier" integer NOT NULL,
"required_level" integer NOT NULL,
"weapon_damage" integer NOT NULL DEFAULT 0,
"bonus_hp" integer NOT NULL DEFAULT 0,
"bonus_attack" integer NOT NULL DEFAULT 0,
"bonus_armor" integer NOT NULL DEFAULT 0,
"sell_price" integer NOT NULL DEFAULT 0,
"icon_path" character varying(255) NOT NULL,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
CONSTRAINT "PK_item_definitions" PRIMARY KEY ("id")
)`);
await queryRunner.query(
'CREATE UNIQUE INDEX "IDX_item_definitions_key" ON "item_definitions" ("key")',
);
await queryRunner.query(`CREATE TABLE "loot_tables" (
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
"key" character varying(100) NOT NULL,
"name" character varying(150) NOT NULL,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
CONSTRAINT "PK_loot_tables" PRIMARY KEY ("id")
)`);
await queryRunner.query(
'CREATE UNIQUE INDEX "IDX_loot_tables_key" ON "loot_tables" ("key")',
);
await queryRunner.query(`CREATE TABLE "loot_table_entries" (
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
"loot_table_id" uuid NOT NULL,
"item_definition_id" uuid NOT NULL,
"position" integer NOT NULL,
"drop_chance" numeric(5,4) NOT NULL,
"min_quantity" integer NOT NULL DEFAULT 1,
"max_quantity" integer NOT NULL DEFAULT 1,
"enabled" boolean NOT NULL DEFAULT true,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
CONSTRAINT "PK_loot_table_entries" PRIMARY KEY ("id"),
CONSTRAINT "CHK_loot_table_entries_drop_chance" CHECK ("drop_chance" >= 0 AND "drop_chance" <= 1),
CONSTRAINT "CHK_loot_table_entries_quantity" CHECK ("min_quantity" >= 1 AND "max_quantity" >= "min_quantity"),
CONSTRAINT "FK_loot_table_entries_loot_table" FOREIGN KEY ("loot_table_id") REFERENCES "loot_tables"("id") ON DELETE CASCADE ON UPDATE NO ACTION,
CONSTRAINT "FK_loot_table_entries_item_definition" FOREIGN KEY ("item_definition_id") REFERENCES "item_definitions"("id") ON DELETE RESTRICT ON UPDATE NO ACTION
)`);
await queryRunner.query(
'CREATE UNIQUE INDEX "IDX_loot_table_entries_table_position" ON "loot_table_entries" ("loot_table_id", "position")',
);
await queryRunner.query(
'CREATE UNIQUE INDEX "IDX_loot_table_entries_table_item" ON "loot_table_entries" ("loot_table_id", "item_definition_id")',
);
await queryRunner.query(
'ALTER TABLE "monster_definitions" ADD COLUMN "loot_table_id" uuid',
);
await queryRunner.query(
'ALTER TABLE "monster_definitions" ADD CONSTRAINT "FK_monster_definitions_loot_table" FOREIGN KEY ("loot_table_id") REFERENCES "loot_tables"("id") ON DELETE RESTRICT ON UPDATE NO ACTION',
);
await queryRunner.query(
'CREATE INDEX "IDX_monster_definitions_loot_table" ON "monster_definitions" ("loot_table_id")',
);
await queryRunner.query(`CREATE TABLE "character_items" (
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
"character_id" uuid NOT NULL,
"item_definition_id" uuid NOT NULL,
"quantity" integer NOT NULL,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
CONSTRAINT "PK_character_items" PRIMARY KEY ("id"),
CONSTRAINT "CHK_character_items_quantity" CHECK ("quantity" >= 1),
CONSTRAINT "FK_character_items_character" FOREIGN KEY ("character_id") REFERENCES "characters"("id") ON DELETE CASCADE ON UPDATE NO ACTION,
CONSTRAINT "FK_character_items_item_definition" FOREIGN KEY ("item_definition_id") REFERENCES "item_definitions"("id") ON DELETE RESTRICT ON UPDATE NO ACTION
)`);
await queryRunner.query(
'CREATE UNIQUE INDEX "IDX_character_items_character_item" ON "character_items" ("character_id", "item_definition_id")',
);
await queryRunner.query(`CREATE TABLE "combat_rewards" (
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
"combat_id" uuid NOT NULL,
"character_id" uuid NOT NULL,
"experience_granted" integer NOT NULL,
"silver_granted" integer NOT NULL,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
CONSTRAINT "PK_combat_rewards" PRIMARY KEY ("id"),
CONSTRAINT "FK_combat_rewards_combat" FOREIGN KEY ("combat_id") REFERENCES "combats"("id") ON DELETE CASCADE ON UPDATE NO ACTION,
CONSTRAINT "FK_combat_rewards_character" FOREIGN KEY ("character_id") REFERENCES "characters"("id") ON DELETE RESTRICT ON UPDATE NO ACTION
)`);
// The database half of the "one reward per combat" invariant (spec §7).
await queryRunner.query(
'CREATE UNIQUE INDEX "IDX_combat_rewards_combat" ON "combat_rewards" ("combat_id")',
);
await queryRunner.query(
'CREATE INDEX "IDX_combat_rewards_character" ON "combat_rewards" ("character_id")',
);
await queryRunner.query(`CREATE TABLE "combat_reward_items" (
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
"combat_reward_id" uuid NOT NULL,
"character_item_id" uuid NOT NULL,
"item_definition_id" uuid NOT NULL,
"quantity" integer NOT NULL,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
CONSTRAINT "PK_combat_reward_items" PRIMARY KEY ("id"),
CONSTRAINT "CHK_combat_reward_items_quantity" CHECK ("quantity" >= 1),
CONSTRAINT "FK_combat_reward_items_reward" FOREIGN KEY ("combat_reward_id") REFERENCES "combat_rewards"("id") ON DELETE CASCADE ON UPDATE NO ACTION,
CONSTRAINT "FK_combat_reward_items_character_item" FOREIGN KEY ("character_item_id") REFERENCES "character_items"("id") ON DELETE RESTRICT ON UPDATE NO ACTION,
CONSTRAINT "FK_combat_reward_items_item_definition" FOREIGN KEY ("item_definition_id") REFERENCES "item_definitions"("id") ON DELETE RESTRICT ON UPDATE NO ACTION
)`);
await queryRunner.query(
'CREATE INDEX "IDX_combat_reward_items_reward" ON "combat_reward_items" ("combat_reward_id")',
);
await queryRunner.query(
'CREATE UNIQUE INDEX "IDX_combat_reward_items_reward_item" ON "combat_reward_items" ("combat_reward_id", "item_definition_id")',
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP INDEX "IDX_combat_reward_items_reward_item"');
await queryRunner.query('DROP INDEX "IDX_combat_reward_items_reward"');
await queryRunner.query('DROP TABLE "combat_reward_items"');
await queryRunner.query('DROP INDEX "IDX_combat_rewards_character"');
await queryRunner.query('DROP INDEX "IDX_combat_rewards_combat"');
await queryRunner.query('DROP TABLE "combat_rewards"');
await queryRunner.query('DROP INDEX "IDX_character_items_character_item"');
await queryRunner.query('DROP TABLE "character_items"');
await queryRunner.query('DROP INDEX "IDX_monster_definitions_loot_table"');
await queryRunner.query(
'ALTER TABLE "monster_definitions" DROP CONSTRAINT "FK_monster_definitions_loot_table"',
);
await queryRunner.query(
'ALTER TABLE "monster_definitions" DROP COLUMN "loot_table_id"',
);
await queryRunner.query('DROP INDEX "IDX_loot_table_entries_table_item"');
await queryRunner.query('DROP INDEX "IDX_loot_table_entries_table_position"');
await queryRunner.query('DROP TABLE "loot_table_entries"');
await queryRunner.query('DROP INDEX "IDX_loot_tables_key"');
await queryRunner.query('DROP TABLE "loot_tables"');
await queryRunner.query('DROP INDEX "IDX_item_definitions_key"');
await queryRunner.query('DROP TABLE "item_definitions"');
await queryRunner.query('DROP TYPE "item_rarity_enum"');
await queryRunner.query('DROP TYPE "equipment_slot_enum"');
await queryRunner.query('DROP TYPE "item_type_enum"');
await queryRunner.query('ALTER TABLE "characters" DROP COLUMN "silver"');
}
}