snapkitty
hardware
erlang
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: [] };
  }
}