final
352
README.md
Normal file
|
@ -0,0 +1,352 @@
|
|||
*Due to file size limitations, only the source code and README.md are included in the submitted zip file. Please pull the full repository from the link below to access all project files*
|
||||
|
||||
*http://git.prototyping.id/20230412/individual_project.git*
|
||||
|
||||
*If you do not have Markdown Preview Enhanced installed, the Mermaid diagrams may not be visible in the preview. In that case, please refer to the `README.pdf` file below.*
|
||||
|
||||
# Personal Information
|
||||
- **Name:** Chaebean Yang
|
||||
- **ID:** 20230412
|
||||
- **Email:** kazed0102@kaist.ac.kr
|
||||
|
||||
*If you do not have Markdown Preview Enhanced installed, the Mermaid diagrams may not be visible in the preview. In that case, please refer to the `README.pdf` file below.*
|
||||
|
||||
|
||||
|
||||
# Git Information
|
||||
- **Repository URL:** [http://git.prototyping.id/20230412/individual_project.git](http://git.prototyping.id/20230412/individual_project.git)
|
||||
|
||||
|
||||
|
||||
|
||||
# Demo Video
|
||||
- **Video URL:** [Game Demo Video](https://youtu.be/gW_LuNeC4WE)
|
||||
|
||||
|
||||
|
||||
|
||||
# Game Screens
|
||||
*Start Screen*
|
||||

|
||||
|
||||
*Game Play Screen*
|
||||

|
||||
|
||||
*Gameover Screen*
|
||||

|
||||
|
||||
*Game Clear Screen*
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
# Game Description
|
||||
|
||||
## Basic Information
|
||||
This game is based on the classic Snake game.
|
||||
To make the gameplay more engaging, we incorporated elements of Aztec mythology—specifically the story of Quetzalcoatl, the serpent-shaped god who becomes the fifth sun—through both graphics and narrative.
|
||||
Additionally, various items and more complex mechanics were introduced to increase difficulty and enhance player experience.
|
||||
|
||||
## How It Works
|
||||
- The user can control both the head and the tail.
|
||||
- Body length increases differently depending on the item collected.
|
||||
- Items provide different scores.
|
||||
- Speed increases as the score rises.
|
||||
- Obstacles appear randomly in the current direction of movement.
|
||||
|
||||
### Head Control
|
||||
The head is controlled using the arrow keys.
|
||||
|
||||
| Key | Effect |
|
||||
|-----|--------------------------|
|
||||
| ↑ | The head faces upward |
|
||||
| ↓ | The head faces downward |
|
||||
| ← | The head faces left |
|
||||
| → | The head faces right |
|
||||
|
||||
### Tail Control
|
||||
The tail is controlled using the W, A, S, and D keys.
|
||||
|
||||
| Key | Effect |
|
||||
|-----|--------------------------|
|
||||
| W | The tail faces upward |
|
||||
| S | The tail faces downward |
|
||||
| A | The tail faces left |
|
||||
| D | The tail faces right |
|
||||
|
||||
### Items
|
||||
|
||||
| Item | Score | Body Length Increase |
|
||||
|------------|-------|----------------------|
|
||||
| Sun | +15 | +3 |
|
||||
| Gemstone | +10 | +2 |
|
||||
| Heart | +5 | +1 |
|
||||
|
||||
### Speed
|
||||
|
||||
| Score Range | Speed Condition |
|
||||
|-------------|--------------------------|
|
||||
| < 70 | `p.frameCount % 7 === 0` |
|
||||
| < 90 | `p.frameCount % 5 === 0` |
|
||||
| ≥ 90 | `p.frameCount % 3 === 0` |
|
||||
|
||||
### Obstacles
|
||||
Obstacles appear in the direction the snake is currently moving.
|
||||
If the snake's head or tail collides with an obstacle, the player loses one life.
|
||||
|
||||
### Game Clear Condition
|
||||
- Score reaches 100 or more.
|
||||
|
||||
### Game Over Conditions
|
||||
- Life reaches 0.
|
||||
- Collision with walls.
|
||||
|
||||
|
||||
|
||||
|
||||
# Code Structure
|
||||
|
||||
The code is organized into two main parts: the Svelte frontend for UI/state management and the `sketch.js` file for game logic and rendering using p5.js.
|
||||
|
||||
## Components
|
||||
|
||||
### 1. `Game.svelte`
|
||||
- Manages the game's state (`start`, `play`, `clear`, `over`)
|
||||
- Contains the main UI layout, life/score display, and `<div id="game-canvas">` for the p5 sketch
|
||||
|
||||
---
|
||||
|
||||
- Handles `onMount()` logic to attach and remove the p5 instance dynamically
|
||||
|
||||
```javascript
|
||||
<script>
|
||||
onMount(() => {
|
||||
const preventArrowScroll = (e) => {
|
||||
const keys = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight",
|
||||
"w", "a", "s", "d", "W", "A", "S", "D"];
|
||||
if (keys.includes(e.key)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", preventArrowScroll, {passive: false});
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", preventArrowScroll);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
- Receives game data via `receiveGameData()` function and updates HUD
|
||||
|
||||
```javascript
|
||||
function receiveGameData(data) {
|
||||
life = data.life;
|
||||
score = data.score;
|
||||
if (data.state && data.state !== gameState && data.state !== "play") {
|
||||
gameState = data.state;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. `sketch.js`
|
||||
- Contains the `createSketch(p, onGameData)` function that defines the p5 sketch
|
||||
- Handles all rendering logic: drawing the snake, items, obstacles, and walls
|
||||
- Manages key logic: movement, collision, growth, scoring, speed change
|
||||
- Sends current score/life/game state to Svelte using the `onGameData()` callback
|
||||
- The snake uses an array to manage the head, body, tail, and corner images according to the situation, and to allow the body to grow in length.
|
||||
|
||||
#### Key Features in `sketch.js`
|
||||
|
||||
#### 1. Grid System & Basic Setup
|
||||
|
||||
- The canvas is divided into cells of size 20 using `cellSize`.
|
||||
- All coordinates are based on `{x, y}` positions in this grid system.
|
||||
|
||||
```javascript
|
||||
let cellSize = 20;
|
||||
p.createCanvas(1000, 520);
|
||||
p.imageMode(p.CENTER);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 2. Snake as an Array
|
||||
|
||||
- The snake is stored as an array of coordinates.
|
||||
- Movement is implemented using `unshift()` to add a new head and `pop()` to remove the tail.
|
||||
- When direction changes, a corner segment is drawn automatically.
|
||||
|
||||
```javascript
|
||||
let snake = [
|
||||
{x: 10, y: 5},
|
||||
{x: 9, y: 5},
|
||||
...
|
||||
];
|
||||
|
||||
snake.unshift(newHead);
|
||||
if (!growNext) snake.pop();
|
||||
```
|
||||
```javascript
|
||||
for (let i = 0; i < snake.length; i++) {
|
||||
const seg = snake[i];
|
||||
const px = seg.x * cellSize + cellSize/2;
|
||||
const py = seg.y * cellSize + cellSize/2;
|
||||
|
||||
p.push();
|
||||
p.translate(px, py);
|
||||
|
||||
if (i == 0) {
|
||||
// head dorection setting
|
||||
}
|
||||
else if (i == snake.length - 1) {
|
||||
// tail direction setting
|
||||
}
|
||||
else {
|
||||
//body parts and corner case
|
||||
const prev = snake[i - 1];
|
||||
const next = snake[i + 1];
|
||||
const dx1 = seg.x - prev.x;
|
||||
const dy1 = seg.y - prev.y;
|
||||
const dx2 = next.x - seg.x;
|
||||
const dy2 = next.y - seg.y;
|
||||
const isCorner = dx1 !== dx2 || dy1 !== dy2;
|
||||
|
||||
if (isCorner) {
|
||||
let angle = ...; // rotation angle logic
|
||||
p.rotate(angle);
|
||||
p.image(cornerImg, 0, 0, cellSize, cellSize);
|
||||
} else {
|
||||
const angle = dx1 == 0 ? p.HALF_PI : 0;
|
||||
p.rotate(angle);
|
||||
p.image(bodyImg, 0, 0, cellSize, cellSize);
|
||||
}
|
||||
}
|
||||
|
||||
p.pop();
|
||||
}
|
||||
```
|
||||
|
||||
- Segment rendering (head/body/corner/tail) is dynamically determined:
|
||||
|
||||
```javascript
|
||||
const isCorner = dx1 !== dx2 || dy1 !== dy2;
|
||||
if (isCorner) p.image(cornerImg, 0, 0, cellSize, cellSize);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 3. Obstacle Collision Logic
|
||||
|
||||
- Obstacles are randomly placed ahead of the snake's direction with spacing and jitter.
|
||||
- Snake colliding with an obstacle (head or tail) reduces life.
|
||||
|
||||
```javascript
|
||||
if ((head.x == o.x && head.y == o.y) || tail.x == o.x && tail.y == o.y) {
|
||||
if (o.hit == null) {
|
||||
life -= 1;
|
||||
o.hit = p.frameCount;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
function makeObstacle() {
|
||||
// moving head or tail? which direction?
|
||||
let base;
|
||||
let dir;
|
||||
|
||||
if (dirHead.x !==0 || dirHead.y !== 0) {
|
||||
base = snake[0];
|
||||
dir = dirHead;
|
||||
}
|
||||
else if (dirTail.x !== 0 || dirTail.y !== 0){
|
||||
base = snake[snake.length - 1];
|
||||
dir = dirTail;
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
...;
|
||||
//gap between the snake and an obstacle
|
||||
const gap = p.floor(p.random(3, 7));
|
||||
let newX = base.x + dir.x * gap + p.floor(p.random(-1, 2));
|
||||
let newY = base.y + dir.y * gap + p.floor(p.random(-1, 2));
|
||||
...;
|
||||
}
|
||||
```
|
||||
|
||||
- Obstacles flash and disappear after impact.
|
||||
|
||||
---
|
||||
|
||||
#### 4. Item Collection System
|
||||
|
||||
- Items (`heart`, `gemstone`, `sun`) are generated at random, non-overlapping grid locations.
|
||||
- Each item provides different score and body growth effects.
|
||||
|
||||
```javascript
|
||||
if (item.type == "gemstone") {
|
||||
score += 10;
|
||||
growNext += 2;
|
||||
}
|
||||
```
|
||||
|
||||
- Items are rendered with different images and refreshed automatically.
|
||||
|
||||
---
|
||||
|
||||
### Interaction Diagram
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
UserInput -->|"Arrow Keys / WASD"| p5Sketch
|
||||
p5Sketch --> GameLogic
|
||||
GameLogic --> Rendering
|
||||
GameLogic -->|"onGameData()"| GameSvelte
|
||||
GameSvelte --> HUD
|
||||
```
|
||||
|
||||
### Flow Chart
|
||||
```mermaid
|
||||
graph TD
|
||||
StartScreen -->|start| PlayScreen
|
||||
GameOverScreen -->|retry| PlayScreen
|
||||
GameOverScreen -->|back to start| StartScreen
|
||||
GameClearScreen -->|try again| StartScreen
|
||||
```
|
||||
|
||||
|
||||
|
||||
# Challenges
|
||||
- I attempted to include background music based on the game state, but it could not be implemented due to browser restrictions.
|
||||
|
||||
- When rendering the corner segments in the snake array, the rotation did not work as expected using theoretically calculated angles; I had to adjust the values empirically for it to display correctly.
|
||||
|
||||
|
||||
|
||||
# References
|
||||
- [Mermaid.js documentation](https://mermaid.js.org/)
|
||||
- [p5.js reference](https://p5js.org/ko/reference/)
|
||||
- [Svelte official tutorial](https://svelte.dev/tutorial)
|
||||
- [MDN: Styling the content](https://developer.mozilla.org/ko/docs/Learn_web_development/Getting_started/Your_first_website/Styling_the_content)
|
||||
- [Svelte: Reactive statements](https://svelte.dev/docs/svelte/legacy-reactive-assignments)
|
||||
- [Svelte: If-else statement](https://svelte.dev/docs/svelte/if)
|
||||
- [MDN: `Array.unshift()` method](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/unshift)
|
||||
- [StackOverflow: Prevent arrow keys from scrolling in Svelte](https://stackoverflow.com/questions/58534362/prevent-focused-div-from-scrolling-with-arrow-keys)
|
||||
- Parts of this README were grammar-checked using ChatGPT and Google translator.
|
||||
|
||||
|
||||
|
||||
|
||||
# Future Imporvements
|
||||
If I had more time, I would like to explore the following features:
|
||||
|
||||
- Adding background music to enhance the atmosphere of the game.
|
||||
- Changing the appearance of the snake dynamically based on the score.
|
||||
- Making the canvas layout responsive, so that it automatically adjusts to different screen sizes. *(Responsive design)*
|
BIN
README.pdf
Normal file
4
sveltep5play/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
/node_modules/
|
||||
/public/build/
|
||||
|
||||
.DS_Store
|
1704
sveltep5play/package-lock.json
generated
Normal file
27
sveltep5play/package.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"name": "svelte-app",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
"dev": "rollup -c -w",
|
||||
"start": "sirv public --no-clear"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^24.0.0",
|
||||
"@rollup/plugin-node-resolve": "^15.0.0",
|
||||
"@rollup/plugin-terser": "^0.4.0",
|
||||
"rollup": "^3.15.0",
|
||||
"rollup-plugin-css-only": "^4.3.0",
|
||||
"rollup-plugin-livereload": "^2.0.0",
|
||||
"rollup-plugin-svelte": "^7.1.2",
|
||||
"svelte": "^3.55.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"p5": "^1.6.0",
|
||||
"p5-svelte": "^3.1.2",
|
||||
"p5play": "^3.8.14",
|
||||
"sirv-cli": "^2.0.0"
|
||||
}
|
||||
}
|
BIN
sveltep5play/public/assets/gameclear.png
Normal file
After Width: | Height: | Size: 73 KiB |
BIN
sveltep5play/public/assets/gameover.png
Normal file
After Width: | Height: | Size: 55 KiB |
BIN
sveltep5play/public/assets/items/gemstone.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
sveltep5play/public/assets/items/heart.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
sveltep5play/public/assets/items/sun.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
sveltep5play/public/assets/obstacle.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
sveltep5play/public/assets/quetzalcoatl.png
Normal file
After Width: | Height: | Size: 8.5 KiB |
BIN
sveltep5play/public/assets/screens/gameClearScreen.png
Normal file
After Width: | Height: | Size: 140 KiB |
BIN
sveltep5play/public/assets/screens/gamePlayScreen.png
Normal file
After Width: | Height: | Size: 146 KiB |
BIN
sveltep5play/public/assets/screens/gameoverScreen.png
Normal file
After Width: | Height: | Size: 151 KiB |
BIN
sveltep5play/public/assets/screens/startScreen.png
Normal file
After Width: | Height: | Size: 81 KiB |
BIN
sveltep5play/public/assets/segments/body.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
sveltep5play/public/assets/segments/corner.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
sveltep5play/public/assets/segments/head.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
sveltep5play/public/assets/segments/tail.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
sveltep5play/public/font/Aztec.ttf
Normal file
15
sveltep5play/public/global.css
Normal file
|
@ -0,0 +1,15 @@
|
|||
html,
|
||||
body {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
color: #333;
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||
Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
|
||||
}
|
25
sveltep5play/public/index.html
Normal file
|
@ -0,0 +1,25 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<meta name='viewport' content='width=device-width,initial-scale=1'>
|
||||
|
||||
<title>Svelte app</title>
|
||||
|
||||
<link rel='icon' type='image/png' href='/favicon.png'>
|
||||
<link rel='stylesheet' href='/global.css'>
|
||||
<link rel='stylesheet' href='/build/bundle.css'>
|
||||
|
||||
<!-- p5 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/p5@1/lib/p5.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/p5@1/lib/addons/p5.sound.min.js"></script>
|
||||
<!-- p5play -->
|
||||
<script src="https://p5play.org/v3/planck.min.js"></script>
|
||||
<script src="https://p5play.org/v3/p5play.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script defer src='/build/bundle.js'></script>
|
||||
</body </html>
|
79
sveltep5play/rollup.config.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
import { spawn } from 'child_process';
|
||||
import svelte from 'rollup-plugin-svelte';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import terser from '@rollup/plugin-terser';
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import livereload from 'rollup-plugin-livereload';
|
||||
import css from 'rollup-plugin-css-only';
|
||||
|
||||
const production = !process.env.ROLLUP_WATCH;
|
||||
|
||||
function serve() {
|
||||
let server;
|
||||
|
||||
function toExit() {
|
||||
if (server) server.kill(0);
|
||||
}
|
||||
|
||||
return {
|
||||
writeBundle() {
|
||||
if (server) return;
|
||||
server = spawn('npm', ['run', 'start', '--', '--dev'], {
|
||||
stdio: ['ignore', 'inherit', 'inherit'],
|
||||
shell: true,
|
||||
});
|
||||
|
||||
process.on('SIGTERM', toExit);
|
||||
process.on('exit', toExit);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
input: 'src/main.js',
|
||||
output: {
|
||||
sourcemap: true,
|
||||
format: 'iife',
|
||||
name: 'app',
|
||||
file: 'public/build/bundle.js',
|
||||
inlineDynamicImports: true,
|
||||
},
|
||||
plugins: [
|
||||
svelte({
|
||||
compilerOptions: {
|
||||
// enable run-time checks when not in production
|
||||
dev: !production,
|
||||
},
|
||||
}),
|
||||
// we'll extract any component CSS out into
|
||||
// a separate file - better for performance
|
||||
css({ output: 'bundle.css' }),
|
||||
|
||||
// If you have external dependencies installed from
|
||||
// npm, you'll most likely need these plugins. In
|
||||
// some cases you'll need additional configuration -
|
||||
// consult the documentation for details:
|
||||
// https://github.com/rollup/plugins/tree/master/packages/commonjs
|
||||
resolve({
|
||||
browser: true,
|
||||
dedupe: ['svelte'],
|
||||
exportConditions: ['svelte'],
|
||||
}),
|
||||
commonjs(),
|
||||
|
||||
// In dev mode, call `npm run start` once
|
||||
// the bundle has been generated
|
||||
!production && serve(),
|
||||
|
||||
// Watch the `public` directory and refresh the
|
||||
// browser on changes when not in production
|
||||
!production && livereload('public'),
|
||||
|
||||
// If we're building for production (npm run build
|
||||
// instead of npm run dev), minify
|
||||
production && terser(),
|
||||
],
|
||||
watch: {
|
||||
clearScreen: false,
|
||||
},
|
||||
};
|
30
sveltep5play/src/App.svelte
Normal file
|
@ -0,0 +1,30 @@
|
|||
<script>
|
||||
import Game from './Game.svelte'
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<Game></Game>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
main {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-color: #111;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Aztec';
|
||||
src: url('/font/Aztec.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
:global(body) {
|
||||
font-family: 'Aztec', sans-serif;
|
||||
}
|
||||
|
||||
</style>
|
145
sveltep5play/src/Game.svelte
Normal file
|
@ -0,0 +1,145 @@
|
|||
<script>
|
||||
import {onMount} from 'svelte';
|
||||
import p5 from 'p5';
|
||||
import {createSketch} from './sketch.js';
|
||||
|
||||
import StartScreen from './screens/StartScreen.svelte';
|
||||
import GameOverScreen from './screens/GameOverScreen.svelte';
|
||||
import GameClearScreen from './screens/GameClearScreen.svelte';
|
||||
|
||||
let gameState = "start"; // 'start', 'play', 'clear', 'over'
|
||||
let p5Instance;
|
||||
let life = 3;
|
||||
let score = 0;
|
||||
|
||||
function startGame() {
|
||||
gameState = "play";
|
||||
}
|
||||
|
||||
function restartGame() {
|
||||
gameState = "start";
|
||||
}
|
||||
|
||||
function retryGame() {
|
||||
gameState = "play";
|
||||
}
|
||||
|
||||
function receiveGameData(data) {
|
||||
life = data.life;
|
||||
score = data.score;
|
||||
if (data.state && data.state !== gameState && data.state !== "play") {
|
||||
gameState = data.state;
|
||||
}
|
||||
}
|
||||
|
||||
let hasStarted = false;
|
||||
|
||||
$: if (gameState === "play" && !hasStarted) {
|
||||
hasStarted = true;
|
||||
setTimeout(() => {
|
||||
const canvasParent = document.getElementById('game-canvas');
|
||||
if (canvasParent) {
|
||||
p5Instance = new p5((p) => createSketch(p, receiveGameData), 'game-canvas');
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
$: if (gameState !== "play" && p5Instance) {
|
||||
p5Instance.remove();
|
||||
p5Instance = null;
|
||||
hasStarted = false;
|
||||
}
|
||||
|
||||
$: if (gameState == "over" || gameState == "clear") {
|
||||
if (p5Instance) {
|
||||
p5Instance.remove();
|
||||
p5Instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const preventArrowScroll = (e) => {
|
||||
const keys = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "w", "a", "s", "d", "W", "A", "S", "D"];
|
||||
if (keys.includes(e.key)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", preventArrowScroll, {passive: false});
|
||||
|
||||
const canvasParent = document.getElementById("game-canvas");
|
||||
if (canvasParent) {
|
||||
canvasParent.setAttribute("tabindex", "0");
|
||||
canvasParent.focus();
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", preventArrowScroll);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
{#if gameState == "start"}
|
||||
<StartScreen on:start={startGame} />
|
||||
{:else if gameState == "play"}
|
||||
<div class="container">
|
||||
<div class="hud">
|
||||
<div>Score: {score}</div>
|
||||
<div class="life-icons">
|
||||
<span class="label">Life:</span>
|
||||
{#each Array(life) as _, i}
|
||||
<img src="/assets/items/heart.png" alt="life" class="life-icon" />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div id="game-canvas" tabindex="0"></div>
|
||||
</div>
|
||||
{:else if gameState === "clear"}
|
||||
<GameClearScreen on:clear={restartGame} />
|
||||
{:else if gameState == "over"}
|
||||
<GameOverScreen on:retry={retryGame} on:restart={restartGame} />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
#game-canvas {
|
||||
width: 1000px;
|
||||
height: 520px;
|
||||
background-color: black;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.hud {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 40px;
|
||||
font-size: 1.2rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.life-icons {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.life-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.life-icons .label {
|
||||
margin-right: 6px;
|
||||
color: white;
|
||||
order: 1;
|
||||
}
|
||||
|
||||
</style>
|
8
sveltep5play/src/main.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
import App from './App.svelte';
|
||||
|
||||
const app = new App({
|
||||
target: document.body,
|
||||
props: {},
|
||||
});
|
||||
|
||||
export default app;
|
42
sveltep5play/src/screens/GameClearScreen.svelte
Normal file
|
@ -0,0 +1,42 @@
|
|||
<script>
|
||||
import {createEventDispatcher} from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function handleClear() {
|
||||
dispatch('clear');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="start-screen">
|
||||
<h1>Game Clear</h1>
|
||||
<button on:click={handleClear}>Try Again</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.start-screen {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: #222;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
button {
|
||||
display: block;
|
||||
margin: 20px auto 0;
|
||||
padding: 10px 20px;
|
||||
font-size: 1.2rem;
|
||||
width: 200px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin: 20px;
|
||||
}
|
||||
</style>
|
89
sveltep5play/src/screens/GameOverScreen.svelte
Normal file
|
@ -0,0 +1,89 @@
|
|||
<script>
|
||||
import {createEventDispatcher} from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function handleRetry() {
|
||||
dispatch('retry');
|
||||
}
|
||||
|
||||
function handleBackToStart() {
|
||||
dispatch('restart')
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="gameover-screen">
|
||||
<h1>Game Over</h1>
|
||||
<div class="buttons">
|
||||
<button on:click={handleRetry}>Retry</button>
|
||||
<button on:click={handleBackToStart}>Back to Start</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.gameover-screen {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: #222;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 20px;
|
||||
gap: 80px;
|
||||
}
|
||||
|
||||
button {
|
||||
position: relative;
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 4px;
|
||||
width: fit-content;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
text-align: center;
|
||||
background-color: transparent;
|
||||
color: white;
|
||||
border: none;
|
||||
font-family: 'Aztec';
|
||||
}
|
||||
|
||||
button::before,
|
||||
button::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-image: url("/assets/obstacle.png");
|
||||
background-size: cover;
|
||||
opacity: 0;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
button::before {
|
||||
left: -30px;
|
||||
}
|
||||
|
||||
button::after {
|
||||
right: -30px;
|
||||
}
|
||||
|
||||
button:hover::before,
|
||||
button:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin: 20px;
|
||||
}
|
||||
</style>
|
87
sveltep5play/src/screens/StartScreen.svelte
Normal file
|
@ -0,0 +1,87 @@
|
|||
<script>
|
||||
import {createEventDispatcher} from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function handleStart() {
|
||||
dispatch("start");
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="start-screen">
|
||||
<div class="title">
|
||||
<h1>QUETZALCOATL</h1>
|
||||
<h2>The Fifth Sun</h2>
|
||||
</div>
|
||||
<button on:click={handleStart}>Start Game</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.start-screen {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: #26071B;
|
||||
color: #BF2A45;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
button {
|
||||
position: relative;
|
||||
display: block;
|
||||
margin: 40px auto 0;
|
||||
padding: 4px;
|
||||
width: fit-content;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
text-align: center;
|
||||
background-color: transparent;
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
font-family: 'Aztec';
|
||||
}
|
||||
|
||||
button::before,
|
||||
button::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-image: url("/assets/obstacle.png");
|
||||
background-size: cover;
|
||||
opacity: 0;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
button::before {
|
||||
left: -30px;
|
||||
}
|
||||
|
||||
button::after {
|
||||
right: -30px;
|
||||
}
|
||||
|
||||
button:hover::before,
|
||||
button:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
font-size: 4rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
450
sveltep5play/src/sketch.js
Normal file
|
@ -0,0 +1,450 @@
|
|||
export function createSketch(p, onGameData) {
|
||||
//cell setting
|
||||
let cellSize = 20;
|
||||
|
||||
let snake = [
|
||||
{x: 10, y: 5},
|
||||
{x: 9, y: 5},
|
||||
{x: 8, y: 5},
|
||||
{x: 7, y: 5},
|
||||
{x: 6, y: 5},
|
||||
{x: 5, y: 5},
|
||||
{x: 4, y: 5},
|
||||
{x: 3, y: 5}
|
||||
]; // initial length = 8
|
||||
|
||||
let headImg, bodyImg, cornerImg, tailImg, obstacleImg, heartImg, gemstoneImg, sunImg;
|
||||
|
||||
//load images
|
||||
p.preload = () => {
|
||||
headImg = p.loadImage("/assets/segments/head.png");
|
||||
bodyImg = p.loadImage("/assets/segments/body.png");
|
||||
cornerImg = p.loadImage("/assets/segments/corner.png");
|
||||
tailImg = p.loadImage("/assets/segments/tail.png");
|
||||
obstacleImg = p.loadImage("/assets/obstacle.png");
|
||||
heartImg = p.loadImage("/assets/items/heart.png");
|
||||
gemstoneImg = p.loadImage("/assets/items/gemstone.png");
|
||||
sunImg = p.loadImage("/assets/items/sun.png");
|
||||
};
|
||||
|
||||
//game state variables
|
||||
let life = 3;
|
||||
let score = 0;
|
||||
let speed = 8;
|
||||
let obstacles = [];
|
||||
let obstacleInterval = 100;
|
||||
let walls = [];
|
||||
let items = [];
|
||||
let growNext = false;
|
||||
let dirHead = { x: 1, y: 0 };
|
||||
let dirTail = { x: -1, y: 0 };
|
||||
let gameState = "play";
|
||||
|
||||
|
||||
//game data
|
||||
function updateGameData() {
|
||||
if (typeof onGameData == "function") {
|
||||
onGameData({life, score, state: gameState});
|
||||
}
|
||||
}
|
||||
|
||||
//rotation direction
|
||||
function rotationByDir(dir) {
|
||||
if (dir.x === 1 && dir.y === 0) return 0;
|
||||
if (dir.x === 0 && dir.y === 1) return p.HALF_PI;
|
||||
if (dir.x === -1 && dir.y === 0) return p.PI;
|
||||
if (dir.x === 0 && dir.y === -1) return -p.HALF_PI;
|
||||
return 0;
|
||||
}
|
||||
|
||||
//make obstacle
|
||||
function makeObstacle() {
|
||||
// moving head or tail? which direction?
|
||||
let base;
|
||||
let dir;
|
||||
|
||||
if (dirHead.x !==0 || dirHead.y !== 0) {
|
||||
base = snake[0];
|
||||
dir = dirHead;
|
||||
}
|
||||
else if (dirTail.x !== 0 || dirTail.y !== 0){
|
||||
base = snake[snake.length - 1];
|
||||
dir = dirTail;
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
|
||||
//gap between the snake and an obstacle
|
||||
const gap = p.floor(p.random(3, 7));
|
||||
let newX = base.x + dir.x * gap + p.floor(p.random(-1, 2));
|
||||
let newY = base.y + dir.y * gap + p.floor(p.random(-1, 2));
|
||||
|
||||
|
||||
//is in canvas? (clamp)
|
||||
newX = p.constrain(newX, 0, p.floor(p.width / cellSize) - 1);
|
||||
newY = p.constrain(newY, 0, p.floor(p.height / cellSize) - 1);
|
||||
|
||||
//overlap check
|
||||
const overlap = [...snake, ...walls, ...items].some(seg => seg.x == newX && seg.y == newY);
|
||||
if (overlap) return;
|
||||
|
||||
obstacles = [{x: newX, y: newY, hit: null, time: p.frameCount}];
|
||||
}
|
||||
|
||||
//make items
|
||||
function makeItem() {
|
||||
const cols = p.width / cellSize;
|
||||
const rows = p.height / cellSize;
|
||||
|
||||
let newX = p.floor(p.random(cols));
|
||||
let newY = p.floor(p.random(rows));
|
||||
|
||||
const overlap = [...snake, ...walls, ...obstacles].some(seg => seg.x == newX && seg.y == newY);
|
||||
if (overlap) return;
|
||||
|
||||
const types = ["heart", "gemstone", "sun"];
|
||||
const type = types[p.floor(p.random(types.length))];
|
||||
|
||||
items.push({x: newX, y: newY, type});
|
||||
}
|
||||
|
||||
p.setup = () => {
|
||||
p.createCanvas(1000, 520);
|
||||
p.imageMode(p.CENTER);
|
||||
|
||||
const cols = p.width / cellSize;
|
||||
const rows = p.height / cellSize;
|
||||
|
||||
for (let i = 0; i < cols; i += 1) {
|
||||
walls.push({x:i, y:0});
|
||||
walls.push({x:i, y: rows - 1});
|
||||
}
|
||||
for (let i = 0; i < rows; i += 1) {
|
||||
walls.push({x:0, y:i});
|
||||
walls.push({x:cols - 1, y: i});
|
||||
}
|
||||
|
||||
makeItem();
|
||||
};
|
||||
|
||||
p.draw = () => {
|
||||
//screen change conditions
|
||||
if (gameState === "over" || gameState === "clear") {
|
||||
updateGameData();
|
||||
p.noLoop();
|
||||
return;
|
||||
}
|
||||
|
||||
p.background(0);
|
||||
|
||||
updateGameData();
|
||||
|
||||
let grow = growNext;
|
||||
growNext = false;
|
||||
|
||||
const head = snake[0];
|
||||
const tail = snake[snake.length - 1];
|
||||
|
||||
//default movement
|
||||
if (gameState !== "play") return;
|
||||
|
||||
if (score >= 90) {
|
||||
speed = 3;
|
||||
}
|
||||
else if (score >= 70){
|
||||
speed = 5;
|
||||
}
|
||||
else {
|
||||
speed = 7;
|
||||
}
|
||||
|
||||
if (snake.length < 2) return;
|
||||
|
||||
if (p.frameCount % speed == 0) {
|
||||
//new head & tail
|
||||
const head = snake[0];
|
||||
const tail = snake[snake.length - 1];
|
||||
const newHead = {
|
||||
x: head.x + dirHead.x,
|
||||
y: head.y + dirHead.y
|
||||
};
|
||||
const newTail = {
|
||||
x: tail.x + dirTail.x,
|
||||
y: tail.y + dirTail.y
|
||||
};
|
||||
|
||||
if(dirHead.x !== 0 || dirHead.y !== 0) {
|
||||
snake.unshift(newHead);
|
||||
|
||||
items = items.filter(item => {
|
||||
const hit = (head.x == item.x && head.y == item.y);
|
||||
|
||||
if (hit) {
|
||||
if (item.type == "heart") {
|
||||
score += 5;
|
||||
growNext += 1;
|
||||
}
|
||||
else if (item.type == "gemstone") {
|
||||
score += 10;
|
||||
growNext += 2;
|
||||
}
|
||||
else if (item.type == "sun") {
|
||||
score += 15;
|
||||
growNext += 3;
|
||||
}
|
||||
makeItem();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if(growNext > 0) {
|
||||
growNext -= 1;
|
||||
}
|
||||
else {
|
||||
snake.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//collision test
|
||||
if (snake.length >= 2) {
|
||||
const updatedHead = snake[0];
|
||||
const updatedTail = snake[snake.length - 1];
|
||||
if (updatedHead.x == updatedTail.x && updatedHead.y == updatedTail.y) {
|
||||
gameState = "over";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
////GET ITEMS////
|
||||
|
||||
//items rendering
|
||||
items.forEach(i => {
|
||||
const px = i.x * cellSize + cellSize / 2;
|
||||
const py = i.y * cellSize + cellSize / 2;
|
||||
|
||||
if (i.type == "heart") {
|
||||
p.image(heartImg, px, py, cellSize, cellSize);
|
||||
}
|
||||
else if (i.type == "gemstone") {
|
||||
p.image(gemstoneImg, px, py, cellSize, cellSize);
|
||||
}
|
||||
if (i.type == "sun") {
|
||||
p.image(sunImg, px, py, cellSize, cellSize);
|
||||
}
|
||||
})
|
||||
|
||||
if (items.length == 0) {
|
||||
makeItem();
|
||||
}
|
||||
|
||||
////SNAKE MOVEMENT////
|
||||
|
||||
//snake rendering
|
||||
for (let i = 0; i < snake.length; i+=1) {
|
||||
const seg = snake[i];
|
||||
const px = seg.x * cellSize + cellSize/2;
|
||||
const py = seg.y * cellSize + cellSize/2;
|
||||
|
||||
p.push();
|
||||
p.translate(px, py);
|
||||
|
||||
//rotate segments
|
||||
if (i == 0) {
|
||||
//head
|
||||
const angle = rotationByDir({
|
||||
x: seg.x - snake[1].x,
|
||||
y: seg.y - snake[1].y
|
||||
});
|
||||
p.rotate(angle);
|
||||
p.image(headImg, 0, 0, cellSize, cellSize);
|
||||
}
|
||||
else if (i == snake.length - 1) {
|
||||
//tail
|
||||
const angle = rotationByDir({
|
||||
x: snake[i - 1].x - seg.x,
|
||||
y: snake[i - 1].y - seg.y
|
||||
});
|
||||
p.rotate(angle);
|
||||
p.image(tailImg, 0, 0, cellSize, cellSize);
|
||||
}
|
||||
else {
|
||||
//body & corner
|
||||
const prev = snake[i - 1];
|
||||
const next = snake[i + 1];
|
||||
|
||||
const dx1 = seg.x - prev.x;
|
||||
const dy1 = seg.y - prev.y;
|
||||
const dx2 = next.x - seg.x;
|
||||
const dy2 = next.y - seg.y;
|
||||
|
||||
const isCorner = dx1 !== dx2 || dy1 !== dy2;
|
||||
|
||||
if (isCorner) {
|
||||
//corner
|
||||
let angle = 0;
|
||||
|
||||
if (dx1 == -1 && dy1 == 0 && dx2 == 0 && dy2 == 1) {
|
||||
angle = 3 * p.HALF_PI;
|
||||
} //←↓
|
||||
else if (dx1 == 0 && dy1 == 1 && dx2 == 1 && dy2 == 0) {
|
||||
angle = p.PI;
|
||||
} //↓→
|
||||
else if (dx1 == 1 && dy1 == 0 && dx2 == 0 && dy2 == -1) {
|
||||
angle = p.HALF_PI;
|
||||
} //→↑
|
||||
else if (dx1 == 0 && dy1 == -1 && dx2 == -1 && dy2 == 0) {
|
||||
angle = 0;
|
||||
} //↑←
|
||||
else if (dx1 == 0 && dy1 == 1 && dx2 == -1 && dy2 == 0) {
|
||||
angle = p.HALF_PI;
|
||||
} //↓←
|
||||
else if (dx1 == 1 && dy1 == 0 && dx2 == 0 && dy2 == 1) {
|
||||
angle = 0;
|
||||
} //→↓
|
||||
else if (dx1 == 0 && dy1 == -1 && dx2 == 1 && dy2 == 0) {
|
||||
angle = -p.HALF_PI;
|
||||
} //↑→
|
||||
else if (dx1 == -1 && dy1 == 0 && dx2 == 0 && dy2 == -1) {
|
||||
angle = p.PI;
|
||||
} //←↑
|
||||
|
||||
p.rotate(angle);
|
||||
p.image(cornerImg, 0, 0, cellSize, cellSize);
|
||||
}
|
||||
else {
|
||||
//body
|
||||
const angle = dx1 == 0 ? p.HALF_PI : 0;
|
||||
p.rotate(angle);
|
||||
p.image(bodyImg, 0, 0, cellSize, cellSize);
|
||||
}
|
||||
}
|
||||
|
||||
p.pop();
|
||||
}
|
||||
|
||||
////COLLISION WITH OBSTACLE////
|
||||
|
||||
//show obstacle
|
||||
if (p.frameCount % obstacleInterval == 0) {
|
||||
const o = obstacles[0];
|
||||
|
||||
const shouldMake = !o || o.hit !== null || (o.time !== undefined && p.frameCount - o.time > 120);
|
||||
|
||||
if (shouldMake) {
|
||||
obstacles = [];
|
||||
makeObstacle();
|
||||
}
|
||||
}
|
||||
|
||||
//collision
|
||||
for (let i = 0; i < obstacles.length; i += 1) {
|
||||
const o = obstacles[i];
|
||||
if ((head.x == o.x && head.y == o.y)
|
||||
|| tail.x == o.x && tail.y == o.y) {
|
||||
if (o.hit == null) {
|
||||
life -= 1;
|
||||
o.hit = p.frameCount;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//game over condition
|
||||
if (life == 0) {
|
||||
gameState = "over";
|
||||
return;
|
||||
}
|
||||
|
||||
//obstacles rendering
|
||||
obstacles.forEach(o => {
|
||||
const px = o.x * cellSize + cellSize / 2;
|
||||
const py = o.y * cellSize + cellSize / 2;
|
||||
|
||||
if (o.hit == null) {
|
||||
p.image(obstacleImg, px, py, cellSize, cellSize);
|
||||
}
|
||||
else {
|
||||
const count = p.frameCount - o.hit;
|
||||
if (count < 32) {
|
||||
if (count % 4 == 0) {
|
||||
p.image(obstacleImg, px, py, cellSize, cellSize);
|
||||
}
|
||||
}
|
||||
else {
|
||||
const index = obstacles.indexOf(o);
|
||||
if (index !== -1) obstacles.splice(index, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
////COLLISION WITH WALL////
|
||||
|
||||
//wall rendering
|
||||
walls.forEach (w => {
|
||||
const px = w.x * cellSize + cellSize / 2;
|
||||
const py = w.y * cellSize + cellSize / 2;
|
||||
p.image(obstacleImg, px, py, cellSize, cellSize);
|
||||
});
|
||||
|
||||
//collision
|
||||
for (let i = 0; i < walls.length; i += 1) {
|
||||
const w = walls[i];
|
||||
if ((head.x == w.x && head.y == w.y) || (tail.x == w.x && tail.y == w.y)) {
|
||||
gameState = "over";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (score >= 100) {
|
||||
gameState = "clear";
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
};//draw end
|
||||
|
||||
//operating snake
|
||||
p.keyPressed = () => {
|
||||
const key = p.key.toLowerCase();
|
||||
|
||||
if (p.keyCode === p.LEFT_ARROW && !(dirHead.x === 1 && dirHead.y === 0)) {
|
||||
dirHead = {x: -1, y: 0};
|
||||
}
|
||||
if (p.keyCode === p.RIGHT_ARROW && !(dirHead.x === -1 && dirHead.y === 0)) {
|
||||
dirHead = {x: 1, y: 0};
|
||||
}
|
||||
if (p.keyCode === p.UP_ARROW && !(dirHead.x === 0 && dirHead.y === 1)) {
|
||||
dirHead = {x: 0, y: -1};
|
||||
}
|
||||
if (p.keyCode === p.DOWN_ARROW && !(dirHead.x === 0 && dirHead.y === -1)) {
|
||||
dirHead = {x: 0, y: 1};
|
||||
}
|
||||
};
|
||||
|
||||
function handleKeyDown(e) {
|
||||
const key = e.key.toLowerCase();
|
||||
|
||||
if (key === 'a' && !(dirTail.x === 1 && dirTail.y === 0)) {
|
||||
dirTail = {x: -1, y: 0};
|
||||
}
|
||||
if (key === 'd' && !(dirTail.x === -1 && dirTail.y === 0)) {
|
||||
dirTail = {x: 1, y: 0};
|
||||
}
|
||||
if (key === 'w' && !(dirTail.x === 0 && dirTail.y === 1)) {
|
||||
dirTail = {x: 0, y: -1};
|
||||
}
|
||||
if (key === 's' && !(dirTail.x === 0 && dirTail.y === -1)) {
|
||||
dirTail = {x: 0, y: 1};
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
|
||||
|
||||
|