Function bodies 653 total
generateWave function · javascript · L5-L10 (6 LOC)ai/mistral.js
export async function generateWave(performanceData) {
// TODO: Send performanceData to Mistral API and receive adjusted wave config
// performanceData = { clearTime, healthRemaining, abilitiesUsed, enemiesKilled }
console.log('[AI] Mistral integration stubbed. Performance data:', performanceData);
return null;
}getPitBounds function · javascript · L33-L40 (8 LOC)config/arena.js
export function getPitBounds() {
return PITS.map(p => ({
minX: p.x - p.w / 2,
maxX: p.x + p.w / 2,
minZ: p.z - p.d / 2,
maxZ: p.z + p.d / 2,
}));
}getCollisionBounds function · javascript · L44-L70 (27 LOC)config/arena.js
export function getCollisionBounds() {
const bounds = [];
// Obstacles
for (const o of OBSTACLES) {
bounds.push({
minX: o.x - o.w / 2,
maxX: o.x + o.w / 2,
minZ: o.z - o.d / 2,
maxZ: o.z + o.d / 2,
});
}
// Walls
const h = ARENA_HALF;
const t = WALL_THICKNESS;
// North wall
bounds.push({ minX: -h - t/2, maxX: h + t/2, minZ: h - t/2, maxZ: h + t/2 });
// South wall
bounds.push({ minX: -h - t/2, maxX: h + t/2, minZ: -h - t/2, maxZ: -h + t/2 });
// East wall
bounds.push({ minX: h - t/2, maxX: h + t/2, minZ: -h - t/2, maxZ: h + t/2 });
// West wall
bounds.push({ minX: -h - t/2, maxX: -h + t/2, minZ: -h - t/2, maxZ: h + t/2 });
return bounds;
}resolveEffectType function · javascript · L439-L459 (21 LOC)config/effectTypes.js
export function resolveEffectType(typeId) {
const type = EFFECT_TYPES[typeId];
if (!type) {
console.warn(`Unknown effect type: ${typeId}`);
return null;
}
// No parent — return as-is
if (!type.parent) {
return { ...type, id: typeId };
}
// Recursively resolve parent
const parentResolved = resolveEffectType(type.parent);
if (!parentResolved) {
return { ...type, id: typeId };
}
// Deep merge: child overrides parent
return deepMerge(parentResolved, { ...type, id: typeId });
}effectTypeMatches function · javascript · L468-L476 (9 LOC)config/effectTypes.js
export function effectTypeMatches(typeId, query) {
if (typeId === query) return true;
// Check if query is a parent of typeId
// 'fire.major' starts with 'fire.' — matches 'fire'
if (typeId.startsWith(query + '.')) return true;
return false;
}deepMerge function · javascript · L492-L515 (24 LOC)config/effectTypes.js
function deepMerge(target, source) {
const result = { ...target };
for (const key of Object.keys(source)) {
if (source[key] === undefined) continue;
if (
source[key] !== null &&
typeof source[key] === 'object' &&
!Array.isArray(source[key]) &&
target[key] !== null &&
typeof target[key] === 'object' &&
!Array.isArray(target[key])
) {
// Recursively merge objects
result[key] = deepMerge(target[key], source[key]);
} else {
// Override with source value
result[key] = source[key];
}
}
return result;
}createAoeRing function · javascript · L34-L66 (33 LOC)engine/aoeTelegraph.js
export function createAoeRing(x, z, maxRadius, durationMs, color) {
if (!ringGeo) {
ringGeo = new THREE.RingGeometry(0.8, 1.0, 32);
ringGeo.rotateX(-Math.PI / 2); // lay flat on ground
}
const mat = new THREE.MeshBasicMaterial({
color: color,
transparent: true,
opacity: 0.8,
side: THREE.DoubleSide,
depthWrite: false,
});
const mesh = new THREE.Mesh(ringGeo, mat);
mesh.position.set(x, 0.05, z); // slightly above ground
mesh.scale.set(0.01, 0.01, 0.01); // start tiny
sceneRef.add(mesh);
const telegraph = {
type: 'ring',
mesh: mesh,
material: mat,
center: { x, z },
maxRadius: maxRadius,
duration: durationMs,
elapsed: 0,
color: color,
};
activeTelegraphs.push(telegraph);
return telegraph;
}Repobility · code-quality intelligence · https://repobility.com
createAoeRect function · javascript · L70-L125 (56 LOC)engine/aoeTelegraph.js
export function createAoeRect(x, z, width, height, rotation, durationMs, color) {
if (!planeGeo) {
planeGeo = new THREE.PlaneGeometry(1, 1);
planeGeo.rotateX(-Math.PI / 2); // lay flat on ground
}
const group = new THREE.Group();
group.position.set(x, 0.05, z);
group.rotation.y = rotation;
// Fill plane
const fillMat = new THREE.MeshBasicMaterial({
color: color,
transparent: true,
opacity: 0.0, // ramps up over first 30%
side: THREE.DoubleSide,
depthWrite: false,
});
const fillMesh = new THREE.Mesh(planeGeo, fillMat);
fillMesh.scale.set(width * 0.8, 1, height * 0.8); // start at 80%
group.add(fillMesh);
// Border outline
const edgeGeo = new THREE.EdgesGeometry(new THREE.PlaneGeometry(width, height));
edgeGeo.rotateX(-Math.PI / 2);
const borderMat = new THREE.LineBasicMaterial({
color: color,
transparent: true,
opacity: 0.9,
depthWrite: false,
});
const borderMesh = new THREE.LineSegments(edgeGeo, borderMatisInRotatedRect function · javascript · L140-L149 (10 LOC)engine/aoeTelegraph.js
export function isInRotatedRect(ex, ez, cx, cz, w, h, rotation, padding) {
const pad = padding || 0;
const dx = ex - cx;
const dz = ez - cz;
const cos = Math.cos(-rotation);
const sin = Math.sin(-rotation);
const localX = dx * cos - dz * sin;
const localZ = dx * sin + dz * cos;
return Math.abs(localX) < (w / 2 + pad) && Math.abs(localZ) < (h / 2 + pad);
}updateAoeTelegraphs function · javascript · L153-L173 (21 LOC)engine/aoeTelegraph.js
export function updateAoeTelegraphs(dt) {
const dtMs = dt * 1000;
for (let i = activeTelegraphs.length - 1; i >= 0; i--) {
const t = activeTelegraphs[i];
t.elapsed += dtMs;
const progress = Math.min(t.elapsed / t.duration, 1);
if (t.type === 'ring') {
updateRing(t, progress);
} else if (t.type === 'rect') {
updateRect(t, progress);
}
// Remove when complete
if (progress >= 1) {
removeTelegraph(t);
activeTelegraphs.splice(i, 1);
}
}
}updateRing function · javascript · L175-L188 (14 LOC)engine/aoeTelegraph.js
function updateRing(t, progress) {
const easedProgress = easeOutQuad(progress);
// Scale — expand from 0 to maxRadius
const currentRadius = t.maxRadius * easedProgress;
t.mesh.scale.set(currentRadius, currentRadius, currentRadius);
// Thickness shrinks (simulated via Y scale stretching the ring thinner feel)
// The ring geometry is 0.8→1.0 inner/outer, so at scale 1 it's 0.2 thick.
// We modulate opacity instead for the thinning effect.
// Opacity fades out
t.material.opacity = 0.8 * (1 - progress);
}updateRect function · javascript · L190-L239 (50 LOC)engine/aoeTelegraph.js
function updateRect(t, progress) {
const easedProgress = easeOutQuad(progress);
// Fill: opacity ramps 0 → 0.3 over first 30%, then fades
let fillOpacity;
if (progress < 0.3) {
fillOpacity = 0.3 * (progress / 0.3);
} else {
fillOpacity = 0.3 * (1 - (progress - 0.3) / 0.7);
}
t.fillMaterial.opacity = Math.max(0, fillOpacity);
// Fill scale: 80% → 100%
const scaleMult = 0.8 + 0.2 * easedProgress;
t.fillMesh.scale.set(t.width * scaleMult, 1, t.height * scaleMult);
// Border: pulse frequency accelerates 2Hz → 8Hz
const freq = 2 + 6 * progress;
const pulse = 0.5 + 0.5 * Math.sin(performance.now() * freq * 0.00628); // 2π/1000
t.borderMaterial.opacity = 0.4 + 0.5 * pulse;
// Brightness ramp in last 100ms — telegraph flashes bright before firing
const flashThreshold = 1 - (100 / t.duration); // last 100ms as fraction of total
if (progress > flashThreshold) {
const flashProgress = (progress - flashThreshold) / (1 - flashThreshold); // 0→1
/removeTelegraph function · javascript · L241-L251 (11 LOC)engine/aoeTelegraph.js
function removeTelegraph(t) {
if (t.type === 'ring') {
t.material.dispose();
sceneRef.remove(t.mesh);
} else if (t.type === 'rect') {
t.fillMaterial.dispose();
t.borderMaterial.dispose();
t.borderEdgeGeo.dispose();
sceneRef.remove(t.mesh);
}
}updatePendingEffects function · javascript · L264-L279 (16 LOC)engine/aoeTelegraph.js
export function updatePendingEffects(dt) {
const dtMs = dt * 1000;
for (let i = pendingEffects.length - 1; i >= 0; i--) {
const p = pendingEffects[i];
p.delay -= dtMs;
if (p.delay <= 0) {
if (p.enemy) {
p.callback(p.enemy);
} else {
p.callback();
}
pendingEffects.splice(i, 1);
}
}
}applyAoeEffect function · javascript · L297-L331 (35 LOC)engine/aoeTelegraph.js
export function applyAoeEffect({ x, z, radius, durationMs, color, label, effectFn, gameState, excludeEnemy }) {
// 1. Create expanding ring visual
createAoeRing(x, z, radius, durationMs, color);
// 2. Schedule cascade effects on each enemy in range
const colorStr = '#' + color.toString(16).padStart(6, '0');
for (const enemy of gameState.enemies) {
if (enemy === excludeEnemy) continue;
const dx = enemy.pos.x - x;
const dz = enemy.pos.z - z;
const dist = Math.sqrt(dx * dx + dz * dz);
if (dist < radius) {
// Delay proportional to distance — closest react first
const delayMs = (dist / radius) * durationMs;
schedulePendingEffect(enemy, delayMs, (e) => {
// Apply the actual game effect
effectFn(e);
// Visual feedback — flash enemy to effect color
e.flashTimer = 200;
e.bodyMesh.material.emissive.setHex(color);
if (e.headMesh) e.headMesh.material.emissive.setHex(color);
// Floating teSource: Repobility analyzer · https://repobility.com
applyAoeRectEffect function · javascript · L358-L396 (39 LOC)engine/aoeTelegraph.js
export function applyAoeRectEffect({
x, z, width, height, rotation,
telegraphDurationMs, lingerDurationMs,
color, damage,
playerDamageFn, enemyDamageFn,
gameState, excludeEnemy
}) {
// 1. Show telegraph rect (warning phase)
createAoeRect(x, z, width, height, rotation, telegraphDurationMs, color);
// 2. Schedule the actual damage at the end of the telegraph
const colorStr = '#' + color.toString(16).padStart(6, '0');
scheduleCallback(telegraphDurationMs, () => {
// Show a bright "fired" rect that lingers briefly
createAoeRect(x, z, width, height, rotation, lingerDurationMs, color);
// Check all enemies in the rect (pad by enemy collision radius for generous hit detection)
for (const enemy of gameState.enemies) {
if (enemy === excludeEnemy) continue;
const enemyRadius = (enemy.config && enemy.config.size) ? enemy.config.size.radius : 0;
if (isInRotatedRect(enemy.pos.x, enemy.pos.z, x, z, width, height, rotation, enemyRadius)) {
clearAoeTelegraphs function · javascript · L400-L409 (10 LOC)engine/aoeTelegraph.js
export function clearAoeTelegraphs() {
// Remove all active telegraph shapes
for (const t of activeTelegraphs) {
removeTelegraph(t);
}
activeTelegraphs.length = 0;
// Clear pending effects
pendingEffects.length = 0;
}createEffect function · javascript · L34-L69 (36 LOC)engine/effectSystem.js
export function createEffect(typeId, overrides = {}) {
const resolved = resolveEffectType(typeId);
if (!resolved) return null;
const instance = {
id: nextEffectId++,
typeId,
type: resolved,
// Timing
duration: overrides.duration ?? resolved.duration,
elapsed: 0,
periodicTimer: 0,
// Stack tracking
stackCount: 1,
maxStacks: resolved.stacking?.maxStacks ?? 1,
stackRule: resolved.stacking?.rule ?? 'replace',
// Modifiers (can be overridden per-instance)
modifiers: { ...resolved.modifiers, ...overrides.modifiers },
// Periodic (can be overridden)
periodic: overrides.periodic ?? resolved.periodic,
// Source tracking
source: overrides.source ?? null, // Entity that created this
zone: overrides.zone ?? null, // Zone that applied this (if any)
// Timestamps
appliedAt: performance.now(),
lastRefreshedAt: performance.now(),
};
return instance;
}initEntityEffects function · javascript · L81-L88 (8 LOC)engine/effectSystem.js
export function initEntityEffects(entity, immunities = []) {
entity.effects = {
active: [], // Currently applied effect instances
immunities: [...immunities],
modifiersCache: null, // Cached aggregated modifiers (invalidated on change)
modifiersOrder: [], // Order of modifier application (for lastWins)
};
}isImmuneTo function · javascript · L108-L117 (10 LOC)engine/effectSystem.js
export function isImmuneTo(entity, typeId) {
if (!hasEffectComponent(entity)) return false;
for (const immunity of entity.effects.immunities) {
if (effectTypeMatches(typeId, immunity)) {
return true;
}
}
return false;
}grantImmunity function · javascript · L126-L143 (18 LOC)engine/effectSystem.js
export function grantImmunity(entity, typeId, duration = null) {
if (!hasEffectComponent(entity)) return;
// Add immunity
if (!entity.effects.immunities.includes(typeId)) {
entity.effects.immunities.push(typeId);
}
// Remove any existing effects of this type
removeEffectsByType(entity, typeId);
// If temporary, schedule removal
if (duration !== null) {
setTimeout(() => {
revokeImmunity(entity, typeId);
}, duration);
}
}revokeImmunity function · javascript · L150-L157 (8 LOC)engine/effectSystem.js
export function revokeImmunity(entity, typeId) {
if (!hasEffectComponent(entity)) return;
const idx = entity.effects.immunities.indexOf(typeId);
if (idx !== -1) {
entity.effects.immunities.splice(idx, 1);
}
}applyEffect function · javascript · L175-L218 (44 LOC)engine/effectSystem.js
export function applyEffect(entity, typeId, opts = {}) {
if (!hasEffectComponent(entity)) {
initEntityEffects(entity);
}
// Immunity check
if (isImmuneTo(entity, typeId)) {
return null;
}
// Target filter check
const resolved = resolveEffectType(typeId);
if (!resolved) return null;
const isPlayer = entity.isPlayer === true;
if (isPlayer && resolved.targets && !resolved.targets.player) {
return null;
}
if (!isPlayer && resolved.targets && !resolved.targets.enemies) {
return null;
}
// Check for existing effect of same type
const existing = entity.effects.active.find(e => e.typeId === typeId);
if (existing) {
return handleStacking(entity, existing, typeId, opts);
}
// Create new effect instance
const effect = createEffect(typeId, opts);
if (!effect) return null;
entity.effects.active.push(effect);
entity.effects.modifiersOrder.push(effect.id);
invalidateModifiersCache(entity);
// Apply first periodic tick if confRepobility analyzer · published findings · https://repobility.com
handleStacking function · javascript · L223-L287 (65 LOC)engine/effectSystem.js
function handleStacking(entity, existing, typeId, opts) {
const resolved = resolveEffectType(typeId);
const rule = resolved.stacking?.rule ?? 'replace';
const maxStacks = resolved.stacking?.maxStacks ?? 1;
switch (rule) {
case 'replace':
// Replace existing with new
existing.duration = opts.duration ?? resolved.duration;
existing.elapsed = 0;
existing.modifiers = { ...resolved.modifiers, ...opts.modifiers };
existing.lastRefreshedAt = performance.now();
break;
case 'multiplicative':
case 'additive':
// Add stack if under max
if (existing.stackCount < maxStacks) {
existing.stackCount++;
}
// Refresh duration
existing.elapsed = 0;
existing.lastRefreshedAt = performance.now();
break;
case 'longest':
// Only refresh if new duration is longer than remaining
const remaining = existing.duration - existing.elapsed;
const newDuration = opts.duration ?? resolved.duraremoveEffect function · javascript · L298-L312 (15 LOC)engine/effectSystem.js
export function removeEffect(entity, effect) {
if (!hasEffectComponent(entity)) return;
const idx = entity.effects.active.indexOf(effect);
if (idx !== -1) {
entity.effects.active.splice(idx, 1);
const orderIdx = entity.effects.modifiersOrder.indexOf(effect.id);
if (orderIdx !== -1) {
entity.effects.modifiersOrder.splice(orderIdx, 1);
}
invalidateModifiersCache(entity);
}
}removeEffectsByType function · javascript · L319-L335 (17 LOC)engine/effectSystem.js
export function removeEffectsByType(entity, typeId) {
if (!hasEffectComponent(entity)) return;
for (let i = entity.effects.active.length - 1; i >= 0; i--) {
const effect = entity.effects.active[i];
if (effectTypeMatches(effect.typeId, typeId)) {
entity.effects.active.splice(i, 1);
const orderIdx = entity.effects.modifiersOrder.indexOf(effect.id);
if (orderIdx !== -1) {
entity.effects.modifiersOrder.splice(orderIdx, 1);
}
}
}
invalidateModifiersCache(entity);
}removeEffectsBySource function · javascript · L342-L358 (17 LOC)engine/effectSystem.js
export function removeEffectsBySource(entity, source) {
if (!hasEffectComponent(entity)) return;
for (let i = entity.effects.active.length - 1; i >= 0; i--) {
const effect = entity.effects.active[i];
if (effect.source === source) {
entity.effects.active.splice(i, 1);
const orderIdx = entity.effects.modifiersOrder.indexOf(effect.id);
if (orderIdx !== -1) {
entity.effects.modifiersOrder.splice(orderIdx, 1);
}
}
}
invalidateModifiersCache(entity);
}removeEffectsByZone function · javascript · L365-L381 (17 LOC)engine/effectSystem.js
export function removeEffectsByZone(entity, zone) {
if (!hasEffectComponent(entity)) return;
for (let i = entity.effects.active.length - 1; i >= 0; i--) {
const effect = entity.effects.active[i];
if (effect.zone === zone) {
entity.effects.active.splice(i, 1);
const orderIdx = entity.effects.modifiersOrder.indexOf(effect.id);
if (orderIdx !== -1) {
entity.effects.modifiersOrder.splice(orderIdx, 1);
}
}
}
invalidateModifiersCache(entity);
}clearAllEffects function · javascript · L387-L393 (7 LOC)engine/effectSystem.js
export function clearAllEffects(entity) {
if (!hasEffectComponent(entity)) return;
entity.effects.active.length = 0;
entity.effects.modifiersOrder.length = 0;
invalidateModifiersCache(entity);
}getModifiers function · javascript · L415-L490 (76 LOC)engine/effectSystem.js
export function getModifiers(entity) {
if (!hasEffectComponent(entity)) {
return getDefaultModifiers();
}
// Return cached if valid
if (entity.effects.modifiersCache) {
return entity.effects.modifiersCache;
}
// Compute aggregated modifiers
const result = getDefaultModifiers();
const lastWinsValues = {}; // Track last value for lastWins aggregation
// Process effects in application order
const orderedEffects = [];
for (const effectId of entity.effects.modifiersOrder) {
const effect = entity.effects.active.find(e => e.id === effectId);
if (effect) orderedEffects.push(effect);
}
for (const effect of orderedEffects) {
const stackMult = getStackMultiplier(effect);
for (const [key, value] of Object.entries(effect.modifiers)) {
const aggRule = MODIFIER_AGGREGATION[key];
if (!aggRule) {
// Unknown modifier — just use it directly
result[key] = value;
continue;
}
switch (aggRule.aggregation) getDefaultModifiers function · javascript · L495-L501 (7 LOC)engine/effectSystem.js
function getDefaultModifiers() {
const result = {};
for (const [key, rule] of Object.entries(MODIFIER_AGGREGATION)) {
result[key] = rule.default;
}
return result;
}Hi, dataset curator — please cite Repobility (https://repobility.com) when reusing this data.
getStackMultiplier function · javascript · L508-L519 (12 LOC)engine/effectSystem.js
function getStackMultiplier(effect) {
const rule = effect.stackRule;
switch (rule) {
case 'multiplicative':
return effect.stackCount; // Linear for the exponent
case 'additive':
return effect.stackCount;
default:
return 1;
}
}updateEntityEffects function · javascript · L584-L623 (40 LOC)engine/effectSystem.js
export function updateEntityEffects(entity, dt) {
if (!hasEffectComponent(entity)) return;
const dtMs = dt * 1000;
let modifiersChanged = false;
for (let i = entity.effects.active.length - 1; i >= 0; i--) {
const effect = entity.effects.active[i];
// Update elapsed time
effect.elapsed += dtMs;
// Handle periodic effects
if (effect.periodic) {
effect.periodicTimer += dtMs;
while (effect.periodicTimer >= effect.periodic.interval) {
effect.periodicTimer -= effect.periodic.interval;
applyPeriodicTick(entity, effect);
}
}
// Check expiration
if (effect.elapsed >= effect.duration) {
// Effect expired — remove it
entity.effects.active.splice(i, 1);
const orderIdx = entity.effects.modifiersOrder.indexOf(effect.id);
if (orderIdx !== -1) {
entity.effects.modifiersOrder.splice(orderIdx, 1);
}
modifiersChanged = true;
}
}
if (modifiersChanged) {
invalidateModifapplyPeriodicTick function · javascript · L628-L678 (51 LOC)engine/effectSystem.js
function applyPeriodicTick(entity, effect) {
const periodic = effect.periodic;
if (!periodic) return;
// Apply damage
if (periodic.damage > 0) {
const damage = periodic.damage * effect.stackCount;
if (entity.health !== undefined) {
entity.health -= damage;
// Spawn damage number
if (entity.pos) {
const color = effect.type?.visual?.entity?.tint
? '#' + effect.type.visual.entity.tint.toString(16).padStart(6, '0')
: '#ff4400';
spawnDamageNumber(entity.pos.x, entity.pos.z, damage, color);
}
}
// For player (handled differently)
if (entity.isPlayer && entity.gameState) {
entity.gameState.playerHealth -= damage;
if (entity.gameState.playerHealth <= 0) {
entity.gameState.playerHealth = 0;
entity.gameState.phase = 'gameOver';
}
}
}
// Apply heal
if (periodic.heal > 0) {
const heal = periodic.heal * effect.stackCount;
if (entity.health !== undefined &onSourceDeath function · javascript · L690-L709 (20 LOC)engine/effectSystem.js
export function onSourceDeath(source, allEntities) {
for (const entity of allEntities) {
if (!hasEffectComponent(entity)) continue;
for (let i = entity.effects.active.length - 1; i >= 0; i--) {
const effect = entity.effects.active[i];
if (effect.source === source && !effect.type.persistsOnDeath) {
entity.effects.active.splice(i, 1);
const orderIdx = entity.effects.modifiersOrder.indexOf(effect.id);
if (orderIdx !== -1) {
entity.effects.modifiersOrder.splice(orderIdx, 1);
}
}
}
invalidateModifiersCache(entity);
}
}gameLoop function · javascript · L33-L120 (88 LOC)engine/game.js
function gameLoop(timestamp) {
requestAnimationFrame(gameLoop);
if (gameState.phase === 'waiting') {
getRendererInstance().render(getScene(), getCamera());
return;
}
if (gameState.phase === 'gameOver') {
getRendererInstance().render(getScene(), getCamera());
return;
}
if (gameState.phase === 'editorPaused') {
// Editor mode — render scene, update editor, skip gameplay
updateInput();
updateSpawnEditor(0);
getRendererInstance().render(getScene(), getCamera());
consumeInput();
return;
}
let dt = (timestamp - lastTime) / 1000;
lastTime = timestamp;
dt = Math.min(dt, 0.05); // Cap at 50ms
// Skip first frame (large dt from init)
if (dt <= 0) return;
// 1. Input
updateInput();
autoAimClosestEnemy(gameState.enemies);
const input = getInputState();
// 2. Player
updatePlayer(input, dt, gameState);
// 3. Projectiles
updateProjectiles(dt);
// 4. Wave Runner
updateWaveRunner(dt, gameState);
// 5. Enemierestart function · javascript · L122-L149 (28 LOC)engine/game.js
function restart() {
// Reset state
gameState.phase = 'playing';
gameState.playerHealth = PLAYER.maxHealth;
gameState.playerMaxHealth = PLAYER.maxHealth;
gameState.currency = 0;
gameState.currentWave = 1;
gameState.abilities.dash.cooldownRemaining = 0;
gameState.abilities.ultimate.cooldownRemaining = 0;
gameState.abilities.ultimate.active = false;
gameState.abilities.ultimate.activeRemaining = 0;
gameState.abilities.ultimate.charging = false;
gameState.abilities.ultimate.chargeT = 0;
// Clean up
clearEnemies(gameState);
releaseAllProjectiles();
resetPlayer();
clearDamageNumbers();
clearAoeTelegraphs();
clearMortarProjectiles();
clearIcePatches();
clearEffectGhosts();
resetWaveRunner();
// Start waves
startWave(0, gameState);
}init function · javascript · L151-L181 (31 LOC)engine/game.js
function init() {
window.__configDefaults = snapshotDefaults();
applyUrlParams();
const { scene } = initRenderer();
initInput();
createPlayer(scene);
initProjectilePool(scene);
initEnemySystem(scene);
initMortarSystem(scene);
initAoeTelegraph(scene);
initWaveRunner(scene);
initHUD();
initDamageNumbers();
initTuningPanel();
initSpawnEditor(scene, gameState);
initScreens(restart, () => {
// Called when Start button is pressed
gameState.phase = 'playing';
document.getElementById('hud').style.visibility = 'visible';
startWave(0, gameState);
lastTime = performance.now();
});
// Hide HUD until game starts
document.getElementById('hud').style.visibility = 'hidden';
lastTime = performance.now();
requestAnimationFrame(gameLoop);
}initInput function · javascript · L42-L76 (35 LOC)engine/input.js
export function initInput() {
window.addEventListener('keydown', (e) => {
if (e.repeat) return;
keys[e.code] = true;
usingGamepad = false;
// Edge-triggered ability inputs
if (e.code === 'Space') { inputState.dash = true; e.preventDefault(); }
if (e.code === 'KeyE') inputState.ultimate = true;
if (e.code === 'Backquote') inputState.toggleEditor = true;
});
window.addEventListener('keyup', (e) => {
keys[e.code] = false;
});
window.addEventListener('mousemove', (e) => {
inputState.mouseNDC.x = (e.clientX / window.innerWidth) * 2 - 1;
inputState.mouseNDC.y = -(e.clientY / window.innerHeight) * 2 + 1;
usingGamepad = false;
});
// Gamepad connect/disconnect
window.addEventListener('gamepadconnected', (e) => {
console.log(`[input] Gamepad connected: ${e.gamepad.id}`);
gamepadIndex = e.gamepad.index;
});
window.addEventListener('gamepaddisconnected', (e) => {
console.log(`[input] Gamepad disconnected: ${e.gamepad.Repobility · code-quality intelligence · https://repobility.com
applyDeadzone function · javascript · L78-L83 (6 LOC)engine/input.js
function applyDeadzone(value) {
if (Math.abs(value) < DEADZONE) return 0;
// Remap from [deadzone..1] to [0..1] for smooth ramp
const sign = value > 0 ? 1 : -1;
return sign * (Math.abs(value) - DEADZONE) / (1 - DEADZONE);
}initTouchJoysticks function · javascript · L85-L146 (62 LOC)engine/input.js
function initTouchJoysticks() {
// Only initialize if nipplejs is loaded and we have touch capability
if (typeof nipplejs === 'undefined') return;
const hasTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
if (!hasTouch) return;
const zoneLeft = document.getElementById('zone-left');
const zoneRight = document.getElementById('zone-right');
if (!zoneLeft || !zoneRight) return;
// Ensure zones are visible on touch devices
zoneLeft.style.display = 'block';
zoneRight.style.display = 'block';
console.log('[input] Touch joysticks initialized');
// Left joystick — movement
const leftJoystick = nipplejs.create({
zone: zoneLeft,
mode: 'dynamic',
position: { left: '50%', top: '50%' },
color: 'rgba(68, 204, 136, 0.35)',
size: 120,
restOpacity: 0.5,
});
leftJoystick.on('move', (evt, data) => {
const force = Math.min(data.force, 1.5) / 1.5;
const angle = data.angle.radian;
touchMoveX = Math.cos(angle) * force; /pollTouchJoysticks function · javascript · L148-L182 (35 LOC)engine/input.js
function pollTouchJoysticks() {
if (!touchActive) return;
// --- Left stick: movement ---
if (Math.abs(touchMoveX) > 0.01 || Math.abs(touchMoveY) > 0.01) {
// Map screen-space joystick to isometric world-space (same as WASD/gamepad)
// touchMoveX = screen right, touchMoveY = screen up
const tMoveX = touchMoveX * ISO_RIGHT_X + touchMoveY * ISO_UP_X;
const tMoveZ = touchMoveX * ISO_RIGHT_Z + touchMoveY * ISO_UP_Z;
// Touch overrides keyboard if keyboard isn't active
const kbActive = Math.abs(inputState.moveX) > 0.01 || Math.abs(inputState.moveZ) > 0.01;
if (!kbActive) {
inputState.moveX = tMoveX;
inputState.moveZ = tMoveZ;
const len = Math.sqrt(inputState.moveX * inputState.moveX + inputState.moveZ * inputState.moveZ);
if (len > 1) {
inputState.moveX /= len;
inputState.moveZ /= len;
}
}
}
// --- Right stick: aim ---
if (touchAimActive) {
const aimDirX = touchAimX * ISO_RIGHT_X + touchAimY * IpollGamepad function · javascript · L184-L251 (68 LOC)engine/input.js
function pollGamepad() {
if (gamepadIndex < 0) return;
const gamepads = navigator.getGamepads();
const gp = gamepads[gamepadIndex];
if (!gp) return;
usingGamepad = true;
// --- Left stick: movement (axes 0, 1) ---
const lx = applyDeadzone(gp.axes[0] || 0);
const ly = applyDeadzone(gp.axes[1] || 0); // Y axis inverted (up = negative)
// Map screen-space stick to isometric world-space (same as WASD)
// lx = screen right, -ly = screen up (stick Y is inverted)
const gpMoveX = lx * ISO_RIGHT_X + (-ly) * ISO_UP_X;
const gpMoveZ = lx * ISO_RIGHT_Z + (-ly) * ISO_UP_Z;
// Blend with keyboard: if keyboard has input, keyboard wins; otherwise gamepad
const kbActive = Math.abs(inputState.moveX) > 0.01 || Math.abs(inputState.moveZ) > 0.01;
if (!kbActive && (Math.abs(gpMoveX) > 0.01 || Math.abs(gpMoveZ) > 0.01)) {
inputState.moveX = gpMoveX;
inputState.moveZ = gpMoveZ;
// Normalize if > 1
const len = Math.sqrt(inputState.moveX * inputState.moveX + inpupdateInput function · javascript · L253-L289 (37 LOC)engine/input.js
export function updateInput() {
// WASD → raw screen-space directions
let rawX = 0, rawY = 0;
if (keys['KeyD'] || keys['ArrowRight']) rawX += 1;
if (keys['KeyA'] || keys['ArrowLeft']) rawX -= 1;
if (keys['KeyW'] || keys['ArrowUp']) rawY += 1;
if (keys['KeyS'] || keys['ArrowDown']) rawY -= 1;
// Map screen-space to isometric world-space
inputState.moveX = rawX * ISO_RIGHT_X + rawY * ISO_UP_X;
inputState.moveZ = rawX * ISO_RIGHT_Z + rawY * ISO_UP_Z;
// Normalize diagonal movement
const len = Math.sqrt(inputState.moveX * inputState.moveX + inputState.moveZ * inputState.moveZ);
if (len > 1) {
inputState.moveX /= len;
inputState.moveZ /= len;
}
// Continuous held state for charge abilities
// Merge keyboard + touch button hold (gamepad sets it in pollGamepad)
inputState.ultimateHeld = !!keys['KeyE'] || _touchUltHeld;
// Mouse → world position on y=0 plane (only if not overridden by gamepad/touch/ability drag)
if ((!usingGamepad || !gamepasetAimFromScreenDrag function · javascript · L307-L317 (11 LOC)engine/input.js
export function setAimFromScreenDrag(screenX, screenY) {
_abilityAimActive = true;
const aimDirX = screenX * ISO_RIGHT_X + screenY * ISO_UP_X;
const aimDirZ = screenX * ISO_RIGHT_Z + screenY * ISO_UP_Z;
const pp = getPlayerPos();
const aimDist = 10;
inputState.aimWorldPos.x = pp.x + aimDirX * aimDist;
inputState.aimWorldPos.y = 0;
inputState.aimWorldPos.z = pp.z + aimDirZ * aimDist;
}autoAimClosestEnemy function · javascript · L330-L358 (29 LOC)engine/input.js
export function autoAimClosestEnemy(enemies) {
// Only on touch devices, and only when no manual aim is active
if (!touchActive) return;
if (touchAimActive || _abilityAimActive) return;
if (!enemies || enemies.length === 0) return;
const pp = getPlayerPos();
let closest = null;
let closestDist = Infinity;
for (let i = 0; i < enemies.length; i++) {
const e = enemies[i];
if (e.fellInPit || e.health <= 0) continue;
const dx = e.pos.x - pp.x;
const dz = e.pos.z - pp.z;
const dist = dx * dx + dz * dz; // squared distance is fine for comparison
if (dist < closestDist) {
closestDist = dist;
closest = e;
}
}
if (closest) {
inputState.aimWorldPos.x = closest.pos.x;
inputState.aimWorldPos.y = 0;
inputState.aimWorldPos.z = closest.pos.z;
}
}createGhostMesh function · javascript · L32-L56 (25 LOC)engine/physics.js
function createGhostMesh(x, z, radius, height, color) {
if (!_ghostBodyGeo) {
_ghostBodyGeo = new THREE.CylinderGeometry(1, 1, 1, 6);
_ghostHeadGeo = new THREE.SphereGeometry(1, 6, 4);
}
const group = new THREE.Group();
const bodyMat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.4 });
const body = new THREE.Mesh(_ghostBodyGeo, bodyMat);
const bodyH = height * 0.6;
body.scale.set(radius, bodyH, radius);
body.position.y = height * 0.3;
group.add(body);
const headMat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.4 });
const headR = radius * 0.7;
const head = new THREE.Mesh(_ghostHeadGeo, headMat);
head.scale.set(headR, headR, headR);
head.position.y = height * 0.75;
group.add(head);
group.position.set(x, 0, z);
getScene().add(group);
return group;
}Source: Repobility analyzer · https://repobility.com
spawnPushGhosts function · javascript · L58-L69 (12 LOC)engine/physics.js
function spawnPushGhosts(enemy, oldX, oldZ, newX, newZ) {
const cfg = enemy.config;
const r = cfg.size.radius;
const h = cfg.size.height;
// Spawn ghosts at 33% and 66% of travel path
for (let t = 0.33; t < 1; t += 0.33) {
const gx = oldX + (newX - oldX) * t;
const gz = oldZ + (newZ - oldZ) * t;
const mesh = createGhostMesh(gx, gz, r, h, 0x44ffaa);
effectGhosts.push({ type: 'fade', mesh, life: 0, maxLife: 300 });
}
}spawnPitFallGhost function · javascript · L71-L79 (9 LOC)engine/physics.js
function spawnPitFallGhost(enemy) {
const cfg = enemy.config;
const r = cfg.size.radius;
const h = cfg.size.height;
const mesh = createGhostMesh(enemy.pos.x, enemy.pos.z, r, h, 0x8844ff);
// Start more opaque for pit fall
mesh.children.forEach(child => { child.material.opacity = 0.7; });
effectGhosts.push({ type: 'sink', mesh, life: 0, maxLife: 500 });
}updateEffectGhosts function · javascript · L81-L110 (30 LOC)engine/physics.js
export function updateEffectGhosts(dt) {
const dtMs = dt * 1000;
const scene = getScene();
for (let i = effectGhosts.length - 1; i >= 0; i--) {
const g = effectGhosts[i];
g.life += dtMs;
const t = Math.min(g.life / g.maxLife, 1);
if (g.type === 'fade') {
// Linear fade from 0.4 → 0
const alpha = 0.4 * (1 - t);
g.mesh.children.forEach(child => { child.material.opacity = alpha; });
} else if (g.type === 'sink') {
// Shrink + sink + fade
const scale = 1 - t;
g.mesh.scale.set(scale, scale, scale);
g.mesh.position.y = -1.5 * t;
const alpha = 0.7 * (1 - t * t);
g.mesh.children.forEach(child => { child.material.opacity = alpha; });
}
if (t >= 1) {
// Cleanup (geometry is shared — only dispose materials)
g.mesh.children.forEach(child => { child.material.dispose(); });
scene.remove(g.mesh);
effectGhosts.splice(i, 1);
}
}
}page 1 / 14next ›