deploy: mirror squirrel forest prototype
This commit is contained in:
parent
bfc0d60ed3
commit
9cfc537b66
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
dist
|
||||
.git
|
||||
.DS_Store
|
||||
npm-debug.log
|
||||
13
Dockerfile
Normal file
13
Dockerfile
Normal file
@ -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
|
||||
47
README.md
47
README.md
@ -1,3 +1,46 @@
|
||||
# squirrel-forest-deploy
|
||||
# Squirrel Forest
|
||||
|
||||
Public deployment mirror for the Squirrel Forest Phaser prototype.
|
||||
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.
|
||||
|
||||
13
index.html
Normal file
13
index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#10251d" />
|
||||
<title>Squirrel Forest</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="game"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
16
nginx.conf
Normal file
16
nginx.conf
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
17
package.json
Normal file
17
package.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
608
src/main.js
Normal file
608
src/main.js
Normal file
@ -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);
|
||||
20
src/style.css
Normal file
20
src/style.css
Normal file
@ -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%;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user