Spaces:
Running
Running
Codex CLI
commited on
Commit
·
fe3647a
1
Parent(s):
73a1f35
feat(hud): implement hitmarker functionality with visual feedback on enemy hits
Browse files- index.html +13 -0
- src/combat.js +2 -0
- src/config.js +6 -1
- src/globals.js +1 -0
- src/hud.js +67 -1
- src/main.js +3 -1
index.html
CHANGED
|
@@ -41,6 +41,15 @@
|
|
| 41 |
}
|
| 42 |
.arm-h { height: 2px; }
|
| 43 |
.arm-v { width: 2px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
/* HUD Cards */
|
| 45 |
.hud-card {
|
| 46 |
position: absolute;
|
|
@@ -180,6 +189,10 @@
|
|
| 180 |
<div id="ch-right" class="crosshair-arm arm-h"></div>
|
| 181 |
<div id="ch-top" class="crosshair-arm arm-v"></div>
|
| 182 |
<div id="ch-bottom" class="crosshair-arm arm-v"></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
</div>
|
| 184 |
<div id="wave-banner"></div>
|
| 185 |
<div id="heal-overlay"></div>
|
|
|
|
| 41 |
}
|
| 42 |
.arm-h { height: 2px; }
|
| 43 |
.arm-v { width: 2px; }
|
| 44 |
+
/* Hitmarker (X) */
|
| 45 |
+
.hit-arm {
|
| 46 |
+
position: absolute;
|
| 47 |
+
height: 2px;
|
| 48 |
+
background: rgba(255,255,255,0.95);
|
| 49 |
+
box-shadow: 0 0 6px rgba(255,255,255,0.6);
|
| 50 |
+
opacity: 0; /* driven by JS */
|
| 51 |
+
transform-origin: 50% 50%;
|
| 52 |
+
}
|
| 53 |
/* HUD Cards */
|
| 54 |
.hud-card {
|
| 55 |
position: absolute;
|
|
|
|
| 189 |
<div id="ch-right" class="crosshair-arm arm-h"></div>
|
| 190 |
<div id="ch-top" class="crosshair-arm arm-v"></div>
|
| 191 |
<div id="ch-bottom" class="crosshair-arm arm-v"></div>
|
| 192 |
+
<div id="ch-hit-a1" class="hit-arm"></div>
|
| 193 |
+
<div id="ch-hit-a2" class="hit-arm"></div>
|
| 194 |
+
<div id="ch-hit-b1" class="hit-arm"></div>
|
| 195 |
+
<div id="ch-hit-b2" class="hit-arm"></div>
|
| 196 |
</div>
|
| 197 |
<div id="wave-banner"></div>
|
| 198 |
<div id="heal-overlay"></div>
|
src/combat.js
CHANGED
|
@@ -90,6 +90,8 @@ export function performShooting(delta) {
|
|
| 90 |
|
| 91 |
const { enemy, zone } = findEnemyAndZone(firstHit.object);
|
| 92 |
if (enemy && enemy.alive) {
|
|
|
|
|
|
|
| 93 |
const isHead = zone === 'head';
|
| 94 |
const dmg = CFG.gun.damage * (isHead ? CFG.gun.headshotMult : 1);
|
| 95 |
enemy.hp -= dmg;
|
|
|
|
| 90 |
|
| 91 |
const { enemy, zone } = findEnemyAndZone(firstHit.object);
|
| 92 |
if (enemy && enemy.alive) {
|
| 93 |
+
// Trigger hitmarker HUD flash
|
| 94 |
+
G.hitFlash = Math.min(1, (G.hitFlash || 0) + 1);
|
| 95 |
const isHead = zone === 'head';
|
| 96 |
const dmg = CFG.gun.damage * (isHead ? CFG.gun.headshotMult : 1);
|
| 97 |
enemy.hp -= dmg;
|
src/config.js
CHANGED
|
@@ -160,7 +160,12 @@ export const CFG = {
|
|
| 160 |
healMaxOpacity: 0.5,
|
| 161 |
healFadeSpeed: 2.5, // per second
|
| 162 |
healPulsePerPickup: 0.45, // adds to flash per orb pickup
|
| 163 |
-
healPulsePerHP: 0.01 // adds per HP healed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
},
|
| 165 |
seed: 1337
|
| 166 |
};
|
|
|
|
| 160 |
healMaxOpacity: 0.5,
|
| 161 |
healFadeSpeed: 2.5, // per second
|
| 162 |
healPulsePerPickup: 0.45, // adds to flash per orb pickup
|
| 163 |
+
healPulsePerHP: 0.01, // adds per HP healed
|
| 164 |
+
// Hitmarker (X) flash
|
| 165 |
+
hitFadeSpeed: 12.0, // higher = faster fade (per second)
|
| 166 |
+
hitMaxOpacity: 0.3, // cap opacity to 30%
|
| 167 |
+
hitSize: 12, // shorter segment length
|
| 168 |
+
hitGapExtra: 6 // extra center gap vs crosshair
|
| 169 |
},
|
| 170 |
seed: 1337
|
| 171 |
};
|
src/globals.js
CHANGED
|
@@ -92,6 +92,7 @@ export const G = {
|
|
| 92 |
helmets: [],
|
| 93 |
damageFlash: 0,
|
| 94 |
healFlash: 0,
|
|
|
|
| 95 |
// Deferred GPU resource disposal queue to avoid frame spikes
|
| 96 |
disposeQueue: [],
|
| 97 |
|
|
|
|
| 92 |
helmets: [],
|
| 93 |
damageFlash: 0,
|
| 94 |
healFlash: 0,
|
| 95 |
+
hitFlash: 0,
|
| 96 |
// Deferred GPU resource disposal queue to avoid frame spikes
|
| 97 |
disposeQueue: [],
|
| 98 |
|
src/hud.js
CHANGED
|
@@ -16,8 +16,15 @@ const HUD = {
|
|
| 16 |
right: /** @type {HTMLElement|null} */(document.getElementById('ch-right')),
|
| 17 |
top: /** @type {HTMLElement|null} */(document.getElementById('ch-top')),
|
| 18 |
bottom: /** @type {HTMLElement|null} */(document.getElementById('ch-bottom')),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
lastGap: -1,
|
| 20 |
-
lastLen: -1
|
|
|
|
|
|
|
|
|
|
| 21 |
},
|
| 22 |
last: {
|
| 23 |
hp: -1,
|
|
@@ -152,3 +159,62 @@ export function updateCrosshair(delta) {
|
|
| 152 |
ch.bottom.style.top = gapPx + 'px';
|
| 153 |
ch.bottom.style.left = '-1px';
|
| 154 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
right: /** @type {HTMLElement|null} */(document.getElementById('ch-right')),
|
| 17 |
top: /** @type {HTMLElement|null} */(document.getElementById('ch-top')),
|
| 18 |
bottom: /** @type {HTMLElement|null} */(document.getElementById('ch-bottom')),
|
| 19 |
+
hitA1: /** @type {HTMLElement|null} */(document.getElementById('ch-hit-a1')),
|
| 20 |
+
hitA2: /** @type {HTMLElement|null} */(document.getElementById('ch-hit-a2')),
|
| 21 |
+
hitB1: /** @type {HTMLElement|null} */(document.getElementById('ch-hit-b1')),
|
| 22 |
+
hitB2: /** @type {HTMLElement|null} */(document.getElementById('ch-hit-b2')),
|
| 23 |
lastGap: -1,
|
| 24 |
+
lastLen: -1,
|
| 25 |
+
hitLen: -1,
|
| 26 |
+
lastHitOpacity: -1,
|
| 27 |
+
hitGap: -1
|
| 28 |
},
|
| 29 |
last: {
|
| 30 |
hp: -1,
|
|
|
|
| 159 |
ch.bottom.style.top = gapPx + 'px';
|
| 160 |
ch.bottom.style.left = '-1px';
|
| 161 |
}
|
| 162 |
+
|
| 163 |
+
// Small white X that flashes briefly when hitting an enemy
|
| 164 |
+
export function updateHitMarker(delta) {
|
| 165 |
+
const { ch } = HUD;
|
| 166 |
+
if (!ch.root || !ch.hitA1 || !ch.hitA2 || !ch.hitB1 || !ch.hitB2) return;
|
| 167 |
+
|
| 168 |
+
// Fade out hit flash
|
| 169 |
+
G.hitFlash = Math.max(0, G.hitFlash - (CFG.hud.hitFadeSpeed || 12) * delta);
|
| 170 |
+
const maxOp = (CFG.hud.hitMaxOpacity != null ? CFG.hud.hitMaxOpacity : 0.3);
|
| 171 |
+
const op = Math.max(0, Math.min(maxOp, G.hitFlash));
|
| 172 |
+
|
| 173 |
+
// Only touch DOM when something changed
|
| 174 |
+
if (Math.abs(op - ch.lastHitOpacity) > 0.01) {
|
| 175 |
+
ch.lastHitOpacity = op;
|
| 176 |
+
ch.hitA1.style.opacity = String(op);
|
| 177 |
+
ch.hitA2.style.opacity = String(op);
|
| 178 |
+
ch.hitB1.style.opacity = String(op);
|
| 179 |
+
ch.hitB2.style.opacity = String(op);
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
// Size and placement: four segments with a center gap
|
| 183 |
+
const L = (CFG.hud.hitSize || 16) | 0; // length of each segment in px
|
| 184 |
+
const extra = (CFG.hud.hitGapExtra || 6) | 0;
|
| 185 |
+
const baseGap = ch.lastGap >= 0 ? ch.lastGap : 10; // from crosshair
|
| 186 |
+
const gap = baseGap + extra; // ensure larger than crosshair gap
|
| 187 |
+
if (L !== ch.hitLen || Math.abs(gap - ch.hitGap) > 0.5) {
|
| 188 |
+
ch.hitLen = L;
|
| 189 |
+
ch.hitGap = gap;
|
| 190 |
+
// Distance from center to each segment center along the diagonal
|
| 191 |
+
const d = gap + L * 0.5;
|
| 192 |
+
const s = Math.SQRT1_2; // 1 / sqrt(2)
|
| 193 |
+
const dx = d * s;
|
| 194 |
+
const dy = d * s;
|
| 195 |
+
const leftBase = (v) => (v - L * 0.5) + 'px';
|
| 196 |
+
const topBase = (v) => (v - 1) + 'px'; // thickness ~2px
|
| 197 |
+
|
| 198 |
+
// A diagonal (+45deg): NE and SW
|
| 199 |
+
ch.hitA1.style.width = L + 'px';
|
| 200 |
+
ch.hitA1.style.left = leftBase(dx);
|
| 201 |
+
ch.hitA1.style.top = topBase(dy);
|
| 202 |
+
ch.hitA1.style.transform = 'rotate(45deg)';
|
| 203 |
+
|
| 204 |
+
ch.hitA2.style.width = L + 'px';
|
| 205 |
+
ch.hitA2.style.left = leftBase(-dx);
|
| 206 |
+
ch.hitA2.style.top = topBase(-dy);
|
| 207 |
+
ch.hitA2.style.transform = 'rotate(45deg)';
|
| 208 |
+
|
| 209 |
+
// B diagonal (-45deg): NW and SE
|
| 210 |
+
ch.hitB1.style.width = L + 'px';
|
| 211 |
+
ch.hitB1.style.left = leftBase(-dx);
|
| 212 |
+
ch.hitB1.style.top = topBase(dy);
|
| 213 |
+
ch.hitB1.style.transform = 'rotate(-45deg)';
|
| 214 |
+
|
| 215 |
+
ch.hitB2.style.width = L + 'px';
|
| 216 |
+
ch.hitB2.style.left = leftBase(dx);
|
| 217 |
+
ch.hitB2.style.top = topBase(-dy);
|
| 218 |
+
ch.hitB2.style.transform = 'rotate(-45deg)';
|
| 219 |
+
}
|
| 220 |
+
}
|
src/main.js
CHANGED
|
@@ -10,7 +10,7 @@ import { setupEvents } from './events.js';
|
|
| 10 |
import { updatePlayer } from './player.js';
|
| 11 |
import { updateEnemies } from './enemies.js';
|
| 12 |
import { startNextWave, updateWaves } from './waves.js';
|
| 13 |
-
import { updateHUD, showOverlay, updateDamageEffect, updateHealEffect, updateCrosshair } from './hud.js';
|
| 14 |
import { updateFX } from './fx.js';
|
| 15 |
import { updateCasings } from './casings.js';
|
| 16 |
import { updateEnemyProjectiles } from './projectiles.js';
|
|
@@ -122,6 +122,7 @@ function startGame() {
|
|
| 122 |
G.camera.position.copy(G.player.pos);
|
| 123 |
G.damageFlash = 0;
|
| 124 |
G.healFlash = 0;
|
|
|
|
| 125 |
// Grenades reset
|
| 126 |
G.grenades.length = 0;
|
| 127 |
G.heldGrenade = null;
|
|
@@ -213,6 +214,7 @@ function animate() {
|
|
| 213 |
updatePickups(delta);
|
| 214 |
updateHUD();
|
| 215 |
updateCrosshair(delta);
|
|
|
|
| 216 |
updateDamageEffect(delta);
|
| 217 |
updateHealEffect(delta);
|
| 218 |
}
|
|
|
|
| 10 |
import { updatePlayer } from './player.js';
|
| 11 |
import { updateEnemies } from './enemies.js';
|
| 12 |
import { startNextWave, updateWaves } from './waves.js';
|
| 13 |
+
import { updateHUD, showOverlay, updateDamageEffect, updateHealEffect, updateCrosshair, updateHitMarker } from './hud.js';
|
| 14 |
import { updateFX } from './fx.js';
|
| 15 |
import { updateCasings } from './casings.js';
|
| 16 |
import { updateEnemyProjectiles } from './projectiles.js';
|
|
|
|
| 122 |
G.camera.position.copy(G.player.pos);
|
| 123 |
G.damageFlash = 0;
|
| 124 |
G.healFlash = 0;
|
| 125 |
+
G.hitFlash = 0;
|
| 126 |
// Grenades reset
|
| 127 |
G.grenades.length = 0;
|
| 128 |
G.heldGrenade = null;
|
|
|
|
| 214 |
updatePickups(delta);
|
| 215 |
updateHUD();
|
| 216 |
updateCrosshair(delta);
|
| 217 |
+
updateHitMarker(delta);
|
| 218 |
updateDamageEffect(delta);
|
| 219 |
updateHealEffect(delta);
|
| 220 |
}
|