Story.
loadGameState.
(any)
isRecurrentEvent.
(any)
(any)
createSelector.
(any)
newId.
(any)
0.9.8
The official HSU game. Play it in a modern browser!
The game is played by using the arrow keys or WASD to move your character around the map. You can zoom in or out by pressing "z"
There are icons that are part of the Heads up Display (HUD) Right underneath them, there is a clock and in-game timer.
You can bring up your current inventory by clicking on the backpack.
You can interact with certain NPCs by pressing the "C" button. Dialogs offer multiple choices of sub-questions and answers.
There is also looping background music playing.
See our roadmap for details on what features are coming up next.
Download and install nodejs
Download the repository (git clone)
git clone https://github.com/micahh2/hsu
Change your current directory
cd hsu
Install dependencies
npm i
Start local http server
npm start
The dev server opens a browser new tab when it starts, pointing to: http://localhost:8080
git status
to see the status of your local instance. If you are on a branch besides master, run git checkout master
to switch to the master branch. If you have changes you don't want anymore try staging them with git add
and using git stash
to hide them for the moment.git pull
to get changes. Pay attention to the output, if there are merge conflicts they will show up here.npm start
npm test
git checkout -b
, or git branch
and git checkout
. Try to give your branch a short but descriptive name.npm run fix-lint
, if there are errors fix themgit add
to stage files as ready for commitgit commit
to create a commit with a useful message describing your changesgit push
your new branch and use github's UI to create a pull request, don't forget to assign someone to merge the pull request. To start the test runner:
npm test
To generate a test report (test-report.md)
npm run test-report
View the test status' here
View the test code coverage here
Documentation can be found on the demo website here.
Building documentation
npm run build-docs
Starting local server for documentation
npm run serve-docs
The project uses a lightly modified version of Airbnb's eslint style guide. To run the linter and show errors:
npm run lint
To run the linter and auto-fix simple mistakes:
npm run fix-lint
While you do not need to do this before making a commit, it is encouraged. There are also a number of editor plugins that can show you linter errors and make it easier to write compliant code.
This software is licensed under AGPL
The whole application starts in index.html. It is a relatively small html file that sets the overall structure of the game and names the other initial files to bring in.
Below is a simplified version of the index.html
<html>
<head>
<title>Hochschule Ulm</title>
<link href="main.css" rel="stylesheet" />
</head>
<body>
<div id="images">
<img id="character-sprite" src="assets/charBatch1_sprite.png">
</div>
<div id="stage">
<canvas id="objects-layer"></canvas>
<canvas id="layout-layer"></canvas>
</div>
</body>
<script type="module" src="main.js"></script>
</html>
Index.html does three main things:
After this file is given to the browser, main.js takes over running the show.
main.js is a ES6 javascript module. This means that it can import other scripts using the import
keyword, and that it runs in what is known as javascript's strict mode.
main.js has two main parts, an event listener that is called when the page is loaded, and auxiliary functions that support that event listener.
"Load" Event Listener
When index.html hands of execution of the game to main.js, much of the page is still being loaded. The first thing that main.js does is register an event listener using window.addEventListener()
. This event listener waits for everything in index.html to be loaded, and then calls the given anonymous function.
window.addEventListener('load', async () => {
// Do something when the page loads
});
window
is a global variable available to most javascript in the browser. We avoid references to global variables outside of main.js because they don't work in NodeJS (where we execute our unit tests), and because it is important to have an organizational method of how and where the browser can be interacted with.
In essence, the 'load' event listener main.js has three responsibilities:
1. Ensure all resources are finished loading
Since some resources depend on other resources, they have to be loaded dynamically and might not be loaded by the time the load
event is triggered. Here we make use of a newer javascript feature called async/await
. There are a number of ways to execute multiple functions simultaneously in javascript (Note: this does NOT mean it's multi-threaded), Promises are simple way to have a result from something asynchronous that you can wait get later.
2. Parse and configure resources (images, json) and output (canvas)
Not everything structural can be defined in index.html or main.css, some information about the canvases need to be determined based on, for example, the size of images after they loaded. This step also involves some basic parsing of the sprites, scaling them to the sizes we will use.
3. Start the game loops
We have two game loops, one for the Story and one for the Physics. Both are not "proper" while
or for
loops but instead functions that are called repetitively, or messages that are passed back-and-forth in a cycle.
The physics loop is recursive, calling it self using window.requestAnimationFrame
. It has two current jobs:
The story loop runs in its own thread. It only has one job: evaluate the game logic and create updates. The story loop is started by sending an 'update-game-state' message to the storyWorker. The story worker uses the updated game state to create changes, and passes those changes back to an event handler in main.js. The main.js event-handler applies those changes to the current state (using Story.applyChanges) and sends the newly updated state back to the story worker with 'update-game-state'. This goes on in a cycle until the game ends.
Auxiliary Event Listeners and Functions
There are a few auxiliary event listeners, and functions that are used to support them. Including two very important event listeners, which handle keyboard events:
There are also some functions for updating on-screen statistics and parts of the general UI.
physicsLoop. Updates the physics state. Called once, then calls itself recursively
// Externally - once!
physicsLoop();
// Internally
window.requestAnimationFrame(physicsLoop);
Take all the input requests give an updated player
Actor
:
an updated actor
movePlayer({ player, width, height, up, down, left, right });
clearStats. This clears all of the statistics/metrics that we've been keeping track o.
clearStats();
updateStats. Used to keep track of statistics/metrics (fps, collision checking time, ect..), at somepoint it will be rewritten to be more flexible
"frames"
| "mapMakingTime"
| "collisionCalls"
| "collisionChecks"
), value: number)(("frames"
| "mapMakingTime"
| "collisionCalls"
| "collisionChecks"
))
(number)
updateDiagnostDisp. Used to update the metrics on screen, at somepoint it will be rewritten to be more flexible
canvasProvider. wraps document.createElement, allowing for easy replacement when testing
const canvas = canvasProvider();
A module for dealing with perspective and rendering
getContextPixels. This returns an 2D array with the values of the alpha layer of a context. We use data calculated in this function to in the physics engine to do collision detection between actors and the terrain.
(Object)
Name | Description |
---|---|
arguments.context CanvasRenderingContext2D
|
context to read the alpha pixels from |
arguments.canvasWidth number
|
absolute width of the canvas to process |
arguments.canvasHeight number
|
absolute height of the canvas to process |
Array<Uint8Array>
:
updateViewport. Calculates what the new viewport should be.
(Object)
Name | Description |
---|---|
arguments.oldViewport Viewport
|
old viewport which will be returned if there is no difference between the new and the old viewport |
arguments.player Actor
|
the player to center the viewport on |
arguments.mapWidth number
|
absolute width of the map |
arguments.mapHeight number
|
absolute height of the map |
arguments.scale number
|
an integer, 1 => no scaling |
arguments.canvasWidth number
|
absolute width of the canvas/screen |
arguments.canvasHeight number
|
absolute height of the canvas/screen |
Viewport
:
drawScene. Draws an entire scene. This function should be called once per frame to redraw the three layers.
(Object)
Name | Description |
---|---|
arguments.player Actor
|
, |
arguments.characters Array<Actor>
|
, |
arguments.items Array<Actor>
|
, |
arguments.oldItems Array<Actor>
|
, |
arguments.context CanvasRenderingContext2D
|
, |
arguments.width number
|
, |
arguments.height number
|
, |
arguments.sprites number
|
, |
arguments.viewport Viewport
|
, |
arguments.oldViewport Viewport
|
, |
arguments.layoutContext CanvasRenderingContext2D
|
, |
arguments.aboveContext CanvasRenderingContext2D
|
, |
arguments.drawActorToContext function
|
, |
any
:
undefined
drawGraph. Draws out a graph of areas and their connection points. This function is used for debugging path-finding and player movement.
(Object)
Name | Description |
---|---|
args.graph Array<Object>
|
the graph |
args.context CanvasRenderingContext2D
|
the context to draw to |
args.viewport Object
|
the vewport to draw in |
undefined
:
drawDestinations. Draws all of the characters paths and This function is used for debugging path-finding and player movement.
(Object)
Name | Description |
---|---|
args.characters Array<Actor>
|
|
args.context CanvasRenderingContext2D
|
|
args.viewport Viewport
|
undefined
:
Characters. A module containing methods for dealing with characters.
const newActor = Characters.moveNPC({ npc: characters[i], width, height });
Map Module. Handles the loading and information gathering for tilemap. More information about the format imported can be found here: https://doc.mapeditor.org/en/stable/reference/json-map-format/
const mapDim = Map.getTileMapDim(tilemap);
convertBlob. Converts a blob to an Image. We need a loaded image to be able to draw it to a sprite. When loading images dynamically, we only get them as Blobs, this converts them. (unfortunately not testable in NodeJS) https://developer.mozilla.org/en-US/docs/Web/API/Blob
(Blob)
Promise<Image>
:
const blob = await response.blob();
await convertBlob(blob);
loads images
(Object)
Name | Description |
---|---|
args.fetch function?
(default window.fetch )
|
to request files from an url, defaults to (window.fetch) |
args.mapJson function
|
map information |
args.convertBlob function?
(default Map.convertBlob )
|
to convert a blob to image |
args.prepend string?
(default '' )
|
string to prepend to image urls (use this to set local folder) |
Array<Promise>
:
Tilesets with image data
const loadedTilesets = await Promise.all(Map.loadImages({ mapJson: tilemap }));
getSpriteable. Convert a loaded map layer into an object that can be converted into a sprite
(LoadedTileSet)
loaded tileset, tileset with the image attached
Spriteable
:
cosnt spriteable = Map.getSpriteable(loadedTileset, [1, 2]);
const sprite = Sprite.loadSprite(spriteable, canvasProvider);
loadTileMapSprites.
Object
:
an object with the keys set as the first guid of the respective tileset,
and the value set to be the coresponding sprite
const tileSprites = Map.loadTileMapSprites({
loadedTilesets,
canvasProvider,
zoomLevels: [1, 2]
});
getTileMapDim.
(TileMap)
getTransformFromFlip.
(Object)
Name | Description |
---|---|
args.horizontally boolean
|
Should flip horizontally |
args.vertically boolean
|
Should flip vertically |
args.diagonally boolean
|
Should flip diagonally |
args.centerx number
|
centerx to translate to (must be moved back after) |
args.centery number
|
centery to translate to (must be moved back after) |
Object
:
translation with the keys a,b,c,d,e,f
Physics. Methods for dealing with physics
This function provides a 1 tick update to the physics state
(Object)
physics state
Name | Description |
---|---|
state.pixels Array<Uint8Array>
|
2d Array with wall data |
state.moveNPC function
|
Function to get next NPC move |
state.movePlayer function
|
Function to move Player |
state.locMap Object
|
Location Look up Map (Empty on first call, modified) |
state.characters Array<Character>
|
Non-playable characters |
state.player Player
|
Current player |
state.updateState function
|
Function to provide timing statistics |
state.getGameState function
|
Function to get game state |
state.gameState any
|
|
state.flags any
|
|
state.updateStats any
|
|
state.width any
|
|
state.height any
|
Object
:
changes
Sprite.
Story.
loadGameState.
(any)
isRecurrentEvent.
(any)
(any)
createSelector.
(any)
newId.
(any)
Util.
Module for dealing with music and sound
use this to play a sound/music track_id must be in the form '#track_id' track ids can be found in index.html the "loop" parameter can be set to true if you want the sound to keep looping
use the volume parameter to change the volume, from 0.001 to 1.0!
if you want to add an audiofile, add it to assets/music and make a line for it in index.html like the following:
(any)
(any)
this function can be used to stop any sound or music track
(any)
PathFinding. Module for path finding function
inArea.
(any)
(any)
moveCost. A sperate cost calculation for each move.
(any)
(any)
updateViewport ✓ should provide a full view ✓ should provide a zoomed view
moveNPC ✓ should always move an npc as an integer
getSpritable ✓ should take loaded tile data and return something we can make into a sprite
loadImages ✓ should fetch images
dijikstras ✓ should generate no path ✓ should find a valid path ✓ should generate an optimal path ✓ should generate a diagonal path ✓ should go straight over a wall, then diagonal to the right ✓ should go diagonal over the wall ✓ should first go up-right (diagonal), then down a corridor
areNeighbors ✓ should return true for two areas are neighbors ✓ should return false for two areas which are not neighbors ✓ should return true for a neighbor below it ✓ should return true for an offset neighbor to the side
gridToGraph ✓ should receive a grid and return a graph ✓ should receive a blocked grid and return an empty graph ✓ should receive a mixed grid and return a graph with neighbors ✓ should should return the right neighbors for a semi-complex graph ✓ grid to graph using dijikstras
hasBlock ✓ should receive a blocked grid and return true ✓ should receive a partially blocked grid and return true
getGatewayPoint ✓ should pick the mid-point between two neighboring areas (x dir) ✓ should pick the mid-point between two neighboring areas (y dir) ✓ should pick the mid-point between two neighboring areas with a partial overlap
splitGraphIntoPoints ✓ should return a new graph with points ✓ should return a new graph with points when areas overlap
getUseableMove ✓ should move a player left if left is requested ✓ should let a player move down if down-right is requested, and right is not allowed ✓ should let a player move left if down-left is requested, and down is not allowed
updateLocationMap ✓ should remove an old player even if it has been modified
loadSpriteData ✓ should set the internal canvas size according to different scales ✓ should load sprite data into parts ✓ should load sprite data with different scales
applyChanges ✓ should apply conversation changes ✓ should apply item changes ✓ should update character waypoints/destinations
newId ✓ should return the highest id+1
setDestination ✓ should return updated destinations
createSelector ✓ should create a selector
startConversation ✓ should return the dialog of a character ✓ should select the nearest npc if no selector ✓ should do nothing if no selector and no near npc
isRecurrentEvent ✓ should be recurrent when it is of type interval
isTime ✓ should be time when it's happening exactly now ✓ should be time when it's happened in the past ✓ should not be time when it has not happened yet
isWithinInterval ✓ should be within the interval when it's happening exactly now ✓ should be within the interval when it's happening exactly now + start ✓ should be within the interval when it's happening now-ish (<threshold) ✓ should be within the interval when it's happening now-ish for the third time ✓ should not be within the interval when triggered more than the threshold ago ✓ should not be within the interval when now is later than end
isWithinDistance ✓ should be within distance when near ✓ should not be within distance when far
isTriggered ✓ should should trigger on time ✓ should trigger on interval ✓ should trigger when in area ✓ should trigger on distance ✓ should trigger on any conversation ✓ should trigger on conversation with specific npc ✓ should trigger on having an item
updateGameState ✓ should update conversation on distance ✓ should set destination on time
loadGameState ✓ shouldn't transform choordinates from relative to abs (formerly we did)
isWithinArea ✓ should be when actor is in area ✓ should not be when actor is above the area ✓ should not be when actor is beyond the area
69 passing (59ms)
This roadmap document shows an overview of our project priorities. It is non-binding and is only useful to have a simple overview of where we are, and where we might be going.
This document is feature focused, so many tasks that support the objectives below, won't be shown. For a more detailed look at progression through these milestones, please refer to Trello and the Sprint planning documents. The roadmap should evolve over time as it is primarily an overview, and not a planning document.
Hello world
M.V.P. (Minimum viable product)
NPCs
Player
Story Engine
UI
Expanded Features I
NPCs
Player
Story Engine
Map
Multi-player
Expanded Features II
Player
Story Engine
UI
Map
Physics
Advanced Features I
Multi-player
Advanced Features II
UI
Multi-player
The general feeling is that we did pretty well completing our goals for this iteration. There was some confusion during this iteration about how the Rational Unified Process works and how to get started on the project, but that was overcome by working together and strong communication.
This iteration was a struggle because many of us had term papers and presentations due. Still, despite having somewhat reduced focus, the general perspective is that we did pretty good hitting our goals, although we are looking to improve next sprint.
renderConversation.
(Object)
Name | Description |
---|---|
conversation.character Character
|
|
conversation.currentDialog currentDialog
|
(any)
renderConversation(gameState.conversation);