Harmony Type
Harmony is one of the core features of the crafting system. In the base game, there are 4 variants of this defined, but new ones can be added by mods to even further flesh out this system. This can be done through the window.modAPI.action.addHarmonyType
function.
window.modAPI.action.addHarmonyType(harmonyType: RecipeHarmonyType, config: HarmonyTypeConfig)
- harmonyType: A unique string identifier for your harmony type (e.g.,
'elemental'
,'temporal'
,'chaos'
). Note, as you are adding new and unknown harmony types you need to tell the compiler this is the case, by ‘casting’ the string to the RecipeHarmonyType'elemental' as RecipeHarmonyType
- config: An object containing all configuration for the harmony type:
HarmonyTypeConfig Fields
1. name
(string, required)
The display name of your harmony type shown to players.
Example: "Elemental Balance"
2. description
(string, required)
HTML-formatted description explaining the harmony mechanics to players. Supports special formatting tags:
<name>text</name>
- Highlights important terms<num>number</num>
- Highlights numbers<li>item</li>
- Creates list items<br/>
- Line breaks- Standard HTML like
<span style="color: green">text</span>
Example:
description: `Balance the elements to maintain <name>Harmony</name>.
<br/>
Each action shifts the elemental balance:
<li>Support: +<num>2</num> heat, -<num>1</num> cold</li>
<li>Refine: +<num>2</num> cold, -<num>1</num> heat</li>`
3. processEffect
(function, required)
Called after each crafting technique is executed. This is where you implement the core mechanics.
Function Signature:
processEffect: (
harmonyData: HarmonyData,
technique: CraftingTechnique,
progressState: ProgressState,
entity: CraftingEntity,
state: CraftingState
) => void
Parameters:
harmonyData
: Store custom data for your harmony type here, in theadditionalData
field.technique
: The crafting action that was just executedprogressState
: Current crafting progress (completion, perfection, harmony, etc.)entity
: The player’s crafting entity with stats and buffsstate
: Overall crafting state including the log
Common Operations:
- Initialize custom data in
harmonyData.additionalData
. e.g.harmonyData.additionalData = { heat: 0, cold: 0 }
- Modify
progressState.harmony
based on player actions - Add/remove buffs to
entity.buffs
- Log messages to
state.craftingLog
- Set
harmonyData.recommendedTechniqueTypes
to guide the player on the best crafting action types to use next (if relevant)
4. initEffect
(function, required)
Called once when crafting begins to initialize your harmony type.
Function Signature:
initEffect: (harmonyData: HarmonyData, entity: CraftingEntity) => void
Common Operations:
- Initialize your custom data structure in
harmonyData.additionalData
- Apply starting buffs to the entity
- Set initial recommended techniques
5. renderComponent
(function, required)
Returns a React component to display your harmony’s visual state during crafting.
Function Signature:
renderComponent: (harmonyData: HarmonyData) => ReactNode
Guidelines:
- Component should be positioned absolutely within the crafting interface
- Use Box components with proper positioning
- Access your custom data from
harmonyData.additionalData
- Return visual feedback showing current state
- Can draw custom assets to be rendered, by drawing them on a blank image using the base cauldron background (below) as a guide. Do not include the cauldron itself in your new asset, simply use it as a guide for the image size and positioning of your new asset.
Complete Example
window.modAPI.actions.addHarmonyType('elemental', {
name: 'Elemental Balance',
description: `Balance fire and water elements to maintain <name>Harmony</name>.
<br/>
<br/>
Actions affect element levels:
<li>Fusion: +<num>3</num> fire</li>
<li>Refine: +<num>3</num> water</li>
<li>Support/Stabilize: +<num>1</num> to lower element</li>
<br/>
Perfect balance (both at 5): +<num>15</num> <name>Harmony</name>
<br/>
Imbalance: -<num>10</num> <name>Harmony</name> per point of difference`,
processEffect: (harmonyData, technique, progressState, entity, state) => {
// Initialize data if needed
harmonyData.additionalData = harmonyData.additionalData || {
fire: 5,
water: 5
};
// Process technique effects
if (technique.type === 'fusion') {
harmonyData.additionalData.fire = Math.min(10, harmonyData.additionalData.fire + 3);
state.craftingLog.push(`Fire element increased to ${harmonyData.elemenadditionalDatatal.fire}`);
} else if (technique.type === 'refine') {
harmonyData.additionalData.water = Math.min(10, harmonyData.additionalData.water + 3);
state.craftingLog.push(`Water element increased to ${harmonyData.additionalData.water}`);
} else {
// Support/Stabilize boost the lower element
if (harmonyData.additionalData.fire < harmonyData.additionalData.water) {
harmonyData.additionalData.fire += 1;
} else {
harmonyData.additionalData.water += 1;
}
}
// Calculate harmony based on balance
const diff = Math.abs(harmonyData.additionalData.fire - harmonyData.additionalData.water);
if (diff === 0 && harmonyData.additionalData.fire === 5) {
progressState.harmony += 15;
state.craftingLog.push(`Perfect balance! +15 harmony`);
} else {
progressState.harmony -= diff * 10;
state.craftingLog.push(`Imbalance penalty: -${diff * 10} harmony`);
}
// Apply buffs based on dominant element
if (harmonyData.additionalData.fire > harmonyData.additionalData.water) {
entity.buffs = [{
name: 'Fire Dominance',
icon: 'flame.png',
canStack: false,
stats: {
intensity: { value: 0.3, stat: 'intensity' }
},
effects: [],
onFusion: [],
onRefine: [],
stacks: 1,
displayLocation: 'none'
}, ...entity.buffs.filter(b => b.name !== 'Fire Dominance' && b.name !== 'Water Dominance')];
} else if (harmonyData.additionalData.water > harmonyData.additionalData.fire) {
entity.buffs = [{
name: 'Water Dominance',
icon: 'water.png',
canStack: false,
stats: {
control: { value: 0.3, stat: 'control' }
},
effects: [],
onFusion: [],
onRefine: [],
stacks: 1,
displayLocation: 'none'
}, ...entity.buffs.filter(b => b.name !== 'Fire Dominance' && b.name !== 'Water Dominance')];
}
// Recommend techniques to balance
if (harmonyData.additionalData.fire > harmonyData.additionalData.water + 2) {
harmonyData.recommendedTechniqueTypes = ['refine'];
} else if (harmonyData.additionalData.water > harmonyData.additionalData.fire + 2) {
harmonyData.recommendedTechniqueTypes = ['fusion'];
} else {
harmonyData.recommendedTechniqueTypes = ['support', 'stabilize'];
}
},
initEffect: (harmonyData, entity) => {
harmonyData.additionalData = {
fire: 5,
water: 5
};
harmonyData.recommendedTechniqueTypes = ['fusion', 'refine'];
},
renderComponent: (harmonyData) => {
const { fire = 5, water = 5 } = harmonyData.additionalData || {};
return (
<Box display="flex" mt={5.2} position="relative" justifyContent="center" id="elemental">
{/* Fit to the bounds of the cauldron. These are the magic numbers for width/height used in the game */}
<Box
width="calc(min(35vw, 35vh))"
height="calc(min(35vw, 35vh))"
position="relative"
sx=
>
{/* The internal box that your styling should sit within, to prevent it going outside the bounds. zIndex is to ensure it renders over the cauldron */}
<Box
display="flex"
position="absolute"
sx=
>
{/* Fire meter, absolutely positioned on the left */}
<Box
flex={1}
display="flex"
flexDirection="column"
alignItems="center"
position="absolute"
sx=
>
<Typography color="red">Fire: {fire}</Typography>
<Box
width="30px"
height="60px"
bgcolor="rgba(255,0,0,0.2)"
border="1px solid red"
position="relative"
>
<Box
position="absolute"
bottom={0}
width="100%"
height={`${fire * 10}%`}
bgcolor="red"
/>
</Box>
</Box>
{/* Water meter, absolutely positioned on the right*/}
<Box
flex={1}
display="flex"
flexDirection="column"
alignItems="center"
position="absolute"
sx=
>
<Typography color="blue">Water: {water}</Typography>
<Box
width="30px"
height="60px"
bgcolor="rgba(0,0,255,0.2)"
border="1px solid blue"
position="relative"
>
<Box
position="absolute"
bottom={0}
width="100%"
height={`${water * 10}%`}
bgcolor="blue"
/>
</Box>
</Box>
{/* Custom border image overlay. The width/height/top/left are the magic numbers used in the game, but tweak to position your custom image perfectly */}
<Box
position="absolute"
width={`128%`}
height={`122%`}
top={`-6.5%`}
left={`-14%`}
sx=${safeUrlEncode(elementalImg)}})`,
backgroundSize: 'contain',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center',
pointerEvents: 'none',
zIndex: 21,
}}
/>
</Box>
</Box>
</Box>
);
}
});
Item Type Mapping
To assign your harmony type to specific item types, use overrideItemTypeToHarmonyType
:
window.modAPI.actions.overrideItemTypeToHarmonyType({
'artefacts': 'elemental' as RecipeHarmonyType,
'cauldrons': 'elemental' as RecipeHarmonyType
});
Or you can add it to specific recipes instead.
const elementalRecipe: RecipeItem = {
kind: 'recipe',
//... recipe fields
harmonyTypeOverride: 'elemental' as RecipeHarmonyType,
}
Best Practices
- Balance Risk/Reward: Higher harmony bonuses should require more skill or risk
- Clear Visual Feedback: Your render component should clearly show the current state
- Informative Logging: Use
state.craftingLog.push()
to explain what’s happening - Buff Management: Always filter out old buffs before applying new ones with the same name
- Recommended Techniques: Use
harmonyData.recommendedTechniqueTypes
to guide players
Utility Functions
Common utilities available in harmony implementations:
// Color formatting for logs
const col = (content, color) => `<span style="color: ${color}">${content}</span>`;
// Technique type formatting
const fusion = `<span style="color: lime">Fusion</span>`;
const refine = `<span style="color: cyan">Refine</span>`;
const support = `<span style="color: #eb34db">Support</span>`;
const stabilize = `<span style="color: orange">Stabilize</span>`;