objects locking system

This commit is contained in:
2025-11-13 16:32:55 +02:00
parent c8e501ff6e
commit a2f9f73e85
10 changed files with 119 additions and 56 deletions
+1 -1
View File
@@ -22,7 +22,7 @@
<v-list nav> <v-list nav>
<v-list-item prepend-icon="mdi-database" to="/game-objects/list" :title="l.gameObjects"></v-list-item> <v-list-item prepend-icon="mdi-database" to="/game-objects/list" :title="l.gameObjects"></v-list-item>
<v-list-item prepend-icon="mdi-receipt-text-edit-outline" to="/scenarios/list" :title="l.gameScenarios"></v-list-item> <v-list-item prepend-icon="mdi-receipt-text-edit-outline" to="/scenarios/list" :title="l.gameScenarios"></v-list-item>
<v-list-item prepend-icon="mdi-cogs" :title="l.gameRules"></v-list-item> <!-- <v-list-item prepend-icon="mdi-cogs" :title="l.gameRules"></v-list-item> -->
<v-divider></v-divider> <v-divider></v-divider>
<v-list-item prepend-icon="mdi-controller" :title="l.games" to="/games/list"></v-list-item> <v-list-item prepend-icon="mdi-controller" :title="l.games" to="/games/list"></v-list-item>
<v-list-item prepend-icon="mdi-cog-play" :title="l.play" to="/play/list"></v-list-item> <v-list-item prepend-icon="mdi-cog-play" :title="l.play" to="/play/list"></v-list-item>
@@ -9,20 +9,6 @@ class GenericObject extends EventDispatcher{
this.object = centerOrigin(this.source.scene) this.object = centerOrigin(this.source.scene)
if (!data.exclude){ if (!data.exclude){
const bckGeometry = new SphereGeometry(getBoundingBoxMaxLength(this.object.userData.bbox)/2,8,8)
const bckMesh = new Mesh(bckGeometry, new MeshStandardMaterial({
transparent: true,
opacity:1, color: 0xaaaaaa,
alphaMap: await engine.loadTexture('locked.webp', '/static/textures/'),
}))
this.object.add(bckMesh)
engine.motionQueue.add(
{ o: bckMesh, a:{
material: { opacity: k=>0.11 + 0.52*Math.sin(k*Math.PI)},
rotation: { y: k=>k*Math.PI*2},
scale: (k,s)=>s.setScalar(1+0.11*Math.sin(2*k*Math.PI))
}, r: true, t: 3 },
)
engine.clickable.add(this.object, async e=>{ engine.clickable.add(this.object, async e=>{
this.object._active = !this.object._active; this.object._active = !this.object._active;
if (engine.dashboard){ if (engine.dashboard){
@@ -1,4 +1,4 @@
import { Group, EventDispatcher } from "three"; import { Group, EventDispatcher, MeshStandardMaterial, Mesh, SphereGeometry } from "three";
import { GenericObject } from "./GenenricObject"; import { GenericObject } from "./GenenricObject";
import { TextObject } from "./TextObject"; import { TextObject } from "./TextObject";
@@ -13,7 +13,8 @@ import { PuzzleGame4 } from "./PuzzleGame4";
// import { Game6 } from "./games/Game6"; // import { Game6 } from "./games/Game6";
import { MazeQuizGame } from "./MazeQuizGame/MazeQuizGame"; import { MazeQuizGame } from "./MazeQuizGame/MazeQuizGame";
import { Particles } from "./Particles"; import { Particles } from "./Particles";
import { assignMaterial, assignParams } from "@/lib/MeshUtils"; import { assignMaterial, assignParams, wrapInGroup, getBoundingBoxMaxLength } from "@/lib/MeshUtils";
import { GameEngine } from "@/lib/GameEngine";
const InteractiveObjectsImports = { const InteractiveObjectsImports = {
GenericObject, CharacterObject, TextObject, ImageObject, VideoPlayer, Particles, GenericObject, CharacterObject, TextObject, ImageObject, VideoPlayer, Particles,
@@ -72,12 +73,52 @@ class InteractiveObject extends EventDispatcher{
this.io.addEventListener?.('finish', this.dispatchEvent.bind(this)) this.io.addEventListener?.('finish', this.dispatchEvent.bind(this))
break; break;
} }
if (obj.shouldBeLocked){
this.object = wrapInGroup(this.object)
this.locker = new Locker(gameEngine, this.object);
this.locker.lock();
}
assignParams(this.object, obj); assignParams(this.object, obj);
resolve(this); resolve(this);
}); });
} }
} }
class Locker{
static materialLocked = new MeshStandardMaterial({
transparent: true, opacity:1, color: 0xaaaaaa
})
constructor(engine, group){
const bckGeometry = new SphereGeometry(getBoundingBoxMaxLength(group.userData.bbox)/2,8,8)
const bckMesh = new Mesh(bckGeometry, Locker.materialLocked);
bckMesh.visible = false;
group.add(bckMesh)
function animate(){
engine.motionQueue.add(
{ o: bckMesh, a:{
material: { opacity: k=>0.11 + 0.52*Math.sin(k*Math.PI)},
rotation: { y: k=>k*Math.PI*2},
scale: (k,s)=>s.setScalar(1+0.11*Math.sin(2*k*Math.PI))
}, r: true, t: 3 },
)
}
this.object = bckMesh;
this.lock = function(){
bckMesh.visible = true;
//bckMesh.material.needsUpdate = true;
engine.motionQueue.clear(bckMesh);
group.__locked = true;
animate();
}
this.unlock = function(){
bckMesh.visible = false;
engine.motionQueue.clear(bckMesh);
group.__locked = false;
}
}
}
GameEngine.loadTexture('locked.webp', '/static/textures/', null, [Locker.materialLocked, 'alphaMap'])
const InteractiveObjectTypes = [ const InteractiveObjectTypes = [
{ {
id: 'CharacterObject', name: 'Character' id: 'CharacterObject', name: 'Character'
@@ -47,7 +47,8 @@ export default {
mounted(){ mounted(){
this.modelValue.questions ??= []; this.modelValue.questions ??= [];
this.modelValue.questionPoints ??= 10; this.modelValue.questionPoints ??= 10;
this.modelValue.questionPenalty ??= 0 this.modelValue.questionPenalty ??= 0;
this.modelValue.exclude = true;
}, },
methods:{ methods:{
addQuestion(){ addQuestion(){
@@ -44,7 +44,7 @@ class VideoPlayer extends EventDispatcher {
//material.opacity = 0.5; //material.opacity = 0.5;
if (data.playInHud){ if (data.playInHud){
engine.dashboard?.detach(plane, { engine.dashboard?.detach(plane, {
skipTransition: !data.skipTransition skipTransition: data.skipTransition
}); });
} }
}) })
+7 -1
View File
@@ -24,7 +24,13 @@ class Clickable {
this.update = function (mouse, camera, event) { this.update = function (mouse, camera, event) {
raycaster.setFromCamera(mouse, camera); raycaster.setFromCamera(mouse, camera);
let forExecute = []; let forExecute = [];
objects.forEach(o => { objects.filter(o=>{
do {
if (o.__locked) return false;
o = o.parent;
} while (o);
return true;
}).forEach(o => {
o.getWorldPosition(v); o.getWorldPosition(v);
if (camera.position.distanceTo(v) <= o._clickable.distance && o.visible) { if (camera.position.distanceTo(v) <= o._clickable.distance && o.visible) {
const intersects = raycaster.intersectObject(o); const intersects = raycaster.intersectObject(o);
+12 -10
View File
@@ -5,6 +5,7 @@ import {
import { Text } from "troika-three-text"; import { Text } from "troika-three-text";
class DashBoard { class DashBoard {
#points = 0;
constructor(engine) { constructor(engine) {
let svg = p=>`<?xml version="1.0" encoding="UTF-8"?> let svg = p=>`<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1"> <svg xmlns="http://www.w3.org/2000/svg" version="1.1">
@@ -25,7 +26,6 @@ class DashBoard {
let updating = false; let updating = false;
let params = {} let params = {}
let occupied = false; let occupied = false;
let points = 0;
img.addEventListener('load', function () { img.addEventListener('load', function () {
ctx.drawImage(img, 0, 0, engine.w, engine.h); ctx.drawImage(img, 0, 0, engine.w, engine.h);
@@ -152,13 +152,13 @@ class DashBoard {
this.levelProgress = levelProgress; this.levelProgress = levelProgress;
this.addPoints = function(p){ this.addPoints = function(p){
points += p; this.#points += p;
engine.motionQueue.add({ engine.motionQueue.add({
o: pointsText, o: pointsText,
a: {rotation:{y: Math.PI}, scale:{x:1.5, y:1.5, z:1.5}}, a: {rotation:{y: Math.PI}, scale:{x:1.5, y:1.5, z:1.5}},
t: 0.25, t: 0.25,
f:()=>{ f:()=>{
pointsText.text = points; pointsText.text = this.#points;
pointsText.sync(); pointsText.sync();
engine.motionQueue.add({ engine.motionQueue.add({
o: pointsText, o: pointsText,
@@ -242,13 +242,11 @@ class DashBoard {
object._hud.parent?.attach(object); object._hud.parent?.attach(object);
hud.rotation.y = 0; hud.rotation.y = 0;
hud.visible = false; hud.visible = false;
if (!opts.skipTransition){ engine.motionQueue.add({
engine.motionQueue.add({ o: object,
o: object, a: object._hud.placement,
a: object._hud.placement, t: opts.skipTransition ? 0 : 1
t: 1 });
});
}
delete object._hud; delete object._hud;
occupied = false; occupied = false;
hudAnimation = null; hudAnimation = null;
@@ -268,6 +266,10 @@ class DashBoard {
set active(v){ set active(v){
this.group.visible = v; this.group.visible = v;
} }
get points(){
return this.#points;
}
} }
class ProgressBar{ class ProgressBar{
+19 -12
View File
@@ -16,6 +16,8 @@ import { Clickable } from './Clickable.js';
import { DashBoard } from './Dashboard.js'; import { DashBoard } from './Dashboard.js';
import { MotionEngine } from './MotionEngine.js'; import { MotionEngine } from './MotionEngine.js';
THREE.Cache.enabled = true
const assetPath = '/asset/default/'; const assetPath = '/asset/default/';
class GameEngine extends THREE.EventDispatcher{ class GameEngine extends THREE.EventDispatcher{
@@ -161,7 +163,7 @@ class GameEngine extends THREE.EventDispatcher{
domNode.appendChild(renderer.domElement); domNode.appendChild(renderer.domElement);
let texture = await this.loadTexture('/static/textures/bck.webp', ''); let texture = await GameEngine.loadTexture('/static/textures/bck.webp', '');
// let bck = await this.loadTexture('/static/textures/bck.webp'); // let bck = await this.loadTexture('/static/textures/bck.webp');
// bck.premultiplyAlpha = true; // bck.premultiplyAlpha = true;
texture.mapping = THREE.EquirectangularReflectionMapping; texture.mapping = THREE.EquirectangularReflectionMapping;
@@ -415,18 +417,8 @@ class GameEngine extends THREE.EventDispatcher{
}) })
} }
async loadTexture(url, path = assetPath, progress) {
return new Promise((resolve, reject) => {
new THREE.TextureLoader().load(`${path}${url}`, texture => {
//texture.encoding = THREE.sRGBEncoding;
texture.colorSpace = THREE.SRGBColorSpace;
resolve(texture)
}, progress, reject)
})
}
async loadPanorama(url, path = assetPath) { async loadPanorama(url, path = assetPath) {
let t = await this.loadTexture(url, path); let t = await GameEngine.loadTexture(url, path);
t.mapping = THREE.EquirectangularReflectionMapping; t.mapping = THREE.EquirectangularReflectionMapping;
this.scene.background = t; this.scene.background = t;
this.scene.environment = t; this.scene.environment = t;
@@ -567,6 +559,21 @@ class GameEngine extends THREE.EventDispatcher{
this.clickable.removeAll(); this.clickable.removeAll();
this.motionQueue.clearAll(); this.motionQueue.clearAll();
} }
static textureLoader = new THREE.TextureLoader();
static async loadTexture(url, path = assetPath, progress, assignTo) {
return new Promise((resolve, reject) => {
GameEngine.textureLoader.load(`${path}${url}`, texture => {
texture.colorSpace = THREE.SRGBColorSpace;
if (assignTo){
assignTo[0][assignTo[1]] = texture;
}
resolve(texture)
}, progress, reject)
})
}
loadTexture = GameEngine.loadTexture
} }
export { GameEngine } export { GameEngine }
+14 -6
View File
@@ -55,16 +55,24 @@ function autoScale(object, mk = 1) {
object.scale.multiplyScalar(mk / k); object.scale.multiplyScalar(mk / k);
} }
function wrapInGroup(object){
if (object.isWrapper) return object;
let group = new Group();
group.userData.bbox = getBoundingBox(object);
group.add(object);
group.userData.object = object;
group.isWrapper = true;
return group;
}
function centerOrigin(object){ function centerOrigin(object){
let result = new Group(); let group = wrapInGroup(object);
result.userData.bbox = getBoundingBox(object); let position = getBoundingBoxCenterPoint(group.userData.bbox, object.position).negate();
let position = getBoundingBoxCenterPoint(result.userData.bbox, object.position).negate();
object.position.copy(position) object.position.copy(position)
result.add(object); return group;
return result;
} }
export { export {
assignParams, assignMaterial, autoScale, centerOrigin, assignParams, assignMaterial, autoScale, centerOrigin, wrapInGroup,
getBoundingBox, getBoundingBoxSize, getBoundingBoxMaxLength, getBoundingBoxCenterPoint getBoundingBox, getBoundingBoxSize, getBoundingBoxMaxLength, getBoundingBoxCenterPoint
} }
+20 -8
View File
@@ -146,7 +146,13 @@ export default {
if (this.scene.data.items){ if (this.scene.data.items){
let loaded = 0; let loaded = 0;
for (let i of this.scene.data.items) { for (let i of this.scene.data.items) {
if (this.env == 'GamePlaying'){
if (i.data.activationTriggers?.length || i.data.activationScore){
i.data.shouldBeLocked = true;
}
}
let io = await new InteractiveObject(gameEngine, i.data) let io = await new InteractiveObject(gameEngine, i.data)
i.__io = io;
this.setObjectAttributes(l, i.data, io.object, io.source, 1); this.setObjectAttributes(l, i.data, io.object, io.source, 1);
gameEngine.activeObjects.add(io.object); gameEngine.activeObjects.add(io.object);
if (this.env == 'GamePlaying'){ if (this.env == 'GamePlaying'){
@@ -167,8 +173,20 @@ export default {
} }
} }
io.addEventListener('finish', ()=>{ io.addEventListener('finish', ()=>{
gameEngine.dashboard?.addPoints(i.data.points) if (!i.data.pointsGiven){
i.data.points = 0; gameEngine.dashboard?.addPoints(i.data.points)
}
i.data.pointsGiven = true;
this.scene.data.items.forEach(di=>{
if (di.data.activationTriggers?.includes(i.data.id)){
let idx = di.data.activationTriggers.indexOf(i.data.id)
di.data.activationTriggers.splice(idx, 1);
}
if (!di.data.activationTriggers?.length && di.__io.object.__locked &&
gameEngine.dashboard.points > di.data.activationScore){
di.__io.locker.unlock();
}
})
}) })
} }
loaded += 1/this.scene.data.items.length loaded += 1/this.scene.data.items.length
@@ -189,12 +207,6 @@ export default {
} }
gameEngine.dashboard?.loading(1) gameEngine.dashboard?.loading(1)
// let camera = new gameEngine.$.PerspectiveCamera();
// let cameraHelper = new gameEngine.$.CameraHelper(camera);
// gameEngine.activeObjects.add(cameraHelper);
// gameEngine.activeObjects.add(camera);
// this.setObjectAttributes(l, { id: 'camera', 'title': 'Main camera' }, { scene: camera })
// cameraHelper.update();
}, },
targetPointerDown(){ targetPointerDown(){