feat(farkel): overhaul turn scoring with combined cycle logic and explain panel
- Replace turn.kept running sum with priorKept + selectionScore model so the banked total always reflects all set-aside dice in the current cycle, enabling higher N-of-a-kind bonuses when extending sets. - Update bestScoring and hasScoring to consider setAsideDice; use brute-force subset search (feasible for ≤6 dice) instead of greedy strategy for correctness. - Add breakdownScoring helper to decompose a combined dice set into labeled groups. - Add updateExplainPanel showing "Prior rolls", scoring groups, and total with "(need 500)" note when off-board requirement isn't met; format scores >999 as K,DDD. - Refactor selectionScore/selectionValid to score the combined set (setAsideDice + selection). - Use bestScoring(rolled, setAsideDice) in onScoreAll and AI for optimal picks. - Fix scratch paper preview to use priorKept + selectionScore consistently. - Add hot dice handling: carry cycle score into priorKept and reset setAsideDice. - Slots: guard card artwork loading to avoid errors when texture is missing; adjust bulb positions accordingly.
This commit is contained in:
parent
d82a8d676f
commit
519dd617d2
Binary file not shown.
|
After Width: | Height: | Size: 235 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 234 KiB |
Binary file not shown.
Binary file not shown.
|
|
@ -199,7 +199,7 @@
|
|||
{
|
||||
"id": "pharaohs-fortune-card",
|
||||
"key": "slots-pharaohs-fortune-card",
|
||||
"path": null
|
||||
"path": "/assets/images/slots/slots-pharaohs-fortune-card.png"
|
||||
},
|
||||
{
|
||||
"id": "abyssal-treasures-bubble",
|
||||
|
|
@ -254,7 +254,7 @@
|
|||
{
|
||||
"id": "abyssal-treasures-card",
|
||||
"key": "slots-abyssal-treasures-card",
|
||||
"path": null
|
||||
"path": "/assets/images/slots/slots-abyssal-treasures-card.png"
|
||||
},
|
||||
{
|
||||
"id": "dragons-hoard-king",
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ import { api } from '../../services/api.js';
|
|||
import { createOpponentPortrait, createPlayerPortrait } from '../../ui/Portrait.js';
|
||||
import { playSound, SFX } from '../../ui/Sounds.js';
|
||||
import { MusicPlayer } from '../../ui/MusicPlayer.js';
|
||||
import { DICE, PIP_POS, PLAYER_COLOR_HEX, SCORING_REFERENCE } from './FarkelData.js';
|
||||
import { DICE, PIP_POS, PLAYER_COLOR_HEX, SCORING_REFERENCE, ON_BOARD_MIN } from './FarkelData.js';
|
||||
import {
|
||||
createInitialState, rollDice, applySetAside, bank, farkleTurn,
|
||||
scoreSelection, bestScoring, isGameOver, getWinners,
|
||||
scoreSelection, bestScoring, breakdownScoring, isGameOver, getWinners,
|
||||
} from './FarkelLogic.js';
|
||||
import { decideReroll, nextThinkDelay } from './FarkelAI.js';
|
||||
|
||||
|
|
@ -45,6 +45,11 @@ const DEPTH = {
|
|||
die: 10, dieSel: 11, ui: 20, toast: 60, modal: 70,
|
||||
};
|
||||
|
||||
// ─── Display helper ────────────────────────────────────────────────────────────
|
||||
function fmtPts(n) {
|
||||
return n >= 1000 ? `${Math.floor(n / 1000)},${String(n % 1000).padStart(3, '0')}` : String(n);
|
||||
}
|
||||
|
||||
export default class FarkelGame extends Phaser.Scene {
|
||||
constructor() { super('FarkelGame'); }
|
||||
|
||||
|
|
@ -89,6 +94,7 @@ export default class FarkelGame extends Phaser.Scene {
|
|||
this.buildTray();
|
||||
this.buildDice();
|
||||
this.buildShelf();
|
||||
this.buildExplainPanel();
|
||||
this.buildScoringPanel();
|
||||
this.buildScratchPaper();
|
||||
this.buildButtons();
|
||||
|
|
@ -179,6 +185,38 @@ export default class FarkelGame extends Phaser.Scene {
|
|||
}).setOrigin(0, 0.5).setDepth(DEPTH.die);
|
||||
}
|
||||
|
||||
buildExplainPanel() {
|
||||
const shelfLeft = TRAY_CX - TRAY_W / 2 + 4;
|
||||
const shelfW = DICE * (SDIE + SDIE_GAP) + 12;
|
||||
const shelfH = SDIE + 70;
|
||||
const shelfY = SHELF_Y - SDIE / 2 - 46;
|
||||
|
||||
const ex = shelfLeft + shelfW + 10;
|
||||
const ew = TRAY_CX + TRAY_W / 2 - ex - 4;
|
||||
const ey = shelfY;
|
||||
const eh = shelfH;
|
||||
|
||||
const bg = this.add.graphics().setDepth(DEPTH.die - 1);
|
||||
bg.fillStyle(0x000000, 0.55);
|
||||
bg.fillRoundedRect(ex, ey, ew, eh, 8);
|
||||
bg.lineStyle(2, COLORS.accent, 0.6);
|
||||
bg.strokeRoundedRect(ex, ey, ew, eh, 8);
|
||||
|
||||
this.add.text(ex + 14, ey + 16, 'If you bank:', {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '14px', color: COLORS.accentHex,
|
||||
}).setOrigin(0, 0.5).setDepth(DEPTH.die);
|
||||
|
||||
this._explainBounds = { ex, ew, ey, eh };
|
||||
this._explainLines = [];
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this._explainLines.push(
|
||||
this.add.text(ex + 14, ey + 34 + i * 20, '', {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '15px', color: COLORS.textHex,
|
||||
}).setOrigin(0, 0).setDepth(DEPTH.die),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
buildScoringPanel() {
|
||||
const rows = SCORING_REFERENCE.length;
|
||||
const rowH = 40;
|
||||
|
|
@ -282,6 +320,7 @@ export default class FarkelGame extends Phaser.Scene {
|
|||
this.updateStatus();
|
||||
this.updateTurnTotal();
|
||||
this.updateControls();
|
||||
this.updateExplainPanel();
|
||||
}
|
||||
|
||||
drawDie(g, x, y, size, face, { selected = false, locked = false } = {}) {
|
||||
|
|
@ -422,13 +461,11 @@ export default class FarkelGame extends Phaser.Scene {
|
|||
for (let seat = 0; seat < this.scratchRows.length; seat++) {
|
||||
const p = this.gs.players[seat];
|
||||
const row = this.scratchRows[seat];
|
||||
const hasPending = this.gs.turn.kept > 0 || (this.gs.phase === 'awaitPick' && this.selectionScore() > 0);
|
||||
const showTurn = (seat === cur && !isGameOver(this.gs) && hasPending);
|
||||
if (showTurn) {
|
||||
const total = (this.gs.phase === 'awaitPick')
|
||||
? this.gs.turn.kept + this.selectionScore()
|
||||
: this.gs.turn.kept;
|
||||
row.score.setText(`${p.score} +${total}`);
|
||||
if (seat === cur && !isGameOver(this.gs)) {
|
||||
// selectionScore() returns the combined cycle score (setAsideDice + selection).
|
||||
// Adding priorKept gives the full prospective bank total in every phase.
|
||||
const preview = (this.gs.turn.priorKept ?? 0) + this.selectionScore();
|
||||
row.score.setText(preview > 0 ? `${p.score} +${preview}` : String(p.score));
|
||||
} else {
|
||||
row.score.setText(String(p.score));
|
||||
}
|
||||
|
|
@ -477,10 +514,56 @@ export default class FarkelGame extends Phaser.Scene {
|
|||
|
||||
updateTurnTotal() {
|
||||
const gs = this.gs;
|
||||
const kept = gs.turn.kept;
|
||||
const sel = (gs.phase === 'awaitPick') ? this.selectionScore() : 0;
|
||||
const total = kept + sel;
|
||||
this.turnTotalText.setText(total > 0 ? `Turn total: ${total}` : '');
|
||||
const preview = (gs.turn.priorKept ?? 0) + this.selectionScore();
|
||||
this.turnTotalText.setText(preview > 0 ? `Turn total: ${preview}` : '');
|
||||
}
|
||||
|
||||
updateExplainPanel() {
|
||||
const gs = this.gs;
|
||||
const p = gs.players[gs.current];
|
||||
const priorKept = gs.turn.priorKept ?? 0;
|
||||
|
||||
// Combined set: already set-aside dice + pending selection (if any)
|
||||
const combined = [
|
||||
...gs.turn.setAsideDice,
|
||||
...(gs.phase === 'awaitPick' ? this.selectionValues() : []),
|
||||
];
|
||||
const groups = breakdownScoring(combined);
|
||||
const cycleScore = groups.reduce((s, g) => s + g.points, 0);
|
||||
const totalKept = priorKept + cycleScore;
|
||||
const bankOk = p.onBoard || totalKept >= ON_BOARD_MIN;
|
||||
|
||||
if (groups.length === 0 && priorKept === 0) {
|
||||
this._explainLines.forEach((t) => t.setText(''));
|
||||
return;
|
||||
}
|
||||
|
||||
// Gold = preview (selection pending), white = committed
|
||||
const inPreview = gs.phase === 'awaitPick' && this.selected.size > 0 && this.selectionScore() > 0;
|
||||
const detailColor = inPreview ? COLORS.goldHex : COLORS.textHex;
|
||||
|
||||
let li = 0;
|
||||
const maxDetail = this._explainLines.length - 1; // reserve last line for total
|
||||
|
||||
if (priorKept > 0 && li < maxDetail) {
|
||||
this._explainLines[li++].setText(`Prior rolls = ${fmtPts(priorKept)}`).setColor(COLORS.textHex);
|
||||
}
|
||||
|
||||
for (const g of groups) {
|
||||
if (li >= maxDetail) break;
|
||||
this._explainLines[li++].setText(`${g.label} = ${fmtPts(g.points)}`).setColor(detailColor);
|
||||
}
|
||||
|
||||
// Show total when there are multiple content lines, or when "need 500" applies
|
||||
const contentLines = (priorKept > 0 ? 1 : 0) + groups.length;
|
||||
if (totalKept > 0 && (contentLines > 1 || !bankOk) && li < this._explainLines.length) {
|
||||
const note = bankOk ? '' : ' (need 500)';
|
||||
this._explainLines[li++]
|
||||
.setText(`Total = ${fmtPts(totalKept)}${note}`)
|
||||
.setColor(bankOk ? COLORS.accentHex : COLORS.dangerHex);
|
||||
}
|
||||
|
||||
for (let i = li; i < this._explainLines.length; i++) this._explainLines[i].setText('');
|
||||
}
|
||||
|
||||
updateControls() {
|
||||
|
|
@ -500,16 +583,21 @@ export default class FarkelGame extends Phaser.Scene {
|
|||
|
||||
// ── selection helpers ─────────────────────────────────────────────────────────
|
||||
selectionValues() { return [...this.selected].map((i) => this.gs.turn.rolled[i]); }
|
||||
|
||||
// Returns the combined score of all set-aside dice this cycle plus the current
|
||||
// selection. This IS the value that would become turn.kept if committed.
|
||||
selectionScore() {
|
||||
const combined = [...this.gs.turn.setAsideDice, ...this.selectionValues()];
|
||||
if (combined.length === 0) return 0;
|
||||
const r = scoreSelection(combined);
|
||||
return r.valid ? r.points : 0;
|
||||
}
|
||||
|
||||
// A selection is valid when the combined set (set-aside + selection) all score.
|
||||
selectionValid() {
|
||||
const values = this.selectionValues();
|
||||
if (values.length === 0) return false;
|
||||
return scoreSelection(values).valid;
|
||||
}
|
||||
selectionScore() {
|
||||
const values = this.selectionValues();
|
||||
if (values.length === 0) return 0;
|
||||
const r = scoreSelection(values);
|
||||
return r.valid ? r.points : 0;
|
||||
return scoreSelection([...this.gs.turn.setAsideDice, ...values]).valid;
|
||||
}
|
||||
|
||||
// ── input ──────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -523,29 +611,8 @@ export default class FarkelGame extends Phaser.Scene {
|
|||
|
||||
onScoreAll() {
|
||||
if (this.busy || !this.isHumanTurn() || this.gs.phase !== 'awaitPick') return;
|
||||
const rolled = this.gs.turn.rolled;
|
||||
const kept = this.gs.turn.setAsideDice;
|
||||
|
||||
// Try all subsets of rolled dice, combine with kept, find best total score
|
||||
let bestScore = 0;
|
||||
let bestIndices = new Set();
|
||||
for (let mask = 0; mask < (1 << rolled.length); mask++) {
|
||||
const subset = [];
|
||||
for (let i = 0; i < rolled.length; i++) {
|
||||
if (mask & (1 << i)) subset.push(rolled[i]);
|
||||
}
|
||||
const combined = [...kept, ...subset];
|
||||
const result = scoreSelection(combined);
|
||||
if (result.valid && result.points > bestScore) {
|
||||
bestScore = result.points;
|
||||
bestIndices = new Set();
|
||||
for (let i = 0; i < rolled.length; i++) {
|
||||
if (mask & (1 << i)) bestIndices.add(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.selected = bestIndices;
|
||||
const { indices } = bestScoring(this.gs.turn.rolled, this.gs.turn.setAsideDice);
|
||||
this.selected = new Set(indices);
|
||||
playSound(this, SFX.PIECE_CLICK);
|
||||
this.render();
|
||||
}
|
||||
|
|
@ -627,7 +694,7 @@ export default class FarkelGame extends Phaser.Scene {
|
|||
this.portraitCtrls[seat]?.controller?.playEmotion?.('upset');
|
||||
break;
|
||||
}
|
||||
const best = bestScoring(this.gs.turn.rolled);
|
||||
const best = bestScoring(this.gs.turn.rolled, this.gs.turn.setAsideDice);
|
||||
this.selected = new Set(best.indices);
|
||||
this.render();
|
||||
await this.delay(480);
|
||||
|
|
|
|||
|
|
@ -75,42 +75,38 @@ export function scoreSelection(dice) {
|
|||
return { valid: true, points: best };
|
||||
}
|
||||
|
||||
// Greedy "take everything that scores" — returns the indices into `dice` to set
|
||||
// aside and the points earned. Used by the AI and the human "Score all" helper.
|
||||
export function bestScoring(dice) {
|
||||
const counts = countsOf(dice);
|
||||
const total = dice.length;
|
||||
const candidates = [];
|
||||
|
||||
// Greedy: all 1s/5s plus every N-of-a-kind.
|
||||
{
|
||||
const take = [0, 0, 0, 0, 0, 0, 0];
|
||||
let pts = 0;
|
||||
for (let v = 1; v <= 6; v++) {
|
||||
const c = counts[v];
|
||||
if (c >= 3) { take[v] = c; pts += bestGroupValue(v, c); }
|
||||
else if (v === 1) { take[v] = c; pts += c * 100; }
|
||||
else if (v === 5) { take[v] = c; pts += c * 50; }
|
||||
// Find the best subset of `dice` (rolled) to set aside given already set-aside
|
||||
// dice from this turn. Brute-forces all 2^n subsets — feasible for n ≤ 6 (63
|
||||
// iterations). Returns the indices of rolled dice and the resulting combined score.
|
||||
export function bestScoring(dice, setAsideDice = []) {
|
||||
const n = dice.length;
|
||||
let bestPts = 0;
|
||||
let bestIndices = [];
|
||||
for (let mask = 1; mask < (1 << n); mask++) {
|
||||
const subset = [];
|
||||
const indices = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
if (mask & (1 << i)) { subset.push(dice[i]); indices.push(i); }
|
||||
}
|
||||
const res = scoreSelection([...setAsideDice, ...subset]);
|
||||
if (res.valid && res.points > bestPts) {
|
||||
bestPts = res.points;
|
||||
bestIndices = indices;
|
||||
}
|
||||
candidates.push({ take, pts });
|
||||
}
|
||||
// Straight / three pairs use all six dice.
|
||||
if (total === 6 && [1, 2, 3, 4, 5, 6].every((v) => counts[v] === 1)) {
|
||||
candidates.push({ take: [0, 1, 1, 1, 1, 1, 1], pts: 1500 });
|
||||
}
|
||||
if (total === 6 && [1, 2, 3, 4, 5, 6].every((v) => counts[v] % 2 === 0)) {
|
||||
candidates.push({ take: counts.slice(), pts: 1500 });
|
||||
}
|
||||
|
||||
const best = candidates.reduce((a, b) => (b.pts > a.pts ? b : a));
|
||||
const need = best.take.slice();
|
||||
const indices = [];
|
||||
dice.forEach((v, i) => { if (need[v] > 0) { indices.push(i); need[v]--; } });
|
||||
return { indices, points: best.pts };
|
||||
return { indices: bestIndices, points: bestPts };
|
||||
}
|
||||
|
||||
export function hasScoring(dice) {
|
||||
return bestScoring(dice).points > 0;
|
||||
// A roll is NOT a farkle if any subset of the rolled dice, when combined with
|
||||
// already set-aside dice from this turn, produces a valid scoring set.
|
||||
export function hasScoring(dice, setAsideDice = []) {
|
||||
const n = dice.length;
|
||||
for (let mask = 1; mask < (1 << n); mask++) {
|
||||
const subset = [];
|
||||
for (let i = 0; i < n; i++) if (mask & (1 << i)) subset.push(dice[i]);
|
||||
if (scoreSelection([...setAsideDice, ...subset]).valid) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── state ────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -142,7 +138,7 @@ export function createInitialState({ playerCount = 4, names = [], skills = {}, s
|
|||
}
|
||||
|
||||
function resetTurn(state) {
|
||||
state.turn = { rolled: [], available: DICE, kept: 0, setAsideDice: [], hotDice: false };
|
||||
state.turn = { rolled: [], available: DICE, kept: 0, priorKept: 0, setAsideDice: [], hotDice: false };
|
||||
state.phase = 'awaitRoll';
|
||||
}
|
||||
|
||||
|
|
@ -154,7 +150,7 @@ export function cloneState(s) {
|
|||
}
|
||||
|
||||
// Roll the available dice into turn.rolled. Sets phase to awaitPick, or farkled
|
||||
// when the roll scores nothing.
|
||||
// when no subset of the rolled dice (combined with already set-aside dice) scores.
|
||||
export function rollDice(state) {
|
||||
const t = state.turn;
|
||||
const rng = state._rng;
|
||||
|
|
@ -162,30 +158,36 @@ export function rollDice(state) {
|
|||
for (let i = 0; i < t.available; i++) out.push(1 + Math.floor(rng() * 6));
|
||||
t.rolled = out;
|
||||
t.hotDice = false;
|
||||
state.phase = hasScoring(out) ? 'awaitPick' : 'farkled';
|
||||
state.phase = hasScoring(out, t.setAsideDice) ? 'awaitPick' : 'farkled';
|
||||
return state;
|
||||
}
|
||||
|
||||
// Set aside the dice at the given indices (into turn.rolled). Adds their points
|
||||
// to the turn total. Triggers hot dice when all six are used. Returns true on a
|
||||
// valid selection.
|
||||
// Set aside the dice at the given indices (into turn.rolled). The turn score is
|
||||
// always the combined value of ALL set-aside dice this cycle (not a running sum),
|
||||
// so extending the set can unlock higher N-of-a-kind bonuses. Triggers hot dice
|
||||
// when all available dice have been used. Returns true on a valid selection.
|
||||
export function applySetAside(state, indices) {
|
||||
const t = state.turn;
|
||||
const values = indices.map((i) => t.rolled[i]);
|
||||
const res = scoreSelection(values);
|
||||
const combined = [...t.setAsideDice, ...values];
|
||||
const res = scoreSelection(combined);
|
||||
if (!res.valid) return false;
|
||||
|
||||
t.kept += res.points;
|
||||
t.setAsideDice.push(...values);
|
||||
t.available -= values.length;
|
||||
t.rolled = [];
|
||||
|
||||
if (t.available === 0) {
|
||||
// Hot dice — every die scored, so roll all six again.
|
||||
// Hot dice — carry this cycle's score into priorKept, then reset for new cycle.
|
||||
t.priorKept += res.points;
|
||||
t.kept = t.priorKept;
|
||||
t.available = DICE;
|
||||
t.setAsideDice = [];
|
||||
t.hotDice = true;
|
||||
} else {
|
||||
t.kept = t.priorKept + res.points;
|
||||
}
|
||||
|
||||
state.phase = 'awaitDecision';
|
||||
return true;
|
||||
}
|
||||
|
|
@ -238,3 +240,29 @@ function computeWinners(state) {
|
|||
|
||||
export function isGameOver(state) { return state.phase === 'gameover'; }
|
||||
export function getWinners(state) { return state.winners; }
|
||||
|
||||
// ── display helper ───────────────────────────────────────────────────────────
|
||||
// Returns an array of { label, points } describing the scoring groups in `dice`.
|
||||
export function breakdownScoring(dice) {
|
||||
if (!dice || dice.length === 0) return [];
|
||||
const total = dice.length;
|
||||
const counts = countsOf(dice);
|
||||
if (total === 6) {
|
||||
if ([1, 2, 3, 4, 5, 6].every((v) => counts[v] === 1)) return [{ label: 'Straight 1–6', points: 1500 }];
|
||||
if ([1, 2, 3, 4, 5, 6].every((v) => counts[v] % 2 === 0)) return [{ label: 'Three Pairs', points: 1500 }];
|
||||
}
|
||||
const K = ['', '', '', 'Three', 'Four', 'Five', 'Six'];
|
||||
const groups = [];
|
||||
for (let v = 1; v <= 6; v++) {
|
||||
const n = counts[v];
|
||||
if (n === 0) continue;
|
||||
if (n >= 3) {
|
||||
groups.push({ label: `${K[n]} ${v}s`, points: bestGroupValue(v, n) });
|
||||
} else if (v === 1) {
|
||||
groups.push({ label: n === 1 ? 'Single 1' : 'Two 1s', points: n * 100 });
|
||||
} else if (v === 5) {
|
||||
groups.push({ label: n === 1 ? 'Single 5' : 'Two 5s', points: n * 50 });
|
||||
}
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -171,13 +171,15 @@ export default class SlotsGame extends Phaser.Scene {
|
|||
const art = this.add.image(0, -6, cardArtKey).setDisplaySize(w - 16, h - 90);
|
||||
card.add(art);
|
||||
}
|
||||
const name = this.add.text(0, -h / 2 + 52, m.name.toUpperCase(), {
|
||||
fontFamily: 'Righteous', fontSize: '30px', color: t.accentHex, align: 'center',
|
||||
}).setOrigin(0.5).setShadow(0, 0, t.accentHex, 16);
|
||||
const tag = this.add.text(0, -h / 2 + 86, m.tagline, {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '17px', color: COLORS.mutedHex,
|
||||
}).setOrigin(0.5);
|
||||
card.add([name, tag]);
|
||||
if (!this.textures.exists(cardArtKey)) {
|
||||
const name = this.add.text(0, -h / 2 + 52, m.name.toUpperCase(), {
|
||||
fontFamily: 'Righteous', fontSize: '30px', color: t.accentHex, align: 'center',
|
||||
}).setOrigin(0.5).setShadow(0, 0, t.accentHex, 16);
|
||||
const tag = this.add.text(0, -h / 2 + 86, m.tagline, {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '17px', color: COLORS.mutedHex,
|
||||
}).setOrigin(0.5);
|
||||
card.add([name, tag]);
|
||||
}
|
||||
|
||||
// Era badge.
|
||||
const era = this.add.text(w / 2 - 16, -h / 2 + 18, m.era.toUpperCase(), {
|
||||
|
|
@ -233,8 +235,9 @@ export default class SlotsGame extends Phaser.Scene {
|
|||
for (let b = 0; b < bulbs; b++) {
|
||||
const lx = -w / 2 + 40 + (b * (w - 80)) / (bulbs - 1);
|
||||
const on = Math.sin(bulbState.phase + b * 0.9) > 0;
|
||||
const ly = this.textures.exists(cardArtKey) ? 100 : (-h / 2 + 110);
|
||||
lights.fillStyle(on ? t.glow : 0x1a140c, on ? 0.95 : 0.8);
|
||||
lights.fillCircle(lx, -h / 2 + 110, 5);
|
||||
lights.fillCircle(lx, ly, 5);
|
||||
}
|
||||
};
|
||||
drawBulbs();
|
||||
|
|
|
|||
Loading…
Reference in New Issue