diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..7230965
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,5 @@
+node_modules
+dist
+.git
+.DS_Store
+npm-debug.log
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..a49d5ed
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,13 @@
+FROM node:22-alpine AS build
+WORKDIR /app
+COPY package*.json ./
+RUN npm install
+COPY . .
+RUN npm run build
+
+FROM nginx:1.27-alpine
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+COPY --from=build /app/dist /usr/share/nginx/html
+EXPOSE 80
+HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=5 \
+ CMD wget -q -O /dev/null http://127.0.0.1/health || exit 1
diff --git a/README.md b/README.md
index 9b23ce4..84804ec 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,46 @@
-# squirrel-forest-deploy
+# Squirrel Forest
-Public deployment mirror for the Squirrel Forest Phaser prototype.
\ No newline at end of file
+A small 2D adventure-platformer prototype about a squirrel exploring a living woodland full of unusual flora, fauna and mushrooms.
+
+## Prototype loop
+
+- Explore a continuous side-scrolling forest.
+- Run and jump across roots and branches.
+- Collect acorns while searching for five unusual mushrooms.
+- Pass through named woodland areas with ferns, flowers, frogs and butterflies.
+- Find all five forest caps to awaken the Moonlit Grove.
+- Follow the fireflies to complete the prototype trail.
+
+Everything in the first prototype uses Phaser-generated textures, so there are no external art dependencies yet.
+
+## Controls
+
+- **Move:** A/D or Left/Right
+- **Jump:** W, Up or Space
+- **Touch:** on-screen left/right/jump controls
+
+## Run
+
+```bash
+npm install
+npm run dev
+```
+
+Production build:
+
+```bash
+npm run build
+```
+
+## Direction
+
+The intended feel is a cosy exploratory woodland adventure: dense layers of forest, lots of discoverable species, small traversal secrets and environmental storytelling rather than combat-first progression.
+
+Good next slices:
+
+1. replace generated graphics with a hand-painted squirrel animation set;
+2. add climbable trunks and branch-to-branch movement;
+3. introduce a field-guide journal for discovered plants, fungi and animals;
+4. split the forest into richer biome pockets such as birch grove, damp hollow, old oak canopy and moonlit marsh;
+5. add tiny NPC animals and lightweight quests;
+6. add ambient audio, leaf particles, weather and a day/night cycle.
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..cedc6e4
--- /dev/null
+++ b/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Squirrel Forest
+
+
+
+
+
+
diff --git a/nginx.conf b/nginx.conf
new file mode 100644
index 0000000..c52884b
--- /dev/null
+++ b/nginx.conf
@@ -0,0 +1,16 @@
+server {
+ listen 80;
+ server_name _;
+ root /usr/share/nginx/html;
+ index index.html;
+
+ location = /health {
+ access_log off;
+ add_header Content-Type text/plain;
+ return 200 'ok';
+ }
+
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..e99c624
--- /dev/null
+++ b/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "squirrel-forest",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --host 0.0.0.0",
+ "build": "vite build",
+ "preview": "vite preview --host 0.0.0.0"
+ },
+ "dependencies": {
+ "phaser": "^3.90.0"
+ },
+ "devDependencies": {
+ "vite": "^7.1.7"
+ }
+}
diff --git a/src/main.js b/src/main.js
new file mode 100644
index 0000000..5abfab4
--- /dev/null
+++ b/src/main.js
@@ -0,0 +1,608 @@
+import Phaser from 'phaser';
+import './style.css';
+
+const WORLD_WIDTH = 5600;
+const WORLD_HEIGHT = 720;
+const GROUND_Y = 628;
+
+class ForestScene extends Phaser.Scene {
+ constructor() {
+ super('forest');
+ this.acorns = 0;
+ this.mushrooms = 0;
+ this.totalMushrooms = 5;
+ this.touch = { left: false, right: false, jump: false };
+ }
+
+ create() {
+ this.physics.world.setBounds(0, 0, WORLD_WIDTH, WORLD_HEIGHT);
+ this.cameras.main.setBounds(0, 0, WORLD_WIDTH, WORLD_HEIGHT);
+ this.cameras.main.setBackgroundColor('#8fc7a2');
+
+ this.createTextures();
+ this.createBackdrop();
+ this.createWorld();
+ this.createPlayer();
+ this.createCollectibles();
+ this.createFauna();
+ this.createHud();
+ this.createControls();
+
+ this.cameras.main.startFollow(this.player, true, 0.08, 0.08, 0, 70);
+ this.cameras.main.setDeadzone(180, 110);
+ this.cameras.main.fadeIn(700, 16, 37, 29);
+
+ this.physics.add.collider(this.player, this.platforms);
+ this.physics.add.collider(this.acornGroup, this.platforms);
+ this.physics.add.collider(this.mushroomGroup, this.platforms);
+
+ this.physics.add.overlap(this.player, this.acornGroup, (_, acorn) => {
+ acorn.disableBody(true, true);
+ this.acorns += 1;
+ this.acornText.setText(`Acorns ${this.acorns}`);
+ this.tweens.add({
+ targets: this.acornText,
+ scale: 1.15,
+ duration: 90,
+ yoyo: true
+ });
+ });
+
+ this.physics.add.overlap(this.player, this.mushroomGroup, (_, mushroom) => {
+ mushroom.disableBody(true, true);
+ this.mushrooms += 1;
+ this.mushroomText.setText(`Forest caps ${this.mushrooms} / ${this.totalMushrooms}`);
+ this.showDiscovery(mushroom.getData('name'));
+
+ if (this.mushrooms === this.totalMushrooms) {
+ this.objectiveText.setText('The moonlit grove is awake — follow the fireflies east');
+ this.revealGrove();
+ }
+ });
+
+ this.physics.add.overlap(this.player, this.groveTrigger, () => {
+ if (this.mushrooms < this.totalMushrooms || this.finished) return;
+ this.finished = true;
+ this.player.setVelocityX(0);
+ this.player.setTint(0xfff2bd);
+ this.showEnding();
+ });
+ }
+
+ createTextures() {
+ const g = this.make.graphics({ add: false });
+
+ g.clear();
+ g.fillStyle(0x6e482b, 1);
+ g.fillEllipse(24, 30, 35, 24);
+ g.fillStyle(0x9c6840, 1);
+ g.fillEllipse(30, 25, 28, 20);
+ g.fillStyle(0x6e482b, 1);
+ g.fillCircle(42, 20, 10);
+ g.fillTriangle(39, 13, 43, 3, 47, 14);
+ g.fillStyle(0xf3d5a0, 1);
+ g.fillCircle(46, 21, 3);
+ g.fillStyle(0x2c2018, 1);
+ g.fillCircle(48, 19, 1.5);
+ g.fillStyle(0x8c5b35, 1);
+ g.fillEllipse(9, 25, 22, 38);
+ g.lineStyle(5, 0x8c5b35, 1);
+ g.beginPath();
+ g.arc(7, 20, 16, Math.PI * 0.6, Math.PI * 1.65);
+ g.strokePath();
+ g.fillStyle(0x4d321f, 1);
+ g.fillRect(17, 38, 7, 12);
+ g.fillRect(33, 38, 7, 12);
+ g.generateTexture('squirrel', 58, 56);
+
+ g.clear();
+ g.fillStyle(0x8f5a2b, 1);
+ g.fillEllipse(10, 13, 15, 13);
+ g.fillStyle(0x4f341c, 1);
+ g.fillRect(6, 2, 8, 5);
+ g.generateTexture('acorn', 20, 22);
+
+ g.clear();
+ g.fillStyle(0xf4ddba, 1);
+ g.fillRoundedRect(12, 14, 7, 18, 3);
+ g.fillStyle(0xb94846, 1);
+ g.fillEllipse(16, 14, 28, 17);
+ g.fillStyle(0xffe8c8, 1);
+ g.fillCircle(10, 10, 2);
+ g.fillCircle(18, 8, 2);
+ g.fillCircle(23, 13, 2);
+ g.generateTexture('mushroom-red', 32, 34);
+
+ g.clear();
+ g.fillStyle(0xe8dbba, 1);
+ g.fillRoundedRect(12, 14, 7, 18, 3);
+ g.fillStyle(0x76528f, 1);
+ g.fillEllipse(16, 14, 28, 17);
+ g.fillStyle(0xdcccf0, 1);
+ g.fillCircle(10, 10, 2);
+ g.fillCircle(20, 9, 2);
+ g.generateTexture('mushroom-purple', 32, 34);
+
+ g.clear();
+ g.fillStyle(0xe0cfaa, 1);
+ g.fillRoundedRect(12, 13, 7, 19, 3);
+ g.fillStyle(0xe1a53a, 1);
+ g.fillEllipse(16, 13, 28, 16);
+ g.fillStyle(0xffefaa, 1);
+ g.fillCircle(10, 10, 2);
+ g.fillCircle(21, 10, 2);
+ g.generateTexture('mushroom-gold', 32, 34);
+
+ g.clear();
+ g.fillStyle(0x35684b, 1);
+ for (let i = 0; i < 5; i++) {
+ g.fillEllipse(17 + i * 2, 22 - i * 4, 9, 25, -0.7 + i * 0.25);
+ }
+ g.fillStyle(0x6aa56d, 1);
+ for (let i = 0; i < 4; i++) {
+ g.fillEllipse(12 + i * 6, 25 - (i % 2) * 6, 7, 19, 0.6 - i * 0.25);
+ }
+ g.generateTexture('fern', 42, 44);
+
+ g.clear();
+ g.fillStyle(0xe8b7c7, 1);
+ for (let i = 0; i < 5; i++) {
+ const a = (Math.PI * 2 * i) / 5;
+ g.fillCircle(9 + Math.cos(a) * 5, 9 + Math.sin(a) * 5, 4);
+ }
+ g.fillStyle(0xf5d067, 1);
+ g.fillCircle(9, 9, 3);
+ g.fillStyle(0x477654, 1);
+ g.fillRect(8, 14, 2, 14);
+ g.generateTexture('flower', 18, 30);
+
+ g.clear();
+ g.fillStyle(0x6c4429, 1);
+ g.fillRoundedRect(32, 0, 28, 120, 10);
+ g.fillStyle(0x244f39, 1);
+ g.fillCircle(22, 15, 28);
+ g.fillCircle(52, 12, 35);
+ g.fillCircle(70, 30, 28);
+ g.fillCircle(32, 42, 33);
+ g.fillStyle(0x35684b, 1);
+ g.fillCircle(10, 35, 22);
+ g.fillCircle(55, 50, 28);
+ g.generateTexture('tree', 92, 120);
+
+ g.clear();
+ g.fillStyle(0x393024, 1);
+ g.fillRoundedRect(0, 0, 140, 22, 10);
+ g.fillStyle(0x567645, 1);
+ g.fillRect(0, 0, 140, 7);
+ g.generateTexture('branch', 140, 22);
+
+ g.clear();
+ g.fillStyle(0xb4d9f2, 0.9);
+ g.fillEllipse(8, 8, 12, 7, -0.45);
+ g.fillEllipse(16, 8, 12, 7, 0.45);
+ g.fillStyle(0x3b3130, 1);
+ g.fillEllipse(12, 9, 4, 10);
+ g.generateTexture('butterfly', 24, 18);
+
+ g.clear();
+ g.fillStyle(0xffef95, 1);
+ g.fillCircle(6, 6, 3);
+ g.fillStyle(0xffef95, 0.18);
+ g.fillCircle(6, 6, 6);
+ g.generateTexture('firefly', 12, 12);
+
+ g.clear();
+ g.fillStyle(0x517a4e, 1);
+ g.fillEllipse(15, 16, 24, 16);
+ g.fillStyle(0x75995b, 1);
+ g.fillCircle(18, 10, 8);
+ g.fillStyle(0x1e2c22, 1);
+ g.fillCircle(15, 8, 1.5);
+ g.fillCircle(21, 8, 1.5);
+ g.generateTexture('frog', 30, 24);
+
+ g.destroy();
+ }
+
+ createBackdrop() {
+ this.add.rectangle(WORLD_WIDTH / 2, 270, WORLD_WIDTH, 540, 0x8fc7a2).setScrollFactor(0.05);
+
+ const far = this.add.graphics().setScrollFactor(0.12);
+ far.fillStyle(0x719f78, 1);
+ for (let x = -100; x < WORLD_WIDTH + 300; x += 170) {
+ far.fillCircle(x, 350 + (x % 3) * 8, 180);
+ }
+
+ const mist = this.add.graphics().setScrollFactor(0.22);
+ mist.fillStyle(0xd9ead6, 0.28);
+ for (let x = 0; x < WORLD_WIDTH; x += 500) {
+ mist.fillEllipse(x, 480 + (x % 2) * 20, 520, 120);
+ }
+
+ const canopy = this.add.graphics().setScrollFactor(0.35);
+ canopy.fillStyle(0x315f45, 0.95);
+ for (let x = -120; x < WORLD_WIDTH + 300; x += 150) {
+ canopy.fillCircle(x, 140 + ((x / 150) % 3) * 28, 110);
+ }
+
+ for (let x = 200; x < WORLD_WIDTH; x += 720) {
+ this.add.image(x, 455, 'tree').setScale(2.5).setAlpha(0.58).setScrollFactor(0.55).setOrigin(0.5, 1);
+ }
+ }
+
+ createWorld() {
+ this.platforms = this.physics.add.staticGroup();
+
+ const ground = this.add.rectangle(WORLD_WIDTH / 2, GROUND_Y + 46, WORLD_WIDTH, 100, 0x263c2d);
+ this.physics.add.existing(ground, true);
+ this.platforms.add(ground);
+
+ this.add.rectangle(WORLD_WIDTH / 2, GROUND_Y + 2, WORLD_WIDTH, 20, 0x55714a);
+
+ const branchPositions = [
+ [520, 520], [900, 445], [1320, 525], [1680, 415], [2050, 505],
+ [2470, 430], [2910, 515], [3370, 405], [3800, 500], [4230, 425],
+ [4680, 520], [5100, 430]
+ ];
+
+ branchPositions.forEach(([x, y], index) => {
+ const branch = this.platforms.create(x, y, 'branch');
+ branch.setScale(index % 3 === 0 ? 1.35 : 1);
+ branch.refreshBody();
+ });
+
+ for (let x = 90; x < WORLD_WIDTH - 120; x += 105) {
+ const n = Math.sin(x * 0.013) + Math.cos(x * 0.021);
+ if (n > -0.15) {
+ this.add.image(x, GROUND_Y - 10, 'fern')
+ .setOrigin(0.5, 1)
+ .setScale(0.65 + ((x % 70) / 160))
+ .setAlpha(0.85);
+ }
+
+ if (x % 315 < 130) {
+ this.add.image(x + 34, GROUND_Y - 5, 'flower')
+ .setOrigin(0.5, 1)
+ .setScale(0.7 + ((x % 50) / 170));
+ }
+ }
+
+ const zones = [
+ { x: 430, label: 'Hazel Hollow' },
+ { x: 1720, label: 'Fernfall' },
+ { x: 3070, label: 'Foxglove Rise' },
+ { x: 4380, label: 'Old Root Wood' },
+ { x: 5260, label: 'Moonlit Grove' }
+ ];
+
+ zones.forEach(zone => {
+ this.add.text(zone.x, 270, zone.label, {
+ fontFamily: 'Georgia, serif',
+ fontSize: '28px',
+ color: '#f2edd9',
+ stroke: '#24372c',
+ strokeThickness: 5
+ }).setOrigin(0.5).setAlpha(0.75);
+ });
+
+ this.groveGlow = this.add.circle(5260, 500, 130, 0xffe3a0, 0.05);
+ this.groveGlow.setVisible(false);
+ this.groveTrigger = this.add.zone(5260, 520, 260, 220);
+ this.physics.add.existing(this.groveTrigger, true);
+ }
+
+ createPlayer() {
+ this.player = this.physics.add.sprite(160, 520, 'squirrel');
+ this.player.setScale(1.12);
+ this.player.setCollideWorldBounds(true);
+ this.player.setDragX(900);
+ this.player.setMaxVelocity(330, 760);
+ this.player.body.setSize(34, 47);
+ this.player.body.setOffset(12, 7);
+ }
+
+ createCollectibles() {
+ this.acornGroup = this.physics.add.group({ allowGravity: true });
+ [
+ [330, 540], [520, 460], [760, 560], [900, 385], [1180, 555],
+ [1320, 465], [1540, 560], [1680, 355], [1900, 555], [2050, 445],
+ [2280, 555], [2470, 370], [2730, 555], [2910, 455], [3150, 555],
+ [3370, 345], [3610, 555], [3800, 440], [4050, 555], [4230, 365],
+ [4470, 555], [4680, 460], [4910, 555], [5100, 370]
+ ].forEach(([x, y], i) => {
+ const acorn = this.acornGroup.create(x, y, 'acorn');
+ acorn.setBounce(0.2);
+ acorn.setData('phase', i * 0.4);
+ });
+
+ this.mushroomGroup = this.physics.add.group({ allowGravity: true });
+ const mushrooms = [
+ [720, 575, 'mushroom-red', 'Scarlet bonnet'],
+ [1700, 360, 'mushroom-purple', 'Amethyst veil'],
+ [2860, 575, 'mushroom-gold', 'Honey lantern'],
+ [4170, 370, 'mushroom-red', 'Foxfire cap'],
+ [4860, 575, 'mushroom-purple', 'Moon gill']
+ ];
+
+ mushrooms.forEach(([x, y, texture, name]) => {
+ const mushroom = this.mushroomGroup.create(x, y, texture);
+ mushroom.setData('name', name);
+ mushroom.setBounce(0.1);
+ this.tweens.add({
+ targets: mushroom,
+ y: y - 6,
+ duration: 900 + (x % 400),
+ yoyo: true,
+ repeat: -1,
+ ease: 'Sine.inOut'
+ });
+ });
+ }
+
+ createFauna() {
+ const butterflies = [
+ [850, 330], [1450, 300], [2300, 350], [3520, 280], [4550, 315]
+ ];
+
+ butterflies.forEach(([x, y], i) => {
+ const b = this.add.image(x, y, 'butterfly').setScale(0.85);
+ this.tweens.add({
+ targets: b,
+ x: x + (i % 2 ? -90 : 90),
+ y: y - 35,
+ duration: 2600 + i * 210,
+ yoyo: true,
+ repeat: -1,
+ ease: 'Sine.inOut'
+ });
+ });
+
+ [1120, 2600, 3980].forEach((x, i) => {
+ const frog = this.add.image(x, GROUND_Y - 8, 'frog').setOrigin(0.5, 1);
+ this.tweens.add({
+ targets: frog,
+ y: GROUND_Y - 20,
+ duration: 220,
+ delay: 2300 + i * 900,
+ yoyo: true,
+ repeat: -1,
+ repeatDelay: 3000 + i * 500
+ });
+ });
+
+ this.fireflies = [];
+ for (let i = 0; i < 24; i++) {
+ const f = this.add.image(5100 + (i % 6) * 55, 360 + Math.floor(i / 6) * 48, 'firefly');
+ f.setAlpha(0.12);
+ this.fireflies.push(f);
+ this.tweens.add({
+ targets: f,
+ x: f.x + Phaser.Math.Between(-28, 28),
+ y: f.y + Phaser.Math.Between(-22, 22),
+ duration: Phaser.Math.Between(1000, 2200),
+ yoyo: true,
+ repeat: -1,
+ ease: 'Sine.inOut'
+ });
+ });
+ }
+
+ createHud() {
+ const panel = this.add.rectangle(18, 18, 300, 112, 0x17271f, 0.78)
+ .setOrigin(0)
+ .setScrollFactor(0)
+ .setDepth(20)
+ .setStrokeStyle(1, 0xc9d6bd, 0.3);
+
+ this.acornText = this.add.text(34, 31, 'Acorns 0', {
+ fontFamily: 'Georgia, serif',
+ fontSize: '20px',
+ color: '#f1d39a'
+ }).setScrollFactor(0).setDepth(21);
+
+ this.mushroomText = this.add.text(34, 59, `Forest caps 0 / ${this.totalMushrooms}`, {
+ fontFamily: 'Georgia, serif',
+ fontSize: '18px',
+ color: '#dfd3ed'
+ }).setScrollFactor(0).setDepth(21);
+
+ this.objectiveText = this.add.text(34, 89, 'Find the five unusual mushrooms hidden through the wood', {
+ fontFamily: 'Georgia, serif',
+ fontSize: '13px',
+ color: '#dbe5d7',
+ wordWrap: { width: 270 }
+ }).setScrollFactor(0).setDepth(21);
+
+ this.discovery = this.add.text(this.scale.width / 2, 150, '', {
+ fontFamily: 'Georgia, serif',
+ fontSize: '25px',
+ color: '#fff3cf',
+ stroke: '#1a2a21',
+ strokeThickness: 6
+ }).setOrigin(0.5).setScrollFactor(0).setDepth(30).setAlpha(0);
+
+ this.scale.on('resize', gameSize => {
+ this.discovery.setPosition(gameSize.width / 2, 150);
+ this.positionTouchControls(gameSize.width, gameSize.height);
+ });
+ }
+
+ createControls() {
+ this.cursors = this.input.keyboard.createCursorKeys();
+ this.keys = this.input.keyboard.addKeys('A,D,W,SPACE');
+
+ const makeButton = (x, y, label, onDown, onUp) => {
+ const circle = this.add.circle(x, y, 34, 0x15261e, 0.55)
+ .setScrollFactor(0)
+ .setDepth(40)
+ .setStrokeStyle(2, 0xe0eadb, 0.35)
+ .setInteractive();
+
+ const text = this.add.text(x, y - 2, label, {
+ fontFamily: 'Arial, sans-serif',
+ fontSize: '24px',
+ color: '#edf4e9'
+ }).setOrigin(0.5).setScrollFactor(0).setDepth(41);
+
+ circle.on('pointerdown', onDown);
+ circle.on('pointerup', onUp);
+ circle.on('pointerout', onUp);
+ return { circle, text };
+ };
+
+ this.leftBtn = makeButton(68, this.scale.height - 70, '‹',
+ () => { this.touch.left = true; },
+ () => { this.touch.left = false; }
+ );
+
+ this.rightBtn = makeButton(146, this.scale.height - 70, '›',
+ () => { this.touch.right = true; },
+ () => { this.touch.right = false; }
+ );
+
+ this.jumpBtn = makeButton(this.scale.width - 74, this.scale.height - 70, '↑',
+ () => { this.touch.jump = true; },
+ () => { this.touch.jump = false; }
+ );
+
+ this.input.keyboard.on('keydown-UP', () => this.tryJump());
+ this.input.keyboard.on('keydown-W', () => this.tryJump());
+ this.input.keyboard.on('keydown-SPACE', () => this.tryJump());
+ }
+
+ positionTouchControls(width, height) {
+ if (!this.leftBtn) return;
+ this.leftBtn.circle.setPosition(68, height - 70);
+ this.leftBtn.text.setPosition(68, height - 72);
+ this.rightBtn.circle.setPosition(146, height - 70);
+ this.rightBtn.text.setPosition(146, height - 72);
+ this.jumpBtn.circle.setPosition(width - 74, height - 70);
+ this.jumpBtn.text.setPosition(width - 74, height - 72);
+ }
+
+ tryJump() {
+ if (!this.player?.body) return;
+ if (this.player.body.blocked.down || this.player.body.touching.down) {
+ this.player.setVelocityY(-470);
+ this.player.setAngularVelocity(this.player.flipX ? -25 : 25);
+ this.time.delayedCall(180, () => this.player?.setAngularVelocity(0));
+ }
+ }
+
+ showDiscovery(name) {
+ this.discovery.setText(`Discovered: ${name}`);
+ this.discovery.setAlpha(0).setY(158);
+ this.tweens.add({
+ targets: this.discovery,
+ alpha: 1,
+ y: 142,
+ duration: 260,
+ yoyo: true,
+ hold: 950
+ });
+ }
+
+ revealGrove() {
+ this.groveGlow.setVisible(true);
+ this.tweens.add({
+ targets: this.groveGlow,
+ alpha: 0.28,
+ scale: 1.35,
+ duration: 1300,
+ yoyo: true,
+ repeat: -1,
+ ease: 'Sine.inOut'
+ });
+
+ this.fireflies.forEach((f, i) => {
+ this.tweens.add({
+ targets: f,
+ alpha: 0.9,
+ duration: 500 + i * 35
+ });
+ });
+ }
+
+ showEnding() {
+ const title = this.add.text(this.scale.width / 2, this.scale.height / 2 - 42,
+ 'The Moonlit Grove remembers you.', {
+ fontFamily: 'Georgia, serif',
+ fontSize: '34px',
+ color: '#fff1bf',
+ stroke: '#15241d',
+ strokeThickness: 7,
+ align: 'center'
+ }).setOrigin(0.5).setScrollFactor(0).setDepth(50).setAlpha(0);
+
+ const sub = this.add.text(this.scale.width / 2, this.scale.height / 2 + 16,
+ `You found ${this.mushrooms} forest caps and ${this.acorns} acorns.\nPrototype trail complete.`, {
+ fontFamily: 'Georgia, serif',
+ fontSize: '18px',
+ color: '#e0eadb',
+ align: 'center',
+ lineSpacing: 8
+ }).setOrigin(0.5).setScrollFactor(0).setDepth(50).setAlpha(0);
+
+ this.tweens.add({ targets: [title, sub], alpha: 1, duration: 900 });
+ }
+
+ update(time) {
+ if (!this.player?.body || this.finished) return;
+
+ const left = this.cursors.left.isDown || this.keys.A.isDown || this.touch.left;
+ const right = this.cursors.right.isDown || this.keys.D.isDown || this.touch.right;
+
+ if (left && !right) {
+ this.player.setAccelerationX(-1050);
+ this.player.setFlipX(true);
+ } else if (right && !left) {
+ this.player.setAccelerationX(1050);
+ this.player.setFlipX(false);
+ } else {
+ this.player.setAccelerationX(0);
+ }
+
+ if (this.touch.jump) {
+ this.tryJump();
+ this.touch.jump = false;
+ }
+
+ const grounded = this.player.body.blocked.down || this.player.body.touching.down;
+ if (grounded) {
+ this.player.setAngle(0);
+ } else {
+ this.player.setAngle(Phaser.Math.Clamp(this.player.body.velocity.x * 0.025, -8, 8));
+ }
+
+ this.acornGroup.children.iterate(acorn => {
+ if (!acorn?.active) return;
+ acorn.setAngle(Math.sin(time * 0.003 + acorn.getData('phase')) * 10);
+ });
+ }
+}
+
+const config = {
+ type: Phaser.AUTO,
+ parent: 'game',
+ width: 1280,
+ height: 720,
+ backgroundColor: '#10251d',
+ physics: {
+ default: 'arcade',
+ arcade: {
+ gravity: { y: 1180 },
+ debug: false
+ }
+ },
+ scale: {
+ mode: Phaser.Scale.RESIZE,
+ autoCenter: Phaser.Scale.CENTER_BOTH
+ },
+ render: {
+ antialias: true,
+ pixelArt: false
+ },
+ scene: [ForestScene]
+};
+
+new Phaser.Game(config);
diff --git a/src/style.css b/src/style.css
new file mode 100644
index 0000000..a3ada45
--- /dev/null
+++ b/src/style.css
@@ -0,0 +1,20 @@
+html,
+body,
+#game {
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ overflow: hidden;
+ background: #10251d;
+}
+
+body {
+ font-family: Georgia, "Times New Roman", serif;
+ touch-action: none;
+}
+
+canvas {
+ display: block;
+ width: 100%;
+ height: 100%;
+}