Preserve the spell-encumbrance mechanic and initial formula. Use one Go equipment aggregator and central spell-cost calculator, and send typed values rather than server-rendered text.
The server remains authoritative, consumes versioned compiled content, and exposes bounded generated Game Protocol 1 messages. Pure rules may use a specifically approved typed CEL environment. Starlark is not part of this issue unless the separate residual-scripting decision explicitly approves it.
Preserved product/design specification and historical implementation notes
Why
Armour's mana penalty is currently an avoidable downtime tax rather than a meaningful combat tradeoff.
A player can rename each equipped piece (for example Set1Helm, Set1Chest, and so on) and bind a semicolon-separated sequence of /apply commands. The client expands that binding into individual commands, and the server eventually processes them through the normal action budget. This makes it easy to:
- unwear the complete protective loadout after combat;
- regenerate mana at the unarmoured rate and repeatedly charge power crystals;
- rewear the heavy loadout before the next encounter.
Manual inventory use has the same outcome. Renaming, keybindings, and command batching only automate the exploit; restricting those interfaces or increasing /apply delay would preserve "strip to recover, dress to fight" as optimal play.
Existing behavior and code path
server/src/server/living.c:living_update_player_item() adds each applied non-weapon item's last_heal (aliased as ARMOUR_SPELLS) to player::gen_sp_armour.
server/src/types/player.c:player_do_some_living() immediately scales mana regeneration by 10 / MAX(gen_sp_armour, 10). Full plate alone has last_heal 30, so it cuts regeneration to one third; a complete heavy set stacks much higher.
server/src/types/common/apply.c:object_apply_item() toggles FLAG_APPLIED and calls living_update() immediately. There is no retained burden or recovery debt after an item is un-worn.
client/src/client/keybind.c:keybind_process() deliberately splits bindings on semicolons. server/src/server/commands.c gives each /apply a delay and server/src/socket/server.c queues commands, but command timing does not change the eventual regeneration advantage.
server/src/types/power_crystal.c transfers mana directly between the player and crystal. Its next refill therefore benefits from the currently equipped regeneration rate.
- The authored penalty occurs on 66 armour definitions under
arch/items/armour/, with values from 0 to 30.
Outcome
Replace the swap-sensitive armour mana-regeneration penalty with spell encumbrance: applied armour increases the mana cost of direct spellcasting, while passive mana regeneration no longer depends on armour.
This puts the protection-versus-magic tradeoff in the same period in which protection matters. Regenerating or charging crystals naked would be no faster than doing so in armour, and equipping armour after reaching full mana would not bypass the cost paid for casting in it.
This is an intentional gameplay/content semantic change. The repository's greenfield posture does not require preserving the old meaning.
Proposed player contract
- Each applicable armour piece contributes a non-negative spell-encumbrance value while worn.
- Direct player-cast spells use one bounded multiplier derived from the summed equipped value. As an initial balance baseline, interpret existing values as percentage points:
adjusted_cost = round(base_cost * (100 + encumbrance) / 100);
- never reduce the base cost;
- use checked/saturating intermediate arithmetic and one documented rounding rule shared by validation, deduction, and display.
- Tune the curve and the 66 existing values against representative light, medium, and heavy complete loadouts before landing. Do not mechanically preserve the old denominator's extreme full-set multipliers.
- Armour does not alter passive mana regeneration. Positive/negative regeneration bonuses from explicitly magical effects can remain a separate mechanic.
- The surcharge applies to ordinary mana-funded player casting. Wands, rods, scrolls, potions, NPC casting, and other device-owned casts do not acquire a mana cost merely because their user wears armour.
- Spell tooltips and item examination show the effective cost and clear wording such as
spell encumbrance +N%; they must not continue to call the property “armour mana regen”.
- Changing equipment immediately changes subsequent spell costs. Existing current mana is neither refunded nor removed merely by changing armour.
/apply, renaming, keybindings, and normal equipment swapping remain supported.
Proposed implementation
1. Give the stat one explicit owner
In server/src/include/player.h, replace gen_sp_armour with a sufficiently wide spell-encumbrance aggregate. Reset and populate it in living_update_player() / living_update_player_item().
Prefer an explicit author-facing armour field and accessor such as spell_encumbrance / ARMOUR_SPELL_ENCUMBRANCE. If the polymorphic object layout continues to store it in last_heal, keep that detail behind the accessor and make the loader/editor/type documentation unambiguous; do not expose a new gameplay meaning only as a misleading “last heal” field.
Migrate every current producer under arch/items/armour/, relevant artifact definitions, editor metadata in arch/dev/editor/conf/types.xml, generated/source documentation, collection inputs, and Python field exposure if the field contract changes.
2. Remove armour from regeneration
Delete the gen_sp = gen_sp * 10 / MAX(pl->gen_sp_armour, 10) adjustment from player_do_some_living() and remove the equipment-change messages about armour impairing concentration by slowing regeneration.
Keep gen_sp, meditation's out-of-combat modifier, food consumption, regeneration remainders, and CS_STAT_REG_MANA behavior otherwise intact. With equal magical regen bonuses, the client-reported and real passive rate must be identical before and after wearing armour.
3. Apply encumbrance at the central spell-cost seam
Extend SP_level_spellpoint_cost() in server/src/server/spell_util.c (or replace it with a clearly named central cost helper) so a player caster's equipped spell encumbrance is included exactly once.
That helper already feeds:
- the insufficient-mana check in
cast_spell();
- the cost returned to
server/src/types/spell.c for mana deduction;
- monster cost checks/deduction, which must remain unaffected;
- spell cost serialization in
server/src/socket/item.c.
Ensure direct casts triggered through plugins or other current callers cannot skip the same calculation. Device casts whose caster is the device should retain their existing resource rules.
4. Refresh client-visible costs on loadout changes
server/src/socket/request.c currently resends UPD_EXTRA for inventory spell objects only when the Wizardry level changes, via last_spell_cost_level. Include the last sent encumbrance value in this invalidation condition so equipping or un-equipping relevant armour refreshes every displayed spell cost immediately.
Coordinate the final cost contract with #18 and #14, which will classify casting traditions and later validate their mana behavior, without waiting for those larger progression issues to make this exploit non-optimal.
5. Update descriptions and content semantics
Change server/src/server/item.c so armour descriptions report spell encumbrance rather than a negative regeneration value. Update field/type documentation and editor labels to match.
Audit complete loadouts, not just individual pieces. A current representative heavy set (full plate, plate helmet, plate leggings, plate gauntlets, bracers, and a heavy shield) sums many independently authored penalties; the final formula and values must produce an intentional total rather than accidental runaway cost.
Alternatives not proposed
- Block semicolon bindings or renamed-item matching: manual clicking and separate bindings still work, while legitimate command composition regresses.
- Increase equip/un-equip delay: adds waiting but leaves stripping optimal after every fight.
- Retain an invisible penalty for N seconds after un-equipping: reduces rapid swapping but creates stale state, remains exploitable by waiting, and makes item behavior harder to understand.
- Forbid armour changes in combat: may be a useful general equipment rule later, but it does not address post-combat recovery and crystal charging.
- Reduce maximum mana while armoured: equipping can clamp current mana, but it changes build capacity more drastically and does not communicate the intended casting-efficiency tradeoff as directly.
Tests and validation
- Add focused server tests for encumbrance aggregation across every relevant equipment slot, replacement/unapply behavior, two rings/trinkets as negative controls, and totals beyond an 8-bit range.
- Prove HP/mana regeneration and
CS_STAT_REG_MANA are unchanged when the same character wears or removes mundane encumbering armour.
- Test the centralized spell cost for no armour, representative light/medium/heavy complete sets, exact rounding boundaries, maximum bounded totals, insufficient mana, and actual post-cast deduction.
- Verify equipping armour after regenerating to full still makes the next direct spell cost the armoured amount.
- Verify wands, rods, scrolls, potions, NPCs, and other non-mana device casts retain their existing costs.
- Verify power-crystal transfer is unchanged and the time to naturally replace transferred mana is identical clothed and naked.
- Verify spell inventory/tooltips refresh after every encumbrance-changing loadout update and match the amount deducted by the server.
- Collect/check the migrated armour content and inspect generated diffs separately.
- Build the legacy server and client as required by any touched shared/UI contract, run the focused C suites and prepared server checks, then run formatting and
git diff --check.
- Manually reproduce the renamed, semicolon-bound full-set sequence and confirm it offers no regeneration or crystal-charging advantage.
Acceptance criteria
Content-schema dependency
This issue is a registered consumer of atrinik/content#15's authoritative schema/field metadata and atrinik/content#8's authored-source migration. Its new fields, registries, reference rules, editor descriptions, loader/compiler tables, Python exposure, documentation, and checker rules must be generated or consumed from that shared source of truth rather than added as a second hand-maintained catalog. If a temporary legacy adapter is required before migration completes, it must be generated from the atrinik/content#15-owned metadata and removed by atrinik/content#8.
Important
This issue is implemented in the fresh MIT-licensed Go server under the replacement program. Its gameplay and content-design decisions remain authoritative. C, CPython, classic packet, file-path, and enum details in the preserved specification are historical evidence only; do not copy, translate, or structurally port GPL implementation code.
Replacement implementation contract
Preserve the spell-encumbrance mechanic and initial formula. Use one Go equipment aggregator and central spell-cost calculator, and send typed values rather than server-rendered text.
The server remains authoritative, consumes versioned compiled content, and exposes bounded generated Game Protocol 1 messages. Pure rules may use a specifically approved typed CEL environment. Starlark is not part of this issue unless the separate residual-scripting decision explicitly approves it.
Required verification
Preserved product/design specification and historical implementation notes
Why
Armour's mana penalty is currently an avoidable downtime tax rather than a meaningful combat tradeoff.
A player can rename each equipped piece (for example
Set1Helm,Set1Chest, and so on) and bind a semicolon-separated sequence of/applycommands. The client expands that binding into individual commands, and the server eventually processes them through the normal action budget. This makes it easy to:Manual inventory use has the same outcome. Renaming, keybindings, and command batching only automate the exploit; restricting those interfaces or increasing
/applydelay would preserve "strip to recover, dress to fight" as optimal play.Existing behavior and code path
server/src/server/living.c:living_update_player_item()adds each applied non-weapon item'slast_heal(aliased asARMOUR_SPELLS) toplayer::gen_sp_armour.server/src/types/player.c:player_do_some_living()immediately scales mana regeneration by10 / MAX(gen_sp_armour, 10). Full plate alone haslast_heal 30, so it cuts regeneration to one third; a complete heavy set stacks much higher.server/src/types/common/apply.c:object_apply_item()togglesFLAG_APPLIEDand callsliving_update()immediately. There is no retained burden or recovery debt after an item is un-worn.client/src/client/keybind.c:keybind_process()deliberately splits bindings on semicolons.server/src/server/commands.cgives each/applya delay andserver/src/socket/server.cqueues commands, but command timing does not change the eventual regeneration advantage.server/src/types/power_crystal.ctransfers mana directly between the player and crystal. Its next refill therefore benefits from the currently equipped regeneration rate.arch/items/armour/, with values from 0 to 30.Outcome
Replace the swap-sensitive armour mana-regeneration penalty with spell encumbrance: applied armour increases the mana cost of direct spellcasting, while passive mana regeneration no longer depends on armour.
This puts the protection-versus-magic tradeoff in the same period in which protection matters. Regenerating or charging crystals naked would be no faster than doing so in armour, and equipping armour after reaching full mana would not bypass the cost paid for casting in it.
This is an intentional gameplay/content semantic change. The repository's greenfield posture does not require preserving the old meaning.
Proposed player contract
adjusted_cost = round(base_cost * (100 + encumbrance) / 100);spell encumbrance +N%; they must not continue to call the property “armour mana regen”./apply, renaming, keybindings, and normal equipment swapping remain supported.Proposed implementation
1. Give the stat one explicit owner
In
server/src/include/player.h, replacegen_sp_armourwith a sufficiently wide spell-encumbrance aggregate. Reset and populate it inliving_update_player()/living_update_player_item().Prefer an explicit author-facing armour field and accessor such as
spell_encumbrance/ARMOUR_SPELL_ENCUMBRANCE. If the polymorphic object layout continues to store it inlast_heal, keep that detail behind the accessor and make the loader/editor/type documentation unambiguous; do not expose a new gameplay meaning only as a misleading “last heal” field.Migrate every current producer under
arch/items/armour/, relevant artifact definitions, editor metadata inarch/dev/editor/conf/types.xml, generated/source documentation, collection inputs, and Python field exposure if the field contract changes.2. Remove armour from regeneration
Delete the
gen_sp = gen_sp * 10 / MAX(pl->gen_sp_armour, 10)adjustment fromplayer_do_some_living()and remove the equipment-change messages about armour impairing concentration by slowing regeneration.Keep
gen_sp, meditation's out-of-combat modifier, food consumption, regeneration remainders, andCS_STAT_REG_MANAbehavior otherwise intact. With equal magical regen bonuses, the client-reported and real passive rate must be identical before and after wearing armour.3. Apply encumbrance at the central spell-cost seam
Extend
SP_level_spellpoint_cost()inserver/src/server/spell_util.c(or replace it with a clearly named central cost helper) so a player caster's equipped spell encumbrance is included exactly once.That helper already feeds:
cast_spell();server/src/types/spell.cfor mana deduction;server/src/socket/item.c.Ensure direct casts triggered through plugins or other current callers cannot skip the same calculation. Device casts whose caster is the device should retain their existing resource rules.
4. Refresh client-visible costs on loadout changes
server/src/socket/request.ccurrently resendsUPD_EXTRAfor inventory spell objects only when the Wizardry level changes, vialast_spell_cost_level. Include the last sent encumbrance value in this invalidation condition so equipping or un-equipping relevant armour refreshes every displayed spell cost immediately.Coordinate the final cost contract with #18 and #14, which will classify casting traditions and later validate their mana behavior, without waiting for those larger progression issues to make this exploit non-optimal.
5. Update descriptions and content semantics
Change
server/src/server/item.cso armour descriptions report spell encumbrance rather than a negative regeneration value. Update field/type documentation and editor labels to match.Audit complete loadouts, not just individual pieces. A current representative heavy set (full plate, plate helmet, plate leggings, plate gauntlets, bracers, and a heavy shield) sums many independently authored penalties; the final formula and values must produce an intentional total rather than accidental runaway cost.
Alternatives not proposed
Tests and validation
CS_STAT_REG_MANAare unchanged when the same character wears or removes mundane encumbering armour.git diff --check.Acceptance criteria
Content-schema dependency
This issue is a registered consumer of atrinik/content#15's authoritative schema/field metadata and atrinik/content#8's authored-source migration. Its new fields, registries, reference rules, editor descriptions, loader/compiler tables, Python exposure, documentation, and checker rules must be generated or consumed from that shared source of truth rather than added as a second hand-maintained catalog. If a temporary legacy adapter is required before migration completes, it must be generated from the atrinik/content#15-owned metadata and removed by atrinik/content#8.