feat: entity packets!
This commit is contained in:
parent
d10f619bde
commit
1a5015f4b6
|
@ -3,6 +3,7 @@ import React from 'react';
|
|||
import ReactDOM from 'react-dom';
|
||||
// 2nd party.
|
||||
import {compose} from '@avocado/core';
|
||||
import {EntityPacketSynchronizer} from '@avocado/entity';
|
||||
import {Stage} from '@avocado/graphics';
|
||||
import {ActionRegistry, InputPacket} from '@avocado/input';
|
||||
import {Vector} from '@avocado/math';
|
||||
|
@ -93,10 +94,10 @@ export class App extends decorate(class {}) {
|
|||
this.pointingAt = [-1, -1];
|
||||
this.pointerMovementHandle = undefined;
|
||||
// Net.
|
||||
// this.AugmentedParser = augmentParserWithThroughput(SocketIoParser);
|
||||
this.AugmentedParser = augmentParserWithThroughput(SocketIoParser);
|
||||
this.entityPacketSynchronizer = new EntityPacketSynchronizer();
|
||||
this.hasReceivedState = false;
|
||||
this.isConnected = false;
|
||||
this.packetsToSend = [];
|
||||
this.socket = undefined;
|
||||
// Simulation.
|
||||
this.tps = new CycleTracker(config.simulationFrequency);
|
||||
|
@ -168,7 +169,7 @@ export class App extends decorate(class {}) {
|
|||
connect() {
|
||||
const config = this.readConfig();
|
||||
this.socket = new SocketClient(config.connectionUrl, {
|
||||
// parser: this.AugmentedParser,
|
||||
parser: this.AugmentedParser,
|
||||
});
|
||||
this.socket.on('connect', () => {
|
||||
this.removeFromDom(document.querySelector('.app'));
|
||||
|
@ -211,26 +212,6 @@ export class App extends decorate(class {}) {
|
|||
return reactContainer;
|
||||
}
|
||||
|
||||
sendPackets() {
|
||||
// Merge.
|
||||
const packetMergeMap = new Map();
|
||||
for (let i = 0; i < this.packetsToSend.length; i++) {
|
||||
const packet = this.packetsToSend[i];
|
||||
const Packet = packet.constructor;
|
||||
if (!packetMergeMap.has(Packet)) {
|
||||
packetMergeMap.set(Packet, packet);
|
||||
continue;
|
||||
}
|
||||
packetMergeMap.get(Packet).mergeWith(packet);
|
||||
}
|
||||
const packetList = Array.from(packetMergeMap.values());
|
||||
for (let i = 0; i < packetList.length; i++) {
|
||||
const packet = packetList[i];
|
||||
this.socket.send(packet);
|
||||
}
|
||||
this.packetsToSend = [];
|
||||
}
|
||||
|
||||
eventIsInUi(event) {
|
||||
let walk = event.target;
|
||||
while (walk) {
|
||||
|
@ -266,6 +247,7 @@ export class App extends decorate(class {}) {
|
|||
}
|
||||
|
||||
onPacket(packet) {
|
||||
this.entityPacketSynchronizer.acceptPacket(packet);
|
||||
if (packet instanceof StateKeysPacket) {
|
||||
this.unpacker.registerKeys(packet.data);
|
||||
}
|
||||
|
@ -313,6 +295,8 @@ export class App extends decorate(class {}) {
|
|||
}
|
||||
|
||||
onRoomEntityAdded(entity) {
|
||||
// Packets.
|
||||
this.entityPacketSynchronizer.trackEntity(entity);
|
||||
// Traits that shouldn't be on client.
|
||||
const noClientTraits = [
|
||||
'behaved',
|
||||
|
@ -454,7 +438,7 @@ export class App extends decorate(class {}) {
|
|||
const DebugUiComponent = <DebugUi
|
||||
actionRegistry={this.actionRegistry}
|
||||
app={this}
|
||||
// Parser={this.AugmentedParser}
|
||||
Parser={this.AugmentedParser}
|
||||
socket={this.socket}
|
||||
stage={this.stage}
|
||||
/>;
|
||||
|
@ -479,7 +463,7 @@ export class App extends decorate(class {}) {
|
|||
this.inputHandle = setInterval(() => {
|
||||
if (this.actionState !== this.actionRegistry.state) {
|
||||
this.actionState = this.actionRegistry.state;
|
||||
this.packetsToSend.push(InputPacket.fromState(this.actionState));
|
||||
this.socket.send(InputPacket.fromState(this.actionState));
|
||||
}
|
||||
}, 1000 * config.inputFrequency);
|
||||
// Mouse/touch movement.
|
||||
|
@ -544,7 +528,12 @@ export class App extends decorate(class {}) {
|
|||
this.synchronizer.tick(elapsed);
|
||||
this.state = this.synchronizer.state;
|
||||
// Emit packets.
|
||||
this.sendPackets();
|
||||
this.entityPacketSynchronizer.flushPackets((packetEntity, packets) => {
|
||||
for (let i = 0; i < packets.length; i++) {
|
||||
const packet = packets[i];
|
||||
this.socket.send(packet);
|
||||
}
|
||||
});
|
||||
// Sample.
|
||||
this.tps.sample(elapsed);
|
||||
}, 1000 * config.simulationFrequency);
|
||||
|
|
|
@ -4,6 +4,8 @@ import React from 'react';
|
|||
import {compose} from '@avocado/core';
|
||||
import contempo from 'contempo';
|
||||
|
||||
import {AFFINITY_FIRE} from '../../common/combat/constants';
|
||||
|
||||
const decorate = compose(
|
||||
contempo(`
|
||||
.damage {
|
||||
|
@ -21,7 +23,7 @@ const decorate = compose(
|
|||
font-weight: bold;
|
||||
text-shadow: 0.5px 0.5px 0px black;
|
||||
}
|
||||
.particle .text.fire.is-damage {
|
||||
.particle .text.affinity-${AFFINITY_FIRE}.is-damage {
|
||||
color: #FFA500;
|
||||
text-shadow:
|
||||
0.5px 0.5px black,
|
||||
|
|
|
@ -7,6 +7,7 @@ import {Vector} from '@avocado/math';
|
|||
import contempo from 'contempo';
|
||||
// 1st party.
|
||||
import {usePropertyChange} from '../hooks/use-property-change';
|
||||
import Connection from './connection';
|
||||
import SelfEntity from './self-entity';
|
||||
import Timers from './timers';
|
||||
|
||||
|
@ -61,6 +62,7 @@ const DebugUi = ({
|
|||
}}
|
||||
>
|
||||
<Timers app={app} />
|
||||
<Connection Parser={app.AugmentedParser} socket={app.socket} />
|
||||
<SelfEntity app={app} />
|
||||
</div>
|
||||
</div>;
|
||||
|
|
2
common/combat/constants.js
Normal file
2
common/combat/constants.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
export const AFFINITY_PHYSICAL = 0;
|
||||
export const AFFINITY_FIRE = 1;
|
24
common/combat/damage.packet.js
Normal file
24
common/combat/damage.packet.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import {EntityPacket} from '@avocado/entity';
|
||||
|
||||
export class DamagePacket extends EntityPacket {
|
||||
|
||||
static get schema() {
|
||||
const superSchema = super.schema;
|
||||
superSchema.data[0].damages = [
|
||||
{
|
||||
amount: 'varuint',
|
||||
damageSpec: {
|
||||
affinity: 'uint8',
|
||||
},
|
||||
from: 'string',
|
||||
isDamage: 'bool',
|
||||
},
|
||||
];
|
||||
return superSchema;
|
||||
}
|
||||
|
||||
mergeWith(other) {
|
||||
this.data[0].damages.push(...other.data[0].damages);
|
||||
}
|
||||
|
||||
}
|
|
@ -2,6 +2,8 @@ import * as I from 'immutable';
|
|||
|
||||
import {Trait} from '@avocado/entity';
|
||||
|
||||
import {AFFINITY_PHYSICAL} from './constants';
|
||||
|
||||
export class Damaging extends Trait {
|
||||
|
||||
static defaultParams() {
|
||||
|
@ -16,7 +18,7 @@ export class Damaging extends Trait {
|
|||
const damageSpecsJSON = this.params.get('damageSpecs').toJS();
|
||||
this._damageSpecs = damageSpecsJSON.map((damageSpec) => {
|
||||
return {
|
||||
affinity: 'physical',
|
||||
affinity: AFFINITY_PHYSICAL,
|
||||
lock: 0.1,
|
||||
power: 0,
|
||||
variance: 0.2,
|
||||
|
|
|
@ -28,7 +28,7 @@ class DamageTextNode extends TextNode {
|
|||
spanClassName() {
|
||||
const {damageSpec, isDamage} = this.damage;
|
||||
let className = super.spanClassName();
|
||||
className += ' ' + damageSpec.affinity;
|
||||
className += ' affinity-' + damageSpec.affinity;
|
||||
if (isDamage) {
|
||||
className += ' is-damage';
|
||||
}
|
||||
|
@ -70,7 +70,7 @@ export class DamageEmitter {
|
|||
new Proton.Mass(1),
|
||||
new Proton.Life(2),
|
||||
new Proton.Velocity(
|
||||
new Proton.Span(80, 120),
|
||||
new Proton.Span(50, 90),
|
||||
new Proton.Vector3D(0, 5, 0),
|
||||
27.5
|
||||
),
|
||||
|
@ -81,7 +81,7 @@ export class DamageEmitter {
|
|||
const behaviors = [
|
||||
new Proton.Alpha(1, .25),
|
||||
new Proton.Scale(.8, 1.2),
|
||||
new Proton.Force(0, -1, 0),
|
||||
new Proton.Force(0, -0.5, 0),
|
||||
rot,
|
||||
];
|
||||
this.emitter.createParticle(initializers, behaviors);
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
} from '@avocado/behavior';
|
||||
import {hasGraphics, TextNodeRenderer} from '@avocado/graphics';
|
||||
|
||||
import {DamagePacket} from './damage.packet';
|
||||
import {DamageEmitter} from './emitter';
|
||||
|
||||
export class Vulnerable extends Trait {
|
||||
|
@ -33,15 +34,8 @@ export class Vulnerable extends Trait {
|
|||
}
|
||||
}
|
||||
|
||||
static defaultState() {
|
||||
return {
|
||||
damageList: I.Map(),
|
||||
};
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.damageId = 0;
|
||||
this.damageList = {};
|
||||
this._hasAddedEmitter = false;
|
||||
this._hasAddedEmitterRenderer = false;
|
||||
this._isHydrating = false;
|
||||
|
@ -58,35 +52,6 @@ export class Vulnerable extends Trait {
|
|||
this.addEmitterRenderer();
|
||||
}
|
||||
|
||||
patchStateStep(key, step) {
|
||||
if ('state' !== key) {
|
||||
return;
|
||||
}
|
||||
const stateKey = step.path.substr(1);
|
||||
const value = this.transformPatchValue(stateKey, step.value);
|
||||
const stateKeyParts = stateKey.split('/');
|
||||
switch (stateKeyParts[0]) {
|
||||
case 'damageList':
|
||||
if (!value) {
|
||||
break;
|
||||
}
|
||||
for (let i = 0; i < value.length; ++i) {
|
||||
const damage = value[i];
|
||||
if (this.entity.is('listed')) {
|
||||
damage.from = this.entity.list.findEntity(damage.from);
|
||||
}
|
||||
else {
|
||||
damage.from = undefined;
|
||||
}
|
||||
this.acceptDamage(damage);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
super.patchStateStep(key, step);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
acceptDamage(damage) {
|
||||
const context = createContext();
|
||||
context.add('entity', this.entity);
|
||||
|
@ -102,6 +67,23 @@ export class Vulnerable extends Trait {
|
|||
});
|
||||
}
|
||||
|
||||
acceptPacket(packet) {
|
||||
if (packet instanceof DamagePacket) {
|
||||
packet.forEachData(({damages}) => {
|
||||
for (let i = 0; i < damages.length; ++i) {
|
||||
const damage = damages[i];
|
||||
if (this.entity.is('listed')) {
|
||||
damage.from = this.entity.list.findEntity(damage.from);
|
||||
}
|
||||
else {
|
||||
damage.from = undefined;
|
||||
}
|
||||
this.acceptDamage(damage);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
addEmitter() {
|
||||
if (!this._isHydrating) {
|
||||
return;
|
||||
|
@ -153,6 +135,18 @@ export class Vulnerable extends Trait {
|
|||
this.addEmitterRenderer();
|
||||
},
|
||||
|
||||
tookDamage: (damage) => {
|
||||
if (AVOCADO_SERVER) {
|
||||
this.entity.emit(
|
||||
'sendPacket',
|
||||
new DamagePacket([{
|
||||
uuid: this.entity.instanceUuid,
|
||||
damages: [damage],
|
||||
}])
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
traitsChanged: () => {
|
||||
this.addEmitter();
|
||||
this.addEmitterRenderer();
|
||||
|
@ -190,9 +184,6 @@ export class Vulnerable extends Trait {
|
|||
}
|
||||
}
|
||||
amount = Math.abs(amount);
|
||||
if (!this.damageList[entity.instanceUuid]) {
|
||||
this.damageList[entity.instanceUuid] = [];
|
||||
}
|
||||
const damage = {
|
||||
id: this.damageId++,
|
||||
isDamage,
|
||||
|
@ -200,7 +191,6 @@ export class Vulnerable extends Trait {
|
|||
damageSpec,
|
||||
from: entity.instanceUuid,
|
||||
};
|
||||
this.damageList[entity.instanceUuid].push(damage);
|
||||
this.entity.emit('tookDamage', damage);
|
||||
}
|
||||
},
|
||||
|
@ -216,11 +206,6 @@ export class Vulnerable extends Trait {
|
|||
}
|
||||
}
|
||||
if (AVOCADO_SERVER) {
|
||||
if (Object.keys(this.damageList).length > 0) {
|
||||
this.state = this.state.set('damageList', I.Map(this.damageList));
|
||||
this.isDirty = true;
|
||||
this.damageList = {};
|
||||
}
|
||||
const keys = Array.from(this.locks.keys());
|
||||
for (let i = 0; i < keys.length; ++i) {
|
||||
const key = keys[i];
|
||||
|
|
|
@ -3,6 +3,8 @@ import {Vector} from '@avocado/math';
|
|||
import {World} from '@avocado/physics/matter/world';
|
||||
import {Room} from '@avocado/topdown';
|
||||
|
||||
import {AFFINITY_FIRE} from '../common/combat/constants';
|
||||
|
||||
// Behaviors!
|
||||
const move = buildInvoke(['entity', 'moveFor'], [
|
||||
buildInvoke(['global', 'randomNumber'], [0.25, 2.5, false])
|
||||
|
@ -65,7 +67,7 @@ function fireJSON(position) {
|
|||
damagingSound: 'fire',
|
||||
damageSpecs: [
|
||||
{
|
||||
affinity: 'fire',
|
||||
affinity: AFFINITY_FIRE,
|
||||
lock: 0.15,
|
||||
power: 5,
|
||||
variance: 0.25,
|
||||
|
|
|
@ -3,6 +3,7 @@ import msgpack from 'msgpack-lite';
|
|||
import {performance} from 'perf_hooks';
|
||||
// 3rd party.
|
||||
// 2nd party.
|
||||
import {EntityPacketSynchronizer} from '@avocado/entity';
|
||||
import {InputPacket} from '@avocado/input';
|
||||
import {Synchronizer} from '@avocado/state';
|
||||
import {Ticker} from '@avocado/timing';
|
||||
|
@ -15,8 +16,14 @@ export default class Game {
|
|||
|
||||
constructor() {
|
||||
const config = this.readConfig();
|
||||
// Packets.
|
||||
this.entityPacketSynchronizer = new EntityPacketSynchronizer();
|
||||
// Room.
|
||||
this.room = createRoom();
|
||||
for (const entity of this.room.allEntities()) {
|
||||
this.onEntityAddedToRoom(entity);
|
||||
}
|
||||
this.room.on('entityAdded', this.onEntityAddedToRoom, this);
|
||||
// World time. Start at 10 am for testing.
|
||||
this.worldTime = new WorldTime();
|
||||
this.worldTime.hour = 10;
|
||||
|
@ -42,6 +49,32 @@ export default class Game {
|
|||
fn();
|
||||
}
|
||||
|
||||
bundleEntityPackets() {
|
||||
const bundledEntityPackets = new Map();
|
||||
this.entityPacketSynchronizer.flushPackets((packetEntity, packets) => {
|
||||
for (let i = 0; i < this.informables.length; ++i) {
|
||||
const entity = this.informables[i];
|
||||
if (!entity.seesEntity(packetEntity)) {
|
||||
return;
|
||||
}
|
||||
if (!bundledEntityPackets.has(entity)) {
|
||||
bundledEntityPackets.set(entity, new Map());
|
||||
}
|
||||
for (let j = 0; j < packets.length; j++) {
|
||||
const packet = packets[j];
|
||||
const Packet = packet.constructor;
|
||||
if (!bundledEntityPackets.get(entity).has(Packet)) {
|
||||
bundledEntityPackets.get(entity).set(Packet, packet);
|
||||
}
|
||||
else {
|
||||
bundledEntityPackets.get(entity).get(Packet).bundleWith(packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return bundledEntityPackets;
|
||||
}
|
||||
|
||||
acceptConnection(socket) {
|
||||
// Create and track a new entity for the connection.
|
||||
const entity = createEntityForConnection(socket);
|
||||
|
@ -87,18 +120,38 @@ export default class Game {
|
|||
createPacketListener(socket) {
|
||||
const {entity} = socket;
|
||||
return (packet) => {
|
||||
this.entityPacketSynchronizer.acceptPacket(packet);
|
||||
if (packet instanceof InputPacket) {
|
||||
entity.inputState = packet.toState();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
flushEntityPackets() {
|
||||
const bundledEntityPackets = this.bundleEntityPackets();
|
||||
const entities = Array.from(bundledEntityPackets.keys());
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
const entity = entities[i];
|
||||
const packets = Array.from(bundledEntityPackets.get(entity).values());
|
||||
for (let j = 0; j < packets.length; j++) {
|
||||
const packet = packets[j];
|
||||
entity.socket.send(packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inform() {
|
||||
// Inform entities of the new state.
|
||||
for (let i = 0; i < this.informables.length; ++i) {
|
||||
const entity = this.informables[i];
|
||||
entity.inform(this.synchronizer.state);
|
||||
}
|
||||
// Flush entity packets.
|
||||
this.flushEntityPackets();
|
||||
}
|
||||
|
||||
onEntityAddedToRoom(entity) {
|
||||
this.entityPacketSynchronizer.trackEntity(entity);
|
||||
}
|
||||
|
||||
readConfig() {
|
||||
|
|
|
@ -30,6 +30,18 @@ export class Informed extends decorate(Trait) {
|
|||
}
|
||||
}
|
||||
|
||||
get areaToInform() {
|
||||
// Reduce entity list to visible.
|
||||
const room = this.entity.room;
|
||||
const camera = this.entity.camera;
|
||||
// Blow up camera rectangle to compensate for camera desync.
|
||||
const size = Rectangle.size(camera.rectangle);
|
||||
return Rectangle.expand(
|
||||
camera.rectangle,
|
||||
Vector.scale(size, 0.5),
|
||||
);
|
||||
}
|
||||
|
||||
entityOverrides(path, fn) {
|
||||
const pathOverrides = [
|
||||
['physical', 'state', 'addedToPhysics'],
|
||||
|
@ -174,18 +186,11 @@ export class Informed extends decorate(Trait) {
|
|||
reduceState(state) {
|
||||
// Set client's self entity.
|
||||
state = state.set('selfEntity', this.entity.instanceUuid);
|
||||
// Reduce entity list to visible.
|
||||
const areaToInform = this.entity.areaToInform;
|
||||
const room = this.entity.room;
|
||||
const camera = this.entity.camera;
|
||||
// Blow up camera rectangle to compensate for camera desync.
|
||||
const size = Rectangle.size(camera.rectangle);
|
||||
const visibleArea = Rectangle.expand(
|
||||
camera.rectangle,
|
||||
Vector.scale(size, 0.5),
|
||||
);
|
||||
// Write over entity list for every layer.
|
||||
for (const {index, layer} of room.layers) {
|
||||
const visibleEntities = layer.visibleEntities(visibleArea);
|
||||
const visibleEntities = layer.visibleEntities(areaToInform);
|
||||
const reduceEntityListRaw = {};
|
||||
for (let i = 0; i < visibleEntities.length; ++i) {
|
||||
const entity = visibleEntities[i];
|
||||
|
@ -236,6 +241,13 @@ export class Informed extends decorate(Trait) {
|
|||
this._socket.send(new StatePacket(packed));
|
||||
},
|
||||
|
||||
seesEntity: (entity) => {
|
||||
const areaToInform = this.entity.areaToInform;
|
||||
const room = this.entity.room;
|
||||
const visibleEntities = room.visibleEntities(areaToInform);
|
||||
return -1 !== visibleEntities.indexOf(entity);
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user