feat(loot): add loot and reward migration
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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"');
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user