mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
655 lines
22 KiB
TypeScript
655 lines
22 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 {
|
|
export namespace evtTools {
|
|
export namespace object {
|
|
/**
|
|
* Keep only the specified object in the lists of picked objects.
|
|
*
|
|
* @param objectsLists The lists of objects to trim
|
|
* @param runtimeObject The object to keep in the lists
|
|
*/
|
|
export const pickOnly = function (
|
|
objectsLists: ObjectsLists,
|
|
runtimeObject: gdjs.RuntimeObject
|
|
) {
|
|
for (const listName in objectsLists.items) {
|
|
if (objectsLists.items.hasOwnProperty(listName)) {
|
|
const list = objectsLists.items[listName];
|
|
|
|
//Be sure not to lose the reference to the original array
|
|
if (list.indexOf(runtimeObject) === -1) {
|
|
list.length = 0;
|
|
} else {
|
|
list.length = 0;
|
|
|
|
//Be sure not to lose the reference to the original array
|
|
list.push(runtimeObject);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Do a test on two tables of objects so as to pick only the pair of objects for which the test is true.
|
|
*
|
|
* Note that the predicate method is not called strictly for each pair: When considering a pair of objects, if
|
|
* these objects have already been marked as picked, the predicate method won't be called again.
|
|
*
|
|
* Cost (Worst case, predicate being always false):
|
|
* Cost(Setting property 'picked' of NbObjList1+NbObjList2 objects to false)
|
|
* + Cost(predicate)*NbObjList1*NbObjList2
|
|
* + Cost(Testing NbObjList1+NbObjList2 booleans)
|
|
* + Cost(Removing NbObjList1+NbObjList2 objects from all the lists)
|
|
*
|
|
* Cost (Best case, predicate being always true):
|
|
* Cost(Setting property 'picked' of NbObjList1+NbObjList2 objects to false)
|
|
* + Cost(predicate)*(NbObjList1+NbObjList2)
|
|
* + Cost(Testing NbObjList1+NbObjList2 booleans)
|
|
*
|
|
*
|
|
* @param predicate The predicate function is called with the two objects to compare, and an optional argument `extraArg`
|
|
* @param objectsLists1 The first lists of objects
|
|
* @param objectsLists2 The second lists of objects
|
|
* @param inverted If `inverted` == true, only the objects of the first table are filtered.
|
|
* @param extraArg (optional) This argument should be used to avoid declaring the predicate as a closure that would be created and destroyed at each call to twoListsTest (potentially multiple time per frame).
|
|
*/
|
|
export const twoListsTest = function (
|
|
predicate: (
|
|
object1: gdjs.RuntimeObject,
|
|
object2: gdjs.RuntimeObject,
|
|
extraArg: any
|
|
) => boolean,
|
|
objectsLists1: ObjectsLists,
|
|
objectsLists2: ObjectsLists,
|
|
inverted: boolean,
|
|
extraArg: any
|
|
) {
|
|
let isTrue = false;
|
|
const objects1Lists = gdjs.staticArray(
|
|
gdjs.evtTools.object.twoListsTest
|
|
);
|
|
objectsLists1.values(objects1Lists);
|
|
const objects2Lists = gdjs.staticArray2(
|
|
gdjs.evtTools.object.twoListsTest
|
|
);
|
|
objectsLists2.values(objects2Lists);
|
|
for (let i = 0, leni = objects1Lists.length; i < leni; ++i) {
|
|
let arr = objects1Lists[i];
|
|
for (let k = 0, lenk = arr.length; k < lenk; ++k) {
|
|
arr[k].pick = false;
|
|
}
|
|
}
|
|
for (let i = 0, leni = objects2Lists.length; i < leni; ++i) {
|
|
let arr = objects2Lists[i];
|
|
for (let k = 0, lenk = arr.length; k < lenk; ++k) {
|
|
arr[k].pick = false;
|
|
}
|
|
}
|
|
|
|
//Launch the function for each object of the first list with each object
|
|
//of the second list.
|
|
for (let i = 0, leni = objects1Lists.length; i < leni; ++i) {
|
|
const arr1 = objects1Lists[i];
|
|
for (let k = 0, lenk = arr1.length; k < lenk; ++k) {
|
|
let atLeastOneObject = false;
|
|
for (let j = 0, lenj = objects2Lists.length; j < lenj; ++j) {
|
|
const arr2 = objects2Lists[j];
|
|
for (let l = 0, lenl = arr2.length; l < lenl; ++l) {
|
|
if (arr1[k].pick && arr2[l].pick) {
|
|
continue;
|
|
}
|
|
|
|
//Avoid unnecessary costly call to predicate.
|
|
if (
|
|
arr1[k].id !== arr2[l].id &&
|
|
predicate(arr1[k], arr2[l], extraArg)
|
|
) {
|
|
if (!inverted) {
|
|
isTrue = true;
|
|
|
|
//Pick the objects
|
|
arr1[k].pick = true;
|
|
arr2[l].pick = true;
|
|
}
|
|
atLeastOneObject = true;
|
|
}
|
|
}
|
|
}
|
|
if (!atLeastOneObject && inverted) {
|
|
//For example, the object is not overlapping any other object.
|
|
isTrue = true;
|
|
arr1[k].pick = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
//Trim not picked objects from lists.
|
|
for (let i = 0, leni = objects1Lists.length; i < leni; ++i) {
|
|
let arr = objects1Lists[i];
|
|
let finalSize = 0;
|
|
for (let k = 0, lenk = arr.length; k < lenk; ++k) {
|
|
let obj = arr[k];
|
|
if (arr[k].pick) {
|
|
arr[finalSize] = obj;
|
|
finalSize++;
|
|
}
|
|
}
|
|
arr.length = finalSize;
|
|
}
|
|
if (!inverted) {
|
|
for (let i = 0, leni = objects2Lists.length; i < leni; ++i) {
|
|
let arr = objects2Lists[i];
|
|
let finalSize = 0;
|
|
for (let k = 0, lenk = arr.length; k < lenk; ++k) {
|
|
let obj = arr[k];
|
|
if (arr[k].pick) {
|
|
arr[finalSize] = obj;
|
|
finalSize++;
|
|
}
|
|
}
|
|
arr.length = finalSize;
|
|
}
|
|
}
|
|
return isTrue;
|
|
};
|
|
|
|
/**
|
|
* Filter objects to keep only the one that fullfil the predicate
|
|
*
|
|
* Objects that do not fullfil the predicate are removed from objects lists.
|
|
*
|
|
* @param predicate The function applied to each object: must return true if the object fulfill the predicate.
|
|
* @param objectsLists The lists of objects to trim
|
|
* @param negatePredicate If set to true, the result of the predicate is negated.
|
|
* @param extraArg Argument passed to the predicate (along with the object). Useful for avoiding relying on temporary closures.
|
|
* @return true if at least one object fulfill the predicate.
|
|
*/
|
|
export const pickObjectsIf = function (
|
|
predicate: Function,
|
|
objectsLists: ObjectsLists,
|
|
negatePredicate: boolean,
|
|
extraArg: any
|
|
): boolean {
|
|
let isTrue = false;
|
|
const lists = gdjs.staticArray(gdjs.evtTools.object.pickObjectsIf);
|
|
objectsLists.values(lists);
|
|
|
|
// Pick only objects that are fulfilling the predicate.
|
|
for (let i = 0, leni = lists.length; i < leni; ++i) {
|
|
const arr = lists[i];
|
|
for (let k = 0, lenk = arr.length; k < lenk; ++k) {
|
|
const object = arr[k];
|
|
// @ts-ignore
|
|
if (negatePredicate ^ predicate(object, extraArg)) {
|
|
isTrue = true;
|
|
object.pick = true;
|
|
} else {
|
|
object.pick = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Trim not picked objects from lists.
|
|
for (let i = 0, leni = lists.length; i < leni; ++i) {
|
|
gdjs.evtTools.object.filterPickedObjectsList(lists[i]);
|
|
}
|
|
return isTrue;
|
|
};
|
|
|
|
/**
|
|
* Filter in-place the specified array to remove objects for which
|
|
* `pick` property is set to false.
|
|
*/
|
|
export const filterPickedObjectsList = function (
|
|
arr: gdjs.RuntimeObject[]
|
|
) {
|
|
let finalSize = 0;
|
|
for (let k = 0, lenk = arr.length; k < lenk; ++k) {
|
|
const obj = arr[k];
|
|
if (obj.pick) {
|
|
arr[finalSize] = obj;
|
|
finalSize++;
|
|
}
|
|
}
|
|
arr.length = finalSize;
|
|
};
|
|
|
|
export const hitBoxesCollisionTest = function (
|
|
objectsLists1: ObjectsLists,
|
|
objectsLists2: ObjectsLists,
|
|
inverted: boolean,
|
|
instanceContainer: gdjs.RuntimeInstanceContainer,
|
|
ignoreTouchingEdges: boolean
|
|
) {
|
|
return gdjs.evtTools.object.twoListsTest(
|
|
gdjs.RuntimeObject.collisionTest,
|
|
objectsLists1,
|
|
objectsLists2,
|
|
inverted,
|
|
ignoreTouchingEdges
|
|
);
|
|
};
|
|
|
|
export const _distanceBetweenObjects = function (obj1, obj2, distance) {
|
|
return obj1.getSqDistanceToObject(obj2) <= distance;
|
|
};
|
|
|
|
export const distanceTest = function (
|
|
objectsLists1: ObjectsLists,
|
|
objectsLists2: ObjectsLists,
|
|
distance: float,
|
|
inverted: boolean
|
|
) {
|
|
return gdjs.evtTools.object.twoListsTest(
|
|
gdjs.evtTools.object._distanceBetweenObjects,
|
|
objectsLists1,
|
|
objectsLists2,
|
|
inverted,
|
|
distance * distance
|
|
);
|
|
};
|
|
|
|
export const _movesToward = function (obj1, obj2, tolerance) {
|
|
if (obj1.hasNoForces()) {
|
|
return false;
|
|
}
|
|
let objAngle = Math.atan2(
|
|
obj2.getDrawableY() +
|
|
obj2.getCenterY() -
|
|
(obj1.getDrawableY() + obj1.getCenterY()),
|
|
obj2.getDrawableX() +
|
|
obj2.getCenterX() -
|
|
(obj1.getDrawableX() + obj1.getCenterX())
|
|
);
|
|
objAngle *= 180 / 3.14159;
|
|
return (
|
|
Math.abs(
|
|
gdjs.evtTools.common.angleDifference(
|
|
obj1.getAverageForce().getAngle(),
|
|
objAngle
|
|
)
|
|
) <=
|
|
tolerance / 2
|
|
);
|
|
};
|
|
|
|
export const movesTowardTest = function (
|
|
objectsLists1: ObjectsLists,
|
|
objectsLists2: ObjectsLists,
|
|
tolerance: float,
|
|
inverted: boolean
|
|
) {
|
|
return gdjs.evtTools.object.twoListsTest(
|
|
gdjs.evtTools.object._movesToward,
|
|
objectsLists1,
|
|
objectsLists2,
|
|
inverted,
|
|
tolerance
|
|
);
|
|
};
|
|
|
|
export const _turnedToward = function (obj1, obj2, tolerance) {
|
|
let objAngle = Math.atan2(
|
|
obj2.getDrawableY() +
|
|
obj2.getCenterY() -
|
|
(obj1.getDrawableY() + obj1.getCenterY()),
|
|
obj2.getDrawableX() +
|
|
obj2.getCenterX() -
|
|
(obj1.getDrawableX() + obj1.getCenterX())
|
|
);
|
|
objAngle *= 180 / 3.14159;
|
|
return (
|
|
Math.abs(
|
|
gdjs.evtTools.common.angleDifference(obj1.getAngle(), objAngle)
|
|
) <=
|
|
tolerance / 2
|
|
);
|
|
};
|
|
|
|
export const turnedTowardTest = function (
|
|
objectsLists1,
|
|
objectsLists2,
|
|
tolerance,
|
|
inverted
|
|
) {
|
|
return gdjs.evtTools.object.twoListsTest(
|
|
gdjs.evtTools.object._turnedToward,
|
|
objectsLists1,
|
|
objectsLists2,
|
|
inverted,
|
|
tolerance
|
|
);
|
|
};
|
|
|
|
export const pickAllObjects = function (objectsContext, objectsLists) {
|
|
for (const name in objectsLists.items) {
|
|
if (objectsLists.items.hasOwnProperty(name)) {
|
|
const allObjects = objectsContext.getObjects(name);
|
|
const objectsList = objectsLists.items[name];
|
|
objectsList.length = 0;
|
|
objectsList.push.apply(objectsList, allObjects);
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
export const pickRandomObject = function (
|
|
instanceContainer: gdjs.RuntimeInstanceContainer,
|
|
objectsLists: ObjectsLists
|
|
) {
|
|
// Compute one many objects we have
|
|
let objectsCount = 0;
|
|
for (let listName in objectsLists.items) {
|
|
if (objectsLists.items.hasOwnProperty(listName)) {
|
|
let list = objectsLists.items[listName];
|
|
objectsCount += list.length;
|
|
}
|
|
}
|
|
if (objectsCount === 0) {
|
|
return false;
|
|
}
|
|
|
|
// Pick one random object
|
|
let index = Math.floor(Math.random() * objectsCount);
|
|
if (index >= objectsCount) {
|
|
index = objectsCount - 1;
|
|
}
|
|
|
|
//Should never happen.
|
|
|
|
// Find the object
|
|
let startIndex = 0;
|
|
let theChosenOne: gdjs.RuntimeObject | null = null;
|
|
for (let listName in objectsLists.items) {
|
|
if (objectsLists.items.hasOwnProperty(listName)) {
|
|
let list = objectsLists.items[listName];
|
|
if (index - startIndex < list.length) {
|
|
theChosenOne = list[index - startIndex];
|
|
break;
|
|
}
|
|
startIndex += list.length;
|
|
}
|
|
}
|
|
// @ts-ignore
|
|
gdjs.evtTools.object.pickOnly(objectsLists, theChosenOne);
|
|
return true;
|
|
};
|
|
|
|
export const pickNearestObject = function (objectsLists, x, y, inverted) {
|
|
let bestObject = null;
|
|
let best = 0;
|
|
let first = true;
|
|
const lists = gdjs.staticArray(gdjs.evtTools.object.pickNearestObject);
|
|
objectsLists.values(lists);
|
|
for (let i = 0, len = lists.length; i < len; ++i) {
|
|
const list = lists[i];
|
|
for (let j = 0; j < list.length; ++j) {
|
|
const object = list[j];
|
|
const distance = object.getSqDistanceToPosition(x, y);
|
|
// @ts-ignore
|
|
if (first || (distance < best) ^ inverted) {
|
|
best = distance;
|
|
bestObject = object;
|
|
}
|
|
first = false;
|
|
}
|
|
}
|
|
if (!bestObject) {
|
|
return false;
|
|
}
|
|
gdjs.evtTools.object.pickOnly(objectsLists, bestObject);
|
|
return true;
|
|
};
|
|
|
|
export const raycastObject = function (
|
|
objectsLists: ObjectsLists,
|
|
x: float,
|
|
y: float,
|
|
angle: float,
|
|
dist: float,
|
|
varX: gdjs.Variable,
|
|
varY: gdjs.Variable,
|
|
inverted: boolean
|
|
) {
|
|
return gdjs.evtTools.object.raycastObjectToPosition(
|
|
objectsLists,
|
|
x,
|
|
y,
|
|
x + dist * Math.cos((angle * Math.PI) / 180.0),
|
|
y + dist * Math.sin((angle * Math.PI) / 180.0),
|
|
varX,
|
|
varY,
|
|
inverted
|
|
);
|
|
};
|
|
|
|
export const raycastObjectToPosition = function (
|
|
objectsLists: ObjectsLists,
|
|
x: float,
|
|
y: float,
|
|
endX: float,
|
|
endY: float,
|
|
varX: gdjs.Variable,
|
|
varY: gdjs.Variable,
|
|
inverted: boolean
|
|
) {
|
|
let matchObject: gdjs.RuntimeObject | null = null;
|
|
let testSqDist = inverted
|
|
? 0
|
|
: (endX - x) * (endX - x) + (endY - y) * (endY - y);
|
|
let resultX = 0;
|
|
let resultY = 0;
|
|
const lists: RuntimeObject[][] = gdjs.staticArray(
|
|
gdjs.evtTools.object.raycastObjectToPosition
|
|
);
|
|
objectsLists.values(lists);
|
|
for (let i = 0; i < lists.length; i++) {
|
|
const list = lists[i];
|
|
for (let j = 0; j < list.length; j++) {
|
|
const object = list[j];
|
|
const result = object.raycastTest(x, y, endX, endY, !inverted);
|
|
if (result.collision) {
|
|
if (!inverted && result.closeSqDist <= testSqDist) {
|
|
testSqDist = result.closeSqDist;
|
|
matchObject = object;
|
|
resultX = result.closeX;
|
|
resultY = result.closeY;
|
|
} else {
|
|
if (inverted && result.farSqDist >= testSqDist) {
|
|
testSqDist = result.farSqDist;
|
|
matchObject = object;
|
|
resultX = result.farX;
|
|
resultY = result.farY;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!matchObject) {
|
|
return false;
|
|
}
|
|
gdjs.evtTools.object.pickOnly(objectsLists, matchObject);
|
|
varX.setNumber(resultX);
|
|
varY.setNumber(resultY);
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Do the work of creating a new object
|
|
*/
|
|
export const doCreateObjectOnScene = function (
|
|
objectsContext: EventsFunctionContext | gdjs.RuntimeScene,
|
|
objectName: string,
|
|
objectsLists: ObjectsLists,
|
|
x: float,
|
|
y: float,
|
|
layerName: string
|
|
) {
|
|
// objectsContext will either be the gdjs.RuntimeScene or, in an events function, the
|
|
// eventsFunctionContext. We can't directly use runtimeScene because the object name could
|
|
// be different than the real object name (this is the case in a function. The eventsFunctionContext
|
|
// will take care of this in createObject).
|
|
const obj = objectsContext.createObject(objectName);
|
|
const layer = objectsContext.getLayer(layerName);
|
|
if (obj !== null) {
|
|
//Do some extra setup
|
|
obj.setPosition(x, y);
|
|
obj.setLayer(layerName);
|
|
obj.setZOrder(layer.getDefaultZOrder());
|
|
|
|
//Let the new object be picked by next actions/conditions.
|
|
if (objectsLists.containsKey(objectName)) {
|
|
objectsLists.get(objectName).push(obj);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Allows events to create a new object on a scene.
|
|
*/
|
|
export const createObjectOnScene = function (
|
|
objectsContext: EventsFunctionContext | gdjs.RuntimeScene,
|
|
objectsLists: ObjectsLists,
|
|
x: float,
|
|
y: float,
|
|
layerName: string
|
|
) {
|
|
gdjs.evtTools.object.doCreateObjectOnScene(
|
|
objectsContext,
|
|
objectsLists.firstKey() as string,
|
|
objectsLists,
|
|
x,
|
|
y,
|
|
layerName
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Allows events to create a new object on a scene.
|
|
*/
|
|
export const createObjectFromGroupOnScene = function (
|
|
objectsContext: EventsFunctionContext | gdjs.RuntimeScene,
|
|
objectsLists: ObjectsLists,
|
|
objectName: string,
|
|
x: float,
|
|
y: float,
|
|
layerName: string
|
|
) {
|
|
gdjs.evtTools.object.doCreateObjectOnScene(
|
|
objectsContext,
|
|
objectName,
|
|
objectsLists,
|
|
x,
|
|
y,
|
|
layerName
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Return the number of instances in the specified lists of objects.
|
|
*/
|
|
export const getPickedInstancesCount = (objectsLists: ObjectsLists) => {
|
|
let count = 0;
|
|
const lists = gdjs.staticArray(
|
|
gdjs.evtTools.object.getPickedInstancesCount
|
|
);
|
|
objectsLists.values(lists);
|
|
for (let i = 0, len = lists.length; i < len; ++i) {
|
|
count += lists[i].length;
|
|
}
|
|
return count;
|
|
};
|
|
|
|
/**
|
|
* Return the number of instances of the specified objects living on the scene.
|
|
*/
|
|
export const getSceneInstancesCount = (
|
|
objectsContext: EventsFunctionContext | gdjs.RuntimeScene,
|
|
objectsLists: ObjectsLists
|
|
) => {
|
|
let count = 0;
|
|
|
|
const objectNames = gdjs.staticArray(
|
|
gdjs.evtTools.object.getSceneInstancesCount
|
|
);
|
|
objectsLists.keys(objectNames);
|
|
|
|
const uniqueObjectNames = new Set(objectNames);
|
|
for (const objectName of uniqueObjectNames) {
|
|
count += objectsContext.getInstancesCountOnScene(objectName);
|
|
}
|
|
return count;
|
|
};
|
|
|
|
/** @deprecated */
|
|
export const pickedObjectsCount = getPickedInstancesCount;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A container for objects lists that should last more than the current frame.
|
|
* It automatically removes objects that were destroyed from the objects lists.
|
|
*/
|
|
export class LongLivedObjectsList {
|
|
private objectsLists = new Map<string, Array<RuntimeObject>>();
|
|
private callbacks = new Map<RuntimeObject, () => void>();
|
|
private parent: LongLivedObjectsList | null = null;
|
|
|
|
/**
|
|
* Create a new container for objects lists, inheriting from another one. This is
|
|
* useful should we get the objects that have not been saved in this context (using
|
|
* `addObject`) but saved in a parent context.
|
|
* This avoids to save all object lists every time we create a new `LongLivedObjectsList`,
|
|
* despite not all objects lists being used.
|
|
*
|
|
* @param parent
|
|
* @returns
|
|
*/
|
|
static from(parent: LongLivedObjectsList): LongLivedObjectsList {
|
|
const newList = new LongLivedObjectsList();
|
|
newList.parent = parent;
|
|
return newList;
|
|
}
|
|
|
|
private getOrCreateList(objectName: string): RuntimeObject[] {
|
|
if (!this.objectsLists.has(objectName))
|
|
this.objectsLists.set(objectName, []);
|
|
return this.objectsLists.get(objectName)!;
|
|
}
|
|
|
|
getObjects(objectName: string): RuntimeObject[] {
|
|
if (!this.objectsLists.has(objectName) && this.parent)
|
|
return this.parent.getObjects(objectName);
|
|
return this.objectsLists.get(objectName) || [];
|
|
}
|
|
|
|
addObject(objectName: string, runtimeObject: gdjs.RuntimeObject): void {
|
|
const list = this.getOrCreateList(objectName);
|
|
if (list.includes(runtimeObject)) return;
|
|
list.push(runtimeObject);
|
|
|
|
// Register callbacks for when the object is destroyed
|
|
const onDestroy = () => this.removeObject(objectName, runtimeObject);
|
|
this.callbacks.set(runtimeObject, onDestroy);
|
|
runtimeObject.registerDestroyCallback(onDestroy);
|
|
}
|
|
|
|
removeObject(objectName: string, runtimeObject: gdjs.RuntimeObject): void {
|
|
const list = this.getOrCreateList(objectName);
|
|
const index = list.indexOf(runtimeObject);
|
|
if (index === -1) return;
|
|
list.splice(index, 1);
|
|
|
|
// Properly remove callbacks to not leak the object
|
|
runtimeObject.unregisterDestroyCallback(
|
|
this.callbacks.get(runtimeObject)!
|
|
);
|
|
this.callbacks.delete(runtimeObject);
|
|
}
|
|
}
|
|
}
|