Codex CLI commited on
Commit
fe3647a
·
1 Parent(s): 73a1f35

feat(hud): implement hitmarker functionality with visual feedback on enemy hits

Browse files
Files changed (6) hide show
  1. index.html +13 -0
  2. src/combat.js +2 -0
  3. src/config.js +6 -1
  4. src/globals.js +1 -0
  5. src/hud.js +67 -1
  6. 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
  }