diff --git a/smart-item/voxter/asset.json b/smart-item/voxter/asset.json index 6f928a5..b0633aa 100644 --- a/smart-item/voxter/asset.json +++ b/smart-item/voxter/asset.json @@ -12,12 +12,120 @@ ], "category": "decorations", "model": "models/voxter_base.glb", - "parameters": [ - { + "parameters": [ + { "id": "tokenId", "label": "TokenId", "type": "integer", "default":32771 - } - ] - } \ No newline at end of file + }, + { + "id": "showBody", + "label": "Show Body", + "type": "boolean", + "default": true + }, + { + "id": "followActive", + "label": "Face Follow User", + "type": "boolean", + "default": false + }, + { + "id": "followSpeed", + "label": "Face Follow Speed", + "type": "float", + "default": 1 + }, + { + "id": "followLockAxis", + "label": "Face Follow Lock Axis", + "type": "boolean", + "default": false + }, + { + "id": "clickable", + "label": "Click on/off", + "type": "boolean", + "default": true + }, + { + "id": "onClick", + "label": "When clicked", + "type": "actions" + }, + { + "id": "onClickText", + "label": "Hover text", + "type": "text", + "default": "Interact" + }, + { + "id": "clickButton", + "label": "Click Button", + "type": "options", + "options": [ + { + "value": "POINTER", + "label": "Click" + }, + { + "value": "PRIMARY", + "label": "Primary Button" + }, + { + "value": "SECONDARY", + "label": "Secondary Button" + } + ], + "default": "POINTER" + } + ], + "actions": [ + { + "id": "activateFollow", + "label": "Activate Follow", + "parameters": [] + }, + { + "id": "deactivateFollow", + "label": "Deactivate Follow", + "parameters": [] + }, + { + "id": "changeTokenId", + "label": "Change Token Id", + "parameters": [ + { + "id": "tokenId", + "label": "TokenId", + "type": "integer", + "default":32771 + } + ] + }, + { + "id": "changeRandomTokenId", + "label": "Change Random Token Id", + "parameters": [ + { + "id": "tokenIdUpper", + "label": "TokenId Upper", + "type": "integer", + "default":32771 + } + ] + }, + { + "id": "putOnItem", + "label": "Wear on Head", + "parameters": [ ] + }, + { + "id": "takeOffItem", + "label": "Take off Head", + "parameters": [ ] + } + + ] +} diff --git a/smart-item/voxter/src/common/faceUerSystem.ts b/smart-item/voxter/src/common/faceUerSystem.ts new file mode 100644 index 0000000..0c04ca3 --- /dev/null +++ b/smart-item/voxter/src/common/faceUerSystem.ts @@ -0,0 +1,55 @@ +@Component('trackUserFlag') +export class TrackUserFlag { + lockXZRotation: boolean = false + active: boolean = false + rotSpeed: number + oneTimeLookAtTarget: Vector3 + constructor(lockXZRotation?: boolean, rotSpeed?: number, active?: boolean) { + if (!faceUserAdded) { + addFaceUserSystem() + } + + this.lockXZRotation = lockXZRotation ? lockXZRotation : false + + this.rotSpeed = rotSpeed ? rotSpeed : 2 + + if (active) { + this.active = true + } + } +} + +let faceUserAdded: boolean = false +const player = Camera.instance + +// Rotates NPC to face the user during interaction +export function addFaceUserSystem() { + faceUserAdded = true + + engine.addSystem(new FaceUserSystem()) +} + +class FaceUserSystem implements ISystem { + private followingNPCs = engine.getComponentGroup(TrackUserFlag) + update(dt: number) { + for (let npc of this.followingNPCs.entities) { + let transform = npc.getComponent(Transform) + let trackUser = npc.getComponent(TrackUserFlag) + if (trackUser.active) { + // Rotate to face the player + let lookAtTarget = new Vector3(player.position.x, player.position.y, player.position.z) + let direction = lookAtTarget.subtract(transform.position) + transform.rotation = Quaternion.Slerp( + transform.rotation, + Quaternion.LookRotation(direction), + dt * trackUser.rotSpeed + ) + + if (trackUser.lockXZRotation) { + transform.rotation.x = 0 + transform.rotation.z = 0 + } + } + } + } +} \ No newline at end of file diff --git a/smart-item/voxter/src/common/voxter/eyes.ts b/smart-item/voxter/src/common/voxter/eyes.ts index 3abb613..36bcb3d 100644 --- a/smart-item/voxter/src/common/voxter/eyes.ts +++ b/smart-item/voxter/src/common/voxter/eyes.ts @@ -1,4 +1,4 @@ -import { COLOR } from "../../lib/decoder"; +import { COLOR } from "../lib/decoder"; const colors = [ `#ff0000`, `#ff00ff`, @@ -21,15 +21,18 @@ const emissiveByColor = [ export const createEyes = (boxter, variationIndex, color, texture) => { const eyes = new Entity(); eyes.setParent(boxter); + //CACHE!?! const mat = new Material(); const eyeShape = new PlaneShape(); eyeShape.withCollisions = false; eyeShape.uvs = getUvs(variationIndex+1); eyes.addComponent(eyeShape); eyes.addComponent(new Transform({ - position:new Vector3(0,1/3-0.025,-0.5001), + //position:new Vector3(0,1/3-0.025,-0.5001), + position:new Vector3(0,1/3-0.025,0.5009), scale:new Vector3(1-0.05,1/3, 1) })); + eyes.getComponent(Transform).rotate(Vector3.Up(), 180) // mat.albedoColor= Color3.FromHexString(color); mat.emissiveColor = Color3.FromHexString(color); mat.emissiveIntensity = emissiveByColor[colors.indexOf(color)]; diff --git a/smart-item/voxter/src/common/voxter/head.ts b/smart-item/voxter/src/common/voxter/head.ts index a0168c0..4b7260d 100644 --- a/smart-item/voxter/src/common/voxter/head.ts +++ b/smart-item/voxter/src/common/voxter/head.ts @@ -12,6 +12,7 @@ export const createHead = (boxter:Entity, index:number) => { ]; const head = new Entity(); head.addComponent(new Transform({position:new Vector3(0,0,0)})) + head.getComponent(Transform).rotate(Vector3.Up(), 180) if(index) head.addComponent(shapes[index-1]); head.setParent(boxter); diff --git a/smart-item/voxter/src/common/voxter/mouth.ts b/smart-item/voxter/src/common/voxter/mouth.ts index 7a0de6e..2080939 100644 --- a/smart-item/voxter/src/common/voxter/mouth.ts +++ b/smart-item/voxter/src/common/voxter/mouth.ts @@ -15,9 +15,11 @@ export const createMouth = (boxter, variationIndex, color, texture) => { const NECK_HEIGHT = 32/256; eyes.addComponent(new Transform({ - position:new Vector3(0,-((1/3)/2),-0.5001), + //position:new Vector3(0,-((1/3)/2),0.5001), + position:new Vector3(0,-((1/3)/2),0.5009), scale:new Vector3(1-0.05,2/3-0.05, 1) })); + eyes.getComponent(Transform).rotate(Vector3.Up(), 180) // mat.albedoColor = Color3.Yellow(); mat.specularIntensity = 0; mat.roughness = 1; diff --git a/smart-item/voxter/src/common/voxter/voxter.ts b/smart-item/voxter/src/common/voxter/voxter.ts index b541d48..5d7ee5a 100644 --- a/smart-item/voxter/src/common/voxter/voxter.ts +++ b/smart-item/voxter/src/common/voxter/voxter.ts @@ -18,18 +18,31 @@ export type VoxterCreationOptions = { position:Vector3, dna:number, rotation:Quaternion + showBody?: boolean }; -export const createVoxter = ({position, dna, rotation}:VoxterCreationOptions) => { +export const createVoxter = ({position, dna, rotation, showBody}:VoxterCreationOptions) => { const [eyeIndex, mouthIndex, eyeColorIndex, headIndex] = decode(Number(dna), propertySizes); const eyeColor = colors[eyeColorIndex]; const state = {dna:Number(dna)}; const entity = new Entity(); - const skin = new Material(); - skin.albedoColor = new Color3(0,0,0); + const boxterShape = new BoxShape(); + boxterShape.withCollisions = true entity.addComponent(boxterShape); - entity.addComponent(skin); + + if(!showBody){ + //cache skin + const skin = new BasicMaterial() + skin.texture = new Texture('models/transparent.png') + skin.alphaTest = 1 + entity.addComponent(skin); + }else{ + const skin = new Material(); + skin.albedoColor = new Color3(0,0,0); + entity.addComponent(skin); + } + const boxterTransform = new Transform({ position, @@ -55,6 +68,7 @@ export const createVoxter = ({position, dna, rotation}:VoxterCreationOptions) => return true; }, getDna:()=>state.dna, + getState:()=>{return state}, getEntity:()=>entity, setParent:(parent:Entity)=>entity.setParent(parent) }; diff --git a/smart-item/voxter/src/game.ts b/smart-item/voxter/src/game.ts index a504461..448d751 100644 --- a/smart-item/voxter/src/game.ts +++ b/smart-item/voxter/src/game.ts @@ -1,12 +1,22 @@ + import { Spawner } from '../node_modules/decentraland-builder-scripts/spawner' -////import Voxter, { Props } from './item' -//// -////const post = new Voxter() -////const spawner = new Spawner(post) -//// -////spawner.spawn( -//// 'voxter', -//// new Transform({ position: new Vector3(8, 1, 8) }), { -//// tokenId: 32771 -////}) -//// \ No newline at end of file +import Voxter, { Props } from './item' + +const button = new Voxter() +const spawner = new Spawner(button) + +spawner.spawn( + 'screen', + new Transform({ + position: new Vector3(4, 0, 8), + }), + { + tokenId: 888, + followActive: true, + followLockAxis: false, + followSpeed: 2, + clickable: true, + onClickText: "What are you looking at motal?" + } +) + diff --git a/smart-item/voxter/src/item.ts b/smart-item/voxter/src/item.ts index 04b0f52..380cdcc 100644 --- a/smart-item/voxter/src/item.ts +++ b/smart-item/voxter/src/item.ts @@ -1,8 +1,24 @@ import {createVoxter} from "./common/voxter/voxter"; +import { TrackUserFlag } from './common/faceUerSystem' export type Props = { - tokenId?: number + tokenId?: number, + followActive?: boolean, + followLockAxis?: boolean, + followSpeed?: number, + clickable?: boolean, + onClick? : Actions, + onClickText?: string, + clickButton?: ActionButton, + showBody?: boolean } +declare enum ActionButton { + POINTER = 'POINTER', + PRIMARY = 'PRIMARY', + SECONDARY = 'SECONDARY', + ANY = 'ANY' +} + export default class Voxter implements IScript { init() { new GLTFShape('models/accesories/acc1.glb'); @@ -15,16 +31,73 @@ export default class Voxter implements IScript { new GLTFShape('models/accesories/acc8.glb'); } - spawn(host: Entity, props: Props, channel: IChannel) { + spawn(host: Entity, props: Props, channel: IChannel) { const voxter = createVoxter({ position:new Vector3(0,0.5,0), rotation:Quaternion.Zero(), - dna:Number(props.tokenId)||32771 + dna:Number(props.tokenId)||32771, + showBody: props.showBody===undefined || props.showBody }); voxter?.setParent(host) + let turningSpeed:number = Number(props.followSpeed)||2; + let isActive:boolean = props.followActive; + let lockXZRotation:boolean = props.followLockAxis; + + host.addComponent(new TrackUserFlag(lockXZRotation, turningSpeed ? turningSpeed : undefined,isActive)) + + if(props.clickable){ + voxter.getEntity().addComponent( + new OnPointerDown( + () => { + //if (this.active[host.name]) { + channel.sendActions(props.onClick) + //} + }, + { + button: props.clickButton, + hoverText: props.onClickText, + distance: 6 + } + ) + ) + } + channel.request('tokenId', (tokenId:number) => (voxter.applyDna(tokenId) && tokenId)) channel.reply('tokenId', () => voxter.getDna()) + + channel.handleAction('changeTokenId', (action) => { + voxter.applyDna(action.values.tokenId) + }) + channel.handleAction('changeRandomTokenId', (action) => { + voxter.applyDna(Math.ceil(Math.random()*258047)) + }) + channel.handleAction('activateFollow', (action) => { + voxter.getEntity().getParent().getComponent(TrackUserFlag).active=true + }) + channel.handleAction('deactivateFollow', (action) => { + voxter.getEntity().getParent().getComponent(TrackUserFlag).active=false + }) + channel.handleAction('putOnItem', (action) => { + //to enable, must keep track of original host to put back when done + //do to host or //voxter.getEntity()? + //host.setParent(Attachable.FIRST_PERSON_CAMERA) + //host.addComponentOrReplace(new Transform({position:new Vector3(0,0,0)})) + //voxter.getEntity().setParent(null); + voxter.getEntity().getComponent(Transform).position=new Vector3(0,0,0) + voxter.getEntity().setParent(Attachable.FIRST_PERSON_CAMERA); + }) + channel.handleAction('takeOffItem', (action) => { + //voxter.getEntity().getParent().getComponent(TrackUserFlag).active=false + //voxter.getEntity().setParent(null); + voxter.getEntity().setParent(host); + voxter.getEntity().getComponent(Transform).position=new Vector3(0,0.5,0) + //put down where? where standing on ground? in space? etc. where last left? + + //voxter.getEntity().setParent(Attachable.FIRST_PERSON_CAMERA); + }) + + } } \ No newline at end of file