587 lines
20 KiB
JavaScript
587 lines
20 KiB
JavaScript
import * as Phaser from 'phaser';
|
|
import { GAME_WIDTH, GAME_HEIGHT, COLORS } from '../../config.js';
|
|
import { api } from '../../services/api.js';
|
|
import { Button } from '../../ui/Button.js';
|
|
import { MusicPlayer } from '../../ui/MusicPlayer.js';
|
|
import { playSound, SFX } from '../../ui/Sounds.js';
|
|
import { MAX_WRONG, maskWord, isWordSolved, countWrong, wrongLetters } from './HangmanLogic.js';
|
|
|
|
// ── Palette ──────────────────────────────────────────────────────────────────
|
|
const PAPER = 0xFFF8F0;
|
|
const PAPER_EDGE = 0xE8D8C0;
|
|
const INK = '#3a2a18';
|
|
const INK_N = 0x3a2a18;
|
|
const TITLE_INK = '#5c3a1e';
|
|
const RED_INK = '#b03a2e';
|
|
const FADED = '#b09a7a';
|
|
const FADED_N = 0xb09a7a;
|
|
const SPIRAL_CLR = 0x8a7060;
|
|
const CARD_EDGE = 0xc8b898;
|
|
const WRONG_RED = '#c0392b';
|
|
const SKULL_FULL = 0x4a3020;
|
|
const SKULL_FADE = 0xd8c8b0;
|
|
|
|
const DEPTH = { bg: 0, paper: 1, card: 2, gallows: 5, ui: 20, overlay: 40 };
|
|
|
|
// ── Layout constants ─────────────────────────────────────────────────────────
|
|
const CX = GAME_WIDTH / 2;
|
|
const CY = GAME_HEIGHT / 2;
|
|
|
|
// Paper sheet
|
|
const PX = 80, PY = 50, PW = 1760, PH = 980;
|
|
|
|
// Spiral binding circles
|
|
const SPIRAL_R = 18;
|
|
const SPIRAL_CNT = 32;
|
|
|
|
// Gallows card (left)
|
|
const GC_X = 260, GC_Y = 220, GC_W = 560, GC_H = 560;
|
|
|
|
// Info card (right)
|
|
const IC_X = 920, IC_Y = 220, IC_W = 820, IC_H = 560;
|
|
|
|
// Keyboard card (bottom)
|
|
const KB_X = 140, KB_Y = 840, KB_W = 1640, KB_H = 190;
|
|
|
|
// Gallows drawing origin inside gallows card
|
|
const GW_OX = GC_X + GC_W / 2 - 20;
|
|
const GW_OY = GC_Y + 50;
|
|
const GW_H = 400; // total height of gallows
|
|
const GW_W = 180; // base width
|
|
|
|
// ── QWERTY rows ───────────────────────────────────────────────────────────────
|
|
const QWERTY = [
|
|
['Q','W','E','R','T','Y','U','I','O','P'],
|
|
['A','S','D','F','G','H','J','K','L'],
|
|
['Z','X','C','V','B','N','M'],
|
|
];
|
|
|
|
export default class HangmanGame extends Phaser.Scene {
|
|
constructor() { super('HangmanGame'); }
|
|
|
|
init(data) {
|
|
this._initData = { ...data };
|
|
this.gameDef = data.game;
|
|
|
|
this.word = '';
|
|
this.hint = '';
|
|
this.category = '';
|
|
this.difficulty = '';
|
|
this.guessed = new Set();
|
|
this.gameEnded = false;
|
|
|
|
this.gallowsGfx = null;
|
|
this.blanksObjs = [];
|
|
this.skullObjs = [];
|
|
this.wrongText = null;
|
|
this.keyButtons = {};
|
|
|
|
this.startObjs = [];
|
|
this.gameObjs = [];
|
|
}
|
|
|
|
async create() {
|
|
const music = this.cache.json.get('music');
|
|
if (music?.tracks) new MusicPlayer(this, music.tracks);
|
|
|
|
this.add.rectangle(CX, CY, GAME_WIDTH, GAME_HEIGHT, COLORS.bg).setDepth(DEPTH.bg);
|
|
|
|
await this.showStartPanel();
|
|
}
|
|
|
|
// ── Start panel ──────────────────────────────────────────────────────────────
|
|
|
|
async showStartPanel() {
|
|
const cx = CX, cy = CY;
|
|
|
|
const sheet = this.add.graphics().setDepth(DEPTH.paper);
|
|
sheet.postFX.addShadow(0, 6, 0.02, 1.2, 0x000000, 10, 0.6);
|
|
sheet.fillStyle(PAPER, 1);
|
|
sheet.fillRoundedRect(cx - 520, cy - 290, 1040, 580, 18);
|
|
sheet.lineStyle(3, PAPER_EDGE, 1);
|
|
sheet.strokeRoundedRect(cx - 520, cy - 290, 1040, 580, 18);
|
|
this.startObjs.push(sheet);
|
|
|
|
this.startObjs.push(
|
|
this.add.text(cx, cy - 210, 'Hangman', {
|
|
fontFamily: 'YummyCupcakes', fontSize: '100px', color: TITLE_INK,
|
|
}).setOrigin(0.5).setDepth(DEPTH.ui),
|
|
);
|
|
|
|
this.startObjs.push(
|
|
this.add.text(cx, cy - 110, 'Choose difficulty', {
|
|
fontFamily: 'YummyCupcakes', fontSize: '46px', color: INK,
|
|
}).setOrigin(0.5).setDepth(DEPTH.ui),
|
|
);
|
|
|
|
const diffs = [['Easy', 'easy'], ['Medium', 'medium'], ['Hard', 'hard']];
|
|
diffs.forEach(([label, id], i) => {
|
|
const b = new Button(this, cx - 290 + i * 290, cy + 10, label,
|
|
() => this.startGame(id), { width: 250, height: 68, fontSize: 30 });
|
|
b.setDepth(DEPTH.ui);
|
|
this.startObjs.push(b);
|
|
});
|
|
|
|
const leave = new Button(this, cx, cy + 130, 'Leave',
|
|
() => this.scene.start('GameMenu'),
|
|
{ variant: 'ghost', width: 200, height: 50, fontSize: 22 });
|
|
leave.setDepth(DEPTH.ui);
|
|
this.startObjs.push(leave);
|
|
}
|
|
|
|
destroyStart() {
|
|
this.startObjs.forEach(o => o.destroy());
|
|
this.startObjs = [];
|
|
}
|
|
|
|
// ── Start game ───────────────────────────────────────────────────────────────
|
|
|
|
async startGame(difficulty) {
|
|
this.destroyStart();
|
|
playSound(this, SFX.PIECE_CLICK);
|
|
|
|
try {
|
|
const data = await api.get(`/words/hangman/start?difficulty=${difficulty}`);
|
|
this.word = data.word;
|
|
this.hint = data.hint;
|
|
this.category = data.category;
|
|
this.difficulty = difficulty;
|
|
} catch (err) {
|
|
console.error('[hangman] failed to fetch word:', err);
|
|
this.showStartPanel();
|
|
return;
|
|
}
|
|
|
|
this.guessed = new Set();
|
|
this.gameEnded = false;
|
|
|
|
this.buildPaper();
|
|
this.buildGallowsCard();
|
|
this.buildInfoCard();
|
|
this.buildKeyboard();
|
|
this.updateGallows();
|
|
this.updateBlanks();
|
|
this.updateSkulls();
|
|
}
|
|
|
|
// ── Paper + spiral ───────────────────────────────────────────────────────────
|
|
|
|
buildPaper() {
|
|
// Paper sheet
|
|
const g = this.add.graphics().setDepth(DEPTH.paper);
|
|
g.postFX.addShadow(0, 8, 0.02, 1.2, 0x000000, 14, 0.55);
|
|
g.fillStyle(PAPER, 1);
|
|
g.fillRoundedRect(PX, PY, PW, PH, 16);
|
|
g.lineStyle(2, PAPER_EDGE, 1);
|
|
g.strokeRoundedRect(PX, PY, PW, PH, 16);
|
|
this.gameObjs.push(g);
|
|
|
|
// Spiral binding — top and bottom rows
|
|
const spiralG = this.add.graphics().setDepth(DEPTH.paper + 1);
|
|
const step = PW / (SPIRAL_CNT + 1);
|
|
for (let i = 1; i <= SPIRAL_CNT; i++) {
|
|
const sx = PX + step * i;
|
|
spiralG.fillStyle(SPIRAL_CLR, 0.75);
|
|
spiralG.fillCircle(sx, PY + 10, SPIRAL_R);
|
|
spiralG.fillStyle(PAPER, 1);
|
|
spiralG.fillCircle(sx, PY + 10, SPIRAL_R - 6);
|
|
spiralG.fillStyle(SPIRAL_CLR, 0.75);
|
|
spiralG.fillCircle(sx, PY + PH - 10, SPIRAL_R);
|
|
spiralG.fillStyle(PAPER, 1);
|
|
spiralG.fillCircle(sx, PY + PH - 10, SPIRAL_R - 6);
|
|
}
|
|
this.gameObjs.push(spiralG);
|
|
|
|
// Title
|
|
const title = this.add.text(CX, PY + 110, 'Hangman', {
|
|
fontFamily: 'YummyCupcakes', fontSize: '92px', color: TITLE_INK,
|
|
}).setOrigin(0.5).setDepth(DEPTH.ui);
|
|
this.gameObjs.push(title);
|
|
|
|
// Leave button
|
|
const leave = new Button(this, PX + PW - 120, PY + 48, 'Leave',
|
|
() => this.scene.start('GameMenu'),
|
|
{ variant: 'ghost', width: 180, height: 46, fontSize: 20 });
|
|
leave.setDepth(DEPTH.ui);
|
|
this.gameObjs.push(leave);
|
|
}
|
|
|
|
// ── Gallows card ─────────────────────────────────────────────────────────────
|
|
|
|
buildGallowsCard() {
|
|
const g = this.add.graphics().setDepth(DEPTH.card);
|
|
this.drawSketchCard(g, GC_X, GC_Y, GC_W, GC_H);
|
|
this.gameObjs.push(g);
|
|
|
|
this.gallowsGfx = this.add.graphics().setDepth(DEPTH.gallows);
|
|
this.gameObjs.push(this.gallowsGfx);
|
|
}
|
|
|
|
updateGallows() {
|
|
const wrong = countWrong(this.word, this.guessed);
|
|
const gfx = this.gallowsGfx;
|
|
if (!gfx) return;
|
|
gfx.clear();
|
|
|
|
const lw = 5;
|
|
gfx.lineStyle(lw, INK_N, 1);
|
|
|
|
// Stage 0 — always: base, pole, beam, rope
|
|
const bx = GC_X + 80, by = GC_Y + GC_H - 60; // base left
|
|
const bx2= bx + GW_W; // base right
|
|
const px = bx + 50, py = by; // pole base
|
|
const ptop = GC_Y + 90; // pole top
|
|
const bm_r = ptop + 36; // beam right end (x)
|
|
const bm_rx= px + 130;
|
|
const ropeX = bm_rx, ropeY1 = bm_r, ropeY2 = bm_r + 50;
|
|
|
|
// base
|
|
gfx.lineBetween(bx, by, bx2, by);
|
|
// pole
|
|
gfx.lineBetween(px, by, px, ptop);
|
|
// beam
|
|
gfx.lineBetween(px, ptop, bm_rx, ptop);
|
|
// rope
|
|
gfx.lineBetween(ropeX, ptop + 4, ropeX, ropeY2);
|
|
|
|
// head center
|
|
const headCY = ropeY2 + 30;
|
|
const headR = 28;
|
|
// body
|
|
const bodyTop = headCY + headR;
|
|
const bodyBot = bodyTop + 90;
|
|
// arm y
|
|
const armY = bodyTop + 28;
|
|
// leg endpoints
|
|
const legBotL = { x: ropeX - 44, y: bodyBot + 70 };
|
|
const legBotR = { x: ropeX + 44, y: bodyBot + 70 };
|
|
|
|
if (wrong >= 1) {
|
|
// Head
|
|
gfx.lineStyle(lw, INK_N, 1);
|
|
gfx.strokeCircle(ropeX, headCY, headR);
|
|
}
|
|
if (wrong >= 2) {
|
|
// Body
|
|
gfx.lineBetween(ropeX, bodyTop, ropeX, bodyBot);
|
|
}
|
|
if (wrong >= 3) {
|
|
// Left arm
|
|
gfx.lineBetween(ropeX, armY, ropeX - 52, armY + 40);
|
|
}
|
|
if (wrong >= 4) {
|
|
// Right arm
|
|
gfx.lineBetween(ropeX, armY, ropeX + 52, armY + 40);
|
|
}
|
|
if (wrong >= 5) {
|
|
// Left leg
|
|
gfx.lineBetween(ropeX, bodyBot, legBotL.x, legBotL.y);
|
|
}
|
|
if (wrong >= 6) {
|
|
// Right leg
|
|
gfx.lineBetween(ropeX, bodyBot, legBotR.x, legBotR.y);
|
|
}
|
|
if (wrong >= 7) {
|
|
// Eyes X
|
|
gfx.lineStyle(3, INK_N, 1);
|
|
const ex = 10;
|
|
gfx.lineBetween(ropeX - ex, headCY - ex, ropeX - ex + 8, headCY - ex + 8);
|
|
gfx.lineBetween(ropeX - ex + 8, headCY - ex, ropeX - ex, headCY - ex + 8);
|
|
gfx.lineBetween(ropeX + ex - 8, headCY - ex, ropeX + ex, headCY - ex + 8);
|
|
gfx.lineBetween(ropeX + ex, headCY - ex, ropeX + ex - 8, headCY - ex + 8);
|
|
}
|
|
}
|
|
|
|
// ── Info card ────────────────────────────────────────────────────────────────
|
|
|
|
buildInfoCard() {
|
|
const g = this.add.graphics().setDepth(DEPTH.card);
|
|
this.drawSketchCard(g, IC_X, IC_Y, IC_W, IC_H);
|
|
this.gameObjs.push(g);
|
|
|
|
// Category label
|
|
const catLabel = this.add.text(IC_X + IC_W / 2, IC_Y + 46,
|
|
`Category: ${this.category}`, {
|
|
fontFamily: 'YummyCupcakes', fontSize: '36px', color: FADED,
|
|
}).setOrigin(0.5).setDepth(DEPTH.ui);
|
|
this.gameObjs.push(catLabel);
|
|
|
|
// Hint label — easy only
|
|
if (this.difficulty === 'easy') {
|
|
const hintLabel = this.add.text(IC_X + IC_W / 2, IC_Y + 96,
|
|
this.hint, {
|
|
fontFamily: 'YummyCupcakes', fontSize: '30px', color: INK,
|
|
wordWrap: { width: IC_W - 60 }, align: 'center',
|
|
}).setOrigin(0.5, 0).setDepth(DEPTH.ui);
|
|
this.gameObjs.push(hintLabel);
|
|
}
|
|
|
|
// Word blanks row — built into this.blanksObjs
|
|
this.buildBlanks();
|
|
|
|
// Skull tally area — rebuilt on each wrong guess
|
|
this.buildSkullArea();
|
|
|
|
// Wrong letters text
|
|
this.wrongText = this.add.text(IC_X + IC_W / 2, IC_Y + IC_H - 48,
|
|
'Wrong: —', {
|
|
fontFamily: 'YummyCupcakes', fontSize: '32px', color: WRONG_RED,
|
|
}).setOrigin(0.5).setDepth(DEPTH.ui);
|
|
this.gameObjs.push(this.wrongText);
|
|
}
|
|
|
|
buildBlanks() {
|
|
// Destroy old
|
|
this.blanksObjs.forEach(o => o.destroy());
|
|
this.blanksObjs = [];
|
|
|
|
const letters = maskWord(this.word, this.guessed);
|
|
const letterW = Math.min(62, Math.floor((IC_W - 60) / this.word.length));
|
|
const totalW = this.word.length * letterW;
|
|
const startX = IC_X + (IC_W - totalW) / 2 + letterW / 2;
|
|
const y = IC_Y + IC_H / 2 - 10;
|
|
|
|
letters.forEach((ch, i) => {
|
|
const x = startX + i * letterW;
|
|
|
|
// Underline
|
|
const line = this.add.graphics().setDepth(DEPTH.ui);
|
|
line.lineStyle(3, INK_N, 1);
|
|
line.lineBetween(x - letterW / 2 + 6, y + 26, x + letterW / 2 - 6, y + 26);
|
|
this.blanksObjs.push(line);
|
|
|
|
// Letter (or blank space)
|
|
const t = this.add.text(x, y, ch === '_' ? '' : ch, {
|
|
fontFamily: 'YummyCupcakes', fontSize: '48px', color: INK,
|
|
}).setOrigin(0.5).setDepth(DEPTH.ui);
|
|
this.blanksObjs.push(t);
|
|
});
|
|
}
|
|
|
|
buildSkullArea() {
|
|
this.skullObjs.forEach(o => o.destroy());
|
|
this.skullObjs = [];
|
|
|
|
const skullG = this.add.graphics().setDepth(DEPTH.ui);
|
|
const y = IC_Y + IC_H - 110;
|
|
const startX = IC_X + (IC_W - MAX_WRONG * 44) / 2;
|
|
|
|
for (let i = 0; i < MAX_WRONG; i++) {
|
|
const x = startX + i * 44 + 22;
|
|
this.drawSkullIcon(skullG, x, y, false); // all faded initially
|
|
}
|
|
|
|
this.skullObjs.push(skullG);
|
|
}
|
|
|
|
updateSkulls() {
|
|
this.skullObjs.forEach(o => o.destroy());
|
|
this.skullObjs = [];
|
|
|
|
const wrong = countWrong(this.word, this.guessed);
|
|
const skullG = this.add.graphics().setDepth(DEPTH.ui);
|
|
const y = IC_Y + IC_H - 110;
|
|
const startX = IC_X + (IC_W - MAX_WRONG * 44) / 2;
|
|
|
|
for (let i = 0; i < MAX_WRONG; i++) {
|
|
const x = startX + i * 44 + 22;
|
|
const lit = i < wrong;
|
|
this.drawSkullIcon(skullG, x, y, lit);
|
|
}
|
|
|
|
this.skullObjs.push(skullG);
|
|
}
|
|
|
|
drawSkullIcon(gfx, x, y, lit) {
|
|
const color = lit ? SKULL_FULL : SKULL_FADE;
|
|
gfx.fillStyle(color, 1);
|
|
// Dome
|
|
gfx.fillCircle(x, y - 4, 11);
|
|
// Jaw rect
|
|
gfx.fillRect(x - 8, y + 4, 16, 8);
|
|
// Eye sockets (cutouts — draw with paper color)
|
|
gfx.fillStyle(PAPER, 1);
|
|
gfx.fillCircle(x - 4, y - 4, 3);
|
|
gfx.fillCircle(x + 4, y - 4, 3);
|
|
}
|
|
|
|
updateBlanks() {
|
|
this.buildBlanks();
|
|
}
|
|
|
|
updateWrongText() {
|
|
if (!this.wrongText) return;
|
|
const wl = wrongLetters(this.word, this.guessed);
|
|
this.wrongText.setText(wl.length ? `Wrong: ${wl.join(' ')}` : 'Wrong: —');
|
|
}
|
|
|
|
// ── Keyboard ─────────────────────────────────────────────────────────────────
|
|
|
|
buildKeyboard() {
|
|
const g = this.add.graphics().setDepth(DEPTH.card);
|
|
this.drawSketchCard(g, KB_X, KB_Y, KB_W, KB_H);
|
|
this.gameObjs.push(g);
|
|
|
|
const btnW = 110, btnH = 54;
|
|
const gapX = 118, gapY = 56;
|
|
const row0Y = KB_Y + 40;
|
|
|
|
QWERTY.forEach((row, ri) => {
|
|
const rowWidth = row.length * gapX - (gapX - btnW);
|
|
const rowStartX = KB_X + (KB_W - rowWidth) / 2 + btnW / 2;
|
|
|
|
row.forEach((letter, ci) => {
|
|
const bx = rowStartX + ci * gapX;
|
|
const by = row0Y + ri * gapY;
|
|
|
|
const bg = this.add.graphics().setDepth(DEPTH.ui);
|
|
bg.lineStyle(2, CARD_EDGE, 1);
|
|
bg.strokeRoundedRect(bx - btnW / 2, by - btnH / 2, btnW, btnH, 8);
|
|
|
|
const txt = this.add.text(bx, by, letter, {
|
|
fontFamily: 'YummyCupcakes', fontSize: '36px', color: INK,
|
|
}).setOrigin(0.5).setDepth(DEPTH.ui + 1);
|
|
|
|
const hit = this.add.rectangle(bx, by, btnW, btnH, 0xffffff, 0.001)
|
|
.setDepth(DEPTH.ui + 2).setInteractive({ useHandCursor: true });
|
|
|
|
hit.on('pointerdown', () => this.onLetterClick(letter, bg, txt, hit));
|
|
hit.on('pointerover', () => {
|
|
if (!this.guessed.has(letter)) {
|
|
bg.clear();
|
|
bg.fillStyle(INK_N, 0.08);
|
|
bg.fillRoundedRect(bx - btnW / 2, by - btnH / 2, btnW, btnH, 8);
|
|
bg.lineStyle(2, INK_N, 0.6);
|
|
bg.strokeRoundedRect(bx - btnW / 2, by - btnH / 2, btnW, btnH, 8);
|
|
}
|
|
});
|
|
hit.on('pointerout', () => {
|
|
if (!this.guessed.has(letter)) {
|
|
bg.clear();
|
|
bg.lineStyle(2, CARD_EDGE, 1);
|
|
bg.strokeRoundedRect(bx - btnW / 2, by - btnH / 2, btnW, btnH, 8);
|
|
}
|
|
});
|
|
|
|
this.keyButtons[letter] = { bg, txt, hit, bx, by, btnW, btnH };
|
|
this.gameObjs.push(bg, txt, hit);
|
|
});
|
|
});
|
|
}
|
|
|
|
onLetterClick(letter, bg, txt, hit) {
|
|
if (this.gameEnded || this.guessed.has(letter)) return;
|
|
|
|
this.guessed.add(letter);
|
|
playSound(this, SFX.PIECE_CLICK);
|
|
|
|
const correct = this.word.includes(letter);
|
|
const { bx, by, btnW, btnH } = this.keyButtons[letter];
|
|
|
|
// Disable and style the key
|
|
hit.disableInteractive();
|
|
bg.clear();
|
|
if (correct) {
|
|
bg.fillStyle(0x3c8a4e, 0.25);
|
|
bg.fillRoundedRect(bx - btnW / 2, by - btnH / 2, btnW, btnH, 8);
|
|
bg.lineStyle(2, 0x3c8a4e, 0.8);
|
|
bg.strokeRoundedRect(bx - btnW / 2, by - btnH / 2, btnW, btnH, 8);
|
|
txt.setColor('#2d6e3e');
|
|
} else {
|
|
bg.fillStyle(0xb03a2e, 0.15);
|
|
bg.fillRoundedRect(bx - btnW / 2, by - btnH / 2, btnW, btnH, 8);
|
|
bg.lineStyle(2, 0xb03a2e, 0.7);
|
|
bg.strokeRoundedRect(bx - btnW / 2, by - btnH / 2, btnW, btnH, 8);
|
|
txt.setColor('#9a3228');
|
|
}
|
|
|
|
this.updateBlanks();
|
|
this.updateGallows();
|
|
this.updateSkulls();
|
|
this.updateWrongText();
|
|
|
|
const wrong = countWrong(this.word, this.guessed);
|
|
if (isWordSolved(this.word, this.guessed)) {
|
|
this.time.delayedCall(400, () => this.handleWin());
|
|
} else if (wrong >= MAX_WRONG) {
|
|
this.time.delayedCall(400, () => this.handleLoss());
|
|
}
|
|
}
|
|
|
|
// ── Win / Loss ───────────────────────────────────────────────────────────────
|
|
|
|
handleWin() {
|
|
this.gameEnded = true;
|
|
this.recordResult('win');
|
|
this.showEndPanel(true);
|
|
}
|
|
|
|
handleLoss() {
|
|
this.gameEnded = true;
|
|
// Reveal the word
|
|
this.word.split('').forEach(ch => this.guessed.add(ch));
|
|
this.updateBlanks();
|
|
this.recordResult('loss');
|
|
this.showEndPanel(false);
|
|
}
|
|
|
|
showEndPanel(won) {
|
|
const cx = CX, cy = CY;
|
|
|
|
this.add.rectangle(cx, cy, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.45)
|
|
.setDepth(DEPTH.overlay);
|
|
|
|
const panel = this.add.graphics().setDepth(DEPTH.overlay + 1);
|
|
panel.postFX.addShadow(0, 6, 0.02, 1.2, 0x000000, 10, 0.55);
|
|
panel.fillStyle(PAPER, 1);
|
|
panel.fillRoundedRect(cx - 420, cy - 220, 840, 440, 18);
|
|
panel.lineStyle(3, PAPER_EDGE, 1);
|
|
panel.strokeRoundedRect(cx - 420, cy - 220, 840, 440, 18);
|
|
|
|
this.add.text(cx, cy - 130, won ? 'You got it!' : 'Game over!', {
|
|
fontFamily: 'YummyCupcakes', fontSize: '88px',
|
|
color: won ? '#3c8a4e' : RED_INK,
|
|
}).setOrigin(0.5).setDepth(DEPTH.overlay + 2);
|
|
|
|
if (!won) {
|
|
this.add.text(cx, cy - 28, `The word was: ${this.word}`, {
|
|
fontFamily: 'YummyCupcakes', fontSize: '44px', color: INK,
|
|
}).setOrigin(0.5).setDepth(DEPTH.overlay + 2);
|
|
} else {
|
|
this.add.text(cx, cy - 28, `"${this.hint}"`, {
|
|
fontFamily: 'YummyCupcakes', fontSize: '36px', color: FADED,
|
|
}).setOrigin(0.5).setDepth(DEPTH.overlay + 2);
|
|
}
|
|
|
|
new Button(this, cx - 160, cy + 130, 'Play again',
|
|
() => this.scene.restart(this._initData),
|
|
{ width: 260, height: 58, fontSize: 26 }).setDepth(DEPTH.overlay + 2);
|
|
|
|
new Button(this, cx + 160, cy + 130, 'Leave',
|
|
() => this.scene.start('GameMenu'),
|
|
{ variant: 'ghost', width: 260, height: 58, fontSize: 26 }).setDepth(DEPTH.overlay + 2);
|
|
}
|
|
|
|
async recordResult(result) {
|
|
try {
|
|
const wrong = countWrong(this.word, this.guessed);
|
|
const score = result === 'win' ? Math.max(0, (MAX_WRONG - wrong) * 15) : 0;
|
|
await api.post('/history/single-player', {
|
|
slug: 'hangman', score, opponentScores: [], result,
|
|
});
|
|
} catch { /* best effort */ }
|
|
}
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
|
|
drawSketchCard(gfx, x, y, w, h) {
|
|
gfx.fillStyle(PAPER, 1);
|
|
gfx.fillRoundedRect(x, y, w, h, 12);
|
|
gfx.lineStyle(2, CARD_EDGE, 1);
|
|
gfx.strokeRoundedRect(x, y, w, h, 12);
|
|
// Subtle inner shadow line for sketch feel
|
|
gfx.lineStyle(1, CARD_EDGE, 0.4);
|
|
gfx.strokeRoundedRect(x + 3, y + 3, w - 6, h - 6, 10);
|
|
}
|
|
}
|