Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 30 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,14 @@ chomp/
│ │ ├── OkayCPU.sol # Mid-tier opponent
│ │ ├── CPUMoveManager.sol # Wraps Engine.execute for CPU-driven battles
│ │ └── ICPU.sol
│ ├── effects/ # Effect system (status effects, stat boosts, battlefield)
│ ├── effects/ # Effect system (status effects, battlefield)
│ │ ├── IEffect.sol # Effect interface with lifecycle hooks
│ │ ├── BasicEffect.sol
│ │ ├── StaminaRegen.sol
│ │ ├── StatBoosts.sol
│ │ ├── status/ # Status effects (Burn, Frostbite, Panic, Sleep, Zap) + StatusEffectLib
│ │ ├── battlefield/ # Battlefield effects (Overclock)
│ │ # NOTE: stat boosts are inlined into the Engine (see "Stat Boosts" below); the math
│ │ # helpers live in src/lib/StatBoostLib.sol, there is no StatBoosts effect contract.
│ ├── game-layer/ # Team / mon registry, gacha, exp, facets, quests, gifts
│ │ ├── GachaTeamRegistry.sol # Concrete leaf: composes the abstracts below
│ │ ├── MonOwnership.sol # monsOwned set + ownership view/check helpers
Expand Down Expand Up @@ -220,6 +221,30 @@ Effects implement `IEffect` with a bitmap indicating which lifecycle steps they

Effects can be per-mon (local) or global (battlefield-wide). The `StaminaRegen` effect is a global default that regenerates 1 stamina per turn.

### Stat Boosts (inlined into the Engine)

Stat modifiers are **not** a separate effect contract — they are native Engine functions:
`addStatBoost` / `addKeyedStatBoost` / `removeStatBoost` / `removeKeyedStatBoost` / `clearAllStatBoosts`
(declared in `IEngine`). Moves, abilities, and shared effects (`BurnStatus`, `FrostbiteStatus`,
`Overclock`, `UpOnly`, `Tinderclaws`, …) call `engine.addStatBoost(...)` directly during `execute`.
The packing/aggregation math lives in `src/lib/StatBoostLib.sol`. (Historically this was an external
`StatBoosts` effect that the Engine called back into ~10–15× per application; inlining removed those
round-trips.)

How it works:
- Each boost **source** is one packed entry stored in the mon's normal effect mapping under the
`STAT_BOOST_ADDRESS` sentinel (steps bitmap `STAT_BOOST_STEPS` = `OnMonSwitchOut | ALWAYS_APPLIES`).
Sources are keyed by `msg.sender` (or `msg.sender` + a salt string), so each move/ability/effect
stacks independently and can remove its own boost. Boosts are **multiplicative** per source; `Temp`
boosts are dropped automatically on switch-out (`_inlineStatBoostSwitchOut`), `Perm` ones persist.
- Boosts apply only to the 5 stat deltas: `Speed`, `Attack`, `Defense`, `SpecialAttack`,
`SpecialDefense`. There is **no globalKV snapshot** — the Engine telescopes off the live `monState`
delta (`new boosted − base − currentDelta`) and fires `OnUpdateMonState` like any other delta write.
- **Ownership invariant:** the stat-boost system is the *sole* writer of those 5 deltas. External
`updateMonState` calls with `Speed`..`SpecialDefense` **revert with `StatRequiresStatBoost`** — to
change a stat you must go through `add`/`removeStatBoost`, never `updateMonState`. (`Hp`, `Stamina`,
`IsKnockedOut`, `ShouldSkipTurn` remain writable via `updateMonState`.)

### Type System

16 types: Yin, Yang, Earth, Liquid, Fire, Metal, Ice, Nature, Lightning, Mythic, Air, Math, Cyber, Wild, Cosmic, None. Type effectiveness is calculated by `ITypeCalculator`.
Expand Down Expand Up @@ -300,8 +325,8 @@ Both per-mon mappings share the same 16-mon bucketing so `_applyExpAndFacetDraws
### Storage Architecture

- `BattleData` and `BattleConfig` are stored per battle key (derived from player addresses)
- `MonState` tracks deltas from base stats (hpDelta, staminaDelta, etc.)
- Effects stored in per-mon mappings with stride-based indexing (64 slots per mon)
- `MonState` tracks deltas from base stats (hpDelta, staminaDelta, etc.). The 5 stat deltas are written only by the inlined stat-boost path (see "Stat Boosts"); other deltas via `updateMonState`.
- Effects stored in per-mon mappings with stride-based indexing (64 slots per mon). Stat-boost sources reuse these same mappings under the `STAT_BOOST_ADDRESS` sentinel.
- Heavy use of bit packing for gas efficiency (KO bitmaps, effect counts, active mon indices)
- Transient storage used for per-transaction state (`battleKeyForWrite`, `tempRNG`)
- `GachaTeamRegistry`'s storage is the union of its abstract bases; each base owns its own mappings/constants so the leaf is integration-only. Reordering the inheritance list would shift slot layout — keep the order in `GachaTeamRegistry.sol` stable across deploys.
Expand Down Expand Up @@ -424,7 +449,7 @@ Effects fall into several categories depending on scope:

- **Status effects** (`src/effects/status/`): Extend `StatusEffect` which enforces one-status-per-mon via a KV flag. Shared across mons — deployed once, injected into moves via constructor parameters. (e.g., `BurnStatus`, `FrostbiteStatus`, `SleepStatus`)
- **Battlefield effects** (`src/effects/battlefield/`): Extend `BasicEffect`, use `targetIndex=2` for global scope. (e.g., `Overclock`)
- **Shared utility effects** (`src/effects/`): Deployed once, used by many contracts. (e.g., `StatBoosts` for stat modifiers, `StaminaRegen` for per-turn recovery)
- **Shared utility effects** (`src/effects/`): Deployed once, used by many contracts. (e.g., `StaminaRegen` for per-turn recovery). NOTE: stat modifiers are **not** an effect — they are inlined Engine functions (`addStatBoost`/`removeStatBoost`/…); see "Stat Boosts" above.
- **Mon-local effects** (`src/mons/<monname>/`): Abilities or move-effect hybrids that only apply to one mon. These live in the mon's directory, not in `src/effects/`.

To implement a new effect:
Expand Down
10 changes: 4 additions & 6 deletions script/EngineAndPeriphery.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import {SimplePM} from "../src/hooks/SimplePM.sol";
import {ReturnerGift} from "../src/game-layer/ReturnerGift.sol";

// Shared effects
import {StatBoosts} from "../src/effects/StatBoosts.sol";
import {BurnStatus} from "../src/effects/status/BurnStatus.sol";
import {FrostbiteStatus} from "../src/effects/status/FrostbiteStatus.sol";
import {PanicStatus} from "../src/effects/status/PanicStatus.sol";
Expand Down Expand Up @@ -117,10 +116,9 @@ contract EngineAndPeriphery is Script {

function deployGameFundamentals() public {

StatBoosts statBoosts = new StatBoosts();
deployedContracts.push(DeployData({name: "STAT BOOSTS", contractAddress: address(statBoosts)}));
// Stat boosts are now inlined into the Engine (no standalone StatBoosts contract to deploy).

Overclock overclock = new Overclock(statBoosts);
Overclock overclock = new Overclock();
deployedContracts.push(DeployData({name: "OVERCLOCK", contractAddress: address(overclock)}));

SleepStatus sleepStatus = new SleepStatus();
Expand All @@ -129,10 +127,10 @@ contract EngineAndPeriphery is Script {
PanicStatus panicStatus = new PanicStatus();
deployedContracts.push(DeployData({name: "PANIC STATUS", contractAddress: address(panicStatus)}));

FrostbiteStatus frostbiteStatus = new FrostbiteStatus(statBoosts);
FrostbiteStatus frostbiteStatus = new FrostbiteStatus();
deployedContracts.push(DeployData({name: "FROSTBITE STATUS", contractAddress: address(frostbiteStatus)}));

BurnStatus burnStatus = new BurnStatus(statBoosts);
BurnStatus burnStatus = new BurnStatus();
deployedContracts.push(DeployData({name: "BURN STATUS", contractAddress: address(burnStatus)}));

ZapStatus zapStatus = new ZapStatus();
Expand Down
33 changes: 13 additions & 20 deletions script/SetupMons.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {MonStats} from "../src/Structs.sol";
import {Type} from "../src/Enums.sol";

import {IEffect} from "../src/effects/IEffect.sol";
import {StatBoosts} from "../src/effects/StatBoosts.sol";
import {Overclock} from "../src/effects/battlefield/Overclock.sol";
import {BullRush} from "../src/mons/aurox/BullRush.sol";
import {GildedRecovery} from "../src/mons/aurox/GildedRecovery.sol";
Expand Down Expand Up @@ -123,7 +122,7 @@ contract SetupMons is Script {
address[4] memory addrs;

{
addrs[0] = address(new EternalGrudge(StatBoosts(vm.envAddress("STAT_BOOSTS"))));
addrs[0] = address(new EternalGrudge());
deployedContracts[0] = DeployData({name: "Eternal Grudge", contractAddress: addrs[0]});
}
{
Expand Down Expand Up @@ -172,7 +171,6 @@ contract SetupMons is Script {
DeployData[] memory deployedContracts = new DeployData[](4);

// Cache commonly used addresses
address statboosts = vm.envAddress("STAT_BOOSTS");
address typecalculator = vm.envAddress("TYPE_CALCULATOR");

address[4] memory addrs;
Expand All @@ -182,15 +180,15 @@ contract SetupMons is Script {
deployedContracts[0] = DeployData({name: "Chain Expansion", contractAddress: addrs[0]});
}
{
addrs[1] = address(new Initialize(StatBoosts(statboosts)));
addrs[1] = address(new Initialize());
deployedContracts[1] = DeployData({name: "Initialize", contractAddress: addrs[1]});
}
{
addrs[2] = address(new HitAndDip(ITypeCalculator(typecalculator)));
deployedContracts[2] = DeployData({name: "Hit And Dip", contractAddress: addrs[2]});
}
{
addrs[3] = address(new Interweaving(StatBoosts(statboosts)));
addrs[3] = address(new Interweaving());
deployedContracts[3] = DeployData({name: "Interweaving", contractAddress: addrs[3]});
}

Expand Down Expand Up @@ -226,17 +224,14 @@ contract SetupMons is Script {
function deployMalalien(GachaTeamRegistry registry) internal returns (DeployData[] memory) {
DeployData[] memory deployedContracts = new DeployData[](2);

// Cache commonly used addresses
address statboosts = vm.envAddress("STAT_BOOSTS");

address[2] memory addrs;

{
addrs[0] = address(new TripleThink(StatBoosts(statboosts)));
addrs[0] = address(new TripleThink());
deployedContracts[0] = DeployData({name: "Triple Think", contractAddress: addrs[0]});
}
{
addrs[1] = address(new ActusReus(StatBoosts(statboosts)));
addrs[1] = address(new ActusReus());
deployedContracts[1] = DeployData({name: "Actus Reus", contractAddress: addrs[1]});
}

Expand Down Expand Up @@ -273,7 +268,6 @@ contract SetupMons is Script {
DeployData[] memory deployedContracts = new DeployData[](5);

// Cache commonly used addresses
address statboosts = vm.envAddress("STAT_BOOSTS");
address typecalculator = vm.envAddress("TYPE_CALCULATOR");

address[5] memory addrs;
Expand All @@ -287,15 +281,15 @@ contract SetupMons is Script {
deployedContracts[1] = DeployData({name: "Unbounded Strike", contractAddress: addrs[1]});
}
{
addrs[2] = address(new Loop(Baselight(addrs[0]), StatBoosts(statboosts)));
addrs[2] = address(new Loop(Baselight(addrs[0])));
deployedContracts[2] = DeployData({name: "Loop", contractAddress: addrs[2]});
}
{
addrs[3] = address(new Brightback(ITypeCalculator(typecalculator), Baselight(addrs[0])));
deployedContracts[3] = DeployData({name: "Brightback", contractAddress: addrs[3]});
}
{
addrs[4] = address(new Renormalize(Baselight(addrs[0]), StatBoosts(statboosts), Loop(addrs[2])));
addrs[4] = address(new Renormalize(Baselight(addrs[0]), Loop(addrs[2])));
deployedContracts[4] = DeployData({name: "Renormalize", contractAddress: addrs[4]});
}

Expand Down Expand Up @@ -434,7 +428,7 @@ contract SetupMons is Script {
address[4] memory addrs;

{
addrs[0] = address(new Deadlift(StatBoosts(vm.envAddress("STAT_BOOSTS"))));
addrs[0] = address(new Deadlift());
deployedContracts[0] = DeployData({name: "Deadlift", contractAddress: addrs[0]});
}
{
Expand Down Expand Up @@ -484,13 +478,12 @@ contract SetupMons is Script {

// Cache commonly used addresses
address burnstatus = vm.envAddress("BURN_STATUS");
address statboosts = vm.envAddress("STAT_BOOSTS");
address typecalculator = vm.envAddress("TYPE_CALCULATOR");

address[5] memory addrs;

{
addrs[0] = address(new HoneyBribe(StatBoosts(statboosts)));
addrs[0] = address(new HoneyBribe());
deployedContracts[0] = DeployData({name: "Honey Bribe", contractAddress: addrs[0]});
}
{
Expand All @@ -506,7 +499,7 @@ contract SetupMons is Script {
deployedContracts[3] = DeployData({name: "Q5", contractAddress: addrs[3]});
}
{
addrs[4] = address(new Tinderclaws(IEffect(burnstatus), StatBoosts(statboosts)));
addrs[4] = address(new Tinderclaws(IEffect(burnstatus)));
deployedContracts[4] = DeployData({name: "Tinderclaws", contractAddress: addrs[4]});
}

Expand Down Expand Up @@ -619,7 +612,7 @@ contract SetupMons is Script {
deployedContracts[3] = DeployData({name: "Bull Rush", contractAddress: addrs[3]});
}
{
addrs[4] = address(new UpOnly(StatBoosts(vm.envAddress("STAT_BOOSTS"))));
addrs[4] = address(new UpOnly());
deployedContracts[4] = DeployData({name: "Up Only", contractAddress: addrs[4]});
}

Expand Down Expand Up @@ -736,7 +729,7 @@ contract SetupMons is Script {
deployedContracts[3] = DeployData({name: "Overflow", contractAddress: addrs[3]});
}
{
addrs[4] = address(new SaviorComplex(StatBoosts(vm.envAddress("STAT_BOOSTS"))));
addrs[4] = address(new SaviorComplex());
deployedContracts[4] = DeployData({name: "Savior Complex", contractAddress: addrs[4]});
}

Expand Down Expand Up @@ -779,7 +772,7 @@ contract SetupMons is Script {
deployedContracts[0] = DeployData({name: "Hard Reset", contractAddress: addrs[0]});
}
{
addrs[1] = address(new Chronoffense(StatBoosts(vm.envAddress("STAT_BOOSTS"))));
addrs[1] = address(new Chronoffense());
deployedContracts[1] = DeployData({name: "Chronoffense", contractAddress: addrs[1]});
}
{
Expand Down
4 changes: 2 additions & 2 deletions snapshots/EngineOptimizationTest.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"ExternalStaminaRegen": "410294",
"InlineStaminaRegen": "1053626"
"ExternalStaminaRegen": "416961",
"InlineStaminaRegen": "1060256"
}
38 changes: 19 additions & 19 deletions snapshots/FullyOptimizedInlineGasTest.json
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
{
"Fast_Battle1_coldGas": "1691495",
"Fast_Battle1_coldSload": "252",
"Fast_Battle1_coldGas": "1445192",
"Fast_Battle1_coldSload": "255",
"Fast_Battle1_noop": "24",
"Fast_Battle1_nzToNz": "78",
"Fast_Battle1_totalSload": "1563",
"Fast_Battle1_totalSstore": "132",
"Fast_Battle1_zToNz": "30",
"Fast_Battle2_coldGas": "1635917",
"Fast_Battle2_coldSload": "258",
"Fast_Battle1_nzToNz": "72",
"Fast_Battle1_totalSload": "1407",
"Fast_Battle1_totalSstore": "123",
"Fast_Battle1_zToNz": "27",
"Fast_Battle2_coldGas": "1362316",
"Fast_Battle2_coldSload": "259",
"Fast_Battle2_noop": "31",
"Fast_Battle2_nzToNz": "99",
"Fast_Battle2_totalSload": "1635",
"Fast_Battle2_totalSstore": "151",
"Fast_Battle2_zToNz": "20",
"Fast_Battle3_coldGas": "1220797",
"Fast_Battle3_coldSload": "252",
"Fast_Battle3_noop": "37",
"Fast_Battle3_nzToNz": "89",
"Fast_Battle3_totalSload": "1563",
"Fast_Battle3_totalSstore": "132",
"Fast_Battle3_zToNz": "4"
"Fast_Battle2_nzToNz": "87",
"Fast_Battle2_totalSload": "1489",
"Fast_Battle2_totalSstore": "137",
"Fast_Battle2_zToNz": "18",
"Fast_Battle3_coldGas": "1047852",
"Fast_Battle3_coldSload": "255",
"Fast_Battle3_noop": "38",
"Fast_Battle3_nzToNz": "78",
"Fast_Battle3_totalSload": "1407",
"Fast_Battle3_totalSstore": "123",
"Fast_Battle3_zToNz": "5"
}
6 changes: 3 additions & 3 deletions snapshots/MatchmakerTest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"Accept1": "341848",
"Accept2": "34250",
"Propose1": "197406"
"Accept1": "344188",
"Accept2": "34385",
"Propose1": "197541"
}
30 changes: 15 additions & 15 deletions snapshots/StartBattleGasTest.json
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
{
"StartBattle_Cold_coldGas": "1419128",
"StartBattle_Cold_coldSload": "119",
"StartBattle_Cold_noop": "12",
"StartBattle_Cold_coldGas": "1419479",
"StartBattle_Cold_coldSload": "120",
"StartBattle_Cold_noop": "13",
"StartBattle_Cold_nzToNz": "5",
"StartBattle_Cold_totalSload": "158",
"StartBattle_Cold_totalSstore": "81",
"StartBattle_Cold_totalSload": "159",
"StartBattle_Cold_totalSstore": "82",
"StartBattle_Cold_zToNz": "64",
"StartBattle_WarmSame_coldGas": "267111",
"StartBattle_WarmSame_coldSload": "129",
"StartBattle_WarmSame_noop": "68",
"StartBattle_WarmSame_coldGas": "267462",
"StartBattle_WarmSame_coldSload": "130",
"StartBattle_WarmSame_noop": "69",
"StartBattle_WarmSame_nzToNz": "4",
"StartBattle_WarmSame_totalSload": "167",
"StartBattle_WarmSame_totalSstore": "81",
"StartBattle_WarmSame_totalSload": "168",
"StartBattle_WarmSame_totalSstore": "82",
"StartBattle_WarmSame_zToNz": "6",
"StartBattle_WarmSteady_coldGas": "267111",
"StartBattle_WarmSteady_coldSload": "129",
"StartBattle_WarmSteady_noop": "44",
"StartBattle_WarmSteady_coldGas": "267462",
"StartBattle_WarmSteady_coldSload": "130",
"StartBattle_WarmSteady_noop": "45",
"StartBattle_WarmSteady_nzToNz": "28",
"StartBattle_WarmSteady_totalSload": "167",
"StartBattle_WarmSteady_totalSstore": "81",
"StartBattle_WarmSteady_totalSload": "168",
"StartBattle_WarmSteady_totalSstore": "82",
"StartBattle_WarmSteady_zToNz": "6"
}
9 changes: 9 additions & 0 deletions src/Constants.sol
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@ uint256 constant EFFECT_COUNT_MASK = 0x3F; // 6 bits = max count of 63

address constant TOMBSTONE_ADDRESS = address(0xdead);

// Sentinel effect address used for inlined stat-boost entries. Boost sources are stored in the
// normal per-mon effect mappings under this address; the Engine recognizes it and runs the
// inlined stat-boost switch-out logic instead of making an external IEffect call (mirrors the
// address(0) StaminaRegen inline path). It is never a real deployed contract.
address constant STAT_BOOST_ADDRESS = address(0x57B); // "STB" - stat boost
// Steps bitmap stored on inlined stat-boost effect entries: ALWAYS_APPLIES | OnMonSwitchOut (bit 5).
// Matches the legacy StatBoosts.getStepsBitmap() (0x8020) so view/round-trip behavior is unchanged.
uint16 constant STAT_BOOST_STEPS = 0x8020;

// Sentinel ruleset address: when passed as battle.ruleset, the Engine adds
// inline StaminaRegen as a global effect without calling an external contract.
address constant INLINE_STAMINA_REGEN_RULESET = address(0x57A); // "STA"mina
Expand Down
Loading