UNPKG

@planarally/dice

Version:

3D dice rolling functionality for babylon.js.

207 lines (206 loc) 8.33 kB
import { Ray } from "@babylonjs/core/Culling/ray"; import { HavokPlugin, PhysicsAggregate, PhysicsShapeType } from "@babylonjs/core/Physics"; import { Engine } from "@babylonjs/core/Engines/engine"; import { SceneLoader } from "@babylonjs/core/Loading/sceneLoader"; import { Color3, Vector3 } from "@babylonjs/core/Maths/math"; import { Scene } from "@babylonjs/core/scene"; import HavokPhysics from "@babylonjs/havok"; import { rollParts } from "../core/roll"; import { uuidv4 } from "../utils/uuid"; // Load side-effects that are not by default loaded with the tree-shaking above import "@babylonjs/core/Loading"; import "@babylonjs/core/Materials/standardMaterial"; import "@babylonjs/core/Materials/PBR/pbrMaterial"; import "@babylonjs/core/Physics/physicsEngineComponent"; import "@babylonjs/core/Physics/v1/physicsEngineComponent"; export class DiceThrower { loaded = false; scene; tresholds = { angular: 0.075, linear: 0.1, }; physics = { friction: 0.5, mass: 20, restitution: 0.3, }; freezeOnDecision = true; clearAfter = 5000; activeDiceSystem; meshMap = new Map(); activeRolls = new Map(); constructor(options) { if (options.scene) { this.scene = options.scene; } else if (options.canvas) { const engine = new Engine(options.canvas, options.antialias, options.engineOptions); this.scene = new Scene(engine); } else { throw new Error("Expected either a scene or a canvas element"); } if (options.tresholds) { this.tresholds = options.tresholds; } if (options.physics) { this.physics = options.physics; } if (options.freezeOnDecision) { this.freezeOnDecision = options.freezeOnDecision; } if (options.clearAfter) { this.clearAfter = options.clearAfter; } } /** * Loads the physics engine * * This NEEDS to be run before any dice throwing can happen. */ async loadPhysics(gravity) { const havokInstance = await HavokPhysics(); const hk = new HavokPlugin(true, havokInstance); this.scene.enablePhysics(gravity ?? new Vector3(0, -10, 0), hk); this.loaded = true; } async loadMeshes(meshUrl, scene) { const asyncLoad = await SceneLoader.ImportMeshAsync("", meshUrl, undefined, scene); for (const mesh of asyncLoad.meshes) { mesh.setEnabled(false); mesh.isVisible = true; this.meshMap.set(mesh.name, mesh); } } async loadSystem(diceSystem) { this.activeDiceSystem = diceSystem; } startRenderLoop() { this.scene.getEngine().runRenderLoop(() => this.scene.render()); this.scene.onAfterPhysicsObservable.add(this.checkSolutions.bind(this)); } checkSolutions() { for (const [key, activeRolls] of this.activeRolls.entries()) { let allDone = true; for (const activeRoll of activeRolls) { if (activeRoll.done) continue; const physicsBody = activeRoll.mesh.physicsBody; if (!physicsBody) { console.warn("No physics body found for mesh", activeRoll.mesh.name); this.activeRolls.delete(key); continue; } const angularVelocity = physicsBody.getAngularVelocity(); const velocity = physicsBody.getLinearVelocity(); const isDone = Math.abs(angularVelocity.x) < this.tresholds.angular && Math.abs(angularVelocity.y) < this.tresholds.angular && Math.abs(angularVelocity.z) < this.tresholds.angular && Math.abs(velocity.x) < this.tresholds.linear && Math.abs(velocity.y) < this.tresholds.linear && Math.abs(velocity.z) < this.tresholds.linear; if (isDone) { activeRoll.done = true; const ray = new Ray(activeRoll.mesh.position, activeRoll.pickVector ?? new Vector3(0, 1, 0), 100); const pickResult = this.scene.pickWithRay(ray, (mesh) => mesh === activeRoll.mesh); if (pickResult?.hit) { activeRoll.resolve({ dieName: activeRoll.dieName, faceId: pickResult.faceId }); } else { activeRoll.reject(); } } else { allDone = false; } } if (allDone && this.clearAfter > 0) { this.activeRolls.delete(key); setTimeout(() => { for (const activeRoll of activeRolls) { activeRoll.mesh.dispose(); } }, this.clearAfter); } } } async rollString(inputString, rollOptions) { if (!this.activeDiceSystem) { throw new Error("No dice system loaded. Call .loadSystem() first!"); } return await this.rollParts(this.activeDiceSystem.parse(inputString), rollOptions); } async rollParts(parts, rollOptions) { if (!this.activeDiceSystem) { throw new Error("No dice system loaded. Call .loadSystem() first!"); } return await rollParts(parts, this.activeDiceSystem, rollOptions); } async throwDice(rolls, defaultOptions) { if (!this.loaded) { throw new Error("Physics Engine has not been properly loaded. first call .load()!"); } const key = uuidv4(); const keyRolls = []; this.activeRolls.set(key, keyRolls); const promises = []; for (const roll of rolls) { const mesh = this.createDie(roll.name, roll.options ?? defaultOptions); promises.push(new Promise((resolve, reject) => keyRolls.push({ dieName: roll.name, done: false, mesh, reject, resolve, pickVector: roll.pickVector, }))); // eslint-disable-next-line no-await-in-loop await new Promise((r) => setTimeout(r, 50)); // wait 50ms to throw next die } return { key, results: await Promise.all(promises) }; } createDie(meshName, options) { const ogMesh = this.meshMap.get(meshName); if (ogMesh === undefined) { throw new Error(`Mesh ${meshName} not found`); } const mesh = ogMesh.clone(); mesh.setEnabled(true); // custom colours if (options?.color !== undefined) { const newMat = mesh.material.clone(options.color); newMat.albedoColor = Color3.FromHexString(options.color); mesh.material = newMat; } const defaultLinearVelocity = new Vector3(Math.random() * 10, 0, Math.random() * 10); const defaultAngularVelocity = new Vector3(Math.random() * 4, 0, Math.random() * 4); const vectors = options?.physics?.(); mesh.position = vectors?.position ?? new Vector3(0, 10, 0); mesh.rotation = vectors?.rotation ?? new Vector3(Math.random() * 2 * Math.PI, Math.random() * 2 * Math.PI, Math.random() * 2 * Math.PI); const agg = new PhysicsAggregate(mesh, PhysicsShapeType.CONVEX_HULL, { friction: this.physics.friction, mass: this.physics.mass, restitution: this.physics.restitution, }); agg.body.setLinearVelocity(vectors?.linear ?? defaultLinearVelocity); agg.body.setAngularVelocity(vectors?.angular ?? defaultAngularVelocity); return mesh; } remove(key) { const activeRolls = this.activeRolls.get(key); for (const activeRoll of activeRolls ?? []) { activeRoll.mesh.dispose(); this.activeRolls.delete(key); } } removeAll() { for (const activeRolls of this.activeRolls.values()) { for (const activeRoll of activeRolls) activeRoll.mesh.dispose(); } this.activeRolls.clear(); } }