programming-game
Version:
The client for programming game, an mmorpg that you interact with entirely through code.
798 lines (797 loc) • 35.3 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 items_1 = require("./items");
var types_1 = require("./types");
var isPlayerUnit = function (unit) {
return !!unit && unit.type === "player";
};
var entries = function (obj) {
return Object.entries(obj);
};
var keys = function (obj) {
return Object.keys(obj);
};
var getHandlers = function (player) {
return {
setRole: function (role) {
return { type: types_1.IntentType.setRole, role: role };
},
attack: function (target) {
return { type: types_1.IntentType.attack, target: target.id };
},
move: function (position) {
return { type: types_1.IntentType.move, position: position };
},
respawn: function () {
return { type: types_1.IntentType.respawn };
},
summonMana: function () {
return { type: types_1.IntentType.summonMana };
},
eat: function (item) {
return {
type: types_1.IntentType.eat,
item: item,
save: (player.inventory[item] || 0) - 1,
};
},
cast: function (spell, target) {
return { type: types_1.IntentType.cast, spell: spell, target: target === null || target === void 0 ? void 0 : target.id };
},
sell: function (opt) {
return {
type: types_1.IntentType.sellItems,
items: entries(opt.items).reduce(function (acc, _a) {
var item = _a[0], amount = _a[1];
var until = (player.inventory[item] || 0) - (amount || 0);
acc[item] = until;
return acc;
}, {}),
to: opt.to.id,
};
},
buy: function (opts) {
return {
type: types_1.IntentType.buyItems,
items: entries(opts.items).reduce(function (acc, _a) {
var item = _a[0], amount = _a[1];
if (!amount)
return acc;
var until = amount + (player.inventory[item] || 0);
if (amount) {
acc[item] = until;
}
return acc;
}, {}),
from: opts.from.id,
};
},
use: function (item, target) {
return {
type: types_1.IntentType.use,
item: item,
until: (player.inventory[item] || 0) - 1,
target: target === null || target === void 0 ? void 0 : target.id,
};
},
seekParty: function () {
return { type: types_1.IntentType.seekParty };
},
inviteToParty: function (playerId) {
return { type: types_1.IntentType.inviteToParty, playerId: playerId };
},
leaveParty: function () {
return { type: types_1.IntentType.leaveParty };
},
acceptPartyInvite: function (playerId) {
return { type: types_1.IntentType.acceptPartyInvite, playerId: playerId };
},
equip: function (item, slot) {
return { type: types_1.IntentType.equip, item: item, slot: slot };
},
unequip: function (slot) {
return { type: types_1.IntentType.unequip, slot: slot };
},
craft: function (item, from) {
return { type: types_1.IntentType.craft, item: item, from: from };
},
useWeaponSkill: function (specifics) {
if (specifics.skill === "misdirectingShot") {
return {
type: types_1.IntentType.weaponSkill,
skill: specifics.skill,
target: specifics.target.id,
options: {
to: specifics.target.id,
},
};
}
return {
type: types_1.IntentType.weaponSkill,
skill: specifics.skill,
target: specifics.target.id,
};
},
drop: function (_a) {
var item = _a.item, amount = _a.amount;
return {
type: types_1.IntentType.drop,
item: item,
until: (player.inventory[item] || 0) - amount,
};
},
setSpellStones: function (item, stones, name) {
return {
type: types_1.IntentType.setSpellStones,
item: item,
stones: stones,
name: name,
};
},
setTrade: function (trades) {
return {
type: types_1.IntentType.setTrade,
trades: trades,
};
},
acceptQuest: function (npc, questId) {
return { type: types_1.IntentType.acceptQuest, npcId: npc.id, questId: questId };
},
abandonQuest: function (questId) {
return { type: types_1.IntentType.abandonQuest, questId: questId };
},
turnInQuest: function (npc, questId) {
return { type: types_1.IntentType.turnInQuest, npcId: npc.id, questId: questId };
},
withdraw: function (npc, items) {
var until = {};
entries(items).forEach(function (_a) {
var item = _a[0], _b = _a[1], amount = _b === void 0 ? 0 : _b;
var charAmount = player.storage[item] || 0;
until[item] = charAmount - amount;
});
return {
type: types_1.IntentType.withdraw,
npcId: npc.id,
until: until,
};
},
deposit: function (npc, items) {
var until = {};
entries(items).forEach(function (_a) {
var item = _a[0], _b = _a[1], amount = _b === void 0 ? 0 : _b;
var charAmount = player.inventory[item] || 0;
until[item] = charAmount - amount;
});
return {
type: types_1.IntentType.deposit,
npcId: npc.id,
until: until,
};
},
};
};
/**
* 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;
var _this = this;
this.time = 0;
this.lastOnTick = Date.now();
this.innerState = {
instances: {},
arenaDurations: {},
};
this.initializeInstance = function (instance, charId) {
var _a, _b;
(_a = _this.innerState.instances)[instance] || (_a[instance] = {
time: 0,
characters: {},
playersSeekingParty: new Map(),
});
(_b = _this.innerState.instances[instance].characters)[charId] || (_b[charId] = {
units: {},
uniqueItems: {},
gameObjects: {},
});
return _this.innerState.instances[instance];
};
this.clearState = function () {
_this.innerState = {
instances: {},
arenaDurations: {},
};
_this.heartBeatInterval && clearInterval(_this.heartBeatInterval);
};
this.eventsHandler = function (events) {
Object.keys(events).forEach(function (instanceId) {
var eventCharMap = events[instanceId];
Object.entries(eventCharMap).forEach(function (_a) {
var charId = _a[0], events = _a[1];
_this.initializeInstance(instanceId, charId);
events.forEach(function (_a) {
var _b;
var eventName = _a[0], eventPayload = _a[1];
(_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);
}
});
});
});
_this.runOnTick();
};
this.runOnTick = function () {
var elapsed = Date.now() - _this.lastOnTick;
_this.lastOnTick = Date.now();
Object.entries(_this.innerState.instances).forEach(function (_a) {
var instanceId = _a[0], instance = _a[1];
if (_this.innerState.arenaDurations[instanceId]) {
_this.innerState.arenaDurations[instanceId] -= elapsed;
}
Object.keys(instance.characters).map(function (charId) {
var charState = instance.characters[charId];
Object.values(charState.units).forEach(function (unit) {
Object.values(unit.statusEffects).forEach(function (effect) {
effect.duration -= elapsed;
});
});
_this.time++;
if (instanceId === "overworld" ||
instanceId.startsWith("instance-")) {
var char = charState.units[charId];
if (!char)
return;
var intent = _this.onTick({
inArena: false,
player: __assign(__assign({}, char), getHandlers(char)),
boundary: instance.boundary,
instanceId: instanceId,
playersSeekingParty: Array.from(instance.playersSeekingParty.values()),
items: __assign(__assign({}, charState.uniqueItems), items_1.items),
gameObjects: __assign({}, charState.gameObjects),
time: _this.time,
units: charState.units,
});
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 onTics
if (!Object.keys(charState.units).length)
return;
var char = charState.units[charId];
var intent = _this.onTick({
inArena: true,
instanceId: instanceId,
arenaTimeRemaining: _this.innerState.arenaDurations[instanceId],
player: char
? __assign(__assign({}, char), getHandlers(char)) : undefined,
boundary: instance.boundary,
items: __assign(__assign({}, charState.uniqueItems), items_1.items),
gameObjects: __assign({}, charState.gameObjects),
playersSeekingParty: Array.from(instance.playersSeekingParty.values()),
time: _this.time,
units: charState.units,
});
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;
});
},
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;
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 = Date.now();
unit.actionDuration = event.duration;
unit.actionTarget = event.target;
});
},
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;
});
},
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 = Date.now();
unit.actionTarget = event.item;
});
},
finishedCrafting: function (instance, charId, event) {
var charState = _this.innerState.instances[instance].characters[charId];
_this.updateUnit(instance, charId, event.unitId, function (unit) {
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;
});
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;
});
if (event.uniqueItems) {
entries(event.uniqueItems).forEach(function (_a) {
var item = _a[0], def = _a[1];
if (!item || !def)
return;
charState.uniqueItems[item] = def;
});
}
unit.action = undefined;
unit.actionDuration = undefined;
unit.actionStart = undefined;
unit.actionUsing = undefined;
unit.actionTarget = undefined;
});
},
traded: function (instance, charId, event) {
_this.updateUnit(instance, charId, event.actingUnitId, function (unit) {
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;
});
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) {
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];
});
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);
instance.characters[player.id] = {
units: event.units,
uniqueItems: event.uniqueItems,
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;
keys(event.uniqueItems).forEach(function (uniqueItemId) {
char.uniqueItems[uniqueItemId] = event.uniqueItems[uniqueItemId];
});
},
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);
instance.playersSeekingParty.set(event.playersSeeking.id, event.playersSeeking);
},
acceptedPartyInvite: function (instanceName, charId, event) {
var instance = _this.initializeInstance(instanceName, charId);
instance.playersSeekingParty.delete(event.inviteeId);
},
updatedParty: function (instanceName, charId, event) {
_this.updateUnit(instanceName, charId, charId, 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;
}
});
},
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)) {
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;
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) {
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) {
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);
}
},
};
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);
};
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, 300);
}
return BaseClient;
}());
exports.BaseClient = BaseClient;