Skip to content

Instantly share code, notes, and snippets.

@timebomb0
Last active January 1, 2020 21:28
Show Gist options
  • Save timebomb0/ac8d5288b481cba66025b63ceb2f04f7 to your computer and use it in GitHub Desktop.
Save timebomb0/ac8d5288b481cba66025b63ceb2f04f7 to your computer and use it in GitHub Desktop.
Hordes.io Item hydration reverse-engineered, for use with items API
const itemJson = {
auction: '2020-01-02T04:27:51.501Z',
auctionprice: 62,
bound: false,
id: 7157356,
name: 'kickbuttowski',
rolls: [62, 76, 17, 11, 98, 80, 93, 75, 11, 67, 37, 49, 36, 47, 28, 71, 18, 83, 72, 37, 27],
slot: null,
stacks: null,
tier: 0,
type: 'boot',
upgrade: 0,
};
const myItem = new Item();
myItem.hydrate(itemJson);
// myItem.stats now contains a Map of the real stats of the item, and myItem's other properties are now filled out`
// Map of stat keys to stat names:
// Derived from debugging entire character screen stats ingame
const statMapping = {
0: 'Strength',
1: 'Stamina',
2: 'Dexterity',
3: 'Intelligence',
4: 'Wisdom',
5: 'Luck',
6: 'HP',
7: 'MP',
8: 'HP Reg./5s', // (MUST Divide by 10 to get HP Reg./5s)
9: 'MP Reg./5s', // (MUST Divide by 10 to get MP Reg./5s)
10: 'Min Dmg.',
11: 'Max Dmg.',
12: 'Defense',
13: 'Block', // (MUST Divide by 10 to get block %, e.g. 15 = 1.5%)
14: 'Critical', // (MUST Divide by 10 to get critical %, e.g. 149 = 14.9% crit)
15: 'Move Spd.'
16: 'Haste', // (MUST Divide by 10 to get haste %, e.g. 16 = 1.6%)
17: 'Attack Spd.',
18: 'Item Find',
19: 'Bag Slots',
20: 'Fame',
//21: ?, (This was set to 0 for me, but deosn't map to anything in character screen)
//22: ? (This was set to 1 for me, but doesn't map to anything in character screen)
}
// A hydrated item's `stats` property contains `keys` mapping to quality+base values of stats. Those `keys` map to these stats.
const Lr = {
6: {
min: 0.2,
max: 0.8,
round: true,
},
7: {
min: 0.2,
max: 0.5,
round: true,
},
8: {
min: 0.1,
max: 0.5,
},
9: {
min: 0.1,
max: 0.2,
},
10: {
min: 0.03,
max: 0.13,
round: true,
},
11: {
min: 0.1,
max: 0.2,
round: true,
},
12: {
min: 0.1,
max: 1,
round: true,
},
13: {
min: 0.1,
max: 0.3,
},
14: {
min: 0.1,
max: 0.4,
},
16: {
min: 0.1,
max: 0.5,
},
2: {
min: 0.08,
max: 0.45,
round: true,
},
0: {
min: 0.08,
max: 0.45,
round: true,
},
3: {
min: 0.08,
max: 0.45,
round: true,
},
4: {
min: 0.08,
max: 0.45,
round: true,
},
1: {
min: 0.08,
max: 0.45,
round: true,
},
5: {
min: 0.08,
max: 0.45,
round: true,
},
18: {
min: 0.01,
max: 0.15,
round: true,
},
};
const Ur = {
6: 3,
7: 3,
8: 3,
9: 2,
10: 1,
11: 1,
12: 5,
13: 5,
14: 5,
15: 0.5,
16: 5,
17: 0,
2: 2,
0: 2,
3: 2,
4: 2,
1: 2,
5: 2,
19: 1,
18: 3,
};
const Br = {
hammer: {
baselvl: 0,
slot: 101,
tiers: 17,
drop: 0.4,
weight: 1,
class: 3,
stats: {
10: {
base: 1,
min: 0.6,
max: 1,
},
11: {
base: 3,
min: 0.8,
max: 1.7,
},
17: {
base: 15,
min: 0.05,
max: 0.1,
},
},
},
bow: {
baselvl: 0,
slot: 101,
tiers: 17,
drop: 0.4,
weight: 1,
class: 2,
stats: {
10: {
base: 1,
min: 0.6,
max: 1,
},
11: {
base: 3,
min: 0.8,
max: 1.7,
},
17: {
base: 10,
min: 0.05,
max: 0.1,
},
},
},
staff: {
baselvl: 0,
slot: 101,
tiers: 17,
drop: 0.4,
weight: 1,
class: 1,
stats: {
10: {
base: 1,
min: 0.6,
max: 1,
},
11: {
base: 3,
min: 0.8,
max: 1.7,
},
17: {
base: 10,
min: 0.05,
max: 0.1,
},
},
},
sword: {
baselvl: 0,
slot: 101,
tiers: 17,
drop: 0.4,
weight: 1,
class: 0,
stats: {
10: {
base: 1,
min: 0.6,
max: 1,
},
11: {
base: 3,
min: 0.8,
max: 1.7,
},
17: {
base: 20,
min: 0.05,
max: 0.1,
},
},
},
armlet: {
baselvl: 1,
slot: 102,
tiers: 13,
drop: 1,
weight: 0.3,
stats: {
6: {
base: 10,
min: 0.5,
max: 0.9,
},
12: {
base: 7,
min: 0.5,
max: 0.9,
},
},
},
armor: {
baselvl: 2,
slot: 103,
tiers: 11,
drop: 1,
weight: 1,
stats: {
12: {
base: 10,
min: 1.5,
max: 3,
},
6: {
base: 20,
min: 1,
max: 2,
},
},
},
bag: {
baselvl: 5,
slot: 104,
tiers: 5,
drop: 1,
weight: 0.1,
stats: {
19: {
base: 1,
min: 0.1,
max: 0.3,
},
},
},
boot: {
baselvl: 2,
slot: 105,
tiers: 13,
drop: 1,
weight: 0.4,
stats: {
6: {
base: 10,
min: 0.6,
max: 1,
},
12: {
base: 8,
min: 0.6,
max: 1.2,
},
15: {
base: 5,
min: 0.03,
max: 0.15,
},
},
},
glove: {
baselvl: 2,
slot: 106,
tiers: 13,
drop: 1,
weight: 0.4,
stats: {
6: {
base: 10,
min: 0.6,
max: 1,
},
12: {
base: 8,
min: 0.7,
max: 1.2,
},
14: {
base: 1,
min: 0.1,
max: 1.5,
},
},
},
ring: {
baselvl: 5,
slot: 107,
tiers: 12,
drop: 0.8,
weight: 0.2,
stats: {
6: {
base: 10,
min: 0.5,
max: 0.9,
},
7: {
base: 5,
min: 0.6,
max: 1,
},
},
},
amulet: {
baselvl: 7,
slot: 108,
tiers: 12,
drop: 0.8,
weight: 0.3,
stats: {
7: {
base: 10,
min: 1,
max: 1.8,
},
9: {
base: 1,
min: 0.2,
max: 0.3,
},
},
},
quiver: {
baselvl: 2,
slot: 109,
tiers: 10,
drop: 0.7,
weight: 0.5,
class: 2,
stats: {
14: {
base: 2,
min: 0.1,
max: 0.5,
},
},
},
shield: {
baselvl: 2,
slot: 109,
tiers: 10,
drop: 0.7,
weight: 0.5,
class: 0,
stats: {
12: {
base: 20,
min: 0.9,
max: 1.5,
},
13: {
base: 4,
min: 1,
max: 3,
},
},
},
totem: {
baselvl: 2,
slot: 109,
tiers: 10,
drop: 0.7,
weight: 0.5,
class: 3,
stats: {
12: {
base: 10,
min: 0.3,
max: 0.8,
},
9: {
base: 1,
min: 0.1,
max: 0.2,
},
},
},
orb: {
baselvl: 2,
slot: 109,
tiers: 10,
drop: 0.7,
weight: 0.5,
class: 1,
stats: {
3: {
base: 10,
min: 0.3,
max: 0.8,
},
9: {
base: 1,
min: 0.1,
max: 0.3,
},
},
},
rune: {
baselvl: 1,
tiers: 11,
drop: 0.8,
quality: 70,
},
misc: {
drop: 7,
weight: 0.1,
},
book: {
drop: 1.5,
weight: 0.5,
},
mount: {
noupgrade: true,
undroppable: true,
drop: 0,
stackable: false,
},
box: {
noupgrade: true,
undroppable: true,
drop: 0,
stackable: false,
},
gold: {
drop: 20,
},
};
// Setting up `df`, used to define item logic
const hf = (t, e) => {
const s = {
level: t.level,
type: t.type,
tier: t.tier,
stats: t.stats ? new Map() : void 0,
class: t.class,
quality: t.quality,
};
t.stats &&
Object.keys(t.stats)
.sort((t, e) => t - e)
.forEach(e => {
const i = t.stats[e];
s.stats.set(parseInt(e), {
min: i.base + t.level * i.min,
max: i.base + (t.level + 10) * i.max,
});
}),
(e[t.type + t.tier] = s);
};
const cf = (t, e) => Br[t].baselvl + Math.floor((e / Br[t].tiers) * 100);
const ia = (t, e) => {
const s = [((t >> 16) & 255) / 255, ((t >> 8) & 255) / 255, (255 & t) / 255];
return void 0 !== e && s.push(e), s;
};
// Setting Sp to empty object because it's only related to the monster model
const Sp = {};
const Su = {
black: ia(0, 0),
white: ia(16777215, 0),
deadgrey: ia(3355443, 0),
teal: ia(3384995, 0),
magicblue: ia(3362252, 0),
paleskin: ia(14267315, 0),
darkskin: ia(8411481, 0),
slimegreen: ia(2542694, 0),
mushgreen: ia(2077529, 0),
linen: ia(13408614, 0),
woodbrown: ia(4859433, 0),
woodbrown2: ia(10250315, 0),
tealsteel: ia(3358797, 0.5),
darksteel: ia(3355443, 0.5),
greysteel: ia(5066061, 0.5),
blacksteel: ia(988178, 0.5),
leather: ia(4337198, 0),
emerald: ia(22866, -0.1),
gold: ia(16751616, 1),
paper: ia(15129011, 0),
richpurple: ia(7484623, 0),
fireorange: ia(14715686, -0.7),
archergreen: ia(10471258, 0),
warbrown: ia(15172191, 0),
bronze: ia(9258571, 0.5),
silver: ia(11527136, 0.8),
shamanblue: ia(3687924, 0),
mageblue: ia(6607340, 0),
bone: ia(14535066, 0.2),
bone2: ia(11836267, 0.2),
ice: ia(14610164, -0.2),
vanguard: ia(2848511, 0),
bloodlust: ia(14289947, 0),
pink: ia(16711935, 0),
warden: ia(14755623, 0),
evilred: ia(13239043, 0),
ruby: ia(15733030, 1),
moss: ia(2925637, 0.2),
rock: ia(5006687, 0.4),
death: ia(3750201, 0.8),
};
// Setting Vm/Mp to empty objects instead of their real values, because they appear to be related to the equipment model, which I don't care about for this
const Vm = {};
const Mp = {};
const Ep = [
[30, 1e4, 50, Vm, {}],
[30, 2e4, 50, Mp, {}],
[
30,
5e4,
80,
Mp,
{
colEyes: ia(11332842, 0),
colPrim: ia(4772165, -0.2),
colSec: ia(1351204, -0.2),
},
],
[
30,
5e4,
80,
Mp,
{
colEyes: ia(16751284, 0),
colPrim: ia(15737892, 0.5),
colSec: ia(15737892, 0.5),
},
],
[
30,
5e4,
95,
Mp,
{
colEyes: ia(14292553, 0),
colPrim: ia(4075311, 0.1),
colSec: ia(1970198, 0.1),
legsize: 0.5,
post: t => {
oi(t.chest.scale, 0.8);
},
},
],
[
30,
5e4,
90,
Mp,
{
colEyes: ia(2029122, 0),
colPrim: ia(2565674, 0.3),
colSec: ia(2236197, 0.3),
legsize: 1.4,
post: t => {
ri(t.chest.scale, 0.6, 0.6, 1), t.chest2.rotation.set(0.5, 0, 0);
},
},
],
[
30,
5e4,
80,
Vm,
{
colEyes: ia(12825266, 0),
colPrim: ia(11313312, 0.1),
colSec: ia(8221555, 0.1),
meshHead: 'rocks/rock_02.ho',
post: t => ri(t.eyes.position, 0, 0, 0.75),
},
],
[
30,
5e4,
90,
Vm,
{
colEyes: ia(15880973, 0),
colPrim: Su.fireorange,
colSec: Su.fireorange,
meshHead: 'rocks/rock_02.ho',
post: t => {
ri(t.eyes.position, 0, 0, 0.75);
const e = ic(Pu.burning, void 0, t);
e && t.effects.push(e);
},
},
],
[
30,
5e4,
95,
Vm,
{
colEyes: ia(12451327, 0),
colPrim: ia(3950066, -0.4),
colSec: ia(3525854, -0.4),
meshHead: 'monsterparts/wyrmhead.ho',
meshEyes: 'monsterparts/wyrmEyes.ho',
post: t => {
const e = ic(Pu.etherwyrm, void 0, t.blobs[0]);
e && t.effects.push(e);
},
},
],
[
30,
5e4,
95,
Vm,
{
colEyes: ia(2747996, 0),
colPrim: ia(1907231, 0.4),
colSec: ia(2293580, -0.8),
meshHead: 'monsterparts/wyrmhead.ho',
meshEyes: 'monsterparts/wyrmEyes.ho',
post: t => {
const e = ic(Pu.shadowwyrm, void 0, t.blobs[0]);
e && t.effects.push(e);
},
},
],
[
30,
5e4,
80,
Mp,
{
colEyes: ia(16758891, 0),
colPrim: ia(15448667, 0.1),
colSec: ia(11370036, 0.1),
legsize: 0.6,
meshHead: 'monsterparts/wyrmhead.ho',
meshBack: 'monsterparts/wyrmhead.ho',
meshEyes: 'monsterparts/wyrmEyes.ho',
chestOffset: 0.5,
post: Sp,
},
],
[
30,
5e4,
95,
Mp,
{
colEyes: ia(16777215, 0),
colPrim: Su.darksteel,
colSec: Su.gold,
legsize: 0.6,
meshHead: 'monsterparts/wyrmhead.ho',
meshBack: 'monsterparts/wyrmhead.ho',
meshEyes: 'monsterparts/wyrmEyes.ho',
chestOffset: 0.5,
post: Sp,
},
],
[
30,
5e4,
95,
Mp,
{
colEyes: ia(16750598, 0),
colPrim: ia(13177876, -0.6),
colSec: ia(16721931, -0.7),
legsize: 1.1,
meshHead: 'misc/misc_bones_skull.ho',
meshBack: 'monsterparts/wyrmhead.ho',
meshEyes: 'monsterparts/wyrmEyes.ho',
post: t => {
oi(t.chest2.scale, 0.7), ri(t.eyes.position, 0, -0.1, 0.55), oi(t.eyes.scale, 0.6);
const e = ic(Pu.burning, void 0, t);
e && t.effects.push(e);
},
},
],
[
30,
5e4,
85,
Vm,
{
colEyes: Su.fireorange,
colPrim: Su.darksteel,
colSec: Su.fireorange,
},
],
[
30,
5e4,
95,
Vm,
{
colEyes: Su.ice,
colPrim: ia(3980543, -0.6),
colSec: Su.ice,
post: t => {
const e = ic(Pu.arcticaura, void 0, t.blobs[1]);
e && (t.effects.push(e), (e.position[1] = -0.5));
},
},
],
].map(t => ({
level: t[0],
goldValue: t[1],
quality: t[2],
visual: t[3],
visualData: t[4],
}));
const lf = [
{
id: 0,
storeValue: 300,
quality: 90,
level: 1,
custom: ['Contains one random mount of rare or epic quality'],
},
];
const df = {};
// initting df
(t => {
for (const e in Br)
if (Br[e].tiers) {
const s = Br[e];
for (let i = 0; i < s.tiers; ++i)
hf(
{
type: e,
tier: i,
stats: s.stats,
level: cf(e, i),
class: s.class,
quality: s.quality,
},
t,
);
}
})(df);
(t => {
const e = [150, 100, 300, 200, 600, 300];
for (let s = 0; s < 6; ++s) {
const i = Math.floor(s / 2),
n = s % 2 == 0;
t['misc' + s] = {
type: 'misc',
tier: s,
level: 1 + 20 * i,
goldvalue: 5 ** i,
quality: 15,
custom: [e[s] + (n ? ' HP recovered' : ' MP recovered')],
useSkill: 100,
use: (t, e, s, i) => {},
};
}
})(df);
// I left out this logic because it appears to be for skill books only, and
// requires us to include all the skills in this code
// (t => {
// Em.forEach(e => {
// if (!e.engineOnly)
// for (let s = 0; s < e.skilllevels; ++s) {
// const i = 5 * e.id + s;
// t[`book${i}`] = {
// type: 'book',
// tier: i,
// level: e.minlevel + 8 * s,
// skillid: e.id,
// skilllevel: s,
// class: e.class,
// noReward: e.noReward || false,
// goldValue: e.goldValue || (e.minlevel + 5 * s < 5 ? 4 : 0),
// quality: Math.round(Math.min(99, 30 + (s / 5) * 70)),
// art: au(i, e.class),
// useSkill: 101,
// use: (t, s, i, n) => {
// of(t, e.id);
// },
// };
// }
// });
// })(df);
(t => {
Ep.forEach((e, s) => {
t['mount' + s] = {
...e,
type: 'mount',
tier: s,
requiredSkills: [39],
useSkill: 102,
use: (t, e, i, n, a) => ({
id: 75,
mode: 4,
stacks: 1,
duration: 600,
caster: t.id,
target: t.id,
buffdata: [s],
sendBuffdata: true,
level: e,
}),
};
});
})(df);
(t => {
lf.forEach(e => {
t['box' + e.id] = {
...e,
type: 'box',
tier: e.id,
unsellable: true,
useSkill: 103,
use: (t, e, s, i) => {},
};
});
})(df);
const Ir = (t, e) => t[Math.floor(e * t.length)];
const rw = Object.keys(Lr);
class Item {
constructor(t) {
(this.dbid = t), (this.stats = new Map()), (this.dirty = true);
}
hydrate(t) {
if (
((this.dirty = false),
(this.bound = t.bound),
(this.type = t.type),
(this.tier = t.tier),
(this.logic = df[this.type + this.tier]),
(this.auction = t.auction ? new Date(t.auction) : undefined),
(this.auctionprice = t.auctionprice),
(this.owner = t.name),
(this.stash = t.stash ? new Date(t.stash) : undefined),
undefined === this.logic)
)
throw 'Unknown item ' + t.type + t.tier;
if (((this.upgrade = t.upgrade), this.stats.clear(), t.rolls)) {
if ((this.setRolls(t.rolls), (this.quality = this.nextRoll()), this.logic.stats)) {
this.logic.stats.forEach((t, e) => {
this.stats.set(e, {
type: 'base',
qual: this.quality,
value: Math.floor(
t.min +
(t.max - t.min) * (this.quality / 100) ** 2 +
Ur[e] * this.upgrade,
),
});
});
const t = Math.round((this.quality / 100) ** 1.5 * 3.6);
for (let e = 0; e < t; ++e) {
let t = this.nextRoll(),
e = -1;
for (; -1 === e || this.stats.has(e); )
(e = parseInt(Ir(rw, t / 101))), (t = (t + 5) % 100);
const s = (this.nextRoll() + this.quality) / 2;
this.stats.set(e, {
type: 'bonus',
qual: s,
value: Math.ceil(
Math.max(
(Lr[e].min + (Lr[e].max - Lr[e].min) * (s / 100) ** 2) *
this.logic.level *
Br[this.type].weight,
Ur[e],
) +
Ur[e] * this.upgrade,
),
});
}
}
(this.quality = this.logic.quality || this.quality), (this.stacks = undefined);
} else (this.stacks = t.stacks), (this.quality = this.logic.quality || 0);
}
setRolls(t) {
(this.rolls = t), (this.currentRoll = 0);
}
nextRoll() {
if (this.currentRoll == this.rolls.length) throw 'roll maximum reached';
return this.rolls[this.currentRoll++];
}
use(t) {
this.logic.use && this.logic.use(t);
}
goldValue(t) {
if (!this.canBeSold()) throw 'Item cant have value because it cant be sold';
return nw(this.logic, this.quality, this.stacks, t);
}
storeValue() {
return this.logic.storeValue || 0;
}
canEquip(t) {
return Gx(t.level, t.skills.skillIds, t.class, this.type, this.tier);
}
canEquipClass(t) {
return Kx(t.class, this.type, this.tier);
}
equipReasons(t) {
const e = [];
return this.logic.level && e.push(['Lv. ' + this.logic.level, true]), e;
}
canBeDropped() {
return !Br[this.type].undroppable && !this.bound;
}
canBeTraded() {
return !this.bound;
}
canBeSold() {
return !this.logic.unsellable;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment