fix: address the final whole-branch review's findings

The branch review approved the slice with no blocking findings. These
are the substantive non-blocking ones.

N1, the one no per-task review could see: every seeded monster rolls
silverMin/silverMax = 0 (R7), so the victory screen showed "Silber +0"
after every fight in the shipped game. Three tasks were each correct in
isolation -- the mechanism stays, the values are zero, the field still
exists -- and the composite was wrong. Now conditional, with a test.
This does not contradict R16: R16 deleted the XP block because the
field ceased to exist, leaving nothing to hide. Silver still exists and
can be non-zero, so a conditional is the right tool.

N2: world.store.ts still said a combat "granted XP and silver". Same
false-fact-in-a-comment defect fixed in b5bcd50, one file over.

N4: resolveReputationRank threw a TypeError on negative input, since
findIndex returns -1 and REPUTATION_RANKS[-1] is undefined. Unreachable
today, but grantReputation is public and accepts any number.

N5/N6: grantReputation resolved factions without the enabled filter the
read path applies, so a disabled faction could accumulate invisible
reputation -- "disabled" was not actually a kill switch. The dense read
also had no ORDER BY, so the list could reorder between requests.

N8: design 13 requires silver stay 0 for every seeded monster; only two
of four were pinned. Re-adding silver to the others would have shipped
silently.

N9: spec 36's "a normal kill grants no Renown" had no test. It was
structurally guaranteed but unasserted -- now locked down against a
later slice wiring renown into combat.

N10: an impossible renown: 0 fixture, and RENOWN_MIN exported but never
used to clamp the floor.

API 268/268, web 230/230, API build zero errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Bastian Wagner
2026-08-21 18:50:30 +02:00
parent 0c9f00d9a8
commit 23ed527eea
9 changed files with 63 additions and 16 deletions

View File

@@ -139,12 +139,14 @@
<section class="rewards" data-combat-rewards aria-label="Belohnungen">
<h3 class="rewards__title">Belohnungen</h3>
<dl class="rewards__currencies">
<div class="rewards__currency" data-reward-silver>
<dt>Silber</dt>
<dd>+{{ rewards.silver }}</dd>
</div>
</dl>
@if (rewards.silver) {
<dl class="rewards__currencies">
<div class="rewards__currency" data-reward-silver>
<dt>Silber</dt>
<dd>+{{ rewards.silver }}</dd>
</div>
</dl>
}
@if (rewards.items.length) {
<h3 class="rewards__title">Beute</h3>

View File

@@ -540,6 +540,20 @@ describe('CombatPageComponent', () => {
expect(element.querySelector('[data-reward-experience]')).toBeNull();
});
it('hides the silver tile entirely when the kill granted none', async () => {
// Every monster seeded in this slice rolls silverMin/silverMax = 0 (design
// R7), so without this guard the victory screen advertises "Silber +0"
// after every single fight in the shipped content.
const fixture = await setup({
...wonWithRewards,
rewards: { silver: 0, items: [] },
});
const element = fixture.nativeElement as HTMLElement;
expect(element.querySelector('[data-reward-silver]')).toBeNull();
expect(element.querySelector('[data-combat-rewards]')).toBeTruthy();
});
it('renders a dropped item with its icon, name, and rarity', async () => {
const fixture = await setup({
...wonWithRewards,

View File

@@ -61,7 +61,7 @@ const equipment: EquipmentResponse = {
const character: CharacterResponse = {
id: 'character-1',
name: 'Aric Duskwalker',
renown: 0,
renown: 1,
silver: 0,
currentHp: 100,
maxHp: 100,

View File

@@ -87,8 +87,10 @@ export class WorldStore implements OnDestroy {
}
/**
* Re-reads the character from the server, e.g. after a combat granted XP and
* silver. Never mutates the values locally: the server owns them (spec §35).
* Re-reads the character from the server, e.g. after a combat granted item
* drops. Renown comes from milestones, and silver reaches the player through
* turn-ins -- a kill grants neither.
* Never mutates the values locally: the server owns them (spec §35).
* A failed refresh leaves the last known character in place rather than
* blanking the HUD.
*/