import {compose, EventEmitter} from '@avocado/core'; import {Vector} from '@avocado/math'; import {Trait} from '../trait'; import {TraitPositionedPacket} from '../packets/trait-positioned.packet'; 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.x >> 2; const y = this.state.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); } } acceptPacket(packet) { if (packet instanceof TraitPositionedPacket) { this.serverX = (packet.data.position & 0xFFFF) >> 2; this.serverY = (packet.data.position >> 16) >> 2; } } on_positionChanged(oldPosition, newPosition) { this.entity.position[0] = newPosition[0]; this.entity.position[1] = newPosition[1]; if (AVOCADO_SERVER) { this.state.x = newPosition[0] << 2; this.state.y = newPosition[1] << 2; } this.entity.emit('positionChanged', oldPosition, newPosition); } onServerPositionChanged() { this.serverPositionDirty = true; } packetsForUpdate() { const packets = []; if (this.isDirty) { // Physics slop can end us up with negatives. Don't allow them in the // packed representation, even though it means a slight loss in accuracy. // The world bounds will (should!) keep things *eventually* correct. const x = Math.max(0, this.state.x); const y = Math.max(0, this.state.y); const packed = (y << 16) | (x << 0); packets.push(new TraitPositionedPacket({ position: packed, }, this.entity)); this.makeClean(); } return packets; } set relaxServerPositionConstraintIfNearerThan(nearerThan) { this._relaxServerPositionConstraintIfNearerThan = nearerThan; } 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)); } }