Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5775f6c96c | |||
| 363f85c06c | |||
| 5cd4ac2dfe | |||
| a5efd14fc8 | |||
| 03cb1b83b9 | |||
| 5bf5f49f03 | |||
| edd9d5c15a | |||
| ea79ba1dfa | |||
| a87b4679f4 | |||
| e34e7e07bd | |||
| 0923ad56dd | |||
| 65b33afa05 | |||
| 530260ac54 | |||
| bf06218815 | |||
| 2a9418c9c4 | |||
| 338934866b | |||
| 23535d17d8 | |||
| db655feedb | |||
| 6edd23f25f | |||
| 0578bfb6ad | |||
| d339dd0a06 | |||
| a7e2d1c201 | |||
| 3a571a9c30 | |||
| b12ac2fab9 | |||
| 2a67ab40ee | |||
| 7dd95f262c | |||
| 4746f34537 | |||
| 40ec4fa09a | |||
| fe22b85387 | |||
| 41cb894ef9 | |||
| 3997e94a68 | |||
| 76f1765be7 | |||
| 45ab40ebcb | |||
| e34c6c75bb | |||
| c5b94b59c5 | |||
| 02e72795cc | |||
| f050baafbc | |||
| 0e0cd3a7d1 | |||
| fc50821ff0 | |||
| 95751be63e | |||
| 93ba24a53a | |||
| 352c3aa16d | |||
| 86cd935f03 | |||
| 4bf204cae4 | |||
| bb932ff863 | |||
| 6b7ff7e86d | |||
| 1df62ff1fe | |||
| 10585fef98 | |||
| de390dff8a | |||
| 03e0c97280 | |||
| beb9598f69 | |||
| daef55781e | |||
| bd6d9f3399 | |||
| 7362b4dc5c | |||
| fe2902cdea | |||
| becfaf9eaf | |||
| 7de127b93e |
@@ -0,0 +1,8 @@
|
||||
node_modules/
|
||||
public/assets/*.png
|
||||
public/assets/*.json
|
||||
public/css/*.css
|
||||
public/js/*.js
|
||||
public/favicon.ico
|
||||
build/*.png
|
||||
|
||||
@@ -0,0 +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
|
||||
CMD /snek/start.sh
|
||||
@@ -0,0 +1,72 @@
|
||||
.PHONY: all clean
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
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
|
||||
JS = public/js/snek.js
|
||||
|
||||
OUTPUT = $(IMAGES) $(TILESETS) $(ANIMATIONS) $(JSON) $(ICON) $(CSS) $(JS)
|
||||
|
||||
all: images tilesets animations json icon css js
|
||||
|
||||
images: $(IMAGES)
|
||||
tilesets: $(TILESETS)
|
||||
animations: $(ANIMATIONS)
|
||||
json: $(JSON)
|
||||
icon: $(ICON)
|
||||
css: $(CSS)
|
||||
js: $(JS)
|
||||
|
||||
public/favicon.ico: assets/icon.jpg
|
||||
convert $^ -resize 32x $@
|
||||
|
||||
public/assets/%32.png: assets/%.png
|
||||
convert $^ -resize 32x $@
|
||||
public/assets/%256.png: assets/%.png
|
||||
convert $^ -resize 256x $@
|
||||
|
||||
public/assets/%32.png: assets/%.jpg
|
||||
convert $^ -resize 32x $@
|
||||
public/assets/%256.png: assets/%.jpg
|
||||
convert $^ -resize 256x $@
|
||||
|
||||
public/assets/%-ts.png: assets/%.png
|
||||
convert $^ -scale 32x $@
|
||||
|
||||
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 $@
|
||||
|
||||
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 $@
|
||||
|
||||
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 $@
|
||||
|
||||
public/assets/%.json: assets/%.json
|
||||
cp $^ $@
|
||||
|
||||
public/css/snek.css: src/less/snek.less $(wildcard src/less/*.less)
|
||||
node_modules/.bin/lessc $< $@
|
||||
|
||||
public/js/snek.js: $(wildcard src/js/*.js)
|
||||
node mergejs.js $^ > $@
|
||||
|
||||
clean:
|
||||
rm -f build/*.*
|
||||
rm -f $(OUTPUT)
|
||||
@@ -1,3 +1,37 @@
|
||||
# Snek
|
||||

|
||||
|
||||
A simple Snake, done as my final JS class project
|
||||
A "simple" Snake, done as my final JS class project
|
||||
|
||||
[Original subject](https://perso.liris.cnrs.fr/pierre-antoine.champin/enseignement/intro-js/s6.html)
|
||||
|
||||
## Features
|
||||
- 60 FPS 2D animations
|
||||
- arcade and speedrun game modes
|
||||
- touchscreen and controller support
|
||||
- playable at [snek.codinget.me](https://snek.codinget.me)
|
||||
|
||||
## Dev dependencies
|
||||
- All the POSIX tools, most importantly a POSIX-compliant shell, `echo`, `rm`, `seq` and `sed`
|
||||
- Busybox is known to work
|
||||
- GNU Coreutils are known to work
|
||||
- On Windows, WSL is known to work
|
||||
- Imagemagick, with the `convert` tool in the PATH
|
||||
- Make
|
||||
- Node.js and npm, both in the PATH
|
||||
- Node.js 10 and 12 are known to work
|
||||
|
||||
## Running the game (dev)
|
||||
- `git clone` this repo
|
||||
- `npm install` the dependencies
|
||||
- `make` the ressources
|
||||
- `npm start` the server
|
||||
|
||||
## Running the game (prod)
|
||||
- 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
|
||||
|
||||
## License
|
||||
[MIT](https://opensource.org/licenses/MIT)
|
||||
|
||||
|
After Width: | Height: | Size: 32 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"input.touchscreen.crosspad.enabled": false,
|
||||
"input.touchscreen.crosspad.overlay": true,
|
||||
|
||||
"input.touchscreen.joystick.enabled": true,
|
||||
"input.touchscreen.joystick.overlay": true,
|
||||
"input.touchscreen.joystick.deadzone": 10,
|
||||
|
||||
"input.touchscreen.swipe.enabled": false,
|
||||
"input.touchscreen.swipe.deadzone": 50,
|
||||
|
||||
"input.gamepad.enabled": true,
|
||||
"input.gamepad.deadzone": 0.5,
|
||||
|
||||
"input.keyboard.enabled": true,
|
||||
|
||||
"input.buffer": false,
|
||||
|
||||
"appearance.grid": "none",
|
||||
"appearance.timer": "both"
|
||||
}
|
||||
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 120 KiB |
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"speedrun": {
|
||||
"desc": "Get all the fruits as fast as possible without touching the walls",
|
||||
"rules": {
|
||||
"fruitRegrow": false,
|
||||
"speedIncrease": false,
|
||||
"worldWrap": false,
|
||||
"winCondition": "fruit",
|
||||
"scoreSystem": "speedrun"
|
||||
},
|
||||
"levelFilename": "level<n>.json",
|
||||
"levelDisplay": "Level <n>",
|
||||
"levels": [
|
||||
1, 2, 3, 4, 5
|
||||
],
|
||||
"nextLevel": true
|
||||
},
|
||||
"arcade": {
|
||||
"desc": "Have fun just like in the good ol' days, walls wrap around, fruits respawn and speed increases",
|
||||
"rules": {
|
||||
"fruitRegrow": true,
|
||||
"speedIncrease": true,
|
||||
"speedMultiplier": 0.9,
|
||||
"speedCap": 50,
|
||||
"worldWrap": true
|
||||
},
|
||||
"levelFilename": "arcade-<l>.json",
|
||||
"levelDisplay": "<n>",
|
||||
"levels": [
|
||||
"Arcade",
|
||||
"Timed",
|
||||
"Survival"
|
||||
],
|
||||
"nextLevel": false,
|
||||
"levelDesc": [
|
||||
"The old classic, try to get as high as a score as you can",
|
||||
"Get a score as high as you can in 30 seconds",
|
||||
"Survive for as long as you can in an increasingly difficult game"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
{
|
||||
"input": {
|
||||
"name": "Input settings"
|
||||
},
|
||||
|
||||
"input.buffer": {
|
||||
"name": "Enable input buffering",
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
"input.touchscreen": {
|
||||
"name": "Touchscreen settings"
|
||||
},
|
||||
|
||||
"input.touchscreen.crosspad": {
|
||||
"name": "Crosspad mode"
|
||||
},
|
||||
"input.touchscreen.crosspad.enabled": {
|
||||
"name": "Enable crosspad",
|
||||
"type": "boolean",
|
||||
"excludes": [
|
||||
"input.touchscreen.joystick.enabled",
|
||||
"input.touchscreen.swipe.enabled"
|
||||
]
|
||||
},
|
||||
"input.touchscreen.crosspad.overlay": {
|
||||
"name": "Show overlay",
|
||||
"type": "boolean",
|
||||
"parent": "input.touchscreen.crosspad.enabled"
|
||||
},
|
||||
|
||||
"input.touchscreen.joystick": {
|
||||
"name": "Joystick mode"
|
||||
},
|
||||
"input.touchscreen.joystick.enabled": {
|
||||
"name": "Enable joystick",
|
||||
"type": "boolean",
|
||||
"excludes": [
|
||||
"input.touchscreen.crosspad.enabled",
|
||||
"input.touchscreen.swipe.enabled"
|
||||
]
|
||||
},
|
||||
"input.touchscreen.joystick.overlay": {
|
||||
"name": "Show overlay",
|
||||
"type": "boolean",
|
||||
"parent": "input.touchscreen.joystick.enabled"
|
||||
},
|
||||
"input.touchscreen.joystick.deadzone": {
|
||||
"name": "Deadzone",
|
||||
"type": "number",
|
||||
"parent": "input.touchscreen.joystick.enabled",
|
||||
"bounds": {
|
||||
"min": 1,
|
||||
"max": 100,
|
||||
"inc": 1
|
||||
}
|
||||
},
|
||||
|
||||
"input.touchscreen.swipe": {
|
||||
"name": "Swipe mode"
|
||||
},
|
||||
"input.touchscreen.swipe.enabled": {
|
||||
"name": "Enable swipe",
|
||||
"type": "boolean",
|
||||
"excludes": [
|
||||
"input.touchscreen.crosspad.enabled",
|
||||
"input.touchscreen.joystick.enabled"
|
||||
]
|
||||
},
|
||||
"input.touchscreen.swipe.deadzone": {
|
||||
"name": "Deadzone",
|
||||
"type": "number",
|
||||
"parent": "input.touchscreen.swipe.enabled",
|
||||
"bounds": {
|
||||
"min": 1,
|
||||
"max": 100,
|
||||
"inc": 1
|
||||
}
|
||||
},
|
||||
|
||||
"input.gamepad": {
|
||||
"name": "Gamepad settings"
|
||||
},
|
||||
"input.gamepad.enabled": {
|
||||
"name": "Enable gamepad",
|
||||
"type": "boolean"
|
||||
},
|
||||
"input.gamepad.deadzone": {
|
||||
"name": "Deadzone",
|
||||
"type": "number",
|
||||
"parent": "input.gamepad.enabled",
|
||||
"bounds": {
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"inc": 0.1
|
||||
}
|
||||
},
|
||||
|
||||
"input.keyboard": {
|
||||
"name": "Keyboard settings"
|
||||
},
|
||||
"input.keyboard.enabled": {
|
||||
"name": "Enable keyboard",
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
"appearance": {
|
||||
"name": "Appearance"
|
||||
},
|
||||
"appearance.grid": {
|
||||
"name": "Grid type",
|
||||
"type": "choice",
|
||||
"bounds": {
|
||||
"choices": [
|
||||
"none",
|
||||
"grid",
|
||||
"checkerboard"
|
||||
]
|
||||
}
|
||||
},
|
||||
"appearance.timer": {
|
||||
"name": "Timer display",
|
||||
"type": "choice",
|
||||
"bounds": {
|
||||
"choices": [
|
||||
"none",
|
||||
"border",
|
||||
"number",
|
||||
"both"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 233 KiB |
|
After Width: | Height: | Size: 23 KiB |
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"color": "#fba49b",
|
||||
"join": "round",
|
||||
"cap": "round",
|
||||
"headSize": 0.8,
|
||||
"tailSize": 0.4,
|
||||
"tailWrapSize": 0.1
|
||||
}
|
||||
|
After Width: | Height: | Size: 382 KiB |
@@ -0,0 +1,10 @@
|
||||
const express=require('express');
|
||||
|
||||
const app=express();
|
||||
const PORT=process.env.PORT || 3000;
|
||||
|
||||
app.use(express.static('public'));
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Listening on 0.0.0.0:${PORT}`);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"dimensions": [32, 32],
|
||||
"delay": 200,
|
||||
"food": [
|
||||
[16, 16]
|
||||
],
|
||||
"snake": [
|
||||
[16, 12],
|
||||
[16, 11],
|
||||
[16, 10]
|
||||
],
|
||||
"rules": {
|
||||
"superFruitGrow": true,
|
||||
"decayingFruitGrow": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"world": [
|
||||
" w ",
|
||||
" w ",
|
||||
" wwwwwww w wwwwwww ",
|
||||
" w w ",
|
||||
" w w ",
|
||||
" w wwwwwwwwwwwww w ",
|
||||
" w w ",
|
||||
" w w ",
|
||||
" w w w ",
|
||||
" w w w ",
|
||||
" w w w ",
|
||||
" w w w ",
|
||||
" w w w ",
|
||||
" w w w ",
|
||||
" w w w ",
|
||||
"www w wwwwwwwwwwwwwww w www",
|
||||
" w w w ",
|
||||
" w w w ",
|
||||
" w w w ",
|
||||
" w w w ",
|
||||
" w w w ",
|
||||
" w w w ",
|
||||
" w w w ",
|
||||
" w w ",
|
||||
" w w ",
|
||||
" w wwwwwwwwwwwww w ",
|
||||
" w w ",
|
||||
" w w ",
|
||||
" wwwwwww w wwwwwww ",
|
||||
" w ",
|
||||
" w "
|
||||
],
|
||||
"delay": 200,
|
||||
"snake": [
|
||||
[16, 3],
|
||||
[15, 3],
|
||||
[14, 3]
|
||||
],
|
||||
"rules": {
|
||||
"worldWrap": false,
|
||||
"autoSpeedIncrease": true,
|
||||
"autoSpeadIncreaseTicks": 10,
|
||||
"autoSizeGrow": true,
|
||||
"autoSizeGrowTicks": 100,
|
||||
"scoreSystem": "survival"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"dimensions": [32, 32],
|
||||
"delay": 100,
|
||||
"food": [
|
||||
[16, 5]
|
||||
],
|
||||
"snake": [
|
||||
[16, 12],
|
||||
[16, 11],
|
||||
[16, 10]
|
||||
],
|
||||
"rules": {
|
||||
"speedMultiplier": 0.8,
|
||||
"gameDuration": 30000,
|
||||
"winCondition": "time"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"dimensions": [80, 40],
|
||||
"delay": 200,
|
||||
"walls": [
|
||||
[5,5], [5,6], [5,7], [5,8], [70, 35], [71, 35], [72, 35]
|
||||
],
|
||||
"food": [
|
||||
[10,10]
|
||||
],
|
||||
"snake": [
|
||||
[60,20],
|
||||
[60,19],
|
||||
[60,18]
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"world": [
|
||||
" ",
|
||||
" f ",
|
||||
" fw w wf ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" w ",
|
||||
" f "
|
||||
],
|
||||
"snake": [
|
||||
[6,6],
|
||||
[6,7],
|
||||
[6,8],
|
||||
[6,9]
|
||||
],
|
||||
"delay": 150
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"world": [
|
||||
" w ",
|
||||
" w ",
|
||||
" wwwwwww w wwwwwww ",
|
||||
" w f w ",
|
||||
" w w ",
|
||||
" w wwwwwwwwwwwww w ",
|
||||
" w w ",
|
||||
" w f w ",
|
||||
" w w w ",
|
||||
" w w w ",
|
||||
" w w w ",
|
||||
" w w w ",
|
||||
" w w w ",
|
||||
" w w w ",
|
||||
" w w w ",
|
||||
"wwwf w fwwwwwwwwwwwwwwwf w fwww",
|
||||
" w w w ",
|
||||
" w w w ",
|
||||
" w w w ",
|
||||
" w w w ",
|
||||
" w w w ",
|
||||
" w w w ",
|
||||
" w w w ",
|
||||
" w f w ",
|
||||
" w w ",
|
||||
" w wwwwwwwwwwwww w ",
|
||||
" w w ",
|
||||
" w f w ",
|
||||
" wwwwwww w wwwwwww ",
|
||||
" w ",
|
||||
" w "
|
||||
],
|
||||
"delay": 200,
|
||||
"snake": [
|
||||
[16, 4],
|
||||
[15, 4],
|
||||
[14, 4]
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"world": [
|
||||
" o ",
|
||||
" o ",
|
||||
" ooooo o ooooo ",
|
||||
" f ",
|
||||
" o o ",
|
||||
" o ooooooooooooo o ",
|
||||
" o o ",
|
||||
" o f o ",
|
||||
" o o o ",
|
||||
" o o o ",
|
||||
" o o o ",
|
||||
" o o o ",
|
||||
" o o o ",
|
||||
" o o o ",
|
||||
" o o ",
|
||||
"ooof o foooooo oooooof o fooo",
|
||||
" o o ",
|
||||
" o o o ",
|
||||
" o o o ",
|
||||
" o o o ",
|
||||
" o o o ",
|
||||
" o o o ",
|
||||
" o o o ",
|
||||
" o f o ",
|
||||
" o o ",
|
||||
" o ooooooooooooo o ",
|
||||
" o o ",
|
||||
" f ",
|
||||
" ooooo o ooooo ",
|
||||
" o ",
|
||||
" o "
|
||||
],
|
||||
"delay": 100,
|
||||
"snake": [
|
||||
[16, 4]
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"world": [
|
||||
" f f",
|
||||
" o ",
|
||||
" o ",
|
||||
" w ",
|
||||
"wwwooowwiIIIIIf",
|
||||
" f I ",
|
||||
" I ",
|
||||
" I ",
|
||||
" i f"
|
||||
],
|
||||
"delay": 200,
|
||||
"snake": [
|
||||
[0, 0]
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
const fs=require('fs');
|
||||
|
||||
const requireFn=`
|
||||
const require=function require(name) {
|
||||
if(require.cache[name]) return require.cache[name];
|
||||
if(!require.source[name]) throw new Error("Cannot require "+name+": not found");
|
||||
require.cache[name]=require.source[name]({}) || true;
|
||||
return require.cache[name];
|
||||
};
|
||||
require.cache=Object.create(null);
|
||||
require.source=Object.create(null);
|
||||
window.require=require;
|
||||
`;
|
||||
|
||||
let outputCode=[requireFn];
|
||||
process.argv
|
||||
.slice(2)
|
||||
.map(a => [a, a.match(/([a-zA-Z_][a-zA-Z0-9_-]*).js$/)[1]])
|
||||
.forEach(([modFile, modName]) => {
|
||||
const modSource=fs.readFileSync(modFile, 'utf8');
|
||||
outputCode.push(`
|
||||
require.source['${modName}']=(a => a.bind(a)) (function ${modName}(module) {
|
||||
'use strict';
|
||||
${modSource}
|
||||
});
|
||||
`);
|
||||
});
|
||||
|
||||
fs.writeSync(1, outputCode.map(a => a.trim()).join('\n'));
|
||||
|
||||
@@ -0,0 +1,817 @@
|
||||
{
|
||||
"name": "snek",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"accepts": {
|
||||
"version": "1.3.7",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
|
||||
"integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
|
||||
"requires": {
|
||||
"mime-types": "~2.1.24",
|
||||
"negotiator": "0.6.2"
|
||||
}
|
||||
},
|
||||
"ajv": {
|
||||
"version": "6.12.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz",
|
||||
"integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
}
|
||||
},
|
||||
"array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
|
||||
},
|
||||
"asap": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
|
||||
"integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=",
|
||||
"optional": true
|
||||
},
|
||||
"asn1": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
|
||||
"integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safer-buffer": "~2.1.0"
|
||||
}
|
||||
},
|
||||
"assert-plus": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
||||
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
|
||||
"optional": true
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
|
||||
"optional": true
|
||||
},
|
||||
"aws-sign2": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
|
||||
"integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=",
|
||||
"optional": true
|
||||
},
|
||||
"aws4": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz",
|
||||
"integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==",
|
||||
"optional": true
|
||||
},
|
||||
"bcrypt-pbkdf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||
"integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"tweetnacl": "^0.14.3"
|
||||
}
|
||||
},
|
||||
"body-parser": {
|
||||
"version": "1.19.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
|
||||
"integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
|
||||
"requires": {
|
||||
"bytes": "3.1.0",
|
||||
"content-type": "~1.0.4",
|
||||
"debug": "2.6.9",
|
||||
"depd": "~1.1.2",
|
||||
"http-errors": "1.7.2",
|
||||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "~2.3.0",
|
||||
"qs": "6.7.0",
|
||||
"raw-body": "2.4.0",
|
||||
"type-is": "~1.6.17"
|
||||
}
|
||||
},
|
||||
"bytes": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
|
||||
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
|
||||
},
|
||||
"caseless": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
|
||||
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
|
||||
"optional": true
|
||||
},
|
||||
"clone": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
||||
"integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18="
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"content-disposition": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
|
||||
"integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
|
||||
"requires": {
|
||||
"safe-buffer": "5.1.2"
|
||||
}
|
||||
},
|
||||
"content-type": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
|
||||
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
|
||||
},
|
||||
"cookie": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
|
||||
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
|
||||
},
|
||||
"cookie-signature": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
|
||||
},
|
||||
"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
|
||||
},
|
||||
"dashdash": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
|
||||
"integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"assert-plus": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"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
|
||||
},
|
||||
"depd": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
|
||||
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
|
||||
},
|
||||
"destroy": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
|
||||
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
|
||||
},
|
||||
"ecc-jsbn": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
|
||||
"integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"jsbn": "~0.1.0",
|
||||
"safer-buffer": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
|
||||
},
|
||||
"encodeurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
|
||||
},
|
||||
"errno": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
|
||||
"integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"prr": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
|
||||
},
|
||||
"etag": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
|
||||
},
|
||||
"express": {
|
||||
"version": "4.17.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
|
||||
"integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
|
||||
"requires": {
|
||||
"accepts": "~1.3.7",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.19.0",
|
||||
"content-disposition": "0.5.3",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.4.0",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "~1.1.2",
|
||||
"encodeurl": "~1.0.2",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"finalhandler": "~1.1.2",
|
||||
"fresh": "0.5.2",
|
||||
"merge-descriptors": "1.0.1",
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "~2.3.0",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "0.1.7",
|
||||
"proxy-addr": "~2.0.5",
|
||||
"qs": "6.7.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.1.2",
|
||||
"send": "0.17.1",
|
||||
"serve-static": "1.14.1",
|
||||
"setprototypeof": "1.1.1",
|
||||
"statuses": "~1.5.0",
|
||||
"type-is": "~1.6.18",
|
||||
"utils-merge": "1.0.1",
|
||||
"vary": "~1.1.2"
|
||||
}
|
||||
},
|
||||
"extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||
"optional": true
|
||||
},
|
||||
"extsprintf": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
|
||||
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
|
||||
"optional": true
|
||||
},
|
||||
"fast-deep-equal": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz",
|
||||
"integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==",
|
||||
"optional": true
|
||||
},
|
||||
"fast-json-stable-stringify": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
||||
"optional": true
|
||||
},
|
||||
"finalhandler": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
|
||||
"integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
|
||||
"requires": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "~1.0.2",
|
||||
"escape-html": "~1.0.3",
|
||||
"on-finished": "~2.3.0",
|
||||
"parseurl": "~1.3.3",
|
||||
"statuses": "~1.5.0",
|
||||
"unpipe": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"forever-agent": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
|
||||
"integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=",
|
||||
"optional": true
|
||||
},
|
||||
"form-data": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
|
||||
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.6",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"forwarded": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
|
||||
"integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
|
||||
},
|
||||
"fresh": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
|
||||
},
|
||||
"getpass": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
|
||||
"integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"assert-plus": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"graceful-fs": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz",
|
||||
"integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==",
|
||||
"optional": true
|
||||
},
|
||||
"har-schema": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
|
||||
"integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=",
|
||||
"optional": true
|
||||
},
|
||||
"har-validator": {
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
|
||||
"integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ajv": "^6.5.5",
|
||||
"har-schema": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"http-errors": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
|
||||
"integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
|
||||
"requires": {
|
||||
"depd": "~1.1.2",
|
||||
"inherits": "2.0.3",
|
||||
"setprototypeof": "1.1.1",
|
||||
"statuses": ">= 1.5.0 < 2",
|
||||
"toidentifier": "1.0.0"
|
||||
}
|
||||
},
|
||||
"http-signature": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
|
||||
"integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"assert-plus": "^1.0.0",
|
||||
"jsprim": "^1.2.2",
|
||||
"sshpk": "^1.7.0"
|
||||
}
|
||||
},
|
||||
"iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"requires": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
}
|
||||
},
|
||||
"image-size": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
|
||||
"integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=",
|
||||
"optional": true
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
|
||||
},
|
||||
"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-typedarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
|
||||
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
|
||||
"optional": true
|
||||
},
|
||||
"isstream": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
|
||||
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
|
||||
"optional": true
|
||||
},
|
||||
"jsbn": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
|
||||
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
|
||||
"optional": true
|
||||
},
|
||||
"json-schema": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
|
||||
"integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
|
||||
"optional": true
|
||||
},
|
||||
"json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||
"optional": true
|
||||
},
|
||||
"json-stringify-safe": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
|
||||
"optional": true
|
||||
},
|
||||
"jsprim": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
|
||||
"integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"assert-plus": "1.0.0",
|
||||
"extsprintf": "1.3.0",
|
||||
"json-schema": "0.2.3",
|
||||
"verror": "1.10.0"
|
||||
}
|
||||
},
|
||||
"less": {
|
||||
"version": "3.11.1",
|
||||
"resolved": "https://registry.npmjs.org/less/-/less-3.11.1.tgz",
|
||||
"integrity": "sha512-tlWX341RECuTOvoDIvtFqXsKj072hm3+9ymRBe76/mD6O5ZZecnlAOVDlWAleF2+aohFrxNidXhv2773f6kY7g==",
|
||||
"requires": {
|
||||
"clone": "^2.1.2",
|
||||
"errno": "^0.1.1",
|
||||
"graceful-fs": "^4.1.2",
|
||||
"image-size": "~0.5.0",
|
||||
"mime": "^1.4.1",
|
||||
"mkdirp": "^0.5.0",
|
||||
"promise": "^7.1.1",
|
||||
"request": "^2.83.0",
|
||||
"source-map": "~0.6.0",
|
||||
"tslib": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
|
||||
},
|
||||
"merge-descriptors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
||||
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
|
||||
},
|
||||
"methods": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
|
||||
},
|
||||
"mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.43.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz",
|
||||
"integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ=="
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.26",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz",
|
||||
"integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==",
|
||||
"requires": {
|
||||
"mime-db": "1.43.0"
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"optional": true
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
},
|
||||
"negotiator": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
|
||||
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
|
||||
},
|
||||
"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
|
||||
},
|
||||
"on-finished": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
|
||||
"requires": {
|
||||
"ee-first": "1.1.1"
|
||||
}
|
||||
},
|
||||
"parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
|
||||
},
|
||||
"path-to-regexp": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
|
||||
},
|
||||
"performance-now": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=",
|
||||
"optional": true
|
||||
},
|
||||
"promise": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
|
||||
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"asap": "~2.0.3"
|
||||
}
|
||||
},
|
||||
"proxy-addr": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
|
||||
"integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==",
|
||||
"requires": {
|
||||
"forwarded": "~0.1.2",
|
||||
"ipaddr.js": "1.9.1"
|
||||
}
|
||||
},
|
||||
"prr": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
|
||||
"integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
|
||||
"optional": true
|
||||
},
|
||||
"psl": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
|
||||
"integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==",
|
||||
"optional": true
|
||||
},
|
||||
"punycode": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
|
||||
"optional": true
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.7.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
|
||||
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
|
||||
},
|
||||
"range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
|
||||
},
|
||||
"raw-body": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
|
||||
"integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
|
||||
"requires": {
|
||||
"bytes": "3.1.0",
|
||||
"http-errors": "1.7.2",
|
||||
"iconv-lite": "0.4.24",
|
||||
"unpipe": "1.0.0"
|
||||
}
|
||||
},
|
||||
"request": {
|
||||
"version": "2.88.2",
|
||||
"resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
|
||||
"integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"aws-sign2": "~0.7.0",
|
||||
"aws4": "^1.8.0",
|
||||
"caseless": "~0.12.0",
|
||||
"combined-stream": "~1.0.6",
|
||||
"extend": "~3.0.2",
|
||||
"forever-agent": "~0.6.1",
|
||||
"form-data": "~2.3.2",
|
||||
"har-validator": "~5.1.3",
|
||||
"http-signature": "~1.2.0",
|
||||
"is-typedarray": "~1.0.0",
|
||||
"isstream": "~0.1.2",
|
||||
"json-stringify-safe": "~5.0.1",
|
||||
"mime-types": "~2.1.19",
|
||||
"oauth-sign": "~0.9.0",
|
||||
"performance-now": "^2.1.0",
|
||||
"qs": "~6.5.2",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"tough-cookie": "~2.5.0",
|
||||
"tunnel-agent": "^0.6.0",
|
||||
"uuid": "^3.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"qs": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
|
||||
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"send": {
|
||||
"version": "0.17.1",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
|
||||
"integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
|
||||
"requires": {
|
||||
"debug": "2.6.9",
|
||||
"depd": "~1.1.2",
|
||||
"destroy": "~1.0.4",
|
||||
"encodeurl": "~1.0.2",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "~1.7.2",
|
||||
"mime": "1.6.0",
|
||||
"ms": "2.1.1",
|
||||
"on-finished": "~2.3.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"statuses": "~1.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ms": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
|
||||
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve-static": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
|
||||
"integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
|
||||
"requires": {
|
||||
"encodeurl": "~1.0.2",
|
||||
"escape-html": "~1.0.3",
|
||||
"parseurl": "~1.3.3",
|
||||
"send": "0.17.1"
|
||||
}
|
||||
},
|
||||
"setprototypeof": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
|
||||
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"optional": true
|
||||
},
|
||||
"sshpk": {
|
||||
"version": "1.16.1",
|
||||
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
|
||||
"integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"asn1": "~0.2.3",
|
||||
"assert-plus": "^1.0.0",
|
||||
"bcrypt-pbkdf": "^1.0.0",
|
||||
"dashdash": "^1.12.0",
|
||||
"ecc-jsbn": "~0.1.1",
|
||||
"getpass": "^0.1.1",
|
||||
"jsbn": "~0.1.0",
|
||||
"safer-buffer": "^2.0.2",
|
||||
"tweetnacl": "~0.14.0"
|
||||
}
|
||||
},
|
||||
"statuses": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
|
||||
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
|
||||
},
|
||||
"toidentifier": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
|
||||
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
|
||||
},
|
||||
"tough-cookie": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
|
||||
"integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"psl": "^1.1.28",
|
||||
"punycode": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"tslib": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz",
|
||||
"integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA=="
|
||||
},
|
||||
"tunnel-agent": {
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"tweetnacl": {
|
||||
"version": "0.14.5",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
|
||||
"optional": true
|
||||
},
|
||||
"type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
||||
"requires": {
|
||||
"media-typer": "0.3.0",
|
||||
"mime-types": "~2.1.24"
|
||||
}
|
||||
},
|
||||
"unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
|
||||
},
|
||||
"uri-js": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
|
||||
"integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
|
||||
},
|
||||
"uuid": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
|
||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
|
||||
"optional": true
|
||||
},
|
||||
"vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
|
||||
},
|
||||
"verror": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
|
||||
"integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"assert-plus": "^1.0.0",
|
||||
"core-util-is": "1.0.2",
|
||||
"extsprintf": "^1.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "snek",
|
||||
"version": "1.0.0",
|
||||
"description": "A simple Snake",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "make",
|
||||
"start": "node index.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@ssh.gitdab.com:Codinget/Snek.git"
|
||||
},
|
||||
"author": "Codinget",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.17.1",
|
||||
"less": "^3.11.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Snek - Help</title>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="favicon" href="favicon.ico">
|
||||
<link rel="stylesheet" href="css/snek.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="help">
|
||||
<article>
|
||||
<h2>Controls</h2>
|
||||
<p>
|
||||
On keyboard, the game is played with the arrow keys.<br>
|
||||
You can use the <code>f</code> key to go fullscreen.<br>
|
||||
You can also use the <code>r</code> key to quickly restart a game.<br>
|
||||
</p>
|
||||
<p>
|
||||
On mobile or with a touchscreen, the game can be played in 3 modes.<br>
|
||||
In crosspad mode, the screen is divided into 4 regions, and each one corresponds to a direction.<br>
|
||||
In joystick mode, the point where you press your finger is the center of a virtual joystick, and moving it will trigger the directions.<br>
|
||||
In swipe mode, you just swipe to move.
|
||||
</p>
|
||||
</article>
|
||||
<article>
|
||||
<h2>Game modes</h2>
|
||||
<section>
|
||||
<h3>Speedrun mode</h3>
|
||||
<p>
|
||||
In speedrun mode, the goal is to collect all the fruits as fast as possible.<br>
|
||||
Attempting to go through the edge of the playfield will kill you.<br>
|
||||
Each level introduces a new mechanic.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h3>Arcade</h3>
|
||||
<p>
|
||||
Arcade mode is mostly like the classics.<br>
|
||||
In arcade mode, you can go through the edge of the playfield and come out on the other side.<br>
|
||||
Super fruits and decaying fruits may also appear. They are worth <em>10</em> and <em>5</em> points respectively.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h3>Timed</h3>
|
||||
<p>
|
||||
Timed mode is a bare-bones mode with a looping playfield.<br>
|
||||
A timed game only lasts for 30 seconds, and the goal is to get as high a score as possible.
|
||||
</p>
|
||||
</section>
|
||||
</article>
|
||||
<article>
|
||||
<h2>Tiles</h2>
|
||||
<ul>
|
||||
<li><em>Walls</em> kill you if you touch them</li>
|
||||
<li><em>Fruits</em> give you one point and make you grow. In arcade gamemodes, they also make the game go faster</li>
|
||||
<li><em>Holes</em> kill you if the entire snake is over a hole, or the head and 3 subsequent tiles are over a hole</li>
|
||||
<li><em>Fire</em> kills you if you touch it</li>
|
||||
<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>
|
||||
</ul>
|
||||
</article>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Snek</title>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="favicon" href="favicon.ico">
|
||||
<link rel="stylesheet" href="css/snek.css">
|
||||
<script src="js/snek.js"></script>
|
||||
<script>
|
||||
window.addEventListener('load', () => require('main'));
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<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>
|
||||
</ul>
|
||||
</header>
|
||||
<main>
|
||||
<nav></nav>
|
||||
<canvas class="hidden"></canvas>
|
||||
<div id="hud" class="hidden"></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>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
../levels/
|
||||
@@ -0,0 +1,128 @@
|
||||
const ProgressBar=require('progress');
|
||||
|
||||
const assetSpecs=[
|
||||
{ name: 'fruit', filename: 'apple32.png', type: 'image' },
|
||||
{ name: 'superFruit', filename: 'peach-rainbow-anim.png', type: 'image' },
|
||||
{ name: 'decayFruit', filename: 'peach-decay-anim.png', type: 'image' },
|
||||
{ name: 'wall', filename: 'wall32.png', type: 'image' },
|
||||
{ name: 'flammable', filename: 'oil32.png', type: 'image' },
|
||||
{ name: 'hole', filename: 'hole-ts.png', type: 'image' },
|
||||
{ name: 'fire', filename: 'fire-anim.png', type: 'image' },
|
||||
{ name: 'snake', filename: 'snake.json', type: 'json' },
|
||||
{ name: 'levelList', filename: 'levelList.json', type: 'json' },
|
||||
{ name: 'config', filename: 'config.json', type: 'json' },
|
||||
{ name: 'metaConfig', filename: 'metaConfig.json', type: 'json' }
|
||||
];
|
||||
|
||||
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: 'superFruit', type: 'animation', steps: 5 },
|
||||
{ from: 'decayFruit', type: 'animation', steps: 5 }
|
||||
];
|
||||
|
||||
const cvs=document.createElement('canvas');
|
||||
cvs.width=400;
|
||||
cvs.height=50;
|
||||
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);
|
||||
|
||||
document.body.appendChild(cvs);
|
||||
setTimeout(() => cvs.classList.remove('hiddenBottom'), 0);
|
||||
|
||||
bar.addReadyListener(() => {
|
||||
cvs.classList.add('hiddenBottom');
|
||||
setTimeout(() => document.body.removeChild(cvs), 1000);
|
||||
});
|
||||
|
||||
//XXX purposefully slow down asset loading
|
||||
const sleep=(ms=500) => new Promise(ok => setTimeout(ok, ms*Math.random()));
|
||||
|
||||
const loadAsset=async (asset) => {
|
||||
const response=await fetch('assets/'+asset.filename);
|
||||
await sleep();
|
||||
bar.update();
|
||||
let result;
|
||||
switch(asset.type) {
|
||||
case 'json':
|
||||
result=await response.json();
|
||||
break;
|
||||
|
||||
case 'image':
|
||||
result=await createImageBitmap(await response.blob());
|
||||
break;
|
||||
}
|
||||
await sleep();
|
||||
bar.update();
|
||||
return [asset.name, result];
|
||||
};
|
||||
|
||||
let assets=Object.create(null);
|
||||
let ready=false;
|
||||
let readyListeners=[];
|
||||
|
||||
(async () => {
|
||||
let arr=await Promise
|
||||
.all(
|
||||
assetSpecs.map(a => loadAsset(a))
|
||||
);
|
||||
|
||||
arr.forEach(([name, value]) => {
|
||||
assets[name]=value;
|
||||
});
|
||||
|
||||
for(let task of tasks) {
|
||||
const source=assets[task.from];
|
||||
switch(task.type) {
|
||||
case 'tileset': {
|
||||
let asset=assets[task.from]=Object.create(null);
|
||||
for(let tId in task.tiles) {
|
||||
const tName=task.tiles[tId];
|
||||
asset[tName]=await createImageBitmap(source, 0, source.width*tId, source.width, source.width);
|
||||
if(tId%(task.tiles.length/task.steps)==0) {
|
||||
await sleep(100);
|
||||
bar.update();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'animation': {
|
||||
let anim=assets[task.from]=[];
|
||||
let frameCount=source.height/source.width;
|
||||
for(let i=0; i<frameCount; i++) {
|
||||
anim[i]=await createImageBitmap(source, 0, source.width*i, source.width, source.width);
|
||||
if(i%(frameCount/task.steps)==0) {
|
||||
await sleep(100);
|
||||
bar.update();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ready=true;
|
||||
readyListeners.forEach(fn => fn.bind(fn)());
|
||||
readyListeners=null;
|
||||
})();
|
||||
|
||||
const onReady=(fn) => {
|
||||
if(ready) fn.bind(fn)();
|
||||
else readyListeners.push(fn);
|
||||
};
|
||||
|
||||
const get=(name) => {
|
||||
let asset=assets[name];
|
||||
if(!asset) throw new Error("Unknown asset: "+name);
|
||||
return asset;
|
||||
};
|
||||
|
||||
return {
|
||||
onReady, get
|
||||
};
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
const assets=require('assets');
|
||||
|
||||
let watchers=Object.create(null);
|
||||
let lastWatchCode=1;
|
||||
|
||||
const toBoolean=v => {
|
||||
if(v=='false' || v==false) return false;
|
||||
return true;
|
||||
};
|
||||
const notify=(key, value) => {
|
||||
const interested=watchers[key];
|
||||
if(interested) Object
|
||||
.keys(interested)
|
||||
.map(key => interested[key])
|
||||
.forEach(fn => fn(key, value));
|
||||
};
|
||||
|
||||
const get=key => {
|
||||
let confVal=localStorage.getItem('config.'+key);
|
||||
if(confVal===null) return assets.get('config')[key];
|
||||
return confVal;
|
||||
};
|
||||
const getB=key => toBoolean(get(key));
|
||||
const getN=key => +get(key);
|
||||
const getS=key => ''+get(key);
|
||||
|
||||
const set=(key, value) => {
|
||||
localStorage.setItem('config.'+key, value);
|
||||
notify(key, value);
|
||||
};
|
||||
|
||||
const remove=key => {
|
||||
localStorage.removeItem('config.'+key, value);
|
||||
notify(key, assets.get('config')[key]);
|
||||
};
|
||||
const clear=() =>
|
||||
Object
|
||||
.keys(assets.get('config'))
|
||||
.forEach(remove);
|
||||
|
||||
const watch=(key, fn) => {
|
||||
if(!watchers[key]) watchers[key]=[];
|
||||
const code='w'+lastWatchCode++;
|
||||
watchers[key][code]=fn;
|
||||
return code;
|
||||
};
|
||||
const watchB=(key, fn) => watch(key, (k, v) => fn(k, toBoolean(v)));
|
||||
const watchN=(key, fn) => watch(key, (k, v) => fn(k, +v));
|
||||
const watchS=(key, fn) => watch(key, (k, v) => fn(k, ''+v));
|
||||
|
||||
const unwatch=(key, code) => {
|
||||
if(!watchers[key]) return;
|
||||
delete watchers[key][code];
|
||||
};
|
||||
|
||||
const list=() =>
|
||||
Object
|
||||
.keys(assets.get('config'));
|
||||
const dict=() => {
|
||||
let dict=Object.create(null);
|
||||
Object
|
||||
.keys(assets.get('config'))
|
||||
.forEach(
|
||||
key => dict[key]={
|
||||
raw: get(key),
|
||||
b: getB(key),
|
||||
n: getN(key),
|
||||
s: getS(key)
|
||||
}
|
||||
);
|
||||
return dict;
|
||||
};
|
||||
|
||||
return module.exports={
|
||||
get, getB, getN, getS,
|
||||
set,
|
||||
remove, clear,
|
||||
watch, watchB, watchN, watchS,
|
||||
unwatch,
|
||||
list, dict
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
const config=require('config');
|
||||
const assets=require('assets');
|
||||
const Popup=require('popup');
|
||||
|
||||
let lastCEId=0;
|
||||
class ConfigEditor extends Popup {
|
||||
constructor() {
|
||||
super("Config editor", [], {ok: "OK"});
|
||||
|
||||
const metaConfig=assets.get('metaConfig');
|
||||
|
||||
this.watchers=[];
|
||||
|
||||
for(let key in metaConfig) {
|
||||
const level=Array.from(key).reduce((a, c) => a+c=='.', 0)+2;
|
||||
const data=metaConfig[key];
|
||||
|
||||
if(!data.type) {
|
||||
this.addHeading(data.name);
|
||||
} else {
|
||||
let span=document.createElement('span');
|
||||
let id='cfgInput-'+(lastCEId++)+'-'+key.replace(/\./g, '-');
|
||||
let label=span.appendChild(document.createElement('label'));
|
||||
label.innerText=data.name;
|
||||
label.title=key;
|
||||
|
||||
let input;
|
||||
if(data.type=='boolean') {
|
||||
input=document.createElement('input');
|
||||
input.type='checkbox';
|
||||
input.checked=config.getB(key);
|
||||
input.addEventListener('change', () => config.set(key, input.checked));
|
||||
} else if(data.type=='choice') {
|
||||
input=document.createElement('select');
|
||||
data.bounds.choices.forEach(choice => {
|
||||
let option=document.createElement('option');
|
||||
option.value=choice;
|
||||
option.innerText=choice;
|
||||
input.appendChild(option);
|
||||
});
|
||||
input.value=config.getS(key);
|
||||
input.addEventListener('change', () => config.set(key, input.value));
|
||||
} else if(data.type=='number') {
|
||||
input=document.createElement('input');
|
||||
input.type='number';
|
||||
if(data.bounds) {
|
||||
input.setAttribute('min', data.bounds.min);
|
||||
input.setAttribute('max', data.bounds.max);
|
||||
input.setAttribute('step', data.bounds.inc);
|
||||
}
|
||||
input.value=config.getN(key);
|
||||
input.addEventListener('change', () => config.set(key, input.value));
|
||||
}
|
||||
|
||||
input.setAttribute('id', id);
|
||||
span.appendChild(input);
|
||||
label.setAttribute('for', id);
|
||||
this.addContent(span);
|
||||
|
||||
if(data.excludes) {
|
||||
const setEnabled=() =>
|
||||
input.disabled=
|
||||
data.excludes
|
||||
.some(key => config.getB(key))
|
||||
|
||||
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);
|
||||
this.watchers.push([data.parent, c]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.large=true;
|
||||
}
|
||||
|
||||
discard() {
|
||||
this.watchers.forEach(([k, c]) => config.unwatch(k, c));
|
||||
}
|
||||
};
|
||||
|
||||
return module.exports={
|
||||
show: async () => {
|
||||
let editor=new ConfigEditor();
|
||||
await editor.display();
|
||||
editor.discard();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,291 @@
|
||||
const config=require('config');
|
||||
|
||||
let currentInputs={};
|
||||
let handlers=[];
|
||||
let hud;
|
||||
|
||||
const toAngleMagnitude=(x, y) => {
|
||||
return {
|
||||
angle: ((Math.atan2(x, y)+2*Math.PI)%(2*Math.PI))/Math.PI,
|
||||
magnitude: Math.hypot(x, y)
|
||||
};
|
||||
};
|
||||
|
||||
const handleAngleMagnitude=(x, y, threshold=0, fn=null) => {
|
||||
const {angle, magnitude}=toAngleMagnitude(x, y);
|
||||
|
||||
if(magnitude>threshold) {
|
||||
let inputs=currentInputs;
|
||||
if(angle>.25 && angle <.75) inputs.right=true;
|
||||
else if(angle>.75 && angle<1.25) inputs.up=true;
|
||||
else if(angle>1.25 && angle<1.75) inputs.left=true;
|
||||
else inputs.down=true;
|
||||
|
||||
if(fn) fn(angle, magnitude);
|
||||
}
|
||||
};
|
||||
|
||||
const removeChild=(parent, child) => {
|
||||
if(child.parentNode==parent) parent.removeChild(child);
|
||||
};
|
||||
|
||||
const handleCrosspad=(() => {
|
||||
const ns='http://www.w3.org/2000/svg';
|
||||
const cross=document.createElementNS(ns, 'svg');
|
||||
cross.classList.add('crosspadOverlay');
|
||||
cross.setAttribute('width', 1000);
|
||||
cross.setAttribute('height', 1000);
|
||||
let dr=document.createElementNS(ns, 'line');
|
||||
dr.setAttribute('x1', 0);
|
||||
dr.setAttribute('y1', 0);
|
||||
dr.setAttribute('x2', 1000);
|
||||
dr.setAttribute('y2', 1000);
|
||||
dr.setAttribute('stroke', 'black');
|
||||
cross.appendChild(dr);
|
||||
let dl=document.createElementNS(ns, 'line');
|
||||
dl.setAttribute('x1', 1000);
|
||||
dl.setAttribute('y1', 0);
|
||||
dl.setAttribute('x2', 0);
|
||||
dl.setAttribute('y2', 1000);
|
||||
dl.setAttribute('stroke', 'black');
|
||||
cross.appendChild(dl);
|
||||
|
||||
let useOverlay=false;
|
||||
let enabled=false;
|
||||
const displayOverlay=() => {
|
||||
if(useOverlay && enabled) hud.appendChild(cross);
|
||||
else removeChild(hud, cross);
|
||||
};
|
||||
config.watchB('input.touchscreen.crosspad.overlay', (k, v) => {
|
||||
useOverlay=v;
|
||||
displayOverlay();
|
||||
});
|
||||
|
||||
const fn=e =>
|
||||
handleAngleMagnitude(
|
||||
e.touches[0].clientX-window.innerWidth/2,
|
||||
e.touches[0].clientY-window.innerHeight/2,
|
||||
0,
|
||||
null
|
||||
);
|
||||
|
||||
const init=() => {
|
||||
useOverlay=config.getB('input.touchscreen.crosspad.overlay');
|
||||
enabled=true;
|
||||
displayOverlay();
|
||||
};
|
||||
const fini=() => {
|
||||
enabled=false;
|
||||
displayOverlay();
|
||||
};
|
||||
|
||||
return {
|
||||
touchstart: fn,
|
||||
touchmove: fn,
|
||||
init, fini
|
||||
};
|
||||
})();
|
||||
|
||||
const handleKeyboard={
|
||||
keydown: e => {
|
||||
let inputs=currentInputs;
|
||||
if(e.key=='ArrowUp') inputs.up=true;
|
||||
else if(e.key=='ArrowDown') inputs.down=true;
|
||||
else if(e.key=='ArrowLeft') inputs.left=true;
|
||||
else if(e.key=='ArrowRight') inputs.right=true;
|
||||
}
|
||||
};
|
||||
|
||||
const handleJoystick=(() => {
|
||||
let cvs=document.createElement('canvas');
|
||||
cvs.classList.add('joystickOverlay');
|
||||
let ctx=cvs.getContext('2d');
|
||||
let enabled=false;
|
||||
let useOverlay=false;
|
||||
let firstTouch=false;
|
||||
|
||||
let center={
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
let deadzone;
|
||||
|
||||
const displayOverlay=() => {
|
||||
if(!enabled || !useOverlay || !firstTouch) return removeChild(hud, cvs);
|
||||
|
||||
cvs.width=cvs.height=4*deadzone+120;
|
||||
hud.appendChild(cvs);
|
||||
ctx.clearRect(0, 0, cvs.width, cvs.width);
|
||||
|
||||
ctx.strokeStyle='black';
|
||||
ctx.lineWidth=1;
|
||||
ctx.beginPath();
|
||||
const rad=2*deadzone+50;
|
||||
ctx.moveTo(rad*Math.cos(Math.PI/4)+cvs.width/2, rad*Math.sin(Math.PI/4)+cvs.width/2);
|
||||
ctx.lineTo(rad*Math.cos(Math.PI/4+Math.PI)+cvs.width/2, rad*Math.sin(Math.PI/4+Math.PI)+cvs.width/2);
|
||||
ctx.moveTo(rad*Math.cos(-Math.PI/4)+cvs.width/2, rad*Math.sin(-Math.PI/4)+cvs.width/2);
|
||||
ctx.lineTo(rad*Math.cos(-Math.PI/4+Math.PI)+cvs.width/2, rad*Math.sin(-Math.PI/4+Math.PI)+cvs.width/2);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.strokeStyle='gray';
|
||||
ctx.lineWidth=2;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(cvs.width/2, cvs.width/2, deadzone, deadzone, 0, 0, Math.PI*2);
|
||||
ctx.stroke();
|
||||
ctx.lineWidth=3;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(cvs.width/2, cvs.width/2, deadzone*2+50, deadzone*2+50, 0, 0, Math.PI*2);
|
||||
ctx.stroke();
|
||||
|
||||
cvs.style.left=center.x+'px';
|
||||
cvs.style.top=center.y+'px';
|
||||
};
|
||||
|
||||
const init=() => {
|
||||
enabled=true;
|
||||
deadzone=config.getN('input.touchscreen.joystick.deadzone');
|
||||
useOverlay=config.getB('input.touchscreen.joystick.overlay');
|
||||
displayOverlay();
|
||||
};
|
||||
const fini=() => {
|
||||
enabled=false;
|
||||
displayOverlay();
|
||||
};
|
||||
config.watchN('input.touchscreen.joystick.deadzone', (k, v) => {
|
||||
deadzone=v;
|
||||
displayOverlay();
|
||||
});
|
||||
config.watchB('input.touchscreen.joystick.overlay', (k, v) => {
|
||||
useOverlay=v;
|
||||
displayOverlay();
|
||||
});
|
||||
|
||||
return {
|
||||
init, fini,
|
||||
touchstart: e => {
|
||||
center.x=e.touches[0].clientX;
|
||||
center.y=e.touches[0].clientY;
|
||||
firstTouch=true;
|
||||
displayOverlay();
|
||||
},
|
||||
touchmove: e =>
|
||||
handleAngleMagnitude(
|
||||
e.touches[0].clientX-center.x,
|
||||
e.touches[0].clientY-center.y,
|
||||
deadzone,
|
||||
null
|
||||
)
|
||||
}
|
||||
})();
|
||||
|
||||
const handleSwipe=(() => {
|
||||
let center={
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
let deadzone;
|
||||
|
||||
let resetCenter=e => {
|
||||
center.x=e.touches[0].clientX;
|
||||
center.y=e.touches[0].clientY;
|
||||
};
|
||||
|
||||
const init=() => {
|
||||
deadzone=config.getN('input.touchscreen.swipe.deadzone');
|
||||
};
|
||||
config.watchN('input.touchscreen.swipe.deadzone', (k, v) => {
|
||||
deadzone=v;
|
||||
});
|
||||
|
||||
return {
|
||||
init,
|
||||
touchstart: resetCenter,
|
||||
touchmove: e =>
|
||||
handleAngleMagnitude(
|
||||
e.touches[0].clientX-center.x,
|
||||
e.touches[0].clientY-center.y,
|
||||
deadzone,
|
||||
() => resetCenter(e)
|
||||
)
|
||||
}
|
||||
})();
|
||||
|
||||
const handleGamepads=(() => {
|
||||
let deadzone;
|
||||
|
||||
const init=() => {
|
||||
deadzone=config.getN('input.touchscreen.swipe.deadzone');
|
||||
};
|
||||
config.watchN('input.touchscreen.swipe.deadzone', (k, v) => {
|
||||
deadzone=v;
|
||||
});
|
||||
|
||||
return {
|
||||
init,
|
||||
frame: () => {
|
||||
const gp=navigator.getGamepads()[0];
|
||||
let inputs=currentInputs;
|
||||
if(!gp || !gp.axes) return;
|
||||
|
||||
handleAngleMagnitude(
|
||||
gp.axes[0],
|
||||
gp.axes[1],
|
||||
deadzone,
|
||||
null
|
||||
);
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
const handleEvent=(type, evt) => {
|
||||
for(let handler of handlers) {
|
||||
let fn=handler[type];
|
||||
if(fn) fn(evt);
|
||||
}
|
||||
};
|
||||
|
||||
const enableHandler=handler => {
|
||||
if(!handlers.includes(handler)) {
|
||||
handlers.push(handler);
|
||||
if(handler.init) handler.init();
|
||||
}
|
||||
};
|
||||
const disableHandler=handler => {
|
||||
let idx=handlers.indexOf(handler);
|
||||
if(idx!=-1) {
|
||||
handlers.splice(idx, 1);
|
||||
if(handler.fini) handler.fini();
|
||||
}
|
||||
};
|
||||
const linkHandler=(handler, key) => {
|
||||
if(config.getB(key)) enableHandler(handler);
|
||||
config.watchB(key, (k, v) => {
|
||||
if(v) enableHandler(handler);
|
||||
else disableHandler(handler);
|
||||
});
|
||||
};
|
||||
|
||||
const init=({hud: hudElem}) => {
|
||||
hud=hudElem;
|
||||
linkHandler(handleCrosspad, 'input.touchscreen.crosspad.enabled');
|
||||
linkHandler(handleJoystick, 'input.touchscreen.joystick.enabled');
|
||||
linkHandler(handleSwipe, 'input.touchscreen.swipe.enabled');
|
||||
linkHandler(handleGamepads, 'input.gamepad.enabled');
|
||||
linkHandler(handleKeyboard, 'input.keyboard.enabled');
|
||||
};
|
||||
|
||||
const clear=() =>
|
||||
Object
|
||||
.keys(currentInputs)
|
||||
.forEach(key => delete currentInputs[key]);
|
||||
|
||||
for(let type of ['keydown', 'touchstart', 'touchmove']) {
|
||||
window.addEventListener(type, handleEvent.bind(null, type));
|
||||
}
|
||||
|
||||
return module.exports={
|
||||
inputs: currentInputs,
|
||||
clear,
|
||||
framefn: handleEvent.bind(null, 'frame'),
|
||||
init
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
const cache=Object.create(null);
|
||||
|
||||
const get=async filename => {
|
||||
if(cache[filename]) return cache[filename];
|
||||
const req=await fetch('levels/'+filename);
|
||||
const json=await req.json();
|
||||
return cache[filename]=json;
|
||||
};
|
||||
|
||||
const clearCache=() =>
|
||||
Object
|
||||
.keys(cache)
|
||||
.forEach(key => delete cache[key]);
|
||||
|
||||
return module.exports={
|
||||
get,
|
||||
clearCache
|
||||
};
|
||||
@@ -0,0 +1,288 @@
|
||||
(async () => {
|
||||
|
||||
// load modules
|
||||
const assets=require('assets');
|
||||
const Popup=require('popup');
|
||||
const SnekGame=require('snek');
|
||||
const configEditor=require('configEditor');
|
||||
const input=require('input');
|
||||
const levels=require('levels');
|
||||
const config=require('config');
|
||||
|
||||
// get a known state
|
||||
await new Promise(ok => assets.onReady(ok));
|
||||
location.hash='menu';
|
||||
|
||||
// get our DOM in check
|
||||
const main=document.querySelector('main');
|
||||
const nav=main.querySelector('nav');
|
||||
const canvas=main.querySelector('canvas');
|
||||
const hud=main.querySelector('#hud');
|
||||
|
||||
// load data from server
|
||||
const levelList=assets.get('levelList');
|
||||
|
||||
// get our global variables
|
||||
let currentGame=null;
|
||||
|
||||
// forward-declare functions
|
||||
let resizeCanvas, getLevel, startGame, stopGame, handleWin, handleDeath, menu, help, settings, restart;
|
||||
|
||||
// handle window resize and fullscreen
|
||||
resizeCanvas=() => {
|
||||
if(document.fullscreenElement) {
|
||||
canvas.width=screen.width;
|
||||
canvas.height=screen.height;
|
||||
} else {
|
||||
canvas.width=main.clientWidth;
|
||||
canvas.height=main.clientHeight;
|
||||
}
|
||||
};
|
||||
resizeCanvas();
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
window.addEventListener('keydown', async e => {
|
||||
if(e.key=='f') {
|
||||
if(document.fullscreenElement) await document.exitFullscreen();
|
||||
else await main.requestFullscreen();
|
||||
resizeCanvas();
|
||||
}
|
||||
});
|
||||
|
||||
// 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];
|
||||
|
||||
const section=nav.appendChild(document.createElement('section'));
|
||||
const h1=section.appendChild(document.createElement('h1'));
|
||||
h1.innerText=category[0].toUpperCase()+category.slice(1)+" Mode";
|
||||
|
||||
const p=section.appendChild(document.createElement('p'));
|
||||
p.innerText=cat.desc;
|
||||
|
||||
const ul=section.appendChild(document.createElement('ul'));
|
||||
cat.levels.forEach((level, i) => {
|
||||
const {displayName, fileName, levelString}=getLevel(category, level);
|
||||
const li=ul.appendChild(document.createElement('li'));
|
||||
const a=li.appendChild(document.createElement('a'));
|
||||
a.href='#'+levelString;
|
||||
a.innerText=displayName;
|
||||
if(cat.levelDesc) {
|
||||
const span=li.appendChild(document.createElement('span'));
|
||||
span.innerText=cat.levelDesc[i];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// stop a running game
|
||||
stopGame=() => {
|
||||
if(currentGame) {
|
||||
// stop the actual game
|
||||
currentGame.playing=false;
|
||||
|
||||
// setup the DOM
|
||||
nav.classList.remove('hidden');
|
||||
canvas.classList.add('hidden');
|
||||
hud.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
// start a new game
|
||||
startGame=async (category, levelId, filename) => {
|
||||
// stop any running games and clear popups
|
||||
stopGame();
|
||||
Popup.dismiss();
|
||||
|
||||
// load rules and level from cache or server
|
||||
const rules=levelList[category].rules || {};
|
||||
const level=await levels.get(filename);
|
||||
|
||||
// stop any running games and clear popups again
|
||||
stopGame();
|
||||
Popup.dismiss();
|
||||
|
||||
// create the game and attach the callbacks and config
|
||||
const snek=currentGame=new SnekGame(level, canvas, rules);
|
||||
snek.callback=evt => {
|
||||
if(evt=='tick') {
|
||||
input.framefn();
|
||||
snek.handleInputs(input.inputs);
|
||||
} else if(evt=='win') {
|
||||
handleWin(snek);
|
||||
} else if(evt=='die') {
|
||||
handleDeath(snek);
|
||||
}
|
||||
};
|
||||
|
||||
// setup the DOM
|
||||
nav.classList.add('hidden');
|
||||
canvas.classList.remove('hidden');
|
||||
hud.classList.remove('hidden');
|
||||
|
||||
// push some userdata to the snake
|
||||
snek.userdata={
|
||||
category,
|
||||
levelId,
|
||||
filename
|
||||
};
|
||||
|
||||
// reset the inputs
|
||||
input.clear();
|
||||
|
||||
// start the actual game
|
||||
snek.start();
|
||||
};
|
||||
|
||||
// return to the menu
|
||||
menu=() => {
|
||||
stopGame();
|
||||
Popup.dismiss();
|
||||
};
|
||||
|
||||
// show config editor
|
||||
settings=async () => {
|
||||
stopGame();
|
||||
Popup.dismiss();
|
||||
await configEditor.show();
|
||||
location.hash='menu';
|
||||
};
|
||||
|
||||
// show help page
|
||||
help=async () => {
|
||||
stopGame();
|
||||
Popup.dismiss();
|
||||
let iframe=document.createElement('iframe');
|
||||
iframe.src='help.html';
|
||||
iframe.style.width='100%';
|
||||
iframe.style.height='100%';
|
||||
await new Popup(
|
||||
"Help",
|
||||
[iframe],
|
||||
{ok: "OK"},
|
||||
true
|
||||
).display();
|
||||
location.hash='menu';
|
||||
};
|
||||
|
||||
// display the win popup
|
||||
handleWin=async snek => {
|
||||
// hide the HUD
|
||||
hud.classList.add('hidden');
|
||||
|
||||
// fetch userdata from the game
|
||||
const {category, levelId, filename}=snek.userdata;
|
||||
|
||||
// create and configure popup
|
||||
let popup=new Popup("Finished!");
|
||||
popup.addStrong("You won!");
|
||||
popup.addContent({
|
||||
"Time": snek.playTime/1000+'s',
|
||||
"Score": snek.score,
|
||||
"Final length": snek.length
|
||||
});
|
||||
popup.buttons={
|
||||
retry: "Retry",
|
||||
menu: "Main menu"
|
||||
};
|
||||
if(levelList[category].nextLevel) {
|
||||
let nextId=(+levelId)+1;
|
||||
if(levelList[category].levels.includes(nextId)) popup.buttons.next="Next level";
|
||||
}
|
||||
|
||||
// show the actual popup
|
||||
let result=await popup.display(main);
|
||||
|
||||
// act on it
|
||||
if(result=='retry') {
|
||||
startGame(category, levelId, filename);
|
||||
} else if(result=='menu') {
|
||||
location.hash='menu';
|
||||
} else if(result=='next') {
|
||||
const {category, levelId}=snek.userdata;
|
||||
let nextId=(+levelId)+1;
|
||||
let {levelString}=getLevel(category, nextId)
|
||||
location.hash=levelString;
|
||||
}
|
||||
};
|
||||
|
||||
// display the death popup
|
||||
handleDeath=async snek => {
|
||||
// hide the HUD
|
||||
hud.classList.add('hidden');
|
||||
|
||||
// create and configure popup
|
||||
let popup=new Popup("Finished!");
|
||||
popup.addStrong("You died...");
|
||||
popup.addContent({
|
||||
"Time": snek.playTime/1000+'s',
|
||||
"Score": snek.score,
|
||||
"Final length": snek.length
|
||||
});
|
||||
popup.buttons={
|
||||
retry: "Retry",
|
||||
menu: "Main menu"
|
||||
};
|
||||
|
||||
// show the actual popup
|
||||
let result=await popup.display(main);
|
||||
|
||||
// act on it
|
||||
if(result=='retry') {
|
||||
const {category, levelId, filename}=snek.userdata;
|
||||
startGame(category, levelId, filename);
|
||||
} else if(result=='menu') {
|
||||
location.hash='menu';
|
||||
}
|
||||
};
|
||||
|
||||
// quick restart
|
||||
restart=() => {
|
||||
if(currentGame && currentGame.playing) {
|
||||
const {category, levelId, filename}=currentGame.userdata;
|
||||
startGame(category, levelId, filename);
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', e => {
|
||||
if(e.key=='r') restart();
|
||||
});
|
||||
(() => {
|
||||
let restartbtn=hud.appendChild(document.createElement('span'));
|
||||
restartbtn.classList.add('restart');
|
||||
restartbtn.addEventListener('click', restart);
|
||||
restartbtn.addEventListener('touchend', restart);
|
||||
})();
|
||||
|
||||
// handle page navigation
|
||||
window.addEventListener('hashchange', () => {
|
||||
const hash=location.hash.substr(1);
|
||||
|
||||
if(hash=='' || hash=='menu') return menu();
|
||||
else if(hash=='help') return help();
|
||||
else if(hash=='settings') return settings();
|
||||
|
||||
const [_, category, levelId, filename]=location.hash.match(/([a-zA-Z0-9_-]+?)\/([a-zA-Z0-9_-]+?)\/(.+)/);
|
||||
startGame(category, levelId, filename);
|
||||
});
|
||||
|
||||
// enable input methods overlay
|
||||
input.init({hud});
|
||||
})();
|
||||
@@ -0,0 +1,106 @@
|
||||
const objToDom=obj => {
|
||||
if(obj instanceof Node) {
|
||||
return obj;
|
||||
} else if(obj[Popup.EM]) {
|
||||
let em=document.createElement('em');
|
||||
em.appendChild(document.createTextNode(obj[Popup.EM]));
|
||||
return em;
|
||||
} else if(obj[Popup.STRONG]) {
|
||||
let em=document.createElement('strong');
|
||||
em.appendChild(document.createTextNode(obj[Popup.STRONG]));
|
||||
return em;
|
||||
} else if(typeof obj=='string' || typeof obj=='number') {
|
||||
return document.createTextNode(obj+'');
|
||||
} else if(Array.isArray(obj)) {
|
||||
let ul=document.createElement('ul');
|
||||
obj.forEach(elem => ul.appendChild(objToDom(elem)));
|
||||
return ul;
|
||||
} else {
|
||||
let table=document.createElement('table');
|
||||
Object
|
||||
.keys(obj)
|
||||
.forEach(key => {
|
||||
let tr=table.appendChild(document.createElement('tr'));
|
||||
tr.appendChild(document.createElement('th')).appendChild(document.createTextNode(key));
|
||||
tr.appendChild(document.createElement('td')).appendChild(objToDom(obj[key]));
|
||||
});
|
||||
return table;
|
||||
}
|
||||
}
|
||||
|
||||
class Popup {
|
||||
constructor(title, content=[], buttons={}, large=false) {
|
||||
this.title=title;
|
||||
this.content=content.map(objToDom);
|
||||
this.buttons={...buttons};
|
||||
this.large=large;
|
||||
}
|
||||
|
||||
addContent(cnt) {
|
||||
this.content.push(objToDom(cnt));
|
||||
}
|
||||
addText(cnt) {
|
||||
this.content.push(document.createTextNode(cnt));
|
||||
}
|
||||
addEm(cnt) {
|
||||
this.content.push(objToDom({[Popup.EM]: cnt}));
|
||||
}
|
||||
addStrong(cnt) {
|
||||
this.content.push(objToDom({[Popup.STRONG]: cnt}));
|
||||
}
|
||||
addHeading(cnt, level=2) {
|
||||
let hn=document.createElement('h'+level);
|
||||
hn.innerText=cnt;
|
||||
this.content.push(hn);
|
||||
}
|
||||
|
||||
async display(parent=document.body) {
|
||||
let outer=document.createElement('div');
|
||||
outer.classList.add('popup');
|
||||
let popup=outer.appendChild(document.createElement('div'));
|
||||
popup.classList.add('content');
|
||||
if(this.large) popup.classList.add('large');
|
||||
|
||||
let title=popup.appendChild(document.createElement('h1'));
|
||||
title.innerText=this.title;
|
||||
|
||||
let contentSection=popup.appendChild(document.createElement('section'));
|
||||
this.content.forEach(elem => contentSection.appendChild(elem));
|
||||
|
||||
let buttonSection=popup.appendChild(document.createElement('section'));
|
||||
let buttons=Object.keys(this.buttons).map(btn => {
|
||||
let button=document.createElement('button');
|
||||
button.innerText=this.buttons[btn];
|
||||
button.dataset.code=btn;
|
||||
return button;
|
||||
});
|
||||
buttons.forEach(btn => buttonSection.appendChild(btn));
|
||||
|
||||
parent.appendChild(outer);
|
||||
Popup.displayed.push(this);
|
||||
|
||||
const btnActions=buttons.map(btn => new Promise(ok => {
|
||||
btn.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
return ok(btn.dataset.code);
|
||||
});
|
||||
}));
|
||||
const dismissAction=new Promise(ok => this.dismiss=ok);
|
||||
|
||||
const code=await Promise.race(btnActions.concat([dismissAction]));
|
||||
|
||||
parent.removeChild(outer);
|
||||
Popup.displayed.splice(Popup.displayed.indexOf(this), 1);
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
Popup.EM=Symbol('EM');
|
||||
Popup.STRONG=Symbol('STRONG');
|
||||
|
||||
Popup.displayed=[];
|
||||
Popup.dismiss=arg => {
|
||||
Popup.displayed.forEach(p => p.dismiss(arg));
|
||||
};
|
||||
|
||||
return module.exports=Popup;
|
||||
@@ -0,0 +1,44 @@
|
||||
class ProgressBar {
|
||||
constructor(taskCount) {
|
||||
this.taskCount=taskCount;
|
||||
this.completeCount=0;
|
||||
this.updateListeneres=[];
|
||||
}
|
||||
|
||||
get percent() {
|
||||
return Math.floor(this.completeCount/this.taskCount*100);
|
||||
}
|
||||
|
||||
get ready() {
|
||||
return this.completeCount==this.taskCount;
|
||||
}
|
||||
|
||||
addUpdateListener(fn) {
|
||||
this.updateListeneres.push(fn.bind(this));
|
||||
}
|
||||
addReadyListener(fn) {
|
||||
this.updateListeneres.push(() => {
|
||||
if(this.ready) fn.bind(this)();
|
||||
});
|
||||
}
|
||||
|
||||
update() {
|
||||
this.completeCount++;
|
||||
this.updateListeneres.forEach(l => l(this));
|
||||
}
|
||||
|
||||
draw(canvas, bgColor='red', textColor='black') {
|
||||
let ctx=canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.fillStyle=bgColor;
|
||||
ctx.fillRect(0, 0, canvas.width*this.completeCount/this.taskCount, canvas.height);
|
||||
ctx.fillStyle=textColor;
|
||||
ctx.textAlign='center';
|
||||
ctx.textBaseline='center';
|
||||
ctx.font=`${canvas.height/2}px 'Fira Code'`;
|
||||
ctx.fillText(this.percent+'%', canvas.width/2, canvas.height/2);
|
||||
}
|
||||
}
|
||||
|
||||
return ProgressBar;
|
||||
|
||||
@@ -0,0 +1,617 @@
|
||||
const [EMPTY, FOOD, SUPER_FOOD, DECAY_FOOD, WALL, FIRE, FLAMMABLE, FLAMMABLE_S, HOLE, HOLE_S, SNAKE]=Array(255).keys();
|
||||
|
||||
class SnekGame {
|
||||
constructor(settings, canvas, rules) {
|
||||
// setup the delay
|
||||
this.delay=settings.delay;
|
||||
|
||||
// world is given in the level
|
||||
if(settings.world) { // explicitly
|
||||
|
||||
// convert the world
|
||||
this.world=Array(settings.world[0].length);
|
||||
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;
|
||||
}
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
// extract the dimensions
|
||||
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]);
|
||||
}
|
||||
));
|
||||
|
||||
// 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]);
|
||||
}
|
||||
));
|
||||
} else { // dimension and objects
|
||||
|
||||
// get the dimensions
|
||||
this.dimensions=[...settings.dimensions];
|
||||
|
||||
// build an empty world
|
||||
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);
|
||||
}
|
||||
|
||||
// add the walls
|
||||
if(settings.walls) settings.walls.forEach(([x, y]) => this.world[x][y]=WALL);
|
||||
|
||||
// add the holes
|
||||
if(settings.holes) settings.holes.forEach(([x, y]) => this.world[x][y]=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);
|
||||
|
||||
// add the food
|
||||
settings.food.forEach(([x, y]) => this.world[x][y]=FOOD);
|
||||
this.fruits=[...settings.food];
|
||||
|
||||
// add the super food
|
||||
if(settings.superFood) settings.superFood.forEach(([x, y]) => this.world[x][y]=SUPER_FOOD);
|
||||
|
||||
// add the decaying food
|
||||
if(settings.decayFood) {
|
||||
settings.decayFood.forEach(([x, y]) => this.world[x][y]=DECAY_FOOD);
|
||||
this.decayFood=settings.decayFood.map(([x, y]) => [x, y, 0]);
|
||||
} else {
|
||||
this.decayFood=[];
|
||||
}
|
||||
}
|
||||
|
||||
// add the snake to the world
|
||||
settings.snake.forEach(([x, y]) => this.world[x][y]=SNAKE);
|
||||
|
||||
|
||||
// get the head and initial direction
|
||||
this.head=[...settings.snake[0]];
|
||||
if(settings.snake.length>=2) this.direction=[
|
||||
settings.snake[0][0]-settings.snake[1][0],
|
||||
settings.snake[0][1]-settings.snake[1][1]
|
||||
];
|
||||
else this.direction=[
|
||||
1,
|
||||
0
|
||||
];
|
||||
this.lastDirection=this.direction
|
||||
|
||||
// store the snake
|
||||
this.snake=[...settings.snake];
|
||||
this.length=this.snake.length;
|
||||
|
||||
// get our canvas, like, if we want to actually draw
|
||||
this.canvas=canvas;
|
||||
this.ctx=canvas.getContext('2d');
|
||||
|
||||
// load the custom rules
|
||||
this.rules=Object.assign({
|
||||
fruitRegrow: true,
|
||||
superFruitGrow: false,
|
||||
decayingFruitGrow: false,
|
||||
speedIncrease: true,
|
||||
worldWrap: true,
|
||||
winCondition: 'none',
|
||||
scoreSystem: 'fruit',
|
||||
fireTickSpeed: 10,
|
||||
autoSizeGrow: false,
|
||||
autoSpeedIncrease: false
|
||||
}, rules, settings.rules || {});
|
||||
}
|
||||
|
||||
get playTime() {
|
||||
return Date.now()-this.firstStep;
|
||||
}
|
||||
|
||||
getTilesOfType(type) {
|
||||
return this
|
||||
.world
|
||||
.map(
|
||||
(l, x) => l
|
||||
.map(
|
||||
(r, y) => r==type?[x,y]:null
|
||||
).filter(
|
||||
a => a
|
||||
)
|
||||
).flat();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
step() {
|
||||
this.tickId++;
|
||||
this.lastDirection=this.direction;
|
||||
|
||||
// compute our new head
|
||||
const head=[
|
||||
this.snake[0][0]+this.direction[0],
|
||||
this.snake[0][1]+this.direction[1]
|
||||
];
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// check for out of world conditions
|
||||
if(head[0]<0 || head[0]>=this.dimensions[0] || head[1]<0 || head[1]>=this.dimensions[1]) {
|
||||
if(this.rules.worldWrap) {
|
||||
head[0]=(head[0]+this.dimensions[0])%this.dimensions[0];
|
||||
head[1]=(head[1]+this.dimensions[1])%this.dimensions[1];
|
||||
} else {
|
||||
return this.die();
|
||||
}
|
||||
}
|
||||
|
||||
switch(this.world[head[0]][head[1]]) {
|
||||
// you hit, you die
|
||||
case WALL:
|
||||
case FIRE:
|
||||
case SNAKE:
|
||||
case HOLE_S:
|
||||
return this.die();
|
||||
|
||||
// if either 3 consecutive segments or the whole snake is on a hole, you die
|
||||
case HOLE:
|
||||
if(
|
||||
this.snake.length==0 ||
|
||||
this.snake.length==1 &&
|
||||
this.world[this.snake[0][0]][this.snake[0][1]]==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();
|
||||
break;
|
||||
|
||||
// you eat, you get a massive score boost
|
||||
case SUPER_FOOD:
|
||||
this.score+=10;
|
||||
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 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
|
||||
this.snake.push(tail);
|
||||
this.length++;
|
||||
|
||||
// remove the fruit from existence
|
||||
this.world[head[0]][head[1]]=SNAKE;
|
||||
this.fruits=this.fruits.filter(
|
||||
([x, y]) => !(x==head[0] && y==head[1])
|
||||
);
|
||||
|
||||
// increase score
|
||||
this.score++;
|
||||
|
||||
// custom rules
|
||||
if(this.rules.fruitRegrow) {
|
||||
const emptyCells=this.getTilesOfType(EMPTY);
|
||||
|
||||
const cell=emptyCells[Math.floor(Math.random()*emptyCells.length)];
|
||||
this.fruits.push(cell);
|
||||
this.world[cell[0]][cell[1]]=FOOD;
|
||||
}
|
||||
|
||||
if(this.rules.superFruitGrow) {
|
||||
if(Math.random()<.1) { // 10% chance
|
||||
const emptyCells=this.getTilesOfType(EMPTY);
|
||||
const cell=emptyCells[Math.floor(Math.random()*emptyCells.length)];
|
||||
this.world[cell[0]][cell[1]]=SUPER_FOOD;
|
||||
}
|
||||
}
|
||||
|
||||
if(this.rules.decayingFruitGrow) {
|
||||
if(Math.random()<.2) { // 20% chance
|
||||
const emptyCells=this.getTilesOfType(EMPTY);
|
||||
const cell=emptyCells[Math.floor(Math.random()*emptyCells.length)];
|
||||
this.world[cell[0]][cell[1]]=DECAY_FOOD;
|
||||
this.decayFood.push([cell[0], cell[1], this.playTime]);
|
||||
}
|
||||
}
|
||||
|
||||
if(this.rules.speedIncrease) {
|
||||
this.delay*=this.rules.speedMultiplier;
|
||||
if(this.delay<this.rules.speedCap) this.delay=this.rules.speedCap;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
this.snake.unshift(head);
|
||||
|
||||
// decay decaying food
|
||||
this.decayFood.forEach(
|
||||
([x, y, birth]) => {
|
||||
if(this.playTime>=birth+2000) this.world[x][y]=EMPTY;
|
||||
}
|
||||
);
|
||||
this.decayFood=this.decayFood.filter(
|
||||
([_, __, birth]) => this.playTime<birth+2000
|
||||
);
|
||||
|
||||
// automatic speed increase
|
||||
if(this.rules.autoSpeedIncrease) {
|
||||
if(this.delay>50 && this.tickId%this.rules.autoSpeadIncreaseTicks==0) this.delay--;
|
||||
}
|
||||
|
||||
// automatic size grow
|
||||
if(this.rules.autoSizeGrow) {
|
||||
if(this.tickId%this.rules.autoSizeGrowTicks==0) this.snake.push(tail);
|
||||
}
|
||||
|
||||
// fire tick
|
||||
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]
|
||||
];
|
||||
return surrounding.some(tile => tile==FIRE);
|
||||
};
|
||||
if(this.getTilesOfType(FLAMMABLE_S).some(touchingFire)) return this.die();
|
||||
this.getTilesOfType(FLAMMABLE).filter(touchingFire).forEach(([x, y]) => this.world[x][y]=FIRE);
|
||||
}
|
||||
|
||||
// victory condition
|
||||
if(this.rules.winCondition=='fruit') {
|
||||
if(!this.fruits.length) return this.win();
|
||||
}
|
||||
if(this.rules.winCondition=='time') {
|
||||
if(this.playTime>=this.rules.gameDuration) return this.win();
|
||||
}
|
||||
if(this.rules.winCondition=='score') {
|
||||
if(this.score>=this.rules.scoreObjective) return this.win();
|
||||
}
|
||||
}
|
||||
|
||||
tick() {
|
||||
if(!this.playing) return;
|
||||
if(!this.lastStep) this.lastStep=this.firstStep;
|
||||
this.draw();
|
||||
if(this.callback) this.callback('tick');
|
||||
if(this.lastStep+this.delay<Date.now()) {
|
||||
this.lastStep+=this.delay;
|
||||
this.step();
|
||||
}
|
||||
requestAnimationFrame(() => this.tick());
|
||||
}
|
||||
|
||||
win() {
|
||||
this.playing=false;
|
||||
this.endPlayTime=this.playTime;
|
||||
if(this.callback) this.callback('win');
|
||||
}
|
||||
|
||||
die() {
|
||||
this.playing=false;
|
||||
this.endPlayTime=this.playTime;
|
||||
if(this.callback) this.callback('die');
|
||||
}
|
||||
|
||||
handleInputs(inputs) {
|
||||
const config=require('config');
|
||||
|
||||
// change direction if the input is valid
|
||||
const trySet=(dir) => {
|
||||
if(!dir.every((e, i) => e==this.lastDirection[i] || e==-this.lastDirection[i])) {
|
||||
this.direction=dir;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// reduce buffer duration
|
||||
Object
|
||||
.keys(inputs)
|
||||
.forEach(k => {
|
||||
let v=inputs[k];
|
||||
if(v===true) v=5;
|
||||
v--;
|
||||
if(!v) delete inputs[k];
|
||||
else inputs[k]=v;
|
||||
});
|
||||
|
||||
// try all inputs in order and unbuffer them if valid
|
||||
if(inputs.left && trySet([-1, 0])) delete inputs.left;
|
||||
else if(inputs.right && trySet([ 1, 0])) delete inputs.right;
|
||||
else if(inputs.up && trySet([ 0,-1])) delete inputs.up;
|
||||
else if(inputs.down && trySet([ 0, 1])) delete inputs.down;
|
||||
|
||||
// buffering might be disabled
|
||||
if(!config.getB('input.buffer')) {
|
||||
Object
|
||||
.keys(inputs)
|
||||
.forEach(k => delete inputs[k]);
|
||||
}
|
||||
}
|
||||
|
||||
start() {
|
||||
this.firstStep=Date.now();
|
||||
this.tickId=0;
|
||||
this.playing=true;
|
||||
this.score=0;
|
||||
requestAnimationFrame(() => this.tick());
|
||||
}
|
||||
}
|
||||
|
||||
return module.exports=SnekGame;
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
html5doctor.com Reset Stylesheet
|
||||
v1.6.1
|
||||
Last Updated: 2010-09-17
|
||||
Author: Richard Clark - http://richclarkdesign.com
|
||||
Twitter: @rich_clark
|
||||
*/
|
||||
|
||||
html, body, div, span, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
abbr, address, cite, code,
|
||||
del, dfn, em, img, ins, kbd, q, samp,
|
||||
small, strong, sub, sup, var,
|
||||
b, i,
|
||||
dl, dt, dd, ol, ul, li,
|
||||
fieldset, form, label, legend,
|
||||
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||
article, aside, canvas, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section, summary,
|
||||
time, mark, audio, video {
|
||||
margin:0;
|
||||
padding:0;
|
||||
border:0;
|
||||
outline:0;
|
||||
font-size:100%;
|
||||
vertical-align:baseline;
|
||||
background:transparent;
|
||||
}
|
||||
|
||||
body {
|
||||
line-height:1;
|
||||
}
|
||||
|
||||
article,aside,details,figcaption,figure,
|
||||
footer,header,hgroup,menu,nav,section {
|
||||
display:block;
|
||||
}
|
||||
|
||||
nav ul {
|
||||
list-style:none;
|
||||
}
|
||||
|
||||
blockquote, q {
|
||||
quotes:none;
|
||||
}
|
||||
|
||||
blockquote:before, blockquote:after,
|
||||
q:before, q:after {
|
||||
content:'';
|
||||
content:none;
|
||||
}
|
||||
|
||||
a {
|
||||
margin:0;
|
||||
padding:0;
|
||||
font-size:100%;
|
||||
vertical-align:baseline;
|
||||
background:transparent;
|
||||
}
|
||||
|
||||
/* change colours to suit your needs */
|
||||
ins {
|
||||
background-color:#ff9;
|
||||
color:#000;
|
||||
text-decoration:none;
|
||||
}
|
||||
|
||||
/* change colours to suit your needs */
|
||||
mark {
|
||||
background-color:#ff9;
|
||||
color:#000;
|
||||
font-style:italic;
|
||||
font-weight:bold;
|
||||
}
|
||||
|
||||
del {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
abbr[title], dfn[title] {
|
||||
border-bottom:1px dotted;
|
||||
cursor:help;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse:collapse;
|
||||
border-spacing:0;
|
||||
}
|
||||
|
||||
/* change border colour to suit your needs */
|
||||
hr {
|
||||
display:block;
|
||||
height:1px;
|
||||
border:0;
|
||||
border-top:1px solid #cccccc;
|
||||
margin:1em 0;
|
||||
padding:0;
|
||||
}
|
||||
|
||||
input, select {
|
||||
vertical-align:middle;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
.help {
|
||||
display: block;
|
||||
|
||||
article {
|
||||
margin-bottom: 2ex;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-bottom: 1ex;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-left: 2ex;
|
||||
font-size: 1.6em;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
#hud {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
& > * {
|
||||
position: fixed;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.restart {
|
||||
top: 0;
|
||||
right: 0;
|
||||
cursor: pointer;
|
||||
|
||||
&::before {
|
||||
content: '⟳';
|
||||
font-size: 5vh;
|
||||
opacity: .5;
|
||||
color: @accentfg;
|
||||
}
|
||||
}
|
||||
|
||||
.crosspadOverlay {
|
||||
pointer-events: none;
|
||||
top: 50vh;
|
||||
left: 50vw;
|
||||
transform: translate(-50%, -50%);
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.joystickOverlay {
|
||||
pointer-events: none;
|
||||
transform: translate(-50%, -50%);
|
||||
opacity: .5;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
nav {
|
||||
flex: 1;
|
||||
font-size: 1.6rem;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
align-self: center;
|
||||
|
||||
section {
|
||||
flex: 1;
|
||||
max-width: 50vh;
|
||||
margin: 2rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
li {
|
||||
list-style-type: disc;
|
||||
|
||||
* {
|
||||
display: block;
|
||||
}
|
||||
|
||||
margin: .5rem;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
@keyframes popupAppear {
|
||||
0% {
|
||||
background: rgba(0, 0, 0, 0%);
|
||||
}
|
||||
100% {
|
||||
background: rgba(0, 0, 0, 90%);
|
||||
}
|
||||
}
|
||||
|
||||
.popup {
|
||||
animation: popupAppear 1s linear;
|
||||
background: rgba(0, 0, 0, 90%);
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
.content {
|
||||
border-radius: 2rem;
|
||||
background: @bg;
|
||||
text-align: center;
|
||||
box-shadow: black 0 0 1rem;
|
||||
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
box-sizing: border-box;
|
||||
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
font-size: 1.4rem;
|
||||
|
||||
&.large {
|
||||
width: 80vw;
|
||||
height: 80vh;
|
||||
}
|
||||
|
||||
& > section {
|
||||
margin: 1rem;
|
||||
|
||||
&:first-of-type {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
|
||||
& > * {
|
||||
margin: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
display: flex;
|
||||
|
||||
& > * {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
tr {
|
||||
display: flex;
|
||||
}
|
||||
th {
|
||||
text-align: right;
|
||||
&::after {
|
||||
content: ':';
|
||||
}
|
||||
}
|
||||
td {
|
||||
text-align: left;
|
||||
}
|
||||
th, td {
|
||||
padding: 0 .5rem;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
label {
|
||||
margin-right: 1ex;
|
||||
}
|
||||
|
||||
button {
|
||||
display: inline;
|
||||
color: @accentfg;
|
||||
background: @accentbg;
|
||||
font-weight: bold;
|
||||
|
||||
border-radius: 1rem;
|
||||
border: 0;
|
||||
padding: 2rem;
|
||||
margin: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
.progressBar {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
background: white;
|
||||
box-shadow: black 0 0 2rem;
|
||||
border-radius: 100vh;
|
||||
transform: translate(-50%, -50%);
|
||||
transition: top .5s ease-in-out;
|
||||
|
||||
&.hiddenBottom {
|
||||
top: 200vh;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
// full CSS reset
|
||||
@import 'fullreset.less';
|
||||
|
||||
// load the font
|
||||
@import url('https://fonts.googleapis.com/css?family=Fira+Code:400,700&display=swap');
|
||||
html {
|
||||
font-family: 'Fira Code', monospace;
|
||||
}
|
||||
|
||||
// setup REM units
|
||||
html {
|
||||
font-size: 62.5% !important;
|
||||
}
|
||||
|
||||
// setup the colors and styles
|
||||
@accentbg: #fba49b;
|
||||
@bg: #ffefdf;
|
||||
@accentfg: #930a16;
|
||||
@fg: #23090d;
|
||||
|
||||
body {
|
||||
color: @fg;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6, strong, a {
|
||||
color: @accentfg;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-bottom: .1em;
|
||||
}
|
||||
|
||||
p + p {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: inherit;
|
||||
display: contents;
|
||||
}
|
||||
|
||||
em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
code {
|
||||
background: @accentbg;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
header, footer {
|
||||
background: @accentbg;
|
||||
|
||||
img {
|
||||
border: 4px solid @accentfg;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// setup the layout
|
||||
html, body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
header img, footer img {
|
||||
float: left;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
header img {
|
||||
height: 8rem;
|
||||
}
|
||||
footer img {
|
||||
height: 4rem;
|
||||
}
|
||||
|
||||
header ul {
|
||||
display: flex;
|
||||
list-style-type: none;
|
||||
|
||||
li {
|
||||
margin-right: 1ex;
|
||||
font-size: 2rem;
|
||||
|
||||
a {
|
||||
color: @fg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header, footer, main {
|
||||
padding: 2rem;
|
||||
}
|
||||
main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
position: relative;
|
||||
background: @bg;
|
||||
|
||||
canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 4rem;
|
||||
}
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.9rem;
|
||||
}
|
||||
h4 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
h5 {
|
||||
font-size: 1.7rem;
|
||||
}
|
||||
p {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
// setup the progress bar
|
||||
@import 'progressBar';
|
||||
|
||||
// setup the main menu
|
||||
@import 'mainMenu';
|
||||
|
||||
// setup the popups
|
||||
@import 'popup';
|
||||
|
||||
// setup the hud
|
||||
@import 'hud';
|
||||
|
||||
// setup the help file
|
||||
@import 'help';
|
||||