goldbox.effects

The active-effect arrays at SAVEDGAME0 $4900, and the spell table that gives a new one its duration.

Ported out of automap/live.py on #13 (Edit traits and active effects, in two separate panels), because the panels that read and write this belong to editor/, and editor/ may not import automaptests/test_wish.py:: test_editor_imports_nothing_live is what enforces it. automap/live.py imports these same names back, so automap/combat.py, tools/combatshot.py, tools/livestrip.py and tests/test_coldread.py still resolve them as live.EFFECT_ID_OFFSET and so on.

No Qt and no emulator here, the same rule the rest of goldbox/ follows – load_effect_table is the one function that touches a disk, and it is bytes in, bytes out, exactly like goldbox/items.py’s load_item_names.

These are not the ten trait slots at record `0x0AD`. The two meet only at LIBRARY $4028 and the $9AD5 dispatch and share one code namespace (goldbox/traits.py); the active effects here are 64 slots for the whole save, own a character, a monster or the party, and expire. A trait never does. docs/133-active-effects.md is the plan for the two panels this module feeds.

Functions

active_effects(save0_bytes)

Every effect slot whose id is non-zero, read across all four arrays.

clear_effect(payload, slot)

Zero one slot across all four arrays.

load_effect_table(disk)

Read ECL65's spell-effect table off a game disk, keyed by the record's own position -- spell id 1-56, then item-only effect argument 57-67.

write_effect(payload, slot, id, owner, ...)

Write one slot across all four arrays.

Classes

Effect

One slot of the effect table.

EffectTableEntry

One record of ECL65's spell-effect table.

class goldbox.effects.Effect[source]

Bases: object

One slot of the effect table.

Expiry clears only the id, so a slot with id 0 is free whatever the other three arrays still hold. active_effects filters on exactly that, and anything that skips it shows effects that ended hours ago.

__init__(slot, id, owner, duration, magnitude)
Parameters:
Return type:

None

property detail: str
duration: int
id: int
property label: str

effect 12 – always the number, never a looked-up name.

goldbox/traits.py does hold an id-to-name table (traits.describe), so the old claim that no such table existed anywhere in the project was wrong. label still prints the bare number, deliberately: it is what a debug log shows for an effect no condition badge covers, and the point there is to say an id is unbadged, not to name it. A caller that wants the name calls traits.describe(effect.id, game) itself.

magnitude: int
property monster: bool
owner: int
property party_wide: bool
property remaining: int

How much time is left, in whatever unit the top two bits select.

slot: int
property unit: int
class goldbox.effects.EffectTableEntry[source]

Bases: object

One record of ECL65’s spell-effect table.

duration is bits 0-5 a count and bits 6-7 a time unit not decoded, the same shape Effect.duration carries – and it is frequently 0: a spell whose whole duration scales with the caster sets only per_level. ENLARGE is exactly this: duration 0, per_level $0A, and a level-1 cast measured live wrote effect duration $0A – one level’s worth, docs/50-experiments.md confirms the live write and this table gives the per-level rate it came from.

__init__(duration, per_level, castable_outside_combat, message_index)
Parameters:
  • duration (int)

  • per_level (int)

  • castable_outside_combat (bool)

  • message_index (int)

Return type:

None

castable_outside_combat: bool
duration: int
message_index: int
per_level: int
goldbox.effects.active_effects(save0_bytes)[source]

Every effect slot whose id is non-zero, read across all four arrays.

Takes the payload, so it is already title-independent: the offsets are inside the save image and follow it wherever the title loads it.

Parameters:

save0_bytes (bytes)

Return type:

tuple[goldbox.effects.Effect, …]

goldbox.effects.clear_effect(payload, slot)[source]

Zero one slot across all four arrays.

Tidier than the game’s own clear: CAMP $131F expires an effect by clearing $4900,X alone and leaving owner, duration and magnitude as residue (docs/125-bug-notes.md N7 – PORSAVE13 carries six slots with a leftover magnitude of 1 from effects that had already lapsed). A slot this function clears is fully free, not merely free by the one field active_effects happens to filter on.

Parameters:
Return type:

None

goldbox.effects.load_effect_table(disk)[source]

Read ECL65’s spell-effect table off a game disk, keyed by the record’s own position – spell id 1-56, then item-only effect argument 57-67.

Pattern: goldbox/items.py::load_item_names. ECL65’s own PRG header claims load address $1000, not $9900 – like every other game overlay (docs/50-experiments.md, “every game overlay loads at $0800, not the $1000 its header claims”), the header is not where the loader actually puts it, and load_payload never reads the header for anything but its length. Record 1’s duration byte reading $06 – CONFIRMED live as BLESS’s – is what says these are the right 469 bytes.

Parameters:

disk (goldbox.d64.D64 | str)

Return type:

dict[int, goldbox.effects.EffectTableEntry]

goldbox.effects.write_effect(payload, slot, id, owner, duration, magnitude)[source]

Write one slot across all four arrays.

Each of the five arguments is a raw byte; nothing here validates that the combination means anything the game would recognise – owner is not checked against PARTY_WIDE/FIRST_MONSTER and magnitude is not checked against whichever ids in docs/50-experiments.md turn out to carry restore data – M2 of #13 (Edit traits and active effects, in two separate panels). Callers behind a write path decide what is safe to offer; this only puts the bytes where the four arrays expect them.

Parameters:
Return type:

None