- Add isChanneling, channelSpeedMultiplier, channelDrainRate to CombatState - Add startChanneling/stopChanneling actions to combat store - Add transference-channeling discipline with 3 perks (channel-efficiency, channel-power, channel-mastery) - Add channelIntensity and channelEfficiency to KNOWN_BONUS_STATS - Create combat-channel.ts with drain + speed multiplier computation - Apply channel speed multiplier to equipment spells and melee attacks - Add Channel Transference hold-button UI to SpireCombatPage - Add compact channel status indicator to SpireCombatControls - Channel state resets on spire exit, persists across room transitions - All 1235 existing tests pass
24 KiB
Room Enchantments System — Design Spec
Describes the Room Enchantments system: a semi-combat, semi-preparation system for the Enchanter attunement. Footwear enchantments passively stamp the room with elemental auras during combat. The longer the fight, the more the room becomes the Enchanter's weapon.
1. Objective
The Enchanter currently has no active combat system. The enchanting pipeline (Design → Prepare → Apply) is entirely offline. The Room Enchantments system adds an always-on combat presence that scales with time: as the Enchanter fights in a room, their enchanted footwear gradually covers the room with elemental aura effects that damage enemies, apply debuffs, or buff the player.
Design goals:
- Give the Enchanter an active combat identity distinct from Invocation (parallel cast track) and Golemancy (summoned units): the environmental controller
- Create a "semi-combat, semi-preparation" loop: choose footwear enchantments offline, then they work passively during combat with no active input required
- Give transference mana a combat application through the
boots_sigil_transferenceenchantment and the coverage-rate discipline - Make longer fights (especially guardians) more rewarding the longer they go
- Keep the system simple: one coverage meter, linear scaling, no thresholds or activation decisions during combat
2. Identity
| Property | Value |
|---|---|
| System name | Room Enchantments |
| Attunement | Enchanter |
| Equipment slot | Feet |
| Core resource | Coverage meter (0–100) per room |
| Primary mana | Transference (discipline fuel + transference sigil regen) |
| Combat role | Environmental aura control — scales with time-in-combat |
| Preparation | Enchant boots via the existing Design → Prepare → Apply pipeline |
| Active element | None — entirely passive during combat |
Attunement Comparison
| Attunement | Combat Identity | Scaling | Player Input |
|---|---|---|---|
| Invoker | Parallel auto-cast (guardian spells) | Charge meter fill/spend | Auto-activate |
| Fabricator | Summoned golems (independent actors) | Golem maintenance | Design golems offline |
| Enchanter | Environmental aura control | Time-in-combat coverage | Enchant boots offline |
3. Core Mechanic: The Coverage Meter
3.1 The Meter
Each room has a single coverage meter from 0 to 100, stored on FloorState:
// Addition to FloorState in types/game.ts
roomEnchantment: {
coverage: number; // 0-100, current coverage percentage
} | null; // null when no footwear room enchantments are equipped
nullwhen the player has no footwear with room enchantment sigils equipped.- Initialized to
{ coverage: 0 }on room entry if footwear has room enchantments. - Resets to 0 on every room transition (fresh room, fresh canvas).
3.2 Coverage Growth
Coverage grows by a flat amount per combat tick:
coveragePerTick = baseRate + disciplineBonus
where:
baseRate = 0.2
disciplineBonus = roomCoverageRateBonus (from the `room-coverage-rate` capped perk)
At base rate (no discipline bonus):
- 0.2 per tick → 500 ticks to reach 100
- At 200ms/tick → 100 seconds real time to full coverage
Coverage growth requires ALL of the following:
- Player is in
climbaction - Current room has living enemies (
floorHP > 0) - Player has at least one footwear room enchantment equipped
Coverage does NOT scale with cast speed, attack speed, transference spending, or any other stat. It is purely time-based. This is intentional — the system is designed to be zero-input during combat.
3.3 Coverage Cap
Coverage is hard-capped at 100. Once full, room enchantments operate at full strength and stop accumulating.
4. Enchantment Definitions
4.1 Effect Category
Room enchantments use category: 'special' with unique specialId values, following the
exact same pattern as existing equipment effects (e.g., sword_fire → specialId: 'fireBlade'). They are defined alongside existing enchantment definitions.
Allowed equipment category: ['feet'] only.
4.2 Effect Structure
// Room enchantment effects use the same EnchantmentEffectDef type
// category: 'special'
// effect.type: 'special'
// effect.specialId: a unique string handled by the room enchantment tick processor
The specialId is a new convention: room_<element>_<effectType> (e.g.,
room_fire_damage, room_frost_debuff, room_transference_buff).
Note: Room enchantment
specialIdvalues use the game's existing mana type names (e.g.,deathnotpoison,transferencenotarcane) for consistency with the 22 defined mana types.
4.3 Scaling Formula
Each room enchantment's effect scales linearly with coverage:
effectMagnitude = baseMagnitude × (coverage / 100)
Examples:
boots_sigil_fireat 25% coverage: burn = 5 × 0.25 = 1.25 damage/tick to all enemiesboots_sigil_fireat 50% coverage: burn = 5 × 0.50 = 2.5 damage/tick to all enemiesboots_sigil_fireat 100% coverage: burn = 5 × 1.00 = 5 damage/tick to all enemies
All magnitudes are floating-point. Damage per tick is applied as-is (not rounded per tick; rounding only occurs on display).
4.4 Enchantment Table
| Enchant ID | Name | specialId | Effect Type | Base Magnitude (at 100%) | Capacity Cost |
|---|---|---|---|---|---|
boots_sigil_fire |
Blazing Footsteps | room_fire_damage |
Room DoT (burn all enemies) | 5 dmg/tick | 30 |
boots_sigil_frost |
Frozen Trail | room_frost_debuff |
Enemy slow | 10% slow | 25 |
boots_sigil_death |
Necrotic Tread | room_death_damage |
Room DoT (death all enemies) | 3 dmg/tick | 25 |
boots_sigil_lightning |
Shocking Stride | room_lightning_damage |
Single-target chain dmg | 3 dmg/tick random enemy | 28 |
boots_sigil_dark |
Shadow Patch | room_dark_dodge_debuff |
Enemy dodge reduction | −10% dodge | 22 |
boots_sigil_earth |
Scoured Earth | room_earth_armor_debuff |
Enemy armor reduction | −5% armor | 28 |
boots_sigil_transference_ground |
Transference Grounds | room_transference_buff |
Player cast speed | +5% cast speed | 20 |
boots_sigil_transference_path |
Conductive Path | room_transference_regen |
Transference mana regen | +10%/hr regen | 20 |
All costs fit within footwear capacity range (15–35), meaning players can fit 1–2 room enchantments per pair of boots.
4.5 Effect Application Rules
DoT effects (damage over time: fire, death, lightning):
- Applied to ALL living enemies in the room each tick
- Bypass armor — room-wide DoT ignores enemy armor, dodge, and barrier
- Do NOT apply to dead enemies (hp ≤ 0)
- For single-target effects (lightning): target is chosen randomly among living enemies each tick
Debuff effects (frost slow, dark dodge reduction, dark armor reduction):
- Applied as a modifier to all living enemies, recalculated each tick based on current coverage
- Slow: reduces enemy dodge chance (negative value subtracted from base dodge)
- Dodge reduction: directly reduces enemy dodge chance
- Armor reduction: directly reduces enemy effectiveArmor (minimum 0)
- Applied at the end of the room enchantment phase (after the DoT/debuff phase). Debuffs take effect on the next tick — there is a one-tick delay before reduced dodge/armor affects incoming attacks. This is intentional: the room enchantment phase runs late in the tick pipeline.
Buff/Regen effects (transference cast speed, transference regen):
- Applied as a modifier to the player's combat bonuses, recalculated each tick
- Stored in the combat tick result and factored into existing
computeAllEffects()math - Cast speed: added to
attackSpeedMultiplieras1 + (baseBonus × coverage%) - Transference regen: added to transference mana regen per hour as
baseRegen × coverage%
Note on debuff timing: Because the room enchantment phase runs after the DoT/debuff phase in the tick pipeline, debuffs (frost slow, dark dodge reduction, dark armor reduction) are applied to enemy state at the end of the tick. The modified enemy stats take effect on the following tick when the enemy defense pipeline runs. This means there is always a one-tick delay between coverage growth and debuff impact on incoming attacks.
4.6 Stacking Multiple Enchantments
If the player has two room enchantments on their boots (e.g., boots_sigil_fire +
boots_sigil_frost):
- Both share the same coverage meter (there is only one)
- Both scale off the same coverage percentage
- Both apply their effects independently each tick
- Total capacity cost must fit within the footwear's total capacity
5. State Changes
5.1 FloorState (types/game.ts)
export interface FloorState {
// ... all existing fields ...
// Room enchantment state — null when no footwear room enchants are equipped
roomEnchantment: {
coverage: number; // 0-100
} | null;
}
5.2 Combat Store Additions
The combat store needs a new field to persist the previous room's coverage across room
transitions (for the resonant-stamps perk). Add to CombatState in
combat-state.types.ts:
// Room enchantment carryover (for resonant-stamps perk)
lastRoomCoverage: number; // 0-20, carryover from previous room, resets on spire exit
And the corresponding action:
setLastRoomCoverage: (value: number) => void;
5.3 Combat Processing
The room enchantment tick is processed in combat-actions.ts → processCombatTick(),
executed in the tick pipeline immediately after the DoT/debuff phase:
Tick order in processCombatTick:
1. Golem maintenance
2. Active spell casting
3. Equipment spell states
4. Invocation tick
5. Melee attacks
6. Golem attacks
7. DoT/debuff tick processing
8. ── Room enchantment tick ← NEW
> **File size note:** `combat-actions.ts` is currently 377 lines. Adding the room
> enchantment phase may push it toward the 400-line limit. If so, extract the phase
> logic into a new file (e.g., `combat-room-enchantments.ts`) and call it from
> `processCombatTick`.
5.4 New Utility File
A new file src/lib/game/utils/room-enchantments-utils.ts exports:
// Compute coverage per tick from discipline stats
export function computeCoveragePerTick(disciplineBonus: number): number;
// Apply all room enchantment effects for one tick
// Returns updated { enemies, rawMana, elementStates, playerBuffs }
export function applyRoomEnchantmentTick(params: {
coverage: number;
auraMagnitude: number; // roomEnchantmentAuraMagnitude from discipline effects
equippedRoomEnchantments: Array<{ specialId: string; baseMagnitude: number }>;
enemies: EnemyState[];
rawMana: number;
elements: Record<string, { current: number; max: number; unlocked: boolean }>;
}): {
enemies: EnemyState[];
rawMana: number;
elements: Record<string, { current: number; max: number; unlocked: boolean }>;
playerBuffs: {
castSpeedBonus: number;
transferenceRegenBonus: number;
};
};
// Get list of room enchantment effects from equipped footwear
export function getEquippedRoomEnchantments(
equippedInstances: Record<string, string | null>,
equipmentInstances: Record<string, EquipmentInstance>,
): Array<{ specialId: string; baseMagnitude: number }>;
5.5 Room Transition Behavior
Coverage resets on every room transition. This is handled in the existing
advanceRoomOrFloor() flow in combat-descent-actions.ts:
- When a new
FloorStateis generated (viagenerateSpireFloorState()), the new room starts withroomEnchantment: null - Just before the
FloorStateis finalized, the current room's coverage is saved tolastRoomCoverageon the combat store (capped at 20 ifresonant-stampsperk is active, otherwise 0) - At the start of the combat tick, if
roomEnchantmentis null and the player has footwear room enchantments equipped, it is initialized to{ coverage: lastRoomCoverage } - If the player has no footwear room enchantments, it stays null and the room enchantment tick is skipped entirely
lastRoomCoverageresets to 0 on spire exit
6. Discipline Integration
6.1 New Discipline: Room Enchanting
A new Enchanter discipline that directly enhances the Room Enchantment system.
Definition:
| Field | Value |
|---|---|
| ID | room-enchanting |
| Name | Room Enchanting |
| Attunement | enchanter |
| Mana Type | transference |
| Base Cost | 10 |
| Stat Bonus | roomEnchantmentAuraMagnitude +0.10 (base) |
| Scaling Factor | 100 |
| Difficulty Factor | 150 |
| Drain Base | 3 |
Main stat: roomEnchantmentAuraMagnitude
This stat is a multiplier on all room enchantment aura magnitudes:
effectMagnitude = baseMagnitude × coverage% × (1 + roomEnchantmentAuraMagnitude)
The stat scales with XP via the standard discipline math:
StatBonus = baseValue × (XP / scalingFactor)^0.65
= 0.10 × (XP / 100)^0.65
This replaces the old empowered-auras perk — the main stat now continuously scales
aura strength, while a new capped perk handles coverage rate.
Magnitude scaling at key XP levels:
| XP | Stat Bonus | Magnitude Multiplier | Fire DoT @ 100% coverage |
|---|---|---|---|
| 0 | +0.000 | 1.000× | 5.00 dmg/tick |
| 100 | +0.100 | 1.100× | 5.50 dmg/tick |
| 300 | +0.204 | 1.204× | 6.02 dmg/tick |
| 500 | +0.285 | 1.285× | 6.42 dmg/tick |
| 1000 | +0.447 | 1.447× | 7.23 dmg/tick |
| 2000 | +0.701 | 1.701× | 8.50 dmg/tick |
Perks:
| Perk ID | Type | Threshold | Bonus | Description |
|---|---|---|---|---|
room-coverage-rate |
capped |
100 | Coverage rate +0.03/tick per tier, max 4 tiers | Room fills faster with each tier |
resonant-stamps |
once |
500 | Carry 20% coverage between rooms (max 20% starting coverage) | Head start on each room, but never instant |
Perk details:
-
room-coverage-rate(capped, threshold 100 XP, interval 150 XP, max 4 tiers): Each tier adds +0.03/tick to the coverage growth rate. Tier progression: 1 tier at 100 XP, 2 tiers at 250 XP, 3 tiers at 400 XP, 4 tiers at 550 XP (capped).Tiers Rate Bonus Total Rate Seconds to Fill 0 (no perk) +0.00 0.20/tick 100.0s 1 (100 XP) +0.03 0.23/tick 87.0s 2 (250 XP) +0.06 0.26/tick 76.9s 3 (400 XP) +0.09 0.29/tick 69.0s 4 (550 XP) +0.12 0.32/tick 62.5s At max tier (4), the room fills in ~62.5 seconds instead of 100 seconds — a 37.5% speedup. The perk is capped so coverage always requires meaningful combat time.
-
resonant-stamps(once @ 500 XP): When transitioning between rooms on the same floor, the carryover is computed asmin(20, previousRoom.coverage × 0.2). This means 20% of the previous room's coverage value carries over, capped at a maximum of 20 percentage points. Examples: ending at 80% → next room starts at 16%; ending at 100% → next room starts at 20% (the cap). This ensures the player always needs to actively build coverage during combat — the perk provides a head start but never eliminates the buildup phase.State persistence: The carryover value is stored in
lastRoomCoverageon the combat store (see §5.2). WhenadvanceRoomOrFloor()generates a newFloorStatewithroomEnchantment: null, the room enchantment tick readslastRoomCoverageto initialize the new room's starting coverage. This field persists across room transitions within a climb but resets to 0 on spire exit.
Combined progression at key XP levels (stat + capped perk):
| XP | Coverage Rate | Magnitude | Fire DoT @ 100% | Time to Fill |
|---|---|---|---|---|
| 0 | 0.20/tick | 1.000× | 5.00 dmg/tick | 100.0s |
| 100 | 0.23/tick | 1.100× | 5.50 dmg/tick | 87.0s |
| 300 | 0.26/tick | 1.204× | 6.02 dmg/tick | 76.9s |
| 500 | 0.29/tick | 1.285× | 6.42 dmg/tick | 69.0s |
| 1000 | 0.32/tick | 1.447× | 7.23 dmg/tick | 62.5s |
| 2000 | 0.32/tick | 1.701× | 8.50 dmg/tick | 62.5s |
The capped perk maxes out at 550 XP (4 tiers, 0.32/tick, 62.5s). Beyond that, only the main stat continues to grow, increasing aura magnitude. At 2000 XP, the room still takes 62.5 seconds to fill but fire does 8.50 dmg/tick instead of 5.00 — a 70% damage increase from the main stat alone.
6.2 Discipline Dependency
room-enchanting (root — no prerequisites)
This is a root discipline with no prerequisites, making it available as soon as the Enchanter attunement is active.
6.3 Stat Registration
The new stat roomEnchantmentAuraMagnitude must be added to:
KNOWN_BONUS_STATSindiscipline-effects.ts- The
addBonus()routing incomputeDisciplineEffects()(no special routing needed — it's a standard bonus stat)
6.4 Enchantment Unlock Perks
Room enchantment effects are unlocked via perks on the new discipline:
| Perk ID | Type | Threshold | Unlocks |
|---|---|---|---|
room-sigil-fire |
once |
50 | boots_sigil_fire |
room-sigil-frost |
once |
75 | boots_sigil_frost |
room-sigil-death |
once |
100 | boots_sigil_death |
room-sigil-lightning |
once |
125 | boots_sigil_lightning |
room-sigil-dark |
once |
150 | boots_sigil_dark |
room-sigil-earth |
once |
175 | boots_sigil_earth |
room-sigil-transference-ground |
once |
100 | boots_sigil_transference_ground |
room-sigil-transference-path |
once |
125 | boots_sigil_transference_path |
7. Data Flow Summary
gameStore.tick()
→ buildTickContext() [snapshots all stores]
→ processCombatTick() [combat-actions.ts]
→ ... (golem, spell, equipment, invocation, melee, DoT phases) ...
→ Room Enchantment Phase:
1. If roomEnchantment is null and player has footwear room enchants:
Initialize roomEnchantment = { coverage: lastRoomCoverage }
2. If roomEnchantment is not null AND floorHP > 0:
a. coverage += 0.2 + roomCoverageRateBonus
b. Clamp coverage to 100
c. Magnitude multiplier = 1.0 + roomEnchantmentAuraMagnitude
d. For each equipped room enchantment:
- Compute magnitude = baseMagnitude × (coverage / 100) × magnitude multiplier
e. Apply to enemies (DoT/debuffs) or player (buffs)
f. Recalculate floorHP from updated enemy HP
→ applyTickWrites() [writes combat store changes back]
On room transition (advanceRoomOrFloor):
→ Before generating new FloorState, save current coverage to combat store:
lastRoomCoverage = resonant-stamps active ? min(20, currentRoom.coverage * 0.2) : 0
→ New FloorState generated with roomEnchantment: null
→ Room enchantment tick reads lastRoomCoverage to initialize starting coverage
8. UI Changes
8.1 RoomDisplay Component
Add a Room Enchantment Bar to the combat room display in RoomDisplay.tsx, shown
only when the player has footwear room enchantments equipped and the room has living
enemies:
┌─────────────────────────────────────────────┐
│ 🔥 Blazing Footsteps ████████████░░░░ 62% │
│ → Enemies burning: 3.1 dmg/tick │
│❄️ Frozen Trail ████████████░░░░ 62% │
│ → Enemies slowed: 6.2% │
└─────────────────────────────────────────────┘
- Progress bar showing coverage percentage (0–100%)
- Color-coded by element (fire = red, frost = blue, etc.)
- Shows current effect magnitude in text below each bar
- Only displayed when
roomEnchantmentis not null
8.2 Enchantment Designer
Room enchantment effects appear in the existing EffectSelector component under a new
"Room" filter category (or under "Special" with a "Room Only" tag). They are only
selectable when the equipment type being designed for is in the feet category.
8.3 No New Tabs
The room enchantment system does not require a new tab. All information is visible in the existing SpireCombatPage layout and the existing CraftingTab enchantment designer.
9. Acceptance Criteria
| # | Criterion |
|---|---|
| AC-1 | roomEnchantment field exists on FloorState and defaults to null. |
| AC-2 | Coverage initializes to { coverage: 0 } on room entry when footwear has room enchantments equipped. |
| AC-3 | Coverage resets to 0 on every room transition (new room = fresh coverage). |
| AC-4 | Coverage grows at 0.2 per tick while in climb action with living enemies. |
| AC-5 | Coverage is hard-capped at 100. |
| AC-6 | Coverage does NOT grow when floorHP <= 0 or currentAction !== 'climb'. |
| AC-7 | At base rate (no discipline), coverage reaches 100 in ~100 seconds real time (500 ticks). |
| AC-8 | Room DoT effects (fire, death, lightning) apply to ALL living enemies each tick, bypassing armor. |
| AC-9 | Room debuff effects (frost slow, dark dodge reduction, earth armor reduction) modify enemy stats at end of room enchantment phase (one-tick delay before affecting incoming attacks). |
| AC-10 | Room buff effects (transference cast speed, transference regen) are factored into player stat computation. |
| AC-11 | Effect magnitudes scale linearly: baseMagnitude × (coverage / 100). |
| AC-12 | Multiple room enchantments on one pair of boots share the same coverage meter and apply independently. |
| AC-13 | Total capacity cost of room enchantments must fit within footwear capacity (15–35). |
| AC-14 | The room-enchanting discipline uses transference mana and has roomEnchantmentAuraMagnitude as its stat. |
| AC-15 | room-coverage-rate capped perk adds +0.03/tick per tier, max 4 tiers. |
| AC-16 | room-coverage-rate capped perk reaches max 4 tiers at 550 XP (0.12/tick, 62.5s to fill). |
| AC-17 | resonant-stamps once perk computes carryover as min(20, previousRoom.coverage × 0.2), stored in lastRoomCoverage on the combat store. |
| AC-18 | Room enchantment effects are unlocked via discipline perks at the specified XP thresholds. |
| AC-19 | Room enchantment effects only appear in the EffectSelector when designing for feet equipment. |
| AC-20 | RoomDisplay shows coverage bar and current effect magnitudes when room enchantments are active. |
| AC-21 | Existing saves without roomEnchantment field default to null (backward compatible). |
| AC-22 | Existing saves without roomEnchantmentAuraMagnitude stat default to 0 (backward compatible). |
10. Files Reference
| File | Role |
|---|---|
src/lib/game/types/game.ts |
Add roomEnchantment field to FloorState |
src/lib/game/data/enchantments/special-effects.ts |
Add room enchantment effect definitions (8 effects) |
src/lib/game/data/disciplines/enchanter.ts |
Add room-enchanting discipline definition with perks |
Implementation note:
enchanter.tscurrently has 4 disciplines (146 lines). Addingroom-enchantingwith its ~8 unlock perks may push it toward the 400-line limit. If so, create a new fileenchanter-combat.ts(following the existing pattern ofenchanter-utility.ts,enchanter-spells.ts, etc.) and re-export fromdata/disciplines/index.ts. |src/lib/game/effects/discipline-effects.ts| AddroomEnchantmentAuraMagnitudetoKNOWN_BONUS_STATS| |src/lib/game/stores/combat-actions.ts| Add room enchantment phase inprocessCombatTick(after DoT phase) | |src/lib/game/utils/room-enchantments-utils.ts| NEW — Coverage computation, effect application, equipment scanning | |src/components/game/tabs/SpireCombatPage/RoomDisplay.tsx| Add coverage bar and effect magnitude display | |src/components/game/crafting/EnchantmentDesigner/EffectSelector.tsx| Show room enchantments when designing for feet | |docs/specs/attunements/enchanter/systems/room-enchantments-spec.md| THIS FILE |
11. Out of Scope
- Active player input during combat (no button to spend transference for faster coverage)
- Coverage scaling with cast speed, attack speed, or any combat stat
- Coverage persisting between rooms at launch (added only via
resonant-stampsperk) - Room enchantments on non-footwear equipment slots
- Room enchantments interacting with golem combat
- Room enchantments affecting non-combat rooms (library, recovery, treasure, puzzle)
- Visual effects / animations for the coverage bar (UI-only indicator in v1)
- Room enchantment effects that heal the player (violates no-healing rule)
- More than 8 room enchantment types in v1
- Composite or exotic element room sigils in v1 (base 7 + transference only)