deploy: mirror squirrel forest prototype

This commit is contained in:
Admin 2026-09-19 04:21:48 +00:00
parent bfc0d60ed3
commit 9cfc537b66
8 changed files with 737 additions and 2 deletions

5
.dockerignore Normal file
View File

@ -0,0 +1,5 @@
node_modules
dist
.git
.DS_Store
npm-debug.log

13
Dockerfile Normal file
View 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

View File

@ -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
View 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
View 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
View 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
View 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
View 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%;
}