fix: replace non-existent Golem icon with Mountain, implement golemancy tick logic
All checks were successful
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 2m4s

- Fix GolemancyTab.tsx icon (Golem -> Mountain)
- Add golem cost helper functions (summon, maintenance, affordability checks)
- Add auto-summoning at floor start for non-puzzle rooms
- Add maintenance cost deduction per tick from appropriate mana pools
- Add golem auto-attack logic with AOE support and armor pierce
- Add floor duration tracking and golem dismissal mechanics
- Integrate all golemancy skills (Mastery, Efficiency, Longevity, Siphon)
- All 44 tests pass
This commit is contained in:
Z User
2026-04-02 10:05:20 +00:00
parent 6df5dc6a53
commit d12076502a

View File

@@ -1146,6 +1146,190 @@ export const useGameStore = create<GameStore>()(
} }
} }
// ─── Golemancy Processing ─────────────────────────────────────────────────
let golemancy = state.golemancy;
const fabricatorLevel = state.attunements.fabricator?.level || 0;
const maxGolemSlots = getGolemSlots(fabricatorLevel);
// Check if golems need to be summoned (floor changed or first summon)
const floorChanged = currentFloor !== golemancy.lastSummonFloor;
const inCombatRoom = currentRoom.roomType !== 'puzzle';
if (state.currentAction === 'climb' && inCombatRoom && floorChanged && maxGolemSlots > 0) {
// Determine which golems should be summoned
const unlockedElementIds = Object.entries(elements)
.filter(([, e]) => e.unlocked)
.map(([id]) => id);
const enabledAndUnlocked = golemancy.enabledGolems.filter(golemId =>
isGolemUnlocked(golemId, state.attunements, unlockedElementIds)
);
// Limit to available slots
const golemsToSummon = enabledAndUnlocked.slice(0, maxGolemSlots);
// Summon golems that can be afforded
const summonedGolems: typeof golemancy.summonedGolems = [];
let summonCostsPaid = 0;
for (const golemId of golemsToSummon) {
if (canAffordGolemSummon(golemId, rawMana, elements)) {
const afterCost = deductGolemSummonCost(golemId, rawMana, elements);
rawMana = afterCost.rawMana;
elements = afterCost.elements;
summonedGolems.push({
golemId,
summonedFloor: currentFloor,
attackProgress: 0,
});
summonCostsPaid++;
}
}
if (summonedGolems.length > 0) {
golemancy = {
...golemancy,
summonedGolems,
lastSummonFloor: currentFloor,
};
if (summonCostsPaid > 0) {
log = [`🗿 Summoned ${summonedGolems.length} golem(s) on floor ${currentFloor}!`, ...log.slice(0, 49)];
}
} else if (golemsToSummon.length > 0) {
log = [`⚠️ Could not afford to summon any golems!`, ...log.slice(0, 49)];
}
}
// Process golem maintenance and attacks each tick
if (golemancy.summonedGolems.length > 0 && state.currentAction === 'climb' && inCombatRoom) {
const floorDuration = getGolemFloorDuration(skills);
const survivingGolems: typeof golemancy.summonedGolems = [];
let anyGolemDismissed = false;
for (const summonedGolem of golemancy.summonedGolems) {
const golemId = summonedGolem.golemId;
// Check floor duration
const floorsActive = currentFloor - summonedGolem.summonedFloor;
if (floorsActive >= floorDuration) {
log = [`${GOLEMS_DEF[golemId]?.name || golemId} returned to the earth after ${floorDuration} floor(s).`, ...log.slice(0, 49)];
anyGolemDismissed = true;
continue;
}
// Check and pay maintenance cost
if (!canAffordGolemMaintenance(golemId, rawMana, elements, skills)) {
log = [`💫 ${GOLEMS_DEF[golemId]?.name || golemId} dismissed - insufficient mana for maintenance!`, ...log.slice(0, 49)];
anyGolemDismissed = true;
continue;
}
const afterMaintenance = deductGolemMaintenance(golemId, rawMana, elements, skills);
rawMana = afterMaintenance.rawMana;
elements = afterMaintenance.elements;
survivingGolems.push(summonedGolem);
}
if (anyGolemDismissed) {
golemancy = {
...golemancy,
summonedGolems: survivingGolems,
};
}
// Process golem attacks
for (const summonedGolem of golemancy.summonedGolems) {
const golemDef = GOLEMS_DEF[summonedGolem.golemId];
if (!golemDef) continue;
// Get attack speed (attacks per hour)
const attackSpeed = getGolemAttackSpeed(summonedGolem.golemId, skills);
const progressGain = HOURS_PER_TICK * attackSpeed;
// Accumulate attack progress
summonedGolem.attackProgress = (summonedGolem.attackProgress || 0) + progressGain;
// Process attacks when progress >= 1
while (summonedGolem.attackProgress >= 1) {
// Find alive enemies
const aliveEnemies = currentRoom.enemies.filter(e => e.hp > 0);
if (aliveEnemies.length === 0) break;
// Calculate damage
const baseDamage = getGolemDamage(summonedGolem.golemId, skills);
// Determine targets (AOE vs single target)
const numTargets = golemDef.isAoe
? Math.min(golemDef.aoeTargets, aliveEnemies.length)
: 1;
const targets = aliveEnemies.slice(0, numTargets);
for (const enemy of targets) {
let damage = baseDamage;
// AOE damage falloff
if (golemDef.isAoe && numTargets > 1) {
damage *= (1 - 0.1 * (targets.indexOf(enemy)));
}
// Apply armor reduction with pierce
const effectiveArmor = Math.max(0, enemy.armor - golemDef.armorPierce);
damage *= (1 - effectiveArmor);
// Apply damage
enemy.hp = Math.max(0, enemy.hp - Math.floor(damage));
}
// Update currentRoom with damaged enemies
currentRoom = { ...currentRoom, enemies: [...currentRoom.enemies] };
// Reduce attack progress
summonedGolem.attackProgress -= 1;
// Check if all enemies are dead
const allDead = currentRoom.enemies.every(e => e.hp <= 0);
if (allDead) {
// Floor cleared by golems - trigger floor change logic
const wasGuardian = GUARDIANS[currentFloor];
if (wasGuardian && !signedPacts.includes(currentFloor)) {
signedPacts = [...signedPacts, currentFloor];
log = [`⚔️ ${wasGuardian.name} defeated by golems! Pact signed! (${wasGuardian.pact}x)`, ...log.slice(0, 49)];
} else if (!wasGuardian) {
const roomTypeName = currentRoom.roomType === 'swarm' ? 'Swarm'
: currentRoom.roomType === 'speed' ? 'Speed floor'
: currentRoom.roomType === 'puzzle' ? 'Puzzle'
: 'Floor';
if (currentFloor % 5 === 0 || currentRoom.roomType !== 'combat') {
log = [`🗿 ${roomTypeName} ${currentFloor} cleared by golems!`, ...log.slice(0, 49)];
}
}
currentFloor = currentFloor + 1;
if (currentFloor > 100) currentFloor = 100;
currentRoom = generateFloorState(currentFloor);
floorMaxHP = getFloorMaxHP(currentFloor);
floorHP = currentRoom.enemies[0]?.hp || floorMaxHP;
maxFloorReached = Math.max(maxFloorReached, currentFloor);
castProgress = 0;
break; // Exit attack loop on floor change
}
}
}
}
// Unsummon golems when not climbing or in puzzle room
if ((state.currentAction !== 'climb' || !inCombatRoom) && golemancy.summonedGolems.length > 0) {
log = [`🗿 Golems returned to the earth.`, ...log.slice(0, 49)];
golemancy = {
...golemancy,
summonedGolems: [],
};
}
// Process crafting actions (design, prepare, enchant) // Process crafting actions (design, prepare, enchant)
const craftingUpdates = processCraftingTick( const craftingUpdates = processCraftingTick(
{ {
@@ -1194,6 +1378,7 @@ export const useGameStore = create<GameStore>()(
elements, elements,
log, log,
castProgress, castProgress,
golemancy,
}); });
return; return;
} }
@@ -1219,6 +1404,7 @@ export const useGameStore = create<GameStore>()(
unlockedEffects, unlockedEffects,
log, log,
castProgress, castProgress,
golemancy,
...craftingUpdates, ...craftingUpdates,
}); });
}, },