feat: guardian defensive stats — shield, barrier, health regen + stat label renames
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m21s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m21s
This commit is contained in:
@@ -38,6 +38,12 @@ export interface CombatState {
|
||||
comboHitCount: number;
|
||||
floorHitCount: number;
|
||||
|
||||
// Guardian defensive state (shield, barrier, regen)
|
||||
guardianShield: number;
|
||||
guardianShieldMax: number;
|
||||
guardianBarrier: number;
|
||||
guardianBarrierMax: number;
|
||||
|
||||
// Spells
|
||||
spells: Record<string, SpellState>;
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { generateFloorState } from '../utils/room-utils';
|
||||
import { generateSpireFloorState } from '../utils/spire-utils';
|
||||
import { addActivityLogEntry } from '../utils/activity-log';
|
||||
import { processCombatTick, makeInitialSpells } from './combat-actions';
|
||||
import { getGuardianForFloor } from '../data/guardian-encounters';
|
||||
import type { CombatStore } from './combat-state.types';
|
||||
|
||||
export const useCombatStore = create<CombatStore>()(
|
||||
@@ -46,6 +47,12 @@ export const useCombatStore = create<CombatStore>()(
|
||||
comboHitCount: 0,
|
||||
floorHitCount: 0,
|
||||
|
||||
// Guardian defensive state
|
||||
guardianShield: 0,
|
||||
guardianShieldMax: 0,
|
||||
guardianBarrier: 0,
|
||||
guardianBarrierMax: 0,
|
||||
|
||||
// Spells
|
||||
spells: {
|
||||
manaBolt: { learned: true, level: 1, studyProgress: 0 },
|
||||
@@ -253,6 +260,22 @@ export const useCombatStore = create<CombatStore>()(
|
||||
set((state) => ({ totalCraftsCompleted: state.totalCraftsCompleted + 1 }));
|
||||
},
|
||||
|
||||
resetGuardianDefensiveState: () => {
|
||||
set({ guardianShield: 0, guardianShieldMax: 0, guardianBarrier: 0, guardianBarrierMax: 0 });
|
||||
},
|
||||
|
||||
initGuardianDefensiveState: () => {
|
||||
const state = get();
|
||||
const guardian = getGuardianForFloor(state.currentFloor);
|
||||
if (!guardian) return;
|
||||
set({
|
||||
guardianShield: guardian.shield ?? 0,
|
||||
guardianShieldMax: guardian.shield ?? 0,
|
||||
guardianBarrier: guardian.barrier ?? 0,
|
||||
guardianBarrierMax: guardian.barrier ?? 0,
|
||||
});
|
||||
},
|
||||
|
||||
processCombatTick: (
|
||||
rawMana: number,
|
||||
elements: Record<string, { current: number; max: number; unlocked: boolean }>,
|
||||
@@ -316,6 +339,10 @@ export const useCombatStore = create<CombatStore>()(
|
||||
totalSpellsCast: state.totalSpellsCast,
|
||||
totalDamageDealt: state.totalDamageDealt,
|
||||
totalCraftsCompleted: state.totalCraftsCompleted,
|
||||
guardianShield: state.guardianShield,
|
||||
guardianShieldMax: state.guardianShieldMax,
|
||||
guardianBarrier: state.guardianBarrier,
|
||||
guardianBarrierMax: state.guardianBarrierMax,
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
// ─── Game Store (Coordinator) ──────────────────────────────────────
|
||||
// Manages: day, hour, incursionStrength, containmentWards
|
||||
// Orchestrates tick across all stores via read → compute → write pipeline.
|
||||
|
||||
// Game Store — coordinator, tick pipeline, time/incursion
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { HOURS_PER_TICK, MAX_DAY } from '../constants';
|
||||
@@ -52,7 +49,6 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
...initialState,
|
||||
|
||||
initGame: () => {
|
||||
// Wire discipline store ↔ combat store callbacks (breaks circular dependency)
|
||||
useDisciplineStore.getState().setPracticingCallbacks({
|
||||
onStartPracticing: () => useCombatStore.getState().startPracticing(),
|
||||
onStopPracticing: () => useCombatStore.getState().stopPracticing(),
|
||||
@@ -62,7 +58,6 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
|
||||
tick: () => {
|
||||
try {
|
||||
// ── Phase 1: Read — snapshot all store states once ──────────────────
|
||||
const ctx = buildTickContext({
|
||||
game: get(),
|
||||
ui: useUIStore.getState(),
|
||||
@@ -76,7 +71,6 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
|
||||
if (ctx.ui.gameOver || ctx.ui.paused) return;
|
||||
|
||||
// Shared setters object — used by every applyTickWrites call below
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const storeSetters = {
|
||||
setGame: set,
|
||||
@@ -90,11 +84,9 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
addLogs: (msgs: string[]) => msgs.forEach((m) => useUIStore.getState().addLog(m)),
|
||||
};
|
||||
|
||||
// ── Phase 2: Compute — derive all updates ───────────────────────────
|
||||
const writes: TickWrites = { logs: [] };
|
||||
const addLog = (msg: string) => writes.logs.push(msg);
|
||||
|
||||
// Compute equipment and discipline effects
|
||||
const steadyHandLevel = ctx.prestige.prestigeUpgrades.steadyHand || 0;
|
||||
const enchantmentPowerMultiplier = 1 + steadyHandLevel * 0.15;
|
||||
const equipmentEffects = computeEquipmentEffects(
|
||||
@@ -120,7 +112,6 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
disciplineEffects,
|
||||
) * (1 + (disciplineEffects.multipliers.regenMultiplier || 0));
|
||||
|
||||
// Time progression
|
||||
let hour = ctx.game.hour + HOURS_PER_TICK;
|
||||
let day = ctx.game.day;
|
||||
if (hour >= 24) {
|
||||
@@ -128,7 +119,6 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
day += 1;
|
||||
}
|
||||
|
||||
// Shared insight params — reused for both loop-end and victory
|
||||
const insightParams = {
|
||||
maxFloorReached: ctx.combat.maxFloorReached,
|
||||
totalManaGathered: ctx.mana.totalManaGathered,
|
||||
@@ -136,11 +126,9 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
prestigeUpgrades: ctx.prestige.prestigeUpgrades,
|
||||
};
|
||||
|
||||
// Check for loop end
|
||||
if (day > MAX_DAY) {
|
||||
const insightGained = calcInsight(insightParams, disciplineEffects);
|
||||
|
||||
addLog(`⏰ The loop ends. Gained ${insightGained} Insight.`);
|
||||
addLog('The loop ends. Gained ' + insightGained + ' Insight.');
|
||||
writes.ui = { ...(writes.ui || {}), gameOver: true, victory: false };
|
||||
writes.prestige = { ...(writes.prestige || {}), loopInsight: insightGained };
|
||||
writes.game = { day, hour };
|
||||
@@ -148,11 +136,9 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for victory (3× insight multiplier)
|
||||
if (ctx.combat.maxFloorReached >= 100 && ctx.prestige.signedPacts.includes(100)) {
|
||||
const insightGained = calcInsight(insightParams, disciplineEffects) * 3;
|
||||
|
||||
addLog(`🏆 VICTORY! The Awakened One falls! Gained ${insightGained} Insight!`);
|
||||
addLog('VICTORY! The Awakened One falls! Gained ' + insightGained + ' Insight!');
|
||||
writes.ui = { ...(writes.ui || {}), gameOver: true, victory: true };
|
||||
writes.prestige = { ...(writes.prestige || {}), loopInsight: insightGained };
|
||||
applyTickWrites(writes, storeSetters);
|
||||
@@ -161,7 +147,6 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
|
||||
const incursionStrength = getIncursionStrength(day, hour);
|
||||
|
||||
// Meditation bonus tracking
|
||||
let meditateTicks = ctx.mana.meditateTicks;
|
||||
let meditationMultiplier = 1;
|
||||
|
||||
@@ -172,7 +157,6 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
meditateTicks = 0;
|
||||
}
|
||||
|
||||
// Calculate total attunement conversion and apply to element pools
|
||||
let totalConversionPerTick = 0;
|
||||
let elements = { ...ctx.mana.elements };
|
||||
Object.entries(ctx.attunement.attunements).forEach(([id, state]) => {
|
||||
@@ -195,11 +179,9 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
|
||||
const effectiveRegen = Math.max(0, baseRegen * (1 - incursionStrength) * meditationMultiplier - totalConversionPerTick);
|
||||
|
||||
// Mana regeneration
|
||||
let rawMana = Math.min(ctx.mana.rawMana + effectiveRegen * HOURS_PER_TICK, maxMana);
|
||||
let totalManaGathered = ctx.mana.totalManaGathered;
|
||||
|
||||
// Convert action
|
||||
if (ctx.combat.currentAction === 'convert') {
|
||||
const convertResult = useManaStore.getState().processConvertAction(rawMana);
|
||||
if (convertResult) {
|
||||
@@ -208,7 +190,6 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
}
|
||||
}
|
||||
|
||||
// Pact ritual
|
||||
const pactResult = processPactRitual(
|
||||
ctx.prestige.pactRitualFloor,
|
||||
ctx.prestige.pactRitualProgress,
|
||||
@@ -222,7 +203,6 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
}
|
||||
pactResult.logs.forEach(l => addLog(l));
|
||||
|
||||
// Discipline tick
|
||||
const disciplineResult = useDisciplineStore.getState().processTick({
|
||||
rawMana,
|
||||
elements,
|
||||
@@ -230,69 +210,50 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
rawMana = disciplineResult.rawMana;
|
||||
elements = disciplineResult.elements;
|
||||
|
||||
// Apply discipline conversions: drain source mana, add to target element
|
||||
for (const [targetElem, conv] of Object.entries(disciplineEffects.conversions)) {
|
||||
const conversionAmount = conv.rate * HOURS_PER_TICK;
|
||||
// Check that all source mana types are available (unlocked and have enough)
|
||||
let canConvert = true;
|
||||
for (const srcType of conv.sourceManaTypes) {
|
||||
if (srcType === 'raw') {
|
||||
if (rawMana < conversionAmount) {
|
||||
canConvert = false;
|
||||
break;
|
||||
}
|
||||
if (rawMana < conversionAmount) { canConvert = false; break; }
|
||||
} else if (!elements[srcType] || !elements[srcType].unlocked || elements[srcType].current < conversionAmount) {
|
||||
canConvert = false;
|
||||
break;
|
||||
canConvert = false; break;
|
||||
}
|
||||
}
|
||||
if (!canConvert) continue;
|
||||
// Drain source mana types
|
||||
for (const srcType of conv.sourceManaTypes) {
|
||||
if (srcType === 'raw') {
|
||||
rawMana -= conversionAmount;
|
||||
} else if (elements[srcType]) {
|
||||
elements[srcType] = {
|
||||
...elements[srcType],
|
||||
current: elements[srcType].current - conversionAmount,
|
||||
};
|
||||
elements[srcType] = { ...elements[srcType], current: elements[srcType].current - conversionAmount };
|
||||
}
|
||||
}
|
||||
// Add to target element
|
||||
if (elements[targetElem]) {
|
||||
elements[targetElem] = {
|
||||
...elements[targetElem],
|
||||
current: Math.min(
|
||||
elements[targetElem].max,
|
||||
elements[targetElem].current + conversionAmount,
|
||||
),
|
||||
current: Math.min(elements[targetElem].max, elements[targetElem].current + conversionAmount),
|
||||
};
|
||||
}
|
||||
}
|
||||
// Unlock enchantment effects from newly unlocked discipline perks
|
||||
if (disciplineResult.unlockedEffects.length > 0) {
|
||||
useCraftingStore.getState().unlockEffects(disciplineResult.unlockedEffects);
|
||||
for (const effectId of disciplineResult.unlockedEffects) {
|
||||
addLog(`✨ Discipline insight unlocked: ${effectId}`);
|
||||
addLog('Discipline insight unlocked: ' + effectId);
|
||||
}
|
||||
}
|
||||
|
||||
// Unlock fabricator recipes from newly unlocked discipline perks
|
||||
if (disciplineResult.unlockedRecipes.length > 0) {
|
||||
useCraftingStore.getState().unlockRecipes(disciplineResult.unlockedRecipes);
|
||||
for (const recipeId of disciplineResult.unlockedRecipes) {
|
||||
addLog(`🔨 Fabricator recipe unlocked: ${recipeId}`);
|
||||
addLog('Fabricator recipe unlocked: ' + recipeId);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply per-element capacity bonuses from disciplines and equipment
|
||||
const perElementCapBonuses = mergePerElementCapBonuses(
|
||||
disciplineEffects.bonuses,
|
||||
equipmentEffects.bonuses,
|
||||
);
|
||||
useManaStore.getState().computeElementMaxWithBonuses(perElementCapBonuses);
|
||||
|
||||
// Sync updated max/baseMax from mana store into tick elements snapshot
|
||||
const manaStateAfter = useManaStore.getState();
|
||||
for (const [ek, es] of Object.entries(manaStateAfter.elements)) {
|
||||
if (elements[ek]) {
|
||||
@@ -310,10 +271,11 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
(floor, wasGuardian) => {
|
||||
if (wasGuardian) {
|
||||
const defeatedGuardian = getGuardianForFloor(floor);
|
||||
addLog(`\u2694\ufe0f ${defeatedGuardian?.name || 'Guardian'} defeated! Visit the Grimoire to sign a pact.`);
|
||||
addLog((defeatedGuardian?.name || 'Unknown') + ' defeated! Visit the Grimoire to sign a pact.');
|
||||
} else if (floor % 5 === 0) {
|
||||
addLog(`🏰 Floor ${floor} cleared!`);
|
||||
addLog('Floor ' + floor + ' cleared!');
|
||||
}
|
||||
useCombatStore.setState({ guardianShield: 0, guardianShieldMax: 0, guardianBarrier: 0, guardianBarrierMax: 0 });
|
||||
},
|
||||
(damage) => {
|
||||
let dmg = damage;
|
||||
@@ -323,6 +285,46 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.BERSERKER) && rawMana < maxMana * 0.5) {
|
||||
dmg *= 1.5;
|
||||
}
|
||||
|
||||
const guardian = getGuardianForFloor(ctx.combat.currentFloor);
|
||||
if (guardian && (guardian.shield || guardian.barrier || guardian.healthRegen)) {
|
||||
let shield = ctx.combat.guardianShield;
|
||||
let shieldMax = ctx.combat.guardianShieldMax;
|
||||
let barrier = ctx.combat.guardianBarrier;
|
||||
let barrierMax = ctx.combat.guardianBarrierMax;
|
||||
|
||||
if (guardian.shieldRegen && shield < shieldMax) {
|
||||
shield = Math.min(shieldMax, shield + guardian.shieldRegen * HOURS_PER_TICK);
|
||||
}
|
||||
if (guardian.barrierRegen && barrier < barrierMax) {
|
||||
barrier = Math.min(barrierMax, barrier + guardian.barrierRegen * HOURS_PER_TICK);
|
||||
}
|
||||
|
||||
if (shield > 0 && dmg > 0) {
|
||||
const absorb = Math.min(shield, dmg);
|
||||
shield -= absorb;
|
||||
dmg -= absorb;
|
||||
}
|
||||
|
||||
if (barrier > 0 && dmg > 0) {
|
||||
dmg *= (1 - barrier);
|
||||
}
|
||||
|
||||
if (guardian.healthRegen && guardian.healthRegen > 0) {
|
||||
const healAmount = guardian.healthRegenIsPercent
|
||||
? Math.floor(ctx.combat.floorMaxHP * guardian.healthRegen / 100 * HOURS_PER_TICK)
|
||||
: Math.floor(guardian.healthRegen * HOURS_PER_TICK);
|
||||
dmg -= healAmount;
|
||||
}
|
||||
|
||||
useCombatStore.setState({
|
||||
guardianShield: shield,
|
||||
guardianShieldMax: shieldMax,
|
||||
guardianBarrier: barrier,
|
||||
guardianBarrierMax: barrierMax,
|
||||
});
|
||||
}
|
||||
|
||||
return { rawMana, elements, modifiedDamage: dmg };
|
||||
},
|
||||
ctx.prestige.signedPacts,
|
||||
@@ -347,7 +349,6 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
};
|
||||
}
|
||||
|
||||
// Equipment crafting tick — advance progress and complete when done
|
||||
if (ctx.combat.currentAction === 'craft') {
|
||||
const craftingResult = useCraftingStore.getState().processEquipmentCraftingTick();
|
||||
if (craftingResult.logMessage) {
|
||||
@@ -355,7 +356,7 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
}
|
||||
}
|
||||
|
||||
// ── Phase 3: Write — batch all state updates ─────────────────────────
|
||||
// Phase 3: Write
|
||||
writes.game = { day, hour, incursionStrength };
|
||||
writes.mana = {
|
||||
rawMana,
|
||||
@@ -366,10 +367,9 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
|
||||
applyTickWrites(writes, storeSetters);
|
||||
} catch (error: unknown) {
|
||||
// Log error to UI store if available, otherwise console error
|
||||
try {
|
||||
const msg = error instanceof Error ? error.message : String(error);
|
||||
useUIStore.getState().addLog(`⚠️ Tick error: ${msg}`);
|
||||
useUIStore.getState().addLog('Tick error: ' + msg);
|
||||
} catch {
|
||||
console.error('Tick error:', error);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user