Files
GDevelop-Heretek/GDJS/Runtime/CustomRuntimeObject.ts
T
John Smith 48ed4e38d0 chore(maintenance): make fork divergence and static-analysis scope deliberate
Two problems made this fork expensive to maintain: scanner noise nobody
could act on, and divergence from upstream that nothing tracked.

Code scanning reported 3,518 open alerts. 3,449 were fallow findings and
2,399 of those sat in vendored bundles `.fallow.toml` never listed
(jolt-physics.wasm.js 436, box2djs/box2d.js 328, the Firebase SDK 655,
Recast, PeerJS, draco). Listing them plus raising the CRAP thresholds from
the defaults to evidence-based values takes fallow from 3,449 findings to
155, all in maintained code. The remaining 69 were CodeQL: 8 third-party
actions are now pinned to full commit SHAs with `# vN` comments (so
Dependabot keeps updating them) and all 10 workflows declare an explicit
least-privilege `permissions:` block.

SonarCloud's 2,845 open issues are mostly not this fork's code — 2,526 sit
in files byte-identical to upstream. The real defect was configuration:
~200 lines of `sonar.issue.ignore.multicriteria.e1..e88` in
.sonarcloud.properties, plus scripts/sonarcloud-apply-suppressions.sh, were
inert. That key is a Project Settings UI setting and cannot be read from
that file under automatic analysis, so two phases of remediation work
chased config the server never saw. Both are deleted; the exclusions that
do work are kept and extended. `sonar.test.inclusions` no longer declares
production source directories as test inclusions.

Divergence is now bounded and checked. `fork-divergence.json` is a
checked-in allowlist of the 111 modified, 51 fork-only and 7
upstream-missing paths, and `scripts/check-fork-divergence.js` fails CI on
divergence that is not listed or on stale entries. It reads upstream
through the GitHub trees API because 4ian/GDevelop is ~1.7GB and this runs
on every PR. Restoring Extensions/Physics3DBehavior/tests/
Physics3DRuntimeBehavior.spec.js — deleted by an earlier SonarCloud pass to
silence one rule — removes one entry each.

Repairs found along the way: every `// nosonar` marker sat on the line
before the statement SonarCloud flags, where the directive does nothing;
each is now on the exact reported line. Four garbled comment fragments left
in multiplayertools.ts by the same pass are gone. A two-line re-export shim
with no importers (AiGeneration/CustomAIClient.js) is deleted.

`scripts/sonarcloud-drift.js` now writes its outputs to $GITHUB_OUTPUT and
seeds its own baseline, so the weekly drift job's ceiling can actually fire;
it previously ran behind `continue-on-error` against a threshold that
predated the exclusions and was unreachable.

Root documentation collapses to MAINTENANCE.md. The three SonarCloud phase
plans and the Dependabot plan described steps that were proven impossible
and are deleted; AI_GAME_STUDIO_ASSESSMENT.md is a design record and stays.

Verified: newIDE/app 176/176 suites and 1,922 tests pass; GDevelop.js 33/33
suites and 1,177 tests pass; GDJS Karma 1,287 pass with the same 9
pre-existing failures (Firebase network tests) reproduced on a pristine
tree. check-format lists 37 files against the documented 41-file baseline,
so no new violations. Lockfile, drift and divergence guards all exit 0.
2026-09-22 20:01:28 -04:00

1013 lines
31 KiB
TypeScript

/*
* GDevelop JS Platform
* Copyright 2013-2022 Florian Rival ([email protected]). All rights reserved.
* This project is released under the MIT License.
*/
namespace gdjs {
const logger = new gdjs.Logger('CustomRuntimeObject');
/**
* @category Objects > Custom Object
*/
export type ObjectConfiguration = {
content: any;
};
/**
* @category Objects > Custom Object
*/
export type CustomObjectConfiguration = ObjectConfiguration & {
animatable?: SpriteAnimationData[];
variant: string;
childrenContent?: { [objectName: string]: ObjectConfiguration & any }; // nosonar: typescript:S4335 -- the `& any` is intentional: a custom object's children carry arbitrary per-object configuration.
isInnerAreaFollowingParentSize: boolean;
};
/**
* @category Objects > Custom Object
*/
export type CustomObjectNetworkSyncDataType = {
anim?: SpriteAnimatorNetworkSyncData;
ifx: boolean;
ify: boolean;
sx: float;
sy: float;
op: float;
cc?: [float, float];
};
/**
* @category Objects > Custom Object
*/
export type CustomObjectNetworkSyncData = ObjectNetworkSyncData &
CustomObjectNetworkSyncDataType;
/**
* An object that contains other object.
*
* This is the base class for objects generated from EventsBasedObject.
*
* @see gdjs.CustomRuntimeObjectInstanceContainer
* @category Objects > Custom Object
*/
export abstract class CustomRuntimeObject
extends gdjs.RuntimeObject
implements
gdjs.Resizable,
gdjs.Scalable,
gdjs.Flippable,
gdjs.OpacityHandler
{
_renderer:
| gdjs.CustomRuntimeObject2DRenderer
| gdjs.CustomRuntimeObject3DRenderer;
/** It contains the children of this object. */
_instanceContainer: gdjs.CustomRuntimeObjectInstanceContainer;
_isUntransformedHitBoxesDirty: boolean = true;
/** It contains shallow copies of the children hitboxes */
private _untransformedHitBoxes: gdjs.Polygon[] = [];
/** The dimension of this object is calculated from its children AABBs. */
private _unrotatedAABB: AABB = { min: [0, 0], max: [0, 0] };
/**
* The bounds of the object content before any transformation.
* @see gdjs.CustomRuntimeObjectInstanceContainer._initialInnerArea
**/
protected _innerArea: {
min: [float, float, float];
max: [float, float, float];
} | null = null;
/**
* When the parent dimensions change:
* - if `false`, the object is stretch proportionally while children local
* positions stay the same ({@link gdjs.CustomRuntimeObject._innerArea} don't change).
* - if `true`, the children local positions need to be adapted by events
* to follow their parent size.
*/
protected _isInnerAreaFollowingParentSize = false;
private _scaleX: float = 1;
private _scaleY: float = 1;
private _flippedX: boolean = false;
private _flippedY: boolean = false;
private opacity: float = 255;
private _customCenter: FloatPoint | null = null;
private _localTransformation: gdjs.AffineTransformation =
new gdjs.AffineTransformation();
private _localInverseTransformation: gdjs.AffineTransformation =
new gdjs.AffineTransformation();
private _isLocalTransformationDirty: boolean = true;
_type: string;
/**
* @param parent The container the object belongs to
* @param objectData The object data used to initialize the object
*/
constructor(
parent: gdjs.RuntimeInstanceContainer,
objectData: ObjectData & CustomObjectConfiguration,
instanceData: InstanceData | undefined
) {
super(parent, objectData, instanceData);
this._type = objectData.type;
this._instanceContainer = new gdjs.CustomRuntimeObjectInstanceContainer(
parent,
this
);
this._renderer = this._createRender();
this._initializeFromObjectData(objectData);
// The generated code calls onCreated at the constructor end
// and onCreated calls its super implementation at its end.
}
private _initializeFromObjectData(
customObjectData: ObjectData & CustomObjectConfiguration
) {
const eventsBasedObjectData = this._runtimeScene
.getGame()
.getEventsBasedObjectData(customObjectData.type);
if (!eventsBasedObjectData) {
logger.error(
`A CustomRuntimeObject was initialized (or re-initialized) from object data referring to an non existing events based object data with type "${customObjectData.type}".`
);
return;
}
const usedVariantData: EventsBasedObjectVariantData | null =
this.getRuntimeScene()
.getGame()
.getEventsBasedObjectVariantData(
customObjectData.type,
customObjectData.variant
);
if (!usedVariantData) {
// This can't actually happen.
logger.error(
`Unknown variant "${customObjectData.variant}" for object "${customObjectData.type}".`
);
return;
}
this._isInnerAreaFollowingParentSize =
eventsBasedObjectData.isInnerAreaFollowingParentSize;
if (usedVariantData.instances.length > 0) {
if (!this._innerArea) {
this._innerArea = {
min: [0, 0, 0],
max: [0, 0, 0],
};
}
this._innerArea.min[0] = usedVariantData.areaMinX;
this._innerArea.min[1] = usedVariantData.areaMinY;
this._innerArea.min[2] = usedVariantData.areaMinZ;
this._innerArea.max[0] = usedVariantData.areaMaxX;
this._innerArea.max[1] = usedVariantData.areaMaxY;
this._innerArea.max[2] = usedVariantData.areaMaxZ;
}
this._instanceContainer.loadFrom(customObjectData, usedVariantData);
}
protected abstract _createRender():
| gdjs.CustomRuntimeObject2DRenderer
| gdjs.CustomRuntimeObject3DRenderer;
protected abstract _reinitializeRenderer(): void;
override reinitialize(objectData: ObjectData & CustomObjectConfiguration) {
super.reinitialize(objectData);
this._reinitializeContentFromObjectData(objectData);
// When changing the variant, the instance is like a new instance.
// We call again `onCreated` at the end, like done by the constructor
// the first time it's created.
this.onCreated();
}
private _reinitializeContentFromObjectData(
objectData: ObjectData & CustomObjectConfiguration
) {
this._reinitializeRenderer();
this._instanceContainer._unloadContent();
this._initializeFromObjectData(objectData);
}
override updateFromObjectData(
oldObjectData: ObjectData & CustomObjectConfiguration,
newObjectData: ObjectData & CustomObjectConfiguration
): boolean {
const animator = this.getAnimator();
if (animator) {
animator.updateFromObjectData(
oldObjectData.animatable || [],
newObjectData.animatable || []
);
}
if (oldObjectData.variant !== newObjectData.variant) {
const width = this.getWidth();
const height = this.getHeight();
const hasInnerAreaChanged =
oldObjectData.isInnerAreaFollowingParentSize &&
this._instanceContainer._initialInnerArea &&
this._innerArea &&
(this._instanceContainer._initialInnerArea.min[0] !==
this._innerArea.min[0] ||
this._instanceContainer._initialInnerArea.min[1] !==
this._innerArea.min[1] ||
this._instanceContainer._initialInnerArea.max[0] !==
this._innerArea.max[0] ||
this._instanceContainer._initialInnerArea.max[1] !==
this._innerArea.max[1]);
this._reinitializeContentFromObjectData(newObjectData);
// The generated code calls the onCreated super implementation at the end.
this.onCreated();
// Keep the custom size
if (hasInnerAreaChanged) {
this.setWidth(width);
this.setHeight(height);
}
}
return true;
}
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): CustomObjectNetworkSyncData {
const animator = this.getAnimator();
const networkSyncData: CustomObjectNetworkSyncData = {
...super.getNetworkSyncData(syncOptions),
ifx: this.isFlippedX(),
ify: this.isFlippedY(),
sx: this._scaleX,
sy: this._scaleY,
op: this.opacity,
};
if (animator) {
networkSyncData.anim = animator.getNetworkSyncData();
}
if (this._customCenter) {
networkSyncData.cc = this._customCenter;
}
return networkSyncData;
}
updateFromNetworkSyncData(
networkSyncData: CustomObjectNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
) {
super.updateFromNetworkSyncData(networkSyncData, options);
if (networkSyncData.ifx !== undefined) {
this.flipX(networkSyncData.ifx);
}
if (networkSyncData.ify !== undefined) {
this.flipY(networkSyncData.ify);
}
if (networkSyncData.sx !== undefined) {
this.setScaleX(Math.abs(networkSyncData.sx));
}
if (networkSyncData.sy !== undefined) {
this.setScaleY(Math.abs(networkSyncData.sy));
}
if (networkSyncData.op !== undefined) {
this.setOpacity(networkSyncData.op);
}
if (networkSyncData.anim) {
const animator = this.getAnimator();
if (animator) {
animator.updateFromNetworkSyncData(networkSyncData.anim);
}
}
if (networkSyncData.cc) {
this.setRotationCenter(networkSyncData.cc[0], networkSyncData.cc[1]);
}
if (
networkSyncData.ifx !== undefined ||
networkSyncData.ify !== undefined ||
networkSyncData.sx !== undefined ||
networkSyncData.sy !== undefined ||
networkSyncData.anim !== undefined ||
networkSyncData.cc !== undefined
) {
this.onChildrenLocationChanged();
}
}
override extraInitializationFromInitialInstance(
initialInstanceData: InstanceData
) {
const animator = this.getAnimator();
if (initialInstanceData.numberProperties) {
for (
let i = 0, len = initialInstanceData.numberProperties.length;
i < len;
++i
) {
const extraData = initialInstanceData.numberProperties[i];
if (animator && extraData.name === 'animation') {
animator.setAnimationIndex(extraData.value);
}
}
}
if (initialInstanceData.customSize) {
this.setWidth(initialInstanceData.width);
this.setHeight(initialInstanceData.height);
}
this.setOpacity(
initialInstanceData.opacity === undefined
? 255
: initialInstanceData.opacity
);
this.flipX(!!initialInstanceData.flippedX);
this.flipY(!!initialInstanceData.flippedY);
}
override onDeletedFromScene(): void {
// Let subclasses do something before the object is destroyed.
this.onDestroy(this._runtimeScene);
// Let behaviors do something before the object is destroyed.
super.onDeletedFromScene();
// Destroy the children.
this._instanceContainer.onDeletedFromScene(this._runtimeScene);
}
override onDestroyed(): void {
this._instanceContainer._destroy();
super.onDestroyed();
}
override update(parent: gdjs.RuntimeInstanceContainer): void {
this._instanceContainer._updateObjectsPreEvents();
this.doStepPreEvents(this._instanceContainer);
const profiler = this.getRuntimeScene().getProfiler();
if (profiler) {
profiler.begin(this.type);
}
// This is a bit like the "scene" events for custom objects.
this.doStepPostEvents(this._instanceContainer);
if (profiler) {
profiler.end(this.type);
}
}
override stepBehaviorsPostEvents(
instanceContainer: gdjs.RuntimeInstanceContainer
): void {
super.stepBehaviorsPostEvents(instanceContainer);
this._instanceContainer._stepBehaviorsPostEvents();
}
/**
* This method is called when the preview is being hot-reloaded.
*
* Custom objects implement this method with code generated from events.
*/
onHotReloading(parent: gdjs.RuntimeInstanceContainer) {}
// This is only to handle trigger once.
doStepPreEvents(parent: gdjs.RuntimeInstanceContainer) {}
/**
* This method is called each tick after events are done.
*
* Custom objects implement this method with code generated from events.
* @param parent The instanceContainer owning the object
*/
doStepPostEvents(parent: gdjs.RuntimeInstanceContainer) {}
/**
* This method is called when the object is being removed from its parent
* container and is about to be destroyed/reused later.
*
* Custom objects implement this method with code generated from events.
*/
onDestroy(parent: gdjs.RuntimeInstanceContainer) {}
override updatePreRender(parent: gdjs.RuntimeInstanceContainer): void {
this._instanceContainer._updateObjectsPreRender();
this.getRenderer().ensureUpToDate();
}
getRenderer():
| gdjs.CustomRuntimeObject2DRenderer
| gdjs.CustomRuntimeObject3DRenderer {
return this._renderer;
}
getChildrenContainer(): gdjs.RuntimeInstanceContainer {
return this._instanceContainer;
}
onChildrenLocationChanged() {
this._isUntransformedHitBoxesDirty = true;
this.invalidateHitboxes();
this.getRenderer().update();
}
override updateHitBoxes(): void {
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
}
// Update the current hitboxes with the frame custom hit boxes
// and apply transformations.
const localTransformation = this.getLocalTransformation();
for (let i = 0; i < this._untransformedHitBoxes.length; ++i) {
if (i >= this.hitBoxes.length) {
this.hitBoxes.push(new gdjs.Polygon());
}
for (
let j = 0;
j < this._untransformedHitBoxes[i].vertices.length;
++j
) {
if (j >= this.hitBoxes[i].vertices.length) {
this.hitBoxes[i].vertices.push([0, 0]);
}
localTransformation.transform(
this._untransformedHitBoxes[i].vertices[j],
this.hitBoxes[i].vertices[j]
);
}
this.hitBoxes[i].vertices.length =
this._untransformedHitBoxes[i].vertices.length;
}
}
/**
* Merge the hitboxes of the children.
*/
_updateUntransformedHitBoxes() {
this._isUntransformedHitBoxesDirty = false;
this._untransformedHitBoxes.length = 0;
let minX = Number.MAX_VALUE;
let minY = Number.MAX_VALUE;
let maxX = -Number.MAX_VALUE;
let maxY = -Number.MAX_VALUE;
for (const childInstance of this._instanceContainer.getAdhocListOfAllInstances()) {
if (!childInstance.isIncludedInParentCollisionMask()) {
continue;
}
Array.prototype.push.apply(
this._untransformedHitBoxes,
childInstance.getHitBoxes()
);
const childAABB = childInstance.getAABB();
minX = Math.min(minX, childAABB.min[0]);
minY = Math.min(minY, childAABB.min[1]);
maxX = Math.max(maxX, childAABB.max[0]);
maxY = Math.max(maxY, childAABB.max[1]);
}
if (minX === Number.MAX_VALUE) {
// The unscaled size can't be 0 because setWidth and setHeight wouldn't
// have any effect.
minX = 0;
minY = 0;
maxX = 1;
maxY = 1;
}
this._unrotatedAABB.min[0] = minX;
this._unrotatedAABB.min[1] = minY;
this._unrotatedAABB.max[0] = maxX;
this._unrotatedAABB.max[1] = maxY;
while (this.hitBoxes.length < this._untransformedHitBoxes.length) {
this.hitBoxes.push(new gdjs.Polygon());
}
this.hitBoxes.length = this._untransformedHitBoxes.length;
}
// Position:
/**
* Return an array containing the coordinates of the point passed as parameter
* in parent coordinate coordinates (as opposed to the object local coordinates).
*
* All transformations (flipping, scale, rotation) are supported.
*
* @param x The X position of the point, in object coordinates.
* @param y The Y position of the point, in object coordinates.
* @param destination Array that will be updated with the result
* (x and y position of the point in parent coordinates).
*/
applyObjectTransformation(x: float, y: float, destination: FloatPoint) {
const source = destination;
source[0] = x;
source[1] = y;
this.getLocalTransformation().transform(source, destination);
}
/**
* Return the affine transformation that represents
* flipping, scale, rotation and translation of the object.
* @returns the affine transformation.
*/
getLocalTransformation(): gdjs.AffineTransformation {
if (this._isLocalTransformationDirty) {
this._updateLocalTransformation();
}
return this._localTransformation;
}
getLocalInverseTransformation(): gdjs.AffineTransformation {
if (this._isLocalTransformationDirty) {
this._updateLocalTransformation();
}
return this._localInverseTransformation;
}
_updateLocalTransformation() {
const absScaleX = Math.abs(this._scaleX);
const absScaleY = Math.abs(this._scaleY);
const centerX = this.getUnscaledCenterX() * absScaleX;
const centerY = this.getUnscaledCenterY() * absScaleY;
const angleInRadians = (this.angle * Math.PI) / 180;
this._localTransformation.setToTranslation(this.x, this.y);
this._localTransformation.rotateAround(angleInRadians, centerX, centerY);
if (this._flippedX) {
this._localTransformation.flipX(centerX);
}
if (this._flippedY) {
this._localTransformation.flipY(centerY);
}
this._localTransformation.scale(absScaleX, absScaleY);
this._localInverseTransformation.copyFrom(this._localTransformation);
this._localInverseTransformation.invert();
this._isLocalTransformationDirty = false;
}
/**
* Return an array containing the coordinates of the point passed as parameter
* in object local coordinates (as opposed to the parent coordinate coordinates).
*
* All transformations (flipping, scale, rotation) are supported.
*
* @param x The X position of the point, in parent coordinates.
* @param y The Y position of the point, in parent coordinates.
* @param destination Array that will be updated with the result
* (x and y position of the point in object coordinates).
*/
applyObjectInverseTransformation(
x: float,
y: float,
destination: FloatPoint
) {
const source = destination;
source[0] = x;
source[1] = y;
this.getLocalInverseTransformation().transform(source, destination);
}
override getDrawableX(): float {
const minX = this.getUnscaledMinX();
const absScaleX = this.getScaleX();
if (!this._flippedX) {
return this.x + minX * absScaleX;
} else {
return (
this.x +
(-minX - this.getUnscaledWidth() + 2 * this.getUnscaledCenterX()) *
absScaleX
);
}
}
private getUnscaledMinX(): float {
if (this._innerArea) {
return this._innerArea.min[0];
}
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
}
return this._unrotatedAABB.min[0];
}
override getDrawableY(): float {
const minY = this.getUnscaledMinY();
const absScaleY = this.getScaleY();
if (!this._flippedY) {
return this.y + minY * absScaleY;
} else {
return (
this.y +
(-minY - this.getUnscaledHeight() + 2 * this.getUnscaledCenterY()) *
absScaleY
);
}
}
private getUnscaledMinY(): float {
if (this._innerArea) {
return this._innerArea.min[1];
}
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
}
return this._unrotatedAABB.min[1];
}
/**
* @return the internal left bound of the object according to its children.
*/
getInnerAreaMinX(): number {
if (this._innerArea) {
return this._innerArea.min[0];
}
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
}
return this._unrotatedAABB.min[0];
}
/**
* @return the internal top bound of the object according to its children.
*/
getInnerAreaMinY(): number {
if (this._innerArea) {
return this._innerArea.min[1];
}
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
}
return this._unrotatedAABB.min[1];
}
/**
* @return the internal right bound of the object according to its children.
*/
getInnerAreaMaxX(): number {
if (this._innerArea) {
return this._innerArea.max[0];
}
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
}
return this._unrotatedAABB.max[0];
}
/**
* @return the internal bottom bound of the object according to its children.
*/
getInnerAreaMaxY(): number {
if (this._innerArea) {
return this._innerArea.max[1];
}
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
}
return this._unrotatedAABB.max[1];
}
getOriginalWidth(): float {
return (
this._instanceContainer.getInitialUnrotatedViewportMaxX() -
this._instanceContainer.getInitialUnrotatedViewportMinX()
);
}
getOriginalHeight(): float {
return (
this._instanceContainer.getInitialUnrotatedViewportMaxY() -
this._instanceContainer.getInitialUnrotatedViewportMinY()
);
}
/**
* @return the internal width of the object according to its children.
*/
getUnscaledWidth(): float {
if (this._innerArea) {
return this._innerArea.max[0] - this._innerArea.min[0];
}
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
}
return this._unrotatedAABB.max[0] - this._unrotatedAABB.min[0];
}
/**
* @return the internal height of the object according to its children.
*/
getUnscaledHeight(): float {
if (this._innerArea) {
return this._innerArea.max[1] - this._innerArea.min[1];
}
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
}
return this._unrotatedAABB.max[1] - this._unrotatedAABB.min[1];
}
/**
* @returns the center X from the local origin (0;0).
*/
getUnscaledCenterX(): float {
if (this._customCenter) {
return this._customCenter[0];
}
return this.getUnscaledWidth() / 2 + this.getUnscaledMinX();
}
/**
* @returns the center Y from the local origin (0;0).
*/
getUnscaledCenterY(): float {
if (this._customCenter) {
return this._customCenter[1];
}
return this.getUnscaledHeight() / 2 + this.getUnscaledMinY();
}
/**
* The center of rotation is defined relatively to the origin (the object
* position).
* This avoids the center to move when children push the bounds.
*
* When no custom center is defined, it will move
* to stay at the center of the children bounds.
*
* @param x coordinate of the custom center
* @param y coordinate of the custom center
*/
setRotationCenter(x: float, y: float) {
if (!this._customCenter) {
this._customCenter = [0, 0];
}
this._customCenter[0] = x;
this._customCenter[1] = y;
this._isLocalTransformationDirty = true;
this.invalidateHitboxes();
}
hasCustomRotationCenter(): boolean {
return !!this._customCenter;
}
override getCenterX(): float {
return (
(this.getUnscaledCenterX() - this.getUnscaledMinX()) * this.getScaleX()
);
}
override getCenterY(): float {
return (
(this.getUnscaledCenterY() - this.getUnscaledMinY()) * this.getScaleY()
);
}
override getWidth(): float {
return this.getUnscaledWidth() * this.getScaleX();
}
override getHeight(): float {
return this.getUnscaledHeight() * this.getScaleY();
}
override setWidth(newWidth: float): void {
const unscaledWidth = this.getUnscaledWidth();
if (unscaledWidth === 0) {
return;
}
const scaleX = newWidth / unscaledWidth;
if (this._innerArea && this._isInnerAreaFollowingParentSize) {
this._innerArea.min[0] *= scaleX;
this._innerArea.max[0] *= scaleX;
} else {
this.setScaleX(scaleX);
}
}
override setHeight(newHeight: float): void {
const unscaledHeight = this.getUnscaledHeight();
if (unscaledHeight === 0) {
return;
}
const scaleY = newHeight / unscaledHeight;
if (this._innerArea && this._isInnerAreaFollowingParentSize) {
this._innerArea.min[1] *= scaleY;
this._innerArea.max[1] *= scaleY;
} else {
this.setScaleY(scaleY);
}
}
/**
* Change the size of the object.
*
* @param newWidth The new width of the object, in pixels.
* @param newHeight The new height of the object, in pixels.
*/
setSize(newWidth: float, newHeight: float): void {
this.setWidth(newWidth);
this.setHeight(newHeight);
}
override setX(x: float): void {
if (x === this.x) {
return;
}
this.x = x;
this._isLocalTransformationDirty = true;
this.invalidateHitboxes();
this.getRenderer().updateX();
}
override setY(y: float): void {
if (y === this.y) {
return;
}
this.y = y;
this._isLocalTransformationDirty = true;
this.invalidateHitboxes();
this.getRenderer().updateY();
}
override setAngle(angle: float): void {
if (this.angle === angle) {
return;
}
this.angle = angle;
this._isLocalTransformationDirty = true;
this.invalidateHitboxes();
this.getRenderer().updateAngle();
}
/**
* Change the scale on X and Y axis of the object.
*
* @param newScale The new scale (must be greater than 0).
*/
setScale(newScale: float): void {
if (this._innerArea && this._isInnerAreaFollowingParentSize) {
// The scale is always 1;
return;
}
if (newScale < 0) {
newScale = 0;
}
if (
newScale === Math.abs(this._scaleX) &&
newScale === Math.abs(this._scaleY)
) {
return;
}
this._scaleX = newScale * (this._flippedX ? -1 : 1);
this._scaleY = newScale * (this._flippedY ? -1 : 1);
this._isLocalTransformationDirty = true;
this.invalidateHitboxes();
this.getRenderer().update();
}
/**
* Change the scale on X axis of the object (changing its width).
*
* @param newScale The new scale (must be greater than 0).
*/
setScaleX(newScale: float): void {
if (this._innerArea && this._isInnerAreaFollowingParentSize) {
// The scale is always 1;
return;
}
if (newScale < 0) {
newScale = 0;
}
if (newScale === Math.abs(this._scaleX)) {
return;
}
this._scaleX = newScale * (this._flippedX ? -1 : 1);
this._isLocalTransformationDirty = true;
this.invalidateHitboxes();
this.getRenderer().update();
}
/**
* Change the scale on Y axis of the object (changing its height).
*
* @param newScale The new scale (must be greater than 0).
*/
setScaleY(newScale: float): void {
if (this._innerArea && this._isInnerAreaFollowingParentSize) {
// The scale is always 1;
return;
}
if (newScale < 0) {
newScale = 0;
}
if (newScale === Math.abs(this._scaleY)) {
return;
}
this._scaleY = newScale * (this._flippedY ? -1 : 1);
this.invalidateHitboxes();
this.getRenderer().update();
}
/**
* Get the scale of the object (or the arithmetic mean of the X and Y scale in case they are different).
*
* @return the scale of the object (or the arithmetic mean of the X and Y scale in case they are different).
* @deprecated Use `getScale` instead.
*/
getScaleMean(): float {
return (Math.abs(this._scaleX) + Math.abs(this._scaleY)) / 2.0;
}
/**
* Get the scale of the object (or the geometric mean of the X and Y scale in case they are different).
*
* @return the scale of the object (or the geometric mean of the X and Y scale in case they are different).
*/
getScale(): float {
const scaleX = Math.abs(this._scaleX);
const scaleY = Math.abs(this._scaleY);
return scaleX === scaleY ? scaleX : Math.sqrt(scaleX * scaleY);
}
/**
* Get the scale of the object on Y axis.
*
* @return the scale of the object on Y axis
*/
getScaleY(): float {
return Math.abs(this._scaleY);
}
/**
* Get the scale of the object on X axis.
*
* @return the scale of the object on X axis
*/
getScaleX(): float {
return Math.abs(this._scaleX);
}
// Visibility and display :
setOpacity(opacity: float): void {
if (opacity < 0) {
opacity = 0;
}
if (opacity > 255) {
opacity = 255;
}
this.opacity = opacity;
this.getRenderer().updateOpacity();
}
getOpacity(): number {
return this.opacity;
}
override hide(enable: boolean): void {
if (enable === undefined) {
enable = true;
}
this.hidden = enable;
this.getRenderer().updateVisibility();
}
flipX(enable: boolean) {
if (enable !== this._flippedX) {
this._scaleX *= -1;
this._flippedX = enable;
this.invalidateHitboxes();
this.getRenderer().update();
}
}
flipY(enable: boolean) {
if (enable !== this._flippedY) {
this._scaleY *= -1;
this._flippedY = enable;
this.invalidateHitboxes();
this.getRenderer().update();
}
}
isFlippedX(): boolean {
return this._flippedX;
}
isFlippedY(): boolean {
return this._flippedY;
}
/**
* Return the sprite animator.
*
* It returns `null` when custom objects don't have the Animatable capability.
*/
getAnimator(): gdjs.SpriteAnimator<any> | null {
return null;
}
}
// Others initialization and internal state management :
// TODO EBO Activate and test instance recycling.
CustomRuntimeObject.supportsReinitialization = false;
}