Mod Hooks Documentation
This document describes the available mod hooks that allow mods to intercept and modify game behavior at specific points.
All hooks are registered via window.modAPI.hooks:
window.modAPI.hooks.onCreateEnemyCombatEntity((enemy, combatEntity, gameFlags) => {
// ...
return combatEntity;
});
Combat Hooks
onCreateEnemyCombatEntity
Intercepts the creation of enemy combat entities, allowing modifications to enemy stats, abilities, or equipment before combat begins.
Parameters:
enemy: EnemyEntity- The base enemy definitioncombatEntity: CombatEntity- The combat entity being createdgameFlags: Record<string, number>- Current game flags/state
Returns: Modified CombatEntity
Example:
window.modAPI.hooks.onCreateEnemyCombatEntity((enemy, combatEntity, gameFlags) => {
// Make the entire game harder
combatEntity.stats.power *= 1.2;
combatEntity.stats.defense *= 1.2;
// Modify defense for specific enemy types
if (enemy.name.includes('Stone')) {
combatEntity.stats.defense *= 1.5;
}
return combatEntity;
});
onCalculateDamage
Intercepts combat damage after all base reductions. Called for every damage application. Return the new damage value.
Parameters:
attacker: CombatEntity- The entity dealing damagedefender: CombatEntity- The entity receiving damagedamage: number- Post-reduction damage valuedamageType: DamageType | undefined- The type of damagegameFlags: Record<string, number>- Current game flags/state
Returns: number - Modified damage value
Example:
window.modAPI.hooks.onCalculateDamage((attacker, defender, damage, damageType, gameFlags) => {
if (gameFlags.iron_body && defender.entityType === 'Player') {
return Math.floor(damage * 0.8);
}
return damage;
});
onBeforeCombat
Fires before combat is initialized. Allows modifying the enemy list and the player’s starting combat entity. Return modified copies — mutations to the originals are not used.
Parameters:
enemies: EnemyEntity[]- The enemies about to be foughtplayerState: CombatEntity- The player’s starting combat entitygameFlags: Record<string, number>- Current game flags/state
Returns: { enemies: EnemyEntity[]; playerState: CombatEntity } - Modified copies
Example:
window.modAPI.hooks.onBeforeCombat((enemies, playerState, gameFlags) => {
if (gameFlags.hard_mode) {
const scaled = enemies.map(e => ({ ...e, stats: { ...e.stats, hp: e.stats.hp * 2 } }));
return { enemies: scaled, playerState };
}
return { enemies, playerState };
});
Crafting Hooks
onDeriveRecipeDifficulty
Modifies the difficulty and stats of crafting recipes during the crafting process.
Parameters:
recipe: RecipeItem- The recipe being craftedrecipeStats: CraftingRecipeStats- The calculated recipe statistics:completion: number- Completion thresholdperfection: number- Perfection thresholdstability: number- Stability valueconditionType: RecipeConditionEffect- Recipe condition effectharmonyType: RecipeHarmonyType- Recipe harmony type
gameFlags: Record<string, number>- Current game flags/state
Returns: Modified CraftingRecipeStats
Example:
window.modAPI.hooks.onDeriveRecipeDifficulty((recipe, recipeStats, gameFlags) => {
if (gameFlags.unlockedUltimateCauldron === 1) {
recipeStats.completion *= 0.8;
recipeStats.perfection *= 0.8;
}
if (gameFlags.totalCraftsCompleted > 100) {
recipeStats.stability *= 1.2;
}
return recipeStats;
});
Completion Hooks
These hooks trigger after specific game activities complete during an event, allowing mods to inject additional event steps into the event flow.
onCompleteCombat
Triggers after combat ends, allowing for custom victory/defeat consequences.
Parameters:
eventStep: CombatStep | FightCharacterStep- The event step that triggered the combatvictory: boolean- Whether the player wonplayerCombatState: CombatEntity- The player’s combat state at endfoughtEnemies: EnemyEntity[]- The enemies that were foughtdroppedItems: Item[]- Items dropped by enemies at the end of combatgameFlags: Record<string, number>- Current game flags/state
Returns: EventStep[] - Additional event steps to execute
Example:
window.modAPI.hooks.onCompleteCombat((eventStep, victory, playerCombatState, foughtEnemies, droppedItems, gameFlags) => {
const events: EventStep[] = [];
if (!victory && eventStep.kind === 'combat' && !eventStep.isSpar) {
events.push({ kind: 'text', text: 'You fall, vision narrowing to black.' });
events.push({ kind: 'changeSocialStat', stat: 'lifespan', amount: '-lifespan' });
}
if (victory && playerCombatState.stats.hp === playerCombatState.stats.maxHp) {
events.push({ kind: 'addItem', item: { name: 'Flawless Victory Token' }, amount: '1' });
}
return events;
});
onCompleteTournament
Triggers after tournament participation with placement results.
Parameters:
eventStep: TournamentStep- The event step that triggered the tournamenttournamentState: 'victory' | 'second' | 'defeat'- Tournament placementgameFlags: Record<string, number>- Current game flags/state
Returns: EventStep[] - Additional event steps to execute
Example:
window.modAPI.hooks.onCompleteTournament((eventStep, tournamentState, gameFlags) => {
const events: EventStep[] = [];
if (tournamentState === 'victory' && !gameFlags.firstTournamentVictory) {
events.push({ kind: 'unlockLocation', location: 'Champion Training Grounds' });
events.push({ kind: 'flag', global: false, flag: 'firstTournamentVictory', value: '1' });
}
return events;
});
onCompleteDualCultivation
Triggers after dual cultivation attempts.
Parameters:
eventStep: DualCultivationStep- The event step that triggered dual cultivationsuccess: boolean- Whether the dual cultivation succeededgameFlags: Record<string, number>- Current game flags/state
Returns: EventStep[] - Additional event steps to execute
Example:
window.modAPI.hooks.onCompleteDualCultivation((eventStep, success, gameFlags) => {
const events: EventStep[] = [];
if (success) {
const streak = (gameFlags.dualCultivationStreak || 0) + 1;
events.push({ kind: 'qi', amount: '' + (100 * streak) });
events.push({ kind: 'flag', global: false, flag: 'dualCultivationStreak', value: '' + streak });
} else if (gameFlags.dualCultivationStreak > 0) {
events.push({ kind: 'flag', global: false, flag: 'dualCultivationStreak', value: '0' });
}
return events;
});
onCompleteCrafting
Triggers after crafting attempts, successful or failed.
Parameters:
eventStep: CraftingStep- The event step that triggered the craftingitem: CraftingResult | undefined- The crafted item (undefinedif failed)gameFlags: Record<string, number>- Current game flags/state
Returns: EventStep[] - Additional event steps to execute
Example:
window.modAPI.hooks.onCompleteCrafting((eventStep, item, gameFlags) => {
const events: EventStep[] = [];
if (item && item.quality >= 4) {
events.push({ kind: 'reputation', name: 'Celadon Flame Brewers', amount: '' + (item.quality * 5) });
}
return events;
});
onCompleteAuction
Triggers after participating in auctions.
Parameters:
eventStep: AuctionStep- The event step that triggered the auctionitemsBought: AuctionItem[]- Items successfully purchasedgameFlags: Record<string, number>- Current game flags/state
Returns: EventStep[] - Additional event steps to execute
Example:
window.modAPI.hooks.onCompleteAuction((eventStep, itemsBought, gameFlags) => {
const events: EventStep[] = [];
if (itemsBought.length >= 5) {
events.push({ kind: 'addItem', item: { name: 'Bulk Buyer Token' }, amount: '1' });
}
return events;
});
onCompleteStoneCutting
Triggers after stone cutting activities.
Parameters:
eventStep: StoneCuttingStep- The event step that triggered stone cuttinggameFlags: Record<string, number>- Current game flags/state
Returns: EventStep[] - Additional event steps to execute
Example:
window.modAPI.hooks.onCompleteStoneCutting((eventStep, gameFlags) => {
const events: EventStep[] = [];
const stonesCount = (gameFlags.totalStonesCut || 0) + 1;
events.push({ kind: 'flag', global: false, flag: 'totalStonesCut', value: '' + stonesCount });
if (stonesCount === 100) {
events.push({ kind: 'addItem', item: { name: 'Master Stone Cutter Badge' }, amount: '1' });
}
return events;
});
Event Item Hooks
onEventDropItem
Intercepts items granted by event steps (addItem, addMultipleItem, dropItem). Return a modified ItemDesc to change the item name or stack count. Return stacks <= 0 to suppress the item entirely.
Parameters:
item: ItemDesc- The item being granted (name and optional stacks)step: AddItemStep | AddMultipleItemStep | DropItemStep- The event step granting the itemgameFlags: Record<string, number>- Current game flags/state
Returns: Modified ItemDesc
Example:
window.modAPI.hooks.onEventDropItem((item, step, gameFlags) => {
// Double Iron Ore drops in bonus mode
if (gameFlags.item_bonus && item.name === 'Iron Ore') {
return { ...item, stacks: (item.stacks ?? 1) * 2 };
}
// Suppress a specific item entirely
if (gameFlags.banned_items && item.name === 'Forbidden Herb') {
return { ...item, stacks: 0 };
}
return item;
});
Exploration Hooks
onGenerateExploreEvents
Modifies the pool of exploration events before one is selected. Fired after base-game eligibility filtering, so every event in the array was already eligible to fire. Add, remove, or reorder events to influence what the player encounters.
Parameters:
locationId: string- The current location identifierevents: LocationEvent[]- The filtered event poolgameFlags: Record<string, number>- Current game flags/state
Returns: LocationEvent[] - Modified event pool
Important Warning on Weighted Events: In the 0.6.50 runtime, onGenerateExploreEvents fires before the game expands weighted explore candidates into repeated { index, event } entries. Repeat-penalty bookkeeping (currentLocationLastEvent / currentLocationLastEventCount) is keyed by that expanded weighted event index. If your mod needs to precisely modify drop rates or probabilities without breaking repeat-penalty semantics, you may still need to carefully scope your modifications or narrowly patch the final weighted candidate array in combination with this hook.
Example:
window.modAPI.hooks.onGenerateExploreEvents((locationId, events, gameFlags) => {
if (gameFlags.lucky_mode) {
return [...events, myBonusEvent];
}
return events;
});
Location Hooks
onLocationEnter
Fires when the player moves to a new location. This is an observation hook — it does not return a value.
Parameters:
locationId: string- The identifier of the location enteredgameFlags: Record<string, number>- Current game flags/state
Example:
window.modAPI.hooks.onLocationEnter((locationId, gameFlags) => {
if (locationId === 'Ancient Library') {
console.log('Player entered the Ancient Library');
}
});
Loot Hooks
onLootDrop
Fires when combat loot is distributed to the player after a fight. This is an observation hook — it does not return a value. Use onCompleteCombat if you need to modify or add drops.
Parameters:
items: Item[]- The items distributed to the playergameFlags: Record<string, number>- Current game flags/state
Example:
window.modAPI.hooks.onLootDrop((items, gameFlags) => {
items.forEach(item => console.log('Received loot:', item.name));
});
Time Hooks
onAdvanceDay
Fires when the game advances time (player rests, travels, etc.).
Parameters:
days: number- Number of days advancedgameFlags: Record<string, number>- Current game flags/state
Example:
window.modAPI.hooks.onAdvanceDay((days, gameFlags) => {
console.log('Time passed:', days, 'days');
});
onAdvanceMonth
Fires once for each month rollover that occurs during a day advance. If the player skips multiple months at once, this fires once per month rolled over.
Parameters:
month: number- The new month (1–12)year: number- The current yeargameFlags: Record<string, number>- Current game flags/state
Example:
window.modAPI.hooks.onAdvanceMonth((month, year, gameFlags) => {
if (month === 3) {
window.modAPI.actions.startEvent(mySpringFestivalEvent);
}
});
Redux Hooks
onReduxAction
Fires after every Redux state update. Receives the action type, the state before, and the state after. Return a modified copy of stateAfter to override what is stored, or return stateAfter unchanged.
This hook runs inside the reducer. Keep the implementation fast, deterministic, and free of side-effects. Do not make network requests, trigger UI work, or run heavy computation here. Thrown exceptions are caught and logged.
If subscribe() can solve your problem, prefer that instead — it runs outside the reducer and is safer for most use cases.
Parameters:
actionType: string- The Redux action type stringstateBefore: RootState- Game state before the actionstateAfter: RootState- Game state after the action
Returns: RootState - The state to store (return stateAfter unchanged if not modifying)
Example:
window.modAPI.hooks.onReduxAction((actionType, stateBefore, stateAfter) => {
if (actionType === 'inventory/addItem') {
if (stateAfter.gameData.flags?.hard_mode) {
// Double every item added in hard mode
return { ...stateAfter, inventory: myDoubledInventory(stateAfter.inventory) };
}
}
return stateAfter;
});
State Access and UI
These functions are on the root window.modAPI object, not under window.modAPI.hooks.
subscribe
Subscribe to any Redux state change. The callback is called after every dispatched action. Returns an unsubscribe function.
const unsub = window.modAPI.subscribe(() => {
const snap = window.modAPI.getGameStateSnapshot();
if (snap) updateMyOverlay(snap.player.player.hp);
});
// Stop listening later
unsub();
For read-only overlays, advisors, and inspectors, subscribe() plus getGameStateSnapshot() should be your first integration path. Prefer this pair over direct window.gameStore access, React Fiber probing, or DOM polling unless the current runtime is missing the specific state you need.
Reactive patterns with subscribe():
// Rate-limited reactive updates
let lastUpdate = 0;
window.modAPI.subscribe(() => {
const now = Date.now();
if (now - lastUpdate < 250) return; // Throttle to 4 Hz
lastUpdate = now;
const snap = window.modAPI.getGameStateSnapshot();
if (snap) refreshMyPanel(snap);
});
// Selective re-render — only update when a specific slice changes
let lastLocation: string | null = null;
window.modAPI.subscribe(() => {
const snap = window.modAPI.getGameStateSnapshot();
const current = snap?.location?.current ?? null;
if (current !== lastLocation) {
lastLocation = current;
onLocationChanged(current);
}
});
getGameStateSnapshot
Returns a read-only snapshot of the complete game state, or null if no save is loaded.
const snap = window.modAPI.getGameStateSnapshot();
if (snap) {
console.log('Player realm:', snap.player.player.realm);
console.log('Spirit stones:', snap.inventory.money);
}
injectUI
Inject React content into a named slot inside an existing game dialog or screen. Returns void.
Slot names:
- For dialogs: the dialog’s DOM id (e.g.
'combat-victory'). Open the game in dev mode and inspect the element to find the id for the dialog you want to target. - For screens: the
ScreenTypevalue (e.g.'combat','location')
Parameters passed to your generator:
api—ModReduxAPIwith game state, actions, and componentselement— root DOM element of the slot; usequerySelectorto find childreninject(selector, content, mode?)— portal helper:selector: CSS selector to target insideelementcontent: React node to rendermode:'overlay'(default, floats over target) or'inline'(inserts as a sibling after target)
window.modAPI.injectUI('combat-victory', (api, element, inject) => {
return inject(
'[aria-live="assertive"]',
<button style= onClick={() => console.log('bonus!')}>
Claim Bonus
</button>,
'inline'
);
});
To add UI to a full mod screen instead, see Adding Screens.