fix: Restrict pact affinity cast speed bonus to invocation spells only
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
Previously (commit 62638d6), the pact affinity cast speed bonus was applied
to ALL spell casts (active, equipment, invocation). Per design intent, it
should only apply to invocation system spells.
Changes:
- invocation-system-spec.md: Update §6.2, §10.2, AC-13 to clarify bonus
applies to invocation spells only
- gameStore.ts: Stop computing pact affinity bonus into attackSpeedMult;
pass base 1.0 instead
- combat-actions.ts: Remove stale AC-13 comment
- combat-invocation.ts: Compute pact affinity cast speed bonus locally
for invocation spells only, using prestige upgrade level + discipline bonus
- invocation-utils.ts: Remove computeAttackSpeedMultFromPactAffinity (no
longer needed)
All 1235 tests pass.
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
# Circular Dependencies
|
# Circular Dependencies
|
||||||
Generated: 2026-06-13T11:42:23.525Z
|
Generated: 2026-06-13T15:26:15.912Z
|
||||||
Found: 4 circular chain(s) — these MUST be fixed before modifying involved files.
|
Found: 4 circular chain(s) — these MUST be fixed before modifying involved files.
|
||||||
|
|
||||||
1. 1) data/guardian-encounters.ts > data/guardian-procedural.ts
|
1. 1) data/guardian-encounters.ts > data/guardian-procedural.ts
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"generated": "2026-06-13T11:42:21.085Z",
|
"generated": "2026-06-13T15:26:13.488Z",
|
||||||
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
|
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
|
||||||
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
|
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
|
||||||
},
|
},
|
||||||
@@ -760,6 +760,7 @@
|
|||||||
"utils/element-cap-bonus.ts",
|
"utils/element-cap-bonus.ts",
|
||||||
"utils/element-distance.ts",
|
"utils/element-distance.ts",
|
||||||
"utils/index.ts",
|
"utils/index.ts",
|
||||||
|
"utils/invocation-utils.ts",
|
||||||
"utils/safe-persist.ts"
|
"utils/safe-persist.ts"
|
||||||
],
|
],
|
||||||
"stores/gameStore.types.ts": [],
|
"stores/gameStore.types.ts": [],
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ auto-cast elemental spells at a fraction of their normal cost.
|
|||||||
- Make pacts feel *active* in combat, not just passive stat sticks
|
- Make pacts feel *active* in combat, not just passive stat sticks
|
||||||
- Reward players who have signed more/higher-tier pacts (faster charge, stronger spells)
|
- Reward players who have signed more/higher-tier pacts (faster charge, stronger spells)
|
||||||
- Create meaningful decisions: when to invoke, which guardian gets channeled, mana management during invocation
|
- Create meaningful decisions: when to invoke, which guardian gets channeled, mana management during invocation
|
||||||
- Give Pact Affinity a combat role (cast speed) so it matters outside of ritual time reduction
|
- Give Pact Affinity a combat role (cast speed for invocation spells) so it matters outside of ritual time reduction
|
||||||
- Add a new Invoker discipline (`guardian-invocation`) for vertical progression of the system
|
- Add a new Invoker discipline (`guardian-invocation`) for vertical progression of the system
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -261,13 +261,13 @@ This gives diminishing returns:
|
|||||||
|
|
||||||
### 6.2 Application
|
### 6.2 Application
|
||||||
|
|
||||||
The cast speed bonus applies to **all spell casts** (active spell, equipment
|
The cast speed bonus applies to **invocation spells only** — the spells auto-cast
|
||||||
spells, and invocation spells) while in combat (`climb` action). It is applied
|
by the Invocation system while channeling a guardian. It does **not** apply to the
|
||||||
as a multiplier to the `totalAttackSpeed` value used in cast progress
|
player's active spell or equipment spells. It is applied as a multiplier to the
|
||||||
calculation:
|
attack speed used in the invocation spell's cast progress calculation:
|
||||||
|
|
||||||
```
|
```
|
||||||
effectiveAttackSpeed = totalAttackSpeed × (1 + castSpeedBonus)
|
effectiveAttackSpeed = baseAttackSpeed × (1 + castSpeedBonus)
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6.3 Affinity Sources (Unchanged)
|
### 6.3 Affinity Sources (Unchanged)
|
||||||
@@ -497,15 +497,16 @@ spell block, add an **invocation block**:
|
|||||||
|
|
||||||
### 10.2 Pact Affinity Cast Speed
|
### 10.2 Pact Affinity Cast Speed
|
||||||
|
|
||||||
In the cast progress calculation, apply the cast speed bonus:
|
In the invocation spell cast progress calculation, apply the cast speed bonus:
|
||||||
|
|
||||||
```
|
```
|
||||||
const castSpeedBonus = computeCastSpeedBonus(pactAffinity);
|
const castSpeedBonus = computeCastSpeedBonus(pactAffinity);
|
||||||
const effectiveAttackSpeed = totalAttackSpeed × (1 + castSpeedBonus);
|
const effectiveAttackSpeed = baseAttackSpeed × (1 + castSpeedBonus);
|
||||||
const progressPerTick = HOURS_PER_TICK × spellCastSpeed × effectiveAttackSpeed;
|
const invProgressPerTick = HOURS_PER_TICK × invCastSpeed × effectiveAttackSpeed;
|
||||||
```
|
```
|
||||||
|
|
||||||
This applies to **all** cast progress calculations (active, equipment, invocation).
|
This applies to **invocation spells only** (not to the player's active spell or
|
||||||
|
equipment spells).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -572,7 +573,7 @@ gameStore.tick()
|
|||||||
| AC-10 | Charge only fills while in `climb` action. |
|
| AC-10 | Charge only fills while in `climb` action. |
|
||||||
| AC-11 | Invocation spell casts in parallel with the player's active spell and equipment spells. |
|
| AC-11 | Invocation spell casts in parallel with the player's active spell and equipment spells. |
|
||||||
| AC-12 | Pact Affinity grants a cast speed bonus using `MAX_BONUS × (1 - 1 / (1 + pactAffinity × 1.5))`, capped at 50%. |
|
| AC-12 | Pact Affinity grants a cast speed bonus using `MAX_BONUS × (1 - 1 / (1 + pactAffinity × 1.5))`, capped at 50%. |
|
||||||
| AC-13 | Cast speed bonus applies to all spell casts (active, equipment, invocation). |
|
| AC-13 | Cast speed bonus applies to invocation spells only (not active/equipment spells). |
|
||||||
| AC-14 | The `guardian-invocation` discipline requires at least one signed pact. |
|
| AC-14 | The `guardian-invocation` discipline requires at least one signed pact. |
|
||||||
| AC-15 | `invocation-efficiency` once perk reduces cost multiplier by 0.02. |
|
| AC-15 | `invocation-efficiency` once perk reduces cost multiplier by 0.02. |
|
||||||
| AC-16 | `invocation-speed` infinite perk grants +0.05 charge rate bonus every 150 XP. |
|
| AC-16 | `invocation-speed` infinite perk grants +0.05 charge rate bonus every 150 XP. |
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ export function processCombatTick(
|
|||||||
bypassBarrier?: boolean,
|
bypassBarrier?: boolean,
|
||||||
) => number,
|
) => number,
|
||||||
equippedSwords?: Record<string, EquipmentInstance>,
|
equippedSwords?: Record<string, EquipmentInstance>,
|
||||||
|
pactAffinityUpgrade?: number,
|
||||||
): CombatTickResult {
|
): CombatTickResult {
|
||||||
const state = get();
|
const state = get();
|
||||||
const logMessages: string[] = [];
|
const logMessages: string[] = [];
|
||||||
@@ -134,7 +135,6 @@ export function processCombatTick(
|
|||||||
// ─── Spell casting (only when a valid spell is configured) ────────────────
|
// ─── Spell casting (only when a valid spell is configured) ────────────────
|
||||||
if (spellDef) {
|
if (spellDef) {
|
||||||
const disciplineEffects = computeDisciplineEffects();
|
const disciplineEffects = computeDisciplineEffects();
|
||||||
// AC-13: Pact affinity cast speed bonus already applied to attackSpeedMult by gameStore
|
|
||||||
const totalAttackSpeed = attackSpeedMult;
|
const totalAttackSpeed = attackSpeedMult;
|
||||||
const spellCastSpeed = spellDef.castSpeed || 1;
|
const spellCastSpeed = spellDef.castSpeed || 1;
|
||||||
const progressPerTick = HOURS_PER_TICK * spellCastSpeed * totalAttackSpeed;
|
const progressPerTick = HOURS_PER_TICK * spellCastSpeed * totalAttackSpeed;
|
||||||
@@ -263,7 +263,7 @@ export function processCombatTick(
|
|||||||
|
|
||||||
// ─── Invocation system (spec §10.1) ───────────────────────────────────
|
// ─── Invocation system (spec §10.1) ───────────────────────────────────
|
||||||
const invResult = processInvocationTick(
|
const invResult = processInvocationTick(
|
||||||
{ get, set, rawMana, elements, attackSpeedMult, signedPacts, currentRoom, floorHP },
|
{ get, set, rawMana, elements, attackSpeedMult, signedPacts, currentRoom, floorHP, pactAffinityUpgrade: pactAffinityUpgrade || 0 },
|
||||||
onFloorCleared,
|
onFloorCleared,
|
||||||
onDamageDealt,
|
onDamageDealt,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ import {
|
|||||||
selectInvocationSpell,
|
selectInvocationSpell,
|
||||||
deductInvocationSpellCost,
|
deductInvocationSpellCost,
|
||||||
computeChargeFillRate,
|
computeChargeFillRate,
|
||||||
|
computeCastSpeedBonus,
|
||||||
computeCostMultiplier,
|
computeCostMultiplier,
|
||||||
computeDrainRateMultiplier,
|
computeDrainRateMultiplier,
|
||||||
computeDrainPerTick,
|
|
||||||
type ActiveInvocation,
|
type ActiveInvocation,
|
||||||
} from '../utils/invocation-utils';
|
} from '../utils/invocation-utils';
|
||||||
|
|
||||||
@@ -30,6 +30,7 @@ export interface InvocationTickParams {
|
|||||||
signedPacts: number[];
|
signedPacts: number[];
|
||||||
currentRoom: FloorState;
|
currentRoom: FloorState;
|
||||||
floorHP: number;
|
floorHP: number;
|
||||||
|
pactAffinityUpgrade: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InvocationTickResult {
|
export interface InvocationTickResult {
|
||||||
@@ -53,7 +54,7 @@ export function processInvocationTick(
|
|||||||
modifiedDamage?: number;
|
modifiedDamage?: number;
|
||||||
},
|
},
|
||||||
): InvocationTickResult {
|
): InvocationTickResult {
|
||||||
const { get, set, rawMana: startRawMana, elements: startElements, attackSpeedMult, signedPacts, currentRoom, floorHP: startFloorHP } = params;
|
const { get, set, rawMana: startRawMana, elements: startElements, attackSpeedMult, signedPacts, currentRoom, floorHP: startFloorHP, pactAffinityUpgrade } = params;
|
||||||
let rawMana = startRawMana;
|
let rawMana = startRawMana;
|
||||||
let elements = startElements;
|
let elements = startElements;
|
||||||
let floorHP = startFloorHP;
|
let floorHP = startFloorHP;
|
||||||
@@ -67,7 +68,7 @@ export function processInvocationTick(
|
|||||||
if (activeInvocation !== null) {
|
if (activeInvocation !== null) {
|
||||||
// ── Invocation is active: drain charge and process cast ──
|
// ── Invocation is active: drain charge and process cast ──
|
||||||
const invResult = processActiveInvocation({
|
const invResult = processActiveInvocation({
|
||||||
get, set, rawMana, elements, attackSpeedMult, signedPacts,
|
get, set, rawMana, elements, attackSpeedMult, signedPacts, pactAffinityUpgrade,
|
||||||
currentFloor, floorHP, floorMaxHP, currentRoom: currentRoomState,
|
currentFloor, floorHP, floorMaxHP, currentRoom: currentRoomState,
|
||||||
invocationCharge, activeInvocation, logMessages,
|
invocationCharge, activeInvocation, logMessages,
|
||||||
}, onFloorCleared, onDamageDealt);
|
}, onFloorCleared, onDamageDealt);
|
||||||
@@ -138,6 +139,7 @@ interface ActiveInvParams {
|
|||||||
elements: Record<string, { current: number; max: number; unlocked: boolean }>;
|
elements: Record<string, { current: number; max: number; unlocked: boolean }>;
|
||||||
attackSpeedMult: number;
|
attackSpeedMult: number;
|
||||||
signedPacts: number[];
|
signedPacts: number[];
|
||||||
|
pactAffinityUpgrade: number;
|
||||||
currentFloor: number;
|
currentFloor: number;
|
||||||
floorHP: number;
|
floorHP: number;
|
||||||
floorMaxHP: number;
|
floorMaxHP: number;
|
||||||
@@ -156,7 +158,7 @@ function processActiveInvocation(
|
|||||||
modifiedDamage?: number;
|
modifiedDamage?: number;
|
||||||
},
|
},
|
||||||
): InvocationTickResult {
|
): InvocationTickResult {
|
||||||
let { get, set, rawMana, elements, attackSpeedMult, signedPacts } = p;
|
let { get, set, rawMana, elements, attackSpeedMult, signedPacts, pactAffinityUpgrade } = p;
|
||||||
let { currentFloor, floorHP, floorMaxHP, currentRoom } = p;
|
let { currentFloor, floorHP, floorMaxHP, currentRoom } = p;
|
||||||
let { invocationCharge, activeInvocation } = p;
|
let { invocationCharge, activeInvocation } = p;
|
||||||
const logMessages = p.logMessages;
|
const logMessages = p.logMessages;
|
||||||
@@ -171,9 +173,13 @@ function processActiveInvocation(
|
|||||||
const drainPerTick = computeDrainPerTick(invSpellDef.cost.amount, drainMult);
|
const drainPerTick = computeDrainPerTick(invSpellDef.cost.amount, drainMult);
|
||||||
invocationCharge = Math.max(0, invocationCharge - drainPerTick);
|
invocationCharge = Math.max(0, invocationCharge - drainPerTick);
|
||||||
|
|
||||||
// Cast progress uses attackSpeedMult which already includes pact affinity bonus (AC-12/AC-13)
|
// AC-12/AC-13: Apply pact affinity cast speed bonus to invocation spells only
|
||||||
|
// pactAffinityUpgrade is the prestige level (0-9), each level = +0.1; pactAffinityBonus comes from discipline
|
||||||
|
const pactAffinity = pactAffinityUpgrade * 0.1 + (disciplineEffects.bonuses.pactAffinityBonus || 0);
|
||||||
|
const castSpeedBonus = computeCastSpeedBonus(pactAffinity);
|
||||||
|
const effectiveAttackSpeed = attackSpeedMult * (1 + castSpeedBonus);
|
||||||
const invCastSpeed = invSpellDef.castSpeed || 1;
|
const invCastSpeed = invSpellDef.castSpeed || 1;
|
||||||
const invProgressPerTick = HOURS_PER_TICK * invCastSpeed * attackSpeedMult;
|
const invProgressPerTick = HOURS_PER_TICK * invCastSpeed * effectiveAttackSpeed;
|
||||||
const newCastProgress = activeInvocation.castProgress + invProgressPerTick;
|
const newCastProgress = activeInvocation.castProgress + invProgressPerTick;
|
||||||
|
|
||||||
if (newCastProgress >= 1 && floorHP > 0) {
|
if (newCastProgress >= 1 && floorHP > 0) {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import type { TickContext, TickWrites } from './tick-pipeline';
|
|||||||
import type { GameCoordinatorState } from './gameStore.types';
|
import type { GameCoordinatorState } from './gameStore.types';
|
||||||
import type { EnemyState } from '../types';
|
import type { EnemyState } from '../types';
|
||||||
import { applyEnemyDefenses as applyEnemyDefensesFromPipeline } from './pipelines/combat-tick';
|
import { applyEnemyDefenses as applyEnemyDefensesFromPipeline } from './pipelines/combat-tick';
|
||||||
import { computeAttackSpeedMultFromPactAffinity } from '../utils/invocation-utils';
|
|
||||||
|
|
||||||
// Track paused conversions already logged to avoid flooding the activity log every tick
|
// Track paused conversions already logged to avoid flooding the activity log every tick
|
||||||
const loggedPausedConversions = new Set<string>();
|
const loggedPausedConversions = new Set<string>();
|
||||||
@@ -290,11 +290,9 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
}));
|
}));
|
||||||
useCombatStore.setState({ equipmentSpellStates });
|
useCombatStore.setState({ equipmentSpellStates });
|
||||||
|
|
||||||
// AC-12/AC-13: Pact affinity cast speed bonus (prestige + discipline)
|
// Pact affinity cast speed bonus only applies to invocation spells (AC-12/AC-13),
|
||||||
const attackSpeedMult = computeAttackSpeedMultFromPactAffinity(
|
// computed locally in combat-invocation.ts. Active/equipment spells use base 1.0.
|
||||||
ctx.prestige.prestigeUpgrades.pactAffinity || 0,
|
const attackSpeedMult = 1;
|
||||||
disciplineEffects.bonuses.pactAffinityBonus || 0,
|
|
||||||
);
|
|
||||||
const cr = useCombatStore.getState().processCombatTick(
|
const cr = useCombatStore.getState().processCombatTick(
|
||||||
rawMana, elements, maxMana, attackSpeedMult,
|
rawMana, elements, maxMana, attackSpeedMult,
|
||||||
combatCbs.onFloorCleared,
|
combatCbs.onFloorCleared,
|
||||||
@@ -305,6 +303,7 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
(dmg, enemy, roomType, addLogFn, bypassArmor, bypassBarrier) =>
|
(dmg, enemy, roomType, addLogFn, bypassArmor, bypassBarrier) =>
|
||||||
applyEnemyDefensesFromPipeline(dmg, enemy, roomType, addLogFn, bypassArmor, bypassBarrier),
|
applyEnemyDefensesFromPipeline(dmg, enemy, roomType, addLogFn, bypassArmor, bypassBarrier),
|
||||||
equippedSwords,
|
equippedSwords,
|
||||||
|
ctx.prestige.prestigeUpgrades.pactAffinity || 0,
|
||||||
);
|
);
|
||||||
rawMana = cr.rawMana; elements = cr.elements;
|
rawMana = cr.rawMana; elements = cr.elements;
|
||||||
totalManaGathered += cr.totalManaGathered || 0;
|
totalManaGathered += cr.totalManaGathered || 0;
|
||||||
|
|||||||
@@ -198,21 +198,6 @@ export function computeDrainRateMultiplier(disciplineBonuses: DisciplineBonuses)
|
|||||||
return Math.max(MIN_DRAIN_MULTIPLIER, BASE_DRAIN_MULTIPLIER + reduction);
|
return Math.max(MIN_DRAIN_MULTIPLIER, BASE_DRAIN_MULTIPLIER + reduction);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Pact Affinity Attack Speed ───────────────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compute the effective attack speed multiplier from pact affinity.
|
|
||||||
* Combines prestige upgrade + discipline bonus, applies diminishing returns formula.
|
|
||||||
* Used by gameStore to compute the attackSpeedMult passed to processCombatTick.
|
|
||||||
*/
|
|
||||||
export function computeAttackSpeedMultFromPactAffinity(
|
|
||||||
pactAffinityUpgrade: number,
|
|
||||||
pactAffinityBonus: number,
|
|
||||||
): number {
|
|
||||||
const total = pactAffinityUpgrade + pactAffinityBonus;
|
|
||||||
return 1 * (1 + computeCastSpeedBonus(total));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Drain Per Tick ────────────────────────────────────────────────────────────
|
// ─── Drain Per Tick ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user