@harlamov/games
v1.0.48
Published
Tiny framework for projects made with Legend17 UI Editor
Readme
@harlamov/games
A lightweight runtime for browser-based 2D games built with PixiJS 8. The library creates and configures the PixiJS application, mounts the root stage, handles screen resizing, and provides shared services for audio, localization, and the game environment.
The library is designed for games built with the Legend17 UI Editor.
Create a new project with:
npm create @harlamov/gameRequirements
- Node.js 20.19 or newer;
- PixiJS 8.19.
Preparing the page
By default, the canvas is appended to the #pixi-container element:
<div id="pixi-container"></div>The container must have a non-zero size:
html,
body,
#pixi-container {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
}Use the pixiContainer option to provide a different container. It accepts
a CSS selector:
await startApp(application, {
gameId: "example-game",
pixiContainer: "#game"
});It also accepts an HTMLElement:
const pixiContainer = document.querySelector<HTMLElement>("#game");
if (!pixiContainer) {
throw new Error("Pixi container was not found.");
}
await startApp(application, {
gameId: "example-game",
pixiContainer
});Starting a game
Create an App class with an asynchronous start() method and mount the game
layers:
export class App {
private readonly layers = new AppLayers();
public async start(): Promise<void> {
mountStage(this.layers);
}
}Pass the application module and game options to startApp in the entry point:
// src/index.ts
const [{ startApp }, application] = await Promise.all([
import("@harlamov/games"),
import("./core/App")
]);
await startApp(application, {
gameId: "example-game"
});mountStage() can only be called once. The root AppLayers container must
implement onScreenResize(). The method is called immediately after the stage
is mounted and whenever the container size, viewport, orientation, or
fullscreen state changes.
Editor element orientation
Elements generated by the Legend17 UI Editor support three orientations:
type Orientation = "landscape" | "portrait" | "square";Generated prefabs implement setOrientation(orientation). This method applies
the properties configured for the selected orientation in the editor and
passes the orientation to nested elements.
AppLayers manages the current orientation. Its onScreenResize() method
determines the orientation from the aspect ratio, stores it in
moduleLoader.orientation, and applies it to elements already on the stage:
class AppLayers extends Container {
public onScreenResize(screenWidth: number, screenHeight: number): void {
const aspectRatio = screenWidth / screenHeight;
const orientation: Orientation = aspectRatio >= Math.sqrt(4 / 3)
? "landscape"
: aspectRatio <= Math.sqrt(3 / 4)
? "portrait"
: "square";
if (moduleLoader.orientation !== orientation) {
moduleLoader.orientation = orientation;
this.applyOrientation(this.content, orientation);
}
}
}applyOrientation() should recursively traverse child elements and call
setOrientation() wherever that method is available. Preserve the current
visible value so that applying an orientation does not change visibility
managed by the game.
A prefab added after the most recent resize must be synchronized before it is shown:
import type { PrefabView } from "@harlamov/games";
type Screen = Container & PrefabView<Orientation>;
const screen = await app.managers.prefabs.get(ScreenClass);
screen.setOrientation(moduleLoader.orientation);
app.layers.screens.addChild(screen);The PrefabView<Orientation> interface guarantees the presence of
prepare() and setOrientation() at the TypeScript level.
Global API
The following global values are available after startApp() completes:
| Value | Purpose |
| --- | --- |
| app | The App instance and game services |
| app.renderer | The PixiJS Application |
| app.audio | Audio loading and playback |
| app.environment | Game, user, device, and language environment |
| locale | Localization |
| mountStage | Root stage mounting |
| delay | Delays in seconds using the shared PixiJS ticker |
| trace | console.log with a library prefix |
| removeHtmlPreloader | Removes #html-preloader or a custom element |
Example:
trace("Game started");
await delay(0.5);Project types
The GlobalTypes interface supports declaration merging. Use it to restrict
the allowed languages, localization keys, and sound identifiers, and to
register the application type:
import type { App } from "./App";
type Language = "en" | "ru";
type LocaleKey =
| "app.title"
| "menu.play"
| "inventory.coins";
type SoundId = "click" | "music" | "win";
declare module "@harlamov/games" {
interface GlobalTypes {
App: App;
Language: Language;
LocaleKey: LocaleKey;
SoundId: SoundId;
}
}TypeScript will then validate arguments passed to locale.translate(),
locale.setLanguage(), app.audio.playSound(), and related methods.
Audio
By default, sounds are loaded as MP3 files from assets/sounds. A sound ID is
used as its file name:
assets/sounds/click.mp3
assets/sounds/music.mp3Sounds can be preloaded or loaded lazily on first playback:
await app.audio.load(["click", "music"]);
await app.audio.playSound("click");
await app.audio.playLoop("music");
app.audio.stopLoop("music");
app.audio.mute();
app.audio.unmute();Web Audio is unlocked automatically after the first pointerdown or
keydown. Loops requested before audio is unlocked start after a permitted
user interaction.
Configure the base path, file format, and retry count through startApp:
await startApp(application, {
gameId: "example-game",
audio: {
basePath: "assets/audio",
extension: "ogg",
loadRetryCount: 2
}
});The path and format can also be resolved dynamically once before the first load:
await startApp(application, {
gameId: "example-game",
audio: {
resolveBasePath: () => {
return navigator.connection?.saveData
? "assets/audio/low"
: "assets/audio/high";
},
resolveExtension: () => {
const audio = document.createElement("audio");
return audio.canPlayType('audio/ogg; codecs="vorbis"')
? "ogg"
: "mp3";
}
}
});The native AudioContext is available through app.audio.context, allowing
the application to manage its lifecycle:
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
void app.audio.context.suspend();
} else {
void app.audio.context.resume();
}
});suspend() preserves the positions of active sounds and loops. mute() has
different semantics: it stops current playback, while unmute() starts the
requested loops again.
Localization
Each language must be stored in a separate JSON file. The files may be located anywhere. A locale file is a flat object whose keys and values are strings.
For example, public/assets/locale/en.json:
{
"app.title": "Example Game",
"inventory.coins": "%count% $(count, [coin, coins])$",
"inventory.leaves": "%count% leaves"
}And public/assets/locale/ru.json:
{
"app.title": "Пример игры",
"inventory.coins": "%count% $(count, [монета, монеты, монет])$",
"inventory.leaves": "лист$[а, ов]"
}Do not use nested objects, arrays, comments, computed values, or executable code. The file must remain a plain object mapping keys to string values. Every language file must contain the same set of keys.
To use localization in the editor, provide a path to one of the locale files.
Set the path through localeFilePath in the editor's .legend17 project file.
The path is resolved relative to the project file:
{
"localeFilePath": "../../public/assets/locale/en.json"
}The editor reads this JSON file to obtain the available keys and their text values while working with views.
At game startup, load the selected language file through PixiJS Assets, register it, and make the language active:
import { Assets } from "pixi.js";
const language = app.environment.getLanguageFromQuery() ?? "en";
const localeData = await Assets.load(`assets/locale/${language}.json`);
locale.addLanguage(language, localeData);
locale.setLanguage(language);
locale.translate("app.title");
locale.translate("inventory.coins", { count: 5 });Translation strings support the following syntax:
%name%— inserts a string or number;$(name, [one, few, other])$— selects a complete form usingIntl.PluralRules;$[suffix1, suffix2]— sequentially selects suffixes using thepluralparameter;- a literal
\\nin the stored string — inserts a line break.
Suffix example:
locale.translate("inventory.leaves", { plural: [1] }); // лист
locale.translate("inventory.leaves", { plural: [2] }); // листа
locale.translate("inventory.leaves", { plural: [5] }); // листовMissing and empty translations return ......
Environment
app.environment.game.id;
app.environment.game.storagePath;
app.environment.designResolution;
app.environment.user.id;
app.environment.user.name;
app.environment.user.language;
app.environment.user.device.id;
app.environment.user.device.isMobile;
app.environment.user.device.hapticSupported;Use app.environment.getLanguageFromQuery() to resolve a language from a
query parameter. By default, it reads lang and normalizes regional language
tags:
?lang=en-US -> en
?lang=ru_RU -> ruThe query parameter name can be changed:
app.environment.getLanguageFromQuery({
queryParameter: "locale"
});startApp options
| Option | Default | Description |
| --- | --- | --- |
| gameId | required | Unique game identifier |
| baseStoragePath | harlamov.games | Storage namespace prefix |
| supportedLanguages | ["en", "ru"] | Supported languages |
| designResolution | 1920 × 1080 | Logical coordinate system |
| user.name | Player 1 | Local player name |
| user.id | 123456 | Local player identifier |
| pixiContainer | #pixi-container | Container HTMLElement or CSS selector |
| backgroundColor | #0e1822 | Canvas background color |
| imageRendering | auto | CSS image-rendering value |
| skipDetections | true | Disables PixiJS texture format detection |
| texturePreference | { format: ["webp"] } | Preferred texture formats and resolutions |
| assetLoadRetryCount | 3 | PixiJS Assets load retries |
| audio | — | Audio manager options |
HTML preloader
When the page contains a static preloader with the html-preloader ID, remove
it only after the in-game preloader has been displayed:
export class App {
public async start(): Promise<void> {
await this.loadInGamePreloader();
this.showInGamePreloader();
removeHtmlPreloader();
await this.loadGameAssets();
}
}Calling removeHtmlPreloader() more than once is safe.
Standard type extensions
The package registers helpers for numbers, strings, and arrays when imported.
(1250).pretty();
String("hello").capitalize();
String("hello world").wrap(5);
String("123").isInteger();
String("example").toNumberHash();
[1, 2].pushOnce(2, 3);
[1, 2, 3].remove(2);These extensions are added as non-enumerable prototype properties.
Contact
- Website: harlamov.games
- Telegram: @harlamov_dev
- Email: [email protected]
License
This library is distributed under the MIT License.
