Summary
JSHack has reached the point where its current interaction and exception-handling model is becoming a maintenance threat.
The problem is not that the game has become too weird. The weirdness is the point. The problem is that too much weirdness is being encoded as bespoke procedural behavior inside heavyweight systems and object-specific payload files.
The path forward is to replace those behavior caves with a proper authoring surface:
Intent = requested action
Rule = decision procedure
Tx = controlled mutation boundary
Fact = committed truth
Event = notification / presentation / integration
Trace = diagnostic explanation
The goal is not a grand rewrite. The goal is an engine cutover: stop adding complex mechanics to the old payload model, establish the new rule/verb/fact surface, and move the first hard object fully onto it.
The first beachhead should be fountains.
Fountains are ideal because they already expose the whole problem:
two verbs: drink and dip
random outcomes
actor-conditioned results
item-conditioned results
charges / dry state
messages
spawn effects
beatitude changes
water exposure
future hooks for moon, deity, weather, luck, hallucination, etc.
Once fountains work through the new surface, the same strategy can cover bump behavior, altars, item use, combat exceptions, material reactions, death consequences, status effects, terrain, AI reactions, and world simulation.
The Core Problem
JSHack’s early architecture correctly centralized interactable behavior. That was useful. It reduced scatter and made object behavior easier to find.
But centralization has now become gravity.
The current model encourages complex objects to grow private mini-engines inside interaction payloads. Fountains are the clear example. Their behavior includes verb selection, effect rolls, item mutation, charge bookkeeping, messages, spawns, water exposure, and special cases.
That is too much for a payload.
The risk is not immediate failure. The risk is slow loss of authorial control. Every new mechanic requires remembering where the engine cave is, which branches are safe, which helper must be called, which events are real, which events are just UI, and which side effects happen indirectly.
That kills the project by weight.
The replacement goal is simple:
Exotic content should feel like a plugin, not a patch.
Or more concretely:
Add component.
Register rule.
Use tx ops.
Record facts.
Emit notification events.
Done.
New Architecture Line
The new line should be explicit:
interactPayloads.js is old world
verb rules are new world
facts are committed truth
events are notification
traces are diagnostics
Do not introduce “rule events” as a new concept. That term is muddy and likely to create a third event bus.
The clean distinction:
Facts can be read later.
Events are heard now.
Traces explain why.
Rules do not emit secret command events.
Rules do not ask other systems to do their real work through event side channels.
Rules decide. Transactions mutate. Facts record. Events notify. Traces explain. The scheduler remains sovereign.
The Engine Contract
The engine owns:
ordering
phase timing
determinism
rule lookup
transaction commit/discard
canonical mutation paths
fact recording
event emission
trace capture
debug visibility
Content owns:
what conditions apply
what verbs exist
what outcomes are possible
how outcomes are weighted
what facts are recorded
what messages/events are emitted
Systems should find the occasion.
Registries should decide the exception.
Transactions should mutate the world.
Facts should preserve committed truth.
Events should notify listeners, UI, logs, sounds, animations, and compatibility surfaces.
Traces should explain why a rule did or did not fire.
Target Rule Shape
The general authoring surface should look like this:
registerVerbRule({
id: "fountain.dip",
verb: "dip",
priority: 100,
match: {
actor: [Inventory],
target: [Fountain],
item: [ItemInfo],
},
when: ({ world, target, item }) => {
const fountain = world.get(target, Fountain);
return item > 0 && fountain && !fountain.dry;
},
apply: resolveChanceTable(fountainDipTable),
});
The table owns the weirdness:
export const fountainDipTable = chanceTable("fountain.dip", [
{
id: "curse-item",
weight: ({ world, actor, item }) => {
let w = 10;
if (hasCondition(world, actor, "unlucky")) w += 15;
if (hasBeatitude(world, item, "blessed")) w += 10;
return w;
},
when: ({ world, item }) =>
!hasBeatitude(world, item, "cursed"),
apply: ({ actor, target, item, tx }) => {
const from = getBeatitude(tx.world, item);
tx.setBeatitude(item, "cursed");
tx.record(new FountainDipped({
actor,
fountain: target,
item,
outcome: "curse-item",
}));
tx.record(new BeatitudeChanged({
entity: item,
from,
to: "cursed",
cause: "fountain.dip",
}));
tx.message("The item grows cold.");
return RuleResult.handled({ outcomeId: "curse-item" });
},
},
]);
This is the important authoring win. A new mechanic becomes a small registered rule or table entry. It does not require bespoke engine surgery.
Facts vs Events vs Traces
Facts
Facts are committed simulation truth.
A fact answers:
Examples:
new FountainDipped({
step,
actor,
fountain,
item,
outcome: "curse-item",
ruleId: "fountain.dip",
});
new BeatitudeChanged({
step,
entity: item,
from: "uncursed",
to: "cursed",
cause: "fountain.dip",
});
new FountainChargeSpent({
step,
fountain,
from: 2,
to: 1,
cause: "fountain.dip",
});
A fact is recorded only if the transaction commits.
Facts may be used by later systems, tests, debug tooling, replay, analytics, and future authoring agents.
Events
Events are notifications.
An event answers:
Who needs to be told now?
Examples:
world.emit(new MessageShown({
text: "The item grows cold."
}));
world.emit(new SoundPlayed({
sound: "fountain-darken",
at: target,
}));
world.emit(new VerbResolved({
actor,
target,
verb: "dip",
ruleId: "fountain.dip",
}));
Events may drive UI, logs, sound, animation, old compatibility listeners, and presentation/debug receipts.
Events should not become secret commands.
Bad:
world.emit(new CurseItemPlease({ item }));
Good:
tx.setBeatitude(item, "cursed");
tx.record(new BeatitudeChanged({
entity: item,
from,
to: "cursed",
cause: "fountain.dip",
}));
Traces
Traces are diagnostic explanation.
A trace answers:
Why did the engine choose that?
Examples:
trace.considered("fountain.dip.curse-item");
trace.weight("fountain.dip.curse-item", "base", 10);
trace.weight("fountain.dip.curse-item", "actor-unlucky", 25, 10);
trace.skipped("fountain.dip.bless-item", "item is cursed");
trace.selected("fountain.dip.curse-item", { roll: 47, total: 105 });
Traces should not drive gameplay.
They are for tests, debug overlays, developer inspection, and future agent comprehension.
First Beachhead: Fountain
Fountain should be moved fully onto the new model.
Not wrapped.
Not half-adapted.
Moved.
The old fountain payload path should be deleted once the new path passes tests.
Target files:
src/rules/content/fountain/Fountain.js
src/rules/content/fountain/fountainVerbs.js
src/rules/content/fountain/fountainTables.js
src/rules/content/fountain/fountainFacts.js
src/rules/content/fountain/fountainMessages.js
Kernel files:
src/rules/kernel/RuleResult.js
src/rules/kernel/verbRegistry.js
src/rules/kernel/verbRunner.js
src/rules/kernel/chanceTable.js
src/rules/kernel/ruleTrace.js
src/rules/kernel/factLog.js
Fountain verbs:
Possible future fountain verbs:
pray
wash
fill
listen
throw coin
bleed into
bless
curse
fish
But the first implementation should only move existing behavior.
Do not invent new gameplay during the cutover.
What the Fountain Cutover Must Prove
The fountain cutover must prove:
verb rules can replace object payload branches
chance tables can replace hand-coded roll bands
transactions can own all mutation
facts can record committed truth
events can remain notification-only
traces can explain skipped and selected outcomes
tests can force deterministic outcomes
Success criteria:
existing drink behavior preserved
existing dip behavior preserved
charges/dry state handled through one path
old fountain payload deleted
all full tests pass
new focused tests cover forced outcomes
trace output explains candidate table resolution
What Else This Strategy Covers
Bump Behavior
Bump behavior is an obvious candidate because it is already close to an ordered resolver table.
Covered mechanics:
hostile melee
pet swapping
NPC interaction
enemy door opening
pushing
tile bump reactions
fragile obstacles
kickable objects
shop exits
webs
locked doors
secret doors
Target surface:
registerBumpRule({
id: "door.open-on-bump",
priority: 100,
match: {
actor: [CanInteract],
target: [DoorState, Collider],
},
when: ({ world, actor, target }) => {
const door = world.get(target, DoorState);
return door && !door.open && canOpen(actor, target, world);
},
apply: ({ actor, target, tx }) => {
tx.patch(target, DoorState, { open: true });
tx.patch(target, Collider, { solid: false });
tx.record(new DoorOpenedFact({
actor,
door: target,
cause: "bump",
}));
tx.message("The door opens.");
return RuleResult.handled();
},
});
Altars, Shrines, and Prayer
Altars are probably the second or third major object candidate after fountains.
Covered mechanics:
pray
offer corpse
sacrifice item
bless item
anger deity
receive boon
summon punishment
convert altar
detect alignment
Inputs can include:
actor alignment
deity mood
item beatitude
corpse type
moon phase
room sanctity
prior sins
luck
hunger
combat state
This should use verb rules plus chance tables plus reaction rules.
Item Use
Item use should eventually move toward the same authoring surface.
Covered verbs:
quaff
read
zap
eat
apply
throw
wear
wield
engrave
break
burn
dip
Examples:
read cursed scroll
quaff unidentified potion
apply horn
eat corpse
zap wand at reflective target
throw silver dagger at werewolf
engrave with wand of fire
This is not just interaction. It is the general player/monster action surface.
Combat Exceptions
Combat should keep its canonical damage path, but special cases should be registered.
Covered mechanics:
silver vs lycanthrope
holy vs undead
fire vs plant
ice vs slime
acid blood
thorns
weapon procs
armor breakage
shield guard
critical effects
life drain
revenge curses
Target pattern:
combat system resolves ordinary hit
damage transaction applies canonical damage
registered combat rules add exceptions before/after damage
facts record what happened
events notify presentation
Material Reactions
Material reaction is likely a major future win.
Covered mechanics:
iron rusts
wood burns
ice melts
glass shatters
cloth catches fire
silver reacts to moonlight
gold conducts divine effects
bone responds to necromancy
This should be table-driven and fact-recorded.
The authoring surface should make it easy to add:
material + stimulus => outcome
Example:
registerMaterialReaction({
id: "iron.rusts-in-water",
material: "iron",
stimulus: "water",
apply: ({ item, source, tx }) => {
tx.applyMaterialDamage(item, {
kind: "rust",
amount: 1,
source,
});
tx.record(new MaterialDamaged({
entity: item,
material: "iron",
kind: "rust",
cause: source,
}));
return RuleResult.handled();
},
});
Status Effects
Status effects can use registered tick rules, entry rules, exit rules, and reaction rules.
Covered mechanics:
confused movement
slowed cadence
stunned action loss
sleep interruption
poison ticks
burning ticks
hallucination substitutions
berserk triggers
fear behavior
invisibility offense
rooted movement denial
The invariant:
status data is component state
status behavior is registered rule behavior
Death Consequences
Death is one of the most important places to keep facts distinct from events.
Covered mechanics:
drop corpse
explode
split
spawn ghost
release gas
trigger deity score
anger faction
record kill credit
spawn tombstone
complete contract
advance infestation
The canonical death path should record facts such as:
DeathApplied
CorpseCreated
LootDropped
FactionWitnessedDeath
ContractKillCredited
Presentation events can still exist, but durable consequences should consume facts or canonical records, not UI receipts.
AI and Social Reactions
AI behavior can use registered reaction rules without becoming random event soup.
Covered mechanics:
witness theft
hear combat
smell blood
respond to shop debt
panic from dragon
wolves hear moonwell backlash
townfolk remember violence
pets defend owner
monsters pick up weapons
The system owns timing. Rules own the exception.
Terrain and Environmental Rules
Terrain should eventually become another registry surface.
Covered mechanics:
step into lava
walk on ice
cross water
enter sacred ground
stand in moonlight
move through smoke
trigger web
activate pressure plate
dig wall
burn tree
Target pattern:
movement system detects terrain occasion
terrain rules decide effects
transactions mutate
facts record
events notify
World Simulation
The same strategy can cover slow systems too.
Covered mechanics:
weather changes
moon phase
fountain regrowth
plant growth
town district condition
infestation pressure
deity mood
shop economy
calendar effects
These may not be verb rules. They may be tick rules or phase rules. But the shape is the same:
registerTickRule({
id: "fountain.regrow-on-rain",
phase: "effects",
priority: 50,
match: {
world: [WeatherState],
target: [Fountain],
},
when: ({ world, target }) =>
isRaining(world) && fountainIsDry(world, target),
apply: ({ target, tx }) => {
tx.patch(target, Fountain, { dry: false, charges: 1 });
tx.record(new FountainRegrown({
fountain: target,
cause: "rain",
}));
return RuleResult.handled();
},
});
Replacement Rules
Adopt these rules immediately:
No new exotic behavior goes into interactPayloads.js.
No new hand-coded chance table branches inside object payloads.
No new events-as-commands.
No new callback behavior on components.
No new world-attached state for rule systems.
No scheduler edits for object-specific behavior.
New complex content must use:
registered rule
transaction mutation
recorded facts
typed notification events
trace support
focused tests
The First Implementation Slice
Implement only the minimum kernel needed for fountain.
Suggested order:
1. RuleResult
2. action transaction fact recording
3. fact base class or simple fact record shape
4. rule trace object
5. chanceTable()
6. verbRegistry
7. verbRunner
8. Fountain component / state normalization if needed
9. fountain.dip rule
10. fountain.drink rule
11. remove old fountain payload path
12. tests
Do not generalize too early.
Do not build a plugin framework.
Do not move altars, combat, materials, or AI until fountain proves the cutover.
Testing Requirements
The new system needs tests that can force outcomes.
Fountain tests should cover:
drink selects expected outcome under deterministic roll
dip selects expected outcome under deterministic roll
actor condition changes table weight
item condition changes table eligibility
charges decrement once
dry fountain blocks verbs
facts are only recorded after commit
events are emitted after commit
trace shows skipped candidates
trace shows selected candidate
old payload path is gone
Test style should prefer small, deterministic fixtures.
The most important regression test:
A failed or unhandled rule must not partially mutate world state or record committed facts.
Authoring Example
A future mechanic should feel this small:
registerVerbRule({
id: "mirror.listen",
verb: "listen",
priority: 100,
match: {
actor: [Soul],
target: [Mirror, Haunted],
},
when: ({ world, target }) =>
!world.get(target, Mirror).broken,
apply: ({ actor, target, tx }) => {
tx.applyStatus(actor, "uneasy", {
turns: 20,
strength: 1,
});
tx.record(new MirrorWhispered({
actor,
mirror: target,
}));
tx.message("Something behind the glass whispers your name.");
return RuleResult.handled();
},
});
No engine branch.
No scheduler edit.
No custom payload cave.
No display coupling.
That is the desired authoring surface.
Desired End State
JSHack should become:
a deterministic simulation kernel
plus a library of registered weirdness
The player experiences strange behavior.
The author writes small spells.
The engine keeps the laws.
That is the turn-the-corner architecture.
Summary
JSHack has reached the point where its current interaction and exception-handling model is becoming a maintenance threat.
The problem is not that the game has become too weird. The weirdness is the point. The problem is that too much weirdness is being encoded as bespoke procedural behavior inside heavyweight systems and object-specific payload files.
The path forward is to replace those behavior caves with a proper authoring surface:
The goal is not a grand rewrite. The goal is an engine cutover: stop adding complex mechanics to the old payload model, establish the new rule/verb/fact surface, and move the first hard object fully onto it.
The first beachhead should be fountains.
Fountains are ideal because they already expose the whole problem:
Once fountains work through the new surface, the same strategy can cover bump behavior, altars, item use, combat exceptions, material reactions, death consequences, status effects, terrain, AI reactions, and world simulation.
The Core Problem
JSHack’s early architecture correctly centralized interactable behavior. That was useful. It reduced scatter and made object behavior easier to find.
But centralization has now become gravity.
The current model encourages complex objects to grow private mini-engines inside interaction payloads. Fountains are the clear example. Their behavior includes verb selection, effect rolls, item mutation, charge bookkeeping, messages, spawns, water exposure, and special cases.
That is too much for a payload.
The risk is not immediate failure. The risk is slow loss of authorial control. Every new mechanic requires remembering where the engine cave is, which branches are safe, which helper must be called, which events are real, which events are just UI, and which side effects happen indirectly.
That kills the project by weight.
The replacement goal is simple:
Or more concretely:
New Architecture Line
The new line should be explicit:
Do not introduce “rule events” as a new concept. That term is muddy and likely to create a third event bus.
The clean distinction:
Rules do not emit secret command events.
Rules do not ask other systems to do their real work through event side channels.
Rules decide. Transactions mutate. Facts record. Events notify. Traces explain. The scheduler remains sovereign.
The Engine Contract
The engine owns:
Content owns:
Systems should find the occasion.
Registries should decide the exception.
Transactions should mutate the world.
Facts should preserve committed truth.
Events should notify listeners, UI, logs, sounds, animations, and compatibility surfaces.
Traces should explain why a rule did or did not fire.
Target Rule Shape
The general authoring surface should look like this:
The table owns the weirdness:
This is the important authoring win. A new mechanic becomes a small registered rule or table entry. It does not require bespoke engine surgery.
Facts vs Events vs Traces
Facts
Facts are committed simulation truth.
A fact answers:
Examples:
A fact is recorded only if the transaction commits.
Facts may be used by later systems, tests, debug tooling, replay, analytics, and future authoring agents.
Events
Events are notifications.
An event answers:
Examples:
Events may drive UI, logs, sound, animation, old compatibility listeners, and presentation/debug receipts.
Events should not become secret commands.
Bad:
Good:
Traces
Traces are diagnostic explanation.
A trace answers:
Examples:
Traces should not drive gameplay.
They are for tests, debug overlays, developer inspection, and future agent comprehension.
First Beachhead: Fountain
Fountain should be moved fully onto the new model.
Not wrapped.
Not half-adapted.
Moved.
The old fountain payload path should be deleted once the new path passes tests.
Target files:
Kernel files:
Fountain verbs:
Possible future fountain verbs:
But the first implementation should only move existing behavior.
Do not invent new gameplay during the cutover.
What the Fountain Cutover Must Prove
The fountain cutover must prove:
Success criteria:
What Else This Strategy Covers
Bump Behavior
Bump behavior is an obvious candidate because it is already close to an ordered resolver table.
Covered mechanics:
Target surface:
Altars, Shrines, and Prayer
Altars are probably the second or third major object candidate after fountains.
Covered mechanics:
Inputs can include:
This should use verb rules plus chance tables plus reaction rules.
Item Use
Item use should eventually move toward the same authoring surface.
Covered verbs:
Examples:
This is not just interaction. It is the general player/monster action surface.
Combat Exceptions
Combat should keep its canonical damage path, but special cases should be registered.
Covered mechanics:
Target pattern:
Material Reactions
Material reaction is likely a major future win.
Covered mechanics:
This should be table-driven and fact-recorded.
The authoring surface should make it easy to add:
Example:
Status Effects
Status effects can use registered tick rules, entry rules, exit rules, and reaction rules.
Covered mechanics:
The invariant:
Death Consequences
Death is one of the most important places to keep facts distinct from events.
Covered mechanics:
The canonical death path should record facts such as:
Presentation events can still exist, but durable consequences should consume facts or canonical records, not UI receipts.
AI and Social Reactions
AI behavior can use registered reaction rules without becoming random event soup.
Covered mechanics:
The system owns timing. Rules own the exception.
Terrain and Environmental Rules
Terrain should eventually become another registry surface.
Covered mechanics:
Target pattern:
World Simulation
The same strategy can cover slow systems too.
Covered mechanics:
These may not be verb rules. They may be tick rules or phase rules. But the shape is the same:
Replacement Rules
Adopt these rules immediately:
New complex content must use:
The First Implementation Slice
Implement only the minimum kernel needed for fountain.
Suggested order:
Do not generalize too early.
Do not build a plugin framework.
Do not move altars, combat, materials, or AI until fountain proves the cutover.
Testing Requirements
The new system needs tests that can force outcomes.
Fountain tests should cover:
Test style should prefer small, deterministic fixtures.
The most important regression test:
Authoring Example
A future mechanic should feel this small:
No engine branch.
No scheduler edit.
No custom payload cave.
No display coupling.
That is the desired authoring surface.
Desired End State
JSHack should become:
The player experiences strange behavior.
The author writes small spells.
The engine keeps the laws.
That is the turn-the-corner architecture.