feat(loot): add item, loot table, and combat reward entities

Declares every new TypeORM entity Slice 0.4 needs (ItemDefinition,
CharacterItem, LootTable, LootTableEntry, CombatReward, CombatRewardItem)
plus the ItemType/EquipmentSlot/ItemRarity enums, and adds the two columns
existing entities gain: Character.silver and MonsterDefinition.lootTableId.
No migration SQL or service logic yet - just schema declarations backed by
a metadata-driven schema spec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Bastian Wagner
2026-08-19 23:29:30 +02:00
parent a5f7772a6d
commit af9d422e8b
15 changed files with 455 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
import {
Column,
CreateDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import { CharacterItem } from '../../items/entities/character-item.entity';
import { ItemDefinition } from '../../items/entities/item-definition.entity';
import { CombatReward } from './combat-reward.entity';
/**
* What this specific combat dropped.
*
* Needed because `CharacterItem.quantity` is a running stack total: after a
* duplicate drop it no longer says how much *this* victory granted, and a
* refreshed reward screen must replay the original result (spec §25, §48).
*/
@Entity({ name: 'combat_reward_items' })
@Index('IDX_combat_reward_items_reward', ['combatRewardId'])
@Index('IDX_combat_reward_items_reward_item', ['combatRewardId', 'itemDefinitionId'], {
unique: true,
})
export class CombatRewardItem {
@PrimaryGeneratedColumn('uuid', { name: 'id' })
id!: string;
@Column({ name: 'combat_reward_id', type: 'uuid' })
combatRewardId!: string;
@Column({ name: 'character_item_id', type: 'uuid' })
characterItemId!: string;
@Column({ name: 'item_definition_id', type: 'uuid' })
itemDefinitionId!: string;
@Column({ name: 'quantity', type: 'integer' })
quantity!: number;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt!: Date;
@ManyToOne(() => CombatReward, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'combat_reward_id' })
combatReward!: CombatReward;
@ManyToOne(() => CharacterItem, { onDelete: 'RESTRICT' })
@JoinColumn({ name: 'character_item_id' })
characterItem!: CharacterItem;
@ManyToOne(() => ItemDefinition, { onDelete: 'RESTRICT' })
@JoinColumn({ name: 'item_definition_id' })
itemDefinition!: ItemDefinition;
}

View File

@@ -0,0 +1,48 @@
import {
Column,
CreateDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import { Character } from '../../characters/entities/character.entity';
import { Combat } from '../../combat/entities/combat.entity';
/**
* Proof that one combat has already been rewarded (spec §8).
*
* The unique index on `combatId` is the database half of the idempotency
* invariant; `CombatRewardService` is the service half.
*/
@Entity({ name: 'combat_rewards' })
@Index('IDX_combat_rewards_combat', ['combatId'], { unique: true })
@Index('IDX_combat_rewards_character', ['characterId'])
export class CombatReward {
@PrimaryGeneratedColumn('uuid', { name: 'id' })
id!: string;
@Column({ name: 'combat_id', type: 'uuid' })
combatId!: string;
@Column({ name: 'character_id', type: 'uuid' })
characterId!: string;
@Column({ name: 'experience_granted', type: 'integer' })
experienceGranted!: number;
@Column({ name: 'silver_granted', type: 'integer' })
silverGranted!: number;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt!: Date;
@ManyToOne(() => Combat, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'combat_id' })
combat!: Combat;
@ManyToOne(() => Character, { onDelete: 'RESTRICT' })
@JoinColumn({ name: 'character_id' })
character!: Character;
}