AdaptiveTactics / js /rules.js
EphAsad's picture
Upload 61 files
076c3cb verified
/**
* Adaptive Tactics - Game Rules
* Movement, attack range, hit/damage calculations, terrain modifiers
*/
import { MapData } from './models.js';
/**
* Calculate movement range for a unit
* Returns array of [row, col] positions the unit can move to
*/
export function calculateMoveRange(unit, map, allUnits) {
const reachable = [];
const visited = new Map();
const queue = [[unit.row, unit.col, unit.mov]];
visited.set(`${unit.row},${unit.col}`, unit.mov);
while (queue.length > 0) {
const [row, col, remaining] = queue.shift();
// Add to reachable if not starting position
if (row !== unit.row || col !== unit.col) {
const tile = map.getTile(row, col);
// Can only move to unoccupied tiles
if (tile && !tile.isOccupied()) {
reachable.push([row, col]);
}
}
if (remaining <= 0) continue;
// Check adjacent tiles
const directions = [[-1, 0], [1, 0], [0, -1], [0, 1]];
for (const [dr, dc] of directions) {
const newRow = row + dr;
const newCol = col + dc;
const key = `${newRow},${newCol}`;
const tile = map.getTile(newRow, newCol);
if (!tile || !tile.isPassable()) continue;
// Can pass through allied units but not enemies
const occupant = tile.unit;
if (occupant && occupant.faction !== unit.faction) continue;
const newRemaining = remaining - 1;
const prevRemaining = visited.get(key) ?? -1;
if (newRemaining > prevRemaining) {
visited.set(key, newRemaining);
queue.push([newRow, newCol, newRemaining]);
}
}
}
return reachable;
}
/**
* Calculate attack range from a position with a weapon
* Returns array of [row, col] positions that can be attacked
*/
export function calculateAttackRange(row, col, weapon, map) {
const targets = [];
if (!weapon) return targets;
for (let r = 1; r <= map.rows; r++) {
for (let c = 1; c <= map.cols; c++) {
const dist = MapData.getDistance(row, col, r, c);
if (dist >= weapon.minRange && dist <= weapon.maxRange) {
const tile = map.getTile(r, c);
if (tile && tile.isPassable()) {
targets.push([r, c]);
}
}
}
}
return targets;
}
/**
* Calculate heal range from a position with a healing weapon
* Returns array of [row, col] positions that can be healed
*/
export function calculateHealRange(row, col, weapon, map, allUnits, faction) {
const targets = [];
if (!weapon || !weapon.isHealingWeapon()) return targets;
for (let r = 1; r <= map.rows; r++) {
for (let c = 1; c <= map.cols; c++) {
const dist = MapData.getDistance(row, col, r, c);
if (dist >= weapon.minRange && dist <= weapon.maxRange) {
const tile = map.getTile(r, c);
if (tile && tile.unit && tile.unit.faction === faction && tile.unit.hp < tile.unit.maxHp) {
targets.push([r, c]);
}
}
}
}
return targets;
}
/**
* Calculate displayed hit chance
* Formula: weapon_hit + skill × 2
*/
export function calculateDisplayedHit(attacker, weapon) {
return weapon.hit + (attacker.skill * 2);
}
/**
* Calculate displayed avoid
* Formula: speed × 2 + terrain_avoid
*/
export function calculateDisplayedAvoid(defender, terrainAvoid = 0) {
return (defender.speed * 2) + terrainAvoid;
}
/**
* Calculate actual hit chance
* Formula: displayed_hit - displayed_avoid
*/
export function calculateHitChance(attacker, defender, weapon, terrainAvoid = 0) {
const hit = calculateDisplayedHit(attacker, weapon);
const avoid = calculateDisplayedAvoid(defender, terrainAvoid);
return Math.max(0, Math.min(100, hit - avoid));
}
/**
* 2RN hit check (average of two random rolls)
* This makes displayed hit rates feel more accurate
*/
export function rollHit(hitChance) {
const roll1 = Math.random() * 100;
const roll2 = Math.random() * 100;
const avgRoll = (roll1 + roll2) / 2;
return avgRoll < hitChance;
}
/**
* Calculate damage
* Physical: power + weapon_might - defender_def
* Magic: power + weapon_might - defender_res
*/
export function calculateDamage(attacker, defender, weapon, terrainDef = 0) {
const might = attacker.power + weapon.might;
let defense;
if (weapon.kind === 'magic') {
defense = defender.res;
} else {
defense = defender.def + terrainDef;
}
return Math.max(0, might - defense);
}
/**
* Check if attacker can double (speed gap >= 4)
*/
export function canDouble(attacker, defender, speedGap = 4) {
return attacker.speed >= defender.speed + speedGap;
}
/**
* Check if defender can counterattack
* Defender can counter if their equipped weapon can reach the attacker's range
*/
export function canCounter(attacker, defender, distance) {
const defenderWeapon = defender.getEquippedWeapon();
if (!defenderWeapon) return false;
if (defenderWeapon.isHealingWeapon()) return false;
return defenderWeapon.canReach(distance);
}
/**
* Get terrain avoid bonus for a tile
*/
export function getTerrainAvoid(tile) {
if (!tile) return 0;
return tile.getAvoidBonus();
}
/**
* Get terrain defense bonus for a tile
*/
export function getTerrainDef(tile) {
if (!tile) return 0;
return tile.getDefBonus();
}
/**
* Calculate full combat preview
*/
export function calculateCombatPreview(attacker, defender, map, gameRules) {
const attackerWeapon = attacker.getEquippedWeapon();
const defenderWeapon = defender.getEquippedWeapon();
const distance = MapData.getDistance(attacker.row, attacker.col, defender.row, defender.col);
const defenderTile = map.getTile(defender.row, defender.col);
const terrainAvoid = getTerrainAvoid(defenderTile);
const terrainDef = getTerrainDef(defenderTile);
const speedGap = gameRules?.double_speed_gap ?? 4;
const preview = {
attacker: {
name: attacker.name,
hp: attacker.hp,
damage: 0,
hit: 0,
doubles: false
},
defender: {
name: defender.name,
hp: defender.hp,
damage: 0,
hit: 0,
doubles: false,
canCounter: false
}
};
// Attacker stats
if (attackerWeapon) {
preview.attacker.damage = calculateDamage(attacker, defender, attackerWeapon, terrainDef);
preview.attacker.hit = calculateHitChance(attacker, defender, attackerWeapon, terrainAvoid);
preview.attacker.doubles = canDouble(attacker, defender, speedGap);
}
// Defender counter stats
const defCanCounter = canCounter(attacker, defender, distance);
preview.defender.canCounter = defCanCounter;
if (defCanCounter && defenderWeapon) {
const attackerTile = map.getTile(attacker.row, attacker.col);
const attackerTerrainAvoid = getTerrainAvoid(attackerTile);
const attackerTerrainDef = getTerrainDef(attackerTile);
preview.defender.damage = calculateDamage(defender, attacker, defenderWeapon, attackerTerrainDef);
preview.defender.hit = calculateHitChance(defender, attacker, defenderWeapon, attackerTerrainAvoid);
preview.defender.doubles = canDouble(defender, attacker, speedGap);
}
return preview;
}