From 593d323affe20e4dc7b1f7de8d1522e577dc13db Mon Sep 17 00:00:00 2001 From: Mal Date: Sun, 17 Sep 2023 14:18:21 +0200 Subject: [PATCH] Tilorswift levels can now be loaded into the running game --- index.html | 3 +- js/Level.js | 13 +- js/UrlParam.js | 10 +- js/module.js | 433 +++++++++++++++++++-------------- js/retro/RetroArchitecture.js | 2 +- js/ui/LoadLevelDialog.js | 42 ++++ js/ui/TextBox.js | 5 +- js/ui/TextLine.js | 9 +- js/ui/TextMessage.js | 6 +- tilorswift/js/dialog/Dialog.js | 23 ++ tilorswift/style.css | 6 +- 11 files changed, 348 insertions(+), 204 deletions(-) create mode 100644 js/ui/LoadLevelDialog.js diff --git a/index.html b/index.html index 14c2407..22b0be2 100644 --- a/index.html +++ b/index.html @@ -17,9 +17,10 @@ } + - \ No newline at end of file + diff --git a/js/Level.js b/js/Level.js index 41efb82..55df447 100644 --- a/js/Level.js +++ b/js/Level.js @@ -84,4 +84,15 @@ export default class Level return level; } -} \ No newline at end of file + + static createFromJson(json) + { + const data = JSON.parse(json); + + const terrain = Terrain.createFromJson(data); + const level = new Level(terrain); + level.setGravity(data.gravity); + + return level; + } +} diff --git a/js/UrlParam.js b/js/UrlParam.js index 08236ac..6afdf0e 100644 --- a/js/UrlParam.js +++ b/js/UrlParam.js @@ -44,15 +44,15 @@ export default class UrlParam getInt(name) { - let value = parseInt(this.get(name)); + const value = parseInt(this.get(name)); - return isNaN(value) ? undefined : value; + return isNaN(value) ? 0 : value; } getFloat(name) { - let value = parseFloat(this.get(name)); + const value = parseFloat(this.get(name)); - return isNaN(value) ? undefined : value; + return isNaN(value) ? 0.0 : value; } -} \ No newline at end of file +} diff --git a/js/module.js b/js/module.js index c1942e8..b2eeef1 100644 --- a/js/module.js +++ b/js/module.js @@ -15,124 +15,267 @@ import UrlParam from "./UrlParam.js"; import UserInterface from "./ui/UserInterface.js"; import TextMessageGisela from "./ui/TextMessageGisela.js"; import TextMessageMrCroc from "./ui/TextMessageMrCroc.js"; +import {LoadLevelDialog} from "./ui/LoadLevelDialog.js"; -function MainLoop(timestamp) +class Game { - if (lastRendered === undefined && lastTimestamp === undefined) { - lastRendered = timestamp; - lastTimestamp = timestamp; - } + static GAME_SPEED = 1 - let delta = (timestamp - lastTimestamp) / (10 / GAME_SPEED); - - let ceilingHeight = Math.max( - architecture.getCeilingHeight(mrCroc.getPositionHeadLeft()), - architecture.getCeilingHeight(mrCroc.getPositionHeadRight()), - ); - - let groundHeight = Math.min( - architecture.getGroundHeight(mrCroc.getPositionFootLeft()), - architecture.getGroundHeight(mrCroc.getPositionFootRight()) - ); - - /* Handle falling */ - mrCroc.position.y += mrCroc.fallSpeed; - mrCroc.fallSpeed += GRAVITY * delta; - - /* Handle ground collision */ - if (mrCroc.position.y > groundHeight && mrCroc.fallSpeed > 0) { - mrCroc.position.y = groundHeight; - mrCroc.fallSpeed = 0; - } - - /* Handle ceiling collision */ - if (mrCroc.position.y - mrCroc.getHeight() <= ceilingHeight) { - mrCroc.fallSpeed = 0; - mrCroc.position.y = ceilingHeight + mrCroc.getHeight() + 1; - } - - /* Handle jumping */ - if (!mrCroc.isJumping && mrCroc.fallSpeed === 0 && mrCroc.position.y === groundHeight && KeyJump.isPressed()) { - mrCroc.jump(); - } else if (!KeyJump.isPressed()) { - mrCroc.isJumping = false; - } - - /* Movement left and right */ - if (!hasPlayerLeftArchitecture && KeyLeft.isPressed()) { - let lastWallLeft = Math.min( - architecture.getWallLeft(mrCroc.getPositionHeadLeft()), - architecture.getWallLeft(mrCroc.getPositionFootLeft()) + constructor(level) { + this.level = level; + this.fps = 0; + this.frameDuration = 0; + this.lastRendered = undefined; + this.lastTimestamp = undefined; + this.canvas = document.getElementById('canvas'); + this.context = this.canvas.getContext('2d'); + this.mrCroc = new MrCroc(); + this.gisela = new Gisela(); + this.architecture = RetroArchitecture.createFromData(this.level); + this.camera = new Camera(); + this.gameFinished = false; + this.hasPlayerLeftArchitecture = false; + this.textBoxGameStart = new TextMessageMrCroc('Mr. Croc: "Where is Gisela? I have to find her!"', this.context); + this.textBoxGameFinished = new TextMessageGisela( + 'Gisela: "Thanks for showing up, Mr. Croc, but I\'m not in danger."', + this.context ); + this.userInterface = new UserInterface(); + this.isPaused = false; - mrCroc.moveLeft(timestamp, delta); + this.KeyLeft = new Key('ArrowLeft'); + this.KeyRight = new Key('ArrowRight'); + this.KeyJump = new Key('Space'); + this.KeyLoad = new Key('KeyL'); - if (mrCroc.position.x <= lastWallLeft + mrCroc.getWidth() * 0.5) { - mrCroc.position.x = lastWallLeft + mrCroc.getWidth() * 0.5 + 1; + this.loader = new ImageLoader(); + this.loader.addImage(Setting.GRAPHICS_LOCATION + 'mr-croc-walk-right.png'); + this.loader.addImage(Setting.GRAPHICS_LOCATION + 'mr-croc-walk-left.png'); + this.loader.addImage(Setting.TILESET_LOCATION + GraphicSet[this.level.getTilesetId()].tileset); + this.loader.addImage(Setting.GRAPHICS_LOCATION + 'gisela-right.png'); + this.loader.addImage(Setting.GRAPHICS_LOCATION + 'gisela-left.png'); + + new FrameRateMeasurer(); + + window.addEventListener( + 'imagesloaded', + () => { + this.init(); + } + ); + } + + render(timestamp) + { + if (timestamp - this.lastRendered < this.frameDuration) { + return; } - } else if (!hasPlayerLeftArchitecture && KeyRight.isPressed()) { - let lastWallRight = Math.max( - architecture.getWallRight(mrCroc.getPositionHeadRight()), - architecture.getWallRight(mrCroc.getPositionFootRight()) + + if (this.gisela.currentAnimation !== 'LOOK_LEFT' && this.mrCroc.position.x < this.gisela.position.x) { + this.gisela.currentAnimation = 'LOOK_LEFT'; + } else if (this.gisela.currentAnimation !== 'LOOK_RIGHT' && this.mrCroc.position.x >= this.gisela.position.x) { + this.gisela.currentAnimation = 'LOOK_RIGHT'; + } + + this.context.clearRect(0, 0, window.innerWidth, window.innerHeight); + this.architecture.draw(this.context, this.camera); + this.mrCroc.draw(this.context, this.camera); + this.gisela.draw(this.context, this.camera); + this.userInterface.draw(this.context); + + this.lastRendered = timestamp; + } + + canBeFinished() + { + return ( + !this.gameFinished && + this.mrCroc.isJumping === false && + this.architecture.isMovableInsideTargetPosition(this.mrCroc) + ); + } + + handlePhysics(delta, timestamp) + { + let ceilingHeight = Math.max( + this.architecture.getCeilingHeight(this.mrCroc.getPositionHeadLeft()), + this.architecture.getCeilingHeight(this.mrCroc.getPositionHeadRight()), ); - mrCroc.moveRight(timestamp, delta); + let groundHeight = Math.min( + this.architecture.getGroundHeight(this.mrCroc.getPositionFootLeft()), + this.architecture.getGroundHeight(this.mrCroc.getPositionFootRight()) + ); - if (mrCroc.position.x >= lastWallRight - mrCroc.getWidth() * 0.5) { - mrCroc.position.x = lastWallRight - mrCroc.getWidth() * 0.5 - 1; + /* Handle falling */ + this.mrCroc.position.y += this.mrCroc.fallSpeed; + this.mrCroc.fallSpeed += this.level.gravity * delta; + + /* Handle ground collision */ + if (this.mrCroc.position.y > groundHeight && this.mrCroc.fallSpeed > 0) { + this.mrCroc.position.y = groundHeight; + this.mrCroc.fallSpeed = 0; + } + + /* Handle ceiling collision */ + if (this.mrCroc.position.y - this.mrCroc.getHeight() <= ceilingHeight) { + this.mrCroc.fallSpeed = 0; + this.mrCroc.position.y = ceilingHeight + this.mrCroc.getHeight() + 1; + } + + this.handlePlayerMovement(delta, timestamp, groundHeight); + } + + updateCamera() + { + this.camera.focusPosition( + this.mrCroc.position.x - this.mrCroc.getWidth() * 0.5, + this.mrCroc.position.y - this.mrCroc.getHeight() * 0.5, + 20 + ); + this.camera.lockCameraIntoBorders(); + } + + handlePlayerMovement(delta, timestamp, groundHeight) + { + /* Jumping */ + if (!this.mrCroc.isJumping && this.mrCroc.fallSpeed === 0 && this.mrCroc.position.y === groundHeight && this.KeyJump.isPressed()) { + this.mrCroc.jump(); + } else if (!this.KeyJump.isPressed()) { + this.mrCroc.isJumping = false; + } + + /* Movement left and right */ + if (!this.hasPlayerLeftArchitecture && this.KeyLeft.isPressed()) { + let lastWallLeft = Math.min( + this.architecture.getWallLeft(this.mrCroc.getPositionHeadLeft()), + this.architecture.getWallLeft(this.mrCroc.getPositionFootLeft()) + ); + + this.mrCroc.moveLeft(timestamp, delta); + + if (this.mrCroc.position.x <= lastWallLeft + this.mrCroc.getWidth() * 0.5) { + this.mrCroc.position.x = lastWallLeft + this.mrCroc.getWidth() * 0.5 + 1; + } + } else if (!this.hasPlayerLeftArchitecture && this.KeyRight.isPressed()) { + let lastWallRight = Math.max( + this.architecture.getWallRight(this.mrCroc.getPositionHeadRight()), + this.architecture.getWallRight(this.mrCroc.getPositionFootRight()) + ); + + this.mrCroc.moveRight(timestamp, delta); + + if (this.mrCroc.position.x >= lastWallRight - this.mrCroc.getWidth() * 0.5) { + this.mrCroc.position.x = lastWallRight - this.mrCroc.getWidth() * 0.5 - 1; + } + } + + if (!this.hasPlayerLeftArchitecture && !this.architecture.isInsideArchitecture(this.mrCroc.position)) { + this.hasPlayerLeftArchitecture = true; + + setTimeout( + () => { + this.architecture.setMovableToStartPosition(this.mrCroc); + this.hasPlayerLeftArchitecture = false; + }, 2000 + ); } } - if (!hasPlayerLeftArchitecture && !architecture.isInsideArchitecture(mrCroc.position)) { - hasPlayerLeftArchitecture = true; + finish() + { + this.gameFinished = true; + this.KeyLeft.pressed = false; + this.KeyRight.pressed = false; + this.KeyJump.pressed = false; + this.lastTimestamp = undefined; + this.lastRendered = undefined; + this.textBoxGameFinished.updateLines(window.innerWidth - 40, this.context); + this.textBoxGameFinished.animate(75); + this.userInterface.addTextBox(this.textBoxGameFinished); + } - setTimeout( + init(loopFunction) { + this.canvas.width = window.innerWidth; + this.canvas.height = window.innerHeight; + this.canvas.style.backgroundAttachment = 'fixed'; + this.canvas.style.backgroundSize = 'cover'; + this.canvas.style.backgroundPosition = 'center center'; + + if (GraphicSet[this.level.getTilesetId()].backgroundImage !== null) { + this.canvas.style.backgroundImage = 'url("' + Setting.GRAPHICS_LOCATION + GraphicSet[this.level.getTilesetId()].backgroundImage +'")'; + } + + this.canvas.style.backgroundColor = this.level.getBackgroundColor(); + + window.addEventListener( + 'resize', function () { - architecture.setMovableToStartPosition(mrCroc); - hasPlayerLeftArchitecture = false; - }, 2000 + this.canvas.width = window.innerWidth; + this.canvas.height = window.innerHeight; + } + ); + + this.textBoxGameStart.animate(75, 1000); + this.textBoxGameStart.show(1000); + this.textBoxGameStart.hide(10000); + this.userInterface.addTextBox(this.textBoxGameStart); + + this.camera.borderRight = this.architecture.columns * this.architecture.tileWidth; + this.camera.borderBottom = this.architecture.rows * this.architecture.tileHeight; + + this.architecture.setMovableToStartPosition(this.mrCroc); + this.architecture.setMovableToTargetPosition(this.gisela); + + window.addEventListener( + InterfaceEvent.FRAME_RATE_MEASURED, + (event) => { + this.fps = event.frameRate; + this.frameDuration = 1000.0 / this.fps; + window.requestAnimationFrame(loopFunction); + } ); } +} - camera.focusPosition( - mrCroc.position.x - mrCroc.getWidth() * 0.5, - mrCroc.position.y - mrCroc.getHeight() * 0.5, - 20 - ); - camera.lockCameraIntoBorders(); +function mainLoop(timestamp) +{ + if (game.isPaused) { + return; + } - /* Drawing */ - if (timestamp - lastRendered >= frameDuration) { - if (gisela.currentAnimation !== 'LOOK_LEFT' && mrCroc.position.x < gisela.position.x) { - gisela.currentAnimation = 'LOOK_LEFT'; - } else if (gisela.currentAnimation !== 'LOOK_RIGHT' && mrCroc.position.x >= gisela.position.x) { - gisela.currentAnimation = 'LOOK_RIGHT'; + if (game.lastRendered === undefined && game.lastTimestamp === undefined) { + game.lastRendered = timestamp; + game.lastTimestamp = timestamp; + } + + let delta = (timestamp - game.lastTimestamp) / (10 / Game.GAME_SPEED); + + game.handlePhysics(delta, timestamp); + game.updateCamera(); + game.render(timestamp); + game.lastTimestamp = timestamp; + + if (game.canBeFinished()) { + game.finish(); + } + + if (game.KeyLoad.isPressed()) { + const dialog = new LoadLevelDialog(); + dialog.onClose = () => { + dialog.close(); + game.isPaused = false; + window.requestAnimationFrame(mainLoop); + } + dialog.onLoad = (data) => { + game = new Game(Level.createFromJson(data)); + game.init(); } - context.clearRect(0, 0, window.innerWidth, window.innerHeight); - architecture.draw(context, camera); - mrCroc.draw(context, camera); - gisela.draw(context, camera); - userInterface.draw(context); - - lastRendered = timestamp; + game.isPaused = true; } - lastTimestamp = timestamp; - - if (!gameFinished && mrCroc.isJumping === false && architecture.isMovableInsideTargetPosition(mrCroc)) { - gameFinished = true; - KeyLeft.pressed = false; - KeyRight.pressed = false; - KeyJump.pressed = false; - lastTimestamp = undefined; - lastRendered = undefined; - textBoxGameFinished.updateLines(window.innerWidth - 40, context); - textBoxGameFinished.animate(75); - userInterface.addTextBox(textBoxGameFinished); - } - - window.requestAnimationFrame(MainLoop); + window.requestAnimationFrame(mainLoop); } const LEVEL = [ @@ -142,98 +285,12 @@ const LEVEL = [ 'terrain8.json', ]; -let urlGetter = new UrlParam(); +const urlGetter = new UrlParam(); -let levelIndex = urlGetter.getInt('level'); +const levelIndex = urlGetter.getInt('level'); +const level = Level.createFromFile( + Setting.LEVELS_LOCATION + LEVEL[levelIndex < 0 || levelIndex >= LEVEL.length ? 0 : levelIndex] +); -if (levelIndex === undefined || levelIndex < 0 || levelIndex >= LEVEL.length) { - levelIndex = 0; -} - -let level = Level.createFromFile(Setting.LEVELS_LOCATION + LEVEL[levelIndex]); - -const GAME_SPEED = 1; -const GRAVITY = level.gravity; -let fps; -let frameDuration; -let lastRendered = undefined; -let lastTimestamp = undefined; -let context; -let mrCroc, gisela, architecture; -let camera = new Camera(); -let gameFinished = false; -let hasPlayerLeftArchitecture = false; -let textBoxGameStart; -let textBoxGameFinished; -let userInterface = new UserInterface(); - -let KeyLeft = new Key('ArrowLeft'); -let KeyRight = new Key('ArrowRight'); -let KeyJump = new Key('Space'); - -let loader = new ImageLoader(); - -loader.addImage(Setting.GRAPHICS_LOCATION + 'mr-croc-walk-right.png'); -loader.addImage(Setting.GRAPHICS_LOCATION + 'mr-croc-walk-left.png'); -loader.addImage(Setting.TILESET_LOCATION + GraphicSet[level.getTilesetId()].tileset); -loader.addImage(Setting.GRAPHICS_LOCATION + 'gisela-right.png'); -loader.addImage(Setting.GRAPHICS_LOCATION + 'gisela-left.png'); - -new FrameRateMeasurer(); - -window.addEventListener( - 'imagesloaded', - () => { - let canvas = document.getElementById('canvas'); - canvas.width = window.innerWidth; - canvas.height = window.innerHeight; - canvas.style.backgroundAttachment = 'fixed'; - canvas.style.backgroundSize = 'cover'; - canvas.style.backgroundPosition = 'center center'; - - if (GraphicSet[level.getTilesetId()].backgroundImage !== null) { - canvas.style.backgroundImage = 'url("' + Setting.GRAPHICS_LOCATION + GraphicSet[level.getTilesetId()].backgroundImage +'")'; - } - - canvas.style.backgroundColor = level.getBackgroundColor(); - - window.addEventListener( - 'resize', - function () { - canvas.width = window.innerWidth; - canvas.height = window.innerHeight; - } - ); - - context = canvas.getContext('2d'); - textBoxGameFinished = new TextMessageGisela( - 'Gisela: "Thanks for showing up, Mr. Croc, but I\'m not in danger."', - context - ); - - textBoxGameStart = new TextMessageMrCroc('Mr. Croc: "Where is Gisela? I have to find her!"', context); - textBoxGameStart.animate(75, 1000); - textBoxGameStart.show(1000); - textBoxGameStart.hide(10000); - userInterface.addTextBox(textBoxGameStart); - - architecture = RetroArchitecture.createFromData(level); - camera.borderRight = architecture.columns * architecture.tileWidth; - camera.borderBottom = architecture.rows * architecture.tileHeight; - - mrCroc = new MrCroc(); - architecture.setMovableToStartPosition(mrCroc); - - gisela = new Gisela(); - architecture.setMovableToTargetPosition(gisela); - - window.addEventListener( - InterfaceEvent.FRAME_RATE_MEASURED, - (event) => { - fps = event.frameRate; - frameDuration = 1000 / fps; - window.requestAnimationFrame(MainLoop); - } - ); - } -); \ No newline at end of file +let game = new Game(level); +game.init(mainLoop); diff --git a/js/retro/RetroArchitecture.js b/js/retro/RetroArchitecture.js index bdd2be9..4a4bf5b 100644 --- a/js/retro/RetroArchitecture.js +++ b/js/retro/RetroArchitecture.js @@ -276,4 +276,4 @@ export default class RetroArchitecture return architecture; } -} \ No newline at end of file +} diff --git a/js/ui/LoadLevelDialog.js b/js/ui/LoadLevelDialog.js new file mode 100644 index 0000000..2d29601 --- /dev/null +++ b/js/ui/LoadLevelDialog.js @@ -0,0 +1,42 @@ +import Dialog from "../../tilorswift/js/dialog/Dialog.js"; + +export class LoadLevelDialog extends Dialog +{ + constructor() { + super(); + + this.setMessage('Level laden'); + this.fileInput = this.createFileInput(['json']); + + this.onClose = () => {}; + this.onLoad = () => {}; + + this.buttonLoad = this.createButton('Laden'); + this.buttonLoad.addEventListener( + 'click', + () => { + if (this.fileInput.files.length === 0) { + return; + } + + const reader = new FileReader(); + reader.addEventListener( + 'load', + (event) => { + this.onClose(); + this.onLoad(event.target.result); + } + ) + reader.readAsBinaryString(this.fileInput.files[0]); + } + ); + + this.buttonCancel = this.createButton('Abbrechen'); + this.buttonCancel.addEventListener( + 'click', + () => { + this.onClose(); + } + ); + } +} diff --git a/js/ui/TextBox.js b/js/ui/TextBox.js index f1165bd..cfb43a1 100644 --- a/js/ui/TextBox.js +++ b/js/ui/TextBox.js @@ -4,11 +4,12 @@ import UserInterfaceElement from "./UserInterfaceElement.js"; export default class TextBox extends UserInterfaceElement { - constructor(text, width, context) + constructor(text, width, context, paused = false) { super(); this.text = text; this.width = width; + this.paused = paused; this.colorText = 'red'; this.colorShadow = 'black'; this.colorBorder = 'black'; @@ -115,4 +116,4 @@ export default class TextBox extends UserInterfaceElement return line; } -} \ No newline at end of file +} diff --git a/js/ui/TextLine.js b/js/ui/TextLine.js index e261947..aaf35a7 100644 --- a/js/ui/TextLine.js +++ b/js/ui/TextLine.js @@ -2,9 +2,10 @@ import TextAlignment from "./TextAlignment.js"; export default class TextLine { - constructor(text) + constructor(text, paused) { this.text = text; + this.paused = paused; this.estimatedTextWidth = null; this.colorText = 'red'; this.colorShadow = 'black'; @@ -24,6 +25,10 @@ export default class TextLine let process = setInterval( () => { + if (this.paused) { + return; + } + this.chars++; if (this.chars === this.text.length) { @@ -83,4 +88,4 @@ export default class TextLine context.fillText(this.text.substr(0, this.chars), x + 2, y + this.size + 2); } } -} \ No newline at end of file +} diff --git a/js/ui/TextMessage.js b/js/ui/TextMessage.js index 89092e7..e7dff63 100644 --- a/js/ui/TextMessage.js +++ b/js/ui/TextMessage.js @@ -3,8 +3,8 @@ import GeometryPoint from "../geometry/GeometryPoint.js"; export default class TextMessage extends TextBox { - constructor(text, context) { - super(text, window.innerWidth - 40, context); + constructor(text, context, paused = false) { + super(text, window.innerWidth - 40, context, paused); this.update(); this.context = context; } @@ -21,4 +21,4 @@ export default class TextMessage extends TextBox this.setPosition(this.defaultPosition.x, this.defaultPosition.y); this.updateLines(this.defaultWidth, this.context); } -} \ No newline at end of file +} diff --git a/tilorswift/js/dialog/Dialog.js b/tilorswift/js/dialog/Dialog.js index 5c0d581..143849f 100644 --- a/tilorswift/js/dialog/Dialog.js +++ b/tilorswift/js/dialog/Dialog.js @@ -126,8 +126,31 @@ export default class Dialog return htmlElement; } + createFileInput(types = []) + { + let input = document.createElement('input'); + input.type = 'file'; + + if (types.length > 0) { + for (const t in types) { + types[t] = '.' + types[t] + } + + input.accept = types.join(', '); + } + + this.inputAreaElement.appendChild(input); + + return input; + } + setMessage(message) { this.messageElement.innerText = message; } + + close() + { + this.htmlElement.remove(); + } } diff --git a/tilorswift/style.css b/tilorswift/style.css index 7408010..1f436d4 100644 --- a/tilorswift/style.css +++ b/tilorswift/style.css @@ -236,6 +236,10 @@ body { margin-bottom: 20px; } +input[type="file"] { + margin-bottom: 20px; +} + .dialog-button { padding: 5px 20px; background-color: grey; @@ -296,4 +300,4 @@ body { tr:hover > td > .selection { opacity: 0.5; } -*/ \ No newline at end of file +*/