import type { BallisticSweep } from "../physics/ballisticSweep.js"; import type { EntityPosition } from "../scene/entityStore.js"; import type { EntityColliderSet } from "../scene/colliders.js"; import { type SceneRaycastApi } from "../scene/sceneRaycast.js"; import type { Aim } from "../scene/spatial.js"; import type { CombatSpatialDeps, EffectResult, EffectSystem, EffectVia } from "./effects.js"; import { type ShotOriginPolicy } from "./shotOrigin.js"; export interface ProjectileShotInput { from: string; via: EffectVia; aim: Aim; effect: string; /** Defaults to `{ kind: "converge" }` — explicit rays pass through; angular aim converges from the muzzle to the sightline. */ originPolicy?: ShotOriginPolicy; /** Opt into simulation-owned travel. Values are captured at launch; time is in game-seconds. */ travel?: ProjectileTravelConfig; } /** Captured launch settings for authoritative live travel. */ export interface ProjectileTravelConfig { speed: number; lifetime: number; /** Forwarded to the injected sweep; the default scene query sweeps a centerline. */ radius?: number; gravity?: EntityPosition; /** Game-owned wind coupling in 1/seconds, from 0 to 1e12; omission leaves wind response off. */ windResponse?: number; /** Target bits read by the injected environmental force sampler. */ forceMask?: number; /** Cap injected acceleration in m/s², from 0 to 1e12, independently of gravity. */ maxAcceleration?: number; } /** Nearest contact returned by a live segment sweep. */ export interface ProjectileTravelImpact { /** Earliest contact fraction along this step, including moving targets. */ fraction: number; at: EntityPosition; target?: ProjectileHit; } /** Authoritative clock, collision and environmental acceleration providers. */ export interface ProjectileTravelDeps { /** Authoritative game-seconds; no wall-clock fallback is used for live travel. */ now(): number; sweep?(from: EntityPosition, to: EntityPosition, step: { shotId: string; input: ProjectileShotInput; radius: number; fromTime: number; toTime: number; }): ProjectileTravelImpact | null; acceleration?(position: EntityPosition, velocity: EntityPosition, time: number, input: ProjectileShotInput): EntityPosition; maxActive?: number; maxRetained?: number; } /** Detached pose of one active projectile pellet for presentation. */ export interface LiveProjectile { shotId: string; pellet: number; from: string; effect: string; via: EffectVia; position: EntityPosition; velocity: EntityPosition; age: number; radius: number; } /** Serializable motion state of one projectile pellet. */ export interface ProjectileFlightState { position: EntityPosition; velocity: EntityPosition; age: number; distance: number; active: boolean; } /** Serializable captured launch and settlement state. */ export interface ProjectileShotState { shotId: string; input: ProjectileShotInput; coneSamples: readonly (readonly [number, number])[]; pellets: number; firedAt: number; settled: boolean; origin?: EntityPosition; flights?: ProjectileFlightState[]; hits: EffectResult[]; at?: EntityPosition; } /** Detached projectile owner state for replay and restore. */ export interface ProjectileSystemState { shotCounter: number; shots: ProjectileShotState[]; } export interface EntityRaycastHit { kind: "entity"; instanceId: string; distance: number; at: EntityPosition; colliderName?: string; damageEligible?: boolean; blocks?: boolean; } export interface ObjectRaycastHit { kind: "object"; instanceId: string; catalogId: string; distance: number; at: EntityPosition; colliderName?: string; damageEligible?: boolean; blocks?: boolean; } export type RaycastHit = EntityRaycastHit | ObjectRaycastHit; export type Raycast = (from: string, aim: Aim, range: number, originPolicy?: ShotOriginPolicy) => RaycastHit[]; /** Exactly-once settlement with launch identity and actual applied effects. */ export interface ProjectileSettleReport { shotId: string; /** Actual applied effects; empty for misses and cover impacts. */ hits: readonly EffectResult[]; from: string; origin: EntityPosition; at: EntityPosition; effect: string; hit: boolean; /** * True for lobbed/exploding shots (grenades, launchers, rockets) whose real path is an arc, * false for direct-fire shots (bullets, bolts) that travel muzzle→impact in a straight line. * Presentation uses it to keep straight-line tracers off arced projectiles. */ ballistic: boolean; } export interface ProjectileObjectsDeps { list(): readonly { instanceId: string; catalogId: string; position: readonly [number, number, number]; rotationY?: number; }[]; inBox?(min: EntityPosition, max: EntityPosition): readonly { instanceId: string; catalogId: string; position: readonly [number, number, number]; rotationY?: number; }[]; halfExtents?(catalogId: string): [number, number, number] | null; collidersOf?(instanceId: string): EntityColliderSet | null | undefined; } export interface ProjectileSystemDeps { effects: EffectSystem; spatial: CombatSpatialDeps; getStat(itemId: string, stat: string): number | null; raycast?: Raycast; sceneRaycast?: SceneRaycastApi; objects?: ProjectileObjectsDeps; entityCollidersOf?(instanceId: string): EntityColliderSet | null | undefined; rotationYOf?(instanceId: string): number | undefined; sweepBallistic?: BallisticSweep; defaultOriginPolicy?: ShotOriginPolicy; now?: () => number; /** Cone samples use two draws per nonzero-spread pellet at fire time; defaults to Math.random. */ rng?: () => number; /** Per-shot ray limit, clamped to [1, 256]; defaults to 64. */ maxPellets?: number; onSettle?(report: ProjectileSettleReport): void; travel?: ProjectileTravelDeps; } export type ProjectileHit = { kind: "entity"; instanceId: string; distance: number; colliderName?: string; damageEligible?: boolean; } | { kind: "object"; instanceId: string; catalogId: string; distance: number; colliderName?: string; damageEligible?: boolean; }; export interface ProjectilePrediction { hits: ProjectileHit[]; blocked?: boolean; origin?: EntityPosition; direction?: EntityPosition; firstImpact?: ProjectileHit | null; } export type SettleResult = { status: "settled"; shotId: string; at: [number, number, number]; hits: EffectResult[]; origin?: [number, number, number]; } | { status: "rejected"; shotId: string; reason: string; }; export interface ProjectileSystem { /** Predict the center ray without consuming random samples or applying effects. */ willHitProjectile(input: ProjectileShotInput): ProjectilePrediction; /** Capture aim, origin-policy vectors and cone samples independently of subsequent caller mutation. */ fireProjectile(input: ProjectileShotInput): string; settleProjectile(shotId: string): SettleResult; /** Advance live shots once per authoritative simulation step; settled shots emit no further effects. */ advanceProjectiles(dt: number, time: number): SettleResult[]; activeProjectiles(): LiveProjectile[]; snapshot(): ProjectileSystemState; restore(state: ProjectileSystemState): void; } /** * Spawn and advance projectiles each frame, resolving travel, lifetime, and hits. * * @capability projectiles spawn and advance projectiles with travel and hit resolution */ export declare function createProjectileSystem(deps: ProjectileSystemDeps): ProjectileSystem;