phaser-game

npm version

phaser icon

This is the repo for the game:

The Supernatural Power Troll

Produced for Gamedev.js Jam 2024

Rate here

Project’s layout

.
├── asprite: Asesprite files used to produce art assets like spritesheets.
├── beepbox: Links to all beepbox songs.
├── bundler: Use by bun.js to bundle the code code in "src" and make it available.
├── dist: Distributed files.
├── example: Where the game is packaged.
│   ├── assets: All assets (art, music, sound) used by the game.
│   ├── dist: Packaged JS files (bundled from typescript)
│   ├── json: All levels in JSON format.
│   ├── node_modules: dependencies
│   └── src: Just to pass source from outer "src" folder.
├── node_modules: dependencies
├── ...
└── src
    ├── config: Just the url to the API for fetching NPC dialog
    ├── example: Sample game started from tutorial. Not used.
    └── hischool: The entire game (initially I was planning a High School setting).

The repo follows the bun-template github template for TypeScript / React project, in which a package is built within the “src” directory, and the “example” directory uses the package as a library to show a demo of that package.

In this case, the core logic is in the “src” directory, while the rest of the game including the assets are all in the example directory.

Start the project

First install bun

This repo us built using bun.js. Install from here.

curl -fsSL https://bun.sh/install | bash

Then run:

bun i

Execute the game

Execute:

./sample.sh

This builds the necessary file and start the server hosting the game, then starts the server from the “example” folder.

Then open the page in a browser “http://localhost:3000”.

GameJam

The theme of the gamejam is: POWER.

The game is a platform puzzle type where you control a troll that can grant supernatural powers to humans, by picking up and tossing powerups or placing them in their path.

The troll has to pick up a key and reach a door. He’s not allowed to touch any human.

The game has various features detailed below. Furtherdowm, I will explain the implementation.

Features

Custom NPC look

Each NPC is composed of various body parts that are randomized at the start of each level. As such, every NPC look different. They can be male or female, wear hat or not, have different hairstyles, wear skirt, pants, or just underwear. They also have different skin colors.

Powerups

Each powerup is acquired by a human one at a time. Acquiring a new powerup cancels the effect of a previous one.

Here are the different powerups available:

AI Generated NPC dialog

Every few seconds, a random NPC makes a comment. It is most likely related to a recent event: Just saw the troll come and disappear, did a high jump, pushed a heavy rock, shrunk down, fell into the water, etc…

To generate the dialog, the history of events from an NPC is accumulated then passed on to OpenAI API to inform what happened to that NPC, and an appropriate response from the NPC is returned. The description of the environment is also pased, if it’s noteworthy. (like the “pizza” level).

Quirky NPC behavior

Some surprising events cause the NPC to jump and pause for a moment. (saw a troll, just pushed a heavy rock). The NPC also stops a walk for a bit before talking.

Spoken voice synthesization

The NPC response is passed to the browser’s voice synthesizer, which will speak the response along with showing the text.

The NPC voice is chosen at random at the beginning of each level. It doesn’t always match the NPC look. Given that each voice has an association with a language, that information is passed to OpenAI, and the NPC ends up using words from their native language along with their response. (For ex: French NPCs love to say “Sacrebleu”, and Mexican NPCs often say “Aye Caramba”.).

Level editor

Each level is composed of several unique tiles, ranging from:

The level editor enables level editing while the game is running, to allow immediate adjustments.

Pressing SHIFT + Clicking duplicates an element, allowing the editor to place more elements.

The editor is limited, so some actions can only be done by modifying the JSON file:

Some level configurations are also set manually in the json files: Decor overlay, if the level has pizza, gold walls, snail or cat, if it’s locked (editor OFF), and what’s the next level after completing it.

Implementation details

Custom NPC look

NPCs are composed of two categories of elements:

Those elements have no animation, so each one is unique. One of each is chosen as random to compose the face. Each of those elements is overlayed on top of each other to produce a unique face when combined. There’s also a random tint applied to the “shape” to change skin color.

When the NPC needs to talk, we alternate between the NPC’s mouth, and the graphics showing the mouth opened. If the NPC’s default mouth is opened, we alternate with a random mouth.

Each body part is animated in sync, when the character is walking or standing still.

The function that sets up the look is function randomSprite(). We pass a seed, stored on each NPC, which will ensure the same seed passed will result to the same character customization. This is needed because we apply random tint when a character acquires a power-up, then we need to restore to normal once its done.

Powerups

When a NPC collides with a power up, that powerup is attached to the NPC (shown over the head). The NPC’s various behavior is then modified depending on the powerup. This is done through custom code applied throughout.

AI generated dialog

As the game progresses, the NPC accumulates a history of events. Each event is added with the addHistory() function. When it is time for an NPC to talk, a random one is chosen, then the history is passed to a rest API which URL is defined in OPEN_AI_URL.

The implementation of that API is at https://github.com/jacklehamster/open-ai-npc.

  const url = `${OPEN_AI_URL}?dictionary=${JSON.stringify(dico)}&situation=${HumanEvent.LANG}.${situation}&seed=${seed ?? ""}&jsonp=fetchAIResponse`;

The idea is that situation can be constantly different, but it will eventually be just a series of numbers. Meanwhile, DICO will always remain the same. That means we could potentially take the MD5 of DICO and pass it to the server, and thus the server can whiltelist that MD5. This could prevent attackers from calling directly the server with random junk, thus dumping garbage to the OpenAI API (which might not be a really big deal).

The implementation could be changed to just pass the strings direcetly into situation, I don’t think the DICO implementation is really necessary.

Once the rest API responds a speech for the NPC, that speech is passed into the Speech Synthesizer which will make the computer “speak” the sentence. We also use utterance “boundary” event to make the words written on the screen match the timing of the speech.

Run the OpenAI api locally

Fetch the source from: https://github.com/jacklehamster/open-ai-npc

Then start the server using the ./start.sh command, which will run the server locally on http://localhost:3001

Then change the OpenAI URL from https://open-ai-npc.onrender.com/comment/ to http://localhost:3001/comment/.

There’s two locations to change OPEN_AI_URL. One in constants.ts and one in example/index.ts.

When fetchAI is called, the “jsonp” parameter can be passed as true or false. When passed as true, the API call is made using jsonp (use a <script> tag which will call the function fetchAIResponse.). If the server is local, that’s not necessary, so the API call can just be made through the fetch() function.

Level Editor

When the game is hosted locally on a server, we enable the level editor.

The local server contains API for saving elements from the game diretly into the mapN.json JSON that was used for configuring the level.

Every time an element is moved, the commit() function is called, passing the ID of the element moved. If a new element is created, a new ID for it is generated, so the server will know to create a new element in the mapN.json file.

During the game, there are some UI components that can be enabled in edit mode, allowing an editor to drag elements to move or resize them. Those are just implemented as part of the game, and enabled only in edit mode. Thus, the game can be edited when it is played. An editor can try a level, find out that a particular jump is impossible, then just resize, place the troll back to its original position and try again. This allows for some very fast iteration.

Game art

The game has unique art for each level. While it’s time consuming, it does make some art development a bit easier.

The way each level is designed, is that I first remove the “overlay” from a map, then place the elements around to make the level solvable. Afterwards, once the platforms are clearly defined, I dump a screenshot of it into “Aseprite”, then draw some art on top of the platform. Then I save that as a PNG. I then set that as the overlay of the level, so every piece of art will just cover the platforms in the game.

{
	"overlay": "assets/overlay1.png",
  ...
}

Music and sound

Each level song is an mp3 played by phaser, depending on the level:

    preload() {
      this.load.audio('main', ['assets/troll-song.mp3']);
      ...
    }

    create()
    {
      this.music = this.sound.add(parseInt(level) % 2 === 1 ? 'main' : 'main2');
      this.music.loop = true;
      this.music.play();
      ...
    }

For sound effect, the zzfx is called directly from the code provided from the web tool.

  zzfx(...[1.52, , 1177, .23, .09, .09, 1, 1.41, 12, , , , , .4, , , .06, .25, .11, .11]); // Random 348

Cut scene

Cut scenes are implemented outside of Phaser, for simplicity. The code is in example/index.html.

The cutscene is defined within the structure in variable CUT_SCENES. Each element is a string with:

[
  imageUrl,
  textToDisplay,
  urlOfMusic,
  loopTheMusic, //  true or false
  fadeToTheImage,   //  true or false. True causes the previous image to fade out, the current one to fade in.
]

Commit process for Github

Repos generated using the bun-template all use the same method to publish.

Using the following command, the repo gets commited, pushed to Github, then published to “npmjs”.

./auto-publish.sh

The script generates its own commit message, so to get a meaningful message, it’s best to first commit manually.

While publishing to npmjs might not make sense for a standalone game, this is useful when producing reusable packages that can be imported in various javascript projects.

Package for game portals (itch.io, Newgrounds)

Game portals require a single zip file which contain the entire website of the game, starting with the “index.html” file. That’s the “example” folder.

Server code is ignored by game portals, so they don’t need to be removed from it. The “node_modules” must be deleted before zipping the “example” folder because they contain a large amount of folders, that are also not needed by the website.

Run example from Github website

https://jacklehamster.github.io/phaser-game/example/

Run game on Itch.io

https://jacklehamster.itch.io/power-troll

Github Source

https://github.com/jacklehamster/phaser-game/

NPM Package

https://www.npmjs.com/package/@dobuki/phaser-game