diff --git a/CHANGELOG.md b/CHANGELOG.md index 7798c8c3..6927fcd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,13 +7,11 @@ - Added `ellipse()` component. - Circle area is no longer a box. - Added restitution and friction. +- Objects can switch parent by assigning the parent property or using setParent. - Added a fake cursor API. + ```js - const myCursor = add([ - fakeMouse(), - sprite("kat"), - pos(100, 100), - ]); + const myCursor = add([fakeMouse(), sprite("kat"), pos(100, 100)]); myCursor.press(); // trigger onClick events if the mouse is over myCursor.release(); @@ -25,7 +23,7 @@ - Added many JSDoc specifiers on many functions (@require, @deprecated, @since, @group, etc) - Added `getLayers()` to get the layers list -- Added `getDefaulLayer()` to get the default layer +- Added `getDefaultLayer()` to get the default layer - Deprecated camera methods `camScale()`, `camPos()` and `camRot()` in favor of `setCamScale()`, `getCamScale()`, `setCamPos()`, `getCamPos()`, `setCamRot()` and `getCamRot`. @@ -724,7 +722,7 @@ player.onBeforePhysicsResolve((collision) => { `stay(["gameover", "menu"])` - (**BREAK**) changed `SpriteComp#flipX` and `SpriteComp#flipY` to properties instead of functions -- (**BEARK**) `sprite.onAnimStart()` and `sprite.onAnimEnd()` now triggers on +- (**BREAK**) `sprite.onAnimStart()` and `sprite.onAnimEnd()` now triggers on any animation ```js diff --git a/examples/parenttest.js b/examples/parenttest.js new file mode 100644 index 00000000..77089e1d --- /dev/null +++ b/examples/parenttest.js @@ -0,0 +1,56 @@ +kaplay(); + +loadBean(); + +const centerBean = add([ + pos(center()), + anchor("center"), + sprite("bean"), + area(), + rotate(0), + scale(2), + { + update() { + this.angle += 20 * dt(); + }, + }, +]); + +const orbitingBean = centerBean.add([ + pos(vec2(100, 0)), + anchor("center"), + sprite("bean"), + area(), + rotate(0), + scale(1), + color(), + { + update() { + this.angle = -this.parent.transform.getRotation(); + if (this.isHovering()) { + this.color = RED; + } + else { + this.color = WHITE; + } + }, + }, +]); + +onMousePress(() => { + if (orbitingBean.parent === centerBean /* && orbitingBean.isHovering()*/) { + orbitingBean.setParent(getTreeRoot(), { keep: KeepFlags.All }); + } +}); + +onMouseMove((pos, delta) => { + if (orbitingBean.parent !== centerBean) { + orbitingBean.pos = orbitingBean.pos.add(delta); + } +}); + +onMouseRelease(() => { + if (orbitingBean.parent !== centerBean) { + orbitingBean.setParent(centerBean, { keep: KeepFlags.All }); + } +}); diff --git a/src/components/index.ts b/src/components/index.ts index fc262deb..c35c6264 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,5 +1,5 @@ -export * from './draw'; -export * from './level'; -export * from './misc'; -export * from './physics'; -export * from './transform'; +export * from "./draw"; +export * from "./level"; +export * from "./misc"; +export * from "./physics"; +export * from "./transform"; diff --git a/src/components/physics/effectors.ts b/src/components/physics/effectors.ts index 11aecdba..30b5c671 100644 --- a/src/components/physics/effectors.ts +++ b/src/components/physics/effectors.ts @@ -26,7 +26,8 @@ export function surfaceEffector( speedVariation: opts.speedVariation ?? 0, forceScale: opts.speedVariation ?? 0.9, add(this: GameObj) { - this.onCollideUpdate("body", (obj, col) => { + this.onCollideUpdate((obj, col) => { + if (!obj.has("body")) return; const dir = col?.normal.normal(); const currentVel = obj.vel.project(dir); const wantedVel = dir?.scale(this.speed); @@ -57,7 +58,8 @@ export function areaEffector(opts: AreaEffectorCompOpt): AreaEffectorComp { linearDrag: opts.linearDrag ?? 0, useGlobalAngle: opts.useGlobalAngle ?? true, add(this: GameObj) { - this.onCollideUpdate("body", obj => { + this.onCollideUpdate(obj => { + if (!obj.has("body")) return; obj.addForce( this.useGlobalAngle ? this.force @@ -97,15 +99,16 @@ export function pointEffector(opts: PointEffectorCompOpt): PointEffectorComp { linearDrag: opts.linearDrag ?? 0, // angularDrag: opts.angularDrag ?? 0, add(this: GameObj) { - this.onCollideUpdate("body", (obj, col) => { + this.onCollideUpdate((obj, col) => { + if (!obj.has("body")) return; const dir = this.pos.sub(obj.pos); const length = dir.len(); const distance = length * this.distanceScale / 10; const forceScale = this.forceMode === "constant" ? 1 : this.forceMode === "inverseLinear" - ? 1 / distance - : 1 / distance ** 2; + ? 1 / distance + : 1 / distance ** 2; const force = dir.scale( this.forceMagnitude * forceScale / length, ); @@ -249,7 +252,8 @@ export function buoyancyEffector( flowMagnitude: opts.flowMagnitude ?? 0, flowVariation: opts.flowVariation ?? 0, add(this: GameObj) { - this.onCollideUpdate("body", (obj, col) => { + this.onCollideUpdate((obj, col) => { + if (!obj.has("body")) return; const o = obj as GameObj; const shape = o.worldArea(); const polygon: Polygon = shape instanceof Polygon diff --git a/src/game/make.ts b/src/game/make.ts index bc6ac3d5..dfeb3160 100644 --- a/src/game/make.ts +++ b/src/game/make.ts @@ -28,6 +28,18 @@ import { type Tag, } from "../types"; import { KEventController, KEventHandler, uid } from "../utils"; +import type { Game } from "./game"; + +export enum KeepFlags { + Pos = 1, + Angle = 2, + Scale = 4, + All = 7, +} + +export type SetParentOpt = { + keep: KeepFlags; +}; export function make(comps: CompList = []): GameObj { const compStates = new Map(); @@ -39,6 +51,7 @@ export function make(comps: CompList = []): GameObj { const treatTagsAsComponents = _k.globalOpt.tagsAsComponents; let onCurCompCleanup: Function | null = null; let paused = false; + let _parent: GameObj; // the game object without the event methods, added later const obj: Omit = { @@ -47,7 +60,42 @@ export function make(comps: CompList = []): GameObj { hidden: false, transform: new Mat23(), children: [], - parent: null, + + get parent() { + return _parent!; + }, + + set parent(p: GameObj) { + if (_parent === p) return; + const index = _parent + ? _parent.children.indexOf(this as GameObj) + : -1; + if (index !== -1) { + _parent.children.splice(index, 1); + } + _parent = p; + p.children.push(this as GameObj); + }, + + setParent(p: GameObj, opt: SetParentOpt) { + if (_parent === p) return; + const oldTransform = _parent.transform; + const newTransform = p.transform; + if ((opt.keep & KeepFlags.Pos) && this.pos !== undefined) { + oldTransform.transformPoint(this.pos, this.pos); + newTransform.inverse.transformPoint(this.pos, this.pos); + } + if ((opt.keep & KeepFlags.Angle) && this.angle !== undefined) { + this.angle += newTransform.getRotation() + - oldTransform.getRotation(); + } + if ((opt.keep & KeepFlags.Scale) && this.scale !== undefined) { + this.scale = this.scale.scale( + oldTransform.getScale().invScale(newTransform.getScale()), + ); + } + this.parent = p; + }, set paused(p) { if (p === paused) return; @@ -254,15 +302,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, }); @@ -274,9 +322,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/index.ts b/src/index.ts index 54ee56cf..79e3724c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -109,9 +109,11 @@ export type { Game, GameObjEventMap, GameObjEventNames, + KeepFlags, LevelOpt, SceneDef, SceneName, + SetParentOpt, TupleWithoutFirst, } from "./game"; export type { diff --git a/src/kaplay.ts b/src/kaplay.ts index a13b3fb1..47e382b5 100644 --- a/src/kaplay.ts +++ b/src/kaplay.ts @@ -254,6 +254,7 @@ import { go, initEvents, initGame, + KeepFlags, layers, make, on, @@ -1540,6 +1541,7 @@ const kaplay = < KEvent, KEventHandler, KEventController, + KeepFlags, cancel: () => EVENT_CANCEL_SYMBOL, }; diff --git a/src/math/math.ts b/src/math/math.ts index 9671d843..e9964a37 100644 --- a/src/math/math.ts +++ b/src/math/math.ts @@ -125,8 +125,8 @@ export class Vec2 { return Math.abs(this.x) > Math.abs(this.y) ? this.x < 0 ? Vec2.LEFT : Vec2.RIGHT : this.y < 0 - ? Vec2.UP - : Vec2.DOWN; + ? Vec2.UP + : Vec2.DOWN; } /** Clone the vector */ @@ -259,6 +259,12 @@ export class Vec2 { return out; } + /** Scale by the inverse of another vector. or a single number */ + invScale(...args: Vec2Args): Vec2 { + const s = vec2(...args); + return new Vec2(this.x / s.x, this.y / s.y); + } + /** Get distance between another vector */ dist(...args: Vec2Args): number { const p2 = vec2(...args); @@ -1379,7 +1385,7 @@ export class Mat4 { const r = Math.sqrt(this.m[0] * this.m[0] + this.m[1] * this.m[1]); return new Vec2( Math.atan(this.m[0] * this.m[4] + this.m[1] * this.m[5]) - / (r * r), + / (r * r), 0, ); } @@ -1388,7 +1394,7 @@ export class Mat4 { return new Vec2( 0, Math.atan(this.m[0] * this.m[4] + this.m[1] * this.m[5]) - / (s * s), + / (s * s), ); } else { @@ -1927,7 +1933,7 @@ export function testPolygonPoint(poly: Polygon, pt: Vec2): boolean { ((p[i].y > pt.y) != (p[j].y > pt.y)) && (pt.x < (p[j].x - p[i].x) * (pt.y - p[i].y) / (p[j].y - p[i].y) - + p[i].x) + + p[i].x) ) { c = !c; } @@ -1945,7 +1951,7 @@ export function testEllipsePoint(ellipse: Ellipse, pt: Vec2): boolean { const vx = pt.x * c + pt.y * s; const vy = -pt.x * s + pt.y * c; return vx * vx / (ellipse.radiusX * ellipse.radiusX) - + vy * vy / (ellipse.radiusY * ellipse.radiusY) < 1; + + vy * vy / (ellipse.radiusY * ellipse.radiusY) < 1; } export function testEllipseCircle(ellipse: Ellipse, circle: Circle): boolean { @@ -2882,7 +2888,7 @@ export class Ellipse { const vx = point.x * c + point.y * s; const vy = -point.x * s + point.y * c; return vx * vx / (this.radiusX * this.radiusX) - + vy * vy / (this.radiusY * this.radiusY) < 1; + + vy * vy / (this.radiusY * this.radiusY) < 1; } raycast(origin: Vec2, direction: Vec2): RaycastResult { return raycastEllipse(origin, direction, this); @@ -3281,21 +3287,21 @@ export function kochanekBartels( const hx = h( pt2.x, 0.5 * (1 - tension) * (1 + bias) * (1 + continuity) * (pt2.x - pt1.x) - + 0.5 * (1 - tension) * (1 - bias) * (1 - continuity) - * (pt3.x - pt2.x), + + 0.5 * (1 - tension) * (1 - bias) * (1 - continuity) + * (pt3.x - pt2.x), 0.5 * (1 - tension) * (1 + bias) * (1 - continuity) * (pt3.x - pt2.x) - + 0.5 * (1 - tension) * (1 - bias) * (1 + continuity) - * (pt4.x - pt3.x), + + 0.5 * (1 - tension) * (1 - bias) * (1 + continuity) + * (pt4.x - pt3.x), pt3.x, ); const hy = h( pt2.y, 0.5 * (1 - tension) * (1 + bias) * (1 + continuity) * (pt2.y - pt1.y) - + 0.5 * (1 - tension) * (1 - bias) * (1 - continuity) - * (pt3.y - pt2.y), + + 0.5 * (1 - tension) * (1 - bias) * (1 - continuity) + * (pt3.y - pt2.y), 0.5 * (1 - tension) * (1 + bias) * (1 - continuity) * (pt3.y - pt2.y) - + 0.5 * (1 - tension) * (1 - bias) * (1 + continuity) - * (pt4.y - pt3.y), + + 0.5 * (1 - tension) * (1 - bias) * (1 + continuity) + * (pt4.y - pt3.y), pt3.y, ); return (t: number) => { diff --git a/src/types.ts b/src/types.ts index f34dadec..d937c934 100644 --- a/src/types.ts +++ b/src/types.ts @@ -98,9 +98,11 @@ import type { Game, GameObjEventMap, GameObjEventNames, + KeepFlags, LevelOpt, SceneDef, SceneName, + SetParentOpt, TupleWithoutFirst, } from "./game"; import type { LCEvents, System } from "./game/systems"; @@ -5585,6 +5587,15 @@ export interface KAPLAYCtx< * @group Events */ cancel: () => Symbol; + /** + * Flags indicating which transform components to keep. When used, the aspect of the transform will not change visually + * even if the parent transform is different. For example a sprite pointing west, will keep pointing west, even if the + * parent transform applies a rotation with an angle different from 0. This is only applied once, during switching parents. + * + * @since v3000.0 + * @group Game Obj + */ + KeepFlags: typeof KeepFlags; /** * Current KAPLAY library version. * @@ -6009,11 +6020,17 @@ export interface GameObjRaw { */ query(opt: QueryOpt): GameObj[]; /** - * Get the parent game obj, if have any. + * Get or set the parent game obj. * * @since v3000.0 */ parent: GameObj | null; + /** + * Set the parent game obj. + * + * @since v4000.0 + */ + setParent(p: GameObj, opt: SetParentOpt): void; /** * @readonly * Get all children game objects.