diff --git a/assets/images/ta/arm-structures.png b/assets/images/ta/arm-structures.png index 6f9c80a..2aba0e3 100644 Binary files a/assets/images/ta/arm-structures.png and b/assets/images/ta/arm-structures.png differ diff --git a/assets/images/ta/arm-structures.psd b/assets/images/ta/arm-structures.psd index 2315de0..2b30ec6 100644 Binary files a/assets/images/ta/arm-structures.psd and b/assets/images/ta/arm-structures.psd differ diff --git a/assets/images/ta/arm-units.png b/assets/images/ta/arm-units.png index e957512..3783add 100644 Binary files a/assets/images/ta/arm-units.png and b/assets/images/ta/arm-units.png differ diff --git a/assets/images/ta/arm-units.psd b/assets/images/ta/arm-units.psd index af31e4d..dc75469 100644 Binary files a/assets/images/ta/arm-units.psd and b/assets/images/ta/arm-units.psd differ diff --git a/assets/images/ta/core-structures.png b/assets/images/ta/core-structures.png index baf2bff..034fc34 100644 Binary files a/assets/images/ta/core-structures.png and b/assets/images/ta/core-structures.png differ diff --git a/assets/images/ta/core-structures.psd b/assets/images/ta/core-structures.psd index 9750a4f..3cce1e0 100644 Binary files a/assets/images/ta/core-structures.psd and b/assets/images/ta/core-structures.psd differ diff --git a/assets/images/ta/core-units.png b/assets/images/ta/core-units.png index b874fb7..f068234 100644 Binary files a/assets/images/ta/core-units.png and b/assets/images/ta/core-units.png differ diff --git a/assets/images/ta/core-units.psd b/assets/images/ta/core-units.psd index 990a9a7..2e8acc1 100644 Binary files a/assets/images/ta/core-units.psd and b/assets/images/ta/core-units.psd differ diff --git a/src/games/totalannihilation/TALogic.js b/src/games/totalannihilation/TALogic.js index 9547b95..b9eaee7 100644 --- a/src/games/totalannihilation/TALogic.js +++ b/src/games/totalannihilation/TALogic.js @@ -710,6 +710,36 @@ function orderDestination(e, order) { return { x: order.sx ?? order.x, y: order.sy ?? order.y }; } +/** Auto-heal priority: lower tier heals first — defensive > factory > other building > unit. */ +function healPriorityTier(def) { + if (!def.isBuilding) return 3; + if (def.weapons?.length) return 0; + if (def.builds?.length) return 1; + return 2; +} + +/** + * Best in-range damaged ally for `e` to auto-repair, or null. Same reach math as a player's + * own repair order (`buildRange` + target radius) — a site under construction is `assist`'s + * job, not this one, so it's excluded even though its partial hp technically reads as damaged. + */ +function findAutoHealTarget(state, rules, e, def) { + const range = def.buildRange ?? 0; + let best = null, bestTier = Infinity, bestDSq = Infinity; + for (const t of state.entities) { + if (t.dead || t.id === e.id || t.army !== e.army || t.site) continue; + if (t.hp >= t.maxHp) continue; + const reach = range + t.radius; + const dSq = (t.x - e.x) ** 2 + (t.y - e.y) ** 2; + if (dSq > reach * reach) continue; + const tier = healPriorityTier(defOf(rules, t)); + if (tier < bestTier || (tier === bestTier && dSq < bestDSq)) { + best = t; bestTier = tier; bestDSq = dSq; + } + } + return best; +} + function stepOrders(state, rules) { const ts = state.tileSize; for (const e of state.entities) { @@ -721,6 +751,14 @@ function stepOrders(state, rules) { if (!order) { e.movingTo = null; e.buildTargetId = 0; + // Auto-heal: an idle builder with something damaged in range starts repairing it on its + // own, no order needed. `auto: true` marks it as self-assigned rather than player-issued, + // so a real order the player gives afterward — pushOrder always replaces e.orders wholesale + // when not queuing — simply overwrites it like any other order, never fights it. + if (def.buildPower > 0) { + const target = findAutoHealTarget(state, rules, e, def); + if (target) e.orders.push({ type: 'repair', targetId: target.id, auto: true }); + } continue; } @@ -732,6 +770,24 @@ function stepOrders(state, rules) { e.buildTargetId = 0; } + // Auto-heal keeps re-prioritising while it works, on the same staggered cadence combat + // uses for target acquisition — a defensive building taking fire should pull a builder off + // a generator it's already patching, but two similarly-urgent targets shouldn't make it + // flicker between them every time this runs. + if (order.type === 'repair' && order.auto && def.buildPower > 0) { + const due = ((state.tick + e.id) % rules.constants.retargetPeriodTicks) === 0; + if (due) { + const current = entityById(state, order.targetId); + const currentValid = !!current && !current.site && current.hp < current.maxHp + && Math.hypot(current.x - e.x, current.y - e.y) <= (def.buildRange ?? 0) + current.radius; + const currentTier = currentValid ? healPriorityTier(defOf(rules, current)) : Infinity; + const candidate = findAutoHealTarget(state, rules, e, def); + if (candidate && (!currentValid || healPriorityTier(defOf(rules, candidate)) < currentTier)) { + order.targetId = candidate.id; + } + } + } + switch (order.type) { case 'stop': e.orders.length = 0; diff --git a/tools/verifyTotalAnnihilation.js b/tools/verifyTotalAnnihilation.js index 0da3c5c..b52c9ea 100644 --- a/tools/verifyTotalAnnihilation.js +++ b/tools/verifyTotalAnnihilation.js @@ -739,8 +739,11 @@ section('6b. Commander self-repair'); run(st4, 120); check('regeneration stops at full health', c4.hp === c4.maxHp, `${c4.hp}/${c4.maxHp}`); + // Well outside any builder's repair range, so this isolates passive selfHeal regen from the + // auto-heal nanolathe mechanic (an idle builder now auto-repairs damaged allies in range — + // a real, separate source of healing, not a regression of this one). const st5 = fresh(); - const tank = L.spawnUnit(st5, hr, 0, 'tank', c4.x + 200, c4.y); + const tank = L.spawnUnit(st5, hr, 0, 'tank', c4.x + 2000, c4.y); tank.hp = tank.maxHp * 0.5; const tankHp = tank.hp; run(st5, 60);