Download universe/ConstructionSystem.ts from Snapkitty/grisp-shadow-fleet: direct link, hf CLI and curl.
- Browser
- Download file 6.91 kB
-
https://huggingface.co/Snapkitty/grisp-shadow-fleet/resolve/main/universe/ConstructionSystem.ts
- Command line
-
hf download hf://Snapkitty/grisp-shadow-fleet/universe/ConstructionSystem.ts
-
curl -L -o ConstructionSystem.ts https://huggingface.co/Snapkitty/grisp-shadow-fleet/resolve/main/universe/ConstructionSystem.ts
6.91 kB
| 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: [] }; | |
| } | |
| } | |