import type { EffectResult } from "../combat/effects.js"; import type { DeathReason } from "../combat/deathReason.js"; import type { TelegraphShape } from "../combat/telegraph.js"; import type { CameraShake } from "../combat/hitReaction.js"; import type { FeedbackCue } from "../audio/feedbackCues.js"; import type { CombatVfxInstanceEvent } from "./vfxInstance.js"; export type { DeathReason }; export interface EntityDiedEvent { instanceId: string; catalogId: string; userId?: string; displayName?: string; /** The cause (`reason.kind`) and, through `source`, what dealt it. */ reason: DeathReason; /** Instance id of the killer, hazard or effect that caused it, when the caller knew one. */ source?: string; position: [number, number, number]; serverId?: string; } /** * One application of damage to one target: what hit it, for how much, and where its health stands. * `target` is always the thing that took the blow, so a hero and a keep hit in the same frame raise * two separate events and a HUD reads only the one whose `target` it owns. */ export interface DamageAppliedEvent { /** The registered target that took the damage. */ target: string; /** Who or what dealt it: an attacker instance id, a hazard, or the status that ticked. */ source?: string; /** Damage type resolved through the target's table (`"kinetic"`, `"fire"`). On the effect path (`ctx.scene.entity.effect`) there is no table, so it is the effect id that landed. */ type: string; /** Damage dealt after the zone, type and armor steps. */ amount: number; /** How much of the zone-adjusted incoming amount resistance and armor stopped. */ absorbed: number; /** Health left after this application, or `null` when the runtime was given no health reader. */ remaining: number | null; /** True only when a health reader reported the pool reaching zero or below. */ lethal: boolean; /** The status that ticked, when the damage came from a carried status rather than a direct hit. */ status?: string; } /** Request that an entity's rig play a one-shot animation clip bound to `event` in its `animation.oneShots` (e.g. an "attack" swing); the shell resolves the clip and plays it once over the locomotion state. With an `animation.graph`, `event` arms the trigger of that name. */ export interface EntityAnimationEvent { instanceId: string; event: string; } /** A named clip moment (`AnimGraph.events`) the rig just crossed — foot plant, hit frame, reload point. Audio and combat listen by `name`. */ export interface AnimationClipEvent { instanceId: string; name: string; clip: string; } /** * One granted reward: an inventory `item` id or a `currency` id, plus how many. Named because every * consumer of `loot.granted` needs it — a toast renderer, a loot log, a session summary — and each * one that re-declares the shape inline is a cast the compiler cannot check. */ export interface LootDrop { item?: string; currency?: string; count: number; } export interface LootGrantedEvent { userId: string; drops: LootDrop[]; source?: string; } /** * Read the drops out of a `loot.granted` payload that arrived as `unknown` — a `FeedEntry.data`, a * replicated event, a persisted log row. Returns `[]` for anything that is not shaped like a loot * grant, so a renderer never has to guard the payload itself: `entry.data as { drops?: ... }` is the * cast four separate call sites were writing, and the one that omitted the `| undefined` would have * thrown on a payload-less entry. */ export declare function lootDropsOf(data: unknown): readonly LootDrop[]; export interface InventoryAddedEvent { userId: string; item: string; count: number; source?: string; } export interface QuestAcceptedEvent { userId: string; questId: string; } export interface QuestUpdatedEvent { userId: string; questId: string; objectiveId?: string; progress?: number; } export interface QuestCompletedEvent { userId: string; questId: string; } export interface SocialFriendAddedEvent { userId: string; friendUserId: string; } export interface SocialPartyJoinedEvent { userId: string; partyId: string; } export interface SocialPartyLeftEvent { userId: string; partyId: string; } export interface SocialWorldInvitedEvent { inviteId: string; fromUserId: string; toUserId: string; serverId: string; joinCode?: string; } export interface SocialWorldAcceptedEvent { inviteId: string; userId: string; fromUserId: string; serverId: string; joinCode?: string; } export interface ChatMessageEvent { id: string; channelId: string; fromUserId: string; body: string; at: number; recipients?: readonly string[]; } export interface StatLevelUpEvent { userId: string; stat: string; level: number; } export interface EntityFloatTextEvent { instanceId?: string; position: [number, number, number]; text: string; kind: string; amount?: number; hitType?: string; element?: string; crit?: boolean; scale?: number; } /** The visual archetype of a spell/ability effect burst: a traveling bolt, a connecting beam, an expanding ground nova, a soft aura glow, or a scattering impact spark. */ export type VfxKind = "projectile" | "beam" | "nova" | "glow" | "spark"; /** A transient sprite-particle effect the shell renders once and expires — one burst of `kind`, tinted `color`, anchored at `from` (and `to` for travel/beam effects). */ export interface CombatVfxEvent { id: number; kind: VfxKind; color: number; from: [number, number, number]; to?: [number, number, number]; radius?: number; durationMs: number; } /** * An endpoint of a retained VFX instance: either an entity instance id (a renderer resolves and follows its live * pose each frame) or a fixed `[x, y, z]` world point. Kept serializable so the effect replicates as plain data. */ export type VfxRef = string | readonly [number, number, number]; /** * The archetype of a retained (long-lived, updatable) VFX effect — an open string, not a closed union, so a * renderer registers new kinds (beam, tether, zone, target line, looping emitter) without a central branch. * `"beam"` is the first shipped retained renderer. */ export type RetainedVfxKind = string; export interface CombatTelegraphEvent { id: number; shape: TelegraphShape; position: [number, number, number]; dir?: number; windupMs: number; kind: string; } export interface CombatTelegraphCancelledEvent { /** The `combat.telegraph` event id whose decal should disappear early. */ id: number; } export interface CombatHitReactionEvent { instanceId?: string; position: [number, number, number]; hitstopMs: number; shake?: CameraShake; /** Trauma (0..1) for a trauma² shake channel — set when the reaction resolved from `trauma` instead of `shake`. */ trauma?: number; } /** Exactly-once projectile result for shot-specific game policy and presentation. */ export interface ProjectileSettledEvent { shotId: string; /** Actual applied effects; empty for misses and cover impacts. */ hits: readonly EffectResult[]; from: string; origin: [number, number, number]; at: [number, number, number]; effect: string; hit: boolean; /** * True for arced/exploding shots (grenades, launchers), false for direct-fire (bullets, bolts). * Straight muzzle→impact tracers only make sense for direct-fire shots, so renderers skip the arced ones. */ ballistic: boolean; } export interface WorldItemDroppedEvent { instanceId: string; itemId: string; rarity: string; count: number; position: [number, number, number]; source?: string; } export interface WorldItemPickedUpEvent { instanceId: string; userId: string; itemId: string; rarity: string; count: number; } export interface CosmeticsChangedEvent { userId: string; slots: Record; } export interface EmotePlayedEvent { from: string; emoteId: string; at: readonly [number, number, number]; recipients: readonly string[]; } export interface PossessionSwappedEvent { userId: string; entityId: string; previousEntityId: string; } export interface FormChangedEvent { instanceId: string; formId: string | null; } export interface AudioPlayEvent { sound: string; at?: readonly [number, number, number]; } /** Request that the shell's audio engine resume its (browser-gesture-suspended) context; carries no payload. */ export type AudioResumeEvent = Record; /** * A gameplay moment the game announces itself, for the moments no engine event covers (a wave * starting, a scripted beat). The shell reacts with the cue's sound, rumble, camera kick and caption. */ export interface FeedbackCueEvent { cue: FeedbackCue; /** Caption text that replaces the cue's default label ("WAVE 3"). */ caption?: string; } /** Crossfade the procedural soundtrack to `theme` (null fades out), optionally transposing the incoming theme by `transpose` semitones. */ export interface AudioMusicEvent { theme: string | null; transpose?: number; } /** * Start (or idempotently keep) the retained, id-keyed audio loop `id` from catalog `sound`, optionally * anchored at world `at`. Restarting with the same `sound` does not restart the source (no click); a * different `sound` replaces it. Drives RPM-pitched engine loops and slip-scaled tire squeal (#1051). */ export interface AudioLoopStartEvent { id: string; sound: string; at?: readonly [number, number, number]; } /** * Live-update the retained loop `id`: `rate` re-pitches it (1 = authored, the shell clamps to 0.25–4), * `gain` rescales its volume (0–1), and `at` repositions its emitter. Emitted every tick to track a * live signal (RPM, tire slip); the shell smooths rate/gain and ignores an unknown `id` (#1051). */ export interface AudioLoopSetEvent { id: string; rate?: number; gain?: number; at?: readonly [number, number, number]; /** Emitter velocity (world units/s) for doppler on sounds that declare `doppler`. */ velocity?: readonly [number, number, number]; /** Lowpass cutoff in Hz. */ lowpass?: number; /** Highpass cutoff in Hz. */ highpass?: number; } /** * A command run through `ctx.game.commands` (or a bound host) was refused. `reason` is the machine code; * `message` is the player-facing text the command's `validate` returned, when it gave one. */ export interface CommandRefusedEvent { command: string; reason: string; message?: string; /** `runAs` actor, or null for a plain `run`. */ actor: string | null; } /** Stop and dispose the retained loop `id`; an unknown `id` is ignored (#1051). */ export interface AudioLoopStopEvent { id: string; } export interface GameEventMap { "entity.died": EntityDiedEvent; "combat.damage": DamageAppliedEvent; "entity.floatText": EntityFloatTextEvent; "combat.telegraph": CombatTelegraphEvent; "combat.vfx": CombatVfxEvent; "combat.vfxInstance": CombatVfxInstanceEvent; "combat.telegraphCancelled": CombatTelegraphCancelledEvent; "combat.hitReaction": CombatHitReactionEvent; "loot.granted": LootGrantedEvent; "inventory.added": InventoryAddedEvent; "quest.accepted": QuestAcceptedEvent; "quest.updated": QuestUpdatedEvent; "quest.completed": QuestCompletedEvent; "social.friend.added": SocialFriendAddedEvent; "social.party.joined": SocialPartyJoinedEvent; "social.party.left": SocialPartyLeftEvent; "social.world.invited": SocialWorldInvitedEvent; "social.world.accepted": SocialWorldAcceptedEvent; "chat.message": ChatMessageEvent; "stat.levelUp": StatLevelUpEvent; "projectile.settled": ProjectileSettledEvent; "worldItem.dropped": WorldItemDroppedEvent; "worldItem.picked_up": WorldItemPickedUpEvent; "cosmetics.changed": CosmeticsChangedEvent; "emote.played": EmotePlayedEvent; "possession.swapped": PossessionSwappedEvent; "form.changed": FormChangedEvent; "audio.play": AudioPlayEvent; "audio.music": AudioMusicEvent; "audio.resume": AudioResumeEvent; "audio.loopStart": AudioLoopStartEvent; "audio.loopSet": AudioLoopSetEvent; "audio.loopStop": AudioLoopStopEvent; "entity.animation": EntityAnimationEvent; "animation.event": AnimationClipEvent; "feedback.cue": FeedbackCueEvent; "command.refused": CommandRefusedEvent; } export type GameEventHandler = (payload: TPayload) => void; export interface GameEvents { on(name: TName, handler: GameEventHandler): () => void; subscribe(name: TName, handler: GameEventHandler): () => void; emit(name: TName, payload: TMap[TName]): void; } /** * A typed publish/subscribe bus for gameplay events that systems and HUDs subscribe to. * * @capability event-bus typed publish/subscribe bus for gameplay events */ export declare function createGameEvents(): GameEvents;