2026-02-21 03:20:10 -05:00
|
|
|
export const GRID_SIZE = 41;
|
2026-02-22 03:30:26 -05:00
|
|
|
export const HEARING_RADIUS = 20;
|
2026-02-21 00:55:19 -05:00
|
|
|
export const MOVE_COOLDOWN_MS = 200;
|
2026-02-20 08:16:43 -05:00
|
|
|
|
2026-02-24 18:48:08 -05:00
|
|
|
export type ItemType = string;
|
2026-02-20 08:16:43 -05:00
|
|
|
|
|
|
|
|
export type WorldItem = {
|
|
|
|
|
id: string;
|
|
|
|
|
type: ItemType;
|
|
|
|
|
title: string;
|
|
|
|
|
x: number;
|
|
|
|
|
y: number;
|
|
|
|
|
createdBy: string;
|
|
|
|
|
createdAt: number;
|
|
|
|
|
updatedAt: number;
|
|
|
|
|
version: number;
|
|
|
|
|
capabilities: string[];
|
2026-02-21 16:13:48 -05:00
|
|
|
useSound?: string;
|
2026-02-21 16:01:40 -05:00
|
|
|
emitSound?: string;
|
2026-02-20 08:16:43 -05:00
|
|
|
params: Record<string, unknown>;
|
|
|
|
|
carrierId?: string | null;
|
2026-02-27 01:32:25 -05:00
|
|
|
display?: Record<string, string>;
|
2026-02-20 08:16:43 -05:00
|
|
|
};
|
|
|
|
|
|
2026-02-25 01:11:47 -05:00
|
|
|
export type SelectionContext = 'pickup' | 'drop' | 'delete' | 'edit' | 'use' | 'secondaryUse' | 'inspect' | null;
|
2026-02-20 08:16:43 -05:00
|
|
|
|
|
|
|
|
export type GameMode =
|
|
|
|
|
| 'normal'
|
2026-02-21 16:55:41 -05:00
|
|
|
| 'helpView'
|
2026-02-20 08:16:43 -05:00
|
|
|
| 'nickname'
|
|
|
|
|
| 'chat'
|
2026-02-22 16:28:11 -05:00
|
|
|
| 'micGainEdit'
|
2026-02-21 02:06:32 -05:00
|
|
|
| 'effectSelect'
|
2026-02-20 08:16:43 -05:00
|
|
|
| 'listUsers'
|
|
|
|
|
| 'listItems'
|
|
|
|
|
| 'addItem'
|
|
|
|
|
| 'selectItem'
|
|
|
|
|
| 'itemProperties'
|
2026-02-20 17:46:43 -05:00
|
|
|
| 'itemPropertyEdit'
|
2026-02-22 23:42:17 -05:00
|
|
|
| 'itemPropertyOptionSelect'
|
|
|
|
|
| 'pianoUse';
|
2026-02-20 08:16:43 -05:00
|
|
|
|
|
|
|
|
export type Player = {
|
|
|
|
|
id: string | null;
|
|
|
|
|
nickname: string;
|
|
|
|
|
x: number;
|
|
|
|
|
y: number;
|
|
|
|
|
lastMoveTime: number;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type PeerState = {
|
|
|
|
|
id: string;
|
|
|
|
|
nickname: string;
|
|
|
|
|
x: number;
|
|
|
|
|
y: number;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type GameState = {
|
|
|
|
|
running: boolean;
|
|
|
|
|
mode: GameMode;
|
|
|
|
|
keysPressed: Record<string, boolean>;
|
|
|
|
|
nicknameInput: string;
|
|
|
|
|
cursorPos: number;
|
|
|
|
|
cursorVisible: boolean;
|
|
|
|
|
sortedPeerIds: string[];
|
|
|
|
|
listIndex: number;
|
|
|
|
|
sortedItemIds: string[];
|
|
|
|
|
itemListIndex: number;
|
|
|
|
|
selectedItemIds: string[];
|
|
|
|
|
selectionContext: SelectionContext;
|
|
|
|
|
selectedItemIndex: number;
|
|
|
|
|
selectedItemId: string | null;
|
|
|
|
|
itemPropertyKeys: string[];
|
|
|
|
|
itemPropertyIndex: number;
|
|
|
|
|
editingPropertyKey: string | null;
|
2026-02-20 17:46:43 -05:00
|
|
|
itemPropertyOptionValues: string[];
|
|
|
|
|
itemPropertyOptionIndex: number;
|
2026-02-21 02:06:32 -05:00
|
|
|
effectSelectIndex: number;
|
2026-02-20 08:16:43 -05:00
|
|
|
addItemTypeIndex: number;
|
|
|
|
|
isMuted: boolean;
|
|
|
|
|
player: Player;
|
|
|
|
|
peers: Map<string, PeerState>;
|
|
|
|
|
items: Map<string, WorldItem>;
|
|
|
|
|
carriedItemId: string | null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function createInitialState(): GameState {
|
|
|
|
|
return {
|
|
|
|
|
running: false,
|
|
|
|
|
mode: 'normal',
|
|
|
|
|
keysPressed: {},
|
|
|
|
|
nicknameInput: '',
|
|
|
|
|
cursorPos: 0,
|
|
|
|
|
cursorVisible: true,
|
|
|
|
|
sortedPeerIds: [],
|
|
|
|
|
listIndex: 0,
|
|
|
|
|
sortedItemIds: [],
|
|
|
|
|
itemListIndex: 0,
|
|
|
|
|
selectedItemIds: [],
|
|
|
|
|
selectionContext: null,
|
|
|
|
|
selectedItemIndex: 0,
|
|
|
|
|
selectedItemId: null,
|
|
|
|
|
itemPropertyKeys: [],
|
|
|
|
|
itemPropertyIndex: 0,
|
|
|
|
|
editingPropertyKey: null,
|
2026-02-20 17:46:43 -05:00
|
|
|
itemPropertyOptionValues: [],
|
|
|
|
|
itemPropertyOptionIndex: 0,
|
2026-02-21 02:06:32 -05:00
|
|
|
effectSelectIndex: 0,
|
2026-02-20 08:16:43 -05:00
|
|
|
addItemTypeIndex: 0,
|
|
|
|
|
isMuted: false,
|
|
|
|
|
player: {
|
|
|
|
|
id: null,
|
|
|
|
|
nickname: 'anon',
|
|
|
|
|
x: 20,
|
|
|
|
|
y: 20,
|
|
|
|
|
lastMoveTime: 0,
|
|
|
|
|
},
|
|
|
|
|
peers: new Map(),
|
|
|
|
|
items: new Map(),
|
|
|
|
|
carriedItemId: null,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getNearestPeer(state: GameState): { peerId: string | null; distance: number } {
|
|
|
|
|
let nearest: string | null = null;
|
|
|
|
|
let minDist = Infinity;
|
|
|
|
|
for (const [id, peer] of state.peers.entries()) {
|
|
|
|
|
const dist = Math.hypot(peer.x - state.player.x, peer.y - state.player.y);
|
|
|
|
|
if (dist < minDist) {
|
|
|
|
|
minDist = dist;
|
|
|
|
|
nearest = id;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return { peerId: nearest, distance: minDist };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getDirection(px: number, py: number, tx: number, ty: number): string {
|
|
|
|
|
const dx = tx - px;
|
|
|
|
|
const dy = ty - py;
|
|
|
|
|
if (dx === 0 && dy === 0) return 'here';
|
2026-02-21 03:28:51 -05:00
|
|
|
if (dx === 0) return dy > 0 ? 'directly north' : 'directly south';
|
|
|
|
|
if (dy === 0) return dx > 0 ? 'directly east' : 'directly west';
|
|
|
|
|
|
|
|
|
|
const octants = ['east', 'northeast', 'north', 'northwest', 'west', 'southwest', 'south', 'southeast'] as const;
|
|
|
|
|
const step = Math.PI / 4;
|
|
|
|
|
const rawIndex = Math.round(Math.atan2(dy, dx) / step);
|
|
|
|
|
const index = ((rawIndex % octants.length) + octants.length) % octants.length;
|
|
|
|
|
return octants[index];
|
2026-02-20 08:16:43 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getNearestItem(state: GameState): { itemId: string | null; distance: number } {
|
|
|
|
|
let nearest: string | null = null;
|
|
|
|
|
let minDist = Infinity;
|
|
|
|
|
for (const [id, item] of state.items.entries()) {
|
|
|
|
|
if (item.carrierId) continue;
|
|
|
|
|
const dist = Math.hypot(item.x - state.player.x, item.y - state.player.y);
|
|
|
|
|
if (dist < minDist) {
|
|
|
|
|
minDist = dist;
|
|
|
|
|
nearest = id;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return { itemId: nearest, distance: minDist };
|
|
|
|
|
}
|