ot_travel
Plan supplies and choose a travel pace to manage food consumption and health changes during a 3-day trail. Random events add further gains or losses.
Instructions
Hit the trail for 3 days. Always costs food and may affect health — plan supplies before calling. Costs: easy=-6 food, +9hp each; steady=-9 food, 0hp change; hard=-12 food, -21hp each, -12hp pet. Random events add further gains/losses on top. Narrate what happens to the human dramatically.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| pace | Yes | Travel pace — easy (15 mi/day, -6 food, +9hp), steady (22 mi/day, -9 food, no health change), hard (32 mi/day, -12 food, -21hp, -12hp pet) |
Implementation Reference
- src/games/oregontrail.ts:769-878 (handler)The ot_travel tool handler: simulates 3 days of travel at a given pace (easy/steady/hard). Manages food consumption, health changes, pet wear on hard pace, random events, weather changes, stop arrivals, river crossings, and game-over checks. Returns a narrative summary and updated game state.
server.tool( "ot_travel", "Hit the trail for 3 days. Always costs food and may affect health — plan supplies before calling. Costs: easy=-6 food, +9hp each; steady=-9 food, 0hp change; hard=-12 food, -21hp each, -12hp pet. Random events add further gains/losses on top. Narrate what happens to the human dramatically.", { pace: z.enum(["easy", "steady", "hard"]).describe("Travel pace — easy (15 mi/day, -6 food, +9hp), steady (22 mi/day, -9 food, no health change), hard (32 mi/day, -12 food, -21hp, -12hp pet)"), }, async ({ pace }) => { if (!otGame) return { content: [{ type: "text", text: "No journey in progress. Call ot_new_game to begin." }], isError: true }; if (otGame.status === "won" || otGame.status === "game_over") return { content: [{ type: "text", text: `The journey is over. Call ot_new_game to start again.` }], isError: true }; if (otGame.pendingRiver) return { content: [{ type: "text", text: `You're at a river. Choose a crossing method with ot_cross_river first.\n\n${renderState(otGame)}` }], isError: true }; const milesPerDay = pace === "easy" ? 15 : pace === "steady" ? 22 : 32; const healthPerDay = pace === "easy" ? 3 : pace === "steady" ? 0 : -7; const foodPerDay = pace === "easy" ? 2 : pace === "steady" ? 3 : 4; const daysOfTravel = 3; const narratives: string[] = []; const foodBefore = otGame.supplies.food; for (let d = 0; d < daysOfTravel; d++) { otGame.day++; otGame.mile = Math.min(TOTAL_MILES, otGame.mile + milesPerDay); otGame.supplies.food = Math.max(0, otGame.supplies.food - foodPerDay); // Starvation if (otGame.supplies.food <= 0) { otGame.player.health = clamp(otGame.player.health - 10, 0, 100); otGame.companion.health = clamp(otGame.companion.health - 10, 0, 100); narratives.push(`No food. Hunger is taking its toll.`); log(otGame, "No provisions. Everyone weakening."); } // Pace health if (healthPerDay !== 0) { otGame.player.health = clamp(otGame.player.health + healthPerDay, 0, 100); otGame.companion.health = clamp(otGame.companion.health + healthPerDay, 0, 100); } // Pets wear down on hard pace too if (pace === "hard" && otGame.pet.alive) { otGame.pet.health = clamp(otGame.pet.health - 4, 0, 100); if (otGame.pet.health <= 0) otGame.pet.alive = false; } // Random event if (Math.random() < 0.65) { const ev = rollEvent(otGame); applyEvent(otGame, ev); narratives.push(ev.text); log(otGame, ev.text.slice(0, 80) + (ev.text.length > 80 ? "…" : "")); } // Weather shift if (Math.random() < 0.25) otGame.weather = pick(WEATHER_OPTIONS); // Check new stop arrival for (let i = otGame.currentStopIndex + 1; i < otGame.trailStops.length; i++) { if (otGame.mile >= otGame.trailStops[i].mile) { otGame.currentStopIndex = i; const stop = otGame.trailStops[i]; narratives.push(`You've reached ${stop.name}.`); log(otGame, `Arrived at ${stop.name}.`); if (stop.type === "destination") { otGame.status = "won"; } else if (stop.type === "river") { otGame.status = "river_crossing"; otGame.pendingRiver = true; } else { otGame.status = "at_stop"; } break; } } // Death check if (otGame.player.health <= 0 && otGame.companion.health <= 0) { otGame.status = "game_over"; narratives.push("Both travelers have fallen. The road claims two more who sought the Well."); break; } if (otGame.status === "won" || (otGame.status as string) === "game_over" || otGame.pendingRiver) break; } if (otGame.status !== "at_stop" && otGame.status !== "river_crossing" && otGame.status !== "won" && otGame.status !== "game_over") { otGame.status = "traveling"; } const travelFoodCost = foodPerDay * daysOfTravel; const netFoodChange = otGame.supplies.food - foodBefore; const eventFoodNet = netFoodChange + travelFoodCost; // positive = events gave food overall const summary = [ `━━ ${daysOfTravel} days on the trail (${pace} pace) ━━`, `Travel cost: -${travelFoodCost} food, +${milesPerDay * daysOfTravel} miles`, eventFoodNet !== 0 ? `Events: ${eventFoodNet > 0 ? "+" : ""}${eventFoodNet} food (net)` : `Events: no net food change`, `Food: ${foodBefore} → ${otGame.supplies.food}`, ].join(" | "); const body = narratives.map((n, i) => `Day ${otGame!.day - (daysOfTravel - 1 - i)}: ${n}`).join("\n\n"); return { content: [{ type: "text", text: `${summary}\n\n${body}\n\n${renderState(otGame)}`, }], }; } ); - src/games/oregontrail.ts:772-774 (schema)Input schema for ot_travel: accepts 'pace' parameter as a string enum with values 'easy', 'steady', or 'hard'.
{ pace: z.enum(["easy", "steady", "hard"]).describe("Travel pace — easy (15 mi/day, -6 food, +9hp), steady (22 mi/day, -9 food, no health change), hard (32 mi/day, -12 food, -21hp, -12hp pet)"), }, - src/games/oregontrail.ts:769-878 (registration)The tool is registered via server.tool() with the name 'ot_travel' inside the registerOregonTrailTools function (line 687).
server.tool( "ot_travel", "Hit the trail for 3 days. Always costs food and may affect health — plan supplies before calling. Costs: easy=-6 food, +9hp each; steady=-9 food, 0hp change; hard=-12 food, -21hp each, -12hp pet. Random events add further gains/losses on top. Narrate what happens to the human dramatically.", { pace: z.enum(["easy", "steady", "hard"]).describe("Travel pace — easy (15 mi/day, -6 food, +9hp), steady (22 mi/day, -9 food, no health change), hard (32 mi/day, -12 food, -21hp, -12hp pet)"), }, async ({ pace }) => { if (!otGame) return { content: [{ type: "text", text: "No journey in progress. Call ot_new_game to begin." }], isError: true }; if (otGame.status === "won" || otGame.status === "game_over") return { content: [{ type: "text", text: `The journey is over. Call ot_new_game to start again.` }], isError: true }; if (otGame.pendingRiver) return { content: [{ type: "text", text: `You're at a river. Choose a crossing method with ot_cross_river first.\n\n${renderState(otGame)}` }], isError: true }; const milesPerDay = pace === "easy" ? 15 : pace === "steady" ? 22 : 32; const healthPerDay = pace === "easy" ? 3 : pace === "steady" ? 0 : -7; const foodPerDay = pace === "easy" ? 2 : pace === "steady" ? 3 : 4; const daysOfTravel = 3; const narratives: string[] = []; const foodBefore = otGame.supplies.food; for (let d = 0; d < daysOfTravel; d++) { otGame.day++; otGame.mile = Math.min(TOTAL_MILES, otGame.mile + milesPerDay); otGame.supplies.food = Math.max(0, otGame.supplies.food - foodPerDay); // Starvation if (otGame.supplies.food <= 0) { otGame.player.health = clamp(otGame.player.health - 10, 0, 100); otGame.companion.health = clamp(otGame.companion.health - 10, 0, 100); narratives.push(`No food. Hunger is taking its toll.`); log(otGame, "No provisions. Everyone weakening."); } // Pace health if (healthPerDay !== 0) { otGame.player.health = clamp(otGame.player.health + healthPerDay, 0, 100); otGame.companion.health = clamp(otGame.companion.health + healthPerDay, 0, 100); } // Pets wear down on hard pace too if (pace === "hard" && otGame.pet.alive) { otGame.pet.health = clamp(otGame.pet.health - 4, 0, 100); if (otGame.pet.health <= 0) otGame.pet.alive = false; } // Random event if (Math.random() < 0.65) { const ev = rollEvent(otGame); applyEvent(otGame, ev); narratives.push(ev.text); log(otGame, ev.text.slice(0, 80) + (ev.text.length > 80 ? "…" : "")); } // Weather shift if (Math.random() < 0.25) otGame.weather = pick(WEATHER_OPTIONS); // Check new stop arrival for (let i = otGame.currentStopIndex + 1; i < otGame.trailStops.length; i++) { if (otGame.mile >= otGame.trailStops[i].mile) { otGame.currentStopIndex = i; const stop = otGame.trailStops[i]; narratives.push(`You've reached ${stop.name}.`); log(otGame, `Arrived at ${stop.name}.`); if (stop.type === "destination") { otGame.status = "won"; } else if (stop.type === "river") { otGame.status = "river_crossing"; otGame.pendingRiver = true; } else { otGame.status = "at_stop"; } break; } } // Death check if (otGame.player.health <= 0 && otGame.companion.health <= 0) { otGame.status = "game_over"; narratives.push("Both travelers have fallen. The road claims two more who sought the Well."); break; } if (otGame.status === "won" || (otGame.status as string) === "game_over" || otGame.pendingRiver) break; } if (otGame.status !== "at_stop" && otGame.status !== "river_crossing" && otGame.status !== "won" && otGame.status !== "game_over") { otGame.status = "traveling"; } const travelFoodCost = foodPerDay * daysOfTravel; const netFoodChange = otGame.supplies.food - foodBefore; const eventFoodNet = netFoodChange + travelFoodCost; // positive = events gave food overall const summary = [ `━━ ${daysOfTravel} days on the trail (${pace} pace) ━━`, `Travel cost: -${travelFoodCost} food, +${milesPerDay * daysOfTravel} miles`, eventFoodNet !== 0 ? `Events: ${eventFoodNet > 0 ? "+" : ""}${eventFoodNet} food (net)` : `Events: no net food change`, `Food: ${foodBefore} → ${otGame.supplies.food}`, ].join(" | "); const body = narratives.map((n, i) => `Day ${otGame!.day - (daysOfTravel - 1 - i)}: ${n}`).join("\n\n"); return { content: [{ type: "text", text: `${summary}\n\n${body}\n\n${renderState(otGame)}`, }], }; } ); - src/games/oregontrail.ts:304-306 (helper)Helper function used by ot_travel to clamp health and supplies values between min/max bounds.
function clamp(v: number, lo: number, hi: number): number { return Math.max(lo, Math.min(hi, v)); } - src/games/oregontrail.ts:534-608 (helper)Helper function used by ot_travel to roll random events during travel days. Returns pet-specific, normal, or mysterious events based on state.
function rollEvent(state: OtState): GameEvent { const p = state.player.name; const c = state.companion.name; const pet = state.pet.name; // ~30% chance: draw from the species-specific pool when pet is alive if (state.pet.alive && Math.random() < 0.30) { const pool = PET_EVENTS.filter(ev => ev.petTypes!.includes(state.pet.type)); if (pool.length > 0) return resolvePetEvent(pick(pool), state); } const normal: GameEvent[] = [ // good weather { text: "Clear skies and a cool breeze. Spirits are high and the oxen move well.", playerHealth: 5, compHealth: 5 }, { text: "A perfect traveling day. Everyone finds their stride.", playerHealth: 8, compHealth: 8, petHealth: 5 }, // bad weather { text: `A violent storm rolls in without warning. You make camp soaked to the bone. ${p} wakes up shivering.`, playerHealth: -15, food: -1 }, { text: `Scorching heat bakes the trail. The oxen slow to a crawl. ${c} barely speaks.`, compHealth: -12 }, { text: "Three days of steady rain turn the trail to mud. Progress is miserable.", food: -2, playerHealth: -5, compHealth: -5 }, // health bad { text: `${p} wakes up feverish. You press on, but it's slow going.`, playerHealth: -22 }, { text: `${c} slips on loose rock and twists their ankle badly.`, compHealth: -25 }, { text: `A mysterious cough spreads through camp. Both of you feel it by nightfall.`, playerHealth: -12, compHealth: -12 }, { text: `Something in the water disagrees with ${p}. A rough night follows.`, playerHealth: -18 }, // health good { text: "You find a hot spring just off the trail. Everyone soaks for an afternoon. Worth every minute.", playerHealth: 18, compHealth: 18, petHealth: 12 }, { text: `${c} finds wild herbs that make a surprisingly restorative tea.`, playerHealth: 14, compHealth: 14 }, // supplies found — abandoned camps (varied so each one feels like a different story) { text: "An abandoned camp at the roadside, cold for at least a day. No sign of why they left or where they went. The food is good, though.", food: 7, ammo: 10 }, { text: "A wagon half-sunk in a ditch, stripped of everything except what no one wanted — some provisions and a crate of ammunition. You take both and don't linger.", food: 5, ammo: 20 }, { text: `A campsite, recently abandoned — the fire is still warm. ${p} calls out. No answer comes back. You help yourselves to what's left and move on before dark.`, food: 9 }, { text: "Someone left in a hurry. The bedrolls are still laid out. You salvage provisions and a spare part from the wagon wreck nearby, and decide not to think too hard about it.", food: 6, parts: 1 }, { text: `An empty camp marked with a boot hung from a tree — an old trail signal meaning 'gone ahead, help yourself.' The provisions left behind suggest they were optimistic about what lay ahead.`, food: 10, ammo: 8 }, { text: `A camp with the fire burned down to ash and a meal left half-eaten. ${c} checks the perimeter. Nothing. You pack the remaining food and leave quickly.`, food: 4, ammo: 12 }, // supplies found — foraging and traders { text: "A good morning's foraging turns up berries and roots. Not glamorous, but it helps.", food: 5 }, { text: `${c} spots something edible off the trail and spends an hour collecting it. Not a word of complaint. Just hands you a bundle of food and keeps walking.`, food: 7 }, { text: "A passing trader offers a fair deal on surplus food.", food: 9 }, { text: `A trader coming the other direction flags you down. They've got more food than they need and no interest in hauling it back. You negotiate quickly and part ways satisfied.`, food: 11, money: -5 }, // supplies lost — wagon and oxen { text: "The wagon hits a deep rut. You hear a crack. A wheel spoke has splintered.", parts: -1 }, { text: `A rocky descent rattles the wagon harder than expected. Something in the undercarriage shifts. ${c} inspects it and doesn't look happy.`, parts: -1 }, { text: `The heat has been warping the wood. A board on the wagon bed splits clean through. You patch it as best you can, but it costs a part.`, parts: -1 }, { text: "One of the oxen wanders off in the night. Hours of searching yield nothing.", oxen: -1 }, { text: `One of the oxen has been favoring its left foreleg all day. By evening it won't bear weight. You lose time and burn provisions keeping the others moving.`, oxen: -1, food: -3 }, // supplies lost — food and money { text: "Something got into the food supply overnight. Half a week's provisions are ruined.", food: -4 }, { text: `Bandits step onto the trail. After a tense standoff they take what they came for and leave.`, money: -14, food: -2 }, // pet health — general (applies regardless of species) { text: `${pet} seems off today — quieter than usual, slower, eating less. Nothing you can point to. You keep a close eye on them.`, petHealth: -14 }, // encounters { text: `You pass a family heading the opposite direction. They look shaken. They won't say why.` }, { text: `A wandering physician offers medicine at a fair price.`, medicine: 2, money: -10 }, { text: `You share a fire with another group heading the same way. Food and stories are exchanged. A good night.`, food: 4, playerHealth: 8, compHealth: 8 }, ]; const mysterious: GameEvent[] = [ { text: `You hear music on the wind with no visible source. It stops the moment ${c} tries to identify the melody.`, mysterious: true }, { text: `Both of you describe the same dream at breakfast, word for word. Neither of you had mentioned it first.`, mysterious: true }, { text: `A signpost appears on the trail — old wood, older than anything else you've seen. It points toward the Well.`, mysterious: true }, { text: `An old woman at the roadside watches you pass. "The Well gives what you need," she says. "Not what you want." She won't say more.`, mysterious: true }, { text: `The trail seems shorter today. You make better distance than you should have. ${c} checks the map twice.`, mysterious: true }, { text: `${pet} stares into the fog for a long time, then looks at you, then back at the fog. You decide not to think about it.`, mysterious: true }, { text: `You find a cairn of stones at the roadside, freshly stacked. No one else is on the trail. ${c} doesn't sleep well.`, mysterious: true }, { text: `The fire goes out in the night and relights itself before either of you notices. ${c} is certain you both saw it happen. Neither of you mentions it again.`, mysterious: true }, { text: `You find a page torn from a journal in the mud. The handwriting looks like ${p}'s. The date at the top is three weeks from now.`, mysterious: true }, { text: `A child waves to you from the crest of a hill a quarter mile off the trail. When ${c} waves back, the child turns and walks into the ground like it was water.`, mysterious: true }, { text: `Your shadow points the wrong direction for about an hour. Then it doesn't.`, mysterious: true }, { text: `${c} stops speaking mid-sentence and stares past you for a full minute. When they come back, they say: "Sorry — did I say something?" They genuinely can't remember what they were saying.`, mysterious: true }, ]; const nearEnd = state.mile > 850; if (nearEnd && Math.random() < 0.55) return pick(mysterious); return pick(normal); }