feat(totalannihilation): add auto-heal for idle builders
Idle builder units now automatically repair damaged allied structures and units within their build range. A priority system ensures defensive buildings are repaired before factories and other buildings. Auto-heal orders are marked as non-player-assigned so explicit player orders always take precedence. Builders can re-prioritize targets on a retargeting cadence, similar to combat target acquisition. Includes visual asset updates for TA arm and core units/structures. Adjusts test to isolate passive self-heal from the new auto-heal mechanic.
|
Before Width: | Height: | Size: 946 KiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 203 KiB |
|
Before Width: | Height: | Size: 1006 KiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 184 KiB |
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||