import { ISystem, System, Component, Components } from '../../common/engine/system'; import { IEntity, State, DoSet, DoCreate, DoDestroy } from '../../common/engine/state'; import * as THREE from 'three'; import { DoImpulse, DoAttack, DoTorqueImpulse } from '../../common/system/ammo-physics'; import { DoDamage } from '../../common/system/stats'; import { IPickupAction } from '../../common/system/items'; import { keyboard } from '../engine/input/devices'; // todo: remove, input system should be connecting to this properly import { IAction } from '../../common/engine/state'; const equals = require('deep-equal'); interface IControllerComponent { turnSpeed: number; forwardSpeed: number; cameraOffset: number[]; cameraLookAt: number[]; cameraLerp: number; } const defaults: IControllerComponent = { turnSpeed: 1, forwardSpeed: 1, cameraLerp: 1, cameraOffset: [0, 5, 5], cameraLookAt: [0, 0, 0] }; const defaultTransform = { position: [0, 0, 0], rotation: [0, 0, 0], quaternion: [0, 0, 0, 0] }; @Components('controller', 'position', 'rotation', 'body') export class Controller extends System { // used for three math operations private static t3d = new THREE.Object3D(); private jumpCooldown = 0; private attackCooldown = 0; private camera: IEntity; private controllers: { [key: string]: IEntity } = {}; private inputToggle: number[]; // (right, rot, fwd) private totalInput: number[]; start(state: State) { super.start(state); this.inputToggle = [0, 0, 0]; this.totalInput = [0, 0, 0]; } // we actually care about the component data add(entity: IEntity) { super.add(entity); if (entity.camera) { if (!entity.camera.position || !entity.camera.rotation) { entity.camera = Object.assign({ position: [0, 0, 0], rotation: [0, 0, 0] }, entity.camera); this.dispatch(DoSet(entity.id, 'camera', entity.camera)); } this.update_camera(entity); } // Require position and rotation set if (!entity.position) { this.dispatch(DoSet(entity.id, 'position', [0, 0, 0])); } if (!entity.velocity) { this.dispatch(DoSet(entity.id, 'velocity', [0, 0, 0])); } if (!entity.rotation) { this.dispatch(DoSet(entity.id, 'rotation', [0, 0, 0])); } if (!entity.quaternion) { this.dispatch(DoSet(entity.id, 'quaternion', [0, 0, 0, 0])); } if (entity.controller) { this.update(entity, 'controller'); } } remove(entity: IEntity) { super.remove(entity); if (entity.controller) { delete this.controllers[entity.id]; } if (entity.camera) { delete this.camera; } } @Component('camera') update_camera(entity) { this.camera = entity; } update(entity, component) { if (entity.controller) { this.controllers[entity.id] = entity; } super.update(entity, component); } tick(delta) { this.actions({ PICKUP: this.HandlePickup, INPUT_forward: this.HandleForward, INPUT_right: this.HandleTurn, INPUT_up: this.HandleUp, INPUT_attack: this.HandleAttack, }); this.jumpCooldown -= delta; this.attackCooldown -= delta; Object.keys(this.controllers).forEach(key => { const entity = this.controllers[key]; const controls: IControllerComponent = Object.assign( {}, defaults, entity.controller); const t3d = Controller.t3d; const keys = keyboard.rawKeys; const forward = this.totalInput[2] + this.inputToggle[2]; const turn = this.totalInput[1] + this.inputToggle[1]; const up = this.totalInput[0] + this.inputToggle[0]; const pos = entity.position || [0, 0, 0]; t3d.position.fromArray(pos); t3d.rotation.fromArray((entity.rotation || [0, 0, 0]).map(THREE.Math.degToRad)); const quat = entity.quaternion || [0, 0, 0, 0]; t3d.quaternion.fromArray(quat); t3d.rotateY(turn * delta * controls.turnSpeed); t3d.translateZ(forward * delta * controls.forwardSpeed); t3d.translateY(up * delta * controls.forwardSpeed); const velocity = t3d.position.toArray().map((n, i) => (n - pos[i]) * 5000); // const rotation = t3d.rotation.toArray().slice(0, 3).map(THREE.Math.radToDeg); const dr = turn * delta * controls.turnSpeed * 900; const rotation = [0, dr, 0]; if (keys.space && this.jumpCooldown < 0) { this.dispatch(DoImpulse(entity, [0, 4000, 0])); this.dispatch(DoDamage(entity, 'health', 20)); this.jumpCooldown = 1; } if (!equals([0, 0, 0], rotation)) { this.dispatch(DoTorqueImpulse(entity, rotation)); } if (!equals([0, 0, 0], velocity)) { this.dispatch(DoImpulse(entity, velocity)); } if (this.camera) { const axis = new THREE.Vector3().fromArray(controls.cameraLookAt); const camera = this.camera.camera; const dstPosition = t3d.position.clone().add(axis); const camPosition = t3d.position.clone().add(new THREE.Vector3().fromArray(controls.cameraOffset)); t3d.position.fromArray(camera.position); t3d.rotation.fromArray(camera.rotation.map(THREE.Math.degToRad)); t3d.position.lerp(camPosition, controls.cameraLerp); const newCamera = Object.assign({}, camera, { target: dstPosition.toArray(), position: t3d.position.toArray() }); if (!equals(newCamera, camera)) { this.dispatch(DoSet(this.camera.id, 'camera', newCamera)); } } this.totalInput = [0, 0, 0]; }); } private HandleForward(a: IAction) { if (a.toggle) this.inputToggle[2] = a.value * -1; else this.totalInput[2] += a.value * -1; } private HandleTurn(a: IAction) { if (a.toggle) this.inputToggle[1] = a.value; else this.totalInput[1] += a.value; } private HandleUp(a: IAction) { if (a.toggle) this.inputToggle[0] = a.value; else this.totalInput[0] += a.value; } private HandleAttack(a: IAction) { const entity = this.controllers[a.entityId]; if(!entity) return; if (this.attackCooldown > 0) return; this.dispatch(DoAttack(entity)); this.attackCooldown = 0.2; } private HandlePickup(a: IPickupAction) { const player = this.entities[a.targetId]; if (!player) return; // find an empty slot: const IsSlotEmpty = (name: string): boolean => { //const ret = player.have && player.have.isArray() && player.have.contains(name); // todo: use this when 'have' is implemented const ret = !player.equipment || !player.equipment[name] || !player.equipment[name]; return ret; } const GetAvailableSlot = (slot: string): Promise => { if (player.slots) { let slotObj = player.slots[a.slot]; if (slotObj) { if (Array.isArray(slotObj)) { // series of slots, pick next available let ret; let foundSlot = slotObj.some((data) => { const name = a.slot + "." + data.subName; if (IsSlotEmpty(name)) { ret = Promise.resolve({ name, data }); return true; } return false; }); if (foundSlot) return ret; } else if (IsSlotEmpty(a.slot)) { return Promise.resolve({ name: a.slot, data: slotObj }); } } } return Promise.reject("slot is full"); } GetAvailableSlot(a.slot) .then((slotData) => { { // todo: remove when 'have' is implemented if(!player.equipment) player.equipment = {}; if(!player.equipment[slotData.name]) player.equipment[slotData.name] = true; this.dispatch(DoSet(player.id, "equipment", player.equipment)); } const newEntity = { is: a.equipObj, position: slotData.data.position, rotation: slotData.data.rotation }; this.dispatch(DoCreate(`${a.targetId}.${slotData.name}`, newEntity), true); this.dispatch(DoDestroy(a.itemId), true); }) .catch((err) => { }); } }