584 lines
16 KiB
JavaScript
584 lines
16 KiB
JavaScript
const preact = require('preact');
|
|
const { useEffect } = require('preact/hooks');
|
|
|
|
const get = require('lodash/get');
|
|
const anime = require('animejs').default;
|
|
|
|
const shapes = require('./components/shapes');
|
|
|
|
const stringSort = (k, desc) => {
|
|
if (desc) {
|
|
return (a, b) => {
|
|
if (!get(a, k)) return 1;
|
|
if (!get(b, k)) return -1;
|
|
return get(b, k).localeCompare(get(a, k));
|
|
};
|
|
}
|
|
return (a, b) => {
|
|
if (!get(a, k)) return 1;
|
|
if (!get(b, k)) return -1;
|
|
return get(a, k).localeCompare(get(b, k));
|
|
};
|
|
};
|
|
|
|
const numSort = (k, desc) => {
|
|
if (desc) {
|
|
return (a, b) => {
|
|
if (!get(a, k)) return 1;
|
|
if (!get(b, k)) return -1;
|
|
return get(b, k) - get(a, k);
|
|
};
|
|
}
|
|
return (a, b) => {
|
|
if (!get(a, k)) return 1;
|
|
if (!get(b, k)) return -1;
|
|
return get(a, k) - get(b, k);
|
|
};
|
|
};
|
|
|
|
const genAvatar = name => {
|
|
let hash = 0;
|
|
if (name.length === 0) return hash;
|
|
// Probs don't need to hash using the whole string
|
|
for (let i = 0; i < name.length; i += 1) {
|
|
const chr = name.charCodeAt(i);
|
|
hash = ((hash << 5) - hash) + chr;
|
|
hash = hash % 10000;
|
|
}
|
|
return `${hash}`;
|
|
};
|
|
|
|
function requestAvatar(name) {
|
|
const id = genAvatar(name);
|
|
const req = new Request(`/assets/molecules/${id}.svg`);
|
|
return fetch(req)
|
|
.then(res => res.text())
|
|
.then(svg => svg);
|
|
}
|
|
|
|
const animations = {};
|
|
function animateCryp(id) {
|
|
if (animations[id]) return false;
|
|
animations[id] = true;
|
|
const target = document.getElementById(id);
|
|
return anime({
|
|
targets: target,
|
|
translateX: () => anime.random(-20, 20),
|
|
translateY: () => anime.random(0, -40),
|
|
rotate: () => anime.random(-15, 15),
|
|
duration: () => anime.random(2000, 5000),
|
|
delay: () => anime.random(0, 2000),
|
|
direction: 'alternate',
|
|
easing: 'linear',
|
|
loop: true,
|
|
complete: () => animations[id] = false,
|
|
});
|
|
}
|
|
|
|
function clearAnimation(id) {
|
|
animations[id] = false;
|
|
}
|
|
|
|
function crypAvatar(name, id) {
|
|
useEffect(() => {
|
|
animateCryp(id);
|
|
return () => clearAnimation(id);
|
|
});
|
|
|
|
return (
|
|
<img
|
|
id={id}
|
|
src={`/molecules/${genAvatar(name)}.svg`}
|
|
height="500"
|
|
onError={event => event.target.setAttribute('src', '/molecules/726.svg')}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function instanceCryp(name, id) {
|
|
useEffect(() => animateCryp(id));
|
|
|
|
return (
|
|
<div
|
|
className="avatar"
|
|
id={id}
|
|
style={{'background-image': `url(/molecules/${genAvatar(name)}.svg)`}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const NULL_UUID = '00000000-0000-0000-0000-000000000000';
|
|
|
|
const STATS = {
|
|
redDamage: {
|
|
stat: 'red_damage',
|
|
colour: 'red',
|
|
svg: shapes.circle,
|
|
},
|
|
greenDamage: {
|
|
stat: 'green_damage',
|
|
colour: 'green',
|
|
svg: shapes.circle,
|
|
},
|
|
blueDamage: {
|
|
stat: 'blue_damage',
|
|
colour: 'blue',
|
|
svg: shapes.circle,
|
|
},
|
|
speed: {
|
|
stat: 'speed',
|
|
colour: 'white',
|
|
svg: shapes.triangle,
|
|
},
|
|
redLife: {
|
|
stat: 'red_life',
|
|
colour: 'red',
|
|
svg: shapes.square,
|
|
},
|
|
greenLife: {
|
|
stat: 'green_life',
|
|
colour: 'green',
|
|
svg: shapes.square,
|
|
},
|
|
blueLife: {
|
|
stat: 'blue_life',
|
|
colour: 'blue',
|
|
svg: shapes.square,
|
|
},
|
|
};
|
|
|
|
const SPECS = {
|
|
Life: {
|
|
colour: 'white',
|
|
caption: 'Life',
|
|
svg: shapes.square
|
|
},
|
|
GreenLifeI: {
|
|
colour: 'green',
|
|
caption: 'Life',
|
|
thresholds: [5, 10, 20],
|
|
svg: shapes.square,
|
|
},
|
|
RedLifeI: {
|
|
colour: 'red',
|
|
caption: 'Life',
|
|
thresholds: [2, 5, 10],
|
|
svg: shapes.square,
|
|
},
|
|
BlueLifeI: {
|
|
colour: 'blue',
|
|
caption: 'Life',
|
|
thresholds: [2, 5, 10],
|
|
svg: shapes.square,
|
|
},
|
|
GRLI: {
|
|
colour: 'yellow',
|
|
caption: 'Life',
|
|
thresholds: [2, 5, 10],
|
|
svg: shapes.square
|
|
},
|
|
GBLI: {
|
|
colour: 'cyan',
|
|
caption: 'Life',
|
|
thresholds: [2, 5, 10],
|
|
svg: shapes.square
|
|
},
|
|
RBLI: {
|
|
colour: 'purple',
|
|
caption: 'Life',
|
|
thresholds: [2, 5, 10],
|
|
svg: shapes.square
|
|
},
|
|
|
|
Damage: {
|
|
colour: 'white',
|
|
caption: 'Damage',
|
|
thresholds: [],
|
|
svg: shapes.circle
|
|
},
|
|
RedDamageI: {
|
|
colour: 'red',
|
|
caption: 'DamageI',
|
|
thresholds: [5, 10, 20],
|
|
svg: shapes.circle
|
|
},
|
|
BlueDamageI: {
|
|
colour: 'blue',
|
|
caption: 'DamageI',
|
|
thresholds: [5, 10, 20],
|
|
svg: shapes.circle
|
|
},
|
|
GreenDamageI: {
|
|
colour: 'green',
|
|
caption: 'DamageI',
|
|
thresholds: [5, 10, 20],
|
|
svg: shapes.circle,
|
|
},
|
|
GRDI: {
|
|
colour: 'yellow',
|
|
caption: 'DamageI',
|
|
thresholds: [2, 5, 10],
|
|
svg: shapes.circle
|
|
},
|
|
GBDI: {
|
|
colour: 'cyan',
|
|
caption: 'DamageI',
|
|
thresholds: [2, 5, 10],
|
|
svg: shapes.circle,
|
|
},
|
|
RBDI: {
|
|
colour: 'purple',
|
|
caption: 'DamageI',
|
|
thresholds: [2, 5, 10],
|
|
svg: shapes.circle,
|
|
},
|
|
|
|
Speed: {
|
|
colour: 'white',
|
|
caption: 'Speed',
|
|
svg: shapes.triangle,
|
|
},
|
|
RedSpeedI: {
|
|
colour: 'red',
|
|
caption: 'Speed',
|
|
thresholds: [5, 10, 20],
|
|
svg: shapes.triangle,
|
|
},
|
|
BlueSpeedI: {
|
|
colour: 'blue',
|
|
caption: 'Speed',
|
|
thresholds: [2, 5, 10],
|
|
svg: shapes.triangle,
|
|
},
|
|
GreenSpeedI: {
|
|
colour: 'green',
|
|
caption: 'Speed',
|
|
thresholds: [2, 5, 10],
|
|
svg: shapes.triangle,
|
|
},
|
|
GRSpeedI: {
|
|
colour: 'yellow',
|
|
caption: 'Speed',
|
|
thresholds: [2, 5, 10],
|
|
svg: shapes.triangle,
|
|
},
|
|
GBSpeedI: {
|
|
colour: 'cyan',
|
|
caption: 'Speed',
|
|
thresholds: [2, 5, 10],
|
|
svg: shapes.triangle,
|
|
},
|
|
RBSpeedI: {
|
|
colour: 'purple',
|
|
caption: 'Speed',
|
|
thresholds: [2, 5, 10],
|
|
svg: shapes.triangle,
|
|
},
|
|
|
|
};
|
|
|
|
const COLOUR_ICONS = {
|
|
red: { colour: 'red', caption: 'red', svg: shapes.square },
|
|
blue: { colour: 'blue', caption: 'blue', svg: shapes.square },
|
|
green: { colour: 'green', caption: 'green', svg: shapes.square },
|
|
};
|
|
|
|
function resoCrypHealth(resolution, currentGame) {
|
|
if (!resolution) return false;
|
|
|
|
const modifyHealth = cryp => {
|
|
if (cryp.id !== resolution.target.id) return false; // not target
|
|
const [type, event] = resolution.event;
|
|
if (type === 'Damage') {
|
|
const { amount, mitigation, colour } = event;
|
|
cryp.green_life.value -= amount;
|
|
if (colour === 'Red') {
|
|
cryp.red_life.value -= mitigation;
|
|
}
|
|
if (colour === 'Blue') {
|
|
cryp.blue_life.value -= mitigation;
|
|
}
|
|
}
|
|
|
|
if (type === 'Healing') {
|
|
const { amount } = event;
|
|
cryp.green_life.value += amount;
|
|
}
|
|
|
|
if (type === 'Recharge') {
|
|
const { red, blue } = event;
|
|
cryp.red_life.value += red;
|
|
cryp.blue_life.value += blue;
|
|
}
|
|
return true;
|
|
};
|
|
currentGame.players.forEach(player => player.cryps.forEach(modifyHealth));
|
|
return true;
|
|
|
|
}
|
|
|
|
function eventClasses(resolution, cryp) {
|
|
if (!resolution) return '';
|
|
const startSkill = resolution.stage === 'START_SKILL';
|
|
const endSkill = resolution.stage === 'END_SKILL';
|
|
const postSkill = resolution.stage === 'POST_SKILL';
|
|
const source = cryp.id === resolution.source.id;
|
|
const target = cryp.id === resolution.target.id;
|
|
// not involved at all. blur them
|
|
if (!(source || target)) return 'unfocus';
|
|
|
|
// not the target. just ignore for now
|
|
// if (cryp.id !== resolution.target.id) return '';
|
|
|
|
const [type, event] = resolution.event;
|
|
|
|
if (type === 'Ko') {
|
|
if (target) return 'ko';
|
|
}
|
|
|
|
if (type === 'Disable') {
|
|
const { skill, disable } = event;
|
|
}
|
|
|
|
if (type === 'Immunity') {
|
|
const { skill, immunity } = event;
|
|
}
|
|
|
|
if (type === 'TargetKo') {
|
|
const { skill } = event;
|
|
}
|
|
|
|
if (type === 'Damage') {
|
|
const { skill, amount, mitigation, colour } = event;
|
|
// Highlight the flow of damage from source -> target
|
|
if (source && startSkill) return 'active-skill';
|
|
if (target && endSkill) return 'active-skill';
|
|
|
|
// Deal damage to cryp and return effect
|
|
if (target && postSkill) {
|
|
if (colour === 'Red') {
|
|
return 'red-damage';
|
|
}
|
|
if (colour === 'Blue') {
|
|
return 'blue-damage';
|
|
}
|
|
if (colour === 'Green') return 'green-damage';
|
|
}
|
|
|
|
}
|
|
|
|
if (type === 'Healing') {
|
|
const { skill, amount, overhealing } = event;
|
|
if (source && startSkill) return 'active-skill';
|
|
if (target && endSkill) return 'active-skill';
|
|
if (target && postSkill) {
|
|
return 'green-damage';
|
|
}
|
|
}
|
|
|
|
if (type === 'Inversion') {
|
|
const { skill } = event;
|
|
}
|
|
|
|
if (type === 'Reflection') {
|
|
const { skill } = event;
|
|
}
|
|
|
|
if (type === 'Effect') {
|
|
const { skill, effect, duration } = event;
|
|
if (source && startSkill) return 'active-skill';
|
|
if (target && endSkill) return 'active-skill';
|
|
}
|
|
|
|
if (type === 'Skill') {
|
|
const { skill } = event;
|
|
// Highlight the flow of damage from source -> target
|
|
if (source && startSkill) return 'active-skill';
|
|
if (target && endSkill) return 'active-skill';
|
|
}
|
|
|
|
if (type === 'Removal') {
|
|
const { effect } = event;
|
|
}
|
|
|
|
if (type === 'Recharge') {
|
|
const { skill, red, blue } = event;
|
|
if (source && startSkill) return 'active-skill';
|
|
if (target && endSkill) return 'active-skill';
|
|
if (target && postSkill) {
|
|
if (red > 0 && blue > 0) return 'purple-damage';
|
|
if (red > 0) return 'red-damage';
|
|
if (blue > 0) return 'blue-damage';
|
|
}
|
|
}
|
|
|
|
if (type === 'Evasion') {
|
|
const { skill, evasion_rating } = event;
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
function getCombatSequence(event) {
|
|
if (!event) return false;
|
|
// Skip combat animations depending on event type, expandable in future
|
|
const dotTicks = ['DecayTick', 'CorruptionTick', 'TriageTick', 'SiphonTick', 'StrangleTick'];
|
|
|
|
if (['Skill', 'AoeSkill'].includes(event[0])) return ['START_SKILL', 'END_SKILL'];
|
|
if (['Immunity'].includes(event[0])) return ['START_SKILL', 'POST_SKILL'];
|
|
if (['Removal'].includes(event[0])) return ['POST_SKILL'];
|
|
|
|
if (['Healing'].includes(event[0])
|
|
&& (event[1].skill === 'Slay'
|
|
|| event[1].skill === 'SiphonTick'
|
|
|| event[1].skill === 'Purify'
|
|
|| event[1].skill === 'Sleep')) return ['POST_SKILL'];
|
|
|
|
if (['Recharge'].includes(event[0])
|
|
&& (event[1].skill === 'Reflect')) return ['POST_SKILL'];
|
|
|
|
|
|
if (event[0] === 'Effect'
|
|
&& (['Ruin', 'Taunt', 'Strangling', 'Parry'].includes(event[1].skill)
|
|
|| (event[1].skill === 'Decay' && event[1].effect === 'Wither'))) return ['POST_SKILL'];
|
|
|
|
if (['Damage'].includes(event[0])
|
|
&& ((event[1].skill === 'Chaos' && event[1].colour === 'Red')
|
|
|| event[1].skill === 'Silence'
|
|
|| event[1].skill === 'Snare')) return ['POST_SKILL'];
|
|
|
|
if (['Ko'].includes(event[0])
|
|
|| (event[1].skill === 'Throw' && event[1].effect === 'Vulnerable')) return ['POST_SKILL'];
|
|
|
|
if (dotTicks.includes(event[1].skill)) return ['END_SKILL', 'POST_SKILL'];
|
|
return ['START_SKILL', 'END_SKILL', 'POST_SKILL'];
|
|
}
|
|
|
|
function getCombatText(cryp, resolution) {
|
|
if (!resolution) return ['', ''];
|
|
|
|
|
|
const [type, event] = resolution.event;
|
|
const source = cryp.id === resolution.source.id;
|
|
const target = cryp.id === resolution.target.id;
|
|
const startSkill = resolution.stage === 'START_SKILL';
|
|
const endSkill = resolution.stage === 'END_SKILL';
|
|
const postSkill = resolution.stage === 'POST_SKILL';
|
|
|
|
if (type === 'Ko') {
|
|
if (postSkill && target) return ['KO!', ''];
|
|
}
|
|
|
|
if (type === 'Disable') {
|
|
const { skill, disable } = event;
|
|
if (startSkill && source) return [`${skill}`, ''];
|
|
if (endSkill && target) return [`${skill}`, ''];
|
|
if (postSkill && target) return [`${disable}`, ''];
|
|
}
|
|
|
|
if (type === 'Immunity') {
|
|
const { skill, immunity } = event;
|
|
if (startSkill && source) return [`${skill}`, ''];
|
|
if (endSkill && target) return [`${skill}`, ''];
|
|
if (postSkill && target) return ['IMMUNE', ''];
|
|
}
|
|
|
|
if (type === 'TargetKo') {
|
|
const { skill } = event;
|
|
if (startSkill && source) return [`${skill}`, ''];
|
|
if (endSkill && target) return [`${skill}`, ''];
|
|
}
|
|
|
|
if (type === 'Damage') {
|
|
const { skill, amount, mitigation, colour } = event;
|
|
const mitigationText = mitigation
|
|
? `(${mitigation})`
|
|
: '';
|
|
if (startSkill && source) return [`${skill}`, `${skill.toLowerCase()}-cast`];
|
|
if (endSkill && target) return [`${skill}`, `${skill.toLowerCase()}-hit`];
|
|
if (postSkill && target) return [`${amount} ${mitigationText}`, ''];
|
|
}
|
|
|
|
if (type === 'Healing') {
|
|
const { skill, amount, overhealing } = event;
|
|
if (startSkill && source) return [`${skill}`, ''];
|
|
if (endSkill && target) return [`${skill}`, ''];
|
|
if (postSkill && target) return [`${amount} (${overhealing} OH)`, ''];
|
|
}
|
|
|
|
if (type === 'Inversion') {
|
|
const { skill } = event;
|
|
if (startSkill && source) return [`${skill}`, ''];
|
|
if (endSkill && target) return [`${skill}`, ''];
|
|
if (postSkill && target) return ['INVERT', ''];
|
|
}
|
|
|
|
if (type === 'Reflection') {
|
|
const { skill } = event;
|
|
if (startSkill && source) return [`${skill}`, ''];
|
|
if (endSkill && target) return [`${skill}`, ''];
|
|
if (postSkill && target) return ['REFLECT', ''];
|
|
}
|
|
|
|
if (type === 'Effect') {
|
|
const { skill, effect, duration } = event;
|
|
if (startSkill && source) return [`${skill}`, ''];
|
|
if (endSkill && target) return [`${skill}`, ''];
|
|
if (postSkill && target) return [`+ ${effect} ${duration}T`, ''];
|
|
}
|
|
|
|
if (type === 'Recharge') {
|
|
const { skill, red, blue } = event;
|
|
if (startSkill && source) return [`${skill}`, ''];
|
|
if (endSkill && target) return [`${skill}`, ''];
|
|
if (postSkill && target) return [`+${red}R ${blue}B`, ''];
|
|
}
|
|
|
|
if (type === 'Skill' || type === 'AoeSkill') {
|
|
const { skill } = event;
|
|
if (startSkill && source) return [`${skill}`, ''];
|
|
if (endSkill && target) return [`${skill}`, ''];
|
|
}
|
|
|
|
if (type === 'Removal') {
|
|
const { effect } = event;
|
|
if (postSkill && target) return [`-${effect}`, ''];
|
|
}
|
|
|
|
|
|
return '';
|
|
}
|
|
|
|
function convertItem(v) {
|
|
if (['Red', 'Green', 'Blue'].includes(v)) {
|
|
return (
|
|
shapes.vboxColour(v.toLowerCase())
|
|
);
|
|
}
|
|
return v || <span> </span>;
|
|
// uncomment for double borders in vbox;
|
|
// if (v) {
|
|
// return <div>{v}</div>;
|
|
// }
|
|
// return;
|
|
}
|
|
|
|
module.exports = {
|
|
animateCryp,
|
|
stringSort,
|
|
convertItem,
|
|
numSort,
|
|
genAvatar,
|
|
crypAvatar,
|
|
instanceCryp,
|
|
requestAvatar,
|
|
eventClasses,
|
|
getCombatSequence,
|
|
getCombatText,
|
|
resoCrypHealth,
|
|
NULL_UUID,
|
|
STATS,
|
|
SPECS,
|
|
COLOUR_ICONS,
|
|
};
|