Merge branch 'feature/post-combat-rewards'
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
createRunState,
|
||||
enterCurrentRoom,
|
||||
getAvailableMoves,
|
||||
isCurrentRoomCombatReady,
|
||||
resolveRunEnemyTurn,
|
||||
resolveRunPlayerTurn,
|
||||
startCombatInCurrentRoom,
|
||||
@@ -184,6 +185,73 @@ describe("run state flow", () => {
|
||||
expect(result.run.dungeon.levels["1"]!.rooms["room.level1.start"]!.encounter?.rewardPending).toBe(
|
||||
false,
|
||||
);
|
||||
expect(result.run.adventurerSnapshot.xp).toBe(2);
|
||||
expect(result.run.xpGained).toBe(2);
|
||||
expect(result.run.defeatedCreatureIds).toEqual(["creature.level1.guard"]);
|
||||
expect(result.run.log.at(-1)?.text).toContain("Victory rewards");
|
||||
});
|
||||
|
||||
it("lists available traversable exits for the current room", () => {
|
||||
const run = createRunState({
|
||||
content: sampleContentPack,
|
||||
campaignId: "campaign.1",
|
||||
adventurer: createAdventurer(),
|
||||
});
|
||||
|
||||
expect(getAvailableMoves(run)).toEqual([
|
||||
expect.objectContaining({
|
||||
direction: "north",
|
||||
generated: false,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("travels through an unresolved exit, generates a room, and enters it", () => {
|
||||
const run = createRunState({
|
||||
content: sampleContentPack,
|
||||
campaignId: "campaign.1",
|
||||
adventurer: createAdventurer(),
|
||||
at: "2026-03-15T14:00:00.000Z",
|
||||
});
|
||||
|
||||
const result = travelCurrentExit({
|
||||
content: sampleContentPack,
|
||||
run,
|
||||
exitDirection: "north",
|
||||
roller: createSequenceRoller([1, 1]),
|
||||
at: "2026-03-15T14:05:00.000Z",
|
||||
});
|
||||
|
||||
expect(result.run.currentRoomId).toBe("room.level1.room.002");
|
||||
expect(result.run.dungeon.levels["1"]!.discoveredRoomOrder).toEqual([
|
||||
"room.level1.start",
|
||||
"room.level1.room.002",
|
||||
]);
|
||||
expect(result.run.dungeon.levels["1"]!.rooms["room.level1.room.002"]!.discovery.entered).toBe(
|
||||
true,
|
||||
);
|
||||
expect(result.run.log[0]?.text).toContain("Travelled north");
|
||||
});
|
||||
|
||||
it("flags combat-ready rooms once entry resolves a hostile encounter", () => {
|
||||
const run = createRunState({
|
||||
content: sampleContentPack,
|
||||
campaignId: "campaign.1",
|
||||
adventurer: createAdventurer(),
|
||||
at: "2026-03-15T14:00:00.000Z",
|
||||
});
|
||||
const room = run.dungeon.levels["1"]!.rooms["room.level1.start"]!;
|
||||
|
||||
room.encounter = {
|
||||
id: `${room.id}.encounter`,
|
||||
sourceTableCode: "L1G",
|
||||
creatureIds: ["a", "b"],
|
||||
creatureNames: ["Guard", "Warrior"],
|
||||
resultLabel: "Guard and Warrior",
|
||||
resolved: true,
|
||||
};
|
||||
|
||||
expect(isCurrentRoomCombatReady(run)).toBe(true);
|
||||
});
|
||||
|
||||
it("lists available traversable exits for the current room", () => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
RunState,
|
||||
} from "@/types/state";
|
||||
import type { LogEntry } from "@/types/rules";
|
||||
import { findCreatureById } from "@/data/contentHelpers";
|
||||
|
||||
import { startCombatFromRoom } from "./combat";
|
||||
import {
|
||||
@@ -232,6 +233,52 @@ function appendLogs(run: RunState, logEntries: LogEntry[]) {
|
||||
run.log.push(...logEntries);
|
||||
}
|
||||
|
||||
function createRewardLog(
|
||||
id: string,
|
||||
at: string,
|
||||
text: string,
|
||||
relatedIds: string[],
|
||||
): LogEntry {
|
||||
return {
|
||||
id,
|
||||
at,
|
||||
type: "progression",
|
||||
text,
|
||||
relatedIds,
|
||||
};
|
||||
}
|
||||
|
||||
function applyCombatRewards(
|
||||
content: ContentPack,
|
||||
run: RunState,
|
||||
completedCombat: CombatState,
|
||||
at: string,
|
||||
) {
|
||||
const defeatedCreatureIds = completedCombat.enemies
|
||||
.filter((enemy) => enemy.hpCurrent === 0 && enemy.sourceDefinitionId)
|
||||
.map((enemy) => enemy.sourceDefinitionId!);
|
||||
const xpAwarded = defeatedCreatureIds.reduce((total, creatureId) => {
|
||||
return total + (findCreatureById(content, creatureId).xpReward ?? 0);
|
||||
}, 0);
|
||||
|
||||
run.defeatedCreatureIds.push(...defeatedCreatureIds);
|
||||
run.xpGained += xpAwarded;
|
||||
run.adventurerSnapshot.xp += xpAwarded;
|
||||
|
||||
if (xpAwarded === 0) {
|
||||
return [] as LogEntry[];
|
||||
}
|
||||
|
||||
return [
|
||||
createRewardLog(
|
||||
`${completedCombat.id}.rewards`,
|
||||
at,
|
||||
`Victory rewards: gained ${xpAwarded} XP from ${defeatedCreatureIds.length} defeated creature${defeatedCreatureIds.length === 1 ? "" : "s"}.`,
|
||||
[completedCombat.id, ...defeatedCreatureIds],
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
export function createRunState(options: CreateRunOptions): RunState {
|
||||
const at = options.at ?? new Date().toISOString();
|
||||
const levelState = initializeDungeonLevel({
|
||||
@@ -256,6 +303,8 @@ export function createRunState(options: CreateRunOptions): RunState {
|
||||
globalFlags: [],
|
||||
},
|
||||
adventurerSnapshot: options.adventurer,
|
||||
defeatedCreatureIds: [],
|
||||
xpGained: 0,
|
||||
log: [],
|
||||
pendingEffects: [],
|
||||
};
|
||||
@@ -298,6 +347,16 @@ export function getAvailableMoves(run: RunState): AvailableMove[] {
|
||||
}));
|
||||
}
|
||||
|
||||
export function isCurrentRoomCombatReady(run: RunState) {
|
||||
const room = requireCurrentRoom(run);
|
||||
|
||||
return Boolean(
|
||||
room.encounter?.resolved &&
|
||||
room.encounter.creatureNames &&
|
||||
room.encounter.creatureNames.length > 0,
|
||||
);
|
||||
}
|
||||
|
||||
export function travelCurrentExit(
|
||||
options: TravelCurrentExitOptions,
|
||||
): RunTransitionResult {
|
||||
@@ -427,9 +486,16 @@ export function resolveRunPlayerTurn(
|
||||
appendLogs(run, result.logEntries);
|
||||
|
||||
if (result.combatEnded) {
|
||||
const completedCombat = result.combat;
|
||||
const levelState = requireCurrentLevel(run);
|
||||
const roomId = requireCurrentRoomId(run);
|
||||
const room = levelState.rooms[roomId];
|
||||
const rewardLogs = applyCombatRewards(
|
||||
options.content,
|
||||
run,
|
||||
completedCombat,
|
||||
options.at ?? new Date().toISOString(),
|
||||
);
|
||||
|
||||
if (room?.encounter) {
|
||||
room.encounter.rewardPending = false;
|
||||
@@ -437,6 +503,7 @@ export function resolveRunPlayerTurn(
|
||||
}
|
||||
|
||||
run.activeCombat = undefined;
|
||||
appendLogs(run, rewardLogs);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user