From 8cfe0b8a30d0de9689e857a7cbf0221193171039 Mon Sep 17 00:00:00 2001 From: AnRil Date: Mon, 1 Jun 2026 01:10:40 +0700 Subject: [PATCH] Selectable backgrounds with live preview in settings - 6 procedural, vertically-tiling backgrounds generated in BootScene: Grid, Hex Nodes, Starfield, Synthwave, Circuit, Void - config/backgrounds.js registry + utils/background.js helper; selection persisted via storage (KEYS.background) - Menu / Game / GameOver use the selected background texture - Settings overlay gains a BACKGROUND selector: in-panel live preview tile + < name > cycling, updates the menu background live and persists choice - Fix: stepper/arrow buttons now add both bg and label to the modal so the -, +, <, > glyphs render above the panel --- src/config/backgrounds.js | 19 ++++ src/scenes/BootScene.js | 171 +++++++++++++++++++++++++++++------- src/scenes/GameOverScene.js | 3 +- src/scenes/GameScene.js | 3 +- src/scenes/MenuScene.js | 58 +++++++++--- src/utils/background.js | 17 ++++ src/utils/storage.js | 1 + 7 files changed, 225 insertions(+), 47 deletions(-) create mode 100644 src/config/backgrounds.js create mode 100644 src/utils/background.js diff --git a/src/config/backgrounds.js b/src/config/backgrounds.js new file mode 100644 index 0000000..ec5e311 --- /dev/null +++ b/src/config/backgrounds.js @@ -0,0 +1,19 @@ +/** + * Selectable backgrounds. Each `texture` is generated procedurally in + * BootScene.createBackgroundTextures and used as a tiling tileSprite. + * All patterns are designed to tile vertically so the parallax scroll is seamless. + */ +export const BACKGROUNDS = [ + { id: 'grid', name: 'Grid', texture: 'bg_grid' }, + { id: 'hex', name: 'Hex Nodes', texture: 'bg_hex' }, + { id: 'starfield', name: 'Starfield', texture: 'bg_starfield' }, + { id: 'synthwave', name: 'Synthwave', texture: 'bg_synthwave' }, + { id: 'circuit', name: 'Circuit', texture: 'bg_circuit' }, + { id: 'void', name: 'Void', texture: 'bg_void' }, +]; + +export const DEFAULT_BACKGROUND = 'grid'; + +export function getBackground(id) { + return BACKGROUNDS.find((b) => b.id === id) || BACKGROUNDS[0]; +} diff --git a/src/scenes/BootScene.js b/src/scenes/BootScene.js index d6df228..7deff47 100644 --- a/src/scenes/BootScene.js +++ b/src/scenes/BootScene.js @@ -18,10 +18,150 @@ export class BootScene extends Scene { create() { this.createBackgrounds(); + this.createBackgroundTextures(); this.createParticleTextures(); this.scene.start('MenuScene'); } + // Generates a texture via an off-screen graphics buffer. + _tex(key, w, h, draw) { + const g = this.make.graphics({ x: 0, y: 0, add: false }); + draw(g); + g.generateTexture(key, w, h); + g.destroy(); + } + + createBackgroundTextures() { + const S = 256; + + // 1) Grid — Monad blockchain grid (default look, larger so kept as bg_grid) + this._tex('bg_grid', S, S, (g) => { + g.fillStyle(0x0f001f, 1); + g.fillRect(0, 0, S, S); + g.lineStyle(1, 0x2e0059, 0.4); + for (let i = 0; i <= S; i += 32) { + g.moveTo(i, 0); g.lineTo(i, S); + g.moveTo(0, i); g.lineTo(S, i); + } + g.strokePath(); + // node dots at some intersections (deterministic -> tiles) + g.fillStyle(0x7c3aed, 0.35); + for (let x = 0; x <= S; x += 64) { + for (let y = 0; y <= S; y += 64) { + if (((x + y) / 64) % 2 === 0) g.fillCircle(x, y, 2); + } + } + }); + + // 2) Hex Nodes — honeycomb + this._tex('bg_hex', S, S, (g) => { + g.fillStyle(0x0d0420, 1); + g.fillRect(0, 0, S, S); + const r = 22; + const w = Math.sqrt(3) * r; + const vSpace = 1.5 * r; + g.lineStyle(1.5, 0x6d28d9, 0.30); + for (let row = -1, ry = 0; ry < S + r; row++, ry = row * vSpace) { + const offset = (row % 2 === 0) ? 0 : w / 2; + for (let cx = -w; cx < S + w; cx += w) { + this._hex(g, cx + offset, ry, r); + } + } + // accent glow nodes + g.fillStyle(0xa855f7, 0.25); + for (let row = 0, ry = 0; ry < S; row += 2, ry = row * vSpace) { + for (let cx = 0; cx < S; cx += w * 2) g.fillCircle(cx, ry, 2.5); + } + }); + + // 3) Starfield — deep space + this._tex('bg_starfield', S, S, (g) => { + g.fillStyle(0x070016, 1); + g.fillRect(0, 0, S, S); + // faint distant dust + const dust = [0x1a0b3a, 0x12082b]; + for (let i = 0; i < 26; i++) { + g.fillStyle(Phaser.Utils.Array.GetRandom(dust), 0.5); + g.fillCircle(Phaser.Math.Between(0, S), Phaser.Math.Between(0, S), Phaser.Math.Between(20, 55)); + } + // stars + const cols = [0xffffff, 0xd8b4fe, 0x93c5fd, 0xa855f7]; + for (let i = 0; i < 150; i++) { + g.fillStyle(Phaser.Utils.Array.GetRandom(cols), Phaser.Math.FloatBetween(0.35, 1)); + g.fillCircle(Phaser.Math.Between(0, S), Phaser.Math.Between(0, S), Phaser.Math.FloatBetween(0.6, 1.8)); + } + // a few bright glows + for (let i = 0; i < 5; i++) { + const x = Phaser.Math.Between(0, S); const y = Phaser.Math.Between(0, S); + g.fillStyle(0xffffff, 0.18); g.fillCircle(x, y, 6); + g.fillStyle(0xffffff, 0.9); g.fillCircle(x, y, 1.6); + } + }); + + // 4) Synthwave — neon horizontal grid + this._tex('bg_synthwave', S, S, (g) => { + g.fillStyle(0x0a0118, 1); + g.fillRect(0, 0, S, S); + // vertical faint lines + g.lineStyle(1, 0x3b0764, 0.5); + for (let x = 0; x <= S; x += 32) { g.moveTo(x, 0); g.lineTo(x, S); } + g.strokePath(); + // neon horizontal lines, alternating magenta/cyan + for (let y = 0; y <= S; y += 28) { + const cyan = (y / 28) % 2 === 0; + const col = cyan ? 0x00e5ff : 0xff2d95; + g.lineStyle(3, col, 0.10); g.beginPath(); g.moveTo(0, y); g.lineTo(S, y); g.strokePath(); + g.lineStyle(1, col, 0.55); g.beginPath(); g.moveTo(0, y); g.lineTo(S, y); g.strokePath(); + } + }); + + // 5) Circuit — PCB traces + this._tex('bg_circuit', S, S, (g) => { + g.fillStyle(0x04120e, 1); + g.fillRect(0, 0, S, S); + g.lineStyle(1, 0x0f766e, 0.45); + for (let i = 0; i <= S; i += 32) { g.moveTo(i, 0); g.lineTo(i, S); g.moveTo(0, i); g.lineTo(S, i); } + g.strokePath(); + // brighter trace accents (deterministic checker -> tiles) + g.lineStyle(2, 0x10b981, 0.5); + for (let x = 0; x < S; x += 32) { + for (let y = 0; y < S; y += 32) { + if (((x / 32) + (y / 32)) % 3 === 0) { g.beginPath(); g.moveTo(x, y); g.lineTo(x + 32, y); g.strokePath(); } + if (((x / 32) + (y / 32)) % 4 === 0) { g.beginPath(); g.moveTo(x, y); g.lineTo(x, y + 32); g.strokePath(); } + } + } + // solder nodes + g.fillStyle(0x34d399, 0.8); + for (let x = 0; x <= S; x += 32) { + for (let y = 0; y <= S; y += 32) { + if (((x + y) / 32) % 2 === 0) g.fillCircle(x, y, 2); + } + } + }); + + // 6) Void — minimal dark + this._tex('bg_void', S, S, (g) => { + g.fillStyle(0x060010, 1); + g.fillRect(0, 0, S, S); + g.fillStyle(0x1b1036, 0.6); + for (let x = 24; x < S; x += 48) { + for (let y = 24; y < S; y += 48) g.fillCircle(x, y, 1); + } + }); + } + + _hex(g, cx, cy, r) { + g.beginPath(); + for (let i = 0; i < 6; i++) { + const a = Math.PI / 180 * (60 * i - 90); + const px = cx + r * Math.cos(a); + const py = cy + r * Math.sin(a); + if (i === 0) g.moveTo(px, py); else g.lineTo(px, py); + } + g.closePath(); + g.strokePath(); + } + createParticleTextures() { // All white so emitters can tint per-use. Keep textures tiny. @@ -59,37 +199,6 @@ export class BootScene extends Scene { } createBackgrounds() { - const gridW = 512; - const gridH = 512; - const gridGraphics = this.make.graphics({ x: 0, y: 0, add: false }); - - // Deep purple background - gridGraphics.fillStyle(0x0f001f, 1); - gridGraphics.fillRect(0, 0, gridW, gridH); - - // Grid lines - gridGraphics.lineStyle(1, 0x2e0059, 0.35); - for (let i = 0; i <= gridW; i += 32) { - gridGraphics.moveTo(i, 0); - gridGraphics.lineTo(i, gridH); - } - for (let i = 0; i <= gridH; i += 32) { - gridGraphics.moveTo(0, i); - gridGraphics.lineTo(gridW, i); - } - gridGraphics.strokePath(); - - // Some accent hexagons / blockchain nodes - gridGraphics.lineStyle(1, 0x581c87, 0.2); - for (let i = 0; i < 8; i++) { - const hx = Phaser.Math.Between(20, gridW - 20); - const hy = Phaser.Math.Between(20, gridH - 20); - const size = Phaser.Math.Between(6, 14); - gridGraphics.strokeCircle(hx, hy, size); - } - - gridGraphics.generateTexture('gridBg', gridW, gridH); - // Star / particle texture const sGfx = this.make.graphics({ x: 0, y: 0, add: false }); sGfx.fillStyle(0xffffff, 0.9); diff --git a/src/scenes/GameOverScene.js b/src/scenes/GameOverScene.js index 786e657..0bebd8f 100644 --- a/src/scenes/GameOverScene.js +++ b/src/scenes/GameOverScene.js @@ -2,6 +2,7 @@ import { Scene } from 'phaser'; import { sound } from '../managers/SoundManager.js'; import { createButton } from '../utils/ui.js'; import { todaySeed } from '../utils/random.js'; +import { currentBgTexture } from '../utils/background.js'; export class GameOverScene extends Scene { constructor() { @@ -28,7 +29,7 @@ export class GameOverScene extends Scene { create() { const { width, height } = this.scale; - this.add.tileSprite(width / 2, height / 2, width, height, 'gridBg'); + this.add.tileSprite(width / 2, height / 2, width, height, currentBgTexture()); const naddie = this.add.image(width / 2, height * 0.22, 'player_dead').setScale(0.32); this.tweens.add({ targets: naddie, angle: -10, duration: 2000, yoyo: true, repeat: -1, ease: 'Sine.easeInOut' }); diff --git a/src/scenes/GameScene.js b/src/scenes/GameScene.js index f6edb01..90df5f9 100644 --- a/src/scenes/GameScene.js +++ b/src/scenes/GameScene.js @@ -10,6 +10,7 @@ import { StatsManager } from '../managers/StatsManager.js'; import { sound } from '../managers/SoundManager.js'; import { storage, KEYS } from '../utils/storage.js'; import { rng } from '../utils/random.js'; +import { currentBgTexture } from '../utils/background.js'; import { GAME_WIDTH, GAME_HEIGHT, SCORE, PHYSICS } from '../config/game.config.js'; export class GameScene extends Scene { @@ -29,7 +30,7 @@ export class GameScene extends Scene { // Seed gameplay RNG so a given seed produces a reproducible run (Daily). rng.seed(this.seed); - this.bg = this.add.tileSprite(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 'gridBg') + this.bg = this.add.tileSprite(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, currentBgTexture()) .setScrollFactor(0) .setDepth(-10); diff --git a/src/scenes/MenuScene.js b/src/scenes/MenuScene.js index a9a7911..265d050 100644 --- a/src/scenes/MenuScene.js +++ b/src/scenes/MenuScene.js @@ -6,6 +6,7 @@ import { ParticleManager } from '../managers/ParticleManager.js'; import { createButton } from '../utils/ui.js'; import { storage, KEYS } from '../utils/storage.js'; import { todaySeed } from '../utils/random.js'; +import { currentBgTexture, currentBackground, setBackground, BACKGROUNDS } from '../utils/background.js'; export class MenuScene extends Scene { constructor() { @@ -15,7 +16,7 @@ export class MenuScene extends Scene { create() { const { width, height } = this.scale; - this.add.tileSprite(width / 2, height / 2, width, height, 'gridBg'); + this.menuBg = this.add.tileSprite(width / 2, height / 2, width, height, currentBgTexture()); this.add.text(width / 2, height * 0.16, 'NADDIE JUMP', { fontFamily: '"Press Start 2P", monospace', @@ -251,15 +252,15 @@ export class MenuScene extends Scene { showSettings() { const { width, height } = this.scale; - const m = this._openModal('SETTINGS', 460); + const m = this._openModal('SETTINGS', 620); const cx = width / 2; - let baseY = height / 2 - 130; + const cy = height / 2; // Volume stepper - m.add(this.add.text(cx, baseY, 'VOLUME', { + m.add(this.add.text(cx, cy - 230, 'VOLUME', { fontFamily: '"Press Start 2P", monospace', fontSize: '11px', color: '#d8b4fe', }).setOrigin(0.5)); - const volText = this.add.text(cx, baseY + 34, `${Math.round(sound.getVolume() * 100)}%`, { + const volText = this.add.text(cx, cy - 200, `${Math.round(sound.getVolume() * 100)}%`, { fontFamily: '"Press Start 2P", monospace', fontSize: '13px', color: '#ffd700', }).setOrigin(0.5); m.add(volText); @@ -270,27 +271,56 @@ export class MenuScene extends Scene { volText.setText(`${Math.round(sound.getVolume() * 100)}%`); sound.click(); }; - m.add(createButton(this, cx - 90, baseY + 34, '-', () => stepVol(-0.1), { width: 44, height: 36, fontSize: '14px' }).bg); - m.add(createButton(this, cx + 90, baseY + 34, '+', () => stepVol(0.1), { width: 44, height: 36, fontSize: '14px' }).bg); + const volMinus = createButton(this, cx - 90, cy - 200, '-', () => stepVol(-0.1), { width: 44, height: 36, fontSize: '16px' }); + const volPlus = createButton(this, cx + 90, cy - 200, '+', () => stepVol(0.1), { width: 44, height: 36, fontSize: '16px' }); + m.add(volMinus.bg); m.add(volMinus.label); + m.add(volPlus.bg); m.add(volPlus.label); // Particle quality toggle - baseY += 96; - m.add(this.add.text(cx, baseY, 'PARTICLE QUALITY', { + m.add(this.add.text(cx, cy - 150, 'PARTICLE QUALITY', { fontFamily: '"Press Start 2P", monospace', fontSize: '11px', color: '#d8b4fe', }).setOrigin(0.5)); const currentQ = () => storage.getItem(KEYS.particleQuality, ParticleManager.resolveQuality(this)); - const qBtn = createButton(this, cx, baseY + 34, currentQ().toUpperCase(), () => { + const qBtn = createButton(this, cx, cy - 120, currentQ().toUpperCase(), () => { const next = currentQ() === 'low' ? 'high' : 'low'; storage.setItem(KEYS.particleQuality, next); qBtn.label.setText(next.toUpperCase()); sound.click(); - }, { width: 160, height: 38, fontSize: '12px' }); + }, { width: 160, height: 36, fontSize: '12px' }); m.add(qBtn.bg); m.add(qBtn.label); + // Background selector with live preview + m.add(this.add.text(cx, cy - 72, 'BACKGROUND', { + fontFamily: '"Press Start 2P", monospace', fontSize: '11px', color: '#d8b4fe', + }).setOrigin(0.5)); + + let bgIndex = Math.max(0, BACKGROUNDS.findIndex((b) => b.id === currentBackground().id)); + const preview = this.add.tileSprite(cx, cy - 8, 280, 90, BACKGROUNDS[bgIndex].texture).setDepth(102); + m.elements.push(preview); + const frame = this.add.rectangle(cx, cy - 8, 284, 94).setStrokeStyle(2, 0xa855f7); + m.add(frame); + const bgName = this.add.text(cx, cy + 58, BACKGROUNDS[bgIndex].name.toUpperCase(), { + fontFamily: '"Press Start 2P", monospace', fontSize: '11px', color: '#ffd700', + }).setOrigin(0.5); + m.add(bgName); + + const applyBg = (dir) => { + bgIndex = (bgIndex + dir + BACKGROUNDS.length) % BACKGROUNDS.length; + const bg = BACKGROUNDS[bgIndex]; + setBackground(bg.id); + preview.setTexture(bg.texture); + bgName.setText(bg.name.toUpperCase()); + if (this.menuBg) this.menuBg.setTexture(bg.texture); + sound.click(); + }; + const bgPrev = createButton(this, cx - 125, cy + 58, '<', () => applyBg(-1), { width: 40, height: 36, fontSize: '16px' }); + const bgNext = createButton(this, cx + 125, cy + 58, '>', () => applyBg(1), { width: 40, height: 36, fontSize: '16px' }); + m.add(bgPrev.bg); m.add(bgPrev.label); + m.add(bgNext.bg); m.add(bgNext.label); + // Reset progress (two-step confirm) - baseY += 96; let armed = false; - const resetBtn = createButton(this, cx, baseY + 10, 'RESET PROGRESS', () => { + const resetBtn = createButton(this, cx, cy + 130, 'RESET PROGRESS', () => { if (!armed) { armed = true; resetBtn.label.setText('TAP AGAIN!'); @@ -302,7 +332,7 @@ export class MenuScene extends Scene { storage.removeItem(KEYS.achievements); resetBtn.label.setText('DONE'); sound.click(); - }, { width: 240, height: 42, fontSize: '12px', bgColor: 0x581c87, hoverColor: 0x7e22ce }); + }, { width: 240, height: 40, fontSize: '12px', bgColor: 0x581c87, hoverColor: 0x7e22ce }); m.add(resetBtn.bg); m.add(resetBtn.label); } diff --git a/src/utils/background.js b/src/utils/background.js new file mode 100644 index 0000000..e214abb --- /dev/null +++ b/src/utils/background.js @@ -0,0 +1,17 @@ +import { storage, KEYS } from './storage.js'; +import { BACKGROUNDS, DEFAULT_BACKGROUND, getBackground } from '../config/backgrounds.js'; + +export function currentBackground() { + const id = storage.getItem(KEYS.background, DEFAULT_BACKGROUND); + return getBackground(id); +} + +export function currentBgTexture() { + return currentBackground().texture; +} + +export function setBackground(id) { + storage.setItem(KEYS.background, id); +} + +export { BACKGROUNDS, getBackground }; diff --git a/src/utils/storage.js b/src/utils/storage.js index 86491d9..3ff7b11 100644 --- a/src/utils/storage.js +++ b/src/utils/storage.js @@ -11,6 +11,7 @@ export const KEYS = { tutorialSeen: 'naddie_tutorial_seen', achievements: 'naddie_achievements_v1', particleQuality: 'naddie_particle_quality', + background: 'naddie_background', stats: 'naddie_stats_v1', dailyPrefix: 'naddie_daily_', };