refactor: sync hardened player surfaces and runtime atlas
This commit is contained in:
parent
fd01d9aa51
commit
8a4efc758b
@ -1,9 +1,9 @@
|
|||||||
FROM node:22-alpine AS build
|
FROM node:22-alpine AS build
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm install
|
RUN npm install --no-audit --no-fund
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run build
|
RUN rm -f public/assets/terrain-sheet.png public/assets/flora-sheet.png public/assets/fauna-sheet.png public/assets/player-sheet.png && npm run build
|
||||||
|
|
||||||
FROM nginx:1.27-alpine
|
FROM nginx:1.27-alpine
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|||||||
46
README.md
46
README.md
@ -1,23 +1,33 @@
|
|||||||
# Squirrel Forest
|
# Squirrel Forest
|
||||||
|
|
||||||
A small 2D adventure-platformer prototype about a squirrel exploring a living woodland full of unusual flora, fauna and mushrooms.
|
A 2D Phaser adventure-platformer about a squirrel exploring a living woodland full of flora, fauna and unusual mushrooms.
|
||||||
|
|
||||||
## Prototype loop
|
## Current prototype
|
||||||
|
|
||||||
- Explore a continuous side-scrolling forest.
|
- Side-scrolling exploration across five named woodland areas.
|
||||||
- Run and jump across roots and branches.
|
- Animated squirrel with separate visual and physics bodies.
|
||||||
- Collect acorns while searching for five unusual mushrooms.
|
- Keyboard and multi-touch movement/jumping.
|
||||||
- Pass through named woodland areas with ferns, flowers, frogs and butterflies.
|
- One-way log platforms: jump through from below and land from above.
|
||||||
- Find all five forest caps to awaken the Moonlit Grove.
|
- Acorn collection and five mushroom discoveries.
|
||||||
- Follow the fireflies to complete the prototype trail.
|
- Ambient frogs, hedgehogs, butterflies and fireflies.
|
||||||
|
- Moonlit Grove completion sequence.
|
||||||
Everything in the first prototype uses Phaser-generated textures, so there are no external art dependencies yet.
|
|
||||||
|
|
||||||
## Controls
|
## Controls
|
||||||
|
|
||||||
- **Move:** A/D or Left/Right
|
- **Move:** A/D or Left/Right
|
||||||
- **Jump:** W, Up or Space
|
- **Jump:** W, Up or Space
|
||||||
- **Touch:** on-screen left/right/jump controls
|
- **Touch:** independent left/right and jump controls
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
- `src/main.js` — scene orchestration, world content, HUD and input.
|
||||||
|
- `src/PlayerController.js` — squirrel physics/visual synchronization and animation state.
|
||||||
|
- `src/worldConfig.js` — shared world/surface geometry and platform metadata.
|
||||||
|
- `public/assets/player-game-sheet.png` — normalized squirrel animation sheet.
|
||||||
|
- `public/assets/world-atlas.png/json` — optimized runtime atlas for terrain, flora and fauna.
|
||||||
|
- `source-assets/` — preserved original source art sheets.
|
||||||
|
|
||||||
|
The original large public source sheets are retained in Git for now, but the production Docker build removes them before Vite runs so they are not shipped to the browser.
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
@ -34,13 +44,13 @@ npm run build
|
|||||||
|
|
||||||
## Direction
|
## 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.
|
The intended feel is a cosy exploratory woodland adventure focused on traversal, discovery, environmental storytelling and small animal interactions rather than combat-first progression.
|
||||||
|
|
||||||
Good next slices:
|
Good next slices:
|
||||||
|
|
||||||
1. replace generated graphics with a hand-painted squirrel animation set;
|
1. climbable trunks and branch-to-branch movement;
|
||||||
2. add climbable trunks and branch-to-branch movement;
|
2. field-guide journal for discovered plants, fungi and animals;
|
||||||
3. introduce a field-guide journal for discovered plants, fungi and animals;
|
3. richer biome pockets such as birch grove, damp hollow, old oak canopy and moonlit marsh;
|
||||||
4. split the forest into richer biome pockets such as birch grove, damp hollow, old oak canopy and moonlit marsh;
|
4. lightweight animal NPC quests;
|
||||||
5. add tiny NPC animals and lightweight quests;
|
5. acorn throwing / interaction using the existing player frames;
|
||||||
6. add ambient audio, leaf particles, weather and a day/night cycle.
|
6. re-enable audio behind a user gesture once the sound pass begins.
|
||||||
|
|||||||
@ -10,7 +10,10 @@
|
|||||||
<div id="game"></div>
|
<div id="game"></div>
|
||||||
<script>
|
<script>
|
||||||
(() => {
|
(() => {
|
||||||
|
window.__squirrelBooted = false;
|
||||||
|
|
||||||
const showBootError = (message) => {
|
const showBootError = (message) => {
|
||||||
|
if (window.__squirrelBooted) return;
|
||||||
if (document.getElementById('boot-error')) return;
|
if (document.getElementById('boot-error')) return;
|
||||||
const box = document.createElement('div');
|
const box = document.createElement('div');
|
||||||
box.id = 'boot-error';
|
box.id = 'boot-error';
|
||||||
|
|||||||
@ -9,9 +9,9 @@
|
|||||||
"preview": "vite preview --host 0.0.0.0"
|
"preview": "vite preview --host 0.0.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"phaser": "^3.90.0"
|
"phaser": "3.90.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^7.1.7"
|
"vite": "7.3.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
99
src/PlayerController.js
Normal file
99
src/PlayerController.js
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import Phaser from 'phaser';
|
||||||
|
|
||||||
|
export class PlayerController {
|
||||||
|
constructor(scene) {
|
||||||
|
this.scene = scene;
|
||||||
|
this.bodySprite = null;
|
||||||
|
this.visual = null;
|
||||||
|
this.wasGrounded = false;
|
||||||
|
this.landing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
create(x, y) {
|
||||||
|
this.bodySprite = this.scene.physics.add.sprite(x, y, 'player-body');
|
||||||
|
this.bodySprite.setAlpha(0);
|
||||||
|
this.bodySprite.setCollideWorldBounds(true);
|
||||||
|
this.bodySprite.setDragX(900);
|
||||||
|
this.bodySprite.setMaxVelocity(330, 760);
|
||||||
|
this.bodySprite.body.setSize(40, 68, true);
|
||||||
|
|
||||||
|
this.visual = this.scene.add.sprite(x, y, 'player-game', 0);
|
||||||
|
this.visual.setScale(0.42);
|
||||||
|
this.visual.setOrigin(0.5, 209 / 220);
|
||||||
|
this.visual.setDepth(12);
|
||||||
|
this.visual.play('squirrel-idle');
|
||||||
|
|
||||||
|
this.syncVisual();
|
||||||
|
return this.bodySprite;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFacing(direction) {
|
||||||
|
if (!this.visual) return;
|
||||||
|
this.visual.setFlipX(direction < 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
tryJump() {
|
||||||
|
if (!this.bodySprite?.body) return;
|
||||||
|
|
||||||
|
if (this.bodySprite.body.blocked.down || this.bodySprite.body.touching.down) {
|
||||||
|
this.bodySprite.setVelocityY(-520);
|
||||||
|
this.landing = false;
|
||||||
|
this.visual.play('squirrel-jump', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAnimation(grounded) {
|
||||||
|
if (!this.bodySprite?.body || !this.visual) return;
|
||||||
|
|
||||||
|
if (grounded && !this.wasGrounded) {
|
||||||
|
this.landing = true;
|
||||||
|
this.visual.play('squirrel-land', true);
|
||||||
|
this.visual.once('animationcomplete-squirrel-land', () => {
|
||||||
|
this.landing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (grounded) {
|
||||||
|
this.visual.setAngle(0);
|
||||||
|
if (!this.landing) {
|
||||||
|
if (Math.abs(this.bodySprite.body.velocity.x) > 35) {
|
||||||
|
this.visual.play('squirrel-run', true);
|
||||||
|
} else {
|
||||||
|
this.visual.play('squirrel-idle', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (this.bodySprite.body.velocity.y < 0) {
|
||||||
|
this.landing = false;
|
||||||
|
this.visual.play('squirrel-jump', true);
|
||||||
|
this.visual.setAngle(
|
||||||
|
Phaser.Math.Clamp(this.bodySprite.body.velocity.x * 0.012, -5, 5)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.landing = false;
|
||||||
|
this.visual.play('squirrel-fall', true);
|
||||||
|
this.visual.setAngle(
|
||||||
|
Phaser.Math.Clamp(this.bodySprite.body.velocity.x * 0.012, -5, 5)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.wasGrounded = grounded;
|
||||||
|
this.syncVisual();
|
||||||
|
}
|
||||||
|
|
||||||
|
syncVisual() {
|
||||||
|
if (!this.bodySprite?.body || !this.visual) return;
|
||||||
|
this.visual.setPosition(this.bodySprite.x, this.bodySprite.body.bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
finish() {
|
||||||
|
if (!this.bodySprite?.body) return;
|
||||||
|
|
||||||
|
this.syncVisual();
|
||||||
|
this.bodySprite.setAcceleration(0, 0);
|
||||||
|
this.bodySprite.setVelocity(0, 0);
|
||||||
|
this.bodySprite.body.allowGravity = false;
|
||||||
|
this.bodySprite.body.enable = false;
|
||||||
|
this.visual.setAngle(0);
|
||||||
|
this.visual.setTint(0xfff2bd);
|
||||||
|
}
|
||||||
|
}
|
||||||
349
src/main.js
349
src/main.js
@ -1,18 +1,7 @@
|
|||||||
import Phaser from 'phaser';
|
import Phaser from 'phaser';
|
||||||
import './style.css';
|
import './style.css';
|
||||||
|
import { PlayerController } from './PlayerController.js';
|
||||||
const WORLD_WIDTH = 5600;
|
import { WORLD, GROUND_ART, BRANCH_POSITIONS, getLogGeometry } from './worldConfig.js';
|
||||||
const WORLD_HEIGHT = 720;
|
|
||||||
const GROUND_Y = 628;
|
|
||||||
const GROUND_SURFACE_Y = GROUND_Y - 4;
|
|
||||||
const GROUND_ART_SCALE = 0.98;
|
|
||||||
const GROUND_ART_SURFACE_OFFSET = 20;
|
|
||||||
const LOG_FRAME_HEIGHT = 79;
|
|
||||||
const LOG_ART_SURFACE_OFFSET = 17;
|
|
||||||
const LOG_COLLIDER_HEIGHT = 16;
|
|
||||||
const LOG_SURFACE_LEFT = 15;
|
|
||||||
const LOG_SURFACE_RIGHT = 266;
|
|
||||||
const LOG_FRAME_WIDTH = 269;
|
|
||||||
|
|
||||||
class ForestScene extends Phaser.Scene {
|
class ForestScene extends Phaser.Scene {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -22,8 +11,6 @@ class ForestScene extends Phaser.Scene {
|
|||||||
this.totalMushrooms = 5;
|
this.totalMushrooms = 5;
|
||||||
this.touch = { left: false, right: false };
|
this.touch = { left: false, right: false };
|
||||||
this.touchPointers = { left: new Set(), right: new Set() };
|
this.touchPointers = { left: new Set(), right: new Set() };
|
||||||
this.wasGrounded = false;
|
|
||||||
this.landing = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
preload() {
|
preload() {
|
||||||
@ -54,18 +41,18 @@ class ForestScene extends Phaser.Scene {
|
|||||||
frameWidth: 280,
|
frameWidth: 280,
|
||||||
frameHeight: 220
|
frameHeight: 220
|
||||||
});
|
});
|
||||||
this.load.image('flora-sheet', '/assets/flora-sheet.png');
|
this.load.atlas('world-atlas', '/assets/world-atlas.png', '/assets/world-atlas.json');
|
||||||
this.load.image('fauna-sheet', '/assets/fauna-sheet.png');
|
|
||||||
this.load.image('terrain-sheet', '/assets/terrain-sheet.png');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
this.physics.world.setBounds(0, 0, WORLD_WIDTH, WORLD_HEIGHT);
|
window.__squirrelBooted = true;
|
||||||
this.cameras.main.setBounds(0, 0, WORLD_WIDTH, WORLD_HEIGHT);
|
document.getElementById('boot-error')?.remove();
|
||||||
|
|
||||||
|
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.cameras.main.setBackgroundColor('#8fc7a2');
|
||||||
|
|
||||||
this.createTextures();
|
this.createTextures();
|
||||||
this.createArtFrames();
|
|
||||||
this.createAnimations();
|
this.createAnimations();
|
||||||
this.createBackdrop();
|
this.createBackdrop();
|
||||||
this.createWorld();
|
this.createWorld();
|
||||||
@ -79,9 +66,19 @@ class ForestScene extends Phaser.Scene {
|
|||||||
this.cameras.main.setDeadzone(180, 110);
|
this.cameras.main.setDeadzone(180, 110);
|
||||||
this.cameras.main.fadeIn(700, 16, 37, 29);
|
this.cameras.main.fadeIn(700, 16, 37, 29);
|
||||||
|
|
||||||
this.physics.add.collider(this.player, this.platforms);
|
this.physics.add.collider(this.player, this.groundPlatforms);
|
||||||
this.physics.add.collider(this.acornGroup, this.platforms);
|
this.physics.add.collider(
|
||||||
this.physics.add.collider(this.mushroomGroup, this.platforms);
|
this.player,
|
||||||
|
this.oneWayPlatforms,
|
||||||
|
undefined,
|
||||||
|
this.shouldCollideWithOneWayPlatform,
|
||||||
|
this
|
||||||
|
);
|
||||||
|
|
||||||
|
this.physics.add.collider(this.acornGroup, this.groundPlatforms);
|
||||||
|
this.physics.add.collider(this.acornGroup, this.oneWayPlatforms);
|
||||||
|
this.physics.add.collider(this.mushroomGroup, this.groundPlatforms);
|
||||||
|
this.physics.add.collider(this.mushroomGroup, this.oneWayPlatforms);
|
||||||
|
|
||||||
this.physics.add.overlap(this.player, this.acornGroup, (_, acorn) => {
|
this.physics.add.overlap(this.player, this.acornGroup, (_, acorn) => {
|
||||||
acorn.disableBody(true, true);
|
acorn.disableBody(true, true);
|
||||||
@ -110,49 +107,12 @@ class ForestScene extends Phaser.Scene {
|
|||||||
this.physics.add.overlap(this.player, this.groveTrigger, () => {
|
this.physics.add.overlap(this.player, this.groveTrigger, () => {
|
||||||
if (this.mushrooms < this.totalMushrooms || this.finished) return;
|
if (this.mushrooms < this.totalMushrooms || this.finished) return;
|
||||||
this.finished = true;
|
this.finished = true;
|
||||||
this.player.setVelocityX(0);
|
this.playerController.finish();
|
||||||
this.playerVisual.setTint(0xfff2bd);
|
|
||||||
this.showEnding();
|
this.showEnding();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
createArtFrames() {
|
|
||||||
const addFrame = (textureKey, name, x, y, width, height) => {
|
|
||||||
this.textures.get(textureKey).add(name, 0, x, y, width, height);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
addFrame('terrain-sheet', 'ground-main', 24, 32, 341, 107);
|
|
||||||
// Interior-only repeat crop: removes the standalone tile's visible
|
|
||||||
// left/right border so adjacent ground pieces join cleanly.
|
|
||||||
addFrame('terrain-sheet', 'ground-repeat', 52, 32, 285, 107);
|
|
||||||
addFrame('terrain-sheet', 'log-platform', 21, 327, 269, 79);
|
|
||||||
addFrame('terrain-sheet', 'root-platform', 1023, 672, 402, 168);
|
|
||||||
|
|
||||||
addFrame('flora-sheet', 'bush', 16, 28, 244, 149);
|
|
||||||
addFrame('flora-sheet', 'fern-cluster', 659, 19, 169, 154);
|
|
||||||
addFrame('flora-sheet', 'bluebells', 15, 180, 100, 131);
|
|
||||||
addFrame('flora-sheet', 'mush-red', 14, 321, 168, 200);
|
|
||||||
addFrame('flora-sheet', 'mush-brown', 388, 328, 195, 195);
|
|
||||||
addFrame('flora-sheet', 'mush-orange', 560, 335, 117, 184);
|
|
||||||
addFrame('flora-sheet', 'mush-purple', 685, 331, 114, 188);
|
|
||||||
addFrame('flora-sheet', 'mush-blue', 1088, 336, 149, 186);
|
|
||||||
addFrame('flora-sheet', 'acorn-art', 178, 567, 58, 71);
|
|
||||||
|
|
||||||
const frogFrames = [
|
|
||||||
[72, 538, 99, 88], [231, 537, 105, 89], [422, 535, 155, 96],
|
|
||||||
[594, 505, 169, 95], [845, 547, 118, 79]
|
|
||||||
];
|
|
||||||
frogFrames.forEach(([x, y, w, h], i) => addFrame('fauna-sheet', `frog${i}`, x, y, w, h));
|
|
||||||
|
|
||||||
const hedgehogFrames = [
|
|
||||||
[18, 66, 139, 106], [169, 64, 131, 108], [310, 63, 131, 105],
|
|
||||||
[449, 66, 129, 107], [592, 57, 120, 115], [729, 67, 105, 105]
|
|
||||||
];
|
|
||||||
hedgehogFrames.forEach(([x, y, w, h], i) => addFrame('fauna-sheet', `hedgehog${i}`, x, y, w, h));
|
|
||||||
}
|
|
||||||
|
|
||||||
createAnimations() {
|
createAnimations() {
|
||||||
const frames = (texture, names) => names.map(frame => ({ key: texture, frame }));
|
const frames = (texture, names) => names.map(frame => ({ key: texture, frame }));
|
||||||
|
|
||||||
@ -193,7 +153,7 @@ class ForestScene extends Phaser.Scene {
|
|||||||
|
|
||||||
this.anims.create({
|
this.anims.create({
|
||||||
key: 'frog-hop',
|
key: 'frog-hop',
|
||||||
frames: frames('fauna-sheet', ['frog0', 'frog1', 'frog2', 'frog3', 'frog4']),
|
frames: frames('world-atlas', ['frog0', 'frog1', 'frog2', 'frog3', 'frog4']),
|
||||||
frameRate: 7,
|
frameRate: 7,
|
||||||
repeat: -1,
|
repeat: -1,
|
||||||
repeatDelay: 1400
|
repeatDelay: 1400
|
||||||
@ -201,7 +161,7 @@ class ForestScene extends Phaser.Scene {
|
|||||||
|
|
||||||
this.anims.create({
|
this.anims.create({
|
||||||
key: 'hedgehog-walk',
|
key: 'hedgehog-walk',
|
||||||
frames: frames('fauna-sheet', ['hedgehog0', 'hedgehog1', 'hedgehog2', 'hedgehog3', 'hedgehog4', 'hedgehog5']),
|
frames: frames('world-atlas', ['hedgehog0', 'hedgehog1', 'hedgehog2', 'hedgehog3', 'hedgehog4', 'hedgehog5']),
|
||||||
frameRate: 8,
|
frameRate: 8,
|
||||||
repeat: -1
|
repeat: -1
|
||||||
});
|
});
|
||||||
@ -210,95 +170,10 @@ class ForestScene extends Phaser.Scene {
|
|||||||
createTextures() {
|
createTextures() {
|
||||||
const g = this.make.graphics({ add: false });
|
const g = this.make.graphics({ add: false });
|
||||||
|
|
||||||
g.clear();
|
|
||||||
g.fillStyle(0xffffff, 1);
|
g.fillStyle(0xffffff, 1);
|
||||||
g.fillRect(0, 0, 44, 72);
|
g.fillRect(0, 0, 44, 72);
|
||||||
g.generateTexture('player-body', 44, 72);
|
g.generateTexture('player-body', 44, 72);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
g.fillStyle(0x6aa56d, 1);
|
|
||||||
for (let i = 0; i < 4; i++) {
|
|
||||||
g.fillEllipse(12 + i * 6, 25 - (i % 2) * 6, 7, 19);
|
|
||||||
}
|
|
||||||
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.clear();
|
||||||
g.fillStyle(0x6c4429, 1);
|
g.fillStyle(0x6c4429, 1);
|
||||||
g.fillRoundedRect(32, 0, 28, 120, 10);
|
g.fillRoundedRect(32, 0, 28, 120, 10);
|
||||||
@ -312,13 +187,6 @@ class ForestScene extends Phaser.Scene {
|
|||||||
g.fillCircle(55, 50, 28);
|
g.fillCircle(55, 50, 28);
|
||||||
g.generateTexture('tree', 92, 120);
|
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.clear();
|
||||||
g.fillStyle(0xb4d9f2, 0.9);
|
g.fillStyle(0xb4d9f2, 0.9);
|
||||||
g.fillEllipse(8, 8, 12, 7);
|
g.fillEllipse(8, 8, 12, 7);
|
||||||
@ -334,117 +202,87 @@ class ForestScene extends Phaser.Scene {
|
|||||||
g.fillCircle(6, 6, 6);
|
g.fillCircle(6, 6, 6);
|
||||||
g.generateTexture('firefly', 12, 12);
|
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();
|
g.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
createBackdrop() {
|
createBackdrop() {
|
||||||
this.add.rectangle(WORLD_WIDTH / 2, 270, WORLD_WIDTH, 540, 0x8fc7a2).setScrollFactor(0.05);
|
this.add.rectangle(WORLD.width / 2, 270, WORLD.width, 540, 0x8fc7a2).setScrollFactor(0.05);
|
||||||
|
|
||||||
const far = this.add.graphics().setScrollFactor(0.12);
|
const far = this.add.graphics().setScrollFactor(0.12);
|
||||||
far.fillStyle(0x719f78, 1);
|
far.fillStyle(0x719f78, 1);
|
||||||
for (let x = -100; x < WORLD_WIDTH + 300; x += 170) {
|
for (let x = -100; x < WORLD.width + 300; x += 170) {
|
||||||
far.fillCircle(x, 350 + (x % 3) * 8, 180);
|
far.fillCircle(x, 350 + (x % 3) * 8, 180);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mist = this.add.graphics().setScrollFactor(0.22);
|
const mist = this.add.graphics().setScrollFactor(0.22);
|
||||||
mist.fillStyle(0xd9ead6, 0.28);
|
mist.fillStyle(0xd9ead6, 0.28);
|
||||||
for (let x = 0; x < WORLD_WIDTH; x += 500) {
|
for (let x = 0; x < WORLD.width; x += 500) {
|
||||||
mist.fillEllipse(x, 480 + (x % 2) * 20, 520, 120);
|
mist.fillEllipse(x, 480 + (x % 2) * 20, 520, 120);
|
||||||
}
|
}
|
||||||
|
|
||||||
const canopy = this.add.graphics().setScrollFactor(0.35);
|
const canopy = this.add.graphics().setScrollFactor(0.35);
|
||||||
canopy.fillStyle(0x315f45, 0.95);
|
canopy.fillStyle(0x315f45, 0.95);
|
||||||
for (let x = -120; x < WORLD_WIDTH + 300; x += 150) {
|
for (let x = -120; x < WORLD.width + 300; x += 150) {
|
||||||
canopy.fillCircle(x, 140 + ((x / 150) % 3) * 28, 110);
|
canopy.fillCircle(x, 140 + ((x / 150) % 3) * 28, 110);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let x = 200; x < WORLD_WIDTH; x += 720) {
|
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);
|
this.add.image(x, 455, 'tree').setScale(2.5).setAlpha(0.58).setScrollFactor(0.55).setOrigin(0.5, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createWorld() {
|
createWorld() {
|
||||||
this.platforms = this.physics.add.staticGroup();
|
this.groundPlatforms = this.physics.add.staticGroup();
|
||||||
|
this.oneWayPlatforms = this.physics.add.staticGroup();
|
||||||
|
|
||||||
// Keep the physics plane and the illustrated grass line on the exact same Y.
|
// Keep the physics plane and the illustrated grass line on the exact same Y.
|
||||||
// In ground-main the continuous walkable grass line is 20 source pixels
|
// In ground-main the continuous walkable grass line is 20 source pixels
|
||||||
// below the cropped frame top.
|
// below the cropped frame top.
|
||||||
const ground = this.add.rectangle(
|
const ground = this.add.rectangle(
|
||||||
WORLD_WIDTH / 2,
|
WORLD.width / 2,
|
||||||
GROUND_SURFACE_Y + 50,
|
WORLD.groundSurfaceY + 50,
|
||||||
WORLD_WIDTH,
|
WORLD.width,
|
||||||
100,
|
100,
|
||||||
0x263c2d,
|
0x263c2d,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
this.physics.add.existing(ground, true);
|
this.physics.add.existing(ground, true);
|
||||||
this.platforms.add(ground);
|
this.groundPlatforms.add(ground);
|
||||||
|
|
||||||
const groundArtY = GROUND_SURFACE_Y - (GROUND_ART_SURFACE_OFFSET * GROUND_ART_SCALE);
|
const groundArtY = WORLD.groundSurfaceY - (GROUND_ART.surfaceOffset * GROUND_ART.scale);
|
||||||
const groundTileWidth = 285 * GROUND_ART_SCALE;
|
const groundTileWidth = GROUND_ART.repeatFrameWidth * GROUND_ART.scale;
|
||||||
const groundTileStep = groundTileWidth - 3; // tiny overlap avoids antialias hairlines
|
const groundTileStep = groundTileWidth - GROUND_ART.overlap; // tiny overlap avoids antialias hairlines
|
||||||
for (let x = groundTileWidth / 2; x < WORLD_WIDTH + groundTileWidth; x += groundTileStep) {
|
for (let x = groundTileWidth / 2; x < WORLD.width + groundTileWidth; x += groundTileStep) {
|
||||||
this.add.image(x, groundArtY, 'terrain-sheet', 'ground-repeat')
|
this.add.image(x, groundArtY, 'world-atlas', 'ground-repeat')
|
||||||
.setOrigin(0.5, 0)
|
.setOrigin(0.5, 0)
|
||||||
.setScale(GROUND_ART_SCALE)
|
.setScale(GROUND_ART.scale)
|
||||||
.setDepth(3);
|
.setDepth(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
const branchPositions = [
|
BRANCH_POSITIONS.forEach(([x, surfaceY], index) => {
|
||||||
[500, 545], [720, 505], [900, 465],
|
|
||||||
[1260, 545], [1480, 505], [1680, 465],
|
|
||||||
[2050, 545], [2260, 505], [2470, 465],
|
|
||||||
[2900, 545], [3140, 505], [3370, 465],
|
|
||||||
[3800, 545], [4010, 505], [4230, 465],
|
|
||||||
[4680, 545], [4890, 505], [5100, 465]
|
|
||||||
];
|
|
||||||
|
|
||||||
// branchPositions store the intended walkable surface Y, not the
|
|
||||||
// collider centre. The illustrated log's usable top edge is 17 source
|
|
||||||
// pixels below its cropped frame top, so derive art + physics from the
|
|
||||||
// same surface coordinate.
|
|
||||||
branchPositions.forEach(([x, surfaceY], index) => {
|
|
||||||
const scale = index % 3 === 0 ? 0.62 : 0.55;
|
const scale = index % 3 === 0 ? 0.62 : 0.55;
|
||||||
const visualY = surfaceY + ((LOG_FRAME_HEIGHT / 2 - LOG_ART_SURFACE_OFFSET) * scale);
|
const geometry = getLogGeometry(x, surfaceY, scale);
|
||||||
|
|
||||||
const visual = this.add.image(x, visualY, 'terrain-sheet', 'log-platform')
|
this.add.image(x, geometry.visualY, 'world-atlas', 'log-platform')
|
||||||
.setScale(scale)
|
.setScale(scale)
|
||||||
.setDepth(6);
|
.setDepth(6);
|
||||||
|
|
||||||
const surfaceWidth = (LOG_SURFACE_RIGHT - LOG_SURFACE_LEFT) * scale;
|
|
||||||
// The visible moss cap is biased left relative to the cropped log frame.
|
|
||||||
// Keep the corrected surface width, but mirror the centre correction so
|
|
||||||
// the collider follows the artwork rather than drifting right.
|
|
||||||
const surfaceCenterOffset = -(
|
|
||||||
((LOG_SURFACE_LEFT + LOG_SURFACE_RIGHT) / 2) - (LOG_FRAME_WIDTH / 2)
|
|
||||||
) * scale;
|
|
||||||
|
|
||||||
const collider = this.add.rectangle(
|
const collider = this.add.rectangle(
|
||||||
x + surfaceCenterOffset,
|
geometry.colliderX,
|
||||||
surfaceY + (LOG_COLLIDER_HEIGHT / 2),
|
geometry.colliderY,
|
||||||
surfaceWidth,
|
geometry.colliderWidth,
|
||||||
LOG_COLLIDER_HEIGHT,
|
geometry.colliderHeight,
|
||||||
0x000000,
|
0x000000,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
this.physics.add.existing(collider, true);
|
this.physics.add.existing(collider, true);
|
||||||
this.platforms.add(collider);
|
this.oneWayPlatforms.add(collider);
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let x = 120; x < WORLD_WIDTH - 120; x += 180) {
|
for (let x = 120; x < WORLD.width - 120; x += 180) {
|
||||||
const n = Math.sin(x * 0.013) + Math.cos(x * 0.021);
|
const n = Math.sin(x * 0.013) + Math.cos(x * 0.021);
|
||||||
if (n > -0.3) {
|
if (n > -0.3) {
|
||||||
this.add.image(x, GROUND_Y - 2, 'flora-sheet', 'fern-cluster')
|
this.add.image(x, WORLD.groundSurfaceY - 2, 'world-atlas', 'fern-cluster')
|
||||||
.setOrigin(0.5, 1)
|
.setOrigin(0.5, 1)
|
||||||
.setScale(0.2 + ((x % 90) / 900))
|
.setScale(0.2 + ((x % 90) / 900))
|
||||||
.setAlpha(0.96)
|
.setAlpha(0.96)
|
||||||
@ -452,14 +290,14 @@ class ForestScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (x % 540 < 190) {
|
if (x % 540 < 190) {
|
||||||
this.add.image(x + 65, GROUND_Y - 2, 'flora-sheet', 'bluebells')
|
this.add.image(x + 65, WORLD.groundSurfaceY - 2, 'world-atlas', 'bluebells')
|
||||||
.setOrigin(0.5, 1)
|
.setOrigin(0.5, 1)
|
||||||
.setScale(0.2)
|
.setScale(0.2)
|
||||||
.setDepth(5);
|
.setDepth(5);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (x % 900 < 190) {
|
if (x % 900 < 190) {
|
||||||
this.add.image(x - 65, GROUND_Y - 3, 'flora-sheet', 'bush')
|
this.add.image(x - 65, WORLD.groundSurfaceY - 3, 'world-atlas', 'bush')
|
||||||
.setOrigin(0.5, 1)
|
.setOrigin(0.5, 1)
|
||||||
.setScale(0.34)
|
.setScale(0.34)
|
||||||
.setDepth(4);
|
.setDepth(4);
|
||||||
@ -491,28 +329,20 @@ class ForestScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createPlayer() {
|
createPlayer() {
|
||||||
// Keep physics independent from animation artwork. The body is a simple,
|
this.playerController = new PlayerController(this);
|
||||||
// stable capsule-like rectangle while the squirrel art follows it.
|
this.player = this.playerController.create(160, 520);
|
||||||
this.player = this.physics.add.sprite(160, 520, 'player-body');
|
this.playerVisual = this.playerController.visual;
|
||||||
this.player.setAlpha(0);
|
|
||||||
this.player.setCollideWorldBounds(true);
|
|
||||||
this.player.setDragX(900);
|
|
||||||
this.player.setMaxVelocity(330, 760);
|
|
||||||
this.player.body.setSize(40, 68, true);
|
|
||||||
|
|
||||||
this.playerVisual = this.add.sprite(160, 520, 'player-game', 0);
|
|
||||||
this.playerVisual.setScale(0.42);
|
|
||||||
// Every normalized player frame has its paw baseline at source y=209.
|
|
||||||
// Pin that exact baseline to the physics body's bottom.
|
|
||||||
this.playerVisual.setOrigin(0.5, 209 / 220);
|
|
||||||
this.playerVisual.setDepth(12);
|
|
||||||
this.playerVisual.play('squirrel-idle');
|
|
||||||
this.syncPlayerVisual();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
syncPlayerVisual() {
|
shouldCollideWithOneWayPlatform(player, platform) {
|
||||||
if (!this.player?.body || !this.playerVisual) return;
|
if (!player?.body || !platform?.body) return false;
|
||||||
this.playerVisual.setPosition(this.player.x, this.player.body.bottom);
|
if (player.body.velocity.y < 0) return false;
|
||||||
|
|
||||||
|
const previousBottom = player.body.prev?.y !== undefined
|
||||||
|
? player.body.prev.y + player.body.height
|
||||||
|
: player.body.bottom;
|
||||||
|
|
||||||
|
return previousBottom <= platform.body.top + 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
createCollectibles() {
|
createCollectibles() {
|
||||||
@ -524,7 +354,7 @@ class ForestScene extends Phaser.Scene {
|
|||||||
[3370, 345], [3610, 555], [3800, 440], [4050, 555], [4230, 365],
|
[3370, 345], [3610, 555], [3800, 440], [4050, 555], [4230, 365],
|
||||||
[4470, 555], [4680, 460], [4910, 555], [5100, 370]
|
[4470, 555], [4680, 460], [4910, 555], [5100, 370]
|
||||||
].forEach(([x, y], i) => {
|
].forEach(([x, y], i) => {
|
||||||
const acorn = this.acornGroup.create(x, y, 'flora-sheet', 'acorn-art');
|
const acorn = this.acornGroup.create(x, y, 'world-atlas', 'acorn-art');
|
||||||
acorn.setScale(0.62);
|
acorn.setScale(0.62);
|
||||||
acorn.setBounce(0.2);
|
acorn.setBounce(0.2);
|
||||||
acorn.body.setSize(38, 48, true);
|
acorn.body.setSize(38, 48, true);
|
||||||
@ -541,7 +371,7 @@ class ForestScene extends Phaser.Scene {
|
|||||||
];
|
];
|
||||||
|
|
||||||
mushrooms.forEach(([x, y, frame, name], index) => {
|
mushrooms.forEach(([x, y, frame, name], index) => {
|
||||||
const mushroom = this.mushroomGroup.create(x, y, 'flora-sheet', frame);
|
const mushroom = this.mushroomGroup.create(x, y, 'world-atlas', frame);
|
||||||
const scale = frame === 'mush-blue' ? 0.3 : 0.28;
|
const scale = frame === 'mush-blue' ? 0.3 : 0.28;
|
||||||
mushroom.setScale(scale);
|
mushroom.setScale(scale);
|
||||||
mushroom.setData('name', name);
|
mushroom.setData('name', name);
|
||||||
@ -578,7 +408,7 @@ class ForestScene extends Phaser.Scene {
|
|||||||
});
|
});
|
||||||
|
|
||||||
[1120, 2600, 3980].forEach((x, i) => {
|
[1120, 2600, 3980].forEach((x, i) => {
|
||||||
const frog = this.add.sprite(x, GROUND_Y - 2, 'fauna-sheet', 'frog0')
|
const frog = this.add.sprite(x, WORLD.groundSurfaceY - 2, 'world-atlas', 'frog0')
|
||||||
.setOrigin(0.5, 1)
|
.setOrigin(0.5, 1)
|
||||||
.setScale(0.32)
|
.setScale(0.32)
|
||||||
.setDepth(7);
|
.setDepth(7);
|
||||||
@ -587,7 +417,7 @@ class ForestScene extends Phaser.Scene {
|
|||||||
});
|
});
|
||||||
|
|
||||||
[1450, 3450].forEach((x, i) => {
|
[1450, 3450].forEach((x, i) => {
|
||||||
const hedgehog = this.add.sprite(x, GROUND_Y - 3, 'fauna-sheet', 'hedgehog0')
|
const hedgehog = this.add.sprite(x, WORLD.groundSurfaceY - 3, 'world-atlas', 'hedgehog0')
|
||||||
.setOrigin(0.5, 1)
|
.setOrigin(0.5, 1)
|
||||||
.setScale(0.34)
|
.setScale(0.34)
|
||||||
.setDepth(7)
|
.setDepth(7)
|
||||||
@ -736,11 +566,7 @@ class ForestScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tryJump() {
|
tryJump() {
|
||||||
if (!this.player?.body) return;
|
this.playerController?.tryJump();
|
||||||
if (this.player.body.blocked.down || this.player.body.touching.down) {
|
|
||||||
this.player.setVelocityY(-520);
|
|
||||||
this.playerVisual.play('squirrel-jump', true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
showDiscovery(name) {
|
showDiscovery(name) {
|
||||||
@ -808,45 +634,16 @@ class ForestScene extends Phaser.Scene {
|
|||||||
|
|
||||||
if (left && !right) {
|
if (left && !right) {
|
||||||
this.player.setAccelerationX(-1050);
|
this.player.setAccelerationX(-1050);
|
||||||
this.playerVisual.setFlipX(true);
|
this.playerController.setFacing(-1);
|
||||||
} else if (right && !left) {
|
} else if (right && !left) {
|
||||||
this.player.setAccelerationX(1050);
|
this.player.setAccelerationX(1050);
|
||||||
this.playerVisual.setFlipX(false);
|
this.playerController.setFacing(1);
|
||||||
} else {
|
} else {
|
||||||
this.player.setAccelerationX(0);
|
this.player.setAccelerationX(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const grounded = this.player.body.blocked.down || this.player.body.touching.down;
|
const grounded = this.player.body.blocked.down || this.player.body.touching.down;
|
||||||
|
this.playerController.updateAnimation(grounded);
|
||||||
if (grounded && !this.wasGrounded) {
|
|
||||||
this.landing = true;
|
|
||||||
this.playerVisual.play('squirrel-land', true);
|
|
||||||
this.playerVisual.once('animationcomplete-squirrel-land', () => {
|
|
||||||
this.landing = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (grounded) {
|
|
||||||
this.playerVisual.setAngle(0);
|
|
||||||
if (!this.landing) {
|
|
||||||
if (Math.abs(this.player.body.velocity.x) > 35) {
|
|
||||||
this.playerVisual.play('squirrel-run', true);
|
|
||||||
} else {
|
|
||||||
this.playerVisual.play('squirrel-idle', true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (this.player.body.velocity.y < 0) {
|
|
||||||
this.landing = false;
|
|
||||||
this.player.play('squirrel-jump', true);
|
|
||||||
this.playerVisual.setAngle(Phaser.Math.Clamp(this.player.body.velocity.x * 0.012, -5, 5));
|
|
||||||
} else {
|
|
||||||
this.landing = false;
|
|
||||||
this.playerVisual.play('squirrel-fall', true);
|
|
||||||
this.playerVisual.setAngle(Phaser.Math.Clamp(this.player.body.velocity.x * 0.012, -5, 5));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.wasGrounded = grounded;
|
|
||||||
this.syncPlayerVisual();
|
|
||||||
|
|
||||||
this.acornGroup.children.iterate(acorn => {
|
this.acornGroup.children.iterate(acorn => {
|
||||||
if (!acorn?.active) return;
|
if (!acorn?.active) return;
|
||||||
|
|||||||
53
src/worldConfig.js
Normal file
53
src/worldConfig.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
export const WORLD = {
|
||||||
|
width: 5600,
|
||||||
|
height: 720,
|
||||||
|
groundSurfaceY: 624
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GROUND_ART = {
|
||||||
|
scale: 0.98,
|
||||||
|
surfaceOffset: 20,
|
||||||
|
repeatFrameWidth: 285,
|
||||||
|
overlap: 3
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LOG_PLATFORM = {
|
||||||
|
frameWidth: 269,
|
||||||
|
frameHeight: 79,
|
||||||
|
surfaceOffsetY: 17,
|
||||||
|
colliderHeight: 16,
|
||||||
|
surfaceLeft: 15,
|
||||||
|
surfaceRight: 266
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BRANCH_POSITIONS = [
|
||||||
|
[500, 545], [720, 505], [900, 465],
|
||||||
|
[1260, 545], [1480, 505], [1680, 465],
|
||||||
|
[2050, 545], [2260, 505], [2470, 465],
|
||||||
|
[2900, 545], [3140, 505], [3370, 465],
|
||||||
|
[3800, 545], [4010, 505], [4230, 465],
|
||||||
|
[4680, 545], [4890, 505], [5100, 465]
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getLogGeometry(x, surfaceY, scale) {
|
||||||
|
const visualY = surfaceY
|
||||||
|
+ ((LOG_PLATFORM.frameHeight / 2 - LOG_PLATFORM.surfaceOffsetY) * scale);
|
||||||
|
|
||||||
|
const colliderWidth = (
|
||||||
|
LOG_PLATFORM.surfaceRight - LOG_PLATFORM.surfaceLeft
|
||||||
|
) * scale;
|
||||||
|
|
||||||
|
const surfaceCenterOffset = -(
|
||||||
|
(
|
||||||
|
(LOG_PLATFORM.surfaceLeft + LOG_PLATFORM.surfaceRight) / 2
|
||||||
|
) - (LOG_PLATFORM.frameWidth / 2)
|
||||||
|
) * scale;
|
||||||
|
|
||||||
|
return {
|
||||||
|
visualY,
|
||||||
|
colliderX: x + surfaceCenterOffset,
|
||||||
|
colliderY: surfaceY + (LOG_PLATFORM.colliderHeight / 2),
|
||||||
|
colliderWidth,
|
||||||
|
colliderHeight: LOG_PLATFORM.colliderHeight
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user