import { CabriIntegration } from "./cabri-integration";
import { CabriDataService } from "../services/cabri-data.service";
import { GlobalService } from "../services/global.service";
import { NgZone, Renderer2 } from "@angular/core";
import { JeuJustePointPage, justePointActivity, operation } from "../page/jeu-juste-point/jeu-juste-point.page";
import {
BoundingBox,
DynamicTexture,
Mesh,
PickingInfo,
StandardMaterial,
Vector3,
SubMesh,
AssetsManager,
AssetContainer,
AbstractAssetTask,
MeshAssetTask,
ImageAssetTask,
TextureAssetTask,
AbstractMesh,
Texture,
MultiMaterial,
Observer,
PointerInfo,
Animatable,
Color3,
Material,
HighlightLayer,
EventState,
Animation,
Scene,
MeshBuilder
} from "babylon4.1";
import { AppUtils, Fraction } from "../app-utils";
import { BabylonJsConfetti, ParticleAnimName } from "./babylonjs-confetti";
// import { JeuJustePointParticles } from "mathjax-angular/types";
import { Subject } from "rxjs";
import { Button, Vector2WithInfo } from "@babylonjs/gui";
import { Control } from "gui4.1";
declare var window: {
store: any;
document: any;
innerWidth: any;
innerHeight: any;
outerWidth: any;
outerHeight: any;
URL: any;
btoa: any;
devicePixelRatio: number;
};
export class CollisionPosition {
positionTopY: number;
positionBottomY: number;
positionLeftX: number;
positionRightX: number;
}
export class TargetMesh extends Mesh {
value: number | Fraction;
rangeValue: any;
draggable: boolean;
index: number;
color: string;
remediation: boolean;
initialPosition: Vector3;
visible: boolean;
diffuseTexture: any;
}
export class TargetSubMesh extends SubMesh {
value: number | Fraction;
visible: boolean;
}
export enum DistanceUnit {
Mm = "Mm",
Dm = "Dm",
Cm = "Cm",
M = "M",
Dam = "Dam",
Hm = "Hm",
Km = "km"
}
declare var MathJax: {
startup: any;
svg: any;
tex: any;
tex2svg: any;
tex2svgPromise: any;
loader: any;
Hub: any;
config: any;
tex2mml: any;
defsGlobalCaching: any;
};
declare var BABYLON;
export interface UserAnswer {
success: boolean;
response: number | Fraction | ResultOnTheRoad;
reversed?: boolean;
}
export class ResultOnTheRoad {
constructor(error: { color: string; correctValue: number | Fraction }[]) {
this.carsError = error;
}
carsError: { color: string; correctValue: number | Fraction }[];
}
export class CabriIntegrationJeuJustePoint extends CabriIntegration {
generatedMeshElement: Mesh[] = [];
stepXRange: number;
bigScaleBars: TargetMesh[] = [];
smallScaleBars: TargetMesh[] = [];
tubesMesh: Mesh[] = [];
targetMeshBrokenBasket: TargetMesh[] = [];
targetMeshHoop: TargetMesh[] = [];
background: Mesh;
targetSubMesh: TargetSubMesh[] = [];
targetMesh: TargetMesh[] = [];
linesXPos: number;
nbBasketItemsRounded: number;
basketTotalRange: number;
KPosttargetMesh: TargetMesh[] = [];
draggablePosition: Vector3;
basketBallMesh: TargetMesh;
defaultBallPosition: Vector3;
defaultBallRotation: Vector3;
defaultBallScaling: Vector3;
assetsManagerOnTheRoad: AssetsManager;
assetsContainerOnTheRoad: AssetContainer;
assetsManagerBasket: AssetsManager;
assetsContainerBasket: AssetContainer;
images: Map<string, HTMLImageElement>;
textures: Map<string, Texture>;
materials: Map<string, StandardMaterial>;
meshs: Map<string, AbstractMesh>;
pointerObs: Observer<PointerInfo>;
numberOfElementsDisplayed = new Array();
currentMesh: Mesh;
fractionResult: string;
linePositionY: number;
hl: HighlightLayer;
basketRatioElement: number;
remediationTargettedIndex: number;
brokenbasket: Mesh;
hoop: Mesh;
basketUnitStepPrecision: number;
hoopNetObservable: Observer<Scene>;
public waitUserAnswer: Subject<UserAnswer>;
private babylonLoaded: any;
advancedTexture;
bubble;
textBlock;
consigne: string[];
buttonValider: Button;
color: { color: Color3; name: string }[];
alpha: number;
remediationPlanes: Mesh[];
initialMaterials: Material[];
mascotteRotation: Vector3;
consigneScenariosArray: string[];
lastCalled: number;
constructor(
public cabriService: CabriDataService,
public globalService: GlobalService,
public _ngZone: NgZone,
public page: JeuJustePointPage,
public renderer: Renderer2
) {
super(cabriService, globalService, _ngZone, page, renderer);
this.page = page;
this.cabriRenderStarted = true;
// (window as any).justePoint = this;
}
onBabylonReady(): Promise<void> {
return new Promise<void>(async (resolve, reject) => {
await super.onBabylonReady();
await this.waitCamera();
this.customLoadingScreen();
await this.importMascotteMeshes(1.14, 1.2).then(() => {
const mascotteXUnit = 0.1;
this.mascotteRotation = new Vector3(
this.initialCameraPosition.beta + mascotteXUnit - this.initialCameraPosition.beta,
0,
0
);
});
this.setOrthographicCamera();
this.addEventPointerOnCanvas();
await this.updateCabriPosition(this.cabriService.holoMode);
// this.scene.debugLayer.show({ embedMode: true });
this.enableDragAndDrop();
this.saveInitialSceneMaterial();
await this.launchActivity();
resolve();
});
}
saveInitialSceneMaterial() {
this.initialMaterials = this.scene.materials.slice();
this.initialMaterials = this.initialMaterials.concat(this.scene.multiMaterials.slice());
}
addEventPointerOnCanvas() {
super.addEventPointerOnCanvas();
(window.store.getters.cabri.Canvas as HTMLCanvasElement).eventListeners("wheel").forEach(eventListener => {
(window.store.getters.cabri.Canvas as HTMLCanvasElement).removeEventListener("wheel", eventListener);
});
}
async launchActivity() {
if (this.page.variable.activity === justePointActivity.basket) {
await this.buildElementActivityBasket();
} else {
await this.buildElementActivityOnTheRoad();
}
}
getBackground(): Mesh {
if (!this.background) {
window.store.getters.cabri.Scene.meshes.forEach(async m => {
if (m.id === "background") {
this.background = m;
}
});
}
return this.background;
}
getCurrentOperation(): string {
let text: string;
if (!this.page.firstTime) {
switch (this.page.justePointExercices.exoType) {
case justePointActivity.basket:
const pasApproximation = this.page.variable.chooseExactNumber ? "Oui" : "Non";
text =
"Activité Juste point -Basket- Difficulté : " +
this.page.variable.difficulty +
", Pas d'approximation : " +
pasApproximation +
", Borne Min : " +
this.page.variable.nbMin.toString() +
", Borne Max : " +
this.page.variable.nbMax.toString() +
", Pas : " +
this.page.variable.unitStep.toString() +
", Resultat : ";
if (Array.isArray(this.page.variable.expectedResult)) {
this.page.variable.expectedResult.forEach(resultat => {
text += "'" + resultat.toString() + "'";
});
} else {
text += "'" + this.page.variable.expectedResult.toString() + "'";
}
break;
case justePointActivity.onTheRoad:
text =
"Activité Juste point -Route- Difficulté : " +
this.page.variable.difficulty +
", Pas de ligne centrale : " +
this.page.variable.noRoadHelp +
", Borne Min : " +
this.page.variable.nbMin.toString() +
", Borne Max : " +
this.page.variable.nbMax.toString() +
", Pas : " +
this.page.variable.unitStep.toString() +
" Resultat : ";
if (Array.isArray(this.page.variable.expectedResult)) {
this.page.variable.expectedResult.forEach(resultat => {
text += "'" + resultat.toString() + "'";
});
} else {
text += "'" + this.page.variable.expectedResult.toString() + "'";
}
break;
default:
break;
}
}
return text;
}
buildElementActivityBasket(): Promise<void> {
return new Promise(resolve => {
this.importAssetBasket().then(async () => {
this.brokenbasket = this.meshs.get("Backboard") as Mesh;
this.hoop = this.meshs.get("Hoop") as Mesh;
this.page.expectedResultIsFractionHTML = this.page.variable?.expectedResult instanceof Fraction;
// this.basketUnitStepPrecision = this.getStepPrecision();
// console.log("this.basketUnitStepPrecision", this.basketUnitStepPrecision);
this.generateBasketBall();
await this.generateStraightLine(22);
this.generateBrokenBasket();
this.assetsContainerBasket.addAllToScene();
resolve();
});
});
}
importAssetBasket(): Promise<void> {
this.assetsManagerBasket = new BABYLON.AssetsManager(this.scene);
this.assetsManagerBasket.useDefaultLoadingScreen = false;
this.assetsContainerBasket = new BABYLON.AssetContainer(this.scene);
this.importMeshBasket();
this.importTexturesBasket();
this.assetsManagerBasket.onFinish = this.importFinishBasket.bind(this);
return this.assetsManagerBasket.loadAsync();
}
importTexturesBasket() {
const textureMap = [
{ name: "brokenBasket", url: "assets/gabarits/jeu_juste_point/backboard.png" },
{ name: "brokenBasketCurrent", url: "assets/gabarits/jeu_juste_point/backboardCurrent.png" }
];
textureMap.forEach(texture => {
this.assetsManagerBasket.addTextureTask(texture.name, texture.url);
});
}
importMeshBasket() {
const mMeshs = [
{ name: "basket_ball", url: "/assets/gabarits/jeu_juste_point/", filename: "basket_ball.babylon" },
{
name: "hoop",
url: "/assets/gabarits/jeu_juste_point/",
filename: "hoop" + (this.globalService.lowPerformanceMode ? "_ios" : "") + ".babylon"
}
];
mMeshs.forEach(mesh => {
this.assetsManagerBasket.addMeshTask(mesh.name, null, mesh.url, mesh.filename);
});
}
importFinishBasket(tasks: AbstractAssetTask[]) {
this.textures = new Map<string, Texture>();
this.images = new Map<string, HTMLImageElement>();
this.meshs = new Map<string, AbstractMesh>();
tasks.forEach(task => {
if (task.isCompleted) {
if (task instanceof BABYLON.MeshAssetTask) {
const lTask = task as MeshAssetTask;
this.assetsContainerBasket.meshes = this.assetsContainerBasket.meshes.concat(lTask.loadedMeshes);
lTask.loadedMeshes.forEach(mesh => {
this.meshs.set(mesh.name, mesh);
if (mesh.material) {
if (mesh.material instanceof BABYLON.MultiMaterial) {
this.assetsContainerBasket.multiMaterials.push(mesh.material as MultiMaterial);
} else {
this.assetsContainerBasket.materials.push(mesh.material);
this.assetsContainerBasket.textures = this.assetsContainerBasket.textures.concat(
mesh.material.getActiveTextures()
);
}
}
});
} else if (task instanceof BABYLON.ImageAssetTask) {
const lTask = task as ImageAssetTask;
this.images.set(task.name, lTask.image);
} else if (task instanceof BABYLON.TextureAssetTask) {
const lTask = task as TextureAssetTask;
this.assetsContainerBasket.textures.push(lTask.texture);
this.textures.set(lTask.name, lTask.texture);
} else {
console.error("Task Not Handle", task);
}
} else {
console.error(task.errorObject);
}
});
}
/**
* RESIZE CABRI (from CM) -> Update DOM Cabri elements
* @param holoMode Holo mode
* @param platform Platform
*/
async updateCabriPosition(holoMode: string = null): Promise<void> {
if (this.page.variable?.activity === justePointActivity.basket) {
this.cabriService.forceBackgroundTexture =
"/assets/gabarits/jeu_juste_point/background_basket" + (this.globalService.lowPerformanceMode ? "_ios" : "") + ".jpg";
} else {
this.cabriService.forceBackgroundTexture = null;
}
return await super.updateCabriPosition(holoMode, "jeuJustePoint").then(()=>this.setBubbleTopPosition());
}
async updateCabriBackground(renderCanvas, condition) {
window.store.getters.cabri.Scene.meshes.forEach(async m => {
if (m.id === "background") {
this.background = m;
this.checkFlatHoloMode(m);
await this.switchBackgroundLauncher(m, renderCanvas.clientHeight, renderCanvas.clientWidth);
console.log("cabri background resized");
this.setBackgroundPosition();
}
});
}
/**
* Remediation meshes to be visible
*/
keepRemediationElements() {
if (this.page.variable.activity === justePointActivity.basket) {
this.assetsContainerBasket.meshes.forEach((currentMesh: TargetMesh) => {
currentMesh.isVisible = currentMesh.remediation ? true : false;
});
}
}
/**
* Show or not meshes according to remediation/newQuestion/scenario/mascotte anim
*/
displayMeshes(show: boolean) {
if (this.page.variable.activity === justePointActivity.basket) {
if (this.assetsContainerBasket) {
this.assetsContainerBasket.meshes.forEach((currentMesh: TargetMesh) => {
currentMesh.isVisible = show;
});
}
} else {
if (this.assetsContainerOnTheRoad) {
this.assetsContainerOnTheRoad.meshes.forEach((currentMesh: TargetMesh) => {
currentMesh.isVisible = show;
currentMesh.getChildMeshes(false).forEach(element => {
element.isVisible = show;
});
});
if (this.advancedTexture) {
this.advancedTexture.rootContainer.isVisible = show;
}
}
}
}
checkActivityChange(): boolean {
return true;
}
saveDom() {
super.saveDom();
this.cabriService.currentSavedDom = "jeuJustePoint";
}
restoreDom() {
super.restoreDom();
this.startRender();
}
clearResponse(mode: string, value: any): Promise<any> {
return;
}
setResponse(mode: string, value: any) {
// display response
}
onScenereadyInit() {
this.scene = window.store.getters.cabri.Scene;
this.engine = window.store.getters.cabri.Engine;
}
/**
* Get background mesh picked point infos when mouse is over the mesh
*/
getGroundPosition(): Vector3 {
const pickinfo = this.scene.pick(
this.scene.pointerX,
this.scene.pointerY,
this.page.variable.activity === justePointActivity.onTheRoad ? this.isRoadGroundMesh : null,
true,
this.page.variable.activity === justePointActivity.onTheRoad ? this.camera : this.cameraHud
);
if (pickinfo.hit) {
return pickinfo.pickedPoint;
}
return null;
}
isRoadGroundMesh(mesh: AbstractMesh) {
const elligibleBackgroundMesh = ["leftRoad", "rightRoad", "upperGround", "lowerGround", "road"];
return elligibleBackgroundMesh.includes(mesh.name);
}
pointerDown(mesh: Mesh) {
this.currentMesh = mesh;
this.draggablePosition = this.getGroundPosition();
}
pointerMove() {
if (this.currentMesh && this.draggablePosition) {
const current = this.getGroundPosition();
if (current) {
const diff = current.subtract(this.draggablePosition);
this.currentMesh.position.addInPlace(diff);
this.draggablePosition = current;
}
if (this.page.variable.activity === justePointActivity.basket) {
this.checkHoopCollisionResult(false);
}
} else if (this.currentMesh) {
const isValidPosition = this.getGroundPosition();
if (isValidPosition) {
const diff = isValidPosition.subtract(this.currentMesh.position);
if (Math.abs(diff.y) > 0) {
diff.z = +diff.y;
diff.y = 0;
}
this.currentMesh.position.addInPlace(diff);
this.draggablePosition = isValidPosition;
}
}
}
async pointerUp() {
this.draggablePosition = null;
if (this.currentMesh) {
if (this.page.variable.activity === justePointActivity.basket) {
this.checkHoopCollisionResult(true);
if (this.page.variable.chooseExactNumber) {
this.checkBigBarsCollisionResult(false, true);
} else {
this.checkSmallBarsCollisionResult();
}
} else {
// Todo make road collision
this.checkRoadCollision();
}
this.currentMesh = null;
}
}
/**
* Collision and define of correct answer
*/
checkHoopCollisionResult(validateAnswer: boolean) {
// const sourceMeshPosition = this.getBallPosition();
const souceMesh = this.getBallPosition(true);
for (const mesh of this.targetMeshBrokenBasket) {
const sizeMesh = this.getMeshSizeInfos(mesh);
const targetMesh = {
positionTopY: mesh.position.y + (sizeMesh.extendSizeWorld.y * 2) / 1.2,
positionBottomY: mesh.position.y,
positionLeftX: mesh.position.x - sizeMesh.extendSizeWorld.x,
positionRightX: mesh.position.x + sizeMesh.extendSizeWorld.x
};
const collision = this.checkResponse(targetMesh, souceMesh);
if (collision) {
if ((mesh.material as any).diffuseTexture.name !== this.textures.get("brokenBasketCurrent").url) {
mesh.material = this.swapBrokenBasketMaterial("brokenBasketCurrent");
}
if (validateAnswer) {
mesh.material = this.swapBrokenBasketMaterial("brokenBasket");
this.manageBallAnimation(mesh);
}
break;
} else {
// Remove all materials
for (const currMesh of this.targetMeshBrokenBasket) {
if ((currMesh.material as any).diffuseTexture.name !== this.textures.get("brokenBasket").url) {
currMesh.material = this.swapBrokenBasketMaterial("brokenBasket");
}
}
}
}
}
checkSmallBarsCollisionResult() {
const touchedMesh = this.checkBigBarsCollisionResult(true, false);
if (touchedMesh) {
this.manageBallAnimation(touchedMesh, true);
} else {
const souceMesh = this.getBallPosition(false);
for (const mesh of this.smallScaleBars) {
const sizeMesh = this.getMeshSizeInfos(mesh);
const targetMeshPosition = {
positionTopY: mesh.position.y + sizeMesh.extendSizeWorld.y,
positionBottomY: mesh.position.y - sizeMesh.extendSizeWorld.y,
positionLeftX: mesh.position.x - sizeMesh.extendSizeWorld.x,
positionRightX: mesh.position.x + sizeMesh.extendSizeWorld.x / 2 // half distance from right avoid overlap two meshes
};
const responded = this.checkResponse(targetMeshPosition, souceMesh, mesh);
if (responded) {
this.manageBallAnimation(mesh);
break;
}
}
}
}
checkBigBarsCollisionResult(ballCenterPosition: boolean, launchAnim: boolean) {
const souceMesh = this.getBallPosition(false);
if (ballCenterPosition) {
const targetSize = this.getMeshSizeInfos(this.bigScaleBars[0]);
souceMesh.positionLeftX = this.basketBallMesh.position.x + targetSize.extendSizeWorld.x;
souceMesh.positionRightX = this.basketBallMesh.position.x - targetSize.extendSizeWorld.x;
}
for (const mesh of this.bigScaleBars) {
const sizeMesh = this.getMeshSizeInfos(mesh);
const targetMeshPosition = {
positionTopY: mesh.position.y + sizeMesh.extendSizeWorld.y,
positionBottomY: mesh.position.y - sizeMesh.extendSizeWorld.y,
positionLeftX: mesh.position.x - sizeMesh.extendSizeWorld.x,
positionRightX: mesh.position.x + sizeMesh.extendSizeWorld.x
};
const responded = this.checkResponse(targetMeshPosition, souceMesh, mesh);
if (responded) {
if (launchAnim) {
this.manageBallAnimation(mesh);
break;
} else {
return mesh;
}
}
}
}
/**
* Collision between ball and hoop
*/
checkResponse(targetMesh: CollisionPosition, souceMesh: CollisionPosition, mesh?) {
if (
souceMesh.positionTopY >= targetMesh.positionBottomY &&
souceMesh.positionBottomY <= targetMesh.positionTopY &&
souceMesh.positionLeftX >= targetMesh.positionLeftX &&
souceMesh.positionRightX <= targetMesh.positionRightX
) {
return true;
}
}
getBallPosition(ballCenterOrigin: boolean): CollisionPosition {
const currentMesh = this.getMeshSizeInfos(this.basketBallMesh);
let positionLeftX: number;
let positionRightX: number;
if (ballCenterOrigin) {
positionRightX = positionLeftX = this.basketBallMesh.position.x;
} else {
positionLeftX = this.basketBallMesh.position.x + currentMesh.extendSizeWorld.x;
positionRightX = this.basketBallMesh.position.x - currentMesh.extendSizeWorld.x;
}
return {
positionLeftX,
positionRightX,
positionBottomY: this.basketBallMesh.position.y - currentMesh.extendSizeWorld.y,
positionTopY: this.basketBallMesh.position.y + currentMesh.extendSizeWorld.y
};
}
async manageBallAnimation(mesh: TargetMesh, forceExactValue: boolean = false) {
const { success, targettedBrokenMesh } = this.checkResponsedMeshValidity(mesh, forceExactValue);
// remove pointer observable
this.scene.onPointerObservable.remove(this.pointerObs);
this.basketBallMesh.position = this.defaultBallPosition.clone();
this.basketBallMesh.rotation = this.defaultBallRotation.clone();
this.basketBallMesh.scaling = this.defaultBallScaling.clone();
this.basketBallMesh.scaling.x = this.basketBallMesh.scaling.y = this.basketBallMesh.scaling.z = 5;
const resultAnim = this.launchBallAnimation(targettedBrokenMesh, success);
resultAnim.onAnimationEnd = async () => {
this.basketBallMesh.animations = new Array();
if (success) {
this.basketBallMesh.isVisible = false;
this.generateConfetti(targettedBrokenMesh.position);
}
await AppUtils.timeOut(300);
if (this.hoopNetObservable) {
this.scene.onAfterRenderObservable.remove(this.hoopNetObservable);
this.hoopNetObservable = null;
}
this.basketBallMesh.scaling = this.defaultBallScaling.clone();
this.basketBallMesh.position = this.defaultBallPosition.clone();
this.basketBallMesh.rotation = this.defaultBallRotation.clone();
this.enableDragAndDrop();
let userAnswer;
if (success) {
userAnswer = this.page.variable.expectedResult.valueOf();
} else {
userAnswer = targettedBrokenMesh.value;
}
this.waitUserAnswer.next({ success, response: userAnswer });
// enable pointer observable
};
}
checkResponsedMeshValidity(mesh: TargetMesh, forceExactValue: boolean) {
let success = false;
let targettedBrokenMesh: TargetMesh;
if (this.page.variable.chooseExactNumber || forceExactValue) {
if (forceExactValue) {
const index = this.bigScaleBars.findIndex(currMesh => {
return currMesh.value === mesh.value;
});
const isLastElement = index === this.bigScaleBars.length - 1;
targettedBrokenMesh = this.targetMeshBrokenBasket.find(currMesh => {
return mesh.value === (isLastElement ? currMesh.rangeValue.to : currMesh.rangeValue.inital);
});
} else {
targettedBrokenMesh = this.targetMeshBrokenBasket.find(currMesh => {
return mesh.value === currMesh.value;
});
}
success = Number(Number(mesh.value).toFixed(8)) === Number(Number(this.page.variable.expectedResult.valueOf()).toFixed(8)) ? true : false;
} else {
targettedBrokenMesh = this.targetMeshBrokenBasket.find(currMesh => {
return mesh.rangeValue.from === currMesh.rangeValue.from;
});
if (
mesh.rangeValue.from <= this.page.variable.expectedResult.valueOf() &&
this.page.variable.expectedResult.valueOf() <= mesh.rangeValue.to
) {
success = true;
}
}
return { success, targettedBrokenMesh };
}
enableDragAndDrop() {
let draggableMesh: Mesh;
let pickResult: PickingInfo;
this.pointerObs = this.scene.onPointerObservable.add(pointerInfo => {
if ((pointerInfo.event as any).preventDefault) {
(pointerInfo.event as any).preventDefault();
}
switch (pointerInfo.type) {
case BABYLON.PointerEventTypes.POINTERDOWN:
pickResult = this.scene.pick(
this.scene.pointerX,
this.scene.pointerY,
null,
false,
this.page.variable.activity === justePointActivity.onTheRoad ? this.camera : this.cameraHud
);
if (pickResult.hit) {
draggableMesh = this.getLastParentCabri(pickResult.pickedMesh as Mesh);
if ((draggableMesh as TargetMesh)?.draggable) {
this.pointerDown(draggableMesh);
}
}
break;
case BABYLON.PointerEventTypes.POINTERUP:
this.pointerUp();
break;
case BABYLON.PointerEventTypes.POINTERMOVE:
this.pointerMove();
break;
}
});
}
/**
* Basket - Generate basket ball
*/
generateBasketBall() {
this.basketBallMesh = null;
this.basketBallMesh = this.meshs.get("BasketballBall") as TargetMesh;
this.basketBallMesh.renderingGroupId = 1;
this.basketBallMesh.checkCollisions = true;
this.basketBallMesh.isPickable = true;
this.basketBallMesh.layerMask = this.cameraHud.layerMask;
const scaling = 2;
const ballPasitionY = this.globalService.isMobile ? -1 : -2;
this.basketBallMesh.scaling.x = this.basketBallMesh.scaling.y = this.basketBallMesh.scaling.z = scaling;
this.basketBallMesh.position = new BABYLON.Vector3(0, ballPasitionY, -10);
this.defaultBallPosition = this.basketBallMesh.position.clone();
this.defaultBallRotation = this.basketBallMesh.rotation.clone();
this.defaultBallScaling = this.basketBallMesh.scaling.clone();
this.basketBallMesh.draggable = true;
this.assetsContainerBasket.meshes.push(this.basketBallMesh);
}
/**
* Basket - Generate two separated meshes broken basket and hoop(resize ball to hoop size during anim).
*/
generateBrokenBasket() {
this.targetMeshBrokenBasket = new Array();
this.targetMeshHoop = new Array();
this.brokenbasket.scaling = new Vector3(0.2, 0.32, 0.15);
// Material for inserting image for broken basket
this.brokenbasket.material = null;
this.brokenbasket.material = this.swapBrokenBasketMaterial("brokenBasket");
this.brokenbasket.rotation = new Vector3(0, Math.PI / 2, -0.36);
this.hoop.rotation = this.brokenbasket.rotation = new Vector3(0, Math.PI / 2, -0.36);
const brokenBasketY = this.globalService.isMobile ? 2.5 : 2;
let numberElementsToTaken = new Array();
// Determine how many broken baskets needs to be created based of the exact mod
if (this.page.variable.chooseExactNumber) {
numberElementsToTaken = this.bigScaleBars;
} else {
numberElementsToTaken = this.smallScaleBars;
}
const currentSize = this.getMeshSizeInfos(this.brokenbasket);
let middleHalfPosY = this.brokenbasket.scaling.x + this.brokenbasket.scaling.y;
// createion of hoop and broken basket
numberElementsToTaken.forEach((currentMesh, index) => {
const clonedBrokenBasket = this.brokenbasket.clone(`brokenbasket${index}`) as TargetMesh;
const hoopCloned = this.hoop.clone(`hoopbasket${index}`) as TargetMesh;
if (this.page.variable.chooseExactNumber) {
hoopCloned.value = clonedBrokenBasket.value = this.displayedNumber(index);
} else {
hoopCloned.rangeValue = clonedBrokenBasket.rangeValue = currentMesh.rangeValue;
}
hoopCloned.morphTargetManager = this.hoop.morphTargetManager.clone();
clonedBrokenBasket.position = new BABYLON.Vector3(currentMesh.position.x, brokenBasketY, -5);
hoopCloned.position = new BABYLON.Vector3(currentMesh.position.x, brokenBasketY - middleHalfPosY, -6);
hoopCloned.layerMask = clonedBrokenBasket.layerMask = this.cameraHud.layerMask;
this.assetsContainerBasket.meshes.push(hoopCloned, clonedBrokenBasket);
this.targetMeshBrokenBasket.push(clonedBrokenBasket);
this.targetMeshHoop.push(hoopCloned);
});
// if collision between broken baskets so make them smaller
if (this.eachBrokenBasketsCollided) {
const paddingBtwn = 0.01;
const sizeInital = this.stepXRange / 10 - paddingBtwn;
this.targetMeshBrokenBasket.forEach((eachMesh, index) => {
const initialSizeBrokenBasket = eachMesh.scaling;
const resizeRatio = sizeInital / initialSizeBrokenBasket.x;
eachMesh.scaling = new Vector3(sizeInital, sizeInital * 2, sizeInital);
this.targetMeshHoop[index].scaling = new Vector3(
this.targetMeshHoop[index].scaling.x * resizeRatio + 0.07,
this.targetMeshHoop[index].scaling.y * resizeRatio + 0.07,
this.targetMeshHoop[index].scaling.z * resizeRatio + 0.07
);
middleHalfPosY = sizeInital + sizeInital * 2;
this.targetMeshHoop[index].position.y = brokenBasketY - middleHalfPosY;
});
}
}
swapBrokenBasketMaterial(materialName: string): Material {
const brokeBasketMaterial = new BABYLON.StandardMaterial("brokenBasketMat", this.scene);
brokeBasketMaterial.diffuseTexture = new BABYLON.Texture(this.textures.get(materialName).url, this.scene);
brokeBasketMaterial.diffuseTexture.hasAlpha = true;
brokeBasketMaterial.alpha = 1;
brokeBasketMaterial.useAlphaFromDiffuseTexture = true;
return brokeBasketMaterial;
}
/**
* Basket - Determine there are collision between broken baskets
*/
get eachBrokenBasketsCollided() {
const brokenbasketSize2 = this.getMeshSizeInfos(this.targetMeshBrokenBasket[0]);
const firstBrokenBasket = this.targetMeshBrokenBasket[0].position.x + brokenbasketSize2.extendSizeWorld.x;
const secondBrokenBasket = this.targetMeshBrokenBasket[1].position.x - brokenbasketSize2.extendSizeWorld.x;
return firstBrokenBasket >= secondBrokenBasket;
}
/**
*Basket - Ball initial position before starting of the animation
*/
getBallAnimInitalPos(targettedHoop: TargetMesh) {
let paddingX;
if (targettedHoop.index === this.targetMeshBrokenBasket.length - 1) {
paddingX = -1.5;
} else if (targettedHoop.index === this.targetMeshBrokenBasket[0].index) {
paddingX = 1.5;
} else {
paddingX = Math.random() > 0.5 ? 1.5 : -1.5;
}
return paddingX;
}
/**
* Basket - Ball animation after response
*/
launchBallAnimation(targettedHoop: TargetMesh, success = true): Animatable {
const paddingX = this.getBallAnimInitalPos(targettedHoop);
this.basketBallMesh.position.x = targettedHoop.position.x + paddingX;
const frameRate = 15;
const animationDistance = 1.5 + frameRate;
const speed = 14;
// ball below position from tragetted broken basket and random position (right/left)
const ballPositionAnim = new BABYLON.Animation(
"positionAnim",
"position",
speed,
BABYLON.Animation.ANIMATIONTYPE_VECTOR3,
BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT
);
const positionKeys = [
{
frame: 0,
value: this.basketBallMesh.position,
outTangent: new BABYLON.Vector3(0, 1, 0)
},
{
frame: animationDistance,
value: new BABYLON.Vector3(
success ? targettedHoop.position.x : targettedHoop.position.x - (paddingX > 0 ? 0.65 : -0.65),
targettedHoop.position.y - 1
),
inTangent: new BABYLON.Vector3(0, -1, 0)
}
];
// Rotation ball random speed
const ballRotationAnim = new BABYLON.Animation(
"rotationAnim",
"rotation",
speed,
BABYLON.Animation.ANIMATIONTYPE_VECTOR3,
BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT
);
const rotationDirection = Math.floor(Math.random() * 10) + 4;
const rotationKeys = [
{
frame: 0,
value: new BABYLON.Vector3(0, 0, 0)
},
{
frame: animationDistance,
value: new BABYLON.Vector3(rotationDirection, rotationDirection, 0)
}
];
// resize ball size to hoop size to get depth effect
const hoopTargetted = this.targetMeshHoop.find(mesh => {
return mesh.value === targettedHoop.value;
});
const hoopMeshInfo = this.getMeshSizeInfos(hoopTargetted);
const hoopMeshTotalSize = hoopMeshInfo.extendSizeWorld.x + 0.12;
const ballScallingAnim = new BABYLON.Animation(
"scallingAnim",
"scaling",
speed / 1.5,
BABYLON.Animation.ANIMATIONTYPE_VECTOR3,
BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT
);
const scallingKeys = [
{
frame: 0,
value: new BABYLON.Vector3(this.basketBallMesh.scaling.x, this.basketBallMesh.scaling.y, this.basketBallMesh.scaling.z)
},
{
frame: frameRate / 1.8,
value: new BABYLON.Vector3(hoopMeshTotalSize, hoopMeshTotalSize, hoopMeshTotalSize)
}
];
ballPositionAnim.setKeys(positionKeys);
ballRotationAnim.setKeys(rotationKeys);
ballScallingAnim.setKeys(scallingKeys);
hoopTargetted.morphTargetManager.getTarget(0).influence = 1;
let morphInfluence = 1;
let numberNetAnimFrame = 0;
if (success) {
this.hoopNetObservable = this.scene.onAfterRenderObservable.add(() => {
// animate hoop net when ball net collided on render
const hoopNetBasketCollided =
this.basketBallMesh.position.y <= hoopTargetted.position.y + hoopMeshInfo.extendSizeWorld.y &&
this.basketBallMesh.scaling.x <= hoopMeshTotalSize;
if (hoopNetBasketCollided) {
// influence value from 0 to 1
hoopTargetted.morphTargetManager.getTarget(0).influence = morphInfluence;
if (numberNetAnimFrame === 0) {
// first frame
morphInfluence -= 0.15;
if (morphInfluence === 0 || morphInfluence <= 0) {
numberNetAnimFrame++;
}
} else if (numberNetAnimFrame === 1) {
// second frame
morphInfluence += 0.03;
if (morphInfluence >= 1.2) {
numberNetAnimFrame++;
}
} else {
this.scene.onAfterRenderObservable.remove(this.hoopNetObservable);
this.hoopNetObservable = null;
}
}
});
}
this.basketBallMesh.animations.push(ballPositionAnim, ballRotationAnim, ballScallingAnim);
return this.scene.beginAnimation(this.basketBallMesh, 0, 2 * frameRate, false);
}
/**
* Basket - Confetti coming out from broken basket good answer
*/
async generateConfetti(targettedHoopPosition: Vector3) {
const particlesRightAnswer = new BabylonJsConfetti(this, 1000, 1500, ParticleAnimName.basket, targettedHoopPosition);
await particlesRightAnswer.runParticles();
}
getMeshSizeInfos(mesh): BoundingBox {
return mesh.getBoundingInfo().boundingBox;
}
/**
* Creation of straight line
*/
async generateStraightLine(width: number) {
let paddingArround = this.globalService.isMobile ? 2.5 : 2;
this.bigScaleBars = [];
this.smallScaleBars = [];
this.tubesMesh = [];
this.nbBasketItemsRounded = Math.round(this.numberItems);
if (document.querySelector("#MJX-SVG-global-cache defs")) {
MathJax.defsGlobalCaching = document.querySelector("#MJX-SVG-global-cache defs");
}
this.basketRatioElement = Math.ceil(this.nbBasketItemsRounded / 11);
this.basketTotalRange = width - 2 * paddingArround;
this.stepXRange = this.basketTotalRange / Math.round(this.nbBasketItemsRounded - 1);
const paddingYTube = 2;
this.linePositionY = this.basketBallMesh.position.y + paddingYTube;
this.linesXPos = -this.basketTotalRange / 2;
// create lines
for (let index = 0; index < this.nbBasketItemsRounded; index++) {
await this.createEachLine(index);
}
// creation of tube
this.bigScaleBars.reduce((previousMesh: TargetMesh, currentMesh: TargetMesh, currentIndex: number) => {
if (previousMesh && currentMesh) {
const currentPath = [previousMesh.position, currentMesh.position];
const currentTube = BABYLON.MeshBuilder.CreateTube(
"tube" + currentIndex,
{ path: currentPath, radius: 0.1 / this.basketRatioElement },
this.scene
) as TargetMesh;
const tubeMaterial = new BABYLON.StandardMaterial("myMaterial", this.scene) as StandardMaterial;
tubeMaterial.diffuseColor = new BABYLON.Color3(1, 1, 1);
currentTube.material = tubeMaterial;
currentTube.remediation = true;
currentTube.layerMask = this.cameraHud.layerMask;
this.tubesMesh.push(currentTube);
this.assetsContainerBasket.meshes.push(currentTube);
}
return currentMesh;
});
this.createBackgroundTube();
}
createBackgroundTube() {
// create backgound tube
const graduatedLineBackgroundMaterial = new BABYLON.StandardMaterial("bckgrGraduatedLine", this.scene) as StandardMaterial;
graduatedLineBackgroundMaterial.diffuseColor = BABYLON.Color3.FromHexString("#36454F");
// graduatedLineBackgroundMaterial.emissiveColor = new BABYLON.Color3(0, 0, 0);
const bSphereStart = BABYLON.MeshBuilder.CreateSphere(
"bSphereStart",
{ diameter: 0.8 / this.basketRatioElement },
this.scene
) as TargetMesh;
bSphereStart.remediation = true;
const bSphereEnd = BABYLON.MeshBuilder.CreateSphere(
"bSphereEnd",
{ diameter: 0.8 / this.basketRatioElement },
this.scene
) as TargetMesh;
bSphereEnd.remediation = true;
bSphereStart.layerMask = this.cameraHud.layerMask;
bSphereEnd.layerMask = this.cameraHud.layerMask;
bSphereStart.material = graduatedLineBackgroundMaterial;
bSphereEnd.material = graduatedLineBackgroundMaterial;
bSphereStart.position = new BABYLON.Vector3(-this.basketTotalRange / 2, this.linePositionY, -2);
bSphereEnd.position = new BABYLON.Vector3(this.basketTotalRange / 2, this.linePositionY, -2);
const path = [bSphereStart.position, bSphereEnd.position];
const tube = BABYLON.MeshBuilder.CreateTube("bTube", { path, radius: 0.4 / this.basketRatioElement }, this.scene) as TargetMesh;
tube.layerMask = this.cameraHud.layerMask;
tube.material = graduatedLineBackgroundMaterial;
tube.remediation = true;
this.assetsContainerBasket.meshes.push(tube);
this.assetsContainerBasket.meshes.push(bSphereStart);
this.assetsContainerBasket.meshes.push(bSphereEnd);
}
/**
* Basket - Create graduated each line (exact and/or not exact mod)
*/
async createEachLine(index) {
let currentXposUnits;
let initalDistanceValue: number;
if (!this.page.variable.chooseExactNumber) {
initalDistanceValue = (this.displayedNumber(1) - this.displayedNumber(0)) / this.page.variable.approximation;
}
// create scale bars
const scaleBarsBig = BABYLON.MeshBuilder.CreateBox("scaleBars", { width: 0.08, height: 0.4 }, this.scene) as TargetMesh;
const scaleBarsMaterial = new BABYLON.StandardMaterial("scaleBarsMaterial", this.scene);
scaleBarsMaterial.diffuseColor = BABYLON.Color3.FromHexString("#fa7610");
scaleBarsBig.layerMask = this.cameraHud.layerMask;
scaleBarsBig.position = new BABYLON.Vector3(this.linesXPos, this.linePositionY, -5);
scaleBarsBig.material = scaleBarsMaterial;
scaleBarsBig.value = this.displayedNumber(index);
scaleBarsBig.remediation = true;
this.bigScaleBars.push(scaleBarsBig);
if (index !== this.nbBasketItemsRounded - 1 && this.page.variable.chooseExactNumber) {
this.drawSemiCircle(scaleBarsBig.position);
}
this.assetsContainerBasket.meshes.push(scaleBarsBig);
currentXposUnits = this.linesXPos;
if (!this.page.variable.chooseExactNumber && index < this.nbBasketItemsRounded - 1) {
for (let indexUnit = 1; indexUnit <= this.page.variable.approximation; indexUnit++) {
const scaleBarsSmallMesh = BABYLON.MeshBuilder.CreateBox(
"box",
{ width: this.stepXRange / this.page.variable.approximation - 0.05, height: 0.2 },
this.scene
) as TargetMesh;
const scaleBarsSmallLineMesh = BABYLON.MeshBuilder.CreateBox("box", { width: 0.06, height: 0.2 }, this.scene) as TargetMesh;
const smallScaleBarsMaterial = new BABYLON.StandardMaterial("smallScaleBarsMaterial", this.scene);
smallScaleBarsMaterial.diffuseColor = new BABYLON.Color3(0, 0, 0);
scaleBarsSmallLineMesh.material = smallScaleBarsMaterial;
scaleBarsSmallLineMesh.layerMask = this.cameraHud.layerMask;
scaleBarsSmallMesh.layerMask = this.cameraHud.layerMask;
const positionXCenterd = currentXposUnits + this.stepXRange / this.page.variable.approximation / 2;
const positionXRight = currentXposUnits + this.stepXRange / this.page.variable.approximation;
scaleBarsSmallMesh.position = new BABYLON.Vector3(positionXCenterd, this.linePositionY, -5);
scaleBarsSmallLineMesh.position = new BABYLON.Vector3(positionXRight, this.linePositionY, -5);
scaleBarsSmallMesh.remediation = true;
this.drawSemiCircle(scaleBarsSmallMesh.position);
let rangeStartValue = 0;
if (indexUnit > 0) {
rangeStartValue = 0.001;
}
scaleBarsSmallMesh.rangeValue = {
from: scaleBarsBig.value + initalDistanceValue * (indexUnit - 1) + rangeStartValue,
to: scaleBarsBig.value + initalDistanceValue * indexUnit,
inital: scaleBarsBig.value
};
scaleBarsSmallLineMesh.remediation = scaleBarsSmallMesh.remediation = true;
currentXposUnits = currentXposUnits + this.stepXRange / this.page.variable.approximation;
this.smallScaleBars.push(scaleBarsSmallMesh);
this.assetsContainerBasket.materials.push(smallScaleBarsMaterial);
this.assetsContainerBasket.meshes.push(scaleBarsSmallMesh, scaleBarsSmallLineMesh);
}
}
// result.position = scaleBarsBig.position;
this.linesXPos = this.linesXPos + this.stepXRange;
}
/**
* Basket - Remediation : Create a semi circle between each line
*/
async drawSemiCircle(finalPosition: Vector3): Promise<void> {
const points = [];
let distanceFinal;
if (this.basketRatioElement > 1) {
distanceFinal = this.stepXRange;
} else {
distanceFinal = this.stepXRange / 2;
}
const xLength = this.page.variable.chooseExactNumber ? 1 : this.page.variable.approximation;
const rayon = distanceFinal / xLength / this.basketRatioElement;
const pas = Math.PI / 20;
// number of points needed to create the semi circle
for (let i = 0; i <= 20; i++) {
points.push(new BABYLON.Vector3(rayon * Math.cos(pas * i), rayon * Math.sin(pas * i), 0));
}
// Path3D
const path3d = new BABYLON.Path3D(points);
const curve = path3d.getCurve();
const xPosition = this.page.variable.chooseExactNumber ? rayon : 0;
// visualisation
const lineMesh = BABYLON.Mesh.CreateLines("lines", curve, this.scene) as Mesh;
lineMesh.position = new Vector3(finalPosition.x + xPosition, finalPosition.y, finalPosition.z);
lineMesh.isVisible = false;
this.drawUnitStepHelp(lineMesh);
this.assetsContainerBasket.meshes.push(lineMesh);
}
/**
* Basket - Remediation : Write unit step for help
*/
async drawUnitStepHelp(lineMesh: Mesh): Promise<void> {
return new Promise<void>(async resolve => {
const sizeMesh = lineMesh.getBoundingInfo().boundingBox;
const boxHelp = BABYLON.MeshBuilder.CreateBox("boxHelp", { width: 0.5, height: 0.7 }, this.scene) as TargetMesh;
boxHelp.position = new Vector3(lineMesh.position.x, lineMesh.position.y + sizeMesh.maximumWorld.y, lineMesh.position.z);
// boxHelp.layerMask = this.cameraHud.layerMask;
const dynamicTexture = new BABYLON.DynamicTexture("text", { width: 50, height: 0.7 * 100 }, this.scene);
dynamicTexture.update();
if (this.page.variable.unitStep instanceof Fraction) {
const textureContext = dynamicTexture.getContext();
const imgSrc = await this.mathJaxSvgToImg(1);
textureContext.clearRect(0, 0, 100, 100);
textureContext.fillStyle = "rgb(255, 255, 255)";
textureContext.fillRect(0, 0, 100, 100);
textureContext.drawImage(imgSrc, 7, 7, 40, 55);
textureContext.fill();
dynamicTexture.update();
} else {
dynamicTexture.drawText(
"+" + this.page.variable.unitStep.valueOf().toString(),
null,
null,
`${dynamicTexture.getBaseSize().width / 2}px solid Arial`,
"black",
"white"
);
}
const material = new BABYLON.StandardMaterial("materialHelp", this.scene);
material.diffuseTexture = dynamicTexture;
boxHelp.material = material;
boxHelp.isVisible = false;
this.assetsContainerBasket.meshes.push(boxHelp);
this.assetsContainerBasket.materials.push(material);
resolve();
});
}
/**
* Basket - Display units into the box randomly
*/
async displayBasketBoxRandomly() {
this.getRandomBoxIndexesDisplayed();
await this.allBoxesToBeDisplayed();
}
/**
* Basket - Boxes to be displayed with their own unit step's. Min and Max are always displayed
*/
async allBoxesToBeDisplayed() {
for (let index = 0; index <= this.bigScaleBars.length - 1; index++) {
// nb min and nb max are displayed / in not exact mode the closest line box is not displayed
const displayNbMinOrNbMax =
(index === 0 || index === this.bigScaleBars.length - 1) &&
(this.bigScaleBars[index].value !== this.expectedResultByAccuracy ||
(!this.page.variable.chooseExactNumber && this.page.variable.difficulty !== 3));
if (this.numberOfElementsDisplayed.includes(index) || displayNbMinOrNbMax) {
await this.generateBoxesResults(index);
}
}
}
/**
* Basket - Remediation : Move the ball to the line
*/
async moveBallAnimation() {
this.basketBallMesh.isVisible = true;
return new Promise<void>(resolveProm => {
const targetIndex = this.smallScaleBars.findIndex((currMesh, meshIndex) => {
const isLastElement = meshIndex === this.bigScaleBars.length - 1;
return (
(isLastElement ? currMesh.rangeValue.to : currMesh.rangeValue.inital) ===
this.bigScaleBars[this.remediationTargettedIndex].value
);
});
const speed = 800;
const ballAnimation = new BABYLON.Animation(
"position",
"position",
speed,
BABYLON.Animation.ANIMATIONTYPE_VECTOR3,
BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE
);
const positionKeys = [
{
frame: 0,
value: new BABYLON.Vector3(
this.basketBallMesh.position.x,
this.basketBallMesh.position.y,
this.basketBallMesh.position.z
)
},
{
frame: speed,
value: new BABYLON.Vector3(
this.smallScaleBars[targetIndex].position.x,
this.smallScaleBars[targetIndex].position.y,
this.smallScaleBars[targetIndex].position.z
)
}
];
ballAnimation.setKeys(positionKeys);
this.basketBallMesh.animations.push(ballAnimation);
const anim = this.scene.beginAnimation(this.basketBallMesh, 0, speed, false);
anim.onAnimationEnd = () => {
resolveProm();
};
});
}
/**
* Basket - Remediation : Determine which 2 box results to be shown and avoiding to display excepted result
*/
async showBasketDistanceGraduation(): Promise<void> {
return new Promise<void>(async resolveProm => {
const boxesDisplayedNumbers = this.assetsContainerBasket.meshes.filter(mesh => {
return mesh.name === "boxResult";
});
const allNumbersToBeDisplayed = new Array();
for (let index = 0; index < this.numberItems; index++) {
allNumbersToBeDisplayed.push(this.displayedNumber(index));
}
let indexOfExpectedResult;
if (this.page.variable.chooseExactNumber) {
indexOfExpectedResult = this.bigScaleBars.findIndex(meshes => {
return meshes.value === this.page.variable.expectedResult.valueOf();
});
} else {
indexOfExpectedResult = this.bigScaleBars.findIndex((currMesh, meshIndex) => {
const isLastElement = meshIndex === this.bigScaleBars.length - 1;
return this.smallScaleBars.some((smallMesh, index) => {
return (isLastElement ? smallMesh.rangeValue.to : smallMesh.rangeValue.inital) === currMesh.value;
});
});
}
const boxesToNoTBeDisplayed = [0, indexOfExpectedResult, indexOfExpectedResult - 1];
let boxIndexToDisplay;
if (indexOfExpectedResult > -1) {
boxIndexToDisplay = this.bigScaleBars.findIndex((mesh, index) => {
return !boxesToNoTBeDisplayed.includes(index);
});
boxesDisplayedNumbers.forEach(mesh => {
mesh.isVisible = false;
});
this.remediationTargettedIndex = allNumbersToBeDisplayed.findIndex((numbersDisplayed, index) => {
return index === boxIndexToDisplay;
});
if (this.remediationTargettedIndex === -1) {
this.remediationTargettedIndex = 1;
} else if (this.remediationTargettedIndex === allNumbersToBeDisplayed.length - 1) {
this.remediationTargettedIndex = this.remediationTargettedIndex - 1;
}
this.generateBoxesResults(this.remediationTargettedIndex);
await AppUtils.timeOut(500);
this.generateBoxesResults(this.remediationTargettedIndex + 1);
}
const drawedLines = this.assetsContainerBasket.meshes.filter(mesh => {
return mesh.name === "lines";
});
const lineMeshInd = drawedLines.findIndex((lineMesh, index) => {
return index === this.remediationTargettedIndex;
});
this.launchBasketLineRemediationAnim().then(async () => {
if (this.page.variable.chooseExactNumber) {
this.showDrawedCircleLine(lineMeshInd);
}
resolveProm();
});
});
}
/**
* Basket-remediation : Show drawed circle with units step
*/
showDrawedCircleLine(lineMeshInd = null) {
let curentLineMeshInd: number;
const drawedLines = this.assetsContainerBasket.meshes.filter(mesh => {
return mesh.name === "lines";
});
if (!lineMeshInd) {
curentLineMeshInd = drawedLines.findIndex((lineMesh, index) => {
return index === this.remediationTargettedIndex;
});
} else {
curentLineMeshInd = lineMeshInd;
}
const boxHelpes = this.assetsContainerBasket.meshes.filter(mesh => {
return mesh.name === "boxHelp";
});
if (drawedLines[curentLineMeshInd]) {
drawedLines[curentLineMeshInd].layerMask = this.cameraHud.layerMask;
drawedLines[curentLineMeshInd].isVisible = true;
if (boxHelpes[curentLineMeshInd]) {
boxHelpes[curentLineMeshInd].layerMask = this.cameraHud.layerMask;
boxHelpes[curentLineMeshInd].isVisible = true;
}
}
}
/**
* Basket - Remediation : Reset meshes position before remediation
*/
resetMeshesAfterRemediation() {
const drawedLines = this.assetsContainerBasket.meshes.filter(mesh => {
return mesh.name === "lines";
});
const boxHelpes = this.assetsContainerBasket.meshes.filter(mesh => {
return mesh.name === "boxHelp";
});
boxHelpes.forEach(currentBox => {
currentBox.layerMask = 0x0fffffff;
});
drawedLines.forEach(lines => {
lines.layerMask = 0x0fffffff;
});
this.basketBallMesh.position = this.defaultBallPosition.clone();
if (this.tubesMesh[this.remediationTargettedIndex]) {
(this.tubesMesh[this.remediationTargettedIndex].material as StandardMaterial).diffuseColor = new BABYLON.Color3(1, 1, 1);
}
}
/**
* Basket - Remediation
*/
async showDrawedCircleLinesByIndexes(): Promise<void> {
return new Promise<void>(async resolveCurrent => {
for (let i = 0; i < this.page.variable.approximation; i++) {
if (!this.page.skipSequence) {
await AppUtils.timeOut(500);
this.page.cabri.showDrawedCircleLine(this.page.cabri.remediationTargettedIndex * this.page.variable.approximation + i);
}
}
resolveCurrent();
});
}
/**
* Basket - Remediation : Display or hide boxes results
*/
showHideBoxesResults(show: boolean) {
this.assetsContainerBasket.meshes.forEach(mesh => {
if (mesh.name === "boxResult") {
mesh.isVisible = show;
}
});
}
/**
* Basket - Remediation : Display or hide lines
*/
showHideLines(displayTogether: boolean, show: boolean): Promise<void> {
return new Promise<void>(async resolve => {
for (const mesh of this.assetsContainerBasket.meshes) {
if (mesh.name === "scaleBars") {
if (displayTogether) {
await AppUtils.timeOut(100);
mesh.isVisible = true;
} else {
mesh.isVisible = show;
}
}
}
resolve();
});
}
/**
* Basket - Remediation display boxes and fill tube mesh progressively
*/
async launchBasketLineRemediationAnim(): Promise<void> {
return new Promise<void>(resolveProm => {
const speed = 800;
const colorsPossibilities = [new BABYLON.Color3(1, 0, 0), new BABYLON.Color3(0, 1, 0), new BABYLON.Color3(0, 0, 1)];
const randomColorIndex = Math.floor(Math.random() * colorsPossibilities.length);
const materialColorAnim = new BABYLON.Animation(
"position",
"material.diffuseColor",
speed,
BABYLON.Animation.ANIMATIONTYPE_COLOR3,
BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE
);
const positionKeys = [
{
frame: 0,
value: new BABYLON.Color3(1, 1, 1)
},
{
frame: speed,
value: colorsPossibilities[randomColorIndex]
}
];
if (this.tubesMesh[this.remediationTargettedIndex]) {
materialColorAnim.setKeys(positionKeys);
this.tubesMesh[this.remediationTargettedIndex].animations.push(materialColorAnim);
const anim = this.scene.beginAnimation(this.tubesMesh[this.remediationTargettedIndex], 0, speed, false);
anim.onAnimationEnd = async () => {
resolveProm();
};
} else {
resolveProm();
}
});
}
/**
* Basket - Remediation
*/
hideAlmostRemediationElements() {
this.page.cabri.showHideBoxesResults(false);
this.page.cabri.showHideLines(false, false);
}
async showLineBoxResult(box: boolean, hide: boolean) {
if (box && !hide) {
this.generateBoxesResults(this.remediationTargettedIndex);
await AppUtils.timeOut(500);
this.generateBoxesResults(this.remediationTargettedIndex + 1);
} else if (!box && !hide) {
await this.launchBasketLineRemediationAnim();
}
}
async removeScene() {
if (this.assetsContainerBasket) {
this.assetsContainerBasket.removeAllFromScene();
this.assetsContainerBasket.dispose();
}
if (this.assetsContainerOnTheRoad) {
this.assetsContainerOnTheRoad.removeAllFromScene();
this.assetsContainerOnTheRoad.dispose();
}
const removeList = [];
this.scene.multiMaterials.forEach(element => {
if (!this.initialMaterials.includes(element)) {
removeList.push(element);
}
});
this.scene.materials.forEach(element => {
if (!this.initialMaterials.includes(element)) {
removeList.push(element);
}
});
for (let material of removeList) {
await new Promise<void>(resolve => {
material.onDisposeObservable.addOnce(() => {
material.isDisposed = true;
material = null;
resolve();
});
material.dispose(true, true, false);
});
}
this.scene.materials = this.scene.materials.filter(material => {
return material !== null && !(material as any).isDisposed;
});
// console.error(this.scene.materials.length, this.initialMaterials.length);
}
async launchAgainActivity() {
await this.removeScene();
await this.launchActivity();
}
/**
* Convert svg generaterd by mathJax into an image in order to display in the box
*/
mathJaxSvgToImg(currIndex: number) {
return new Promise(resolveFinal => {
let denominateur: number;
if (this.page.variable.unitStep instanceof Fraction) {
denominateur = this.page.variable.unitStep.denominateur;
} else if (this.page.variable.nbMin instanceof Fraction) {
denominateur = this.page.variable.nbMin.denominateur;
} else {
denominateur = this.page.variable.unitStep.valueOf();
}
MathJax.tex2svgPromise(`\\frac{${this.page.variable.nbMin.valueOf() + currIndex}}{${denominateur}}`).then(element => {
this.page.mathJaxDiv.nativeElement.appendChild(element);
MathJax.startup.document.clear();
const allMySvgs = this.page.mathJaxDiv.nativeElement.querySelectorAll("svg");
const svgsLength = allMySvgs.length - 1;
allMySvgs[svgsLength].appendChild(MathJax.defsGlobalCaching);
const url = "data:image/svg+xml;base64," + window.btoa(allMySvgs[svgsLength].outerHTML);
const img = new Image();
return new Promise(() => {
img.onload = () => {
resolveFinal(img);
};
img.onerror = err => {
console.log("errerr", err);
};
img.src = url;
});
});
});
}
/**
* Basket - Results written into the box according if fraction type or not
*/
generateBoxesResults(currIndex: number): Promise<void> {
return new Promise(async resolve => {
let values: { dynamicTexture: DynamicTexture; box: TargetMesh; material: StandardMaterial; boxHeight: number };
if (this.nbMinMaxisNotFraction(currIndex) || this.isWholeNumber(currIndex)) {
// not fraction
values = this.generateBoxes(currIndex);
} else if (this.page.variable.unitStep instanceof Fraction) {
// fraction
values = await this.generateFractionBoxes(currIndex);
} else {
if (this.page.variable.nbMin instanceof Fraction && this.page.variable.nbMax instanceof Fraction) {
if (currIndex === 0 || currIndex === this.bigScaleBars.length - 1) {
// fraction
values = await this.generateFractionBoxes(currIndex);
} else {
// not fraction
values = this.generateBoxes(currIndex);
}
} else {
// not fraction
values = this.generateBoxes(currIndex);
}
}
// common ad in the dynamic texture
values.material.diffuseTexture = values.dynamicTexture;
values.box.material = values.material;
values.box.layerMask = this.cameraHud.layerMask;
values.box.position = new BABYLON.Vector3(this.bigScaleBars[currIndex].position.x, this.linePositionY - values.boxHeight, -5);
values.box.remediation = true;
this.assetsContainerBasket.meshes.push(values.box);
this.assetsContainerBasket.textures.push(values.dynamicTexture);
this.assetsContainerBasket.materials.push(values.material);
resolve();
});
}
/**
* If displayed number is a whole number so display as number and not as Fraction
*/
isWholeNumber(currIndex: number): boolean {
return this.bigScaleBars[currIndex].value.valueOf() - Math.floor(this.bigScaleBars[currIndex].value.valueOf()) === 0;
}
/**
* Basket - Check if nbMin and nbMax is not in the fraction type
*/
nbMinMaxisNotFraction(currIndex: number): boolean {
return (
(this.bigScaleBars[currIndex] &&
this.bigScaleBars[currIndex].value === this.page.variable.nbMin.valueOf() &&
!(this.page.variable.nbMin instanceof Fraction)) ||
(this.bigScaleBars[currIndex].value === this.page.variable.nbMax.valueOf() && !(this.page.variable.nbMax instanceof Fraction))
);
}
/**
* Basket - generate boxes without fraction
*/
generateBoxes(currIndex: number): { dynamicTexture; box; material; boxHeight } {
let box: TargetMesh;
let material: StandardMaterial;
let boxHeight: number;
let dynamicTexture;
boxHeight = 0.7;
box = BABYLON.MeshBuilder.CreateBox("boxResult", { width: 0.5, height: boxHeight }, this.scene) as TargetMesh;
material = new BABYLON.StandardMaterial("redMat", this.scene);
dynamicTexture = new BABYLON.DynamicTexture("text", { width: 50, height: boxHeight * 100 }, this.scene);
dynamicTexture.update();
dynamicTexture.drawText(
this.displayedNumber(currIndex),
null,
null,
`${dynamicTexture.getBaseSize().width / 2}px solid Arial`,
"black",
"white"
);
return { dynamicTexture, box, material, boxHeight };
}
/**
* Basket -generate boxes with fraction by using MathJax
*/
async generateFractionBoxes(
currIndex: number
): Promise<{ dynamicTexture: DynamicTexture; box: TargetMesh; material: StandardMaterial; boxHeight: number }> {
return new Promise(async resolve => {
let box: TargetMesh;
let material: StandardMaterial;
let boxHeight: number;
let dynamicTexture;
boxHeight = 0.7;
// convert svg to img
const imgSrc = await this.mathJaxSvgToImg(currIndex);
box = BABYLON.MeshBuilder.CreateBox("boxResult", { width: 0.6, height: boxHeight }, this.scene) as TargetMesh;
material = new BABYLON.StandardMaterial("redMat", this.scene);
dynamicTexture = new BABYLON.DynamicTexture("textureNumber", { width: 90, height: 100 }, this.scene);
const textureContext = dynamicTexture.getContext();
// draw img into the canvas
textureContext.clearRect(0, 0, 100, 100);
textureContext.fillStyle = "rgb(255, 255, 255)";
textureContext.fillRect(0, 0, 100, 100);
textureContext.drawImage(imgSrc, 20, 10, 55, 80);
textureContext.fill();
dynamicTexture.update();
resolve({ dynamicTexture, box, material, boxHeight });
});
}
/**
* Precision determine not apply when the result is 0
*/
displayedNumberPrecision(value) {
if (value.toString().includes(".")) {
return this.basketUnitStepPrecision;
} else {
return 0;
}
}
/**
* Basket - Display number of result by it's position
*/
displayedNumber(index: number) {
if (this.page.variable.unitStep instanceof Fraction) {
return (this.page.variable.nbMin.valueOf() + index) / this.page.variable.unitStep.denominateur;
} else {
const value = this.page.variable.nbMin.valueOf() + index * this.page.variable.unitStep;
return Number(value.toFixed(this.displayedNumberPrecision(value)));
}
}
/**
* Basket - Get random indexes to be displayed into the box for all difficulties
*/
getRandomBoxIndexesDisplayed() {
this.numberOfElementsDisplayed = new Array();
if (this.page.variable.difficulty === 0) {
this.bigScaleBars.forEach((val, index) => {
this.numberOfElementsDisplayed.push(index);
});
} else {
let numElement = new Array();
numElement = new Array(this.displayedNumberAllowed).fill(null);
const expectedResultIndex = this.bigScaleBars.findIndex(mesh => {
return this.expectedResultByAccuracy === mesh.value;
});
numElement.forEach(() => {
this.getRandomBoxIndexesDisplayedRecursive(this.bigScaleBars.length - 1, 1, expectedResultIndex);
});
}
}
/**
* Basket - Number results displayed by difficulty
*/
get displayedNumberAllowed() {
let numberItems: number;
const allIntevralNumbersDisplay = this.numberItems - 2;
if (this.page.variable.difficulty === 1) {
numberItems = Math.floor(allIntevralNumbersDisplay / 2);
if (numberItems === 0) {
numberItems = 1;
}
} else if (this.page.variable.difficulty === 2) {
if (allIntevralNumbersDisplay > 2) {
numberItems = 2;
} else {
numberItems = 1;
}
} else if (this.page.variable.difficulty === 3) {
numberItems = 0;
}
return numberItems;
}
get expectedResultByAccuracy() {
return this.page.variable.chooseExactNumber
? this.page.variable.expectedResult.valueOf()
: Math.floor(this.page.variable.expectedResult.valueOf() as any);
}
/**
* Basket - Get random indexes to be displayed into the box for all difficulties except for first level.
* Avoid displaying excepted result box
*/
getRandomBoxIndexesDisplayedRecursive(min, max, expectedResultIndex) {
const num = Math.floor(Math.random() * (max - min + 1)) + min;
if (this.numberOfElementsDisplayed.includes(num) || num === expectedResultIndex) {
return this.getRandomBoxIndexesDisplayedRecursive(min, max, expectedResultIndex);
} else {
this.numberOfElementsDisplayed.push(num);
return num;
}
}
/**
* Get number elements in the graduated scale
*/
get numberItems(): number {
// step for integer is one
let nbElement;
if (this.page.variable.unitStep instanceof Fraction) {
nbElement =
(this.page.variable.nbMax.valueOf() - this.page.variable.nbMin.valueOf()) * this.page.variable.unitStep.denominateur + 1;
} else {
nbElement = (this.page.variable.nbMax.valueOf() - this.page.variable.nbMin.valueOf()) / this.page.variable.unitStep + 1;
}
return nbElement;
}
/**********************************************************************************************************************
*************************************************** ACTIVITY ON THE ROAD **********************************************
***********************************************************************************************************************/
async buildElementActivityOnTheRoad(): Promise<void> {
return new Promise(resolve => {
this.importAssetOnTheRoad().then(() => {
this.createGroundTile();
this.createCars();
if (!this.globalService.lowPerformanceMode) {
this.createEnvElement();
}
this.createHighLighter();
[this.consigne, this.page.justePointExercices.consigneTTS] = this.getConsigneExerciceRoad();
this.createSceneGui(this.consigne);
this.assetsContainerOnTheRoad.addAllToScene();
resolve();
});
});
}
createHighLighter() {
if (!this.hl) {
this.hl = new BABYLON.HighlightLayer("hl1", this.scene);
this.alpha = 0;
this.hl.renderingGroupId = 0;
this.scene.registerBeforeRender(() => {
this.alpha += 0.1;
this.hl.blurHorizontalSize = 0.3 + Math.cos(this.alpha) * 0.6 + 0.6;
this.hl.blurVerticalSize = 0.3 + Math.sin(this.alpha / 3) * 0.6 + 0.6;
});
}
}
importAssetOnTheRoad(): Promise<void> {
this.assetsManagerOnTheRoad = new BABYLON.AssetsManager(this.scene);
this.assetsContainerOnTheRoad = new BABYLON.AssetContainer(this.scene);
// (window as any).containerManager = this.assetsContainerOnTheRoad;
this.importTextureOnTheRoad();
this.importImageOnTheRoad();
this.importMeshOnTheRoad();
this.assetsManagerOnTheRoad.onFinish = this.importFinishOnTheRoad.bind(this);
return this.assetsManagerOnTheRoad.loadAsync();
}
importTextureOnTheRoad() {
const textureMap = [
{ name: "parking", url: "assets/babylon/justePoint/textures/parking.png" },
{ name: "parkingR", url: "assets/babylon/justePoint/textures/parkingR.png" },
{
name: "emptyRoad",
url: "assets/babylon/justePoint/textures/road_empty" + (this.globalService.lowPerformanceMode ? "_ios" : "") + ".png"
},
{
name: "linedRoad",
url: "assets/babylon/justePoint/textures/road_lined" + (this.globalService.lowPerformanceMode ? "_ios" : "") + ".png"
},
{
name: "grass",
url: "assets/babylon/justePoint/textures/Grass_03" + (this.globalService.lowPerformanceMode ? "_ios" : "") + ".png"
}
];
if(!this.globalService.lowPerformanceMode){
textureMap.push({
name: "normalGrass",
url: "assets/babylon/justePoint/textures/NormalMap" + (this.globalService.lowPerformanceMode ? "_ios" : "") + ".png"
});
}
textureMap.forEach(texture => {
this.assetsManagerOnTheRoad.addTextureTask(texture.name, texture.url);
});
}
importMeshOnTheRoad() {
const mMeshs = this.globalService.lowPerformanceMode
? [{ name: "Car", url: "/assets/babylon/justePoint/models/", filename: "Low-Poly-Racing-Car.babylon" }]
: [
{ name: "Car", url: "/assets/babylon/justePoint/models/", filename: "Low-Poly-Racing-Car.babylon" },
{ name: "Tree", url: "/assets/babylon/justePoint/models/", filename: "tree.babylon" },
{ name: "Pine", url: "/assets/babylon/justePoint/models/", filename: "pine.babylon" },
{ name: "House", url: "/assets/babylon/justePoint/models/", filename: "house.babylon" }
];
mMeshs.forEach(mesh => {
this.assetsManagerOnTheRoad.addMeshTask(mesh.name, null, mesh.url, mesh.filename);
});
}
importImageOnTheRoad() {
const images = [
{
name: "kpost",
url: "assets/babylon/justePoint/textures/kpost" + (this.globalService.lowPerformanceMode ? "_ios" : "") + ".png"
}
];
images.forEach(image => {
this.assetsManagerOnTheRoad.addImageTask(image.name, image.url);
});
}
importFinishOnTheRoad(tasks: AbstractAssetTask[]) {
this.textures = new Map<string, Texture>();
this.images = new Map<string, HTMLImageElement>();
this.meshs = new Map<string, AbstractMesh>();
tasks.forEach(task => {
if (task.isCompleted) {
if (task instanceof BABYLON.MeshAssetTask) {
const lTask = task as MeshAssetTask;
this.assetsContainerOnTheRoad.meshes = this.assetsContainerOnTheRoad.meshes.concat(lTask.loadedMeshes);
lTask.loadedMeshes.forEach(mesh => {
this.meshs.set(mesh.name, mesh);
if (mesh.material) {
if (mesh.material instanceof BABYLON.MultiMaterial) {
this.assetsContainerOnTheRoad.multiMaterials.push(mesh.material as MultiMaterial);
} else {
this.assetsContainerOnTheRoad.materials.push(mesh.material);
this.assetsContainerOnTheRoad.textures = this.assetsContainerOnTheRoad.textures.concat(
mesh.material.getActiveTextures()
);
}
}
});
} else if (task instanceof BABYLON.ImageAssetTask) {
const lTask = task as ImageAssetTask;
this.images.set(task.name, lTask.image);
} else if (task instanceof BABYLON.TextureAssetTask) {
const lTask = task as TextureAssetTask;
this.assetsContainerOnTheRoad.textures.push(lTask.texture);
this.textures.set(lTask.name, lTask.texture);
} else {
console.error("Task Not Handle", task);
}
} else {
console.error(task.errorObject);
}
});
}
setBackgroundPosition() {
if (this.page.variable.activity === justePointActivity.onTheRoad) {
this.background.position = new BABYLON.Vector3(0, -12, 40);
this.background.scaling = new BABYLON.Vector3(2.2, 2.2, 2.2);
} else {
this.background.position = new BABYLON.Vector3(0, 0, 0);
this.background.scaling = new BABYLON.Vector3(1.1, 1, 1.1);
}
}
/**
* CheckCollision to detec the result of the exercise call on dpointer up event after drag
*/
checkRoadCollision() {
const allOnRoadGround =
this.targetMesh.length > 0 &&
this.targetMesh.every(carMesh => {
return carMesh.intersectsMesh(this.meshs.get("road"), false, true);
});
this.buttonValider.isVisible = allOnRoadGround;
}
createEnvElement() {
const tree = this.meshs.get("Tree1") as Mesh;
this.freezeMaterialOnMesh(tree);
tree.layerMask = this.camera.layerMask;
this.assetsContainerOnTheRoad.meshes.push(tree);
tree.scaling = new Vector3(0.1, 0.1, 0.1);
tree.position = new Vector3(6, 1.4, 11);
const tree2 = tree.clone("tree2");
tree2.scaling = new Vector3(0.1, 0.1, 0.1);
tree2.position = new Vector3(8, 1.4, 10);
tree2.rotation.y = (80 * Math.PI) / 180;
this.assetsContainerOnTheRoad.meshes.push(tree2);
const tree3 = tree.clone("tree3");
tree3.scaling = new Vector3(0.1, 0.1, 0.1);
tree3.position = new Vector3(10, 1.4, 8);
tree3.rotation.y = (180 * Math.PI) / 180;
this.assetsContainerOnTheRoad.meshes.push(tree3);
const tree4 = tree.clone("tree4");
tree4.scaling = new Vector3(0.1, 0.1, 0.1);
tree4.position = new Vector3(11, 1.4, 7);
tree4.rotation.y = (300 * Math.PI) / 180;
this.assetsContainerOnTheRoad.meshes.push(tree4);
// pine
const pine = this.meshs.get("Tree2") as Mesh;
this.freezeMaterialOnMesh(pine);
pine.layerMask = this.camera.layerMask;
this.assetsContainerOnTheRoad.meshes.push(pine);
pine.scaling = new Vector3(0.1, 0.1, 0.1);
pine.position = new Vector3(3, 1.4, 11);
const pine2 = pine.clone("Pine2");
pine2.scaling = new Vector3(0.16, 0.16, 0.16);
pine2.position = new Vector3(9, 1.4, 10);
pine2.rotation.y = (80 * Math.PI) / 180;
this.assetsContainerOnTheRoad.meshes.push(pine2);
const pine3 = pine.clone("Pine3");
pine3.scaling = new Vector3(0.1, 0.1, 0.1);
pine3.position = new Vector3(-10, 1.4, 2.5);
pine3.rotation.y = (180 * Math.PI) / 180;
this.assetsContainerOnTheRoad.meshes.push(pine3);
const pine4 = pine.clone("Pine4");
pine4.scaling = new Vector3(0.17, 0.17, 0.17);
pine4.position = new Vector3(-11.5, 1.4, 9);
pine4.rotation.y = (300 * Math.PI) / 180;
this.assetsContainerOnTheRoad.meshes.push(pine4);
const house = this.meshs.get("Cubo.001") as Mesh;
this.freezeMaterialOnMesh(house);
house.getChildMeshes().forEach(element => {
element.layerMask = this.camera.layerMask;
});
house.layerMask = this.camera.layerMask;
this.assetsContainerOnTheRoad.meshes.push(house);
house.scaling = new Vector3(0.1, 0.1, 0.1);
house.position = new Vector3(4.5, 0.56, 8);
house.rotation = new Vector3(0, (118.8 * Math.PI) / 180, 0);
}
/**
* freezeMaterial material into a mesh and it's children
* @param mesh
*/
freezeMaterialOnMesh(mesh: Mesh) {
mesh.material.freeze();
if (mesh.getChildMeshes && mesh.getChildMeshes.length > 0) {
mesh.getChildMeshes().forEach(mesh => {
this.freezeMaterialOnMesh(mesh as Mesh);
});
}
}
/**
* Create Car whith innerValue for expected result
*/
createCars() {
// first car
const car: TargetMesh = this.meshs.get("Car") as TargetMesh;
let color = [
{ color: new BABYLON.Color3(0, 0, 0) as Color3, name: $localize`noire` },
{ color: new BABYLON.Color3(0, 0, 1) as Color3, name: $localize`bleue` },
{ color: new BABYLON.Color3(0.839, 0.898, 0.024) as Color3, name: $localize`jaune` },
{ color: new BABYLON.Color3(0.5, 0, 1) as Color3, name: $localize`violette` },
{ color: new BABYLON.Color3(0, 0.541, 0.184) as Color3, name: $localize`verte` },
{ color: new BABYLON.Color3(1, 0.349, 0) as Color3, name: $localize`orange` },
{ color: new BABYLON.Color3(1, 0, 0.5) as Color3, name: $localize`rose` },
{ color: new BABYLON.Color3(0.435, 0.427, 0.427) as Color3, name: $localize`grise` }
//{ color: new BABYLON.Color3(1, 0, 0) as Color3, name: $localize`rouge` }
];
this.color = color.slice();
car.renderingGroupId = 1;
car.rotationQuaternion = null;
car.rotation = new BABYLON.Vector3((268 * Math.PI) / 180, (360 * Math.PI) / 180, (268 * Math.PI) / 180);
car.scaling = new BABYLON.Vector3(0.005, 0.005, 0.005);
car.getChildMeshes().forEach(subMesh => {
subMesh.layerMask = this.camera.layerMask;
subMesh.isPickable = true;
});
car.layerMask = this.camera.layerMask;
car.checkCollisions = true;
car.isPickable = true;
car.draggable = true;
let expectedResult = this.page.variable.expectedResult;
//beware bug in bounding box the loaded default car with modify value not work when the car is clone the new object work
this.generateBoundingBoxFromAllChild(car);
if (Array.isArray(expectedResult)) {
let xPosition = -(Math.floor(expectedResult.length / 2) * 1.5 - (expectedResult.length % 2 === 0 ? 0.75 : 0));
car.position = new BABYLON.Vector3(xPosition, 0.17, -5);
if (expectedResult.length > 0) {
for (let index = 0; index < expectedResult.length; index++) {
const colorNumber = AppUtils.getRandomIntInclusive(0, color.length - 1);
const choosenColor = color.splice(colorNumber, 1)[0];
const nextCar = car.clone("car_" + choosenColor.name) as TargetMesh;
nextCar.color = choosenColor.name;
nextCar.value = expectedResult[index];
nextCar.renderingGroupId = 2;
const original = this.scene.getMaterialByName("car_body");
const colorMaterial = this.createColorMaterialFromClone(
original,
choosenColor.color,
choosenColor.name,
this.assetsContainerOnTheRoad
);
this.replaceMaterialOnMesh(nextCar, original, colorMaterial, this.assetsContainerOnTheRoad, this.materials);
nextCar.position.x = xPosition;
this.generateBoundingBoxFromAllChild(nextCar);
nextCar.layerMask = this.camera.layerMask;
nextCar.initialPosition = nextCar.position.clone();
this.meshs.set(nextCar.name, nextCar);
this.assetsContainerOnTheRoad.meshes.push(nextCar);
this.targetMesh.push(nextCar as TargetMesh);
xPosition += 1.5;
}
}
} else {
const colorNumber = AppUtils.getRandomIntInclusive(0, color.length - 1);
const choosenColor = color.splice(colorNumber, 1)[0];
const nextCar = car.clone("car_" + choosenColor.name) as TargetMesh;
nextCar.color = choosenColor.name;
const original = this.scene.getMaterialByName("car_body");
const colorMaterial = this.createColorMaterialFromClone(
original,
choosenColor.color,
choosenColor.name,
this.assetsContainerOnTheRoad
);
this.replaceMaterialOnMesh(nextCar, original, colorMaterial, this.assetsContainerOnTheRoad, this.materials);
nextCar.layerMask = this.camera.layerMask;
this.meshs.set(nextCar.name, nextCar);
this.assetsContainerOnTheRoad.meshes.push(nextCar);
this.targetMesh.push(nextCar as TargetMesh);
nextCar.position = new BABYLON.Vector3(0, 0.17, -5);
// clone also need to redeclare a bounding box
this.generateBoundingBoxFromAllChild(nextCar);
nextCar.value = expectedResult.valueOf();
}
car.dispose();
}
/**
* Replace car on starting position
*/
resetAllCarPosition(result?: ResultOnTheRoad) {
if (result) {
result.carsError.forEach(car => {
this.resetCarPosition(car.color);
});
} else {
this.targetMesh.forEach(car => {
this.resetCarPosition(car);
});
}
}
/**
* a mesh or the color
* @param car
*/
resetCarPosition(car: TargetMesh | string) {
if (car instanceof TargetMesh) {
car.position = car.initialPosition.clone();
} else {
const carMesh = this.targetMesh.find(m => m.color === car);
carMesh.position = carMesh.initialPosition.clone();
}
}
/**
* Move all car or a list in the good position
*/
setAllCarOnCorrectPosition(result?: ResultOnTheRoad) {
if (result) {
result.carsError.forEach(car => {
this.setCarOnCorrectPosition(car.color);
});
} else {
this.targetMesh.forEach(car => {
this.setCarOnCorrectPosition(car);
});
}
}
/**
* Position a car mesh in good answer position can be a mesh or a string color
* @param car
*/
setCarOnCorrectPosition(car: TargetMesh | string) {
if (car instanceof TargetMesh) {
const groundTile = this.targetSubMesh.find(s => s.value === car.value);
if (groundTile) {
car.position.x = groundTile.getBoundingInfo().boundingBox.centerWorld.x;
car.position.z = groundTile.getBoundingInfo().boundingBox.centerWorld.z;
}
} else {
const lCar = this.targetMesh.find(m => m.color === car);
const groundTile = this.targetSubMesh.find(s => s.value === lCar.value);
if (groundTile) {
lCar.position.x = groundTile.getBoundingInfo().boundingBox.centerWorld.x;
lCar.position.z = groundTile.getBoundingInfo().boundingBox.centerWorld.z;
}
}
}
/**
* Create the ground floor with the rad subdivide into segment holding her value
*/
createGroundTile() {
this.targetSubMesh = [];
this.targetMesh = [];
const grid = {
h: 1,
w: this.numberItems
};
// create Tiled Mesh for a graduated Road
const tiledGround: Mesh = new BABYLON.MeshBuilder.CreateTiledGround("road", {
xmin: -8,
zmin: -3,
xmax: 8,
zmax: 3,
subdivisions: grid,
Updatable: true
});
tiledGround.isPickable = false;
tiledGround.layerMask = this.camera.layerMask;
this.meshs.set(tiledGround.name, tiledGround);
this.assetsContainerOnTheRoad.meshes.push(tiledGround);
this.createMaterialOnTheRoad();
// Create Multi Material
const multimat: MultiMaterial = new BABYLON.MultiMaterial("multi", this.scene);
if (this.page.variable.noRoadHelp) {
multimat.subMaterials.push(this.materials.get("emptyRoad"));
} else {
multimat.subMaterials.push(this.materials.get("linedRoad"));
}
this.assetsContainerOnTheRoad.multiMaterials.push(multimat);
// Apply the multi material
// Define multimat as material of the tiled ground
tiledGround.material = multimat;
// Needed variables to set subMeshes
//const verticesCount = tiledGround.getTotalVertices();
// if we send total vertice into subMesh the subMesh become global of parent and not generate the bounding box so we send 1
const verticesCount = 1;
const tileIndicesLength = tiledGround.getIndices().length / (grid.w * grid.h);
// Set subMeshes of the tiled ground
tiledGround.subMeshes = [];
this.KPosttargetMesh = [];
let base = 0;
const visibleItem = this.randomKPostVisibility(grid.w);
for (let col = 0; col < grid.w; col++) {
const subMesh = new BABYLON.SubMesh(
col % multimat.subMaterials.length,
0,
verticesCount,
base,
tileIndicesLength,
tiledGround,
null,
true
) as TargetSubMesh;
if (this.page.variable.unitStep instanceof Fraction) {
// TODO calculate value in fraction for each graduation
subMesh.value = this.page.variable.nbMin.valueOf() + col * this.page.variable.unitStep.valueOf();
} else {
subMesh.value = this.page.variable.nbMin.valueOf() + col * this.page.variable.unitStep;
}
tiledGround.subMeshes.push(subMesh);
this.generateKilometerPost(subMesh, visibleItem ? visibleItem.includes(col) : true);
this.targetSubMesh.push(subMesh);
base += tileIndicesLength;
}
//duplicate road mesh to fill left and right around the road
const leftRoad = tiledGround.clone("leftRoad");
leftRoad.position = new BABYLON.Vector3(-16, 0, 0);
this.assetsContainerOnTheRoad.meshes.push(leftRoad);
this.meshs.set(leftRoad.name, leftRoad);
const rightRoad = tiledGround.clone("rightRoad");
this.assetsContainerOnTheRoad.meshes.push(rightRoad);
this.meshs.set(rightRoad.name, rightRoad);
rightRoad.position = new BABYLON.Vector3(16, 0, 0);
//generate top and bottom ground around the road.
const upperTileGround = new BABYLON.MeshBuilder.CreateGround("upperGround", { width: 40, height: 10 }, this.scene);
upperTileGround.isPickable = false;
this.assetsContainerOnTheRoad.meshes.push(upperTileGround);
this.meshs.set(upperTileGround.name, upperTileGround);
upperTileGround.layerMask = this.camera.layerMask;
upperTileGround.material = this.materials.get("grass");
upperTileGround.position = new BABYLON.Vector3(0, 0, 8);
const lowerTileGround = new BABYLON.MeshBuilder.CreateGround("lowerGround", { width: 40, height: 20 }, this.scene);
lowerTileGround.isPickable = false;
this.assetsContainerOnTheRoad.meshes.push(lowerTileGround);
this.meshs.set(lowerTileGround.name, lowerTileGround);
lowerTileGround.layerMask = this.camera.layerMask;
lowerTileGround.material = this.materials.get("grass");
lowerTileGround.position = new BABYLON.Vector3(0, 0, -13);
const carNumber = Array.isArray(this.page.variable.expectedResult) ? this.page.variable.expectedResult.length : 1;
const parkingGround = new BABYLON.MeshBuilder.CreateGround("parking", { width: 2 * carNumber, height: 3 }, this.scene);
parkingGround.isPickable = false;
this.assetsContainerOnTheRoad.meshes.push(parkingGround);
this.meshs.set(parkingGround.name, parkingGround);
parkingGround.layerMask = this.camera.layerMask;
parkingGround.material = this.materials.get("parking");
parkingGround.position = new BABYLON.Vector3(0, 0.001, -4.5);
const parkingGroundR: Mesh = new BABYLON.MeshBuilder.CreateGround("parkingR", { width: 0.3, height: 3 }, this.scene);
parkingGroundR.isPickable = false;
this.assetsContainerOnTheRoad.meshes.push(parkingGroundR);
this.meshs.set(parkingGroundR.name, parkingGroundR);
parkingGroundR.layerMask = this.camera.layerMask;
const mat = this.materials.get("parkingR");
parkingGroundR.material = this.materials.get("parkingR");
mat.diffuseTexture.hasAlpha = true;
parkingGroundR.material = mat;
const parkingGroundR2: Mesh = new BABYLON.MeshBuilder.CreateGround("parkingR2", { width: 0.3, height: 3 }, this.scene);
parkingGroundR.position = new BABYLON.Vector3(-(carNumber + 0.15), 0.001, -4.5);
parkingGroundR2.isPickable = false;
this.assetsContainerOnTheRoad.meshes.push(parkingGroundR2);
this.meshs.set(parkingGroundR2.name, parkingGroundR2);
parkingGroundR2.layerMask = this.camera.layerMask;
const material = this.materials.get("parkingR").clone("parkingR2");
parkingGroundR2.material = material;
this.assetsContainerOnTheRoad.materials.push(parkingGroundR2.material);
const texture: Texture = material.diffuseTexture.clone() as Texture;
this.assetsContainerOnTheRoad.textures.push(texture);
texture.uAng = (180 * Math.PI) / 180;
texture.wAng = (180 * Math.PI) / 180;
material.diffuseTexture = texture;
material.ambientTexture = texture;
material.specularTexture = texture;
material.emissiveTexture = texture;
parkingGroundR2.position = new BABYLON.Vector3(carNumber + 0.15, 0.001, -4.5);
}
displayedNumbberAllowedRoad() {
let numberItems: number;
if (this.page.variable.difficulty === 1) {
numberItems = 4;
} else if (this.page.variable.difficulty === 2) {
numberItems = 2;
} else if (this.page.variable.difficulty === 3) {
numberItems = 0;
}
return numberItems;
}
randomKPostVisibility(totalItem: number): number[] {
const quantityVisible = this.displayedNumbberAllowedRoad();
if (quantityVisible !== undefined) {
const visibleItem = [];
const bagNumber = Array.from({ length: totalItem - 2 }, (v, k) => k + 1);
// remove from the bag of possible kpost the index of the expected result
if (Array.isArray(this.page.variable.expectedResult)) {
this.page.variable.expectedResult.forEach(result => {
const valueToRemove = Math.round(
(result.valueOf() - this.page.variable.nbMin.valueOf()) / this.page.variable.unitStep.valueOf()
);
if (valueToRemove > 0) {
const indexInsideBag = bagNumber.findIndex(f => valueToRemove === f);
if (indexInsideBag > -1) {
bagNumber.splice(indexInsideBag, 1);
}
}
});
} else {
const valueToRemove = Math.round(
(this.page.variable.expectedResult.valueOf() - this.page.variable.nbMin.valueOf()) /
this.page.variable.unitStep.valueOf()
);
if (valueToRemove > 0) {
const indexInsideBag = bagNumber.findIndex(f => valueToRemove === f);
if (indexInsideBag > -1) {
bagNumber.splice(indexInsideBag, 1);
}
}
}
// push first and last visible
visibleItem.push(0, totalItem - 1);
for (let index = 0; index < quantityVisible; index++) {
if (bagNumber.length > 0) {
const selectedIndex: number = AppUtils.getRandomIntInclusive(0, bagNumber.length - 1);
visibleItem.push(bagNumber.splice(selectedIndex, 1)[0]);
}
}
return visibleItem;
} else {
return null;
}
}
/**
* Generate the kilometer post with the value for each road subdivision
* @param subMesh
* @param visible
*/
generateKilometerPost(subMesh: TargetSubMesh, visible = true) {
const kpost: TargetMesh = BABYLON.MeshBuilder.CreatePlane("kpost_" + subMesh.value, { width: 1, height: 1 }, this.scene);
this.faceCameraMesh(kpost, this.camera);
this.KPosttargetMesh.push(kpost);
this.assetsContainerOnTheRoad.meshes.push(kpost);
this.meshs.set(kpost.name, kpost);
const KpostPositionVector = subMesh.getBoundingInfo().boundingBox.center.clone();
KpostPositionVector.z = 3;
KpostPositionVector.y = 0.5;
kpost.position = KpostPositionVector;
kpost.layerMask = this.camera.layerMask;
const textureSize = this.globalService.lowPerformanceMode ? 128 : 512;
const textureKpost: DynamicTexture = new BABYLON.DynamicTexture("textureKpost", textureSize, this.scene);
this.textures.set(textureKpost.name, textureKpost);
this.assetsContainerOnTheRoad.textures.push(textureKpost);
const textureContext = textureKpost.getContext();
const materialKPost = new BABYLON.StandardMaterial("MatkPost", this.scene);
this.materials.set(materialKPost.name, materialKPost);
this.assetsContainerOnTheRoad.materials.push(materialKPost);
materialKPost.diffuseTexture = textureKpost;
materialKPost.specularColor = BABYLON.Color3.Black();
kpost.material = materialKPost;
textureContext.clearRect(0, 0, textureSize, textureSize);
textureContext.fillStyle = "rgba(0, 0, 0, 0.2)";
textureContext.fillRect(0, 0, textureSize, textureSize);
textureContext.drawImage(this.images.get("kpost"), 0, 0);
textureContext.fill();
textureKpost.update();
this.drawText(subMesh.value.toString(), textureKpost, DistanceUnit.Km);
kpost.visible = visible;
kpost.setEnabled(kpost.visible);
}
public showAllKpost() {
this.enableAndHideAllKpost();
this.scene.onAfterRenderObservable.addOnce(() => {
this.KPosttargetMesh.forEach(kpost => {
if (!this.checkKpostsCollisions(kpost)) {
kpost.setEnabled(true);
} else {
kpost.setEnabled(false);
}
});
});
this.resetVisibilityAllKpost();
}
public enableAndHideAllKpost() {
this.KPosttargetMesh.forEach(kpost => {
kpost.visibility = 0;
kpost.setEnabled(true);
});
}
public resetVisibilityAllKpost() {
this.KPosttargetMesh.forEach(kpost => {
kpost.visibility = 1;
});
}
checkKpostsCollisions(kpost: Mesh) {
return this.KPosttargetMesh.some(kpost2 => {
const km = Number(kpost.id.split("_")[1]);
const km2 = Number(kpost2.id.split("_")[1]);
if (km2 < km || (kpost2.id === this.KPosttargetMesh[this.KPosttargetMesh.length - 1].id && kpost.id !== kpost2.id)) {
const collision = kpost2.intersectsMesh(kpost);
if (collision && kpost2.isEnabled()) {
return true;
}
}
return false;
});
}
public showOnlyVisiblepost() {
this.KPosttargetMesh.forEach(kpost => {
kpost.setEnabled(kpost.visible);
});
}
private drawText(text: string, textureKpost: DynamicTexture, unit: DistanceUnit) {
const ratio = this.globalService.lowPerformanceMode ? 128 / 512 : 1;
let font;
const maxChar = Math.max(String(this.page.variable.nbMin.valueOf()).length, String(this.page.variable.nbMax.valueOf()).length);
// Add text to dynamic texture
switch (maxChar) {
case 1:
font = "bold " + 300 * ratio + "px monospace";
textureKpost.drawText(text, 175 * ratio, 450 * ratio, font, "black", null, true, true);
break;
case 2:
font = "bold " + 250 * ratio + "px monospace";
textureKpost.drawText(text, (110 + (maxChar - text.length) * 65) * ratio, 450 * ratio, font, "black", null, true, true);
break;
case 3:
font = "bold " + 140 * ratio + "px monospace";
textureKpost.drawText(text, (130 + (maxChar - text.length) * 25) * ratio, 400 * ratio, font, "black", null, true, true);
break;
case 4:
font = "bold " + 100 * ratio + "px monospace";
textureKpost.drawText(text, (138 + (maxChar - text.length) * 10) * ratio, 390 * ratio, font, "black", null, true, true);
break;
default:
font = "bold " + 90 * ratio + "px monospace";
textureKpost.drawText(text, 130 * ratio, 390 * ratio, font, "black", null, true, true);
break;
}
font = "bold " + 150 * ratio + "px monospace";
textureKpost.drawText(unit, 160 * ratio, 150 * ratio, font, "black", null, true, true);
textureKpost.hasAlpha = true;
}
private createMaterialOnTheRoad() {
this.materials = new Map<string, StandardMaterial>();
this.textures.forEach((texture, key) => {
const mat = new BABYLON.StandardMaterial(key, this.scene);
this.materials.set(mat.name, mat);
this.assetsContainerOnTheRoad.materials.push(mat);
mat.diffuseTexture = texture;
mat.specularTexture = texture;
mat.emissiveTexture = texture;
mat.ambientTexture = texture;
});
if(!this.globalService.lowPerformanceMode){
this.materials.get("grass").bumpTexture = this.textures.get("normalGrass");
this.materials.get("grass").bumpTexture.level = 20;
(this.materials.get("grass").bumpTexture as unknown as DynamicTexture).uAng = (40 * Math.PI) / 180;
(this.materials.get("grass").bumpTexture as unknown as DynamicTexture).vScale = 5;
(this.materials.get("grass").bumpTexture as unknown as DynamicTexture).uScale = 20;
}
(this.materials.get("grass").diffuseTexture as unknown as DynamicTexture).uAng = (40 * Math.PI) / 180;
(this.materials.get("grass").diffuseTexture as unknown as DynamicTexture).level = 2;
/*(this.materials.get("grass").bumpTexture as unknown as DynamicTexture).vScale = 2.5;
(this.materials.get("grass").bumpTexture as unknown as DynamicTexture).uScale = 10;
(this.materials.get("grass").diffuseTexture as unknown as DynamicTexture).vScale = 2.5;
(this.materials.get("grass").diffuseTexture as unknown as DynamicTexture).uScale = 10;
(this.materials.get("grass").specularTexture as unknown as DynamicTexture).vScale = 2.5;
(this.materials.get("grass").specularTexture as unknown as DynamicTexture).uScale = 10;
(this.materials.get("grass").emissiveTexture as unknown as DynamicTexture).vScale = 2.5;
(this.materials.get("grass").emissiveTexture as unknown as DynamicTexture).uScale = 10;
(this.materials.get("grass").ambientTexture as unknown as DynamicTexture).vScale = 2.5;
(this.materials.get("grass").ambientTexture as unknown as DynamicTexture).uScale = 10;*/
(this.materials.get("grass").diffuseTexture as unknown as DynamicTexture).uAng = (40 * Math.PI) / 180;
(this.materials.get("grass").diffuseTexture as unknown as DynamicTexture).vScale = 5;
(this.materials.get("grass").diffuseTexture as unknown as DynamicTexture).uScale = 20;
(this.materials.get("grass").specularTexture as unknown as DynamicTexture).uAng = (40 * Math.PI) / 180;
(this.materials.get("grass").specularTexture as unknown as DynamicTexture).vScale = 5;
(this.materials.get("grass").specularTexture as unknown as DynamicTexture).uScale = 20;
(this.materials.get("grass").emissiveTexture as unknown as DynamicTexture).uAng = (40 * Math.PI) / 180;
(this.materials.get("grass").emissiveTexture as unknown as DynamicTexture).vScale = 5;
(this.materials.get("grass").emissiveTexture as unknown as DynamicTexture).uScale = 20;
(this.materials.get("grass").ambientTexture as unknown as DynamicTexture).uAng = (40 * Math.PI) / 180;
(this.materials.get("grass").ambientTexture as unknown as DynamicTexture).vScale = 5;
(this.materials.get("grass").ambientTexture as unknown as DynamicTexture).uScale = 20;
(this.materials.get("parkingR").specularTexture as unknown as DynamicTexture).wrapU = BABYLON.Constants.TEXTURE_CLAMP_ADDRESSMODE;
(this.materials.get("parkingR").emissiveTexture as unknown as DynamicTexture).wrapU = BABYLON.Constants.TEXTURE_CLAMP_ADDRESSMODE;
(this.materials.get("parkingR").ambientTexture as unknown as DynamicTexture).wrapU = BABYLON.Constants.TEXTURE_CLAMP_ADDRESSMODE;
(this.materials.get("parkingR").diffuseTexture as unknown as DynamicTexture).wrapU = BABYLON.Constants.TEXTURE_CLAMP_ADDRESSMODE;
(this.materials.get("parkingR").specularTexture as unknown as DynamicTexture).wrapV = BABYLON.Constants.TEXTURE_CLAMP_ADDRESSMODE;
(this.materials.get("parkingR").emissiveTexture as unknown as DynamicTexture).wrapV = BABYLON.Constants.TEXTURE_CLAMP_ADDRESSMODE;
(this.materials.get("parkingR").ambientTexture as unknown as DynamicTexture).wrapV = BABYLON.Constants.TEXTURE_CLAMP_ADDRESSMODE;
(this.materials.get("parkingR").diffuseTexture as unknown as DynamicTexture).wrapV = BABYLON.Constants.TEXTURE_CLAMP_ADDRESSMODE;
}
public createSceneGui(text: string[]) {
this.advancedTexture = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI("myUI", true, this.scene);
this.assetsContainerOnTheRoad.textures.push(this.advancedTexture);
this.advancedTexture.layer.layerMask = this.cameraHud.layerMask;
this.bubble = new BABYLON.GUI.Rectangle();
const maxCharLine = Math.max(...text.map(line => line.length));
this.bubble.width = 0.01 * maxCharLine + 0.005;
this.bubble.height = 0.05 * text.length;
this.bubble.background = "#224295";
this.bubble.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_LEFT;
this.bubble.verticalAlignment = Control.VERTICAL_ALIGNMENT_TOP;
this.bubble.left = "5%";
this.bubble.adaptWidthToChildren = false;
this.bubble.adaptHeightToChildren = false;
this.bubble.cornerRadius = 10;
this.bubble.thickness = 10;
this.bubble.color = "#ffffff";
this.bubble.alpha = 1;
this.setBubbleTopPosition();
this.textBlock = new BABYLON.GUI.TextBlock();
this.textBlock.alpha = 1;
this.textBlock.text = text.reduce((previousValue, currentValue) => previousValue + "\n" + currentValue);
this.textBlock.textWrapping = true;
this.textBlock.resizeToFit = false;
this.textBlock.color = "#ffffff";
this.textBlock.fontSize = "20%";
this.textBlock.fontFamily = "Quicksand-Bold";
this.textBlock.fontWeight = "bold";
this.buttonValider = BABYLON.GUI.Button.CreateImageWithCenterTextButton(
"Valider",
$localize`Valider`,
"assets/babylon/justePoint/textures/start.png"
);
this.buttonValider.width = "250px";
this.buttonValider.height = "250px";
this.buttonValider.fontSize = "45px";
this.buttonValider.fontFamily = "Quicksand-Bold";
this.buttonValider.fontWeight = "bold";
this.buttonValider.thickness = 0;
this.buttonValider.color = "#ffffff";
this.buttonValider.top = "17%";
this.buttonValider.left = "35%";
this.buttonValider.isVisible = false;
this.lastCalled = new Date().getTime();
this.buttonValider.onPointerClickObservable.add((x, state) => {
const now = new Date().getTime();
if(now - this.lastCalled < 500 /* half a second */) {
// skip next calls
// AppUtils.debug("skip next calls");
state.skipNextObservers = true;
} else {
// AppUtils.debug("sendButtonValidate()");
this.lastCalled = now;
this.sendButtonValidate();
}
});
// if double event bug persist use this:
// this.buttonValider.onPointerDownObservable.add(this.sendButtonValidate.bind(this));
this.bubble.addControl(this.textBlock);
this.advancedTexture.addControl(this.buttonValider);
this.advancedTexture.addControl(this.bubble);
}
setBubbleTopPosition(){
if (this.bubble){
if (this.marginTop && this.marginTop < 0 ){
this.bubble.top= ((Math.round((Math.abs(this.marginTop) + 10) * window.devicePixelRatio)));
}
else {
this.bubble.top = "5%";
}
}
}
toggleGui(visible: boolean) {
this.bubble.isVisible = visible;
}
sendButtonValidate(eventData?: Vector2WithInfo, eventState?: EventState) {
this.buttonValider.isVisible = false;
let carTrue = 0;
const error = [];
let success = false;
let reversed = false;
this.targetMesh.forEach(car => {
const groundTile = this.targetSubMesh.find(s => s.value === car.value);
if (groundTile) {
if (car.intersectsMesh(groundTile as unknown as AbstractMesh, false, false)) {
carTrue += 1;
} else {
if (Array.isArray(this.page.variable.expectedResult)) {
this.page.variable.expectedResult.forEach(result => {
const altGroundTile = this.targetSubMesh.find(s => s.value === result.valueOf());
if (car.intersectsMesh(altGroundTile as unknown as AbstractMesh, false, false)) {
reversed = true;
}
});
}
error.push({ color: car.color, correctValue: car.value });
}
} else {
throw new Error("Probleme de 'variable exercice' le nombre rechercher n'existe pas dans les bornes");
}
});
success = carTrue === this.targetMesh.length;
this.waitUserAnswer.next({ success, response: new ResultOnTheRoad(error), reversed: reversed });
console.log("success", success);
console.log("error", error);
}
getStepPrecision(currentUnitStep?: number) {
let unitStep;
if (!currentUnitStep) {
unitStep = this.page.variable.unitStep.valueOf();
} else {
unitStep = currentUnitStep;
}
if (unitStep.toString().includes(".")) {
const untilCommaInd = unitStep.toString().indexOf(".");
return unitStep.toString().length - 1 - untilCommaInd;
} else {
return 0;
}
}
/**
* @returns Return an array of string with the consigne for the exercice
* @param secondShot to know if it is the second try and adapt scenario accordingly
*/
getConsigneExerciceBasketTTS(secondShot?: boolean): string[] {
const consigneArrayTTS = [];
const theFraction = $localize`:consigne jeu juste point type Fraction:la fraction `;
const toFraction = $localize`:consigne jeu juste point type Fraction:à la fraction `;
const ofFraction = $localize`:consigne jeu juste point type Fraction:de la fraction `;
const theNumber = $localize`:consigne jeu juste point type Nombre:le nombre `;
const toNombre = $localize`:consigne jeu juste point type Nombre:au nombre `;
const ofNumber = $localize`:consigne jeu juste point type Nombre:du nombre `;
const toTypeResult = this.page.variable.expectedResult instanceof Fraction ? toFraction : toNombre;
const theTypeResult = this.page.variable.expectedResult instanceof Fraction ? theFraction : theNumber;
const ofTypeResult = this.page.variable.expectedResult instanceof Fraction ? ofFraction : ofNumber;
let phrase = $localize`Mets le ballon dans le panier correspondant ${toTypeResult}`;
if (!secondShot && this.cabriService.teamTotalAwards > 1 && this.cabriService.teamTotalAwards < 5) {
// after the first answer:
if (!this.consigneScenariosArray || this.consigneScenariosArray.length === 0) {
if (this.page.accountService.team.length === 1) {
this.consigneScenariosArray = [
$localize`Peux-tu atteindre ${theTypeResult}`,
$localize`Essaye de viser ${theTypeResult}`,
$localize`Maintenant vise ${theTypeResult}`,
$localize`Et maintenant ${theTypeResult}`,
$localize`Lance le ballon dans le panier ${ofTypeResult}`
]
} else {
this.consigneScenariosArray = [
$localize`Peux-tu atteindre ${theTypeResult}`,
$localize`Essaye de viser ${theTypeResult}`,
$localize`Vise ${theTypeResult}`,
$localize`Peux-tu atteindre ${theTypeResult}`,
$localize`Lance le ballon dans le panier ${ofTypeResult}`
]
}
AppUtils.shuffleArray(this.consigneScenariosArray);
}
const [first, ...rest] = this.consigneScenariosArray;
this.consigneScenariosArray = rest;
phrase = first;
} else if (!secondShot && this.cabriService.teamTotalAwards >= 6) {
// after 6 answers:
const consigneScenariosFastArray = [
$localize`Vise ${theTypeResult}`,
$localize`Dans ${theTypeResult}`,
this.page.variable.expectedResult instanceof Fraction ? theTypeResult : $localize`Le`,
this.page.variable.expectedResult instanceof Fraction ? $localize`Maintenant la` : $localize`Maintenant le`,
]
const index = Math.floor(Math.random() * consigneScenariosFastArray.length);
phrase = consigneScenariosFastArray[index];
} else if (secondShot) {
// after remediation:
const consigneScenariosFastArray = [
$localize`Alors, dans quel panier est ${theTypeResult}`,
$localize`Mais alors, où se cache donc ${theTypeResult}`,
$localize`Où est donc ${theTypeResult}`,
$localize`Mais alors, où se cache ${theTypeResult}`,
$localize`Donc… Où est ${theTypeResult}`,
$localize`Du coup, où se trouve ${theTypeResult}`
]
const index = Math.floor(Math.random() * consigneScenariosFastArray.length);
phrase = consigneScenariosFastArray[index];
}
this.page.basketCommonPhrase = phrase;
let expectedResult = "";
if (this.page.variable.expectedResult instanceof Fraction) {
expectedResult = this.page.scenario.ttsFraction(this.page.variable.expectedResult);
} else {
expectedResult = this.page.variable.expectedResult.valueOf().toString();
}
consigneArrayTTS.push(this.page.basketCommonPhrase + expectedResult);
return consigneArrayTTS;
}
/**
* @returns Return an array of string with the consigne for the Road exercice
*/
getConsigneExerciceRoad(): [string[], string] {
let opSign: string;
const stepPrecision = this.getStepPrecision();
const nb1 = Number(this.page.variable.expectedResult[0].toFixed(stepPrecision));
const nb2 = Number(this.page.variable.expectedResult[1].toFixed(stepPrecision));
let pas: number;
this.page.scenario.resultPhrase = undefined;
if (this.page.variable.operation !== operation.PlacementDePoint) {
switch (this.page.variable.operation) {
case operation.addition:
opSign = "+";
break;
case operation.soustraction:
opSign = "-";
break;
}
pas = nb2 - nb1;
const result = eval(nb1 + opSign + pas).toFixed(stepPrecision);
this.page.scenario.resultPhrase = $localize`Oui`+`, ${nb1} ${opSign} ${pas} =${result} !`;
this.page.scenario.resultPhraseRemediation = `${nb1} ${opSign} ${pas} =${result} !`;
// console.error("XXX nb1 = ", nb1);
// console.error("XXX nb2 = ", nb2);
// console.error("XXX result phrase = ", this.page.scenario.resultPhrase);
}
const consigneArrayDisplayed = [];
let consigneArrayTTS: string[] = [];
let consigneTTSRoad;
const nbrItem = this.targetMesh.length;
const lesVoitures = $localize`les voitures`;
const laVoiture = $localize`la voiture`;
const laLesVoiture = nbrItem > 1 ? lesVoitures : laVoiture;
consigneArrayDisplayed.push($localize`Place ${laLesVoiture} selon les consignes suivantes : `);
this.targetMesh.forEach((voiture, index, array) => {
if (index === 0 || this.page.variable.operation === operation.PlacementDePoint) {
const kmValue = voiture.value.toString();
// TEXT:
consigneArrayDisplayed.push($localize`La voiture ${voiture.color} a roulé ${kmValue} ${DistanceUnit.Km}`);
// TTS:
consigneArrayTTS.push($localize`La voiture ${voiture.color} a roulé ${kmValue} ${DistanceUnit.Km}`)
} else {
const currentColor = array[index - 1].color;
if (this.page.variable.operation === operation.addition) {
const currentKmValue = (voiture.value.valueOf() - array[index - 1].value.valueOf()).toFixed(stepPrecision);
// TEXT:
consigneArrayDisplayed.push(
$localize`La voiture ${voiture.color} est à ${currentKmValue} ${DistanceUnit.Km} devant la voiture ${currentColor}`
);
// TTS:
consigneArrayTTS.push($localize`La ${voiture.color} est à ${currentKmValue} ${DistanceUnit.Km} devant`);
} else {
const currentKmValue = (array[index - 1].value.valueOf() - voiture.value.valueOf()).toFixed(stepPrecision);
// TEXT:
consigneArrayDisplayed.push(
$localize`La ${voiture.color} est à ${currentKmValue} ${DistanceUnit.Km} derrière la voiture ${currentColor}`
);
// TTS:
consigneArrayTTS.push($localize`La ${voiture.color} est à ${currentKmValue} ${DistanceUnit.Km} derrière ${currentColor}`);
}
}
});
if (this.page.currentUser.awardsCurrent.total < 1) {
// read full consigne the first time
consigneArrayTTS = consigneArrayDisplayed;
}
const virgule = $localize`virgule`;
const kilometre = $localize`kilomètre`;
consigneTTSRoad = consigneArrayTTS.reduce((p, n) => p + "! " + n)
.replace(/\./g, " "+ virgule +" ")
.replace(/km/g, kilometre);
console.error("consigneTTSRoad ", consigneTTSRoad);
return [consigneArrayDisplayed, consigneTTSRoad];
}
highlightMesh(active: boolean, mesh: Mesh, wSubMesh = false, color?: Color3) {
if (wSubMesh && mesh.getChildMeshes()?.length > 0) {
mesh.getChildMeshes().forEach(subMesh => {
if (active) {
this.hl.addMesh(subMesh as Mesh, color ? color : (new BABYLON.Color3(0.01, 0.81, 0.01) as Color3), false);
} else {
this.hl.removeMesh(subMesh as Mesh);
}
});
}
if (active) {
this.hl.addMesh(mesh, color ? color : (new BABYLON.Color3(0.01, 0.81, 0.01) as Color3), false);
} else {
this.hl.removeMesh(mesh);
}
}
highlightCar(active: boolean, color: string | string[] | ResultOnTheRoad) {
if (Array.isArray(color)) {
color.forEach(c => {
const color = this.color.find(co => co.name === c);
const carMesh = this.targetMesh.find(m => m.color === c);
this.highlightMesh(
active,
carMesh.getChildMeshes(false, cMesh => {
return cMesh.id.includes("body.body");
})[0] as Mesh,
false,
color.color
);
});
} else if (color instanceof ResultOnTheRoad) {
color.carsError.forEach(c => {
const color = this.color.find(co => co.name === c.color);
const carMesh = this.targetMesh.find(m => m.color === c.color);
this.highlightMesh(
active,
carMesh.getChildMeshes(false, cMesh => {
return cMesh.id.includes("body.body");
})[0] as Mesh,
false,
color.color
);
});
} else {
const color3 = this.color.find(co => co.name === color);
const carMesh = this.targetMesh.find(m => m.color === color);
this.highlightMesh(
active,
carMesh.getChildMeshes(false, cMesh => {
return cMesh.id.includes("body.body");
})[0] as Mesh,
false,
color3.color
);
}
}
remediationRoadPart1(): Promise<{ turn: number; range: number; value: number }> {
return new Promise<{ turn: number; range: number; value: number }>((resolve, reject) => {
let firstMesh: TargetSubMesh;
let firstIndex: number;
const expectedResult = this.page.variable.expectedResult;
//find interval with two consecutive road suMesh not in the expected result
for (let index = 0; index < this.targetSubMesh.length; index++) {
if (Array.isArray(expectedResult)) {
if (
index + 1 < this.targetSubMesh.length &&
!expectedResult.includes(this.targetSubMesh[index].value.valueOf()) &&
!expectedResult.includes(this.targetSubMesh[index + 1].value.valueOf())
) {
firstMesh = this.targetSubMesh[index];
firstIndex = index;
break;
}
} else {
if (
index + 1 < this.targetSubMesh.length &&
expectedResult.valueOf() !== this.targetSubMesh[index].value.valueOf() &&
expectedResult.valueOf() !== this.targetSubMesh[index + 1].value.valueOf()
) {
firstMesh = this.targetSubMesh[index];
firstIndex = index;
break;
}
}
}
if (!firstMesh) {
firstMesh = this.targetSubMesh[0];
firstIndex = 0;
}
let turnToLast = this.targetSubMesh.length - firstIndex - 2;
if (turnToLast > 9) {
turnToLast = 9;
}
const option = {
width: firstMesh.getBoundingInfo().boundingBox.extendSize.x * 2,
height: firstMesh.getBoundingInfo().boundingBox.extendSize.z * 2
};
this.remediationPlanes = [];
const plane: Mesh = BABYLON.MeshBuilder.CreateGround("remediation", option, this.scene);
this.assetsContainerOnTheRoad.meshes.push(plane);
this.remediationPlanes.push(plane);
plane.material = this.createColorMaterial(
new BABYLON.Color3(1, 0.5, 0) as Color3,
"remediation",
this.assetsContainerOnTheRoad
);
this.assetsContainerOnTheRoad.materials.push(plane.material);
plane.layerMask = this.camera.layerMask;
plane.setPivotPoint(new BABYLON.Vector3(-firstMesh.getBoundingInfo().boundingBox.extendSize.x, 0, 0));
plane.position.x = firstMesh.getBoundingInfo().boundingBox.centerWorld.x + firstMesh.getBoundingInfo().boundingBox.extendSize.x;
plane.position.y = 0.01;
plane.visibility = 0.7;
const animationBox: Animation = new BABYLON.Animation(
"tutoAnimation",
"scaling.x",
30,
BABYLON.Animation.ANIMATIONTYPE_FLOAT,
BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE
);
const keys = [];
keys.push({
frame: 0,
value: 0.1
});
keys.push({
frame: 30,
value: 1
});
animationBox.setKeys(keys);
plane.animations.push(animationBox);
this.scene.beginAnimation(plane, 0, 30, false).onAnimationEndObservable.addOnce(() => {});
const stepPrecision = this.getStepPrecision();
resolve({
turn: turnToLast,
range: Number(((turnToLast + 1) * this.page.variable.unitStep.valueOf()).toFixed(stepPrecision)),
value: this.targetSubMesh[turnToLast + firstIndex + 1].value.valueOf()
});
});
}
remediationRoadPart2(turn: number): Promise<void> {
return new Promise<void>((resolve, reject) => {
const plane = this.remediationPlanes[0];
const remediationPlanes = this.remediationPlanes;
const scene = this.scene;
const primaryMaterial = plane.material;
const secondaryMaterial = this.createColorMaterial(
new BABYLON.Color3(0.01, 0.81, 0.01) as Color3,
"remediation",
this.assetsContainerOnTheRoad
);
let secondaryMat = true;
repeatPlane(turn, plane, this.page);
function repeatPlane(turn: number, iplane: Mesh, page: JeuJustePointPage) {
if (!page.scenario.skipSequence) {
iplane.animations = [];
const newPlane = iplane.clone("remediation_" + turn);
if (secondaryMat) {
newPlane.material = secondaryMaterial;
} else {
newPlane.material = primaryMaterial;
}
secondaryMat = !secondaryMat;
remediationPlanes.push(newPlane);
newPlane.position = iplane.position.clone();
newPlane.position.x += iplane.getBoundingInfo().boundingBox.extendSize.x * 2;
const animationBox: Animation = new BABYLON.Animation(
"tutoAnimation" + turn,
"rotation.z",
5,
BABYLON.Animation.ANIMATIONTYPE_FLOAT,
BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE
);
const keys = [];
keys.push({
frame: 0,
value: (90 * Math.PI) / 180
});
keys.push({
frame: 5,
value: 0
});
animationBox.setKeys(keys);
newPlane.animations.push(animationBox);
scene.beginAnimation(newPlane, 0, 5, false, 1.5).onAnimationEndObservable.addOnce(() => {
if (turn > 1) {
repeatPlane(turn - 1, newPlane, page);
} else {
resolve();
}
});
} else {
resolve();
}
}
});
}
customLoadingScreen() {
BABYLON.DefaultLoadingScreen.prototype.displayLoadingUI = () => {
this.page.customLoader = true;
};
BABYLON.DefaultLoadingScreen.prototype.hideLoadingUI = () => {
this.page.customLoader = false;
};
}
disposeRemediationRoad() {
this.remediationPlanes.forEach(plane => {
plane.material?.dispose();
plane.dispose();
});
}
}