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. Every hook returns a function — call it to remove the interceptor:
const unsubscribe = window.modAPI.hooks.onCreateEnemyCombatEntity((enemy, combatEntity, gameFlags) => {
// ...
return combatEntity;
});
// Later: stop intercepting
unsubscribe();
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;
});
onModifyRecipeIngredients
Modifies the ingredients or amounts used in a recipe before crafting calculations run. This fires before onDeriveRecipeDifficulty.
Parameters:
recipe: RecipeItem- The recipe being craftedgameFlags: Record<string, number>- Current game flags/state
Returns: Modified RecipeItem
Example:
window.modAPI.hooks.onModifyRecipeIngredients((recipe, gameFlags) => {
if (gameFlags.efficient_crafting) {
const modified = { ...recipe };
modified.ingredients = recipe.ingredients.map(ing => ({
...ing,
count: Math.max(1, Math.floor(ing.count * 0.5)),
}));
return modified;
}
return recipe;
});
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 the 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.
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. 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 to 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);
}
});
New Game Hooks
onNewGame
Fires when the player starts a new game (including after the optional tutorial). Called after all character creation choices are finalized but before the game state is committed. This is the last point where any aspect of the new game can be modified.
Parameters:
intent: NewGameIntent- The full new game state:items: ItemDesc[]- items granted at game starttechniques: string[]- technique IDs granted at game startrecipes: string[]- recipe IDs granted at game startdestinies: string[]- destiny IDs granted at game startquests: string[]- quest IDs granted at game startmoney: number- starting silverfavour: number- starting favourflags: Record<string, number>- initial game flags (background flags are set via dispatch after this hook fires)player: PlayerEntity- the player entity after backgrounds are appliedcraftingActions: string[]- crafting action IDs granted at game start
Returns: NewGameIntent - modified intent (all fields are optional; return only what you changed)
Example:
window.modAPI.hooks.onNewGame((intent) => {
// Grant a bonus item and extra starting silver
return {
...intent,
items: [...intent.items, { name: 'Bonus Jade' }],
money: intent.money + 500,
};
});
Usage notes:
- Flags at this point are empty, because background flag dispatch runs after the hook returns. For flag-aware modifications, use
onReduxActionoronReduxActionPayload. - The
playerfield is the entity after backgrounds are applied but beforealternativeStartmodifications. Mods can adjust stats, techniques, buffs, etc. on this entity. - All hooks run in registration order. If multiple mods use this hook, chain the modifications by returning an intent that carries the previous hooks’ changes.
onGameLoad
Fires when a saved game is loaded, allowing mods to mutate the initial state. The interceptor receives the loaded RootState and returns a modified copy. Use this to adjust game state based on loaded save data.
Parameters:
state: RootState- The complete game state that was loaded
Returns: RootState - Modified state
Example:
window.modAPI.hooks.onGameLoad((state) => ({
...state,
player: { ...state.player, flags: { ...state.player.flags, my_mod_flag: 1 } },
}));
Redux Hooks
onReduxAction
Fires after every Redux action is dispatched. Receives the action type, the state before the action was applied, the state after, and a read-only copy of the action payload. 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 actionpayload: Readonly<unknown>- Read-only snapshot of the (post-interceptor) action payload
Returns: RootState - The state to store (return stateAfter unchanged if not modifying)
Example:
window.modAPI.hooks.onReduxAction((actionType, stateBefore, stateAfter, payload) => {
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;
});
onReduxActionPayload
Fires before the reducer runs. Interceptors receive the action type and payload, and return a modified payload to replace it, or null to drop the action entirely. Interceptors are chained; each receives the output of the previous.
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.
Parameters:
actionType: string- The Redux action type stringpayload: unknown- The action payloadstateBefore: RootState- Game state before the action
Returns: unknown - The payload to pass to the reducer (return a modified payload, or null to drop the action)
Example:
window.modAPI.hooks.onReduxActionPayload((actionType, payload, stateBefore) => {
if (actionType === 'inventory/removeItem') {
const p = payload as { name: string; stacks: number };
if (modAPI.gameData.items[p.name]?.kind === 'blueprint') {
return { ...p, stacks: 0 }; // prevent removal of blueprint items
}
}
return payload;
});
Equipment Hooks
onDeriveEquipmentUpgradeRequirement
Called before the equipment upgrade dialog is shown. Allows mutating upgrade cost items and result item quality tier and hidden potential, but not the base item.
Parameters:
baseItem: Item- The item being upgradedcostItems: Item[]- Items consumed as upgrade costresultItem: { resultItemName: string; resultQualityTier: number; resultHiddenPotential?: number; resultEnchantment?: EnchantmentDesc }- Preview of the result itemgameFlags: Record<string, number>- Current game flags/state
Returns: { costItems?: Item[]; resultItem?: Partial<{ resultItemName: string; resultQualityTier: number; resultHiddenPotential: number; resultEnchantment: EnchantmentDesc }> } | undefined
Example:
window.modAPI.hooks.onDeriveEquipmentUpgradeRequirement((baseItem, costItems, resultItem, gameFlags) => {
return {
costItems,
resultItem: { ...resultItem, resultQualityTier: resultItem.resultQualityTier + 2 },
};
});
onCompleteEquipmentUpgrade
Called when an equipment upgrade completes. Read-only: cannot modify the result item. Use onDeriveEquipmentUpgradeRequirement to change the result before the upgrade.
Parameters:
baseItem: Item- The item that was upgradedcostItems: Item[]- Items that were consumedresultItem: Item- The resulting upgraded itemgameFlags: Record<string, number>- Current game flags/state
Example:
window.modAPI.hooks.onCompleteEquipmentUpgrade((baseItem, costItems, resultItem, gameFlags) => {
console.log('Upgraded:', resultItem.name, 'from', baseItem.name);
});
onDeriveEquipmentReforgeRequirement
Called before the equipment reforge dialog is shown. Allows mutating reforge cost items and result item quality tier and hidden potential, but not the base item.
Parameters:
baseItem: Item- The item being reforgedcostItems: Item[]- Items consumed as reforge costresultItem: { resultItemName: string; resultQualityTier: number; resultHiddenPotential?: number; resultEnchantment?: EnchantmentDesc }- Preview of the result itemgameFlags: Record<string, number>- Current game flags/state
Returns: { costItems?: Item[]; resultItem?: Partial<{ resultItemName: string; resultQualityTier: number; resultHiddenPotential: number; resultEnchantment: EnchantmentDesc }> } | undefined
Example:
window.modAPI.hooks.onDeriveEquipmentReforgeRequirement((baseItem, costItems, resultItem, gameFlags) => {
return { costItems, resultItem: { ...resultItem, resultQualityTier: 10 } };
});
onCompleteEquipmentReforge
Called after the reforge completes, before the result dialog is shown. Allows modifying the cost items and result item.
Parameters:
baseItem: Item- The item that was reforgedcostItems: Item[]- Items that were consumedresultItem: Item- The resulting reforged itemgameFlags: Record<string, number>- Current game flags/state
Returns: { costItems?: Item[]; resultItem?: Item } | undefined
Example:
window.modAPI.hooks.onCompleteEquipmentReforge((baseItem, costItems, resultItem, gameFlags) => {
return {
costItems,
resultItem: { ...resultItem, damage: (resultItem.damage ?? 0) + 5 },
};
});
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.
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.