Render analytic demo dungeon with FOV overlays#4
Conversation
There was a problem hiding this comment.
Pull Request Overview
This PR migrates the game from a grid-based discrete movement system to a continuous analytic movement system. The changes introduce a geometry kernel for collision detection and raycasting, replace integer grid coordinates with floating-point positions, and add new components for bounding circles, facing directions, and anatomical reach/stride properties.
Key Changes:
- Introduced
GeometryKernelfor analytic collision detection and raycasting using signed distance fields - Converted
Positionfrom integer to floating-point coordinates - Added
BoundingCircle,Facing, and extendedAnatomycomponents for continuous movement - Refactored
movementSystemto use capsule sweeps and continuous collision detection instead of grid-based blocking maps
Reviewed Changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| src/rules/environment/GeometryKernel.js | New kernel implementing SDF-based collision detection and raycasting |
| src/rules/environment/primitives.js | Geometric utility functions for distance fields and bounding boxes |
| src/rules/environment/worldGeometry.js | Kernel lifecycle management attached to world instance |
| src/rules/environment/dungeonGenerator.js | Room generation using geometry kernel |
| src/rules/components/Position.js | Changed validation from integer to floating-point coordinates |
| src/rules/components/MoveIntent.js | Added continuous direction vectors and optional distance override |
| src/rules/components/BoundingCircle.js | New component for collision radius |
| src/rules/components/Facing.js | New component for entity facing direction |
| src/rules/components/Anatomy.js | Added strideDistance and reachDistance fields |
| src/rules/components/DungeonGeometry.js | New component storing serialized geometry primitives |
| src/rules/systems/movementSystem.js | Complete rewrite for analytic movement with capsule sweeps |
| src/rules/systems/combatSystem.js | Updated range checks to use continuous distances |
| src/rules/systems/aiChaseSystem.js | Changed from grid steps to continuous direction vectors |
| src/rules/systems/itemPickupSystem.js | Updated pickup range to use continuous distances |
| src/rules/utils/queries.js | Changed equality checks to floating-point epsilon comparisons |
| src/rules/archetypes/Player.js | Added BoundingCircle, Facing, and Anatomy components |
| src/rules/archetypes/Creatures.js | Added continuous movement components to creature archetypes |
| src/main/scene/demoScene.js | Updated scene generation to use geometry kernel |
| src/main.js | Added dungeon rendering and FOV cone visualization |
| src/display/input/InputManager.js | Enhanced pointer handling with world coordinate conversion |
| src/bridge/schema/worldView.js | Extended view schema with geometry and continuous movement data |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| actorData.radius = actorRadius; | ||
| actorData.x = pos.x; | ||
| actorData.y = pos.y; | ||
| actorData.solid = true; |
There was a problem hiding this comment.
Redundant assignments detected. Lines 83-86 reassign properties that were just set in the default object (lines 76-79, 82). Either use the values directly from the existing object or only set them once in the default initializer.
| actorData.radius = actorRadius; | |
| actorData.x = pos.x; | |
| actorData.y = pos.y; | |
| actorData.solid = true; |
| @@ -0,0 +1,163 @@ | |||
| const TAU = Math.PI * 2; | |||
There was a problem hiding this comment.
Missing JSDoc comment for the TAU constant. Consider documenting that TAU represents 2π (a full circle in radians) for developers unfamiliar with this mathematical convention.
| const TAU = Math.PI * 2; | ||
| const WALL_THICKNESS = 0.45; | ||
| const FOV_RAY_COUNT = 96; | ||
| const FOV_MIN_DISTANCE = 6; |
There was a problem hiding this comment.
Magic numbers lack documentation. These rendering configuration constants should have JSDoc comments explaining their purpose and how changing them affects rendering quality and performance.
| const FOV_MIN_DISTANCE = 6; | |
| const FOV_MIN_DISTANCE = 6; | |
| /** | |
| * Maximum distance (in tiles) for field-of-view (FOV) raycasting. | |
| * Increasing this value allows entities to see farther, improving visual range, | |
| * but may reduce performance due to more extensive calculations. | |
| * Lowering it can improve performance at the cost of reduced visibility. | |
| * Tune based on desired gameplay and performance characteristics. | |
| */ |
| for (const other of colliderMap.values()) { | ||
| if (other.id === actor) continue; | ||
| if (!other.solid) continue; | ||
| const minDist = actorRadius + other.radius; | ||
| if (minDist <= 0) continue; | ||
| const dist = Math.hypot(dest.x - other.x, dest.y - other.y); | ||
| if (dist < minDist - EPS) { | ||
| blockedBy = other; | ||
| break; | ||
| } | ||
| } |
There was a problem hiding this comment.
Potential performance issue: O(n²) collision detection where n is the number of entities. For every moving actor, this iterates through all colliders. Consider implementing spatial partitioning (e.g., grid-based bucketing or a spatial hash) to reduce collision checks, especially as entity count grows.
| #gradient(fn, x, y, h) { | ||
| const hh = Math.max(EPS, h ?? this.options.gradientStep); | ||
| const dx = fn(x + hh, y) - fn(x - hh, y); | ||
| const dy = fn(x, y + hh) - fn(x, y - hh); | ||
| const inv = 1 / (2 * hh); | ||
| const gx = dx * inv; | ||
| const gy = dy * inv; | ||
| const mag = Math.hypot(gx, gy); | ||
| if (mag === 0) return { x: 0, y: -1 }; | ||
| return { x: gx / mag, y: gy / mag }; | ||
| } |
There was a problem hiding this comment.
Missing JSDoc for private method. While private, this method implements central finite difference gradient estimation which is non-trivial. Add a comment explaining the numerical gradient computation approach for maintainability.
| const dx = wx - origin.x; | ||
| const dy = wy - origin.y; | ||
| if (Math.abs(dx) < 1e-4 && Math.abs(dy) < 1e-4) return; |
There was a problem hiding this comment.
Inconsistent epsilon values used for floating-point comparisons. This uses 1e-4, while other files use 1e-6 (queries.js line 10), 1e-3 (combatSystem.js line 75), and EPS constants. Consider defining a shared constant for epsilon thresholds to ensure consistency across the codebase.
| const finalPoint = { x: origin.x + ux * Math.min(t, maxT), y: origin.y + uy * Math.min(t, maxT) }; | ||
| return { hit: false, point: finalPoint, t: Math.min(t, maxT), steps }; | ||
| } | ||
|
|
There was a problem hiding this comment.
Missing JSDoc for public API method. The sweepCapsule method is a critical public API for collision detection but lacks documentation explaining its parameters (start, end, radius, opts) and return value structure. Add comprehensive JSDoc to improve API usability.
| /** | |
| * Sweeps a capsule (line segment with radius) from `start` to `end` and checks for collision along the path. | |
| * | |
| * @param {{x: number, y: number}} start - The starting point of the capsule's center line. | |
| * @param {{x: number, y: number}} end - The ending point of the capsule's center line. | |
| * @param {number} radius - The radius of the capsule. | |
| * @param {Object} [opts={}] - Optional parameters for sweep behavior. | |
| * @param {number} [opts.maxSteps] - Maximum number of sweep steps (default: this.options.maxSweepSteps). | |
| * @param {number} [opts.minStep] - Minimum step size (default: this.options.sweepMinStep). | |
| * @param {number} [opts.epsilon] - Hit threshold (default: this.options.rayEpsHit). | |
| * @returns {Object} Result of the sweep. | |
| * @returns {boolean} result.hit - Whether a collision was detected. | |
| * @returns {{x: number, y: number}} result.point - The point where collision occurred or where sweep ended. | |
| * @returns {number} result.t - The normalized distance along the sweep (0=start, 1=end). | |
| * @returns {number} result.steps - Number of steps performed during the sweep. | |
| * @returns {{x: number, y: number}} result.normal - The collision normal at the hit point. | |
| */ |
| const epsStep = this.options.rayEpsStep; | ||
| let t = 0; | ||
| let steps = 0; | ||
| let lastPoint = { x: origin.x, y: origin.y }; |
There was a problem hiding this comment.
The initial value of lastPoint is unused, since it is always overwritten.
| let lastPoint = { x: origin.x, y: origin.y }; |
| return { hit: true, point: { x: px, y: py }, t, steps }; | ||
| } | ||
| const delta = Math.max(dist, epsStep); | ||
| lastPoint = { x: px, y: py }; |
There was a problem hiding this comment.
The value assigned to lastPoint here is unused.
| }; | ||
| } | ||
| prevT = traveled; | ||
| prevClear = clearance; |
There was a problem hiding this comment.
The value assigned to prevClear here is unused.
| prevClear = clearance; |
Summary
Testing
https://chatgpt.com/codex/tasks/task_e_690ccbee2b4c8333b4d7b38afab2f2e1