import { Hooks, RpgCommonMap, RpgShape, MapPhysicsInitContext, MapPhysicsEntityContext, RpgActionInput, WorldMapsManager, LightingState, LightingTransitionOptions, WeatherState, WorldMapConfig, RpgWritableSignal } from '@rpgjs/common'; import { RpgPlayer, RpgEvent } from '../Player/Player'; import { BehaviorSubject } from 'rxjs'; import { MapOptions } from '../decorators/map'; import { EventMode } from '../decorators/event'; import { RpgMapProjectiles } from '../projectiles'; import { DamageFormulas } from '../Player/BattleManager'; /** * Interface for input controls configuration * * Defines the structure for input validation and anti-cheat controls */ export interface Controls { /** Maximum allowed time delta between inputs in milliseconds */ maxTimeDelta?: number; /** Maximum allowed frame delta between inputs */ maxFrameDelta?: number; /** Minimum time between inputs in milliseconds */ minTimeBetweenInputs?: number; /** Whether to enable anti-cheat validation */ enableAntiCheat?: boolean; /** Maximum number of queued inputs processed per server tick */ maxInputsPerTick?: number; } /** * Interface representing hook methods available for map events * * These hooks are triggered at specific moments during the event lifecycle. * * `onInit()` is intended for base event setup when the event instance is created. * At this stage, the event is not reacting to a specific player yet. * * `onChanges(player)` is reactive. It is called during the change-detection cycle, * for example after player state changes such as variable updates or when * `player.syncChanges()` is executed manually. */ export interface EventHooks { /** * Called when the event is first initialized. * * Use this hook for default setup that does not depend on a player interaction, * such as setting the initial graphic, speed, or movement route. */ onInit?: (this: RpgEvent) => void; /** * Called during the change-detection cycle for the current player. * * Use this hook to recompute the event state from player data, especially * player variables. This is useful for reactive visuals such as an opened * chest, a hidden door, or a conditional NPC graphic. */ onChanges?: (this: RpgEvent, player: RpgPlayer) => void; /** Called when a player performs an action on this event */ onAction?: (this: RpgEvent, player: RpgPlayer, input: RpgActionInput) => void | Promise; /** Called when a player touches this event */ onPlayerTouch?: (this: RpgEvent, player: RpgPlayer) => void; /** Called when this event starts touching a player or another event */ onTouch?: (this: RpgEvent, other: RpgPlayer | RpgEvent, context: RpgTouchContext) => void | Promise; /** Called when this event stops touching a player or another event */ onTouchEnd?: (this: RpgEvent, other: RpgPlayer | RpgEvent, context: RpgTouchContext) => void | Promise; /** Called when a player enters a shape attached to the event */ onInShape?: (this: RpgEvent, zone: RpgShape, player: RpgPlayer) => void; /** Called when a player exits a shape attached to the event */ onOutShape?: (this: RpgEvent, zone: RpgShape, player: RpgPlayer) => void; /** Called when a player is detected entering a detection shape attached to the event */ onDetectInShape?: (this: RpgEvent, player: RpgPlayer, shape: RpgShape) => void; /** Called when a player is detected exiting a detection shape attached to the event */ onDetectOutShape?: (this: RpgEvent, player: RpgPlayer, shape: RpgShape) => void; } export interface RpgTouchContext { self: RpgEvent; other: RpgPlayer | RpgEvent; otherType: "player" | "event"; player?: RpgPlayer; phase: "start" | "end"; pairId: string; map: RpgMap; } /** Type for event class constructor */ export type EventConstructor = new () => RpgEvent; /** * Object-based event definition. * * Coordinates belong to the surrounding map event wrapper, not the event definition itself. */ export type EventDefinition = EventHooks & { /** Optional display name copied to the runtime event instance */ name?: string; /** Shared or scenario event mode */ mode?: EventMode | "shared" | "scenario"; /** Whether players can physically push this event. `false` by default. */ pushable?: boolean; /** Physical mass used when the event is pushable. `0` or `Infinity` makes it immovable. */ mass?: number; /** Allow custom event metadata while keeping placement fields typed separately */ [key: string]: unknown; /** Disallow placement fields on the event definition itself */ id?: never; event?: never; x?: never; y?: never; scenarioOwnerId?: never; }; /** Public event definition type accepted by map events and dynamic event creation */ export type MapEventDefinition = EventConstructor | EventDefinition; /** Options for positioning and defining an event on the map */ export type EventPosOption = { /** ID of the event */ id?: string; /** X position of the event on the map */ x?: number; /** Y position of the event on the map */ y?: number; /** Event mode override */ mode?: EventMode | "shared" | "scenario"; /** Owner player id when mode is scenario */ scenarioOwnerId?: string; /** Initial event hitbox in RPGJS pixels */ hitbox?: { width?: number; height?: number; w?: number; h?: number; }; /** * Event definition - can be either: * - A class that extends RpgEvent * - An object with hook methods */ event: MapEventDefinition; }; /** Public placed map event type */ export type MapEventPlacement = EventPosOption; type CreateDynamicEventOptions = { mode?: EventMode | "shared" | "scenario"; scenarioOwnerId?: string; }; interface WeatherSetOptions { sync?: boolean; } interface LightingSetOptions { sync?: boolean; cancelTransition?: boolean; } /** * Stable connection surface passed to RPGJS room lifecycle methods. * * The room runtime owns the connection. Game code may send data, close the * socket, or replace its application state without depending on a transport * implementation. */ export interface RpgRoomConnection { /** Stable public connection identifier. */ readonly id: string; /** Private session identifier retained by supported reconnection flows. */ readonly sessionId?: string; /** Current application-owned state. Use `setState()` to replace it. */ readonly state: Readonly | null; /** Replace the application-owned connection state. */ setState(state: TState | ((previous: Readonly | null) => TState) | null): Readonly | null; /** Send data to this connection. */ send(data: string | ArrayBuffer | ArrayBufferView): void; /** Close this connection. */ close(code?: number, reason?: string): void; } export declare class RpgMap extends RpgCommonMap { private readonly partyRoom; private _clientListeners; private activeTouchCollisions; private trackedTouchCollisions; private spatialVisibleEventIds; private spatialVisiblePlayerIds; /** * Synchronized signal containing all players currently on the map * * This signal is automatically synchronized with clients by RPGJS. * Players are indexed by their unique ID. * * @example * ```ts * // Get all players * const allPlayers = map.players(); * * // Get a specific player * const player = map.players()['player-id']; * ``` */ players: RpgWritableSignal>; /** * Synchronized signal containing all events (NPCs, objects) on the map * * This signal is automatically synchronized with clients by RPGJS. * Events are indexed by their unique ID. * * @example * ```ts * // Get all events * const allEvents = map.events(); * * // Get a specific event * const event = map.events()['event-id']; * ``` */ events: RpgWritableSignal>; /** * Signal containing the map's database of items, classes, and other game data * * This database can be dynamically populated using `addInDatabase()` and * `removeInDatabase()` methods. It's used to store game entities like items, * classes, skills, etc. that are specific to this map. * * @example * ```ts * // Add data to database * map.addInDatabase('Potion', PotionClass); * * // Access database * const potion = map.database()['Potion']; * ``` */ database: RpgWritableSignal>; variables: RpgWritableSignal>; /** * Array of map configurations - can contain MapOptions objects or instances of map classes * * This array stores the configuration for this map and any related maps. * It's populated when the map is loaded via `updateMap()`. */ maps: (MapOptions | any)[]; /** * Array of sound IDs to play when players join the map * * These sounds are automatically played for each player when they join the map. * Sounds must be defined on the client side. * * @example * ```ts * // Set sounds for the map * map.sounds = ['background-music', 'ambient-forest']; * ``` */ sounds: string[]; /** * BehaviorSubject that completes when the map data is ready * * This subject is used to signal when the map has finished loading all its data. * Players wait for this to complete before the map is fully initialized. * * @example * ```ts * // Wait for map data to be ready * map.dataIsReady$.subscribe(() => { * console.log('Map is ready!'); * }); * ``` */ dataIsReady$: BehaviorSubject; /** * Global configuration object for the map * * This object contains configuration settings that apply to the entire map. * It's populated from the map data when `updateMap()` is called. */ globalConfig: any; /** * Damage formulas configuration for the map * * Contains formulas for calculating damage from skills, physical attacks, * critical hits, and element coefficients. Default formulas are merged * with custom formulas when the map is loaded. */ damageFormulas: DamageFormulas; private _weatherState; private _lightingState; private _lightingTransitionTimer?; /** Internal: Map of shapes by name */ private _shapes; /** Internal: Map of shape entity UUIDs to RpgShape instances */ private _shapeEntities; private _serverTickInProgress; private _queuedServerTickDelta; private _serverTickLoopVersion; private _pendingAckFrames; /** Enable/disable automatic tick processing (useful for unit tests) */ private _autoTickEnabled; /** Runtime templates for scenario events to instantiate per player */ private _scenarioEventTemplates; /** Runtime registry of event mode by id */ private _eventModeById; /** Runtime registry of scenario owner by event id */ private _eventOwnerById; /** Runtime registry of spawned scenario event ids by player id */ private _scenarioEventIdsByPlayer; private _syncChangesDepth; projectiles: RpgMapProjectiles; autoSync: boolean; constructor(room: any); onStart(): Promise; /** Rebuild non-serializable map resources after a room restart or hibernation. */ onRestore(): Promise; private getRuntimeMapUpdateToken; private restoreMapStreamingRuntime; private restoreWorldMapsRuntime; private hasActiveConnections; protected emitPhysicsInit(context: MapPhysicsInitContext): void; protected emitPhysicsEntityAdd(context: MapPhysicsEntityContext): void; protected emitPhysicsEntityRemove(context: MapPhysicsEntityContext): void; protected emitPhysicsReset(): void; protected runFixedTicks(deltaMs: number, hooks?: { beforeStep?: () => void; afterStep?: (tick: number) => void; }): number; protected runFixedTicksAsync(deltaMs: number, hooks?: { beforeStep?: () => void | Promise; afterStep?: (tick: number) => void | Promise; }): Promise; loadPhysic(): void; clearPhysic(): void; private isPositiveNumber; private resolveTrustedMapDimensions; private normalizeEventMode; private resolveEventMode; private resolveScenarioOwnerId; private resolveEventMass; private resolveEventPushable; private resolveEventHitbox; private normalizeEventObject; private cloneEventTemplate; private buildRuntimeEventId; private setEventRuntimeMetadata; private clearEventRuntimeMetadata; private getEventModeById; private getScenarioOwnerIdByEventId; isEventVisibleForPlayer(eventOrId: string | RpgEvent, playerOrId: string | RpgPlayer): boolean; private spawnScenarioEventsForPlayer; private removeScenarioEventsForPlayer; private readBooleanSignal; private isGroundTouchSensorEntity; private haveDifferentTouchableZ; private buildTouchPairId; private getPhysicsRect; private getSensorCoverage; private hasEnoughGroundSensorCoverage; private dispatchTouch; private dispatchTouchCollision; private canActivateTouchCollision; private updateTrackedTouchCollision; private refreshTrackedTouchCollisions; private trackTouchCollision; private untrackTouchCollision; /** * Setup collision detection between players, events, and shapes * * This method listens to physics collision events and triggers hooks: * - `onPlayerTouch` on events when a player collides with them * - `onInShape` on players and events when they enter a shape * - `onOutShape` on players and events when they exit a shape * * ## Architecture * * Uses the physics engine's collision event system to detect when entities collide. * When a collision is detected: * - Between a player and an event: triggers `onPlayerTouch` on the event * - Between a player/event and a shape: triggers `onInShape`/`onOutShape` hooks * * @example * ```ts * // Event with onPlayerTouch hook * map.createDynamicEvent({ * x: 100, * y: 200, * event: { * onPlayerTouch(player) { * console.log(`Player ${player.id} touched this event!`); * } * } * }); * * // Player with onInShape hook * const player: RpgPlayerHooks = { * onInShape(player: RpgPlayer, shape: RpgShape) { * console.log('in', player.name, shape.name); * }, * onOutShape(player: RpgPlayer, shape: RpgShape) { * console.log('out', player.name, shape.name); * } * }; * ``` */ private setupCollisionDetection; setVariable(key: string, val: T): void; getVariable(key: string): T | undefined; removeVariable(key: string): boolean; hasVariable(key: string): boolean; getVariableKeys(): string[]; clearVariables(): void; syncChanges(): void; /** * Intercepts and modifies packets before they are sent to clients * * This method is automatically called by the RPGJS room runtime for each packet sent to clients. * It adds timestamp and acknowledgment information to sync packets for client-side * prediction reconciliation. This helps with network synchronization and reduces * perceived latency. * * ## Architecture * * Adds metadata to packets: * - `timestamp`: Current server time for client-side prediction * - `ack`: Acknowledgment info with last processed frame and authoritative position * * @param player - The player receiving the packet * @param packet - The packet data to intercept * @param conn - The connection object * @returns Modified packet with timestamp and ack info, or null if player is invalid * * @example * ```ts * // This method is called automatically by the framework * // You typically don't call it directly * ``` */ interceptorPacket(player: RpgPlayer, packet: any, conn: RpgRoomConnection): any; /** * Called when a player joins the map * * This method is automatically called by the RPGJS room runtime when a player connects to the map. * It initializes the player's connection, sets up the map context, and waits for * the map data to be ready before playing sounds and triggering hooks. * * ## Architecture * * 1. Sets player's map reference and context * 2. Initializes the player * 3. Waits for map data to be ready * 4. Plays map sounds for the player * 5. Triggers `server-player-onJoinMap` hook * * @param player - The player joining the map * @param conn - The connection object for the player * * @example * ```ts * // This method is called automatically by the framework * // You can listen to the hook to perform custom logic * server.addHook('server-player-onJoinMap', (player, map) => { * console.log(`Player ${player.id} joined map ${map.id}`); * }); * ``` */ onJoin(player: RpgPlayer, conn: RpgRoomConnection): Promise; /** * Called when a player leaves the map * * This method is automatically called by the RPGJS room runtime when a player disconnects from the map. * It cleans up the player's pending inputs and triggers the appropriate hooks. * * ## Architecture * * 1. Triggers `server-player-onLeaveMap` hook * 2. Clears pending inputs to prevent processing after disconnection * * @param player - The player leaving the map * @param conn - The connection object for the player * * @example * ```ts * // This method is called automatically by the framework * // You can listen to the hook to perform custom cleanup * server.addHook('server-player-onLeaveMap', (player, map) => { * console.log(`Player ${player.id} left map ${map.id}`); * }); * ``` */ onLeave(player: RpgPlayer, conn: RpgRoomConnection): Promise; /** * Get the hooks system for this map * * Returns the dependency-injected Hooks instance that allows you to trigger * and listen to various game events. * * @returns The Hooks instance for this map * * @example * ```ts * // Trigger a custom hook * map.hooks.callHooks('custom-event', data).subscribe(); * ``` */ get hooks(): Hooks; private _getClientListenerBucket; private _dispatchClientEvent; onSessionRestore(payload: { userSnapshot: any; user?: RpgPlayer; }): Promise; /** * Handle GUI interaction from a player * * This method is called when a player interacts with a GUI element. * It synchronizes the player's changes to ensure the client state is up to date. * * @param player - The player performing the interaction * @param value - The interaction data from the client * * @example * ```ts * // This method is called automatically when a player interacts with a GUI * // The interaction data is sent from the client * ``` */ guiInteraction(player: RpgPlayer, value: { guiId: string; name: string; data: any; }): Promise; /** * Handle GUI exit from a player * * This method is called when a player closes or exits a GUI. * It removes the GUI from the player's active GUIs. * * @param player - The player exiting the GUI * @param guiId - The ID of the GUI being exited * @param data - Optional data associated with the GUI exit * * @example * ```ts * // This method is called automatically when a player closes a GUI * // The GUI is removed from the player's active GUIs * ``` */ guiExit(player: RpgPlayer, { guiId, data, guiOpenId }: { guiId: any; data: any; guiOpenId: any; }): void; /** * Handle action input from a player * * This method is called when a player performs an action (like pressing a button). * It checks for collisions with events and triggers the appropriate hooks. * * ## Architecture * * 1. Gets all entities colliding with the player * 2. Triggers `onAction` hook on colliding events * 3. Triggers `onInput` hook on the player * * @param player - The player performing the action * @param action - The action data (button pressed, etc.) * * @example * ```ts * // This method is called automatically when a player presses an action button * // Events near the player will have their onAction hook triggered * ``` */ onAction(player: RpgPlayer, action: RpgActionInput): void; /** * Handle movement input from a player * * This method is called when a player sends movement input from the client. * It queues the input for processing by the game loop. Inputs are processed * with frame numbers to ensure proper ordering and client-side prediction. * * ## Architecture * * - Inputs are queued in `player.pendingInputs` * - Duplicate frames are skipped to prevent processing the same input twice * - Inputs are processed asynchronously by the game loop * * @param player - The player sending the movement input * @param input - The input data containing frame number, input direction, and timestamp * * @example * ```ts * // This method is called automatically when a player moves * // The input is queued and processed by processInput() * ``` */ onInput(player: RpgPlayer, input: any): Promise; onPing(player: RpgPlayer, payload: { clientTime?: number; clientFrame?: number; }): void; onMapStreamRequest(player: RpgPlayer, payload?: { mapId?: string; }): Promise; saveSlot(player: RpgPlayer, value: { requestId: string; index: number; meta?: any; }): Promise; loadSlot(player: RpgPlayer, value: { requestId: string; index: number; }): Promise; listSaveSlots(player: RpgPlayer, value: { requestId: string; }): Promise; /** * Listen to custom websocket events sent by clients on this map. * * The callback receives the player who sent the event and the payload. * This is useful for map-wide custom interactions that are not covered * by built-in actions such as movement, GUI events, or the action button. * * @method map.on(type, cb) * @param type - Custom event name emitted by clients * @param cb - Callback invoked with the sending player and payload * @returns {void} * * @example * ```ts * map.on("chat:message", (player, data) => { * console.log(player.id, data.text); * }); * ``` */ on(type: string, cb: (player: RpgPlayer, data: T) => void | Promise): void; /** * Remove all listeners for a custom client event on this map. * * @method map.off(type) * @param type - Custom event name to clear * @returns {void} */ off(type: string): void; /** * Broadcast a custom websocket event to all clients connected to this map. * * This is a convenience wrapper around `$broadcast({ type, value })`. * On the client side, receive the event by injecting `WebSocketToken` * and subscribing with `socket.on(type, cb)`. * * @method map.broadcast(type, value) * @param type - Custom event name sent to all clients on the map * @param value - Payload sent with the event * @returns {void} * * @example * ```ts * map.broadcast("weather:warning", { * level: "storm", * }); * ``` * * @example * ```ts * import { inject } from "@rpgjs/client"; * import { WebSocketToken, type AbstractWebsocket } from "@rpgjs/client"; * * const socket = inject(WebSocketToken); * * socket.on("weather:warning", (payload) => { * console.log(payload.level); * }); * ``` */ broadcast(type: string, value?: T): void; _onUnhandledAction(player: RpgPlayer, message: { action: string; value: unknown; }): Promise; /** * Update the map configuration and data * * This endpoint receives map data from the client and initializes the map. * It loads the map configuration, damage formulas, events, and physics. * * ## Architecture * * 1. Validates the request body using MapUpdateSchema * 2. Updates map data, global config, and damage formulas * 3. Merges events and sounds from map configuration * 4. Triggers hooks for map loading * 5. Loads physics engine * 6. Creates all events on the map * 7. Completes the dataIsReady$ subject * * @param request - HTTP request containing map data * @returns Promise that resolves when the map is fully loaded * * @example * ```ts * // This endpoint is called automatically when a map is loaded * // POST /map/update * // Body: { * // id: string, * // width: number, * // height: number, * // config?: any, * // damageFormulas?: any, * // database?: any[] | Record * // } * ``` */ updateMap(request: Request): Promise; /** * Update (or create) a world configuration and propagate to all maps in that world * * This endpoint receives world map configuration data (typically from Tiled world import) * and creates or updates the world manager. The world ID is extracted from the URL path. * * ## Architecture * * 1. Authenticates the administrative update request * 2. Extracts the world ID from the `/world/:id/update` path segment * 3. Normalizes input to array of WorldMapConfig * 4. Persists the topology so it survives Durable Object hibernation * 5. Creates or updates the world manager * * Expected payload examples: * - `{ id: string, maps: WorldMapConfig[] }` * - `WorldMapConfig[]` * * @param request - HTTP request containing world configuration * @returns Promise resolving to `{ ok: true }` when complete * * @example * ```ts * // POST /world/my-world/update * // Body: [{ id: 'map1', worldX: 0, worldY: 0, width: 800, height: 600 }] * * // Or with nested structure * // Body: { id: 'my-world', maps: [{ id: 'map1', ... }] } * ``` */ updateWorld(request: Request): Promise; /** * Process pending inputs for a player with anti-cheat validation * * This method processes pending inputs for a player while performing * anti-cheat validation to prevent time manipulation and frame skipping. * It validates the time deltas between inputs and ensures they are within * acceptable ranges. To preserve movement itinerary under network bursts, * the number of distinct client ticks processed per call is capped. * * ## Architecture * * **Important**: This method only updates entity velocities - it does NOT step * the physics engine. Physics simulation is handled centrally by the game loop * (`tick$` -> `runFixedTicks`). This ensures: * - Consistent physics timing (60fps fixed timestep) * - No double-stepping when multiple inputs are processed * - Deterministic physics regardless of input frequency * * @param playerId - The ID of the player to process inputs for * @param controls - Optional anti-cheat configuration * @returns Promise containing the player and processed movement inputs * * @example * ```ts * // Process inputs with default anti-cheat settings * const result = await map.processInput('player1'); * console.log('Processed inputs:', result.inputs); * * // Process inputs with custom anti-cheat configuration * const result = await map.processInput('player1', { * maxTimeDelta: 100, * maxFrameDelta: 5, * minTimeBetweenInputs: 16, * enableAntiCheat: true * }); * ``` */ processInput(playerId: string, controls?: Controls): Promise<{ player: RpgPlayer; inputs: any[]; }>; /** * Main server game loop. * * A single tick subscription drives input processing, fixed physics steps, * projectiles, and sync side effects in a deterministic order. */ private startServerTickLoop; private stopServerTickLoop; private runQueuedServerTick; private runServerTick; private captureProcessedInputPositions; private processPendingInputsForTick; private getServerTickTime; private getPendingInputCount; nextTickAsync(deltaMs?: number): Promise; /** * Enable or disable automatic server tick processing * * When disabled, the unified input/physics/projectile loop will not run * automatically. This is useful for unit tests where you want manual control * over server ticks. * * @param enabled - Whether to enable automatic tick processing (default: true) * * @example * ```ts * // Disable auto tick for testing * map.setAutoTick(false); * * // Manually trigger tick processing * await map.nextTickAsync(); * ``` */ setAutoTick(enabled: boolean): void; /** * Get a world manager by id * * Returns the world maps manager for the given world ID. Currently, only * one world manager is supported per map instance. * * @param id - The world ID (currently unused, returns the single manager) * @returns The WorldMapsManager instance, or null if not initialized * * @example * ```ts * const worldManager = map.getWorldMaps('my-world'); * if (worldManager) { * const mapInfo = worldManager.getMapInfo('map1'); * } * ``` */ getWorldMaps(id: string): WorldMapsManager | null; /** * Delete a world manager by id * * Removes the world maps manager from this map instance. Currently, only * one world manager is supported, so this clears the single manager. * * @param id - The world ID (currently unused) * @returns true if the manager was deleted, false if it didn't exist * * @example * ```ts * const deleted = map.deleteWorldMaps('my-world'); * if (deleted) { * console.log('World manager removed'); * } * ``` */ deleteWorldMaps(id: string): boolean; /** * Create a world manager dynamically * * Creates a new WorldMapsManager instance and configures it with the provided * map configurations. This is used when loading world data from Tiled or * other map editors. * * @param world - World configuration object * @param world.id - Optional world identifier * @param world.maps - Array of map configurations for the world * @returns The newly created WorldMapsManager instance * * @example * ```ts * const manager = map.createDynamicWorldMaps({ * id: 'my-world', * maps: [ * { id: 'map1', worldX: 0, worldY: 0, width: 800, height: 600 }, * { id: 'map2', worldX: 800, worldY: 0, width: 800, height: 600 } * ] * }); * ``` */ createDynamicWorldMaps(world: { id?: string; maps: WorldMapConfig[]; }): WorldMapsManager; /** * Update world maps by id. Auto-create when missing. * * Updates the world maps configuration. If the world manager doesn't exist, * it is automatically created. This is useful for dynamically loading world * data or updating map positions. * * @param id - The world ID * @param maps - Array of map configurations to update * @returns Promise that resolves when the update is complete * * @example * ```ts * await map.updateWorldMaps('my-world', [ * { id: 'map1', worldX: 0, worldY: 0, width: 800, height: 600 }, * { id: 'map2', worldX: 800, worldY: 0, width: 800, height: 600 } * ]); * ``` */ updateWorldMaps(id: string, maps: WorldMapConfig[]): Promise; /** * Add data to the map's database * * This method delegates to BaseRoom's implementation to avoid code duplication. * * @param id - Unique identifier for the data * @param data - The data to store (can be a class, object, or any value) * @param options - Optional configuration * @param options.force - If true, overwrites existing data even if ID already exists (default: false) * @returns true if data was added, false if ignored (ID already exists) * * @example * ```ts * // Add an item class to the database * map.addInDatabase('Potion', PotionClass); * * // Add an item object to the database * map.addInDatabase('custom-item', { * name: 'Custom Item', * price: 100 * }); * * // Force overwrite existing data * map.addInDatabase('Potion', UpdatedPotionClass, { force: true }); * ``` */ addInDatabase(id: string, data: any, options?: { force?: boolean; }): boolean; /** * Remove data from the map's database * * This method delegates to BaseRoom's implementation to avoid code duplication. * * @param id - Unique identifier of the data to remove * @returns true if data was removed, false if ID didn't exist * * @example * ```ts * // Remove an item from the database * map.removeInDatabase('Potion'); * * // Check if removal was successful * const removed = map.removeInDatabase('custom-item'); * if (removed) { * console.log('Item removed successfully'); * } * ``` */ removeInDatabase(id: string): boolean; /** * Creates a dynamic event on the map * * This method handles both class-based events and object-based events with hooks. * For class-based events, it creates a new instance of the class. * For object-based events, it creates a dynamic class that extends RpgEvent and * implements the hook methods from the object. * * @param eventObj - The event position and definition * * @example * // Using a class-based event * class MyEvent extends RpgEvent { * onInit() { * console.log('Event initialized'); * } * } * * map.createDynamicEvent({ * x: 100, * y: 200, * event: MyEvent * }); * * // Using an object-based event * map.createDynamicEvent({ * x: 100, * y: 200, * event: { * onInit() { * console.log('Event initialized'); * }, * onPlayerTouch(player) { * console.log('Player touched event'); * } * } * }); */ createDynamicEvent(eventObj: EventPosOption, options?: CreateDynamicEventOptions): Promise; /** * Get an event by its ID * * Returns the event with the specified ID, or undefined if not found. * The return type can be narrowed using TypeScript generics. * * @param eventId - The unique identifier of the event * @returns The event instance, or undefined if not found * * @example * ```ts * // Get any event * const event = map.getEvent('npc-1'); * * // Get event with type narrowing * const npc = map.getEvent('npc-1'); * if (npc) { * npc.speak('Hello!'); * } * ``` */ getEvent(eventId: string): T | undefined; /** * Get a player by their ID * * Returns the player with the specified ID, or undefined if not found. * * @param playerId - The unique identifier of the player * @returns The player instance, or undefined if not found * * @example * ```ts * const player = map.getPlayer('player-123'); * if (player) { * console.log(`Player ${player.name} is on the map`); * } * ``` */ getPlayer(playerId: string): RpgPlayer | undefined; /** * Get all players currently on the map * * Returns an array of all players that are currently connected to this map. * * @returns Array of all RpgPlayer instances on the map * * @example * ```ts * const players = map.getPlayers(); * console.log(`There are ${players.length} players on the map`); * * players.forEach(player => { * console.log(`- ${player.name}`); * }); * ``` */ getPlayers(): RpgPlayer[]; /** * Get all events on the map * * Returns an array of all events (NPCs, objects, etc.) that are currently * on this map. * * @returns Array of all RpgEvent instances on the map * * @example * ```ts * const events = map.getEvents(); * console.log(`There are ${events.length} events on the map`); * * events.forEach(event => { * console.log(`- ${event.name} at (${event.x}, ${event.y})`); * }); * ``` */ getEvents(): RpgEvent[]; getEventsForPlayer(playerOrId: string | RpgPlayer): RpgEvent[]; /** * Get the first event that matches a condition * * Searches through all events on the map and returns the first one that * matches the provided callback function. * * @param cb - Callback function that returns true for the desired event * @returns The first matching event, or undefined if none found * * @example * ```ts * // Find an event by name * const npc = map.getEventBy(event => event.name === 'Merchant'); * * // Find an event at a specific position * const chest = map.getEventBy(event => * event.x === 100 && event.y === 200 * ); * ``` */ getEventBy(cb: (event: RpgEvent) => boolean): RpgEvent | undefined; /** * Get all events that match a condition * * Searches through all events on the map and returns all events that * match the provided callback function. * * @param cb - Callback function that returns true for desired events * @returns Array of all matching events * * @example * ```ts * // Find all NPCs * const npcs = map.getEventsBy(event => event.name.startsWith('NPC-')); * * // Find all events in a specific area * const nearbyEvents = map.getEventsBy(event => * event.x >= 0 && event.x <= 100 && * event.y >= 0 && event.y <= 100 * ); * ``` */ getEventsBy(cb: (event: RpgEvent) => boolean): RpgEvent[]; /** * Remove an event from the map * * Removes the event with the specified ID from the map. The event will * be removed from the synchronized events signal, causing it to disappear * on all clients. * * @param eventId - The unique identifier of the event to remove * * @example * ```ts * // Remove an event * map.removeEvent('npc-1'); * * // Remove event after interaction * const chest = map.getEvent('chest-1'); * if (chest) { * // ... do something with chest ... * map.removeEvent('chest-1'); * } * ``` */ removeEvent(eventId: string): void; /** * Display a component animation at a specific position on the map * * This method broadcasts a component animation to all clients connected to the map, * allowing temporary visual effects to be displayed at any location on the map. * Component animations are custom Canvas Engine components that can display * complex effects with custom logic and parameters. * * @param id - The ID of the component animation to display * @param position - The x, y coordinates where to display the animation * @param params - Parameters to pass to the component animation * * @example * ```ts * // Show explosion at specific coordinates * map.showComponentAnimation("explosion", { x: 300, y: 400 }, { * intensity: 2.5, * duration: 1500 * }); * * // Show area damage effect * map.showComponentAnimation("area-damage", { x: player.x, y: player.y }, { * radius: 100, * color: "red", * damage: 50 * }); * * // Show treasure spawn effect * map.showComponentAnimation("treasure-spawn", { x: 150, y: 200 }, { * sparkle: true, * sound: "treasure-appear" * }); * ``` */ showComponentAnimation(id: string, position: { x: number; y: number; }, params: any): void; /** * Display a spritesheet animation at a specific position on the map * * This method displays a temporary visual animation using a spritesheet at any * location on the map. It's a convenience method that internally uses showComponentAnimation * with the built-in 'animation' component. This is useful for spell effects, environmental * animations, or any visual feedback that uses predefined spritesheets. * * @param position - The x, y coordinates where to display the animation * @param graphic - The ID of the spritesheet to use for the animation * @param animationName - The name of the animation within the spritesheet (default: 'default') * * @example * ```ts * // Show explosion at specific coordinates * map.showAnimation({ x: 100, y: 200 }, "explosion"); * * // Show spell effect at player position * const playerPos = { x: player.x, y: player.y }; * map.showAnimation(playerPos, "spell-effects", "lightning"); * * // Show environmental effect * map.showAnimation({ x: 300, y: 150 }, "nature-effects", "wind-gust"); * * // Show portal opening animation * map.showAnimation({ x: 500, y: 400 }, "portals", "opening"); * ``` */ showAnimation(position: { x: number; y: number; }, graphic: string, animationName?: string): void; private cloneWeatherState; /** * Get the current map weather state. */ getWeather(): WeatherState | null; /** * Set the full weather state for this map. * * When `sync` is true (default), all connected clients receive the new weather. */ setWeather(next: WeatherState | null, options?: WeatherSetOptions): WeatherState | null; /** * Patch the current weather state. * * Nested `params` values are merged. */ patchWeather(patch: Partial, options?: WeatherSetOptions): WeatherState | null; /** * Clear weather for this map. */ clearWeather(options?: WeatherSetOptions): void; private clearLightingTransition; private interpolateNumber; private easeLightingProgress; private interpolateLighting; /** * Get the current map lighting state. */ getLighting(): LightingState | null; /** * Set the full lighting state for this map. * * When `sync` is true (default), all connected clients receive the new lighting. */ setLighting(next: LightingState | null, options?: LightingSetOptions): LightingState | null; /** * Patch the current lighting state. * * Nested `ambient`, `sun`, and `shadows` values are merged. */ patchLighting(patch: Partial, options?: LightingSetOptions): LightingState | null; /** * Clear lighting for this map. */ clearLighting(options?: LightingSetOptions): void; /** * Apply the default daytime lighting preset. */ setDay(options?: LightingSetOptions): LightingState | null; /** * Apply the default nighttime lighting preset. */ setNight(options?: LightingSetOptions): LightingState | null; /** * Transition lighting over time by broadcasting intermediate lighting states. */ transitionLighting(toLighting: Partial, options?: LightingTransitionOptions & LightingSetOptions): LightingState | null; /** * Configure runtime synchronized properties on the map * * This method allows you to dynamically add synchronized properties to the map * that will be automatically synced with clients. The schema follows the same * structure as module properties with `$initial`, `$syncWithClient`, and `$permanent` options. * * ## Architecture * * - Reads a schema object shaped like module props * - Creates typed synchronized signals through the RPGJS gameplay contract * - Properties are accessible as `map.propertyName` * * @param schema - Schema object defining the properties to sync * @param schema[key].$initial - Initial value for the property * @param schema[key].$syncWithClient - Whether to sync this property to clients * @param schema[key].$permanent - Whether to persist this property * * @example * ```ts * // Add synchronized properties to the map * map.setSync({ * weather: { * $initial: 'sunny', * $syncWithClient: true, * $permanent: false * }, * timeOfDay: { * $initial: 12, * $syncWithClient: true, * $permanent: false * } * }); * * // Use the properties * map.weather.set('rainy'); * const currentWeather = map.weather(); * ``` */ setSync(schema: Record): void; /** * Apply sync to the client * * This method applies sync to the client by calling the `$applySync()` method. * * @example * ```ts * map.applySyncToClient(); * ``` */ applySyncToClient(): void; /** * Create a shape dynamically on the map * * This method creates a static hitbox on the map that can be used for * collision detection, area triggers, or visual boundaries. The shape is * backed by the physics engine's static entity system for accurate collision detection. * * ## Architecture * * Creates a static entity (hitbox) in the physics engine at the specified position and size. * The shape is stored internally and can be retrieved by name. When players or events * collide with this hitbox, the `onInShape` and `onOutShape` hooks are automatically * triggered on both the player and the event. * * @param obj - Shape configuration object * @param obj.x - X position of the shape (top-left corner) (required) * @param obj.y - Y position of the shape (top-left corner) (required) * @param obj.width - Width of the shape in pixels (required) * @param obj.height - Height of the shape in pixels (required) * @param obj.name - Name of the shape (optional, auto-generated if not provided) * @param obj.z - Z position/depth for rendering (optional) * @param obj.color - Color in hexadecimal format, shared with client (optional) * @param obj.collision - Whether the shape has collision (optional) * @param obj.properties - Additional custom properties (optional) * @returns The created RpgShape instance * * @example * ```ts * // Create a simple rectangular shape * const shape = map.createShape({ * x: 100, * y: 200, * width: 50, * height: 50, * name: "spawn-zone" * }); * * // Create a shape with visual properties * const triggerZone = map.createShape({ * x: 300, * y: 400, * width: 100, * height: 100, * name: "treasure-area", * color: "#FFD700", * z: 1, * collision: false, * properties: { * type: "treasure", * value: 100 * } * }); * * // Player hooks will be triggered automatically * const player: RpgPlayerHooks = { * onInShape(player: RpgPlayer, shape: RpgShape) { * console.log('in', player.name, shape.name); * }, * onOutShape(player: RpgPlayer, shape: RpgShape) { * console.log('out', player.name, shape.name); * } * }; * ``` */ createShape(obj: { x: number; y: number; width: number; height: number; name?: string; z?: number; color?: string; collision?: boolean; properties?: Record; }): RpgShape; /** * Delete a shape from the map * * Removes a shape by its name and cleans up the associated static hitbox entity. * If the shape doesn't exist, the method does nothing. * * @param name - Name of the shape to remove * @returns void * * @example * ```ts * // Create and then remove a shape * const shape = map.createShape({ * x: 100, * y: 200, * width: 50, * height: 50, * name: "temp-zone" * }); * * // Later, remove it * map.removeShape("temp-zone"); * ``` */ removeShape(name: string): void; /** * Get all shapes on the map * * Returns an array of all shapes that have been created on this map, * regardless of whether they are static shapes or player-attached shapes. * * @returns Array of RpgShape instances * * @example * ```ts * // Create multiple shapes * map.createShape({ x: 0, y: 0, width: 50, height: 50, name: "zone1" }); * map.createShape({ x: 100, y: 100, width: 50, height: 50, name: "zone2" }); * * // Get all shapes * const allShapes = map.getShapes(); * console.log(allShapes.length); // 2 * ``` */ getShapes(): RpgShape[]; /** * Get a shape by its name * * Returns a shape with the specified name, or undefined if no shape * with that name exists on the map. * * @param name - Name of the shape to retrieve * @returns The RpgShape instance, or undefined if not found * * @example * ```ts * // Create a shape with a specific name * map.createShape({ * x: 100, * y: 200, * width: 50, * height: 50, * name: "spawn-point" * }); * * // Retrieve it later * const spawnZone = map.getShape("spawn-point"); * if (spawnZone) { * console.log(`Spawn zone at (${spawnZone.x}, ${spawnZone.y})`); * } * ``` */ getShape(name: string): RpgShape | undefined; /** * Play a sound for all players on the map * * This method plays a sound for all players currently on the map by iterating * over each player and calling `player.playSound()`. The sound must be defined * on the client side (in the client module configuration). * This is ideal for environmental sounds, battle music, or map-wide events that * all players should hear simultaneously. * * ## Design * * Iterates over all players on the map and calls `player.playSound()` for each one. * This avoids code duplication and reuses the existing player sound logic. * For player-specific sounds, use `player.playSound()` directly. * * @param soundId - Sound identifier, defined on the client side * @param options - Optional sound configuration * @param options.volume - Volume level (0.0 to 1.0, default: 1.0) * @param options.loop - Whether the sound should loop (default: false) * * @example * ```ts * // Play a sound for all players on the map * map.playSound("explosion"); * * // Play background music for everyone with volume and loop * map.playSound("battle-theme", { * volume: 0.7, * loop: true * }); * * // Play a door opening sound at low volume * map.playSound("door-open", { volume: 0.4 }); * ``` */ playSound(soundId: string, options?: { volume?: number; loop?: boolean; }): void; /** * Trigger a named client visual for all players on the map. * * Client visuals are registered in the client module with `clientVisuals`. * They are client-side macros for grouping existing visual primitives such as * flash, sound, component animations, sprite animations, or camera shake. * The map broadcasts one compact packet containing the visual name and a * serializable payload; each client resolves and renders the visual locally. * * Prefer direct APIs such as `playSound()`, `showComponentAnimation()`, or * `flash()` for a single visual operation. Use `clientVisual()` when one * gameplay moment should trigger several client-side visuals together. * * @param name - Visual name registered on the client * @param data - Serializable payload passed to the client visual handler * * @example * ```ts * map.clientVisual("explosion", { * position: { x: 320, y: 180 }, * power: 2, * }); * ``` */ clientVisual = Record>(name: string, data?: TData): void; /** * Stop a sound for all players on the map * * This method stops a sound that was previously started with `map.playSound()` * for all players on the map by iterating over each player and calling `player.stopSound()`. * * @param soundId - Sound identifier to stop * * @example * ```ts * // Start background music for everyone * map.playSound("battle-theme", { loop: true }); * * // Later, stop it for everyone * map.stopSound("battle-theme"); * ``` */ stopSound(soundId: string): void; /** * Shake the map for all players * * This method triggers a shake animation on the map for all players currently on the map. * The shake effect creates a visual feedback that can be used for earthquakes, explosions, * impacts, or any dramatic event that should affect the entire map visually. * * ## Architecture * * Broadcasts a shake event to all clients connected to the map. Each client receives * the shake configuration and triggers the shake animation on the map container using * Canvas Engine's shake directive. * * @param options - Optional shake configuration * @param options.intensity - Shake intensity in pixels (default: 10) * @param options.duration - Duration of the shake animation in milliseconds (default: 500) * @param options.frequency - Number of shake oscillations during the animation (default: 10) * @param options.direction - Direction of the shake - 'x', 'y', or 'both' (default: 'both') * * @example * ```ts * // Basic shake with default settings * map.shakeMap(); * * // Intense earthquake effect * map.shakeMap({ * intensity: 25, * duration: 1000, * frequency: 15, * direction: 'both' * }); * * // Horizontal shake for side impact * map.shakeMap({ * intensity: 15, * duration: 400, * direction: 'x' * }); * * // Vertical shake for ground impact * map.shakeMap({ * intensity: 20, * duration: 600, * direction: 'y' * }); * ``` */ shakeMap(options?: { intensity?: number; duration?: number; frequency?: number; direction?: 'x' | 'y' | 'both'; }): void; /** * Clear all server resources and reset state * * This method should be called to clean up all server-side resources when * shutting down or resetting the map. It stops the input processing loop * and ensures that all subscriptions are properly cleaned up. * * ## Design * * This method is used primarily in testing environments to ensure clean * state between tests. It stops the tick subscription to prevent memory leaks. * * @example * ```ts * // In test cleanup * afterEach(() => { * map.clear(); * }); * ``` */ clear(): void; } export interface RpgMap { $send(connection: RpgRoomConnection, packet: unknown): void; $broadcast(packet: unknown, without?: string[]): void; $applySync(): void; $sessionTransfer(connection: RpgRoomConnection, roomId: string): Promise; } export {};