Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions packages/core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

#### setupMoxi
- `width` and `height` top-level props for convenient canvas sizing (alternative to `renderOptions.width/height`)
- `scaleMode` option ('none' | 'fit' | 'fill' | 'stretch') for automatic canvas scaling with CSS injection

#### Entity Helpers
- `asEntityGraphics(props)` - Creates Graphics wrapped with `asEntity()` for Logic support
- `asEntitySprite(constructorArgs, props)` - Creates Sprite wrapped with `asEntity()`
- `asEntityContainer(props)` - Creates Container wrapped with `asEntity()`
- `asEntityText(constructorArgs, props)` - Creates Text wrapped with `asEntity()`

### Changed

#### ClientEvents
- Enhanced JSDoc documentation clarifying key format uses `event.key` (e.g., 'a', 'ArrowRight', ' ') not `event.code`
- Added common key values table and MDN reference link

#### ActionManager
- Enhanced JSDoc with comprehensive examples for adding/removing listeners
- Documented common event targets (window, document, canvas) and event types
- Added cleanup pattern documentation for Logic components

## [0.3.2] - 2025-12-06

### Changed
Expand Down
213 changes: 213 additions & 0 deletions packages/core/llms.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
# @moxijs/core

> Game framework on PIXI.js with ECS architecture and physics.

## Install

```bash
npm install @moxijs/core pixi.js
```

## Quick Start

```ts
import { setupMoxi, asEntity, Logic } from '@moxijs/core';

const { scene, engine } = await setupMoxi({
hostElement: document.getElementById('game'),
});

class RotateLogic extends Logic<PIXI.Sprite> {
update(entity, dt) { entity.rotation += 0.01 * dt; }
}

const sprite = asEntity(new PIXI.Sprite(texture));
sprite.moxiEntity.addLogic(new RotateLogic());
scene.addChild(sprite);
engine.start();
```

## Core API

### setupMoxi(options) → Promise
Main entry point. Returns `{ scene, engine, camera, physicsWorld?, loadAssets }`.

```ts
await setupMoxi({
hostElement: HTMLElement, // Required
renderOptions?: PIXIOptions, // PIXI renderer options
physics?: boolean | PhysicsOpts, // Enable Planck.js physics
pixelPerfect?: boolean, // Pixel-art mode
});
```

### asEntity(displayObject) → AsEntity
Wraps PIXI.Container with logic system.

```ts
const entity = asEntity(new PIXI.Sprite(texture));
entity.moxiEntity.addLogic(logic);
entity.moxiEntity.getLogic<T>('LogicName');
entity.moxiEntity.removeLogic(logic);
```

### Logic<T>
Base class for entity behaviors.

```ts
class MyLogic extends Logic<PIXI.Sprite> {
name = 'MyLogic';
active = true;

init(entity, renderer) { /* once on scene.init() */ }
update(entity, deltaTime) { /* every frame */ }
}
```

### Scene
Root container. Extends PIXI.Container.

```ts
scene.addChild(entity);
scene.init(); // Initializes all entity logic
```

### Engine
Game loop manager.

```ts
engine.start();
engine.stop();
engine.loadStage(newScene);
```

## Physics (Planck.js)

```ts
const { physicsWorld } = await setupMoxi({ physics: true });

// Add physics body to entity
entity.moxiEntity.addLogic(new PhysicsBodyLogic(physicsWorld, {
type: 'dynamic', // 'static' | 'dynamic' | 'kinematic'
shape: { type: 'box', width: 32, height: 32 },
// or: { type: 'circle', radius: 16 }
// or: { type: 'polygon', vertices: [{x,y}...] }
collisionTags: ['player'],
collidesWith: ['enemy', 'ground'],
}));

// Collision events
physicsWorld.onCollision('player', 'enemy', (event) => {
console.log(event.bodyA, event.bodyB);
});

// Debug visualization
physicsWorld.enableDebugRenderer(scene);
```

### Physics Helpers

```ts
import { asPhysicsEntity, hasPhysics, getPhysicsBody } from '@moxijs/core';

const entity = asPhysicsEntity(sprite, physicsWorld, bodyOptions);
if (hasPhysics(entity)) {
const body = getPhysicsBody(entity);
}
```

## State Machine

```ts
import { StateMachine, StateLogic } from '@moxijs/core';

class IdleState extends StateLogic<Player> {
name = 'idle';
onEnter() { }
onExit() { }
update(entity, dt) {
if (jumping) this.machine.transition('jump');
}
}

const fsm = new StateMachine<Player>();
fsm.addState(new IdleState());
fsm.start('idle');
```

## Camera

```ts
camera.follow(entity);
camera.setZoom(2);
camera.pan(100, 50);
camera.shake(intensity, duration);
```

## PIXI Helpers

```ts
import { asSprite, asText, asGraphics, asContainer } from '@moxijs/core';

const sprite = asSprite(texture, { x: 100, y: 50, anchor: 0.5, scale: 2 });
const text = asText('Hello', { style: { fill: 0xffffff } });
```

## Texture Utilities

```ts
import { asTextureFrames, TextureFrameSequences } from '@moxijs/core';

// Split spritesheet
const frames = asTextureFrames(texture, { frameWidth: 32, frameHeight: 32 });

// Named sequences
const seq = new TextureFrameSequences(texture, {
idle: { start: 0, end: 3 },
walk: { start: 4, end: 11 },
});
const walkFrames = seq.get('walk');
```

## Input

```ts
import { ClientEvents } from '@moxijs/core';

const input = ClientEvents.getInstance();
if (input.isKeyDown('ArrowRight')) player.x += 5;
const mousePos = input.movePosition;
```

## Events

```ts
import { EventEmitter } from '@moxijs/core';

interface Events {
'score': (n: number) => void;
}
const events = new EventEmitter<Events>();
events.on('score', (n) => console.log(n));
events.emit('score', 100);
```

## Exports Summary

**Core:** `setupMoxi`, `Engine`, `Scene`, `asEntity`, `Logic`, `StateMachine`, `StateLogic`

**Physics:** `PhysicsWorld`, `PhysicsBodyLogic`, `asPhysicsEntity`, `hasPhysics`, `getPhysicsBody`

**Camera:** `Camera`, `CameraLogic`

**Helpers:** `asSprite`, `asText`, `asGraphics`, `asContainer`, `asTextureFrames`, `TextureFrameSequences`

**Input/Events:** `ClientEvents`, `EventEmitter`, `ActionManager`

**Utils:** `utils.lerp`, `utils.clamp`, `utils.getRandomInt`, `utils.rad2deg`, `utils.deg2rad`

## See Also

- **@moxijs/ui** - UI components (UIButton, UILabel, FlexContainer, etc.)
- **Docs:** https://ineffably.github.io/moxijs/docs/core/
- **Examples:** https://ineffably.github.io/moxijs/
3 changes: 2 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"lib",
"scripts",
"README.md",
"LICENSE"
"LICENSE",
"llms.txt"
],
"scripts": {
"generate-msdf-font": "node scripts/generate-msdf-font.mjs",
Expand Down
14 changes: 11 additions & 3 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { loadFonts } from './library/font-loader';
import { asEntity, MoxiEntity } from './main/moxi-entity';
import { Camera, CameraLogic } from './main/camera';
import { asTextureFrames } from './library/texture-frames';
import { asBitmapText, asSprite, asText, asTextDPR, asGraphics, asContainer, asMSDFText, PixiProps, TextDPROptions } from './library/as-pixi';
import { asBitmapText, asSprite, asText, asTextDPR, asGraphics, asContainer, asMSDFText, asEntityGraphics, asEntitySprite, asEntityContainer, asEntityText, PixiProps, TextDPROptions } from './library/as-pixi';
import { TextureFrameSequences, SequenceInfo } from './library/texture-frame-sequences';
import { createResizeHandler, setupResponsiveCanvas, ResizeHandlerOptions } from './library/resize-handler';
import { StateMachine } from './library/state-machine';
Expand Down Expand Up @@ -64,8 +64,8 @@ export type {
} from './library/physics';
export type { SVGToTextureOptions } from './library/svg-utils/svg-to-texture';
export type { PixelGridConfig, BorderConfig } from './library/pixel-grid';
export type { SpriteOptions, TextOptions, TextDPROptions, BitmapTextOptions, PixiProps, MSDFTextOptions } from './library/as-pixi';
export type { SetupMoxiArgs, PixelPerfectOptions } from './library/setup';
export type { SpriteOptions, TextOptions, TextDPROptions, BitmapTextOptions, PixiProps, PixiPropsWithEntity, MSDFTextOptions, GraphicsDrawOptions } from './library/as-pixi';
export type { SetupMoxiArgs, PixelPerfectOptions, ScaleMode } from './library/setup';
export type { AssetLoaderEvents } from './main/asset-loader';

export type Asset = { src: string, alias?: string };
Expand Down Expand Up @@ -109,6 +109,10 @@ const exportedObjects = {
asGraphics,
asContainer,
asMSDFText,
asEntityGraphics,
asEntitySprite,
asEntityContainer,
asEntityText,
Logic,
Camera,
CameraLogic,
Expand Down Expand Up @@ -169,6 +173,10 @@ export {
asGraphics,
asContainer,
asMSDFText,
asEntityGraphics,
asEntitySprite,
asEntityContainer,
asEntityText,
Logic,
Camera,
CameraLogic,
Expand Down
27 changes: 26 additions & 1 deletion packages/core/src/library/action-manager.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,49 @@
/**
* ActionManager - Centralized event listener management utility
* ActionManager - Centralized event listener management utility.
*
* Prevents memory leaks by tracking all registered event listeners and providing
* batch cleanup functionality. Particularly useful for Logic components that need to
* register multiple window/document listeners and clean them up on destroy.
*
* ## Why Use ActionManager?
* When adding event listeners in game logic, forgetting to remove them causes memory leaks.
* ActionManager tracks all listeners and provides a single `removeAll()` call for cleanup.
*
* ## Common Event Targets
* - `window` - Global events like resize, blur, focus
* - `document` - Keyboard events, pointer events that shouldn't be canvas-bound
* - `renderer.canvas` - Canvas-specific events
* - Any DOM element
*
* ## Common Event Types
* - Pointer: 'pointerdown', 'pointermove', 'pointerup', 'pointercancel'
* - Mouse: 'mousedown', 'mousemove', 'mouseup', 'wheel', 'contextmenu'
* - Keyboard: 'keydown', 'keyup'
* - Window: 'resize', 'blur', 'focus', 'visibilitychange'
*
* @example
* ```ts
* // In a Logic component - drag handling with cleanup
* class DragLogic extends Logic<Container> {
* private actions = new ActionManager();
*
* init(entity: Container) {
* // Add listeners - they're tracked automatically
* this.actions.add(window, 'pointermove', this.onMove.bind(this));
* this.actions.add(window, 'pointerup', this.onUp.bind(this));
* this.actions.add(window, 'blur', this.onBlur.bind(this));
* }
*
* destroy() {
* // Clean up all listeners at once - no memory leaks!
* this.actions.removeAll();
* }
* }
*
* // Removing individual actions
* const wheelAction = actions.add(window, 'wheel', onWheel);
* // ... later ...
* actions.remove(wheelAction); // Remove just this one
* ```
*/

Expand Down
Loading