fertig-classic-games/public/src/games/forbiddenisland/IslandChat.js

125 lines
5.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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.', 'Ill route us.', 'Stay coordinated.'],
diver: ['The water doesnt scare me.', 'Going deep.', 'Ill take the wet path.'],
explorer: ['Ill 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}.`,
]) };
}