mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
2636 lines
83 KiB
TypeScript
2636 lines
83 KiB
TypeScript
/*
|
|
* GDevelop JS Platform
|
|
* Copyright 2013-2016 Florian Rival (Florian.Rival@gmail.com). All rights reserved.
|
|
* This project is released under the MIT License.
|
|
*/
|
|
namespace gdjs {
|
|
/** An axis-aligned bounding box. Used to represents a box around an object for example. */
|
|
export type AABB = {
|
|
/** The [x,y] coordinates of the top left point */
|
|
min: FloatPoint;
|
|
/** The [x,y] coordinates of the bottom right point */
|
|
max: FloatPoint;
|
|
};
|
|
|
|
export type RendererObjectInterface = {
|
|
visible: boolean;
|
|
};
|
|
|
|
/**
|
|
* Return the squared bounding radius of an object given its width/height and its center of rotation
|
|
* (relative to the top-left of the object). The radius is relative to the center of rotation.
|
|
*/
|
|
const computeSqBoundingRadius = (
|
|
width: float,
|
|
height: float,
|
|
centerX: float,
|
|
centerY: float
|
|
) => {
|
|
const radiusX = Math.max(centerX, width - centerX);
|
|
const radiusY = Math.max(centerY, height - centerY);
|
|
return Math.pow(radiusX, 2) + Math.pow(radiusY, 2);
|
|
};
|
|
|
|
/**
|
|
* Arrays and data structure that are (re)used by
|
|
* {@link RuntimeObject.separateFromObjects} to avoid any allocation.
|
|
*/
|
|
const separateFromObjectsStatics: {
|
|
moveXArray: Array<float>;
|
|
moveYArray: Array<float>;
|
|
} = {
|
|
moveXArray: [],
|
|
moveYArray: [],
|
|
};
|
|
|
|
/**
|
|
* Data structure that are (re)used by
|
|
* {@link RuntimeObject.raycastTest} to avoid any allocation.
|
|
*/
|
|
const raycastTestStatics: {
|
|
result: RaycastTestResult;
|
|
} = {
|
|
result: gdjs.Polygon.makeNewRaycastTestResult(),
|
|
};
|
|
|
|
/**
|
|
* Move the object using the results from collisionTest call.
|
|
* This moves the object according to the direction of the longest vector,
|
|
* and projects the others on the orthogonal vector.
|
|
*
|
|
* See {@link RuntimeObject.separateFromObjects}
|
|
*
|
|
* @param object The object to move.
|
|
* @param moveXArray The X coordinates of the vectors to move the object.
|
|
* @param moveYArray The Y coordinates of the vectors to move the object.
|
|
* @return true if the object was moved.
|
|
*/
|
|
const moveFollowingSeparatingVectors = (
|
|
object: gdjs.RuntimeObject,
|
|
moveXArray: Array<float>,
|
|
moveYArray: Array<float>
|
|
): boolean => {
|
|
if (moveXArray.length === 0) {
|
|
moveXArray.length = 0;
|
|
moveYArray.length = 0;
|
|
return false;
|
|
}
|
|
if (moveXArray.length === 1) {
|
|
// Move according to the results returned by the collision algorithm.
|
|
object.setPosition(
|
|
object.getX() + moveXArray[0],
|
|
object.getY() + moveYArray[0]
|
|
);
|
|
moveXArray.length = 0;
|
|
moveYArray.length = 0;
|
|
return true;
|
|
}
|
|
|
|
// Find the longest vector
|
|
let squaredDistanceMax = 0;
|
|
let distanceMaxIndex = 0;
|
|
for (let index = 0; index < moveXArray.length; index++) {
|
|
const moveX = moveXArray[index];
|
|
const moveY = moveYArray[index];
|
|
|
|
const squaredDistance = moveX * moveX + moveY * moveY;
|
|
if (squaredDistance > squaredDistanceMax) {
|
|
squaredDistanceMax = squaredDistance;
|
|
distanceMaxIndex = index;
|
|
}
|
|
}
|
|
|
|
const distanceMax = Math.sqrt(squaredDistanceMax);
|
|
// unit vector of the longest vector
|
|
const uX = moveXArray[distanceMaxIndex] / distanceMax;
|
|
const uY = moveYArray[distanceMaxIndex] / distanceMax;
|
|
|
|
// normal vector of the longest vector
|
|
const vX = -uY;
|
|
const vY = uX;
|
|
|
|
// Project other vectors on the normal
|
|
let scalarProductMin = 0;
|
|
let scalarProductMax = 0;
|
|
for (let index = 0; index < moveXArray.length; index++) {
|
|
const moveX = moveXArray[index];
|
|
const moveY = moveYArray[index];
|
|
|
|
const scalarProduct = moveX * vX + moveY * vY;
|
|
scalarProductMin = Math.min(scalarProductMin, scalarProduct);
|
|
scalarProductMax = Math.max(scalarProductMax, scalarProduct);
|
|
}
|
|
|
|
// Apply the longest vector
|
|
let deltaX = moveXArray[distanceMaxIndex];
|
|
let deltaY = moveYArray[distanceMaxIndex];
|
|
|
|
// Apply the longest projected vector if they all are in the same direction
|
|
// Some projections could have rounding errors,
|
|
// they are considered negligible under a 1 for 1,000,000 ratio.
|
|
const scalarProductMinIsNegligible =
|
|
-scalarProductMin < scalarProductMax / 1048576;
|
|
const scalarProductMaxIsNegligible =
|
|
scalarProductMax < -scalarProductMin / 1048576;
|
|
if (scalarProductMinIsNegligible !== scalarProductMaxIsNegligible) {
|
|
if (scalarProductMaxIsNegligible) {
|
|
deltaX += scalarProductMin * vX;
|
|
deltaY += scalarProductMin * vY;
|
|
} else {
|
|
deltaX += scalarProductMax * vX;
|
|
deltaY += scalarProductMax * vY;
|
|
}
|
|
}
|
|
object.setPosition(object.getX() + deltaX, object.getY() + deltaY);
|
|
moveXArray.length = 0;
|
|
moveYArray.length = 0;
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* RuntimeObject represents an object being used on a RuntimeScene.
|
|
*
|
|
* A `gdjs.RuntimeObject` should not be instantiated directly, always a child class
|
|
* (because gdjs.RuntimeObject don't call onCreated at the end of its constructor).
|
|
*/
|
|
export class RuntimeObject
|
|
implements EffectsTarget, gdjs.Resizable, gdjs.Scalable, gdjs.Flippable {
|
|
name: string;
|
|
type: string;
|
|
x: float = 0;
|
|
y: float = 0;
|
|
angle: float = 0;
|
|
zOrder: integer = 0;
|
|
hidden: boolean = false;
|
|
layer: string = '';
|
|
protected _nameId: integer;
|
|
protected _livingOnScene: boolean = true;
|
|
|
|
readonly id: integer;
|
|
private destroyCallbacks = new Set<() => void>();
|
|
_runtimeScene: gdjs.RuntimeInstanceContainer;
|
|
|
|
/**
|
|
* An optional UUID associated to the object to be used
|
|
* for hot reload. Don't modify or use otherwise.
|
|
*/
|
|
persistentUuid: string | null = null;
|
|
|
|
/**
|
|
* A property to be used by external algorithms to indicate if the
|
|
* object is picked or not in an object selection. By construction, this is
|
|
* not "thread safe" or "re-entrant algorithm" safe.
|
|
*/
|
|
pick: boolean = false;
|
|
|
|
//Hit boxes:
|
|
protected _defaultHitBoxes: gdjs.Polygon[] = [];
|
|
protected hitBoxes: gdjs.Polygon[];
|
|
protected hitBoxesDirty: boolean = true;
|
|
protected aabb: AABB = { min: [0, 0], max: [0, 0] };
|
|
protected _isIncludedInParentCollisionMask = true;
|
|
|
|
//Variables:
|
|
protected _variables: gdjs.VariablesContainer;
|
|
|
|
//Effects:
|
|
protected _rendererEffects: Record<
|
|
string,
|
|
gdjs.PixiFiltersTools.Filter
|
|
> = {};
|
|
|
|
//Forces:
|
|
protected _forces: gdjs.Force[] = [];
|
|
_averageForce: gdjs.Force;
|
|
|
|
/**
|
|
* Contains the behaviors of the object.
|
|
*/
|
|
protected _behaviors: gdjs.RuntimeBehavior[] = [];
|
|
protected _behaviorsTable: Hashtable<gdjs.RuntimeBehavior>;
|
|
protected _timers: Hashtable<gdjs.Timer>;
|
|
|
|
/**
|
|
* @param runtimeScene The scene the object belongs to..
|
|
* @param objectData The initial properties of the object.
|
|
*/
|
|
constructor(
|
|
instanceContainer: gdjs.RuntimeInstanceContainer,
|
|
objectData: ObjectData & any
|
|
) {
|
|
this.name = objectData.name || '';
|
|
this.type = objectData.type || '';
|
|
this._nameId = RuntimeObject.getNameIdentifier(this.name);
|
|
this.id = instanceContainer.getScene().createNewUniqueId();
|
|
this._runtimeScene = instanceContainer;
|
|
this._defaultHitBoxes.push(gdjs.Polygon.createRectangle(0, 0));
|
|
this.hitBoxes = this._defaultHitBoxes;
|
|
this._variables = new gdjs.VariablesContainer(
|
|
objectData ? objectData.variables : undefined
|
|
);
|
|
this._averageForce = new gdjs.Force(0, 0, 0);
|
|
this._behaviorsTable = new Hashtable();
|
|
for (let i = 0; i < objectData.effects.length; ++i) {
|
|
this._runtimeScene
|
|
.getGame()
|
|
.getEffectsManager()
|
|
.initializeEffect(objectData.effects[i], this._rendererEffects, this);
|
|
this.updateAllEffectParameters(objectData.effects[i]);
|
|
}
|
|
|
|
//Also contains the behaviors: Used when a behavior is accessed by its name ( see getBehavior ).
|
|
for (let i = 0, len = objectData.behaviors.length; i < len; ++i) {
|
|
const autoData = objectData.behaviors[i];
|
|
const Ctor = gdjs.getBehaviorConstructor(autoData.type);
|
|
this._behaviors.push(new Ctor(instanceContainer, autoData, this));
|
|
this._behaviorsTable.put(autoData.name, this._behaviors[i]);
|
|
}
|
|
this._timers = new Hashtable();
|
|
}
|
|
|
|
//Common members functions related to the object and its runtimeScene :
|
|
/**
|
|
* To be called by the child classes in their constructor, at the very end.
|
|
* Notify the behaviors that they have been constructed (this must be done when
|
|
* the object is ready, otherwise behaviors can do operations on the object which
|
|
* could be not initialized yet).
|
|
*
|
|
* If you redefine this function, **make sure to call the original method**
|
|
* (`RuntimeObject.prototype.onCreated.call(this);`).
|
|
*/
|
|
onCreated(): void {
|
|
const rendererObject = this.getRendererObject();
|
|
if (rendererObject) {
|
|
for (const effectName in this._rendererEffects) {
|
|
this._rendererEffects[effectName].applyEffect(this);
|
|
}
|
|
}
|
|
|
|
for (let i = 0; i < this._behaviors.length; ++i) {
|
|
this._behaviors[i].onCreated();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called to reset the object to its default state. This is used for objects that are
|
|
* "recycled": they are dismissed (at which point `onDestroyFromScene` is called) but still
|
|
* stored in a cache to be reused next time an object must be created. At this point,
|
|
* `reinitialize` will be called. The object must then work as if it was a newly constructed
|
|
* object.
|
|
*
|
|
* To implement this in your object:
|
|
* * Set `gdjs.YourRuntimeObject.supportsReinitialization = true;` to declare support for recycling.
|
|
* * Implement `reinitialize`. It **must** call the `reinitialize` of `gdjs.RuntimeObject`, and call `this.onCreated();`
|
|
* at the end of `reinitialize`.
|
|
* * It must reset the object as if it was newly constructed (be careful about your renderers and any global state).
|
|
* * The `_runtimeScene`, `_nameId`, `name` and `type` are guaranteed to stay the same and do not
|
|
* need to be set again.
|
|
*
|
|
*/
|
|
reinitialize(objectData: ObjectData): void {
|
|
const runtimeScene = this._runtimeScene;
|
|
this.x = 0;
|
|
this.y = 0;
|
|
this.angle = 0;
|
|
this.zOrder = 0;
|
|
this.hidden = false;
|
|
this.layer = '';
|
|
this._livingOnScene = true;
|
|
//@ts-ignore Reinitialize is like a constructor, it can overwrite the readonly property.
|
|
this.id = runtimeScene.createNewUniqueId();
|
|
this.persistentUuid = null;
|
|
this.pick = false;
|
|
this.hitBoxesDirty = true;
|
|
this._defaultHitBoxes.length = 0;
|
|
this._defaultHitBoxes.push(gdjs.Polygon.createRectangle(0, 0));
|
|
this.aabb.min[0] = 0;
|
|
this.aabb.min[1] = 0;
|
|
this.aabb.max[0] = 0;
|
|
this.aabb.max[1] = 0;
|
|
this._variables = new gdjs.VariablesContainer(objectData.variables);
|
|
this.clearForces();
|
|
|
|
// Reinitialize behaviors.
|
|
this._behaviorsTable.clear();
|
|
let i = 0;
|
|
for (const len = objectData.behaviors.length; i < len; ++i) {
|
|
const behaviorData = objectData.behaviors[i];
|
|
const Ctor = gdjs.getBehaviorConstructor(behaviorData.type);
|
|
if (i < this._behaviors.length) {
|
|
// TODO: Add support for behavior recycling with a `reinitialize` method.
|
|
this._behaviors[i] = new Ctor(runtimeScene, behaviorData, this);
|
|
} else {
|
|
this._behaviors.push(new Ctor(runtimeScene, behaviorData, this));
|
|
}
|
|
this._behaviorsTable.put(behaviorData.name, this._behaviors[i]);
|
|
}
|
|
this._behaviors.length = i;
|
|
|
|
// Reinitialize effects.
|
|
for (let i = 0; i < objectData.effects.length; ++i) {
|
|
this._runtimeScene
|
|
.getGame()
|
|
.getEffectsManager()
|
|
.initializeEffect(objectData.effects[i], this._rendererEffects, this);
|
|
this.updateAllEffectParameters(objectData.effects[i]);
|
|
}
|
|
|
|
// Make sure to delete existing timers.
|
|
this._timers.clear();
|
|
|
|
this.destroyCallbacks.clear();
|
|
|
|
this.invalidateHitboxes();
|
|
}
|
|
|
|
static supportsReinitialization = false;
|
|
|
|
/**
|
|
* Return the time elapsed since the last frame,
|
|
* in milliseconds, for the object.
|
|
*
|
|
* Objects can have different elapsed time if they are on layers with different time scales.
|
|
*
|
|
* @param instanceContainer The instance container the object belongs to (deprecated - can be omitted).
|
|
*/
|
|
getElapsedTime(instanceContainer?: gdjs.RuntimeInstanceContainer): float {
|
|
const theLayer = this._runtimeScene.getLayer(this.layer);
|
|
return theLayer.getElapsedTime();
|
|
}
|
|
|
|
/**
|
|
* The gdjs.RuntimeScene the object belongs to.
|
|
*/
|
|
getParent(): gdjs.RuntimeInstanceContainer {
|
|
return this._runtimeScene;
|
|
}
|
|
|
|
/**
|
|
* The gdjs.RuntimeScene the object belongs to.
|
|
*/
|
|
getRuntimeScene(): gdjs.RuntimeScene {
|
|
return this._runtimeScene.getScene();
|
|
}
|
|
|
|
/**
|
|
* The container the object belongs to.
|
|
*/
|
|
getInstanceContainer(): gdjs.RuntimeInstanceContainer {
|
|
return this._runtimeScene;
|
|
}
|
|
|
|
/**
|
|
* Called once during the game loop, before events and rendering.
|
|
* @param instanceContainer The container the object belongs to.
|
|
*/
|
|
update(instanceContainer: gdjs.RuntimeInstanceContainer): void {}
|
|
|
|
/**
|
|
* Called once during the game loop, after events and before rendering.
|
|
* @param instanceContainer The container the object belongs to.
|
|
*/
|
|
updatePreRender(instanceContainer: gdjs.RuntimeInstanceContainer): void {}
|
|
|
|
/**
|
|
* Called when the object is created from an initial instance at the startup of the scene.<br>
|
|
* Note that common properties (position, angle, z order...) have already been setup.
|
|
*
|
|
* @param initialInstanceData The data of the initial instance.
|
|
*/
|
|
extraInitializationFromInitialInstance(
|
|
initialInstanceData: InstanceData
|
|
): void {}
|
|
|
|
/**
|
|
* Called when the object must be updated using the specified objectData. This is the
|
|
* case during hot-reload, and is only called if the object was modified.
|
|
*
|
|
* @param oldObjectData The previous data for the object.
|
|
* @param newObjectData The new data for the object.
|
|
* @returns true if the object was updated, false if it could not (i.e: hot-reload is not supported).
|
|
*/
|
|
updateFromObjectData(
|
|
oldObjectData: ObjectData,
|
|
newObjectData: ObjectData
|
|
): boolean {
|
|
// If not redefined, mark by default the hot-reload as failed.
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Remove an object from a scene.
|
|
*
|
|
* Do not change/redefine this method. Instead, redefine the onDestroyFromScene method.
|
|
* @param instanceContainer The container owning the object.
|
|
*/
|
|
deleteFromScene(instanceContainer: gdjs.RuntimeInstanceContainer): void {
|
|
if (this._livingOnScene) {
|
|
instanceContainer.markObjectForDeletion(this);
|
|
this._livingOnScene = false;
|
|
}
|
|
}
|
|
|
|
registerDestroyCallback(callback: () => void) {
|
|
this.destroyCallbacks.add(callback);
|
|
}
|
|
|
|
unregisterDestroyCallback(callback: () => void) {
|
|
this.destroyCallbacks.delete(callback);
|
|
}
|
|
|
|
/**
|
|
* Called when the object is destroyed (because it is removed from a scene or the scene
|
|
* is being unloaded). If you redefine this function, **make sure to call the original method**
|
|
* (`RuntimeObject.prototype.onDestroyFromScene.call(this, runtimeScene);`).
|
|
*
|
|
* @param instanceContainer The container owning the object.
|
|
*/
|
|
onDestroyFromScene(instanceContainer: gdjs.RuntimeInstanceContainer): void {
|
|
const theLayer = instanceContainer.getLayer(this.layer);
|
|
const rendererObject = this.getRendererObject();
|
|
if (rendererObject) {
|
|
theLayer.getRenderer().removeRendererObject(rendererObject);
|
|
}
|
|
const rendererObject3D = this.get3DRendererObject();
|
|
if (rendererObject3D) {
|
|
theLayer.getRenderer().remove3DRendererObject(rendererObject3D);
|
|
}
|
|
for (let j = 0, lenj = this._behaviors.length; j < lenj; ++j) {
|
|
this._behaviors[j].onDestroy();
|
|
}
|
|
this.destroyCallbacks.forEach((c) => c());
|
|
this.clearEffects();
|
|
}
|
|
|
|
/**
|
|
* Called whenever the scene owning the object is paused.
|
|
* This should *not* impact objects, but some may need to inform their renderer.
|
|
*
|
|
* @param runtimeScene The scene owning the object.
|
|
*/
|
|
onScenePaused(runtimeScene: gdjs.RuntimeScene): void {}
|
|
|
|
/**
|
|
* Called whenever the scene owning the object is resumed after a pause.
|
|
* This should *not* impact objects, but some may need to inform their renderer.
|
|
*
|
|
* @param runtimeScene The scene owning the object.
|
|
*/
|
|
onSceneResumed(runtimeScene: gdjs.RuntimeScene): void {}
|
|
|
|
//Rendering:
|
|
/**
|
|
* @return The internal object for a 2D rendering (PIXI.DisplayObject...)
|
|
*/
|
|
getRendererObject(): RendererObjectInterface | null | undefined {
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* @return The internal object for a 3D rendering (PIXI.DisplayObject...)
|
|
*/
|
|
get3DRendererObject(): THREE.Object3D | null | undefined {
|
|
return undefined;
|
|
}
|
|
|
|
//Common properties:
|
|
/**
|
|
* Get the name of the object.
|
|
* @return The object's name.
|
|
*/
|
|
getName(): string {
|
|
return this.name;
|
|
}
|
|
|
|
/**
|
|
* Get the name identifier of the object.
|
|
* @return The object's name identifier.
|
|
*/
|
|
getNameId(): integer {
|
|
return this._nameId;
|
|
}
|
|
|
|
/**
|
|
* Get the unique identifier of the object.<br>
|
|
* The identifier is set by the runtimeScene owning the object.<br>
|
|
* You can also use the id property (this._object.id) for increased efficiency instead of
|
|
* calling this method.
|
|
*
|
|
* @return The object identifier
|
|
*/
|
|
getUniqueId(): integer {
|
|
return this.id;
|
|
}
|
|
|
|
/**
|
|
* Set the position of the object.
|
|
*
|
|
* @param x The new X position
|
|
* @param y The new Y position
|
|
*/
|
|
setPosition(x: float, y: float): void {
|
|
this.setX(x);
|
|
this.setY(y);
|
|
}
|
|
|
|
/**
|
|
* Set the X position of the object.
|
|
*
|
|
* @param x The new X position
|
|
*/
|
|
setX(x: float): void {
|
|
if (x === this.x) {
|
|
return;
|
|
}
|
|
this.x = x;
|
|
this.invalidateHitboxes();
|
|
}
|
|
|
|
/**
|
|
* Send a signal that the object hitboxes are no longer up to date.
|
|
*
|
|
* The signal is propagated to parents so
|
|
* {@link gdjs.RuntimeObject.hitBoxesDirty} should never be modified
|
|
* directly.
|
|
*/
|
|
invalidateHitboxes(): void {
|
|
// TODO EBO Check that no community extension set hitBoxesDirty to true
|
|
// directly.
|
|
this.hitBoxesDirty = true;
|
|
this._runtimeScene.onChildrenLocationChanged();
|
|
}
|
|
|
|
/**
|
|
* Get the X position of the object.
|
|
*
|
|
* @return The X position of the object
|
|
*/
|
|
getX(): float {
|
|
return this.x;
|
|
}
|
|
|
|
/**
|
|
* Set the Y position of the object.
|
|
*
|
|
* @param y The new Y position
|
|
*/
|
|
setY(y: float): void {
|
|
if (y === this.y) {
|
|
return;
|
|
}
|
|
this.y = y;
|
|
this.invalidateHitboxes();
|
|
}
|
|
|
|
/**
|
|
* Get the Y position of the object.
|
|
*
|
|
* @return The Y position of the object
|
|
*/
|
|
getY(): float {
|
|
return this.y;
|
|
}
|
|
|
|
/**
|
|
* Get the X position of the rendered object.
|
|
*
|
|
* For most objects, this will returns the same value as getX(). But if the object
|
|
* has an origin that is not the same as the point (0,0) of the object displayed,
|
|
* getDrawableX will differ.
|
|
*
|
|
* @return The X position of the rendered object.
|
|
*/
|
|
getDrawableX(): float {
|
|
return this.getX();
|
|
}
|
|
|
|
/**
|
|
* Get the Y position of the rendered object.
|
|
*
|
|
* For most objects, this will returns the same value as getY(). But if the object
|
|
* has an origin that is not the same as the point (0,0) of the object displayed,
|
|
* getDrawableY will differ.
|
|
*
|
|
* @return The Y position of the rendered object.
|
|
*/
|
|
getDrawableY(): float {
|
|
return this.getY();
|
|
}
|
|
|
|
rotateTowardPosition(
|
|
x: float,
|
|
y: float,
|
|
speed: float,
|
|
scene: gdjs.RuntimeScene
|
|
): void {
|
|
this.rotateTowardAngle(
|
|
gdjs.toDegrees(
|
|
Math.atan2(
|
|
y - (this.getDrawableY() + this.getCenterY()),
|
|
x - (this.getDrawableX() + this.getCenterX())
|
|
)
|
|
),
|
|
speed,
|
|
scene
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param angle The targeted direction angle.
|
|
* @param speed The rotation speed.
|
|
* @param instanceContainer The container the object belongs to (deprecated - can be omitted).
|
|
*/
|
|
rotateTowardAngle(
|
|
angle: float,
|
|
speed: float,
|
|
instanceContainer?: gdjs.RuntimeInstanceContainer
|
|
): void {
|
|
if (speed === 0) {
|
|
this.setAngle(angle);
|
|
return;
|
|
}
|
|
const angularDiff = gdjs.evtTools.common.angleDifference(
|
|
this.getAngle(),
|
|
angle
|
|
);
|
|
const diffWasPositive = angularDiff >= 0;
|
|
let newAngle =
|
|
this.getAngle() +
|
|
((diffWasPositive ? -1.0 : 1.0) * speed * this.getElapsedTime()) / 1000;
|
|
|
|
if (
|
|
// @ts-ignore
|
|
(gdjs.evtTools.common.angleDifference(newAngle, angle) > 0) ^
|
|
diffWasPositive
|
|
) {
|
|
newAngle = angle;
|
|
}
|
|
this.setAngle(newAngle);
|
|
if (
|
|
//Objects like sprite in 8 directions does not handle small increments...
|
|
this.getAngle() !== newAngle
|
|
) {
|
|
this.setAngle(
|
|
//...so force them to be in the path angle anyway.
|
|
angle
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Rotate the object at the given speed
|
|
*
|
|
* @param speed The speed, in degrees per second.
|
|
* @param instanceContainer The container the object belongs to (deprecated - can be omitted).
|
|
*/
|
|
rotate(
|
|
speed: float,
|
|
instanceContainer?: gdjs.RuntimeInstanceContainer
|
|
): void {
|
|
this.setAngle(this.getAngle() + (speed * this.getElapsedTime()) / 1000);
|
|
}
|
|
|
|
/**
|
|
* Set the angle of the object.
|
|
*
|
|
* @param angle The new angle of the object
|
|
*/
|
|
setAngle(angle: float): void {
|
|
if (this.angle === angle) {
|
|
return;
|
|
}
|
|
this.angle = angle;
|
|
this.invalidateHitboxes();
|
|
}
|
|
|
|
/**
|
|
* Get the rotation of the object.
|
|
*
|
|
* @return The rotation of the object, in degrees.
|
|
*/
|
|
getAngle(): float {
|
|
return this.angle;
|
|
}
|
|
|
|
/**
|
|
* Set the layer of the object.
|
|
*
|
|
* @param layer The new layer of the object
|
|
*/
|
|
setLayer(layer: string): void {
|
|
if (layer === this.layer) {
|
|
return;
|
|
}
|
|
const oldLayer = this._runtimeScene.getLayer(this.layer);
|
|
this.layer = layer;
|
|
const newLayer = this._runtimeScene.getLayer(this.layer);
|
|
const rendererObject = this.getRendererObject();
|
|
if (rendererObject) {
|
|
oldLayer.getRenderer().removeRendererObject(rendererObject);
|
|
newLayer.getRenderer().addRendererObject(rendererObject, this.zOrder);
|
|
}
|
|
const rendererObject3D = this.get3DRendererObject();
|
|
if (rendererObject3D) {
|
|
oldLayer.getRenderer().remove3DRendererObject(rendererObject3D);
|
|
newLayer.getRenderer().add3DRendererObject(rendererObject3D);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the layer of the object.
|
|
*
|
|
* @return The layer of the object
|
|
*/
|
|
getLayer(): string {
|
|
return this.layer;
|
|
}
|
|
|
|
/**
|
|
* Return true if the object is on the specified layer
|
|
*
|
|
* @param layer The layer to be tested.
|
|
* @return true if the object is on the specified layer
|
|
*/
|
|
isOnLayer(layer: string): boolean {
|
|
return this.layer === layer;
|
|
}
|
|
|
|
/**
|
|
* Set the Z order of the object.
|
|
*
|
|
* @param z The new Z order position of the object
|
|
*/
|
|
setZOrder(z: integer): void {
|
|
if (z === this.zOrder) {
|
|
return;
|
|
}
|
|
this.zOrder = z;
|
|
const rendererObject = this.getRendererObject();
|
|
if (rendererObject) {
|
|
const theLayer = this._runtimeScene.getLayer(this.layer);
|
|
theLayer.getRenderer().changeRendererObjectZOrder(rendererObject, z);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the Z order of the object.
|
|
*
|
|
* @return The Z order of the object
|
|
*/
|
|
getZOrder(): float {
|
|
return this.zOrder;
|
|
}
|
|
|
|
/**
|
|
* Get the container of the object variables
|
|
* @return The variables of the object
|
|
*/
|
|
getVariables(): gdjs.VariablesContainer {
|
|
return this._variables;
|
|
}
|
|
|
|
/**
|
|
* Get the value of a variable considered as a number. Equivalent of variable.getAsNumber()
|
|
* @param variable The variable to be accessed
|
|
* @return The value of the specified variable
|
|
* @static
|
|
*/
|
|
static getVariableNumber(variable: gdjs.Variable): number {
|
|
return variable.getAsNumber();
|
|
}
|
|
|
|
/**
|
|
* Return the variable passed as argument without any change.
|
|
* Only for usage by events.
|
|
*
|
|
* @param variable The variable to be accessed
|
|
* @return The specified variable
|
|
* @static
|
|
*/
|
|
static returnVariable(variable: gdjs.Variable): gdjs.Variable {
|
|
return variable;
|
|
}
|
|
|
|
/**
|
|
* Get the value of a variable considered as a string. Equivalent of variable.getAsString()
|
|
* @param variable The variable to be accessed
|
|
* @return The string of the specified variable
|
|
* @static
|
|
*/
|
|
static getVariableString(variable: gdjs.Variable): string {
|
|
return variable.getAsString();
|
|
}
|
|
|
|
/**
|
|
* Shortcut to set the value of a variable considered as a boolean.
|
|
* This shortcut function is needed for events code generation.
|
|
*
|
|
* @private
|
|
* @param {gdjs.Variable} variable
|
|
* @param {boolean} newValue
|
|
*/
|
|
static setVariableBoolean = function (
|
|
variable: gdjs.Variable,
|
|
newValue: boolean
|
|
) {
|
|
variable.setBoolean(newValue);
|
|
};
|
|
|
|
/**
|
|
* Shortcut to compare the value of a variable considered as a boolean.
|
|
* This shortcut function is needed for events code generation.
|
|
*
|
|
* @private
|
|
* @param {gdjs.Variable} variable
|
|
* @param {boolean} compareWith
|
|
* @returns {boolean}
|
|
*/
|
|
static getVariableBoolean = function (
|
|
variable: gdjs.Variable,
|
|
compareWith: boolean
|
|
): boolean {
|
|
return gdjs.evtTools.common.getVariableBoolean(variable, compareWith);
|
|
};
|
|
|
|
/**
|
|
* Toggles a variable.
|
|
* This shortcut function is needed for events code generation.
|
|
*
|
|
* @private
|
|
* @param {gdjs.Variable} variable
|
|
* @see {gdjs.evtTools.common.toggleVariableBoolean}
|
|
*/
|
|
static toggleVariableBoolean = function (variable: gdjs.Variable) {
|
|
gdjs.evtTools.common.toggleVariableBoolean(variable);
|
|
};
|
|
|
|
/**
|
|
* Get the number of children from a variable
|
|
* @param variable The variable to be accessed
|
|
* @return The number of children
|
|
* @static
|
|
*/
|
|
static getVariableChildCount(variable: gdjs.Variable): integer {
|
|
return variable.getChildrenCount();
|
|
}
|
|
|
|
/**
|
|
* Shortcut to set the value of a variable considered as a number
|
|
* @param variable The variable to be changed
|
|
* @param newValue The value to be set
|
|
*/
|
|
static setVariableNumber(variable: gdjs.Variable, newValue: float): void {
|
|
variable.setNumber(newValue);
|
|
}
|
|
|
|
/**
|
|
* Shortcut to set the value of a variable considered as a string
|
|
* @param variable The variable to be changed
|
|
* @param newValue {String} The value to be set
|
|
*/
|
|
static setVariableString(variable: gdjs.Variable, newValue: string) {
|
|
variable.setString(newValue);
|
|
}
|
|
|
|
/**
|
|
* @static
|
|
* @param variable The variable to be tested
|
|
* @param childName The name of the child
|
|
*/
|
|
private static variableChildExists(
|
|
variable: gdjs.Variable,
|
|
childName: string
|
|
): boolean {
|
|
return variable.hasChild(childName);
|
|
}
|
|
|
|
/**
|
|
* @static
|
|
* @param variable The variable to be changed
|
|
* @param childName The name of the child
|
|
*/
|
|
private static variableRemoveChild(
|
|
variable: gdjs.Variable,
|
|
childName: string
|
|
): void {
|
|
variable.removeChild(childName);
|
|
}
|
|
|
|
/**
|
|
* @static
|
|
* @param variable The variable to be cleared
|
|
*/
|
|
private static variableClearChildren(variable: gdjs.Variable): void {
|
|
variable.clearChildren();
|
|
}
|
|
|
|
/**
|
|
* This shortcut function is needed for events code generation.
|
|
* @private
|
|
*/
|
|
static variablePushCopy = function (
|
|
array: gdjs.Variable,
|
|
variable: gdjs.Variable
|
|
) {
|
|
array.pushVariableCopy(variable);
|
|
};
|
|
|
|
/**
|
|
* This shortcut function is needed for events code generation.
|
|
* @private
|
|
*/
|
|
static valuePush = function (
|
|
array: gdjs.Variable,
|
|
value: string | float | boolean
|
|
) {
|
|
array.pushValue(value);
|
|
};
|
|
|
|
/**
|
|
* This shortcut function is needed for events code generation.
|
|
* @private
|
|
*/
|
|
static variableRemoveAt = function (array: gdjs.Variable, index: number) {
|
|
array.removeAtIndex(index);
|
|
};
|
|
|
|
/**
|
|
* Shortcut to get the first value of an array variable as a string.
|
|
*/
|
|
static getFirstVariableString = function (array: gdjs.Variable): string {
|
|
if (array.getChildrenCount() === 0) {
|
|
return '';
|
|
}
|
|
return array.getAllChildrenArray()[0].getAsString();
|
|
};
|
|
|
|
/**
|
|
* Shortcut to get the first value of an array variable as a number.
|
|
*/
|
|
static getFirstVariableNumber = function (array: gdjs.Variable): number {
|
|
if (array.getChildrenCount() === 0) {
|
|
return 0;
|
|
}
|
|
return array.getAllChildrenArray()[0].getAsNumber();
|
|
};
|
|
|
|
/**
|
|
* Shortcut to get the last value of an array variable as a string.
|
|
*/
|
|
static getLastVariableString = function (array: gdjs.Variable): string {
|
|
const children = array.getAllChildrenArray();
|
|
return children.length === 0
|
|
? ''
|
|
: children[children.length - 1].getAsString();
|
|
};
|
|
|
|
/**
|
|
* Shortcut to get the last value of an array variable as a number.
|
|
*/
|
|
static getLastVariableNumber = function (array: gdjs.Variable): number {
|
|
const children = array.getAllChildrenArray();
|
|
return children.length === 0
|
|
? 0
|
|
: children[children.length - 1].getAsNumber();
|
|
};
|
|
|
|
/**
|
|
* Shortcut to test if a variable exists for the object.
|
|
* @param name The variable to be tested
|
|
* @return true if the variable exists.
|
|
*/
|
|
hasVariable(name: string): boolean {
|
|
return this._variables.has(name);
|
|
}
|
|
|
|
/**
|
|
* Returns the collection of effects to be rendered by the
|
|
* underlying renderer.
|
|
* @returns The renderer effects.
|
|
*/
|
|
getRendererEffects() {
|
|
return this._rendererEffects;
|
|
}
|
|
|
|
/**
|
|
* Add a new effect, or replace the one with the same name.
|
|
* @param effectData The data describing the effect to add.
|
|
*/
|
|
addEffect(effectData: EffectData): boolean {
|
|
const rendererObject = this.getRendererObject();
|
|
if (!rendererObject) return false;
|
|
|
|
return this._runtimeScene
|
|
.getGame()
|
|
.getEffectsManager()
|
|
.addEffect(effectData, this._rendererEffects, this);
|
|
}
|
|
|
|
/**
|
|
* Remove the effect with the specified name
|
|
* @param effectName The name of the effect.
|
|
*/
|
|
removeEffect(effectName: string): boolean {
|
|
const rendererObject = this.getRendererObject();
|
|
if (!rendererObject) return false;
|
|
|
|
return this._runtimeScene
|
|
.getGame()
|
|
.getEffectsManager()
|
|
.removeEffect(this._rendererEffects, this, effectName);
|
|
}
|
|
|
|
/**
|
|
* Remove all effects.
|
|
*/
|
|
clearEffects(): boolean {
|
|
const rendererObject = this.getRendererObject();
|
|
if (!rendererObject) return false;
|
|
|
|
this._rendererEffects = {};
|
|
return (
|
|
this._runtimeScene
|
|
.getGame()
|
|
.getEffectsManager()
|
|
// @ts-expect-error - the effects manager is typed with the PIXI object.
|
|
.clearEffects(rendererObject)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Change an effect parameter value (for parameters that are numbers).
|
|
* @param name The name of the effect to update.
|
|
* @param parameterName The name of the parameter to update.
|
|
* @param value The new value (number).
|
|
*/
|
|
setEffectDoubleParameter(
|
|
name: string,
|
|
parameterName: string,
|
|
value: float
|
|
): boolean {
|
|
return this._runtimeScene
|
|
.getGame()
|
|
.getEffectsManager()
|
|
.setEffectDoubleParameter(
|
|
this._rendererEffects,
|
|
name,
|
|
parameterName,
|
|
value
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Change an effect parameter value (for parameters that are strings).
|
|
* @param name The name of the effect to update.
|
|
* @param parameterName The name of the parameter to update.
|
|
* @param value The new value (string).
|
|
*/
|
|
setEffectStringParameter(
|
|
name: string,
|
|
parameterName: string,
|
|
value: string
|
|
): boolean {
|
|
return this._runtimeScene
|
|
.getGame()
|
|
.getEffectsManager()
|
|
.setEffectStringParameter(
|
|
this._rendererEffects,
|
|
name,
|
|
parameterName,
|
|
value
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Change an effect parameter value (for parameters that are booleans).
|
|
* @param name The name of the effect to update.
|
|
* @param parameterName The name of the parameter to update.
|
|
* @param value The new value (boolean).
|
|
*/
|
|
setEffectBooleanParameter(
|
|
name: string,
|
|
parameterName: string,
|
|
value: boolean
|
|
): boolean {
|
|
return this._runtimeScene
|
|
.getGame()
|
|
.getEffectsManager()
|
|
.setEffectBooleanParameter(
|
|
this._rendererEffects,
|
|
name,
|
|
parameterName,
|
|
value
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Updates all the parameters of an effect.
|
|
* @param effectData The data describing the effect
|
|
*/
|
|
updateAllEffectParameters(effectData: EffectData): boolean {
|
|
return this._runtimeScene
|
|
.getGame()
|
|
.getEffectsManager()
|
|
.updateAllEffectParameters(this._rendererEffects, effectData);
|
|
}
|
|
|
|
/**
|
|
* Enable or disable an effect.
|
|
* @param name The name of the effect to enable or disable.
|
|
* @param enable true to enable, false to disable
|
|
*/
|
|
enableEffect(name: string, enable: boolean): void {
|
|
this._runtimeScene
|
|
.getGame()
|
|
.getEffectsManager()
|
|
.enableEffect(this._rendererEffects, this, name, enable);
|
|
}
|
|
|
|
/**
|
|
* Check if an effect is enabled
|
|
* @param name The name of the effect
|
|
* @return true if the effect is enabled, false otherwise.
|
|
*/
|
|
isEffectEnabled(name: string): boolean {
|
|
return this._runtimeScene
|
|
.getGame()
|
|
.getEffectsManager()
|
|
.isEffectEnabled(this._rendererEffects, this, name);
|
|
}
|
|
|
|
/**
|
|
* Check if an effect exists on this object
|
|
* @param name The name of the effect
|
|
* @return true if the effect exists, false otherwise.
|
|
*/
|
|
hasEffect(name: string): boolean {
|
|
return this._runtimeScene
|
|
.getGame()
|
|
.getEffectsManager()
|
|
.hasEffect(this._rendererEffects, name);
|
|
}
|
|
|
|
/**
|
|
* Hide (or show) the object.
|
|
* @param enable Set it to true to hide the object, false to show it.
|
|
*/
|
|
hide(enable: boolean): void {
|
|
if (enable === undefined) {
|
|
enable = true;
|
|
}
|
|
this.hidden = enable;
|
|
}
|
|
|
|
/**
|
|
* Return true if the object is not hidden.
|
|
*
|
|
* Note: This is unrelated to the actual visibility of the object on the screen.
|
|
* For this, see `getVisibilityAABB` to get the bounding boxes of the object as displayed
|
|
* on the scene.
|
|
*
|
|
* @return true if the object is not hidden.
|
|
*/
|
|
isVisible(): boolean {
|
|
return !this.hidden;
|
|
}
|
|
|
|
/**
|
|
* Return true if the object is hidden.
|
|
* @return true if the object is hidden.
|
|
*/
|
|
isHidden(): boolean {
|
|
return this.hidden;
|
|
}
|
|
|
|
/**
|
|
* Return the width of the object.
|
|
* @return The width of the object
|
|
*/
|
|
getWidth(): float {
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Return the width of the object.
|
|
* @return The height of the object
|
|
*/
|
|
getHeight(): float {
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Return the X position of the object center, **relative to the object X position** (`getDrawableX`).
|
|
* Use `getCenterXInScene` to get the position of the center in the scene.
|
|
*
|
|
* @return the X position of the object center, relative to `getDrawableX()`.
|
|
*/
|
|
getCenterX(): float {
|
|
return this.getWidth() / 2;
|
|
}
|
|
|
|
/**
|
|
* Return the Y position of the object center, **relative to the object position** (`getDrawableY`).
|
|
* Use `getCenterYInScene` to get the position of the center in the scene.
|
|
*
|
|
* @return the Y position of the object center, relative to `getDrawableY()`.
|
|
*/
|
|
getCenterY(): float {
|
|
return this.getHeight() / 2;
|
|
}
|
|
|
|
/**
|
|
* Return the X position of the object center, **relative to the scene origin**.
|
|
* @returns the X position of the object center, **relative to the scene origin**.
|
|
*/
|
|
getCenterXInScene(): float {
|
|
return this.getDrawableX() + this.getCenterX();
|
|
}
|
|
|
|
/**
|
|
* Return the Y position of the object center, **relative to the scene origin**.
|
|
* @returns the Y position of the object center, **relative to the scene origin**.
|
|
*/
|
|
getCenterYInScene(): float {
|
|
return this.getDrawableY() + this.getCenterY();
|
|
}
|
|
|
|
/**
|
|
* Change the object center position in the scene.
|
|
* @param x The new X position of the center in the scene.
|
|
* @param y The new Y position of the center in the scene.
|
|
*/
|
|
setCenterPositionInScene(x: float, y: float): void {
|
|
this.setX(x + this.x - (this.getDrawableX() + this.getCenterX()));
|
|
this.setY(y + this.y - (this.getDrawableY() + this.getCenterY()));
|
|
}
|
|
|
|
/**
|
|
* Change the object center X position in the scene.
|
|
* @param x The new X position of the center in the scene.
|
|
*/
|
|
setCenterXInScene(x: float): void {
|
|
this.setX(x + this.x - (this.getDrawableX() + this.getCenterX()));
|
|
}
|
|
|
|
/**
|
|
* Change the object center Y position in the scene.
|
|
* @param x The new Y position of the center in the scene.
|
|
*/
|
|
setCenterYInScene(y: float): void {
|
|
this.setY(y + this.y - (this.getDrawableY() + this.getCenterY()));
|
|
}
|
|
|
|
//Forces :
|
|
/**
|
|
* Get a force from the garbage, or create a new force is garbage is empty.<br>
|
|
* To be used each time a force is created so as to avoid temporaries objects.
|
|
*
|
|
* @param x The x coordinates of the force
|
|
* @param y The y coordinates of the force
|
|
* @param multiplier Set the force multiplier
|
|
*/
|
|
private _getRecycledForce(
|
|
x: float,
|
|
y: float,
|
|
multiplier: integer
|
|
): gdjs.Force {
|
|
if (RuntimeObject.forcesGarbage.length === 0) {
|
|
return new gdjs.Force(x, y, multiplier);
|
|
} else {
|
|
const recycledForce = RuntimeObject.forcesGarbage.pop() as gdjs.Force;
|
|
recycledForce.setX(x);
|
|
recycledForce.setY(y);
|
|
recycledForce.setMultiplier(multiplier);
|
|
return recycledForce;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a force to the object to move it.
|
|
* @param x The x coordinates of the force
|
|
* @param y The y coordinates of the force
|
|
* @param multiplier Set the force multiplier
|
|
*/
|
|
addForce(x: float, y: float, multiplier: integer): void {
|
|
this._forces.push(this._getRecycledForce(x, y, multiplier));
|
|
}
|
|
|
|
/**
|
|
* Add a force using polar coordinates.
|
|
* @param angle The angle of the force, in degrees.
|
|
* @param len The length of the force, in pixels.
|
|
* @param multiplier Set the force multiplier
|
|
*/
|
|
addPolarForce(angle: float, len: float, multiplier: integer): void {
|
|
const angleInRadians = gdjs.toRad(angle);
|
|
|
|
//TODO: Benchmark with Math.PI
|
|
const forceX = Math.cos(angleInRadians) * len;
|
|
const forceY = Math.sin(angleInRadians) * len;
|
|
this._forces.push(this._getRecycledForce(forceX, forceY, multiplier));
|
|
}
|
|
|
|
/**
|
|
* Add a force oriented toward a position
|
|
* @param x The target x position
|
|
* @param y The target y position
|
|
* @param len The force length, in pixels.
|
|
* @param multiplier Set the force multiplier
|
|
*/
|
|
addForceTowardPosition(
|
|
x: float,
|
|
y: float,
|
|
len: float,
|
|
multiplier: integer
|
|
): void {
|
|
const angleInRadians = Math.atan2(
|
|
y - (this.getDrawableY() + this.getCenterY()),
|
|
x - (this.getDrawableX() + this.getCenterX())
|
|
);
|
|
const forceX = Math.cos(angleInRadians) * len;
|
|
const forceY = Math.sin(angleInRadians) * len;
|
|
this._forces.push(this._getRecycledForce(forceX, forceY, multiplier));
|
|
}
|
|
|
|
/**
|
|
* Add a force oriented toward another object.<br>
|
|
* (Shortcut for addForceTowardPosition)
|
|
* @param object The target object
|
|
* @param len The force length, in pixels.
|
|
* @param multiplier Set the force multiplier
|
|
*/
|
|
addForceTowardObject(
|
|
object: gdjs.RuntimeObject,
|
|
len: float,
|
|
multiplier: integer
|
|
): void {
|
|
if (object == null) {
|
|
return;
|
|
}
|
|
this.addForceTowardPosition(
|
|
object.getDrawableX() + object.getCenterX(),
|
|
object.getDrawableY() + object.getCenterY(),
|
|
len,
|
|
multiplier
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Deletes all forces applied on the object
|
|
*/
|
|
clearForces(): void {
|
|
RuntimeObject.forcesGarbage.push.apply(
|
|
RuntimeObject.forcesGarbage,
|
|
this._forces
|
|
);
|
|
this._forces.length = 0;
|
|
}
|
|
|
|
/**
|
|
* Return true if no forces are applied on the object.
|
|
* @return true if no forces are applied on the object.
|
|
*/
|
|
hasNoForces(): boolean {
|
|
return this._forces.length === 0;
|
|
}
|
|
|
|
/**
|
|
* Called once a step by runtimeScene to update forces magnitudes and
|
|
* remove null ones.
|
|
*/
|
|
updateForces(elapsedTime: float): void {
|
|
for (let i = 0; i < this._forces.length; ) {
|
|
const force = this._forces[i];
|
|
const multiplier = force.getMultiplier();
|
|
if (multiplier === 1) {
|
|
// Permanent force
|
|
++i;
|
|
} else {
|
|
if (
|
|
multiplier === 0 ||
|
|
// Instant or force disappearing
|
|
force.getLength() <= 0.001
|
|
) {
|
|
RuntimeObject.forcesGarbage.push(force);
|
|
this._forces.splice(i, 1);
|
|
} else {
|
|
// Deprecated way of updating forces progressively.
|
|
force.setLength(
|
|
force.getLength() -
|
|
force.getLength() * (1 - multiplier) * elapsedTime
|
|
);
|
|
++i;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return a force which is the sum of all forces applied on the object.
|
|
*
|
|
* @return A force object.
|
|
*/
|
|
getAverageForce(): gdjs.Force {
|
|
let averageX = 0;
|
|
let averageY = 0;
|
|
for (let i = 0, len = this._forces.length; i < len; ++i) {
|
|
averageX += this._forces[i].getX();
|
|
averageY += this._forces[i].getY();
|
|
}
|
|
this._averageForce.setX(averageX);
|
|
this._averageForce.setY(averageY);
|
|
return this._averageForce;
|
|
}
|
|
|
|
/**
|
|
* Return true if the average angle of the forces applied on the object
|
|
* is in a given range.
|
|
*
|
|
* @param angle The angle to be tested.
|
|
* @param toleranceInDegrees The length of the range :
|
|
* @return true if the difference between the average angle of the forces
|
|
* and the angle parameter is inferior to toleranceInDegrees parameter.
|
|
*/
|
|
averageForceAngleIs(angle: float, toleranceInDegrees: float): boolean {
|
|
let averageAngle = this.getAverageForce().getAngle();
|
|
if (averageAngle < 0) {
|
|
averageAngle += 360;
|
|
}
|
|
return Math.abs(angle - averageAngle) < toleranceInDegrees / 2;
|
|
}
|
|
|
|
//Hit boxes and collision :
|
|
/**
|
|
* Get all the hit boxes for the object.
|
|
*
|
|
* For collision checks, {@link getHitBoxesAround} should be used instead.
|
|
*
|
|
* The default implementation returns a basic bounding box based the size (getWidth and
|
|
* getHeight) and the center point of the object (getCenterX and getCenterY).
|
|
*
|
|
* You should probably redefine {@link updateHitBoxes} instead of this function.
|
|
*
|
|
* @return An array composed of polygon.
|
|
*/
|
|
getHitBoxes(): gdjs.Polygon[] {
|
|
//Avoid a naive implementation requiring to recreate temporaries each time
|
|
//the function is called:
|
|
//(var rectangle = gdjs.Polygon.createRectangle(this.getWidth(), this.getHeight());
|
|
//...)
|
|
if (this.hitBoxesDirty) {
|
|
this.updateHitBoxes();
|
|
this.updateAABB();
|
|
this.hitBoxesDirty = false;
|
|
}
|
|
return this.hitBoxes;
|
|
}
|
|
|
|
/**
|
|
* Return at least all the hit boxes that overlap a given area.
|
|
*
|
|
* The hit boxes don't need to actually overlap the area,
|
|
* (i.e: it's correct to return more hit boxes than those in the specified area)
|
|
* but the ones that do must be returned.
|
|
*
|
|
* The default implementation returns the same as {@link getHitBoxes}.
|
|
*
|
|
* This method can be overridden by grid based objects (or other objects
|
|
* that can quickly compute which hitboxes are touching a given area)
|
|
* to optimize collision checks.
|
|
*
|
|
* When overriding this method, the following ones should be overridden too:
|
|
* * {@link getHitBoxes}
|
|
* * {@link getAABB}
|
|
* * {@link updateHitBoxes}
|
|
* * {@link updateAABB}
|
|
*
|
|
* @param left bound of the area in scene coordinates
|
|
* @param top bound of the area in scene coordinates
|
|
* @param right bound of the area in scene coordinates
|
|
* @param bottom bound of the area in scene coordinates
|
|
*
|
|
* @return at least all the hit boxes that overlap a given area.
|
|
*/
|
|
getHitBoxesAround(
|
|
left: float,
|
|
top: float,
|
|
right: float,
|
|
bottom: float
|
|
): Iterable<gdjs.Polygon> {
|
|
return this.getHitBoxes();
|
|
}
|
|
|
|
/**
|
|
* Update the hit boxes for the object.
|
|
*
|
|
* The default implementation set a basic bounding box based on the size (getWidth and
|
|
* getHeight) and the center point of the object (getCenterX and getCenterY).
|
|
* Result is cached until invalidated (by a position change, angle change...).
|
|
*
|
|
* You should not call this function by yourself, it is called when necessary by getHitBoxes method.
|
|
* However, you can redefine it if your object need custom hit boxes.
|
|
*/
|
|
updateHitBoxes(): void {
|
|
this.hitBoxes = this._defaultHitBoxes;
|
|
const width = this.getWidth();
|
|
const height = this.getHeight();
|
|
const centerX = this.getCenterX();
|
|
const centerY = this.getCenterY();
|
|
if (centerX === width / 2 && centerY === height / 2) {
|
|
this.hitBoxes[0].vertices[0][0] = -centerX;
|
|
this.hitBoxes[0].vertices[0][1] = -centerY;
|
|
this.hitBoxes[0].vertices[1][0] = +centerX;
|
|
this.hitBoxes[0].vertices[1][1] = -centerY;
|
|
this.hitBoxes[0].vertices[2][0] = +centerX;
|
|
this.hitBoxes[0].vertices[2][1] = +centerY;
|
|
this.hitBoxes[0].vertices[3][0] = -centerX;
|
|
this.hitBoxes[0].vertices[3][1] = +centerY;
|
|
} else {
|
|
this.hitBoxes[0].vertices[0][0] = 0 - centerX;
|
|
this.hitBoxes[0].vertices[0][1] = 0 - centerY;
|
|
this.hitBoxes[0].vertices[1][0] = width - centerX;
|
|
this.hitBoxes[0].vertices[1][1] = 0 - centerY;
|
|
this.hitBoxes[0].vertices[2][0] = width - centerX;
|
|
this.hitBoxes[0].vertices[2][1] = height - centerY;
|
|
this.hitBoxes[0].vertices[3][0] = 0 - centerX;
|
|
this.hitBoxes[0].vertices[3][1] = height - centerY;
|
|
}
|
|
this.hitBoxes[0].rotate(gdjs.toRad(this.getAngle()));
|
|
this.hitBoxes[0].move(
|
|
this.getDrawableX() + centerX,
|
|
this.getDrawableY() + centerY
|
|
);
|
|
}
|
|
|
|
isIncludedInParentCollisionMask(): boolean {
|
|
return this._isIncludedInParentCollisionMask;
|
|
}
|
|
|
|
setIncludedInParentCollisionMask(isIncluded: boolean): void {
|
|
const wasIncluded = this._isIncludedInParentCollisionMask;
|
|
this._isIncludedInParentCollisionMask = isIncluded;
|
|
if (wasIncluded !== isIncluded) {
|
|
this._runtimeScene.onChildrenLocationChanged();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the AABB (axis aligned bounding box) for the object.
|
|
*
|
|
* The default implementation uses either the position/size of the object (when angle is 0) or
|
|
* hitboxes (when angle is not 0) to compute the bounding box.
|
|
* Result is cached until invalidated (by a position change, angle change...).
|
|
*
|
|
* You should probably redefine updateAABB instead of this function.
|
|
*
|
|
* @return The bounding box
|
|
*/
|
|
getAABB(): AABB {
|
|
if (this.hitBoxesDirty) {
|
|
this.updateHitBoxes();
|
|
this.updateAABB();
|
|
this.hitBoxesDirty = false;
|
|
}
|
|
return this.aabb;
|
|
}
|
|
|
|
/**
|
|
* Get the AABB (axis aligned bounding box) to be used to determine if the object
|
|
* is visible on screen. The gdjs.RuntimeScene will hide the renderer object if
|
|
* the object is not visible on screen ("culling").
|
|
*
|
|
* The default implementation uses the AABB returned by getAABB.
|
|
*
|
|
* If `null` is returned, the object is assumed to be always visible.
|
|
*
|
|
* @return The bounding box or `null`.
|
|
*/
|
|
getVisibilityAABB(): AABB | null {
|
|
return this.getAABB();
|
|
}
|
|
|
|
/**
|
|
* Update the AABB (axis aligned bounding box) for the object.
|
|
*
|
|
* Default implementation uses either the position/size of the object (when angle is 0) or
|
|
* hitboxes (when angle is not 0) to compute the bounding box.
|
|
*
|
|
* You should not call this function by yourself, it is called when necessary by getAABB method.
|
|
* However, you can redefine it if your object can have a faster implementation.
|
|
*/
|
|
updateAABB(): void {
|
|
if (this.getAngle() === 0) {
|
|
// Fast/simple computation of AABB for non rotated object
|
|
// (works even for object with non default center/origin
|
|
// because we're using getDrawableX/Y)
|
|
this.aabb.min[0] = this.getDrawableX();
|
|
this.aabb.min[1] = this.getDrawableY();
|
|
this.aabb.max[0] = this.aabb.min[0] + this.getWidth();
|
|
this.aabb.max[1] = this.aabb.min[1] + this.getHeight();
|
|
} else {
|
|
// Use hitboxes if object is rotated to ensure that the AABB
|
|
// is properly bounding the whole object.
|
|
// Slower (10-15% slower).
|
|
let first = true;
|
|
for (let i = 0; i < this.hitBoxes.length; i++) {
|
|
for (let j = 0; j < this.hitBoxes[i].vertices.length; j++) {
|
|
const vertex = this.hitBoxes[i].vertices[j];
|
|
if (first) {
|
|
this.aabb.min[0] = vertex[0];
|
|
this.aabb.max[0] = vertex[0];
|
|
this.aabb.min[1] = vertex[1];
|
|
this.aabb.max[1] = vertex[1];
|
|
first = false;
|
|
} else {
|
|
this.aabb.min[0] = Math.min(this.aabb.min[0], vertex[0]);
|
|
this.aabb.max[0] = Math.max(this.aabb.max[0], vertex[0]);
|
|
this.aabb.min[1] = Math.min(this.aabb.min[1], vertex[1]);
|
|
this.aabb.max[1] = Math.max(this.aabb.max[1], vertex[1]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Shortcut for `getAABB().min[0]`.
|
|
* See {@link getAABB}.
|
|
*/
|
|
getAABBLeft(): float {
|
|
return this.getAABB().min[0];
|
|
}
|
|
|
|
/**
|
|
* Shortcut for `getAABB().min[1]`.
|
|
* See {@link getAABB}.
|
|
*/
|
|
getAABBTop(): float {
|
|
return this.getAABB().min[1];
|
|
}
|
|
|
|
/**
|
|
* Shortcut for `getAABB().max[0]`.
|
|
* See {@link getAABB}.
|
|
*/
|
|
getAABBRight(): float {
|
|
return this.getAABB().max[0];
|
|
}
|
|
|
|
/**
|
|
* Shortcut for `getAABB().max[1]`.
|
|
* See {@link getAABB}.
|
|
*/
|
|
getAABBBottom(): float {
|
|
return this.getAABB().max[1];
|
|
}
|
|
|
|
/**
|
|
* Shortcut for getting the center on the X coordinates of the object AABB.
|
|
* See {@link getAABB}.
|
|
*/
|
|
getAABBCenterX(): float {
|
|
return this.getAABB().min[0] / 2 + this.getAABB().max[0] / 2;
|
|
}
|
|
|
|
/**
|
|
* Shortcut for getting the center on the Y coordinates of the object AABB.
|
|
* See {@link getAABB}.
|
|
*/
|
|
getAABBCenterY(): float {
|
|
return this.getAABB().min[1] / 2 + this.getAABB().max[1] / 2;
|
|
}
|
|
|
|
//Behaviors:
|
|
/**
|
|
* Call each behavior stepPreEvents method.
|
|
*/
|
|
stepBehaviorsPreEvents(
|
|
instanceContainer: gdjs.RuntimeInstanceContainer
|
|
): void {
|
|
for (let i = 0, len = this._behaviors.length; i < len; ++i) {
|
|
this._behaviors[i].stepPreEvents(instanceContainer);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Call each behavior stepPostEvents method.
|
|
*/
|
|
stepBehaviorsPostEvents(
|
|
instanceContainer: gdjs.RuntimeInstanceContainer
|
|
): void {
|
|
for (let i = 0, len = this._behaviors.length; i < len; ++i) {
|
|
this._behaviors[i].stepPostEvents(instanceContainer);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called when the object was hot reloaded, to notify behaviors
|
|
* that the object was modified. Useful for behaviors that
|
|
*/
|
|
notifyBehaviorsObjectHotReloaded(): void {
|
|
for (let i = 0, len = this._behaviors.length; i < len; ++i) {
|
|
this._behaviors[i].onObjectHotReloaded();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get a behavior from its name.
|
|
* If the behavior does not exists, `undefined` is returned.
|
|
*
|
|
* **Never keep a reference** to a behavior, as they can be hot-reloaded. Instead,
|
|
* always call getBehavior on the object.
|
|
*
|
|
* @param name {String} The behavior name.
|
|
* @return The behavior with the given name, or undefined.
|
|
*/
|
|
getBehavior(name: string): gdjs.RuntimeBehavior | null {
|
|
return this._behaviorsTable.get(name);
|
|
}
|
|
|
|
/**
|
|
* Check if a behavior is used by the object.
|
|
*
|
|
* @param name {String} The behavior name.
|
|
*/
|
|
hasBehavior(name: string): boolean {
|
|
return this._behaviorsTable.containsKey(name);
|
|
}
|
|
|
|
/**
|
|
* De/activate a behavior of the object.
|
|
*
|
|
* @param name {String} The behavior name.
|
|
* @param enable {boolean} true to activate the behavior
|
|
*/
|
|
activateBehavior(name: string, enable: boolean): void {
|
|
if (this._behaviorsTable.containsKey(name)) {
|
|
this._behaviorsTable.get(name).activate(enable);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a behavior is activated
|
|
*
|
|
* @param name The behavior name.
|
|
* @return true if the behavior is activated.
|
|
*/
|
|
behaviorActivated(name: string): boolean {
|
|
if (this._behaviorsTable.containsKey(name)) {
|
|
return this._behaviorsTable.get(name).activated();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Remove the behavior with the given name. Usually only used by
|
|
* hot-reloading, as performance of this operation is not guaranteed
|
|
* (in the future, this could lead to re-organization of arrays
|
|
* holding behaviors).
|
|
*
|
|
* @param name The name of the behavior to remove.
|
|
* @returns true if the behavior was properly removed, false otherwise.
|
|
*/
|
|
removeBehavior(name: string): boolean {
|
|
const behavior = this._behaviorsTable.get(name);
|
|
if (!behavior) {
|
|
return false;
|
|
}
|
|
behavior.onDestroy();
|
|
const behaviorIndex = this._behaviors.indexOf(behavior);
|
|
if (behaviorIndex !== -1) {
|
|
this._behaviors.splice(behaviorIndex, 1);
|
|
}
|
|
this._behaviorsTable.remove(name);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Create the behavior described by the given BehaviorData
|
|
*
|
|
* @param behaviorData The data to be used to construct the behavior.
|
|
* @returns true if the behavior was properly created, false otherwise.
|
|
*/
|
|
addNewBehavior(behaviorData: BehaviorData): boolean {
|
|
const Ctor = gdjs.getBehaviorConstructor(behaviorData.type);
|
|
if (!Ctor) {
|
|
return false;
|
|
}
|
|
const newRuntimeBehavior = new Ctor(
|
|
this._runtimeScene,
|
|
behaviorData,
|
|
this
|
|
);
|
|
this._behaviors.push(newRuntimeBehavior);
|
|
this._behaviorsTable.put(behaviorData.name, newRuntimeBehavior);
|
|
return true;
|
|
}
|
|
|
|
//Timers:
|
|
/**
|
|
* Updates the object timers. Called once during the game loop, before events and rendering.
|
|
* @param elapsedTime The elapsed time since the previous frame in milliseconds.
|
|
*/
|
|
updateTimers(elapsedTime: float): void {
|
|
for (const name in this._timers.items) {
|
|
if (this._timers.items.hasOwnProperty(name)) {
|
|
this._timers.items[name].updateTime(elapsedTime);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compare a timer elapsed time. If the timer does not exist, it is created.
|
|
*
|
|
* @deprecated prefer using `getTimerElapsedTimeInSecondsOrNaN`.
|
|
*
|
|
* @param timerName The timer name.
|
|
* @param timeInSeconds The time value to check in seconds.
|
|
* @return True if the timer exists and its value is greater than or equal than the given time, false otherwise.
|
|
*/
|
|
timerElapsedTime(timerName: string, timeInSeconds: float): boolean {
|
|
if (!this._timers.containsKey(timerName)) {
|
|
this._timers.put(timerName, new gdjs.Timer(timerName));
|
|
return false;
|
|
}
|
|
return this.getTimerElapsedTimeInSeconds(timerName) >= timeInSeconds;
|
|
}
|
|
|
|
/**
|
|
* Test a if a timer is paused.
|
|
* @param timerName The timer name.
|
|
* @return True if the timer exists and is paused, false otherwise.
|
|
*/
|
|
timerPaused(timerName: string): boolean {
|
|
if (!this._timers.containsKey(timerName)) {
|
|
return false;
|
|
}
|
|
return this._timers.get(timerName).isPaused();
|
|
}
|
|
|
|
/**
|
|
* Reset a timer. If the timer doesn't exist it is created.
|
|
* @param timerName The timer name.
|
|
*/
|
|
resetTimer(timerName: string): void {
|
|
if (!this._timers.containsKey(timerName)) {
|
|
this._timers.put(timerName, new gdjs.Timer(timerName));
|
|
}
|
|
this._timers.get(timerName).reset();
|
|
}
|
|
|
|
/**
|
|
* Pause a timer. If the timer doesn't exist it is created.
|
|
* @param timerName The timer name.
|
|
*/
|
|
pauseTimer(timerName: string): void {
|
|
if (!this._timers.containsKey(timerName)) {
|
|
this._timers.put(timerName, new gdjs.Timer(timerName));
|
|
}
|
|
this._timers.get(timerName).setPaused(true);
|
|
}
|
|
|
|
/**
|
|
* Unpause a timer. If the timer doesn't exist it is created.
|
|
* @param timerName The timer name.
|
|
*/
|
|
unpauseTimer(timerName: string): void {
|
|
if (!this._timers.containsKey(timerName)) {
|
|
this._timers.put(timerName, new gdjs.Timer(timerName));
|
|
}
|
|
this._timers.get(timerName).setPaused(false);
|
|
}
|
|
|
|
/**
|
|
* Remove a timer
|
|
* @param timerName The timer name.
|
|
*/
|
|
removeTimer(timerName: string): void {
|
|
if (this._timers.containsKey(timerName)) {
|
|
this._timers.remove(timerName);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get a timer elapsed time.
|
|
*
|
|
* This is used by expressions to return 0 when a timer doesn't exist
|
|
* because numeric expressions must always return a number.
|
|
*
|
|
* @param timerName The timer name.
|
|
* @return The timer elapsed time in seconds, 0 if the timer doesn't exist.
|
|
*/
|
|
getTimerElapsedTimeInSeconds(timerName: string): float {
|
|
if (!this._timers.containsKey(timerName)) {
|
|
return 0;
|
|
}
|
|
return this._timers.get(timerName).getTime() / 1000.0;
|
|
}
|
|
|
|
/**
|
|
* Get a timer elapsed time.
|
|
*
|
|
* This is used by conditions to return false when a timer doesn't exist,
|
|
* no matter the relational operator.
|
|
*
|
|
* @param timerName The timer name.
|
|
* @return The timer elapsed time in seconds, NaN if the timer doesn't exist.
|
|
*/
|
|
getTimerElapsedTimeInSecondsOrNaN(timerName: string): float {
|
|
if (!this._timers.containsKey(timerName)) {
|
|
return Number.NaN;
|
|
}
|
|
return this._timers.get(timerName).getTime() / 1000.0;
|
|
}
|
|
|
|
//Other :
|
|
/**
|
|
* Separate the object from others objects, using their hitboxes.
|
|
* @param objects Objects
|
|
* @param ignoreTouchingEdges If true, then edges that are touching each other, without the hitbox polygons actually overlapping, won't be considered in collision.
|
|
* @return true if the object was moved
|
|
*/
|
|
separateFromObjects(
|
|
objects: gdjs.RuntimeObject[],
|
|
ignoreTouchingEdges: boolean
|
|
): boolean {
|
|
let moveXArray: Array<float> = separateFromObjectsStatics.moveXArray;
|
|
let moveYArray: Array<float> = separateFromObjectsStatics.moveYArray;
|
|
moveXArray.length = 0;
|
|
moveYArray.length = 0;
|
|
|
|
// We can assume that the moving object is not grid based,
|
|
// so there is no need for optimization:
|
|
// getHitBoxes can be called directly.
|
|
const hitBoxes = this.getHitBoxes();
|
|
let aabb: AABB | null = null;
|
|
|
|
// Check if there is a collision with each object
|
|
for (const otherObject of objects) {
|
|
if (otherObject.id === this.id) {
|
|
continue;
|
|
}
|
|
let otherHitBoxesArray = otherObject.getHitBoxes();
|
|
let otherHitBoxes: Iterable<gdjs.Polygon> = otherHitBoxesArray;
|
|
if (otherHitBoxesArray.length > 4) {
|
|
// The other object has a lot of hit boxes.
|
|
// Try to reduce the amount of hitboxes to check.
|
|
if (!aabb) {
|
|
aabb = this.getAABB();
|
|
}
|
|
otherHitBoxes = otherObject.getHitBoxesAround(
|
|
aabb.min[0],
|
|
aabb.min[1],
|
|
aabb.max[0],
|
|
aabb.max[1]
|
|
);
|
|
}
|
|
for (const hitBox of hitBoxes) {
|
|
for (const otherHitBox of otherHitBoxes) {
|
|
const result = gdjs.Polygon.collisionTest(
|
|
hitBox,
|
|
otherHitBox,
|
|
ignoreTouchingEdges
|
|
);
|
|
if (result.collision) {
|
|
moveXArray.push(result.move_axis[0]);
|
|
moveYArray.push(result.move_axis[1]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return moveFollowingSeparatingVectors(this, moveXArray, moveYArray);
|
|
}
|
|
|
|
/**
|
|
* Separate the object from others objects, using their hitboxes.
|
|
* @param objectsLists Tables of objects
|
|
* @param ignoreTouchingEdges If true, then edges that are touching each other, without the hitbox polygons actually overlapping, won't be considered in collision.
|
|
* @return true if the object was moved
|
|
*/
|
|
separateFromObjectsList(
|
|
objectsLists: ObjectsLists,
|
|
ignoreTouchingEdges: boolean
|
|
): boolean {
|
|
let moveXArray: Array<float> = separateFromObjectsStatics.moveXArray;
|
|
let moveYArray: Array<float> = separateFromObjectsStatics.moveYArray;
|
|
moveXArray.length = 0;
|
|
moveYArray.length = 0;
|
|
|
|
// We can assume that the moving object is not grid based
|
|
// So there is no need for optimization
|
|
// getHitBoxes can be called directly.
|
|
const hitBoxes = this.getHitBoxes();
|
|
let aabb: AABB | null = null;
|
|
|
|
for (const name in objectsLists.items) {
|
|
if (objectsLists.items.hasOwnProperty(name)) {
|
|
const otherObjects = objectsLists.items[name];
|
|
|
|
// Check if their is a collision with each object
|
|
for (const otherObject of otherObjects) {
|
|
if (otherObject.id === this.id) {
|
|
continue;
|
|
}
|
|
let otherHitBoxesArray = otherObject.getHitBoxes();
|
|
let otherHitBoxes: Iterable<gdjs.Polygon> = otherHitBoxesArray;
|
|
if (otherHitBoxesArray.length > 4) {
|
|
// The other object has a lot of hit boxes.
|
|
// Try to reduce the amount of hitboxes to check.
|
|
if (!aabb) {
|
|
aabb = this.getAABB();
|
|
}
|
|
otherHitBoxes = otherObject.getHitBoxesAround(
|
|
aabb.min[0],
|
|
aabb.min[1],
|
|
aabb.max[0],
|
|
aabb.max[1]
|
|
);
|
|
}
|
|
for (const hitBox of hitBoxes) {
|
|
for (const otherHitBox of otherHitBoxes) {
|
|
const result = gdjs.Polygon.collisionTest(
|
|
hitBox,
|
|
otherHitBox,
|
|
ignoreTouchingEdges
|
|
);
|
|
if (result.collision) {
|
|
moveXArray.push(result.move_axis[0]);
|
|
moveYArray.push(result.move_axis[1]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return moveFollowingSeparatingVectors(this, moveXArray, moveYArray);
|
|
}
|
|
|
|
/**
|
|
* Get the distance, in pixels, between *the center* of this object and another object.
|
|
* @param otherObject The other object
|
|
*/
|
|
getDistanceToObject(otherObject: gdjs.RuntimeObject): float {
|
|
return Math.sqrt(this.getSqDistanceToObject(otherObject));
|
|
}
|
|
|
|
/**
|
|
* Get the squared distance, in pixels, between *the center* of this object and another object.
|
|
* @param otherObject The other object
|
|
*/
|
|
getSqDistanceToObject(otherObject: gdjs.RuntimeObject): float {
|
|
if (otherObject === null) {
|
|
return 0;
|
|
}
|
|
const x =
|
|
this.getDrawableX() +
|
|
this.getCenterX() -
|
|
(otherObject.getDrawableX() + otherObject.getCenterX());
|
|
const y =
|
|
this.getDrawableY() +
|
|
this.getCenterY() -
|
|
(otherObject.getDrawableY() + otherObject.getCenterY());
|
|
return x * x + y * y;
|
|
}
|
|
|
|
/**
|
|
* Get the distance, in pixels, between *the center* of this object and a position.
|
|
* @param targetX Target X position
|
|
* @param targetY Target Y position
|
|
*/
|
|
getDistanceToPosition(targetX: float, targetY: float): float {
|
|
return Math.sqrt(this.getSqDistanceToPosition(targetX, targetY));
|
|
}
|
|
|
|
/**
|
|
* Get the squared distance, in pixels, between *the center* of this object and a position.
|
|
* @param targetX Target X position
|
|
* @param targetY Target Y position
|
|
*/
|
|
getSqDistanceToPosition(targetX: float, targetY: float): float {
|
|
const x = this.getDrawableX() + this.getCenterX() - targetX;
|
|
const y = this.getDrawableY() + this.getCenterY() - targetY;
|
|
return x * x + y * y;
|
|
}
|
|
|
|
/**
|
|
* Get the angle, in degrees, from the *object center* to another object.
|
|
* @param otherObject The other object
|
|
*/
|
|
getAngleToObject(otherObject: gdjs.RuntimeObject): float {
|
|
if (otherObject === null) {
|
|
return 0;
|
|
}
|
|
const x =
|
|
this.getDrawableX() +
|
|
this.getCenterX() -
|
|
(otherObject.getDrawableX() + otherObject.getCenterX());
|
|
const y =
|
|
this.getDrawableY() +
|
|
this.getCenterY() -
|
|
(otherObject.getDrawableY() + otherObject.getCenterY());
|
|
return gdjs.toDegrees(Math.atan2(-y, -x));
|
|
}
|
|
|
|
/**
|
|
* Compute the X position when given an angle and distance relative to the starting object.
|
|
* This is also known as getting the cartesian coordinates of a 2D vector, using its polar coordinates.
|
|
* @param angle The angle, in degrees.
|
|
* @param distance The distance from the object, in pixels
|
|
*/
|
|
getXFromAngleAndDistance(angle: float, distance: float): float {
|
|
return (
|
|
this.getDrawableX() +
|
|
this.getCenterX() +
|
|
distance * Math.cos(gdjs.toRad(angle))
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Compute the Y position when given an angle and distance relative to the starting object.
|
|
* This is also known as getting the cartesian coordinates of a 2D vector, using its polar coordinates.
|
|
* @param angle The angle, in degrees.
|
|
* @param distance The distance from the object, in pixels
|
|
*/
|
|
getYFromAngleAndDistance(angle: float, distance: float): float {
|
|
return (
|
|
this.getDrawableY() +
|
|
this.getCenterY() +
|
|
distance * Math.sin(gdjs.toRad(angle))
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get the angle, in degrees, from the *object center* to a position.
|
|
* @param targetX Target X position
|
|
* @param targetY Target Y position
|
|
*/
|
|
getAngleToPosition(targetX: float, targetY: float): float {
|
|
const x = this.getDrawableX() + this.getCenterX() - targetX;
|
|
const y = this.getDrawableY() + this.getCenterY() - targetY;
|
|
return gdjs.toDegrees(Math.atan2(-y, -x));
|
|
}
|
|
|
|
/**
|
|
* Put the object around a position, with a specific distance and angle.
|
|
* The distance and angle are computed between the position and *the center of the object*.
|
|
*
|
|
* @param x The x position of the target
|
|
* @param y The y position of the target
|
|
* @param distance The distance between the object and the target, in pixels.
|
|
* @param angleInDegrees The angle between the object and the target, in degrees.
|
|
*/
|
|
putAround(
|
|
x: float,
|
|
y: float,
|
|
distance: float,
|
|
angleInDegrees: float
|
|
): void {
|
|
const angleInRadians = gdjs.toRad(angleInDegrees);
|
|
|
|
this.setCenterXInScene(x + Math.cos(angleInRadians) * distance);
|
|
this.setCenterYInScene(y + Math.sin(angleInRadians) * distance);
|
|
}
|
|
|
|
/**
|
|
* Put the object around another object, with a specific distance and angle.
|
|
* The distance and angle are computed between *the centers of the objects*.
|
|
*
|
|
* @param obj The target object
|
|
* @param distance The distance between the object and the target
|
|
* @param angleInDegrees The angle between the object and the target, in degrees.
|
|
*/
|
|
putAroundObject(
|
|
obj: gdjs.RuntimeObject,
|
|
distance: float,
|
|
angleInDegrees: float
|
|
): void {
|
|
this.putAround(
|
|
obj.getDrawableX() + obj.getCenterX(),
|
|
obj.getDrawableY() + obj.getCenterY(),
|
|
distance,
|
|
angleInDegrees
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @deprecated
|
|
* @param objectsLists Tables of objects
|
|
*/
|
|
separateObjectsWithoutForces(objectsLists: ObjectsLists): void {
|
|
//Prepare the list of objects to iterate over.
|
|
const objects = gdjs.staticArray(
|
|
RuntimeObject.prototype.separateObjectsWithoutForces
|
|
);
|
|
objects.length = 0;
|
|
const lists = gdjs.staticArray2(
|
|
RuntimeObject.prototype.separateObjectsWithoutForces
|
|
);
|
|
objectsLists.values(lists);
|
|
for (let i = 0, len = lists.length; i < len; ++i) {
|
|
objects.push.apply(objects, lists[i]);
|
|
}
|
|
for (let i = 0, len = objects.length; i < len; ++i) {
|
|
if (objects[i].id != this.id) {
|
|
if (this.getDrawableX() < objects[i].getDrawableX()) {
|
|
this.setX(objects[i].getDrawableX() - this.getWidth());
|
|
} else {
|
|
if (
|
|
this.getDrawableX() + this.getWidth() >
|
|
objects[i].getDrawableX() + objects[i].getWidth()
|
|
) {
|
|
this.setX(objects[i].getDrawableX() + objects[i].getWidth());
|
|
}
|
|
}
|
|
if (this.getDrawableY() < objects[i].getDrawableY()) {
|
|
this.setY(objects[i].getDrawableY() - this.getHeight());
|
|
} else {
|
|
if (
|
|
this.getDrawableY() + this.getHeight() >
|
|
objects[i].getDrawableY() + objects[i].getHeight()
|
|
) {
|
|
this.setY(objects[i].getDrawableY() + objects[i].getHeight());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @deprecated
|
|
* @param objectsLists Tables of objects
|
|
*/
|
|
separateObjectsWithForces(objectsLists: ObjectsLists): void {
|
|
//Prepare the list of objects to iterate over.
|
|
const objects = gdjs.staticArray(
|
|
RuntimeObject.prototype.separateObjectsWithForces
|
|
);
|
|
objects.length = 0;
|
|
const lists = gdjs.staticArray2(
|
|
RuntimeObject.prototype.separateObjectsWithForces
|
|
);
|
|
objectsLists.values(lists);
|
|
for (let i = 0, len = lists.length; i < len; ++i) {
|
|
objects.push.apply(objects, lists[i]);
|
|
}
|
|
for (let i = 0, len = objects.length; i < len; ++i) {
|
|
if (objects[i].id != this.id) {
|
|
if (
|
|
this.getDrawableX() + this.getCenterX() <
|
|
objects[i].getDrawableX() + objects[i].getCenterX()
|
|
) {
|
|
let av = this.hasNoForces() ? 0 : this.getAverageForce().getX();
|
|
this.addForce(-av - 10, 0, 0);
|
|
} else {
|
|
let av = this.hasNoForces() ? 0 : this.getAverageForce().getX();
|
|
this.addForce(-av + 10, 0, 0);
|
|
}
|
|
if (
|
|
this.getDrawableY() + this.getCenterY() <
|
|
objects[i].getDrawableY() + objects[i].getCenterY()
|
|
) {
|
|
let av = this.hasNoForces() ? 0 : this.getAverageForce().getY();
|
|
this.addForce(0, -av - 10, 0);
|
|
} else {
|
|
let av = this.hasNoForces() ? 0 : this.getAverageForce().getY();
|
|
this.addForce(0, -av + 10, 0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return true if the hitboxes of two objects are overlapping
|
|
* @static
|
|
* @param obj1 The first runtimeObject
|
|
* @param obj2 The second runtimeObject
|
|
* @param ignoreTouchingEdges If true, then edges that are touching each other, without the hitbox polygons actually overlapping, won't be considered in collision.
|
|
* @return true if obj1 and obj2 are in collision
|
|
*/
|
|
static collisionTest(
|
|
obj1: gdjs.RuntimeObject,
|
|
obj2: gdjs.RuntimeObject,
|
|
ignoreTouchingEdges: boolean
|
|
): boolean {
|
|
//First check if bounding circle are too far.
|
|
const o1centerX = obj1.getCenterX();
|
|
const o1centerY = obj1.getCenterY();
|
|
const obj1BoundingRadius = Math.sqrt(
|
|
computeSqBoundingRadius(
|
|
obj1.getWidth(),
|
|
obj1.getHeight(),
|
|
o1centerX,
|
|
o1centerY
|
|
)
|
|
);
|
|
|
|
const o2centerX = obj2.getCenterX();
|
|
const o2centerY = obj2.getCenterY();
|
|
const obj2BoundingRadius = Math.sqrt(
|
|
computeSqBoundingRadius(
|
|
obj2.getWidth(),
|
|
obj2.getHeight(),
|
|
o2centerX,
|
|
o2centerY
|
|
)
|
|
);
|
|
|
|
const o1AbsoluteCenterX = obj1.getDrawableX() + o1centerX;
|
|
const o1AbsoluteCenterY = obj1.getDrawableY() + o1centerY;
|
|
const o2AbsoluteCenterX = obj2.getDrawableX() + o2centerX;
|
|
const o2AbsoluteCenterY = obj2.getDrawableY() + o2centerY;
|
|
|
|
const diffX = o1AbsoluteCenterX - o2AbsoluteCenterX;
|
|
const diffY = o1AbsoluteCenterY - o2AbsoluteCenterY;
|
|
if (
|
|
Math.sqrt(diffX * diffX + diffY * diffY) >
|
|
obj1BoundingRadius + obj2BoundingRadius
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
// Do a real check if necessary.
|
|
const hitBoxes1 = obj1.getHitBoxesAround(
|
|
o2AbsoluteCenterX - obj2BoundingRadius,
|
|
o2AbsoluteCenterY - obj2BoundingRadius,
|
|
o2AbsoluteCenterX + obj2BoundingRadius,
|
|
o2AbsoluteCenterY + obj2BoundingRadius
|
|
);
|
|
const hitBoxes2 = obj2.getHitBoxesAround(
|
|
o1AbsoluteCenterX - obj1BoundingRadius,
|
|
o1AbsoluteCenterY - obj1BoundingRadius,
|
|
o1AbsoluteCenterX + obj1BoundingRadius,
|
|
o1AbsoluteCenterY + obj1BoundingRadius
|
|
);
|
|
|
|
for (const hitBox1 of hitBoxes1) {
|
|
for (const hitBox2 of hitBoxes2) {
|
|
if (
|
|
gdjs.Polygon.collisionTest(hitBox1, hitBox2, ignoreTouchingEdges)
|
|
.collision
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param x The raycast source X
|
|
* @param y The raycast source Y
|
|
* @param endX The raycast end position X
|
|
* @param endY The raycast end position Y
|
|
* @param closest Get the closest or farthest collision mask result?
|
|
* @return A raycast result with the contact points and distances
|
|
*/
|
|
raycastTest(
|
|
x: float,
|
|
y: float,
|
|
endX: float,
|
|
endY: float,
|
|
closest: boolean
|
|
): RaycastTestResult {
|
|
// First check if bounding circles are too far
|
|
const objCenterX = this.getCenterX();
|
|
const objCenterY = this.getCenterY();
|
|
const objSqBoundingRadius = computeSqBoundingRadius(
|
|
this.getWidth(),
|
|
this.getHeight(),
|
|
objCenterX,
|
|
objCenterY
|
|
);
|
|
|
|
const rayCenterWorldX = (x + endX) / 2;
|
|
const rayCenterWorldY = (y + endY) / 2;
|
|
const raySqBoundingRadius =
|
|
(endX - x) * (endX - x) + (endY - y) * (endY - y);
|
|
|
|
const diffX = this.getDrawableX() + objCenterX - rayCenterWorldX;
|
|
const diffY = this.getDrawableY() + objCenterY - rayCenterWorldY;
|
|
|
|
let result = raycastTestStatics.result;
|
|
result.collision = false;
|
|
if (
|
|
// As an optimization, avoid computing the square root of the two boundings radius
|
|
// and the distance by comparing the squared values instead.
|
|
diffX * diffX + diffY * diffY >
|
|
objSqBoundingRadius +
|
|
raySqBoundingRadius +
|
|
2 * Math.sqrt(raySqBoundingRadius * objSqBoundingRadius)
|
|
) {
|
|
return result;
|
|
}
|
|
|
|
// Do a real check if necessary.
|
|
if (closest) {
|
|
let sqDistMin = Number.MAX_VALUE;
|
|
const hitBoxes = this.getHitBoxesAround(x, y, endX, endY);
|
|
for (const hitBox of hitBoxes) {
|
|
const res = gdjs.Polygon.raycastTest(hitBox, x, y, endX, endY);
|
|
if (res.collision && res.closeSqDist < sqDistMin) {
|
|
sqDistMin = res.closeSqDist;
|
|
gdjs.Polygon.copyRaycastTestResult(res, result);
|
|
}
|
|
}
|
|
} else {
|
|
let sqDistMax = -Number.MAX_VALUE;
|
|
const hitBoxes = this.getHitBoxesAround(x, y, endX, endY);
|
|
for (const hitBox of hitBoxes) {
|
|
const res = gdjs.Polygon.raycastTest(hitBox, x, y, endX, endY);
|
|
if (
|
|
res.collision &&
|
|
res.farSqDist > sqDistMax &&
|
|
res.farSqDist <= raySqBoundingRadius
|
|
) {
|
|
sqDistMax = res.farSqDist;
|
|
gdjs.Polygon.copyRaycastTestResult(res, result);
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Return true if the specified position is inside object bounding box.
|
|
*
|
|
* The position should be in "world" coordinates, i.e use gdjs.Layer.convertCoords
|
|
* if you need to pass the mouse or a touch position that you get from gdjs.InputManager.
|
|
* To check if a point is inside the object collision mask, you can use `isCollidingWithPoint` instead.
|
|
*
|
|
*/
|
|
insideObject(x: float, y: float): boolean {
|
|
if (this.hitBoxesDirty) {
|
|
this.updateHitBoxes();
|
|
this.updateAABB();
|
|
this.hitBoxesDirty = false;
|
|
}
|
|
return (
|
|
this.aabb.min[0] <= x &&
|
|
this.aabb.max[0] >= x &&
|
|
this.aabb.min[1] <= y &&
|
|
this.aabb.max[1] >= y
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check the distance between two objects.
|
|
* @static
|
|
*/
|
|
static distanceTest(
|
|
obj1: RuntimeObject,
|
|
obj2: RuntimeObject,
|
|
distance: float
|
|
): boolean {
|
|
return obj1.getSqDistanceToObject(obj2) <= distance;
|
|
}
|
|
|
|
/**
|
|
* Return true if the cursor, or any touch, is on the object.
|
|
*
|
|
* @return true if the cursor, or any touch, is on the object.
|
|
*/
|
|
cursorOnObject(instanceContainer: gdjs.RuntimeInstanceContainer): boolean {
|
|
const workingPoint: FloatPoint = gdjs.staticArray(
|
|
RuntimeObject.prototype.cursorOnObject
|
|
) as FloatPoint;
|
|
workingPoint.length = 2;
|
|
const inputManager = instanceContainer.getGame().getInputManager();
|
|
const layer = instanceContainer.getLayer(this.layer);
|
|
const mousePos = layer.convertCoords(
|
|
inputManager.getCursorX(),
|
|
inputManager.getCursorY(),
|
|
0,
|
|
workingPoint
|
|
);
|
|
if (this.insideObject(mousePos[0], mousePos[1])) {
|
|
return true;
|
|
}
|
|
const touchIds = inputManager.getAllTouchIdentifiers();
|
|
for (let i = 0; i < touchIds.length; ++i) {
|
|
const touchPos = layer.convertCoords(
|
|
inputManager.getTouchX(touchIds[i]),
|
|
inputManager.getTouchY(touchIds[i]),
|
|
0,
|
|
workingPoint
|
|
);
|
|
if (this.insideObject(touchPos[0], touchPos[1])) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check if a point is inside the object collision hitboxes.
|
|
* @param pointX The point x coordinate.
|
|
* @param pointY The point y coordinate.
|
|
* @return true if the point is inside the object collision hitboxes.
|
|
*/
|
|
isCollidingWithPoint(pointX: float, pointY: float): boolean {
|
|
const hitBoxes = this.getHitBoxesAround(pointX, pointY, pointX, pointY);
|
|
for (const hitBox of hitBoxes) {
|
|
if (gdjs.Polygon.isPointInside(hitBox, pointX, pointY)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Implementation of the Resizable interface.
|
|
setWidth(width: float): void {}
|
|
setHeight(height: float): void {}
|
|
setSize(newWidth: number, newHeight: number): void {}
|
|
|
|
// Implementation of the Scalable interface.
|
|
setScale(newScale: number): void {}
|
|
setScaleX(newScale: number): void {}
|
|
setScaleY(newScale: number): void {}
|
|
getScale(): number {
|
|
const scaleX = Math.abs(this.getScaleX());
|
|
const scaleY = Math.abs(this.getScaleY());
|
|
return scaleX === scaleY ? scaleX : Math.sqrt(scaleX * scaleY);
|
|
}
|
|
getScaleY(): number {
|
|
return 1;
|
|
}
|
|
getScaleX(): number {
|
|
return 1;
|
|
}
|
|
|
|
// Implementation of the Flippable interface.
|
|
flipX(enable: boolean) {}
|
|
flipY(enable: boolean) {}
|
|
isFlippedX(): boolean {
|
|
return false;
|
|
}
|
|
isFlippedY(): boolean {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get the identifier associated to an object name.
|
|
* Some features may want to compare objects name a large number of time. In this case,
|
|
* it may be more efficient to compare objects name identifiers.
|
|
*
|
|
* @static
|
|
*/
|
|
static getNameIdentifier(name: string): integer {
|
|
if (RuntimeObject._identifiers.containsKey(name)) {
|
|
return RuntimeObject._identifiers.get(name);
|
|
}
|
|
RuntimeObject._newId = (RuntimeObject._newId || 0) + 1;
|
|
const newIdentifier = RuntimeObject._newId;
|
|
RuntimeObject._identifiers.put(name, newIdentifier);
|
|
return newIdentifier;
|
|
}
|
|
|
|
/**
|
|
* Table containing the id corresponding to an object name. Do not use directly or modify.
|
|
* @static
|
|
*/
|
|
static _identifiers = new Hashtable<integer>();
|
|
|
|
/**
|
|
* The next available unique identifier for an object. Do not use directly or modify.
|
|
* @static
|
|
*/
|
|
static _newId = 0;
|
|
|
|
/**
|
|
* Global container for unused forces, avoiding recreating forces each tick.
|
|
* @static
|
|
*/
|
|
static forcesGarbage: Array<gdjs.Force> = [];
|
|
|
|
getVariableNumber = RuntimeObject.getVariableNumber;
|
|
returnVariable = RuntimeObject.returnVariable;
|
|
getVariableString = RuntimeObject.getVariableString;
|
|
setVariableNumber = RuntimeObject.setVariableNumber;
|
|
setVariableString = RuntimeObject.setVariableString;
|
|
getVariableBoolean = RuntimeObject.getVariableBoolean;
|
|
setVariableBoolean = RuntimeObject.setVariableBoolean;
|
|
getVariableChildCount = RuntimeObject.getVariableChildCount;
|
|
getFirstVariableNumber = RuntimeObject.getFirstVariableNumber;
|
|
getFirstVariableString = RuntimeObject.getFirstVariableString;
|
|
getLastVariableNumber = RuntimeObject.getLastVariableNumber;
|
|
getLastVariableString = RuntimeObject.getLastVariableString;
|
|
toggleVariableBoolean = RuntimeObject.toggleVariableBoolean;
|
|
variableChildExists = RuntimeObject.variableChildExists;
|
|
variableRemoveChild = RuntimeObject.variableRemoveChild;
|
|
variableClearChildren = RuntimeObject.variableClearChildren;
|
|
variablePushCopy = RuntimeObject.variablePushCopy;
|
|
valuePush = RuntimeObject.valuePush;
|
|
variableRemoveAt = RuntimeObject.variableRemoveAt;
|
|
|
|
/**
|
|
* Get the squared distance, in pixels, from the *object center* to a position.
|
|
* @param pointX X position
|
|
* @param pointY Y position
|
|
* @deprecated Use `getSqDistanceToPosition` instead.
|
|
*/
|
|
getSqDistanceTo = RuntimeObject.prototype.getSqDistanceToPosition;
|
|
}
|
|
gdjs.registerObject('', gdjs.RuntimeObject);
|
|
}
|