Thank you for considering contributing to the myst2 engine. I welcome all contributions with an open mind, but to ensure long-term stability and quality of this project, I need to ask you to please familiarize yourself with the rules outlined in this document before attempting to contribute. This is particularly important for code contributions, which must adhere to the strict code style guide. You are also expected to document your code after the PR has been merged. Above all, the contribution needs to be of highest possible quality.
Before you start, please also see the Code of Conduct.
- How to Contribute
1.1. Reporting Bugs
1.2. Suggesting Features
1.3. Submitting Code - Code Style Guide
2.1. Write optimal and performant code
2.2. Keep the code simple and try not to repeat yourself too much
2.3. Use tabs and only tabs to indent code
2.4. Use semicolons
2.5. Stick to 80 columns as best you can
2.6. Make variables const by default
2.7. All-uppercase any hardcoded constants
2.8. Use comments
2.9. Add spaces when listing function parameters or items in an array
2.10. Expand, and expand a lot
2.11. Break long ternary expressions into multiple lines
2.12. When chaining functions, break the calls into lines
2.13. Use empty lines to separate code into logical units
2.14. Use the following if statement style
2.15. Use the following switch statement style
2.16. Use the following class style
2.17. Don't overuse functional styles and avoid exposing functional API
2.18. Don't use getters and setters (unless you have a very good reason)
2.19. Use JSDoc basically everywhere
2.20. When adding a new file, follow these steps
2.21. No AI - Why don't you use [my favorite autoformatter]?
- Updating Documentation
- Finding Help
- Check the issue tracker to ensure the bug hasn't already been reported.
- Open a new issue with a clear title and detailed description, including:
- Steps to reproduce the bug
- Expected behavior
- Actual behavior
- Screenshots or logs (if applicable)
- Your environment (OS, browser version)
- Open an issue in the issue tracker with the label
feature-request. - Describe the feature, why it's valuable, and any potential implementation ideas.
- Be open to having your feature turned down or delayed, perhaps indefinitely (unless you're willing to implement it yourself). I only have so much time and I generally implement features that I personally need. Occasionally I may make an exception if a feature is something relevant to my needs or the greater good.
To contribute code, use the pull request (PR) workflow as described below.
- Fork the repository to your GitHub account.
- Clone your fork locally:
git clone https://github.com/your-username/myst2.git- Create a new branch for your changes:
git checkout -b feature/your-feature-name-
Follow the project's coding style and conventions (see style guide).
-
Write clear, concise commit messages.
-
Add playground snippets or examples as needed. This is a good way to be able to test the code easily and see it in action.
Run:
npm run lintAnd make sure it passes with no issues. PRs with a failed lint test will be discarded.
- Push your branch to your fork:
git push origin feature/your-feature-name-
Open a pull request against the main repository's
stagingbranch. -
In the PR description, include:
- A summary of changes.
- Reference to related issues (e.g.
Fixes #123) - Any additional contexts, screenshots or relevant information.
All code is inspected and approved by me so it may take some time before I get to it.
- If your PR has been accepted, update the documentation as necessary to document your new features.
Please adhere, as strictly as physics allows at any given moment, to the following style guide when adding code to this codebase.
Write code that performs optimally. Always consider performance.
Avoid obvious things like string comparisons, duplicating memory, adding unnecessary objects to memory, having poor locality in your memory and similar. Avoid needless complexity. Use optimal algorithms and data structures. Prefer simple for loops to functional nesting, especially when dealing with realtime functions. A 6-deep for loop has significantly less overhead for the JS engine than a 6-deep functional tower.
Opposite to what you normally hear, when working on this codebase I want you to optimize early and plan ahead. Above all, this is a game engine and as such, performance is a first-class citizen and should be on your mind regardless of what you're doing. Don't do something foolish like executing expensive code 60 frames per second when better options exist. Off-load render surfaces when possible. In the myst.ui extension for example, all labels are prerendered onto surfaces because rendering text is expensive but rendering surfaces is cheap, and you only have to render text once and not 60 times per second. Use common sense and be vigilant about performance at all times.
This is a strict requirement and rule #1 for a reason.
This is a given but is stated here explicitly for added emphasis.
Simple, straightforward code is maintainable code and it also tends to be fast code.
You can use spaces to align items on occasion, but always indent code with tabs.
Some codebases go fancy with the no-semicolon style, but this is not one of them. Use semicolons as intended.
This is not strictly enforced, but highly recommended. Try to code in a way where if you shrink the editor view down to 80 columns (with 4-wide tabs), you can still read most of the code. Especially try to limit comments to the 80-column boundary. An occasional line of code may go beyond, but try to limit those instances.
Everything should be const by default, except the things you explicitly don't want to be const. Always default to const before using let if you don't already know that the variable you're going to need will change down the line. Never use var.
Use an all-uppercase style on hardcoded (non-computed) constants, for example:
const SOME_STRING = "turtles";
const SOME_VALUE = 13.7;Or, when used as an enum:
const RENDER_MODE = {
FILL: 0,
STROKE: 1,
BOTH: 2
};This goes for class members too:
export class MyClass {
// ~ private
#SOME_STRING = "turtles";
}Use comments judiciously. Comments provide cognitive context for code and are as such invaluable in the long-term. The author of this project does not believe in "self-documenting code", only self-documenting mess.
Don't overuse comments. You shouldn't have // this is bridge followed by a bridge, but you should add comments where they add value and document what's going on or explain your thought process. Plan ahead and be mindful about accuracy so that comments are actually relevant to the code and don't become inaccurate historic artifacts. When changing code, you should update comments accordingly.
It's perfectly fine to leave a lot of code uncommented too, but only if it's obvious what it is doing. Use your best judgment.
Use proper punctuation and grammar in your comments. Capitalize your comments when they are more than a single sentence long. As a convention, capitalize comments within /**/ blocks while keeping single-line // comments lowercase.
When writing comments, stick to the 80 columns (with 4-wide tabs) boundary, this is especially important for JSDoc-style comments so that documentation fits into a narrow editor view.
foo(1, 2, 'a', 'b'); // do this
foo(1,2,'a','b'); // not this[1, 2, 'a', 'b'] // do this
[1,2,'a','b'] // not thisIf your line of code is getting long, then you should consider expanding it into multiple lines from the first bracket onward, and using an indent level for the inner items.
Examples:
return new Vector2D(
this.x / magnitude,
this.y / magnitude
);[
someLongLine,
someOtherLongLine
].forEach(item => item.hi());For example:
const canvas = (
(options.canvas instanceof HTMLCanvasElement)
? options.canvas // we received <canvas> directly
: document.querySelector(options.canvas) // we probably have a query
);For example:
// draw some shapes
this.render
.fill('orange')
.rect(10,10,50,50)
.stroke('blue', 2)
.circle(100, 100, 30);For example:
// configure <canvas>
const canvas = (
(options.canvas instanceof HTMLCanvasElement)
? options.canvas // we received <canvas> directly
: document.querySelector(options.canvas) // we probably have a query
);
if (!(canvas instanceof HTMLCanvasElement)) {
// we got something funny, perhaps an upside down cat but definitely
// not a <canvas>
throw 'Game needs to be initialized on a <canvas> element.';
}
this.#canvas = canvas;
// set intial game state
if (options.state === undefined) {
throw 'Game needs an initial state.';
}
this.setState(options.state);Two blocks of code that do distinct things are clearly visible here, and context is immediately obvious.
if (condition) {
/* ... */
}
else if (other_condition) {
/* ... */
}
else {
/* ... */
}switch (condition) {
case optionA:
/* ... */
return;
case optionB:
case optionC:
/* ... */
break;
case optionD:
{
/* larger code block */
}
break;
}export class MyClass {
// ~ private
/**
* Something private.
*
* @type {object}
* @private
*/
#something = null;
// ~ public
/**
* Constructor is the first thing in the public block.
*/
constructor() {
}
/**
* Something public.
*
* @type {string}
*/
somethingElse = "";
}Use a single blank line to visually separate items in a class.
The // ~ private and // ~ public lines are custom quirks of this codebase that denote private and public blocks in the class. Love them or hate them, but they are required. This is for easier readability down the line.
Typically a class is a private block followed by a public block. But you may also use a public block first if you want to expose, for example an enumerable property so it's visible at the top of the class. The Game class, for example is implemented like this:
export class Game {
// ~ public
/**
* Various available game view modes.
*/
static VIEW_MODE = {
// Default view mode. Behaves the same as a <canvas> would normally.
DEFAULT: 0,
// <canvas> is centered relative to parent.
CENTER: 1,
// <canvas> is scaled to fit parent, and centered. This is useful if you
// want proportional scaling with a fixed resolution.
SCALE_FIT: 2,
// <canvas> is stretched to fit parent.
SCALE_STRETCH: 3,
// <canvas> is expanded to fit parent. Physical dimensions of the element
// are altered.
EXPAND: 4
};
// ~ private
/* ... private code ... */
// ~ public
/* constructor */
/* ... public code ... */
}This is primarily an object-oriented codebase so your mindset needs to be objects over functions. Make your API as idiomatically object oriented as you can, within the bounds of the es6 class system. You may sprinkle functional styles on implementations (while not overusing them and turning the code into a functional mess), but do your best to avoid exposing the user API in functional ways so it stays consistent with the rest of the codebase.
In practice this usually means to expect the user to provide objects with state rather than functions. Exceptions to this rule may be applied where they make sense, but will be carefully reviewed by a team of skilled hamsters.
Unless you have an exceedingly good reason to do so, you should avoid using get/set statements altogether as they can cause confusion down the line with their atypical semantics.
Note that the myst2.ui extension does use getters and setters extensively, but it's for a good cause (to make component properties reactive and make API usage easier).
In general, avoid getters and setters and prefer methods and functions.
This serves both as on-the-go documentation for future developers as well as making API documentation generation available on the fly. Try to be descriptive and consistent. See the rest of the source code and try to match the same style.
Use one of the following options to document basic parameter types:
/**
* @param {boolean}
* @param {number}
* @param {string}
* @param {object}
* @param {*}
*/
Add empty lines between the description, the list of parameters and the return value:
/**
* A function that does this and that.
*
* @param {number} a First parameter.
* @param {number} [b] Second, optional parameter.
*
* @returns {number}
*/Do not use - between parameters and their descriptions as it is sometimes common:
// don't do this:
/**
* @param {number} a - First parameter
*/When breaking text lines in the description (to stay within 80 columns), use two spaces to indent the next line, like in this example:
/**
* @param {number} [options.framerate=60] Game's framerate. Ignored when
* the simple loop is used.
*/If you're making a feature that has multiple components and thinking about having src/yourfeature/ - don't do it. Instead, create a library extension and then use that extension's /src/ folder for all of its functionality.
Always name things my_thing.js and not, for example mything.js.
Make the file names short and descriptive. No overly long names.
////////////////////////////////////////////////////////////////////////////////
// _ ___
// _____ _ _ ___| |_|_ |
// | | | |_ -| _| _| 2D game engine for the web
// |_|_|_|_ |___|_| |___| https://github.com/metayeti/myst2
// |___|
//
// Licensed under the terms of the MIT license ( https://mit-license.org/ )
//
// Copyright (c) 2025 Danijel Durakovic
//
// ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
//
// myst2/src/your_file.js
//
////////////////////////////////////////////////////////////////////////////////If you are working on an engine extension, use the following header instead:
////////////////////////////////////////////////////////////////////////////////
// _ ___
// _____ _ _ ___| |_|_ |
// | | | |_ -| _| _| 2D game engine for the web
// |_|_|_|_ |___|_| |___| https://github.com/metayeti/myst2
// |___|
//
// Licensed under the terms of the MIT license ( https://mit-license.org/ )
//
// Copyright (c) 2025 Danijel Durakovic
//
// ::: EXTENSION ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
//
// myst2.your_extension
//
// ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
//
// myst2/extensions/your_extension/src/your_file.js
//
/////////////////////////////////////////////////////////////////////////////////**
* This sourcefile does this and that.
*/You may also add @author tags here:
/**
* This sourcefile does this and that.
*
* @author Author1
* @author Author2
*/For namespace exports:
//
// namespace exports
//
/* ... */
export * as your_namespace from './your_source.js';For class exports:
//
// class exports
//
/* ... */
export { YourFeature } from './your_source.js';Do the same if working on an extension, except in the main export hub of your extension (for example, /extensions/your_extension/your_extension.js);
Don't use AI-generated code in this project.
While autoformatters are a necessary evil in large projects to save teams time, I consider them unholy, especially for small, passion-driven projects like this one. Code formatters tend to prioritize consistency over flexibility, which steamrolls any number of nuanced code styling choices that add to the uniqueness of the codebase.
You are expected to extensively document any features you implement in the myst 2 wiki which serves as the official project's documentation. You will be given wiki privileges after your first PR is accepted. Use the style consistent with the rest of the wiki.
Tip: Clone from the other pages on the wiki and use them as templates.
You can try the #myst2 IRC channel on Libera.Chat. This is the official community channel for this game engine, but I can't guarantee activity.