Merge branch 'release/1.5.3'

This commit is contained in:
ntr 2019-10-09 10:23:19 +11:00
commit 7972665515
39 changed files with 410 additions and 268 deletions

View File

@ -1,8 +1,8 @@
# mnml
mnml is a turn-based 1v1 strategy game in an abstract setting.
players craft a team of 3 constructs combining a deep pool of skills, effects and specialisations to mindgame & outplay their opponents in a rapid series of duels.
featuring complex interactions arising from simple rules, simultaneous turns to increase the pace, and a unique speed mechanic mnml is a tactical game in a genre of its own.
it is completely free to play and requires no installation.
Craft a team of 3 constructs combining a deep pool of skills, effects and specialisations to mindgame & outplay opponents in a rapid series of duels.
Featuring complex interactions arising from simple rules, simultaneous turns to increase the pace, and a unique speed mechanic mnml is a tactical game in a genre of its own.
It is completely free to play and requires no installation.
minimal studios is ntr & mashy: 2 mates with a friendship forged in the fires of warcraft 3 dota.
we have both bailed out of the big city life and have dedicated ourselves to growing farm fresh, organic, ethical gaming produce in the rolling hills of brisbane and leaves of melbourne.

View File

@ -1 +1 @@
1.5.2
1.5.3

View File

@ -6,6 +6,8 @@
* mobile styles
* mobile info page
* fix info page for tablet layout
* Add TOS and accept to register page
* can't reset password without knowing password =\
@ -45,8 +47,11 @@
reconnect based on time delta
consolidate game and instance
* return of the combat log (last few events with condensed descriptions)
- click in to scroll
* elo + leaderboards
* reflect event stages (for animations)
* mnml tv
## LATER

View File

@ -1,6 +1,6 @@
{
"name": "mnml-client",
"version": "1.5.2",
"version": "1.5.3",
"description": "",
"main": "index.js",
"scripts": {

View File

@ -8,7 +8,7 @@ require('./assets/styles/instance.less');
require('./assets/styles/vbox.less');
require('./assets/styles/game.less');
require('./assets/styles/player.less');
require('./assets/styles/styles.mobile.css');
require('./assets/styles/instance.mobile.css');
require('./assets/styles/styles.mobile.less');
require('./assets/styles/instance.mobile.less');
require('./src/animations.test.jsx');

View File

@ -44,7 +44,10 @@ aside {
button {
width: 100%;
font-size: 150%;
margin-bottom: 0.5em;
&:last-child {
margin-bottom: 0;
}
}
button.ready:enabled {

View File

@ -3,13 +3,13 @@
.game {
overflow: hidden;
display: grid;
grid-template-rows: 1fr 0.5fr 1.5fr;
grid-template-columns: 1fr;
grid-template-areas:
"opponent"
"target "
"player ";
// display: grid;
// grid-template-rows: 1fr 0.75fr 1.5fr;
// grid-template-columns: 1fr;
// grid-template-areas:
// "opponent"
// "target "
// "player ";
}
.game .team {
@ -24,11 +24,22 @@
.player {
grid-area: player;
z-index: 5;
position: absolute;
bottom: 0;
height: 50%;
width: 90%;
}
.opponent {
grid-area: opponent;
position: absolute;
top: 0;
height: 35%;
width: 90%;
margin-top: 0.5em;
.game-construct {
align-items: flex-start;
grid-template-columns: 1fr 2fr;
@ -66,14 +77,16 @@
justify-items: center;
grid-template-columns: 1fr 3fr;
grid-template-columns: 1fr;
grid-template-rows: min-content 1fr;
.left {
width: 100%;
display: grid;
grid-template-rows: 1fr 1fr;
grid-template-columns: 1fr 2fr;
grid-template-areas:
"skills "
"effects";
"skills effects";
}
.right {
@ -92,7 +105,6 @@
transition-timing-function: ease;
.effects {
align-self: flex-end;
text-align: right;
}
@ -108,13 +120,14 @@
}
.skills {
grid-area: skills;
width: 100%;
button {
width: 100%;
height: 2em;
height: 25%;
margin-right: 1em;
}
button.active {
background: #2c2c2c;
}
}
@ -176,12 +189,19 @@
#targeting {
grid-area: target;
height: 100%;
width: 100%;
// height: 100%;
// width: 100%;
stroke-width: 2px;
stroke: whitesmoke;
}
#targeting, .resolving-skill {
position: absolute;
top: 35%;
height: 15%;
width: 90%;
}
.resolving-skill {
grid-area: target;
align-self: center;
@ -203,9 +223,8 @@
.combat-anim {
width: 100%;
height: 100%;
position: absolute;
display: flex;
flex-flow: column;
}
.combat-anim svg {
@ -310,7 +329,7 @@
.skill-animation {
opacity: 0;
stroke-width: 5px;
height: 5em;
// height: 5em;
}
@media (max-width: 1000px) {
@ -329,6 +348,11 @@
grid-template-rows: min-content 1fr;
.left {
width: 100%;
grid-template-areas:
"skills"
"effects";
grid-template-columns: 1fr;
grid-template-rows: min-content min-content;
}
@ -337,6 +361,9 @@
padding: 0 0.5em ;
margin: 0;
}
button.active {
background: #2c2c2c;
}
}
.stats div {
@ -363,17 +390,38 @@
font-size: 100%;
}
.skills button, .stats, .name {
.stats, .name {
font-size: 75%;
}
.skills button {
font-size: 50%;
}
}
.player {
width: calc(100% - 1em);
bottom: 3em;
height: calc(50% - 3em);
}
.opponent {
width: calc(100% - 1em);
.game-construct {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: min-content 1fr;
}
}
#targeting, .resolving-skill {
width: calc(100% - 1em);
}
}
}
.player {
width: calc(100% - 1em);
bottom: 3em;
height: calc(50% - 3em);
}
}

View File

@ -1,5 +1,20 @@
@import 'colours.less';
// tablet / ipad
@media (max-width: 1100px) {
.instance {
grid-template-columns: 1fr;
grid-template-rows: min-content 1fr;
grid-template-areas:
"vbox"
"constructs";
.info {
display: none;
}
}
}
@media (max-width: 800px) {
.instance {
font-size: 8pt;
@ -32,6 +47,10 @@
&:not(.visible) {
display: none;
}
.vbox-vbox {
margin-bottom: 1em;
}
}
.vbox-arrow {
@ -88,7 +107,7 @@
"avatar "
"stats ";
border-top: 0;
border: 0;
padding: 0;
transition-property: all;

View File

@ -12,6 +12,9 @@ html body {
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
overflow-x: hidden;
overflow-y: hidden;
}
#mnml {
@ -23,7 +26,7 @@ html body {
/* stops inspector going skitz*/
overflow-x: hidden;
// overflow-y: hidden;
overflow-y: hidden;
}
@media (min-width: 1921px) {
@ -97,14 +100,8 @@ dl {
padding: 0.5em 1em;
&.animations-test {
grid-template-columns: 1fr 9fr 1fr;
grid-template-areas:
"nav hdr ctrl"
"nav main ctrl"
"nav main ctrl";
nav {
display: initial;
aside button {
font-size: 50%;
}
}
}
@ -124,6 +121,7 @@ button, input {
font-size: 100%;
flex: 1;
border-radius: 0.5em;
line-height: 2em;
/*the transitions */
transition-property: border-color, color, background;

View File

@ -1,4 +1,8 @@
@media (max-width: 800px) {
body {
overflow-y: initial;
}
#mnml {
font-size: 12pt;
padding: 0;
@ -13,10 +17,6 @@
min-height: initial;
}
#mnml button {
font-size: 10pt;
}
table td {
height: 2.5em;
}
@ -34,6 +34,10 @@
position: fixed;
bottom: 0;
width: 100%;
button {
font-size: 90%;
}
}
#nav-btn, #instance-nav {
@ -51,10 +55,6 @@
display: none;
}
/* header {
display: none;
}
*/
main {
overflow-x: hidden;
overflow-y: initial;
@ -93,4 +93,34 @@
"inventory"
"games";
}
.menu {
.options {
display: grid;
grid-template-columns: 1fr;
button:not(:last-child) {
border: 2px solid #222;
}
button.logo {
border: none;
margin-right: 0;
margin-top: 0.5em;
background-position: center;
}
}
}
section {
.list {
grid-template-columns: 1fr;
}
}
.account {
div {
padding: 0;
}
}
}

View File

@ -33,6 +33,7 @@
margin: 0;
background-color: @gray-box;
height: 3em;
line-height: 1em;
border-width: 0;
:active, :hover, :focus {

View File

@ -1,13 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>mnml - abstract strategy</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name=apple-mobile-web-app-capable content=yes>
<meta name=apple-mobile-web-app-status-bar-style content=black>
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="mobile-web-app-capable" content="yes">
<meta name="application-name" content="mnml">
<meta name="description" content="mnml pvp tbs">
<meta name="author" content="ntr@smokestack.io">
<title>mnml - abstract strategy</title>
<link rel="manifest" href="manifest.webmanifest">
<link rel="stylesheet" href="./node_modules/izitoast/dist/css/iziToast.min.css"></script>
<link href="https://fonts.googleapis.com/css?family=Jura" rel="stylesheet">

View File

@ -8,7 +8,7 @@ require('./assets/styles/instance.less');
require('./assets/styles/vbox.less');
require('./assets/styles/game.less');
require('./assets/styles/player.less');
require('./assets/styles/styles.mobile.css');
require('./assets/styles/styles.mobile.less');
require('./assets/styles/instance.mobile.less');
// kick it off

View File

@ -1,6 +1,6 @@
{
"name": "mnml-client",
"version": "1.5.2",
"version": "1.5.3",
"description": "",
"main": "index.js",
"scripts": {
@ -24,6 +24,7 @@
"keymaster": "^1.6.2",
"linkstate": "^1.1.1",
"lodash": "^4.17.15",
"logrocket": "^1.0.3",
"node-sass": "^4.12.0",
"parcel": "^1.12.3",
"preact": "^8.4.2",
@ -40,7 +41,7 @@
"babel-plugin-module-resolver": "^3.2.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1",
"eslint": "^5.6.0",
"eslint": "^5.16.0",
"eslint-config-airbnb-base": "^13.1.0",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-react": "^7.11.1",

View File

@ -4,6 +4,7 @@ export const setActiveConstruct = value => ({ type: 'SET_ACTIVE_CONSTRUCT', valu
export const setAnimating = value => ({ type: 'SET_ANIMATING', value });
export const setAnimCb = value => ({ type: 'SET_ANIM_CB', value });
export const setAnimFocus = value => ({ type: 'SET_ANIM_FOCUS', value });
export const setAnimSkill = value => ({ type: 'SET_ANIM_SKILL', value });
export const setAnimSource = value => ({ type: 'SET_ANIM_SOURCE', value });
export const setAnimTarget = value => ({ type: 'SET_ANIM_TARGET', value });
export const setAnimText = value => ({ type: 'SET_ANIM_TEXT', value });

View File

@ -56,9 +56,9 @@ document.fonts.load('16pt "Jura"').then(() => {
const Animations = () => (
<Provider store={store}>
<div id="mnml" class="animations-test">
<nav>
<aside>
{animationsNav(ws)}
</nav>
</aside>
<Game />
</div>
</Provider>
@ -69,13 +69,31 @@ document.fonts.load('16pt "Jura"').then(() => {
});
const SKILLS = [
'Absorb',
'Absorption',
'Amplify',
'Attack',
'Banish',
'Bash',
'Blast',
'Block',
'Break',
'Buff',
'Chaos',
'Counter',
'CounterAttack',
'Curse',
'Debuff',
'Decay',
'DecayTick',
'Electrify',
'Electrocute',
'ElectrocuteTick',
'Haste',
'HasteStrike',
'Heal',
'HybridBlast',
'Hybrid',
'HybridBlast',
'Intercept',
'Invert',
'Link',
@ -95,22 +113,4 @@ const SKILLS = [
'Sustain',
'Triage',
'TriageTick',
'Absorb',
'Absorption',
'Amplify',
'Attack',
'Banish',
'Bash',
'Blast',
'Block',
'Break',
'Buff',
'Chaos',
'CounterAttack',
'Counter',
'Curse',
'Debuff',
'Decay',
'DecayTick',
'Electrify',
];

View File

@ -36,7 +36,7 @@ function getObjects(resolution, stages, game, account) {
const i = sourceIsPlayer
? playerTeamIds.findIndex(c => c === resolution.source.id)
: otherTeamIds.findIndex(c => c === resolution.source.id);
const j = targetIsPlayer
? playerTeamIds.findIndex(c => c === resolution.target.id)
: otherTeamIds.findIndex(c => c === resolution.target.id);
@ -68,6 +68,7 @@ function getObjects(resolution, stages, game, account) {
return {
animSource,
animTarget,
animSkill: event.skill,
};
}
@ -85,7 +86,7 @@ function getSequence(resolution) {
case 'EndPost': return ['END_SKILL', 'POST_SKILL'];
case 'EndOnly': return ['END_SKILL'];
case 'PostOnly': return ['POST_SKILL'];
case 'None': return [];
case 'NoStages': return [];
default: return ['START_SKILL', 'END_SKILL', 'POST_SKILL'];
}
}
@ -177,7 +178,10 @@ function getText(resolution, sequence) {
if (type === 'Recharge') {
const { red, blue } = event;
return { text: [`+${red}R ${blue}B`, ''], css: '' };
if (red > 0 && blue > 0) return { text: [`+${red}R +${blue}B`, ''], css: 'purple-damage' };
if (red > 0) return { text: [`+${red}R`, ''], css: 'red-damage' };
if (blue > 0) return { text: [`+${blue}B`, ''], css: 'blue-damage' };
return nullText;
}
if (type === 'Removal') {

View File

@ -1,6 +1,8 @@
const preact = require('preact');
// const logger = require('redux-diff-logger');
const LogRocket = require('logrocket');
const { Provider, connect } = require('preact-redux');
const { createStore, combineReducers } = require('redux');
const { StripeProvider } = require('react-stripe-elements');
@ -13,6 +15,10 @@ const registerEvents = require('./events');
const Mnml = require('./components/mnml');
if (process.env.NODE_ENV !== 'development') {
LogRocket.init('yh0dy3/mnml');
}
function stripeKey() {
if (window.location.host === 'mnml.gg') return 'pk_live_fQGrL1uWww2ot8W1G7vTySAv004ygmnMXq';
return 'pk_test_Cb49tTqTXpzk7nEmlGzRrNJg00AU0aNZDj';

View File

@ -19,10 +19,7 @@ function projectile(x, y, radius, colour) {
cx={x}
cy={y}
r={radius}
fill="url(#grad1)"
stroke-width="2"
stroke={colour}
filter="url(#explosion)"
fill={colour}
/>
);
}
@ -31,8 +28,11 @@ class Chaos extends Component {
constructor() {
super();
this.animations = [];
const points = randomPoints(7, 30, { x: 0, y: 0, width: 300, height: 100 });
this.charges = points.map(coord => projectile(coord[0], coord[1], 14, '#A25AC1'));
const points = randomPoints(20, 30, { x: 0, y: 0, width: 300, height: 100 });
this.charges = points.map(coord => {
const colour = Math.random() >= 0.5 ? '#a52a2a' : '#3050f8';
return projectile(coord[0], coord[1], 14, colour);
});
}
render() {
@ -43,19 +43,6 @@ class Chaos extends Component {
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 300 400">
// {this.charges}
<defs>
<radialGradient id="grad1" cx="50%" cy="0%" r="85%" fx="50%" fy="50%">
<stop offset="0%" style="stop-color:#dba9a9;stop-opacity:0.6" />
<stop offset="100%" style={'stop-color:#A25AC1;stop-opacity:1'} />
</radialGradient>
</defs>
<filter id="explosion">
<feGaussianBlur stdDeviation="4"/>
<feTurbulence type="turbulence" baseFrequency="0.01" numOctaves="3" result="turbulence"/>
<feDisplacementMap in2="turbulence" in="SourceGraphic" scale="1" xChannelSelector="A" yChannelSelector="A"/>
</filter>
{this.charges}
</svg>
);
@ -63,20 +50,11 @@ class Chaos extends Component {
componentDidMount() {
const projectiles = document.querySelectorAll('.skill-anim circle');
projectiles.forEach(proj => {
const colour = Math.random() >= 0.5 ? '#a52a2a' : '#3050f8';
anime.set(proj, {
stroke: colour,
});
});
anime.set('.skill-anim', {
translateY: -(window.screen.height) * 0.35 * this.props.direction.y,
translateX: -(window.screen.width) * 0.15 * this.props.direction.x,
translateY: -(window.innerHeight) * 0.35 * this.props.direction.y,
translateX: -(window.innerWidth) * 0.15 * this.props.direction.x,
opacity: 0,
});
anime.set('#explosion feDisplacementMap', {
scale: 1,
});
this.animations.push(anime({
targets: '.skill-anim',
@ -96,23 +74,19 @@ class Chaos extends Component {
duration: (TIMES.TARGET_DURATION_MS * 1 / 2),
easing: 'easeInQuad',
}));
this.animations.push(anime({
targets: '#explosion feDisplacementMap',
scale: 75,
loop: false,
delay: (TIMES.TARGET_DELAY_MS + TIMES.TARGET_DURATION_MS * 2 / 3),
duration: (TIMES.TARGET_DURATION_MS * 1 / 3),
easing: 'easeInQuad',
}));
projectiles.forEach(proj => anime({
projectiles.forEach(proj => this.animations.push(anime({
targets: proj,
cx: Math.random() * 250 + 25,
cy: Math.random() * 300 + 50,
cx: 150 + (Math.random() * 50 * (Math.random() < 0.5 ? -1 : 1)),
cy: 200 + (Math.random() * 50 * (Math.random() < 0.5 ? -1 : 1)),
// cx: 150,
// cy: 200,
// opacity: 0,
delay: TIMES.TARGET_DELAY_MS,
duration: (TIMES.TARGET_DURATION_MS * 2 / 3),
easing: 'easeInQuad',
}));
})));
}
componentWillUnmount() {

View File

@ -61,7 +61,7 @@ class Heal extends Component {
targets: ['#heal'],
opacity: [
{ value: 1, delay: TIMES.TARGET_DELAY_MS, duration: TIMES.TARGET_DURATION_MS * 0.2 },
{ value: 0, delay: TIMES.TARGET_DURATION_MS * 0.4, duration: TIMES.TARGET_DURATION_MS * 0.2 },
{ value: 0, delay: TIMES.TARGET_DURATION_MS / 4, duration: TIMES.TARGET_DURATION_MS * 0.2 },
],
easing: 'easeInOutSine',
}));
@ -70,8 +70,8 @@ class Heal extends Component {
targets: ['#heal circle'],
cx: 150,
cy: 200,
delay: TIMES.TARGET_DELAY_MS * 2,
duration: TIMES.TARGET_DURATION_MS,
delay: TIMES.TARGET_DELAY_MS * 4,
duration: TIMES.TARGET_DURATION_MS * 0.9,
easing: 'easeOutCirc',
direction: 'reverse',
}));

View File

@ -57,7 +57,7 @@ class Intercept extends Component {
targets: ['#intercept'],
transform: [
`scale(1 1) ${this.props.player ? 'rotate(180)' : ''}`,
`scale(30 3) ${this.props.player ? 'rotate(180)' : ''}`,
`scale(5 5) ${this.props.player ? 'rotate(180)' : ''}`,
],
strokeWidth: 0,

View File

@ -27,12 +27,9 @@ class Siphon extends Component {
version="1.1"
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 300 400"
viewBox="0 0 300 300"
opacity="0">
<filter id="blur">
<feGaussianBlur in="SourceGraphic" stdDeviation="2" />
</filter>
<circle id="siphon" r="140" cx="150" cy="150" stroke="#3050f8" stroke-width="2.5%" filter="url(#blur)"/>
<circle id="siphon" r="140" cx="150" cy="150" stroke="#3050f8" stroke-width="2.5%"/>
</svg>
);
}
@ -53,15 +50,10 @@ class Siphon extends Component {
anime({
targets: '#siphon',
keyframes: [
{ r: '110', stroke: '#1FF01F' },
{ r: '80', stroke: '#1FF01F' },
{ r: '50', stroke: '#3050f8' },
{ r: '20', stroke: '#3050f8' },
],
r: 0,
delay: TIMES.TARGET_DELAY_MS,
duration,
easing: 'easeInCubic',
easing: 'easeInSine',
});
}

View File

@ -20,9 +20,7 @@ function projectile(x, y, radius, colour) {
cx={x}
cy={y}
r={radius}
fill="url(#grad1)"
stroke-width="0.1"
stroke={colour}
fill={colour}
/>
);
}
@ -44,24 +42,16 @@ class SiphonTick extends Component {
version="1.1"
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 300 400">
viewBox="0 0 300 300">
<filter id="blur">
<feGaussianBlur in="SourceGraphic" stdDeviation="2" />
</filter>
<circle id="siphon" r="20" cx="150" cy="150" stroke="#3050f8" stroke-width="2.5%" filter="url(#blur)"/>
<defs>
<radialGradient id="grad1" cx="50%" cy="0%" r="85%" fx="50%" fy="50%">
<stop offset="0%" style="stop-color:#3050f8;stop-opacity:0.4" />
<stop offset="100%" style={'stop-color:#1FF01F;stop-opacity:1'} />
</radialGradient>
</defs>
<filter id="explosion">
<feGaussianBlur stdDeviation="4"/>
<feTurbulence type="turbulence" baseFrequency="0.001" numOctaves="3" result="turbulence"/>
<feDisplacementMap in2="turbulence" in="SourceGraphic" scale="1" xChannelSelector="A" yChannelSelector="A"/>
</filter>
{this.charges}
</svg>
);
@ -84,27 +74,23 @@ class SiphonTick extends Component {
});
anime.set('#siphon', {
r: '80',
r: 0,
stroke: '#3050f8',
});
anime({
targets: '#siphon',
keyframes: [
{ r: '50', stroke: '#3050f8' },
{ r: '20', stroke: '#3050f8' },
{ r: '0', stroke: '#3050f8' },
],
r: 600,
duration: duration * 2 / 3,
easing: 'easeInCubic',
easing: 'easeInSine',
});
const projectiles = document.querySelectorAll('.skill-anim circle');
projectiles.forEach(proj => {
anime({
targets: proj,
cx: Math.random() * 250 + 25,
cy: Math.random() * 200 - 100,
cx: 150 + (Math.random() * 300 * (Math.random() < 0.5 ? -1 : 1)),
cy: 150 + (Math.random() * 300 * (Math.random() < 0.5 ? -1 : 1)),
delay: (Math.random() * duration * 1 / 2),
duration,
easing: 'easeInQuad',

View File

@ -21,20 +21,14 @@ function projectile(x, y, radius, colour) {
cx={x}
cy={y}
r={radius}
fill="url(#grad1)"
stroke-width="2"
stroke={colour}
fill={colour}
/>
);
}
function sword(colour) {
return (
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720">
<g transform="rotate(90, 640, 360) translate(0,720) scale(0.1,-0.1)" fill={colour} stroke={colour} filter="url(#explosion)" id="sword">
<path d="M4110 5071 c-294 -119 -436 -228 -457 -350 -4 -25 -7 -230 -6 -454 2 -380 1 -408 -15 -402 -9 4 -48 16 -87 26 -160 41 -215 44 -815 41 -719 -4 -1088 13 -1475 69 -270 38 -384 75 -468 148 -67 58 -111 75 -193 74 -104 -1 -204 -59 -264 -153 -130 -202 -159 -698 -59 -998 62 -188 174 -282 335 -282 77 0 114 14 174 67 87 77 198 114 456 152 386 56 724 73 1459 70 676 -2 728 1 937 68 15 4 16 -29 15 -419 -2 -500 -4 -492 93 -582 45 -41 99 -74 217 -133 87 -43 182 -86 212 -96 172 -55 197 16 78 226 -114 202 -160 347 -187 587 -16 144 -27 371 -18 376 4 3 299 0 655 -5 2030 -31 2627 -35 3953 -23 903 8 1433 17 1665 28 l340 16 450 79 c248 44 491 88 540 99 234 50 777 180 796 190 18 10 19 14 7 26 -7 8 -198 57 -423 109 -346 80 -483 106 -885 170 -417 66 -517 78 -815 100 -312 24 -424 27 -1370 36 -1039 11 -2548 7 -3645 -10 -327 -5 -748 -11 -934 -13 l-339 -3 2 75 c3 130 22 331 41 441 25 147 71 276 142 404 34 61 70 127 80 147 21 42 24 108 6 126 -23 23 -100 13 -198 -27z"/>
</g>
</svg>
<polygon points='150,150 75,75, 150,300, 225,75' fill={colour} id="sword" filter="url(#slayFilter)"></polygon>
);
}
@ -54,18 +48,11 @@ class Slay extends Component {
version="1.1"
id="slay"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 300 400">
<defs>
<radialGradient id="grad1" cx="50%" cy="0%" r="85%" fx="50%" fy="50%">
<stop offset="0%" style="stop-color:#dba9a9;stop-opacity:0.6" />
<stop offset="100%" style={`stop-color:${this.colour};stop-opacity:1`} />
</radialGradient>
</defs>
<filter id="explosion">
viewBox="0 0 300 300">
<filter id="slayFilter">
<feGaussianBlur stdDeviation="4"/>
<feTurbulence type="turbulence" baseFrequency="0.001" numOctaves="3" result="turbulence"/>
<feDisplacementMap in2="turbulence" in="SourceGraphic" scale="1" xChannelSelector="A" yChannelSelector="A"/>
</filter>
{sword(this.colour)}
{this.charges}
@ -91,16 +78,17 @@ class Slay extends Component {
});
anime.set('#slay', {
translateY: -1 * (window.screen.height) * 0.35,
translateY: -1 * (window.innerHeight) * 0.35,
translateX: 0,
});
anime.set('#explosion feDisplacementMap', {
scale: 100,
anime.set('#slayFilter feDisplacementMap', {
scale: 0,
});
anime.set('#sword', {
fill: this.colour,
stroke: this.colour,
opacity: 1,
});
this.animations.push(anime({
@ -119,10 +107,11 @@ class Slay extends Component {
duration: (duration * 1 / 2),
easing: 'easeInQuad',
}));
this.animations.push(anime({
targets: '#explosion feDisplacementMap',
scale: 10000,
loop: false,
targets: ['#slayFilter feTurbulence', '#slayFilter feDisplacementMap'],
baseFrequency: 10,
scale: 100,
delay: (TIMES.TARGET_DELAY_MS + duration * 1 / 2),
duration: (duration * 1 / 2),
easing: 'easeInQuad',
@ -130,8 +119,7 @@ class Slay extends Component {
this.animations.push(anime({
targets: '#sword',
fill: '#1FF01F',
stroke: '#1FF01F',
opacity: 0,
delay: (TIMES.TARGET_DELAY_MS + duration + TIMES.POST_SKILL_DURATION_MS * 0.7),
}));

View File

@ -6,8 +6,8 @@ function sourceCast(id, direction, idle) {
const { x, y } = direction;
return anime({
targets: [document.getElementById(id)],
translateX: x * window.screen.width * 0.15,
translateY: y * window.screen.height * 0.15,
translateX: x * window.screen.width * 0.1,
translateY: y * window.screen.height * 0.1,
easing: 'easeInOutElastic',
direction: 'alternate',
duration: TIMES.SOURCE_DURATION_MS,

View File

@ -54,7 +54,7 @@ class Strike extends Component {
x: [200, 0, 200],
height: [200, 10, 0],
width: [20, 400, 0],
delay: TIMES.TARGET_DELAY_MS / 2,
delay: TIMES.TARGET_DELAY_MS * 0.5,
duration: TIMES.TARGET_DURATION_MS,
}));

View File

@ -3,10 +3,10 @@ const anime = require('animejs').default;
function wiggle(id, idle) {
const duration = 300;
const target = document.getElementById(id);
const x = window.screen.width * 0.01 * (Math.round(Math.random()) ? Math.random() : -Math.random());
const y = window.screen.height * 0.01 * (Math.round(Math.random()) ? Math.random() : -Math.random());
const x = window.innerWidth * 0.01 * (Math.round(Math.random()) ? Math.random() : -Math.random());
const y = window.innerHeight * 0.01 * (Math.round(Math.random()) ? Math.random() : -Math.random());
console.log(x, y);
// console.log(x, y);
return anime({
targets: target,
rotate: 0,

View File

@ -10,11 +10,13 @@ const GameCtrlTopButtons = require('./game.ctrl.btns.top');
const addState = connect(
function receiveState(state) {
const {
animating,
game,
account,
} = state;
return {
animating,
game,
account,
};
@ -23,6 +25,7 @@ const addState = connect(
function Controls(args) {
const {
animating,
account,
game,
} = args;
@ -31,10 +34,10 @@ function Controls(args) {
const opponent = game.players.find(t => t.id !== account.id);
const player = game.players.find(t => t.id === account.id);
const zero = Date.parse(game.phase_start);
const now = Date.now();
const now = animating ? zero : Date.now();
const end = Date.parse(game.phase_end);
const timerPct = game.phase_end
? ((now - zero) / (end - zero) * 100)
: 100;

View File

@ -93,8 +93,6 @@ class Instance extends Component {
if (!instance) return setTimeout(this.bindSwipes, 50);
if (this.h) this.h.destroy();
this.h = new Hammer(instance);
const display = ['vbox', 'c0', 'c1', 'c2'];
this.h.on('swiperight', () => {
const {
navInstance,
@ -111,6 +109,7 @@ class Instance extends Component {
setNavInstance((navInstance + 1) % 4);
});
console.log('hammer gestures bound');
return true;
}
}

View File

@ -55,7 +55,6 @@ function Skill(props) {
// if (skillChosen && !targeting) {
// return false;
// }
const cdText = construct.skills[i].cd > 0
? `- ${s.cd}T`
: '';
@ -72,7 +71,7 @@ function Skill(props) {
return (
<button
disabled={cdText || s.disabled || ko}
class={`construct-skill-btn ${(targeting || highlight) ? 'active' : ''}`}
class={`${(targeting || highlight) ? 'active' : ''}`}
type="submit"
onClick={onClick}>
{s.skill} {cdText}

View File

@ -9,8 +9,8 @@ const shapes = require('./shapes');
const { removeTier } = require('../utils');
const addState = connect(
({ game, account, animTarget, animating, itemInfo }) =>
({ game, account, animTarget, animating, itemInfo })
({ game, account, animSkill, animating, itemInfo }) =>
({ game, account, animSkill, animating, itemInfo })
);
class TargetSvg extends Component {
@ -28,15 +28,15 @@ class TargetSvg extends Component {
}
render(props, state) {
const { game, account, animating, animTarget, itemInfo } = props;
const { game, account, animating, animSkill, itemInfo } = props;
const { width, height } = state;
if (!game) return false; // game will be null when battle ends
// resolutions happening
// just put skill name up
if (animating) {
if (!animTarget) return false;
const itemSource = itemInfo.combos.filter(c => c.item === removeTier(animTarget.skill));
if (!animSkill) return false;
const itemSource = itemInfo.combos.filter(c => c.item === removeTier(animSkill));
const itemSourceInfo = itemSource.length
? `${itemSource[0].components[0]} ${itemSource[0].components[1]} ${itemSource[0].components[2]}`
: false;
@ -44,7 +44,7 @@ class TargetSvg extends Component {
const itemSourceDescription = reactStringReplace(itemSourceInfo, itemRegEx, match => shapes[match]());
return (
<div class="resolving-skill">
<h1>{animTarget.skill}</h1>
<h1>{animSkill}</h1>
<div> {itemSourceDescription} </div>
</div>
);

View File

@ -7,7 +7,6 @@ const addState = connect(
function receiveState(state) {
const {
teamSelect,
showNav,
ws,
} = state;
@ -18,32 +17,18 @@ const addState = connect(
return {
sendAccountSetTeam,
teamSelect,
showNav,
};
},
function receiveDispatch(dispatch) {
function setShowNav(v) {
return dispatch(actions.setShowNav(v));
}
return {
setShowNav,
};
}
);
function TeamFooter(args) {
const {
showNav,
teamSelect,
sendAccountSetTeam,
setShowNav,
} = args;
return (
<footer>
<button id="nav-btn" onClick={() => setShowNav(!showNav)} ></button>
<button
disabled={teamSelect.some(c => !c)}
onClick={sendAccountSetTeam}>

View File

@ -1,10 +1,10 @@
const preact = require('preact');
const SOURCE_DURATION_MS = 1000;
const TARGET_DELAY_MS = 500;
const TARGET_DURATION_MS = 1500;
const POST_SKILL_DURATION_MS = 1000;
const SOURCE_AND_TARGET_TOTAL_DURATION = TARGET_DELAY_MS + TARGET_DURATION_MS;
const SOURCE_DURATION_MS = 1000; // Time for SOURCE ONLY
const TARGET_DELAY_MS = 500; // Used for Source + Target
const TARGET_DURATION_MS = 1500; // Time for TARGET ONLY
const POST_SKILL_DURATION_MS = 1000; // Time for all POST
const SOURCE_AND_TARGET_TOTAL_DURATION = TARGET_DELAY_MS + TARGET_DURATION_MS; // SOURCE + TARGET time
module.exports = {
TIMES: {

View File

@ -1,3 +1,4 @@
const LogRocket = require('logrocket');
const querystring = require('query-string');
const eachSeries = require('async/eachSeries');
@ -73,6 +74,7 @@ function registerEvents(store) {
const anims = animations.getObjects(r, sequence, game, account);
const text = animations.getText(r, sequence);
store.dispatch(actions.setAnimFocus(animations.getFocusTargets(r, game)));
if (anims.animSkill) store.dispatch(actions.setAnimSkill(anims.animSkill));
if (sequence.includes('START_SKILL') && anims.animSource) store.dispatch(actions.setAnimSource(anims.animSource));
if (sequence.includes('END_SKILL') && anims.animTarget) {
@ -81,23 +83,28 @@ function registerEvents(store) {
}
if (sequence.includes('POST_SKILL') && text) {
// timeout to prevent text classes from being added too soon
setTimeout(
() => store.dispatch(actions.setAnimText(text)),
timeout - TIMES.POST_SKILL_DURATION_MS
);
if (timeout === TIMES.POST_SKILL_DURATION_MS) {
store.dispatch(actions.setAnimText(text));
} else {
setTimeout(
() => store.dispatch(actions.setAnimText(text)),
timeout - TIMES.POST_SKILL_DURATION_MS
);
}
}
return setTimeout(() => {
const animSkill = anims.animTarget ? removeTier(anims.animTarget.skill) : false;
store.dispatch(actions.setAnimSkill(null));
store.dispatch(actions.setAnimSource(null));
store.dispatch(actions.setAnimTarget(null));
store.dispatch(actions.setAnimText(null));
store.dispatch(actions.setAnimFocus([]));
if (!sequence.includes('END_SKILL') || (animSkill && ['Banish', 'Invert'].includes(animSkill))) return cb();
if (!sequence.includes('END_SKILL') || (anims.animSkill && ['Banish', 'Invert'].includes(anims.animSkill))) return cb();
return true;
}, timeout);
}, err => {
if (err) return console.error(err);
// clear animation state
store.dispatch(actions.setAnimSkill(null));
store.dispatch(actions.setAnimSource(null));
store.dispatch(actions.setAnimTarget(null));
store.dispatch(actions.setAnimText(null));
@ -117,6 +124,10 @@ function registerEvents(store) {
}
function setAccount(account) {
if (account) {
LogRocket.identify(account.id, account);
}
store.dispatch(actions.setAccount(account));
}

View File

@ -1,10 +1,8 @@
function createReducer(defaultState, actionType) {
return function reducer(state = defaultState, action) {
switch (action.type) {
case actionType:
return action.value;
default:
return state;
case actionType: return action.value;
default: return state;
}
};
}
@ -18,6 +16,7 @@ module.exports = {
animating: createReducer(false, 'SET_ANIMATING'),
animCb: createReducer(null, 'SET_ANIM_CB'),
animSkill: createReducer(null, 'SET_ANIM_SKILL'),
animSource: createReducer(null, 'SET_ANIM_SOURCE'),
animFocus: createReducer(null, 'SET_ANIM_FOCUS'),
animTarget: createReducer(null, 'SET_ANIM_TARGET'),

View File

@ -1,6 +1,6 @@
{
"name": "mnml-ops",
"version": "1.5.2",
"version": "1.5.3",
"description": "",
"main": "index.js",
"scripts": {
@ -8,13 +8,15 @@
"migrate": "knex migrate:latest",
"migrate:make": "knex migrate:make --",
"test": "echo \"Error: no test specified\" && exit 1",
"nginx:dev": "sudo cp mnml.gg.DEV.SAMPLE.nginx.conf /etc/nginx/sites-available/mnml.gg.DEV.nginx.conf && sudo ln -nfs /etc/nginx/sites-available/mnml.gg.DEV.nginx.conf /etc/nginx/sites-enabled"
"nginx:dev": "sudo cp mnml.gg.DEV.SAMPLE.nginx.conf /etc/nginx/sites-available/mnml.gg.DEV.nginx.conf && sudo ln -nfs /etc/nginx/sites-available/mnml.gg.DEV.nginx.conf /etc/nginx/sites-enabled",
"qr": "qrcode-svg --color whitesmoke --background black -f -o mnml.qr.svg \"https://mnml.gg\""
},
"author": "",
"license": "UNLICENSED",
"dependencies": {
"knex": "^0.15.2",
"pg": "^7.4.3",
"qrcode-svg": "^1.0.0",
"request": "^2.88.0",
"sdftosvg": "0.0.4",
"uuid": "^3.3.3"

View File

@ -1,6 +1,6 @@
[package]
name = "mnml"
version = "1.5.2"
version = "1.5.3"
authors = ["ntr <ntr@smokestack.io>"]
[dependencies]

View File

@ -161,14 +161,12 @@ impl Game {
self.skill_phase_start(0)
}
fn skill_phase_start(mut self, num_resolutions: usize) -> Game {
let resolution_animation_ms = num_resolutions as i64 * 2500;
fn skill_phase_start(mut self, resolution_time: i64) -> Game {
self.phase_start = Utc::now()
.checked_add_signed(Duration::milliseconds(resolution_animation_ms))
.checked_add_signed(Duration::milliseconds(resolution_time))
.expect("could not set phase start");
self.phase_end = self.time_control.game_phase_end(resolution_animation_ms);
self.phase_end = self.time_control.game_phase_end(resolution_time);
for player in self.players.iter_mut() {
if player.skills_required() == 0 {
@ -428,13 +426,12 @@ impl Game {
// temp vec of this round's resolving skills
// because need to check cooldown use before pushing them into the complete list
let mut casts = vec![];
let mut turn_events = 0;
let mut resolution_delay = 0;
while let Some(cast) = self.stack.pop() {
// info!("{:} casts ", cast);
let mut resolutions = resolution_steps(&cast, &mut self);
turn_events += resolutions.len();
resolution_delay = resolutions.iter().fold(resolution_delay, |acc, r| acc + r.clone().get_delay());
self.resolved.append(&mut resolutions);
// while let Some(resolution) = resolutions.pop() {
@ -460,7 +457,7 @@ impl Game {
return self.finish()
}
self.skill_phase_start(turn_events)
self.skill_phase_start(resolution_delay)
}
fn progress_durations(&mut self, resolved: &Vec<Cast>) -> &mut Game {

View File

@ -78,7 +78,7 @@ pub fn resolve(skill: Skill, source: &mut Construct, target: &mut Construct, mut
return resolutions;
}
if target.affected(Effect::Reflect) && skill.colours().contains(&Colour::Blue) {
if target.affected(Effect::Reflect) && skill.colours().contains(&Colour::Blue) && !skill.is_tick() {
// guard against overflow
if source.affected(Effect::Reflect) {
return resolutions;
@ -299,7 +299,17 @@ fn post_resolve(_skill: Skill, game: &mut Game, mut resolutions: Resolutions) ->
.find(|e| e.effect == Effect::Electric).unwrap().clone();
match meta {
Some(EffectMeta::Skill(s)) => {
resolutions = electrocute(&mut target, &mut source, resolutions, s);
// Gurad against reflect overflow
if !(source.affected(Effect::Reflect) && target.affected(Effect::Reflect)) {
// Check reflect don't bother if electrocute is procing on death
if source.affected(Effect::Reflect) && !target.is_ko() {
resolutions.push(Resolution::new(&target, &source)
.event(Event::Reflection { skill: s }).stages(EventStages::EndPost));
resolutions = electrocute(&mut source, &mut target, resolutions, s);
} else {
resolutions = electrocute(&mut target, &mut source, resolutions, s);
}
}
},
_ => panic!("no electrify skill"),
};
@ -405,7 +415,7 @@ pub enum EventStages {
EndPost, // Skip Anim Anim
EndOnly, // Skip Anim Skip
PostOnly, // Skip Skip Anim
None, // Skip Skip Skip
NoStages, // Skip Skip Skip
}
#[derive(Debug,Clone,PartialEq,Serialize,Deserialize)]
@ -445,6 +455,25 @@ impl Resolution {
self.stages = s;
self
}
pub fn get_delay(self) -> i64 {
let source_duration = 1000; // Time for SOURCE ONLY
let target_delay = 500; // Used for Source + Target
let target_duration = 1500; // Time for TARGET ONLY
let post_skill = 1000; // Time for all POST
let source_and_target_total = target_delay + target_duration; // SOURCE + TARGET time
match self.stages {
EventStages::AllStages => source_and_target_total + post_skill, // Anim Anim Anim
EventStages::StartEnd => source_and_target_total, // Anim Anim Skip
EventStages::StartPost => source_duration + post_skill, // Anim Skip Anim
EventStages::StartOnly => source_duration, // Anim Skip Skip
EventStages::EndPost => target_duration + post_skill, // Skip Anim Anim
EventStages::EndOnly => target_duration, // Skip Anim Skip
EventStages::PostOnly => post_skill, // Skip Skip Anim
EventStages::NoStages => 0, // Skip Skip Skip
}
}
}
@ -779,7 +808,7 @@ impl Skill {
Skill::BashPlusPlus => 140,
// Debuff Base
Skill::DecayTick=> 25,
Skill::DecayTick=> 33,
Skill::DecayTickPlus => 45,
Skill::DecayTickPlusPlus => 70,
Skill::Silence=> 55, // Deals more per blue skill on target
@ -1348,22 +1377,36 @@ fn sleep(source: &mut Construct, target: &mut Construct, mut results: Resolution
fn sustain(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions {
let red_amount = source.red_power().pct(skill.multiplier());
results.push(Resolution::new(source, target)
.event(target.recharge(skill, red_amount, 0)));
results.push(Resolution::new(source, target).event(target.add_effect(skill, skill.effect()[0])));
let e = target.recharge(skill, red_amount, 0);
let stages = match e {
Event::Recharge { red, blue, skill: _ } => {
if red > 0 || blue > 0 { EventStages::PostOnly }
else { EventStages::NoStages }
}
_ => panic!("not recharge")
};
results.push(Resolution::new(source, target)
.event(target.add_effect(skill, skill.effect()[0]))
.stages(EventStages::PostOnly));
results.push(Resolution::new(source, target).event(e).stages(stages));
return results;
}
fn intercept(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions {
let red_amount = source.red_power().pct(skill.multiplier());
results.push(Resolution::new(source, target).event(target.recharge(skill, red_amount, 0)));
let intercept = skill.effect()[0];
results.push(Resolution::new(source, target).event(target.add_effect(skill, intercept)).stages(EventStages::PostOnly));
results.push(Resolution::new(source, target).event(target.add_effect(skill, intercept)));
let red_amount = source.red_power().pct(skill.multiplier());
let e = target.recharge(skill, red_amount, 0);
let stages = match e {
Event::Recharge { red, blue, skill: _ } => {
if red > 0 || blue > 0 { EventStages::PostOnly }
else { EventStages::NoStages }
}
_ => panic!("not recharge")
};
results.push(Resolution::new(source, target).event(e).stages(stages));
return results;
}
@ -1458,6 +1501,7 @@ fn heal(source: &mut Construct, target: &mut Construct, mut results: Resolutions
}
fn triage(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions {
let skip_tick = target.effects.iter().any(|e| e.effect == Effect::Triage);
let ConstructEffect { effect, duration, meta, tick: _ } = skill.effect()[0];
let tick_skill = match meta {
Some(EffectMeta::Skill(s)) => s,
@ -1465,7 +1509,11 @@ fn triage(source: &mut Construct, target: &mut Construct, mut results: Resolutio
};
let triage = ConstructEffect::new(effect, duration).set_tick(Cast::new_tick(source, target, tick_skill));
results.push(Resolution::new(source, target).event(target.add_effect(skill, triage)));
return triage_tick(source, target, results, tick_skill);
match skip_tick {
true => return results,
false => return triage_tick(source, target, results, tick_skill)
}
}
fn triage_tick(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions {
@ -1519,6 +1567,7 @@ fn decay(source: &mut Construct, target: &mut Construct, mut results: Resolution
let wither = skill.effect()[0];
results.push(Resolution::new(source, target).event(target.add_effect(skill, wither)));
let skip_tick = target.effects.iter().any(|e| e.effect == Effect::Decay);
let ConstructEffect { effect, duration, meta, tick: _ } = skill.effect()[1];
let tick_skill = match meta {
Some(EffectMeta::Skill(s)) => s,
@ -1529,7 +1578,10 @@ fn decay(source: &mut Construct, target: &mut Construct, mut results: Resolution
.event(target.add_effect(skill, decay))
.stages(EventStages::PostOnly));
return decay_tick(source, target, results, tick_skill);
match skip_tick {
true => return results,
false => return decay_tick(source, target, results, tick_skill)
}
}
fn decay_tick(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions {
@ -1549,17 +1601,37 @@ fn electrify(source: &mut Construct, target: &mut Construct, mut results: Resolu
}
fn electrocute(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions {
// Remove electric buff, no need to display if construct is dead
if !source.is_ko() {
let electric = source.effects.iter().position(|e| e.effect == Effect::Electric);
match electric {
Some(eff) => {
let ce = source.effects.remove(eff);
results.push(Resolution::new(source, source)
.event(Event::Removal { effect: ce.effect, construct_effects: source.effects.clone() })
.stages(EventStages::PostOnly));
}
None => ()
}
}
let ConstructEffect { effect, duration, meta, tick: _ } = skill.effect()[0];
let tick_skill = match meta {
Some(EffectMeta::Skill(s)) => s,
_ => panic!("no electrocute tick skill"),
};
let skip_tick = target.effects.iter().any(|e| e.effect == Effect::Electrocute);
let electrocute = ConstructEffect::new(effect, duration).set_tick(Cast::new_tick(source, target, tick_skill));
results.push(Resolution::new(source, target)
.event(target.add_effect(skill, electrocute))
.stages(EventStages::EndPost));
return electrocute_tick(source, target, results, tick_skill);
.stages(EventStages::PostOnly));
match skip_tick {
true => return results,
false => return electrocute_tick(source, target, results, tick_skill)
}
}
fn electrocute_tick(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions {
@ -1617,22 +1689,36 @@ fn reflect(source: &mut Construct, target: &mut Construct, mut results: Resoluti
results.push(Resolution::new(source, target).event(target.add_effect(skill, skill.effect()[0])));
let blue_amount = source.blue_power().pct(skill.multiplier());
results.push(Resolution::new(source, target)
.event(target.recharge(skill, 0, blue_amount))
.stages(EventStages::PostOnly));
let e = target.recharge(skill, 0, blue_amount);
let stages = match e {
Event::Recharge { red, blue, skill: _ } => {
if red > 0 || blue > 0 { EventStages::PostOnly }
else { EventStages::NoStages }
}
_ => panic!("not recharge")
};
results.push(Resolution::new(source, target).event(e).stages(stages));
return results;;
}
fn recharge(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions {
let red_amount = source.red_power().pct(skill.multiplier());
let blue_amount = source.blue_power().pct(skill.multiplier());
results.push(Resolution::new(source, target).event(target.recharge(skill, red_amount, blue_amount)));
let e = target.recharge(skill, red_amount, blue_amount);
let stages = match e {
Event::Recharge { red, blue, skill: _ } => {
if red > 0 || blue > 0 { EventStages::AllStages }
else { EventStages::StartEnd }
}
_ => panic!("not recharge")
};
results.push(Resolution::new(source, target).event(e).stages(stages));
return results;
}
fn siphon(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions {
let skip_tick = target.effects.iter().any(|e| e.effect == Effect::Siphon);
let ConstructEffect { effect, duration, meta, tick: _ } = skill.effect()[0];
let tick_skill = match meta {
Some(EffectMeta::Skill(s)) => s,
@ -1641,7 +1727,10 @@ fn siphon(source: &mut Construct, target: &mut Construct, mut results: Resolutio
let siphon = ConstructEffect::new(effect, duration).set_tick(Cast::new_tick(source, target, tick_skill));
results.push(Resolution::new(source, target).event(target.add_effect(skill, siphon)));
return siphon_tick(source, target, results, tick_skill);
match skip_tick {
true => return results,
false => return siphon_tick(source, target, results, tick_skill)
}
}
fn siphon_tick(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions {