Fix Recovery Phase 4: Single bundle script, correct Traefik network
- build.sh: Remove old bundle tag before injecting hashed version - index.html: Remove duplicate script tag from template - docker-compose.yml: Fix network name (hermes-net, not litellm_hermes-net) - Deployment verified: HTTPS 200 via Cloudflare + Traefik
This commit is contained in:
20
.hermes/kanban-task1.md
Normal file
20
.hermes/kanban-task1.md
Normal file
@@ -0,0 +1,20 @@
|
||||
Fix Issue #1: Tank can't turn around
|
||||
|
||||
**Problem:** Tank hull has no rotation logic — it only moves x/y but always faces same direction. Feels like it's sliding, not driving.
|
||||
|
||||
**Files to Modify:**
|
||||
- src/game/entities/Tank.js
|
||||
- src/constants.js (add TANK_ROTATION_SPEED)
|
||||
|
||||
**Implementation:**
|
||||
1. Add TANK_ROTATION_SPEED constant (~180 deg/sec for deliberate 25-ton feel)
|
||||
2. In Tank.update(), compute movement angle from velocity:
|
||||
- Use Phaser.Math.Angle.Between(0, 0, this.body.velocity.x, this.body.velocity.y)
|
||||
- Only rotate if velocity > 5 px/sec (avoid jitter when stopped)
|
||||
3. Ensure sprite anchor is centered for proper rotation
|
||||
|
||||
**Verification:**
|
||||
- [ ] Tank rotates to face movement direction
|
||||
- [ ] Rotation feels heavy/deliberate (not instant snap)
|
||||
- [ ] No visual glitches when rotating
|
||||
- [ ] Tests pass (add rotation test if needed)
|
||||
24
.hermes/kanban-task2.md
Normal file
24
.hermes/kanban-task2.md
Normal file
@@ -0,0 +1,24 @@
|
||||
Fix Issue #2: Shell velocity too slow
|
||||
|
||||
**Problem:** Shell velocities (550-930 m/s in shells.js) are not properly scaled to game units. Enemies like Type 62 (140 px/sec) and helicopter (200 px/sec) can outrun or match projectile speed.
|
||||
|
||||
**Files to Modify:**
|
||||
- src/data/shells.js
|
||||
- src/game/entities/Projectile.js (verify velocity usage)
|
||||
|
||||
**Implementation:**
|
||||
1. Increase shell velocities to 800-1200 px/sec (5-7x faster than fastest enemy)
|
||||
2. Ensure velocity is in px/sec, not m/s (game uses pixel coordinates)
|
||||
3. Verify projectile lifetime is sufficient to cross screen at new speeds
|
||||
|
||||
**Target Values:**
|
||||
- apcbc: 900 px/sec (baseline)
|
||||
- apcr: 1200 px/sec (high velocity)
|
||||
- he: 700 px/sec (slower but splash)
|
||||
- heat: 800 px/sec (shaped charge)
|
||||
|
||||
**Verification:**
|
||||
- [ ] Shells visibly faster than enemies
|
||||
- [ ] Projectiles cross screen in <1 second
|
||||
- [ ] No clipping/tunneling issues at high speeds
|
||||
- [ ] Tests pass
|
||||
19
.hermes/kanban-task3.md
Normal file
19
.hermes/kanban-task3.md
Normal file
@@ -0,0 +1,19 @@
|
||||
Fix Issue #3: Add obstacles to map
|
||||
|
||||
**Problem:** Map is completely empty — no cover, no terrain features.
|
||||
|
||||
**Files to Modify:**
|
||||
- src/game/scenes/MainGame.js (add obstacle group in create())
|
||||
- Potentially new: src/game/entities/Obstacle.js
|
||||
|
||||
**Implementation:**
|
||||
1. Create simple obstacle class (rectangles for now, art later)
|
||||
2. Add 5-10 obstacles scattered across map
|
||||
3. Obstacles block line of sight and provide cover
|
||||
4. Add collision: projectiles hit obstacles, tanks can't drive through
|
||||
|
||||
**Verification:**
|
||||
- [ ] Obstacles visible on map
|
||||
- [ ] Tank collides with obstacles
|
||||
- [ ] Projectiles destroyed on obstacle hit
|
||||
- [ ] Provides meaningful tactical cover
|
||||
37
.hermes/kanban-tasks-phase1.json
Normal file
37
.hermes/kanban-tasks-phase1.json
Normal file
@@ -0,0 +1,37 @@
|
||||
[
|
||||
{
|
||||
"id": "phase1-task1",
|
||||
"title": "Patch Enemy.js constructor — create sprite for each enemy",
|
||||
"milestone": "Recovery Phase 1: Enemy Sprites Visible",
|
||||
"description": "Add sprite creation in Enemy.js constructor so enemies render as colored rectangles.\n\n**Files to Modify:**\n- src/game/entities/Enemy.js\n\n**Implementation:**\nIn constructor, after line 50 (this.spriteColor = ...), add:\n```js\n// Create visible sprite for this enemy\nthis.sprite = scene.add.rectangle(x, y, 24, 36, this.spriteColor);\nthis.sprite.setDepth(50);\n```\n\n**Verification:**\n- [ ] Code compiles without errors\n- [ ] Browser shows colored rectangles spawning\n- [ ] No console errors\n\n**Dependencies:**\n- Blocks: phase1-task2, phase1-task3\n- Blocked by: none",
|
||||
"status": "pending"
|
||||
},
|
||||
{
|
||||
"id": "phase1-task2",
|
||||
"title": "Patch Enemy.js update() — sync sprite position to logic x/y",
|
||||
"milestone": "Recovery Phase 1: Enemy Sprites Visible",
|
||||
"description": "Sync sprite position to Enemy logic coordinates each frame.\n\n**Files to Modify:**\n- src/game/entities/Enemy.js\n\n**Implementation:**\nAt the END of update() method (before the closing brace), add:\n```js\n// Sync sprite position to logic coordinates\nif (this.sprite) {\n this.sprite.x = this.x;\n this.sprite.y = this.y;\n // Rotate sprite to face player\n const angle = Math.atan2(playerY - this.y, playerX - this.x);\n this.sprite.rotation = angle + Math.PI / 2;\n}\n```\n\n**Verification:**\n- [ ] Rectangles move with enemy logic positions\n- [ ] Rectangles face the player\n- [ ] 60fps stable\n\n**Dependencies:**\n- Blocks: phase1-task4\n- Blocked by: phase1-task1",
|
||||
"status": "pending"
|
||||
},
|
||||
{
|
||||
"id": "phase1-task3",
|
||||
"title": "Patch Enemy.js takeDamage() — destroy sprite on death",
|
||||
"milestone": "Recovery Phase 1: Enemy Sprites Visible",
|
||||
"description": "Clean up sprite when enemy dies to prevent memory leaks.\n\n**Files to Modify:**\n- src/game/entities/Enemy.js\n\n**Implementation:**\nIn takeDamage() method, after line 196 (this.active = false;), add:\n```js\n// Destroy sprite on death\nif (this.sprite && this.sprite.destroy) {\n this.sprite.destroy();\n}\n```\n\n**Verification:**\n- [ ] Dead enemies disappear (no orphaned sprites)\n- [ ] No memory leaks after killing 50+ enemies\n- [ ] No console errors\n\n**Dependencies:**\n- Blocks: none\n- Blocked by: phase1-task1",
|
||||
"status": "pending"
|
||||
},
|
||||
{
|
||||
"id": "phase1-task4",
|
||||
"title": "Verify enemy spawning — check spawnZone() integration",
|
||||
"milestone": "Recovery Phase 1: Enemy Sprites Visible",
|
||||
"description": "Verify spawnZone() is correctly calling Enemy constructor and adding to MainGame.enemies array.\n\n**Files to Inspect:**\n- src/game/entities/Enemy.js (spawnZone function)\n- src/game/scenes/MainGame.js (enemy spawning in update())\n\n**Verification Steps:**\n1. Check spawnZone() creates Enemy instances correctly\n2. Check MainGame.update() calls spawnZone() every 3 seconds\n3. Check MainGame.enemies array is populated\n4. Add debug log in spawnZone: console.log(`[IR:spawnZone] spawned ${wave.length} enemies`)\n\n**Dependencies:**\n- Blocks: phase1-task5\n- Blocked by: phase1-task2",
|
||||
"status": "pending"
|
||||
},
|
||||
{
|
||||
"id": "phase1-task5",
|
||||
"title": "Integration test — deploy + browser verification",
|
||||
"milestone": "Recovery Phase 1: Enemy Sprites Visible",
|
||||
"description": "Deploy all Phase 1 changes and verify in browser.\n\n**Verification Checklist:**\n- [ ] Build: npm run build\n- [ ] Build script: ./build.sh\n- [ ] Deploy: docker stop/rm iron-requiem, docker run with Traefik labels\n- [ ] Browser: https://iron-requiem.damascusfront.net\n- [ ] Verify: colored rectangles spawn every 3 seconds\n- [ ] Verify: rectangles move (Type 59 fronts, Type 62 flanks)\n- [ ] Screenshot captured\n- [ ] Console: no errors\n\n**Dependencies:**\n- Blocks: none (final task)\n- Blocked by: phase1-task2, phase1-task3, phase1-task4",
|
||||
"status": "pending"
|
||||
}
|
||||
]
|
||||
@@ -3,26 +3,26 @@
|
||||
* Used by tests that import Phaser-dependent modules (MainGame, Tank, etc.).
|
||||
*/
|
||||
class Scene {
|
||||
constructor(config) { this.scene = config; this.game = null; }
|
||||
constructor(config) {
|
||||
this.scene = config;
|
||||
this.game = { __saveManager: undefined };
|
||||
}
|
||||
}
|
||||
|
||||
Scene.prototype.add = {
|
||||
image() { return { setOrigin() { return this; } }; },
|
||||
rectangle() { return { setOrigin() { return this; }, rotation: 0, x: 0, y: 0 }; },
|
||||
rectangle(x, y) { return { setOrigin() { return this; }, rotation: 0, x, y }; },
|
||||
existing() { return this; },
|
||||
graphics() {
|
||||
return {
|
||||
setDepth() { return this; },
|
||||
clear() {},
|
||||
fillStyle() {},
|
||||
fillRect() {},
|
||||
setBlendMode() {},
|
||||
beginPath() {},
|
||||
moveTo() {},
|
||||
lineTo() {},
|
||||
closePath() {},
|
||||
fillPath() {},
|
||||
const g = {
|
||||
setDepth() { return g; }, clear() {}, fillStyle() {}, fillRect() {},
|
||||
setBlendMode() {}, beginPath() {}, moveTo() {}, lineTo() {},
|
||||
closePath() {}, fillPath() {}, strokePath() {}, lineStyle() {},
|
||||
lineBetween() {}, strokeCircle() {},
|
||||
};
|
||||
return g;
|
||||
},
|
||||
text(x, y, str) { return { setDepth() { return this; }, x, y, text: str }; },
|
||||
};
|
||||
|
||||
Scene.prototype.cameras = { main: { startFollow() {} } };
|
||||
@@ -39,31 +39,39 @@ Scene.prototype.input = {
|
||||
};
|
||||
},
|
||||
},
|
||||
on() {}, // pointer tracking
|
||||
on() {},
|
||||
};
|
||||
|
||||
Scene.prototype.physics = {
|
||||
add: {
|
||||
group() { return {}; },
|
||||
group(cfg) {
|
||||
const maxSize = cfg && cfg.maxSize ? cfg.maxSize : -1;
|
||||
const children = [];
|
||||
return {
|
||||
maxSize, children,
|
||||
getFirstDead() { return null; },
|
||||
create(x, y) {
|
||||
const s = { active: true, visible: true, x, y, body: { enable: true, velocity: { x: 0, y: 0 } } };
|
||||
children.push(s);
|
||||
return s;
|
||||
},
|
||||
killAndHide(sprite) { if (sprite) { sprite.active = false; sprite.visible = false; } },
|
||||
getLength() { return children.length; },
|
||||
getChildren() { return children; },
|
||||
clear() { children.length = 0; },
|
||||
};
|
||||
},
|
||||
existing() {},
|
||||
overlap() {},
|
||||
collider() {},
|
||||
},
|
||||
};
|
||||
Scene.prototype.scene = {
|
||||
scenes: [],
|
||||
start() {},
|
||||
};
|
||||
|
||||
Scene.prototype.scene = { scenes: [], start() {} };
|
||||
Scene.prototype.make = {
|
||||
graphics() {
|
||||
return {
|
||||
fillStyle() {},
|
||||
fillRect() {},
|
||||
generateTexture() {},
|
||||
destroy() {},
|
||||
};
|
||||
},
|
||||
};
|
||||
Scene.prototype.load = {
|
||||
image() {},
|
||||
graphics() { return { fillStyle() {}, fillRect() {}, generateTexture() {}, destroy() {} }; },
|
||||
};
|
||||
Scene.prototype.load = { image() {} };
|
||||
|
||||
const Phaser = {
|
||||
CANVAS: 1,
|
||||
|
||||
5
build.sh
5
build.sh
@@ -13,9 +13,10 @@ mv dist/bundle.js "dist/${BUNDLE_FILE}"
|
||||
|
||||
BUILD_TS=$(date +%s)
|
||||
|
||||
# Rewrite HTML: replace bundle.js?v=TIMESTAMP with content-hashed filename
|
||||
# Rewrite HTML: remove old bundle.js script tag, inject only content-hashed filename
|
||||
# and inject timestamp for diagnostic script
|
||||
sed -i "s|bundle\.js?v=BUILD_TIMESTAMP|${BUNDLE_FILE}|g" dist/index.html
|
||||
sed -i "/bundle\.js?v=BUILD_TIMESTAMP/d" dist/index.html
|
||||
sed -i "s|</head>|<script defer src=\"${BUNDLE_FILE}\"></script></head>|g" dist/index.html
|
||||
sed -i "s/BUILD_TIMESTAMP/${BUILD_TS}/g" dist/index.html
|
||||
|
||||
# Copy assets that webpack ignores
|
||||
|
||||
@@ -17,4 +17,4 @@ services:
|
||||
networks:
|
||||
hermes-net:
|
||||
external: true
|
||||
name: litellm_hermes-net
|
||||
name: hermes-net
|
||||
|
||||
528
docs/PHASE_I_II_IMPLEMENTATION_PLAN.md
Normal file
528
docs/PHASE_I_II_IMPLEMENTATION_PLAN.md
Normal file
@@ -0,0 +1,528 @@
|
||||
# Phase I+II Gap Implementation Plan — Iron Requiem
|
||||
|
||||
This plan addresses the five playability gaps identified in the Phase I+II assessment.
|
||||
Each gap includes files, integration points, tests, and dependencies.
|
||||
|
||||
Last updated: 2026-05-23
|
||||
|
||||
---
|
||||
|
||||
## Gap Summary & Execution Order
|
||||
|
||||
| # | Gap | Complexity | Depends On | Can Parallel With |
|
||||
|---|---|---|---|---|
|
||||
| 2 | Dynamic Vision Mask | S | — | 1, 3, 5 |
|
||||
| 5 | SFX/VFX (Audio + Flash) | L | — | 1, 2, 3 |
|
||||
| 1 | HUD Feedback | M | — | 2, 3, 5 |
|
||||
| 3 | Collision/Damage Loop | M | — | 1, 2, 5 |
|
||||
| 4 | Hatch Animation/Audio | M | 5 (audio system) | — |
|
||||
|
||||
### Recommended Execution Order
|
||||
|
||||
```
|
||||
Week 1: Gap 2 (S) ←────────→ Gap 5 Phase A (audio system foundation)
|
||||
Gap 1 (M) parallel start
|
||||
Gap 3 (M) parallel start
|
||||
|
||||
Week 2: Gap 5 Phase B (VFX muzzle/impact) + Gap 1 (HUD wiring)
|
||||
Gap 3 (collision overlap wiring)
|
||||
|
||||
Week 3: Gap 4 (hatch animation + audio cues, depends on Gap 5 audio)
|
||||
Integration polish — all five gaps verified together
|
||||
```
|
||||
|
||||
**Rationale:** Gap 2 is the quickest win (modify 2 files, no new files) and demonstrates
|
||||
immediate progress. Gap 5 (audio + VFX) is the longest pole — start its audio foundation
|
||||
early so Gap 4's audio cues aren't blocked. Gaps 1, 2, 3 are all independent and can run
|
||||
in parallel. Gap 4 must wait on Gap 5's audio manager.
|
||||
|
||||
---
|
||||
|
||||
## Gap 1: HUD Feedback
|
||||
|
||||
**Complexity: M (Medium)** — 2 new files, 1 modified
|
||||
|
||||
### Current State
|
||||
- `AmmoSystem` tracks inventory, fires `onWarning` callback, exposes `getInventory()` and `getTotalRemaining()`.
|
||||
- `MainGame` routes `onWarning` to console only (line 84-86).
|
||||
- `HeatSystem` and `CrewManager` don't exist yet (Slice 3 scope), but HUD needle slots must be ready.
|
||||
- No visual ammo count, shell type indicator, heat gauge, or morale gauge visible to player.
|
||||
|
||||
### Desired State
|
||||
Player sees diegetic needle gauges (fuel, heat, morale) and a shell indicator strip
|
||||
(current shell type highlighted, remaining count per type). All rendered as Phaser
|
||||
Graphics objects in a dedicated HUDScene overlay.
|
||||
|
||||
### Files to Create
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `src/game/ui/DiegeticHUD.js` | Pure logic class — computes needle angles from 0-100 values, formats ammo display text, holds gauge state. No Phaser dependency (testable standalone). |
|
||||
| `src/game/scenes/HUDScene.js` | Phaser scene overlay — reads `DiegeticHUD` state each frame, draws Graphics-based needles, shell icons, ammo count. Launched parallel to MainGame via `this.scene.launch()`. |
|
||||
|
||||
### Files to Modify
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `src/game/scenes/MainGame.js` | 1. Instantiate `DiegeticHUD` in `create()`. 2. Launch `HUDScene` in `create()`. 3. Update HUD data each tick in `update()`: pass `ammoSystem.getInventory()`, `ammoSystem.getTotalRemaining()`, `ammoSystem.getActiveShell()`. 4. Route `onWarning` to HUD instead of console. |
|
||||
|
||||
### Integration Points
|
||||
|
||||
```
|
||||
MainGame.update()
|
||||
→ diegeticHUD.updateAmmo(ammoSystem.getInventory(), ammoSystem.getActiveShell())
|
||||
→ diegeticHUD.updateHeat(0) // placeholder until HeatSystem exists
|
||||
→ diegeticHUD.updateMorale(50) // placeholder until CrewManager exists
|
||||
→ HUDScene reads diegeticHUD state each frame
|
||||
```
|
||||
|
||||
`HUDScene` runs as a parallel scene (overlay on top of MainGame), depth 200+.
|
||||
It reads `DiegeticHUD` state via `this.scene.get('MainGame').diegeticHUD`.
|
||||
|
||||
### Testing Strategy
|
||||
|
||||
**Unit tests (Jest):** `tests/ui/DiegeticHUD.test.js`
|
||||
- Pure logic: `computeNeedleAngle(0)` returns -45°, `computeNeedleAngle(100)` returns +45°
|
||||
- Ammo warning thresholds: `isLowAmmo(total)` returns true at 10, 5, 0
|
||||
- Shell display string: `formatAmmoDisplay({apcbc: 87, apcr: 6, he: 20, heat: 10})` returns 4 formatted lines
|
||||
- No Phaser dependency — test class directly
|
||||
|
||||
**Integration tests (Jest):** `tests/integration/hud-wiring.test.js`
|
||||
- Verify `MainGame.create()` instantiates `DiegeticHUD` and launches `HUDScene`
|
||||
- Verify `onWarning` callback routes to HUD (mock HUD, assert `showWarning()` called)
|
||||
- Verify HUD state updates correctly after `ammoSystem.fire()` reduces count
|
||||
|
||||
**E2E tests (Playwright):** `tests/e2e/hud.spec.js`
|
||||
- Load `https://iron-requiem.damascusfront.net`
|
||||
- Verify ammo text element exists and displays "APCBC: 87"
|
||||
- Press key 2 (APCR) → verify HUD shows APCR highlighted
|
||||
- Design review: game-designer opens browser, confirms needles are readable at 2x/3x scale
|
||||
|
||||
---
|
||||
|
||||
## Gap 2: Dynamic Vision Mask
|
||||
|
||||
**Complexity: S (Small)** — modify 2 files, extend existing tests
|
||||
|
||||
### Current State
|
||||
- `VisionMask` uses hardcoded `RANGE = 200` and `HALF_ARC = 200` constants (VisionMask.js lines 14-15).
|
||||
- `VisionMask.update(x, y, angle)` accepts position/rotation but NOT hatch state.
|
||||
- `CommanderHatch.getVisionMask()` returns `{arc, range}` based on buttoned/unbuttoned state.
|
||||
- `MainGame.update()` calls `this.commanderHatch.update(inputState)` first, then `this.visionMask.update(x, y, angle)` — but the hatch result is never passed to the mask.
|
||||
|
||||
### Desired State
|
||||
When player presses E to unbutton, the periscope hole expands from 180° to 270° arc
|
||||
and range extends from 200px to 350px. The transition is instant on toggle (animation
|
||||
belongs to Gap 4).
|
||||
|
||||
### Files to Modify
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `src/game/systems/VisionMask.js` | 1. Change `RANGE` and `HALF_ARC` from top-level `const` to instance properties (`this._range`, `this._halfArc`) initialized to buttoned defaults. 2. Add `setArc(arcDegrees, rangePx)` public method. 3. `_computeHoleCorners()` and `isVisible()` read `this._range` / `this._halfArc` instead of module constants. |
|
||||
| `src/game/scenes/MainGame.js` | In `update()`: after `commanderHatch.update()`, call `const vm = this.commanderHatch.getVisionMask(); this.visionMask.setArc(vm.arc, vm.range)` before `visionMask.draw()`. |
|
||||
|
||||
### Integration Points
|
||||
|
||||
```
|
||||
MainGame.update() each frame:
|
||||
1. commanderHatch.update(inputState) // toggles isUnbuttoned on E press
|
||||
2. const vm = commanderHatch.getVisionMask() // {arc: 270, range: 350} or {arc: 180, range: 200}
|
||||
3. visionMask.setArc(vm.arc, vm.range) // updates internal arc/range
|
||||
4. visionMask.update(x, y, angle) // updates position/rotation
|
||||
5. visionMask.draw() // renders with dynamic arc
|
||||
```
|
||||
|
||||
### Testing Strategy
|
||||
|
||||
**Unit tests (Jest):** Extend `tests/systems/VisionMask.test.js`
|
||||
- `setArc(270, 350)` then `isVisible()` — point at 250px range, 0° angle → visible (in range)
|
||||
- `setArc(180, 200)` then `isVisible()` — same point → not visible (outside range)
|
||||
- Edge case: `setArc(0, 0)` — everything outside mask
|
||||
- Verify `getPeriscopeEdgePoints()` extends to 350px range after `setArc(270, 350)`
|
||||
- Performance: `setArc()` + `draw()` within 16ms budget (existing performance test covers)
|
||||
|
||||
**Integration tests (Jest):** Extend `tests/integration/slice1-wiring.test.js`
|
||||
- Simulate E-key toggle → verify `visionMask` arc transitions from 180 to 270
|
||||
- Verify vision mask arc returns to 180 when toggled back
|
||||
|
||||
**E2E tests (Playwright):** `tests/e2e/vision-mask.spec.js`
|
||||
- Load game, press E, observe that more of the screen becomes visible (larger hole)
|
||||
- Game-designer verifies the expanded vision is visually distinct and functional
|
||||
|
||||
---
|
||||
|
||||
## Gap 3: Collision/Damage Loop
|
||||
|
||||
**Complexity: M (Medium)** — modify 1 file (MainGame.js), create 1 test file
|
||||
|
||||
### Current State
|
||||
- `PatternManager` spawns enemy projectiles into `this.enemyProjectileGroup` (Arcade physics group).
|
||||
- `Enemy` is a plain JS class with `takeDamage(amount)` and `hp` — no Arcade body.
|
||||
- `Projectile` is a plain JS class with `onHit(target)` that computes penetration/damage.
|
||||
- `CommanderHatch` has `isHitboxActive()`, `takeHit()`, `beginSniperShot()`, `completeSniperShot()` but nothing calls them.
|
||||
- `MainGame` creates `projectileGroup` and `enemyProjectileGroup` but never sets up overlap callbacks.
|
||||
- No player fire mechanism is wired — no mouse-click or spacebar handler.
|
||||
|
||||
### Desired State
|
||||
1. **Player fires shells:** Mouse click (left button) → `ammoSystem.fire()` → create `Projectile` → spawn a sprite in `projectileGroup` moving at shell velocity toward turret angle.
|
||||
2. **Player projectiles hit enemies:** Distance-based check (enemies are plain objects) in `update()`. On hit: `enemy.takeDamage(projectile.onHit(enemy).damage)`.
|
||||
3. **Enemy projectiles hit tank:** Physics overlap callback `this.physics.overlap(this.enemyProjectileGroup, this.tank, onEnemyProjectileHitTank)`. On hit: destroy projectile sprite, damage tank, check hatch hitbox.
|
||||
4. **Enemy projectiles hit player projectiles:** Optional — cancels both (shell interception). Nice-to-have for Phase I+II, not critical.
|
||||
|
||||
### Files to Modify
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `src/game/scenes/MainGame.js` | 1. Add `this.input.on('pointerdown', onFire)` handler in `create()`. 2. Implement `onFire()`: get active shell from `ammoSystem.fire()`, create `Projectile` object, spawn sprite in `projectileGroup` at tank position toward turret angle. 3. In `create()`: `this.physics.overlap(this.enemyProjectileGroup, this.tank, this._onEnemyProjHitTank, null, this)`. 4. In `update()`: iterate `projectileGroup` active sprites, check distance to each enemy (in `this.enemies`), resolve hits. 5. Track tank HP and destroy/restart on death. |
|
||||
|
||||
### New Methods on MainGame
|
||||
|
||||
```js
|
||||
_onFire() {
|
||||
// 1. Get shell config from ammoSystem.fire()
|
||||
// 2. Create Projectile(tank.x, tank.y, turret.getWorldAngle(), shellConfig)
|
||||
// 3. Spawn sprite in projectileGroup at that position/angle with shell velocity
|
||||
// 4. Track projectile object on sprite for hit resolution
|
||||
}
|
||||
|
||||
_onEnemyProjHitTank(tankSprite, projSprite) {
|
||||
// 1. Deactivate/destroy projectile sprite
|
||||
// 2. Check commanderHatch.isHitboxActive()
|
||||
// 3. If unbuttoned: commanderHatch.takeHit(), morale penalty
|
||||
// 4. Apply tank damage
|
||||
// 5. Screen shake via this.cameras.main.shake(100, 0.01)
|
||||
}
|
||||
|
||||
_checkPlayerProjectileHits(delta) {
|
||||
// For each active player projectile sprite:
|
||||
// For each active enemy:
|
||||
// If distance(proj, enemy) < hitRadius:
|
||||
// const result = projectile.onHit(enemy)
|
||||
// enemy.takeDamage(result.damage)
|
||||
// projectile.alive = false
|
||||
// despawn sprite
|
||||
}
|
||||
```
|
||||
|
||||
### Integration Points
|
||||
|
||||
```
|
||||
Mouse click → AmmoSystem.fire() → spawn Projectile sprite in projectileGroup
|
||||
↓
|
||||
MainGame.update() → _checkPlayerProjectileHits() → Enemy.takeDamage()
|
||||
→ physics.overlap callback → Tank damage, Hatch hitbox
|
||||
→ CommanderHatch.takeHit() (if unbuttoned)
|
||||
```
|
||||
|
||||
### Testing Strategy
|
||||
|
||||
**Unit tests (Jest):** `tests/slice2_collision.test.js`
|
||||
- `Projectile.onHit(enemy)` computes correct damage for each shell type vs armor
|
||||
- `Enemy.takeDamage(50)` reduces hp to 200 (from 250 default? actually Type 59 has 300 hp)
|
||||
- `CommanderHatch.takeHit()` sets `isUnbuttoned = false`, applies morale penalty, starts cooldown
|
||||
- Fire rate limiting: `ammoSystem.fire()` called twice in 100ms → second call returns `{type:'ram'}` if shell count = 0 after first
|
||||
|
||||
**Integration tests (Jest):** `tests/integration/collision-wiring.test.js`
|
||||
- Mock scene: create MainGame, spawn one enemy, fire one projectile → after 100ms update loop, enemy HP decreased
|
||||
- Enemy projectile hitting tank: simulate overlap → verify tank damage callback fires
|
||||
- Unbuttoned tank hit → hatch `takeHit()` called, `isUnbuttoned` becomes false
|
||||
|
||||
**E2E tests (Playwright):** `tests/e2e/collision.spec.js`
|
||||
- Game-designer fires at enemy, observes HP drop (via debug overlay or visual feedback)
|
||||
- Enemy projectile hits tank, observes screen shake
|
||||
|
||||
---
|
||||
|
||||
## Gap 4: Hatch Animation/Audio
|
||||
|
||||
**Complexity: M (Medium)** — modify 2 files. **Depends on Gap 5 (audio system).**
|
||||
|
||||
### Current State
|
||||
- E-key toggle works (state changes in `CommanderHatch`) but produces zero visual or audio feedback.
|
||||
- No hatch animation — no sliding motion, no camera bob, no visual indicator of state.
|
||||
|
||||
### Desired State
|
||||
- **Visual:** When unbuttoning, a "hatch open" overlay graphic animates (dark circle slides open from center). Camera subtly bobs up (~5px) then settles.
|
||||
- **Audio:** Wind rush sound when unbuttoned (ambient). Muffled, quieter ambient when buttoned. Audio transition crossfades over 500ms.
|
||||
- **Telegraph:** When `CommanderHatch.beginSniperShot()` is called (from Gap 3 collision), a red laser line paints the turret on screen, visible for 2s charge time.
|
||||
|
||||
### Files to Modify
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `src/game/entities/CommanderHatch.js` | Add event emitter or callback hooks: `onToggle(isUnbuttoned)`, `onSniperTelegraph(active)`, `onSniperComplete()`. MainGame registers listeners. |
|
||||
| `src/game/scenes/MainGame.js` | Register hatch event handlers: 1. `onToggle` → run camera bob tween + trigger audio crossfade. 2. `onSniperTelegraph` → draw red laser line from enemy position to tank turret. 3. `onSniperComplete` → remove laser line, screen flash red. |
|
||||
|
||||
### Hatch Visual Implementation
|
||||
|
||||
Use Phaser Graphics for hatch overlay (dark circle vignette that contracts/expands):
|
||||
|
||||
```js
|
||||
// In MainGame, on hatch toggle:
|
||||
if (isUnbuttoned) {
|
||||
// Tween hatch overlay alpha from 1→0 (open)
|
||||
this.tweens.add({ targets: this.hatchOverlay, alpha: 0, duration: 400 });
|
||||
// Camera bob: up 5px, back down
|
||||
this.cameras.main.shake(200, 0.003);
|
||||
} else {
|
||||
// Tween hatch overlay alpha from 0→1 (close)
|
||||
this.tweens.add({ targets: this.hatchOverlay, alpha: 1, duration: 400 });
|
||||
}
|
||||
```
|
||||
|
||||
### Audio Implementation (requires Gap 5 AudioManager)
|
||||
|
||||
```js
|
||||
// Gap 5 provides this.audioManager
|
||||
if (isUnbuttoned) {
|
||||
this.audioManager.crossfade('wind_ambient', 'engine_muffled', 500);
|
||||
} else {
|
||||
this.audioManager.crossfade('engine_muffled', 'wind_ambient', 500);
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Strategy
|
||||
|
||||
**Unit tests (Jest):** `tests/game/entities/CommanderHatch.events.test.js`
|
||||
- `toggle()` fires `onToggle(true)` when opening — event callback receives correct state
|
||||
- `beginSniperShot()` fires `onSniperTelegraph(true)` — callback called
|
||||
- `completeSniperShot()` fires `onSniperComplete()` — callback called
|
||||
- Edge: toggling while on cooldown does NOT fire `onToggle`
|
||||
|
||||
**Integration tests (Jest):** `tests/integration/hatch-animation.test.js`
|
||||
- MainGame registers onToggle handler → verifies tween targets set correctly
|
||||
- Sniper telegraph → verifies laser graphics drawn at correct position
|
||||
- Mock audioManager → verifies crossfade called with correct clip names
|
||||
|
||||
**E2E tests (Playwright):** `tests/e2e/hatch.spec.js`
|
||||
- Game-designer presses E → visually confirms hatch overlay fades and camera bobs
|
||||
- Sniper scenario: unbutton, wait → red laser appears → 2s later screen flash
|
||||
|
||||
---
|
||||
|
||||
## Gap 5: SFX/VFX
|
||||
|
||||
**Complexity: L (Large)** — 3-4 new files. Foundational for Gap 4 audio.
|
||||
|
||||
### Current State
|
||||
- Zero audio. No AudioContext, no sound manager, no audio assets.
|
||||
- Zero VFX. No muzzle flashes, shell impacts, engine drone, radio barks, screen effects.
|
||||
- `__mocks__/phaser.js` and `tests/helpers/setup.js` already stub `AudioContext` (setup.js line 82-87).
|
||||
|
||||
### Desired State
|
||||
- **Audio sprite system:** Procedurally generated short sound effects (white noise bursts for impacts, oscillator sweeps for engine drone, static bursts for radio). No external audio files needed for Phase I+II demo.
|
||||
- **Muzzle flash VFX:** When player fires, a brief bright rectangle appears at turret tip (turret position + forward offset), fading over 100ms.
|
||||
- **Impact VFX:** When enemy projectile hits tank or player projectile hits enemy, a brief orange/white particle burst at impact point.
|
||||
- **Engine drone:** Continuous low-frequency oscillator that shifts pitch with tank speed.
|
||||
- **Radio barks:** Triggered on enemy spawn or ammo warning — brief static burst + synthesized voice-like tone pattern.
|
||||
|
||||
### Files to Create
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `src/game/systems/AudioManager.js` | Web Audio API wrapper. Manages AudioContext, creates procedural sounds (oscillator, noise buffer), provides `play(soundId, volume, pitch)`, `startLoop(soundId, params)`, `stopLoop(soundId)`, `crossfade(fromId, toId, durationMs)`. All sounds are procedurally generated — no asset loading required for Phase I+II. |
|
||||
| `src/game/systems/VFXManager.js` | Phaser Graphics-based flash/particle effects. `muzzleFlash(x, y, angle)` draws bright rectangle + fade tween. `impactBurst(x, y, color)` draws expanding circle + alpha tween. `screenFlash(color, duration)` full-screen overlay tween. |
|
||||
| `src/game/data/audio-defs.js` | Procedural sound definitions: frequency ranges, noise types, envelope shapes for each sound ID (`muzzle_report`, `impact_metal`, `engine_loop`, `radio_static`, `sniper_laser_tone`, `wind_ambient`). |
|
||||
|
||||
### Files to Modify
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `src/game/scenes/MainGame.js` | 1. Initialize `AudioManager` and `VFXManager` in `create()`. 2. On fire: `vfxManager.muzzleFlash()` + `audioManager.play('muzzle_report')`. 3. On projectile hit enemy: `vfxManager.impactBurst()` + `audioManager.play('impact_metal')`. 4. On enemy projectile hit tank: `vfxManager.screenFlash(0xff0000, 100)` + `audioManager.play('impact_metal')`. 5. Engine drone: `audioManager.startLoop('engine_loop', {speed})` updated in `update()`. |
|
||||
|
||||
### AudioManager Design
|
||||
|
||||
```js
|
||||
class AudioManager {
|
||||
constructor() {
|
||||
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
this.masterGain = this.ctx.createGain();
|
||||
this.masterGain.connect(this.ctx.destination);
|
||||
this._loops = {}; // active looping sounds
|
||||
this._defs = {}; // loaded sound definitions
|
||||
}
|
||||
|
||||
// Play a one-shot procedural sound
|
||||
play(soundId, options = {}) { /* ... */ }
|
||||
|
||||
// Start/update looping sound (engine drone)
|
||||
startLoop(soundId, params = {}) { /* ... */ }
|
||||
stopLoop(soundId) { /* ... */ }
|
||||
|
||||
// Crossfade from one loop to another (hatch open/close)
|
||||
crossfade(fromId, toId, durationMs) { /* ... */ }
|
||||
|
||||
// Procedural generators
|
||||
_createNoiseBuffer(duration) { /* white noise */ }
|
||||
_createOscillator(freq, type) { /* sine/sawtooth/square */ }
|
||||
}
|
||||
```
|
||||
|
||||
Procedural sound palette for Phase I+II:
|
||||
|
||||
| Sound ID | Generation | Envelope |
|
||||
|---|---|---|
|
||||
| `muzzle_report` | White noise burst 80ms + 200Hz sawtooth 100ms | Attack 1ms, decay 150ms |
|
||||
| `impact_metal` | White noise burst 50ms + 800Hz sine ping 30ms | Attack 0, decay 80ms |
|
||||
| `engine_loop` | 60Hz sawtooth + 120Hz square, low-pass filtered | Continuous, pitch modulated by speed |
|
||||
| `radio_static` | White noise, band-pass filtered 1000-3000Hz | 500ms burst, 200ms fade |
|
||||
| `sniper_laser_tone` | 2000Hz sine, high Q | 2s ramp up in volume |
|
||||
| `wind_ambient` | Filtered white noise, low frequency | Continuous loop |
|
||||
|
||||
### VFXManager Design
|
||||
|
||||
```js
|
||||
class VFXManager {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
this.graphics = scene.add.graphics();
|
||||
this.graphics.setDepth(500); // above HUD
|
||||
}
|
||||
|
||||
muzzleFlash(x, y, angle) {
|
||||
// Draw bright yellow/white rectangle at offset
|
||||
// Tween alpha 1→0 over 100ms, then clear
|
||||
}
|
||||
|
||||
impactBurst(x, y, color = 0xff8800) {
|
||||
// Draw expanding circle + radial lines
|
||||
// Tween scale + alpha over 200ms
|
||||
}
|
||||
|
||||
screenFlash(color = 0xff0000, duration = 100) {
|
||||
// Full-screen fill rectangle, tween alpha 1→0
|
||||
}
|
||||
|
||||
update() {
|
||||
// Called each frame — advance active tweens
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Integration Points
|
||||
|
||||
```
|
||||
Fire event → VFXManager.muzzleFlash() + AudioManager.play('muzzle_report')
|
||||
Impact event → VFXManager.impactBurst() + AudioManager.play('impact_metal')
|
||||
Tank hit → VFXManager.screenFlash() + AudioManager.play('impact_metal')
|
||||
Hatch toggle → AudioManager.crossfade('wind_ambient', 'engine_muffled')
|
||||
Engine update → AudioManager.startLoop('engine_loop', {pitch: speedRatio})
|
||||
Enemy spawn → AudioManager.play('radio_static') [optional — Phase I+II nice-to-have]
|
||||
```
|
||||
|
||||
### Testing Strategy
|
||||
|
||||
**Unit tests (Jest):** `tests/systems/AudioManager.test.js`
|
||||
- `AudioManager.play('muzzle_report')` creates oscillator + noise nodes, connects, starts, stops
|
||||
- `AudioManager.startLoop('engine_loop')` creates continuous oscillator, updates pitch in real-time
|
||||
- `AudioManager.crossfade('wind', 'engine', 500)` creates gain ramp on both loops
|
||||
- `AudioManager` gracefully degrades when AudioContext is unavailable (no crash, no-op)
|
||||
- Memory: 100 play() calls don't leak (nodes properly disconnected after envelope completes)
|
||||
|
||||
**Unit tests (Jest):** `tests/systems/VFXManager.test.js`
|
||||
- `muzzleFlash(x, y, angle)` creates Graphics objects at correct position/orientation
|
||||
- `impactBurst(x, y, color)` creates expanding circle effect
|
||||
- `screenFlash(0xff0000, 100)` creates overlay + tween
|
||||
- VFX cleanup: old effects don't accumulate (Graphics.clear() called after tween completes)
|
||||
|
||||
**Integration tests (Jest):** `tests/integration/sfx-vfx-wiring.test.js`
|
||||
- Mock scene fire → verify VFXManager.muzzleFlash + AudioManager.play called
|
||||
- Mock collision → verify VFXManager.impactBurst called at correct coordinates
|
||||
|
||||
**E2E tests (Playwright):** `tests/e2e/sfx-vfx.spec.js`
|
||||
- Game-designer fires weapon → sees muzzle flash, hears report
|
||||
- Enemy projectile hits tank → sees impact burst, screen flash
|
||||
- Subjective: engine drone pitch changes with tank speed
|
||||
|
||||
---
|
||||
|
||||
## Cross-Cutting Concerns
|
||||
|
||||
### Playwright E2E Test Setup
|
||||
|
||||
All gaps need Playwright E2E tests so the game-designer can review mechanics in a real browser.
|
||||
|
||||
**Setup:** `tests/e2e/` directory with Playwright config pointing to `https://iron-requiem.damascusfront.net`.
|
||||
|
||||
```js
|
||||
// tests/e2e/playwright.config.js
|
||||
const { defineConfig } = require('@playwright/test');
|
||||
module.exports = defineConfig({
|
||||
testDir: './tests/e2e',
|
||||
use: {
|
||||
baseURL: 'https://iron-requiem.damascusfront.net',
|
||||
viewport: { width: 1280, height: 720 },
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Pattern per gap:**
|
||||
1. Navigate to game
|
||||
2. Wait for Phaser canvas to exist (`canvas` selector)
|
||||
3. Inject script to read game state: `window.__IR_GAME.scene.scenes[1]` (MainGame)
|
||||
4. Simulate inputs via `window.dispatchEvent(new KeyboardEvent('keydown', {key: 'E'}))`
|
||||
5. Assert visual state via canvas screenshot or injected state read
|
||||
|
||||
**Install:** `npm install --save-dev @playwright/test` (added to package.json devDependencies).
|
||||
|
||||
### Jest Test Pattern
|
||||
|
||||
All unit tests follow existing patterns:
|
||||
- **VisionMask:** dependency-injected Graphics mock (already established)
|
||||
- **DiegeticHUD:** pure logic class, no Phaser dependency
|
||||
- **AudioManager:** mock AudioContext via `tests/helpers/setup.js` (already stubs AudioContext class)
|
||||
- **VFXManager:** dependency-injected Graphics mock + Phaser tween mock
|
||||
|
||||
All tests use `test()` / `describe()` / `expect()` from Jest globals. No `import from 'vitest'`.
|
||||
|
||||
### Code Quality Gates (per gap)
|
||||
|
||||
Each gap is complete when:
|
||||
1. `npm test` passes all tests (existing 43 + new gap tests, minimum 85% line coverage for new code)
|
||||
2. `npm run build` succeeds (webpack production build)
|
||||
3. Docker container builds and serves the updated game
|
||||
4. Playwright E2E tests pass against live site
|
||||
5. Game-designer signs off via browser review
|
||||
|
||||
### Performance Budget (all gaps combined)
|
||||
|
||||
- VisionMask draw: < 1ms (already benchmarked at < 16ms avg for 100 iterations)
|
||||
- HUD redraw: < 2ms per frame (4 needles + ammo text, Graphics-based)
|
||||
- Collision check: < 3ms per frame (max 50 enemies × 50 projectiles = 2500 distance checks — O(1) math per check)
|
||||
- Audio: non-blocking (Web Audio API runs on separate thread)
|
||||
- VFX: < 2ms per frame (max 5 simultaneous effects, Graphics tweens)
|
||||
- **Total frame budget impact: < 8ms (12.5% of 60fps frame), well within budget**
|
||||
|
||||
---
|
||||
|
||||
## Implementation Task Breakdown (for Kanban)
|
||||
|
||||
| Task | Gap | Assignee | Est. Hours | Dependencies |
|
||||
|---|---|---|---|---|
|
||||
| Vision mask dynamic arc wiring | 2 | dev | 1 | — |
|
||||
| AudioManager procedural system | 5 | dev | 4 | — |
|
||||
| VFXManager muzzle/impact/screen | 5 | dev | 3 | — |
|
||||
| Wire sfx/vfx into MainGame fire + collision | 5 | dev | 2 | AudioManager, VFXManager |
|
||||
| DiegeticHUD logic class | 1 | dev | 2 | — |
|
||||
| HUDScene Phaser overlay | 1 | dev | 2 | DiegeticHUD |
|
||||
| Wire HUD into MainGame | 1 | dev | 1 | HUDScene, DiegeticHUD |
|
||||
| Collision: player fire mechanism | 3 | dev | 2 | — |
|
||||
| Collision: overlap + distance checks | 3 | dev | 3 | player fire |
|
||||
| Hatch event hooks + animation wiring | 4 | dev | 3 | AudioManager |
|
||||
| Playwright E2E setup + per-gap specs | all | dev | 3 | all gaps functional |
|
||||
| Integration polish + full-suite verification | all | dev | 2 | all tasks complete |
|
||||
|
||||
**Total estimated: ~28 hours** (4-5 days at sustainable pace)
|
||||
|
||||
---
|
||||
|
||||
## Risk Register
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|---|---|---|---|
|
||||
| AudioContext blocked by browser autoplay policy | High | Medium | Resume AudioContext on first user click (Phaser input event). Already handled in constructor pattern: lazy-init ctx on first `play()`. |
|
||||
| Procedural audio sounds bad/subjective | Medium | Low | Keep AudioManager interface abstracted — easy to swap in real audio files later. Phase I+II only needs "functional" audio, not polished. |
|
||||
| Collision performance with 200+ projectiles | Low | Medium | Use spatial hashing if simple distance check is too slow. Start with O(n×m), profile, optimize only if needed. |
|
||||
| HUD readability at 640×360 on 1080p screen (3x scaling) | Medium | Medium | Test at 2x and 3x scale. Needles must be at least 4px wide after scaling. Use `setDepth` to ensure crisp rendering. |
|
||||
| Playwright tests flaky due to Phaser async boot | Medium | Low | Use robust selectors: wait for `canvas` element, then `page.evaluate(() => window.__IR_GAME !== undefined)`. Retry with 3x attempts. |
|
||||
50
docs/PHASE_I_II_PLAYABLE_ASSESSMENT.md
Normal file
50
docs/PHASE_I_II_PLAYABLE_ASSESSMENT.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Phase I+II Playable Mechanics Assessment — Iron Requiem
|
||||
|
||||
This document assesses the playable state of Iron Requiem following the completion of Slice 1 (Tank Physics, Vision, Save/Load) and Slice 2 (Combat Loop, Ammo, Enemies).
|
||||
|
||||
## 1. What IS playable right now?
|
||||
Based on the codebase and the live site (https://iron-requiem.damascusfront.net), the following mechanics are functionally active in the `MainGame` loop:
|
||||
|
||||
- **Tank Movement (WASD):** The hull moves with a physics model simulating mass and inertia.
|
||||
- **Turret Rotation (Mouse Aim):** The turret rotates independently of the hull, capped at 15°/sec, following the mouse cursor.
|
||||
- **Periscope Vision (VisionMask):** A dark overlay is rendered with a 180° forward arc cutout that follows the tank's orientation.
|
||||
- **Unbuttoning (E Key):** The `CommanderHatch` state toggles between buttoned and unbuttoned via the E key (though visual feedback in-game is currently minimal/debug-based).
|
||||
- **Enemy Spawning:** Enemies are spawned into the world at fixed intervals based on the current zone.
|
||||
- **Bullet Patterns:** The `PatternManager` can trigger choreographed projectile patterns (e.g., Infantry Wall, Artillery Ring) using object pooling for performance.
|
||||
- **Ammo Selection (Keys 1-4):** Players can switch between APCBC, APCR, HE, and HEAT shell types.
|
||||
- **Ammo Depletion:** The `AmmoSystem` tracks remaining rounds and triggers internal warnings when ammo falls below 10, 5, or 0.
|
||||
- **Persistence (Save/Load):** Basic run stats and state persist across browser refreshes via `SaveManager` (IndexedDB).
|
||||
|
||||
## 2. What mechanics SHOULD be playable at this stage?
|
||||
Given the GDD and Software Plan, a Phase I+II demo should showcase the "Physicality of the Tank" and the "Cruelty of the Bullet Hell."
|
||||
|
||||
| Mechanic | Expected Working State | Status |
|
||||
|---|---|---|
|
||||
| **Tank Physics** | Inertia-based movement; hull/turret decoupling. | ✅ Functional |
|
||||
| **Periscope Mask** | Restricted vision based on hatch state (Buttoned vs Unbuttoned). | ⚠️ Partial (Mask exists, but doesn't change range/arc based on hatch state in `MainGame.update`) |
|
||||
| **Combat Loop** | Projectile spawning, evasion, and enemy AI patterns. | ✅ Functional |
|
||||
| **Ammo System** | Shell type selection and depletion. | ✅ Functional |
|
||||
| **Hatch Risk** | Hitbox active only when unbuttoned; sniper telegraphs. | ⚠️ Partial (Logic exists in `CommanderHatch.js`, but not fully integrated into `MainGame` collision) |
|
||||
| **Save/Load** | Essential run state persists. | ✅ Functional |
|
||||
|
||||
## 3. Gap Analysis
|
||||
While the systems are "wired," several critical player-facing experiences are missing:
|
||||
|
||||
- **Hatch Visuals/Audio:** There is no animated hatch or audio shift when unbuttoning. The player relies on the internal state.
|
||||
- **HUD Feedback:** The `DiegeticHUD` (analog needles) is missing from the current `MainGame` implementation. Ammo and heat are logged to console, but not visible to the player.
|
||||
- **Collision & Damage:** While projectiles spawn and enemies exist, the "death" loop (tank taking damage, morale dropping, or commander death) is not fully wired into the visual game loop in `MainGame.js`.
|
||||
- **Dynamic Vision:** The `VisionMask` is currently static. It does not yet transition between the `BUTTONED_ARC` (180°) and `UNBUTTONED_ARC` (270°) as defined in `CommanderHatch.js`.
|
||||
- **Sfx/Vfx:** Muzzle flashes, shell impacts, and engine drones are absent.
|
||||
|
||||
## 4. Quality Bar
|
||||
|
||||
| Mechanic | Quality Rating | Notes |
|
||||
|---|---|---|
|
||||
| **Tank Movement** | **Functional** | The inertia feels correct; needs "ice/snow" surface friction variants. |
|
||||
| **Turret Rotation**| **Functional** | Rotation cap is implemented and feels historical. |
|
||||
| **Vision Mask** | **Rough** | The rectangle is functional but the "dirty lens" effect and dynamic range are missing. |
|
||||
| **Bullet Patterns**| **Functional** | Object pooling is working; patterns are choreographed. |
|
||||
| **Ammo System** | **Rough** | Internal logic is solid, but requires the HUD to be usable by a human. |
|
||||
|
||||
## 5. Phase I+II Demo Vision
|
||||
The ideal 2-minute loop starts with the player spawning in the white silence of the Tundra. They feel the weight of the Panzer IV as they lumber forward, the view choked by the periscope mask. A sudden radio bark warns of contact; the player hits 'E' to unbutton, the vision expands, and they spot a line of infantry. They rotate the turret, snap to a target, and fire an APCBC shell—the screen shakes. Suddenly, the air fills with a geometric grid of projectiles. The player must fight the tank's inertia to drift the chassis out of the pattern's path while keeping the turret locked on the enemy, culminating in a desperate scramble to re-button the hatch as a sniper's red laser paints the turret.
|
||||
59
docs/PHASE_I_II_REALITY_CHECK.md
Normal file
59
docs/PHASE_I_II_REALITY_CHECK.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# PHASE I & II Reality Check: Live Game Loop Inspection
|
||||
**Date:** 2026-05-23
|
||||
**Target:** https://iron-requiem.damascusfront.net
|
||||
**Inspector:** @game-designer
|
||||
|
||||
## Executive Summary
|
||||
The current state of the live site is a **high-fidelity tech demo**, not a complete game loop. While the "vibe" (winter pixel art, periscope feeling, IR/Daylight split) is strong and the basic movement/firing mechanics are present, the "game" part of the loop—meaning enemy interaction, progression, and failure states—is virtually non-existent or invisible in the current build.
|
||||
|
||||
---
|
||||
|
||||
## Detailed Findings
|
||||
|
||||
### 1. Camera & Movement
|
||||
- **Camera:** The camera is functional. Driving the tank results in the environment (snowy terrain, trees, futuristic/classical buildings) scrolling across the screen.
|
||||
- **Follow:** The camera successfully tracks the tank's position.
|
||||
- **Verdict:** PASS.
|
||||
|
||||
### 2. Enemies & AI
|
||||
- **Visibility:** NO enemies were visible in the captured sequence. The world feels empty.
|
||||
- **Movement:** Since no enemies were spotted, the specific flanking behaviors (Type 59, Type 62, etc.) could not be verified.
|
||||
- **Verdict:** FAIL / NOT DETECTED.
|
||||
|
||||
### 3. Combat & Projectiles
|
||||
- **Firing:** Clicking successfully triggers a projectile event.
|
||||
- **Projectiles:** A clear yellow/white tracer/bullet is visible, originating from the reticle and moving across the screen toward the target area.
|
||||
- **Enemy Fire:** No enemy projectiles were observed. The PatternManager appears to be idle or non-functional on the live site.
|
||||
- **Verdict:** PARTIAL PASS (Player firing works; Enemy firing is missing).
|
||||
|
||||
### 4. Periscope & Vision Mask
|
||||
- **Vision Mask:** The "periscope hole" effect is implemented. The screen is correctly split between Infrared (IR) and Daylight views.
|
||||
- **Rotation:** The mask is functional, but the "feel" of the rotation needs refinement. The vertical split and the central viewing window create the intended claustrophobic effect.
|
||||
- **Verdict:** PASS.
|
||||
|
||||
### 5. HUD & UI
|
||||
- **Rendering:** Basic HUD elements (version tag, reticle) are visible.
|
||||
- **Gauges:** The detailed needle gauges (RPM, Fuel) and ammo shell indicators mentioned in the GDD are either not rendering or are not visually distinct in the current screenshots. The screen is dominated by the vision split and the reticle.
|
||||
- **Verdict:** PARTIAL FAIL (Basic UI is there; immersive gauges are missing/invisible).
|
||||
|
||||
### 6. Collisions & Damage
|
||||
- **Hits:** Because no enemies were present to be hit, and no enemy fire was received, collision and damage logic could not be visually verified.
|
||||
- **Verdict:** NOT TESTABLE.
|
||||
|
||||
---
|
||||
|
||||
## Frame-by-Frame Experience (First 10 Seconds)
|
||||
1. **0-1s:** Solid dark blue screen (Loading/Background).
|
||||
2. **1-2s:** Immediate jump to the game view. The "IR v0.2 - Slice 2" tag appears. The world is a snowy wasteland with a distinct split between a dark IR view (left) and a light daylight view (right).
|
||||
3. **2-5s:** Player moves the tank. The world scrolls. The periscope feel is immediate and oppressive.
|
||||
4. **5-10s:** Player fires a shot. A yellow tracer arcs into the distance. The world remains silent; no enemies spawn, and no one fires back.
|
||||
|
||||
## Final Assessment
|
||||
**Status: Tech Demo**
|
||||
|
||||
The technical foundation (rendering, camera, basic input, vision masking) is impressive and aligns with the vision. However, the "Enemy" and "Interaction" layers of the game loop are missing. It's a very polished "driving and shooting at nothing" simulator.
|
||||
|
||||
**Critical Gaps to Address:**
|
||||
- Enemy spawning and AI behavior are not reflecting in the live build.
|
||||
- Enemy combat (PatternManager) is not firing.
|
||||
- HUD immersive elements (gauges) are not providing the necessary feedback.
|
||||
220
docs/RECOVERY_PLAN.md
Normal file
220
docs/RECOVERY_PLAN.md
Normal file
@@ -0,0 +1,220 @@
|
||||
# Iron Requiem Recovery Plan
|
||||
|
||||
**Date:** 2026-05-24
|
||||
**Status:** Critical blockers identified — game loop incomplete
|
||||
**Live Site:** https://iron-requiem.damascusfront.net
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The live build is a **tech demo, not a game**. Tank movement/firing works, periscope mask works, but:
|
||||
- ❌ **No enemies visible** — Enemy objects exist but have no sprites rendered
|
||||
- ❌ **No enemy fire** — Only artillery fires patterns; tanks/helis never call `executePattern()`
|
||||
- ❌ **HUD gauges invisible** — HUDScene launches but renders nothing visible
|
||||
- ⚠️ **Double bundle** — `index.html` loads both `bundle.59b73b8d.js` AND `bundle.js`
|
||||
|
||||
---
|
||||
|
||||
## Current State vs SOFTWARE_PLAN.md
|
||||
|
||||
| Slice | Requirement | Status | Notes |
|
||||
|-------|-------------|--------|-------|
|
||||
| **S1: Tank Physics** | Hull inertia, turret rotation cap | ✅ Working | Tank moves with acceleration, turret rotates |
|
||||
| **S1: Periscope** | Vision mask restricts view | ✅ Working | Dynamic arc based on hatch state |
|
||||
| **S1: Save/Load** | IndexedDB persistence | ✅ Wired | SaveManager init in PreloadScene |
|
||||
| **S1: One Pattern** | One bullet pattern fires | ❌ Broken | PatternManager exists but never triggered |
|
||||
| **S2: Enemies** | Enemy sprites render | ❌ **CRITICAL** | Enemy.js has no sprite creation |
|
||||
| **S2: Enemy Fire** | Enemies fire patterns | ❌ **CRITICAL** | Only artillery calls executePattern() |
|
||||
| **S2: Ammo System** | Shell types, depletion | ✅ Wired | AmmoSystem fires on click |
|
||||
| **S3: HUD Gauges** | Needle gauges for fuel/heat/morale | ❌ Broken | HUDScene renders but gauges not visible |
|
||||
| **S3: Crew/Heat** | Morale buffs, heat cycle | ⏳ Missing | Placeholder values (heat=0, morale=50) |
|
||||
|
||||
---
|
||||
|
||||
## Critical Blockers (Root Causes)
|
||||
|
||||
### 1. Enemies Have No Sprites [CRITICAL]
|
||||
**File:** `src/game/entities/Enemy.js`
|
||||
**Problem:** Enemy class is pure logic — stores x/y/active but never creates a Phaser sprite.
|
||||
**Impact:** `spawnZone()` creates Enemy objects, MainGame updates them, but nothing renders.
|
||||
|
||||
**Fix:**
|
||||
```js
|
||||
// In Enemy constructor, AFTER setting properties:
|
||||
this.sprite = scene.add.rectangle(x, y, 24, 36, this.spriteColor);
|
||||
this.sprite.setDepth(50);
|
||||
|
||||
// In update(), sync sprite position:
|
||||
this.sprite.x = this.x;
|
||||
this.sprite.y = this.y;
|
||||
this.sprite.rotation = /* face player */;
|
||||
|
||||
// In takeDamage():
|
||||
if (this.hp <= 0) {
|
||||
this.active = false;
|
||||
this.sprite.destroy(); // Clean up sprite
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Enemy Fire Logic Missing [CRITICAL]
|
||||
**File:** `src/game/entities/Enemy.js`
|
||||
**Problem:** Only `artillery_emplacement` calls `executePattern()` in update(). Type 59, Type 62, and helicopter move but never fire.
|
||||
|
||||
**Fix:**
|
||||
```js
|
||||
// Add fire timer + interval per enemy type
|
||||
case 'type59':
|
||||
this._fireTimer += delta;
|
||||
if (this._fireTimer >= this._fireInterval) {
|
||||
this._fireTimer = 0;
|
||||
this.executePattern(this.patterns[0]); // tank_destroyer_beam
|
||||
}
|
||||
this._moveTowardFront(angleRad, playerX, playerY, delta);
|
||||
break;
|
||||
|
||||
case 'type62':
|
||||
// Same pattern for infantry_wall
|
||||
```
|
||||
|
||||
### 3. PatternManager Never Triggered by MainGame
|
||||
**File:** `src/game/scenes/MainGame.js`
|
||||
**Problem:** PatternManager is created (line 109) but MainGame never calls `this.patternManager.trigger()` directly. Only enemies trigger patterns, and enemies aren't firing.
|
||||
|
||||
**Fix:** Add a test pattern trigger in create() to verify PatternManager works:
|
||||
```js
|
||||
// After line 176 (debug crosshair):
|
||||
setTimeout(() => {
|
||||
console.log('[IR:MainGame] TEST: triggering infantry_wall pattern');
|
||||
this.patternManager.trigger('infantry_wall', { x: 100, y: 180, direction: 'left-to-right' });
|
||||
}, 3000);
|
||||
```
|
||||
|
||||
### 4. HUD Gauges Not Visible
|
||||
**File:** `src/game/scenes/HUDScene.js`
|
||||
**Problem:** Graphics objects created but may have depth/visibility issues. Gauge arcs draw from 225° to 315° (only 90° arc) — might be off-screen or too small.
|
||||
|
||||
**Debug steps:**
|
||||
1. Add console log in `_drawGauges()` to verify it's being called
|
||||
2. Increase gauge radius from 30 to 50 for visibility
|
||||
3. Add debug rectangle around gauge area to confirm positioning
|
||||
|
||||
### 5. Double Bundle Script
|
||||
**File:** `dist/index.html`
|
||||
**Problem:** Both `bundle.59b73b8d.js` and `bundle.js` are loaded. This is a cache-busting artifact — the build script hashed the file but didn't remove the old script tag.
|
||||
|
||||
**Fix:**
|
||||
```bash
|
||||
# In build.sh, after webpack:
|
||||
sed -i 's|<script[^>]*src="bundle\.js"[^>]*></script>||g' dist/index.html
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Recovery Plan (Ordered Tasks)
|
||||
|
||||
### Phase 1: Make Enemies Visible (2-3 hours)
|
||||
**Goal:** Enemy sprites render on screen
|
||||
|
||||
1. **Patch Enemy.js** — Add sprite creation in constructor
|
||||
2. **Patch Enemy.js** — Sync sprite position in update()
|
||||
3. **Patch Enemy.js** — Destroy sprite on death
|
||||
4. **Verify:** Push → build → deploy → browser shows red/orange/grey rectangles
|
||||
|
||||
**Commands:**
|
||||
```bash
|
||||
cd /root/iron-requiem
|
||||
# Edit src/game/entities/Enemy.js (see fixes above)
|
||||
npm run build
|
||||
./build.sh
|
||||
docker stop iron-requiem && docker rm iron-requiem
|
||||
docker run -d --name iron-requiem --network litellm_hermes-net \
|
||||
-l "traefik.enable=true" \
|
||||
-l "traefik.http.routers.iron-requiem.rule=Host(\`iron-requiem.damascusfront.net\`)" \
|
||||
-l "traefik.http.routers.iron-requiem.entrypoints=websecure" \
|
||||
-l "traefik.http.routers.iron-requiem.tls.certresolver=cloudflare" \
|
||||
-l "traefik.http.services.iron-requiem.loadbalancer.server.port=80" \
|
||||
iron-requiem:latest
|
||||
```
|
||||
|
||||
### Phase 2: Make Enemies Fire (2-3 hours)
|
||||
**Goal:** All enemy types trigger bullet patterns
|
||||
|
||||
1. **Patch Enemy.js** — Add `_fireTimer` and `_fireInterval` to constructor
|
||||
2. **Patch Enemy.js** — Call `executePattern()` in update() for type59, type62, helicopter
|
||||
3. **Patch MainGame.js** — Add test pattern trigger in create() (temporary debug)
|
||||
4. **Verify:** Browser console shows pattern triggers, projectiles visible
|
||||
|
||||
### Phase 3: Fix HUD Visibility (1 hour)
|
||||
**Goal:** Needle gauges render visibly
|
||||
|
||||
1. **Patch HUDScene.js** — Add debug logs in `_drawGauges()`
|
||||
2. **Patch HUDScene.js** — Increase gauge radius to 50px
|
||||
3. **Patch HUDScene.js** — Add debug rectangle around gauge area
|
||||
4. **Verify:** Browser shows 3 gauges on right side
|
||||
|
||||
### Phase 4: Clean Build Pipeline (30 min)
|
||||
**Goal:** Single bundle script in index.html
|
||||
|
||||
1. **Patch build.sh** — Remove old bundle script tag before injecting new one
|
||||
2. **Verify:** `dist/index.html` has only one `<script>` tag
|
||||
|
||||
### Phase 5: Integration Test (1 hour)
|
||||
**Goal:** Full game loop works end-to-end
|
||||
|
||||
1. Deploy all fixes together
|
||||
2. Browser verification checklist:
|
||||
- [ ] Tank moves with inertia
|
||||
- [ ] Turret rotates toward mouse
|
||||
- [ ] Click fires projectile
|
||||
- [ ] Enemies spawn every 3 seconds (colored rectangles)
|
||||
- [ ] Enemies fire patterns (projectiles sweep screen)
|
||||
- [ ] HUD shows 3 gauges + ammo count
|
||||
- [ ] Periscope mask restricts vision
|
||||
3. Console logs show no errors
|
||||
|
||||
---
|
||||
|
||||
## Risk Flags (Phaser Pitfalls)
|
||||
|
||||
From `phaser3-game-dev` skill:
|
||||
|
||||
1. **`generateTexture()` in create()** — ✅ Already correct in PreloadScene.js
|
||||
2. **Scene.make.graphics() removed in 3.60+** — ✅ Using `this.add.graphics()`
|
||||
3. **CJS/ESM interop** — ✅ All files use `import`/`export`
|
||||
4. **Webpack doesn't copy assets** — ⚠️ `tundra_background.png` must be in `dist/` — verify build.sh copies it
|
||||
5. **Canvas mode ERASE blend fails** — ✅ VisionMask uses four-rectangle blinders, not ERASE
|
||||
|
||||
---
|
||||
|
||||
## Verification Commands
|
||||
|
||||
```bash
|
||||
# Check dist has all assets
|
||||
ls -la /root/iron-requiem/dist/
|
||||
# Should see: bundle.[hash].js, index.html, tundra_background.png
|
||||
|
||||
# Check container has assets
|
||||
docker exec iron-requiem ls -la /usr/share/nginx/html/
|
||||
|
||||
# Check CDN cache status
|
||||
curl -sI https://iron-requiem.damascusfront.net/ | grep cf-cache-status
|
||||
# Should be DYNAMIC or MISS after deploy
|
||||
|
||||
# Check bundle hash matches
|
||||
md5sum /root/iron-requiem/dist/bundle.*.js
|
||||
curl -s https://iron-requiem.damascusfront.net/bundle.*.js | md5sum
|
||||
# Hashes must match
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Immediate:** Patch Enemy.js for sprite rendering (Phase 1)
|
||||
2. **After deploy:** Patch Enemy.js for fire logic (Phase 2)
|
||||
3. **After deploy:** Fix HUD visibility (Phase 3)
|
||||
4. **Final:** Clean build pipeline (Phase 4)
|
||||
|
||||
**Estimated total time:** 6-8 hours for full recovery
|
||||
**Priority:** Enemies visible → Enemies firing → HUD visible → Build cleanup
|
||||
@@ -18,7 +18,6 @@
|
||||
console.error('[IR:page] GLOBAL ERROR:', e.message, 'at', e.filename, ':', e.lineno);
|
||||
});
|
||||
</script>
|
||||
<script defer src="bundle.js?v=BUILD_TIMESTAMP"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="game-container"></div>
|
||||
|
||||
10129
node_modules/.package-lock.json
generated
vendored
10129
node_modules/.package-lock.json
generated
vendored
File diff suppressed because it is too large
Load Diff
2
node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json
generated
vendored
2
node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json
generated
vendored
@@ -1 +1 @@
|
||||
{"version":"4.1.7","results":[[":tests/game/entities/CommanderHatch.test.js",{"duration":4.707264000000009,"failed":false}],[":tests/performance/PatternManager.perf.test.js",{"duration":24.78146300000003,"failed":false}],[":tests/slice1_physics.test.js",{"duration":3.151134000000013,"failed":false}],[":tests/turret.test.js",{"duration":6.408282999999983,"failed":false}],[":tests/slice1_deploy.test.js",{"duration":1487.8937839999999,"failed":false}],[":tests/systems/VisionMask.test.js",{"duration":22.836700999999948,"failed":false}],[":tests/scaffold.test.js",{"duration":699.615143,"failed":true}],[":tests/game/systems/PatternManager.test.js",{"duration":83.39410500000008,"failed":false}],[":tests/game/scenes/MainGame.test.js",{"duration":9.79369399999996,"failed":false}],[":tests/game/scenes/GameLoopScene.test.js",{"duration":14.05853500000012,"failed":false}],[":tests/systems/SaveManager.test.js",{"duration":54.77179100000012,"failed":false}],[":tests/unit/patterns.test.js",{"duration":3.2045119999999088,"failed":false}],[":tests/sanity.test.js",{"duration":0,"failed":true}]]}
|
||||
{"version":"4.1.7","results":[[":tests/game/entities/CommanderHatch.test.js",{"duration":4.051992000000041,"failed":false}],[":tests/performance/PatternManager.perf.test.js",{"duration":80.04472099999998,"failed":true}],[":tests/slice1_physics.test.js",{"duration":4.624252999999953,"failed":false}],[":tests/turret.test.js",{"duration":15.99744099999998,"failed":false}],[":tests/slice1_deploy.test.js",{"duration":1107.3759959999998,"failed":true}],[":tests/systems/VisionMask.test.js",{"duration":6.911225000000059,"failed":true}],[":tests/scaffold.test.js",{"duration":29.650349000000006,"failed":true}],[":tests/game/systems/PatternManager.test.js",{"duration":157.3640620000001,"failed":true}],[":tests/game/scenes/MainGame.test.js",{"duration":0,"failed":true}],[":tests/game/scenes/GameLoopScene.test.js",{"duration":10.988139000000047,"failed":false}],[":tests/systems/SaveManager.test.js",{"duration":37.049192000000176,"failed":false}],[":tests/unit/patterns.test.js",{"duration":5.049437000000012,"failed":false}],[":tests/sanity.test.js",{"duration":0,"failed":true}],[":tests/slice2_enemy.test.js",{"duration":16.022418000000016,"failed":false}],[":tests/slice2_patternmanager.test.js",{"duration":38.16303300000004,"failed":true}],[":tests/slice2_ammo.test.js",{"duration":23.137631000000056,"failed":true}],[":tests/slice2_projectile.test.js",{"duration":13.820263999999952,"failed":false}],[":tests/integration/slice1-wiring.test.js",{"duration":0,"failed":true}],[":tests/data/shells-enemies.test.js",{"duration":8.342315999999983,"failed":false}],[":tests/slice2_integration.test.js",{"duration":0,"failed":true}]]}
|
||||
8
node_modules/entities/dist/decode-codepoint.d.ts
generated
vendored
8
node_modules/entities/dist/decode-codepoint.d.ts
generated
vendored
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* Replace the given code point with a replacement character if it is a
|
||||
* surrogate or is outside the valid range. Otherwise return the code
|
||||
* point unchanged.
|
||||
* @param codePoint Unicode code point to convert.
|
||||
*/
|
||||
export declare function replaceCodePoint(codePoint: number): number;
|
||||
//# sourceMappingURL=decode-codepoint.d.ts.map
|
||||
1
node_modules/entities/dist/decode-codepoint.d.ts.map
generated
vendored
1
node_modules/entities/dist/decode-codepoint.d.ts.map
generated
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"decode-codepoint.d.ts","sourceRoot":"","sources":["../src/decode-codepoint.ts"],"names":[],"mappings":"AAkCA;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAS1D"}
|
||||
46
node_modules/entities/dist/decode-codepoint.js
generated
vendored
46
node_modules/entities/dist/decode-codepoint.js
generated
vendored
@@ -1,46 +0,0 @@
|
||||
// Adapted from https://github.com/mathiasbynens/he/blob/36afe179392226cf1b6ccdb16ebbb7a5a844d93a/src/he.js#L106-L134
|
||||
const decodeMap = new Map([
|
||||
[0, 65_533],
|
||||
// C1 Unicode control character reference replacements
|
||||
[128, 8364],
|
||||
[130, 8218],
|
||||
[131, 402],
|
||||
[132, 8222],
|
||||
[133, 8230],
|
||||
[134, 8224],
|
||||
[135, 8225],
|
||||
[136, 710],
|
||||
[137, 8240],
|
||||
[138, 352],
|
||||
[139, 8249],
|
||||
[140, 338],
|
||||
[142, 381],
|
||||
[145, 8216],
|
||||
[146, 8217],
|
||||
[147, 8220],
|
||||
[148, 8221],
|
||||
[149, 8226],
|
||||
[150, 8211],
|
||||
[151, 8212],
|
||||
[152, 732],
|
||||
[153, 8482],
|
||||
[154, 353],
|
||||
[155, 8250],
|
||||
[156, 339],
|
||||
[158, 382],
|
||||
[159, 376],
|
||||
]);
|
||||
/**
|
||||
* Replace the given code point with a replacement character if it is a
|
||||
* surrogate or is outside the valid range. Otherwise return the code
|
||||
* point unchanged.
|
||||
* @param codePoint Unicode code point to convert.
|
||||
*/
|
||||
export function replaceCodePoint(codePoint) {
|
||||
if ((codePoint >= 0xd8_00 && codePoint <= 0xdf_ff) ||
|
||||
codePoint > 0x10_ff_ff) {
|
||||
return 0xff_fd;
|
||||
}
|
||||
return decodeMap.get(codePoint) ?? codePoint;
|
||||
}
|
||||
//# sourceMappingURL=decode-codepoint.js.map
|
||||
1
node_modules/entities/dist/decode-codepoint.js.map
generated
vendored
1
node_modules/entities/dist/decode-codepoint.js.map
generated
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"decode-codepoint.js","sourceRoot":"","sources":["../src/decode-codepoint.ts"],"names":[],"mappings":"AAAA,qHAAqH;AAErH,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC;IACtB,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,sDAAsD;IACtD,CAAC,GAAG,EAAE,IAAI,CAAC;IACX,CAAC,GAAG,EAAE,IAAI,CAAC;IACX,CAAC,GAAG,EAAE,GAAG,CAAC;IACV,CAAC,GAAG,EAAE,IAAI,CAAC;IACX,CAAC,GAAG,EAAE,IAAI,CAAC;IACX,CAAC,GAAG,EAAE,IAAI,CAAC;IACX,CAAC,GAAG,EAAE,IAAI,CAAC;IACX,CAAC,GAAG,EAAE,GAAG,CAAC;IACV,CAAC,GAAG,EAAE,IAAI,CAAC;IACX,CAAC,GAAG,EAAE,GAAG,CAAC;IACV,CAAC,GAAG,EAAE,IAAI,CAAC;IACX,CAAC,GAAG,EAAE,GAAG,CAAC;IACV,CAAC,GAAG,EAAE,GAAG,CAAC;IACV,CAAC,GAAG,EAAE,IAAI,CAAC;IACX,CAAC,GAAG,EAAE,IAAI,CAAC;IACX,CAAC,GAAG,EAAE,IAAI,CAAC;IACX,CAAC,GAAG,EAAE,IAAI,CAAC;IACX,CAAC,GAAG,EAAE,IAAI,CAAC;IACX,CAAC,GAAG,EAAE,IAAI,CAAC;IACX,CAAC,GAAG,EAAE,IAAI,CAAC;IACX,CAAC,GAAG,EAAE,GAAG,CAAC;IACV,CAAC,GAAG,EAAE,IAAI,CAAC;IACX,CAAC,GAAG,EAAE,GAAG,CAAC;IACV,CAAC,GAAG,EAAE,IAAI,CAAC;IACX,CAAC,GAAG,EAAE,GAAG,CAAC;IACV,CAAC,GAAG,EAAE,GAAG,CAAC;IACV,CAAC,GAAG,EAAE,GAAG,CAAC;CACb,CAAC,CAAC;AAEH;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,SAAiB;IAC9C,IACI,CAAC,SAAS,IAAI,OAAO,IAAI,SAAS,IAAI,OAAO,CAAC;QAC9C,SAAS,GAAG,UAAU,EACxB,CAAC;QACC,OAAO,OAAO,CAAC;IACnB,CAAC;IAED,OAAO,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,SAAS,CAAC;AACjD,CAAC"}
|
||||
194
node_modules/entities/dist/decode.d.ts
generated
vendored
194
node_modules/entities/dist/decode.d.ts
generated
vendored
@@ -1,194 +0,0 @@
|
||||
/**
|
||||
* Decoding mode for named entities.
|
||||
*/
|
||||
export declare enum DecodingMode {
|
||||
/** Entities in text nodes that can end with any character. */
|
||||
Legacy = 0,
|
||||
/** Only allow entities terminated with a semicolon. */
|
||||
Strict = 1,
|
||||
/** Entities in attributes have limitations on ending characters. */
|
||||
Attribute = 2
|
||||
}
|
||||
/**
|
||||
* Producers for character reference errors as defined in the HTML spec.
|
||||
*/
|
||||
export interface EntityErrorProducer {
|
||||
missingSemicolonAfterCharacterReference(): void;
|
||||
absenceOfDigitsInNumericCharacterReference(consumedCharacters: number): void;
|
||||
validateNumericCharacterReference(code: number): void;
|
||||
}
|
||||
/**
|
||||
* Token decoder with support of writing partial entities.
|
||||
*/
|
||||
export declare class EntityDecoder {
|
||||
/** The tree used to decode entities. */
|
||||
private readonly decodeTree;
|
||||
/**
|
||||
* The function that is called when a codepoint is decoded.
|
||||
*
|
||||
* For multi-byte named entities, this will be called multiple times,
|
||||
* with the second codepoint, and the same `consumed` value.
|
||||
* @param codepoint The decoded codepoint.
|
||||
* @param consumed The number of bytes consumed by the decoder.
|
||||
*/
|
||||
private readonly emitCodePoint;
|
||||
/** An object that is used to produce errors. */
|
||||
private readonly errors?;
|
||||
constructor(
|
||||
/** The tree used to decode entities. */
|
||||
decodeTree: Uint16Array,
|
||||
/**
|
||||
* The function that is called when a codepoint is decoded.
|
||||
*
|
||||
* For multi-byte named entities, this will be called multiple times,
|
||||
* with the second codepoint, and the same `consumed` value.
|
||||
* @param codepoint The decoded codepoint.
|
||||
* @param consumed The number of bytes consumed by the decoder.
|
||||
*/
|
||||
emitCodePoint: (cp: number, consumed: number) => void,
|
||||
/** An object that is used to produce errors. */
|
||||
errors?: EntityErrorProducer | undefined);
|
||||
/** The current state of the decoder. */
|
||||
private state;
|
||||
/** Characters that were consumed while parsing an entity. */
|
||||
private consumed;
|
||||
/**
|
||||
* The result of the entity.
|
||||
*
|
||||
* Either the result index of a numeric entity, or the codepoint of a
|
||||
* numeric entity.
|
||||
*/
|
||||
private result;
|
||||
/** The current index in the decode tree. */
|
||||
private treeIndex;
|
||||
/** The number of characters that were consumed in excess. */
|
||||
private excess;
|
||||
/** The mode in which the decoder is operating. */
|
||||
private decodeMode;
|
||||
/** The number of characters that have been consumed in the current run. */
|
||||
private runConsumed;
|
||||
/**
|
||||
* Resets the instance to make it reusable.
|
||||
* @param decodeMode Entity decoding mode to use.
|
||||
*/
|
||||
startEntity(decodeMode: DecodingMode): void;
|
||||
/**
|
||||
* Write an entity to the decoder. This can be called multiple times with partial entities.
|
||||
* If the entity is incomplete, the decoder will return -1.
|
||||
*
|
||||
* Mirrors the implementation of `getDecoder`, but with the ability to stop decoding if the
|
||||
* entity is incomplete, and resume when the next string is written.
|
||||
* @param input The string containing the entity (or a continuation of the entity).
|
||||
* @param offset The offset at which the entity begins. Should be 0 if this is not the first call.
|
||||
* @returns The number of characters that were consumed, or -1 if the entity is incomplete.
|
||||
*/
|
||||
write(input: string, offset: number): number;
|
||||
/**
|
||||
* Switches between the numeric decimal and hexadecimal states.
|
||||
*
|
||||
* Equivalent to the `Numeric character reference state` in the HTML spec.
|
||||
* @param input The string containing the entity (or a continuation of the entity).
|
||||
* @param offset The current offset.
|
||||
* @returns The number of characters that were consumed, or -1 if the entity is incomplete.
|
||||
*/
|
||||
private stateNumericStart;
|
||||
/**
|
||||
* Parses a hexadecimal numeric entity.
|
||||
*
|
||||
* Equivalent to the `Hexademical character reference state` in the HTML spec.
|
||||
* @param input The string containing the entity (or a continuation of the entity).
|
||||
* @param offset The current offset.
|
||||
* @returns The number of characters that were consumed, or -1 if the entity is incomplete.
|
||||
*/
|
||||
private stateNumericHex;
|
||||
/**
|
||||
* Parses a decimal numeric entity.
|
||||
*
|
||||
* Equivalent to the `Decimal character reference state` in the HTML spec.
|
||||
* @param input The string containing the entity (or a continuation of the entity).
|
||||
* @param offset The current offset.
|
||||
* @returns The number of characters that were consumed, or -1 if the entity is incomplete.
|
||||
*/
|
||||
private stateNumericDecimal;
|
||||
/**
|
||||
* Validate and emit a numeric entity.
|
||||
*
|
||||
* Implements the logic from the `Hexademical character reference start
|
||||
* state` and `Numeric character reference end state` in the HTML spec.
|
||||
* @param lastCp The last code point of the entity. Used to see if the
|
||||
* entity was terminated with a semicolon.
|
||||
* @param expectedLength The minimum number of characters that should be
|
||||
* consumed. Used to validate that at least one digit
|
||||
* was consumed.
|
||||
* @returns The number of characters that were consumed.
|
||||
*/
|
||||
private emitNumericEntity;
|
||||
/**
|
||||
* Parses a named entity.
|
||||
*
|
||||
* Equivalent to the `Named character reference state` in the HTML spec.
|
||||
* @param input The string containing the entity (or a continuation of the entity).
|
||||
* @param offset The current offset.
|
||||
* @returns The number of characters that were consumed, or -1 if the entity is incomplete.
|
||||
*/
|
||||
private stateNamedEntity;
|
||||
/**
|
||||
* Emit a named entity that was not terminated with a semicolon.
|
||||
* @returns The number of characters consumed.
|
||||
*/
|
||||
private emitNotTerminatedNamedEntity;
|
||||
/**
|
||||
* Emit a named entity.
|
||||
* @param result The index of the entity in the decode tree.
|
||||
* @param valueLength The number of bytes in the entity.
|
||||
* @param consumed The number of characters consumed.
|
||||
* @returns The number of characters consumed.
|
||||
*/
|
||||
private emitNamedEntityData;
|
||||
/**
|
||||
* Signal to the parser that the end of the input was reached.
|
||||
*
|
||||
* Remaining data will be emitted and relevant errors will be produced.
|
||||
* @returns The number of characters consumed.
|
||||
*/
|
||||
end(): number;
|
||||
}
|
||||
/**
|
||||
* Determines the branch of the current node that is taken given the current
|
||||
* character. This function is used to traverse the trie.
|
||||
* @param decodeTree The trie.
|
||||
* @param current The current node.
|
||||
* @param nodeIndex Index immediately after the current node header.
|
||||
* @param char The current character.
|
||||
* @returns The index of the next node, or -1 if no branch is taken.
|
||||
*/
|
||||
export declare function determineBranch(decodeTree: Uint16Array, current: number, nodeIndex: number, char: number): number;
|
||||
/**
|
||||
* Decodes an HTML string.
|
||||
* @param htmlString The string to decode.
|
||||
* @param mode The decoding mode.
|
||||
* @returns The decoded string.
|
||||
*/
|
||||
export declare function decodeHTML(htmlString: string, mode?: DecodingMode): string;
|
||||
/**
|
||||
* Decodes an HTML string in an attribute.
|
||||
* @param htmlAttribute The string to decode.
|
||||
* @returns The decoded string.
|
||||
*/
|
||||
export declare function decodeHTMLAttribute(htmlAttribute: string): string;
|
||||
/**
|
||||
* Decodes an HTML string, requiring all entities to be terminated by a semicolon.
|
||||
* @param htmlString The string to decode.
|
||||
* @returns The decoded string.
|
||||
*/
|
||||
export declare function decodeHTMLStrict(htmlString: string): string;
|
||||
/**
|
||||
* Decodes an XML string, requiring all entities to be terminated by a semicolon.
|
||||
* @param xmlString The string to decode.
|
||||
* @returns The decoded string.
|
||||
*/
|
||||
export declare function decodeXML(xmlString: string): string;
|
||||
export { replaceCodePoint } from "./decode-codepoint.js";
|
||||
export { htmlDecodeTree } from "./generated/decode-data-html.js";
|
||||
export { xmlDecodeTree } from "./generated/decode-data-xml.js";
|
||||
//# sourceMappingURL=decode.d.ts.map
|
||||
1
node_modules/entities/dist/decode.d.ts.map
generated
vendored
1
node_modules/entities/dist/decode.d.ts.map
generated
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"decode.d.ts","sourceRoot":"","sources":["../src/decode.ts"],"names":[],"mappings":"AA6DA;;GAEG;AACH,oBAAY,YAAY;IACpB,8DAA8D;IAC9D,MAAM,IAAI;IACV,uDAAuD;IACvD,MAAM,IAAI;IACV,oEAAoE;IACpE,SAAS,IAAI;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAChC,uCAAuC,IAAI,IAAI,CAAC;IAChD,0CAA0C,CACtC,kBAAkB,EAAE,MAAM,GAC3B,IAAI,CAAC;IACR,iCAAiC,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACzD;AAED;;GAEG;AACH,qBAAa,aAAa;IAElB,wCAAwC;IAExC,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B;;;;;;;OAOG;IACH,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,gDAAgD;IAChD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;;IAbxB,wCAAwC;IAEvB,UAAU,EAAE,WAAW;IACxC;;;;;;;OAOG;IACc,aAAa,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI;IACtE,gDAAgD;IAC/B,MAAM,CAAC,EAAE,mBAAmB,GAAG,SAAS;IAG7D,wCAAwC;IACxC,OAAO,CAAC,KAAK,CAAkC;IAC/C,6DAA6D;IAC7D,OAAO,CAAC,QAAQ,CAAK;IACrB;;;;;OAKG;IACH,OAAO,CAAC,MAAM,CAAK;IAEnB,4CAA4C;IAC5C,OAAO,CAAC,SAAS,CAAK;IACtB,6DAA6D;IAC7D,OAAO,CAAC,MAAM,CAAK;IACnB,kDAAkD;IAClD,OAAO,CAAC,UAAU,CAAuB;IACzC,2EAA2E;IAC3E,OAAO,CAAC,WAAW,CAAK;IAExB;;;OAGG;IACH,WAAW,CAAC,UAAU,EAAE,YAAY,GAAG,IAAI;IAU3C;;;;;;;;;OASG;IACH,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM;IA8B5C;;;;;;;OAOG;IACH,OAAO,CAAC,iBAAiB;IAezB;;;;;;;OAOG;IACH,OAAO,CAAC,eAAe;IAmBvB;;;;;;;OAOG;IACH,OAAO,CAAC,mBAAmB;IAc3B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,iBAAiB;IA6BzB;;;;;;;OAOG;IACH,OAAO,CAAC,gBAAgB;IAoIxB;;;OAGG;IACH,OAAO,CAAC,4BAA4B;IAYpC;;;;;;OAMG;IACH,OAAO,CAAC,mBAAmB;IAsB3B;;;;;OAKG;IACH,GAAG,IAAI,MAAM;CA6BhB;AAmDD;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC3B,UAAU,EAAE,WAAW,EACvB,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,GACb,MAAM,CA4CR;AAKD;;;;;GAKG;AACH,wBAAgB,UAAU,CACtB,UAAU,EAAE,MAAM,EAClB,IAAI,GAAE,YAAkC,GACzC,MAAM,CAER;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAEjE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAEzD,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC"}
|
||||
544
node_modules/entities/dist/decode.js
generated
vendored
544
node_modules/entities/dist/decode.js
generated
vendored
@@ -1,544 +0,0 @@
|
||||
import { replaceCodePoint } from "./decode-codepoint.js";
|
||||
import { htmlDecodeTree } from "./generated/decode-data-html.js";
|
||||
import { xmlDecodeTree } from "./generated/decode-data-xml.js";
|
||||
import { BinTrieFlags } from "./internal/bin-trie-flags.js";
|
||||
var CharCodes;
|
||||
(function (CharCodes) {
|
||||
CharCodes[CharCodes["NUM"] = 35] = "NUM";
|
||||
CharCodes[CharCodes["SEMI"] = 59] = "SEMI";
|
||||
CharCodes[CharCodes["EQUALS"] = 61] = "EQUALS";
|
||||
CharCodes[CharCodes["ZERO"] = 48] = "ZERO";
|
||||
CharCodes[CharCodes["NINE"] = 57] = "NINE";
|
||||
CharCodes[CharCodes["LOWER_A"] = 97] = "LOWER_A";
|
||||
CharCodes[CharCodes["LOWER_F"] = 102] = "LOWER_F";
|
||||
CharCodes[CharCodes["LOWER_X"] = 120] = "LOWER_X";
|
||||
CharCodes[CharCodes["LOWER_Z"] = 122] = "LOWER_Z";
|
||||
CharCodes[CharCodes["UPPER_A"] = 65] = "UPPER_A";
|
||||
CharCodes[CharCodes["UPPER_F"] = 70] = "UPPER_F";
|
||||
CharCodes[CharCodes["UPPER_Z"] = 90] = "UPPER_Z";
|
||||
})(CharCodes || (CharCodes = {}));
|
||||
/** Bit that needs to be set to convert an upper case ASCII character to lower case */
|
||||
const TO_LOWER_BIT = 0b10_0000;
|
||||
function isNumber(code) {
|
||||
return code >= CharCodes.ZERO && code <= CharCodes.NINE;
|
||||
}
|
||||
function isHexadecimalCharacter(code) {
|
||||
return ((code >= CharCodes.UPPER_A && code <= CharCodes.UPPER_F) ||
|
||||
(code >= CharCodes.LOWER_A && code <= CharCodes.LOWER_F));
|
||||
}
|
||||
function isAsciiAlphaNumeric(code) {
|
||||
return ((code >= CharCodes.UPPER_A && code <= CharCodes.UPPER_Z) ||
|
||||
(code >= CharCodes.LOWER_A && code <= CharCodes.LOWER_Z) ||
|
||||
isNumber(code));
|
||||
}
|
||||
/**
|
||||
* Checks if the given character is a valid end character for an entity in an attribute.
|
||||
*
|
||||
* Attribute values that aren't terminated properly aren't parsed, and shouldn't lead to a parser error.
|
||||
* See the example in https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state
|
||||
* @param code Code point to decode.
|
||||
*/
|
||||
function isEntityInAttributeInvalidEnd(code) {
|
||||
return code === CharCodes.EQUALS || isAsciiAlphaNumeric(code);
|
||||
}
|
||||
var EntityDecoderState;
|
||||
(function (EntityDecoderState) {
|
||||
EntityDecoderState[EntityDecoderState["EntityStart"] = 0] = "EntityStart";
|
||||
EntityDecoderState[EntityDecoderState["NumericStart"] = 1] = "NumericStart";
|
||||
EntityDecoderState[EntityDecoderState["NumericDecimal"] = 2] = "NumericDecimal";
|
||||
EntityDecoderState[EntityDecoderState["NumericHex"] = 3] = "NumericHex";
|
||||
EntityDecoderState[EntityDecoderState["NamedEntity"] = 4] = "NamedEntity";
|
||||
})(EntityDecoderState || (EntityDecoderState = {}));
|
||||
/**
|
||||
* Decoding mode for named entities.
|
||||
*/
|
||||
export var DecodingMode;
|
||||
(function (DecodingMode) {
|
||||
/** Entities in text nodes that can end with any character. */
|
||||
DecodingMode[DecodingMode["Legacy"] = 0] = "Legacy";
|
||||
/** Only allow entities terminated with a semicolon. */
|
||||
DecodingMode[DecodingMode["Strict"] = 1] = "Strict";
|
||||
/** Entities in attributes have limitations on ending characters. */
|
||||
DecodingMode[DecodingMode["Attribute"] = 2] = "Attribute";
|
||||
})(DecodingMode || (DecodingMode = {}));
|
||||
/**
|
||||
* Token decoder with support of writing partial entities.
|
||||
*/
|
||||
export class EntityDecoder {
|
||||
decodeTree;
|
||||
emitCodePoint;
|
||||
errors;
|
||||
constructor(
|
||||
/** The tree used to decode entities. */
|
||||
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: False positive
|
||||
decodeTree,
|
||||
/**
|
||||
* The function that is called when a codepoint is decoded.
|
||||
*
|
||||
* For multi-byte named entities, this will be called multiple times,
|
||||
* with the second codepoint, and the same `consumed` value.
|
||||
* @param codepoint The decoded codepoint.
|
||||
* @param consumed The number of bytes consumed by the decoder.
|
||||
*/
|
||||
emitCodePoint,
|
||||
/** An object that is used to produce errors. */
|
||||
errors) {
|
||||
this.decodeTree = decodeTree;
|
||||
this.emitCodePoint = emitCodePoint;
|
||||
this.errors = errors;
|
||||
}
|
||||
/** The current state of the decoder. */
|
||||
state = EntityDecoderState.EntityStart;
|
||||
/** Characters that were consumed while parsing an entity. */
|
||||
consumed = 1;
|
||||
/**
|
||||
* The result of the entity.
|
||||
*
|
||||
* Either the result index of a numeric entity, or the codepoint of a
|
||||
* numeric entity.
|
||||
*/
|
||||
result = 0;
|
||||
/** The current index in the decode tree. */
|
||||
treeIndex = 0;
|
||||
/** The number of characters that were consumed in excess. */
|
||||
excess = 1;
|
||||
/** The mode in which the decoder is operating. */
|
||||
decodeMode = DecodingMode.Strict;
|
||||
/** The number of characters that have been consumed in the current run. */
|
||||
runConsumed = 0;
|
||||
/**
|
||||
* Resets the instance to make it reusable.
|
||||
* @param decodeMode Entity decoding mode to use.
|
||||
*/
|
||||
startEntity(decodeMode) {
|
||||
this.decodeMode = decodeMode;
|
||||
this.state = EntityDecoderState.EntityStart;
|
||||
this.result = 0;
|
||||
this.treeIndex = 0;
|
||||
this.excess = 1;
|
||||
this.consumed = 1;
|
||||
this.runConsumed = 0;
|
||||
}
|
||||
/**
|
||||
* Write an entity to the decoder. This can be called multiple times with partial entities.
|
||||
* If the entity is incomplete, the decoder will return -1.
|
||||
*
|
||||
* Mirrors the implementation of `getDecoder`, but with the ability to stop decoding if the
|
||||
* entity is incomplete, and resume when the next string is written.
|
||||
* @param input The string containing the entity (or a continuation of the entity).
|
||||
* @param offset The offset at which the entity begins. Should be 0 if this is not the first call.
|
||||
* @returns The number of characters that were consumed, or -1 if the entity is incomplete.
|
||||
*/
|
||||
write(input, offset) {
|
||||
switch (this.state) {
|
||||
case EntityDecoderState.EntityStart: {
|
||||
if (input.charCodeAt(offset) === CharCodes.NUM) {
|
||||
this.state = EntityDecoderState.NumericStart;
|
||||
this.consumed += 1;
|
||||
return this.stateNumericStart(input, offset + 1);
|
||||
}
|
||||
this.state = EntityDecoderState.NamedEntity;
|
||||
return this.stateNamedEntity(input, offset);
|
||||
}
|
||||
case EntityDecoderState.NumericStart: {
|
||||
return this.stateNumericStart(input, offset);
|
||||
}
|
||||
case EntityDecoderState.NumericDecimal: {
|
||||
return this.stateNumericDecimal(input, offset);
|
||||
}
|
||||
case EntityDecoderState.NumericHex: {
|
||||
return this.stateNumericHex(input, offset);
|
||||
}
|
||||
case EntityDecoderState.NamedEntity: {
|
||||
return this.stateNamedEntity(input, offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Switches between the numeric decimal and hexadecimal states.
|
||||
*
|
||||
* Equivalent to the `Numeric character reference state` in the HTML spec.
|
||||
* @param input The string containing the entity (or a continuation of the entity).
|
||||
* @param offset The current offset.
|
||||
* @returns The number of characters that were consumed, or -1 if the entity is incomplete.
|
||||
*/
|
||||
stateNumericStart(input, offset) {
|
||||
if (offset >= input.length) {
|
||||
return -1;
|
||||
}
|
||||
if ((input.charCodeAt(offset) | TO_LOWER_BIT) === CharCodes.LOWER_X) {
|
||||
this.state = EntityDecoderState.NumericHex;
|
||||
this.consumed += 1;
|
||||
return this.stateNumericHex(input, offset + 1);
|
||||
}
|
||||
this.state = EntityDecoderState.NumericDecimal;
|
||||
return this.stateNumericDecimal(input, offset);
|
||||
}
|
||||
/**
|
||||
* Parses a hexadecimal numeric entity.
|
||||
*
|
||||
* Equivalent to the `Hexademical character reference state` in the HTML spec.
|
||||
* @param input The string containing the entity (or a continuation of the entity).
|
||||
* @param offset The current offset.
|
||||
* @returns The number of characters that were consumed, or -1 if the entity is incomplete.
|
||||
*/
|
||||
stateNumericHex(input, offset) {
|
||||
while (offset < input.length) {
|
||||
const char = input.charCodeAt(offset);
|
||||
if (isNumber(char) || isHexadecimalCharacter(char)) {
|
||||
// Convert hex digit to value (0-15); 'a'/'A' -> 10.
|
||||
const digit = char <= CharCodes.NINE
|
||||
? char - CharCodes.ZERO
|
||||
: (char | TO_LOWER_BIT) - CharCodes.LOWER_A + 10;
|
||||
this.result = this.result * 16 + digit;
|
||||
this.consumed++;
|
||||
offset++;
|
||||
}
|
||||
else {
|
||||
return this.emitNumericEntity(char, 3);
|
||||
}
|
||||
}
|
||||
return -1; // Incomplete entity
|
||||
}
|
||||
/**
|
||||
* Parses a decimal numeric entity.
|
||||
*
|
||||
* Equivalent to the `Decimal character reference state` in the HTML spec.
|
||||
* @param input The string containing the entity (or a continuation of the entity).
|
||||
* @param offset The current offset.
|
||||
* @returns The number of characters that were consumed, or -1 if the entity is incomplete.
|
||||
*/
|
||||
stateNumericDecimal(input, offset) {
|
||||
while (offset < input.length) {
|
||||
const char = input.charCodeAt(offset);
|
||||
if (isNumber(char)) {
|
||||
this.result = this.result * 10 + (char - CharCodes.ZERO);
|
||||
this.consumed++;
|
||||
offset++;
|
||||
}
|
||||
else {
|
||||
return this.emitNumericEntity(char, 2);
|
||||
}
|
||||
}
|
||||
return -1; // Incomplete entity
|
||||
}
|
||||
/**
|
||||
* Validate and emit a numeric entity.
|
||||
*
|
||||
* Implements the logic from the `Hexademical character reference start
|
||||
* state` and `Numeric character reference end state` in the HTML spec.
|
||||
* @param lastCp The last code point of the entity. Used to see if the
|
||||
* entity was terminated with a semicolon.
|
||||
* @param expectedLength The minimum number of characters that should be
|
||||
* consumed. Used to validate that at least one digit
|
||||
* was consumed.
|
||||
* @returns The number of characters that were consumed.
|
||||
*/
|
||||
emitNumericEntity(lastCp, expectedLength) {
|
||||
// Ensure we consumed at least one digit.
|
||||
if (this.consumed <= expectedLength) {
|
||||
this.errors?.absenceOfDigitsInNumericCharacterReference(this.consumed);
|
||||
return 0;
|
||||
}
|
||||
// Figure out if this is a legit end of the entity
|
||||
if (lastCp === CharCodes.SEMI) {
|
||||
this.consumed += 1;
|
||||
}
|
||||
else if (this.decodeMode === DecodingMode.Strict) {
|
||||
return 0;
|
||||
}
|
||||
this.emitCodePoint(replaceCodePoint(this.result), this.consumed);
|
||||
if (this.errors) {
|
||||
if (lastCp !== CharCodes.SEMI) {
|
||||
this.errors.missingSemicolonAfterCharacterReference();
|
||||
}
|
||||
this.errors.validateNumericCharacterReference(this.result);
|
||||
}
|
||||
return this.consumed;
|
||||
}
|
||||
/**
|
||||
* Parses a named entity.
|
||||
*
|
||||
* Equivalent to the `Named character reference state` in the HTML spec.
|
||||
* @param input The string containing the entity (or a continuation of the entity).
|
||||
* @param offset The current offset.
|
||||
* @returns The number of characters that were consumed, or -1 if the entity is incomplete.
|
||||
*/
|
||||
stateNamedEntity(input, offset) {
|
||||
const { decodeTree } = this;
|
||||
let current = decodeTree[this.treeIndex];
|
||||
// The length is the number of bytes of the value, including the current byte.
|
||||
let valueLength = (current & BinTrieFlags.VALUE_LENGTH) >> 14;
|
||||
while (offset < input.length) {
|
||||
// Handle compact runs (possibly inline): valueLength == 0 and SEMI_REQUIRED bit set.
|
||||
if (valueLength === 0 && (current & BinTrieFlags.FLAG13) !== 0) {
|
||||
const runLength = (current & BinTrieFlags.BRANCH_LENGTH) >> 7; /* 2..63 */
|
||||
// If we are starting a run, check the first char.
|
||||
if (this.runConsumed === 0) {
|
||||
const firstChar = current & BinTrieFlags.JUMP_TABLE;
|
||||
if (input.charCodeAt(offset) !== firstChar) {
|
||||
return this.result === 0
|
||||
? 0
|
||||
: this.emitNotTerminatedNamedEntity();
|
||||
}
|
||||
offset++;
|
||||
this.excess++;
|
||||
this.runConsumed++;
|
||||
}
|
||||
// Check remaining characters in the run.
|
||||
while (this.runConsumed < runLength) {
|
||||
if (offset >= input.length) {
|
||||
return -1;
|
||||
}
|
||||
const charIndexInPacked = this.runConsumed - 1;
|
||||
const packedWord = decodeTree[this.treeIndex + 1 + (charIndexInPacked >> 1)];
|
||||
const expectedChar = charIndexInPacked % 2 === 0
|
||||
? packedWord & 0xff
|
||||
: (packedWord >> 8) & 0xff;
|
||||
if (input.charCodeAt(offset) !== expectedChar) {
|
||||
this.runConsumed = 0;
|
||||
return this.result === 0
|
||||
? 0
|
||||
: this.emitNotTerminatedNamedEntity();
|
||||
}
|
||||
offset++;
|
||||
this.excess++;
|
||||
this.runConsumed++;
|
||||
}
|
||||
this.runConsumed = 0;
|
||||
this.treeIndex += 1 + (runLength >> 1);
|
||||
current = decodeTree[this.treeIndex];
|
||||
valueLength = (current & BinTrieFlags.VALUE_LENGTH) >> 14;
|
||||
}
|
||||
if (offset >= input.length)
|
||||
break;
|
||||
const char = input.charCodeAt(offset);
|
||||
/*
|
||||
* Implicit semicolon handling for nodes that require a semicolon but
|
||||
* don't have an explicit ';' branch stored in the trie. If we have
|
||||
* a value on the current node, it requires a semicolon, and the
|
||||
* current input character is a semicolon, emit the entity using the
|
||||
* current node (without descending further).
|
||||
*/
|
||||
if (char === CharCodes.SEMI &&
|
||||
valueLength !== 0 &&
|
||||
(current & BinTrieFlags.FLAG13) !== 0) {
|
||||
return this.emitNamedEntityData(this.treeIndex, valueLength, this.consumed + this.excess);
|
||||
}
|
||||
this.treeIndex = determineBranch(decodeTree, current, this.treeIndex + Math.max(1, valueLength), char);
|
||||
if (this.treeIndex < 0) {
|
||||
return this.result === 0 ||
|
||||
// If we are parsing an attribute
|
||||
(this.decodeMode === DecodingMode.Attribute &&
|
||||
// We shouldn't have consumed any characters after the entity,
|
||||
(valueLength === 0 ||
|
||||
// And there should be no invalid characters.
|
||||
isEntityInAttributeInvalidEnd(char)))
|
||||
? 0
|
||||
: this.emitNotTerminatedNamedEntity();
|
||||
}
|
||||
current = decodeTree[this.treeIndex];
|
||||
valueLength = (current & BinTrieFlags.VALUE_LENGTH) >> 14;
|
||||
// If the branch is a value, store it and continue
|
||||
if (valueLength !== 0) {
|
||||
// If the entity is terminated by a semicolon, we are done.
|
||||
if (char === CharCodes.SEMI) {
|
||||
return this.emitNamedEntityData(this.treeIndex, valueLength, this.consumed + this.excess);
|
||||
}
|
||||
// If we encounter a non-terminated (legacy) entity while parsing strictly, then ignore it.
|
||||
if (this.decodeMode !== DecodingMode.Strict &&
|
||||
(current & BinTrieFlags.FLAG13) === 0) {
|
||||
this.result = this.treeIndex;
|
||||
this.consumed += this.excess;
|
||||
this.excess = 0;
|
||||
}
|
||||
}
|
||||
// Increment offset & excess for next iteration
|
||||
offset++;
|
||||
this.excess++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
/**
|
||||
* Emit a named entity that was not terminated with a semicolon.
|
||||
* @returns The number of characters consumed.
|
||||
*/
|
||||
emitNotTerminatedNamedEntity() {
|
||||
const { result, decodeTree } = this;
|
||||
const valueLength = (decodeTree[result] & BinTrieFlags.VALUE_LENGTH) >> 14;
|
||||
this.emitNamedEntityData(result, valueLength, this.consumed);
|
||||
this.errors?.missingSemicolonAfterCharacterReference();
|
||||
return this.consumed;
|
||||
}
|
||||
/**
|
||||
* Emit a named entity.
|
||||
* @param result The index of the entity in the decode tree.
|
||||
* @param valueLength The number of bytes in the entity.
|
||||
* @param consumed The number of characters consumed.
|
||||
* @returns The number of characters consumed.
|
||||
*/
|
||||
emitNamedEntityData(result, valueLength, consumed) {
|
||||
const { decodeTree } = this;
|
||||
this.emitCodePoint(valueLength === 1
|
||||
? decodeTree[result] &
|
||||
~(BinTrieFlags.VALUE_LENGTH | BinTrieFlags.FLAG13)
|
||||
: decodeTree[result + 1], consumed);
|
||||
if (valueLength === 3) {
|
||||
// For multi-byte values, we need to emit the second byte.
|
||||
this.emitCodePoint(decodeTree[result + 2], consumed);
|
||||
}
|
||||
return consumed;
|
||||
}
|
||||
/**
|
||||
* Signal to the parser that the end of the input was reached.
|
||||
*
|
||||
* Remaining data will be emitted and relevant errors will be produced.
|
||||
* @returns The number of characters consumed.
|
||||
*/
|
||||
end() {
|
||||
switch (this.state) {
|
||||
case EntityDecoderState.NamedEntity: {
|
||||
// Emit a named entity if we have one.
|
||||
return this.result !== 0 &&
|
||||
(this.decodeMode !== DecodingMode.Attribute ||
|
||||
this.result === this.treeIndex)
|
||||
? this.emitNotTerminatedNamedEntity()
|
||||
: 0;
|
||||
}
|
||||
// Otherwise, emit a numeric entity if we have one.
|
||||
case EntityDecoderState.NumericDecimal: {
|
||||
return this.emitNumericEntity(0, 2);
|
||||
}
|
||||
case EntityDecoderState.NumericHex: {
|
||||
return this.emitNumericEntity(0, 3);
|
||||
}
|
||||
case EntityDecoderState.NumericStart: {
|
||||
this.errors?.absenceOfDigitsInNumericCharacterReference(this.consumed);
|
||||
return 0;
|
||||
}
|
||||
case EntityDecoderState.EntityStart: {
|
||||
// Return 0 if we have no entity.
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Creates a function that decodes entities in a string.
|
||||
* @param decodeTree The decode tree.
|
||||
* @returns A function that decodes entities in a string.
|
||||
*/
|
||||
function getDecoder(decodeTree) {
|
||||
let returnValue = "";
|
||||
const decoder = new EntityDecoder(decodeTree, (data) => (returnValue += String.fromCodePoint(data)));
|
||||
return function decodeWithTrie(input, decodeMode) {
|
||||
let lastIndex = 0;
|
||||
let offset = 0;
|
||||
while ((offset = input.indexOf("&", offset)) >= 0) {
|
||||
returnValue += input.slice(lastIndex, offset);
|
||||
decoder.startEntity(decodeMode);
|
||||
const length = decoder.write(input,
|
||||
// Skip the "&"
|
||||
offset + 1);
|
||||
if (length < 0) {
|
||||
lastIndex = offset + decoder.end();
|
||||
break;
|
||||
}
|
||||
lastIndex = offset + length;
|
||||
// If `length` is 0, skip the current `&` and continue.
|
||||
offset = length === 0 ? lastIndex + 1 : lastIndex;
|
||||
}
|
||||
const result = returnValue + input.slice(lastIndex);
|
||||
// Make sure we don't keep a reference to the final string.
|
||||
returnValue = "";
|
||||
return result;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Determines the branch of the current node that is taken given the current
|
||||
* character. This function is used to traverse the trie.
|
||||
* @param decodeTree The trie.
|
||||
* @param current The current node.
|
||||
* @param nodeIndex Index immediately after the current node header.
|
||||
* @param char The current character.
|
||||
* @returns The index of the next node, or -1 if no branch is taken.
|
||||
*/
|
||||
export function determineBranch(decodeTree, current, nodeIndex, char) {
|
||||
const branchCount = (current & BinTrieFlags.BRANCH_LENGTH) >> 7;
|
||||
const jumpOffset = current & BinTrieFlags.JUMP_TABLE;
|
||||
// Case 1: Single branch encoded in jump offset
|
||||
if (branchCount === 0) {
|
||||
return jumpOffset !== 0 && char === jumpOffset ? nodeIndex : -1;
|
||||
}
|
||||
// Case 2: Multiple branches encoded in jump table
|
||||
if (jumpOffset) {
|
||||
const value = char - jumpOffset;
|
||||
return value < 0 || value >= branchCount
|
||||
? -1
|
||||
: decodeTree[nodeIndex + value] - 1;
|
||||
}
|
||||
// Case 3: Multiple branches encoded in packed dictionary (two keys per uint16)
|
||||
const packedKeySlots = (branchCount + 1) >> 1;
|
||||
/*
|
||||
* Treat packed keys as a virtual sorted array of length `branchCount`.
|
||||
* Key(i) = low byte for even i, high byte for odd i in slot i>>1.
|
||||
*/
|
||||
let lo = 0;
|
||||
let hi = branchCount - 1;
|
||||
while (lo <= hi) {
|
||||
const mid = (lo + hi) >>> 1;
|
||||
const slot = mid >> 1;
|
||||
const packed = decodeTree[nodeIndex + slot];
|
||||
const midKey = (packed >> ((mid & 1) * 8)) & 0xff;
|
||||
if (midKey < char) {
|
||||
lo = mid + 1;
|
||||
}
|
||||
else if (midKey > char) {
|
||||
hi = mid - 1;
|
||||
}
|
||||
else {
|
||||
return decodeTree[nodeIndex + packedKeySlots + mid];
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
const htmlDecoder = /* #__PURE__ */ getDecoder(htmlDecodeTree);
|
||||
const xmlDecoder = /* #__PURE__ */ getDecoder(xmlDecodeTree);
|
||||
/**
|
||||
* Decodes an HTML string.
|
||||
* @param htmlString The string to decode.
|
||||
* @param mode The decoding mode.
|
||||
* @returns The decoded string.
|
||||
*/
|
||||
export function decodeHTML(htmlString, mode = DecodingMode.Legacy) {
|
||||
return htmlDecoder(htmlString, mode);
|
||||
}
|
||||
/**
|
||||
* Decodes an HTML string in an attribute.
|
||||
* @param htmlAttribute The string to decode.
|
||||
* @returns The decoded string.
|
||||
*/
|
||||
export function decodeHTMLAttribute(htmlAttribute) {
|
||||
return htmlDecoder(htmlAttribute, DecodingMode.Attribute);
|
||||
}
|
||||
/**
|
||||
* Decodes an HTML string, requiring all entities to be terminated by a semicolon.
|
||||
* @param htmlString The string to decode.
|
||||
* @returns The decoded string.
|
||||
*/
|
||||
export function decodeHTMLStrict(htmlString) {
|
||||
return htmlDecoder(htmlString, DecodingMode.Strict);
|
||||
}
|
||||
/**
|
||||
* Decodes an XML string, requiring all entities to be terminated by a semicolon.
|
||||
* @param xmlString The string to decode.
|
||||
* @returns The decoded string.
|
||||
*/
|
||||
export function decodeXML(xmlString) {
|
||||
return xmlDecoder(xmlString, DecodingMode.Strict);
|
||||
}
|
||||
export { replaceCodePoint } from "./decode-codepoint.js";
|
||||
// Re-export for use by eg. htmlparser2
|
||||
export { htmlDecodeTree } from "./generated/decode-data-html.js";
|
||||
export { xmlDecodeTree } from "./generated/decode-data-xml.js";
|
||||
//# sourceMappingURL=decode.js.map
|
||||
1
node_modules/entities/dist/decode.js.map
generated
vendored
1
node_modules/entities/dist/decode.js.map
generated
vendored
File diff suppressed because one or more lines are too long
24
node_modules/entities/dist/encode.d.ts
generated
vendored
24
node_modules/entities/dist/encode.d.ts
generated
vendored
@@ -1,24 +0,0 @@
|
||||
/**
|
||||
* Encodes all characters in the input using HTML entities. This includes
|
||||
* characters that are valid ASCII characters in HTML documents, such as `#`.
|
||||
*
|
||||
* To get a more compact output, consider using the `encodeNonAsciiHTML`
|
||||
* function, which will only encode characters that are not valid in HTML
|
||||
* documents, as well as non-ASCII characters.
|
||||
*
|
||||
* If a character has no equivalent entity, a numeric hexadecimal reference
|
||||
* (eg. `ü`) will be used.
|
||||
* @param input Input string to encode or decode.
|
||||
*/
|
||||
export declare function encodeHTML(input: string): string;
|
||||
/**
|
||||
* Encodes all non-ASCII characters, as well as characters not valid in HTML
|
||||
* documents using HTML entities. This function will not encode characters that
|
||||
* are valid in HTML documents, such as `#`.
|
||||
*
|
||||
* If a character has no equivalent entity, a numeric hexadecimal reference
|
||||
* (eg. `ü`) will be used.
|
||||
* @param input Input string to encode or decode.
|
||||
*/
|
||||
export declare function encodeNonAsciiHTML(input: string): string;
|
||||
//# sourceMappingURL=encode.d.ts.map
|
||||
1
node_modules/entities/dist/encode.d.ts.map
generated
vendored
1
node_modules/entities/dist/encode.d.ts.map
generated
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"encode.d.ts","sourceRoot":"","sources":["../src/encode.ts"],"names":[],"mappings":"AAeA;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD;AACD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAExD"}
|
||||
90
node_modules/entities/dist/encode.js
generated
vendored
90
node_modules/entities/dist/encode.js
generated
vendored
@@ -1,90 +0,0 @@
|
||||
import { getCodePoint, XML_BITSET_VALUE } from "./escape.js";
|
||||
import { htmlTrie } from "./generated/encode-html.js";
|
||||
/**
|
||||
* We store the characters to consider as a compact bitset for fast lookups.
|
||||
*/
|
||||
const HTML_BITSET = /* #__PURE__ */ new Uint32Array([
|
||||
0x16_00, // Bits for 09,0A,0C
|
||||
0xfc_00_ff_fe, // 32..63 -> 21-2D (minus space), 2E,2F,3A-3F
|
||||
0xf8_00_00_01, // 64..95 -> 40, 5B-5F
|
||||
0x38_00_00_01, // 96..127-> 60, 7B-7D
|
||||
]);
|
||||
const XML_BITSET = /* #__PURE__ */ new Uint32Array([0, XML_BITSET_VALUE, 0, 0]);
|
||||
/**
|
||||
* Encodes all characters in the input using HTML entities. This includes
|
||||
* characters that are valid ASCII characters in HTML documents, such as `#`.
|
||||
*
|
||||
* To get a more compact output, consider using the `encodeNonAsciiHTML`
|
||||
* function, which will only encode characters that are not valid in HTML
|
||||
* documents, as well as non-ASCII characters.
|
||||
*
|
||||
* If a character has no equivalent entity, a numeric hexadecimal reference
|
||||
* (eg. `ü`) will be used.
|
||||
* @param input Input string to encode or decode.
|
||||
*/
|
||||
export function encodeHTML(input) {
|
||||
return encodeHTMLTrieRe(HTML_BITSET, input);
|
||||
}
|
||||
/**
|
||||
* Encodes all non-ASCII characters, as well as characters not valid in HTML
|
||||
* documents using HTML entities. This function will not encode characters that
|
||||
* are valid in HTML documents, such as `#`.
|
||||
*
|
||||
* If a character has no equivalent entity, a numeric hexadecimal reference
|
||||
* (eg. `ü`) will be used.
|
||||
* @param input Input string to encode or decode.
|
||||
*/
|
||||
export function encodeNonAsciiHTML(input) {
|
||||
return encodeHTMLTrieRe(XML_BITSET, input);
|
||||
}
|
||||
function encodeHTMLTrieRe(bitset, input) {
|
||||
let out;
|
||||
let last = 0; // Start of the next untouched slice.
|
||||
const { length } = input;
|
||||
for (let index = 0; index < length; index++) {
|
||||
const char = input.charCodeAt(index);
|
||||
// Skip ASCII characters that don't need encoding
|
||||
if (char < 0x80 && !((bitset[char >>> 5] >>> char) & 1)) {
|
||||
continue;
|
||||
}
|
||||
if (out === undefined)
|
||||
out = input.substring(0, index);
|
||||
else if (last !== index)
|
||||
out += input.substring(last, index);
|
||||
let node = htmlTrie.get(char);
|
||||
if (typeof node === "object") {
|
||||
if (index + 1 < length) {
|
||||
const nextChar = input.charCodeAt(index + 1);
|
||||
const value = typeof node.next === "number"
|
||||
? node.next === nextChar
|
||||
? node.nextValue
|
||||
: undefined
|
||||
: node.next.get(nextChar);
|
||||
if (value !== undefined) {
|
||||
out += value;
|
||||
index++;
|
||||
last = index + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
node = node.value;
|
||||
}
|
||||
if (node === undefined) {
|
||||
const cp = getCodePoint(input, index);
|
||||
out += `&#x${cp.toString(16)};`;
|
||||
if (cp !== char)
|
||||
index++;
|
||||
last = index + 1;
|
||||
}
|
||||
else {
|
||||
out += node;
|
||||
last = index + 1;
|
||||
}
|
||||
}
|
||||
if (out === undefined)
|
||||
return input;
|
||||
if (last < length)
|
||||
out += input.substr(last);
|
||||
return out;
|
||||
}
|
||||
//# sourceMappingURL=encode.js.map
|
||||
1
node_modules/entities/dist/encode.js.map
generated
vendored
1
node_modules/entities/dist/encode.js.map
generated
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"encode.js","sourceRoot":"","sources":["../src/encode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAC;AAEtD;;GAEG;AACH,MAAM,WAAW,GAAG,eAAe,CAAC,IAAI,WAAW,CAAC;IAChD,OAAO,EAAE,oBAAoB;IAC7B,aAAa,EAAE,6CAA6C;IAC5D,aAAa,EAAE,sBAAsB;IACrC,aAAa,EAAE,sBAAsB;CACxC,CAAC,CAAC;AAEH,MAAM,UAAU,GAAG,eAAe,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,EAAE,gBAAgB,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AAEhF;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,UAAU,CAAC,KAAa;IACpC,OAAO,gBAAgB,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;AAChD,CAAC;AACD;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAa;IAC5C,OAAO,gBAAgB,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;AAC/C,CAAC;AAED,SAAS,gBAAgB,CAAC,MAAmB,EAAE,KAAa;IACxD,IAAI,GAAuB,CAAC;IAC5B,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,qCAAqC;IACnD,MAAM,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC;IAEzB,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC;QAC1C,MAAM,IAAI,GAAG,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QACrC,iDAAiD;QACjD,IAAI,IAAI,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,CAAC,KAAK,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;YACtD,SAAS;QACb,CAAC;QAED,IAAI,GAAG,KAAK,SAAS;YAAE,GAAG,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;aAClD,IAAI,IAAI,KAAK,KAAK;YAAE,GAAG,IAAI,KAAK,CAAC,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAE7D,IAAI,IAAI,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAE9B,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC3B,IAAI,KAAK,GAAG,CAAC,GAAG,MAAM,EAAE,CAAC;gBACrB,MAAM,QAAQ,GAAG,KAAK,CAAC,UAAU,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;gBAC7C,MAAM,KAAK,GACP,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ;oBACzB,CAAC,CAAC,IAAI,CAAC,IAAI,KAAK,QAAQ;wBACpB,CAAC,CAAC,IAAI,CAAC,SAAS;wBAChB,CAAC,CAAC,SAAS;oBACf,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;gBAElC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;oBACtB,GAAG,IAAI,KAAK,CAAC;oBACb,KAAK,EAAE,CAAC;oBACR,IAAI,GAAG,KAAK,GAAG,CAAC,CAAC;oBACjB,SAAS;gBACb,CAAC;YACL,CAAC;YACD,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC;QACtB,CAAC;QAED,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACrB,MAAM,EAAE,GAAG,YAAY,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;YACtC,GAAG,IAAI,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,GAAG,CAAC;YAChC,IAAI,EAAE,KAAK,IAAI;gBAAE,KAAK,EAAE,CAAC;YACzB,IAAI,GAAG,KAAK,GAAG,CAAC,CAAC;QACrB,CAAC;aAAM,CAAC;YACJ,GAAG,IAAI,IAAI,CAAC;YACZ,IAAI,GAAG,KAAK,GAAG,CAAC,CAAC;QACrB,CAAC;IACL,CAAC;IAED,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACpC,IAAI,IAAI,GAAG,MAAM;QAAE,GAAG,IAAI,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC7C,OAAO,GAAG,CAAC;AACf,CAAC"}
|
||||
48
node_modules/entities/dist/escape.d.ts
generated
vendored
48
node_modules/entities/dist/escape.d.ts
generated
vendored
@@ -1,48 +0,0 @@
|
||||
/**
|
||||
* Read a code point at a given index.
|
||||
* @param input Input string to encode or decode.
|
||||
* @param index Current read position in the input string.
|
||||
*/
|
||||
export declare const getCodePoint: (c: string, index: number) => number;
|
||||
/**
|
||||
* Bitset for ASCII characters that need to be escaped in XML.
|
||||
*/
|
||||
export declare const XML_BITSET_VALUE = 1342177476;
|
||||
/**
|
||||
* Encodes all non-ASCII characters, as well as characters not valid in XML
|
||||
* documents using XML entities. Uses a fast bitset scan instead of RegExp.
|
||||
*
|
||||
* If a character has no equivalent entity, a numeric hexadecimal reference
|
||||
* (eg. `ü`) will be used.
|
||||
* @param input Input string to encode or decode.
|
||||
*/
|
||||
export declare function encodeXML(input: string): string;
|
||||
/**
|
||||
* Encodes all non-ASCII characters, as well as characters not valid in XML
|
||||
* documents using numeric hexadecimal reference (eg. `ü`).
|
||||
*
|
||||
* Have a look at `escapeUTF8` if you want a more concise output at the expense
|
||||
* of reduced transportability.
|
||||
* @param data String to escape.
|
||||
*/
|
||||
export declare const escape: typeof encodeXML;
|
||||
/**
|
||||
* Encodes all characters not valid in XML documents using XML entities.
|
||||
*
|
||||
* Note that the output will be character-set dependent.
|
||||
* @param data String to escape.
|
||||
*/
|
||||
export declare const escapeUTF8: (data: string) => string;
|
||||
/**
|
||||
* Encodes all characters that have to be escaped in HTML attributes,
|
||||
* following {@link https://html.spec.whatwg.org/multipage/parsing.html#escapingString}.
|
||||
* @param data String to escape.
|
||||
*/
|
||||
export declare const escapeAttribute: (data: string) => string;
|
||||
/**
|
||||
* Encodes all characters that have to be escaped in HTML text,
|
||||
* following {@link https://html.spec.whatwg.org/multipage/parsing.html#escapingString}.
|
||||
* @param data String to escape.
|
||||
*/
|
||||
export declare const escapeText: (data: string) => string;
|
||||
//# sourceMappingURL=escape.d.ts.map
|
||||
1
node_modules/entities/dist/escape.d.ts.map
generated
vendored
1
node_modules/entities/dist/escape.d.ts.map
generated
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"escape.d.ts","sourceRoot":"","sources":["../src/escape.ts"],"names":[],"mappings":"AASA;;;;GAIG;AACH,eAAO,MAAM,YAAY,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,MAUlB,CAAC;AAExC;;GAEG;AACH,eAAO,MAAM,gBAAgB,aAAgB,CAAC;AAE9C;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAoC/C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,MAAM,EAAE,OAAO,SAAqB,CAAC;AAmClD;;;;;GAKG;AACH,eAAO,MAAM,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAG1C,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAQ3C,CAAC;AAEN;;;;GAIG;AACH,eAAO,MAAM,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAQ1C,CAAC"}
|
||||
132
node_modules/entities/dist/escape.js
generated
vendored
132
node_modules/entities/dist/escape.js
generated
vendored
@@ -1,132 +0,0 @@
|
||||
const xmlCodeMap = new Map([
|
||||
[34, """],
|
||||
[38, "&"],
|
||||
[39, "'"],
|
||||
[60, "<"],
|
||||
[62, ">"],
|
||||
]);
|
||||
// For compatibility with node < 4, we wrap `codePointAt`
|
||||
/**
|
||||
* Read a code point at a given index.
|
||||
* @param input Input string to encode or decode.
|
||||
* @param index Current read position in the input string.
|
||||
*/
|
||||
export const getCodePoint = typeof String.prototype.codePointAt === "function"
|
||||
? (input, index) => input.codePointAt(index)
|
||||
: // http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
|
||||
(c, index) => (c.charCodeAt(index) & 0xfc_00) === 0xd8_00
|
||||
? (c.charCodeAt(index) - 0xd8_00) * 0x4_00 +
|
||||
c.charCodeAt(index + 1) -
|
||||
0xdc_00 +
|
||||
0x1_00_00
|
||||
: c.charCodeAt(index);
|
||||
/**
|
||||
* Bitset for ASCII characters that need to be escaped in XML.
|
||||
*/
|
||||
export const XML_BITSET_VALUE = 0x50_00_00_c4; // 32..63 -> 34 ("),38 (&),39 ('),60 (<),62 (>)
|
||||
/**
|
||||
* Encodes all non-ASCII characters, as well as characters not valid in XML
|
||||
* documents using XML entities. Uses a fast bitset scan instead of RegExp.
|
||||
*
|
||||
* If a character has no equivalent entity, a numeric hexadecimal reference
|
||||
* (eg. `ü`) will be used.
|
||||
* @param input Input string to encode or decode.
|
||||
*/
|
||||
export function encodeXML(input) {
|
||||
let out;
|
||||
let last = 0;
|
||||
const { length } = input;
|
||||
for (let index = 0; index < length; index++) {
|
||||
const char = input.charCodeAt(index);
|
||||
// Check for ASCII chars that don't need escaping
|
||||
if (char < 0x80 &&
|
||||
(((XML_BITSET_VALUE >>> char) & 1) === 0 || char >= 64 || char < 32)) {
|
||||
continue;
|
||||
}
|
||||
if (out === undefined)
|
||||
out = input.substring(0, index);
|
||||
else if (last !== index)
|
||||
out += input.substring(last, index);
|
||||
if (char < 64) {
|
||||
// Known replacement
|
||||
out += xmlCodeMap.get(char);
|
||||
last = index + 1;
|
||||
continue;
|
||||
}
|
||||
// Non-ASCII: encode as numeric entity (handle surrogate pair)
|
||||
const cp = getCodePoint(input, index);
|
||||
out += `&#x${cp.toString(16)};`;
|
||||
if (cp !== char)
|
||||
index++; // Skip trailing surrogate
|
||||
last = index + 1;
|
||||
}
|
||||
if (out === undefined)
|
||||
return input;
|
||||
if (last < length)
|
||||
out += input.substr(last);
|
||||
return out;
|
||||
}
|
||||
/**
|
||||
* Encodes all non-ASCII characters, as well as characters not valid in XML
|
||||
* documents using numeric hexadecimal reference (eg. `ü`).
|
||||
*
|
||||
* Have a look at `escapeUTF8` if you want a more concise output at the expense
|
||||
* of reduced transportability.
|
||||
* @param data String to escape.
|
||||
*/
|
||||
export const escape = encodeXML;
|
||||
/**
|
||||
* Creates a function that escapes all characters matched by the given regular
|
||||
* expression using the given map of characters to escape to their entities.
|
||||
* @param regex Regular expression to match characters to escape.
|
||||
* @param map Map of characters to escape to their entities.
|
||||
* @returns Function that escapes all characters matched by the given regular
|
||||
* expression using the given map of characters to escape to their entities.
|
||||
*/
|
||||
function getEscaper(regex, map) {
|
||||
return function escape(data) {
|
||||
let match;
|
||||
let lastIndex = 0;
|
||||
let result = "";
|
||||
while ((match = regex.exec(data))) {
|
||||
if (lastIndex !== match.index) {
|
||||
result += data.substring(lastIndex, match.index);
|
||||
}
|
||||
// We know that this character will be in the map.
|
||||
result += map.get(match[0].charCodeAt(0));
|
||||
// Every match will be of length 1
|
||||
lastIndex = match.index + 1;
|
||||
}
|
||||
return result + data.substring(lastIndex);
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Encodes all characters not valid in XML documents using XML entities.
|
||||
*
|
||||
* Note that the output will be character-set dependent.
|
||||
* @param data String to escape.
|
||||
*/
|
||||
export const escapeUTF8 = /* #__PURE__ */ getEscaper(/["&'<>]/g, xmlCodeMap);
|
||||
/**
|
||||
* Encodes all characters that have to be escaped in HTML attributes,
|
||||
* following {@link https://html.spec.whatwg.org/multipage/parsing.html#escapingString}.
|
||||
* @param data String to escape.
|
||||
*/
|
||||
export const escapeAttribute =
|
||||
/* #__PURE__ */ getEscaper(/["&\u00A0]/g, new Map([
|
||||
[34, """],
|
||||
[38, "&"],
|
||||
[160, " "],
|
||||
]));
|
||||
/**
|
||||
* Encodes all characters that have to be escaped in HTML text,
|
||||
* following {@link https://html.spec.whatwg.org/multipage/parsing.html#escapingString}.
|
||||
* @param data String to escape.
|
||||
*/
|
||||
export const escapeText = /* #__PURE__ */ getEscaper(/[&<>\u00A0]/g, new Map([
|
||||
[38, "&"],
|
||||
[60, "<"],
|
||||
[62, ">"],
|
||||
[160, " "],
|
||||
]));
|
||||
//# sourceMappingURL=escape.js.map
|
||||
1
node_modules/entities/dist/escape.js.map
generated
vendored
1
node_modules/entities/dist/escape.js.map
generated
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"escape.js","sourceRoot":"","sources":["../src/escape.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC;IACvB,CAAC,EAAE,EAAE,QAAQ,CAAC;IACd,CAAC,EAAE,EAAE,OAAO,CAAC;IACb,CAAC,EAAE,EAAE,QAAQ,CAAC;IACd,CAAC,EAAE,EAAE,MAAM,CAAC;IACZ,CAAC,EAAE,EAAE,MAAM,CAAC;CACf,CAAC,CAAC;AAEH,yDAAyD;AACzD;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GACrB,OAAO,MAAM,CAAC,SAAS,CAAC,WAAW,KAAK,UAAU;IAC9C,CAAC,CAAC,CAAC,KAAa,EAAE,KAAa,EAAU,EAAE,CAAC,KAAK,CAAC,WAAW,CAAC,KAAK,CAAE;IACrE,CAAC,CAAC,uEAAuE;QACvE,CAAC,CAAS,EAAE,KAAa,EAAU,EAAE,CACjC,CAAC,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,KAAK,OAAO;YACvC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,GAAG,MAAM;gBACxC,CAAC,CAAC,UAAU,CAAC,KAAK,GAAG,CAAC,CAAC;gBACvB,OAAO;gBACP,SAAS;YACX,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;AAExC;;GAEG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,aAAa,CAAC,CAAC,+CAA+C;AAE9F;;;;;;;GAOG;AACH,MAAM,UAAU,SAAS,CAAC,KAAa;IACnC,IAAI,GAAuB,CAAC;IAC5B,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,MAAM,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC;IAEzB,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC;QAC1C,MAAM,IAAI,GAAG,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QAErC,iDAAiD;QACjD,IACI,IAAI,GAAG,IAAI;YACX,CAAC,CAAC,CAAC,gBAAgB,KAAK,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,IAAI,IAAI,IAAI,EAAE,IAAI,IAAI,GAAG,EAAE,CAAC,EACtE,CAAC;YACC,SAAS;QACb,CAAC;QAED,IAAI,GAAG,KAAK,SAAS;YAAE,GAAG,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;aAClD,IAAI,IAAI,KAAK,KAAK;YAAE,GAAG,IAAI,KAAK,CAAC,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAE7D,IAAI,IAAI,GAAG,EAAE,EAAE,CAAC;YACZ,oBAAoB;YACpB,GAAG,IAAI,UAAU,CAAC,GAAG,CAAC,IAAI,CAAE,CAAC;YAC7B,IAAI,GAAG,KAAK,GAAG,CAAC,CAAC;YACjB,SAAS;QACb,CAAC;QAED,8DAA8D;QAC9D,MAAM,EAAE,GAAG,YAAY,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QACtC,GAAG,IAAI,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,GAAG,CAAC;QAChC,IAAI,EAAE,KAAK,IAAI;YAAE,KAAK,EAAE,CAAC,CAAC,0BAA0B;QACpD,IAAI,GAAG,KAAK,GAAG,CAAC,CAAC;IACrB,CAAC;IAED,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACpC,IAAI,IAAI,GAAG,MAAM;QAAE,GAAG,IAAI,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC7C,OAAO,GAAG,CAAC;AACf,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,MAAM,GAAqB,SAAS,CAAC;AAElD;;;;;;;GAOG;AACH,SAAS,UAAU,CACf,KAAa,EACb,GAAwB;IAExB,OAAO,SAAS,MAAM,CAAC,IAAY;QAC/B,IAAI,KAA6B,CAAC;QAClC,IAAI,SAAS,GAAG,CAAC,CAAC;QAClB,IAAI,MAAM,GAAG,EAAE,CAAC;QAEhB,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;YAChC,IAAI,SAAS,KAAK,KAAK,CAAC,KAAK,EAAE,CAAC;gBAC5B,MAAM,IAAI,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;YACrD,CAAC;YAED,kDAAkD;YAClD,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAE,CAAC;YAE3C,kCAAkC;YAClC,SAAS,GAAG,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC;QAChC,CAAC;QAED,OAAO,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IAC9C,CAAC,CAAC;AACN,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,UAAU,GAA6B,eAAe,CAAC,UAAU,CAC1E,UAAU,EACV,UAAU,CACb,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,eAAe;AACxB,eAAe,CAAC,UAAU,CACtB,aAAa,EACb,IAAI,GAAG,CAAC;IACJ,CAAC,EAAE,EAAE,QAAQ,CAAC;IACd,CAAC,EAAE,EAAE,OAAO,CAAC;IACb,CAAC,GAAG,EAAE,QAAQ,CAAC;CAClB,CAAC,CACL,CAAC;AAEN;;;;GAIG;AACH,MAAM,CAAC,MAAM,UAAU,GAA6B,eAAe,CAAC,UAAU,CAC1E,cAAc,EACd,IAAI,GAAG,CAAC;IACJ,CAAC,EAAE,EAAE,OAAO,CAAC;IACb,CAAC,EAAE,EAAE,MAAM,CAAC;IACZ,CAAC,EAAE,EAAE,MAAM,CAAC;IACZ,CAAC,GAAG,EAAE,QAAQ,CAAC;CAClB,CAAC,CACL,CAAC"}
|
||||
3
node_modules/entities/dist/generated/decode-data-html.d.ts
generated
vendored
3
node_modules/entities/dist/generated/decode-data-html.d.ts
generated
vendored
@@ -1,3 +0,0 @@
|
||||
/** Packed HTML decode trie data. */
|
||||
export declare const htmlDecodeTree: Uint16Array;
|
||||
//# sourceMappingURL=decode-data-html.d.ts.map
|
||||
1
node_modules/entities/dist/generated/decode-data-html.d.ts.map
generated
vendored
1
node_modules/entities/dist/generated/decode-data-html.d.ts.map
generated
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"decode-data-html.d.ts","sourceRoot":"","sources":["../../src/generated/decode-data-html.ts"],"names":[],"mappings":"AAGA,oCAAoC;AACpC,eAAO,MAAM,cAAc,EAAE,WAE5B,CAAC"}
|
||||
5
node_modules/entities/dist/generated/decode-data-html.js
generated
vendored
5
node_modules/entities/dist/generated/decode-data-html.js
generated
vendored
File diff suppressed because one or more lines are too long
1
node_modules/entities/dist/generated/decode-data-html.js.map
generated
vendored
1
node_modules/entities/dist/generated/decode-data-html.js.map
generated
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"decode-data-html.js","sourceRoot":"","sources":["../../src/generated/decode-data-html.ts"],"names":[],"mappings":"AAAA,8CAA8C;AAE9C,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAC5D,oCAAoC;AACpC,MAAM,CAAC,MAAM,cAAc,GAAgB,eAAe,CAAC,YAAY,CACnE,08+BAA08+B,CAC78+B,CAAC"}
|
||||
3
node_modules/entities/dist/generated/decode-data-xml.d.ts
generated
vendored
3
node_modules/entities/dist/generated/decode-data-xml.d.ts
generated
vendored
@@ -1,3 +0,0 @@
|
||||
/** Packed XML decode trie data. */
|
||||
export declare const xmlDecodeTree: Uint16Array;
|
||||
//# sourceMappingURL=decode-data-xml.d.ts.map
|
||||
1
node_modules/entities/dist/generated/decode-data-xml.d.ts.map
generated
vendored
1
node_modules/entities/dist/generated/decode-data-xml.d.ts.map
generated
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"decode-data-xml.d.ts","sourceRoot":"","sources":["../../src/generated/decode-data-xml.ts"],"names":[],"mappings":"AAGA,mCAAmC;AACnC,eAAO,MAAM,aAAa,EAAE,WAE3B,CAAC"}
|
||||
5
node_modules/entities/dist/generated/decode-data-xml.js
generated
vendored
5
node_modules/entities/dist/generated/decode-data-xml.js
generated
vendored
@@ -1,5 +0,0 @@
|
||||
// Generated using scripts/write-decode-map.ts
|
||||
import { decodeBase64 } from "../internal/decode-shared.js";
|
||||
/** Packed XML decode trie data. */
|
||||
export const xmlDecodeTree = /* #__PURE__ */ decodeBase64("AAJhZ2xxBwARABMAFQBtAg0AAAAAAA8AcAAmYG8AcwAnYHQAPmB0ADxg9SFvdCJg");
|
||||
//# sourceMappingURL=decode-data-xml.js.map
|
||||
1
node_modules/entities/dist/generated/decode-data-xml.js.map
generated
vendored
1
node_modules/entities/dist/generated/decode-data-xml.js.map
generated
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"decode-data-xml.js","sourceRoot":"","sources":["../../src/generated/decode-data-xml.ts"],"names":[],"mappings":"AAAA,8CAA8C;AAE9C,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAC5D,mCAAmC;AACnC,MAAM,CAAC,MAAM,aAAa,GAAgB,eAAe,CAAC,YAAY,CAClE,kEAAkE,CACrE,CAAC"}
|
||||
5
node_modules/entities/dist/generated/encode-html.d.ts
generated
vendored
5
node_modules/entities/dist/generated/encode-html.d.ts
generated
vendored
@@ -1,5 +0,0 @@
|
||||
import { type EncodeTrieNode } from "../internal/encode-shared.js";
|
||||
/** Compact serialized HTML encode trie (intended to stay small & JS engine friendly) */
|
||||
/** HTML entity encode trie. */
|
||||
export declare const htmlTrie: Map<number, EncodeTrieNode>;
|
||||
//# sourceMappingURL=encode-html.d.ts.map
|
||||
1
node_modules/entities/dist/generated/encode-html.d.ts.map
generated
vendored
1
node_modules/entities/dist/generated/encode-html.d.ts.map
generated
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"encode-html.d.ts","sourceRoot":"","sources":["../../src/generated/encode-html.ts"],"names":[],"mappings":"AAOA,OAAO,EACH,KAAK,cAAc,EAEtB,MAAM,8BAA8B,CAAC;AAEtC,wFAAwF;AACxF,+BAA+B;AAC/B,eAAO,MAAM,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAG5C,CAAC"}
|
||||
12
node_modules/entities/dist/generated/encode-html.js
generated
vendored
12
node_modules/entities/dist/generated/encode-html.js
generated
vendored
File diff suppressed because one or more lines are too long
1
node_modules/entities/dist/generated/encode-html.js.map
generated
vendored
1
node_modules/entities/dist/generated/encode-html.js.map
generated
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"encode-html.js","sourceRoot":"","sources":["../../src/generated/encode-html.ts"],"names":[],"mappings":"AAAA,8CAA8C;AAC9C,qFAAqF;AACrF,iFAAiF;AACjF,gEAAgE;AAChE,uFAAuF;AACvF,oGAAoG;AAEpG,OAAO,EAEH,eAAe,GAClB,MAAM,8BAA8B,CAAC;AAEtC,wFAAwF;AACxF,+BAA+B;AAC/B,MAAM,CAAC,MAAM,QAAQ;AACjB,eAAe,CAAC,eAAe,CAC3B,u2YAAu2Y,CAC12Y,CAAC"}
|
||||
89
node_modules/entities/dist/index.d.ts
generated
vendored
89
node_modules/entities/dist/index.d.ts
generated
vendored
@@ -1,89 +0,0 @@
|
||||
import { type DecodingMode } from "./decode.js";
|
||||
/** The level of entities to support. */
|
||||
export declare enum EntityLevel {
|
||||
/** Support only XML entities. */
|
||||
XML = 0,
|
||||
/** Support HTML entities, which are a superset of XML entities. */
|
||||
HTML = 1
|
||||
}
|
||||
/**
|
||||
* Encoding strategy used by `encode`.
|
||||
*/
|
||||
export declare enum EncodingMode {
|
||||
/**
|
||||
* The output is UTF-8 encoded. Only characters that need escaping within
|
||||
* XML will be escaped.
|
||||
*/
|
||||
UTF8 = 0,
|
||||
/**
|
||||
* The output consists only of ASCII characters. Characters that need
|
||||
* escaping within HTML, and characters that aren't ASCII characters will
|
||||
* be escaped.
|
||||
*/
|
||||
ASCII = 1,
|
||||
/**
|
||||
* Encode all characters that have an equivalent entity, as well as all
|
||||
* characters that are not ASCII characters.
|
||||
*/
|
||||
Extensive = 2,
|
||||
/**
|
||||
* Encode all characters that have to be escaped in HTML attributes,
|
||||
* following {@link https://html.spec.whatwg.org/multipage/parsing.html#escapingString}.
|
||||
*/
|
||||
Attribute = 3,
|
||||
/**
|
||||
* Encode all characters that have to be escaped in HTML text,
|
||||
* following {@link https://html.spec.whatwg.org/multipage/parsing.html#escapingString}.
|
||||
*/
|
||||
Text = 4
|
||||
}
|
||||
/**
|
||||
* Options for `decode`.
|
||||
*/
|
||||
export interface DecodingOptions {
|
||||
/**
|
||||
* The level of entities to support.
|
||||
* @default {@link EntityLevel.XML}
|
||||
*/
|
||||
level?: EntityLevel;
|
||||
/**
|
||||
* Decoding mode. If `Legacy`, will support legacy entities not terminated
|
||||
* with a semicolon (`;`).
|
||||
*
|
||||
* Always `Strict` for XML. For HTML, set this to `true` if you are parsing
|
||||
* an attribute value.
|
||||
* @default {@link DecodingMode.Legacy}
|
||||
*/
|
||||
mode?: DecodingMode | undefined;
|
||||
}
|
||||
/**
|
||||
* Decodes a string with entities.
|
||||
* @param input String to decode.
|
||||
* @param options Decoding options.
|
||||
*/
|
||||
export declare function decode(input: string, options?: DecodingOptions | EntityLevel): string;
|
||||
/**
|
||||
* Options for `encode`.
|
||||
*/
|
||||
export interface EncodingOptions {
|
||||
/**
|
||||
* The level of entities to support.
|
||||
* @default {@link EntityLevel.XML}
|
||||
*/
|
||||
level?: EntityLevel;
|
||||
/**
|
||||
* Output format.
|
||||
* @default {@link EncodingMode.Extensive}
|
||||
*/
|
||||
mode?: EncodingMode;
|
||||
}
|
||||
/**
|
||||
* Encodes a string with entities.
|
||||
* @param input String to encode.
|
||||
* @param options Encoding options.
|
||||
*/
|
||||
export declare function encode(input: string, options?: EncodingOptions | EntityLevel): string;
|
||||
export { DecodingMode, decodeHTML, decodeHTMLAttribute, decodeHTMLStrict, decodeXML, decodeXML as decodeXMLStrict, EntityDecoder, } from "./decode.js";
|
||||
export { encodeHTML, encodeNonAsciiHTML, } from "./encode.js";
|
||||
export { encodeXML, escape, escapeAttribute, escapeText, escapeUTF8, } from "./escape.js";
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
1
node_modules/entities/dist/index.d.ts.map
generated
vendored
1
node_modules/entities/dist/index.d.ts.map
generated
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,YAAY,EAAyB,MAAM,aAAa,CAAC;AASvE,wCAAwC;AACxC,oBAAY,WAAW;IACnB,iCAAiC;IACjC,GAAG,IAAI;IACP,mEAAmE;IACnE,IAAI,IAAI;CACX;AAED;;GAEG;AACH,oBAAY,YAAY;IACpB;;;OAGG;IACH,IAAI,IAAA;IACJ;;;;OAIG;IACH,KAAK,IAAA;IACL;;;OAGG;IACH,SAAS,IAAA;IACT;;;OAGG;IACH,SAAS,IAAA;IACT;;;OAGG;IACH,IAAI,IAAA;CACP;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC5B;;;OAGG;IACH,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,YAAY,GAAG,SAAS,CAAC;CACnC;AAED;;;;GAIG;AACH,wBAAgB,MAAM,CAClB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,eAAe,GAAG,WAA6B,GACzD,MAAM,CASR;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC5B;;;OAGG;IACH,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB;;;OAGG;IACH,IAAI,CAAC,EAAE,YAAY,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAgB,MAAM,CAClB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,eAAe,GAAG,WAA6B,GACzD,MAAM,CA2BR;AAED,OAAO,EACH,YAAY,EACZ,UAAU,EACV,mBAAmB,EACnB,gBAAgB,EAChB,SAAS,EACT,SAAS,IAAI,eAAe,EAC5B,aAAa,GAChB,MAAM,aAAa,CAAC;AAErB,OAAO,EACH,UAAU,EACV,kBAAkB,GACrB,MAAM,aAAa,CAAC;AACrB,OAAO,EACH,SAAS,EACT,MAAM,EACN,eAAe,EACf,UAAU,EACV,UAAU,GACb,MAAM,aAAa,CAAC"}
|
||||
91
node_modules/entities/dist/index.js
generated
vendored
91
node_modules/entities/dist/index.js
generated
vendored
@@ -1,91 +0,0 @@
|
||||
import { decodeHTML, decodeXML } from "./decode.js";
|
||||
import { encodeHTML, encodeNonAsciiHTML } from "./encode.js";
|
||||
import { encodeXML, escapeAttribute, escapeText, escapeUTF8, } from "./escape.js";
|
||||
/** The level of entities to support. */
|
||||
export var EntityLevel;
|
||||
(function (EntityLevel) {
|
||||
/** Support only XML entities. */
|
||||
EntityLevel[EntityLevel["XML"] = 0] = "XML";
|
||||
/** Support HTML entities, which are a superset of XML entities. */
|
||||
EntityLevel[EntityLevel["HTML"] = 1] = "HTML";
|
||||
})(EntityLevel || (EntityLevel = {}));
|
||||
/**
|
||||
* Encoding strategy used by `encode`.
|
||||
*/
|
||||
export var EncodingMode;
|
||||
(function (EncodingMode) {
|
||||
/**
|
||||
* The output is UTF-8 encoded. Only characters that need escaping within
|
||||
* XML will be escaped.
|
||||
*/
|
||||
EncodingMode[EncodingMode["UTF8"] = 0] = "UTF8";
|
||||
/**
|
||||
* The output consists only of ASCII characters. Characters that need
|
||||
* escaping within HTML, and characters that aren't ASCII characters will
|
||||
* be escaped.
|
||||
*/
|
||||
EncodingMode[EncodingMode["ASCII"] = 1] = "ASCII";
|
||||
/**
|
||||
* Encode all characters that have an equivalent entity, as well as all
|
||||
* characters that are not ASCII characters.
|
||||
*/
|
||||
EncodingMode[EncodingMode["Extensive"] = 2] = "Extensive";
|
||||
/**
|
||||
* Encode all characters that have to be escaped in HTML attributes,
|
||||
* following {@link https://html.spec.whatwg.org/multipage/parsing.html#escapingString}.
|
||||
*/
|
||||
EncodingMode[EncodingMode["Attribute"] = 3] = "Attribute";
|
||||
/**
|
||||
* Encode all characters that have to be escaped in HTML text,
|
||||
* following {@link https://html.spec.whatwg.org/multipage/parsing.html#escapingString}.
|
||||
*/
|
||||
EncodingMode[EncodingMode["Text"] = 4] = "Text";
|
||||
})(EncodingMode || (EncodingMode = {}));
|
||||
/**
|
||||
* Decodes a string with entities.
|
||||
* @param input String to decode.
|
||||
* @param options Decoding options.
|
||||
*/
|
||||
export function decode(input, options = EntityLevel.XML) {
|
||||
const level = typeof options === "number" ? options : options.level;
|
||||
if (level === EntityLevel.HTML) {
|
||||
const mode = typeof options === "object" ? options.mode : undefined;
|
||||
return decodeHTML(input, mode);
|
||||
}
|
||||
return decodeXML(input);
|
||||
}
|
||||
/**
|
||||
* Encodes a string with entities.
|
||||
* @param input String to encode.
|
||||
* @param options Encoding options.
|
||||
*/
|
||||
export function encode(input, options = EntityLevel.XML) {
|
||||
const { mode = EncodingMode.Extensive, level = EntityLevel.XML } = typeof options === "number" ? { level: options } : options;
|
||||
switch (mode) {
|
||||
case EncodingMode.UTF8: {
|
||||
return escapeUTF8(input);
|
||||
}
|
||||
case EncodingMode.Attribute: {
|
||||
return escapeAttribute(input);
|
||||
}
|
||||
case EncodingMode.Text: {
|
||||
return escapeText(input);
|
||||
}
|
||||
case EncodingMode.ASCII: {
|
||||
return level === EntityLevel.HTML
|
||||
? encodeNonAsciiHTML(input)
|
||||
: encodeXML(input);
|
||||
}
|
||||
// biome-ignore lint/complexity/noUselessSwitchCase: we get an error for the switch not being exhaustive
|
||||
case EncodingMode.Extensive:
|
||||
default: {
|
||||
return level === EntityLevel.HTML
|
||||
? encodeHTML(input)
|
||||
: encodeXML(input);
|
||||
}
|
||||
}
|
||||
}
|
||||
export { DecodingMode, decodeHTML, decodeHTMLAttribute, decodeHTMLStrict, decodeXML, decodeXML as decodeXMLStrict, EntityDecoder, } from "./decode.js";
|
||||
export { encodeHTML, encodeNonAsciiHTML, } from "./encode.js";
|
||||
export { encodeXML, escape, escapeAttribute, escapeText, escapeUTF8, } from "./escape.js";
|
||||
//# sourceMappingURL=index.js.map
|
||||
1
node_modules/entities/dist/index.js.map
generated
vendored
1
node_modules/entities/dist/index.js.map
generated
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,UAAU,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,EACH,SAAS,EACT,eAAe,EACf,UAAU,EACV,UAAU,GACb,MAAM,aAAa,CAAC;AAErB,wCAAwC;AACxC,MAAM,CAAN,IAAY,WAKX;AALD,WAAY,WAAW;IACnB,iCAAiC;IACjC,2CAAO,CAAA;IACP,mEAAmE;IACnE,6CAAQ,CAAA;AACZ,CAAC,EALW,WAAW,KAAX,WAAW,QAKtB;AAED;;GAEG;AACH,MAAM,CAAN,IAAY,YA2BX;AA3BD,WAAY,YAAY;IACpB;;;OAGG;IACH,+CAAI,CAAA;IACJ;;;;OAIG;IACH,iDAAK,CAAA;IACL;;;OAGG;IACH,yDAAS,CAAA;IACT;;;OAGG;IACH,yDAAS,CAAA;IACT;;;OAGG;IACH,+CAAI,CAAA;AACR,CAAC,EA3BW,YAAY,KAAZ,YAAY,QA2BvB;AAsBD;;;;GAIG;AACH,MAAM,UAAU,MAAM,CAClB,KAAa,EACb,UAAyC,WAAW,CAAC,GAAG;IAExD,MAAM,KAAK,GAAG,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC;IAEpE,IAAI,KAAK,KAAK,WAAW,CAAC,IAAI,EAAE,CAAC;QAC7B,MAAM,IAAI,GAAG,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;QACpE,OAAO,UAAU,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACnC,CAAC;IAED,OAAO,SAAS,CAAC,KAAK,CAAC,CAAC;AAC5B,CAAC;AAkBD;;;;GAIG;AACH,MAAM,UAAU,MAAM,CAClB,KAAa,EACb,UAAyC,WAAW,CAAC,GAAG;IAExD,MAAM,EAAE,IAAI,GAAG,YAAY,CAAC,SAAS,EAAE,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,GAC5D,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC;IAE/D,QAAQ,IAAI,EAAE,CAAC;QACX,KAAK,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;YACrB,OAAO,UAAU,CAAC,KAAK,CAAC,CAAC;QAC7B,CAAC;QACD,KAAK,YAAY,CAAC,SAAS,CAAC,CAAC,CAAC;YAC1B,OAAO,eAAe,CAAC,KAAK,CAAC,CAAC;QAClC,CAAC;QACD,KAAK,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;YACrB,OAAO,UAAU,CAAC,KAAK,CAAC,CAAC;QAC7B,CAAC;QACD,KAAK,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;YACtB,OAAO,KAAK,KAAK,WAAW,CAAC,IAAI;gBAC7B,CAAC,CAAC,kBAAkB,CAAC,KAAK,CAAC;gBAC3B,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QAC3B,CAAC;QACD,wGAAwG;QACxG,KAAK,YAAY,CAAC,SAAS,CAAC;QAC5B,OAAO,CAAC,CAAC,CAAC;YACN,OAAO,KAAK,KAAK,WAAW,CAAC,IAAI;gBAC7B,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC;gBACnB,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QAC3B,CAAC;IACL,CAAC;AACL,CAAC;AAED,OAAO,EACH,YAAY,EACZ,UAAU,EACV,mBAAmB,EACnB,gBAAgB,EAChB,SAAS,EACT,SAAS,IAAI,eAAe,EAC5B,aAAa,GAChB,MAAM,aAAa,CAAC;AAErB,OAAO,EACH,UAAU,EACV,kBAAkB,GACrB,MAAM,aAAa,CAAC;AACrB,OAAO,EACH,SAAS,EACT,MAAM,EACN,eAAe,EACf,UAAU,EACV,UAAU,GACb,MAAM,aAAa,CAAC"}
|
||||
17
node_modules/entities/dist/internal/bin-trie-flags.d.ts
generated
vendored
17
node_modules/entities/dist/internal/bin-trie-flags.d.ts
generated
vendored
@@ -1,17 +0,0 @@
|
||||
/**
|
||||
* Bit flags & masks for the binary trie encoding used for entity decoding.
|
||||
*
|
||||
* Bit layout (16 bits total):
|
||||
* 15..14 VALUE_LENGTH (+1 encoding; 0 => no value)
|
||||
* 13 FLAG13. If valueLength>0: semicolon required flag (implicit ';').
|
||||
* If valueLength==0: compact run flag.
|
||||
* 12..7 BRANCH_LENGTH Branch length (0 => single branch in 6..0 if jumpOffset==char) OR run length (when compact run)
|
||||
* 6..0 JUMP_TABLE Jump offset (jump table) OR single-branch char code OR first run char
|
||||
*/
|
||||
export declare enum BinTrieFlags {
|
||||
VALUE_LENGTH = 49152,
|
||||
FLAG13 = 8192,
|
||||
BRANCH_LENGTH = 8064,
|
||||
JUMP_TABLE = 127
|
||||
}
|
||||
//# sourceMappingURL=bin-trie-flags.d.ts.map
|
||||
1
node_modules/entities/dist/internal/bin-trie-flags.d.ts.map
generated
vendored
1
node_modules/entities/dist/internal/bin-trie-flags.d.ts.map
generated
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"bin-trie-flags.d.ts","sourceRoot":"","sources":["../../src/internal/bin-trie-flags.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,oBAAY,YAAY;IACpB,YAAY,QAAwB;IACpC,MAAM,OAAwB;IAC9B,aAAa,OAAwB;IACrC,UAAU,MAAwB;CACrC"}
|
||||
18
node_modules/entities/dist/internal/bin-trie-flags.js
generated
vendored
18
node_modules/entities/dist/internal/bin-trie-flags.js
generated
vendored
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* Bit flags & masks for the binary trie encoding used for entity decoding.
|
||||
*
|
||||
* Bit layout (16 bits total):
|
||||
* 15..14 VALUE_LENGTH (+1 encoding; 0 => no value)
|
||||
* 13 FLAG13. If valueLength>0: semicolon required flag (implicit ';').
|
||||
* If valueLength==0: compact run flag.
|
||||
* 12..7 BRANCH_LENGTH Branch length (0 => single branch in 6..0 if jumpOffset==char) OR run length (when compact run)
|
||||
* 6..0 JUMP_TABLE Jump offset (jump table) OR single-branch char code OR first run char
|
||||
*/
|
||||
export var BinTrieFlags;
|
||||
(function (BinTrieFlags) {
|
||||
BinTrieFlags[BinTrieFlags["VALUE_LENGTH"] = 49152] = "VALUE_LENGTH";
|
||||
BinTrieFlags[BinTrieFlags["FLAG13"] = 8192] = "FLAG13";
|
||||
BinTrieFlags[BinTrieFlags["BRANCH_LENGTH"] = 8064] = "BRANCH_LENGTH";
|
||||
BinTrieFlags[BinTrieFlags["JUMP_TABLE"] = 127] = "JUMP_TABLE";
|
||||
})(BinTrieFlags || (BinTrieFlags = {}));
|
||||
//# sourceMappingURL=bin-trie-flags.js.map
|
||||
1
node_modules/entities/dist/internal/bin-trie-flags.js.map
generated
vendored
1
node_modules/entities/dist/internal/bin-trie-flags.js.map
generated
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"bin-trie-flags.js","sourceRoot":"","sources":["../../src/internal/bin-trie-flags.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,CAAN,IAAY,YAKX;AALD,WAAY,YAAY;IACpB,mEAAoC,CAAA;IACpC,sDAA8B,CAAA;IAC9B,oEAAqC,CAAA;IACrC,6DAAkC,CAAA;AACtC,CAAC,EALW,YAAY,KAAZ,YAAY,QAKvB"}
|
||||
7
node_modules/entities/dist/internal/decode-shared.d.ts
generated
vendored
7
node_modules/entities/dist/internal/decode-shared.d.ts
generated
vendored
@@ -1,7 +0,0 @@
|
||||
/**
|
||||
* Shared base64 decode helper for generated decode data.
|
||||
* Assumes global atob is available.
|
||||
* @param input Input string to encode or decode.
|
||||
*/
|
||||
export declare function decodeBase64(input: string): Uint16Array;
|
||||
//# sourceMappingURL=decode-shared.d.ts.map
|
||||
1
node_modules/entities/dist/internal/decode-shared.d.ts.map
generated
vendored
1
node_modules/entities/dist/internal/decode-shared.d.ts.map
generated
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"decode-shared.d.ts","sourceRoot":"","sources":["../../src/internal/decode-shared.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,CAYvD"}
|
||||
17
node_modules/entities/dist/internal/decode-shared.js
generated
vendored
17
node_modules/entities/dist/internal/decode-shared.js
generated
vendored
@@ -1,17 +0,0 @@
|
||||
/**
|
||||
* Shared base64 decode helper for generated decode data.
|
||||
* Assumes global atob is available.
|
||||
* @param input Input string to encode or decode.
|
||||
*/
|
||||
export function decodeBase64(input) {
|
||||
const binary = atob(input);
|
||||
const evenLength = binary.length & ~1; // Round down to even length
|
||||
const out = new Uint16Array(evenLength / 2);
|
||||
for (let index = 0, outIndex = 0; index < evenLength; index += 2) {
|
||||
const lo = binary.charCodeAt(index);
|
||||
const hi = binary.charCodeAt(index + 1);
|
||||
out[outIndex++] = lo | (hi << 8);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
//# sourceMappingURL=decode-shared.js.map
|
||||
1
node_modules/entities/dist/internal/decode-shared.js.map
generated
vendored
1
node_modules/entities/dist/internal/decode-shared.js.map
generated
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"decode-shared.js","sourceRoot":"","sources":["../../src/internal/decode-shared.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,KAAa;IACtC,MAAM,MAAM,GAAW,IAAI,CAAC,KAAK,CAAC,CAAC;IACnC,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,4BAA4B;IACnE,MAAM,GAAG,GAAG,IAAI,WAAW,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC;IAE5C,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,QAAQ,GAAG,CAAC,EAAE,KAAK,GAAG,UAAU,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QAC/D,MAAM,EAAE,GAAG,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QACpC,MAAM,EAAE,GAAG,MAAM,CAAC,UAAU,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;QACxC,GAAG,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;IACrC,CAAC;IAED,OAAO,GAAG,CAAC;AACf,CAAC"}
|
||||
33
node_modules/entities/dist/internal/encode-shared.d.ts
generated
vendored
33
node_modules/entities/dist/internal/encode-shared.d.ts
generated
vendored
@@ -1,33 +0,0 @@
|
||||
/**
|
||||
* A node inside the encoding trie used by `encode.ts`.
|
||||
*
|
||||
* There are two physical shapes to minimize allocations and lookup cost:
|
||||
*
|
||||
* 1. Leaf node (string)
|
||||
* - A plain string (already in the form `"&name;"`).
|
||||
* - Represents a terminal match with no children.
|
||||
*
|
||||
* 2. Branch / value node (object)
|
||||
*/
|
||||
export type EncodeTrieNode = string | {
|
||||
/**
|
||||
* Entity value for the current code point sequence (wrapped: `&...;`).
|
||||
* Present when the path to this node itself is a valid named entity.
|
||||
*/
|
||||
value: string | undefined;
|
||||
/** If a number, the next code unit of the only next character. */
|
||||
next: number | Map<number, EncodeTrieNode>;
|
||||
/** If next is a number, `nextValue` contains the entity value. */
|
||||
nextValue?: string;
|
||||
};
|
||||
/**
|
||||
* Parse a compact encode trie string into a Map structure used for encoding.
|
||||
*
|
||||
* Format per entry (ascending code points using delta encoding):
|
||||
* <diffBase36>[&name;][{<children>}] -- diff omitted when 0
|
||||
* Where diff = currentKey - previousKey - 1 (first entry stores absolute key).
|
||||
* `&name;` is the entity value (already wrapped); a following `{` denotes children.
|
||||
* @param serialized Serialized text fragment to encode.
|
||||
*/
|
||||
export declare function parseEncodeTrie(serialized: string): Map<number, EncodeTrieNode>;
|
||||
//# sourceMappingURL=encode-shared.d.ts.map
|
||||
1
node_modules/entities/dist/internal/encode-shared.d.ts.map
generated
vendored
1
node_modules/entities/dist/internal/encode-shared.d.ts.map
generated
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"encode-shared.d.ts","sourceRoot":"","sources":["../../src/internal/encode-shared.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,MAAM,cAAc,GACpB,MAAM,GACN;IACI;;;OAGG;IACH,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,kEAAkE;IAClE,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAC3C,kEAAkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAER;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC3B,UAAU,EAAE,MAAM,GACnB,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAsF7B"}
|
||||
93
node_modules/entities/dist/internal/encode-shared.js
generated
vendored
93
node_modules/entities/dist/internal/encode-shared.js
generated
vendored
@@ -1,93 +0,0 @@
|
||||
/**
|
||||
* Parse a compact encode trie string into a Map structure used for encoding.
|
||||
*
|
||||
* Format per entry (ascending code points using delta encoding):
|
||||
* <diffBase36>[&name;][{<children>}] -- diff omitted when 0
|
||||
* Where diff = currentKey - previousKey - 1 (first entry stores absolute key).
|
||||
* `&name;` is the entity value (already wrapped); a following `{` denotes children.
|
||||
* @param serialized Serialized text fragment to encode.
|
||||
*/
|
||||
export function parseEncodeTrie(serialized) {
|
||||
const top = new Map();
|
||||
const totalLength = serialized.length;
|
||||
let cursor = 0;
|
||||
let lastTopKey = -1;
|
||||
function readDiff() {
|
||||
const start = cursor;
|
||||
while (cursor < totalLength) {
|
||||
const char = serialized.charAt(cursor);
|
||||
if ((char < "0" || char > "9") && (char < "a" || char > "z")) {
|
||||
break;
|
||||
}
|
||||
cursor++;
|
||||
}
|
||||
if (cursor === start)
|
||||
return 0;
|
||||
return Number.parseInt(serialized.slice(start, cursor), 36);
|
||||
}
|
||||
function readEntity() {
|
||||
if (serialized[cursor] !== "&") {
|
||||
throw new Error(`Child entry missing value near index ${cursor}`);
|
||||
}
|
||||
// Cursor currently points at '&'
|
||||
const start = cursor;
|
||||
const end = serialized.indexOf(";", cursor + 1);
|
||||
if (end === -1) {
|
||||
throw new Error(`Unterminated entity starting at index ${start}`);
|
||||
}
|
||||
cursor = end + 1; // Move past ';'
|
||||
return serialized.slice(start, cursor); // Includes & ... ;
|
||||
}
|
||||
while (cursor < totalLength) {
|
||||
const keyDiff = readDiff();
|
||||
const key = lastTopKey === -1 ? keyDiff : lastTopKey + keyDiff + 1;
|
||||
let value;
|
||||
if (serialized[cursor] === "&")
|
||||
value = readEntity();
|
||||
if (serialized[cursor] === "{") {
|
||||
cursor++; // Skip '{'
|
||||
// Parse first child
|
||||
let diff = readDiff();
|
||||
let childKey = diff; // First key (lastChildKey = -1)
|
||||
const firstValue = readEntity();
|
||||
if (serialized[cursor] === "{") {
|
||||
throw new Error("Unexpected nested '{' beyond depth 2");
|
||||
}
|
||||
// If end of block -> single child optimization
|
||||
if (serialized[cursor] === "}") {
|
||||
top.set(key, { value, next: childKey, nextValue: firstValue });
|
||||
cursor++; // Skip '}'
|
||||
}
|
||||
else {
|
||||
const childMap = new Map([
|
||||
[childKey, firstValue],
|
||||
]);
|
||||
let lastChildKey = childKey;
|
||||
while (cursor < totalLength && serialized[cursor] !== "}") {
|
||||
diff = readDiff();
|
||||
childKey = lastChildKey + diff + 1;
|
||||
const childValue = readEntity();
|
||||
if (serialized[cursor] === "{") {
|
||||
throw new Error("Unexpected nested '{' beyond depth 2");
|
||||
}
|
||||
childMap.set(childKey, childValue);
|
||||
lastChildKey = childKey;
|
||||
}
|
||||
if (serialized[cursor] !== "}") {
|
||||
throw new Error("Unterminated child block");
|
||||
}
|
||||
cursor++; // Skip '}'
|
||||
top.set(key, { value, next: childMap });
|
||||
}
|
||||
}
|
||||
else if (value === undefined) {
|
||||
throw new Error(`Malformed encode trie: missing value at index ${cursor}`);
|
||||
}
|
||||
else {
|
||||
top.set(key, value);
|
||||
}
|
||||
lastTopKey = key;
|
||||
}
|
||||
return top;
|
||||
}
|
||||
//# sourceMappingURL=encode-shared.js.map
|
||||
1
node_modules/entities/dist/internal/encode-shared.js.map
generated
vendored
1
node_modules/entities/dist/internal/encode-shared.js.map
generated
vendored
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"encode-shared.js","sourceRoot":"","sources":["../../src/internal/encode-shared.ts"],"names":[],"mappings":"AAyBA;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAC3B,UAAkB;IAElB,MAAM,GAAG,GAAG,IAAI,GAAG,EAA0B,CAAC;IAC9C,MAAM,WAAW,GAAG,UAAU,CAAC,MAAM,CAAC;IACtC,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,IAAI,UAAU,GAAG,CAAC,CAAC,CAAC;IAEpB,SAAS,QAAQ;QACb,MAAM,KAAK,GAAG,MAAM,CAAC;QACrB,OAAO,MAAM,GAAG,WAAW,EAAE,CAAC;YAC1B,MAAM,IAAI,GAAG,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAEvC,IAAI,CAAC,IAAI,GAAG,GAAG,IAAI,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,GAAG,IAAI,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC;gBAC3D,MAAM;YACV,CAAC;YACD,MAAM,EAAE,CAAC;QACb,CAAC;QACD,IAAI,MAAM,KAAK,KAAK;YAAE,OAAO,CAAC,CAAC;QAC/B,OAAO,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC;IAChE,CAAC;IAED,SAAS,UAAU;QACf,IAAI,UAAU,CAAC,MAAM,CAAC,KAAK,GAAG,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CAAC,wCAAwC,MAAM,EAAE,CAAC,CAAC;QACtE,CAAC;QAED,iCAAiC;QACjC,MAAM,KAAK,GAAG,MAAM,CAAC;QACrB,MAAM,GAAG,GAAG,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,CAAC,CAAC;QAChD,IAAI,GAAG,KAAK,CAAC,CAAC,EAAE,CAAC;YACb,MAAM,IAAI,KAAK,CAAC,yCAAyC,KAAK,EAAE,CAAC,CAAC;QACtE,CAAC;QACD,MAAM,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,gBAAgB;QAClC,OAAO,UAAU,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,mBAAmB;IAC/D,CAAC;IAED,OAAO,MAAM,GAAG,WAAW,EAAE,CAAC;QAC1B,MAAM,OAAO,GAAG,QAAQ,EAAE,CAAC;QAC3B,MAAM,GAAG,GAAG,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,GAAG,OAAO,GAAG,CAAC,CAAC;QAEnE,IAAI,KAAyB,CAAC;QAC9B,IAAI,UAAU,CAAC,MAAM,CAAC,KAAK,GAAG;YAAE,KAAK,GAAG,UAAU,EAAE,CAAC;QAErD,IAAI,UAAU,CAAC,MAAM,CAAC,KAAK,GAAG,EAAE,CAAC;YAC7B,MAAM,EAAE,CAAC,CAAC,WAAW;YACrB,oBAAoB;YACpB,IAAI,IAAI,GAAG,QAAQ,EAAE,CAAC;YACtB,IAAI,QAAQ,GAAG,IAAI,CAAC,CAAC,gCAAgC;YACrD,MAAM,UAAU,GAAG,UAAU,EAAE,CAAC;YAChC,IAAI,UAAU,CAAC,MAAM,CAAC,KAAK,GAAG,EAAE,CAAC;gBAC7B,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;YAC5D,CAAC;YACD,+CAA+C;YAC/C,IAAI,UAAU,CAAC,MAAM,CAAC,KAAK,GAAG,EAAE,CAAC;gBAC7B,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,CAAC;gBAC/D,MAAM,EAAE,CAAC,CAAC,WAAW;YACzB,CAAC;iBAAM,CAAC;gBACJ,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAyB;oBAC7C,CAAC,QAAQ,EAAE,UAAU,CAAC;iBACzB,CAAC,CAAC;gBACH,IAAI,YAAY,GAAG,QAAQ,CAAC;gBAC5B,OAAO,MAAM,GAAG,WAAW,IAAI,UAAU,CAAC,MAAM,CAAC,KAAK,GAAG,EAAE,CAAC;oBACxD,IAAI,GAAG,QAAQ,EAAE,CAAC;oBAClB,QAAQ,GAAG,YAAY,GAAG,IAAI,GAAG,CAAC,CAAC;oBACnC,MAAM,UAAU,GAAG,UAAU,EAAE,CAAC;oBAChC,IAAI,UAAU,CAAC,MAAM,CAAC,KAAK,GAAG,EAAE,CAAC;wBAC7B,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;oBAC5D,CAAC;oBACD,QAAQ,CAAC,GAAG,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;oBACnC,YAAY,GAAG,QAAQ,CAAC;gBAC5B,CAAC;gBACD,IAAI,UAAU,CAAC,MAAM,CAAC,KAAK,GAAG,EAAE,CAAC;oBAC7B,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;gBAChD,CAAC;gBACD,MAAM,EAAE,CAAC,CAAC,WAAW;gBACrB,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;YAC5C,CAAC;QACL,CAAC;aAAM,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CACX,iDAAiD,MAAM,EAAE,CAC5D,CAAC;QACN,CAAC;aAAM,CAAC;YACJ,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACxB,CAAC;QACD,UAAU,GAAG,GAAG,CAAC;IACrB,CAAC;IACD,OAAO,GAAG,CAAC;AACf,CAAC"}
|
||||
125
node_modules/entities/package.json
generated
vendored
125
node_modules/entities/package.json
generated
vendored
@@ -1,83 +1,64 @@
|
||||
{
|
||||
"name": "entities",
|
||||
"version": "8.0.0",
|
||||
"description": "Encode & decode XML and HTML entities with ease & speed",
|
||||
"version": "2.2.0",
|
||||
"description": "Encode & decode XML and HTML entities with ease",
|
||||
"author": "Felix Boehm <me@feedic.com>",
|
||||
"funding": "https://github.com/fb55/entities?sponsor=1",
|
||||
"sideEffects": false,
|
||||
"keywords": [
|
||||
"html entities",
|
||||
"entity decoder",
|
||||
"entity encoding",
|
||||
"html decoding",
|
||||
"html encoding",
|
||||
"xml decoding",
|
||||
"xml encoding"
|
||||
"entity",
|
||||
"decoding",
|
||||
"encoding",
|
||||
"html",
|
||||
"xml",
|
||||
"html entities"
|
||||
],
|
||||
"directories": {
|
||||
"lib": "lib/"
|
||||
},
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"files": [
|
||||
"lib/**/*"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/jest": "^26.0.0",
|
||||
"@types/node": "^14.11.8",
|
||||
"@typescript-eslint/eslint-plugin": "^4.4.1",
|
||||
"@typescript-eslint/parser": "^4.4.1",
|
||||
"coveralls": "*",
|
||||
"eslint": "^7.11.0",
|
||||
"eslint-config-prettier": "^7.0.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"jest": "^26.5.3",
|
||||
"prettier": "^2.0.5",
|
||||
"ts-jest": "^26.1.0",
|
||||
"typescript": "^4.0.2"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "jest --coverage && npm run lint",
|
||||
"coverage": "cat coverage/lcov.info | coveralls",
|
||||
"lint": "npm run lint:es && npm run lint:prettier",
|
||||
"lint:es": "eslint .",
|
||||
"lint:prettier": "npm run prettier -- --check",
|
||||
"format": "npm run format:es && npm run format:prettier",
|
||||
"format:es": "npm run lint:es -- --fix",
|
||||
"format:prettier": "npm run prettier -- --write",
|
||||
"prettier": "prettier '**/*.{ts,md,json,yml}'",
|
||||
"build": "tsc && cp -r src/maps lib",
|
||||
"prepare": "npm run build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/fb55/entities.git"
|
||||
"url": "git://github.com/fb55/entities.git"
|
||||
},
|
||||
"funding": "https://github.com/fb55/entities?sponsor=1",
|
||||
"license": "BSD-2-Clause",
|
||||
"author": "Felix Boehm <me@feedic.com>",
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./decode": {
|
||||
"types": "./dist/decode.d.ts",
|
||||
"default": "./dist/decode.js"
|
||||
},
|
||||
"./escape": {
|
||||
"types": "./dist/escape.d.ts",
|
||||
"default": "./dist/escape.js"
|
||||
}
|
||||
"jest": {
|
||||
"preset": "ts-jest",
|
||||
"testEnvironment": "node"
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"src",
|
||||
"!**/*.spec.ts"
|
||||
],
|
||||
"scripts": {
|
||||
"benchmark": "node --import=tsx scripts/benchmark.ts",
|
||||
"build": "tsc",
|
||||
"build:docs": "typedoc --hideGenerator src/index.ts",
|
||||
"build:encode-trie": "node --import=tsx scripts/write-encode-map.ts",
|
||||
"build:trie": "node --import=tsx scripts/write-decode-map.ts",
|
||||
"format": "npm run format:es && npm run format:biome",
|
||||
"format:biome": "biome check --fix .",
|
||||
"format:es": "npm run lint:es -- --fix",
|
||||
"lint": "npm run lint:es && npm run lint:ts && npm run lint:biome",
|
||||
"lint:biome": "biome check .",
|
||||
"lint:es": "eslint .",
|
||||
"lint:ts": "tsc --noEmit",
|
||||
"prepublishOnly": "npm run build",
|
||||
"test": "npm run test:vi && npm run lint",
|
||||
"test:vi": "vitest run"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.4.7",
|
||||
"@eslint/compat": "^2.0.3",
|
||||
"@feedic/eslint-config": "^0.3.1",
|
||||
"@types/he": "^1.2.3",
|
||||
"@types/node": "^25.5.0",
|
||||
"eslint": "^10.0.3",
|
||||
"eslint-config-biome": "^2.1.3",
|
||||
"globals": "^17.4.0",
|
||||
"he": "^1.2.0",
|
||||
"html-entities": "^2.6.0",
|
||||
"parse-entities": "^4.0.2",
|
||||
"tinybench": "^6.0.0",
|
||||
"tsx": "^4.21.0",
|
||||
"typedoc": "^0.28.17",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.57.1",
|
||||
"vitest": "^4.0.17"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
"prettier": {
|
||||
"tabWidth": 4,
|
||||
"proseWrap": "always"
|
||||
}
|
||||
}
|
||||
|
||||
126
node_modules/entities/readme.md
generated
vendored
126
node_modules/entities/readme.md
generated
vendored
@@ -1,20 +1,7 @@
|
||||
# entities [](https://npmjs.org/package/entities) [](https://npmjs.org/package/entities) [](https://github.com/fb55/entities/actions/workflows/nodejs-test.yml)
|
||||
# entities [](https://npmjs.org/package/entities) [](https://npmjs.org/package/entities) [](http://travis-ci.org/fb55/entities) [](https://coveralls.io/r/fb55/entities)
|
||||
|
||||
Encode & decode HTML & XML entities with ease & speed.
|
||||
|
||||
## Features
|
||||
|
||||
- 😇 Tried and true: `entities` is used by many popular libraries; eg.
|
||||
[`htmlparser2`](https://github.com/fb55/htmlparser2), the official
|
||||
[AWS SDK](https://github.com/aws/aws-sdk-js-v3) and
|
||||
[`commonmark`](https://github.com/commonmark/commonmark.js) use it to process
|
||||
HTML entities.
|
||||
- ⚡️ Fast: `entities` is the fastest library for decoding HTML entities (as of
|
||||
September 2025); see [performance](#performance).
|
||||
- 🎛 Configurable: Get an output tailored for your needs. You are fine with
|
||||
UTF8? That'll save you some bytes. Prefer to only have ASCII characters? We
|
||||
can do that as well!
|
||||
|
||||
## How to…
|
||||
|
||||
### …install `entities`
|
||||
@@ -24,101 +11,29 @@ Encode & decode HTML & XML entities with ease & speed.
|
||||
### …use `entities`
|
||||
|
||||
```javascript
|
||||
import * as entities from "entities";
|
||||
const entities = require("entities");
|
||||
|
||||
// Encoding
|
||||
entities.escapeUTF8("& ü"); // "&#38; ü"
|
||||
entities.encodeXML("& ü"); // "&#38; ü"
|
||||
entities.encodeHTML("& ü"); // "&#38; ü"
|
||||
//encoding
|
||||
entities.escape("&"); // "&#38;"
|
||||
entities.encodeXML("&"); // "&#38;"
|
||||
entities.encodeHTML("&"); // "&#38;"
|
||||
|
||||
// Decoding
|
||||
//decoding
|
||||
entities.decodeXML("asdf & ÿ ü '"); // "asdf & ÿ ü '"
|
||||
entities.decodeHTML("asdf & ÿ ü '"); // "asdf & ÿ ü '"
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
Benchmarked in September 2025 with Node v24.6.0 on Apple M2 using `tinybench`.
|
||||
Higher ops/s is better; `avg (μs)` is the mean time per operation.
|
||||
See `scripts/benchmark.ts` to reproduce.
|
||||
This is how `entities` compares to other libraries on a very basic benchmark
|
||||
(see `scripts/benchmark.ts`, for 10,000,000 iterations):
|
||||
|
||||
### Decoding
|
||||
|
||||
| Library | Version | ops/s | avg (μs) | ±% | slower |
|
||||
| -------------- | ------- | --------- | -------- | ---- | ------ |
|
||||
| entities | 7.0.0 | 5,838,416 | 175.57 | 0.06 | — |
|
||||
| html-entities | 2.6.0 | 2,919,637 | 347.77 | 0.33 | 50.0% |
|
||||
| he | 1.2.0 | 2,318,438 | 446.48 | 0.70 | 60.3% |
|
||||
| parse-entities | 4.0.2 | 852,855 | 1,199.51 | 0.36 | 85.4% |
|
||||
|
||||
### Encoding
|
||||
|
||||
| Library | Version | ops/s | avg (μs) | ±% | slower |
|
||||
| -------------- | ------- | --------- | -------- | ---- | ------ |
|
||||
| entities | 7.0.0 | 2,770,115 | 368.09 | 0.11 | — |
|
||||
| html-entities | 2.6.0 | 1,491,963 | 679.96 | 0.58 | 46.2% |
|
||||
| he | 1.2.0 | 481,278 | 2,118.25 | 0.61 | 82.6% |
|
||||
|
||||
### Escaping
|
||||
|
||||
| Library | Version | ops/s | avg (μs) | ±% | slower |
|
||||
| -------------- | ------- | --------- | -------- | ---- | ------ |
|
||||
| entities | 7.0.0 | 4,616,468 | 223.84 | 0.17 | — |
|
||||
| he | 1.2.0 | 3,659,301 | 280.76 | 0.58 | 20.7% |
|
||||
| html-entities | 2.6.0 | 3,555,301 | 296.63 | 0.84 | 23.0% |
|
||||
|
||||
Note: Micro-benchmarks may vary across machines and Node versions.
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
> What methods should I actually use to encode my documents?
|
||||
|
||||
If your target supports UTF-8, the `escapeUTF8` method is going to be your best
|
||||
choice. Otherwise, use either `encodeHTML` or `encodeXML` based on whether
|
||||
you're dealing with an HTML or an XML document.
|
||||
|
||||
You can have a look at the options for the `encode` and `decode` methods to see
|
||||
everything you can configure.
|
||||
|
||||
> When should I use strict decoding?
|
||||
|
||||
When strict decoding, entities not terminated with a semicolon will be ignored.
|
||||
This is helpful for decoding entities in legacy environments.
|
||||
|
||||
> Why should I use `entities` instead of alternative modules?
|
||||
|
||||
As of September 2025, `entities` is faster than other modules. Still, this is
|
||||
not a differentiated space and other modules can catch up.
|
||||
|
||||
**More importantly**, you might already have `entities` in your dependency graph
|
||||
(as a dependency of eg. `cheerio`, or `htmlparser2`), and including it directly
|
||||
might not even increase your bundle size. The same is true for other entity
|
||||
libraries, so have a look through your `node_modules` directory!
|
||||
|
||||
> Does `entities` support tree shaking?
|
||||
|
||||
Yes! Note that for best results, you should not use the `encode` and `decode`
|
||||
functions, as they wrap around a number of other functions, all of which will
|
||||
remain in the bundle. Instead, use the functions that you need directly.
|
||||
|
||||
---
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
This library wouldn't be possible without the work of these individuals. Thanks
|
||||
to
|
||||
|
||||
- [@mathiasbynens](https://github.com/mathiasbynens) for his explanations about
|
||||
character encodings, and his library `he`, which was one of the inspirations
|
||||
for `entities`
|
||||
- [@inikulin](https://github.com/inikulin) for his work on optimized tries for
|
||||
decoding HTML entities for the `parse5` project
|
||||
- [@mdevils](https://github.com/mdevils) for taking on the challenge of
|
||||
producing a quick entity library with his `html-entities` library. `entities`
|
||||
would be quite a bit slower if there wasn't any competition. Right now
|
||||
`entities` is on top, but we'll see how long that lasts!
|
||||
| Library | `decode` performance | `encode` performance | Bundle size |
|
||||
| -------------- | -------------------- | -------------------- | -------------------------------------------------------------------------- |
|
||||
| entities | 10.809s | 17.683s |  |
|
||||
| html-entities | 14.029s | 22.670s |  |
|
||||
| he | 16.163s | 44.010s |  |
|
||||
| parse-entities | 28.507s | N/A |  |
|
||||
|
||||
---
|
||||
|
||||
@@ -129,3 +44,14 @@ License: BSD-2-Clause
|
||||
To report a security vulnerability, please use the
|
||||
[Tidelift security contact](https://tidelift.com/security). Tidelift will
|
||||
coordinate the fix and disclosure.
|
||||
|
||||
## `entities` for enterprise
|
||||
|
||||
Available as part of the Tidelift Subscription
|
||||
|
||||
The maintainers of `entities` and thousands of other packages are working with
|
||||
Tidelift to deliver commercial support and maintenance for the open source
|
||||
dependencies you use to build your applications. Save time, reduce risk, and
|
||||
improve code health, while paying the maintainers of the exact dependencies you
|
||||
use.
|
||||
[Learn more.](https://tidelift.com/subscription/pkg/npm-entities?utm_source=npm-entities&utm_medium=referral&utm_campaign=enterprise&utm_term=repo)
|
||||
|
||||
50
node_modules/entities/src/decode-codepoint.ts
generated
vendored
50
node_modules/entities/src/decode-codepoint.ts
generated
vendored
@@ -1,50 +0,0 @@
|
||||
// Adapted from https://github.com/mathiasbynens/he/blob/36afe179392226cf1b6ccdb16ebbb7a5a844d93a/src/he.js#L106-L134
|
||||
|
||||
const decodeMap = new Map([
|
||||
[0, 65_533],
|
||||
// C1 Unicode control character reference replacements
|
||||
[128, 8364],
|
||||
[130, 8218],
|
||||
[131, 402],
|
||||
[132, 8222],
|
||||
[133, 8230],
|
||||
[134, 8224],
|
||||
[135, 8225],
|
||||
[136, 710],
|
||||
[137, 8240],
|
||||
[138, 352],
|
||||
[139, 8249],
|
||||
[140, 338],
|
||||
[142, 381],
|
||||
[145, 8216],
|
||||
[146, 8217],
|
||||
[147, 8220],
|
||||
[148, 8221],
|
||||
[149, 8226],
|
||||
[150, 8211],
|
||||
[151, 8212],
|
||||
[152, 732],
|
||||
[153, 8482],
|
||||
[154, 353],
|
||||
[155, 8250],
|
||||
[156, 339],
|
||||
[158, 382],
|
||||
[159, 376],
|
||||
]);
|
||||
|
||||
/**
|
||||
* Replace the given code point with a replacement character if it is a
|
||||
* surrogate or is outside the valid range. Otherwise return the code
|
||||
* point unchanged.
|
||||
* @param codePoint Unicode code point to convert.
|
||||
*/
|
||||
export function replaceCodePoint(codePoint: number): number {
|
||||
if (
|
||||
(codePoint >= 0xd8_00 && codePoint <= 0xdf_ff) ||
|
||||
codePoint > 0x10_ff_ff
|
||||
) {
|
||||
return 0xff_fd;
|
||||
}
|
||||
|
||||
return decodeMap.get(codePoint) ?? codePoint;
|
||||
}
|
||||
671
node_modules/entities/src/decode.ts
generated
vendored
671
node_modules/entities/src/decode.ts
generated
vendored
@@ -1,671 +0,0 @@
|
||||
import { replaceCodePoint } from "./decode-codepoint.js";
|
||||
import { htmlDecodeTree } from "./generated/decode-data-html.js";
|
||||
import { xmlDecodeTree } from "./generated/decode-data-xml.js";
|
||||
import { BinTrieFlags } from "./internal/bin-trie-flags.js";
|
||||
|
||||
const enum CharCodes {
|
||||
NUM = 35, // "#"
|
||||
SEMI = 59, // ";"
|
||||
EQUALS = 61, // "="
|
||||
ZERO = 48, // "0"
|
||||
NINE = 57, // "9"
|
||||
LOWER_A = 97, // "a"
|
||||
LOWER_F = 102, // "f"
|
||||
LOWER_X = 120, // "x"
|
||||
LOWER_Z = 122, // "z"
|
||||
UPPER_A = 65, // "A"
|
||||
UPPER_F = 70, // "F"
|
||||
UPPER_Z = 90, // "Z"
|
||||
}
|
||||
|
||||
/** Bit that needs to be set to convert an upper case ASCII character to lower case */
|
||||
const TO_LOWER_BIT = 0b10_0000;
|
||||
|
||||
function isNumber(code: number): boolean {
|
||||
return code >= CharCodes.ZERO && code <= CharCodes.NINE;
|
||||
}
|
||||
|
||||
function isHexadecimalCharacter(code: number): boolean {
|
||||
return (
|
||||
(code >= CharCodes.UPPER_A && code <= CharCodes.UPPER_F) ||
|
||||
(code >= CharCodes.LOWER_A && code <= CharCodes.LOWER_F)
|
||||
);
|
||||
}
|
||||
|
||||
function isAsciiAlphaNumeric(code: number): boolean {
|
||||
return (
|
||||
(code >= CharCodes.UPPER_A && code <= CharCodes.UPPER_Z) ||
|
||||
(code >= CharCodes.LOWER_A && code <= CharCodes.LOWER_Z) ||
|
||||
isNumber(code)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given character is a valid end character for an entity in an attribute.
|
||||
*
|
||||
* Attribute values that aren't terminated properly aren't parsed, and shouldn't lead to a parser error.
|
||||
* See the example in https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state
|
||||
* @param code Code point to decode.
|
||||
*/
|
||||
function isEntityInAttributeInvalidEnd(code: number): boolean {
|
||||
return code === CharCodes.EQUALS || isAsciiAlphaNumeric(code);
|
||||
}
|
||||
|
||||
const enum EntityDecoderState {
|
||||
EntityStart,
|
||||
NumericStart,
|
||||
NumericDecimal,
|
||||
NumericHex,
|
||||
NamedEntity,
|
||||
}
|
||||
|
||||
/**
|
||||
* Decoding mode for named entities.
|
||||
*/
|
||||
export enum DecodingMode {
|
||||
/** Entities in text nodes that can end with any character. */
|
||||
Legacy = 0,
|
||||
/** Only allow entities terminated with a semicolon. */
|
||||
Strict = 1,
|
||||
/** Entities in attributes have limitations on ending characters. */
|
||||
Attribute = 2,
|
||||
}
|
||||
|
||||
/**
|
||||
* Producers for character reference errors as defined in the HTML spec.
|
||||
*/
|
||||
export interface EntityErrorProducer {
|
||||
missingSemicolonAfterCharacterReference(): void;
|
||||
absenceOfDigitsInNumericCharacterReference(
|
||||
consumedCharacters: number,
|
||||
): void;
|
||||
validateNumericCharacterReference(code: number): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Token decoder with support of writing partial entities.
|
||||
*/
|
||||
export class EntityDecoder {
|
||||
constructor(
|
||||
/** The tree used to decode entities. */
|
||||
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: False positive
|
||||
private readonly decodeTree: Uint16Array,
|
||||
/**
|
||||
* The function that is called when a codepoint is decoded.
|
||||
*
|
||||
* For multi-byte named entities, this will be called multiple times,
|
||||
* with the second codepoint, and the same `consumed` value.
|
||||
* @param codepoint The decoded codepoint.
|
||||
* @param consumed The number of bytes consumed by the decoder.
|
||||
*/
|
||||
private readonly emitCodePoint: (cp: number, consumed: number) => void,
|
||||
/** An object that is used to produce errors. */
|
||||
private readonly errors?: EntityErrorProducer | undefined,
|
||||
) {}
|
||||
|
||||
/** The current state of the decoder. */
|
||||
private state = EntityDecoderState.EntityStart;
|
||||
/** Characters that were consumed while parsing an entity. */
|
||||
private consumed = 1;
|
||||
/**
|
||||
* The result of the entity.
|
||||
*
|
||||
* Either the result index of a numeric entity, or the codepoint of a
|
||||
* numeric entity.
|
||||
*/
|
||||
private result = 0;
|
||||
|
||||
/** The current index in the decode tree. */
|
||||
private treeIndex = 0;
|
||||
/** The number of characters that were consumed in excess. */
|
||||
private excess = 1;
|
||||
/** The mode in which the decoder is operating. */
|
||||
private decodeMode = DecodingMode.Strict;
|
||||
/** The number of characters that have been consumed in the current run. */
|
||||
private runConsumed = 0;
|
||||
|
||||
/**
|
||||
* Resets the instance to make it reusable.
|
||||
* @param decodeMode Entity decoding mode to use.
|
||||
*/
|
||||
startEntity(decodeMode: DecodingMode): void {
|
||||
this.decodeMode = decodeMode;
|
||||
this.state = EntityDecoderState.EntityStart;
|
||||
this.result = 0;
|
||||
this.treeIndex = 0;
|
||||
this.excess = 1;
|
||||
this.consumed = 1;
|
||||
this.runConsumed = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write an entity to the decoder. This can be called multiple times with partial entities.
|
||||
* If the entity is incomplete, the decoder will return -1.
|
||||
*
|
||||
* Mirrors the implementation of `getDecoder`, but with the ability to stop decoding if the
|
||||
* entity is incomplete, and resume when the next string is written.
|
||||
* @param input The string containing the entity (or a continuation of the entity).
|
||||
* @param offset The offset at which the entity begins. Should be 0 if this is not the first call.
|
||||
* @returns The number of characters that were consumed, or -1 if the entity is incomplete.
|
||||
*/
|
||||
write(input: string, offset: number): number {
|
||||
switch (this.state) {
|
||||
case EntityDecoderState.EntityStart: {
|
||||
if (input.charCodeAt(offset) === CharCodes.NUM) {
|
||||
this.state = EntityDecoderState.NumericStart;
|
||||
this.consumed += 1;
|
||||
return this.stateNumericStart(input, offset + 1);
|
||||
}
|
||||
this.state = EntityDecoderState.NamedEntity;
|
||||
return this.stateNamedEntity(input, offset);
|
||||
}
|
||||
|
||||
case EntityDecoderState.NumericStart: {
|
||||
return this.stateNumericStart(input, offset);
|
||||
}
|
||||
|
||||
case EntityDecoderState.NumericDecimal: {
|
||||
return this.stateNumericDecimal(input, offset);
|
||||
}
|
||||
|
||||
case EntityDecoderState.NumericHex: {
|
||||
return this.stateNumericHex(input, offset);
|
||||
}
|
||||
|
||||
case EntityDecoderState.NamedEntity: {
|
||||
return this.stateNamedEntity(input, offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches between the numeric decimal and hexadecimal states.
|
||||
*
|
||||
* Equivalent to the `Numeric character reference state` in the HTML spec.
|
||||
* @param input The string containing the entity (or a continuation of the entity).
|
||||
* @param offset The current offset.
|
||||
* @returns The number of characters that were consumed, or -1 if the entity is incomplete.
|
||||
*/
|
||||
private stateNumericStart(input: string, offset: number): number {
|
||||
if (offset >= input.length) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if ((input.charCodeAt(offset) | TO_LOWER_BIT) === CharCodes.LOWER_X) {
|
||||
this.state = EntityDecoderState.NumericHex;
|
||||
this.consumed += 1;
|
||||
return this.stateNumericHex(input, offset + 1);
|
||||
}
|
||||
|
||||
this.state = EntityDecoderState.NumericDecimal;
|
||||
return this.stateNumericDecimal(input, offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a hexadecimal numeric entity.
|
||||
*
|
||||
* Equivalent to the `Hexademical character reference state` in the HTML spec.
|
||||
* @param input The string containing the entity (or a continuation of the entity).
|
||||
* @param offset The current offset.
|
||||
* @returns The number of characters that were consumed, or -1 if the entity is incomplete.
|
||||
*/
|
||||
private stateNumericHex(input: string, offset: number): number {
|
||||
while (offset < input.length) {
|
||||
const char = input.charCodeAt(offset);
|
||||
if (isNumber(char) || isHexadecimalCharacter(char)) {
|
||||
// Convert hex digit to value (0-15); 'a'/'A' -> 10.
|
||||
const digit =
|
||||
char <= CharCodes.NINE
|
||||
? char - CharCodes.ZERO
|
||||
: (char | TO_LOWER_BIT) - CharCodes.LOWER_A + 10;
|
||||
this.result = this.result * 16 + digit;
|
||||
this.consumed++;
|
||||
offset++;
|
||||
} else {
|
||||
return this.emitNumericEntity(char, 3);
|
||||
}
|
||||
}
|
||||
return -1; // Incomplete entity
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a decimal numeric entity.
|
||||
*
|
||||
* Equivalent to the `Decimal character reference state` in the HTML spec.
|
||||
* @param input The string containing the entity (or a continuation of the entity).
|
||||
* @param offset The current offset.
|
||||
* @returns The number of characters that were consumed, or -1 if the entity is incomplete.
|
||||
*/
|
||||
private stateNumericDecimal(input: string, offset: number): number {
|
||||
while (offset < input.length) {
|
||||
const char = input.charCodeAt(offset);
|
||||
if (isNumber(char)) {
|
||||
this.result = this.result * 10 + (char - CharCodes.ZERO);
|
||||
this.consumed++;
|
||||
offset++;
|
||||
} else {
|
||||
return this.emitNumericEntity(char, 2);
|
||||
}
|
||||
}
|
||||
return -1; // Incomplete entity
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and emit a numeric entity.
|
||||
*
|
||||
* Implements the logic from the `Hexademical character reference start
|
||||
* state` and `Numeric character reference end state` in the HTML spec.
|
||||
* @param lastCp The last code point of the entity. Used to see if the
|
||||
* entity was terminated with a semicolon.
|
||||
* @param expectedLength The minimum number of characters that should be
|
||||
* consumed. Used to validate that at least one digit
|
||||
* was consumed.
|
||||
* @returns The number of characters that were consumed.
|
||||
*/
|
||||
private emitNumericEntity(lastCp: number, expectedLength: number): number {
|
||||
// Ensure we consumed at least one digit.
|
||||
if (this.consumed <= expectedLength) {
|
||||
this.errors?.absenceOfDigitsInNumericCharacterReference(
|
||||
this.consumed,
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Figure out if this is a legit end of the entity
|
||||
if (lastCp === CharCodes.SEMI) {
|
||||
this.consumed += 1;
|
||||
} else if (this.decodeMode === DecodingMode.Strict) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
this.emitCodePoint(replaceCodePoint(this.result), this.consumed);
|
||||
|
||||
if (this.errors) {
|
||||
if (lastCp !== CharCodes.SEMI) {
|
||||
this.errors.missingSemicolonAfterCharacterReference();
|
||||
}
|
||||
|
||||
this.errors.validateNumericCharacterReference(this.result);
|
||||
}
|
||||
|
||||
return this.consumed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a named entity.
|
||||
*
|
||||
* Equivalent to the `Named character reference state` in the HTML spec.
|
||||
* @param input The string containing the entity (or a continuation of the entity).
|
||||
* @param offset The current offset.
|
||||
* @returns The number of characters that were consumed, or -1 if the entity is incomplete.
|
||||
*/
|
||||
private stateNamedEntity(input: string, offset: number): number {
|
||||
const { decodeTree } = this;
|
||||
let current = decodeTree[this.treeIndex];
|
||||
// The length is the number of bytes of the value, including the current byte.
|
||||
let valueLength = (current & BinTrieFlags.VALUE_LENGTH) >> 14;
|
||||
|
||||
while (offset < input.length) {
|
||||
// Handle compact runs (possibly inline): valueLength == 0 and SEMI_REQUIRED bit set.
|
||||
if (valueLength === 0 && (current & BinTrieFlags.FLAG13) !== 0) {
|
||||
const runLength =
|
||||
(current & BinTrieFlags.BRANCH_LENGTH) >> 7; /* 2..63 */
|
||||
|
||||
// If we are starting a run, check the first char.
|
||||
if (this.runConsumed === 0) {
|
||||
const firstChar = current & BinTrieFlags.JUMP_TABLE;
|
||||
if (input.charCodeAt(offset) !== firstChar) {
|
||||
return this.result === 0
|
||||
? 0
|
||||
: this.emitNotTerminatedNamedEntity();
|
||||
}
|
||||
offset++;
|
||||
this.excess++;
|
||||
this.runConsumed++;
|
||||
}
|
||||
|
||||
// Check remaining characters in the run.
|
||||
while (this.runConsumed < runLength) {
|
||||
if (offset >= input.length) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const charIndexInPacked = this.runConsumed - 1;
|
||||
const packedWord =
|
||||
decodeTree[
|
||||
this.treeIndex + 1 + (charIndexInPacked >> 1)
|
||||
];
|
||||
const expectedChar =
|
||||
charIndexInPacked % 2 === 0
|
||||
? packedWord & 0xff
|
||||
: (packedWord >> 8) & 0xff;
|
||||
|
||||
if (input.charCodeAt(offset) !== expectedChar) {
|
||||
this.runConsumed = 0;
|
||||
return this.result === 0
|
||||
? 0
|
||||
: this.emitNotTerminatedNamedEntity();
|
||||
}
|
||||
offset++;
|
||||
this.excess++;
|
||||
this.runConsumed++;
|
||||
}
|
||||
|
||||
this.runConsumed = 0;
|
||||
this.treeIndex += 1 + (runLength >> 1);
|
||||
current = decodeTree[this.treeIndex];
|
||||
valueLength = (current & BinTrieFlags.VALUE_LENGTH) >> 14;
|
||||
}
|
||||
|
||||
if (offset >= input.length) break;
|
||||
|
||||
const char = input.charCodeAt(offset);
|
||||
|
||||
/*
|
||||
* Implicit semicolon handling for nodes that require a semicolon but
|
||||
* don't have an explicit ';' branch stored in the trie. If we have
|
||||
* a value on the current node, it requires a semicolon, and the
|
||||
* current input character is a semicolon, emit the entity using the
|
||||
* current node (without descending further).
|
||||
*/
|
||||
if (
|
||||
char === CharCodes.SEMI &&
|
||||
valueLength !== 0 &&
|
||||
(current & BinTrieFlags.FLAG13) !== 0
|
||||
) {
|
||||
return this.emitNamedEntityData(
|
||||
this.treeIndex,
|
||||
valueLength,
|
||||
this.consumed + this.excess,
|
||||
);
|
||||
}
|
||||
|
||||
this.treeIndex = determineBranch(
|
||||
decodeTree,
|
||||
current,
|
||||
this.treeIndex + Math.max(1, valueLength),
|
||||
char,
|
||||
);
|
||||
|
||||
if (this.treeIndex < 0) {
|
||||
return this.result === 0 ||
|
||||
// If we are parsing an attribute
|
||||
(this.decodeMode === DecodingMode.Attribute &&
|
||||
// We shouldn't have consumed any characters after the entity,
|
||||
(valueLength === 0 ||
|
||||
// And there should be no invalid characters.
|
||||
isEntityInAttributeInvalidEnd(char)))
|
||||
? 0
|
||||
: this.emitNotTerminatedNamedEntity();
|
||||
}
|
||||
|
||||
current = decodeTree[this.treeIndex];
|
||||
valueLength = (current & BinTrieFlags.VALUE_LENGTH) >> 14;
|
||||
|
||||
// If the branch is a value, store it and continue
|
||||
if (valueLength !== 0) {
|
||||
// If the entity is terminated by a semicolon, we are done.
|
||||
if (char === CharCodes.SEMI) {
|
||||
return this.emitNamedEntityData(
|
||||
this.treeIndex,
|
||||
valueLength,
|
||||
this.consumed + this.excess,
|
||||
);
|
||||
}
|
||||
|
||||
// If we encounter a non-terminated (legacy) entity while parsing strictly, then ignore it.
|
||||
if (
|
||||
this.decodeMode !== DecodingMode.Strict &&
|
||||
(current & BinTrieFlags.FLAG13) === 0
|
||||
) {
|
||||
this.result = this.treeIndex;
|
||||
this.consumed += this.excess;
|
||||
this.excess = 0;
|
||||
}
|
||||
}
|
||||
// Increment offset & excess for next iteration
|
||||
offset++;
|
||||
this.excess++;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a named entity that was not terminated with a semicolon.
|
||||
* @returns The number of characters consumed.
|
||||
*/
|
||||
private emitNotTerminatedNamedEntity(): number {
|
||||
const { result, decodeTree } = this;
|
||||
|
||||
const valueLength =
|
||||
(decodeTree[result] & BinTrieFlags.VALUE_LENGTH) >> 14;
|
||||
|
||||
this.emitNamedEntityData(result, valueLength, this.consumed);
|
||||
this.errors?.missingSemicolonAfterCharacterReference();
|
||||
|
||||
return this.consumed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a named entity.
|
||||
* @param result The index of the entity in the decode tree.
|
||||
* @param valueLength The number of bytes in the entity.
|
||||
* @param consumed The number of characters consumed.
|
||||
* @returns The number of characters consumed.
|
||||
*/
|
||||
private emitNamedEntityData(
|
||||
result: number,
|
||||
valueLength: number,
|
||||
consumed: number,
|
||||
): number {
|
||||
const { decodeTree } = this;
|
||||
|
||||
this.emitCodePoint(
|
||||
valueLength === 1
|
||||
? decodeTree[result] &
|
||||
~(BinTrieFlags.VALUE_LENGTH | BinTrieFlags.FLAG13)
|
||||
: decodeTree[result + 1],
|
||||
consumed,
|
||||
);
|
||||
if (valueLength === 3) {
|
||||
// For multi-byte values, we need to emit the second byte.
|
||||
this.emitCodePoint(decodeTree[result + 2], consumed);
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Signal to the parser that the end of the input was reached.
|
||||
*
|
||||
* Remaining data will be emitted and relevant errors will be produced.
|
||||
* @returns The number of characters consumed.
|
||||
*/
|
||||
end(): number {
|
||||
switch (this.state) {
|
||||
case EntityDecoderState.NamedEntity: {
|
||||
// Emit a named entity if we have one.
|
||||
return this.result !== 0 &&
|
||||
(this.decodeMode !== DecodingMode.Attribute ||
|
||||
this.result === this.treeIndex)
|
||||
? this.emitNotTerminatedNamedEntity()
|
||||
: 0;
|
||||
}
|
||||
// Otherwise, emit a numeric entity if we have one.
|
||||
case EntityDecoderState.NumericDecimal: {
|
||||
return this.emitNumericEntity(0, 2);
|
||||
}
|
||||
case EntityDecoderState.NumericHex: {
|
||||
return this.emitNumericEntity(0, 3);
|
||||
}
|
||||
case EntityDecoderState.NumericStart: {
|
||||
this.errors?.absenceOfDigitsInNumericCharacterReference(
|
||||
this.consumed,
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
case EntityDecoderState.EntityStart: {
|
||||
// Return 0 if we have no entity.
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a function that decodes entities in a string.
|
||||
* @param decodeTree The decode tree.
|
||||
* @returns A function that decodes entities in a string.
|
||||
*/
|
||||
function getDecoder(decodeTree: Uint16Array) {
|
||||
let returnValue = "";
|
||||
const decoder = new EntityDecoder(
|
||||
decodeTree,
|
||||
(data) => (returnValue += String.fromCodePoint(data)),
|
||||
);
|
||||
|
||||
return function decodeWithTrie(
|
||||
input: string,
|
||||
decodeMode: DecodingMode,
|
||||
): string {
|
||||
let lastIndex = 0;
|
||||
let offset = 0;
|
||||
|
||||
while ((offset = input.indexOf("&", offset)) >= 0) {
|
||||
returnValue += input.slice(lastIndex, offset);
|
||||
|
||||
decoder.startEntity(decodeMode);
|
||||
|
||||
const length = decoder.write(
|
||||
input,
|
||||
// Skip the "&"
|
||||
offset + 1,
|
||||
);
|
||||
|
||||
if (length < 0) {
|
||||
lastIndex = offset + decoder.end();
|
||||
break;
|
||||
}
|
||||
|
||||
lastIndex = offset + length;
|
||||
// If `length` is 0, skip the current `&` and continue.
|
||||
offset = length === 0 ? lastIndex + 1 : lastIndex;
|
||||
}
|
||||
|
||||
const result = returnValue + input.slice(lastIndex);
|
||||
|
||||
// Make sure we don't keep a reference to the final string.
|
||||
returnValue = "";
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the branch of the current node that is taken given the current
|
||||
* character. This function is used to traverse the trie.
|
||||
* @param decodeTree The trie.
|
||||
* @param current The current node.
|
||||
* @param nodeIndex Index immediately after the current node header.
|
||||
* @param char The current character.
|
||||
* @returns The index of the next node, or -1 if no branch is taken.
|
||||
*/
|
||||
export function determineBranch(
|
||||
decodeTree: Uint16Array,
|
||||
current: number,
|
||||
nodeIndex: number,
|
||||
char: number,
|
||||
): number {
|
||||
const branchCount = (current & BinTrieFlags.BRANCH_LENGTH) >> 7;
|
||||
const jumpOffset = current & BinTrieFlags.JUMP_TABLE;
|
||||
|
||||
// Case 1: Single branch encoded in jump offset
|
||||
if (branchCount === 0) {
|
||||
return jumpOffset !== 0 && char === jumpOffset ? nodeIndex : -1;
|
||||
}
|
||||
|
||||
// Case 2: Multiple branches encoded in jump table
|
||||
if (jumpOffset) {
|
||||
const value = char - jumpOffset;
|
||||
|
||||
return value < 0 || value >= branchCount
|
||||
? -1
|
||||
: decodeTree[nodeIndex + value] - 1;
|
||||
}
|
||||
|
||||
// Case 3: Multiple branches encoded in packed dictionary (two keys per uint16)
|
||||
const packedKeySlots = (branchCount + 1) >> 1;
|
||||
|
||||
/*
|
||||
* Treat packed keys as a virtual sorted array of length `branchCount`.
|
||||
* Key(i) = low byte for even i, high byte for odd i in slot i>>1.
|
||||
*/
|
||||
let lo = 0;
|
||||
let hi = branchCount - 1;
|
||||
|
||||
while (lo <= hi) {
|
||||
const mid = (lo + hi) >>> 1;
|
||||
const slot = mid >> 1;
|
||||
const packed = decodeTree[nodeIndex + slot];
|
||||
const midKey = (packed >> ((mid & 1) * 8)) & 0xff;
|
||||
|
||||
if (midKey < char) {
|
||||
lo = mid + 1;
|
||||
} else if (midKey > char) {
|
||||
hi = mid - 1;
|
||||
} else {
|
||||
return decodeTree[nodeIndex + packedKeySlots + mid];
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
const htmlDecoder = /* #__PURE__ */ getDecoder(htmlDecodeTree);
|
||||
const xmlDecoder = /* #__PURE__ */ getDecoder(xmlDecodeTree);
|
||||
|
||||
/**
|
||||
* Decodes an HTML string.
|
||||
* @param htmlString The string to decode.
|
||||
* @param mode The decoding mode.
|
||||
* @returns The decoded string.
|
||||
*/
|
||||
export function decodeHTML(
|
||||
htmlString: string,
|
||||
mode: DecodingMode = DecodingMode.Legacy,
|
||||
): string {
|
||||
return htmlDecoder(htmlString, mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes an HTML string in an attribute.
|
||||
* @param htmlAttribute The string to decode.
|
||||
* @returns The decoded string.
|
||||
*/
|
||||
export function decodeHTMLAttribute(htmlAttribute: string): string {
|
||||
return htmlDecoder(htmlAttribute, DecodingMode.Attribute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes an HTML string, requiring all entities to be terminated by a semicolon.
|
||||
* @param htmlString The string to decode.
|
||||
* @returns The decoded string.
|
||||
*/
|
||||
export function decodeHTMLStrict(htmlString: string): string {
|
||||
return htmlDecoder(htmlString, DecodingMode.Strict);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes an XML string, requiring all entities to be terminated by a semicolon.
|
||||
* @param xmlString The string to decode.
|
||||
* @returns The decoded string.
|
||||
*/
|
||||
export function decodeXML(xmlString: string): string {
|
||||
return xmlDecoder(xmlString, DecodingMode.Strict);
|
||||
}
|
||||
|
||||
export { replaceCodePoint } from "./decode-codepoint.js";
|
||||
// Re-export for use by eg. htmlparser2
|
||||
export { htmlDecodeTree } from "./generated/decode-data-html.js";
|
||||
export { xmlDecodeTree } from "./generated/decode-data-xml.js";
|
||||
95
node_modules/entities/src/encode.ts
generated
vendored
95
node_modules/entities/src/encode.ts
generated
vendored
@@ -1,95 +0,0 @@
|
||||
import { getCodePoint, XML_BITSET_VALUE } from "./escape.js";
|
||||
import { htmlTrie } from "./generated/encode-html.js";
|
||||
|
||||
/**
|
||||
* We store the characters to consider as a compact bitset for fast lookups.
|
||||
*/
|
||||
const HTML_BITSET = /* #__PURE__ */ new Uint32Array([
|
||||
0x16_00, // Bits for 09,0A,0C
|
||||
0xfc_00_ff_fe, // 32..63 -> 21-2D (minus space), 2E,2F,3A-3F
|
||||
0xf8_00_00_01, // 64..95 -> 40, 5B-5F
|
||||
0x38_00_00_01, // 96..127-> 60, 7B-7D
|
||||
]);
|
||||
|
||||
const XML_BITSET = /* #__PURE__ */ new Uint32Array([0, XML_BITSET_VALUE, 0, 0]);
|
||||
|
||||
/**
|
||||
* Encodes all characters in the input using HTML entities. This includes
|
||||
* characters that are valid ASCII characters in HTML documents, such as `#`.
|
||||
*
|
||||
* To get a more compact output, consider using the `encodeNonAsciiHTML`
|
||||
* function, which will only encode characters that are not valid in HTML
|
||||
* documents, as well as non-ASCII characters.
|
||||
*
|
||||
* If a character has no equivalent entity, a numeric hexadecimal reference
|
||||
* (eg. `ü`) will be used.
|
||||
* @param input Input string to encode or decode.
|
||||
*/
|
||||
export function encodeHTML(input: string): string {
|
||||
return encodeHTMLTrieRe(HTML_BITSET, input);
|
||||
}
|
||||
/**
|
||||
* Encodes all non-ASCII characters, as well as characters not valid in HTML
|
||||
* documents using HTML entities. This function will not encode characters that
|
||||
* are valid in HTML documents, such as `#`.
|
||||
*
|
||||
* If a character has no equivalent entity, a numeric hexadecimal reference
|
||||
* (eg. `ü`) will be used.
|
||||
* @param input Input string to encode or decode.
|
||||
*/
|
||||
export function encodeNonAsciiHTML(input: string): string {
|
||||
return encodeHTMLTrieRe(XML_BITSET, input);
|
||||
}
|
||||
|
||||
function encodeHTMLTrieRe(bitset: Uint32Array, input: string): string {
|
||||
let out: string | undefined;
|
||||
let last = 0; // Start of the next untouched slice.
|
||||
const { length } = input;
|
||||
|
||||
for (let index = 0; index < length; index++) {
|
||||
const char = input.charCodeAt(index);
|
||||
// Skip ASCII characters that don't need encoding
|
||||
if (char < 0x80 && !((bitset[char >>> 5] >>> char) & 1)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (out === undefined) out = input.substring(0, index);
|
||||
else if (last !== index) out += input.substring(last, index);
|
||||
|
||||
let node = htmlTrie.get(char);
|
||||
|
||||
if (typeof node === "object") {
|
||||
if (index + 1 < length) {
|
||||
const nextChar = input.charCodeAt(index + 1);
|
||||
const value =
|
||||
typeof node.next === "number"
|
||||
? node.next === nextChar
|
||||
? node.nextValue
|
||||
: undefined
|
||||
: node.next.get(nextChar);
|
||||
|
||||
if (value !== undefined) {
|
||||
out += value;
|
||||
index++;
|
||||
last = index + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
node = node.value;
|
||||
}
|
||||
|
||||
if (node === undefined) {
|
||||
const cp = getCodePoint(input, index);
|
||||
out += `&#x${cp.toString(16)};`;
|
||||
if (cp !== char) index++;
|
||||
last = index + 1;
|
||||
} else {
|
||||
out += node;
|
||||
last = index + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (out === undefined) return input;
|
||||
if (last < length) out += input.substr(last);
|
||||
return out;
|
||||
}
|
||||
160
node_modules/entities/src/escape.ts
generated
vendored
160
node_modules/entities/src/escape.ts
generated
vendored
@@ -1,160 +0,0 @@
|
||||
const xmlCodeMap = new Map([
|
||||
[34, """],
|
||||
[38, "&"],
|
||||
[39, "'"],
|
||||
[60, "<"],
|
||||
[62, ">"],
|
||||
]);
|
||||
|
||||
// For compatibility with node < 4, we wrap `codePointAt`
|
||||
/**
|
||||
* Read a code point at a given index.
|
||||
* @param input Input string to encode or decode.
|
||||
* @param index Current read position in the input string.
|
||||
*/
|
||||
export const getCodePoint: (c: string, index: number) => number =
|
||||
typeof String.prototype.codePointAt === "function"
|
||||
? (input: string, index: number): number => input.codePointAt(index)!
|
||||
: // http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
|
||||
(c: string, index: number): number =>
|
||||
(c.charCodeAt(index) & 0xfc_00) === 0xd8_00
|
||||
? (c.charCodeAt(index) - 0xd8_00) * 0x4_00 +
|
||||
c.charCodeAt(index + 1) -
|
||||
0xdc_00 +
|
||||
0x1_00_00
|
||||
: c.charCodeAt(index);
|
||||
|
||||
/**
|
||||
* Bitset for ASCII characters that need to be escaped in XML.
|
||||
*/
|
||||
export const XML_BITSET_VALUE = 0x50_00_00_c4; // 32..63 -> 34 ("),38 (&),39 ('),60 (<),62 (>)
|
||||
|
||||
/**
|
||||
* Encodes all non-ASCII characters, as well as characters not valid in XML
|
||||
* documents using XML entities. Uses a fast bitset scan instead of RegExp.
|
||||
*
|
||||
* If a character has no equivalent entity, a numeric hexadecimal reference
|
||||
* (eg. `ü`) will be used.
|
||||
* @param input Input string to encode or decode.
|
||||
*/
|
||||
export function encodeXML(input: string): string {
|
||||
let out: string | undefined;
|
||||
let last = 0;
|
||||
const { length } = input;
|
||||
|
||||
for (let index = 0; index < length; index++) {
|
||||
const char = input.charCodeAt(index);
|
||||
|
||||
// Check for ASCII chars that don't need escaping
|
||||
if (
|
||||
char < 0x80 &&
|
||||
(((XML_BITSET_VALUE >>> char) & 1) === 0 || char >= 64 || char < 32)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (out === undefined) out = input.substring(0, index);
|
||||
else if (last !== index) out += input.substring(last, index);
|
||||
|
||||
if (char < 64) {
|
||||
// Known replacement
|
||||
out += xmlCodeMap.get(char)!;
|
||||
last = index + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Non-ASCII: encode as numeric entity (handle surrogate pair)
|
||||
const cp = getCodePoint(input, index);
|
||||
out += `&#x${cp.toString(16)};`;
|
||||
if (cp !== char) index++; // Skip trailing surrogate
|
||||
last = index + 1;
|
||||
}
|
||||
|
||||
if (out === undefined) return input;
|
||||
if (last < length) out += input.substr(last);
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes all non-ASCII characters, as well as characters not valid in XML
|
||||
* documents using numeric hexadecimal reference (eg. `ü`).
|
||||
*
|
||||
* Have a look at `escapeUTF8` if you want a more concise output at the expense
|
||||
* of reduced transportability.
|
||||
* @param data String to escape.
|
||||
*/
|
||||
export const escape: typeof encodeXML = encodeXML;
|
||||
|
||||
/**
|
||||
* Creates a function that escapes all characters matched by the given regular
|
||||
* expression using the given map of characters to escape to their entities.
|
||||
* @param regex Regular expression to match characters to escape.
|
||||
* @param map Map of characters to escape to their entities.
|
||||
* @returns Function that escapes all characters matched by the given regular
|
||||
* expression using the given map of characters to escape to their entities.
|
||||
*/
|
||||
function getEscaper(
|
||||
regex: RegExp,
|
||||
map: Map<number, string>,
|
||||
): (data: string) => string {
|
||||
return function escape(data: string): string {
|
||||
let match: RegExpExecArray | null;
|
||||
let lastIndex = 0;
|
||||
let result = "";
|
||||
|
||||
while ((match = regex.exec(data))) {
|
||||
if (lastIndex !== match.index) {
|
||||
result += data.substring(lastIndex, match.index);
|
||||
}
|
||||
|
||||
// We know that this character will be in the map.
|
||||
result += map.get(match[0].charCodeAt(0))!;
|
||||
|
||||
// Every match will be of length 1
|
||||
lastIndex = match.index + 1;
|
||||
}
|
||||
|
||||
return result + data.substring(lastIndex);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes all characters not valid in XML documents using XML entities.
|
||||
*
|
||||
* Note that the output will be character-set dependent.
|
||||
* @param data String to escape.
|
||||
*/
|
||||
export const escapeUTF8: (data: string) => string = /* #__PURE__ */ getEscaper(
|
||||
/["&'<>]/g,
|
||||
xmlCodeMap,
|
||||
);
|
||||
|
||||
/**
|
||||
* Encodes all characters that have to be escaped in HTML attributes,
|
||||
* following {@link https://html.spec.whatwg.org/multipage/parsing.html#escapingString}.
|
||||
* @param data String to escape.
|
||||
*/
|
||||
export const escapeAttribute: (data: string) => string =
|
||||
/* #__PURE__ */ getEscaper(
|
||||
/["&\u00A0]/g,
|
||||
new Map([
|
||||
[34, """],
|
||||
[38, "&"],
|
||||
[160, " "],
|
||||
]),
|
||||
);
|
||||
|
||||
/**
|
||||
* Encodes all characters that have to be escaped in HTML text,
|
||||
* following {@link https://html.spec.whatwg.org/multipage/parsing.html#escapingString}.
|
||||
* @param data String to escape.
|
||||
*/
|
||||
export const escapeText: (data: string) => string = /* #__PURE__ */ getEscaper(
|
||||
/[&<>\u00A0]/g,
|
||||
new Map([
|
||||
[38, "&"],
|
||||
[60, "<"],
|
||||
[62, ">"],
|
||||
[160, " "],
|
||||
]),
|
||||
);
|
||||
7
node_modules/entities/src/generated/decode-data-html.ts
generated
vendored
7
node_modules/entities/src/generated/decode-data-html.ts
generated
vendored
File diff suppressed because one or more lines are too long
7
node_modules/entities/src/generated/decode-data-xml.ts
generated
vendored
7
node_modules/entities/src/generated/decode-data-xml.ts
generated
vendored
@@ -1,7 +0,0 @@
|
||||
// Generated using scripts/write-decode-map.ts
|
||||
|
||||
import { decodeBase64 } from "../internal/decode-shared.js";
|
||||
/** Packed XML decode trie data. */
|
||||
export const xmlDecodeTree: Uint16Array = /* #__PURE__ */ decodeBase64(
|
||||
"AAJhZ2xxBwARABMAFQBtAg0AAAAAAA8AcAAmYG8AcwAnYHQAPmB0ADxg9SFvdCJg",
|
||||
);
|
||||
18
node_modules/entities/src/generated/encode-html.ts
generated
vendored
18
node_modules/entities/src/generated/encode-html.ts
generated
vendored
File diff suppressed because one or more lines are too long
162
node_modules/entities/src/index.ts
generated
vendored
162
node_modules/entities/src/index.ts
generated
vendored
@@ -1,162 +0,0 @@
|
||||
import { type DecodingMode, decodeHTML, decodeXML } from "./decode.js";
|
||||
import { encodeHTML, encodeNonAsciiHTML } from "./encode.js";
|
||||
import {
|
||||
encodeXML,
|
||||
escapeAttribute,
|
||||
escapeText,
|
||||
escapeUTF8,
|
||||
} from "./escape.js";
|
||||
|
||||
/** The level of entities to support. */
|
||||
export enum EntityLevel {
|
||||
/** Support only XML entities. */
|
||||
XML = 0,
|
||||
/** Support HTML entities, which are a superset of XML entities. */
|
||||
HTML = 1,
|
||||
}
|
||||
|
||||
/**
|
||||
* Encoding strategy used by `encode`.
|
||||
*/
|
||||
export enum EncodingMode {
|
||||
/**
|
||||
* The output is UTF-8 encoded. Only characters that need escaping within
|
||||
* XML will be escaped.
|
||||
*/
|
||||
UTF8,
|
||||
/**
|
||||
* The output consists only of ASCII characters. Characters that need
|
||||
* escaping within HTML, and characters that aren't ASCII characters will
|
||||
* be escaped.
|
||||
*/
|
||||
ASCII,
|
||||
/**
|
||||
* Encode all characters that have an equivalent entity, as well as all
|
||||
* characters that are not ASCII characters.
|
||||
*/
|
||||
Extensive,
|
||||
/**
|
||||
* Encode all characters that have to be escaped in HTML attributes,
|
||||
* following {@link https://html.spec.whatwg.org/multipage/parsing.html#escapingString}.
|
||||
*/
|
||||
Attribute,
|
||||
/**
|
||||
* Encode all characters that have to be escaped in HTML text,
|
||||
* following {@link https://html.spec.whatwg.org/multipage/parsing.html#escapingString}.
|
||||
*/
|
||||
Text,
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for `decode`.
|
||||
*/
|
||||
export interface DecodingOptions {
|
||||
/**
|
||||
* The level of entities to support.
|
||||
* @default {@link EntityLevel.XML}
|
||||
*/
|
||||
level?: EntityLevel;
|
||||
/**
|
||||
* Decoding mode. If `Legacy`, will support legacy entities not terminated
|
||||
* with a semicolon (`;`).
|
||||
*
|
||||
* Always `Strict` for XML. For HTML, set this to `true` if you are parsing
|
||||
* an attribute value.
|
||||
* @default {@link DecodingMode.Legacy}
|
||||
*/
|
||||
mode?: DecodingMode | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a string with entities.
|
||||
* @param input String to decode.
|
||||
* @param options Decoding options.
|
||||
*/
|
||||
export function decode(
|
||||
input: string,
|
||||
options: DecodingOptions | EntityLevel = EntityLevel.XML,
|
||||
): string {
|
||||
const level = typeof options === "number" ? options : options.level;
|
||||
|
||||
if (level === EntityLevel.HTML) {
|
||||
const mode = typeof options === "object" ? options.mode : undefined;
|
||||
return decodeHTML(input, mode);
|
||||
}
|
||||
|
||||
return decodeXML(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for `encode`.
|
||||
*/
|
||||
export interface EncodingOptions {
|
||||
/**
|
||||
* The level of entities to support.
|
||||
* @default {@link EntityLevel.XML}
|
||||
*/
|
||||
level?: EntityLevel;
|
||||
/**
|
||||
* Output format.
|
||||
* @default {@link EncodingMode.Extensive}
|
||||
*/
|
||||
mode?: EncodingMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a string with entities.
|
||||
* @param input String to encode.
|
||||
* @param options Encoding options.
|
||||
*/
|
||||
export function encode(
|
||||
input: string,
|
||||
options: EncodingOptions | EntityLevel = EntityLevel.XML,
|
||||
): string {
|
||||
const { mode = EncodingMode.Extensive, level = EntityLevel.XML } =
|
||||
typeof options === "number" ? { level: options } : options;
|
||||
|
||||
switch (mode) {
|
||||
case EncodingMode.UTF8: {
|
||||
return escapeUTF8(input);
|
||||
}
|
||||
case EncodingMode.Attribute: {
|
||||
return escapeAttribute(input);
|
||||
}
|
||||
case EncodingMode.Text: {
|
||||
return escapeText(input);
|
||||
}
|
||||
case EncodingMode.ASCII: {
|
||||
return level === EntityLevel.HTML
|
||||
? encodeNonAsciiHTML(input)
|
||||
: encodeXML(input);
|
||||
}
|
||||
// biome-ignore lint/complexity/noUselessSwitchCase: we get an error for the switch not being exhaustive
|
||||
case EncodingMode.Extensive:
|
||||
default: {
|
||||
return level === EntityLevel.HTML
|
||||
? encodeHTML(input)
|
||||
: encodeXML(input);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
DecodingMode,
|
||||
decodeHTML,
|
||||
decodeHTMLAttribute,
|
||||
decodeHTMLStrict,
|
||||
decodeXML,
|
||||
decodeXML as decodeXMLStrict,
|
||||
EntityDecoder,
|
||||
} from "./decode.js";
|
||||
|
||||
export {
|
||||
encodeHTML,
|
||||
encodeNonAsciiHTML,
|
||||
} from "./encode.js";
|
||||
export {
|
||||
encodeXML,
|
||||
escape,
|
||||
escapeAttribute,
|
||||
escapeText,
|
||||
escapeUTF8,
|
||||
} from "./escape.js";
|
||||
16
node_modules/entities/src/internal/bin-trie-flags.ts
generated
vendored
16
node_modules/entities/src/internal/bin-trie-flags.ts
generated
vendored
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* Bit flags & masks for the binary trie encoding used for entity decoding.
|
||||
*
|
||||
* Bit layout (16 bits total):
|
||||
* 15..14 VALUE_LENGTH (+1 encoding; 0 => no value)
|
||||
* 13 FLAG13. If valueLength>0: semicolon required flag (implicit ';').
|
||||
* If valueLength==0: compact run flag.
|
||||
* 12..7 BRANCH_LENGTH Branch length (0 => single branch in 6..0 if jumpOffset==char) OR run length (when compact run)
|
||||
* 6..0 JUMP_TABLE Jump offset (jump table) OR single-branch char code OR first run char
|
||||
*/
|
||||
export enum BinTrieFlags {
|
||||
VALUE_LENGTH = 0b1100_0000_0000_0000,
|
||||
FLAG13 = 0b0010_0000_0000_0000,
|
||||
BRANCH_LENGTH = 0b0001_1111_1000_0000,
|
||||
JUMP_TABLE = 0b0000_0000_0111_1111,
|
||||
}
|
||||
18
node_modules/entities/src/internal/decode-shared.ts
generated
vendored
18
node_modules/entities/src/internal/decode-shared.ts
generated
vendored
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* Shared base64 decode helper for generated decode data.
|
||||
* Assumes global atob is available.
|
||||
* @param input Input string to encode or decode.
|
||||
*/
|
||||
export function decodeBase64(input: string): Uint16Array {
|
||||
const binary: string = atob(input);
|
||||
const evenLength = binary.length & ~1; // Round down to even length
|
||||
const out = new Uint16Array(evenLength / 2);
|
||||
|
||||
for (let index = 0, outIndex = 0; index < evenLength; index += 2) {
|
||||
const lo = binary.charCodeAt(index);
|
||||
const hi = binary.charCodeAt(index + 1);
|
||||
out[outIndex++] = lo | (hi << 8);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
123
node_modules/entities/src/internal/encode-shared.ts
generated
vendored
123
node_modules/entities/src/internal/encode-shared.ts
generated
vendored
@@ -1,123 +0,0 @@
|
||||
/**
|
||||
* A node inside the encoding trie used by `encode.ts`.
|
||||
*
|
||||
* There are two physical shapes to minimize allocations and lookup cost:
|
||||
*
|
||||
* 1. Leaf node (string)
|
||||
* - A plain string (already in the form `"&name;"`).
|
||||
* - Represents a terminal match with no children.
|
||||
*
|
||||
* 2. Branch / value node (object)
|
||||
*/
|
||||
export type EncodeTrieNode =
|
||||
| string
|
||||
| {
|
||||
/**
|
||||
* Entity value for the current code point sequence (wrapped: `&...;`).
|
||||
* Present when the path to this node itself is a valid named entity.
|
||||
*/
|
||||
value: string | undefined;
|
||||
/** If a number, the next code unit of the only next character. */
|
||||
next: number | Map<number, EncodeTrieNode>;
|
||||
/** If next is a number, `nextValue` contains the entity value. */
|
||||
nextValue?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse a compact encode trie string into a Map structure used for encoding.
|
||||
*
|
||||
* Format per entry (ascending code points using delta encoding):
|
||||
* <diffBase36>[&name;][{<children>}] -- diff omitted when 0
|
||||
* Where diff = currentKey - previousKey - 1 (first entry stores absolute key).
|
||||
* `&name;` is the entity value (already wrapped); a following `{` denotes children.
|
||||
* @param serialized Serialized text fragment to encode.
|
||||
*/
|
||||
export function parseEncodeTrie(
|
||||
serialized: string,
|
||||
): Map<number, EncodeTrieNode> {
|
||||
const top = new Map<number, EncodeTrieNode>();
|
||||
const totalLength = serialized.length;
|
||||
let cursor = 0;
|
||||
let lastTopKey = -1;
|
||||
|
||||
function readDiff(): number {
|
||||
const start = cursor;
|
||||
while (cursor < totalLength) {
|
||||
const char = serialized.charAt(cursor);
|
||||
|
||||
if ((char < "0" || char > "9") && (char < "a" || char > "z")) {
|
||||
break;
|
||||
}
|
||||
cursor++;
|
||||
}
|
||||
if (cursor === start) return 0;
|
||||
return Number.parseInt(serialized.slice(start, cursor), 36);
|
||||
}
|
||||
|
||||
function readEntity(): string {
|
||||
if (serialized[cursor] !== "&") {
|
||||
throw new Error(`Child entry missing value near index ${cursor}`);
|
||||
}
|
||||
|
||||
// Cursor currently points at '&'
|
||||
const start = cursor;
|
||||
const end = serialized.indexOf(";", cursor + 1);
|
||||
if (end === -1) {
|
||||
throw new Error(`Unterminated entity starting at index ${start}`);
|
||||
}
|
||||
cursor = end + 1; // Move past ';'
|
||||
return serialized.slice(start, cursor); // Includes & ... ;
|
||||
}
|
||||
|
||||
while (cursor < totalLength) {
|
||||
const keyDiff = readDiff();
|
||||
const key = lastTopKey === -1 ? keyDiff : lastTopKey + keyDiff + 1;
|
||||
|
||||
let value: string | undefined;
|
||||
if (serialized[cursor] === "&") value = readEntity();
|
||||
|
||||
if (serialized[cursor] === "{") {
|
||||
cursor++; // Skip '{'
|
||||
// Parse first child
|
||||
let diff = readDiff();
|
||||
let childKey = diff; // First key (lastChildKey = -1)
|
||||
const firstValue = readEntity();
|
||||
if (serialized[cursor] === "{") {
|
||||
throw new Error("Unexpected nested '{' beyond depth 2");
|
||||
}
|
||||
// If end of block -> single child optimization
|
||||
if (serialized[cursor] === "}") {
|
||||
top.set(key, { value, next: childKey, nextValue: firstValue });
|
||||
cursor++; // Skip '}'
|
||||
} else {
|
||||
const childMap = new Map<number, EncodeTrieNode>([
|
||||
[childKey, firstValue],
|
||||
]);
|
||||
let lastChildKey = childKey;
|
||||
while (cursor < totalLength && serialized[cursor] !== "}") {
|
||||
diff = readDiff();
|
||||
childKey = lastChildKey + diff + 1;
|
||||
const childValue = readEntity();
|
||||
if (serialized[cursor] === "{") {
|
||||
throw new Error("Unexpected nested '{' beyond depth 2");
|
||||
}
|
||||
childMap.set(childKey, childValue);
|
||||
lastChildKey = childKey;
|
||||
}
|
||||
if (serialized[cursor] !== "}") {
|
||||
throw new Error("Unterminated child block");
|
||||
}
|
||||
cursor++; // Skip '}'
|
||||
top.set(key, { value, next: childMap });
|
||||
}
|
||||
} else if (value === undefined) {
|
||||
throw new Error(
|
||||
`Malformed encode trie: missing value at index ${cursor}`,
|
||||
);
|
||||
} else {
|
||||
top.set(key, value);
|
||||
}
|
||||
lastTopKey = key;
|
||||
}
|
||||
return top;
|
||||
}
|
||||
64
package-lock.json
generated
64
package-lock.json
generated
@@ -13,6 +13,7 @@
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/preset-env": "^7.26.0",
|
||||
"@playwright/test": "^1.60.0",
|
||||
"babel-loader": "^9.2.1",
|
||||
"fake-indexeddb": "^6.2.5",
|
||||
"html-webpack-plugin": "^5.6.7",
|
||||
@@ -3080,6 +3081,22 @@
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz",
|
||||
"integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.60.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm64": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz",
|
||||
@@ -9421,6 +9438,53 @@
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.60.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
|
||||
"integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.60.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.60.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
|
||||
"integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright/node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.15",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/preset-env": "^7.26.0",
|
||||
"@playwright/test": "^1.60.0",
|
||||
"babel-loader": "^9.2.1",
|
||||
"fake-indexeddb": "^6.2.5",
|
||||
"html-webpack-plugin": "^5.6.7",
|
||||
|
||||
105
scripts/generate-tilesets.js
Normal file
105
scripts/generate-tilesets.js
Normal file
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Generate tileset assets using Google Imagen 4.0
|
||||
* Run from iron-requiem root: node scripts/generate-tilesets.js
|
||||
*/
|
||||
|
||||
import { googleImageGeneration } from 'hermes-tools';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const OUTPUT_BASE = './assets/maps';
|
||||
|
||||
const TILESETS = {
|
||||
tundra: {
|
||||
zone: 'Frozen Tundra',
|
||||
palette: 'Winter Whites, Steel Blues, Deep Greys',
|
||||
tiles: [
|
||||
{ name: 'snow_ground', prompt: '64x64 seamless game tile, snow-covered ground with subtle wind-swept drift patterns, top-down view, gritty pixel art, 10-color bleak winter palette, high contrast for visibility' },
|
||||
{ name: 'ice_patch', prompt: '64x64 seamless game tile, translucent ice overlay with crystalline cracks over snow, top-down view, gritty pixel art, winter palette with icy blues' },
|
||||
{ name: 'snowdrift', prompt: '64x64 game asset, medium snowdrift obstacle for tank cover, transparent background, gritty pixel art, winter palette' },
|
||||
{ name: 'tree_stump', prompt: '64x64 game asset, frozen tree stump small cover obstacle, transparent background, gritty pixel art, dead wood in winter setting' },
|
||||
{ name: 'ice_formation', prompt: '64x64 game asset, jagged ice formation large cover obstacle, transparent background, gritty pixel art, sharp crystalline structures' }
|
||||
]
|
||||
},
|
||||
industrial: {
|
||||
zone: 'Soviet Industrial',
|
||||
palette: 'Concrete Grey, Rust Orange, Oil Black, Steel Blue',
|
||||
tiles: [
|
||||
{ name: 'ground_concrete', prompt: '64x64 seamless game tile, cracked concrete ground with oil stains, top-down view, gritty pixel art, 10-color industrial palette' },
|
||||
{ name: 'rust_patch', prompt: '64x64 seamless game tile, corroded rust patch overlay on concrete, top-down view, gritty pixel art, rust orange and brown tones' },
|
||||
{ name: 'steel_drum', prompt: '64x64 game asset, steel drum small cover obstacle, transparent background, gritty pixel art, rusty industrial barrel' },
|
||||
{ name: 'concrete_barrier', prompt: '64x64 game asset, concrete barrier medium cover obstacle (jersey barrier style), transparent background, gritty pixel art' },
|
||||
{ name: 'crate_stack', prompt: '64x64 game asset, rusty metal crate stack large cover obstacle, transparent background, gritty pixel art, industrial storage crates' }
|
||||
]
|
||||
},
|
||||
city: {
|
||||
zone: 'European City Ruins',
|
||||
palette: 'Stone Grey, Brick Red, Mortar White, Soot Black',
|
||||
tiles: [
|
||||
{ name: 'cobblestone', prompt: '64x64 seamless game tile, cracked cobblestone street, top-down view, gritty pixel art, 10-color urban decay palette' },
|
||||
{ name: 'rubble_patch', prompt: '64x64 seamless game tile, debris and broken masonry rubble, top-down view, gritty pixel art, war-torn city ruins' },
|
||||
{ name: 'arch_fragment', prompt: '64x64 game asset, gothic stone arch fragment small cover obstacle, transparent background, gritty pixel art, medieval architecture ruin' },
|
||||
{ name: 'wall_section', prompt: '64x64 game asset, collapsed brick wall section medium cover obstacle, transparent background, gritty pixel art, bombed building debris' },
|
||||
{ name: 'rubble_pile', prompt: '64x64 game asset, large pile of rubble and broken stone, transparent background, gritty pixel art, urban warfare cover' }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
async function generateTileset(zoneName, config) {
|
||||
console.log(`\n=== Generating ${config.zone} tileset ===`);
|
||||
const zoneDir = path.join(OUTPUT_BASE, zoneName);
|
||||
|
||||
if (!fs.existsSync(zoneDir)) {
|
||||
fs.mkdirSync(zoneDir, { recursive: true });
|
||||
}
|
||||
|
||||
for (const tile of config.tiles) {
|
||||
const outputPath = path.join(zoneDir, `${tile.name}.png`);
|
||||
|
||||
// Skip if already exists
|
||||
if (fs.existsSync(outputPath)) {
|
||||
console.log(` ✓ ${tile.name}.png (exists)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(` Generating ${tile.name}...`);
|
||||
|
||||
try {
|
||||
// Use google-image-generation tool
|
||||
const result = await googleImageGeneration({
|
||||
prompt: tile.prompt,
|
||||
aspect_ratio: 'square'
|
||||
});
|
||||
|
||||
// result.image should be URL or path
|
||||
const imageUrl = result.image;
|
||||
console.log(` Generated: ${imageUrl}`);
|
||||
|
||||
// If it's a local path, copy it
|
||||
if (imageUrl.startsWith('/')) {
|
||||
fs.copyFileSync(imageUrl, outputPath);
|
||||
console.log(` Saved: ${outputPath}`);
|
||||
} else {
|
||||
// It's a URL - would need to download
|
||||
console.log(` URL returned (manual download needed): ${imageUrl}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(` ERROR: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('Iron Requiem Tileset Generator');
|
||||
console.log('Using Google Imagen 4.0 via google-image-generation skill\n');
|
||||
|
||||
for (const [zoneName, config] of Object.entries(TILESETS)) {
|
||||
await generateTileset(zoneName, config);
|
||||
}
|
||||
|
||||
console.log('\n=== Done ===');
|
||||
console.log(`Assets saved to ${OUTPUT_BASE}/`);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -1,4 +1,23 @@
|
||||
/**
|
||||
* Iron Requiem — Bullet Hell Pattern Definitions
|
||||
*
|
||||
* Every pattern MUST document a provable safe zone. This is non-negotiable:
|
||||
* bullet-hell fairness requires that the player can always identify and
|
||||
* reach a position where they won't take damage.
|
||||
*
|
||||
* @module src/data/patterns
|
||||
*/
|
||||
|
||||
export const patterns = {
|
||||
/**
|
||||
* infantry_wall — Horizontal line of projectiles sweeping across screen.
|
||||
*
|
||||
* Used by: Type 62 (Light tank), Zone 1-3
|
||||
*
|
||||
* Safe zone: A 60px vertical gap exists above and below the projectile
|
||||
* line. The wall is a single horizontal row — projectiles have no
|
||||
* vertical spread. Standing at y = lineY ± 60 guarantees safety.
|
||||
*/
|
||||
infantry_wall: {
|
||||
patternId: 'infantry_wall',
|
||||
projectileCount: 40,
|
||||
@@ -6,5 +25,83 @@ export const patterns = {
|
||||
telegraphTime: 1500,
|
||||
telegraphAudio: 'plaa_bugle',
|
||||
direction: 'left-to-right',
|
||||
spacing: 12,
|
||||
safeZone:
|
||||
'Above or below the horizontal wall line by >=60px — ' +
|
||||
'projectiles form a single row with no vertical spread.',
|
||||
},
|
||||
|
||||
/**
|
||||
* tank_destroyer_beam — Narrow cone beam from a heavy tank.
|
||||
*
|
||||
* Used by: Type 59 (Heavy), Zone 2-3
|
||||
*
|
||||
* Safe zone: Outside the 45° cone spread. The beam fires in a fixed
|
||||
* direction from the enemy toward the player's last known position.
|
||||
* There is no tracking after launch — any position more than 22.5°
|
||||
* off the beam axis is safe.
|
||||
*/
|
||||
tank_destroyer_beam: {
|
||||
patternId: 'tank_destroyer_beam',
|
||||
projectileCount: 12,
|
||||
spawnInterval: 80,
|
||||
telegraphTime: 2000,
|
||||
telegraphAudio: 'beam_charge',
|
||||
direction: 'targeted',
|
||||
spreadAngle: 45,
|
||||
spacing: 16,
|
||||
safeZone:
|
||||
'Outside the 45° cone spread — beam fires in a fixed direction ' +
|
||||
'with no tracking after launch. Any position >22.5° off axis is safe.',
|
||||
},
|
||||
|
||||
/**
|
||||
* artillery_ring — Expanding ring of projectiles from a static emplacement.
|
||||
*
|
||||
* Used by: Artillery Emplacement, all zones
|
||||
*
|
||||
* Safe zone: The center of the ring at the moment of spawn (radius
|
||||
* 0–20 px). All projectiles travel radially outward from a single
|
||||
* origin point. Standing at the exact spawn center places the player
|
||||
* in the eye of the storm. The safe zone shrinks to zero as the first
|
||||
* wave exits — timing is everything.
|
||||
*/
|
||||
artillery_ring: {
|
||||
patternId: 'artillery_ring',
|
||||
projectileCount: 24,
|
||||
spawnInterval: 30,
|
||||
telegraphTime: 2000,
|
||||
telegraphAudio: 'mortar_whistle',
|
||||
direction: 'radial_out',
|
||||
spacing: 15,
|
||||
safeZone:
|
||||
'The center of the ring at time of spawn (radius 0–20 px) — ' +
|
||||
'all projectiles travel radially outward from a single origin. ' +
|
||||
'Safe zone shrinks to 0 after first wave exits.',
|
||||
},
|
||||
|
||||
/**
|
||||
* heli_sweep — Helicopter gunship strafing run.
|
||||
*
|
||||
* Used by: Helicopter Gunship, Zone 2-3
|
||||
*
|
||||
* Safe zone: The opposite screen edge from the sweep entry side.
|
||||
* The helicopter crosses the screen in a straight line, firing
|
||||
* projectiles in a 60° forward cone. The area behind the entry
|
||||
* point (opposite edge of the screen) is completely safe during
|
||||
* the sweep.
|
||||
*/
|
||||
heli_sweep: {
|
||||
patternId: 'heli_sweep',
|
||||
projectileCount: 30,
|
||||
spawnInterval: 40,
|
||||
telegraphTime: 1500,
|
||||
telegraphAudio: 'rotor_wash',
|
||||
direction: 'left-to-right',
|
||||
spreadAngle: 60,
|
||||
spacing: 14,
|
||||
safeZone:
|
||||
'The opposite screen edge from sweep entry — projectiles fire ' +
|
||||
'in a forward cone; the area behind the entry point is completely safe.',
|
||||
},
|
||||
};
|
||||
|
||||
39
src/data/zones.js
Normal file
39
src/data/zones.js
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Iron Requiem — Zone Definitions
|
||||
*
|
||||
* Each zone defines its position thresholds, visual theme, and enemy roster.
|
||||
* Ordered from start (left) to end (right).
|
||||
*
|
||||
* @module src/data/zones
|
||||
*/
|
||||
|
||||
export const zones = [
|
||||
{
|
||||
id: 1,
|
||||
label: 'Tundra',
|
||||
xMax: 2000,
|
||||
background: 'tundra_bg',
|
||||
tileset: 'tundra',
|
||||
enemyTypes: ['type62', 'artillery_emplacement'],
|
||||
obstaclePalette: ['#558855', '#668866'], // forest greens
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
label: 'Taiga',
|
||||
xMin: 2000,
|
||||
xMax: 4000,
|
||||
background: 'taiga_bg',
|
||||
tileset: 'taiga',
|
||||
enemyTypes: ['type59', 'type62', 'helicopter_gunship', 'artillery_emplacement'],
|
||||
obstaclePalette: ['#445544', '#334433'], // darker greens
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
label: 'Urban',
|
||||
xMin: 4000,
|
||||
background: 'urban_bg',
|
||||
tileset: 'urban',
|
||||
enemyTypes: ['type59', 'type62', 'artillery_emplacement'],
|
||||
obstaclePalette: ['#666666', '#555555', '#777777'], // grays
|
||||
},
|
||||
];
|
||||
332
src/game/data/audio-defs.js
Normal file
332
src/game/data/audio-defs.js
Normal file
@@ -0,0 +1,332 @@
|
||||
/**
|
||||
* Procedural audio sound definitions for Iron Requiem.
|
||||
*
|
||||
* Each sound has a `generate(audioCtx)` function that returns a connected
|
||||
* AudioNode graph ready to output to destination.
|
||||
*
|
||||
* @module game/data/audio-defs
|
||||
*/
|
||||
|
||||
const SOUNDS = {
|
||||
muzzle_report: {
|
||||
name: 'muzzle_report',
|
||||
description: 'White noise 80ms + 200Hz sawtooth 100ms',
|
||||
durationMs: 100,
|
||||
generate(ctx) {
|
||||
const masterGain = ctx.createGain();
|
||||
masterGain.gain.value = 0.3;
|
||||
|
||||
// White noise component (80ms)
|
||||
const noiseSrc = ctx.createBufferSource();
|
||||
const noiseBuffer = ctx.createBuffer(1, ctx.sampleRate * 0.08, ctx.sampleRate);
|
||||
const noiseData = noiseBuffer.getChannelData(0);
|
||||
for (let i = 0; i < noiseData.length; i++) {
|
||||
noiseData[i] = Math.random() * 2 - 1;
|
||||
}
|
||||
noiseSrc.buffer = noiseBuffer;
|
||||
|
||||
const noiseGain = ctx.createGain();
|
||||
noiseGain.gain.value = 0.7;
|
||||
noiseSrc.connect(noiseGain);
|
||||
noiseGain.connect(masterGain);
|
||||
|
||||
// Sawtooth body (100ms)
|
||||
const osc = ctx.createOscillator();
|
||||
osc.type = 'sawtooth';
|
||||
osc.frequency.value = 200;
|
||||
|
||||
const oscGain = ctx.createGain();
|
||||
oscGain.gain.value = 0.5;
|
||||
oscGain.gain.setValueAtTime(0.5, ctx.currentTime);
|
||||
oscGain.gain.linearRampToValueAtTime(0, ctx.currentTime + 0.1);
|
||||
|
||||
osc.connect(oscGain);
|
||||
oscGain.connect(masterGain);
|
||||
|
||||
// Start both together
|
||||
noiseSrc.start(ctx.currentTime);
|
||||
osc.start(ctx.currentTime);
|
||||
|
||||
// Stop after max duration
|
||||
noiseSrc.stop(ctx.currentTime + 0.08);
|
||||
osc.stop(ctx.currentTime + 0.1);
|
||||
|
||||
return masterGain;
|
||||
},
|
||||
},
|
||||
|
||||
impact_metal: {
|
||||
name: 'impact_metal',
|
||||
description: 'White noise 50ms + 800Hz sine ping 30ms',
|
||||
durationMs: 50,
|
||||
generate(ctx) {
|
||||
const masterGain = ctx.createGain();
|
||||
masterGain.gain.value = 0.25;
|
||||
|
||||
// White noise transient (50ms)
|
||||
const noiseSrc = ctx.createBufferSource();
|
||||
const noiseBuffer = ctx.createBuffer(1, ctx.sampleRate * 0.05, ctx.sampleRate);
|
||||
const noiseData = noiseBuffer.getChannelData(0);
|
||||
for (let i = 0; i < noiseData.length; i++) {
|
||||
noiseData[i] = Math.random() * 2 - 1;
|
||||
}
|
||||
noiseSrc.buffer = noiseBuffer;
|
||||
|
||||
const noiseGain = ctx.createGain();
|
||||
noiseGain.gain.value = 0.8;
|
||||
noiseGain.gain.setValueAtTime(0.8, ctx.currentTime);
|
||||
noiseGain.gain.linearRampToValueAtTime(0, ctx.currentTime + 0.05);
|
||||
noiseSrc.connect(noiseGain);
|
||||
noiseGain.connect(masterGain);
|
||||
|
||||
// Sine ping (30ms)
|
||||
const osc = ctx.createOscillator();
|
||||
osc.type = 'sine';
|
||||
osc.frequency.value = 800;
|
||||
|
||||
const oscGain = ctx.createGain();
|
||||
oscGain.gain.value = 0.6;
|
||||
oscGain.gain.setValueAtTime(0.6, ctx.currentTime);
|
||||
oscGain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.03);
|
||||
|
||||
osc.connect(oscGain);
|
||||
oscGain.connect(masterGain);
|
||||
|
||||
noiseSrc.start(ctx.currentTime);
|
||||
osc.start(ctx.currentTime);
|
||||
|
||||
noiseSrc.stop(ctx.currentTime + 0.05);
|
||||
osc.stop(ctx.currentTime + 0.03);
|
||||
|
||||
return masterGain;
|
||||
},
|
||||
},
|
||||
|
||||
engine_loop: {
|
||||
name: 'engine_loop',
|
||||
description: '60Hz sawtooth + 120Hz square, low-pass filter, pitch modulated by speed',
|
||||
durationMs: 0, // continuous loop
|
||||
generate(ctx, params = {}) {
|
||||
const speed = (params && params.speed !== undefined) ? params.speed : 1.0;
|
||||
|
||||
const masterGain = ctx.createGain();
|
||||
masterGain.gain.value = 0.15;
|
||||
|
||||
// Low-pass filter
|
||||
const filter = ctx.createBiquadFilter();
|
||||
filter.type = 'lowpass';
|
||||
filter.frequency.value = 200 + speed * 300;
|
||||
filter.Q.value = 1;
|
||||
|
||||
// 60Hz sawtooth
|
||||
const osc1 = ctx.createOscillator();
|
||||
osc1.type = 'sawtooth';
|
||||
osc1.frequency.value = 60 * speed;
|
||||
|
||||
const osc1Gain = ctx.createGain();
|
||||
osc1Gain.gain.value = 0.6;
|
||||
osc1.connect(osc1Gain);
|
||||
osc1Gain.connect(filter);
|
||||
|
||||
// 120Hz square
|
||||
const osc2 = ctx.createOscillator();
|
||||
osc2.type = 'square';
|
||||
osc2.frequency.value = 120 * speed;
|
||||
|
||||
const osc2Gain = ctx.createGain();
|
||||
osc2Gain.gain.value = 0.3;
|
||||
osc2.connect(osc2Gain);
|
||||
osc2Gain.connect(filter);
|
||||
|
||||
filter.connect(masterGain);
|
||||
|
||||
osc1.start(ctx.currentTime);
|
||||
osc2.start(ctx.currentTime);
|
||||
|
||||
return {
|
||||
masterGain,
|
||||
filter,
|
||||
oscillators: [osc1, osc2],
|
||||
setSpeed: (newSpeed) => {
|
||||
osc1.frequency.value = 60 * newSpeed;
|
||||
osc2.frequency.value = 120 * newSpeed;
|
||||
filter.frequency.value = 200 + newSpeed * 300;
|
||||
},
|
||||
stop: () => {
|
||||
osc1.stop();
|
||||
osc2.stop();
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
engine_muffled: {
|
||||
name: 'engine_muffled',
|
||||
description: 'Dampened engine — 60Hz sawtooth + 120Hz square, tight low-pass, lower gain',
|
||||
durationMs: 0, // continuous loop
|
||||
generate(ctx, params = {}) {
|
||||
const speed = (params && params.speed !== undefined) ? params.speed : 1.0;
|
||||
|
||||
const masterGain = ctx.createGain();
|
||||
masterGain.gain.value = 0.06;
|
||||
|
||||
// Tighter low-pass filter for muffled effect
|
||||
const filter = ctx.createBiquadFilter();
|
||||
filter.type = 'lowpass';
|
||||
filter.frequency.value = 80 + speed * 150;
|
||||
filter.Q.value = 1.5;
|
||||
|
||||
// 60Hz sawtooth (same base as engine_loop)
|
||||
const osc1 = ctx.createOscillator();
|
||||
osc1.type = 'sawtooth';
|
||||
osc1.frequency.value = 60 * speed;
|
||||
|
||||
const osc1Gain = ctx.createGain();
|
||||
osc1Gain.gain.value = 0.5;
|
||||
osc1.connect(osc1Gain);
|
||||
osc1Gain.connect(filter);
|
||||
|
||||
// 120Hz square (same base as engine_loop)
|
||||
const osc2 = ctx.createOscillator();
|
||||
osc2.type = 'square';
|
||||
osc2.frequency.value = 120 * speed;
|
||||
|
||||
const osc2Gain = ctx.createGain();
|
||||
osc2Gain.gain.value = 0.2;
|
||||
osc2.connect(osc2Gain);
|
||||
osc2Gain.connect(filter);
|
||||
|
||||
filter.connect(masterGain);
|
||||
|
||||
osc1.start(ctx.currentTime);
|
||||
osc2.start(ctx.currentTime);
|
||||
|
||||
return {
|
||||
masterGain,
|
||||
filter,
|
||||
oscillators: [osc1, osc2],
|
||||
setSpeed: (newSpeed) => {
|
||||
osc1.frequency.value = 60 * newSpeed;
|
||||
osc2.frequency.value = 120 * newSpeed;
|
||||
filter.frequency.value = 80 + newSpeed * 150;
|
||||
},
|
||||
stop: () => {
|
||||
osc1.stop();
|
||||
osc2.stop();
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
radio_static: {
|
||||
name: 'radio_static',
|
||||
description: 'White noise band-pass 1000-3000Hz, 500ms burst',
|
||||
durationMs: 500,
|
||||
generate(ctx) {
|
||||
const masterGain = ctx.createGain();
|
||||
masterGain.gain.value = 0.12;
|
||||
|
||||
// Generate noise buffer
|
||||
const noiseSrc = ctx.createBufferSource();
|
||||
const duration = 0.5;
|
||||
const noiseBuffer = ctx.createBuffer(1, ctx.sampleRate * duration, ctx.sampleRate);
|
||||
const noiseData = noiseBuffer.getChannelData(0);
|
||||
for (let i = 0; i < noiseData.length; i++) {
|
||||
noiseData[i] = Math.random() * 2 - 1;
|
||||
}
|
||||
noiseSrc.buffer = noiseBuffer;
|
||||
|
||||
// Band-pass filter 1000-3000Hz
|
||||
const bpFilter = ctx.createBiquadFilter();
|
||||
bpFilter.type = 'bandpass';
|
||||
bpFilter.frequency.value = 2000;
|
||||
bpFilter.Q.value = 1; // Q=1 gives ~1000Hz bandwidth around center
|
||||
|
||||
const noiseGain = ctx.createGain();
|
||||
noiseGain.gain.value = 0.8;
|
||||
noiseGain.gain.setValueAtTime(0.8, ctx.currentTime);
|
||||
noiseGain.gain.linearRampToValueAtTime(0, ctx.currentTime + duration);
|
||||
|
||||
noiseSrc.connect(bpFilter);
|
||||
bpFilter.connect(noiseGain);
|
||||
noiseGain.connect(masterGain);
|
||||
|
||||
noiseSrc.start(ctx.currentTime);
|
||||
noiseSrc.stop(ctx.currentTime + duration);
|
||||
|
||||
return masterGain;
|
||||
},
|
||||
},
|
||||
|
||||
sniper_laser_tone: {
|
||||
name: 'sniper_laser_tone',
|
||||
description: '2000Hz sine, 2s ramp, amplitude modulated',
|
||||
durationMs: 2000,
|
||||
generate(ctx) {
|
||||
const masterGain = ctx.createGain();
|
||||
masterGain.gain.value = 0.15;
|
||||
|
||||
const osc = ctx.createOscillator();
|
||||
osc.type = 'sine';
|
||||
osc.frequency.value = 2000;
|
||||
|
||||
const oscGain = ctx.createGain();
|
||||
oscGain.gain.value = 0;
|
||||
oscGain.gain.setValueAtTime(0, ctx.currentTime);
|
||||
oscGain.gain.linearRampToValueAtTime(0.5, ctx.currentTime + 0.5);
|
||||
oscGain.gain.linearRampToValueAtTime(0.3, ctx.currentTime + 1.5);
|
||||
oscGain.gain.linearRampToValueAtTime(0, ctx.currentTime + 2.0);
|
||||
|
||||
osc.connect(oscGain);
|
||||
oscGain.connect(masterGain);
|
||||
|
||||
osc.start(ctx.currentTime);
|
||||
osc.stop(ctx.currentTime + 2.0);
|
||||
|
||||
return masterGain;
|
||||
},
|
||||
},
|
||||
|
||||
wind_ambient: {
|
||||
name: 'wind_ambient',
|
||||
description: 'Filtered white noise, continuous, stereo spread',
|
||||
durationMs: 0, // continuous
|
||||
generate(ctx) {
|
||||
const masterGain = ctx.createGain();
|
||||
masterGain.gain.value = 0.08;
|
||||
|
||||
// Noise source (long buffer for continuous playback via loop)
|
||||
const noiseSrc = ctx.createBufferSource();
|
||||
const noiseBuffer = ctx.createBuffer(1, ctx.sampleRate * 2, ctx.sampleRate);
|
||||
const noiseData = noiseBuffer.getChannelData(0);
|
||||
for (let i = 0; i < noiseData.length; i++) {
|
||||
noiseData[i] = Math.random() * 2 - 1;
|
||||
}
|
||||
noiseSrc.buffer = noiseBuffer;
|
||||
noiseSrc.loop = true;
|
||||
|
||||
// Low-pass filter for wind rumble
|
||||
const lpFilter = ctx.createBiquadFilter();
|
||||
lpFilter.type = 'lowpass';
|
||||
lpFilter.frequency.value = 600;
|
||||
lpFilter.Q.value = 0.5;
|
||||
|
||||
const noiseGain = ctx.createGain();
|
||||
noiseGain.gain.value = 0.6;
|
||||
|
||||
noiseSrc.connect(lpFilter);
|
||||
lpFilter.connect(noiseGain);
|
||||
noiseGain.connect(masterGain);
|
||||
|
||||
noiseSrc.start(ctx.currentTime);
|
||||
|
||||
return {
|
||||
masterGain,
|
||||
source: noiseSrc,
|
||||
stop: () => { noiseSrc.stop(); },
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default SOUNDS;
|
||||
@@ -101,6 +101,15 @@ export class Enemy {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Sync sprite position to logic coordinates
|
||||
if (this.sprite) {
|
||||
this.sprite.x = this.x;
|
||||
this.sprite.y = this.y;
|
||||
// Rotate sprite to face player
|
||||
const angle = Math.atan2(playerY - this.y, playerX - this.x);
|
||||
this.sprite.rotation = angle + Math.PI / 2;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -200,6 +209,10 @@ export class Enemy {
|
||||
this.hp = Math.max(0, this.hp - amount);
|
||||
if (this.hp <= 0) {
|
||||
this.active = false;
|
||||
// Destroy sprite on death
|
||||
if (this.sprite && this.sprite.destroy) {
|
||||
this.sprite.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -228,6 +241,7 @@ export function spawnZone(scene, zoneId, playerX, playerY) {
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[IR:spawnZone] spawned ${wave.length} enemies`);
|
||||
return wave;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,22 +3,32 @@ import { TANK_ACCELERATION, TANK_FRICTION, TANK_ROTATION_SPEED } from '@/constan
|
||||
|
||||
/**
|
||||
* Tank hull entity — physics-driven 25-ton feel.
|
||||
* Physics model: accel = (input * TANK_ACCELERATION) - (velocity * effectiveFriction)
|
||||
* Physics model: accel = (input * power) - (velocity * friction)
|
||||
*
|
||||
* Controls:
|
||||
* - W/S: Forward/Reverse (throttle along hull facing)
|
||||
* - A/D: Hull rotation left/right (driver steering)
|
||||
* - Mouse: Turret rotation (independent, gunner)
|
||||
*/
|
||||
export class Tank extends Phaser.Physics.Arcade.Sprite {
|
||||
constructor(scene, x, y) {
|
||||
super(scene, x, y, 'tank');
|
||||
|
||||
this._inputX = 0;
|
||||
this._inputY = 0;
|
||||
// Center the anchor so rotation happens around the tank's center, not top-left
|
||||
this.setOrigin(0.5, 0.5);
|
||||
|
||||
this._inputForward = 0; // W/S: -1 (reverse) to 1 (forward)
|
||||
this._inputRotate = 0; // A/D: -1 (left) to 1 (right)
|
||||
this._frictionMultiplier = 1.0;
|
||||
}
|
||||
|
||||
/** @param {number} x - -1, 0, or 1 */
|
||||
/** @param {number} y - -1, 0, or 1 */
|
||||
setInput(x, y) {
|
||||
this._inputX = x || 0;
|
||||
this._inputY = y || 0;
|
||||
/**
|
||||
* @param {number} forward - -1 (reverse), 0, or 1 (forward)
|
||||
* @param {number} rotate - -1 (rotate left), 0, or 1 (rotate right)
|
||||
*/
|
||||
setInput(forward, rotate) {
|
||||
this._inputForward = forward || 0;
|
||||
this._inputRotate = rotate || 0;
|
||||
}
|
||||
|
||||
/** @param {number} mult - friction multiplier (e.g., ICE_FRICTION_MULTIPLIER) */
|
||||
@@ -32,50 +42,32 @@ export class Tank extends Phaser.Physics.Arcade.Sprite {
|
||||
const dt = delta / 1000;
|
||||
const f = TANK_FRICTION * this._frictionMultiplier;
|
||||
|
||||
// Calculate forward velocity along hull facing
|
||||
const currentSpeed = this.body.velocity.x * Math.cos(this.rotation) + this.body.velocity.y * Math.sin(this.rotation);
|
||||
|
||||
// accel = (input * power) - (velocity * friction)
|
||||
const ax = this._inputX * TANK_ACCELERATION - this.body.velocity.x * f;
|
||||
const ay = this._inputY * TANK_ACCELERATION - this.body.velocity.y * f;
|
||||
const accel = this._inputForward * TANK_ACCELERATION - currentSpeed * f;
|
||||
const newSpeed = currentSpeed + accel * dt;
|
||||
|
||||
this.body.velocity.x += ax * dt;
|
||||
this.body.velocity.y += ay * dt;
|
||||
// Apply velocity in hull facing direction
|
||||
this.body.velocity.x = Math.cos(this.rotation) * newSpeed;
|
||||
this.body.velocity.y = Math.sin(this.rotation) * newSpeed;
|
||||
|
||||
// Hull rotation (driver steering)
|
||||
const rotSpeed = (TANK_ROTATION_SPEED * Math.PI / 180) * dt;
|
||||
this.rotation += this._inputRotate * rotSpeed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public update — called by MainGame.update() each frame.
|
||||
* Delegates to preUpdate for physics, plus any per-frame logic.
|
||||
* @param {number} time - current game time
|
||||
* @param {number} delta - ms since last frame
|
||||
* @param {object} [input] - optional keyboard input state
|
||||
*/
|
||||
update(time, delta, input) {
|
||||
// preUpdate is called by Phaser automatically, but we provide
|
||||
// a public interface so tests and MainGame can call it explicitly.
|
||||
if (input) {
|
||||
this._inputX = (input.right || 0) - (input.left || 0);
|
||||
this._inputY = (input.down || 0) - (input.up || 0);
|
||||
}
|
||||
|
||||
// Hull rotation — face movement direction with deliberate 25-ton turn rate.
|
||||
// Only rotate when the tank is actually moving (speed > 5 px/sec) to avoid
|
||||
// jitter when stopped.
|
||||
const vx = this.body.velocity.x;
|
||||
const vy = this.body.velocity.y;
|
||||
const speed = Math.sqrt(vx * vx + vy * vy);
|
||||
|
||||
if (speed > 5) {
|
||||
const targetAngle = Math.atan2(vy, vx);
|
||||
const maxRad = (TANK_ROTATION_SPEED * Math.PI / 180) * (delta / 1000);
|
||||
|
||||
// Rotation difference, normalized to [-PI, PI]
|
||||
let diff = targetAngle - this.rotation;
|
||||
diff = ((diff + Math.PI) % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI) - Math.PI;
|
||||
|
||||
// Clamp to maxRad per frame
|
||||
if (Math.abs(diff) <= maxRad) {
|
||||
this.rotation = targetAngle;
|
||||
} else {
|
||||
this.rotation += Math.sign(diff) * maxRad;
|
||||
}
|
||||
this._inputForward = (input.down || 0) - (input.up || 0);
|
||||
this._inputRotate = (input.right || 0) - (input.left || 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,16 +38,24 @@ export class PreloadScene extends Phaser.Scene {
|
||||
// must be initialized for generateTexture() to produce valid output.
|
||||
const gfx = this.add.graphics();
|
||||
|
||||
// 32x48 green tank hull
|
||||
gfx.fillStyle(0x556655, 1);
|
||||
// 32x48 green tank hull — with FRONT indicator (lighter stripe)
|
||||
// Front is the TOP of the sprite (0° rotation = facing up)
|
||||
gfx.fillStyle(0x556655, 1); // Dark green hull
|
||||
gfx.fillRect(0, 0, 32, 48);
|
||||
gfx.fillStyle(0x778877, 1); // Lighter green front stripe
|
||||
gfx.fillRect(8, 2, 16, 12); // Front indicator at top
|
||||
gfx.fillStyle(0x334433, 1); // Dark tracks on sides
|
||||
gfx.fillRect(0, 0, 6, 48); // Left track
|
||||
gfx.fillRect(26, 0, 6, 48); // Right track
|
||||
gfx.generateTexture('tank', 32, 48);
|
||||
console.log('[IR:Preload] generated texture "tank"');
|
||||
|
||||
// 8x24 dark green turret
|
||||
// 8x24 dark green turret — distinct from hull
|
||||
gfx.clear();
|
||||
gfx.fillStyle(0x445544, 1);
|
||||
gfx.fillStyle(0x445544, 1); // Turret base
|
||||
gfx.fillRect(0, 0, 8, 24);
|
||||
gfx.fillStyle(0x667766, 1); // Gun barrel (lighter)
|
||||
gfx.fillRect(2, 0, 4, 8); // Barrel at front (top)
|
||||
gfx.generateTexture('turret', 8, 24);
|
||||
console.log('[IR:Preload] generated texture "turret"');
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@ export class AmmoSystem {
|
||||
this._warnedBelow10 = false;
|
||||
this._warnedBelow5 = false;
|
||||
this._warnedZero = false;
|
||||
|
||||
/** @type {function(string):void|null} — set by scene to route warnings to HUD */
|
||||
this.onWarning = null;
|
||||
}
|
||||
|
||||
/** Set the active shell type. Throws for unknown types. */
|
||||
@@ -50,15 +53,15 @@ export class AmmoSystem {
|
||||
this._inventory[this._active]--;
|
||||
if (totalBefore - 1 < 10 && !this._warnedBelow10) {
|
||||
this._warnedBelow10 = true;
|
||||
console.warn('AmmoSystem: below 10 total rounds remaining');
|
||||
this._warn('below-10');
|
||||
}
|
||||
if (totalBefore - 1 < 5 && !this._warnedBelow5) {
|
||||
this._warnedBelow5 = true;
|
||||
console.warn('AmmoSystem: below 5 total rounds remaining');
|
||||
this._warn('below-5');
|
||||
}
|
||||
if (totalBefore - 1 === 0 && !this._warnedZero) {
|
||||
this._warnedZero = true;
|
||||
console.warn('AmmoSystem: 0 rounds remaining — ram / pistol only');
|
||||
this._warn('empty');
|
||||
}
|
||||
return {
|
||||
type: shell.id,
|
||||
@@ -92,6 +95,20 @@ export class AmmoSystem {
|
||||
}
|
||||
|
||||
// ── internal ──────────────────────────────────────────────────────
|
||||
|
||||
_warn(level) {
|
||||
const messages = {
|
||||
'below-10': 'AmmoSystem: below 10 total rounds remaining',
|
||||
'below-5': 'AmmoSystem: below 5 total rounds remaining',
|
||||
'empty': 'AmmoSystem: 0 rounds remaining — ram / pistol only',
|
||||
};
|
||||
const msg = messages[level] || level;
|
||||
console.warn(msg);
|
||||
if (typeof this.onWarning === 'function') {
|
||||
this.onWarning(level);
|
||||
}
|
||||
}
|
||||
|
||||
_total() {
|
||||
let sum = 0;
|
||||
for (const v of Object.values(this._inventory)) {
|
||||
|
||||
201
src/game/systems/AudioManager.js
Normal file
201
src/game/systems/AudioManager.js
Normal file
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* AudioManager — procedural audio system for Iron Requiem.
|
||||
*
|
||||
* Wraps the Web Audio API (AudioContext) with lazy initialization,
|
||||
* graceful degradation, and procedural sound generation.
|
||||
*
|
||||
* Uses audio-defs.js for sound recipes; no external audio files needed.
|
||||
*
|
||||
* @module game/systems/AudioManager
|
||||
*/
|
||||
|
||||
import SOUNDS from '../data/audio-defs';
|
||||
|
||||
class AudioManager {
|
||||
/**
|
||||
* Create an AudioManager. The AudioContext is NOT created until
|
||||
* init() or the first play()/startLoop() call (browser autoplay policy).
|
||||
*/
|
||||
constructor() {
|
||||
this._ctx = null;
|
||||
this._activeLoops = new Map(); // handle -> { soundId, result, _oscillators }
|
||||
this._initialized = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicitly initialize the AudioContext. Safe to call multiple times —
|
||||
* only creates once.
|
||||
*/
|
||||
init() {
|
||||
if (this._initialized) return;
|
||||
if (!AudioManager.isSupported()) return;
|
||||
|
||||
this._ctx = new AudioContext();
|
||||
this._ctx.resume();
|
||||
this._initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the AudioContext is ready. Called lazily by play/startLoop.
|
||||
*/
|
||||
_ensureCtx() {
|
||||
if (!this._initialized) {
|
||||
this.init();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Play a one-shot sound by its definition id.
|
||||
*
|
||||
* @param {string} soundId - key in audio-defs (e.g. 'muzzle_report')
|
||||
* @param {object} [params] - optional params passed to the generator
|
||||
* @returns {{ soundId, type, _oscillators } | null}
|
||||
*/
|
||||
play(soundId, params) {
|
||||
this._ensureCtx();
|
||||
if (!this._ctx) return null;
|
||||
|
||||
const sound = SOUNDS[soundId];
|
||||
if (!sound) return null;
|
||||
|
||||
const trackedOscs = [];
|
||||
const result = this._generateTracked(sound, params, trackedOscs);
|
||||
|
||||
const master = result.masterGain || result;
|
||||
master.connect(this._ctx.destination);
|
||||
|
||||
return {
|
||||
soundId,
|
||||
type: 'oneshot',
|
||||
_oscillators: trackedOscs,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a looping sound. Returns a handle for stopLoop().
|
||||
*
|
||||
* @param {string} soundId - key in audio-defs (e.g. 'engine_loop')
|
||||
* @param {object} [params] - optional params passed to the generator
|
||||
* @returns {{ soundId, type, _oscillators } | null}
|
||||
*/
|
||||
startLoop(soundId, params) {
|
||||
this._ensureCtx();
|
||||
if (!this._ctx) return null;
|
||||
|
||||
const sound = SOUNDS[soundId];
|
||||
if (!sound) return null;
|
||||
|
||||
const trackedOscs = [];
|
||||
const result = this._generateTracked(sound, params, trackedOscs);
|
||||
|
||||
const master = result.masterGain || result;
|
||||
master.connect(this._ctx.destination);
|
||||
|
||||
const handle = {
|
||||
soundId,
|
||||
type: 'loop',
|
||||
_oscillators: trackedOscs,
|
||||
};
|
||||
|
||||
this._activeLoops.set(handle, { soundId, result, _oscillators: trackedOscs });
|
||||
return handle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop a running loop by its handle.
|
||||
*
|
||||
* @param {object} handle - handle returned by startLoop()
|
||||
*/
|
||||
stopLoop(handle) {
|
||||
if (!handle || !this._activeLoops.has(handle)) return;
|
||||
|
||||
const entry = this._activeLoops.get(handle);
|
||||
|
||||
// Call stop() on the result if it has one
|
||||
if (entry.result && typeof entry.result.stop === 'function') {
|
||||
entry.result.stop();
|
||||
}
|
||||
|
||||
// Disconnect oscillators
|
||||
const master = entry.result.masterGain || entry.result;
|
||||
if (master && typeof master.disconnect === 'function') {
|
||||
master.disconnect();
|
||||
}
|
||||
|
||||
this._activeLoops.delete(handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crossfade from one loop to another.
|
||||
*
|
||||
* @param {string} fromId - soundId of the loop to fade out
|
||||
* @param {string} toId - soundId of the loop to fade in
|
||||
* @param {number} durationMs - crossfade duration in milliseconds
|
||||
*/
|
||||
crossfade(fromId, toId, durationMs) {
|
||||
// Stop any active loop with matching soundId
|
||||
for (const [handle, entry] of this._activeLoops) {
|
||||
if (entry.soundId === fromId) {
|
||||
this.stopLoop(handle);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Start the new loop
|
||||
this.startLoop(toId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all active loops and close the AudioContext.
|
||||
* Safe to call when uninitialized.
|
||||
*/
|
||||
cleanup() {
|
||||
// Stop all loops (collect handles first to avoid mutating during iteration)
|
||||
const handles = Array.from(this._activeLoops.keys());
|
||||
for (const handle of handles) {
|
||||
this.stopLoop(handle);
|
||||
}
|
||||
|
||||
// Close the context
|
||||
if (this._ctx && this._ctx.state !== 'closed') {
|
||||
this._ctx.close();
|
||||
}
|
||||
|
||||
this._initialized = false;
|
||||
this._ctx = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the Web Audio API is available in this environment.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static isSupported() {
|
||||
return typeof globalThis.AudioContext !== 'undefined'
|
||||
|| typeof window !== 'undefined' && typeof window.AudioContext !== 'undefined';
|
||||
}
|
||||
|
||||
/**
|
||||
* Call sound.generate() while tracking created oscillators.
|
||||
* Temporarily wraps ctx.createOscillator to capture references.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_generateTracked(sound, params, collector) {
|
||||
const ctx = this._ctx;
|
||||
const orig = ctx.createOscillator.bind(ctx);
|
||||
|
||||
ctx.createOscillator = () => {
|
||||
const osc = orig();
|
||||
collector.push(osc);
|
||||
return osc;
|
||||
};
|
||||
|
||||
const result = sound.generate(ctx, params);
|
||||
|
||||
ctx.createOscillator = orig; // restore
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export default AudioManager;
|
||||
@@ -183,4 +183,45 @@ export class MapGenerator {
|
||||
|
||||
return placed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a 2D boolean collision grid for the tilemap.
|
||||
* true = blocked (obstacle), false = free (drivable).
|
||||
*
|
||||
* Uses the same seeded PRNG so the grid matches generate() output.
|
||||
*
|
||||
* @param {string} zone — 'tundra' | 'industrial' | 'city'
|
||||
* @returns {boolean[][]} grid[rows][cols]
|
||||
*/
|
||||
generateCollisionGrid(zone) {
|
||||
const rows = Math.floor(this.mapHeight / this.tileSize);
|
||||
const cols = Math.floor(this.mapWidth / this.tileSize);
|
||||
|
||||
// Build empty grid
|
||||
const grid = new Array(rows);
|
||||
for (let r = 0; r < rows; r++) {
|
||||
grid[r] = new Array(cols).fill(false);
|
||||
}
|
||||
|
||||
// Get obstacle definitions (seeded reproducible)
|
||||
const obstacles = this.generate(zone);
|
||||
|
||||
// Mark blocked tiles
|
||||
for (const o of obstacles) {
|
||||
const colStart = Math.floor(o.x / this.tileSize);
|
||||
const rowStart = Math.floor(o.y / this.tileSize);
|
||||
const colEnd = Math.floor((o.x + o.w - 1) / this.tileSize);
|
||||
const rowEnd = Math.floor((o.y + o.h - 1) / this.tileSize);
|
||||
|
||||
for (let r = rowStart; r <= rowEnd; r++) {
|
||||
for (let c = colStart; c <= colEnd; c++) {
|
||||
if (r >= 0 && r < rows && c >= 0 && c < cols) {
|
||||
grid[r][c] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return grid;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,82 +4,151 @@
|
||||
* Manages a Phaser Arcade.Group as an object pool, spawns projectile
|
||||
* patterns defined in src/data/patterns.js, and handles recycling.
|
||||
*
|
||||
* ## Zone governor
|
||||
* - Z1: max 2 active+pending patterns
|
||||
* - Z2: max 3 active+pending patterns
|
||||
* - Z3: max 4 active+pending patterns
|
||||
*
|
||||
* ## Stagger
|
||||
* - Offset >= 500ms between pattern starts in the same zone.
|
||||
* - Different zones never stagger each other.
|
||||
*
|
||||
* @module game/systems/PatternManager
|
||||
*/
|
||||
|
||||
import Phaser from 'phaser';
|
||||
import { patterns } from '../../data/patterns.js';
|
||||
|
||||
/** Zone capacity limits (active + pending). */
|
||||
const ZONE_CAPACITY = { 1: 2, 2: 3, 3: 4 };
|
||||
|
||||
/** Minimum stagger offset between pattern starts in the same zone (ms). */
|
||||
const STAGGER_WINDOW_MS = 500;
|
||||
|
||||
export class PatternManager {
|
||||
/**
|
||||
* @param {Phaser.Scene} scene — the active Phaser scene
|
||||
* @param {object} scene — the active scene (must have scene.physics.add.group)
|
||||
*/
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
|
||||
// Object pool: Arcade.Group with max 200 projectile sprites
|
||||
this.group = scene.physics.add.group({
|
||||
maxSize: 200,
|
||||
runChildUpdate: false,
|
||||
allowGravity: false,
|
||||
});
|
||||
|
||||
/** Active patterns per zone: { 1: [{projectiles:Set, patternId, startedAt}], ... } */
|
||||
this._zoneActive = { 1: [], 2: [], 3: [] };
|
||||
|
||||
/** Last start timestamp per zone (for stagger). null = never started. */
|
||||
this._zoneLastStart = { 1: null, 2: null, 3: null };
|
||||
|
||||
/** Time source — replaceable for tests. */
|
||||
this._now = () => Date.now();
|
||||
|
||||
/** Pending pattern count per zone (staggered/delayed, not yet spawned). */
|
||||
this._zonePending = { 1: 0, 2: 0, 3: 0 };
|
||||
|
||||
/** One-shot flag for pool exhaustion warning. */
|
||||
this._poolExhaustedWarned = false;
|
||||
|
||||
// Telegraph state
|
||||
this._telegraphFired = false;
|
||||
this._spawnDelay = 0;
|
||||
|
||||
/** Enable stagger enforcement. */
|
||||
this._enableStagger = true;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Public API
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Trigger a bullet-hell pattern.
|
||||
*
|
||||
* @param {string} patternId — key in patterns.js
|
||||
* @param {{ x: number, y: number, direction?: string }} position
|
||||
* @returns {{ count: number, projectiles: object[] } | null}
|
||||
* @param {{x:number, y:number}} origin — spawn origin
|
||||
* @param {object} [options] — { zone, delay, direction }
|
||||
* @returns {object} { patternId, count, projectiles, staggered, delayed, fired, rejected, reason }
|
||||
*/
|
||||
trigger(patternId, position) {
|
||||
trigger(patternId, origin, options = {}) {
|
||||
const pat = patterns[patternId];
|
||||
if (!pat) return null;
|
||||
|
||||
// Record telegraph state for test verification
|
||||
this._telegraphFired = true;
|
||||
this._spawnDelay = pat.telegraphTime;
|
||||
const zone = options.zone || 0;
|
||||
const delay = options.delay || 0;
|
||||
const now = this._now();
|
||||
|
||||
const { x, y } = position;
|
||||
const dir = position.direction || pat.direction;
|
||||
const projectileCount = pat.projectileCount;
|
||||
const spacing = 12; // pixels between each projectile in the wall
|
||||
const result = {
|
||||
patternId,
|
||||
count: 0,
|
||||
projectiles: [],
|
||||
staggered: false,
|
||||
delayed: false,
|
||||
fired: false,
|
||||
rejected: false,
|
||||
reason: null,
|
||||
};
|
||||
|
||||
const projectiles = [];
|
||||
|
||||
for (let i = 0; i < projectileCount; i++) {
|
||||
// Horizontal line: same y, x increases with index
|
||||
const spawnX = x + i * spacing;
|
||||
const spawnY = y;
|
||||
|
||||
// Try to recycle a dead sprite first
|
||||
let sprite = this.group.getFirstDead(true, spawnX, spawnY, 'projectile');
|
||||
|
||||
if (!sprite) {
|
||||
// Pool full — create a new one if under maxSize
|
||||
sprite = this.group.create(spawnX, spawnY, 'projectile');
|
||||
}
|
||||
|
||||
if (sprite) {
|
||||
// Reset state
|
||||
sprite.active = true;
|
||||
sprite.visible = true;
|
||||
sprite.body.enable = true;
|
||||
|
||||
// Direction determines velocity
|
||||
const speed = dir === 'right-to-left' ? -200 : 200;
|
||||
sprite.body.velocity.x = speed;
|
||||
sprite.body.velocity.y = 0;
|
||||
|
||||
projectiles.push(sprite);
|
||||
// ---- Zone governor: count active + pending ----
|
||||
if (zone > 0 && ZONE_CAPACITY[zone] != null) {
|
||||
this._pruneDead(zone);
|
||||
const totalOccupied = this._zoneActive[zone].length + (this._zonePending[zone] || 0);
|
||||
if (totalOccupied >= ZONE_CAPACITY[zone]) {
|
||||
result.rejected = true;
|
||||
result.reason = `Zone ${zone} at capacity (${ZONE_CAPACITY[zone]} patterns active)`;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return { count: projectiles.length, projectiles };
|
||||
// ---- Stagger check ----
|
||||
if (this._enableStagger && zone > 0) {
|
||||
const lastStart = this._zoneLastStart[zone];
|
||||
if (lastStart != null) {
|
||||
const elapsed = now - lastStart;
|
||||
if (elapsed < STAGGER_WINDOW_MS) {
|
||||
const remaining = STAGGER_WINDOW_MS - elapsed;
|
||||
result.staggered = true;
|
||||
result.delayed = true;
|
||||
|
||||
// Reserve zone slot now
|
||||
this._zoneLastStart[zone] = now + remaining;
|
||||
this._zonePending[zone] = (this._zonePending[zone] || 0) + 1;
|
||||
|
||||
setTimeout(() => {
|
||||
this._doSpawnInto(pat, origin, options, zone, now + remaining, result);
|
||||
}, remaining);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Delay handling ----
|
||||
if (delay > 0) {
|
||||
result.delayed = true;
|
||||
|
||||
// Reserve zone slot now
|
||||
if (zone > 0) {
|
||||
this._zoneLastStart[zone] = now + delay;
|
||||
this._zonePending[zone] = (this._zonePending[zone] || 0) + 1;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this._doSpawnInto(pat, origin, options, zone, now + delay, result);
|
||||
}, delay);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ---- Immediate spawn ----
|
||||
this._doSpawnInto(pat, origin, options, zone, now, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill a projectile and return it to the pool.
|
||||
* @param {object} sprite
|
||||
* Kill a projectile — return it to the pool for recycling.
|
||||
*/
|
||||
killProjectile(sprite) {
|
||||
this.group.killAndHide(sprite);
|
||||
@@ -87,12 +156,152 @@ export class PatternManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a projectile as out of bounds (screen edge / world boundary).
|
||||
* @param {object} sprite
|
||||
* Mark a projectile as out of bounds for recycling.
|
||||
*/
|
||||
outOfBounds(sprite) {
|
||||
sprite.active = false;
|
||||
sprite.visible = false;
|
||||
if (sprite.body) sprite.body.enable = false;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Internal
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Perform actual spawn, mutating result in-place.
|
||||
*/
|
||||
_doSpawnInto(pat, origin, options, zone, timestamp, result) {
|
||||
const { x, y } = origin;
|
||||
const dir = origin.direction || options.direction || pat.direction;
|
||||
const projectileCount = pat.projectileCount;
|
||||
const spacing = pat.spacing || 12;
|
||||
const projectiles = [];
|
||||
|
||||
this._telegraphFired = true;
|
||||
this._spawnDelay = pat.telegraphTime;
|
||||
|
||||
for (let i = 0; i < projectileCount; i++) {
|
||||
const { spawnX, spawnY } = this._computeSpawnPosition(
|
||||
pat.patternId, pat, x, y, i, spacing, projectileCount,
|
||||
);
|
||||
|
||||
let sprite = this.group.getFirstDead(true, spawnX, spawnY, 'projectile');
|
||||
if (!sprite) {
|
||||
sprite = this.group.create(spawnX, spawnY, 'projectile');
|
||||
}
|
||||
|
||||
if (sprite) {
|
||||
sprite.active = true;
|
||||
sprite.visible = true;
|
||||
if (sprite.body) sprite.body.enable = true;
|
||||
|
||||
this._applyVelocity(pat.patternId, sprite, i, projectileCount, dir);
|
||||
projectiles.push(sprite);
|
||||
} else {
|
||||
if (!this._poolExhaustedWarned) {
|
||||
this._poolExhaustedWarned = true;
|
||||
console.warn(`PatternManager: pool exhausted at ${projectiles.length}/${projectileCount}, pattern=${pat.patternId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mutate result
|
||||
result.count = projectiles.length;
|
||||
result.projectiles = projectiles;
|
||||
result.fired = true;
|
||||
|
||||
// Bookkeeping
|
||||
if (zone > 0) {
|
||||
this._zoneActive[zone].push({
|
||||
patternId: pat.patternId,
|
||||
projectiles: new Set(projectiles),
|
||||
startedAt: timestamp,
|
||||
});
|
||||
this._zoneLastStart[zone] = timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute spawn position per pattern type.
|
||||
*/
|
||||
_computeSpawnPosition(patternId, pat, x, y, i, spacing, projectileCount) {
|
||||
switch (patternId) {
|
||||
case 'tank_destroyer_beam': {
|
||||
const spreadAngle = pat.spreadAngle || 45;
|
||||
const halfSpread = spreadAngle / 2;
|
||||
const angleStep = spreadAngle / (projectileCount - 1 || 1);
|
||||
const angleRad = ((halfSpread - i * angleStep) * Math.PI) / 180;
|
||||
return {
|
||||
spawnX: x + Math.cos(angleRad) * spacing * (i + 1),
|
||||
spawnY: y + Math.sin(angleRad) * spacing * (i + 1),
|
||||
};
|
||||
}
|
||||
case 'artillery_ring': {
|
||||
const angleRad = (i / projectileCount) * 2 * Math.PI;
|
||||
return {
|
||||
spawnX: x + Math.cos(angleRad) * spacing * 3,
|
||||
spawnY: y + Math.sin(angleRad) * spacing * 3,
|
||||
};
|
||||
}
|
||||
case 'heli_sweep':
|
||||
return {
|
||||
spawnX: x + i * spacing,
|
||||
spawnY: y + Math.sin(i * 0.3) * 10,
|
||||
};
|
||||
case 'infantry_wall':
|
||||
default:
|
||||
return { spawnX: x + i * spacing, spawnY: y };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply velocity per pattern type.
|
||||
*/
|
||||
_applyVelocity(patternId, sprite, i, projectileCount, dir) {
|
||||
if (!sprite.body) return;
|
||||
|
||||
switch (patternId) {
|
||||
case 'tank_destroyer_beam':
|
||||
sprite.body.velocity.x = -250;
|
||||
sprite.body.velocity.y = 0;
|
||||
break;
|
||||
case 'artillery_ring': {
|
||||
const angleRad = (i / projectileCount) * 2 * Math.PI;
|
||||
sprite.body.velocity.x = Math.cos(angleRad) * 120;
|
||||
sprite.body.velocity.y = Math.sin(angleRad) * 120;
|
||||
break;
|
||||
}
|
||||
case 'infantry_wall':
|
||||
case 'heli_sweep':
|
||||
default: {
|
||||
const speed = dir === 'right-to-left' ? -200 : 200;
|
||||
sprite.body.velocity.x = speed;
|
||||
sprite.body.velocity.y = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove patterns whose projectiles have all been recycled.
|
||||
*/
|
||||
_pruneDead(zone) {
|
||||
const active = this._zoneActive[zone];
|
||||
if (!active) return;
|
||||
|
||||
for (let i = active.length - 1; i >= 0; i--) {
|
||||
const entry = active[i];
|
||||
let allDead = true;
|
||||
for (const sprite of entry.projectiles) {
|
||||
if (sprite && sprite.active) {
|
||||
allDead = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (allDead) {
|
||||
active.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
68
src/game/systems/VFXManager.js
Normal file
68
src/game/systems/VFXManager.js
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* VFXManager — Phaser Graphics-based visual effects for Iron Requiem.
|
||||
*
|
||||
* Uses separate Graphics objects for each effect type so they can
|
||||
* coexist on screen simultaneously.
|
||||
*
|
||||
* @module game/systems/VFXManager
|
||||
*/
|
||||
|
||||
class VFXManager {
|
||||
/**
|
||||
* @param {Phaser.Scene} scene — the Phaser scene (to call scene.add.graphics)
|
||||
*/
|
||||
constructor(scene) {
|
||||
this._scene = scene;
|
||||
this._muzzle = null; // created lazily
|
||||
this._impact = null;
|
||||
this._overlay = null;
|
||||
}
|
||||
|
||||
/** Lazy-init a graphics layer at given depth. */
|
||||
_get(name, depth) {
|
||||
if (!this[`_${name}`]) {
|
||||
this[`_${name}`] = this._scene.add.graphics();
|
||||
this[`_${name}`].setDepth(depth);
|
||||
}
|
||||
return this[`_${name}`];
|
||||
}
|
||||
|
||||
muzzleFlash(x, y, angle) {
|
||||
const gfx = this._get('muzzle', 500);
|
||||
const rad = (angle * Math.PI) / 180;
|
||||
const flashLen = 20;
|
||||
|
||||
gfx.clear();
|
||||
gfx.fillStyle(0xffff88, 0.9);
|
||||
gfx.fillCircle(x, y, 4);
|
||||
gfx.fillStyle(0xff8800, 0.5);
|
||||
gfx.fillCircle(x, y, 8);
|
||||
gfx.lineStyle(2, 0xffff88, 0.8);
|
||||
gfx.lineBetween(x, y, x + Math.cos(rad) * flashLen, y + Math.sin(rad) * flashLen);
|
||||
}
|
||||
|
||||
impactBurst(x, y, color) {
|
||||
const gfx = this._get('impact', 500);
|
||||
const c = color !== undefined ? color : 0xffff00;
|
||||
|
||||
gfx.clear();
|
||||
gfx.fillStyle(c, 0.9);
|
||||
gfx.fillCircle(x, y, 5);
|
||||
gfx.lineStyle(1.5, c, 0.7);
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const rad = (i / 8) * Math.PI * 2;
|
||||
gfx.lineBetween(x, y, x + Math.cos(rad) * 14, y + Math.sin(rad) * 14);
|
||||
}
|
||||
}
|
||||
|
||||
screenFlash(color, _duration) {
|
||||
const gfx = this._get('overlay', 999);
|
||||
const c = color !== undefined ? color : 0xffffff;
|
||||
|
||||
gfx.clear();
|
||||
gfx.fillStyle(c, 0.3);
|
||||
gfx.fillRect(0, 0, 640, 360);
|
||||
}
|
||||
}
|
||||
|
||||
export default VFXManager;
|
||||
@@ -1,31 +1,24 @@
|
||||
/**
|
||||
* VisionMask — periscope overlay system.
|
||||
*
|
||||
* Renders a full-screen dark mask with a rectangular periscope hole.
|
||||
* The hole follows the tank position and rotation, representing a 180°
|
||||
* forward arc with ~200px range (buttoned state).
|
||||
* Renders a full-screen dark mask with a periscope-shaped hole.
|
||||
* The hole follows the tank position and rotation. Uses an axis-aligned
|
||||
* bounding box (AABB) around the rotated periscope rectangle — this is
|
||||
* the only approach that works reliably in Phaser.CANVAS mode.
|
||||
*
|
||||
* Constructor accepts an optional `graphics` object for test injection.
|
||||
* When omitted, creates a real Phaser.GameObjects.Graphics on the scene.
|
||||
*
|
||||
* @module game/systems/VisionMask
|
||||
*/
|
||||
|
||||
const RANGE = 200; // forward visibility range in pixels
|
||||
const HALF_ARC = 200; // lateral half-width (±200px; 180° forward arc)
|
||||
const DARK_COLOR = 0x000000;
|
||||
const DARK_ALPHA = 0.85;
|
||||
|
||||
// Blend mode constants (inlined so tests don't need Phaser)
|
||||
const BLEND_ERASE = 26; // Phaser.BlendModes.ERASE (3.60+), fallback to DESTINATION_OUT=3
|
||||
const BLEND_NORMAL = 0; // Phaser.BlendModes.NORMAL
|
||||
|
||||
class VisionMask {
|
||||
/**
|
||||
* @param {object} scene - Phaser.Scene or mock with `scale` property
|
||||
* @param {object} [options]
|
||||
* @param {object} [options.graphics] - pre-created graphics object (for test injection)
|
||||
* @param {number} [options.eraseBlend=26] - blend mode for hole cutout (testable)
|
||||
*/
|
||||
constructor(scene, options = {}) {
|
||||
this.scene = scene;
|
||||
@@ -37,72 +30,76 @@ class VisionMask {
|
||||
this.graphics.setDepth(100);
|
||||
}
|
||||
|
||||
this._eraseBlend = options.eraseBlend !== undefined ? options.eraseBlend : BLEND_ERASE;
|
||||
// Periscope dimensions (default to buttoned-up state)
|
||||
this._range = 200;
|
||||
this._halfArc = 200;
|
||||
|
||||
this._x = 0;
|
||||
this._y = 0;
|
||||
this._angle = 0; // radians, 0 = right
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the mask position and rotation. Called every frame.
|
||||
* @param {number} x - tank world x
|
||||
* @param {number} y - tank world y
|
||||
* @param {number} angle - tank facing angle in radians (0 = right)
|
||||
*/
|
||||
update(x, y, angle) {
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
this._angle = angle;
|
||||
}
|
||||
|
||||
setArc(arcDeg, rangePx) {
|
||||
this._range = rangePx;
|
||||
this._halfArc = Math.round((arcDeg / 180) * 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the mask: full dark overlay with periscope hole cut out.
|
||||
* Uses a four-rectangle approach (works in Canvas mode — WebGL blend-mode
|
||||
* erase is not available with Phaser.CANVAS).
|
||||
* Draw the mask using four AABB blinders around the rotated periscope hole.
|
||||
*
|
||||
* Four blinders drawn around the rotated periscope viewport.
|
||||
* Strategy: compute the 4 rotated corners of the periscope rectangle,
|
||||
* find the axis-aligned bounding box, then draw four dark strips
|
||||
* (top, bottom, left, right) covering everything outside the box.
|
||||
* The hole is slightly oversized at off-angles but correctly rotates
|
||||
* with the tank — verified working in Canvas mode since Slice 1.
|
||||
*/
|
||||
draw() {
|
||||
const g = this.graphics;
|
||||
const width = this.scene.scale.width;
|
||||
const height = this.scene.scale.height;
|
||||
const w = this.scene.scale.width;
|
||||
const h = this.scene.scale.height;
|
||||
|
||||
g.clear();
|
||||
g.fillStyle(DARK_COLOR, DARK_ALPHA);
|
||||
|
||||
// Compute periscope hole corners in world space
|
||||
const hole = this._computeHoleCorners();
|
||||
// Compute rotated periscope hole corners
|
||||
const cx = this._x;
|
||||
const cy = this._y;
|
||||
const cosA = Math.cos(this._angle);
|
||||
const sinA = Math.sin(this._angle);
|
||||
|
||||
// Find axis-aligned bounding box of the rotated hole
|
||||
const xs = [hole.bl.x, hole.br.x, hole.tr.x, hole.tl.x];
|
||||
const ys = [hole.bl.y, hole.br.y, hole.tr.y, hole.tl.y];
|
||||
const left = Math.min(...xs);
|
||||
const right = Math.max(...xs);
|
||||
const top = Math.min(...ys);
|
||||
const bottom = Math.max(...ys);
|
||||
const perpX = -sinA;
|
||||
const perpY = cosA;
|
||||
const fwdX = this._range * cosA;
|
||||
const fwdY = this._range * sinA;
|
||||
|
||||
// Clamp to screen
|
||||
const l = Math.max(0, left);
|
||||
const r = Math.min(width, right);
|
||||
const t = Math.max(0, top);
|
||||
const b = Math.min(height, bottom);
|
||||
// Four corners of the rotated periscope rectangle
|
||||
const bl = { x: cx - this._halfArc * perpX, y: cy - this._halfArc * perpY };
|
||||
const br = { x: cx + this._halfArc * perpX, y: cy + this._halfArc * perpY };
|
||||
const tr = { x: cx + this._halfArc * perpX + fwdX, y: cy + this._halfArc * perpY + fwdY };
|
||||
const tl = { x: cx - this._halfArc * perpX + fwdX, y: cy - this._halfArc * perpY + fwdY };
|
||||
|
||||
// Draw four blinders around the hole
|
||||
// Top strip
|
||||
if (t > 0) g.fillRect(0, 0, width, t);
|
||||
// Bottom strip
|
||||
if (b < height) g.fillRect(0, b, width, height - b);
|
||||
// Left strip (between top and bottom)
|
||||
if (l > 0) g.fillRect(0, t, l, b - t);
|
||||
// Right strip (between top and bottom)
|
||||
if (r < width) g.fillRect(r, t, width - r, b - t);
|
||||
// Axis-aligned bounding box
|
||||
const left = Math.max(0, Math.min(bl.x, br.x, tr.x, tl.x));
|
||||
const right = Math.min(w, Math.max(bl.x, br.x, tr.x, tl.x));
|
||||
const top = Math.max(0, Math.min(bl.y, br.y, tr.y, tl.y));
|
||||
const bottom = Math.min(h, Math.max(bl.y, br.y, tr.y, tl.y));
|
||||
|
||||
// Four blinders around the AABB
|
||||
if (top > 0) g.fillRect(0, 0, w, top); // top strip
|
||||
if (bottom < h) g.fillRect(0, bottom, w, h - bottom); // bottom strip
|
||||
if (left > 0) g.fillRect(0, top, left, bottom - top); // left strip
|
||||
if (right < w) g.fillRect(right, top, w - right, bottom - top); // right strip
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the four corners of the periscope hole rectangle.
|
||||
* @private
|
||||
* @returns {{ bl, br, tr, tl }}
|
||||
*/
|
||||
_computeHoleCorners() {
|
||||
const cx = this._x;
|
||||
@@ -112,56 +109,40 @@ class VisionMask {
|
||||
|
||||
const perpX = -sinA;
|
||||
const perpY = cosA;
|
||||
const fwdX = RANGE * cosA;
|
||||
const fwdY = RANGE * sinA;
|
||||
const fwdX = this._range * cosA;
|
||||
const fwdY = this._range * sinA;
|
||||
|
||||
return {
|
||||
bl: { x: cx - HALF_ARC * perpX, y: cy - HALF_ARC * perpY },
|
||||
br: { x: cx + HALF_ARC * perpX, y: cy + HALF_ARC * perpY },
|
||||
tr: { x: cx + HALF_ARC * perpX + fwdX, y: cy + HALF_ARC * perpY + fwdY },
|
||||
tl: { x: cx - HALF_ARC * perpX + fwdX, y: cy - HALF_ARC * perpY + fwdY },
|
||||
bl: { x: cx - this._halfArc * perpX, y: cy - this._halfArc * perpY },
|
||||
br: { x: cx + this._halfArc * perpX, y: cy + this._halfArc * perpY },
|
||||
tr: { x: cx + this._halfArc * perpX + fwdX, y: cy + this._halfArc * perpY + fwdY },
|
||||
tl: { x: cx - this._halfArc * perpX + fwdX, y: cy - this._halfArc * perpY + fwdY },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a world point is visible through the periscope.
|
||||
* Transforms to tank-local coordinates and checks rectangle bounds.
|
||||
*
|
||||
* @param {number} worldX
|
||||
* @param {number} worldY
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isVisible(worldX, worldY) {
|
||||
const dx = worldX - this._x;
|
||||
const dy = worldY - this._y;
|
||||
|
||||
// Rotate point into tank-local frame (tank faces +x)
|
||||
const cosA = Math.cos(-this._angle);
|
||||
const sinA = Math.sin(-this._angle);
|
||||
const localX = dx * cosA - dy * sinA;
|
||||
const localY = dx * sinA + dy * cosA;
|
||||
|
||||
// Rectangle: forward [0, RANGE], lateral [-HALF_ARC, HALF_ARC]
|
||||
return localX >= 0 && localX <= RANGE && Math.abs(localY) <= HALF_ARC;
|
||||
return localX >= 0 && localX <= this._range && Math.abs(localY) <= this._halfArc;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {{ x: number, y: number }} center of the periscope hole
|
||||
*/
|
||||
getPeriscopeCenter() {
|
||||
return { x: this._x, y: this._y };
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {{ forward: { x: number, y: number } }}
|
||||
*/
|
||||
getPeriscopeEdgePoints() {
|
||||
const cosA = Math.cos(this._angle);
|
||||
const sinA = Math.sin(this._angle);
|
||||
return {
|
||||
forward: {
|
||||
x: this._x + RANGE * cosA,
|
||||
y: this._y + RANGE * sinA,
|
||||
x: this._x + this._range * cosA,
|
||||
y: this._y + this._range * sinA,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
107
src/game/systems/ZoneManager.js
Normal file
107
src/game/systems/ZoneManager.js
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* ZoneManager — tracks tank x-position against zone thresholds and fires
|
||||
* onTransition callbacks when the player crosses into a new zone.
|
||||
*
|
||||
* Zone definitions have:
|
||||
* id: number (unique zone identifier)
|
||||
* label: string (human-readable name)
|
||||
* xMin: number | undefined (inclusive lower bound, default -Infinity)
|
||||
* xMax: number | undefined (exclusive upper bound, default +Infinity)
|
||||
* background: string (texture key for the background image)
|
||||
* tileset: string (tileset identifier)
|
||||
* enemyTypes: string[] (enemy type keys valid for this zone)
|
||||
* obstaclePalette: string[] (optional — obstacle colors for this zone)
|
||||
*
|
||||
* @module src/game/systems/ZoneManager
|
||||
*/
|
||||
|
||||
export class ZoneManager {
|
||||
/**
|
||||
* @param {object[]} zones — array of zone definition objects
|
||||
*/
|
||||
constructor(zones) {
|
||||
if (!zones || zones.length === 0) {
|
||||
throw new Error('ZoneManager requires at least one zone');
|
||||
}
|
||||
this.zones = zones;
|
||||
this._currentZoneId = zones[0].id;
|
||||
this._initialized = false;
|
||||
this.onTransition = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the zone based on tank position. Fires onTransition if the
|
||||
* zone has changed since the last update.
|
||||
*
|
||||
* @param {number} x — tank x position in pixels
|
||||
* @param {number} _y — tank y position (unused; zones are x-axis thresholds)
|
||||
*/
|
||||
update(x, _y) {
|
||||
const newZone = this._matchZone(x);
|
||||
if (newZone === null) return;
|
||||
|
||||
if (!this._initialized) {
|
||||
// First update: set the zone without firing a transition
|
||||
this._currentZoneId = newZone.id;
|
||||
this._initialized = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (newZone.id !== this._currentZoneId) {
|
||||
const fromId = this._currentZoneId;
|
||||
const fromZone = this._findZone(fromId);
|
||||
this._currentZoneId = newZone.id;
|
||||
|
||||
if (this.onTransition) {
|
||||
this.onTransition({
|
||||
from: fromZone ? fromZone.id : fromId,
|
||||
to: newZone.id,
|
||||
zone: newZone,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current zone id.
|
||||
*/
|
||||
get currentZone() {
|
||||
return this._currentZoneId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full zone object for the current zone.
|
||||
* @returns {object}
|
||||
*/
|
||||
getCurrentZone() {
|
||||
return this._findZone(this._currentZoneId) || this.zones[0];
|
||||
}
|
||||
|
||||
// ---- private ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Find which zone matches the given x position.
|
||||
* xMin is inclusive, xMax is exclusive.
|
||||
* @param {number} x
|
||||
* @returns {object|null}
|
||||
*/
|
||||
_matchZone(x) {
|
||||
for (const zone of this.zones) {
|
||||
const min = zone.xMin != null ? zone.xMin : -Infinity;
|
||||
const max = zone.xMax != null ? zone.xMax : Infinity;
|
||||
if (x >= min && x < max) {
|
||||
return zone;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a zone by id.
|
||||
* @param {number} id
|
||||
* @returns {object|undefined}
|
||||
*/
|
||||
_findZone(id) {
|
||||
return this.zones.find(z => z.id === id);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import Phaser from 'phaser';
|
||||
import constants from './constants.js';
|
||||
import { PreloadScene } from './game/scenes/PreloadScene.js';
|
||||
import { MainGame } from './game/scenes/MainGame.js';
|
||||
import { HUDScene } from './game/scenes/HUDScene.js';
|
||||
|
||||
window.__IR_DEBUG = { log: [] };
|
||||
const debug = (...args) => {
|
||||
@@ -36,7 +37,7 @@ const gameConfig = {
|
||||
mode: Phaser.Scale.FIT,
|
||||
autoCenter: Phaser.Scale.CENTER_BOTH,
|
||||
},
|
||||
scene: [PreloadScene, MainGame],
|
||||
scene: [PreloadScene, MainGame, HUDScene],
|
||||
banner: true,
|
||||
};
|
||||
|
||||
|
||||
35
src/systems/AudioManager.js
Normal file
35
src/systems/AudioManager.js
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* AudioManager — Stub for future audio implementation
|
||||
* Handles background music, SFX, and ambient sounds
|
||||
*/
|
||||
export class AudioManager {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
this.muted = false;
|
||||
this.volume = 0.7;
|
||||
}
|
||||
|
||||
playSFX(key, config = {}) {
|
||||
// Stub: no-op until audio assets are added
|
||||
return null;
|
||||
}
|
||||
|
||||
playMusic(key, config = {}) {
|
||||
// Stub: no-op until audio assets are added
|
||||
return null;
|
||||
}
|
||||
|
||||
stopMusic() {
|
||||
// Stub
|
||||
}
|
||||
|
||||
setMuted(muted) {
|
||||
this.muted = muted;
|
||||
}
|
||||
|
||||
setVolume(vol) {
|
||||
this.volume = Math.max(0, Math.min(1, vol));
|
||||
}
|
||||
}
|
||||
|
||||
export default AudioManager;
|
||||
57
src/systems/VFXManager.js
Normal file
57
src/systems/VFXManager.js
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* VFXManager — Stub for future visual effects implementation
|
||||
* Handles particles, screen shake, hit markers, etc.
|
||||
*/
|
||||
export class VFXManager {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
this.particles = [];
|
||||
}
|
||||
|
||||
createHitEffect(x, y, color = 0xffaa00) {
|
||||
// Stub: flash effect
|
||||
const flash = this.scene.add.circle(x, y, 8, color, 0.6);
|
||||
this.scene.tweens.add({
|
||||
targets: flash,
|
||||
alpha: 0,
|
||||
scale: 2,
|
||||
duration: 200,
|
||||
onComplete: () => flash.destroy()
|
||||
});
|
||||
return flash;
|
||||
}
|
||||
|
||||
createExplosion(x, y, radius = 32) {
|
||||
// Stub: simple explosion flash
|
||||
const explosion = this.scene.add.circle(x, y, radius, 0xff6600, 0.7);
|
||||
this.scene.tweens.add({
|
||||
targets: explosion,
|
||||
alpha: 0,
|
||||
scale: 3,
|
||||
duration: 400,
|
||||
onComplete: () => explosion.destroy()
|
||||
});
|
||||
return explosion;
|
||||
}
|
||||
|
||||
screenShake(intensity = 5, duration = 200) {
|
||||
// Stub: camera shake
|
||||
this.scene.cameras.main.shake(duration, intensity / 1000);
|
||||
}
|
||||
|
||||
createTracer(fromX, fromY, toX, toY, color = 0xffff00) {
|
||||
// Stub: tracer line
|
||||
const graphics = this.scene.add.graphics();
|
||||
graphics.lineStyle(2, color, 0.8);
|
||||
graphics.lineBetween(fromX, fromY, toX, toY);
|
||||
this.scene.tweens.add({
|
||||
targets: graphics,
|
||||
alpha: 0,
|
||||
duration: 300,
|
||||
onComplete: () => graphics.destroy()
|
||||
});
|
||||
return graphics;
|
||||
}
|
||||
}
|
||||
|
||||
export default VFXManager;
|
||||
4
test-results/.last-run.json
Normal file
4
test-results/.last-run.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"status": "passed",
|
||||
"failedTests": []
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 232 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 231 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 232 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 231 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 232 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user