File size: 7,764 Bytes
b8a03fa | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | import { clamp } from './deterministic.js';
import { emitWorldEvent, findRequired } from './state-utils.js';
import type { CommandResult, EntityId, MissionObjectiveState, MissionState, UniverseState, WorldEventState } from './types.js';
export class MissionSystem {
private lastProcessedEventIndex: number;
constructor(private state: UniverseState) {
this.lastProcessedEventIndex = state.events.length;
}
replaceState(state: UniverseState): void {
this.state = state;
this.lastProcessedEventIndex = state.events.length;
}
accept(missionId: EntityId): CommandResult {
const mission = findRequired(this.state.missions, missionId, 'Mission');
if (mission.status !== 'available') return this.failure(`${mission.title} is not available.`);
mission.status = 'active';
mission.acceptedTick = this.state.tick;
if (!this.state.player.activeMissionIds.includes(mission.id)) this.state.player.activeMissionIds.push(mission.id);
const event = emitWorldEvent(this.state, 'mission-accepted', this.state.player.id, mission.id, `Mission accepted: ${mission.title}.`);
return { ok: true, message: event.summary, eventIds: [event.id] };
}
addProgress(missionId: EntityId, objectiveId: EntityId, amount = 1): CommandResult {
const mission = findRequired(this.state.missions, missionId, 'Mission');
if (mission.status !== 'active') return this.failure(`${mission.title} is not active.`);
const objective = findRequired(mission.objectives, objectiveId, 'Mission objective');
this.progressObjective(objective, amount);
const events: string[] = [];
if (mission.objectives.every((item) => item.completed)) events.push(this.complete(mission).id);
return { ok: true, message: `${objective.description}: ${objective.currentAmount}/${objective.requiredAmount}`, eventIds: events };
}
tick(): void {
for (const mission of this.state.missions) {
if (mission.status === 'locked' && this.triggered(mission)) {
mission.status = 'available';
emitWorldEvent(this.state, 'mission-available', mission.issuerId, mission.id, `New mission available: ${mission.title}.`, { persistent: true });
}
if (mission.status === 'active' && mission.expiresTick !== null && this.state.tick >= mission.expiresTick) {
mission.status = 'failed';
this.state.player.activeMissionIds = this.state.player.activeMissionIds.filter((id) => id !== mission.id);
emitWorldEvent(this.state, 'mission-failed', this.state.player.id, mission.id, `Mission expired: ${mission.title}.`, { severity: 0.5 });
}
}
const newEvents = this.state.events.slice(this.lastProcessedEventIndex);
this.lastProcessedEventIndex = this.state.events.length;
for (const event of newEvents) this.applyEventProgress(event);
}
private triggered(mission: MissionState): boolean {
const trigger = mission.trigger;
if (trigger.kind === 'always') return true;
if (trigger.kind === 'traffic-distress') return this.state.traffic.some((traffic) => traffic.id === trigger.targetId && traffic.status === 'distress');
if (trigger.kind === 'shortage' && trigger.targetId) {
return this.state.economy.markets.some((market) => {
const listing = market.listings.find((candidate) => candidate.commodityId === trigger.targetId);
return Boolean(listing && (listing.shortage || listing.inventory <= trigger.threshold));
});
}
if (trigger.kind === 'damage' && trigger.targetId) {
const hull = this.state.hullSections.find((candidate) => candidate.id === trigger.targetId);
const room = this.state.rooms.find((candidate) => candidate.id === trigger.targetId);
return (hull?.hazardLevel ?? room?.damage ?? 0) >= trigger.threshold;
}
if (trigger.kind === 'discovery' && trigger.targetId) {
const discovery = this.state.discoveries.find((candidate) => candidate.id === trigger.targetId);
return Boolean(discovery && !discovery.discovered);
}
if (trigger.kind === 'relationship' && trigger.targetId) return (this.state.player.factionReputation[trigger.targetId] ?? 0) <= trigger.threshold;
if (trigger.kind === 'construction' && trigger.targetId) return this.state.construction.projects.some((project) => project.id === trigger.targetId && project.status !== 'completed');
if (trigger.kind === 'conflict' && trigger.targetId) {
const targetId = trigger.targetId;
return this.state.factions.some((faction) => faction.activeConflictIds.includes(targetId));
}
return false;
}
private applyEventProgress(event: WorldEventState): void {
for (const mission of this.state.missions.filter((candidate) => candidate.status === 'active')) {
for (const objective of mission.objectives.filter((candidate) => !candidate.completed)) {
if (!this.eventMatchesObjective(event, objective)) continue;
const eventAmount = typeof event.data.quantity === 'number' ? event.data.quantity : 1;
this.progressObjective(objective, eventAmount);
}
if (mission.objectives.every((objective) => objective.completed)) this.complete(mission);
}
}
private eventMatchesObjective(event: WorldEventState, objective: MissionObjectiveState): boolean {
if (objective.kind === 'interact') return event.type === 'interaction-used' && event.targetId === objective.targetId;
if (objective.kind === 'repair') return event.type.includes('repaired') && (event.targetId === objective.targetId || event.data.repairNodeId === objective.targetId);
if (objective.kind === 'rescue') return event.type === 'traffic-rescued' && event.targetId === objective.targetId;
if (objective.kind === 'scan') return (event.type === 'location-discovered' || event.type === 'agent-task-completed') && event.targetId === objective.targetId;
if (objective.kind === 'visit') return (event.type === 'location-discovered' || event.type === 'ship-jump-completed') && event.targetId === objective.targetId;
if (objective.kind === 'deliver') return event.type === 'cargo-delivered' && event.targetId === objective.targetId;
if (objective.kind === 'trade') return event.type === 'market-trade' && event.targetId === objective.targetId && event.data.side === 'sell';
if (objective.kind === 'construct') return event.type === 'construction-completed' && event.targetId === objective.targetId;
if (objective.kind === 'defend') return event.type === 'patrol-completed' && event.targetId === objective.targetId;
if (objective.kind === 'negotiate') return event.type === 'faction-agreement' && event.targetId === objective.targetId;
return false;
}
private progressObjective(objective: MissionObjectiveState, amount: number): void {
objective.currentAmount = clamp(objective.currentAmount + Math.max(0, amount), 0, objective.requiredAmount);
objective.completed = objective.currentAmount >= objective.requiredAmount;
}
private complete(mission: MissionState): WorldEventState {
mission.status = 'completed';
mission.completedTick = this.state.tick;
this.state.player.activeMissionIds = this.state.player.activeMissionIds.filter((id) => id !== mission.id);
this.state.player.credits += mission.rewardCredits;
this.state.player.factionReputation[mission.factionId] = clamp((this.state.player.factionReputation[mission.factionId] ?? 0) + mission.reputationReward, -100, 100);
return emitWorldEvent(this.state, 'mission-completed', this.state.player.id, mission.id, `Mission completed: ${mission.title}.`, { data: { rewardCredits: mission.rewardCredits, reputationReward: mission.reputationReward } });
}
private failure(message: string): CommandResult {
return { ok: false, message, eventIds: [] };
}
}
|