feat(conditions): evaluate QUEST_ACTIVE and QUEST_COMPLETED

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Bastian Wagner
2026-08-22 22:48:02 +02:00
parent e13f6533e5
commit d1cca086c6
4 changed files with 185 additions and 4 deletions

View File

@@ -4,6 +4,9 @@ import { Character } from '../characters/entities/character.entity';
import { CharacterItem } from '../items/entities/character-item.entity';
import { ItemDefinition } from '../items/entities/item-definition.entity';
import { CharacterNpcState } from '../npcs/entities/character-npc-state.entity';
import { CharacterQuest } from '../quests/entities/character-quest.entity';
import { QuestDefinition } from '../quests/entities/quest-definition.entity';
import { CharacterQuestStatus } from '../quests/quest.types';
import { CharacterReputation } from '../reputation/entities/character-reputation.entity';
import { ReputationFaction } from '../reputation/entities/reputation-faction.entity';
import {
@@ -124,11 +127,63 @@ export class GameConditionService {
return this.evaluateFlag(context, condition, scope);
case GameConditionType.HAS_ITEM:
return this.evaluateHasItem(context, condition, scope);
case GameConditionType.QUEST_ACTIVE:
return this.evaluateQuestStatus(
context,
condition,
scope,
CharacterQuestStatus.ACTIVE,
);
case GameConditionType.QUEST_COMPLETED:
return this.evaluateQuestStatus(
context,
condition,
scope,
CharacterQuestStatus.COMPLETED,
);
default:
return { met: false, actual: null };
}
}
/**
* Whether the character stands in a given place with a given quest (§19).
*
* Boolean-shaped like `FLAG_SET` rather than numeric: a quest is active or it
* is not, so `actual` stays null -- reporting "Current: 0" for it would be a
* lie about a boolean, and the shop view renders these outcomes as
* player-facing requirements.
*
* `value: false` is how content asks for the negative. The warden's offer
* line needs "not active and not completed", and this keeps that expressible
* without adding a NOT to the condition vocabulary.
*/
private async evaluateQuestStatus(
context: ConditionContext,
condition: GameCondition,
scope: RepositoryScope,
status: CharacterQuestStatus,
): Promise<ConditionEvaluation> {
if (!condition.key) {
return { met: false, actual: null };
}
const quest = await scope
.getRepository(QuestDefinition)
.findOneBy({ key: condition.key, enabled: true });
if (!quest) {
return { met: false, actual: null };
}
const row = await scope.getRepository(CharacterQuest).findOneBy({
characterId: context.characterId,
questId: quest.id,
});
const expected = condition.value ?? true;
return { met: (row?.status === status) === expected, actual: null };
}
private async evaluateRegionReputation(
context: ConditionContext,
condition: GameCondition,