first attempt at movement

This commit is contained in:
Daniel Bulant 2021-04-07 08:14:16 +02:00
parent 2fc5fb2c3b
commit 52f07c09bb
34 changed files with 6436 additions and 32 deletions

BIN
images/png/clouds.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
images/png/level1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 KiB

BIN
images/png/lyre.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
images/png/wind/wind1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
images/png/wind/wind10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
images/png/wind/wind11.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
images/png/wind/wind2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

BIN
images/png/wind/wind3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

BIN
images/png/wind/wind4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
images/png/wind/wind5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

BIN
images/png/wind/wind6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

BIN
images/png/wind/wind7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

BIN
images/png/wind/wind8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

BIN
images/png/wind/wind9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

5735
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -23,6 +23,7 @@
"workbox-strategies": "^6.1.1"
},
"dependencies": {
"fabric": "^4.3.1",
"howler": "^2.2.1",
"sirv-cli": "^1.0.0"
}

View file

@ -6,6 +6,7 @@ html, body {
body {
color: #333;
background: #01021B;
margin: 0;
padding: 0;
box-sizing: border-box;

BIN
public/sprite/clouds.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
public/sprite/level1.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

BIN
public/sprite/lyre.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
public/sprite/michael.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
public/sprite/uriel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
public/sprite/wind.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/sprite/wind.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View file

@ -5,8 +5,8 @@
import { characters } from "./stores/characters.js";
import { dialog } from "./stores/dialog.js";
import Game from "./pages/game.svelte";
import { gameActive } from "./stores/gameActive";
var page = "game";
var current = localStorage.getItem("dialog-page") || 0;
var preloads = new Map;
@ -29,14 +29,11 @@
autoplay: true
});
var gameActive = true;
function startPlaying(e) {
if(!music.playing()) music.play();
if(e.key === "Escape") {
gameActive = !gameActive;
}
}
console.log(dialog[current]);
console.log("Pancake recipe at https://github.com/danbulant/heaventaker");
</script>
@ -46,8 +43,10 @@
<title>Heaventaker</title>
</svelte:head>
<Game bind:current />
{#if dialog[current].map}
<Game bind:current />
{/if}
<Overlay active={gameActive}>
<Dialog bind:current page />
<Overlay active={$gameActive}>
<Dialog bind:current />
</Overlay>

15
src/game/fpsCalc.js Normal file
View file

@ -0,0 +1,15 @@
var lastCalledTime;
var last = [];
export function getFPS(time) {
if(!lastCalledTime) {
lastCalledTime = time
return 0;
}
var delta = (time - lastCalledTime)/1000;
lastCalledTime = time;
last.push(1/delta);
if(last.length >= 120) last.shift();
return last.reduce((a, b) => a + b, 0) / last.length;
}

70
src/game/images.js Normal file
View file

@ -0,0 +1,70 @@
class ImageHandler {
constructor() {
/** @type {Map<string, HTMLImageElement>} */
this.images = new Map();
/** @type {Function[]} */
this.onloadHandlers = [];
this.loaded = false;
this.shouldFire = false;
}
/**
* Runs the handler when all images are loaded
* @param {(images: Map<string, Image>) => void} handler
*/
onload(handler) {
this.onloadHandlers.push(handler);
}
areLoaded() {
return this.loaded && this.areAllLoaded();
}
startLoad() {
this.shouldFire = true;
if(this.areAllLoaded()) {
this.onloadHandlers.forEach(cb => cb(this.images));
this.onloadHandlers = [];
this.loaded = true;
this.shouldFire = false;
}
}
areAllLoaded() {
return [...this.images.values()].every(a => a.complete && a.naturalHeight !== 0);
}
update() {
if(!this.areAllLoaded() || !this.shouldFire) return; // not yet
this.onloadHandlers.forEach(cb => cb(this.images));
this.onloadHandlers = [];
this.loaded = true;
}
get(image) {
return this.images.get(image);
}
/**
* Adds the image to requested load
* @param {string} key
* @param {string | Image} image
*/
load(key, image) {
if(typeof image === "string") {
var i = new Image();
i.onload = () => this.update();
i.src = image;
image = i;
} else {
image.onload = () => this.update();
}
this.loaded = false;
this.images.set(key, image);
}
}
var images = new ImageHandler;
export { ImageHandler };
export default images;

273
src/game/index.js Normal file
View file

@ -0,0 +1,273 @@
import { fabric } from "fabric";
import images from "./images";
import { keys } from "./input";
import { maps } from "./maps";
import { Sprite } from "./sprite";
/**
* @typedef Sprite
* @property {"sprite"} type
* @property {number} spriteWidth
* @property {number} spriteHeight
* @property {number} spriteIndex
* @property {number} frameTime
*
* @property {(element: HTMLImageElement, options: any) => Sprite} constructor
*/
/** @type {HTMLCanvasElement} */
var htmlcanvas;
/** @type {fabric.StaticCanvas} */
var canvas;
export function setCanvas(htmlCanvas) {
htmlcanvas = htmlCanvas;
canvas = new fabric.StaticCanvas(htmlcanvas);
canvas.backgroundColor = "#01021B";
load();
}
/**
* @type {Map<string, fabric.Object>}
*/
const objects = new Map;
function load() {
objects.set("loadingText", new fabric.Text("Loading", {
left: canvas.getWidth() / 2,
top: 0,
fill: "white",
textAlign: "center",
originX: "center",
fontFamily: "monospace"
}))
canvas.add(objects.get("loadingText"));
images.load("level1", "/sprite/level1.webp");
images.load("lyre", "/sprite/lyre.webp");
images.load("wind", "/sprite/wind.png");
images.load("cloud", "/sprite/clouds.webp");
images.load("uriel", "/sprite/uriel.png");
images.load("michael", "/sprite/michael.png");
images.load("spawn", "/sprite/michael.png");
images.startLoad();
loading = true;
}
var map;
/** @type {{
background: string,
sprite: string,
offset: { x: number, y: nunber },
size: { x: number, y: number },
px: number,
map: string[][]
}}
*/
var mapdata;
var mapName;
export function setMap(name) {
if(mapName === name) return;
mapdata = maps[name];
mapName = name;
map = mapdata.map.map(m => m.map(piece => typeof piece === "string" && { type: piece } || piece));
console.table(map);
}
/**
* @type {{
* object: fabric.Object,
* property: string,
* value: number,
* onComplete: Function,
* start: Date,
* update: Function
* }[]}
*/
var animations = [];
/**
* Animates given property
* @param {fabric.Object} object
* @param {string} property
* @param {number} value
* @param {Function} onComplete
*/
function animate(object, property, value, onComplete) {
const length = 400;
animations.push({
object,
property,
value,
onComplete,
start: new Date,
initial: object[property],
update() {
var diff = (new Date - this.start) / length;
var toUpdate = {
originX: "center",
originY: "center"
};
if(diff > 1) {
onComplete();
animations.splice(animations.indexOf(this), 1);
toUpdate[this.property] = this.value
this.object.set(toUpdate);
return;
}
toUpdate[this.property] = (this.value - this.initial) * diff + this.initial;
this.object.set(toUpdate);
}
})
}
/**
* Moves given object with animation
* @param {fabric.Object} source
* @param {number} fromX
* @param {number} fromY
* @param {number} toX
* @param {number} toY
* @param {Function} done
*/
function move(source, fromX, fromY, toX, toY, done) {
if(fromX !== toX) animate(source, "left", toX * mapdata.px + (mapdata.px / 2), done);
if(fromY !== toY) animate(source, "top", toY * mapdata.px + (mapdata.px / 2), done);
console.log(arguments);
// map[toY][toX] = map[fromY][fromX];
// map[fromY][fromX] = undefined;
}
export function resize() {
canvas.setWidth(htmlcanvas.parentElement.clientWidth);
canvas.setHeight(htmlcanvas.parentElement.clientHeight - 7);
}
var canMove = true;
function tryMove(toX, toY) {
const player = objects.get("player");
// if(toX > mapdata.size.x - 1 || toY > mapdata.size.y - 1 || toX < 0 || toY < 0) return;
if(!canMove) return;
canMove = false;
move(player, position.x, position.y, toX, toY, () => canMove = true);
position.x = toX;
position.y = toY;
console.log(position, player.left / mapdata.px, player.top / mapdata.px);
}
keys.addEventListener("keyDown", key => {
console.log(key);
const { x, y } = position;
switch(key) {
case "right":
tryMove(x + 1, y);
break;
case "left":
tryMove(x - 1, y);
break;
case "up":
tryMove(x, y - 1);
break;
case "down":
tryMove(x, y + 1);
break;
default:
console.error("Unrecognized key", key);
}
});
var position = {
x: 0,
y: 0
}
var loading = true;
export function render(delta) {
if(images.areAllLoaded() && loading) {
loading = false;
objects.set("background", new fabric.Image(images.get(mapdata.background), {
left: canvas.getWidth() / 2,
top: canvas.getHeight() / 2,
originX: "center",
originY: "center",
}));
const field = new fabric.Group([], {
left: canvas.getWidth() / 2 - (mapdata.offset.x / 2) - (mapdata.size.x * mapdata.px / 2),
top: canvas.getHeight() / 2 - (mapdata.offset.y / 2) - (mapdata.size.y * mapdata.px / 2),
originX: "left",
originY: "top",
width: mapdata.size.x * mapdata.px,
height: mapdata.size.y * mapdata.px
});
objects.set("field", field);
for(const y in map) {
const pieces = map[y];
for(const x in pieces) {
const piece = pieces[x];
if(!piece || piece.type === "barrier") {
objects.set("object-" + x + "-" + y, null);
continue;
}
let type = piece.type;
if(type === "angel") type = mapName;
/** @type {fabric.Image || Sprite} */
let object;
if(piece.type === "angel" || piece.type === "spawn" || piece.type === "wind") {
object = new Sprite(images.get(type), {
spriteWidth: 100,
spriteHeight: 100
});
object.play();
if(piece.type === "spawn") {
objects.set("player", object);
position = { x: parseInt(x), y: parseInt(y) };
console.log(position);
}
} else {
object = new fabric.Image(images.get(type));
}
object.set({
originX: "center",
originY: "center",
left: x * mapdata.px + (mapdata.px / 2),
top: y * mapdata.px + (mapdata.px / 2),
angle: 90 * (piece.direction || 0)
});
console.log(object.left / mapdata.px, object.top / mapdata.px);
objects.set("object-" + x + "-" + y, object);
field.addWithUpdate(object);
}
}
field.set({
left: canvas.getWidth() / 2 - (mapdata.offset.x / 2),
top: canvas.getHeight() / 2 - (mapdata.offset.y / 2),
width: mapdata.size.x * mapdata.px,
height: mapdata.size.y * mapdata.px
});
canvas.add(objects.get("background"));
canvas.add(field);
canvas.remove(objects.get("loadingText"));
} else if(loading) return canvas.renderAll();
var background = objects.get("background");
background.set({
left: canvas.getWidth() / 2,
top: canvas.getHeight() / 2
});
/** @type {fabric.Group} */
var field = objects.get("field");
field.set({
left: canvas.getWidth() / 2 - (mapdata.offset.x / 2) - (mapdata.size.x * mapdata.px / 2),
top: canvas.getHeight() / 2 - (mapdata.offset.y / 2) - (mapdata.size.y * mapdata.px / 2) + mapdata.px / 2,
});
for(var animation of animations) {
animation.update();
}
canvas.renderAll();
}

190
src/game/input.js Normal file
View file

@ -0,0 +1,190 @@
const keybinds = {
"right": "ArrowRight",
"left": "ArrowLeft",
"up": "ArrowUp",
"down": "ArrowDown"
};
class KeyHandler {
constructor() {
this.keys = new Map();
this.treshold = 0.3;
this.axis = new Map([
["x", ["moveRight", "moveLeft"]],
["y", ["moveUp", "moveDown"]],
["rotation", 0]
]);
/** @type {{ type: keyof DocumentEventMap, listener: (this: Document, ev: Event) => any, options?: boolean | EventListenerOptions}} */
this.handlers = [];
this.addHandlers();
this.mounted = false;
this.mountHandlers();
/** @type {Map<string, Function[]>} */
this.listeners = new Map();
}
/**
* @param {K} type
* @param {(this: Document, ev: DocumentEventMap[K]) => any} listener
* @param {boolean | EventListenerOptions} [options]
* @template {keyof DocumentEventMap} K
*/
addDocumentEventListener(type, listener, options) {
this.handlers.push({
type,
listener,
options
});
if(this.mounted) document.addEventListener(type, listener, options);
}
/**
* Adds an event listener
* @param {string} type
* @param {(ev: keyof keybinds) => any} listener
*/
addEventListener(type, listener) {
if(!this.listeners.has(type)) this.listeners.set(type, []);
this.listeners.get(type).push(listener);
}
/**
* Emits an event
* @param {string} type
* @param {keyof keybinds} data
*/
emit(type, data) {
if(!this.listeners.has(type)) return;
this.listeners.get(type).forEach(listener => listener(data));
}
/**
* @param {K} type
* @param {(this: Document, ev: DocumentEventMap[K]) => any} listener
* @param {boolean | EventListenerOptions} [options]
* @template {keyof DocumentEventMap} K
*/
removeEventListener(type, listener, options) {
var handler = this.handlers.findIndex(e => e.type === type && e.listener === listener && e.options === options);
if(handler !== -1) this.handlers.splice(handler, 1);
document.removeEventListener(type, listener, options);
}
addHandlers() {
this.addDocumentEventListener("keydown", (ev) => {
this.pressKeyBind(ev.key);
});
this.addDocumentEventListener("keyup", (ev) => {
this.unpressKeyBind(ev.key);
});
// this.addDocumentEventListener("mousemove", (ev) => {
// var rotation = Math.atan2(ev.pageY - window.innerHeight / 2, ev.pageX - window.innerWidth / 2) * 180 / Math.PI;
// rotation += 180;
// this.setAxis("rotation", rotation);
// });
// this.addDocumentEventListener("mousedown", (ev) => {
// switch(ev.button) {
// case 0:
// if(this.pressKeyBind("mouseLeft")) ev.preventDefault();
// break;
// case 1:
// if(this.pressKeyBind("mouseMiddle")) ev.preventDefault();
// break;
// case 2:
// if(this.pressKeyBind("mouseRight")) ev.preventDefault();
// break;
// case 3:
// if(this.pressKeyBind("mouseSpecial1")) ev.preventDefault();
// break;
// case 4:
// if(this.pressKeyBind("mouseSpecial2")) ev.preventDefault();
// break;
// }
// });
// this.addDocumentEventListener("mouseup", (ev) => {
// switch(ev.button) {
// case 0:
// if(this.unpressKeyBind("mouseLeft")) ev.preventDefault();
// break;
// case 1:
// if(this.unpressKeyBind("mouseMiddle")) ev.preventDefault();
// break;
// case 2:
// if(this.unpressKeyBind("mouseRight")) ev.preventDefault();
// break;
// case 3:
// if(this.unpressKeyBind("mouseSpecial1")) ev.preventDefault();
// break;
// case 4:
// if(this.unpressKeyBind("mouseSpecial2")) ev.preventDefault();
// break;
// }
// });
}
mountHandlers() {
for(var { type, listener, options } of this.handlers) {
document.addEventListener(type, listener, options);
}
this.mounted = true;
}
unmountHandlers() {
for(var { type, listener, options } of this.handlers) {
document.removeEventListener(type, listener, options);
}
this.mounted = false;
}
pressKeyBind(key) {
var kb = this.getKeyBind(key);
if(!kb) return null;
this.emit("keyDown", kb);
return this.pressKey(kb);
}
unpressKeyBind(key) {
var kb = this.getKeyBind(key);
if(!kb) return null;
this.emit("keyUp", kb);
return this.unpressKey(kb);
}
setKeyBindAxis(key, value) {
var kb = this.getKeyBind(key);
if(!kb) return null;
return this.setAxis(kb, value);
}
getKeyBind(key) {
var index = Object.values(keybinds).indexOf(key);
if(index === -1) return null;
return Object.keys(keybinds)[index];
}
isKeyPressed(key) {
return this.keys.has(key) && this.keys.get(key) > this.treshold;
}
getAxis(key) {
if(!this.axis.has(key)) return;
var val = this.axis.get(key);
if(typeof val === "number") return val;
if(typeof val === "string") return this.keys.get(val);
var res = 0;
res += this.isKeyPressed(val[0]) ? 1 : 0;
res -= this.isKeyPressed(val[1]) ? 1 : 0;
return res;
}
pressKey(key) {
return this.keys.set(key, 1);
}
unpressKey(key) {
return this.keys.set(key, 0);
}
setAxis(key, val) {
return this.axis.set(key, val);
}
}
var keys = new KeyHandler;
export { keys };

47
src/game/maps.js Normal file
View file

@ -0,0 +1,47 @@
function wind(direction) {
return {
type: "wind",
direction
}
}
/**
* @type {
[key: string]: {
background: string,
sprite: string,
offset: { x: number, y: nunber },
size: { x: number, y: number },
px: number,
map: {
x: number,
y: number,
type: string
}[]
}
}
*/
export const maps = {
uriel: {
background: "level1",
sprite: "/sprite/uriel.gif",
offset: { // map offset for alignment
x: 90,
y: 0
},
size: { // map size (per block)
x: 5,
y: 7
},
px: 100, // block size
map: [
["barrier", "barrier", "angel" , "barrier", "barrier"],
["barrier", "barrier", null , null , "barrier"],
[null , null , wind(1) , null , null ],
[null , "lyre" , wind(1) , null , null ],
["lyre" , null , "cloud" , null , null ],
[null , null , null , "lyre" , null ],
["spawn" , null , null , null , null ]
]
}
};

95
src/game/sprite.js Normal file
View file

@ -0,0 +1,95 @@
import { fabric } from "fabric";
/**
* @typedef ISprite
* @property {"sprite"} type
* @property {number} spriteWidth
* @property {number} spriteHeight
* @property {number} spriteIndex
* @property {number} frameTime
*
* @property {(element: HTMLImageElement, options: any) => Sprite} constructor
*/
/**
* @type {ISprite}
*/
const Sprite = fabric.util.createClass(fabric.Image, {
type: 'sprite',
spriteWidth: 50,
spriteHeight: 72,
spriteIndex: 0,
frameTime: 100,
initialize: function (element, options) {
options || (options = {});
this.spriteWidth = options.spriteWidth;
this.spriteHeight = options.spriteHeight;
this.frameTime = options.frameTime || 100;
options.width = this.spriteWidth;
options.height = this.spriteHeight;
this.callSuper('initialize', element, options);
this.createTmpCanvas();
this.createSpriteImages();
},
createTmpCanvas: function () {
this.tmpCanvasEl = fabric.util.createCanvasElement();
this.tmpCanvasEl.width = this.spriteWidth || this.width;
this.tmpCanvasEl.height = this.spriteHeight || this.height;
},
createSpriteImages: function () {
this.spriteImages = [];
var steps = this._element.width / this.spriteWidth;
for (var i = 0; i < steps; i++) {
this.createSpriteImage(i);
}
},
createSpriteImage: function (i) {
var tmpCtx = this.tmpCanvasEl.getContext('2d');
tmpCtx.clearRect(0, 0, this.tmpCanvasEl.width, this.tmpCanvasEl.height);
tmpCtx.drawImage(this._element, -i * this.spriteWidth, 0);
var dataURL = this.tmpCanvasEl.toDataURL('image/png');
var tmpImg = fabric.util.createImage();
tmpImg.src = dataURL;
this.spriteImages.push(tmpImg);
},
_render: function (ctx) {
ctx.drawImage(
this.spriteImages[this.spriteIndex],
-this.width / 2,
-this.height / 2
);
},
play: function () {
var _this = this;
this.animInterval = setInterval(function () {
_this.onPlay && _this.onPlay();
_this.set({
dirty: true
});
_this.spriteIndex++;
if (_this.spriteIndex === _this.spriteImages.length) {
_this.spriteIndex = 0;
}
}, this.frameTime);
},
stop: function () {
clearInterval(this.animInterval);
}
});
export { Sprite };

View file

@ -2,6 +2,8 @@
import GameOverlay from "./gameOverlay.svelte";
import { dialog } from "../stores/dialog.js";
import { characters } from "../stores/characters.js";
import { onMount } from "svelte";
import { setCanvas, render, setMap, resize } from "../game";
export var current;
@ -28,7 +30,25 @@
}
var steps = 11;
var canvas;
onMount(() => {
setMap(dialog[current].map);
setCanvas(canvas);
resize();
function update(delta) {
render(delta);
frame = requestAnimationFrame(update);
}
var frame = requestAnimationFrame(update);
return () => cancelAnimationFrame(frame);
});
$: setMap(dialog[current].map);
</script>
<svelte:window on:resize={resize} />
<GameOverlay {steps} chapter={toRoman(characterIndex + 1)} />
<GameOverlay {steps} chapter={toRoman(characterIndex + 1)} />
<canvas bind:this={canvas} />

View file

@ -4,6 +4,7 @@ export const dialog = [{
character: "Uriel",
pose: "side_normal",
text: "Ummm... I don't want to be rude so just leave or go to the main gate.",
map: "uriel",
buttons: [{
text: "Step aside, I got heaven to conquer and angels to take.",
next: "uriel_restart"

3
src/stores/gameActive.js Normal file
View file

@ -0,0 +1,3 @@
import { writable } from "svelte/store";
export const gameActive = writable(false);