Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a34318b4f2 | |||
| 3c32dd5aed | |||
| 5cf47cd241 | |||
| 200c716393 | |||
| 142024a315 | |||
| 3e8eb69329 | |||
| 45e7a0d323 | |||
| 7df0f14e99 | |||
| 1cfa5e38f3 | |||
| 17356e0d8f | |||
| fb1653e231 | |||
| 7121c230fc | |||
| 7b20b0e8ec | |||
| 3d50bf805d | |||
| bbb34e63b6 | |||
| 7d97132aa1 | |||
| c850d9ffa9 | |||
| c90bfd5103 | |||
| a27b8fcd9e | |||
| 52a2e41e05 | |||
| 17ab288b63 | |||
| f607034a33 | |||
| a934d5537b | |||
| efa4d9f80f | |||
| 5a569cbaf9 | |||
| b0106f9df0 | |||
| 7b083fda11 | |||
| 1ca8e46461 | |||
| db52ff95ae | |||
| 462668e5cd |
@@ -1,8 +1,6 @@
|
||||
node_modules/
|
||||
public/assets/*.png
|
||||
public/assets/*.json
|
||||
public/css/*.css
|
||||
public/js/*.js
|
||||
public/favicon.ico
|
||||
build/*.png
|
||||
db.sqlite
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
FROM alpine:3.11.5
|
||||
RUN apk add --no-cache nodejs npm git imagemagick make && git clone https://gitdab.com/Codinget/Snek /snek && cd /snek && npm i && make && apk del git make imagemagick && printf '#!/bin/sh\ncd /snek\nnpm start\n' > start.sh && chmod +x start.sh
|
||||
RUN apk add --no-cache nodejs npm git python3 python2 alpine-sdk && npm i -g node-gyp && git clone https://gitdab.com/Codinget/Snek /snek && cd /snek && npm i --unsafe-perm && apk del git python2 python3 alpine-sdk && printf '#!/bin/sh\ncd /snek\nnpm start\n' > start.sh && chmod +x start.sh
|
||||
CMD /snek/start.sh
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
.PHONY: all clean
|
||||
.PHONY: all clean mrproper
|
||||
|
||||
SIZE = 32
|
||||
TEMPSIZE = $(shell echo $(SIZE) '*4' | bc)
|
||||
|
||||
FIRE_ANIM = $(foreach angle, $(shell seq 0 6 359), build/fire$(angle).png)
|
||||
PEACH_DECAY_ANIM = $(foreach percent, $(shell seq 99 -1 0), build/peach-decay$(percent).png)
|
||||
PEACH_RAINBOW_ANIM = $(foreach percent, $(shell seq 100 2 299), build/peach-rainbow$(percent).png)
|
||||
PORTAL_ANIM = $(foreach angle, $(shell seq 0 6 359), build/portal-a$(angle).png)
|
||||
|
||||
IMAGES = $(foreach name, apple wall oil, public/assets/$(name)32.png)
|
||||
TILESETS = $(foreach name, hole, public/assets/$(name)-ts.png)
|
||||
ANIMATIONS = $(foreach name, fire peach-decay peach-rainbow, public/assets/$(name)-anim.png)
|
||||
IMAGES = $(foreach name, apple wall oil key door, public/assets/$(name)$(SIZE).png)
|
||||
TILESETS = $(foreach name, hole switch spikes, public/assets/$(name)-ts.png)
|
||||
ANIMATIONS = $(foreach name, fire peach-decay peach-rainbow portal-a portal-b portal-c portal-d, public/assets/$(name)-anim.png)
|
||||
JSON = $(foreach name, snake levelList config metaConfig, public/assets/$(name).json)
|
||||
ICON = public/assets/icon32.png public/assets/icon256.png public/favicon.ico
|
||||
CSS = public/css/snek.css
|
||||
@@ -31,35 +35,54 @@ public/assets/%32.png: assets/%.png
|
||||
convert $^ -resize 32x $@
|
||||
public/assets/%256.png: assets/%.png
|
||||
convert $^ -resize 256x $@
|
||||
public/assets/%$(SIZE).png: assets/%.png
|
||||
convert $^ -resize $(SIZE)x $@
|
||||
|
||||
public/assets/%32.png: assets/%.jpg
|
||||
convert $^ -resize 32x $@
|
||||
public/assets/%256.png: assets/%.jpg
|
||||
convert $^ -resize 256x $@
|
||||
public/assets/%$(SIZE).png: assets/%.jpg
|
||||
convert $^ -resize $(SIZE)x $@
|
||||
|
||||
build/%-smol.png: assets/%.png
|
||||
convert $^ -resize $(TEMPSIZE)x\> $@
|
||||
|
||||
public/assets/%-ts.png: assets/%.png
|
||||
convert $^ -scale 32x $@
|
||||
convert $^ -scale $(SIZE)x $@
|
||||
|
||||
public/assets/fire-anim.png: $(FIRE_ANIM)
|
||||
convert $^ -append $@
|
||||
|
||||
build/fire%.png: assets/fire.png
|
||||
convert $^ -distort ScaleRotateTranslate $(shell echo $@ | sed 's/[^0-9]*//g') -resize 32x $@
|
||||
build/fire%.png: build/fire-smol.png
|
||||
convert $^ -distort ScaleRotateTranslate $(shell echo $@ | sed 's/[^0-9]*//g') -resize $(SIZE)x $@
|
||||
|
||||
public/assets/peach-decay-anim.png: $(PEACH_DECAY_ANIM)
|
||||
convert $^ -append $@
|
||||
|
||||
build/peach-decay%.png: assets/peach.png
|
||||
convert $^ -modulate 100,$(shell echo $@ | sed 's/[^0-9]*//g') -resize 32x $@
|
||||
build/peach-decay%.png: build/peach-smol.png
|
||||
convert $^ -modulate 100,$(shell echo $@ | sed 's/[^0-9]*//g') -resize $(SIZE)x $@
|
||||
|
||||
public/assets/peach-rainbow-anim.png: $(PEACH_RAINBOW_ANIM)
|
||||
convert $^ -append $@
|
||||
|
||||
build/peach-rainbow%.png: assets/peach.png
|
||||
convert $^ -modulate 100,100,$(shell echo $@ | sed 's/[^0-9]*//g') -resize 32x $@
|
||||
build/peach-rainbow%.png: build/peach-smol.png
|
||||
convert $^ -modulate 100,100,$(shell echo $@ | sed 's/[^0-9]*//g') -resize $(SIZE)x $@
|
||||
|
||||
build/portal-a%.png: build/portal-smol.png
|
||||
convert $^ -distort ScaleRotateTranslate $(shell echo $@ | sed 's/[^0-9]*//g') -resize $(SIZE)x $@
|
||||
|
||||
public/assets/portal-a-anim.png: $(PORTAL_ANIM)
|
||||
convert $^ -append $@
|
||||
public/assets/portal-b-anim.png: public/assets/portal-a-anim.png
|
||||
convert $^ -modulate 100,100,200 $@
|
||||
public/assets/portal-c-anim.png: public/assets/portal-a-anim.png
|
||||
convert $^ -modulate 100,100,150 $@
|
||||
public/assets/portal-d-anim.png: public/assets/portal-a-anim.png
|
||||
convert $^ -modulate 100,100,50 $@
|
||||
|
||||
public/assets/%.json: assets/%.json
|
||||
cp $^ $@
|
||||
ln -s ../../$^ $@
|
||||
|
||||
public/css/snek.css: src/less/snek.less $(wildcard src/less/*.less)
|
||||
node_modules/.bin/lessc $< $@
|
||||
@@ -69,4 +92,5 @@ public/js/snek.js: $(wildcard src/js/*.js)
|
||||
|
||||
clean:
|
||||
rm -f build/*.*
|
||||
mrproper: clean
|
||||
rm -f $(OUTPUT)
|
||||
|
||||
@@ -7,7 +7,7 @@ A "simple" Snake, done as my final JS class project
|
||||
|
||||
## Features
|
||||
- 60 FPS 2D animations
|
||||
- arcade and speedrun game modes
|
||||
- arcade, speedrun and puzzle game modes
|
||||
- touchscreen and controller support
|
||||
- playable at [snek.codinget.me](https://snek.codinget.me)
|
||||
|
||||
@@ -17,21 +17,28 @@ A "simple" Snake, done as my final JS class project
|
||||
- GNU Coreutils are known to work
|
||||
- On Windows, WSL is known to work
|
||||
- Imagemagick, with the `convert` tool in the PATH
|
||||
- `bc`
|
||||
- Make
|
||||
- Node.js and npm, both in the PATH
|
||||
- Node.js 10 and 12 are known to work
|
||||
- node-gyp and python are required for the database
|
||||
|
||||
## Running the game (dev)
|
||||
- `git clone` this repo
|
||||
- `npm install` the dependencies
|
||||
- `make` the ressources
|
||||
- `npm install` the dependencies (this will also build the less and js and initialize the database)
|
||||
- `npm start` the server
|
||||
- `make` every time you change something
|
||||
|
||||
## Running the game (prod)
|
||||
## Running the game (prod, docker)
|
||||
- Get the [Dockerfile](https://gitdab.com/Codinget/Snek/raw/branch/master/Dockerfile)
|
||||
- `docker build` it
|
||||
- `docker run -it -p80:3000` the container
|
||||
- ideally, put it behind a reverse proxy
|
||||
|
||||
## Running the game (prod, direct)
|
||||
- `git clone` this repo
|
||||
- `npm install` the dependencies (this will also build the less and js and initialize the database)
|
||||
- `npm start` the server
|
||||
|
||||
## License
|
||||
[MIT](https://opensource.org/licenses/MIT)
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
const DB=require('better-sqlite3');
|
||||
const {Router}=require('express');
|
||||
|
||||
const db=new DB('db.sqlite');
|
||||
const api=new Router();
|
||||
|
||||
const leaderboardSelect='SELECT username, score, length, time, speed FROM leaderboards';
|
||||
const getLeaderboardsBy={
|
||||
score: db.prepare(`${leaderboardSelect} WHERE mode=? ORDER BY score DESC LIMIT ? OFFSET ?`),
|
||||
timeA: db.prepare(`${leaderboardSelect} WHERE mode=? ORDER BY time ASC LIMIT ? OFFSET ?`),
|
||||
timeD: db.prepare(`${leaderboardSelect} WHERE mode=? ORDER BY time DESC LIMIT ? OFFSET ?`)
|
||||
};
|
||||
const addLeaderboard=db.prepare(`
|
||||
INSERT
|
||||
INTO leaderboards(mode, username, score, length, time, speed)
|
||||
VALUES(@mode, @username, @score, @length, @time, @speed)
|
||||
`);
|
||||
|
||||
const levelList=require('./assets/levelList.json');
|
||||
const validMode=mode => {
|
||||
try {
|
||||
const [category, name]=mode.split('/');
|
||||
return levelList[category].levels.map(l => ''+l).includes(name);
|
||||
} catch(e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
api.get('/leaderboards/:category/:id', (req, res) => {
|
||||
const sort=req.query.sort || 'score';
|
||||
if(!['score', 'timeA', 'timeD'].includes(sort)) return res.status(400).json({
|
||||
ok: false,
|
||||
err: 'Invalid sort'
|
||||
});
|
||||
const results=+req.query.results || 20;
|
||||
if((typeof results)!='number' || results<1 || results >100) return res.status(400).json({
|
||||
ok: false,
|
||||
err: 'Invalid result count'
|
||||
});
|
||||
const page=+req.query.page || 1;
|
||||
if((typeof page)!='number' || page<1) return res.status(400).json({
|
||||
ok: false,
|
||||
err: 'Invalid page'
|
||||
});
|
||||
|
||||
const data=getLeaderboardsBy[sort]
|
||||
.all(req.params.category+'/'+req.params.id, results, (page-1)*results);
|
||||
return res.status(200).json({
|
||||
ok: true,
|
||||
data
|
||||
});
|
||||
});
|
||||
|
||||
api.post('/leaderboards/:category/:id', (req, res) => {
|
||||
const mode=req.params.category+'/'+req.params.id;
|
||||
if((typeof mode)!='string' || !validMode(mode)) return res.status(400).json({
|
||||
ok: false,
|
||||
err: 'Invalid mode'
|
||||
});
|
||||
const username=req.body.username;
|
||||
if((typeof username)!='string' || username.length>100) return res.status(400).json({
|
||||
ok: false,
|
||||
err: 'Invalid username'
|
||||
});
|
||||
const score=req.body.score;
|
||||
if((typeof score)!='number' || score%1 || score<0) return res.status(400).json({
|
||||
ok: false,
|
||||
err: 'Invalid score'
|
||||
});
|
||||
const length=req.body.length;
|
||||
if((typeof length)!='number' || length%1 || length<1) return res.status(400).json({
|
||||
ok: false,
|
||||
err: 'Invalid length'
|
||||
});
|
||||
const time=req.body.time;
|
||||
if((typeof time)!='number' || time%1 || time<0) return res.status(400).json({
|
||||
ok: false,
|
||||
err: 'Invalid time'
|
||||
});
|
||||
const speed=req.body.speed;
|
||||
if((typeof speed)!='number' || speed%1 || speed<0) return res.status(400).json({
|
||||
ok: false,
|
||||
err: 'Invalid speed'
|
||||
});
|
||||
|
||||
try {
|
||||
addLeaderboard.run({
|
||||
mode, username,
|
||||
score, length,
|
||||
time, speed
|
||||
});
|
||||
res.json({
|
||||
ok: true
|
||||
});
|
||||
} catch(e) {
|
||||
res.status(500).json({
|
||||
ok: false,
|
||||
err: 'Failed to add score'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return module.exports=exports=api;
|
||||
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"debug": false,
|
||||
|
||||
"player.name": "Player",
|
||||
"player.leaderboards": false,
|
||||
|
||||
"input.touchscreen.crosspad.enabled": false,
|
||||
"input.touchscreen.crosspad.overlay": true,
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 390 KiB |
|
After Width: | Height: | Size: 33 KiB |
@@ -6,7 +6,9 @@
|
||||
"speedIncrease": false,
|
||||
"worldWrap": false,
|
||||
"winCondition": "fruit",
|
||||
"scoreSystem": "speedrun"
|
||||
"scoreSystem": "speedrun",
|
||||
"uploadOnDeath": false,
|
||||
"leaderboardsSort": "timeA"
|
||||
},
|
||||
"levelFilename": "level<n>.json",
|
||||
"levelDisplay": "Level <n>",
|
||||
@@ -22,7 +24,9 @@
|
||||
"speedIncrease": true,
|
||||
"speedMultiplier": 0.9,
|
||||
"speedCap": 50,
|
||||
"worldWrap": true
|
||||
"worldWrap": true,
|
||||
"uploadOnDeath": true,
|
||||
"leaderboardsSort": "score"
|
||||
},
|
||||
"levelFilename": "arcade-<l>.json",
|
||||
"levelDisplay": "<n>",
|
||||
@@ -37,5 +41,25 @@
|
||||
"Get a score as high as you can in 30 seconds",
|
||||
"Survive for as long as you can in an increasingly difficult game"
|
||||
]
|
||||
},
|
||||
"puzzle": {
|
||||
"desc": "Time doesn't flow in these puzzles. Try getting the fruits in as little moves as possible",
|
||||
"rules": {
|
||||
"fruitRegrow": false,
|
||||
"timeFlow": false,
|
||||
"speedIncrease": false,
|
||||
"worldWrap": false,
|
||||
"winCondition": "fruit",
|
||||
"scoreSystem": "moves",
|
||||
"moveCount": 50,
|
||||
"uploadOnDeath": false,
|
||||
"leaderboardsSort": "score"
|
||||
},
|
||||
"levelFilename": "puzzle<n>.json",
|
||||
"levelDisplay": "Level <n>",
|
||||
"levels": [
|
||||
1, 2, 3
|
||||
],
|
||||
"nextLevel": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,17 @@
|
||||
{
|
||||
"player": {
|
||||
"name": "Player settings"
|
||||
},
|
||||
"player.name": {
|
||||
"name": "Player name",
|
||||
"type": "string"
|
||||
},
|
||||
"player.leaderboards": {
|
||||
"name": "Upload scores to leaderboards",
|
||||
"type": "boolean",
|
||||
"needsBackend": true
|
||||
},
|
||||
|
||||
"input": {
|
||||
"name": "Input settings"
|
||||
},
|
||||
|
||||
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
@@ -3,8 +3,14 @@ const express=require('express');
|
||||
const app=express();
|
||||
const PORT=process.env.PORT || 3000;
|
||||
|
||||
app.use(express.json());
|
||||
app.use(express.static('public'));
|
||||
|
||||
app.use('/api', require('./api'));
|
||||
app.get('/api/has-nodejs', (req, res) => {
|
||||
res.json("yes");
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Listening on 0.0.0.0:${PORT}`);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
CREATE TABLE leaderboards (
|
||||
mode TEXT,
|
||||
username TEXT,
|
||||
score INTEGER,
|
||||
length INTEGER,
|
||||
time INTEGER,
|
||||
speed INTEGER
|
||||
);
|
||||
@@ -0,0 +1,19 @@
|
||||
const DB=require('better-sqlite3');
|
||||
const fs=require('fs');
|
||||
const child_process=require('child_process');
|
||||
|
||||
// prepare database
|
||||
console.log('Preparing database');
|
||||
if(fs.existsSync('db.sqlite')) fs.unlinkSync('db.sqlite');
|
||||
const db=new DB('db.sqlite');
|
||||
db.exec(fs.readFileSync('init.sql', 'utf8'));
|
||||
|
||||
// compile less
|
||||
console.log('Compiling less');
|
||||
child_process.execFileSync('node_modules/.bin/lessc', ['src/less/snek.less', 'public/css/snek.css']);
|
||||
|
||||
// merge js
|
||||
console.log('Merging js');
|
||||
const jsFiles=fs.readdirSync('src/js').map(f => 'src/js/'+f);
|
||||
const merged=child_process.execFileSync('node', ['mergejs.js', ...jsFiles]);
|
||||
fs.writeFileSync('public/js/snek.js', merged);
|
||||
@@ -44,6 +44,7 @@
|
||||
"autoSpeadIncreaseTicks": 10,
|
||||
"autoSizeGrow": true,
|
||||
"autoSizeGrowTicks": 100,
|
||||
"scoreSystem": "survival"
|
||||
"scoreSystem": "survival",
|
||||
"leaderboardsSort": "timeD"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"dimensions": [15, 10],
|
||||
"walls": [
|
||||
[5, 0], [5, 1], [5, 2], [5, 3], [5, 4], [5, 5], [5, 6], [5, 7], [5, 8], [5, 9],
|
||||
[10, 0], [10, 1], [10, 2], [10, 3], [10, 4], [10, 5], [10, 6], [10, 7], [10, 8], [10, 9]
|
||||
],
|
||||
"food": [
|
||||
[4, 5],
|
||||
[8, 7],
|
||||
[14, 9]
|
||||
],
|
||||
"snake": [
|
||||
[0, 0]
|
||||
],
|
||||
"portals": {
|
||||
"a": [4, 9],
|
||||
"b": [6, 0],
|
||||
"c": [9, 9],
|
||||
"d": [11, 0]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"world": [
|
||||
"k wA wf",
|
||||
" w wB",
|
||||
" w ww",
|
||||
" fw ",
|
||||
" w ",
|
||||
" K f"
|
||||
],
|
||||
"snake": [
|
||||
[0, 5]
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"world": [
|
||||
"A i B",
|
||||
" i s",
|
||||
" Cik ",
|
||||
"SSSSSSSSSSSiiiii",
|
||||
" i f",
|
||||
" i ",
|
||||
" s itttt",
|
||||
" t wKKKK",
|
||||
" w ",
|
||||
" wD "
|
||||
],
|
||||
"snake": [
|
||||
[0, 9],
|
||||
[1, 9],
|
||||
[2, 9]
|
||||
],
|
||||
"rules": {
|
||||
"moveCount": 60
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,25 @@
|
||||
"uri-js": "^4.2.2"
|
||||
}
|
||||
},
|
||||
"ansi-regex": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
|
||||
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
|
||||
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw=="
|
||||
},
|
||||
"are-we-there-yet": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
|
||||
"integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
|
||||
"requires": {
|
||||
"delegates": "^1.0.0",
|
||||
"readable-stream": "^2.0.6"
|
||||
}
|
||||
},
|
||||
"array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
@@ -69,6 +88,11 @@
|
||||
"integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==",
|
||||
"optional": true
|
||||
},
|
||||
"base64-js": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
|
||||
"integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
|
||||
},
|
||||
"bcrypt-pbkdf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||
@@ -78,6 +102,52 @@
|
||||
"tweetnacl": "^0.14.3"
|
||||
}
|
||||
},
|
||||
"better-sqlite3": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-6.0.1.tgz",
|
||||
"integrity": "sha512-4aV1zEknM9g1a6B0mVBx1oIlmYioEJ8gSS3J6EpN1b1bKYEE+N5lmpmXHKNKTi0qjHziSd7XrXwHl1kpqvEcHQ==",
|
||||
"requires": {
|
||||
"bindings": "^1.5.0",
|
||||
"integer": "^3.0.1",
|
||||
"prebuild-install": "^5.3.3",
|
||||
"tar": "4.4.10"
|
||||
}
|
||||
},
|
||||
"bindings": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||
"requires": {
|
||||
"file-uri-to-path": "1.0.0"
|
||||
}
|
||||
},
|
||||
"bl": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz",
|
||||
"integrity": "sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ==",
|
||||
"requires": {
|
||||
"buffer": "^5.5.0",
|
||||
"inherits": "^2.0.4",
|
||||
"readable-stream": "^3.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
|
||||
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
|
||||
"requires": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"body-parser": {
|
||||
"version": "1.19.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
|
||||
@@ -95,6 +165,15 @@
|
||||
"type-is": "~1.6.17"
|
||||
}
|
||||
},
|
||||
"buffer": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.5.0.tgz",
|
||||
"integrity": "sha512-9FTEDjLjwoAkEwyMGDjYJQN2gfRgOKBKRfiglhvibGbpeeU/pQn1bJxQqm32OD/AIeEuHxU9roxXxg34Byp/Ww==",
|
||||
"requires": {
|
||||
"base64-js": "^1.0.2",
|
||||
"ieee754": "^1.1.4"
|
||||
}
|
||||
},
|
||||
"bytes": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
|
||||
@@ -106,11 +185,21 @@
|
||||
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
|
||||
"optional": true
|
||||
},
|
||||
"chownr": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
|
||||
},
|
||||
"clone": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
||||
"integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18="
|
||||
},
|
||||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
|
||||
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
@@ -120,6 +209,11 @@
|
||||
"delayed-stream": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
|
||||
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4="
|
||||
},
|
||||
"content-disposition": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
|
||||
@@ -146,8 +240,7 @@
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
|
||||
"optional": true
|
||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
|
||||
},
|
||||
"dashdash": {
|
||||
"version": "1.14.1",
|
||||
@@ -166,12 +259,30 @@
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"decompress-response": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz",
|
||||
"integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==",
|
||||
"requires": {
|
||||
"mimic-response": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"deep-extend": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="
|
||||
},
|
||||
"delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
|
||||
"optional": true
|
||||
},
|
||||
"delegates": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
|
||||
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o="
|
||||
},
|
||||
"depd": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
|
||||
@@ -182,6 +293,11 @@
|
||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
|
||||
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
|
||||
},
|
||||
"detect-libc": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups="
|
||||
},
|
||||
"ecc-jsbn": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
|
||||
@@ -202,6 +318,14 @@
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
|
||||
},
|
||||
"end-of-stream": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
|
||||
"requires": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"errno": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
|
||||
@@ -221,6 +345,11 @@
|
||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
|
||||
},
|
||||
"expand-template": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
||||
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="
|
||||
},
|
||||
"express": {
|
||||
"version": "4.17.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
|
||||
@@ -282,6 +411,11 @@
|
||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
||||
"optional": true
|
||||
},
|
||||
"file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="
|
||||
},
|
||||
"finalhandler": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
|
||||
@@ -323,6 +457,34 @@
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
|
||||
},
|
||||
"fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
|
||||
},
|
||||
"fs-minipass": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz",
|
||||
"integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==",
|
||||
"requires": {
|
||||
"minipass": "^2.6.0"
|
||||
}
|
||||
},
|
||||
"gauge": {
|
||||
"version": "2.7.4",
|
||||
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
|
||||
"integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
|
||||
"requires": {
|
||||
"aproba": "^1.0.3",
|
||||
"console-control-strings": "^1.0.0",
|
||||
"has-unicode": "^2.0.0",
|
||||
"object-assign": "^4.1.0",
|
||||
"signal-exit": "^3.0.0",
|
||||
"string-width": "^1.0.1",
|
||||
"strip-ansi": "^3.0.1",
|
||||
"wide-align": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"getpass": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
|
||||
@@ -332,6 +494,11 @@
|
||||
"assert-plus": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"github-from-package": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
||||
"integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4="
|
||||
},
|
||||
"graceful-fs": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz",
|
||||
@@ -354,6 +521,11 @@
|
||||
"har-schema": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"has-unicode": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
|
||||
"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk="
|
||||
},
|
||||
"http-errors": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
|
||||
@@ -385,6 +557,11 @@
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
}
|
||||
},
|
||||
"ieee754": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
|
||||
"integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="
|
||||
},
|
||||
"image-size": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
|
||||
@@ -396,17 +573,44 @@
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
|
||||
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw=="
|
||||
},
|
||||
"integer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/integer/-/integer-3.0.1.tgz",
|
||||
"integrity": "sha512-OqtER6W2GIJTIcnT5o2B/pWGgvurnVOYs4OZCgay40QEIbMTnNq4R0KSaIw1TZyFtPWjm5aNM+pBBMTfc3exmw==",
|
||||
"requires": {
|
||||
"bindings": "^1.5.0",
|
||||
"prebuild-install": "^5.3.3"
|
||||
}
|
||||
},
|
||||
"ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
|
||||
},
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
|
||||
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"is-typedarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
|
||||
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
|
||||
"optional": true
|
||||
},
|
||||
"isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
|
||||
},
|
||||
"isstream": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
|
||||
@@ -499,37 +703,101 @@
|
||||
"mime-db": "1.43.0"
|
||||
}
|
||||
},
|
||||
"mimic-response": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz",
|
||||
"integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA=="
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"optional": true
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz",
|
||||
"integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"minizlib": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz",
|
||||
"integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==",
|
||||
"requires": {
|
||||
"minipass": "^2.9.0"
|
||||
}
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.4.tgz",
|
||||
"integrity": "sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "^1.2.5"
|
||||
}
|
||||
},
|
||||
"mkdirp-classic": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.2.tgz",
|
||||
"integrity": "sha512-ejdnDQcR75gwknmMw/tx02AuRs8jCtqFoFqDZMjiNxsu85sRIJVXDKHuLYvUUPRBUtV2FpSZa9bL1BUa3BdR2g=="
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
},
|
||||
"napi-build-utils": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
|
||||
"integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg=="
|
||||
},
|
||||
"negotiator": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
|
||||
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
|
||||
},
|
||||
"node-abi": {
|
||||
"version": "2.15.0",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.15.0.tgz",
|
||||
"integrity": "sha512-FeLpTS0F39U7hHZU1srAK4Vx+5AHNVOTP+hxBNQknR/54laTHSFIJkDWDqiquY1LeLUgTfPN7sLPhMubx0PLAg==",
|
||||
"requires": {
|
||||
"semver": "^5.4.1"
|
||||
}
|
||||
},
|
||||
"noop-logger": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz",
|
||||
"integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI="
|
||||
},
|
||||
"npmlog": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
|
||||
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
|
||||
"requires": {
|
||||
"are-we-there-yet": "~1.1.2",
|
||||
"console-control-strings": "~1.1.0",
|
||||
"gauge": "~2.7.3",
|
||||
"set-blocking": "~2.0.0"
|
||||
}
|
||||
},
|
||||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
|
||||
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
|
||||
},
|
||||
"oauth-sign": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
|
||||
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
|
||||
"optional": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
|
||||
},
|
||||
"on-finished": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||
@@ -538,6 +806,14 @@
|
||||
"ee-first": "1.1.1"
|
||||
}
|
||||
},
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
@@ -554,6 +830,33 @@
|
||||
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=",
|
||||
"optional": true
|
||||
},
|
||||
"prebuild-install": {
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.3.tgz",
|
||||
"integrity": "sha512-GV+nsUXuPW2p8Zy7SarF/2W/oiK8bFQgJcncoJ0d7kRpekEA0ftChjfEaF9/Y+QJEc/wFR7RAEa8lYByuUIe2g==",
|
||||
"requires": {
|
||||
"detect-libc": "^1.0.3",
|
||||
"expand-template": "^2.0.3",
|
||||
"github-from-package": "0.0.0",
|
||||
"minimist": "^1.2.0",
|
||||
"mkdirp": "^0.5.1",
|
||||
"napi-build-utils": "^1.0.1",
|
||||
"node-abi": "^2.7.0",
|
||||
"noop-logger": "^0.1.1",
|
||||
"npmlog": "^4.0.1",
|
||||
"pump": "^3.0.0",
|
||||
"rc": "^1.2.7",
|
||||
"simple-get": "^3.0.3",
|
||||
"tar-fs": "^2.0.0",
|
||||
"tunnel-agent": "^0.6.0",
|
||||
"which-pm-runs": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
||||
},
|
||||
"promise": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
|
||||
@@ -584,6 +887,15 @@
|
||||
"integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==",
|
||||
"optional": true
|
||||
},
|
||||
"pump": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
|
||||
"requires": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"punycode": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||
@@ -611,6 +923,31 @@
|
||||
"unpipe": "1.0.0"
|
||||
}
|
||||
},
|
||||
"rc": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||||
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
||||
"requires": {
|
||||
"deep-extend": "^0.6.0",
|
||||
"ini": "~1.3.0",
|
||||
"minimist": "^1.2.0",
|
||||
"strip-json-comments": "~2.0.1"
|
||||
}
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "2.3.7",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
|
||||
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"request": {
|
||||
"version": "2.88.2",
|
||||
"resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
|
||||
@@ -657,6 +994,11 @@
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
||||
},
|
||||
"send": {
|
||||
"version": "0.17.1",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
|
||||
@@ -695,11 +1037,36 @@
|
||||
"send": "0.17.1"
|
||||
}
|
||||
},
|
||||
"set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
|
||||
},
|
||||
"setprototypeof": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
|
||||
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
|
||||
},
|
||||
"signal-exit": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
|
||||
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA=="
|
||||
},
|
||||
"simple-concat": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz",
|
||||
"integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY="
|
||||
},
|
||||
"simple-get": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz",
|
||||
"integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==",
|
||||
"requires": {
|
||||
"decompress-response": "^4.2.0",
|
||||
"once": "^1.3.1",
|
||||
"simple-concat": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
@@ -728,6 +1095,86 @@
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
|
||||
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
|
||||
},
|
||||
"string-width": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
|
||||
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
"strip-ansi": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"requires": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
||||
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"strip-json-comments": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo="
|
||||
},
|
||||
"tar": {
|
||||
"version": "4.4.10",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-4.4.10.tgz",
|
||||
"integrity": "sha512-g2SVs5QIxvo6OLp0GudTqEf05maawKUxXru104iaayWA09551tFCTI8f1Asb4lPfkBr91k07iL4c11XO3/b0tA==",
|
||||
"requires": {
|
||||
"chownr": "^1.1.1",
|
||||
"fs-minipass": "^1.2.5",
|
||||
"minipass": "^2.3.5",
|
||||
"minizlib": "^1.2.1",
|
||||
"mkdirp": "^0.5.0",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.3"
|
||||
}
|
||||
},
|
||||
"tar-fs": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz",
|
||||
"integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==",
|
||||
"requires": {
|
||||
"chownr": "^1.1.1",
|
||||
"mkdirp-classic": "^0.5.2",
|
||||
"pump": "^3.0.0",
|
||||
"tar-stream": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"tar-stream": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.2.tgz",
|
||||
"integrity": "sha512-UaF6FoJ32WqALZGOIAApXx+OdxhekNMChu6axLJR85zMMjXKWFGjbIRe+J6P4UnRGg9rAwWvbTT0oI7hD/Un7Q==",
|
||||
"requires": {
|
||||
"bl": "^4.0.1",
|
||||
"end-of-stream": "^1.4.1",
|
||||
"fs-constants": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"readable-stream": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
|
||||
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
|
||||
"requires": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"toidentifier": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
|
||||
@@ -752,7 +1199,6 @@
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
"integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
@@ -786,6 +1232,11 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||
},
|
||||
"utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
@@ -812,6 +1263,29 @@
|
||||
"core-util-is": "1.0.2",
|
||||
"extsprintf": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"which-pm-runs": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz",
|
||||
"integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs="
|
||||
},
|
||||
"wide-align": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
|
||||
"integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
|
||||
"requires": {
|
||||
"string-width": "^1.0.2 || 2"
|
||||
}
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,8 @@
|
||||
"description": "A simple Snake",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "make",
|
||||
"start": "node index.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"prepare": "node install.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -15,6 +14,7 @@
|
||||
"author": "Codinget",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^6.0.1",
|
||||
"express": "^4.17.1",
|
||||
"less": "^3.11.1"
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 7.8 KiB |
@@ -0,0 +1 @@
|
||||
../../assets/config.json
|
||||
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
@@ -0,0 +1 @@
|
||||
../../assets/levelList.json
|
||||
@@ -0,0 +1 @@
|
||||
../../assets/metaConfig.json
|
||||
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 38 KiB |
@@ -0,0 +1 @@
|
||||
../../assets/snake.json
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
@@ -47,6 +47,22 @@
|
||||
A timed game only lasts for 30 seconds, and the goal is to get as high a score as possible.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h3>Survival</h3>
|
||||
<p>
|
||||
In survival mode, the playfield doesn't loop, fruits don't spawn, and you can't win.<br>
|
||||
This modes get progressively harder, with speed increases, and snake growth.<br>
|
||||
At first, this process is slow, but it gets faster with time.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h3>Puzzle mode</h3>
|
||||
<p>
|
||||
In puzzle mode, time doesn't flow until you move.<br>
|
||||
Weird tiles make their debut, like portals, keys, etc.<br>
|
||||
Your goal is to get the fruits in as little moves as you can.
|
||||
</p>
|
||||
</section>
|
||||
</article>
|
||||
<article>
|
||||
<h2>Tiles</h2>
|
||||
@@ -58,6 +74,8 @@
|
||||
<li><em>Oil</em> is flammable and will periodically catch on fire, which will kill you. It is otherwise perfectly safe</li>
|
||||
<li><em>Super fruits</em> give you 10 points, and sometimes spawn in arcade mode</li>
|
||||
<li><em>Decaying fruits</em> give you 5 points and sometimes spawn in arcade mode, but they also decay after 2 seconds and disappear</li>
|
||||
<li><em>Portals</em> teleport you to the corresponding portal</li>
|
||||
<li><em>Keys</em> make <em>Doors</em> disappear</li>
|
||||
</ul>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
@@ -10,26 +10,38 @@
|
||||
window.addEventListener('load', () => require('main'));
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<body class="loading">
|
||||
<header>
|
||||
<a href="#"><img src="assets/icon256.png"></a>
|
||||
<h1>Snek</h1>
|
||||
<h2>A "simple" Snake</h2>
|
||||
<ul>
|
||||
<li><a href="#">Menu</a></li>
|
||||
<li><a href="#settings">Config</a></li>
|
||||
<li><a href="#help">Help</a></li>
|
||||
<li class="loaded"><a href="#settings">Config</a></li>
|
||||
<li class="loaded"><a href="#help">Help</a></li>
|
||||
<li class="server loaded"><a href="#leaderboards">Leaderboards</a></li>
|
||||
</ul>
|
||||
</header>
|
||||
<main>
|
||||
<nav></nav>
|
||||
<canvas class="hidden"></canvas>
|
||||
<div id="hud" class="hidden"></div>
|
||||
<div id="hud" class="hidden">
|
||||
<div class="status">
|
||||
<span class="speed"></span>
|
||||
<span class="score"></span>
|
||||
<span class="time"></span>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
<img src="assets/icon32.png">
|
||||
<p>Snek by <a href="https://codinget.me">Codinget</a> on <a href="https://gitdab.com/Codinget/Snek">GitDab</a></p>
|
||||
<p>Original <a href="https://perso.liris.cnrs.fr/pierre-antoine.champin/enseignement/intro-js/s6.html">subject</a> by <a href="https://perso.liris.cnrs.fr/pierre-antoine.champin/">P.A. Champin</a></p>
|
||||
<p>
|
||||
Snek by <a href="https://codinget.me">Codinget</a> on <a href="https://gitdab.com/Codinget/Snek">GitDab</a>
|
||||
<span class="serverless loaded">running without Nodejs backend</span>
|
||||
</p>
|
||||
<p>
|
||||
Original <a href="https://perso.liris.cnrs.fr/pierre-antoine.champin/enseignement/intro-js/s6.html">subject</a> by <a href="https://perso.liris.cnrs.fr/pierre-antoine.champin/">P.A. Champin</a>
|
||||
</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -8,6 +8,14 @@ const assetSpecs=[
|
||||
{ name: 'flammable', filename: 'oil32.png', type: 'image' },
|
||||
{ name: 'hole', filename: 'hole-ts.png', type: 'image' },
|
||||
{ name: 'fire', filename: 'fire-anim.png', type: 'image' },
|
||||
{ name: 'portalA', filename: 'portal-a-anim.png', type: 'image' },
|
||||
{ name: 'portalB', filename: 'portal-b-anim.png', type: 'image' },
|
||||
{ name: 'portalC', filename: 'portal-c-anim.png', type: 'image' },
|
||||
{ name: 'portalD', filename: 'portal-d-anim.png', type: 'image' },
|
||||
{ name: 'key', filename: 'key32.png', type: 'image' },
|
||||
{ name: 'door', filename: 'door32.png', type: 'image' },
|
||||
{ name: 'switch', filename: 'switch-ts.png', type: 'image' },
|
||||
{ name: 'spikes', filename: 'spikes-ts.png', type: 'image' },
|
||||
{ name: 'snake', filename: 'snake.json', type: 'json' },
|
||||
{ name: 'levelList', filename: 'levelList.json', type: 'json' },
|
||||
{ name: 'config', filename: 'config.json', type: 'json' },
|
||||
@@ -15,10 +23,16 @@ const assetSpecs=[
|
||||
];
|
||||
|
||||
const tasks=[
|
||||
{ from: 'hole', type: 'tileset', steps: 3, tiles: ['base', 'ul', 'dr', 'dl', 'ur', 'l', 'r', 'd', 'u'] },
|
||||
{ from: 'fire', type: 'animation', steps: 6 },
|
||||
{ from: 'hole', type: 'tileset', steps: 1, tiles: ['base', 'ul', 'dr', 'dl', 'ur', 'l', 'r', 'd', 'u'] },
|
||||
{ from: 'fire', type: 'animation', steps: 3 },
|
||||
{ from: 'portalA', type: 'animation', steps: 3 },
|
||||
{ from: 'portalB', type: 'animation', steps: 3 },
|
||||
{ from: 'portalC', type: 'animation', steps: 3 },
|
||||
{ from: 'portalD', type: 'animation', steps: 3 },
|
||||
{ from: 'superFruit', type: 'animation', steps: 5 },
|
||||
{ from: 'decayFruit', type: 'animation', steps: 5 }
|
||||
{ from: 'decayFruit', type: 'animation', steps: 5 },
|
||||
{ from: 'switch', type: 'tileset', steps: 1, tiles: ['on', 'off'] },
|
||||
{ from: 'spikes', type: 'tileset', steps: 1, tiles: ['off', 'on'] }
|
||||
];
|
||||
|
||||
const cvs=document.createElement('canvas');
|
||||
@@ -28,8 +42,8 @@ cvs.classList.add('progressBar');
|
||||
cvs.classList.add('hiddenBottom');
|
||||
|
||||
const bar=new ProgressBar(assetSpecs.length*2+tasks.reduce((a, t) => a+t.steps, 0));
|
||||
bar.addUpdateListener(() => bar.draw(cvs));
|
||||
bar.draw(cvs);
|
||||
bar.addUpdateListener(() => bar.draw(cvs, '#fba49b', '#930a16'));
|
||||
bar.draw(cvs, '#fba49b', '#930a16');
|
||||
|
||||
document.body.appendChild(cvs);
|
||||
setTimeout(() => cvs.classList.remove('hiddenBottom'), 0);
|
||||
|
||||
@@ -22,7 +22,7 @@ class ConfigEditor extends Popup {
|
||||
let id='cfgInput-'+(lastCEId++)+'-'+key.replace(/\./g, '-');
|
||||
let label=span.appendChild(document.createElement('label'));
|
||||
label.innerText=data.name;
|
||||
label.title=key;
|
||||
if(config.getB('debug')) label.title=key;
|
||||
|
||||
let input;
|
||||
if(data.type=='boolean') {
|
||||
@@ -50,6 +50,11 @@ class ConfigEditor extends Popup {
|
||||
}
|
||||
input.value=config.getN(key);
|
||||
input.addEventListener('change', () => config.set(key, input.value));
|
||||
} else if(data.type=='string') {
|
||||
input=document.createElement('input');
|
||||
input.type='text';
|
||||
input.value=config.getS(key);
|
||||
input.addEventListener('change', () => config.set(key, input.value));
|
||||
}
|
||||
|
||||
input.setAttribute('id', id);
|
||||
@@ -58,22 +63,34 @@ class ConfigEditor extends Popup {
|
||||
this.addContent(span);
|
||||
|
||||
if(data.excludes) {
|
||||
const setEnabled=() =>
|
||||
const setEnabled=() => {
|
||||
input.disabled=
|
||||
data.excludes
|
||||
.some(key => config.getB(key))
|
||||
.some(key => config.getB(key));
|
||||
input.title=input.disabled?`Disable '${data.excludes.map(k => metaConfig[k].name).join('\', \'')}' to enable`:'';
|
||||
};
|
||||
|
||||
setEnabled();
|
||||
data.excludes.forEach(key => {
|
||||
let c=config.watchB(key, setEnabled);
|
||||
this.watchers.push([key, c]);
|
||||
});
|
||||
}
|
||||
if(data.parent) {
|
||||
input.disabled=!config.getB(data.parent);
|
||||
let c=config.watchB(data.parent, (k, v) => input.disabled=!v);
|
||||
} else if(data.parent) {
|
||||
const setEnabled=() => {
|
||||
input.disabled=!config.getB(data.parent);
|
||||
input.title=input.disabled?`Enable '${metaConfig[data.parent].name}' to enable`:'';
|
||||
};
|
||||
|
||||
setEnabled();
|
||||
let c=config.watchB(data.parent, setEnabled);
|
||||
this.watchers.push([data.parent, c]);
|
||||
}
|
||||
if(data.needsBackend) {
|
||||
if(window.serverless) {
|
||||
input.disabled=true;
|
||||
input.title="Needs backend";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
const Popup=require('popup');
|
||||
const levels=require('levels');
|
||||
const config=require('config');
|
||||
|
||||
const upload=async (mode, win, snek) => {
|
||||
if(!win && !snek.rules.uploadOnDeath) return;
|
||||
if(window.serverless) return;
|
||||
|
||||
const username=config.getS('player.name');
|
||||
const score=snek.score;
|
||||
const length=snek.length;
|
||||
const time=snek.endPlayTime;
|
||||
const speed=snek.speed;
|
||||
|
||||
const rst=await fetch('api/leaderboards/'+mode, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
score, length,
|
||||
time, speed
|
||||
})
|
||||
});
|
||||
const dat=await rst.json();
|
||||
if(!dat.ok) console.error(rst.err);
|
||||
};
|
||||
|
||||
const show=async (mode='speedrun/1', page=1) => {
|
||||
let popup=new Popup("Leaderboards: "+mode);
|
||||
|
||||
const [category, id]=mode.split('/');
|
||||
let modes=[];
|
||||
(() => {
|
||||
Object.keys(window.levelList).forEach(cat => {
|
||||
window.levelList[cat].levels.forEach(lvl => {
|
||||
modes.push(cat+'/'+lvl);
|
||||
});
|
||||
});
|
||||
})();
|
||||
const prevMode=() => {
|
||||
let idx=modes.indexOf(mode);
|
||||
return modes[idx-1]||modes[modes.length-1];
|
||||
};
|
||||
const nextMode=() => {
|
||||
let idx=modes.indexOf(mode);
|
||||
return modes[idx+1]||modes[0];
|
||||
};
|
||||
|
||||
const rules=await levels.getRules(category, id);
|
||||
const sort=rules.leaderboardsSort;
|
||||
const rst=await fetch('api/leaderboards/'+mode+'?sort='+sort+'&page='+page+'&results=10');
|
||||
const {ok, data, err}=await rst.json();
|
||||
|
||||
popup.buttons.close="Close";
|
||||
popup.buttons.modeP="Previous mode";
|
||||
popup.buttons.modeN="Next mode";
|
||||
popup.large=true;
|
||||
popup.animation=false;
|
||||
|
||||
if(ok) {
|
||||
popup.addStrong("Page "+page);
|
||||
if(data.length==10) popup.buttons.next="Next page";
|
||||
if(page>1) popup.buttons.prev="Previous page";
|
||||
|
||||
if(data.length==0) {
|
||||
popup.addEm("No data");
|
||||
} else {
|
||||
const rpad=(n, digits=2, pad=' ') =>
|
||||
((''+n).length>=digits)?(''+n):(rpad(pad+n, digits, pad));
|
||||
|
||||
popup.addTable(data.map(({username, score, length, speed, time}, i) => {
|
||||
return {
|
||||
rank: '#'+(i+(page-1)*10+1),
|
||||
username,
|
||||
score: score+'pts',
|
||||
length,
|
||||
speed: speed+'tps',
|
||||
time: rpad(Math.floor(time/60000), 2, '0')+
|
||||
':'+rpad(Math.floor(time/1000)%60, 2, '0')+
|
||||
':'+rpad(time%1000, 3, '0')
|
||||
};
|
||||
}), [
|
||||
'rank',
|
||||
'username',
|
||||
'score',
|
||||
'length',
|
||||
'speed',
|
||||
'time'
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
popup.addStrong("Error loading leaderboards");
|
||||
popup.addEm(err);
|
||||
}
|
||||
|
||||
Popup.dismiss();
|
||||
const verb=await popup.display();
|
||||
if(verb=='next') return show(mode, page+1);
|
||||
else if(verb=='prev') return show(mode, page-1);
|
||||
else if(verb=='modeP') return show(prevMode());
|
||||
else if(verb=='modeN') return show(nextMode());
|
||||
location.hash='';
|
||||
};
|
||||
|
||||
return module.exports={
|
||||
upload, show
|
||||
};
|
||||
@@ -7,12 +7,37 @@ const get=async filename => {
|
||||
return cache[filename]=json;
|
||||
};
|
||||
|
||||
const getInfo=(category, id) => {
|
||||
const cat=levelList[category];
|
||||
id=''+id;
|
||||
|
||||
const displayName=cat.levelDisplay
|
||||
.replace(/<n>/g, id)
|
||||
.replace(/<l>/g, id.toLowerCase());
|
||||
const fileName=cat.levelFilename
|
||||
.replace(/<n>/g, id)
|
||||
.replace(/<l>/g, id.toLowerCase());
|
||||
const levelString=category+'/'+id+'/'+fileName;
|
||||
|
||||
return {
|
||||
displayName,
|
||||
fileName,
|
||||
levelString
|
||||
};
|
||||
};
|
||||
|
||||
const getRules=async (category, id) => {
|
||||
const {fileName}=getInfo(category, id);
|
||||
const json=await get(fileName);
|
||||
return Object.assign({}, window.levelList[category].rules, json.rules);
|
||||
};
|
||||
|
||||
const clearCache=() =>
|
||||
Object
|
||||
.keys(cache)
|
||||
.forEach(key => delete cache[key]);
|
||||
|
||||
return module.exports={
|
||||
get,
|
||||
get, getRules, getInfo,
|
||||
clearCache
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
const input=require('input');
|
||||
const levels=require('levels');
|
||||
const config=require('config');
|
||||
const leaderboards=require('leaderboards');
|
||||
|
||||
// get a known state
|
||||
await new Promise(ok => assets.onReady(ok));
|
||||
@@ -20,13 +21,29 @@
|
||||
const hud=main.querySelector('#hud');
|
||||
|
||||
// load data from server
|
||||
const levelList=assets.get('levelList');
|
||||
const levelList=window.levelList=assets.get('levelList');
|
||||
|
||||
// detect if we're running with a server
|
||||
const serverless=window.serverless=await (async() => {
|
||||
const res=await fetch('api/has-nodejs');
|
||||
if(!res.ok) return true;
|
||||
const msg=await res.json();
|
||||
return msg!='yes';
|
||||
})();
|
||||
if(serverless) {
|
||||
document.body.classList.add('serverless');
|
||||
} else {
|
||||
document.body.classList.add('server');
|
||||
}
|
||||
|
||||
// flag the body as loaded
|
||||
document.body.classList.remove('loading');
|
||||
|
||||
// get our global variables
|
||||
let currentGame=null;
|
||||
|
||||
// forward-declare functions
|
||||
let resizeCanvas, getLevel, startGame, stopGame, handleWin, handleDeath, menu, help, settings, restart;
|
||||
let resizeCanvas, startGame, stopGame, handleWin, handleDeath, menu, help, settings, showLeaderboards, restart, updateHud;
|
||||
|
||||
// handle window resize and fullscreen
|
||||
resizeCanvas=() => {
|
||||
@@ -41,6 +58,7 @@
|
||||
resizeCanvas();
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
window.addEventListener('keydown', async e => {
|
||||
if(e.target.tagName.toLowerCase()=='input') return;
|
||||
if(e.key=='f') {
|
||||
if(document.fullscreenElement) await document.exitFullscreen();
|
||||
else await main.requestFullscreen();
|
||||
@@ -48,26 +66,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
// get a level for a category and an id
|
||||
getLevel=(category, id) => {
|
||||
const cat=levelList[category];
|
||||
id=''+id;
|
||||
|
||||
const displayName=cat.levelDisplay
|
||||
.replace(/<n>/g, id)
|
||||
.replace(/<l>/g, id.toLowerCase());
|
||||
const fileName=cat.levelFilename
|
||||
.replace(/<n>/g, id)
|
||||
.replace(/<l>/g, id.toLowerCase());
|
||||
const levelString=category+'/'+id+'/'+fileName;
|
||||
|
||||
return {
|
||||
displayName,
|
||||
fileName,
|
||||
levelString
|
||||
};
|
||||
};
|
||||
|
||||
// buid menu from level list
|
||||
Object.keys(levelList).forEach(category => {
|
||||
const cat=levelList[category];
|
||||
@@ -81,7 +79,7 @@
|
||||
|
||||
const ul=section.appendChild(document.createElement('ul'));
|
||||
cat.levels.forEach((level, i) => {
|
||||
const {displayName, fileName, levelString}=getLevel(category, level);
|
||||
const {displayName, fileName, levelString}=levels.getInfo(category, level);
|
||||
const li=ul.appendChild(document.createElement('li'));
|
||||
const a=li.appendChild(document.createElement('a'));
|
||||
a.href='#'+levelString;
|
||||
@@ -106,6 +104,12 @@
|
||||
}
|
||||
};
|
||||
|
||||
// display the leaderboards
|
||||
showLeaderboards=() => {
|
||||
stopGame();
|
||||
leaderboards.show();
|
||||
};
|
||||
|
||||
// start a new game
|
||||
startGame=async (category, levelId, filename) => {
|
||||
// stop any running games and clear popups
|
||||
@@ -126,6 +130,7 @@
|
||||
if(evt=='tick') {
|
||||
input.framefn();
|
||||
snek.handleInputs(input.inputs);
|
||||
updateHud();
|
||||
} else if(evt=='win') {
|
||||
handleWin(snek);
|
||||
} else if(evt=='die') {
|
||||
@@ -191,13 +196,17 @@
|
||||
// fetch userdata from the game
|
||||
const {category, levelId, filename}=snek.userdata;
|
||||
|
||||
// upload scores
|
||||
if(config.getB('player.leaderboards')) leaderboards.upload(category+'/'+levelId, true, snek);
|
||||
|
||||
// create and configure popup
|
||||
let popup=new Popup("Finished!");
|
||||
popup.addStrong("You won!");
|
||||
popup.addContent({
|
||||
"Time": snek.playTime/1000+'s',
|
||||
"Time": snek.endPlayTime/1000+'s',
|
||||
"Score": snek.score,
|
||||
"Final length": snek.length
|
||||
"Final length": snek.length,
|
||||
"Final speed": snek.speed+'tps'
|
||||
});
|
||||
popup.buttons={
|
||||
retry: "Retry",
|
||||
@@ -219,7 +228,7 @@
|
||||
} else if(result=='next') {
|
||||
const {category, levelId}=snek.userdata;
|
||||
let nextId=(+levelId)+1;
|
||||
let {levelString}=getLevel(category, nextId)
|
||||
let {levelString}=levels.getInfo(category, nextId)
|
||||
location.hash=levelString;
|
||||
}
|
||||
};
|
||||
@@ -229,13 +238,21 @@
|
||||
// hide the HUD
|
||||
hud.classList.add('hidden');
|
||||
|
||||
// fetch userdata from the game
|
||||
const {category, levelId, filename}=snek.userdata;
|
||||
|
||||
// upload scores
|
||||
if(config.getB('player.leaderboards')) leaderboards.upload(category+'/'+levelId, false, snek);
|
||||
|
||||
// create and configure popup
|
||||
let popup=new Popup("Finished!");
|
||||
popup.addStrong("You died...");
|
||||
popup.addStrong(config.getS('player.name')+' '+snek.death.message);
|
||||
popup.addEm('('+config.getS('player.name')+' '+snek.death.reason+')');
|
||||
popup.addContent({
|
||||
"Time": snek.playTime/1000+'s',
|
||||
"Time": snek.endPlayTime/1000+'s',
|
||||
"Score": snek.score,
|
||||
"Final length": snek.length
|
||||
"Final length": snek.length,
|
||||
"Final speed": snek.speed+'tps'
|
||||
});
|
||||
popup.buttons={
|
||||
retry: "Retry",
|
||||
@@ -247,13 +264,34 @@
|
||||
|
||||
// act on it
|
||||
if(result=='retry') {
|
||||
const {category, levelId, filename}=snek.userdata;
|
||||
startGame(category, levelId, filename);
|
||||
} else if(result=='menu') {
|
||||
location.hash='menu';
|
||||
}
|
||||
};
|
||||
|
||||
// draw status hud
|
||||
updateHud=() => {
|
||||
// stay safe
|
||||
if(!currentGame) return;
|
||||
|
||||
// get the actual elements
|
||||
const speedDisplay=document.querySelector('#hud .speed');
|
||||
const scoreDisplay=document.querySelector('#hud .score');
|
||||
const timeDisplay=document.querySelector('#hud .time');
|
||||
|
||||
// rpad is useful
|
||||
const rpad=(n, digits=2, pad=' ') =>
|
||||
((''+n).length>=digits)?(''+n):(rpad(pad+n, digits, pad));
|
||||
|
||||
// actually do the hud
|
||||
speedDisplay.innerText=rpad(currentGame.speed, 2, '0')+'tps';
|
||||
scoreDisplay.innerText=currentGame.score;
|
||||
timeDisplay.innerText=rpad(Math.floor(currentGame.playTime/60000), 2, '0')+
|
||||
':'+rpad(Math.floor(currentGame.playTime/1000)%60, 2, '0')+
|
||||
':'+rpad(currentGame.playTime%1000, 3, '0');
|
||||
};
|
||||
|
||||
// quick restart
|
||||
restart=() => {
|
||||
if(currentGame && currentGame.playing) {
|
||||
@@ -278,6 +316,7 @@
|
||||
if(hash=='' || hash=='menu') return menu();
|
||||
else if(hash=='help') return help();
|
||||
else if(hash=='settings') return settings();
|
||||
else if(hash=='leaderboards') return showLeaderboards();
|
||||
|
||||
const [_, category, levelId, filename]=location.hash.match(/([a-zA-Z0-9_-]+?)\/([a-zA-Z0-9_-]+?)\/(.+)/);
|
||||
startGame(category, levelId, filename);
|
||||
|
||||
@@ -17,6 +17,7 @@ const objToDom=obj => {
|
||||
return ul;
|
||||
} else {
|
||||
let table=document.createElement('table');
|
||||
table.classList.add('dual');
|
||||
Object
|
||||
.keys(obj)
|
||||
.forEach(key => {
|
||||
@@ -34,6 +35,7 @@ class Popup {
|
||||
this.content=content.map(objToDom);
|
||||
this.buttons={...buttons};
|
||||
this.large=large;
|
||||
this.animation=true;
|
||||
}
|
||||
|
||||
addContent(cnt) {
|
||||
@@ -53,6 +55,25 @@ class Popup {
|
||||
hn.innerText=cnt;
|
||||
this.content.push(hn);
|
||||
}
|
||||
addTable(data, heading=Object.keys(data)) {
|
||||
let table=document.createElement('table');
|
||||
table.classList.add('table');
|
||||
let thead=table.appendChild(document.createElement('thead'));
|
||||
let headingRow=thead.appendChild(document.createElement('tr'));
|
||||
heading.forEach(key => {
|
||||
let th=headingRow.appendChild(document.createElement('th'));
|
||||
th.innerText=key;
|
||||
});
|
||||
let tbody=table.appendChild(document.createElement('tbody'));
|
||||
data.forEach(row => {
|
||||
let tr=tbody.appendChild(document.createElement('tr'));
|
||||
heading.forEach(key => {
|
||||
let td=tr.appendChild(document.createElement('td'));
|
||||
td.innerText=row[key];
|
||||
});
|
||||
});
|
||||
this.content.push(table);
|
||||
}
|
||||
|
||||
async display(parent=document.body) {
|
||||
let outer=document.createElement('div');
|
||||
@@ -60,6 +81,7 @@ class Popup {
|
||||
let popup=outer.appendChild(document.createElement('div'));
|
||||
popup.classList.add('content');
|
||||
if(this.large) popup.classList.add('large');
|
||||
if(this.animation) outer.classList.add('animation');
|
||||
|
||||
let title=popup.appendChild(document.createElement('h1'));
|
||||
title.innerText=this.title;
|
||||
|
||||
@@ -34,7 +34,7 @@ class ProgressBar {
|
||||
ctx.fillRect(0, 0, canvas.width*this.completeCount/this.taskCount, canvas.height);
|
||||
ctx.fillStyle=textColor;
|
||||
ctx.textAlign='center';
|
||||
ctx.textBaseline='center';
|
||||
ctx.textBaseline='middle';
|
||||
ctx.font=`${canvas.height/2}px 'Fira Code'`;
|
||||
ctx.fillText(this.percent+'%', canvas.width/2, canvas.height/2);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
const assets=require('assets');
|
||||
const config=require('config');
|
||||
const {tiles: T}=require('tiles');
|
||||
|
||||
// declare our tiles
|
||||
let snake,
|
||||
wall, hole,
|
||||
fire, flammable,
|
||||
fruit, superFruit, decayFruit,
|
||||
portalA, portalB, portalC, portalD,
|
||||
key, door,
|
||||
switchTile, spikes;
|
||||
|
||||
// load our tiles
|
||||
assets.onReady(() => {
|
||||
wall=assets.get('wall');
|
||||
hole=assets.get('hole');
|
||||
fire=assets.get('fire');
|
||||
flammable=assets.get('flammable');
|
||||
superFruit=assets.get('superFruit');
|
||||
decayFruit=assets.get('decayFruit');
|
||||
portalA=assets.get('portalA');
|
||||
portalB=assets.get('portalB');
|
||||
portalC=assets.get('portalC');
|
||||
portalD=assets.get('portalD');
|
||||
key=assets.get('key');
|
||||
door=assets.get('door');
|
||||
switchTile=assets.get('switch');
|
||||
spikes=assets.get('spikes');
|
||||
snake=assets.get('snake');
|
||||
fruit=assets.get('fruit');
|
||||
});
|
||||
|
||||
const draw=(game, canvas=game.canvas, ctx=canvas.getContext('2d')) => {
|
||||
// clear the canvas, because it's easier than having to deal with everything
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// get the cell size and offset
|
||||
const cellSize=Math.min(
|
||||
canvas.width/game.dimensions[0],
|
||||
canvas.height/game.dimensions[1]
|
||||
);
|
||||
const offsetX=(canvas.width-cellSize*game.dimensions[0])/2;
|
||||
const offsetY=(canvas.height-cellSize*game.dimensions[1])/2;
|
||||
|
||||
// tile draw functions
|
||||
const putTile=(x, y, tile) => ctx.drawImage(
|
||||
tile,
|
||||
offsetX+cellSize*x,
|
||||
offsetY+cellSize*y,
|
||||
cellSize,
|
||||
cellSize
|
||||
);
|
||||
const putTileAnim=(x, y, tile) => putTile(x, y, tile[
|
||||
Math.floor(Date.now()/1000*60+x+y)%tile.length
|
||||
]);
|
||||
const putTileAnimPercent=(x, y, tile, percent) => putTile(x, y, tile[
|
||||
Math.min(Math.round(percent*tile.length), tile.length-1)
|
||||
]);
|
||||
|
||||
// adjascence check
|
||||
const checkAdj=(x, y) => {
|
||||
let adj={};
|
||||
adj.u=game.world[x][y-1];
|
||||
adj.d=game.world[x][y+1];
|
||||
adj.l=(game.world[x-1] || [])[y];
|
||||
adj.r=(game.world[x+1] || [])[y];
|
||||
adj.ul=(game.world[x-1] || [])[y-1];
|
||||
adj.ur=(game.world[x+1] || [])[y-1];
|
||||
adj.dl=(game.world[x-1] || [])[y+1];
|
||||
adj.dr=(game.world[x+1] || [])[y+1];
|
||||
return adj;
|
||||
};
|
||||
|
||||
// draw a grid/checkerboard if requested
|
||||
if(config.getS('appearance.grid')=='grid') {
|
||||
ctx.strokeStyle='rgba(0, 0, 0, 50%)';
|
||||
ctx.lineCap='square';
|
||||
ctx.lineWidth=1;
|
||||
ctx.beginPath();
|
||||
for(let x=1; x<game.dimensions[0]; x++) {
|
||||
ctx.moveTo(offsetX+x*cellSize, offsetY);
|
||||
ctx.lineTo(offsetX+x*cellSize, canvas.height-offsetY);
|
||||
}
|
||||
for(let y=1; y<game.dimensions[1]; y++) {
|
||||
ctx.moveTo(offsetX, offsetY+y*cellSize);
|
||||
ctx.lineTo(canvas.width-offsetX, offsetY+y*cellSize);
|
||||
}
|
||||
ctx.stroke();
|
||||
} else if(config.getS('appearance.grid')=='checkerboard') {
|
||||
ctx.fillStyle='rgba(0, 0, 0, 10%)';
|
||||
for(let x=0; x<game.dimensions[0]; x++) {
|
||||
for(let y=(x+1)%2; y<game.dimensions[1]; y+=2) {
|
||||
ctx.fillRect(offsetX+x*cellSize, offsetY+y*cellSize, cellSize, cellSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// draw our tiles
|
||||
for(let x=0; x<game.dimensions[0]; x++) {
|
||||
for(let y=0; y<game.dimensions[1]; y++) {
|
||||
switch(game.world[x][y]) {
|
||||
case T.WALL:
|
||||
putTile(x, y, wall);
|
||||
break;
|
||||
|
||||
case T.FIRE:
|
||||
putTileAnim(x, y, fire);
|
||||
break;
|
||||
|
||||
case T.HOLE:
|
||||
case T.HOLE_S: {
|
||||
putTile(x, y, hole.base);
|
||||
let adj=checkAdj(x, y);
|
||||
Object
|
||||
.keys(adj)
|
||||
.filter(k => adj[k]==T.HOLE || adj[k]==T.HOLE_S)
|
||||
.forEach(k => putTile(x, y, hole[k]));
|
||||
} break;
|
||||
|
||||
case T.FLAMMABLE:
|
||||
case T.FLAMMABLE_S:
|
||||
putTile(x, y, flammable);
|
||||
break;
|
||||
|
||||
case T.SUPER_FOOD:
|
||||
putTileAnim(x, y, superFruit);
|
||||
break;
|
||||
|
||||
case T.PORTAL_A:
|
||||
case T.PORTAL_A_S:
|
||||
putTileAnim(x, y, portalA);
|
||||
break;
|
||||
case T.PORTAL_B:
|
||||
case T.PORTAL_B_S:
|
||||
putTileAnim(x, y, portalB);
|
||||
break;
|
||||
case T.PORTAL_C:
|
||||
case T.PORTAL_C_S:
|
||||
putTileAnim(x, y, portalC);
|
||||
break;
|
||||
case T.PORTAL_D:
|
||||
case T.PORTAL_D_S:
|
||||
putTileAnim(x, y, portalD);
|
||||
break;
|
||||
|
||||
case T.KEY:
|
||||
putTile(x, y, key);
|
||||
break;
|
||||
case T.DOOR:
|
||||
putTile(x, y, door);
|
||||
break;
|
||||
|
||||
case T.SWITCH_ON:
|
||||
case T.SWITCH_ON_S:
|
||||
putTile(x, y, switchTile.on);
|
||||
break;
|
||||
case T.SWITCH_OFF:
|
||||
case T.SWITCH_OFF_S:
|
||||
putTile(x, y, switchTile.off);
|
||||
break;
|
||||
|
||||
case T.SPIKES_ON:
|
||||
putTile(x, y, spikes.on);
|
||||
break;
|
||||
case T.SPIKES_OFF:
|
||||
case T.SPIKES_OFF_S:
|
||||
putTile(x, y, spikes.off);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// draw our decaying fruits (they have more information than just XY, so they need to be drawn here
|
||||
game.decayFood.forEach(([x, y, birth]) =>
|
||||
putTileAnimPercent(x, y, decayFruit, (game.playTime-birth)/2000)
|
||||
);
|
||||
|
||||
// draw the lines between portals
|
||||
if(Object.keys(game.portals).length) {
|
||||
ctx.strokeStyle='rgba(128, 128, 128, 20%)';
|
||||
ctx.lineCap='round';
|
||||
ctx.lineWidth=cellSize*.15;
|
||||
const drawTunnel=([xa, ya], [xb, yb]) => {
|
||||
const angle=(Math.floor(Date.now()/10)%360)*(Math.PI/180);
|
||||
for(let i=0; i<=1; i++) {
|
||||
const dx=cellSize/3*Math.cos(angle+i*Math.PI);
|
||||
const dy=cellSize/3*Math.sin(angle+i*Math.PI);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(
|
||||
offsetX+cellSize*(xa+1/2)+dx,
|
||||
offsetY+cellSize*(ya+1/2)+dy
|
||||
);
|
||||
ctx.lineTo(
|
||||
offsetX+cellSize*(xb+1/2)+dx,
|
||||
offsetY+cellSize*(yb+1/2)+dy
|
||||
);
|
||||
ctx.stroke();
|
||||
}
|
||||
};
|
||||
if(game.portals.a && game.portals.b) drawTunnel(game.portals.a, game.portals.b);
|
||||
if(game.portals.c && game.portals.d) drawTunnel(game.portals.c, game.portals.d);
|
||||
}
|
||||
|
||||
// draw our snake (it gets drawn completely differently, so here it goes)
|
||||
{
|
||||
ctx.fillStyle=snake.color;
|
||||
ctx.strokeStyle=snake.color;
|
||||
ctx.lineCap=snake.cap;
|
||||
ctx.lineJoin=snake.join;
|
||||
ctx.lineWidth=cellSize*snake.tailSize;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(
|
||||
offsetX+cellSize*(game.snake[0][0]+1/2),
|
||||
offsetY+cellSize*(game.snake[0][1]+1/2),
|
||||
cellSize/2*snake.headSize,
|
||||
cellSize/2*snake.headSize,
|
||||
0,
|
||||
0,
|
||||
Math.PI*2
|
||||
);
|
||||
ctx.fill();
|
||||
|
||||
ctx.beginPath();
|
||||
game.snake.forEach(([x, y], i, a) => {
|
||||
ctx.lineTo(
|
||||
offsetX+cellSize*(x+1/2),
|
||||
offsetY+cellSize*(y+1/2)
|
||||
);
|
||||
if(i!=0 && Math.hypot(x-a[i-1][0], y-a[i-1][1])>1) {
|
||||
ctx.lineWidth=cellSize*snake.tailWrapSize;
|
||||
} else {
|
||||
ctx.lineWidth=cellSize*snake.tailSize;
|
||||
}
|
||||
ctx.stroke();
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(
|
||||
offsetX+cellSize*(x+1/2),
|
||||
offsetY+cellSize*(y+1/2)
|
||||
);
|
||||
});
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// our fruit has a nice animation to it between .8 and 1.2 scale
|
||||
{
|
||||
const ms=Date.now();
|
||||
const fruitScale=Math.sin(ms/400*Math.PI)*.2+1
|
||||
game.fruits.forEach(([x, y]) => {
|
||||
ctx.drawImage(
|
||||
fruit,
|
||||
offsetX+cellSize*x+(1-fruitScale)*cellSize/2,
|
||||
offsetY+cellSize*y+(1-fruitScale)*cellSize/2,
|
||||
cellSize*fruitScale,
|
||||
cellSize*fruitScale
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// show the timer
|
||||
if(game.rules.winCondition=='time') {
|
||||
if(config.getS('appearance.timer')=='border' || config.getS('appearance.timer')=='both') {
|
||||
let remaining=(game.rules.gameDuration-game.playTime)/game.rules.gameDuration;
|
||||
const w=game.dimensions[0]*cellSize;
|
||||
const h=game.dimensions[1]*cellSize;
|
||||
const p=w*2+h*2;
|
||||
|
||||
const wp=w/p;
|
||||
const hp=h/p;
|
||||
|
||||
const pdst=(st, ed, frac) =>
|
||||
(ed-st)*frac+st;
|
||||
|
||||
ctx.strokeStyle='#930a16';
|
||||
ctx.lineJoin='miter';
|
||||
ctx.lineCap='round';
|
||||
ctx.lineWidth=5;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(canvas.width/2, offsetY+2);
|
||||
|
||||
let sp=Math.min(wp/2, remaining);
|
||||
remaining-=sp;
|
||||
ctx.lineTo(pdst(canvas.width/2, w+offsetX-2, sp/wp*2), offsetY+2);
|
||||
if(remaining) {
|
||||
sp=Math.min(hp, remaining);
|
||||
remaining-=sp;
|
||||
ctx.lineTo(w+offsetX-2, pdst(offsetY+2, offsetY+h-2, sp/hp));
|
||||
}
|
||||
if(remaining) {
|
||||
sp=Math.min(wp, remaining);
|
||||
remaining-=sp;
|
||||
ctx.lineTo(pdst(w+offsetX-2, offsetX+2, sp/wp), offsetY+h-2);
|
||||
}
|
||||
if(remaining) {
|
||||
sp=Math.min(hp, remaining);
|
||||
remaining-=sp;
|
||||
ctx.lineTo(offsetX+2, pdst(offsetY+h-2, offsetY+2, sp/hp));
|
||||
}
|
||||
if(remaining) {
|
||||
ctx.lineTo(pdst(offsetX+2, canvas.width/2, remaining/wp*2), offsetY+2);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
if(config.getS('appearance.timer')=='number' || config.getS('appearance.timer')=='both') {
|
||||
let remaining=''+Math.ceil((game.rules.gameDuration-game.playTime)/1000);
|
||||
while(remaining.length<(''+game.rules.gameDuration/1000).length) remaining='0'+remaining;
|
||||
|
||||
ctx.fillStyle='#930a16';
|
||||
ctx.textAlign='center';
|
||||
ctx.textBaseline='middle';
|
||||
ctx.font='4rem "Fira Code"';
|
||||
ctx.fillText(remaining, canvas.width/2, canvas.height/2);
|
||||
}
|
||||
}
|
||||
|
||||
// draw the border around our game area
|
||||
{
|
||||
ctx.fillStyle='black';
|
||||
ctx.fillRect(0, 0, canvas.width, offsetY);
|
||||
ctx.fillRect(0, 0, offsetX, canvas.height);
|
||||
ctx.fillRect(offsetX+cellSize*game.dimensions[0], 0, offsetX, canvas.height);
|
||||
ctx.fillRect(0, offsetY+cellSize*game.dimensions[1], canvas.width, offsetY);
|
||||
}
|
||||
};
|
||||
|
||||
return module.exports={
|
||||
draw
|
||||
};
|
||||
@@ -1,9 +1,18 @@
|
||||
const [EMPTY, FOOD, SUPER_FOOD, DECAY_FOOD, WALL, FIRE, FLAMMABLE, FLAMMABLE_S, HOLE, HOLE_S, SNAKE]=Array(255).keys();
|
||||
const {
|
||||
tiles: T,
|
||||
forChar,
|
||||
getType,
|
||||
isPortal,
|
||||
snakeVersion, nonSnakeVersion
|
||||
}=require('tiles');
|
||||
|
||||
class SnekGame {
|
||||
constructor(settings, canvas, rules) {
|
||||
// setup the delay
|
||||
this.delay=settings.delay;
|
||||
this.delay=settings.delay || Infinity;
|
||||
|
||||
// score starts at 0
|
||||
this.score=0;
|
||||
|
||||
// world is given in the level
|
||||
if(settings.world) { // explicitly
|
||||
@@ -13,18 +22,7 @@ class SnekGame {
|
||||
for(let x=0; x<this.world.length; x++) {
|
||||
this.world[x]=Array(settings.world.length);
|
||||
for(let y=0; y<this.world[x].length; y++) {
|
||||
this.world[x][y]=(() => {
|
||||
switch(settings.world[y][x]) {
|
||||
case ' ': return EMPTY;
|
||||
case 'f': return FOOD;
|
||||
case 'F': return SUPER_FOOD;
|
||||
case 'd': return DECAY_FOOD;
|
||||
case 'w': return WALL;
|
||||
case 'o': return HOLE;
|
||||
case 'i': return FIRE;
|
||||
case 'I': return FLAMMABLE;
|
||||
}
|
||||
})();
|
||||
this.world[x][y]=forChar(settings.world[y][x]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,22 +30,21 @@ class SnekGame {
|
||||
this.dimensions=[this.world.length, this.world[0].length];
|
||||
|
||||
// extract the fruits
|
||||
this.fruits=[];
|
||||
this.world
|
||||
.forEach((l, x) => l.forEach(
|
||||
(c, y) => {
|
||||
if(c==FOOD) this.fruits.push([x, y]);
|
||||
}
|
||||
));
|
||||
this.fruits=this.getTilesOfType(T.FOOD);
|
||||
|
||||
// extract the decaying fruits
|
||||
this.decayFood=[];
|
||||
this.world
|
||||
.forEach((l, x) => l.forEach(
|
||||
(c, y) => {
|
||||
if(c==DECAY_FOOD) this.decaying.push([x, y, 0]);
|
||||
}
|
||||
));
|
||||
this.decayFood=this.getTilesOfType(T.DECAY_FOOD);
|
||||
|
||||
// extract the portals
|
||||
this.portals={};
|
||||
this.world.forEach((l, x) =>
|
||||
l.forEach((c, y) => {
|
||||
if(c==T.PORTAL_A) this.portals.a=[x, y];
|
||||
if(c==T.PORTAL_B) this.portals.b=[x, y];
|
||||
if(c==T.PORTAL_C) this.portals.c=[x, y];
|
||||
if(c==T.PORTAL_D) this.portals.d=[x, y];
|
||||
})
|
||||
);
|
||||
} else { // dimension and objects
|
||||
|
||||
// get the dimensions
|
||||
@@ -57,37 +54,61 @@ class SnekGame {
|
||||
this.world=Array(settings.dimensions[0]);
|
||||
for(let i=0; i<settings.dimensions[0]; i++) {
|
||||
this.world[i]=Array(settings.dimensions[1]);
|
||||
this.world[i].fill(EMPTY);
|
||||
this.world[i].fill(T.EMPTY);
|
||||
}
|
||||
|
||||
// add the walls
|
||||
if(settings.walls) settings.walls.forEach(([x, y]) => this.world[x][y]=WALL);
|
||||
if(settings.walls) settings.walls.forEach(([x, y]) => this.world[x][y]=T.WALL);
|
||||
|
||||
// add the holes
|
||||
if(settings.holes) settings.holes.forEach(([x, y]) => this.world[x][y]=HOLE);
|
||||
if(settings.holes) settings.holes.forEach(([x, y]) => this.world[x][y]=T.HOLE);
|
||||
|
||||
// add the fires and flammable tiles
|
||||
if(settings.fires) settings.fires.forEach(([x, y]) => this.world[x][y]=FIRE);
|
||||
if(settings.flammable) settings.flammable.forEach(([x, y]) => this.world[x][y]=FLAMMABLE);
|
||||
if(settings.fires) settings.fires.forEach(([x, y]) => this.world[x][y]=T.FIRE);
|
||||
if(settings.flammable) settings.flammable.forEach(([x, y]) => this.world[x][y]=T.FLAMMABLE);
|
||||
|
||||
// add the food
|
||||
settings.food.forEach(([x, y]) => this.world[x][y]=FOOD);
|
||||
settings.food.forEach(([x, y]) => this.world[x][y]=T.FOOD);
|
||||
this.fruits=[...settings.food];
|
||||
|
||||
// add the super food
|
||||
if(settings.superFood) settings.superFood.forEach(([x, y]) => this.world[x][y]=SUPER_FOOD);
|
||||
if(settings.superFood) settings.superFood.forEach(([x, y]) => this.world[x][y]=T.SUPER_FOOD);
|
||||
|
||||
// add the decaying food
|
||||
if(settings.decayFood) {
|
||||
settings.decayFood.forEach(([x, y]) => this.world[x][y]=DECAY_FOOD);
|
||||
settings.decayFood.forEach(([x, y]) => this.world[x][y]=T.DECAY_FOOD);
|
||||
this.decayFood=settings.decayFood.map(([x, y]) => [x, y, 0]);
|
||||
} else {
|
||||
this.decayFood=[];
|
||||
}
|
||||
|
||||
// add the portals
|
||||
if(settings.portals) {
|
||||
if(settings.portals.a) this.world[settings.portals.a[0]][settings.portals.a[1]]=T.PORTAL_A;
|
||||
if(settings.portals.b) this.world[settings.portals.b[0]][settings.portals.b[1]]=T.PORTAL_B;
|
||||
if(settings.portals.c) this.world[settings.portals.c[0]][settings.portals.c[1]]=T.PORTAL_C;
|
||||
if(settings.portals.d) this.world[settings.portals.d[0]][settings.portals.d[1]]=T.PORTAL_D;
|
||||
this.portals={...settings.portals};
|
||||
} else {
|
||||
this.portals={};
|
||||
}
|
||||
|
||||
// add the keys
|
||||
if(settings.keys) settings.keys.forEach(([x, y]) => this.world[x][y]=T.KEY);
|
||||
|
||||
// add the doors
|
||||
if(settings.doors) settings.doors.forEach(([x, y]) => this.world[x][y]=T.DOOR);
|
||||
|
||||
// add the switches
|
||||
if(settings.switches) settings.switches.forEach(([x, y]) => this.world[x][y]=T.SWITCH_OFF);
|
||||
|
||||
// add the spikes
|
||||
if(settings.spikesOn) settings.spikesOn.forEach(([x, y]) => this.world[x][y]=T.SPIKES_ON);
|
||||
if(settings.spikesOff) settings.spikesOff.forEach(([x, y]) => this.world[x][y]=T.SPIKES_OFF);
|
||||
}
|
||||
|
||||
// add the snake to the world
|
||||
settings.snake.forEach(([x, y]) => this.world[x][y]=SNAKE);
|
||||
settings.snake.forEach(([x, y]) => this.world[x][y]=T.SNAKE);
|
||||
|
||||
|
||||
// get the head and initial direction
|
||||
@@ -121,14 +142,43 @@ class SnekGame {
|
||||
scoreSystem: 'fruit',
|
||||
fireTickSpeed: 10,
|
||||
autoSizeGrow: false,
|
||||
autoSpeedIncrease: false
|
||||
autoSpeedIncrease: false,
|
||||
timeFlow: true
|
||||
}, rules, settings.rules || {});
|
||||
|
||||
// reset direction if time doesn't flow
|
||||
if(!this.rules.timeFlow) {
|
||||
this.lastDirection=[0, 0];
|
||||
this.direction=[0, 0];
|
||||
}
|
||||
|
||||
// set score if move-based
|
||||
if(this.rules.scoreSystem=='moves') {
|
||||
this.score=this.rules.moveCount;
|
||||
}
|
||||
}
|
||||
|
||||
get playTime() {
|
||||
return Date.now()-this.firstStep;
|
||||
}
|
||||
|
||||
get speed() {
|
||||
return Math.round(1000/this.delay);
|
||||
}
|
||||
|
||||
getTile(x, y) {
|
||||
return (this.world[x]||[])[y];
|
||||
}
|
||||
getTileA([x, y]) {
|
||||
return (this.world[x]||[])[y];
|
||||
}
|
||||
putTile(x, y, t) {
|
||||
this.world[x][y]=t;
|
||||
}
|
||||
putTileA([x, y], t) {
|
||||
this.world[x][y]=t;
|
||||
}
|
||||
|
||||
getTilesOfType(type) {
|
||||
return this
|
||||
.world
|
||||
@@ -141,235 +191,16 @@ class SnekGame {
|
||||
)
|
||||
).flat();
|
||||
}
|
||||
replaceTilesOfType(type, newType) {
|
||||
this.world.forEach(l =>
|
||||
l.forEach((t, i) => {
|
||||
if(t==type) l[i]=newType;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
draw() {
|
||||
const assets=require('assets');
|
||||
const config=require('config');
|
||||
|
||||
// clear the canvas, because it's easier than having to deal with everything
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// get the cell size and offset
|
||||
const cellSize=Math.min(
|
||||
this.canvas.width/this.dimensions[0],
|
||||
this.canvas.height/this.dimensions[1]
|
||||
);
|
||||
const offsetX=(this.canvas.width-cellSize*this.dimensions[0])/2;
|
||||
const offsetY=(this.canvas.height-cellSize*this.dimensions[1])/2;
|
||||
|
||||
// draw a grid/checkerboard if requested
|
||||
if(config.getS('appearance.grid')=='grid') {
|
||||
this.ctx.strokeStyle='rgba(0, 0, 0, 50%)';
|
||||
this.ctx.lineCap='square';
|
||||
this.ctx.lineWidth=1;
|
||||
this.ctx.beginPath();
|
||||
for(let x=1; x<this.dimensions[0]; x++) {
|
||||
this.ctx.moveTo(offsetX+x*cellSize, offsetY);
|
||||
this.ctx.lineTo(offsetX+x*cellSize, this.canvas.height-offsetY);
|
||||
}
|
||||
for(let y=1; y<this.dimensions[1]; y++) {
|
||||
this.ctx.moveTo(offsetX, offsetY+y*cellSize);
|
||||
this.ctx.lineTo(this.canvas.width-offsetX, offsetY+y*cellSize);
|
||||
}
|
||||
this.ctx.stroke();
|
||||
} else if(config.getS('appearance.grid')=='checkerboard') {
|
||||
this.ctx.fillStyle='rgba(0, 0, 0, 10%)';
|
||||
for(let x=0; x<this.dimensions[0]; x++) {
|
||||
for(let y=(x+1)%2; y<this.dimensions[1]; y+=2) {
|
||||
this.ctx.fillRect(offsetX+x*cellSize, offsetY+y*cellSize, cellSize, cellSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// draw our tiles
|
||||
const wall=assets.get('wall');
|
||||
const hole=assets.get('hole');
|
||||
const fire=assets.get('fire');
|
||||
const flammable=assets.get('flammable');
|
||||
const superFruit=assets.get('superFruit');
|
||||
const decayFruit=assets.get('decayFruit');
|
||||
const putTile=(x, y, tile) => this.ctx.drawImage(
|
||||
tile,
|
||||
offsetX+cellSize*x,
|
||||
offsetY+cellSize*y,
|
||||
cellSize,
|
||||
cellSize
|
||||
);
|
||||
const putTileAnim=(x, y, tile) => putTile(x, y, tile[
|
||||
Math.floor(Date.now()/1000*60+x+y)%tile.length
|
||||
]);
|
||||
const putTileAnimPercent=(x, y, tile, percent) => putTile(x, y, tile[
|
||||
Math.min(Math.round(percent*tile.length), tile.length-1)
|
||||
]);
|
||||
const checkAdj=(x, y) => {
|
||||
let adj={};
|
||||
adj.u=this.world[x][y-1];
|
||||
adj.d=this.world[x][y+1];
|
||||
adj.l=(this.world[x-1] || [])[y];
|
||||
adj.r=(this.world[x+1] || [])[y];
|
||||
adj.ul=(this.world[x-1] || [])[y-1];
|
||||
adj.ur=(this.world[x+1] || [])[y-1];
|
||||
adj.dl=(this.world[x-1] || [])[y+1];
|
||||
adj.dr=(this.world[x+1] || [])[y+1];
|
||||
return adj;
|
||||
};
|
||||
for(let x=0; x<this.dimensions[0]; x++) {
|
||||
for(let y=0; y<this.dimensions[1]; y++) {
|
||||
switch(this.world[x][y]) {
|
||||
case WALL:
|
||||
putTile(x, y, wall);
|
||||
break;
|
||||
|
||||
case FIRE:
|
||||
putTileAnim(x, y, fire);
|
||||
break;
|
||||
|
||||
case HOLE:
|
||||
case HOLE_S: {
|
||||
putTile(x, y, hole.base);
|
||||
let adj=checkAdj(x, y);
|
||||
Object
|
||||
.keys(adj)
|
||||
.filter(k => adj[k]==HOLE || adj[k]==HOLE_S)
|
||||
.forEach(k => putTile(x, y, hole[k]));
|
||||
break;
|
||||
// technically, this works for all shapes
|
||||
// however, the tileset only handles convex shapes
|
||||
}
|
||||
|
||||
case FLAMMABLE:
|
||||
case FLAMMABLE_S:
|
||||
putTile(x, y, flammable);
|
||||
break;
|
||||
|
||||
case SUPER_FOOD:
|
||||
putTileAnim(x, y, superFruit);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// draw our decaying fruits (they have more information than just XY, so they need to be drawn here
|
||||
this.decayFood.forEach(([x, y, birth]) =>
|
||||
putTileAnimPercent(x, y, decayFruit, (this.playTime-birth)/2000)
|
||||
);
|
||||
|
||||
// draw our snake (it gets drawn completely differently, so here it goes)
|
||||
const snake=assets.get('snake');
|
||||
this.ctx.fillStyle=snake.color;
|
||||
this.ctx.strokeStyle=snake.color;
|
||||
this.ctx.lineCap=snake.cap;
|
||||
this.ctx.lineJoin=snake.join;
|
||||
this.ctx.lineWidth=cellSize*snake.tailSize;
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.ellipse(
|
||||
offsetX+cellSize*(this.snake[0][0]+1/2),
|
||||
offsetY+cellSize*(this.snake[0][1]+1/2),
|
||||
cellSize/2*snake.headSize,
|
||||
cellSize/2*snake.headSize,
|
||||
0,
|
||||
0,
|
||||
Math.PI*2
|
||||
);
|
||||
this.ctx.fill();
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.snake.forEach(([x, y], i, a) => {
|
||||
this.ctx.lineTo(
|
||||
offsetX+cellSize*(x+1/2),
|
||||
offsetY+cellSize*(y+1/2)
|
||||
);
|
||||
if(i!=0 && Math.hypot(x-a[i-1][0], y-a[i-1][1])>1) {
|
||||
this.ctx.lineWidth=cellSize*snake.tailWrapSize;
|
||||
} else {
|
||||
this.ctx.lineWidth=cellSize*snake.tailSize;
|
||||
}
|
||||
this.ctx.stroke();
|
||||
this.ctx.beginPath()
|
||||
this.ctx.moveTo(
|
||||
offsetX+cellSize*(x+1/2),
|
||||
offsetY+cellSize*(y+1/2)
|
||||
);
|
||||
});
|
||||
this.ctx.stroke();
|
||||
|
||||
// our fruit has a nice animation to it between .8 and 1.2 scale
|
||||
const ms=Date.now();
|
||||
const fruitScale=Math.sin(ms/400*Math.PI)*.2+1
|
||||
const fruit=assets.get('fruit');
|
||||
this.fruits.forEach(([x, y]) => {
|
||||
this.ctx.drawImage(
|
||||
fruit,
|
||||
offsetX+cellSize*x+(1-fruitScale)*cellSize/2,
|
||||
offsetY+cellSize*y+(1-fruitScale)*cellSize/2,
|
||||
cellSize*fruitScale,
|
||||
cellSize*fruitScale
|
||||
);
|
||||
});
|
||||
|
||||
// show the timer
|
||||
if(this.rules.winCondition=='time') {
|
||||
if(config.getS('appearance.timer')=='border' || config.getS('appearance.timer')=='both') {
|
||||
let remaining=(this.rules.gameDuration-this.playTime)/this.rules.gameDuration;
|
||||
const w=this.dimensions[0]*cellSize;
|
||||
const h=this.dimensions[1]*cellSize;
|
||||
const p=w*2+h*2;
|
||||
|
||||
const wp=w/p;
|
||||
const hp=h/p;
|
||||
|
||||
const pdst=(st, ed, frac) =>
|
||||
(ed-st)*frac+st;
|
||||
|
||||
this.ctx.strokeStyle='#930a16';
|
||||
this.ctx.lineJoin='miter';
|
||||
this.ctx.lineCap='round';
|
||||
this.ctx.lineWidth=5;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(this.canvas.width/2, offsetY+2);
|
||||
|
||||
let sp=Math.min(wp/2, remaining);
|
||||
remaining-=sp;
|
||||
this.ctx.lineTo(pdst(this.canvas.width/2, w+offsetX-2, sp/wp*2), offsetY+2);
|
||||
if(remaining) {
|
||||
sp=Math.min(hp, remaining);
|
||||
remaining-=sp;
|
||||
this.ctx.lineTo(w+offsetX-2, pdst(offsetY+2, offsetY+h-2, sp/hp));
|
||||
}
|
||||
if(remaining) {
|
||||
sp=Math.min(wp, remaining);
|
||||
remaining-=sp;
|
||||
this.ctx.lineTo(pdst(w+offsetX-2, offsetX+2, sp/wp), offsetY+h-2);
|
||||
}
|
||||
if(remaining) {
|
||||
sp=Math.min(hp, remaining);
|
||||
remaining-=sp;
|
||||
this.ctx.lineTo(offsetX+2, pdst(offsetY+h-2, offsetY+2, sp/hp));
|
||||
}
|
||||
if(remaining) {
|
||||
this.ctx.lineTo(pdst(offsetX+2, this.canvas.width/2, remaining/wp*2), offsetY+2);
|
||||
}
|
||||
this.ctx.stroke();
|
||||
}
|
||||
if(config.getS('appearance.timer')=='number' || config.getS('appearance.timer')=='both') {
|
||||
let remaining=''+Math.ceil((this.rules.gameDuration-this.playTime)/1000);
|
||||
while(remaining.length<(''+this.rules.gameDuration/1000).length) remaining='0'+remaining;
|
||||
|
||||
this.ctx.fillStyle='#930a16';
|
||||
this.ctx.textAlign='center';
|
||||
this.ctx.textBaseline='middle';
|
||||
this.ctx.font='4rem "Fira Code"';
|
||||
this.ctx.fillText(remaining, this.canvas.width/2, this.canvas.height/2);
|
||||
}
|
||||
}
|
||||
|
||||
// draw the border around our game area
|
||||
this.ctx.fillStyle='black';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, offsetY);
|
||||
this.ctx.fillRect(0, 0, offsetX, this.canvas.height);
|
||||
this.ctx.fillRect(offsetX+cellSize*this.dimensions[0], 0, offsetX, this.canvas.height);
|
||||
this.ctx.fillRect(0, offsetY+cellSize*this.dimensions[1], this.canvas.width, offsetY);
|
||||
require('renderer').draw(this, this.canvas, this.ctx);
|
||||
}
|
||||
|
||||
step() {
|
||||
@@ -377,23 +208,25 @@ class SnekGame {
|
||||
this.lastDirection=this.direction;
|
||||
|
||||
// compute our new head
|
||||
const head=[
|
||||
this.snake[0][0]+this.direction[0],
|
||||
this.snake[0][1]+this.direction[1]
|
||||
];
|
||||
let head;
|
||||
if(!this.portaled && isPortal(this.getTileA(this.snake[0]))) {
|
||||
const tile=this.getTileA(this.snake[0]);
|
||||
if(tile==T.PORTAL_A_S) head=this.portals.b;
|
||||
if(tile==T.PORTAL_B_S) head=this.portals.a;
|
||||
if(tile==T.PORTAL_C_S) head=this.portals.d;
|
||||
if(tile==T.PORTAL_D_S) head=this.portals.c;
|
||||
this.portaled=true;
|
||||
} else {
|
||||
head=[
|
||||
this.snake[0][0]+this.direction[0],
|
||||
this.snake[0][1]+this.direction[1]
|
||||
];
|
||||
this.portaled=false;
|
||||
}
|
||||
|
||||
// get our tail out of the way
|
||||
const tail=this.snake.pop();
|
||||
switch(this.world[tail[0]][tail[1]]) {
|
||||
case HOLE_S:
|
||||
this.world[tail[0]][tail[1]]=HOLE;
|
||||
break;
|
||||
case FLAMMABLE_S:
|
||||
this.world[tail[0]][tail[1]]=FLAMMABLE;
|
||||
break;
|
||||
default:
|
||||
this.world[tail[0]][tail[1]]=EMPTY;
|
||||
}
|
||||
this.putTileA(tail, nonSnakeVersion(this.getTileA(tail)));
|
||||
|
||||
// check for out of world conditions
|
||||
if(head[0]<0 || head[0]>=this.dimensions[0] || head[1]<0 || head[1]>=this.dimensions[1]) {
|
||||
@@ -401,51 +234,76 @@ class SnekGame {
|
||||
head[0]=(head[0]+this.dimensions[0])%this.dimensions[0];
|
||||
head[1]=(head[1]+this.dimensions[1])%this.dimensions[1];
|
||||
} else {
|
||||
return this.die();
|
||||
return this.die("literally fell out of the world", "exited the grid");
|
||||
}
|
||||
}
|
||||
|
||||
switch(this.world[head[0]][head[1]]) {
|
||||
let tile=this.getTileA(head);
|
||||
switch(getType(tile)) {
|
||||
// you hit, you die
|
||||
case WALL:
|
||||
case FIRE:
|
||||
case SNAKE:
|
||||
case HOLE_S:
|
||||
return this.die();
|
||||
case 'wall':
|
||||
switch(tile) {
|
||||
case T.WALL: return this.die("thought walls were edible", "hit a wall");
|
||||
case T.FIRE: return this.die("burned to a crisp", "hit fire");
|
||||
case T.DOOR: return this.die("forgot to OPEN the door", "hit a door");
|
||||
case T.SPIKES_ON: return this.die("thought they were a girl's drink in a nightclub", "hit spikes");
|
||||
}
|
||||
|
||||
// congratilations, you played yourself!
|
||||
case 'snake':
|
||||
return this.die("achieved every dog's dream", "ate their own tail");
|
||||
|
||||
// if either 3 consecutive segments or the whole snake is on a hole, you die
|
||||
case HOLE:
|
||||
case 'hole':
|
||||
if(
|
||||
this.snake.length==0 ||
|
||||
this.snake.length==1 &&
|
||||
this.world[this.snake[0][0]][this.snake[0][1]]==HOLE_S ||
|
||||
this.getTileA(this.snake[0])==T.HOLE_S ||
|
||||
this.snake.length>=2 &&
|
||||
this.world[this.snake[0][0]][this.snake[0][1]]==HOLE_S &&
|
||||
this.world[this.snake[1][0]][this.snake[1][1]]==HOLE_S
|
||||
) return this.die();
|
||||
this.getTileA(this.snake[0])==T.HOLE_S &&
|
||||
this.getTileA(this.snake[1])==T.HOLE_S
|
||||
) return this.die("fell harder than their grades", "fell in a hole");
|
||||
break;
|
||||
|
||||
// you eat, you get a massive score boost
|
||||
case SUPER_FOOD:
|
||||
this.score+=10;
|
||||
case 'bonus':
|
||||
this.putTileA(head, T.EMPTY);
|
||||
switch(tile) {
|
||||
case T.SUPER_FOOD:
|
||||
this.score+=10;
|
||||
|
||||
// you eat, you get a small score boost
|
||||
case T.DECAY_FOOD:
|
||||
this.score+=5;
|
||||
this.decayFood=this.decayFood.filter(
|
||||
([x, y, _]) => !(x==head[0] && y==head[1])
|
||||
);
|
||||
} break;
|
||||
|
||||
// you eat, you destroy all doors
|
||||
case 'key':
|
||||
this.putTileA(head, T.EMPTY);
|
||||
this.replaceTilesOfType(T.DOOR, T.EMPTY);
|
||||
break;
|
||||
|
||||
// you eat, you get a small score boost
|
||||
case DECAY_FOOD:
|
||||
this.score+=5;
|
||||
this.decayFood=this.decayFood.filter(
|
||||
([x, y, _]) => !(x==head[0] && y==head[1])
|
||||
);
|
||||
break;
|
||||
// you step on, you trigger
|
||||
case 'switch': {
|
||||
this.putTileA(head, tile==T.SWITCH_ON?T.SWITCH_OFF:T.SWITCH_ON);
|
||||
if(this.getTilesOfType(T.SPIKES_OFF_S).length) return this.die("spiked themselves", "activated spikes");
|
||||
const oldSpikes=this.getTilesOfType(T.SPIKES_ON);
|
||||
this.replaceTilesOfType(T.SPIKES_OFF, T.SPIKES_ON);
|
||||
oldSpikes.forEach(pos => this.putTileA(pos, T.SPIKES_OFF));
|
||||
} break;
|
||||
|
||||
// you eat, you grow
|
||||
case FOOD:
|
||||
// re-grow the snake partially (can't hit the tail, but it's there for all other intents and purposes
|
||||
case 'food':
|
||||
// re-grow the snake
|
||||
this.snake.push(tail);
|
||||
this.putTileA(tail, snakeVersion(this.getTileA(tail)));
|
||||
this.length++;
|
||||
|
||||
// remove the fruit from existence
|
||||
this.world[head[0]][head[1]]=SNAKE;
|
||||
this.putTileA(head, T.EMPTY);
|
||||
this.fruits=this.fruits.filter(
|
||||
([x, y]) => !(x==head[0] && y==head[1])
|
||||
);
|
||||
@@ -455,26 +313,26 @@ class SnekGame {
|
||||
|
||||
// custom rules
|
||||
if(this.rules.fruitRegrow) {
|
||||
const emptyCells=this.getTilesOfType(EMPTY);
|
||||
const emptyCells=this.getTilesOfType(T.EMPTY);
|
||||
|
||||
const cell=emptyCells[Math.floor(Math.random()*emptyCells.length)];
|
||||
this.fruits.push(cell);
|
||||
this.world[cell[0]][cell[1]]=FOOD;
|
||||
this.putTileA(cell, T.FOOD);
|
||||
}
|
||||
|
||||
if(this.rules.superFruitGrow) {
|
||||
if(Math.random()<.1) { // 10% chance
|
||||
const emptyCells=this.getTilesOfType(EMPTY);
|
||||
const emptyCells=this.getTilesOfType(T.EMPTY);
|
||||
const cell=emptyCells[Math.floor(Math.random()*emptyCells.length)];
|
||||
this.world[cell[0]][cell[1]]=SUPER_FOOD;
|
||||
this.putTileA(cell, T.SUPER_FOOD);
|
||||
}
|
||||
}
|
||||
|
||||
if(this.rules.decayingFruitGrow) {
|
||||
if(Math.random()<.2) { // 20% chance
|
||||
const emptyCells=this.getTilesOfType(EMPTY);
|
||||
const emptyCells=this.getTilesOfType(T.EMPTY);
|
||||
const cell=emptyCells[Math.floor(Math.random()*emptyCells.length)];
|
||||
this.world[cell[0]][cell[1]]=DECAY_FOOD;
|
||||
this.putTileA(cell, T.DECAY_FOOD);
|
||||
this.decayFood.push([cell[0], cell[1], this.playTime]);
|
||||
}
|
||||
}
|
||||
@@ -486,22 +344,14 @@ class SnekGame {
|
||||
}
|
||||
|
||||
// move our head forward
|
||||
switch(this.world[head[0]][head[1]]) {
|
||||
case HOLE:
|
||||
this.world[head[0]][head[1]]=HOLE_S;
|
||||
break;
|
||||
case FLAMMABLE:
|
||||
this.world[head[0]][head[1]]=FLAMMABLE_S;
|
||||
break;
|
||||
default:
|
||||
this.world[head[0]][head[1]]=SNAKE;
|
||||
}
|
||||
tile=this.getTileA(head);
|
||||
this.putTileA(head, snakeVersion(tile));
|
||||
this.snake.unshift(head);
|
||||
|
||||
// decay decaying food
|
||||
this.decayFood.forEach(
|
||||
([x, y, birth]) => {
|
||||
if(this.playTime>=birth+2000) this.world[x][y]=EMPTY;
|
||||
if(this.playTime>=birth+2000) this.putTile(x, y, T.EMPTY);
|
||||
}
|
||||
);
|
||||
this.decayFood=this.decayFood.filter(
|
||||
@@ -522,15 +372,21 @@ class SnekGame {
|
||||
if(this.tickId%this.rules.fireTickSpeed==0) {
|
||||
const touchingFire=([x, y]) => {
|
||||
const surrounding=[
|
||||
this.world[x][y-1],
|
||||
this.world[x][y+1],
|
||||
(this.world[x-1]||[])[y],
|
||||
(this.world[x+1]||[])[y]
|
||||
this.getTile(x, y-1),
|
||||
this.getTile(x, y+1),
|
||||
this.getTile(x-1, y),
|
||||
this.getTile(x-1, y)
|
||||
];
|
||||
return surrounding.some(tile => tile==FIRE);
|
||||
return surrounding.some(tile => tile==T.FIRE);
|
||||
};
|
||||
if(this.getTilesOfType(FLAMMABLE_S).some(touchingFire)) return this.die();
|
||||
this.getTilesOfType(FLAMMABLE).filter(touchingFire).forEach(([x, y]) => this.world[x][y]=FIRE);
|
||||
if(this.getTilesOfType(T.FLAMMABLE_S).some(touchingFire)) return this.die("didn't know oil was flammable", "stood on oil when it caught on fire");
|
||||
this.getTilesOfType(T.FLAMMABLE).filter(touchingFire).forEach(pos => this.putTileA(pos, T.FIRE));
|
||||
}
|
||||
|
||||
// THE WORLD!
|
||||
if(!this.rules.timeFlow) {
|
||||
this.lastDirection=[0, 0];
|
||||
this.direction=[0, 0];
|
||||
}
|
||||
|
||||
// victory condition
|
||||
@@ -543,6 +399,9 @@ class SnekGame {
|
||||
if(this.rules.winCondition=='score') {
|
||||
if(this.score>=this.rules.scoreObjective) return this.win();
|
||||
}
|
||||
if(this.rules.scoreSystem=='moves') {
|
||||
if(this.score) this.score--;
|
||||
}
|
||||
}
|
||||
|
||||
tick() {
|
||||
@@ -550,10 +409,13 @@ class SnekGame {
|
||||
if(!this.lastStep) this.lastStep=this.firstStep;
|
||||
this.draw();
|
||||
if(this.callback) this.callback('tick');
|
||||
if(this.lastStep+this.delay<Date.now()) {
|
||||
if(this.rules.timeFlow && this.lastStep+this.delay<Date.now()) {
|
||||
this.lastStep+=this.delay;
|
||||
this.step();
|
||||
}
|
||||
if(!this.rules.timeFlow && (this.direction[0]!=0 || this.direction[1]!=0)) {
|
||||
this.step();
|
||||
}
|
||||
requestAnimationFrame(() => this.tick());
|
||||
}
|
||||
|
||||
@@ -563,9 +425,10 @@ class SnekGame {
|
||||
if(this.callback) this.callback('win');
|
||||
}
|
||||
|
||||
die() {
|
||||
die(message='died', reason='died') {
|
||||
this.playing=false;
|
||||
this.endPlayTime=this.playTime;
|
||||
this.death={message, reason};
|
||||
if(this.callback) this.callback('die');
|
||||
}
|
||||
|
||||
@@ -609,7 +472,6 @@ class SnekGame {
|
||||
this.firstStep=Date.now();
|
||||
this.tickId=0;
|
||||
this.playing=true;
|
||||
this.score=0;
|
||||
requestAnimationFrame(() => this.tick());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
const [
|
||||
EMPTY, SNAKE,
|
||||
FOOD, SUPER_FOOD, DECAY_FOOD,
|
||||
WALL,
|
||||
FIRE, FLAMMABLE, FLAMMABLE_S,
|
||||
HOLE, HOLE_S,
|
||||
PORTAL_A, PORTAL_A_S, PORTAL_B, PORTAL_B_S, PORTAL_C, PORTAL_C_S, PORTAL_D, PORTAL_D_S,
|
||||
KEY, DOOR,
|
||||
SWITCH_ON, SWITCH_ON_S, SWITCH_OFF, SWITCH_OFF_S, SPIKES_OFF, SPIKES_OFF_S, SPIKES_ON
|
||||
]=Array(255).keys();
|
||||
|
||||
const tiles={
|
||||
EMPTY, SNAKE,
|
||||
FOOD, SUPER_FOOD, DECAY_FOOD,
|
||||
WALL,
|
||||
FIRE, FLAMMABLE, FLAMMABLE_S,
|
||||
HOLE, HOLE_S,
|
||||
PORTAL_A, PORTAL_A_S, PORTAL_B, PORTAL_B_S, PORTAL_C, PORTAL_C_S, PORTAL_D, PORTAL_D_S,
|
||||
KEY, DOOR,
|
||||
SWITCH_ON, SWITCH_ON_S, SWITCH_OFF, SWITCH_OFF_S, SPIKES_OFF, SPIKES_OFF_S, SPIKES_ON
|
||||
};
|
||||
|
||||
const tileNames=(() => {
|
||||
let tileNames=[];
|
||||
Object.keys(tiles).forEach(key => tileNames[tiles[key]]=key);
|
||||
return tileNames;
|
||||
})();
|
||||
|
||||
const getName=t =>
|
||||
tileNames[t] || `Unknown tile ${t}`;
|
||||
|
||||
const forChar=c => {
|
||||
switch(c) {
|
||||
case ' ': return EMPTY;
|
||||
case 'f': return FOOD;
|
||||
case 'F': return SUPER_FOOD;
|
||||
case 'd': return DECAY_FOOD;
|
||||
case 'w': return WALL;
|
||||
case 'o': return HOLE;
|
||||
case 'i': return FIRE;
|
||||
case 'I': return FLAMMABLE;
|
||||
case 'A': return PORTAL_A;
|
||||
case 'B': return PORTAL_B;
|
||||
case 'C': return PORTAL_C;
|
||||
case 'D': return PORTAL_D;
|
||||
case 'k': return KEY;
|
||||
case 'K': return DOOR;
|
||||
case 's': return SWITCH_OFF;
|
||||
case 'S': return SPIKES_ON;
|
||||
case 't': return SPIKES_OFF;
|
||||
}
|
||||
throw TypeError(`'${c}' doesn't correspond to any tile`);
|
||||
};
|
||||
|
||||
const charFor=t => {
|
||||
switch(t) {
|
||||
case EMPTY: return ' ';
|
||||
case FOOD: return 'f';
|
||||
case SUPER_FOOD: return 'F';
|
||||
case DECAY_FOOD: return 'd';
|
||||
case WALL: return 'w';
|
||||
case HOLE: return 'o';
|
||||
case FIRE: return 'i';
|
||||
case FLAMMABLE: return 'I';
|
||||
case PORTAL_A: return 'A';
|
||||
case PORTAL_B: return 'B';
|
||||
case PORTAL_C: return 'C';
|
||||
case PORTAL_D: return 'D';
|
||||
case KEY: return 'k';
|
||||
case DOOR: return 'K';
|
||||
case SWITCH_OFF: return 's';
|
||||
case SPIKES_ON: return 'S';
|
||||
case SPIKES_OFF: return 't';
|
||||
}
|
||||
throw TypeError(`'${getName(t)}' doesn't have a corresponding character'`);
|
||||
};
|
||||
|
||||
const snakeVersion=t => {
|
||||
switch(t) {
|
||||
case EMPTY: return SNAKE;
|
||||
case HOLE: return HOLE_S;
|
||||
case FLAMMABLE: return FLAMMABLE_S;
|
||||
case PORTAL_A: return PORTAL_A_S;
|
||||
case PORTAL_B: return PORTAL_B_S;
|
||||
case PORTAL_C: return PORTAL_C_S;
|
||||
case PORTAL_D: return PORTAL_D_S;
|
||||
case SWITCH_OFF: return SWITCH_OFF_S;
|
||||
case SWITCH_ON: return SWITCH_ON_S;
|
||||
case SPIKES_OFF: return SPIKES_OFF_S;
|
||||
}
|
||||
throw TypeError(`'${getName(t)}' doesn't have a snake version'`);
|
||||
};
|
||||
|
||||
const nonSnakeVersion=t => {
|
||||
switch(t) {
|
||||
case SNAKE: return EMPTY;
|
||||
case HOLE_S: return HOLE;
|
||||
case FLAMMABLE_S: return FLAMMABLE;
|
||||
case PORTAL_A_S: return PORTAL_A;
|
||||
case PORTAL_B_S: return PORTAL_B;
|
||||
case PORTAL_C_S: return PORTAL_C;
|
||||
case PORTAL_D_S: return PORTAL_D;
|
||||
case SWITCH_OFF_S: return SWITCH_OFF;
|
||||
case SWITCH_ON_S: return SWITCH_ON;
|
||||
case SPIKES_OFF_S: return SPIKES_OFF;
|
||||
}
|
||||
throw TypeError(`'${getName(t)}' doesn't have a non-snake version'`);
|
||||
};
|
||||
|
||||
const isSnakeVersion=t => {
|
||||
switch(t) {
|
||||
case SNAKE:
|
||||
case HOLE_S:
|
||||
case FLAMMABLE_S:
|
||||
case PORTAL_A_S: case PORTAL_B_S: case PORTAL_C_S: case PORTAL_D_S:
|
||||
case SWITCH_OFF_S: case SWITCH_ON_S:
|
||||
case SPIKES_OFF_S:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const isNonSnakeVersion=t => {
|
||||
switch(t) {
|
||||
case EMPTY:
|
||||
case HOLE:
|
||||
case FLAMMABLE:
|
||||
case PORTAL_A: case PORTAL_B: case PORTAL_C: case PORTAL_D:
|
||||
case SWITCH_OFF: case SWITCH_ON:
|
||||
case SPIKES_OFF:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const isSafe=t => {
|
||||
switch(t) {
|
||||
case EMPTY:
|
||||
case HOLE:
|
||||
case FLAMMABLE:
|
||||
case PORTAL_A: case PORTAL_B: case PORTAL_C: case PORTAL_D:
|
||||
case SWITCH_OFF: case SWITCH_ON:
|
||||
case SPIKES_OFF:
|
||||
case FOOD: case SUPER_FOOD: case DECAY_FOOD:
|
||||
case KEY:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const isWall=t => {
|
||||
switch(t) {
|
||||
case WALL:
|
||||
case FIRE:
|
||||
case DOOR:
|
||||
case SPIKES_ON:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const getType=t => {
|
||||
if(isSnakeVersion(t)) return 'snake';
|
||||
if(isWall(t)) return 'wall';
|
||||
|
||||
switch(t) {
|
||||
case EMPTY:
|
||||
case FLAMMABLE:
|
||||
case SPIKES_OFF:
|
||||
return 'empty';
|
||||
|
||||
case FOOD:
|
||||
return 'food';
|
||||
|
||||
case SUPER_FOOD:
|
||||
case DECAY_FOOD:
|
||||
return 'bonus';
|
||||
|
||||
case HOLE:
|
||||
return 'hole';
|
||||
|
||||
case PORTAL_A:
|
||||
case PORTAL_B:
|
||||
case PORTAL_C:
|
||||
case PORTAL_D:
|
||||
return 'portal';
|
||||
|
||||
case KEY:
|
||||
return 'key';
|
||||
|
||||
case SWITCH_ON:
|
||||
case SWITCH_OFF:
|
||||
return 'switch';
|
||||
}
|
||||
|
||||
throw TypeError(`'${getName(t)}' isn't a valid tile`);
|
||||
};
|
||||
|
||||
const isPortal=t => {
|
||||
switch(t) {
|
||||
case PORTAL_A:
|
||||
case PORTAL_B:
|
||||
case PORTAL_C:
|
||||
case PORTAL_D:
|
||||
case PORTAL_A_S:
|
||||
case PORTAL_B_S:
|
||||
case PORTAL_C_S:
|
||||
case PORTAL_D_S:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
return module.exports={
|
||||
tiles,
|
||||
tileNames, getName,
|
||||
forChar, charFor,
|
||||
snakeVersion, nonSnakeVersion, isSnakeVersion, isNonSnakeVersion,
|
||||
isSafe, isWall, isPortal,
|
||||
getType
|
||||
};
|
||||
@@ -38,4 +38,21 @@
|
||||
transform: translate(-50%, -50%);
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.status {
|
||||
bottom: 1rem;
|
||||
right: 1rem;
|
||||
padding: .2rem;
|
||||
font-size: 1.4rem;
|
||||
|
||||
.score::before {
|
||||
content: '\1f34e';
|
||||
}
|
||||
.time::before {
|
||||
content: '\23f1';
|
||||
}
|
||||
.speed::before {
|
||||
content: '\1f684';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
}
|
||||
|
||||
.popup {
|
||||
animation: popupAppear 1s linear;
|
||||
&.animation {
|
||||
animation: popupAppear 1s linear;
|
||||
}
|
||||
|
||||
background: rgba(0, 0, 0, 90%);
|
||||
|
||||
position: absolute;
|
||||
@@ -64,7 +67,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
.dual {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
@@ -86,6 +89,15 @@
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
|
||||
td, th {
|
||||
padding: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
margin-right: 1ex;
|
||||
}
|
||||
@@ -96,10 +108,20 @@
|
||||
background: @accentbg;
|
||||
font-weight: bold;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
border-radius: 1rem;
|
||||
border: 0;
|
||||
padding: 2rem;
|
||||
margin: 1rem;
|
||||
|
||||
transition: box-shadow .5s;
|
||||
|
||||
&:hover {
|
||||
color: @fg;
|
||||
text-decoration: underline;
|
||||
box-shadow: black 0 0 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,11 @@ a {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: @fg;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
em {
|
||||
font-style: italic;
|
||||
}
|
||||
@@ -71,6 +76,9 @@ header img, footer img {
|
||||
}
|
||||
header img {
|
||||
height: 8rem;
|
||||
&:hover {
|
||||
border-color: @fg;
|
||||
}
|
||||
}
|
||||
footer img {
|
||||
height: 4rem;
|
||||
@@ -86,6 +94,10 @@ header ul {
|
||||
|
||||
a {
|
||||
color: @fg;
|
||||
|
||||
&:hover {
|
||||
color: @accentfg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,6 +141,16 @@ p {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body.serverless .server {
|
||||
display: none !important;
|
||||
}
|
||||
body.server .serverless {
|
||||
display: none !important;
|
||||
}
|
||||
body.loading .loaded {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
// setup the progress bar
|
||||
@import 'progressBar';
|
||||
|
||||
|
||||