// Forbidden Island β€” team-chat phrase library. 100% offline string templates. // // The heuristic planner (IslandAI.describeIntent) produces a rationale object; // this turns it into a line of table talk in the speaking role's "voice". // Game events (a tile sinking, Waters Rise!, a capture) get their own lines so // the chat reads like a real co-op table. import { TREASURES, ROLES } from './IslandData.js'; const EMOJI = { pilot: '✈️', engineer: 'πŸ”§', messenger: 'βœ‰οΈ', navigator: '🧭', diver: '🀿', explorer: 'πŸ§—', }; export function roleEmoji(role) { return EMOJI[role] ?? '🧩'; } export function roleName(role) { return ROLES[role]?.name ?? role; } export function roleColorHex(role) { return ROLES[role]?.colorHex ?? '#f2ead8'; } const pick = (arr) => arr[Math.floor(Math.random() * arr.length)]; const tName = (key) => TREASURES[key]?.name ?? 'the treasure'; // Little role-specific interjections, sprinkled in for character. const FLAVOR = { pilot: ['Wings ready.', 'I can be anywhere in a heartbeat.', 'Just say the word.'], engineer: ['Efficient as always.', 'On it.', 'Consider it done.'], messenger: ['Happy to help.', 'Cards incoming.', "I've got the network."], navigator: ['Follow my lead.', 'I’ll route us.', 'Stay coordinated.'], diver: ['The water doesn’t scare me.', 'Going deep.', 'I’ll take the wet path.'], explorer: ['I’ll take the diagonal.', 'Scouting ahead.', 'I see a way.'], }; const INTENT = { CAPTURE: (r) => pick([ `I'm standing on the temple with a full set β€” claiming ${tName(r.treasure)} now!`, `Four cards in hand and feet on the tile. ${tName(r.treasure)} is ours this turn.`, `Securing ${tName(r.treasure)} β€” that's one less treasure to worry about.`, ]), SEEK: (r) => { const base = pick([ `I've got ${r.cards} card${r.cards === 1 ? '' : 's'} toward ${tName(r.treasure)} β€” pushing for its temple.`, `Working on ${tName(r.treasure)}. Heading for the tile, ${r.cards}/4 so far.`, `${tName(r.treasure)} is my project β€” making my way there.`, ]); if (r.request) return `${base} ${roleName(r.request.fromRole)}, if you're holding a matching card, send it my way?`; return base; }, SHORE: (r) => pick([ `Shoring up β€” we can't afford to lose ground here.`, `Patching the flooding before it spreads. Watch the map.`, `Holding the line on the flooded tiles near me.`, ]), ESCAPE: () => pick([ `All four treasures are in. Regrouping at Fools' Landing β€” let's fly out together!`, `Treasures secured! Everyone to the helipad, we need a Helicopter Lift.`, `This is the home stretch β€” converging on Fools' Landing.`, ]), SUPPORT: () => pick([ `Nothing urgent for me β€” repositioning to help where it counts.`, `Holding steady and keeping my options open.`, `I'll back up whoever needs it this turn.`, ]), }; // A line announcing the AI partner's plan for the turn. export function lineForIntent(rationale) { const make = INTENT[rationale.intent] ?? INTENT.SUPPORT; let text = make(rationale); // Occasionally append a threat warning. const t = rationale.threats?.[0]; if (t && Math.random() < 0.5) { text += ` ${pick([ `Heads up β€” ${t.name} floods next, someone keep an eye on it.`, `Also: ${t.name} is in danger. Don't let it sink.`, `Watch ${t.name}, it's one flood from trouble.`, ])}`; } else if (Math.random() < 0.25) { text += ` ${pick(FLAVOR[rationale.role] ?? [])}`; } return { role: rationale.role, text }; } // Lines for notable game events. `ctx` carries the names already resolved. export function lineForEvent(kind, ctx = {}) { switch (kind) { case 'sink': return { role: ctx.role ?? null, text: pick([ `${ctx.tileName} just sank beneath the waves.`, `We lost ${ctx.tileName} β€” it's gone for good.`, ]) }; case 'watersRise': return { role: ctx.role ?? null, text: pick([ `Waters Rise! The flood is accelerating β€” water level ${ctx.waterLevel}.`, `That's a Waters Rise card. Everything we flooded is coming back around.`, ]) }; case 'capture': return { role: ctx.role, text: pick([ `Got it! ${tName(ctx.treasure)} is secured. ${4 - (ctx.remaining ?? 0)} of 4 down.`, `${tName(ctx.treasure)} claimed β€” great teamwork.`, ]) }; case 'sandbags': return { role: ctx.role, text: pick([ `Dropping sandbags on ${ctx.tileName} β€” that holds the line, no action spent.`, `Sandbags out on ${ctx.tileName}. We can't lose that one.`, ]) }; case 'heliMove': return { role: ctx.role, text: pick([ `Helicopter lift β€” flying ${ctx.who} straight to ${ctx.tileName} for the capture.`, `Burning a Helicopter to get ${ctx.who} onto ${ctx.tileName} before it's too late.`, ]) }; case 'swim': return { role: ctx.role, text: pick([ `Tile sank under me β€” swimming to safety.`, `Had to bail to ${ctx.tileName} as the ground gave way.`, ]) }; case 'won': return { role: null, text: pick([ `We made it off the island β€” together! πŸŽ‰`, `Helicopter's airborne with all four treasures. We win!`, ]) }; case 'lost': return { role: null, text: ctx.reason ?? 'The island is lost.' }; default: return null; } } // Acknowledgement when the human sets a strategy priority. export function lineForAck(role, label) { return { role, text: pick([ `Copy that β€” ${label}.`, `Understood. Re-planning around: ${label}.`, `Roger. Prioritizing ${label}.`, ]) }; }