import {compose, EventEmitter} from '@avocado/core'; import {Vector} from '@avocado/math'; import {Trait} from '../trait'; const decorate = compose( EventEmitter, Vector.Mixin('_position', 'x', 'y', { track: true, }), Vector.Mixin('serverPosition', 'serverX', 'serverY', { track: true, }), ); // < 16768 will pack into 1 short per axe and give +/- 0.25 precision. export class Positioned extends decorate(Trait) { static defaultState() { return { x: 0, y: 0, }; } static type() { return 'positioned'; } constructor(entity, params, state) { super(entity, params, state); this.on('_positionChanged', this.on_positionChanged, this); const x = this.state.get('x') >> 2; const y = this.state.get('y') >> 2; this._position = [x, y]; this.entity.position[0] = x; this.entity.position[1] = y; if (AVOCADO_CLIENT) { this._relaxServerPositionConstraintIfNearerThan = 0; this.serverPosition = this._position; this.serverPositionDirty = false; this.on('serverPositionChanged', this.onServerPositionChanged, this); } } destroy() { this.off('_positionChanged', this.on_positionChanged); if (AVOCADO_CLIENT) { this.off('serverPositionChanged', this.onServerPositionChanged); } } on_positionChanged(oldPosition, newPosition) { this.entity.position[0] = newPosition[0]; this.entity.position[1] = newPosition[1]; if (AVOCADO_SERVER) { const x = newPosition[0] << 2; const y = newPosition[1] << 2; this.state = this.state.withMutations((state) => { state.set('x', x).set('y', y); }); this.entity.setTraitDirty('positioned'); } this.entity.emit('positionChanged', oldPosition, newPosition); } onServerPositionChanged() { this.serverPositionDirty = true; } patchStateStep(key, step) { if ('state' !== key) { return; } const stateKey = step.path.substr(1); const value = this.transformPatchValue(stateKey, step.value); switch (stateKey) { case 'x': this.serverX = value; break; case 'y': this.serverY = value; break; default: super.patchStateStep(key, step); break; } } set relaxServerPositionConstraintIfNearerThan(nearerThan) { this._relaxServerPositionConstraintIfNearerThan = nearerThan; } transformPatchValue(key, value) { if (-1 !== ['x', 'y'].indexOf(key)) { return value / 4; } super.transformPatchValue(key, value); } listeners() { return { isTickingChanged: () => { // Snap position on ticking change. if (AVOCADO_CLIENT) { this._position = this.serverPosition; } }, }; } methods() { return { setPosition: (position) => { this._position = position; }, }; } renderTick(elapsed) { if (!this.serverPositionDirty) { return; } if (Vector.equals(this._position, this.serverPosition)) { this.serverPositionDirty = false; return; } if (Vector.equalsClose(this._position, this.serverPosition, 0.1)) { this._position = this.serverPosition; this.serverPositionDirty = false; return; } const diff = Vector.sub(this.serverPosition, this._position); const nearerThan = this._relaxServerPositionConstraintIfNearerThan; let lerp; if ( nearerThan && Vector.equalsClose(this._position, this.serverPosition, nearerThan) ) { lerp = .01; } else { lerp = .5; } this._position = Vector.add(this._position, Vector.scale(diff, lerp)); } }