149 lines
6.5 KiB
JavaScript
149 lines
6.5 KiB
JavaScript
// Forbidden Island — static data. No Phaser, no game state here: just the board
|
||
// silhouette, the tile/treasure/role catalog, and deck composition. Everything
|
||
// dynamic lives in IslandLogic.js.
|
||
|
||
// The island is a 6×6 grid with the four corners of each edge removed, giving
|
||
// the classic 24-tile diamond:
|
||
//
|
||
// . . X X . .
|
||
// . X X X X .
|
||
// X X X X X X
|
||
// X X X X X X
|
||
// . X X X X .
|
||
// . . X X . .
|
||
//
|
||
export const GRID = 6;
|
||
|
||
// Playable cells in reading order (row, col). 24 of them.
|
||
export const CELLS = [
|
||
[0, 2], [0, 3],
|
||
[1, 1], [1, 2], [1, 3], [1, 4],
|
||
[2, 0], [2, 1], [2, 2], [2, 3], [2, 4], [2, 5],
|
||
[3, 0], [3, 1], [3, 2], [3, 3], [3, 4], [3, 5],
|
||
[4, 1], [4, 2], [4, 3], [4, 4],
|
||
[5, 2], [5, 3],
|
||
];
|
||
|
||
export function isCell(r, c) {
|
||
return CELLS.some(([cr, cc]) => cr === r && cc === c);
|
||
}
|
||
|
||
// Treasures, each tied to two island tiles. `element` keys the treasure cards.
|
||
export const TREASURES = {
|
||
earth: { key: 'earth', name: 'The Earth Stone', tiles: ['temple-moon', 'temple-sun'], color: 0x8d6e3c },
|
||
wind: { key: 'wind', name: 'The Statue of the Wind', tiles: ['whispering-garden', 'howling-garden'], color: 0x9aa7b0 },
|
||
fire: { key: 'fire', name: 'The Crystal of Fire', tiles: ['cave-embers', 'cave-shadows'], color: 0xc0492f },
|
||
ocean: { key: 'ocean', name: "The Ocean's Chalice", tiles: ['coral-palace', 'tidal-palace'], color: 0x2f7fb0 },
|
||
};
|
||
|
||
export const TREASURE_KEYS = ['earth', 'wind', 'fire', 'ocean'];
|
||
|
||
// All 24 tiles. `treasure` links a tile to a treasure; `landing` marks the
|
||
// helipad you must escape from. The rest are neutral.
|
||
export const TILES = [
|
||
{ id: 'temple-moon', name: 'Temple of the Moon', treasure: 'earth' },
|
||
{ id: 'temple-sun', name: 'Temple of the Sun', treasure: 'earth' },
|
||
{ id: 'whispering-garden', name: 'Whispering Garden', treasure: 'wind' },
|
||
{ id: 'howling-garden', name: 'Howling Garden', treasure: 'wind' },
|
||
{ id: 'cave-embers', name: 'Cave of Embers', treasure: 'fire' },
|
||
{ id: 'cave-shadows', name: 'Cave of Shadows', treasure: 'fire' },
|
||
{ id: 'coral-palace', name: 'Coral Palace', treasure: 'ocean' },
|
||
{ id: 'tidal-palace', name: 'Tidal Palace', treasure: 'ocean' },
|
||
{ id: 'fools-landing', name: "Fools' Landing", landing: true },
|
||
{ id: 'bronze-gate', name: 'Bronze Gate' },
|
||
{ id: 'copper-gate', name: 'Copper Gate' },
|
||
{ id: 'silver-gate', name: 'Silver Gate' },
|
||
{ id: 'gold-gate', name: 'Gold Gate' },
|
||
{ id: 'iron-gate', name: 'Iron Gate' },
|
||
{ id: 'watchtower', name: 'Watchtower' },
|
||
{ id: 'observatory', name: 'Observatory' },
|
||
{ id: 'phantom-rock', name: 'Phantom Rock' },
|
||
{ id: 'misty-marsh', name: 'Misty Marsh' },
|
||
{ id: 'breakers-bridge', name: 'Breakers Bridge' },
|
||
{ id: 'crimson-forest', name: 'Crimson Forest' },
|
||
{ id: 'dunes-deception', name: 'Dunes of Deception' },
|
||
{ id: 'lost-lagoon', name: 'Lost Lagoon' },
|
||
{ id: 'twilight-hollow', name: 'Twilight Hollow' },
|
||
{ id: 'cliffs-abandon', name: 'Cliffs of Abandon' },
|
||
];
|
||
|
||
// The six adventurer roles. `power` is read by the engine/AI to branch special
|
||
// abilities; `color` is the pawn colour.
|
||
export const ROLES = {
|
||
pilot: { key: 'pilot', name: 'Pilot', color: 0x4a90d9, colorHex: '#4a90d9', power: 'Once per turn, fly to any tile.' },
|
||
engineer: { key: 'engineer', name: 'Engineer', color: 0xd0473a, colorHex: '#d0473a', power: 'Shore up two tiles for one action.' },
|
||
messenger: { key: 'messenger', name: 'Messenger', color: 0xb9bfc6, colorHex: '#b9bfc6', power: 'Give Treasure cards to anyone, any distance.' },
|
||
navigator: { key: 'navigator', name: 'Navigator', color: 0xe2b53c, colorHex: '#e2b53c', power: 'Move another adventurer up to 2 tiles.' },
|
||
diver: { key: 'diver', name: 'Diver', color: 0x2b2b30, colorHex: '#2b2b30', power: 'Swim through flooded and missing tiles.' },
|
||
explorer: { key: 'explorer', name: 'Explorer', color: 0x49a25a, colorHex: '#49a25a', power: 'Move and shore up diagonally.' },
|
||
};
|
||
|
||
export const ROLE_KEYS = ['pilot', 'engineer', 'messenger', 'navigator', 'diver', 'explorer'];
|
||
|
||
// Special (non-treasure) Treasure-deck cards.
|
||
export const SPECIAL = {
|
||
WATERS_RISE: 'waters-rise',
|
||
HELICOPTER: 'helicopter',
|
||
SANDBAGS: 'sandbags',
|
||
};
|
||
|
||
// Treasure deck: 5 of each treasure (20) + 3 Waters Rise! + 3 Helicopter Lift +
|
||
// 2 Sandbags = 28 cards. Cards are plain ids: `treasure:earth`, `waters-rise`, …
|
||
export function buildTreasureDeck() {
|
||
const deck = [];
|
||
for (const key of TREASURE_KEYS) for (let i = 0; i < 5; i++) deck.push(`treasure:${key}`);
|
||
for (let i = 0; i < 3; i++) deck.push(SPECIAL.WATERS_RISE);
|
||
for (let i = 0; i < 3; i++) deck.push(SPECIAL.HELICOPTER);
|
||
for (let i = 0; i < 2; i++) deck.push(SPECIAL.SANDBAGS);
|
||
return deck;
|
||
}
|
||
|
||
// Water-level track. Index = level (1..10); value = flood cards drawn per turn.
|
||
// Level 10 is the skull: reaching it loses the game.
|
||
export const WATER_TRACK = [null, 2, 2, 3, 3, 3, 4, 4, 5, 5, 'skull'];
|
||
export const MAX_WATER = 10;
|
||
|
||
export const DIFFICULTY = {
|
||
novice: { key: 'novice', name: 'Novice', start: 1 },
|
||
normal: { key: 'normal', name: 'Normal', start: 2 },
|
||
elite: { key: 'elite', name: 'Elite', start: 3 },
|
||
legendary: { key: 'legendary', name: 'Legendary', start: 4 },
|
||
};
|
||
|
||
export function floodDrawCount(waterLevel) {
|
||
const v = WATER_TRACK[Math.min(waterLevel, MAX_WATER)];
|
||
return typeof v === 'number' ? v : 5;
|
||
}
|
||
|
||
// Cards needed to capture a treasure.
|
||
export const CARDS_TO_CAPTURE = 4;
|
||
// Hand limit; over this at end of turn you must discard.
|
||
export const HAND_LIMIT = 5;
|
||
// Actions per turn.
|
||
export const ACTIONS_PER_TURN = 3;
|
||
|
||
export function tileById(id) {
|
||
return TILES.find((t) => t.id === id) ?? null;
|
||
}
|
||
|
||
// Tile id → its row in the `forbiddenisland-tiles` spritesheet. Dry side is
|
||
// frame 2·row, flooded side is frame 2·row+1.
|
||
export const TILE_FRAME_ROW = Object.fromEntries(TILES.map((t, i) => [t.id, i]));
|
||
|
||
// Card id → frame in the `forbiddenisland-cards` spritesheet (320×420 cells, a
|
||
// 4-col × 2-row grid). Top row: the four treasure cards. Bottom row: the three
|
||
// special cards (frame 7 is unused / a card back).
|
||
export const CARD_FRAME = {
|
||
'treasure:earth': 0,
|
||
'treasure:wind': 1,
|
||
'treasure:fire': 2,
|
||
'treasure:ocean': 3,
|
||
[SPECIAL.WATERS_RISE]: 4,
|
||
[SPECIAL.HELICOPTER]: 5,
|
||
[SPECIAL.SANDBAGS]: 6,
|
||
};
|
||
|
||
export function cardFrame(card) {
|
||
return CARD_FRAME[card] ?? null;
|
||
}
|