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 definition
  • combatEntity: CombatEntity - The combat entity being created
  • gameFlags: 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 damage
  • defender: CombatEntity - The entity receiving damage
  • damage: number - Post-reduction damage value
  • damageType: DamageType | undefined - The type of damage
  • gameFlags: 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 fought
  • playerState: CombatEntity - The player’s starting combat entity
  • gameFlags: 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 crafted
  • recipeStats: CraftingRecipeStats - The calculated recipe statistics:
    • completion: number - Completion threshold
    • perfection: number - Perfection threshold
    • stability: number - Stability value
    • conditionType: RecipeConditionEffect - Recipe condition effect
    • harmonyType: 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 crafted
  • gameFlags: 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 combat
  • victory: boolean - Whether the player won
  • playerCombatState: CombatEntity - The player’s combat state at end
  • foughtEnemies: EnemyEntity[] - The enemies that were fought
  • droppedItems: Item[] - Items dropped by enemies at the end of combat
  • gameFlags: 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 tournament
  • tournamentState: 'victory' | 'second' | 'defeat' - Tournament placement
  • gameFlags: 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 cultivation
  • success: boolean - Whether the dual cultivation succeeded
  • gameFlags: 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 crafting
  • item: CraftingResult | undefined - The crafted item (undefined if 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 auction
  • itemsBought: AuctionItem[] - Items successfully purchased
  • gameFlags: 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 cutting
  • gameFlags: 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 item
  • gameFlags: 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 identifier
  • events: LocationEvent[] - The filtered event pool
  • gameFlags: 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 entered
  • gameFlags: 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 player
  • gameFlags: 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 advanced
  • gameFlags: 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 year
  • gameFlags: 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 start
    • techniques: string[] - technique IDs granted at game start
    • recipes: string[] - recipe IDs granted at game start
    • destinies: string[] - destiny IDs granted at game start
    • quests: string[] - quest IDs granted at game start
    • money: number - starting silver
    • favour: number - starting favour
    • flags: Record<string, number> - initial game flags (background flags are set via dispatch after this hook fires)
    • player: PlayerEntity - the player entity after backgrounds are applied
    • craftingActions: 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 onReduxAction or onReduxActionPayload.
  • The player field is the entity after backgrounds are applied but before alternativeStart modifications. 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 string
  • stateBefore: RootState - Game state before the action
  • stateAfter: RootState - Game state after the action
  • payload: 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 string
  • payload: unknown - The action payload
  • stateBefore: 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 upgraded
  • costItems: Item[] - Items consumed as upgrade cost
  • resultItem: { resultItemName: string; resultQualityTier: number; resultHiddenPotential?: number; resultEnchantment?: EnchantmentDesc } - Preview of the result item
  • gameFlags: 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 upgraded
  • costItems: Item[] - Items that were consumed
  • resultItem: Item - The resulting upgraded item
  • gameFlags: 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 reforged
  • costItems: Item[] - Items consumed as reforge cost
  • resultItem: { resultItemName: string; resultQualityTier: number; resultHiddenPotential?: number; resultEnchantment?: EnchantmentDesc } - Preview of the result item
  • gameFlags: 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 reforged
  • costItems: Item[] - Items that were consumed
  • resultItem: Item - The resulting reforged item
  • gameFlags: 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 ScreenType value (e.g. 'combat', 'location')

Parameters passed to your generator:

  • apiModReduxAPI with game state, actions, and components
  • element — root DOM element of the slot; use querySelector to find children
  • inject(selector, content, mode?) — portal helper:
    • selector: CSS selector to target inside element
    • content: React node to render
    • mode: '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.