Skip to content

Commit

Permalink
feat: global trigger (#573)
Browse files Browse the repository at this point in the history
  • Loading branch information
lajbel authored Dec 27, 2024
1 parent a7ccb3a commit 53c3e45
Show file tree
Hide file tree
Showing 14 changed files with 153 additions and 109 deletions.
2 changes: 1 addition & 1 deletion .vscode/snippets.code-snippets
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// kpd = KAPLAY Development
"prefix": "kpd-experimental",
"body": [
"@experimental This feature is in experimental phase, it will be fully released in $1",
"@experimental This feature is in experimental phase, it will be fully released in v3001.1.0",
],
"description": "Add @experimental tag in JSDoc"
}
Expand Down
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,24 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
(`GJK`) distance algorithm.
- Changed default behaviour of `kaplay({ tagsAsComponents: false })` to `false`.

## [3001.0.6] "Santa Events" - 2024-12-26

### Added

- Added `trigger(event, tag, ...args)` for global triggering events on a
specific tag (**experimental**)

```js
trigger("shoot", "target", 140);

on("shoot", "target", (obj, score) => {
obj.destroy();
debug.log(140); // every bomb was 140 score points!
});
```

- Added TypeScript definition for all App Events and missing Game Object Events

## [3001.0.5] - 2024-12-18

### Added
Expand Down
30 changes: 2 additions & 28 deletions src/app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from "../utils";

import GAMEPAD_MAP from "../data/gamepad.json" assert { type: "json" };
import type { AppEventMap } from "../game";
import {
type ButtonBinding,
type ButtonsDef,
Expand Down Expand Up @@ -125,34 +126,7 @@ export const initAppState = (opt: {
isMouseMoved: false,
lastWidth: opt.canvas.offsetWidth,
lastHeight: opt.canvas.offsetHeight,
events: new KEventHandler<{
mouseMove: [];
mouseDown: [MouseButton];
mousePress: [MouseButton];
mouseRelease: [MouseButton];
charInput: [string];
keyPress: [Key];
keyDown: [Key];
keyPressRepeat: [Key];
keyRelease: [Key];
touchStart: [Vec2, Touch];
touchMove: [Vec2, Touch];
touchEnd: [Vec2, Touch];
gamepadButtonDown: [KGamepadButton, KGamepad];
gamepadButtonPress: [KGamepadButton, KGamepad];
gamepadButtonRelease: [KGamepadButton, KGamepad];
gamepadStick: [string, Vec2, KGamepad];
gamepadConnect: [KGamepad];
gamepadDisconnect: [KGamepad];
buttonDown: [string];
buttonPress: [string];
buttonRelease: [string];
scroll: [Vec2];
hide: [];
show: [];
resize: [];
input: [];
}>(),
events: new KEventHandler<AppEventMap>(),
};
};

Expand Down
96 changes: 52 additions & 44 deletions src/game/events/eventMap.ts → src/events/eventMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,58 +9,30 @@ import {
type patrol,
type sentry,
type sprite,
} from "../../components";
import type { Vec2 } from "../../math";
import type { Collision, GameObj } from "../../types";
import { type addLevel } from "../level";

// exclude mapped types
export type GameObjEventNames =
| "update"
| "draw"
| "add"
| "destroy"
| "use"
| "unuse"
| "tag"
| "untag"
| "collide"
| "collideUpdate"
| "collideEnd"
| "hurt"
| "heal"
| "death"
| "beforePhysicsResolve"
| "physicsResolve"
| "ground"
| "fall"
| "fallOff"
| "headbutt"
| "doubleJump"
| "exitView"
| "enterView"
| "animStart"
| "animEnd"
| "navigationNext"
| "navigationEnded"
| "navigationStarted"
| "targetReached"
| "patrolFinished"
| "objectSpotted"
| "animateChannelFinished"
| "animateFinished"
| "spatialMapChanged"
| "navigationMapInvalid"
| "navigationMapChanged";
} from "../components";
import { type addLevel } from "../game/level";
import type { Vec2 } from "../math";
import type {
Collision,
GameObj,
Key,
KGamepad,
KGamepadButton,
MouseButton,
} from "../types";

/**
* Game Object events.
* Game Object events with their arguments.
*
* If looking for use it with `obj.on()`, ignore first parameter (Game Obj)
*
* @group Events
*/
export type GameObjEventMap = {
/** Triggered every frame */
"update": [GameObj];
/** Triggered every frame at a fixed 50fps rate */
"fixedUpdate": [GameObj];
/** Triggered every frame before update */
"draw": [GameObj];
/** Triggered when object is added */
Expand Down Expand Up @@ -233,6 +205,42 @@ export type GameObjEventMap = {
* From level of {@link addLevel `addLevel()`} function
*/
"navigationMapChanged": [GameObj];
};

export type GameObjEvents = GameObjEventMap & {
[key: string]: any[];
};

export type GameObjEventNames = keyof GameObjEventMap;

/**
* App events with their arguments
*/
export type AppEventMap = {
mouseMove: [];
mouseDown: [MouseButton];
mousePress: [MouseButton];
mouseRelease: [MouseButton];
charInput: [string];
keyPress: [Key];
keyDown: [Key];
keyPressRepeat: [Key];
keyRelease: [Key];
touchStart: [Vec2, Touch];
touchMove: [Vec2, Touch];
touchEnd: [Vec2, Touch];
gamepadButtonDown: [KGamepadButton, KGamepad];
gamepadButtonPress: [KGamepadButton, KGamepad];
gamepadButtonRelease: [KGamepadButton, KGamepad];
gamepadStick: [string, Vec2, KGamepad];
gamepadConnect: [KGamepad];
gamepadDisconnect: [KGamepad];
buttonDown: [string];
buttonPress: [string];
buttonRelease: [string];
scroll: [Vec2];
hide: [];
show: [];
resize: [];
input: [];
};
10 changes: 7 additions & 3 deletions src/utils/events.ts → src/events/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ export class KEventController {
}

export class KEvent<Args extends any[] = any[]> {
private cancellers: WeakMap<(...args: Args) => unknown, () => void> = new WeakMap();
private cancellers: WeakMap<(...args: Args) => unknown, () => void> =
new WeakMap();
private handlers: Registry<(...args: Args) => unknown> = new Registry();

add(action: (...args: Args) => unknown): KEventController {
Expand Down Expand Up @@ -97,8 +98,12 @@ export class KEvent<Args extends any[] = any[]> {
const result = action(...args);
let cancel;

if (result === EVENT_CANCEL_SYMBOL && (cancel = this.cancellers.get(action)))
if (
result === EVENT_CANCEL_SYMBOL
&& (cancel = this.cancellers.get(action))
) {
cancel();
}
});
}
numListeners(): number {
Expand All @@ -123,7 +128,6 @@ export class KEventHandler<EventMap extends Record<string, any[]>> {
>;
}
> = {};

on<Name extends keyof EventMap>(
name: Name,
action: (...args: EventMap[Name]) => void,
Expand Down
46 changes: 30 additions & 16 deletions src/game/events/events.ts → src/events/globalEvents.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,46 @@
// add an event to a tag

import { type Asset, getFailedAssets } from "../../assets";
import { _k } from "../../kaplay";
import type { Collision, GameObj, Tag } from "../../types";
import { KEventController, overload2, Registry } from "../../utils";
import type { GameObjEventMap, GameObjEventNames } from "./eventMap";
import { type Asset, getFailedAssets } from "../assets";
import { _k } from "../kaplay";
import type { Collision, GameObj, Tag } from "../types";
import { KEventController, overload2, Registry } from "../utils";
import type {
GameObjEventMap,
GameObjEventNames,
GameObjEvents,
} from "./eventMap";

export type TupleWithoutFirst<T extends any[]> = T extends [infer R, ...infer E]
? E
: never;

export function on<Ev extends GameObjEventNames | string & {}>(
export function on<Ev extends GameObjEventNames>(
event: Ev,
tag: Tag,
cb: (obj: GameObj, ...args: TupleWithoutFirst<GameObjEventMap[Ev]>) => void,
cb: (obj: GameObj, ...args: TupleWithoutFirst<GameObjEvents[Ev]>) => void,
): KEventController {
if (!_k.game.objEvents.registers[<keyof GameObjEventMap> event]) {
_k.game.objEvents.registers[<keyof GameObjEventMap> event] =
new Registry() as any;
if (!_k.game.objEvents.registers[event]) {
_k.game.objEvents.registers[event] = new Registry() as any;
}

return _k.game.objEvents.on(
<keyof GameObjEventMap> event,
(obj, ...args) => {
if (obj.is(tag)) {
cb(obj, ...args as TupleWithoutFirst<GameObjEventMap[Ev]>);
cb(obj, ...args as TupleWithoutFirst<GameObjEvents[Ev]>);
}
},
);
}

export const trigger = (event: string, tag: string, ...args: any[]) => {
for (const obj of _k.game.root.children) {
if (obj.is(tag)) {
obj.trigger(event);
}
}
};

export const onFixedUpdate = overload2(
(action: () => void): KEventController => {
const obj = _k.game.root.add([{ fixedUpdate: action }]);
Expand Down Expand Up @@ -111,11 +122,14 @@ export const onTag = overload2((action: (obj: GameObj, id: string) => void) => {
return on("tag", tag, action);
});

export const onUntag = overload2((action: (obj: GameObj, id: string) => void) => {
return _k.game.events.on("untag", action);
}, (tag: Tag, action: (obj: GameObj) => void) => {
return on("untag", tag, action);
});
export const onUntag = overload2(
(action: (obj: GameObj, id: string) => void) => {
return _k.game.events.on("untag", action);
},
(tag: Tag, action: (obj: GameObj) => void) => {
return on("untag", tag, action);
},
);

// add an event that runs with objs with t1 collides with objs with t2
export function onCollide(
Expand Down
2 changes: 2 additions & 0 deletions src/events/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./eventMap";
export * from "./globalEvents";
2 changes: 0 additions & 2 deletions src/game/events/index.ts

This file was deleted.

4 changes: 2 additions & 2 deletions src/game/game.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { Asset } from "../assets";
import type { TimerComp } from "../components";
import type { GameObjEventMap, GameObjEvents } from "../events";
import { Mat4, Vec2 } from "../math/math";
import { type GameObj, type Key, type MouseButton } from "../types";
import { KEventHandler } from "../utils";
import type { GameObjEventMap } from "./events";
import { make } from "./make";
import type { SceneDef, SceneName } from "./scenes";

Expand Down Expand Up @@ -50,7 +50,7 @@ export const initGame = () => {
}>(),

// object events
objEvents: new KEventHandler<GameObjEventMap>(),
objEvents: new KEventHandler<GameObjEvents>(),

// root game object
root: make([]) as GameObj<TimerComp>,
Expand Down
2 changes: 1 addition & 1 deletion src/game/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export * from "../events";
export * from "./camera";
export * from "./events";
export * from "./game";
export * from "./gravity";
export * from "./initEvents";
Expand Down
14 changes: 7 additions & 7 deletions src/game/make.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,15 +250,15 @@ export function make<T>(comps: CompList<T> = []): GameObj<T> {
comp[k]?.();
onCurCompCleanup = null;
}
: comp[<keyof typeof comp>k];
gc.push(this.on(k, <any>func).cancel);
: comp[<keyof typeof comp> k];
gc.push(this.on(k, <any> func).cancel);
}
else {
if (this[k] === undefined) {
// assign comp fields to game obj
Object.defineProperty(this, k, {
get: () => comp[<keyof typeof comp>k],
set: (val) => comp[<keyof typeof comp>k] = val,
get: () => comp[<keyof typeof comp> k],
set: (val) => comp[<keyof typeof comp> k] = val,
configurable: true,
enumerable: true,
});
Expand All @@ -270,9 +270,9 @@ export function make<T>(comps: CompList<T> = []): GameObj<T> {
)?.id;
throw new Error(
`Duplicate component property: "${k}" while adding component "${comp.id}"`
+ (originalCompId
? ` (originally added by "${originalCompId}")`
: ""),
+ (originalCompId
? ` (originally added by "${originalCompId}")`
: ""),
);
}
}
Expand Down
4 changes: 3 additions & 1 deletion src/kaplay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ import {
shake,
toScreen,
toWorld,
trigger,
} from "./game";

import boomSpriteSrc from "./kassets/boom.png";
Expand Down Expand Up @@ -1186,7 +1187,8 @@ const kaplay = <
patrol,
pathfinder,
// group events
on,
trigger,
on: on as KAPLAYCtx["on"], // our internal on should be strict, user shouldn't
onFixedUpdate,
onUpdate,
onDraw,
Expand Down
Loading

0 comments on commit 53c3e45

Please sign in to comment.