← back to jjzh__Iso_Shooter

Function bodies 653 total

All specs Real LLM only Function bodies
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, borderMat
isInRotatedRect 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 te
Source: 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 conf
Repobility 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.dura
removeEffect 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) {
    invalidateModif
applyPeriodicTick 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. Enemie
restart 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 * I
pollGamepad 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 + inp
updateInput 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 || !gamepa
setAimFromScreenDrag 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 ›