Exact source used by this build. SHA-256: 8448e7976fea4764cea9ccef0e032341f3b64fb62709a7d895b58f31e39c0ce7.

1 import { z } from "zod";
2 import type { Player, PointOfInterest, SimEvent, WorldState } from "../../types";
3 import { withPlayer } from "../../state";
4 import { chargePassOut } from "../wallet";
5 import { defineIntent, defineSystem } from "../registry";
6 export const MAX_ENERGY = 270;
7 export interface SleepReadiness { playerId: string; name: string; sleeping: boolean }
8 export function getSleepReadiness(state: WorldState): SleepReadiness[] {
9   return (state.systems.energy as { sleepReadiness?: SleepReadiness[] } | undefined)?.sleepReadiness ?? [];
10 }
11 /** Shared presence is deliberately tiny, so players in other areas can see sleep requests. */
12 export function refreshSleepReadiness(state: WorldState): WorldState {
13   const sleepReadiness = Object.values(state.players).filter((player) => player.connected)
14     .map(({ id, name, sleeping }) => ({ playerId: id, name, sleeping })).sort((a, b) => a.playerId.localeCompare(b.playerId));
15   if (JSON.stringify(sleepReadiness) === JSON.stringify(getSleepReadiness(state))) return state;
16   return { ...state, revision: state.revision + 1, systems: { ...state.systems,
17     energy: { ...state.systems.energy as object | undefined, sleepReadiness } } };
18 }
19 /** Current players share the starter home; NPC/inn beds are not player-owned beds. */
20 export function sleepBed(state: WorldState, player: Player): PointOfInterest | undefined {
21   if (player.areaId !== state.starterPlayer.areaId) return undefined;
22   const area = state.areas[player.areaId];
23   return area?.pois?.find((poi) => poi.kind === "bed" && area.landmarks.some((home) => home.kind === "home"
24     && poi.x >= home.x && poi.y >= home.y && poi.x + poi.width <= home.x + home.width && poi.y + poi.height <= home.y + home.height
25     && player.x >= home.x && player.y >= home.y && player.x < home.x + home.width && player.y < home.y + home.height)
26     && Math.hypot(player.x - Math.max(poi.x, Math.min(player.x, poi.x + poi.width)), player.y - Math.max(poi.y, Math.min(player.y, poi.y + poi.height))) <= 1);
27 }
28 export function spendToolEnergy(player: Player): Player | null { return player.energy < 2 ? null : { ...player, energy: player.energy - 2 }; }
29 export function passOut(state: WorldState, playerIds: readonly string[]): WorldState {
30   const players = { ...state.players };
31   for (const id of playerIds) {
32     const player = players[id]; if (!player) continue;
33     players[id] = { ...player, areaId: state.starterPlayer.areaId, x: state.starterPlayer.x, y: state.starterPlayer.y, energy: Math.floor(player.maxEnergy / 2), sleeping: false, sleepConfirmedAt: null, passedOut: true };
34   }
35   return { ...chargePassOut(state, playerIds), revision: state.revision + 1, players };
36 }
37 export const energySystem = defineSystem({
38   id: "energy", worldDefaults: () => ({ sleepReadiness: [] }), playerDefaults: () => ({}), events: ["sleep", "passout"],
39   initializePlayer: () => ({ energy: MAX_ENERGY, maxEnergy: MAX_ENERGY, sleeping: false, sleepConfirmedAt: null, passedOut: false }),
40   intents: [defineIntent("sleep", z.object({ type: z.literal("sleep") }), ({ state, player, playerId, overnight }) => {
41     if (player.sleeping) return { state, events: [], error: "Player is already sleeping" };
42     if (!sleepBed(state, player)) return { state, events: [], error: "Return to your bed to sleep" };
43     const sleeping = refreshSleepReadiness(withPlayer(state, { ...player, sleeping: true, sleepConfirmedAt: state.clock.minuteOfDay }));
44     const events: SimEvent[] = [{ type: "sleep", playerId, message: `${player.name} wants to sleep` }];
45     const active = Object.values(sleeping.players).filter((candidate) => candidate.connected);
46     return active.length > 0 && active.every((candidate) => candidate.sleeping) ? overnight(sleeping, events) : { state: sleeping, events };
47   }, { allowSleeping: true })],
48   onPlayerAdded: ({ state }) => ({ state: refreshSleepReadiness(state), events: [] }),
49   onPresenceChange: ({ state, playerId, connected, overnight }) => {
50     const player = playerId ? state.players[playerId] : undefined;
51     if (connected === false && player) state = withPlayer(state, { ...player, sleeping: false, sleepConfirmedAt: null });
52     state = refreshSleepReadiness(state);
53     const active = Object.values(state.players).filter((player) => player.connected);
54     return connected === false && active.length > 0 && active.every((player) => player.sleeping) ? overnight(state) : { state, events: [] };
55   },
56   onClockTick: ({ state, overnight }) => {
57     if (state.clock.minuteOfDay < 1560) return { state, events: [] };
58     const awake = Object.values(state.players).filter((player) => player.connected && !player.sleeping).map((player) => player.id);
59     return overnight(awake.length ? passOut(state, awake) : state, awake.map((playerId) => ({ type: "passout", playerId })), true);
60   },
61   onOvernight: ({ state }) => {
62     const players = Object.fromEntries(Object.entries(state.players).map(([id, player]) => {
63       if (!player.connected) return [id, player];
64       const confirmed = player.sleepConfirmedAt;
65       const restored = player.passedOut ? player.energy : (confirmed === null || confirmed <= 1440 ? player.maxEnergy : Math.round(player.maxEnergy * (1 - 0.5 * ((confirmed - 1440) / 120))));
66       return [id, { ...player, energy: Math.max(0, Math.min(player.maxEnergy, restored)), sleeping: false, sleepConfirmedAt: null, passedOut: false }];
67     }));
68     return { state: refreshSleepReadiness({ ...state, revision: state.revision + 1, players }), events: [] };
69   },
70 });
71