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
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:
@@ -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,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user