import { config } from '../config/Config.js'; import { ensureUiCameras, assignWorld, assignUi } from '../visuals/UiCameras.js'; export { ensureUiCameras, assignWorld, assignUi }; /** * The combat CAMERA KICK — a subtle world-camera shake on the big beats: * you taking a hit, a hostile going down, the shield collapse. * * It fires on the WORLD camera only. The UI pass (js/visuals/UiCameras.js * → `fx-ui`) renders the bar, feed, and gauge flashes on its own fixed * pass, so the HUD stays perfectly still while the world kicks — that's * the whole point of the split. Call `ensureUiCameras(scene)` at scene * setup (both combat contexts do) so the split exists when the shake * lands; without a split (a scene without cameras, the headless tests) * this is a guarded no-op. * * Tuning: data/combat.json → combat.fx.shake — `hit` / `kill` / `break` * are the Phaser shake intensities (a small FRACTION of the viewport, * the same idiom as the tether contact's 0.003–0.007 in GameScene), and * `ms` is the duration. */ /** * @param {object} scene the combat scene * @param {string} kind 'hit' | 'kill' | 'break' — the fx.shake.* intensity * to use (the beat's weight: a player hit < an enemy kill < the * shield-collapse) */ export function combatShake(scene, kind = 'hit') { const cam = scene?.cameras?.main; if (!cam || typeof cam.shake !== 'function') return; const fallback = { hit: 0.0045, kill: 0.007, break: 0.011 }[kind] ?? 0.005; const intensity = Number(config.get(`combat.fx.shake.${kind}`, fallback)); if (!Number.isFinite(intensity) || intensity <= 0) return; const ms = Math.max(60, Number(config.get('combat.fx.shake.ms', 230)) || 230); // The UI split (once) so the kick hits the WORLD pass only — the bar, // feed, and gauge flashes render on the untouched `fx-ui` pass and stay // perfectly still. (Idempotent; null-safe for camera-less scenes.) ensureUiCameras(scene); cam.shake(ms, intensity); }