Tonic / Salve Making
Implementation of tonic / salve making that defines how players interact with this aspect of the game, including feedback and progression.
Overview
The tonic / salve making mechanic provides a framework that provides meaningful choices and consequences for player actions. When well-implemented, this mechanic creates a satisfying feedback loop that keeps players engaged and motivated to continue playing. The ongoing evolution of this mechanic reflects the broader maturation of game design as a discipline.
Game Examples
Mech Games
Mech Games use this mechanic where players coordinate with teammates to min-max their character. Randomized elements ensure variety across sessions, resulting in emergent storytelling.
Soulslike Games
Soulslike Games use this mechanic where players allocate limited resources to reach the highest tier. Visual and audio feedback make the interaction satisfying, resulting in narrative investment.
Boxing Games
Boxing Games use this mechanic where players balance risk and reward to maximize their effectiveness. Emergent gameplay arises from simple rules, resulting in build diversity.
Pros & Cons
Advantages
- Rewards both mechanical skill and strategic thinking
- Rewards both reaction time and game knowledge
- Provides clear delayed feedback on player actions
Disadvantages
- May overwhelm returning players with too many options
- Risk of feature bloat in multiplayer contexts
- Can feel punishing if progression is too slow
- Requires extensive QA testing to avoid edge cases
Implementation Patterns
Assembly Pipeline
Core implementation pattern for handling tonic / salve making logic with clean state management.
class TonicSalveMakingEngine {
recipes: Recipe[] = [];
craft(recipeId: string, inventory: Inventory) {
const recipe = this.recipes.find(r => r.id === recipeId);
if (!recipe) return null;
for (const ingredient of recipe.ingredients) {
if (!inventory.has(ingredient.id, ingredient.amount)) {
return null; // Missing materials
}
}
for (const ingredient of recipe.ingredients) {
inventory.remove(ingredient.id, ingredient.amount);
}
const quality = this.rollQuality(0.2);
return { ...recipe.output, quality };
}
rollQuality(baseChance: number) {
const roll = Math.random();
if (roll < baseChance * 0.01) return "legendary";
if (roll < baseChance * 0.2) return "rare";
if (roll < baseChance) return "uncommon";
return "common";
}
}