programming-game
Version:
The client for programming game, an mmorpg that you interact with entirely through code.
767 lines (766 loc) • 35.5 kB
JavaScript
;
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BaseClient = void 0;
var constants_1 = require("./constants");
var get_handlers_1 = require("./get-handlers");
var items_1 = require("./items");
var recipes_1 = require("./recipes");
var utils_1 = require("./utils");
var localItems = items_1.items;
var localConstants = constants_1.constants;
var localRecipes = recipes_1.recipes;
var isPlayerUnit = function (unit) {
return !!unit && unit.type === "player";
};
var isEmptyObject = function (obj) {
for (var _key in obj) {
if (obj.hasOwnProperty(_key)) {
return false;
}
}
return true;
};
/**
* Base class for the client.
* This class cares about the events, but NOT the implementation of the events.
* Helpful for testing the server/client integration without firing up websockets.
*/
var BaseClient = /** @class */ (function () {
function BaseClient(_a) {
var onTick = _a.onTick, onEvent = _a.onEvent, setIntent = _a.setIntent, tickInterval = _a.tickInterval;
var _this = this;
this.lastOnTick = Date.now();
this.innerState = {
gameTime: 0,
instances: {},
arenaDurations: {},
};
this.initializeInstance = function (instance, charId) {
var _a, _b;
(_a = _this.innerState.instances)[instance] || (_a[instance] = {
time: 0,
characters: {},
playersSeekingParty: [],
});
(_b = _this.innerState.instances[instance].characters)[charId] || (_b[charId] = {
units: {},
gameObjects: {},
});
return _this.innerState.instances[instance];
};
this.clearState = function () {
_this.innerState = {
gameTime: 0,
instances: {},
arenaDurations: {},
};
_this.heartBeatInterval && clearInterval(_this.heartBeatInterval);
};
this.eventsHandler = function (events) {
var _loop_1 = function (instanceId) {
var eventCharMap = events[instanceId];
var _loop_2 = function (charId) {
var events_1 = eventCharMap[charId];
_this.initializeInstance(instanceId, charId);
events_1.forEach(function (_a) {
var _b;
var eventName = _a[0], eventPayload = _a[1];
// it's important that this happens before the event is processed so that logging works correctly.
(_b = _this.onEvent) === null || _b === void 0 ? void 0 : _b.call(_this, instanceId, charId, eventName, eventPayload);
var handler = _this.socketEventHandlers[eventName];
if (handler) {
// @ts-expect-error
handler(instanceId, charId, eventPayload);
}
});
};
for (var charId in eventCharMap) {
_loop_2(charId);
}
};
for (var instanceId in events) {
_loop_1(instanceId);
}
_this.runOnTick();
};
this.runOnTick = function () {
var elapsed = Date.now() - _this.lastOnTick;
_this.lastOnTick = Date.now();
_this.innerState.gameTime += elapsed;
for (var instanceId in _this.innerState.instances) {
var instance = _this.innerState.instances[instanceId];
if (_this.innerState.arenaDurations[instanceId]) {
_this.innerState.arenaDurations[instanceId] -= elapsed;
}
for (var charId in instance.characters) {
var charState = instance.characters[charId];
for (var unitId in charState.units) {
var unit = charState.units[unitId];
for (var effectId in unit.statusEffects) {
var effect = unit.statusEffects[effectId];
effect.duration -= elapsed;
}
}
if (instanceId === "overworld" || instanceId.startsWith("instance-")) {
var char = charState.units[charId];
if (!char)
continue;
var intent = _this.onTick({
inArena: false,
player: Object.assign((0, get_handlers_1.getHandlers)(char), char),
boundary: instance.boundary,
instanceId: instanceId,
playersSeekingParty: instance.playersSeekingParty,
items: localItems,
gameObjects: charState.gameObjects,
constants: localConstants,
recipes: localRecipes,
units: charState.units,
gameTime: _this.innerState.gameTime,
});
if (intent) {
var updated = {
c: charId,
i: instanceId,
intent: intent,
unitId: charId,
};
_this.setIntent(updated);
}
}
else {
// because we run on a timer, we may not have units in our local state
// if we're not fighting in the arena, when this is the case, avoid running onTicks
if (isEmptyObject(charState.units))
continue;
var char = charState.units[charId];
var intent = _this.onTick({
inArena: true,
instanceId: instanceId,
arenaTimeRemaining: _this.innerState.arenaDurations[instanceId],
player: char ? Object.assign((0, get_handlers_1.getHandlers)(char), char) : undefined,
boundary: instance.boundary,
items: localItems,
gameObjects: charState.gameObjects,
constants: localConstants,
recipes: localRecipes,
playersSeekingParty: instance.playersSeekingParty,
units: charState.units,
gameTime: _this.innerState.gameTime,
});
if (intent) {
var updated = {
c: charId,
i: instanceId,
intent: intent,
unitId: charId,
};
_this.setIntent(updated);
}
}
}
}
};
/**
* Handles events coming from the server.
*/
this.socketEventHandlers = {
boundary: function (instance, charId, event) {
var instanceState = _this.initializeInstance(instance, charId);
instanceState.boundary = event;
},
focus: function (instance, charId, event) {
_this.updateUnit(instance, charId, event.monsterId, function (unit) {
if (!unit)
return;
if (unit.type !== "monster")
return;
unit.focus = event.focus;
});
},
hazardDamaged: function (instance, charId, event) {
_this.updateUnit(instance, charId, event.unitId, function (unit) {
if (!unit)
return;
unit.hp = event.hp;
});
},
used: function (instance, charId, event) {
_this.updateUnit(instance, charId, event.unitId, function (unit) {
var _a, _b;
(_a = unit.inventory)[_b = event.item] || (_a[_b] = 1);
unit.inventory[event.item] -= 1;
});
},
combatSkillIncreased: function (instance, charId, event) {
_this.updatePlayer(instance, charId, event.unitId, function (unit) {
unit.combatSkills[event.skill] += event.amount;
});
},
dropped: function (instance, charId, event) {
_this.updateUnit(instance, charId, event.unitId, function (unit) {
var _a, _b;
(_a = unit.inventory)[_b = event.item] || (_a[_b] = 0);
unit.inventory[event.item] -= event.amount;
});
},
loot: function (instance, charId, event) {
_this.updateUnit(instance, charId, event.unitId, function (unit) {
if (!unit)
return;
if (!("inventory" in unit))
return;
(0, utils_1.entries)(event.items).forEach(function (_a) {
var _b;
var item = _a[0], amount = _a[1];
if (!amount)
return;
(_b = unit.inventory)[item] || (_b[item] = 0);
unit.inventory[item] += amount;
});
});
},
arena: function (instance, charId, event) {
_this.innerState.arenaDurations[instance] = event.duration;
},
beganCasting: function (instance, charId, event) {
_this.updateUnit(instance, charId, event.unitId, function (unit) {
if (!unit)
return;
unit.action = "cast";
unit.actionStart = event.gameTime;
_this.innerState.gameTime = event.gameTime;
unit.actionUsing = event.spell;
unit.actionDuration = event.duration;
unit.actionTarget = event.target;
});
},
beganEquippingSpell: function (instance, charId, event) {
_this.updateUnit(instance, charId, event.unitId, function (unit) {
unit.action = "equipSpell";
unit.actionStart = event.gameTime;
unit.actionDuration = event.duration;
unit.actionTarget = event.spell;
_this.innerState.gameTime = event.gameTime;
});
},
beganHarvesting: function (instance, charId, event) {
_this.updateUnit(instance, charId, event.unitId, function (unit) {
unit.action = "harvest";
unit.actionStart = event.gameTime;
unit.actionDuration = event.duration;
unit.actionTarget = event.objectId;
_this.innerState.gameTime = event.gameTime;
});
},
harvested: function (instanceName, charId, event) {
// we don't clear the object, the server should
// send a second event telling us the object is gone.
_this.updateUnit(instanceName, charId, event.unitId, function (unit) {
_this.clearUnitActions(unit);
});
},
castSpell: function (instance, charId, event) {
_this.updateUnit(instance, charId, event.unitId, function (unit) {
if (!unit)
return;
unit.action = undefined;
unit.actionStart = undefined;
unit.actionDuration = undefined;
unit.actionTarget = undefined;
});
},
stats: function (instance, charId, event) {
_this.updateUnit(instance, charId, event.unitId, function (unit) {
if (!unit)
return;
unit.stats = event.stats;
});
},
lostStatus: function (instance, charId, event) {
_this.updateUnit(instance, charId, event.unitId, function (unit) {
if (!unit)
return;
delete unit.statusEffects[event.effect];
});
},
gainedStatus: function (instance, charId, event) {
_this.updateUnit(instance, charId, event.unitId, function (unit) {
if (!unit)
return;
unit.statusEffects[event.effect] = {
duration: event.duration,
source: event.source,
stacks: event.stacks,
effects: event.effects,
};
});
},
mp: function (instance, charId, event) {
_this.updateUnit(instance, charId, event.unitId, function (unit) {
if (!unit)
return;
unit.mp = event.mp;
});
},
usedWeaponSkill: function (instance, charId, event) {
_this.updateUnit(instance, charId, event.unitId, function (unit) {
if (!unit)
return;
unit.tp = event.tp;
});
},
ate: function (instance, charId, event) {
_this.updateUnit(instance, charId, event.unitId, function (unit) {
if (!unit)
return;
if ("calories" in unit) {
unit.calories = event.calories;
}
if ("inventory" in unit) {
unit.inventory[event.item] = event.remaining;
}
});
},
equipped: function (instance, charId, event) {
_this.updateUnit(instance, charId, event.unitId, function (unit) {
var _a, _b;
if (!unit)
return;
if (isPlayerUnit(unit)) {
// @ts-ignore
unit.equipment[event.slot] = event.item;
}
(_a = unit.inventory)[_b = event.item] || (_a[_b] = 1);
unit.inventory[event.item] -= 1;
if (unit.inventory[event.item] === 0) {
delete unit.inventory[event.item];
}
});
},
unequipped: function (instance, charId, event) {
_this.updateUnit(instance, charId, event.unitId, function (unit) {
var _a, _b;
if (!unit)
return;
if ((unit === null || unit === void 0 ? void 0 : unit.type) === "player") {
var un = unit;
// @ts-ignore
unit.equipment[event.slot] = null;
(_a = un.inventory)[_b = event.item] || (_a[_b] = 0);
un.inventory[event.item] += 1;
}
});
},
beganCrafting: function (instance, charId, event) {
_this.updateUnit(instance, charId, event.unitId, function (unit) {
unit.action = "craft";
unit.actionDuration = event.duration;
unit.actionStart = event.gameTime;
unit.actionTarget = event.item;
_this.innerState.gameTime = event.gameTime;
});
},
finishedCrafting: function (instance, charId, event) {
var charState = _this.innerState.instances[instance].characters[charId];
_this.updateUnit(instance, charId, event.unitId, function (unit) {
(0, utils_1.entries)(event.items).forEach(function (_a) {
var _b;
var item = _a[0], amount = _a[1];
(_b = unit.inventory)[item] || (_b[item] = 0);
unit.inventory[item] += amount;
});
(0, utils_1.entries)(event.spent).forEach(function (_a) {
var _b;
var item = _a[0], amount = _a[1];
(_b = unit.inventory)[item] || (_b[item] = amount);
unit.inventory[item] -= amount;
});
_this.clearUnitActions(unit);
});
},
traded: function (instance, charId, event) {
_this.updateUnit(instance, charId, event.actingUnitId, function (unit) {
(0, utils_1.entries)(event.gave).forEach(function (_a) {
var _b;
var item = _a[0], amount = _a[1];
(_b = unit.inventory)[item] || (_b[item] = 0);
unit.inventory[item] -= amount;
});
(0, utils_1.entries)(event.got).forEach(function (_a) {
var _b;
var item = _a[0], amount = _a[1];
(_b = unit.inventory)[item] || (_b[item] = 0);
unit.inventory[item] += amount;
delete unit.trades.wants[item];
});
});
_this.updateUnit(instance, charId, event.targetUnitId, function (unit) {
(0, utils_1.entries)(event.gave).forEach(function (_a) {
var _b;
var item = _a[0], amount = _a[1];
(_b = unit.inventory)[item] || (_b[item] = 0);
unit.inventory[item] += amount;
delete unit.trades.wants[item];
});
(0, utils_1.entries)(event.got).forEach(function (_a) {
var _b;
var item = _a[0], amount = _a[1];
(_b = unit.inventory)[item] || (_b[item] = 0);
unit.inventory[item] -= amount;
});
});
},
moved: function (instance, charId, event) {
_this.updateUnit(instance, charId, event.id, function (unit) {
if (!unit)
return;
var x = event.x, y = event.y;
unit.position = { x: x, y: y };
});
},
connectionEvent: function (instanceName, charId, event) {
var player = event.player;
var instance = _this.initializeInstance(instanceName, charId);
// This will only come through if our local version doesn't match the server version
if (event.items) {
localItems = __assign(__assign({}, localItems), event.items);
}
if (event.constants) {
localConstants = __assign(__assign({}, localConstants), event.constants);
}
localRecipes = __assign(__assign({}, localRecipes), event.recipes);
instance.characters[player.id] = {
units: event.units,
gameObjects: event.gameObjects,
};
},
unitAppeared: function (instanceName, charId, event) {
var instance = _this.initializeInstance(instanceName, charId);
var char = instance.characters[charId];
char.units[event.unit.id] = event.unit;
},
unitDisappeared: function (instanceName, charId, event) {
var instance = _this.initializeInstance(instanceName, charId);
delete instance.characters[charId].units[event.unitId];
},
seekParty: function (instanceName, charId, event) {
var instance = _this.initializeInstance(instanceName, charId);
var mapped = false;
instance.playersSeekingParty = instance.playersSeekingParty.map(function (val) {
if (val.id === event.playersSeeking.id) {
mapped = true;
return event.playersSeeking;
}
return val;
});
if (!mapped) {
instance.playersSeekingParty.push(event.playersSeeking);
}
},
acceptedPartyInvite: function (instanceName, charId, event) {
var instance = _this.initializeInstance(instanceName, charId);
_this.updatePlayer(instanceName, charId, event.inviteeId, function (player) {
player.partyInvites = [];
});
instance.playersSeekingParty = instance.playersSeekingParty.filter(function (val) {
return val.id !== event.inviteeId;
});
},
updatedPartyInvites: function (instanceName, charId, event) {
_this.updatePlayer(instanceName, charId, event.unitId, function (unit) {
if (!unit)
return;
if (!isPlayerUnit(unit))
return;
unit.partyInvites = event.invites;
});
},
updatedParty: function (instanceName, charId, event) {
Object.keys(event.party).forEach(function (memberId) {
_this.updateUnit(instanceName, charId, memberId, function (unit) {
if (!unit)
return;
unit.party = event.party;
});
});
},
updatedPartyV2: function (instanceName, charId, event) {
Object.keys(event.party).forEach(function (memberId) {
_this.updateUnit(instanceName, charId, memberId, function (unit) {
if (!unit)
return;
unit.party = event.party;
});
});
_this.updateUnit(instanceName, charId, event.playerId, function (unit) {
if (!unit)
return;
unit.party = event.party;
});
},
updatedRole: function (instanceName, charId, event) {
_this.updateUnit(instanceName, charId, event.unitId, function (unit) {
unit.role = event.role;
});
},
setIntent: function (instanceName, charId, event) {
_this.updateUnit(instanceName, charId, event.unitId, function (innerUnit) {
if (innerUnit) {
innerUnit.intent = event.intent;
// clear out any pending actions
if (event.intent === null) {
_this.clearUnitActions(innerUnit);
}
}
});
},
attacked: function (instanceName, charId, event) {
_this.updateUnit(instanceName, charId, event.attacked, function (attackedUnit) {
if (!attackedUnit)
return;
attackedUnit.hp = event.hp;
});
_this.updateUnit(instanceName, charId, event.attacker, function (attackerUnit) {
if (!attackerUnit)
return;
attackerUnit.tp = event.attackerTp;
});
},
despawn: function (instanceName, charId, event) {
var instance = _this.initializeInstance(instanceName, charId);
delete instance.characters[charId].units[event.unitId];
},
hp: function (instanceName, charId, event) {
_this.updateUnit(instanceName, charId, event.unitId, function (unit) {
if (!unit)
return;
unit.hp = event.hp;
});
},
tp: function (instanceName, charId, event) {
_this.updateUnit(instanceName, charId, event.unitId, function (unit) {
if (!unit)
return;
unit.tp = event.tp;
});
},
calories: function (instanceName, charId, event) {
_this.updateUnit(instanceName, charId, event.unitId, function (unit) {
if (unit && "calories" in unit) {
unit.calories = event.calories;
}
});
},
died: function (instanceName, charId, event) {
_this.updateUnit(instanceName, charId, event.unitId, function (unit) {
if (!unit)
return;
unit.hp = 0;
});
},
invited: function (instanceName, charId, event) {
var instance = _this.initializeInstance(instanceName, charId);
var player = instance.characters[charId].units[charId];
if (player && isPlayerUnit(player)) {
if (!player.partyInvites.find(function (invite) { return invite.id === event.inviter.id; })) {
player.partyInvites.push({
id: event.inviter.id,
name: event.inviter.name,
role: event.inviter.role,
});
}
}
},
updatedTrade: function (instanceName, charId, event) {
_this.updateUnit(instanceName, charId, event.unitId, function (unit) {
if (!unit)
return;
unit.trades = event.trades;
});
},
acceptedQuest: function (instanceName, charId, event) {
_this.updatePlayer(instanceName, charId, event.unitId, function (unit) {
unit.quests[event.quest.id] = event.quest;
});
if (charId === event.unitId) {
_this.updateNpc(instanceName, charId, event.quest.start_npc, function (npc) {
delete npc.availableQuests[event.quest.id];
});
}
},
abandonedQuest: function (instanceName, charId, event) {
_this.updatePlayer(instanceName, charId, event.unitId, function (unit) {
delete unit.quests[event.questId];
});
},
completedQuest: function (instanceName, charId, event) {
_this.updatePlayer(instanceName, charId, event.unitId, function (unit) {
delete unit.quests[event.questId];
});
},
questAvailable: function (instanceName, charId, event) {
_this.updateNpc(instanceName, charId, event.npcId, function (npc) {
npc.availableQuests[event.quest.id] = event.quest;
});
},
questUpdate: function (instanceName, charId, event) {
_this.updatePlayer(instanceName, charId, event.unitId, function (unit) {
if (!unit)
return;
unit.quests[event.quest.id] = event.quest;
});
},
inventory: function (instanceName, charId, event) {
_this.updateUnit(instanceName, charId, event.unitId, function (unit) {
if (!unit)
return;
(0, utils_1.entries)(event.inventory).forEach(function (_a) {
var item = _a[0], amount = _a[1];
if ((amount || 0) <= 0) {
delete unit.inventory[item];
}
else {
unit.inventory[item] = amount;
}
});
});
},
storageEmptied: function (instanceName, charId) {
_this.updatePlayer(instanceName, charId, charId, function (player) {
player.storage = {};
});
},
storageCharged: function (instanceName, charId, event) {
_this.updatePlayer(instanceName, charId, charId, function (player) {
if (event.coinsLeft) {
player.storage.copperCoin = event.coinsLeft;
}
else {
delete player.storage.copperCoin;
}
});
},
withdrew: function (instanceName, charId, event) {
_this.updatePlayer(instanceName, charId, charId, function (player) {
(0, utils_1.entries)(event.items).forEach(function (_a) {
var itemId = _a[0], _b = _a[1], amount = _b === void 0 ? 0 : _b;
player.storage[itemId] = (player.storage[itemId] || 0) - amount;
player.inventory[itemId] = (player.inventory[itemId] || 0) + amount;
if (itemId in player.storage && (player.storage[itemId] || 0) <= 0) {
delete player.storage[itemId];
}
});
});
},
deposited: function (instanceName, charId, event) {
_this.updatePlayer(instanceName, charId, charId, function (player) {
(0, utils_1.entries)(event.items).forEach(function (_a) {
var itemId = _a[0], _b = _a[1], amount = _b === void 0 ? 0 : _b;
player.storage[itemId] = (player.storage[itemId] || 0) + amount;
player.inventory[itemId] = (player.inventory[itemId] || 0) - amount;
if (itemId in player.inventory &&
(player.inventory[itemId] || 0) <= 0) {
delete player.inventory[itemId];
}
});
});
},
objectAppeared: function (instanceName, charId, event) {
var instance = _this.initializeInstance(instanceName, charId);
var char = instance.characters[charId];
char.gameObjects[event.object.id] = event.object;
},
objectDisappeared: function (instanceName, charId, event) {
var instance = _this.initializeInstance(instanceName, charId);
var char = instance.characters[charId];
delete char.gameObjects[event.objectId];
},
objectUpdated: function (instanceName, charId, event) {
var instance = _this.initializeInstance(instanceName, charId);
var char = instance.characters[charId];
var gameObject = char.gameObjects[event.objectId];
if (gameObject) {
// Update the game object with the new properties
Object.assign(gameObject, event.properties);
}
},
unequippedSpell: function (instanceName, charId, event) {
_this.updateUnit(instanceName, charId, event.unitId, function (unit) {
var _a;
var spell = unit.spellbook[0];
if (!spell)
return;
unit.spellbook.splice(0, 1);
if (unit.type === "player") {
(_a = unit.inventory)[spell] || (_a[spell] = 0);
unit.inventory[spell] += 1;
}
});
},
equippedSpell: function (instanceName, charId, event) {
_this.updateUnit(instanceName, charId, event.unitId, function (unit) {
var _a;
var spell = event.spell;
(_a = unit.inventory)[spell] || (_a[spell] = 1);
unit.inventory[spell] -= 1;
if (unit.inventory[spell] <= 0) {
delete unit.inventory[spell];
}
unit.spellbook.push(spell);
_this.clearUnitActions(unit);
});
},
};
this.updateUnit = function (instance, charId, unitId, cb) {
var char = _this.innerState.instances[instance].characters[charId];
if (!char)
return;
var unit = char.units[unitId];
if (!unit)
return;
cb(unit);
};
/**
* Called when the unit completes a cast, or craft, or other action that takes time.
*/
this.clearUnitActions = function (unit) {
unit.action = undefined;
unit.actionDuration = undefined;
unit.actionStart = undefined;
unit.actionUsing = undefined;
unit.actionTarget = undefined;
};
this.updateNpc = function (instance, charId, npcId, cb) {
_this.updateUnit(instance, charId, npcId, function (possiblyNpc) {
if (possiblyNpc.type === "npc") {
cb(possiblyNpc);
}
});
};
this.updatePlayer = function (instance, charId, unitId, cb) {
_this.updateUnit(instance, charId, unitId, function (unit) {
if (!isPlayerUnit(unit))
return;
cb(unit);
});
};
this.onTick = onTick;
this.onEvent = onEvent;
this.setIntent = setIntent;
this.heartBeatInterval = setInterval(this.runOnTick, tickInterval);
}
return BaseClient;
}());
exports.BaseClient = BaseClient;