Blueprints declare constructionDurationMinutes instead of tick counts. Delivering the last material starts construction at delivery time via maybeStartConstruction, setting constructionStartedAt/CompletesAt on the site; the game tick only sweeps completed sites. Structure detail page extracts server data helpers and shows a ConstructionPanel with a literal ETA countdown. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
81 lines
2.7 KiB
TypeScript
81 lines
2.7 KiB
TypeScript
import type { SanitizedConfig } from "payload";
|
|
import { getPayload } from "payload";
|
|
import type { GameStructure, GameVehicle, Shipment, Vehicle } from "@/payload-types";
|
|
import { processShipmentTick } from "./processShipmentTick";
|
|
import { processConstructionTick } from "@/lib/map/construction";
|
|
import { createBinLogger, errorMessage } from "./lib/binFileLogger";
|
|
import { emitGameEvent } from "@/utils/event-log/emit";
|
|
import { EventTypes } from "@/utils/event-log/eventTypes";
|
|
|
|
export const script = async (config: SanitizedConfig) => {
|
|
const payload = await getPayload({ config });
|
|
const log = createBinLogger("game-tick", payload.logger);
|
|
|
|
try {
|
|
log.info("[Game Tick] Starting game tick...");
|
|
|
|
const activeShipments = await payload.find({
|
|
collection: "shipments",
|
|
where: {
|
|
status: { in: ["dispatched", "in_transit"] },
|
|
},
|
|
limit: 100,
|
|
depth: 2,
|
|
});
|
|
|
|
let processedCount = 0;
|
|
for (const shipment of activeShipments.docs) {
|
|
try {
|
|
await processShipmentTick(
|
|
payload,
|
|
shipment as unknown as Shipment & {
|
|
transportVehicle: GameVehicle & { type: Vehicle };
|
|
origin: GameStructure;
|
|
destination: GameStructure;
|
|
},
|
|
log,
|
|
);
|
|
processedCount++;
|
|
} catch (error) {
|
|
log.error(`[Game Tick] Error processing shipment ${shipment.id}: ${errorMessage(error)}`);
|
|
}
|
|
}
|
|
|
|
const construction = await processConstructionTick(payload, log);
|
|
|
|
await emitGameEvent(payload, {
|
|
type: EventTypes.GameTick,
|
|
message: `Game tick processed ${processedCount} of ${activeShipments.docs.length} active shipments`,
|
|
data: {
|
|
processedCount,
|
|
totalActive: activeShipments.docs.length,
|
|
construction,
|
|
},
|
|
});
|
|
|
|
const notifyUrl = (process.env.APP_URL ?? "http://localhost:3000").replace(/\/$/, "");
|
|
const notifySecret = process.env.GAME_TICK_NOTIFY_SECRET;
|
|
if (notifySecret) {
|
|
await fetch(`${notifyUrl}/api/game-tick/notify`, {
|
|
method: "POST",
|
|
headers: {
|
|
"content-type": "application/json",
|
|
"x-game-tick-secret": notifySecret,
|
|
},
|
|
body: JSON.stringify({ processedAt: new Date().toISOString() }),
|
|
}).catch((error: unknown) => {
|
|
log.error(`[Game Tick] Failed to notify app clients: ${errorMessage(error)}`);
|
|
});
|
|
} else {
|
|
log.warn("[Game Tick] GAME_TICK_NOTIFY_SECRET not set; skipping client notification");
|
|
}
|
|
|
|
log.info(`[Game Tick] Complete. Processed ${processedCount} shipments.`);
|
|
process.exit(0);
|
|
} catch (error) {
|
|
log.error(
|
|
`[Game Tick] Fatal: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
};
|