Release Healer Man 0.1.22
This commit is contained in:
1 parent
f6f2375145
commit
7f6001723a
144 files changed
+54107
-690
No files matched your search
@@ -20,6 +20,15 @@ function unique(values: readonly string[]): readonly string[] {
|
||||
return Object.freeze([...new Set(values)].sort());
|
||||
}
|
||||
|
||||
function orderedUnique(values: readonly string[]): readonly string[] {
|
||||
const seen = new Set<string>();
|
||||
return Object.freeze(values.filter((value) => {
|
||||
if (seen.has(value)) return false;
|
||||
seen.add(value);
|
||||
return true;
|
||||
}));
|
||||
}
|
||||
|
||||
export function createDungeonEncounterSnapshot(
|
||||
definition: DungeonEncounterDefinition,
|
||||
now = 0,
|
||||
@@ -53,12 +62,37 @@ export function validateDungeonEncounterDefinition(definition: DungeonEncounterD
|
||||
validateStableId(definition.id, "Encounter");
|
||||
validateStableId(definition.dungeonId, `Encounter ${definition.id} dungeon`);
|
||||
validateStableId(definition.initialPhase, `Encounter ${definition.id} initial phase`);
|
||||
if (definition.source) {
|
||||
if (!definition.source.files.length || definition.source.files.some((file) => !file.trim())) {
|
||||
throw new Error(`Encounter ${definition.id} source review must name at least one source file`);
|
||||
}
|
||||
if (!definition.source.reviewedBehavior.trim()) {
|
||||
throw new Error(`Encounter ${definition.id} source review must describe the reviewed behavior`);
|
||||
}
|
||||
if (definition.source.fidelity === "source-exact"
|
||||
&& (definition.source.adaptations.length || definition.source.remainingGaps.length)) {
|
||||
throw new Error(`Source-exact encounter ${definition.id} cannot retain adaptations or gaps`);
|
||||
}
|
||||
}
|
||||
const ruleIds = new Set<string>();
|
||||
const actionIds = new Set<string>();
|
||||
for (const rule of definition.rules) {
|
||||
validateStableId(rule.id, `Encounter ${definition.id} rule`);
|
||||
if (ruleIds.has(rule.id)) throw new Error(`Encounter ${definition.id} has duplicate rule id ${rule.id}`);
|
||||
ruleIds.add(rule.id);
|
||||
for (const guard of rule.guards ?? []) {
|
||||
if (guard.kind === "difficulty") {
|
||||
if (!guard.difficultyIds.length) {
|
||||
throw new Error(`Encounter ${definition.id} rule ${rule.id} has an empty difficulty guard`);
|
||||
}
|
||||
for (const difficultyId of guard.difficultyIds) {
|
||||
validateStableId(
|
||||
difficultyId,
|
||||
`Encounter ${definition.id} rule ${rule.id} difficulty`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const action of rule.actions) {
|
||||
validateStableId(action.id, `Encounter ${definition.id} action`);
|
||||
if (actionIds.has(action.id)) {
|
||||
@@ -72,8 +106,19 @@ export function validateDungeonEncounterDefinition(definition: DungeonEncounterD
|
||||
}
|
||||
}
|
||||
|
||||
function guardMatches(snapshot: DungeonEncounterSnapshot, guard: DungeonEncounterGuard): boolean {
|
||||
export interface DungeonEncounterRuntimeContext {
|
||||
readonly difficultyId?: string;
|
||||
}
|
||||
|
||||
function guardMatches(
|
||||
snapshot: DungeonEncounterSnapshot,
|
||||
guard: DungeonEncounterGuard,
|
||||
context: DungeonEncounterRuntimeContext,
|
||||
): boolean {
|
||||
if (guard.kind === "phase") return guard.phases.includes(snapshot.phase);
|
||||
if (guard.kind === "difficulty") {
|
||||
return context.difficultyId !== undefined && guard.difficultyIds.includes(context.difficultyId);
|
||||
}
|
||||
if (guard.kind === "flag") return (snapshot.flags[guard.flagId] ?? false) === guard.equals;
|
||||
if (guard.kind === "objective-complete") {
|
||||
return snapshot.completedObjectiveIds.includes(guard.objectiveId) === guard.equals;
|
||||
@@ -90,33 +135,55 @@ function guardMatches(snapshot: DungeonEncounterSnapshot, guard: DungeonEncounte
|
||||
return current <= guard.value;
|
||||
}
|
||||
|
||||
type DungeonEncounterTriggerMatch = "event" | "timeout" | false;
|
||||
|
||||
function triggerMatches(
|
||||
snapshot: DungeonEncounterSnapshot,
|
||||
trigger: DungeonEncounterRule["trigger"],
|
||||
event: DungeonEncounterEvent,
|
||||
): boolean {
|
||||
if (trigger.kind === "tick") return event.kind === "tick";
|
||||
if (trigger.kind === "signal") return event.kind === "signal" && event.signalId === trigger.signalId;
|
||||
): DungeonEncounterTriggerMatch {
|
||||
if (trigger.kind === "tick") return event.kind === "tick" ? "event" : false;
|
||||
if (trigger.kind === "signal") return event.kind === "signal" && event.signalId === trigger.signalId ? "event" : false;
|
||||
if (trigger.kind === "entity-engaged" || trigger.kind === "entity-death") {
|
||||
return event.kind === trigger.kind && event.entityId === trigger.entityId;
|
||||
return event.kind === trigger.kind && event.entityId === trigger.entityId ? "event" : false;
|
||||
}
|
||||
if (trigger.kind === "entity-health") {
|
||||
return event.kind === "entity-health"
|
||||
&& event.entityId === trigger.entityId
|
||||
&& event.healthPercent <= trigger.atOrBelowPercent;
|
||||
&& event.healthPercent <= trigger.atOrBelowPercent
|
||||
? "event"
|
||||
: false;
|
||||
}
|
||||
if (trigger.kind === "interact") return event.kind === "interact" && event.objectId === trigger.objectId;
|
||||
if (trigger.kind === "interact") return event.kind === "interact" && event.objectId === trigger.objectId ? "event" : false;
|
||||
if (trigger.kind === "elapsed") {
|
||||
return event.kind === "tick" && event.now - snapshot.phaseStartedAt >= trigger.afterMs;
|
||||
return event.kind === "tick" && event.now - snapshot.phaseStartedAt >= trigger.afterMs ? "event" : false;
|
||||
}
|
||||
if (event.kind !== "tick") return false;
|
||||
const registered = new Set(event.facts?.registeredEntityIds ?? []);
|
||||
const dead = new Set(event.facts?.deadEntityIds ?? []);
|
||||
const allDead = trigger.entityIds.length > 0
|
||||
&& trigger.entityIds.every((id) => registered.has(id) && dead.has(id));
|
||||
if (allDead) return "event";
|
||||
const hasAuthoritativeFacts = event.facts?.registeredEntityIds !== undefined
|
||||
&& event.facts.deadEntityIds !== undefined;
|
||||
const hasRegisteredLivingTarget = trigger.entityIds.some((id) => (
|
||||
registered.has(id) && !dead.has(id)
|
||||
));
|
||||
const timedOut = trigger.timeoutMs !== undefined
|
||||
&& hasAuthoritativeFacts
|
||||
&& !hasRegisteredLivingTarget
|
||||
&& event.now - snapshot.phaseStartedAt >= trigger.timeoutMs;
|
||||
return allDead || timedOut;
|
||||
return timedOut ? "timeout" : false;
|
||||
}
|
||||
|
||||
function ruleCanAdvanceOnMissingEntityTimeout(rule: DungeonEncounterRule): boolean {
|
||||
return !rule.actions.some((action) => (
|
||||
action.persistence === "checkpoint"
|
||||
|| action.persistence === "run"
|
||||
|| action.kind === "complete-objective"
|
||||
|| action.kind === "set-checkpoint"
|
||||
|| action.kind === "grant-loot"
|
||||
));
|
||||
}
|
||||
|
||||
function ruleAlreadyConsumed(snapshot: DungeonEncounterSnapshot, rule: DungeonEncounterRule): boolean {
|
||||
@@ -193,7 +260,7 @@ function applyAction(
|
||||
}
|
||||
return {
|
||||
snapshot: consume
|
||||
? { ...next, consumedActionIds: unique([...next.consumedActionIds, action.id]) }
|
||||
? { ...next, consumedActionIds: orderedUnique([...next.consumedActionIds, action.id]) }
|
||||
: next,
|
||||
effect: actionEffect(action),
|
||||
};
|
||||
@@ -203,6 +270,7 @@ export function reduceDungeonEncounter(
|
||||
definition: DungeonEncounterDefinition,
|
||||
snapshot: DungeonEncounterSnapshot,
|
||||
event: DungeonEncounterEvent,
|
||||
context: DungeonEncounterRuntimeContext = {},
|
||||
): DungeonEncounterReduction {
|
||||
if (snapshot.encounterId !== definition.id) {
|
||||
throw new Error(`Snapshot ${snapshot.encounterId} does not belong to encounter ${definition.id}`);
|
||||
@@ -215,8 +283,10 @@ export function reduceDungeonEncounter(
|
||||
.sort((left, right) => (right.rule.priority ?? 0) - (left.rule.priority ?? 0) || left.index - right.index);
|
||||
for (const { rule } of rules) {
|
||||
if (ruleAlreadyConsumed(next, rule)) continue;
|
||||
if (!triggerMatches(next, rule.trigger, event)) continue;
|
||||
if (!rule.guards?.every((guard) => guardMatches(next, guard)) && rule.guards?.length) continue;
|
||||
const triggerMatch = triggerMatches(next, rule.trigger, event);
|
||||
if (!triggerMatch) continue;
|
||||
if (triggerMatch === "timeout" && !ruleCanAdvanceOnMissingEntityTimeout(rule)) continue;
|
||||
if (!rule.guards?.every((guard) => guardMatches(next, guard, context)) && rule.guards?.length) continue;
|
||||
let fired = false;
|
||||
for (const action of rule.actions) {
|
||||
const applied = applyAction(next, action, event.now, rule.once ?? true);
|
||||
@@ -246,44 +316,54 @@ function retainedActionIds(
|
||||
return Object.freeze(snapshot.consumedActionIds.filter((id) => retained.has(id)));
|
||||
}
|
||||
|
||||
function retainedStateKeys(definition: DungeonEncounterDefinition): {
|
||||
readonly flags: ReadonlySet<string>;
|
||||
readonly counters: ReadonlySet<string>;
|
||||
readonly worldObjects: ReadonlySet<string>;
|
||||
readonly transitions: ReadonlySet<string>;
|
||||
readonly objectives: ReadonlySet<string>;
|
||||
readonly checkpoints: boolean;
|
||||
function reconstructedPersistentState(
|
||||
definition: DungeonEncounterDefinition,
|
||||
snapshot: DungeonEncounterSnapshot,
|
||||
initial: DungeonEncounterSnapshot,
|
||||
): {
|
||||
readonly flags: Readonly<Record<string, boolean>>;
|
||||
readonly counters: Readonly<Record<string, number>>;
|
||||
readonly worldObjectStates: Readonly<Record<string, string>>;
|
||||
readonly transitionStates: Readonly<Record<string, boolean>>;
|
||||
readonly completedObjectiveIds: readonly string[];
|
||||
readonly checkpointId: string | null;
|
||||
} {
|
||||
const actions = definition.rules.flatMap((rule) => rule.actions)
|
||||
const actionsById = new Map(definition.rules
|
||||
.flatMap((rule) => rule.actions)
|
||||
.map((action) => [action.id, action] as const));
|
||||
const actions = snapshot.consumedActionIds
|
||||
.map((id) => actionsById.get(id))
|
||||
.filter((action): action is DungeonEncounterAction => action !== undefined)
|
||||
.filter((action) => action.persistence === "checkpoint" || action.persistence === "run");
|
||||
return {
|
||||
flags: new Set(actions.filter((action) => action.kind === "set-flag").map((action) => action.flagId)),
|
||||
counters: new Set(actions
|
||||
.filter((action) => action.kind === "set-counter" || action.kind === "increment-counter")
|
||||
.map((action) => action.counterId)),
|
||||
worldObjects: new Set(actions
|
||||
.filter((action) => action.kind === "set-world-object-state")
|
||||
.map((action) => action.objectId)),
|
||||
transitions: new Set(actions
|
||||
.filter((action) => action.kind === "set-transition-active")
|
||||
.map((action) => action.transitionId)),
|
||||
objectives: new Set(actions
|
||||
.filter((action) => action.kind === "complete-objective")
|
||||
.map((action) => action.objectiveId)),
|
||||
checkpoints: actions.some((action) => action.kind === "set-checkpoint"),
|
||||
};
|
||||
}
|
||||
|
||||
function retainedRecord<T>(
|
||||
initial: Readonly<Record<string, T>>,
|
||||
current: Readonly<Record<string, T>>,
|
||||
keys: ReadonlySet<string>,
|
||||
): Readonly<Record<string, T>> {
|
||||
const retained: Record<string, T> = { ...initial };
|
||||
for (const key of keys) {
|
||||
if (current[key] !== undefined) retained[key] = current[key];
|
||||
const flags = { ...initial.flags };
|
||||
const counters = { ...initial.counters };
|
||||
const worldObjectStates = { ...initial.worldObjectStates };
|
||||
const transitionStates = { ...initial.transitionStates };
|
||||
const completedObjectiveIds = new Set<string>();
|
||||
let checkpointId: string | null = null;
|
||||
for (const action of actions) {
|
||||
if (action.kind === "set-flag") flags[action.flagId] = action.value;
|
||||
else if (action.kind === "set-counter") counters[action.counterId] = action.value;
|
||||
else if (action.kind === "increment-counter") {
|
||||
counters[action.counterId] = (counters[action.counterId] ?? 0) + (action.amount ?? 1);
|
||||
} else if (action.kind === "set-world-object-state") {
|
||||
worldObjectStates[action.objectId] = action.state;
|
||||
} else if (action.kind === "set-transition-active") {
|
||||
transitionStates[action.transitionId] = action.active;
|
||||
} else if (action.kind === "complete-objective") {
|
||||
completedObjectiveIds.add(action.objectiveId);
|
||||
} else if (action.kind === "set-checkpoint") {
|
||||
checkpointId = action.checkpointId;
|
||||
}
|
||||
}
|
||||
return Object.freeze(retained);
|
||||
return {
|
||||
flags: Object.freeze(flags),
|
||||
counters: Object.freeze(counters),
|
||||
worldObjectStates: Object.freeze(worldObjectStates),
|
||||
transitionStates: Object.freeze(transitionStates),
|
||||
completedObjectiveIds: Object.freeze([...completedObjectiveIds].sort()),
|
||||
checkpointId,
|
||||
};
|
||||
}
|
||||
|
||||
export function resetDungeonEncounterSnapshot(
|
||||
@@ -297,27 +377,18 @@ export function resetDungeonEncounterSnapshot(
|
||||
const phase = scope === "boss-wipe"
|
||||
? definition.resetPolicy?.bossWipePhase ?? definition.initialPhase
|
||||
: definition.resetPolicy?.checkpointPhase ?? definition.initialPhase;
|
||||
const retained = retainedStateKeys(definition);
|
||||
const retained = reconstructedPersistentState(definition, snapshot, initial);
|
||||
return Object.freeze({
|
||||
...initial,
|
||||
phase,
|
||||
phaseStartedAt: finiteTime(now),
|
||||
flags: retainedRecord(initial.flags, snapshot.flags, retained.flags),
|
||||
counters: retainedRecord(initial.counters, snapshot.counters, retained.counters),
|
||||
worldObjectStates: retainedRecord(
|
||||
initial.worldObjectStates,
|
||||
snapshot.worldObjectStates,
|
||||
retained.worldObjects,
|
||||
),
|
||||
transitionStates: retainedRecord(
|
||||
initial.transitionStates,
|
||||
snapshot.transitionStates,
|
||||
retained.transitions,
|
||||
),
|
||||
completedObjectiveIds: Object.freeze(snapshot.completedObjectiveIds
|
||||
.filter((id) => retained.objectives.has(id))),
|
||||
flags: retained.flags,
|
||||
counters: retained.counters,
|
||||
worldObjectStates: retained.worldObjectStates,
|
||||
transitionStates: retained.transitionStates,
|
||||
completedObjectiveIds: retained.completedObjectiveIds,
|
||||
consumedActionIds: retainedActionIds(definition, snapshot, scope),
|
||||
checkpointId: retained.checkpoints ? snapshot.checkpointId : null,
|
||||
checkpointId: retained.checkpointId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -330,6 +401,8 @@ export interface DungeonEncounterProvider {
|
||||
readonly reset: (scope: DungeonEncounterResetScope, now?: number) => void;
|
||||
/** Hydrates authoritative online/save state without involving renderer state. */
|
||||
readonly replaceSnapshot?: (snapshot: DungeonEncounterSnapshot) => void;
|
||||
/** Updates ephemeral run context without polluting serialized snapshots. */
|
||||
readonly setContext?: (context: DungeonEncounterRuntimeContext) => void;
|
||||
}
|
||||
|
||||
const providers = new Map<string, DungeonEncounterProvider>();
|
||||
@@ -337,6 +410,14 @@ const pendingEffects: DungeonEncounterEffectEnvelope[] = [];
|
||||
const effectHandlers = new Set<(value: DungeonEncounterEffectEnvelope) => void>();
|
||||
let activeDungeonId: string | null = null;
|
||||
|
||||
function clearPendingDungeonEncounterEffects(dungeonId?: string): void {
|
||||
for (let index = pendingEffects.length - 1; index >= 0; index -= 1) {
|
||||
if (!dungeonId || pendingEffects[index].dungeonId === dungeonId) {
|
||||
pendingEffects.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function queueProviderEffects(
|
||||
provider: DungeonEncounterProvider,
|
||||
effects: readonly DungeonEncounterEffect[],
|
||||
@@ -347,8 +428,11 @@ function queueProviderEffects(
|
||||
encounterId: provider.encounterId,
|
||||
effect,
|
||||
});
|
||||
pendingEffects.push(value);
|
||||
for (const handler of effectHandlers) handler(value);
|
||||
if (effectHandlers.size) {
|
||||
for (const handler of effectHandlers) handler(value);
|
||||
} else {
|
||||
pendingEffects.push(value);
|
||||
}
|
||||
}
|
||||
return effects;
|
||||
}
|
||||
@@ -416,6 +500,22 @@ export function dispatchActiveDungeonInteraction(
|
||||
return Object.freeze(effects);
|
||||
}
|
||||
|
||||
export function dispatchActiveDungeonEntityEvent(
|
||||
event: Extract<
|
||||
DungeonEncounterEvent,
|
||||
{ readonly kind: "entity-engaged" | "entity-health" | "entity-death" }
|
||||
>,
|
||||
): readonly DungeonEncounterEffect[] {
|
||||
if (!activeDungeonId) return [];
|
||||
const effects: DungeonEncounterEffect[] = [];
|
||||
for (const provider of providers.values()) {
|
||||
if (provider.dungeonId !== activeDungeonId) continue;
|
||||
effects.push(...queueProviderEffects(provider, provider.dispatch(event)));
|
||||
}
|
||||
notifyEncounterSubscribers();
|
||||
return Object.freeze(effects);
|
||||
}
|
||||
|
||||
export function advanceDungeonEncounters(
|
||||
dungeonId: string,
|
||||
event: Extract<DungeonEncounterEvent, { readonly kind: "tick" }>,
|
||||
@@ -435,6 +535,7 @@ export function resetDungeonEncounters(
|
||||
dungeonId?: string,
|
||||
now = 0,
|
||||
): void {
|
||||
clearPendingDungeonEncounterEffects(dungeonId);
|
||||
for (const provider of providers.values()) {
|
||||
if (!dungeonId || provider.dungeonId === dungeonId) provider.reset(scope, now);
|
||||
}
|
||||
@@ -449,7 +550,11 @@ export function resetDungeonEncounters(
|
||||
export function activateDungeonEncounterDefinitions(
|
||||
dungeonId: string,
|
||||
definitions: readonly DungeonEncounterDefinition[],
|
||||
difficultyId?: string,
|
||||
): void {
|
||||
if (activeDungeonId && activeDungeonId !== dungeonId) {
|
||||
clearPendingDungeonEncounterEffects(activeDungeonId);
|
||||
}
|
||||
activeDungeonId = dungeonId;
|
||||
for (const definition of definitions) {
|
||||
validateDungeonEncounterDefinition(definition);
|
||||
@@ -459,16 +564,18 @@ export function activateDungeonEncounterDefinitions(
|
||||
const existing = providers.get(definition.id);
|
||||
if (existing) {
|
||||
if (existing.dungeonId !== dungeonId) throw new Error(`Encounter ${definition.id} has conflicting dungeon owners`);
|
||||
existing.setContext?.({ difficultyId });
|
||||
continue;
|
||||
}
|
||||
let snapshot = createDungeonEncounterSnapshot(definition);
|
||||
let context: DungeonEncounterRuntimeContext = { difficultyId };
|
||||
const provider: DungeonEncounterProvider = {
|
||||
definition,
|
||||
encounterId: definition.id,
|
||||
dungeonId,
|
||||
getSnapshot: () => snapshot,
|
||||
dispatch: (event) => {
|
||||
const result = reduceDungeonEncounter(definition, snapshot, event);
|
||||
const result = reduceDungeonEncounter(definition, snapshot, event, context);
|
||||
snapshot = result.snapshot;
|
||||
return result.effects;
|
||||
},
|
||||
@@ -481,6 +588,9 @@ export function activateDungeonEncounterDefinitions(
|
||||
}
|
||||
snapshot = cloneEncounterSnapshot(value);
|
||||
},
|
||||
setContext: (value) => {
|
||||
context = Object.freeze({ ...value });
|
||||
},
|
||||
};
|
||||
providers.set(definition.id, provider);
|
||||
}
|
||||
@@ -501,7 +611,7 @@ function cloneEncounterSnapshot(value: DungeonEncounterSnapshot): DungeonEncount
|
||||
transitionStates: Object.freeze({ ...value.transitionStates }),
|
||||
activeSpawnGroupIds: unique(value.activeSpawnGroupIds),
|
||||
completedObjectiveIds: unique(value.completedObjectiveIds),
|
||||
consumedActionIds: unique(value.consumedActionIds),
|
||||
consumedActionIds: orderedUnique(value.consumedActionIds),
|
||||
revision: Math.max(0, Math.trunc(value.revision)),
|
||||
});
|
||||
}
|
||||
@@ -542,13 +652,14 @@ export function exportDungeonEncounterCheckpointSnapshots(
|
||||
export function importDungeonEncounterSnapshots(
|
||||
dungeonId: string,
|
||||
snapshots: readonly DungeonEncounterSnapshot[],
|
||||
options: { readonly authoritative?: boolean } = {},
|
||||
): readonly string[] {
|
||||
const imported: string[] = [];
|
||||
for (const raw of snapshots) {
|
||||
const provider = providers.get(raw.encounterId);
|
||||
if (!provider || provider.dungeonId !== dungeonId || !provider.replaceSnapshot) continue;
|
||||
const snapshot = cloneEncounterSnapshot(raw);
|
||||
if (snapshot.revision < provider.getSnapshot().revision) continue;
|
||||
if (!options.authoritative && snapshot.revision < provider.getSnapshot().revision) continue;
|
||||
provider.replaceSnapshot(snapshot);
|
||||
imported.push(snapshot.encounterId);
|
||||
}
|
||||
@@ -567,7 +678,7 @@ export function drainDungeonEncounterEffects(
|
||||
return Object.freeze(drained);
|
||||
}
|
||||
|
||||
/** Immediate application hook; effects also remain drainable for frame-bound consumers. */
|
||||
/** Immediate application hook. While one is mounted, effects bypass the headless drain queue. */
|
||||
export function registerDungeonEncounterEffectHandler(
|
||||
handler: (value: DungeonEncounterEffectEnvelope) => void,
|
||||
): () => void {
|
||||
@@ -598,3 +709,28 @@ export function activeDungeonEncounterSpawnGroupIds(): readonly string[] {
|
||||
return Object.freeze([...new Set(exportDungeonEncounterSnapshots()
|
||||
.flatMap((snapshot) => snapshot.activeSpawnGroupIds))].sort());
|
||||
}
|
||||
|
||||
export function activeDungeonCompletedObjectiveIds(dungeonId = activeDungeonId): readonly string[] {
|
||||
return unique(exportDungeonEncounterSnapshots(dungeonId)
|
||||
.flatMap((snapshot) => snapshot.completedObjectiveIds));
|
||||
}
|
||||
|
||||
function definitionControlsDungeonCompletion(definition: DungeonEncounterDefinition): boolean {
|
||||
return definition.rules.some((rule) => rule.actions.some((action) => (
|
||||
action.kind === "set-flag"
|
||||
&& action.flagId === "dungeon-complete"
|
||||
&& action.value
|
||||
)));
|
||||
}
|
||||
|
||||
export function activeDungeonRunIsComplete(dungeonId = activeDungeonId): boolean {
|
||||
if (!dungeonId) return false;
|
||||
const completionProviders = [...providers.values()].filter((provider) => (
|
||||
provider.dungeonId === dungeonId
|
||||
&& definitionControlsDungeonCompletion(provider.definition)
|
||||
));
|
||||
return completionProviders.length > 0
|
||||
&& completionProviders.every(
|
||||
(provider) => provider.getSnapshot().flags["dungeon-complete"] === true,
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user