Merge branch 'master' into worktree-playable-slice-0.6.5-renown-reputation

Reconciles Slice 0.6.5 (Renown & Reputation Foundation) against master's
persistent-HP-and-regeneration slice, which landed independently and
touches several of the same files (Character entity, CombatService,
EquipmentService, the inventory detail panel).

Conflict resolutions:
- CharacterStatsService/EquipmentService constructor wiring: kept
  master's CharacterVitalsService injection, which this branch's
  version of the same files didn't have yet.
- CombatService.performAction: kept master's HP-guard logic
  (characterTooWounded, vitals pause-on-enter) alongside this branch's
  multi-line calculate() call style.
- Inventory detail panel (.html/.ts/.scss/.spec.ts): master had
  redesigned the panel (wrapping section, rarity styling, flavour
  text, a shared inventory.labels.ts) on top of the OLD level-gated
  component, since this branch's removal of the level gate (R4, Task
  8/14) hadn't reached master yet. Kept master's visual redesign in
  full, but with the level-gate concept removed throughout: no
  requiredLevel stat block, no meetsLevelRequirement() branch in the
  equip button, no now-dead .detail__value--unmet SCSS rule. Kept both
  branches' independent tests (non-equippable-item, flavour-text).
- inventory-page.component.ts: dropped master's dead characterLevel
  computed (nothing in the template read it, and the level concept is
  gone); kept its independent bagCells/bagUsed/bagCapacity grid
  feature, which has nothing to do with renown or level.

Post-merge fixture repairs (three files failed the Angular bundle
compile because they predate master's hpRegenPerSecond/hpRegenSince
fields or master's item description field, neither conflict-marked
since git considered them non-overlapping edits):
- app.spec.ts: a 'renders loaded character values' test added on
  master after this branch forked still used the abolished level/
  experience fields on its decoy fixture -- retargeted to renown.
- inventory-detail-panel.component.spec.ts: the ashPelt fixture added
  by this branch's final-review follow-up predates master's required
  description field.
- top-bar.component.spec.ts: this branch's fixture predates master's
  required hpRegenPerSecond/hpRegenSince fields.

No database migration touches the same column: master's
1792000000000-AddHpRegeneration only adds characters.hp_regen_since,
independent of this slice's 1791000000000-CreateRenownAndReputation.
Timestamp ordering between the two was already correct with no rename
needed.

Verified: API 288/288 (267 from this slice + 21 from master), API
build zero errors, web 237/237 (230 from this slice + 7 from master).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Bastian Wagner
2026-08-21 19:23:10 +02:00
75 changed files with 9250 additions and 1840 deletions

View File

@@ -0,0 +1,25 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddHpRegeneration1792000000000 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'ALTER TABLE "characters" ADD COLUMN "hp_regen_since" TIMESTAMP WITH TIME ZONE',
);
// Existing characters start regenerating immediately from their current
// HP. A character whose fight is still ACTIVE keeps regeneration paused
// until that fight resolves, matching the "no combat-time regen" rule
// (persistent-hp-and-regeneration design, R4) -- this migration must not
// gift them free healing mid-fight.
await queryRunner.query('UPDATE "characters" SET "hp_regen_since" = now()');
await queryRunner.query(`UPDATE "characters" AS "character"
SET "hp_regen_since" = NULL
FROM "combats" AS "combat"
WHERE "combat"."character_id" = "character"."id"
AND "combat"."status" = 'ACTIVE'`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE "characters" DROP COLUMN "hp_regen_since"');
}
}

View File

@@ -0,0 +1,17 @@
import 'reflect-metadata';
import { getMetadataArgsStorage } from 'typeorm';
import { Character } from '../../characters/entities/character.entity';
describe('characters.hp_regen_since schema', () => {
it('stores the regeneration anchor as a nullable timestamptz', () => {
const metadata = getMetadataArgsStorage();
const column = metadata.columns.find(
(candidate) =>
candidate.target === Character && candidate.propertyName === 'hpRegenSince',
);
expect(column).toBeDefined();
expect(column?.options.type).toBe('timestamptz');
expect(column?.options.nullable).toBe(true);
});
});

View File

@@ -251,6 +251,7 @@ export async function seedVisibleVerticalSlice(
baseHp: 100,
baseAttack: 6,
currentHp: 100,
hpRegenSince: new Date(),
currentLocationId: southGateId,
});
}