snapkitty
hardware
erlang
File size: 6,909 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
import { clamp } from './deterministic.js';
import { emitWorldEvent, findRequired } from './state-utils.js';
import type { CommandResult, ConstructionProjectState, EntityId, UniverseState } from './types.js';

export class ConstructionSystem {
  constructor(private state: UniverseState) {}

  replaceState(state: UniverseState): void {
    this.state = state;
  }

  start(projectId: EntityId): CommandResult {
    const project = findRequired(this.state.construction.projects, projectId, 'Construction project');
    if (project.ownerId !== this.state.player.id) return this.failure('Player does not own this construction project.');
    if (!this.isOwnedTarget(project)) return this.failure('Construction target is not within an owned space.');
    if (project.status !== 'planned' && project.status !== 'paused') return this.failure(`${project.name} cannot be started from '${project.status}'.`);
    project.status = 'building';
    const event = emitWorldEvent(this.state, 'construction-started', this.state.player.id, project.id, `Construction started: ${project.name}.`, { data: { locationId: project.locationId } });
    return { ok: true, message: event.summary, eventIds: [event.id] };
  }

  deliver(projectId: EntityId, commodityId: EntityId, quantity: number): CommandResult {
    const project = findRequired(this.state.construction.projects, projectId, 'Construction project');
    if (project.status === 'completed' || project.status === 'cancelled') return this.failure(`${project.name} cannot accept resources.`);
    const required = project.requiredResources[commodityId];
    if (required === undefined) return this.failure(`${commodityId} is not required by ${project.name}.`);
    if (!Number.isInteger(quantity) || quantity <= 0) return this.failure('Delivery quantity must be a positive integer.');
    const stack = this.state.player.inventory.find((item) => item.itemId === commodityId);
    if (!stack || stack.quantity < quantity) return this.failure(`Player lacks ${quantity} units of ${commodityId}.`);
    const delivered = project.deliveredResources[commodityId] ?? 0;
    const accepted = Math.min(quantity, required - delivered);
    if (accepted <= 0) return this.failure(`${project.name} already has all required ${commodityId}.`);
    stack.quantity -= accepted;
    project.deliveredResources[commodityId] = delivered + accepted;
    this.state.player.inventory = this.state.player.inventory.filter((item) => item.quantity > 0);
    this.recalculate(project);
    const event = emitWorldEvent(this.state, 'construction-resource-delivered', this.state.player.id, project.id, `Delivered ${accepted} ${commodityId} to ${project.name}.`, { data: { commodityId, quantity: accepted } });
    return { ok: true, message: event.summary, eventIds: [event.id] };
  }

  work(projectId: EntityId, labor: number): CommandResult {
    const project = findRequired(this.state.construction.projects, projectId, 'Construction project');
    if (project.status !== 'building') return this.failure(`${project.name} is not actively building.`);
    if (this.state.player.locationId !== project.locationId) return this.failure('Player must be at the construction location.');
    if (!this.hasAllResources(project)) return this.failure(`${project.name} is waiting for resources.`);
    if (!Number.isFinite(labor) || labor <= 0) return this.failure('Labor must be positive.');
    project.laborCompleted = clamp(project.laborCompleted + labor, 0, project.laborRequired);
    this.recalculate(project);
    const eventIds: string[] = [];
    if (project.progress >= 1) eventIds.push(this.complete(project).id);
    return { ok: true, message: `${project.name} is ${Math.round(project.progress * 100)}% complete.`, eventIds };
  }

  tick(deltaMs: number): void {
    const labor = deltaMs / 3_600_000;
    for (const project of this.state.construction.projects.filter((candidate) => candidate.status === 'building')) {
      const assignedWorkers = this.state.npcs.filter((npc) => npc.alive && npc.currentLocationId === project.locationId && (npc.occupation.includes('engineer') || npc.occupation.includes('technician'))).length;
      if (assignedWorkers <= 0 || !this.hasAllResources(project)) continue;
      project.laborCompleted = clamp(project.laborCompleted + labor * assignedWorkers, 0, project.laborRequired);
      this.recalculate(project);
      if (project.progress >= 1) this.complete(project);
    }
  }

  private complete(project: ConstructionProjectState) {
    project.status = 'completed';
    project.progress = 1;
    if (!this.state.construction.completedBlueprintIds.includes(project.blueprintId)) this.state.construction.completedBlueprintIds.push(project.blueprintId);
    const owned = this.state.construction.ownedSpaces.find((space) => space.ownerId === project.ownerId && space.targetId === project.locationId);
    if (owned && !owned.modificationIds.includes(project.id)) owned.modificationIds.push(project.id);
    const room = this.state.rooms.find((candidate) => candidate.id === project.locationId);
    if (room && project.resultingInteractionKind && !room.interactionPoints.some((interaction) => interaction.id === `interaction-${project.id}`)) {
      room.interactionPoints.push({
        id: `interaction-${project.id}`,
        name: project.name,
        kind: project.resultingInteractionKind,
        purpose: `Player-constructed ${project.targetKind}.`,
        enabled: true,
        permissions: [],
        state: { constructedTick: this.state.tick, uses: 0 },
      });
    }
    return emitWorldEvent(this.state, 'construction-completed', project.ownerId, project.id, `Construction completed: ${project.name}.`, { data: { blueprintId: project.blueprintId, locationId: project.locationId } });
  }

  private recalculate(project: ConstructionProjectState): void {
    const resourceEntries = Object.entries(project.requiredResources);
    const resourceProgress = resourceEntries.length === 0 ? 1 : resourceEntries.reduce((sum, [commodityId, required]) => sum + clamp((project.deliveredResources[commodityId] ?? 0) / Math.max(1, required), 0, 1), 0) / resourceEntries.length;
    const laborProgress = clamp(project.laborCompleted / Math.max(1, project.laborRequired), 0, 1);
    project.progress = resourceProgress * 0.4 + laborProgress * 0.6;
  }

  private hasAllResources(project: ConstructionProjectState): boolean {
    return Object.entries(project.requiredResources).every(([commodityId, required]) => (project.deliveredResources[commodityId] ?? 0) >= required);
  }

  private isOwnedTarget(project: ConstructionProjectState): boolean {
    return this.state.construction.ownedSpaces.some((space) => space.ownerId === project.ownerId && (space.targetId === project.locationId || space.targetId === project.targetId));
  }

  private failure(message: string): CommandResult {
    return { ok: false, message, eventIds: [] };
  }
}