125 lines
5.4 KiB
JavaScript
125 lines
5.4 KiB
JavaScript
// 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}.`,
|
||
]) };
|
||
}
|