diff --git a/.vscode/snippets.code-snippets b/.vscode/snippets.code-snippets index e74538c8..ad1f62e5 100644 --- a/.vscode/snippets.code-snippets +++ b/.vscode/snippets.code-snippets @@ -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" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 438650c1..92c335aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/app/app.ts b/src/app/app.ts index 499b84a7..4e341dca 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -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, @@ -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(), }; }; diff --git a/src/game/events/eventMap.ts b/src/events/eventMap.ts similarity index 78% rename from src/game/events/eventMap.ts rename to src/events/eventMap.ts index 7a386c08..ad8ac4e1 100644 --- a/src/game/events/eventMap.ts +++ b/src/events/eventMap.ts @@ -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 */ @@ -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: []; +}; diff --git a/src/utils/events.ts b/src/events/events.ts similarity index 96% rename from src/utils/events.ts rename to src/events/events.ts index 5c44b8c7..28abde36 100644 --- a/src/utils/events.ts +++ b/src/events/events.ts @@ -66,7 +66,8 @@ export class KEventController { } export class KEvent { - 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 { @@ -97,8 +98,12 @@ export class KEvent { 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 { @@ -123,7 +128,6 @@ export class KEventHandler> { >; } > = {}; - on( name: Name, action: (...args: EventMap[Name]) => void, diff --git a/src/game/events/events.ts b/src/events/globalEvents.ts similarity index 86% rename from src/game/events/events.ts rename to src/events/globalEvents.ts index 90701b34..a2271fbd 100644 --- a/src/game/events/events.ts +++ b/src/events/globalEvents.ts @@ -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 [infer R, ...infer E] ? E : never; -export function on( +export function on( event: Ev, tag: Tag, - cb: (obj: GameObj, ...args: TupleWithoutFirst) => void, + cb: (obj: GameObj, ...args: TupleWithoutFirst) => void, ): KEventController { - if (!_k.game.objEvents.registers[ event]) { - _k.game.objEvents.registers[ 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( event, (obj, ...args) => { if (obj.is(tag)) { - cb(obj, ...args as TupleWithoutFirst); + cb(obj, ...args as TupleWithoutFirst); } }, ); } +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 }]); @@ -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( diff --git a/src/events/index.ts b/src/events/index.ts new file mode 100644 index 00000000..549c25be --- /dev/null +++ b/src/events/index.ts @@ -0,0 +1,2 @@ +export * from "./eventMap"; +export * from "./globalEvents"; diff --git a/src/game/events/index.ts b/src/game/events/index.ts deleted file mode 100644 index 568cf121..00000000 --- a/src/game/events/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./eventMap"; -export * from "./events"; diff --git a/src/game/game.ts b/src/game/game.ts index 474ba6ab..8124c635 100644 --- a/src/game/game.ts +++ b/src/game/game.ts @@ -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"; @@ -50,7 +50,7 @@ export const initGame = () => { }>(), // object events - objEvents: new KEventHandler(), + objEvents: new KEventHandler(), // root game object root: make([]) as GameObj, diff --git a/src/game/index.ts b/src/game/index.ts index 9a5c7f19..9f5a6c9a 100644 --- a/src/game/index.ts +++ b/src/game/index.ts @@ -1,5 +1,5 @@ +export * from "../events"; export * from "./camera"; -export * from "./events"; export * from "./game"; export * from "./gravity"; export * from "./initEvents"; diff --git a/src/game/make.ts b/src/game/make.ts index b4c3c291..98695db9 100644 --- a/src/game/make.ts +++ b/src/game/make.ts @@ -250,15 +250,15 @@ export function make(comps: CompList = []): GameObj { comp[k]?.(); onCurCompCleanup = null; } - : comp[k]; - gc.push(this.on(k, func).cancel); + : comp[ k]; + gc.push(this.on(k, func).cancel); } else { if (this[k] === undefined) { // assign comp fields to game obj Object.defineProperty(this, k, { - get: () => comp[k], - set: (val) => comp[k] = val, + get: () => comp[ k], + set: (val) => comp[ k] = val, configurable: true, enumerable: true, }); @@ -270,9 +270,9 @@ export function make(comps: CompList = []): GameObj { )?.id; throw new Error( `Duplicate component property: "${k}" while adding component "${comp.id}"` - + (originalCompId - ? ` (originally added by "${originalCompId}")` - : ""), + + (originalCompId + ? ` (originally added by "${originalCompId}")` + : ""), ); } } diff --git a/src/kaplay.ts b/src/kaplay.ts index 393559a4..ada17287 100644 --- a/src/kaplay.ts +++ b/src/kaplay.ts @@ -282,6 +282,7 @@ import { shake, toScreen, toWorld, + trigger, } from "./game"; import boomSpriteSrc from "./kassets/boom.png"; @@ -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, diff --git a/src/types.ts b/src/types.ts index 3c4b64bb..9ddafa2b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -93,8 +93,8 @@ import type { import type { BoomOpt, Game, - GameObjEventMap, GameObjEventNames, + GameObjEvents, LevelOpt, SceneDef, SceneName, @@ -1407,6 +1407,27 @@ export interface KAPLAYCtx< * @group Math */ raycast(origin: Vec2, direction: Vec2, exclude?: string[]): RaycastResult; + /** + * Trigger an event on all game objs with certain tag. + * + * @param tag - The tag to trigger to. + * @param args - Arguments to pass to the `on()` functions + * + * @example + * ```js + * trigger("shoot", "target", 140); + * + * on("shoot", "target", (obj, score) => { + * obj.destroy(); + * debug.log(140); // every bomb was 140 score points! + * }); + * ``` + * + * @since v3001.0.6 + * @group Events + * @experimental This feature is in experimental phase, it will be fully released in v3001.1.0 + */ + trigger(event: string, tag: string, ...args: any): void; /** * Register an event on all game objs with certain tag. * @@ -1450,7 +1471,7 @@ export interface KAPLAYCtx< tag: Tag, action: ( obj: GameObj, - ...args: TupleWithoutFirst + ...args: TupleWithoutFirst ) => void, ): KEventController; /** @@ -5682,7 +5703,10 @@ export interface GameObjRaw { * @returns The event controller. * @since v2000.0 */ - on(event: string, action: (...args: any) => void): KEventController; + on( + event: GameObjEventNames | (string & {}), + action: (...args: any) => void, + ): KEventController; /** * Trigger an event. * diff --git a/src/utils/index.ts b/src/utils/index.ts index 1814eea7..0e745a8d 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,9 +1,9 @@ +export * from "../events/events"; export * from "./asserts"; export * from "./benchmark"; export * from "./binaryheap"; export * from "./dataURL"; export * from "./deepEq"; -export * from "./events"; export * from "./log"; export * from "./numbers"; export * from "./overload";