diff --git a/COMBOS.md b/COMBOS.md deleted file mode 100644 index cf563a8c..00000000 --- a/COMBOS.md +++ /dev/null @@ -1,142 +0,0 @@ -# item_info -> - -combos [strike, [R R Attack]] -specs [spec [bonus amount, [r g b]] - -# Playthrough - -constructs join game - stats randomised - -initial stash drops - 6 skills - 6 colours - 6 specs - -play first round - basically duke it out - -# Colours # - -### Red ### -Real world concepts -Aggressive -Apply Buffs -Fast & Chaotic - -### Green ### -Healing Specialisation -Defensive -Purge buffs & debuffs - -### Blue ### -Fantasy concepts (magical) -Aggressive & Defensive -Apply Debuffs -Slow & Reliable - -# Classes # - -Class names to be changed -==================== -Pure Red `Nature` -Pure Green `Non-Violence` -Pure Blue `Destruction` -Hybrid Red / Blue `Chaos` -Hybrid Red / Green `Purity` -Hybrid Blue / Green `Technology` - - -Skills -========== - -Basic Type -------------------------------------------------------------------------- -Attack `Basic offensive skill - deal damage` -Buff `Base ally targetted skill - increase ally speed` -Stun `Base enemy disable - disable enemy for 2 rounds` -Block `Base self targetted defensive - reduced damage taken for 2 rounds` -Debuff `Base enemy debuff - reduce enemy speed` - -# Attack Base # - -RR - Strike -GG - Heal -BB - Blast -RG - Purify -GB - Decay -RB - Blast - -# Stun Base # - -RR - Strangle -GG - Break -BB - Ruin -RG - Banish -GB - Silence -RB - Hex - -# Buff Base # - -RR - Empower -GR - Triage -BB - Absorb -RG - Sustain -GB - Amplify -RB - Haste - -# Debuff Base # - -RR - Restrict -GG - Purge -BB - Curse -RG - Slow -GB - Siphon -RB - Invert - -# Block Base # - -RR - Counter -GG - Reflect -BB - Electrify -RG - Intercept -GB - Life `rename?` -RB - Recharge - - -## Advanced combos ## - -Two ways of upgrading - #1 -> combine more of the same for a stronger version of the same skill / spec (T2 / T3 Combos) - #2 -> combine skill with two matching colour specs to change the way the skill works (Spec / Skill hybrid) - -### T2 / T3 Combos ### - -All current specs / items can be further combo'd into T2 and T3 versions - -# 3 of same base => 1 upgraded tier # -`3 x T1 Red Damage Spec => T2 Red Damage Spec` -`3 x T2 Red Damage Spec => T3 Red Damage Spec` -`3 x T1 Strike => T2 Strike` -`3 x T2 Strike => T3 Strike` - -Upgraded skills will have a combination of higher damage / longer duration / reduced cooldown -Upgraded skills use the same speed formula as previously - -### Spec / Skill hybrid specs ### - -# Strike # -2 x Red Damage + Strike => Strike damage bonus (crit?) -2 x Red Speed + Strike => Strike reduces enemy speed -2 x Red Life + Strike => Strike reduces enemy healing (% reduction) - -# Heal # -2 x Green Damage + Heal => Heal target for additional 20% of caster's maximum life -2 x Green Speed + Heal => Heal target gets bonus speed -2 x Green Life + Heal => Heal increases target's max hp for 2 turns - -etc etc - -30 skills * 3 specs => 90 spec / skill hybrid specs -> might be overcomplicated - - diff --git a/ECONOMY.md b/ECONOMY.md deleted file mode 100644 index e40a894c..00000000 --- a/ECONOMY.md +++ /dev/null @@ -1,75 +0,0 @@ -# Everything costs money (gold?) - -Items - Base colours / skills / specs and associated upgrades - -### Sources of money -- Start with money and gain income after each battle -- Higher income from winning - -- Selling items in inventory or equipped on character refunds - - Selling from inventory full refund - - Selling from charcter 50% refund - -### Uses for money - -- Buying items -- Rerolling vbox - -### Base Costs - -Base colours have a base 1 cost -Base skills have a base 2 cost -Base specs have a base 3 cost - -### Actual Costs - -- Costs increase as more of an item is used on constructs in the game -- The cost increases by the base cost for every 6 allocations of base item -- Allocation is based on all constructs in the game - -### Example ### - -Round #1 - -All costs are base costs -# Player #1 and Player #2 (They both bought the same things) -Construct #1 Strike (Attack + RR), (2 + 1 + 1) = (4) cost -Construct #1 Empower (Buff + RR), (2 + 1 + 1) = (4) cost -Construct #3 Attack, 2 cost - -Total cost - 10 - -Round #2 - -Items used on constructs include: - -Red x 8 -Attack x 4 -Buff x 2 - -The costs of red for round #2 are now (1 + 1) = 2 - -If they were to buy the same skill setup it would be as follows: - -# Player #1 and Player #2 (They both bought the same things) -Construct #1 Strike (Attack + RR), (2 + 2 + 2) = (6) cost -Construct #1 Empower (Buff + RR), (2 + 2 + 2) = (6) cost -Construct #3 Attack, 2 cost - -Total cost - 14 - -### Philosophy of increasing item costs - -- Two games will never feel exactly the same -- Costs change over rounds to diversify skill choice and gameplay -- As optimal builds emerge the paths to reach them will change every game -- Rewarded for going (hipster) builds nobody else is trying -- Some reward for hoarding items in your inventory while they cheaper (hodl red) - -### Income values - -Could try with 9 base income -Income increases by 3 each round and winning bonus of 6 - - - diff --git a/NODES.md b/NODES.md deleted file mode 100644 index 52dd0e80..00000000 --- a/NODES.md +++ /dev/null @@ -1,49 +0,0 @@ - -# Stat Multipliers # - -### Defenses ### - -Rare `Increased GreenLife` - -Common `Increased Evasion rating` -Common `Increased Blue Life rating` -Common `Increased RedLife rating` -Common `Increased Healing done` -Common `Increased Healing received` -Common `Increased Blue Damage` -Common `Increased Red Damage` - -Uncommon `Reduced hp loss penalty to evade chance` -Uncommon `Increased base evasion chance per X evasion rating` -Uncommon `Increased % mitigation from red_life` -Uncommon `Increased % mitigation from spell shield` -Uncommon `Increased damage over time` - -Rare `gain empower on KO` -Rare `cannot be restrictd` -Rare `cannot be silenced` -Rare `cannot be intercepted` - -Rare `25% stun for attack` -Rare `25% hex for blast` - -Rare `cooldown reduction` -Rare `effect duration` - -Rare `increased phys damage, 0 spell damage` -Rare `increased spell damage, 0 phys damage` - -Rare `increased phys damage, silenced` -Rare `increased spell damage, restrictd` - -Rare `increased speed, increased durations` -Rare `increased speed, increased cooldowns` - -# Nature - Technology - Nonviolence - Destruction - Purity - Chaos # - - - Increased power - - Increased speed - - Increased stat - - ??? Related Notables - -# ??? Constructs need to have a minimum of X of the construct stat to learn a skill # diff --git a/ROADMAP.md b/ROADMAP.md deleted file mode 100644 index 7f9ff085..00000000 --- a/ROADMAP.md +++ /dev/null @@ -1,63 +0,0 @@ - -### Road Map ### - -# NOW Phase 1 (Dev -> Alpha) - -Form company structure - Brainstorm Names? - Finalise documents - -Game - Server T2 / T3 / Custom Specs - Any other outstanding "major" features ??? - -# Phase 2 (Alpha -> Beta) - -Friends / Word of mouth testing - Server balance adjustments based on data - Client improvements based on feedback - -Combat animations - -Make in game shop - Payment processors / CC etc - Handler for game purchases - MTX - Construct Avatars - MTX - Skill anims - -Setup company bank accounts - Accounting system - Xero etc - -# Phase 3 (Beta -> Release) - -Player Events e.g. chatwheel -Matchmaking + ELO / Leaderboard -Game skill private fields - -Refine artwork, icons, scaling etc -Music - -Marketing materials - Videos - Twitch - Advertisments? - Information - - - -# china shit -You need to read the details more carefully. Playsaurus messed up: - -1. They launched in China without registering a trademark - -2. A competitor registered the trademark after 3 months of their launch - -3. They continued to sell for 4 years under a name trademarked by another company, making $73,000+ yearly from that one country - -Now, they complain about it on Reddit, even though China is a 'First to File' company. - -The fault lies entirely with Playsaurus, nothing illegal occurred here. - -https://www.trademarknow.com/blog/first-to-file-versus-first... - -This situation could have occurred in many other countries - the difference is probably that the competitor is content to just sell in China, under a Chinese name, whereas products sold anywhere else would need to use the English name. \ No newline at end of file diff --git a/SPECS.md b/SPECS.md deleted file mode 100644 index e1c4cc64..00000000 --- a/SPECS.md +++ /dev/null @@ -1,224 +0,0 @@ -### Specs ### - -Numbers are placeholder -`Specs get a bonus dependent on the total of Red / Green / Blue in player skills & specs` - -# Example to meet 5 red gem bonus from skills only -In your player Construct #1 has `Strike`, Construct #2 has `Slay` and `Heal`, Construct #3 has `Restrict` -- RR skill `Strike` contributes 2 red gems to the total red gems (2 total) -- RG skill `Slay` contributes 1 red gem to the total red gems (3 total) -- GG skill `Heal` contirubtes 0 red gems to the total red gems (3 total) -- RR skill `Restrict` contirubtes 2 red gems to the total red gems (5 total) - -# Advanced specs also require a minimum number of Red / Green / Blue gems on the construct to take effect - - Tier 1 Basic specs (Damage / Health / Defense) will have no requirements - - Advanced specs will require a certain threshold of red / green / blue gems to be enabled - - Provided spec requirements are met, all specs will add gems to the construct - -# Starting from scratch with a vbox - -### Round 1 - - - Buy 4 reds (items) - - Buy two 'Attack' Skills & 1 Stun skill (items) - - Buy 1 Basic Damage Spec (item) - - Combine 2 Red + 'Attack' -> Strike - Combine 2 Red + 'Basic Damage Spec' -> Red Damage - - Construct #1 -> Give Strike & Red Damage Spec -> Strike + 1 x Red Damage Spec - Construct #2 -> Give Attack -> Attack - Construct #3 -> Give Stun -> Stun - - Player Total (4 Red + 2 Basic gems) - -### Round 2 - - - Buy 2 reds & 2 green & 2 blue (all available colour items) - - Buy 2 Basic Damage Spec (item) - - - Construct #2 Unequip Attack - - Combine 2 Green + 'Attack' -> Heal - - - Construct #3 Unequip Stun - - Combine 2 Blue + 'Stun' -> Ruin - - - Combine 2 Red + 'Basic Damage Spec' -> Red Damage - - Construct #1 -> Give Red Damage items -> Strike + 2 x Red Damage Spec (6R) - Construct #2 -> Give Heal item -> Heal (2G) - Construct #3 -> Give Ruin item -> Ruin (2B) - -## Round 3 - - - Buy 4 reds - - Buy 1 Attack, 1 Stun, 1 Block (item) - - Buy 2 Basic Damage Spec (item) - - - Combine 2 Red + 'Stun' -> Strangle - - Combine 2 Red + 'Block' -> Counter - - Construct #1 -> Give 'Stun' & 'Strangle' -> Strike, Stun, Strangle + 2 x Red Damage Spec (10R) - Construct #2 -> 'No change' -> Heal (2G) - Construct #3 -> Give Attack item & 2 Basic Damage Spec -> Attack + Ruin + 2 x Basic Damage Spec (2B) - -## Round 4 - - - Buy 4 reds (getting lucky with reds!) - - Buy 1 Attack, 1 Buff - - - Combine 2 Red + 'Attack' -> Strike - - Combine 2 Red + 'Buff' -> Empower - - - Construct #1 Unequip 2 x Red Damage spec, Equip Empower -> Strike, Stun, Strangle, Empower (8R) - - Combine 'Strike' + 2 x Red Damage spec -> 'Increased Strike Damage spec' - - ### Note 'Increased Strike Damage spec' requires 8R on the construct - - Construct #1 Equip Increased Strike Damage spec -> Strike, Stun, Strangle, Empower + Increased Strike Damage Spec (14R) - Construct #2 -> 'No change' -> Heal - Construct #3 -> 'No change' -> Attack + Ruin + 2 x Basic Damage Spec - -## Round 5 - - We already lost cause we went all in on 1 red construct like a noob - -### Generic Specs - -# Basic % GreenLife -`Base` -> 5% inc hp -`Player Bonus` -> 3 basic gems -> +5% // 6 basic gems -> +10% // 12 basic gems -> +15% -Maximum 35% inc hp - -# Basic Speed -`Base` -> 5% inc speed -`Player Bonus` -> 3 basic gems -> +10% // 6 basic gems -> +15% // 12 basic gems -> +20% -Maximum 50% inc speed - -# Basic Class Spec -`Base` -> +2 red, +2 green +2 blue gems on construct -# Basic Duration - -### Increased Damage Combos ### - -Generate by combining `Generic Spec (Basic Damage)` with respective RGB - -# Red Damage (Dmg + RR) -Add 2 `red gems` -`Base` -> 10% inc red dmg -`Player Bonus` 5 red gems -> +10% // 10 red gems -> +15% // 20 red gems -> +25% -Maximum +60% red damage - -# Blue Damage (Dmg + BB) # -Add 2 `blue gems` -`Base` -> 10% inc blue dmg -`Player Bonus` 5 blue gems -> +10% // 10 blue gems -> +15% // 20 blue gems -> +25% -Maximum +60% blue damage - -# Healing (Dmg + GG) # -Add 2 `green gems` -`Base` -> 10% inc healing -`Player Bonus` 5 green gems -> +10% // 10 green gems -> +15% // 20 green gems -> +25% -Maximum +60% inc healing - -# Red damage and healing (Dmg + RG) -Add 1 red 1 green gem -`Base` -> 5% inc red damage and 5% inc healing -`Player Bonus` (2R + 2G gems) -> +5% + 5% // (5R + 5G gems) -> +10% + 10% % // (10R + 10G) gems -> +15% + 15% -Maximum +35% inc red damage and 35% inc healing - -# Red and blue damage (Dmg + RB) -Add 1 red and 1 blue gem -`Base` -> 5% inc red damage and 5% inc healing -`Player Bonus` (2 red + 2 green gems) -> +5% + 5% // (5 red + 5 green gems) -> +10% + 10% % // 20 green gems -> +15% + 15% -Maximum +35% inc damage and 35% inc healing - -# Blue damage and healing (Dmg + BG) -Add 1 blue and 1 green gem -`Base` -> 5% inc blue damage and 5% inc healing -`Player Bonus` (2B + 2G gems) -> +5% + 5% // (5B + 5G gems) -> +10% + 10% % // (10B + 10G) gems -> +15% + 15% -Maximum +35% inc blue damage and 35% inc healing - -### Increased GreenLife Combos ### - -Generate by combining `Generic Spec (Basic GreenLife)` with respective RGB - -# Increased % Red Life (Basic %HP + 2R) -Add 2 `red gems` -`Base` -> 10% inc red shield -`Player Bonus` 5 red gems -> +10% // 10 red gems -> +15% // 20 red gems -> +20% -Maximum +55% inc red shield - -# Increased % Red Life and GreenLife (Basic %HP + 1R1G) -Add 1 red 1 green gem -`Base` -> 5% inc red shield and 5% inc hp -`Player Bonus` (2R + 2G gems) -> +5% + 5% // (5R + 5G gems) -> +10% + 10% % // (10R + 10G) gems -> +15% + 15% -Maximum +35% inc red shield and 35% inc hp - -# Increased % Blue Life (Basic %HP + 2B) -Add 2 `blue gems` -`Base` -> 10% inc red shield -`Player Bonus` 5 blue gems -> +10% // 10 blue gems -> +15% // 20 blue gems -> +20% -Maximum +55% inc blue shield - -# Increased % Blue Life and GreenLife (Basic %HP + 1B1G) -Add `1 blue and 1 green gems` -`Base` -> 5% inc red shield and 5% inc hp -`Player Bonus` (2B + 2G gems) -> +5% + 5% // (5B + 5G gems) -> +10% + 10% % // (10B + 10G) gems -> +15% + 15% -Maximum +35% inc blue shield and 35% inc hp - -# Increased % GreenLife (Basic %HP + 2G) -Add `2 green gems` -`Base` -> 10% inc hp -`Player Bonus` 5 green gems -> +10% // 10 green gems -> +15% // 20 green gems -> +20% -Maximum +55% inc hp - -# Increased % Blue and Red Life (Basic %HP + 1B1R) -Add `1 blue and 1 red gem` -`Base` -> 5% inc red shield and 5% inc hp -`Player Bonus` (2B + 2R gems) -> +5% + 5% // (5B + 5R gems) -> +10% + 10% % // (10B + 10R) gems -> +15% + 15% -Maximum +35% inc blue shield and 35% inc red shield - -## Upgraded Attack Spec Combos - -# Increased Strike Damage (Combine Strike + Red Damage Spec x 2) -Construct Requires `8 red gems` -Adds `6 red gems` -`Base` -> 15% increased strike damage -`Player Bonus` 15 red gems -> +15% // 20 red gems -> +20% // 30 red gems -> +30% -Maximum 80% increased strike damage - -# Improved Heal (Combine Heal + Healing Spec x 2) -Construct Requires `8 green gems` -`Base` -> 15% increased heal healing -`Player Bonus` 15 green gems -> +15% // 20 green gems -> +20% // 30 green gems -> +30% -Maximum 80% increased heal healing - -# Increased Blast Damage (Combine Blast + Blue Spec x 2) -Construct Requires `8 blue gems` -`Base` -> 15% increased blast damage -`Player Bonus` 15 blue gems -> +15% // 20 blue gems -> +20% // 30 blue gems -> +30% -Maximum 80% increased blast damage - -# Increased Slay Damage (Combine Slay + Red Damage Spec + Healing Spec) -Construct Requires `4 red 4 green gems` -`Base` -> 15% increased slay damage -`Player Bonus` (8R + 8G) gems -> +15% // (10R + 10G) gems -> +20% // (15R + 15G) gems -> +30% -Maximum 80% increased slay damage - -# Increased Banish Damage (Combine Slay + Red Damage Spec + Blue Damage Spec) -Construct Requires `4 red 4 blue gems` -`Base` -> 15% increased slay damage -`Player Bonus` (8R + 8B) gems -> +15% // (10R + 10B) gems -> +20% // (15R + 15B) gems -> +30% -Maximum 80% increased banish damage - -## Other Combos - -# Increased % Red Speed (Basic Speed + 2R) -Add 2 red gems -`Base` -> 15% inc red speed -`Player Bonus` 5 red gems -> +15% // 10 red gems -> +20% // 20 red gems -> +25% -Maximum 80% inc red speed - -# Nature Affinity (Basic Class spec + 2R) -`Base` -> Add 10 red gems diff --git a/WORKLOG.md b/WORKLOG.md index cdd37a00..0dfdeccb 100644 --- a/WORKLOG.md +++ b/WORKLOG.md @@ -2,31 +2,19 @@ ## NOW _ntr_ -* can't reset password without knowing password =\ +* effects rework + +Siphon = [ + Apply(Siphon(2T), target) + Apply(Siphoning(2T), source) + Skill(SiphonTick, source, target) + DamageBlue(50% BluePower, target), +] + * change cooldowns to delay & recharge - delay is cooldown before skill can first be used - recharge is cooldown after using skill - every x speed reduces delay of skills -* audio - * animation effects - * vbox combine / buy / equip etc - * background music -* effects rework - -Siphon = -[ - DamageBlue(50%), - Apply( - Siphon(2T) - - Siphoning(2T) - ), -] - -Hexagon Set -- Pick Colour -- Random Walk -- Draw hex -- Increase intensity for each visit _mashy_ * represent construct colours during game phase (try %bar or dots) @@ -50,6 +38,19 @@ _tba_ ## SOON +* can't reset password without knowing password =\ + +* audio + * animation effects + * vbox combine / buy / equip etc + * background music + +Hexagon Set +- Pick Colour +- Random Walk +- Draw hex +- Increase intensity for each visit + * combo rework - reduce number of items for creating t2/t3 items from 3 -> 2 - add lost complexity by adding skill spec items diff --git a/core/.cargo/config b/core/.cargo/config new file mode 100755 index 00000000..3c3311fc --- /dev/null +++ b/core/.cargo/config @@ -0,0 +1,3 @@ +[target.x86_64-pc-windows-msvc.gnu] +rustc-link-search = ["C:\\Program Files\\PostgreSQL\\pg96\\lib"] + diff --git a/core/.gitignore b/core/.gitignore new file mode 100644 index 00000000..9e853f6c --- /dev/null +++ b/core/.gitignore @@ -0,0 +1,4 @@ +target/ +Cargo.lock +log/ +.env \ No newline at end of file diff --git a/core/Cargo.toml b/core/Cargo.toml new file mode 100644 index 00000000..66e37389 --- /dev/null +++ b/core/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "mnml-core" +version = "1.10.0" +authors = ["ntr ", "mashy "] + +[dependencies] +serde = "1" +serde_derive = "1" + +rand = "0.6" +uuid = { version = "0.5", features = ["serde", "v4"] } +chrono = { version = "0.4", features = ["serde"] } +bcrypt = "0.2" + +failure = "0.1" + +log = "0.4" diff --git a/core/src/construct.rs b/core/src/construct.rs new file mode 100644 index 00000000..725387b6 --- /dev/null +++ b/core/src/construct.rs @@ -0,0 +1,1009 @@ +use uuid::Uuid; +use rand::prelude::*; + +use failure::Error; +use failure::err_msg; + +use skill::{Skill, Cast, Immunity, Disable, Event}; +use effect::{Cooldown, Effect, Colour}; +use spec::{Spec}; +use item::{Item}; + +#[derive(Debug,Clone,Serialize,Deserialize)] +pub struct Colours { + pub red: u8, + pub green: u8, + pub blue: u8, +} + +impl Colours { + pub fn new() -> Colours { + Colours { red: 0, green: 0, blue: 0 } + } + + pub fn from_construct(construct: &Construct) -> Colours { + let mut count = Colours::new(); + + for spec in construct.specs.iter() { + let v = Item::from(*spec); + v.colours(&mut count); + } + + for cs in construct.skills.iter() { + let v = Item::from(cs.skill); + v.colours(&mut count); + } + + count + } +} + + +#[derive(Debug,Clone,Copy,PartialEq,Serialize,Deserialize)] +pub struct ConstructSkill { + pub skill: Skill, + pub cd: Cooldown, + // used for Uon client + pub disabled: bool, +} + +impl ConstructSkill { + pub fn new(skill: Skill) -> ConstructSkill { + ConstructSkill { + skill, + cd: skill.base_cd(), + disabled: false, + } + } +} + +#[derive(Debug,Clone,Copy,PartialEq,Serialize,Deserialize)] +pub enum EffectMeta { + Skill(Skill), + TickAmount(u64), + AddedDamage(u64), + LinkTarget(Uuid), + Multiplier(u64), +} + +#[derive(Debug,Clone,Copy,PartialEq,Serialize,Deserialize)] +pub struct ConstructEffect { + pub effect: Effect, + pub duration: u8, + pub meta: Option, + pub tick: Option, +} + +impl ConstructEffect { + pub fn new(effect: Effect, duration: u8) -> ConstructEffect { + ConstructEffect { effect, duration, meta: None, tick: None } + } + + pub fn set_tick(mut self, tick: Cast) -> ConstructEffect { + self.tick = Some(tick); + self + } + + pub fn set_meta(mut self, meta: EffectMeta) -> ConstructEffect { + self.meta = Some(meta); + self + } + + pub fn get_duration(&self) -> u8 { + self.duration + } + + pub fn get_multiplier(&self) -> u64 { + match self.meta { + Some(EffectMeta::Multiplier(s)) => s, + _ => 0 + } + } + + pub fn get_skill(&self) -> Option { + match self.meta { + Some(EffectMeta::Skill(s)) => Some(s), + _ => None, + } + + } +} + +#[derive(Debug,Clone,Copy,PartialEq,Serialize,Deserialize)] +pub enum Stat { + Str, + Agi, + Int, + GreenLife, + Speed, + RedPower, + BluePower, + GreenPower, + RedDamageTaken, + BlueDamageTaken, + GreenDamageTaken, + RedLife, + BlueLife, + Evasion, +} + +#[derive(Debug,Clone,Copy,PartialEq,Serialize,Deserialize)] +pub struct ConstructStat { + base: u64, + value: u64, + max: u64, + pub stat: Stat, +} + +impl ConstructStat { + // pub fn set(&mut self, v: u64, specs: &Vec) -> &mut ConstructStat { + // self.base = v; + // self.recalculate(specs) + // } + + pub fn recalculate(&mut self, specs: &Vec, player_colours: &Colours) -> &mut ConstructStat { + let specs = specs + .iter() + .filter(|s| s.affects().contains(&self.stat)) + .map(|s| *s) + .collect::>(); + + // applied with fold because it can be zeroed or multiplied + // but still needs access to the base amount + let value = specs.iter().fold(self.base, |acc, s| s.apply(acc, self.base, player_colours)); + self.value = value; + self.max = value; + + self + } + + pub fn reduce(&mut self, amt: u64) -> &mut ConstructStat { + self.value = self.value.saturating_sub(amt); + self + } + + pub fn increase(&mut self, amt: u64) -> &mut ConstructStat { + self.value = *[ + self.value.saturating_add(amt), + self.max + ].iter().min().unwrap(); + + self + } + + pub fn force(&mut self, v: u64) -> &mut ConstructStat { + self.base = v; + self.value = v; + self.max = v; + + self + } +} + +#[derive(Debug,Clone,Serialize,Deserialize)] +pub struct ConstructSkeleton { + pub id: Uuid, + pub account: Uuid, + pub img: Uuid, + pub name: String, +} + +#[derive(Debug,Clone,Serialize,Deserialize)] +pub struct Construct { + pub id: Uuid, + pub account: Uuid, + pub img: Uuid, + pub red_power: ConstructStat, + pub red_life: ConstructStat, + pub blue_life: ConstructStat, + pub blue_power: ConstructStat, + pub green_power: ConstructStat, + pub speed: ConstructStat, + pub green_life: ConstructStat, + // pub evasion: ConstructStat, + pub skills: Vec, + pub effects: Vec, + pub specs: Vec, + pub colours: Colours, + pub name: String, +} + +impl Construct { + pub fn new() -> Construct { + let id = Uuid::new_v4(); + return Construct { + id, + account: id, + img: Uuid::new_v4(), + red_power: ConstructStat { base: 320, value: 320, max: 320, stat: Stat::RedPower }, + red_life: ConstructStat { base: 125, value: 125, max: 125, stat: Stat::RedLife }, + blue_power: ConstructStat { base: 320, value: 320, max: 320, stat: Stat::BluePower }, + blue_life: ConstructStat { base: 125, value: 125, max: 125, stat: Stat::BlueLife }, + green_power: ConstructStat { base: 300, value: 300, max: 300, stat: Stat::GreenPower }, + green_life: ConstructStat { base: 800, value: 800, max: 800, stat: Stat::GreenLife }, + speed: ConstructStat { base: 100, value: 100, max: 100, stat: Stat::Speed }, + // evasion: ConstructStat { base: 0, value: 0, max: 0, stat: Stat::Evasion }, + skills: vec![], + effects: vec![], + specs: vec![], + colours: Colours::new(), + name: String::new(), + }; + } + + pub fn from_skeleton(skeleton: &ConstructSkeleton) -> Construct { + return Construct { + id: skeleton.id, + account: skeleton.account, + img: skeleton.img, + name: skeleton.name.clone(), + + .. Construct::new() + }; + } + + pub fn to_skeleton(&self) -> ConstructSkeleton { + ConstructSkeleton { + id: self.id, + account: self.account, + img: self.img, + name: self.name.clone(), + } + } + + + pub fn named(mut self, name: &String) -> Construct { + self.name = name.clone(); + self + } + + pub fn set_account(mut self, account: Uuid) -> Construct { + self.account = account; + self + } + + pub fn new_img(mut self) -> Construct { + self.img = Uuid::new_v4(); + self + } + + pub fn new_name(self, name: String) -> Result { + if name.len() > 20 { + return Err(err_msg("20 character name maximum")); + } + Ok(self.named(&name)) + } + + pub fn learn(mut self, s: Skill) -> Construct { + self.skills.push(ConstructSkill::new(s)); + self.colours = Colours::from_construct(&self); + self + } + + pub fn learn_mut(&mut self, s: Skill) -> &mut Construct { + self.skills.push(ConstructSkill::new(s)); + self.calculate_colours() + } + + pub fn forget(&mut self, skill: Skill) -> Result<&mut Construct, Error> { + match self.skills.iter().position(|s| s.skill == skill) { + Some(i) => { + self.skills.remove(i); + return Ok(self.calculate_colours()); + }, + None => Err(format_err!("{:?} does not know {:?}", self.name, skill)), + } + } + + pub fn spec_add(&mut self, spec: Spec) -> Result<&mut Construct, Error> { + if self.specs.len() >= 3 { + return Err(err_msg("maximum specs equipped")); + } + + self.specs.push(spec); + return Ok(self.calculate_colours()); + } + + pub fn spec_remove(&mut self, spec: Spec) -> Result<&mut Construct, Error> { + match self.specs.iter().position(|s| *s == spec) { + Some(p) => self.specs.remove(p), + None => return Err(err_msg("spec not found")), + }; + + Ok(self.calculate_colours()) + } + + fn calculate_colours(&mut self) -> &mut Construct { + self.colours = Colours::from_construct(&self); + self + } + + pub fn apply_modifiers(&mut self, player_colours: &Colours) -> &mut Construct { + self.red_power.recalculate(&self.specs, player_colours); + self.red_life.recalculate(&self.specs, player_colours); + self.blue_power.recalculate(&self.specs, player_colours); + self.blue_life.recalculate(&self.specs, player_colours); + // self.evasion.recalculate(&self.specs, &self.colours, player_colours); + self.speed.recalculate(&self.specs, player_colours); + self.green_power.recalculate(&self.specs, player_colours); + self.green_life.recalculate(&self.specs, player_colours); + + self + } + + pub fn is_ko(&self) -> bool { + self.green_life.value == 0 + } + + pub fn force_ko(&mut self) -> &mut Construct { + self.green_life.value = 0; + self + } + + pub fn immune(&self, skill: Skill) -> Option { + // also checked in resolve stage so shouldn't happen really + if self.is_ko() { + return Some(vec![Effect::Ko]); + } + + let immunities = self.effects.iter() + .filter(|e| e.effect.immune(skill)) + .map(|e| e.effect) + .collect::>(); + + if immunities.len() > 0 { + return Some(immunities); + } + + None + } + + pub fn disabled(&self, skill: Skill) -> Option { + if self.is_ko() && !skill.ko_castable() { + return Some(vec![Effect::Ko]); + } + + let disables = self.effects.iter() + .filter(|e| e.effect.disables_skill(skill)) + .map(|e| e.effect) + .collect::>(); + + if disables.len() > 0 { + return Some(disables); + } + + None + } + + pub fn is_stunned(&self) -> bool { + self.available_skills().len() == 0 + } + + pub fn affected(&self, effect: Effect) -> bool { + self.effects.iter().any(|s| s.effect == effect) + } + + pub fn available_skills(&self) -> Vec<&ConstructSkill> { + self.skills.iter() + .filter(|s| s.cd.is_none()) + .filter(|s| self.disabled(s.skill).is_none()) + .collect() + } + + pub fn mob_select_skill(&self) -> Option { + let available = self.available_skills(); + + if available.len() == 0 { + return None; + } + + let mut rng = thread_rng(); + + let i = match available.len() { + 1 => 0, + _ => rng.gen_range(0, available.len()), + }; + + return Some(available[i].skill); + + // let highest_cd = available.iter() + // .filter(|s| s.skill.base_cd().is_some()) + // .max_by_key(|s| s.skill.base_cd().unwrap()); + + // return match highest_cd { + // Some(s) => Some(s.skill), + // None => Some(available[0].skill), + // }; + } + + pub fn knows(&self, skill: Skill) -> bool { + self.skills.iter().any(|s| s.skill == skill) + } + + pub fn skill_on_cd(&self, skill: Skill) -> Option<&ConstructSkill> { + self.skills.iter().find(|s| s.skill == skill && s.cd.is_some()) + } + + pub fn skill_set_cd(&mut self, skill: Skill) -> &mut Construct { + let i = self.skills.iter().position(|s| s.skill == skill).unwrap(); + self.skills.remove(i); + self.skills.push(ConstructSkill::new(skill)); + + self + } + + pub fn reduce_cooldowns(&mut self) -> &mut Construct { + for skill in self.skills.iter_mut() { + // if used cooldown + if skill.skill.base_cd().is_some() { + // what is the current cd + if let Some(current_cd) = skill.cd { + + // if it's 1 set it to none + if current_cd == 1 { + skill.cd = None; + continue; + } + + // otherwise decrement it + skill.cd = Some(current_cd.saturating_sub(1)); + } + + } + } + + self + } + + pub fn reduce_effect_durations(&mut self) -> &mut Construct { + self.effects = self.effects.clone().into_iter().filter_map(|mut effect| { + effect.duration = effect.duration.saturating_sub(1); + + if effect.duration == 0 { + return None; + } + + // info!("reduced effect {:?}", effect); + return Some(effect); + }).collect::>(); + + self + } + + // Stats + pub fn red_power(&self) -> u64 { + let red_power_mods = self.effects.iter() + .filter(|e| e.effect.modifications().contains(&Stat::RedPower)) + .map(|e| (e.effect, e.meta)) + .collect::)>>(); + + let modified_red_power = red_power_mods.iter() + .fold(self.red_power.value, |acc, fx| fx.0.apply(acc, fx.1)); + return modified_red_power; + } + + pub fn blue_power(&self) -> u64 { + let blue_power_mods = self.effects.iter() + .filter(|e| e.effect.modifications().contains(&Stat::BluePower)) + .map(|e| (e.effect, e.meta)) + .collect::)>>(); + + let modified_blue_power = blue_power_mods.iter() + .fold(self.blue_power.value, |acc, fx| fx.0.apply(acc, fx.1)); + return modified_blue_power; + } + + pub fn green_power(&self) -> u64 { + let green_power_mods = self.effects.iter() + .filter(|e| e.effect.modifications().contains(&Stat::GreenPower)) + .map(|e| (e.effect, e.meta)) + .collect::)>>(); + + let modified_green_power = green_power_mods.iter() + .fold(self.green_power.value, |acc, fx| fx.0.apply(acc, fx.1)); + return modified_green_power; + } + + pub fn skill_speed(&self, s: Skill) -> u64 { + self.speed().saturating_mul(s.speed() as u64) + } + + // todo complete with specs + pub fn skill_is_aoe(&self, s: Skill) -> bool { + s.aoe() + } + + pub fn speed(&self) -> u64 { + let speed_mods = self.effects.iter() + .filter(|e| e.effect.modifications().contains(&Stat::Speed)) + .map(|e| (e.effect, e.meta)) + .collect::)>>(); + + let modified_speed = speed_mods.iter() + .fold(self.speed.value, |acc, fx| fx.0.apply(acc, fx.1)); + return modified_speed; + } + + pub fn red_life(&self) -> u64 { + self.red_life.value + } + + pub fn blue_life(&self) -> u64 { + self.blue_life.value + } + + pub fn green_life(&self) -> u64 { + self.green_life.value + } + + fn reduce_green_life(&mut self, amount: u64) { + self.green_life.reduce(amount); + if self.affected(Effect::Sustain) && self.green_life() == 0 { + self.green_life.value = 1; + } + } + + pub fn recharge(&mut self, skill: Skill, red_amount: u64, blue_amount: u64) -> Vec { + let mut events = vec![]; + + // Should red type immunity block recharge??? + if let Some(immunity) = self.immune(skill) { + if !self.is_ko() { + events.push(Event::Immunity { skill, immunity }); + } + return events; + } + + match self.affected(Effect::Invert) { + false => { + // Do we need inversion? + let current_red_life = self.red_life(); + self.red_life.increase(red_amount); + let new_red_life = self.red_life.value; + let red = new_red_life - current_red_life; + + let current_blue_life = self.blue_life(); + self.blue_life.increase(blue_amount); + let new_blue_life = self.blue_life.value; + let blue = new_blue_life - current_blue_life; + + if red != 0 || blue != 0 { + events.push(Event::Recharge { red, blue, skill }); + } + }, + true => { + // Recharge takes a red and blue amount so check for them + if red_amount != 0 { + let red_mods = self.effects.iter() + .filter(|e| e.effect.modifications().contains(&Stat::RedDamageTaken)) + .map(|e| (e.effect, e.meta)) + .collect::)>>(); + + let red_modified_power = red_mods.iter() + .fold(red_amount, |acc, fx| fx.0.apply(acc, fx.1)); + + + let red_remainder = red_modified_power.saturating_sub(self.red_life.value); + let red_mitigation = red_modified_power.saturating_sub(red_remainder); + + // reduce red_life by mitigation amount + self.red_life.reduce(red_mitigation); + + // deal remainder to green_life + let red_current_green_life = self.green_life(); + self.reduce_green_life(red_remainder); + let red_damage_amount = red_current_green_life - self.green_life(); + + events.push(Event::Damage { + skill, + amount: red_damage_amount, + mitigation: red_mitigation, + colour: Colour::Red + }); + } + + if blue_amount != 0 { + let blue_mods = self.effects.iter() + .filter(|e| e.effect.modifications().contains(&Stat::BlueDamageTaken)) + .map(|e| (e.effect, e.meta)) + .collect::)>>(); + + let blue_modified_power = blue_mods.iter() + .fold(blue_amount, |acc, fx| fx.0.apply(acc, fx.1)); + + + let blue_remainder = blue_modified_power.saturating_sub(self.blue_life.value); + let blue_mitigation = blue_modified_power.saturating_sub(blue_remainder); + + // reduce blue_life by mitigation amount + self.blue_life.reduce(blue_mitigation); + + // deal remainder to green_life + let blue_current_green_life = self.green_life(); + self.reduce_green_life(blue_remainder); + let blue_damage_amount = blue_current_green_life - self.green_life(); + + events.push(Event::Damage { + skill, + amount: blue_damage_amount, + mitigation: blue_mitigation, + colour: Colour::Blue + }); + } + } + } + return events; + } + + pub fn deal_green_damage(&mut self, skill: Skill, amount: u64) -> Vec { + let mut events = vec![]; + if let Some(immunity) = self.immune(skill) { + if !self.is_ko() { + events.push(Event::Immunity { skill, immunity }); + } + return events; + } + + let mods = self.effects.iter() + .filter(|e| e.effect.modifications().contains(&Stat::GreenDamageTaken)) + .map(|e| (e.effect, e.meta)) + .collect::)>>(); + + let modified_power = mods.iter() + .fold(amount, |acc, fx| fx.0.apply(acc, fx.1)); + + match self.affected(Effect::Invert) { + false => { + let current_green_life = self.green_life(); + self.green_life.increase(modified_power); + let new_green_life = self.green_life.value; + + let healing = new_green_life - current_green_life; + let overhealing = modified_power - healing; + + events.push(Event::Healing { + skill, + amount: healing, + overhealing, + }); + }, + true => { + // events.push(Event::Inversion { skill }); + + // there is no green shield (yet) + let current_green_life = self.green_life(); + self.reduce_green_life(modified_power); + let delta = current_green_life - self.green_life(); + + events.push(Event::Damage { + skill, + amount: delta, + mitigation: 0, + colour: Colour::Green, + }); + } + } + + return events; + } + + pub fn deal_red_damage(&mut self, skill: Skill, amount: u64) -> Vec { + let mut events = vec![]; + + if let Some(immunity) = self.immune(skill) { + if !self.is_ko() { + events.push(Event::Immunity { skill, immunity }); + } + return events; + } + + let mods = self.effects.iter() + .filter(|e| e.effect.modifications().contains(&Stat::RedDamageTaken)) + .map(|e| (e.effect, e.meta)) + .collect::)>>(); + + let modified_power = mods.iter() + .fold(amount, |acc, fx| fx.0.apply(acc, fx.1)); + + match self.affected(Effect::Invert) { + false => { + // calculate amount of damage red_life will not absorb + // eg 50 red_life 25 damage -> 0 remainder 25 mitigation + // 50 red_life 100 damage -> 50 remainder 50 mitigation + // 50 red_life 5 damage -> 0 remainder 5 mitigation + let remainder = modified_power.saturating_sub(self.red_life.value); + let mitigation = modified_power.saturating_sub(remainder); + + // reduce red_life by mitigation amount + self.red_life.reduce(mitigation); + + // deal remainder to green_life + let current_green_life = self.green_life(); + self.reduce_green_life(remainder); + let delta = current_green_life - self.green_life(); + + events.push(Event::Damage { + skill, + amount: delta, + mitigation, + colour: Colour::Red, + }); + }, + true => { + // events.push(Event::Inversion { skill }); + + let current_green_life = self.green_life(); + self.green_life.increase(modified_power); + let new_green_life = self.green_life.value; + let healing = new_green_life - current_green_life; + let overhealing = modified_power - healing; + + let current_life = self.red_life.value; + self.red_life.increase(overhealing); + let recharge = self.red_life.value - current_life; + + if healing > 0 { + events.push(Event::Healing { + skill, + amount: healing, + overhealing: overhealing - recharge, + }); + } + + if recharge > 0 { + events.push(Event::Recharge { red: recharge, blue: 0, skill }); + } + } + }; + + return events; + } + + pub fn deal_blue_damage(&mut self, skill: Skill, amount: u64) -> Vec { + let mut events = vec![]; + + if let Some(immunity) = self.immune(skill) { + if !self.is_ko() { + events.push(Event::Immunity { skill, immunity }); + } + return events; + } + + let mods = self.effects.iter() + .filter(|e| e.effect.modifications().contains(&Stat::BlueDamageTaken)) + .map(|e| (e.effect, e.meta)) + .collect::)>>(); + + let modified_power = mods.iter() + .fold(amount, |acc, fx| fx.0.apply(acc, fx.1)); + + match self.affected(Effect::Invert) { + false => { + let remainder = modified_power.saturating_sub(self.blue_life.value); + let mitigation = modified_power.saturating_sub(remainder); + + // reduce blue_life by mitigation amount + self.blue_life.reduce(mitigation); + + // deal remainder to green_life + let current_green_life = self.green_life(); + self.reduce_green_life(remainder); + let delta = current_green_life - self.green_life(); + + events.push(Event::Damage { + skill, + amount: delta, + mitigation, + colour: Colour::Blue, + }); + }, + true => { + // events.push(Event::Inversion { skill }); + + let current_green_life = self.green_life(); + self.green_life.increase(modified_power); + let new_green_life = self.green_life.value; + let healing = new_green_life - current_green_life; + let overhealing = modified_power - healing; + + let current_life = self.blue_life.value; + self.blue_life.increase(overhealing); + let recharge = self.blue_life.value - current_life; + + if healing > 0 { + events.push(Event::Healing { + skill, + amount: healing, + overhealing, + }); + } + + if recharge > 0 { + events.push(Event::Recharge { red: 0, blue: recharge, skill }); + } + } + }; + + return events; + } + + pub fn add_effect(&mut self, skill: Skill, effect: ConstructEffect) -> Event { + if let Some(immunity) = self.immune(skill) { + return Event::Immunity { + skill, + immunity, + }; + } + + if let Some(p) = self.effects.iter().position(|ce| ce.effect == effect.effect) { + // duplicate effect + // replace existing + + self.effects[p] = effect; + } else { + // new effect + // info!("{:?} {:?} adding effect", self.name, effect.effect); + self.effects.push(effect); + } + + // todo modified durations cause of buffs + let result = Event::Effect { + effect: effect.effect, + duration: effect.duration, + construct_effects: self.effects.clone(), + skill, + }; + return result; + } + + // pub fn evade(&self, skill: Skill) -> Option { + // if self.evasion.value == 0 { + // return None; + // } + + // let mut rng = thread_rng(); + // let green_life_pct = (self.green_life.value * 100) / self.green_life.value; + // let evasion_rating = (self.evasion.value * green_life_pct) / 100; + // let roll = rng.gen_range(0, 100); + // info!("{:} < {:?}", roll, evasion_rating); + + // match roll < evasion_rating { + // true => Some(Event::Evasion { + // skill, + // evasion_rating: evasion_rating, + // }), + // false => None, + // } + // } +} + + +#[cfg(test)] +mod tests { + use construct::*; + use util::IntPct; + + #[test] + fn create_construct_test() { + let construct = Construct::new() + .named(&"hatchling".to_string()); + + assert_eq!(construct.name, "hatchling".to_string()); + return; + } + + #[test] + fn construct_colours_test() { + let mut construct = Construct::new() + .named(&"redboi".to_string()); + + construct.learn_mut(Skill::Strike); + construct.spec_add(Spec::LifeGG).unwrap(); + construct.spec_add(Spec::PowerRR).unwrap(); + construct.spec_add(Spec::LifeBB).unwrap(); + + assert_eq!(construct.colours.red, 4); + assert_eq!(construct.colours.green, 2); + assert_eq!(construct.colours.blue, 2); + + return; + } + + #[test] + fn construct_player_modifiers_test() { + let mut construct = Construct::new() + .named(&"player player".to_string()); + + construct.spec_add(Spec::PowerRR).unwrap(); + construct.spec_add(Spec::PowerGG).unwrap(); + construct.spec_add(Spec::PowerBB).unwrap(); + construct.learn_mut(Skill::StrikePlusPlus); // 18 reds (24 total) + + let player_colours = Colours { + red: 5, + green: 15, + blue: 25, + }; + + construct.apply_modifiers(&player_colours); + + assert!(construct.red_power.value == construct.red_power.base + construct.red_power.base.pct(35)); + assert!(construct.green_power.value == construct.green_power.base + construct.green_power.base.pct(50)); + assert!(construct.blue_power.value == construct.blue_power.base + construct.blue_power.base.pct(70)); + + return; + } + + #[test] + fn construct_player_modifiers_base_test() { + let mut construct = Construct::new() + .named(&"player player".to_string()); + + construct.spec_add(Spec::Power).unwrap(); + construct.spec_add(Spec::Life).unwrap(); + + let player_colours = Colours { + red: 5, + green: 15, + blue: 25, + }; + + construct.apply_modifiers(&player_colours); + + assert!(construct.red_power.value == construct.red_power.base + construct.red_power.base.pct(10)); + assert!(construct.green_power.value == construct.green_power.base + construct.green_power.base.pct(10)); + assert!(construct.blue_power.value == construct.blue_power.base + construct.blue_power.base.pct(10)); + assert!(construct.green_life.value == construct.green_life.base + 125); + + return; + } + + #[test] + fn construct_colour_calc_test() { + let mut construct = Construct::new() + .named(&"player player".to_string()); + + construct.spec_add(Spec::PowerRR).unwrap(); + construct.spec_add(Spec::PowerGG).unwrap(); + construct.spec_add(Spec::PowerBB).unwrap(); + + let colours = Colours::from_construct(&construct); + assert!(colours.red == 2); + assert!(colours.blue == 2); + assert!(colours.green == 2); + + let construct = construct + .learn(Skill::Strike) + .learn(Skill::BlastPlusPlus); + + let colours = Colours::from_construct(&construct); + assert!(colours.red == 4); + assert!(colours.blue == 20); + assert!(colours.green == 2); + } + + #[test] + fn construct_player_modifiers_spec_bonus_test() { + let mut construct = Construct::new() + .named(&"player player".to_string()); + + construct.spec_add(Spec::PowerRR).unwrap(); + construct.spec_add(Spec::PowerGG).unwrap(); + construct.spec_add(Spec::PowerBB).unwrap(); + + let player_colours = Colours { + red: 5, + green: 0, + blue: 0, + }; + + construct.apply_modifiers(&player_colours); + + assert!(construct.red_power.value == construct.red_power.base + construct.red_power.base.pct(35)); + assert!(construct.green_power.value == construct.green_power.base + construct.green_power.base.pct(25)); + assert!(construct.blue_power.value == construct.blue_power.base + construct.blue_power.base.pct(25)); + + return; + } + +} diff --git a/core/src/effect.rs b/core/src/effect.rs new file mode 100644 index 00000000..e1a87604 --- /dev/null +++ b/core/src/effect.rs @@ -0,0 +1,209 @@ +use construct::{Stat, EffectMeta}; +use skill::{Skill}; +use util::{IntPct}; + +pub type Cooldown = Option; + +#[derive(Debug,Clone,Copy,PartialEq,Serialize,Deserialize)] +pub enum Effect { + Amplify, + Banish, + Block, + Buff, + Counter, + Curse, + Haste, + Hybrid, + Intercept, + Invert, + Pure, + Purge, + Reflect, + Restrict, + Silence, + Slow, + Stun, + Sustain, + Vulnerable, + Wither, // Reduce green dmg (healing) taken + + // electric is the buff that applies + // electrocute the dmg debuff + Electric, + Electrocute, + + // absorbtion is the buff + // absorb is the increased damage + Absorb, + Absorption, + + // magic immunity + + // effects over time + Triage, + Decay, + Regen, + Siphon, + + // Airborne, + // Boost + // Bleed, + // Blind, + // Deadly, + // Enslave, + // Fury, + // Injured, + // Leech, + // Mesmerise, + // Untouchable, + // SpeedSiphon, + // SpeedIncrease, + + Ko, +} + +impl Effect { + pub fn immune(&self, skill: Skill) -> bool { + match self { + Effect::Banish => true, + Effect::Sustain => [ + Skill::Stun, + Skill::Silence, + Skill::SilencePlus, + Skill::SilencePlusPlus, + Skill::Ruin, + Skill::RuinPlus, + Skill::RuinPlusPlus, + Skill::Restrict, + Skill::RestrictPlus, + Skill::RestrictPlusPlus + ].contains(&skill), + _ => false, + } + } + + pub fn disables_skill(&self, skill: Skill) -> bool { + if skill.is_tick() { + return false; + } + + match self { + Effect::Stun => true, + Effect::Banish => true, + Effect::Silence => skill.colours().contains(&Colour::Blue), + Effect::Restrict => skill.colours().contains(&Colour::Red), + Effect::Purge => skill.colours().contains(&Colour::Green), + Effect::Ko => skill.ko_castable(), + _ => false, + } + } + + pub fn modifications(&self) -> Vec { + match self { + // Bases + Effect::Block => vec![Stat::RedDamageTaken, Stat::BlueDamageTaken], + Effect::Buff => vec![Stat::BluePower, Stat::RedPower, Stat::Speed], + Effect::Slow => vec![Stat::Speed], + + // Power changes + Effect::Absorption => vec![Stat::RedPower, Stat::BluePower], + Effect::Amplify => vec![Stat::RedPower, Stat::BluePower], + Effect::Hybrid => vec![Stat::GreenPower], + + // Damage taken changes + Effect::Curse => vec![Stat::RedDamageTaken, Stat::BlueDamageTaken], + Effect::Pure => vec![Stat::GreenDamageTaken], // increased green taken + Effect::Vulnerable => vec![Stat::RedDamageTaken], + Effect::Wither => vec![Stat::GreenDamageTaken], // reduced green taken + + // Speed + Effect::Haste => vec![Stat::Speed], + + _ => vec![], + } + } + + pub fn apply(&self, value: u64, meta: Option) -> u64 { + match self { + Effect::Amplify | + Effect::Vulnerable | + Effect::Block | + Effect::Buff | + Effect::Curse | + Effect::Haste | + Effect::Slow | + Effect::Hybrid | + Effect::Pure | + Effect::Wither => value.pct(match meta { + Some(EffectMeta::Multiplier(d)) => d, + _ => 100, + }), + + Effect::Absorption => value + match meta { + Some(EffectMeta::AddedDamage(d)) => d, + _ => { + warn!("absorb meta not damage"); + return 0; + } + }, + + _ => { + warn!("{:?} does not have a mod effect", self); + return value; + }, + } + } + + pub fn colour(&self) -> Option { + match self { + // physical + Effect::Stun => Some(Colour::Red), + Effect::Block => Some(Colour::Green), + Effect::Buff => Some(Colour::Green), + Effect::Counter => Some(Colour::Green), + Effect::Vulnerable => Some(Colour::Red), + Effect::Restrict => Some(Colour::Red), + Effect::Sustain => Some(Colour::Green), + Effect::Intercept => Some(Colour::Green), + + // magic + Effect::Curse => Some(Colour::Blue), + Effect::Banish => None, + // Effect::Banish => rng.gen_bool(0.5), + + Effect::Slow => Some(Colour::Blue), + Effect::Haste => Some(Colour::Green), + Effect::Absorption => Some(Colour::Green), + Effect::Reflect => Some(Colour::Green), + Effect::Amplify => Some(Colour::Green), + Effect::Silence => Some(Colour::Blue), + Effect::Wither => Some(Colour::Blue), + Effect::Purge => Some(Colour::Blue), + + Effect::Electric => Some(Colour::Green), + Effect::Electrocute => Some(Colour::Blue), + + Effect::Absorb => Some(Colour::Green), + + // magic + Effect::Hybrid => Some(Colour::Green), + Effect::Invert => Some(Colour::Green), + + // effects over time + Effect::Triage => Some(Colour::Green), + Effect::Decay => Some(Colour::Blue), + Effect::Regen => Some(Colour::Green), + Effect::Siphon => Some(Colour::Blue), + Effect::Pure => Some(Colour::Green), + + Effect::Ko => None, + } + } +} + +#[derive(Debug,Clone,Copy,PartialEq,Serialize,Deserialize)] +pub enum Colour { + Red, + Blue, + Green, +} diff --git a/core/src/game.rs b/core/src/game.rs new file mode 100644 index 00000000..818e5643 --- /dev/null +++ b/core/src/game.rs @@ -0,0 +1,1315 @@ +use rand::prelude::*; +use uuid::Uuid; + +// timekeeping +use chrono::prelude::*; +use chrono::Duration; + +// Db Commons +use failure::Error; +use failure::err_msg; + +use construct::{Construct}; +use skill::{Skill, Cast, Resolution, Event, resolve}; +use effect::{Effect}; +use player::{Player}; +use instance::{TimeControl}; + +#[derive(Debug,Clone,Copy,PartialEq,Serialize,Deserialize)] +pub enum Phase { + Start, + Skill, + Resolve, + Finished, +} + +#[derive(Debug,Clone,Serialize,Deserialize)] +pub struct Game { + pub id: Uuid, + pub player_constructs: usize, + pub player_num: usize, + pub players: Vec, + pub phase: Phase, + pub stack: Vec, + pub resolved: Vec, + pub instance: Option, + time_control: TimeControl, + phase_start: DateTime, + phase_end: Option>, +} + +impl Game { + pub fn new() -> Game { + return Game { + id: Uuid::new_v4(), + player_constructs: 0, + player_num: 0, + players: vec![], + phase: Phase::Start, + stack: vec![], + resolved: vec![], + instance: None, + time_control: TimeControl::Standard, + phase_end: None, + phase_start: Utc::now(), + }; + } + + pub fn redact(mut self, account: Uuid) -> Game { + self.players = self.players.into_iter() + .map(|p| p.redact(account)) + .collect(); + + self.stack + .retain(|s| s.source_player_id == account); + + self + } + + pub fn set_time_control(&mut self, tc: TimeControl) -> &mut Game { + self.time_control = tc; + self.phase_end = Some(tc.lobby_timeout()); + self + } + + pub fn set_player_num(&mut self, size: usize) -> &mut Game { + self.player_num = size; + self + } + + pub fn set_player_constructs(&mut self, size: usize) -> &mut Game { + self.player_constructs = size; + self + } + + pub fn set_instance(&mut self, id: Uuid) -> &mut Game { + self.instance = Some(id); + self + } + + pub fn player_add(&mut self, mut player: Player) -> Result<&mut Game, Error> { + if self.players.len() == self.player_num { + return Err(err_msg("maximum number of players")); + } + + if self.players.iter().any(|t| t.id == player.id) { + return Err(err_msg("player already in game")); + } + + if player.constructs.iter().all(|c| c.skills.len() == 0) { + info!("WARNING: {:?} has no skills and has forfeited {:?}", player.name, self.id); + // self.log.push(format!("{:} has forfeited the game", player.name)); + player.forfeit(); + } + + // let player_description = player.constructs.iter().map(|c| c.name.clone()).collect::>().join(", "); + // self.log.push(format!("{:} has joined the game. [{:}]", player.name, player_description)); + + player.constructs.sort_unstable_by_key(|c| c.id); + self.players.push(player); + + Ok(self) + } + + // handle missing player properly + pub fn player_by_id(&mut self, id: Uuid) -> Result<&mut Player, Error> { + self.players + .iter_mut() + .find(|t| t.id == id) + .ok_or(format_err!("{:?} not in game", id)) + } + + pub fn construct_by_id(&mut self, id: Uuid) -> Option<&mut Construct> { + match self.players.iter_mut().find(|t| t.constructs.iter().any(|c| c.id == id)) { + Some(player) => player.constructs.iter_mut().find(|c| c.id == id), + None => None, + } + } + + fn all_constructs(&self) -> Vec { + self.players.clone() + .into_iter() + .flat_map( + |t| t.constructs + .into_iter()) + .collect::>() + } + + pub fn update_construct(&mut self, construct: &mut Construct) -> &mut Game { + match self.players.iter_mut().find(|t| t.constructs.iter().any(|c| c.id == construct.id)) { + Some(player) => { + let index = player.constructs.iter().position(|t| t.id == construct.id).unwrap(); + player.constructs.remove(index); + player.constructs.push(construct.clone()); + player.constructs.sort_unstable_by_key(|c| c.id); + }, + None => panic!("construct not in game"), + }; + + self + } + + pub fn can_start(&self) -> bool { + return self.players.len() == self.player_num + && self.players.iter().all(|t| t.constructs.len() == self.player_constructs) + } + + pub fn start(self) -> Game { + // self.log.push("Game starting...".to_string()); + + // both forfeit ddue to no skills + if self.finished() { + return self.finish(); + } + + self.skill_phase_start(0) + } + + fn skill_phase_start(mut self, resolution_animation_ms: i64) -> Game { + self.phase_start = Utc::now() + .checked_add_signed(Duration::milliseconds(resolution_animation_ms)) + .expect("could not set phase start"); + + self.phase_end = self.time_control.game_phase_end(resolution_animation_ms); + + for player in self.players.iter_mut() { + if player.skills_required() == 0 { + continue; + } + + player.set_ready(false); + + for construct in player.constructs.iter_mut() { + for i in 0..construct.skills.len() { + if let Some(_d) = construct.disabled(construct.skills[i].skill) { + // info!("{:?} disabled {:?}", construct.skills[i].skill, d); + construct.skills[i].disabled = true; + } else { + construct.skills[i].disabled = false; + } + } + } + } + + // self.log.push("".to_string()); + + if ![Phase::Start, Phase::Resolve].contains(&self.phase) { + panic!("game not in Resolve or start phase"); + } + + self.phase = Phase::Skill; + + self.pve_add_skills(); + + if self.skill_phase_finished() { + return self.resolve_phase_start() + } + + self + } + + fn pve_add_skills(&mut self) -> &mut Game { + let mut pve_skills = vec![]; + let mut rng = thread_rng(); + + for mobs in self.players + .iter() + .filter(|t| t.bot) { + let player_player = self.players.iter().find(|t| t.id != mobs.id).unwrap(); + + for mob in mobs.constructs.iter() { + let skill = mob.mob_select_skill(); + // info!("{:?} {:?}", mob.name, skill); + match skill { + Some(s) => { + // the mut marks it as being able to be called + // more than once + let mut find_target = || { + match s.defensive() { + true => &mobs.constructs[rng.gen_range(0, mobs.constructs.len())], + false => &player_player.constructs[rng.gen_range(0, player_player.constructs.len())], + } + }; + + let mut target = find_target(); + + while target.is_ko() { + target = find_target(); + } + + pve_skills.push((mobs.id, mob.id, target.id, s)); + }, + None => continue, + }; + } + } + + for (player_id, mob_id, target_id, s) in pve_skills { + match self.add_skill(player_id, mob_id, target_id, s) { + Ok(_) => (), + Err(e) => { + info!("{:?}", self.construct_by_id(mob_id)); + panic!("{:?} unable to add pve mob skill {:?}", e, s); + }, + } + + self.player_ready(player_id).unwrap(); + } + + self + } + + fn add_skill(&mut self, player_id: Uuid, source_construct_id: Uuid, target_construct_id: Uuid, skill: Skill) -> Result<&mut Game, Error> { + // check player in game + self.player_by_id(player_id)?; + + if self.phase != Phase::Skill { + return Err(err_msg("game not in skill phase")); + } + + // target checks + { + let target = match self.construct_by_id(target_construct_id) { + Some(c) => c, + None => return Err(err_msg("target construct not in game")), + }; + + // fixme for rez + if target.is_ko() { + return Err(err_msg("target construct is ko")); + } + } + + // construct checks + { + let construct = match self.construct_by_id(source_construct_id) { + Some(c) => c, + None => return Err(err_msg("construct not in game")), + }; + + if construct.is_ko() { + return Err(err_msg("construct is ko")); + } + + // check the construct has the skill + if !construct.knows(skill) { + return Err(err_msg("construct does not have that skill")); + } + + if construct.skill_on_cd(skill).is_some() { + return Err(err_msg("abiltity on cooldown")); + } + + // check here as well so uncastable spells don't go on the stack + if let Some(disable) = construct.disabled(skill) { + return Err(format_err!("skill disabled {:?}", disable)); + } + } + + // replace construct skill + if let Some(s) = self.stack.iter_mut().position(|s| s.source_construct_id == source_construct_id) { + self.stack.remove(s); + } + + let skill = Cast::new(source_construct_id, player_id, target_construct_id, skill); + self.stack.push(skill); + + return Ok(self); + } + + fn offer_draw(mut self, player_id: Uuid) -> Result { + if self.phase != Phase::Skill { + return Err(err_msg("game not in skill phase")); + } + + { + let player = self.player_by_id(player_id)?; + player.draw_offered = true; + } + + // bots automatically accept draws + for player in self.players.iter_mut() { + if player.bot { + player.draw_offered = true; + } + } + + if self.players.iter().all(|p| p.draw_offered) { + return Ok(self.finish()); + } + + return Ok(self); + } + + fn concede(mut self, player_id: Uuid) -> Result { + if self.phase != Phase::Skill { + return Err(err_msg("game not in skill phase")); + } + + self.player_by_id(player_id)? + .forfeit(); + + return Ok(self.finish()); + } + + + fn clear_skill(&mut self, player_id: Uuid) -> Result<&mut Game, Error> { + self.player_by_id(player_id)?; + if self.phase != Phase::Skill { + return Err(err_msg("game not in skill phase")); + } + let mut game_state = self.clone(); + self.stack.retain(|s| game_state.construct_by_id(s.source_construct_id).unwrap().account != player_id); + + return Ok(self); + } + + fn player_ready(&mut self, player_id: Uuid) -> Result<&mut Game, Error> { + if self.phase != Phase::Skill { + return Err(err_msg("game not in skill phase")); + } + + self.player_by_id(player_id)? + .set_ready(true); + + Ok(self) + } + + fn skill_phase_finished(&self) -> bool { + self.players.iter().all(|t| t.ready) + // self.players.iter() + // // for every player + // .all(|t| self.stack.iter() + // // the number of skills they have cast + // .filter(|s| s.source_player_id == t.id).collect::>() + // // should equal the number required this turn + // .len() == t.skills_required() + // ) + } + + // requires no input + // just do it + fn resolve_phase_start(mut self) -> Game { + if self.phase != Phase::Skill { + panic!("game not in skill phase"); + } + assert!(self.skill_phase_finished()); + + self.phase = Phase::Resolve; + // self.log.push("".to_string()); + + self.resolve_skills() + } + + fn stack_sort_speed(&mut self) -> &mut Game { + let mut sorted = self.stack.clone(); + sorted.iter_mut() + .for_each(|s| { + if !s.skill.is_tick() { + let caster = self.construct_by_id(s.source_construct_id).unwrap(); + let speed = caster.skill_speed(s.skill); + s.speed = speed; + } + }); + + sorted.sort_unstable_by_key(|s| s.speed); + + self.stack = sorted; + + self + } + + fn construct_aoe_targets(&self, construct_id: Uuid) -> Vec { + self.players.iter() + .find(|t| t.constructs.iter().any(|c| c.id == construct_id)) + .unwrap() + .constructs + .iter() + .map(|c| c.id) + .collect() + } + + pub fn get_targets(&self, skill: Skill, source: &Construct, target_construct_id: Uuid) -> Vec { + let target_player = self.players.iter() + .find(|t| t.constructs.iter().any(|c| c.id == target_construct_id)) + .unwrap(); + + if let Some(t) = target_player.intercepting() { + return vec![t.id]; + } + + match source.skill_is_aoe(skill) { + true => self.construct_aoe_targets(target_construct_id), + false => vec![target_construct_id], + } + } + + fn resolve_skills(mut self) -> Game { + if self.phase != Phase::Resolve { + panic!("game not in Resolve phase"); + } + + // find their statuses with ticks + let mut ticks = self.all_constructs() + .iter() + .flat_map( + |c| c.effects + .iter() + .cloned() + .filter_map(|e| e.tick)) + .collect::>(); + + // add them to the stack + self.stack.append(&mut ticks); + + self.stack_sort_speed(); + + // 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 r_animation_ms = 0; + while let Some(cast) = self.stack.pop() { + // info!("{:} casts ", cast); + + let mut resolutions = resolve(&cast, &mut self); + r_animation_ms = resolutions.iter().fold(r_animation_ms, |acc, r| acc + r.clone().get_delay()); + + + // the cast itself goes into this temp vec to handle cooldowns + // if theres no resolution events, the skill didn't trigger (disable etc) + if resolutions.len() > 0 { + casts.push(cast); + } + + self.resolved.append(&mut resolutions); + + // while let Some(resolution) = resolutions.pop() { + // self.log_resolution(cast.speed, &resolution); + // // the results go into the resolutions + // self.resolved.push(resolution); + // } + + // sort the stack again in case speeds have changed + self.stack_sort_speed(); + }; + + // info!("{:#?}", self.casts); + + // handle cooldowns and statuses + self.progress_durations(&casts); + + if self.finished() { + return self.finish() + } + + self.skill_phase_start(r_animation_ms) + } + + fn progress_durations(&mut self, resolved: &Vec) -> &mut Game { + for mut construct in self.all_constructs() { + // info!("progressing durations for {:}", construct.name); + + if construct.is_ko() { + continue; + } + + // only reduce cooldowns if no cd was used + { + if let Some(skill) = resolved.iter() + .filter(|s| s.source_construct_id == construct.id) + .find(|s| s.used_cooldown()) { + construct.skill_set_cd(skill.skill); + } else { + construct.reduce_cooldowns(); + } + } + + // always reduce durations + construct.reduce_effect_durations(); + self.update_construct(&mut construct); + } + + self + } + + // fn log_resolution(&mut self, speed: u64, resolution: &Resolution) -> &mut Game { + // let Resolution { source, target, event, stages: _ } = resolution; + // match event { + // Event::Ko { skill: _ }=> + // self.log.push(format!("{:} KO!", target.name)), + + // Event::Disable { skill, disable } => + // self.log.push(format!("{:} {:?} {:} disabled {:?}", + // source.name, skill, target.name, disable)), + + // Event::Immunity { skill, immunity } => + // self.log.push(format!("[{:}] {:} {:?} {:} immune {:?}", + // speed, source.name, skill, target.name, immunity)), + + // Event::TargetKo { skill } => + // self.log.push(format!("[{:}] {:} {:?} {:} - target is KO", + // speed, source.name, skill, target.name)), + + // Event::Damage { skill, amount, mitigation, colour: _ } => + // self.log.push(format!("[{:}] {:} {:?} {:} {:} ({:} mitigated)", + // speed, source.name, skill, target.name, amount, mitigation)), + + // Event::Healing { skill, amount, overhealing } => + // self.log.push(format!("[{:}] {:} {:?} {:} {:} healing ({:}OH)", + // speed, source.name, skill, target.name, amount, overhealing)), + + // Event::Inversion { skill } => + // self.log.push(format!("[{:}] {:} {:?} {:} INVERTED", + // speed, source.name, skill, target.name)), + + // Event::Reflection { skill } => + // self.log.push(format!("[{:}] {:} {:?} {:} REFLECTED", + // speed, source.name, skill, target.name)), + + // Event::Effect { skill, effect, duration, construct_effects: _ } => + // self.log.push(format!("[{:}] {:} {:?} {:} {:?} {:}T", + // speed, source.name, skill, target.name, effect, duration)), + + // Event::Skill { skill } => + // self.log.push(format!("[{:}] {:} {:?} {:}", + // speed, source.name, skill, target.name)), + + // Event::Removal { effect, construct_effects: _ } => + // self.log.push(format!("[{:}] {:?} removed {:} {:?}", + // speed, source.name, target.name, effect)), + + // Event::Recharge { skill, red, blue } => + // self.log.push(format!("[{:}] {:} {:?} {:} {:}R {:}B", + // speed, source.name, skill, target.name, red, blue)), + + // Event::Evasion { skill, evasion_rating } => + // self.log.push(format!("[{:}] {:} {:?} {:} evaded ({:}%)", + // speed, source.name, skill, target.name, evasion_rating)), + + // Event::Incomplete => panic!("incomplete resolution {:?}", resolution), + // } + + // self + // } + + pub fn finished(&self) -> bool { + self.phase == Phase::Finished || self.players.iter().any(|t| t.constructs.iter().all(|c| c.is_ko())) + } + + pub fn winner(&self) -> Option<&Player> { + match self.players.iter().any(|t| t.constructs.iter().all(|c| c.is_ko())) { + true => self.players.iter().find(|t| t.constructs.iter().any(|c| !c.is_ko())), + false => None, + } + } + + fn finish(mut self) -> Game { + self.phase = Phase::Finished; + // self.log.push(format!("Game finished.")); + + // { + // let winner = self.players.iter().find(|t| t.constructs.iter().any(|c| !c.is_ko())); + // match winner { + // Some(w) => self.log.push(format!("Winner: {:}", w.name)), + // None => self.log.push(format!("Game was drawn.")), + // }; + // } + + self + } + + fn phase_timed_out(&self) -> bool { + match self.phase_end { + Some(t) => Utc::now().signed_duration_since(t).num_milliseconds() > 0, + None => false, + } + } + + pub fn upkeep(mut self) -> Game { + if self.phase == Phase::Finished { + return self; + } + + if !self.phase_timed_out() { + return self; + } + + info!("upkeep game: {:} vs {:}", self.players[0].name, self.players[1].name); + + for player in self.players.iter_mut() { + if !player.ready { + player.set_ready(true); + // player.add_warning(); + // info!("upkeep: {:} warned", player.name); + // if player.warnings >= 3 { + // player.forfeit(); + // info!("upkeep: {:} forfeited", player.name); + // //todo + // // self.resolved.push(forfeit) + // // self.log.push(format!("{:} forfeited.", player.name)); + // } + } + } + + self = self.resolve_phase_start(); + self + } +} + +#[cfg(test)] +mod tests { + use game::*; + use construct::*; + use util::IntPct; + + fn create_test_game() -> Game { + let mut x = Construct::new() + .named(&"pronounced \"creeep\"".to_string()) + .learn(Skill::Attack) + .learn(Skill::Stun) + .learn(Skill::Attack) + .learn(Skill::Block) + .learn(Skill::Counter) + .learn(Skill::Siphon) + .learn(Skill::Amplify) + .learn(Skill::Stun) + .learn(Skill::Block) + .learn(Skill::Sleep) + .learn(Skill::Decay); + + let mut y = Construct::new() + .named(&"lemongrass tea".to_string()) + .learn(Skill::Attack) + .learn(Skill::Stun) + .learn(Skill::Attack) + .learn(Skill::Block) + .learn(Skill::Counter) + .learn(Skill::Siphon) + .learn(Skill::Amplify) + .learn(Skill::Stun) + .learn(Skill::Block); + + let mut game = Game::new(); + + game + .set_player_num(2) + .set_player_constructs(1); + + let x_player_id = Uuid::new_v4(); + x.account = x_player_id; + let x_player = Player::new(x_player_id, &"ntr".to_string(), vec![x]); + + let y_player_id = Uuid::new_v4(); + y.account = y_player_id; + let y_player = Player::new(y_player_id, &"mash".to_string(), vec![y]); + + game + .player_add(x_player).unwrap() + .player_add(y_player).unwrap(); + + assert!(game.can_start()); + + return game.start(); + } + + fn create_2v2_test_game() -> Game { + let mut i = Construct::new() + .named(&"pretaliate".to_string()) + .learn(Skill::Attack) + .learn(Skill::Attack); + + let mut j = Construct::new() + .named(&"poy sian".to_string()) + .learn(Skill::Attack) + .learn(Skill::Attack); + + let mut x = Construct::new() + .named(&"pronounced \"creeep\"".to_string()) + .learn(Skill::Attack) + .learn(Skill::Attack); + + let mut y = Construct::new() + .named(&"lemongrass tea".to_string()) + .learn(Skill::Attack) + .learn(Skill::Attack); + + let mut game = Game::new(); + + game + .set_player_num(2) + .set_player_constructs(2); + + let i_player_id = Uuid::new_v4(); + i.account = i_player_id; + j.account = i_player_id; + let i_player = Player::new(i_player_id, &"ntr".to_string(), vec![i, j]); + + let x_player_id = Uuid::new_v4(); + x.account = x_player_id; + y.account = x_player_id; + let x_player = Player::new(x_player_id, &"mashy".to_string(), vec![x, y]); + + game + .player_add(i_player).unwrap() + .player_add(x_player).unwrap(); + + assert!(game.can_start()); + + return game.start(); + } + + #[test] + fn phase_test() { + let mut game = create_test_game(); + + let x_player = game.players[0].clone(); + let y_player = game.players[1].clone(); + + let x_construct = x_player.constructs[0].clone(); + let y_construct = y_player.constructs[0].clone(); + + game.add_skill(x_player.id, x_construct.id, y_construct.id, Skill::Attack).unwrap(); + game.add_skill(y_player.id, y_construct.id, x_construct.id, Skill::Attack).unwrap(); + + game.player_ready(x_player.id).unwrap(); + game.player_ready(y_player.id).unwrap(); + + assert!(game.skill_phase_finished()); + + game = game.resolve_phase_start(); + + assert!([Phase::Skill, Phase::Finished].contains(&game.phase)); + + return; + } + + #[test] + fn stun_test() { + let mut game = create_test_game(); + + let x_player = game.players[0].clone(); + let y_player = game.players[1].clone(); + + let x_construct = x_player.constructs[0].clone(); + let y_construct = y_player.constructs[0].clone(); + + while game.construct_by_id(x_construct.id).unwrap().skill_on_cd(Skill::Stun).is_some() { + game.construct_by_id(x_construct.id).unwrap().reduce_cooldowns(); + } + + game.add_skill(x_player.id, x_construct.id, y_construct.id, Skill::Stun).unwrap(); + game.add_skill(y_player.id, y_construct.id, x_construct.id, Skill::Attack).unwrap(); + + game.player_ready(x_player.id).unwrap(); + game.player_ready(y_player.id).unwrap(); + + assert!(game.skill_phase_finished()); + game = game.resolve_phase_start(); + + // should auto progress back to skill phase + assert!(game.phase == Phase::Skill); + + // assert!(game.player_by_id(y_player.id).constructs[0].is_stunned()); + // assert!(game.player_by_id(y_player.id).skills_required() == 0); + } + + #[test] + fn ko_resolution_test() { + let mut game = create_test_game(); + + let x_player = game.players[0].clone(); + let y_player = game.players[1].clone(); + + let x_construct = x_player.constructs[0].clone(); + let y_construct = y_player.constructs[0].clone(); + + game.player_by_id(y_player.id).unwrap().construct_by_id(y_construct.id).unwrap().red_power.force(1000000000); + game.player_by_id(y_player.id).unwrap().construct_by_id(y_construct.id).unwrap().speed.force(1000000000); + + while game.construct_by_id(x_construct.id).unwrap().skill_on_cd(Skill::Stun).is_some() { + game.construct_by_id(x_construct.id).unwrap().reduce_cooldowns(); + } + + // just in case + // remove all mitigation + game.player_by_id(x_player.id).unwrap().construct_by_id(x_construct.id).unwrap().red_life.force(0); + + game.add_skill(x_player.id, x_construct.id, y_construct.id, Skill::Stun).unwrap(); + game.add_skill(y_player.id, y_construct.id, x_construct.id, Skill::Attack).unwrap(); + + game.player_ready(x_player.id).unwrap(); + game.player_ready(y_player.id).unwrap(); + + assert!(game.skill_phase_finished()); + game = game.resolve_phase_start(); + + assert!(!game.player_by_id(y_player.id).unwrap().constructs[0].is_stunned()); + assert!(game.phase == Phase::Finished); + } + + #[test] + fn cooldown_test() { + let mut game = create_test_game(); + + let x_player = game.players[0].clone(); + let y_player = game.players[1].clone(); + + let x_construct = x_player.constructs[0].clone(); + let y_construct = y_player.constructs[0].clone(); + + // should auto progress back to skill phase + assert!(game.phase == Phase::Skill); + + assert!(game.player_by_id(y_player.id).unwrap().constructs[0].skill_on_cd(Skill::Block).is_none()); + assert!(game.player_by_id(y_player.id).unwrap().constructs[0].skill_on_cd(Skill::Stun).is_some()); + assert!(game.player_by_id(x_player.id).unwrap().constructs[0].skill_on_cd(Skill::Block).is_none()); + + game.add_skill(x_player.id, x_construct.id, y_construct.id, Skill::Attack).unwrap(); + game.add_skill(y_player.id, y_construct.id, x_construct.id, Skill::Attack).unwrap(); + + game.player_ready(x_player.id).unwrap(); + game.player_ready(y_player.id).unwrap(); + + game = game.resolve_phase_start(); + + // should auto progress back to skill phase + assert!(game.phase == Phase::Skill); + assert!(game.player_by_id(y_player.id).unwrap().constructs[0].skill_on_cd(Skill::Stun).is_some()); + + // second round + // now we block and it should go back on cd + // game.add_skill(x_player.id, x_construct.id, y_construct.id, Skill::Stun).unwrap(); + game.add_skill(y_player.id, y_construct.id, x_construct.id, Skill::Attack).unwrap(); + + game.player_ready(x_player.id).unwrap(); + game.player_ready(y_player.id).unwrap(); + + game = game.resolve_phase_start(); + + assert!(game.player_by_id(x_player.id).unwrap().constructs[0].skill_on_cd(Skill::Stun).is_none()); + assert!(game.player_by_id(y_player.id).unwrap().constructs[0].skill_on_cd(Skill::Block).is_none()); + } + + #[test] + fn sleep_cooldown_test() { + let mut game = create_test_game(); + + let x_player = game.players[0].clone(); + let y_player = game.players[1].clone(); + + let x_construct = x_player.constructs[0].clone(); + let y_construct = y_player.constructs[0].clone(); + + + for _n in 1..10 { + // should auto progress back to skill phase + assert!(game.phase == Phase::Skill); + + // Sleep 2T CD + assert!(game.player_by_id(x_player.id).unwrap().constructs[0].skill_on_cd(Skill::Decay).is_none()); + assert!(game.player_by_id(x_player.id).unwrap().constructs[0].skill_on_cd(Skill::Sleep).is_some()); + + game.player_ready(x_player.id).unwrap(); + game.player_ready(y_player.id).unwrap(); + game = game.resolve_phase_start(); + + // Sleep 1T CD + assert!(game.player_by_id(x_player.id).unwrap().constructs[0].skill_on_cd(Skill::Decay).is_none()); + assert!(game.player_by_id(x_player.id).unwrap().constructs[0].skill_on_cd(Skill::Sleep).is_some()); + + game.add_skill(x_player.id, x_construct.id, y_construct.id, Skill::Decay).unwrap(); + // game.add_skill(x_player.id, x_construct.id, y_construct.id, Skill::Attack).unwrap(); + game.player_ready(x_player.id).unwrap(); + game.player_ready(y_player.id).unwrap(); + game = game.resolve_phase_start(); + + // Sleep 0T CD (we use it here) + assert!(game.player_by_id(x_player.id).unwrap().constructs[0].skill_on_cd(Skill::Decay).is_none()); + assert!(game.player_by_id(x_player.id).unwrap().constructs[0].skill_on_cd(Skill::Sleep).is_none()); + + game.add_skill(x_player.id, x_construct.id, y_construct.id, Skill::Sleep).unwrap(); + game.player_ready(x_player.id).unwrap(); + game.player_ready(y_player.id).unwrap(); + game = game.resolve_phase_start(); + + // Sleep back to 2T CD + assert!(game.player_by_id(x_player.id).unwrap().constructs[0].skill_on_cd(Skill::Decay).is_none()); + assert!(game.player_by_id(x_player.id).unwrap().constructs[0].skill_on_cd(Skill::Sleep).is_some()); + } + + } + + #[test] + fn counter_test() { + let mut game = create_test_game(); + + let x_player = game.players[0].clone(); + let y_player = game.players[1].clone(); + + let x_construct = x_player.constructs[0].clone(); + let y_construct = y_player.constructs[0].clone(); + + while game.construct_by_id(y_construct.id).unwrap().skill_on_cd(Skill::Stun).is_some() { + game.construct_by_id(y_construct.id).unwrap().reduce_cooldowns(); + } + + while game.construct_by_id(x_construct.id).unwrap().skill_on_cd(Skill::Counter).is_some() { + game.construct_by_id(x_construct.id).unwrap().reduce_cooldowns(); + } + + game.add_skill(x_player.id, x_construct.id, x_construct.id, Skill::Counter).unwrap(); + game.add_skill(y_player.id, y_construct.id, x_construct.id, Skill::Attack).unwrap(); + + game.player_ready(x_player.id).unwrap(); + game.player_ready(y_player.id).unwrap(); + + game = game.resolve_phase_start(); + + // don't get stunned but not really stunning ¯\_(ツ)_/¯ + assert!(game.player_by_id(x_player.id).unwrap().constructs[0].is_stunned() == false); + // riposte + assert_eq!(game.player_by_id(y_player.id).unwrap().constructs[0].green_life(), ( + y_construct.green_life() + y_construct.red_life() - x_construct.red_power().pct(Skill::CounterAttack.multiplier()))); + } + + #[test] + fn electrify_test() { + let mut game = create_test_game(); + + let x_player = game.players[0].clone(); + let y_player = game.players[1].clone(); + + let x_construct = x_player.constructs[0].clone(); + let y_construct = y_player.constructs[0].clone(); + + // one shot the target construct (should still get debuffed) + game.player_by_id(y_player.id).unwrap().construct_by_id(y_construct.id).unwrap().red_power.force(1000000000); + + game.construct_by_id(x_construct.id).unwrap().learn_mut(Skill::Electrify); + + while game.construct_by_id(x_construct.id).unwrap().skill_on_cd(Skill::Electrify).is_some() { + game.construct_by_id(x_construct.id).unwrap().reduce_cooldowns(); + } + + // apply buff + game.add_skill(x_player.id, x_construct.id, x_construct.id, Skill::Electrify).unwrap(); + game.player_ready(x_player.id).unwrap(); + game.player_ready(y_player.id).unwrap(); + // game = game.resolve_phase_start(); + // assert!(game.construct_by_id(x_construct.id).unwrap().affected(Effect::Electric)); + + // attack and receive debuff + game.add_skill(y_player.id, y_construct.id, x_construct.id, Skill::Attack).unwrap(); + game.player_ready(x_player.id).unwrap(); + game.player_ready(y_player.id).unwrap(); + game = game.resolve_phase_start(); + + assert!(game.construct_by_id(y_construct.id).unwrap().affected(Effect::Electrocute)); + } + + // #[test] + // fn link_test() { + // let mut game = create_test_game(); + + // let x_player = game.players[0].clone(); + // let y_player = game.players[1].clone(); + + // let x_construct = x_player.constructs[0].clone(); + // let y_construct = y_player.constructs[0].clone(); + + // game.construct_by_id(x_construct.id).unwrap().learn_mut(Skill::Link); + + // while game.construct_by_id(x_construct.id).unwrap().skill_on_cd(Skill::Link).is_some() { + // game.construct_by_id(x_construct.id).unwrap().reduce_cooldowns(); + // } + + // // apply buff + // game.add_skill(x_player.id, x_construct.id, y_construct.id, Skill::Link).unwrap(); + // game.player_ready(x_player.id).unwrap(); + // game.player_ready(y_player.id).unwrap(); + // game = game.resolve_phase_start(); + // assert!(game.construct_by_id(x_construct.id).unwrap().affected(Effect::Link)); + + // let Resolution { source: _, target: _, event, stages: _ } = game.resolved.pop().unwrap(); + // match event { + // Event::Effect { effect, skill: _, duration: _, construct_effects: _ } => assert_eq!(effect, Effect::Link), + // _ => panic!("not siphon"), + // }; + + // let Resolution { source: _, target: _, event, stages: _ } = game.resolved.pop().unwrap(); + // match event { + // Event::Recharge { red: _, blue: _, skill: _ } => (), + // _ => panic!("link result was not recharge"), + // } + + // // attack and receive link hit + // game.add_skill(y_player.id, y_construct.id, x_construct.id, Skill::Attack).unwrap(); + // game.player_ready(x_player.id).unwrap(); + // game.player_ready(y_player.id).unwrap(); + // game = game.resolve_phase_start(); + + // let Resolution { source: _, target, event, stages: _ } = game.resolved.pop().unwrap(); + // assert_eq!(target.id, y_construct.id); + // match event { + // Event::Damage { amount, skill: _, mitigation: _, colour: _} => + // assert_eq!(amount, x_construct.red_power().pct(Skill::Attack.multiplier()) >> 1), + // _ => panic!("not damage link"), + // }; + // } + + // #[test] + // fn absorb_test() { + // let mut game = create_test_game(); + + // let x_player = game.players[0].clone(); + // let y_player = game.players[1].clone(); + + // let x_construct = x_player.constructs[0].clone(); + // let y_construct = y_player.constructs[0].clone(); + + // game.construct_by_id(x_construct.id).unwrap().learn_mut(Skill::Absorb); + + // while game.construct_by_id(x_construct.id).unwrap().skill_on_cd(Skill::Absorb).is_some() { + // game.construct_by_id(x_construct.id).unwrap().reduce_cooldowns(); + // } + + // // apply buff + // game.add_skill(x_player.id, x_construct.id, x_construct.id, Skill::Absorb).unwrap(); + // game.player_ready(x_player.id).unwrap(); + // game.player_ready(y_player.id).unwrap(); + // game = game.resolve_phase_start(); + // assert!(game.construct_by_id(x_construct.id).unwrap().affected(Effect::Absorb)); + + // // attack and receive debuff + // game.add_skill(y_player.id, y_construct.id, x_construct.id, Skill::TestAttack).unwrap(); + // game.player_ready(x_player.id).unwrap(); + // game.player_ready(y_player.id).unwrap(); + // game = game.resolve_phase_start(); + + // info!("{:#?}", game); + // assert!(game.construct_by_id(y_construct.id).unwrap().affected(Effect::Absorption)); + // } + + #[test] + fn aoe_test() { + let mut game = create_2v2_test_game(); + + let i_player = game.players[0].clone(); + let x_player = game.players[1].clone(); + + let i_construct = i_player.constructs[0].clone(); + let j_construct = i_player.constructs[1].clone(); + let x_construct = x_player.constructs[0].clone(); + let y_construct = x_player.constructs[1].clone(); + + game.construct_by_id(x_construct.id).unwrap().learn_mut(Skill::Ruin); + + while game.construct_by_id(x_construct.id).unwrap().skill_on_cd(Skill::Ruin).is_some() { + game.construct_by_id(x_construct.id).unwrap().reduce_cooldowns(); + } + + game.add_skill(i_player.id, i_construct.id, x_construct.id, Skill::Attack).unwrap(); + game.add_skill(i_player.id, j_construct.id, x_construct.id, Skill::Attack).unwrap(); + game.add_skill(x_player.id, x_construct.id, i_construct.id, Skill::Ruin).unwrap(); + game.add_skill(x_player.id, y_construct.id, i_construct.id, Skill::Attack).unwrap(); + + game.player_ready(i_player.id).unwrap(); + game.player_ready(x_player.id).unwrap(); + + assert!(game.skill_phase_finished()); + game = game.resolve_phase_start(); + let ruins = game.resolved + .into_iter() + .filter(|r| { + let Resolution { source, target: _, event, stages: _ } = r; + match source.id == x_construct.id { + true => match event { + Event::Effect { effect, duration, skill: _, construct_effects: _ } => { + assert!(*effect == Effect::Stun); + assert!(*duration == 1); + true + }, + Event::AoeSkill { skill: _ } => false, + Event::Damage { amount: _, mitigation: _, colour: _, skill: _ } => false, + _ => panic!("ruin result not effect {:?}", event), + } + false => false, + } + }) + .count(); + + assert!(ruins == 2); + } + + #[test] + fn intercept_test() { + let mut game = create_2v2_test_game(); + + let i_player = game.players[0].clone(); + let x_player = game.players[1].clone(); + + let i_construct = i_player.constructs[0].clone(); + let j_construct = i_player.constructs[1].clone(); + let x_construct = x_player.constructs[0].clone(); + let y_construct = x_player.constructs[1].clone(); + + game.construct_by_id(x_construct.id).unwrap().learn_mut(Skill::Intercept); + + while game.construct_by_id(x_construct.id).unwrap().skill_on_cd(Skill::Intercept).is_some() { + game.construct_by_id(x_construct.id).unwrap().reduce_cooldowns(); + } + + game.add_skill(i_player.id, i_construct.id, x_construct.id, Skill::Attack).unwrap(); + game.add_skill(i_player.id, j_construct.id, x_construct.id, Skill::Attack).unwrap(); + game.add_skill(x_player.id, x_construct.id, i_construct.id, Skill::Intercept).unwrap(); + game.add_skill(x_player.id, y_construct.id, i_construct.id, Skill::Attack).unwrap(); + + game.player_ready(i_player.id).unwrap(); + game.player_ready(x_player.id).unwrap(); + + game = game.resolve_phase_start(); + + assert!(game.resolved.len() == 4); + while let Some(r) = game.resolved.pop() { + let Resolution { source , target, event: _, stages: _ } = r; + if [i_construct.id, j_construct.id].contains(&source.id) { + assert!(target.id == x_construct.id); + } + } + } + + #[test] + fn ko_pve_test() { + let mut game = create_2v2_test_game(); + + let i_player = game.players[0].clone(); + let x_player = game.players[1].clone(); + + let i_construct = i_player.constructs[0].clone(); + let j_construct = i_player.constructs[1].clone(); + let x_construct = x_player.constructs[0].clone(); + let y_construct = x_player.constructs[1].clone(); + + game.add_skill(i_player.id, i_construct.id, x_construct.id, Skill::Attack).unwrap() + .add_skill(i_player.id, j_construct.id, x_construct.id, Skill::Attack).unwrap() + .add_skill(x_player.id, x_construct.id, i_construct.id, Skill::Attack).unwrap() + .add_skill(x_player.id, y_construct.id, i_construct.id, Skill::Attack).unwrap() + .player_ready(i_player.id).unwrap() + .player_ready(x_player.id).unwrap(); + + assert!(game.skill_phase_finished()); + game = game.resolve_phase_start(); + + assert!([Phase::Skill, Phase::Finished].contains(&game.phase)); + + // kill a construct + game.player_by_id(i_player.id).unwrap().construct_by_id(i_construct.id).unwrap().green_life.reduce(u64::max_value()); + + assert!(game.player_by_id(i_player.id).unwrap().skills_required() == 1); + assert!(game.player_by_id(x_player.id).unwrap().skills_required() == 2); + + // add some more skills + game.add_skill(i_player.id, j_construct.id, x_construct.id, Skill::Attack).unwrap(); + game.add_skill(x_player.id, x_construct.id, j_construct.id, Skill::Attack).unwrap(); + game.add_skill(x_player.id, y_construct.id, j_construct.id, Skill::Attack).unwrap(); + assert!(game.add_skill(x_player.id, x_construct.id, i_construct.id, Skill::Attack).is_err()); + + game.player_ready(i_player.id).unwrap(); + game.player_ready(x_player.id).unwrap(); + + assert!(game.skill_phase_finished()); + game = game.resolve_phase_start(); + + assert!(game.player_by_id(i_player.id).unwrap().skills_required() == 1); + assert!(game.player_by_id(x_player.id).unwrap().skills_required() == 2); + return; + } + + #[test] + fn tick_removal_test() { + let mut game = create_test_game(); + + let x_player = game.players[0].clone(); + let y_player = game.players[1].clone(); + + let x_construct = x_player.constructs[0].clone(); + let y_construct = y_player.constructs[0].clone(); + + // make the purify construct super fast so it beats out decay + game.construct_by_id(y_construct.id).unwrap().speed.force(10000000); + + game.construct_by_id(x_construct.id).unwrap().learn_mut(Skill::Decay); + while game.construct_by_id(x_construct.id).unwrap().skill_on_cd(Skill::Decay).is_some() { + game.construct_by_id(x_construct.id).unwrap().reduce_cooldowns(); + } + + game.construct_by_id(x_construct.id).unwrap().learn_mut(Skill::Siphon); + while game.construct_by_id(x_construct.id).unwrap().skill_on_cd(Skill::Siphon).is_some() { + game.construct_by_id(x_construct.id).unwrap().reduce_cooldowns(); + } + + game.construct_by_id(y_construct.id).unwrap().learn_mut(Skill::Purify); + while game.construct_by_id(y_construct.id).unwrap().skill_on_cd(Skill::Purify).is_some() { + game.construct_by_id(y_construct.id).unwrap().reduce_cooldowns(); + } + + // apply buff + game.add_skill(x_player.id, x_construct.id, y_construct.id, Skill::Decay).unwrap(); + game.player_ready(x_player.id).unwrap(); + game.player_ready(y_player.id).unwrap(); + game = game.resolve_phase_start(); + assert!(game.construct_by_id(y_construct.id).unwrap().affected(Effect::Decay)); + + let Resolution { source: _, target: _, event, stages: _ } = game.resolved.pop().unwrap(); + match event { + Event::Damage { amount: _, skill, mitigation: _, colour: _ } => assert_eq!(skill, Skill::DecayTick), + _ => panic!("not decay"), + }; + + game.resolved.clear(); + + // remove + game.add_skill(y_player.id, y_construct.id, y_construct.id, Skill::Purify).unwrap(); + game.player_ready(x_player.id).unwrap(); + game.player_ready(y_player.id).unwrap(); + game = game.resolve_phase_start(); + + while let Some(Resolution { source: _, target: _, event, stages: _ }) = game.resolved.pop() { + match event { + Event::Damage { amount: _, skill: _, mitigation: _, colour: _ } => + panic!("{:?} damage event", event), + _ => (), + } + }; + + game.add_skill(y_player.id, x_construct.id, y_construct.id, Skill::Siphon).unwrap(); + game.player_ready(x_player.id).unwrap(); + game.player_ready(y_player.id).unwrap(); + game = game.resolve_phase_start(); + + game.resolved.clear(); + + game.add_skill(y_player.id, y_construct.id, y_construct.id, Skill::Purify).unwrap(); + game.player_ready(x_player.id).unwrap(); + game.player_ready(y_player.id).unwrap(); + game = game.resolve_phase_start(); + + while let Some(Resolution { source: _, target: _, event, stages: _ }) = game.resolved.pop() { + match event { + Event::Damage { amount: _, skill: _, mitigation: _, colour: _ } => + panic!("{:#?} {:#?} damage event", game.resolved, event), + _ => (), + } + }; + + } + + #[test] + fn upkeep_test() { + let mut game = create_2v2_test_game(); + game.players[0].set_ready(true); + game.phase_end = Some(Utc::now().checked_sub_signed(Duration::seconds(500)).unwrap()); + game = game.upkeep(); + // assert!(game.players[1].warnings == 1); + } +} diff --git a/core/src/instance.rs b/core/src/instance.rs new file mode 100644 index 00000000..1923172f --- /dev/null +++ b/core/src/instance.rs @@ -0,0 +1,631 @@ + +use std::collections::{HashMap}; + +use uuid::Uuid; + +use failure::Error; +use failure::err_msg; + +// timekeeping +use chrono::prelude::*; +use chrono::Duration; + +use player::{Player, Score}; +use mob::{bot_player, instance_mobs}; +use game::{Game}; +use item::{Item}; +use vbox; + + +#[derive(Debug,Clone,Copy,PartialEq,Serialize,Deserialize)] +enum InstancePhase { + Lobby, + InProgress, + Finished, +} + +pub type ChatState = HashMap; + +#[derive(Debug,Clone,Serialize,Deserialize)] +struct Round { + game_id: Option, + finished: bool, +} + +impl Round { + fn new() -> Round { + Round { game_id: None, finished: false } + } +} + +#[derive(Debug,Clone,Copy,Serialize,Deserialize)] +pub enum TimeControl { + Standard, + Slow, + Practice, +} + +impl TimeControl { + fn vbox_time_seconds(&self) -> i64 { + match self { + TimeControl::Standard => 180, + TimeControl::Slow => 240, + TimeControl::Practice => panic!("practice vbox seconds called"), + } + } + + fn game_time_seconds(&self) -> i64 { + match self { + TimeControl::Standard => 60, + TimeControl::Slow => 120, + TimeControl::Practice => panic!("practice game seconds called"), + } + } + + pub fn vbox_phase_end(&self) -> Option> { + match self { + TimeControl::Practice => None, + _ => Some(Utc::now() + .checked_add_signed(Duration::seconds(self.vbox_time_seconds())) + .expect("could not set vbox phase end")), + } + } + + pub fn lobby_timeout(&self) -> DateTime { + Utc::now() + .checked_add_signed(Duration::seconds(15)) + .expect("could not set phase end") + } + + pub fn game_phase_end(&self, resolution_time_ms: i64) -> Option> { + match self { + TimeControl::Practice => None, + _ => Some(Utc::now() + .checked_add_signed(Duration::milliseconds(self.game_time_seconds() * 1000 + resolution_time_ms)) + .expect("could not set game phase end")), + } + } +} + +#[derive(Debug,Clone,Serialize,Deserialize)] +pub struct Instance { + pub id: Uuid, + pub name: String, + + players: Vec, + rounds: Vec, + + max_players: usize, + time_control: TimeControl, + + phase: InstancePhase, + phase_end: Option>, + phase_start: DateTime, + + winner: Option, +} + +impl Instance { + fn new() -> Instance { + Instance { + id: Uuid::new_v4(), + players: vec![], + rounds: vec![], + phase: InstancePhase::Lobby, + max_players: 2, + name: String::new(), + time_control: TimeControl::Standard, + phase_start: Utc::now(), + phase_end: Some(TimeControl::Standard.lobby_timeout()), + winner: None, + } + } + + pub fn redact(mut self, account: Uuid) -> Instance { + self.players = self.players.into_iter() + .map(|p| p.redact(account)) + .collect(); + + self + } + + fn phase_timed_out(&self) -> bool { + match self.phase_end { + Some(t) => Utc::now().signed_duration_since(t).num_milliseconds() > 0, + None => false, + } + } + + fn timed_out_players(&self) -> Vec { + self.players + .iter() + .filter(|p| !p.ready) + .map(|p| p.id) + .collect::>() + } + + pub fn upkeep(mut self) -> (Instance, Option) { + // time out lobbies that have been open too long + if self.phase == InstancePhase::Lobby && self.phase_timed_out() { + self.finish(); + return (self, None); + } + + if self.phase != InstancePhase::InProgress { + return (self, None); + } + + if !self.phase_timed_out() { + return (self, None); + } + + let new_game = self + .timed_out_players() + .iter() + .filter_map(|p| self.player_ready(*p).unwrap()) + .collect::>() + .into_iter() + .next(); + + (self, new_game) + } + + fn set_name(mut self, name: String) -> Result { + if name.len() == 0 { + return Err(err_msg("name must have a length")); + } + + self.name = name; + Ok(self) + } + + fn set_time_control(mut self, tc: TimeControl) -> Instance { + self.time_control = tc; + self + } + + fn add_player(&mut self, player: Player) -> Result<&mut Instance, Error> { + if self.players.len() >= self.max_players { + return Err(err_msg("game full")) + } + + match self.players.iter().find(|p| p.id == player.id) { + Some(_p) => return Err(err_msg("already joined")), + None => (), + }; + + self.players.push(player); + Ok(self) + } + + fn player_ready(&mut self, player_id: Uuid) -> Result, Error> { + if ![InstancePhase::InProgress, InstancePhase::Lobby].contains(&self.phase) { + return Err(err_msg("instance not in start or vbox phase")); + } + + // LOBBY CHECKS + if self.phase == InstancePhase::Lobby { + let i = self.players + .iter_mut() + .position(|p| p.id == player_id) + .ok_or(err_msg("player_id not found"))?; + + let v = !self.players[i].ready; + self.players[i].set_ready(v); + + match self.can_start() { + true => { + self.start(); + return Ok(None); + } + false => return Ok(None), + }; + } + + // GAME PHASE READY + let i = self.players + .iter_mut() + .position(|p| p.id == player_id) + .ok_or(err_msg("player_id not found"))?; + + let v = !self.players[i].ready; + self.players[i].set_ready(v); + + // start the game even if afk noobs have no skills + if !self.phase_timed_out() && self.players[i].constructs.iter().all(|c| c.skills.len() == 0) { + return Err(err_msg("your constructs have no skills")); + } + + // create a game object if both players are ready + // this should only happen once + + let all_ready = self.round_ready_check(); + + if !all_ready { + return Ok(None); + } + + let game = self.create_round_game(); + + let current_round = self.rounds + .last_mut() + .expect("instance does not have any rounds"); + + current_round.game_id = Some(game.id); + + return Ok(Some(game)); + + } + + fn round_ready_check(&mut self) -> bool { + self.players + .iter() + .all(|p| p.ready) + } + + // maybe just embed the games in the instance + // but seems hella inefficient + fn create_round_game(&mut self) -> Game { + let current_round = self.rounds + .last_mut() + .expect("instance does not have any rounds"); + + + let mut game = Game::new(); + current_round.game_id = Some(game.id); + + // disable upkeep until players finish their game + self.phase_end = None; + + game + .set_player_num(2) + .set_player_constructs(3) + .set_time_control(self.time_control) + .set_instance(self.id); + + for player in self.players.clone().into_iter() { + game.player_add(player).unwrap(); + } + + assert!(game.can_start()); + return game.start(); + } + + fn can_start(&self) -> bool { + self.players.len() == self.max_players && self.all_ready() + } + + fn start(&mut self) -> &mut Instance { + // self.players.sort_unstable_by_key(|p| p.id); + self.next_round() + } + + fn next_round(&mut self) -> &mut Instance { + if self.finish_condition() { + return self.finish(); + } + + self.phase = InstancePhase::InProgress; + self.phase_start = Utc::now(); + self.phase_end = self.time_control.vbox_phase_end(); + + let bits = match self.rounds.len() > 0 { + true => 30, + false => 0, + }; + + self.players.iter_mut().for_each(|p| { + p.vbox.balance_add(bits); + p.set_ready(false); + p.vbox.fill(); + }); + + self.rounds.push(Round::new()); + self.bot_round_actions(); + + self + } + + fn finish_condition(&mut self) -> bool { + self.players.iter().any(|p| p.score == Score::Win) + } + + pub fn finish(&mut self) -> &mut Instance { + self.phase = InstancePhase::Finished; + + for player in self.players.iter() { + if player.score == Score::Win { + self.winner = Some(player.id); + } + } + + self + } + + fn finished(&self) -> bool { + self.phase == InstancePhase::Finished + } + + fn bot_round_actions(&mut self) -> &mut Instance { + for bot in self.players.iter_mut().filter(|p| p.bot) { + bot.vbox.fill(); + bot.autobuy(); + } + + let games = self.players + .clone() + .iter() + .filter(|b| b.bot) + .filter_map(|b| self.player_ready(b.id).unwrap()) + .collect::>(); + + for game in games { + if game.finished() { + self.game_finished(&game).unwrap(); + } else { + info!("{:?} unfishededes", game); + } + } + + self + } + + fn current_game_id(&self) -> Option { + if self.phase != InstancePhase::InProgress { + return None; + } + + let current_round = self.rounds + .last() + .expect("instance does not have any rounds"); + + if current_round.finished || current_round.game_id.is_none() { + return None; + } + + return current_round.game_id; + } + + fn game_finished(&mut self, game: &Game) -> Result<&mut Instance, Error> { + { + let current_round = self.rounds + .iter_mut() + .filter(|r| r.game_id.is_some()) + .find(|r| r.game_id.unwrap() == game.id); + + match current_round { + Some(c) => c.finished = true, + None => return Err(err_msg("instance does not have a round for this game")), + }; + } + + // if you don't win, you lose + // ties can happen if both players agree to a draw + // or ticks fire and knock everybody out + if let Some(winner) = game.winner() { + let winner = self.players.iter_mut() + .find(|p| p.id == winner.id) + .unwrap(); + winner.score = winner.score.add_win(&Score::Zero); + }; + + if self.all_games_finished() { + self.next_round(); + } + + Ok(self) + } + + fn all_ready(&self) -> bool { + self.players.iter().all(|p| p.ready) + } + + fn all_games_finished(&self) -> bool { + match self.rounds.last() { + Some(r) => r.finished, + None => true, + } + } + + // PLAYER ACTIONS + fn account_player(&mut self, account: Uuid) -> Result<&mut Player, Error> { + self.players + .iter_mut() + .find(|p| p.id == account) + .ok_or(err_msg("account not in instance")) + } + + fn account_opponent(&mut self, account: Uuid) -> Result<&mut Player, Error> { + self.players + .iter_mut() + .find(|p| p.id != account) + .ok_or(err_msg("opponent not in instance")) + } + + pub fn vbox_action_allowed(&self, account: Uuid) -> Result<(), Error> { + if self.players.iter().find(|p| p.id == account).is_none() { + return Err(err_msg("player not in this instance")); + } + + if self.phase == InstancePhase::Lobby { + return Err(err_msg("game not yet started")); + } + + if self.current_game_id().is_some() { + return Err(err_msg("you cannot perform vbox actions while in a game")); + } + + Ok(()) + } + + pub fn vbox_refill(mut self, account: Uuid) -> Result { + self.vbox_action_allowed(account)?; + self.account_player(account)? + .vbox_refill()?; + Ok(self) + } + + pub fn vbox_buy(mut self, account: Uuid, group: vbox::ItemType, index: String, construct_id: Option) -> Result { + self.vbox_action_allowed(account)?; + self.account_player(account)? + .vbox_buy(group, index, construct_id)?; + Ok(self) + } + + pub fn vbox_combine(mut self, account: Uuid, inv_indices: Vec, vbox_indices: vbox::VboxIndices) -> Result { + self.vbox_action_allowed(account)?; + self.account_player(account)? + .vbox_combine(inv_indices, vbox_indices)?; + Ok(self) + } + + pub fn vbox_refund(mut self, account: Uuid, index: String) -> Result { + self.vbox_action_allowed(account)?; + self.account_player(account)? + .vbox_refund(index)?; + Ok(self) + } + + pub fn vbox_apply(mut self, account: Uuid, index: String, construct_id: Uuid) -> Result { + self.vbox_action_allowed(account)?; + self.account_player(account)? + .vbox_equip(index, construct_id)?; + Ok(self) + } + + pub fn vbox_unequip(mut self, account: Uuid, target: Item, construct_id: Uuid, target_construct_id: Option) -> Result { + self.vbox_action_allowed(account)?; + self.account_player(account)? + .vbox_unequip(target, construct_id, target_construct_id)?; + Ok(self) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn instance_pve_test() { + let mut instance = Instance::new(); + + let bot = bot_player(); + let bot_one = bot.id; + instance.add_player(bot).unwrap(); + + let bot = bot_player(); + let bot_two = bot.id; + instance.add_player(bot).unwrap(); + + assert_eq!(instance.phase, InstancePhase::Lobby); + instance.player_ready(bot_one).unwrap(); + instance.player_ready(bot_two).unwrap(); + + assert_eq!(instance.phase, InstancePhase::Finished); + } + + #[test] + fn instance_bot_vbox_test() { + let _instance = Instance::new(); + let player_account = Uuid::new_v4(); + let constructs = instance_mobs(player_account); + let _player = Player::new(player_account, &"test".to_string(), constructs).set_bot(true); + } + + #[test] + fn instance_start_test() { + let mut instance = Instance::new(); + + assert_eq!(instance.max_players, 2); + + let player_account = Uuid::new_v4(); + let constructs = instance_mobs(player_account); + let player = Player::new(player_account, &"a".to_string(), constructs); + let a_id = player.id; + + instance.add_player(player).expect("could not add player"); + assert!(!instance.can_start()); + + let player_account = Uuid::new_v4(); + let constructs = instance_mobs(player_account); + let player = Player::new(player_account, &"b".to_string(), constructs); + let b_id = player.id; + + instance.add_player(player).expect("could not add player"); + + assert_eq!(instance.phase, InstancePhase::Lobby); + instance.player_ready(a_id).expect("a ready"); + assert!(!instance.can_start()); + + instance.player_ready(b_id).expect("b ready"); + assert_eq!(instance.phase, InstancePhase::InProgress); + + assert!(!instance.can_start()); + + instance.players[0].autobuy(); + instance.players[1].autobuy(); + + instance.player_ready(a_id).expect("a ready"); + let game = instance.player_ready(b_id).expect("b ready"); + + assert!(game.is_some()); + } + + #[test] + fn instance_upkeep_test() { + let mut instance = Instance::new(); + + let player_account = Uuid::new_v4(); + let constructs = instance_mobs(player_account); + let player = Player::new(player_account, &"a".to_string(), constructs); + let a_id = player.id; + + instance.add_player(player).expect("could not add player"); + assert!(!instance.can_start()); + + let player_account = Uuid::new_v4(); + let constructs = instance_mobs(player_account); + let player = Player::new(player_account, &"b".to_string(), constructs); + let b_id = player.id; + instance.add_player(player).expect("could not add player"); + + instance.players[0].autobuy(); + + instance.player_ready(a_id).expect("a ready"); + instance.player_ready(b_id).expect("b ready"); + + instance.phase_end = Some(Utc::now().checked_sub_signed(Duration::seconds(500)).unwrap()); + + let (mut instance, new_games) = instance.upkeep(); + + assert!(new_games.is_some()); + + let game = new_games.unwrap(); + assert!(game.finished()); + + instance.game_finished(&game).unwrap(); + + assert_eq!(instance.rounds.len(), 2); + assert!(instance.players.iter().all(|p| !p.ready)); + + // info!("{:#?}", instance); + } + + #[test] + fn instance_upkeep_idle_lobby_test() { + let mut instance = Instance::new(); + + let player_account = Uuid::new_v4(); + let constructs = instance_mobs(player_account); + let player = Player::new(player_account, &"a".to_string(), constructs); + let _a_id = player.id; + + instance.add_player(player).expect("could not add player"); + assert!(!instance.can_start()); + + instance.phase_end = Some(Utc::now().checked_sub_signed(Duration::minutes(61)).unwrap()); + let (instance, _new_games) = instance.upkeep(); + + assert!(instance.finished()); + } +} diff --git a/core/src/item.rs b/core/src/item.rs new file mode 100644 index 00000000..72639073 --- /dev/null +++ b/core/src/item.rs @@ -0,0 +1,1581 @@ +use skill::{Skill}; +use spec::{Spec, SpecValues}; +use construct::{Colours}; +use effect::{Colour, Cooldown}; + +#[derive(Debug,Copy,Clone,Serialize,Deserialize,PartialEq,PartialOrd,Ord,Eq)] +pub enum Item { + // colours + Blue, + Green, + Red, + + // base skills + Attack, + Block, + Stun, + Buff, + Debuff, + + // specs + // Base + Power, + Life, + Speed, + + // Lifes Upgrades + LifeGG, + LifeRR, + LifeBB, + LifeRG, + LifeGB, + LifeRB, + LifeGGPlus, + LifeRRPlus, + LifeBBPlus, + LifeRGPlus, + LifeGBPlus, + LifeRBPlus, + LifeGGPlusPlus, + LifeRRPlusPlus, + LifeBBPlusPlus, + LifeRGPlusPlus, + LifeGBPlusPlus, + LifeRBPlusPlus, + + // Power Upgrades + PowerGG, + PowerRR, + PowerBB, + PowerRG, + PowerGB, + PowerRB, + PowerGGPlus, + PowerRRPlus, + PowerBBPlus, + PowerRGPlus, + PowerGBPlus, + PowerRBPlus, + PowerGGPlusPlus, + PowerRRPlusPlus, + PowerBBPlusPlus, + PowerRGPlusPlus, + PowerGBPlusPlus, + PowerRBPlusPlus, + + // Speed Upgrades + SpeedGG, + SpeedRR, + SpeedBB, + SpeedRG, + SpeedGB, + SpeedRB, + SpeedGGPlus, + SpeedRRPlus, + SpeedBBPlus, + SpeedRGPlus, + SpeedGBPlus, + SpeedRBPlus, + SpeedGGPlusPlus, + SpeedRRPlusPlus, + SpeedBBPlusPlus, + SpeedRGPlusPlus, + SpeedGBPlusPlus, + SpeedRBPlusPlus, + + Amplify, + #[serde(rename = "Amplify+")] + AmplifyPlus, + #[serde(rename = "Amplify++")] + AmplifyPlusPlus, + + Absorb, + #[serde(rename = "Absorb+")] + AbsorbPlus, + #[serde(rename = "Absorb++")] + AbsorbPlusPlus, + + Banish, + #[serde(rename = "Banish+")] + BanishPlus, + #[serde(rename = "Banish++")] + BanishPlusPlus, + + Bash, + #[serde(rename = "Bash+")] + BashPlus, + #[serde(rename = "Bash++")] + BashPlusPlus, + + Blast, + #[serde(rename = "Blast+")] + BlastPlus, + #[serde(rename = "Blast++")] + BlastPlusPlus, + + Chaos, + #[serde(rename = "Chaos+")] + ChaosPlus, + #[serde(rename = "Chaos++")] + ChaosPlusPlus, + + Sustain, + #[serde(rename = "Sustain+")] + SustainPlus, + #[serde(rename = "Sustain++")] + SustainPlusPlus, + + Electrify, + #[serde(rename = "Electrify+")] + ElectrifyPlus, + #[serde(rename = "Electrify++")] + ElectrifyPlusPlus, + + Curse, + #[serde(rename = "Curse+")] + CursePlus, + #[serde(rename = "Curse++")] + CursePlusPlus, + + Decay, + #[serde(rename = "Decay+")] + DecayPlus, + #[serde(rename = "Decay++")] + DecayPlusPlus, + + Haste, + #[serde(rename = "Haste+")] + HastePlus, + #[serde(rename = "Haste++")] + HastePlusPlus, + + Heal, + #[serde(rename = "Heal+")] + HealPlus, + #[serde(rename = "Heal++")] + HealPlusPlus, + + Hybrid, + #[serde(rename = "Hybrid+")] + HybridPlus, + #[serde(rename = "Hybrid++")] + HybridPlusPlus, + + Invert, + #[serde(rename = "Invert+")] + InvertPlus, + #[serde(rename = "Invert++")] + InvertPlusPlus, + + Counter, + #[serde(rename = "Counter+")] + CounterPlus, + #[serde(rename = "Counter++")] + CounterPlusPlus, + + Purge, + #[serde(rename = "Purge+")] + PurgePlus, + #[serde(rename = "Purge++")] + PurgePlusPlus, + + Purify, + #[serde(rename = "Purify+")] + PurifyPlus, + #[serde(rename = "Purify++")] + PurifyPlusPlus, + + Reflect, + #[serde(rename = "Reflect+")] + ReflectPlus, + #[serde(rename = "Reflect++")] + ReflectPlusPlus, + + Recharge, + #[serde(rename = "Recharge+")] + RechargePlus, + #[serde(rename = "Recharge++")] + RechargePlusPlus, + + Ruin, + #[serde(rename = "Ruin+")] + RuinPlus, + #[serde(rename = "Ruin++")] + RuinPlusPlus, + + Link, + #[serde(rename = "Link+")] + LinkPlus, + #[serde(rename = "Link++")] + LinkPlusPlus, + + Silence, + #[serde(rename = "Silence+")] + SilencePlus, + #[serde(rename = "Silence++")] + SilencePlusPlus, + + Slay, + #[serde(rename = "Slay+")] + SlayPlus, + #[serde(rename = "Slay++")] + SlayPlusPlus, + + Sleep, + #[serde(rename = "Sleep+")] + SleepPlus, + #[serde(rename = "Sleep++")] + SleepPlusPlus, + + Restrict, + #[serde(rename = "Restrict+")] + RestrictPlus, + #[serde(rename = "Restrict++")] + RestrictPlusPlus, + + Strike, + #[serde(rename = "Strike+")] + StrikePlus, + #[serde(rename = "Strike++")] + StrikePlusPlus, + + Siphon, + #[serde(rename = "Siphon+")] + SiphonPlus, + #[serde(rename = "Siphon++")] + SiphonPlusPlus, + + Intercept, + #[serde(rename = "Intercept+")] + InterceptPlus, + #[serde(rename = "Intercept++")] + InterceptPlusPlus, + + Break, + #[serde(rename = "Break+")] + BreakPlus, + #[serde(rename = "Break++")] + BreakPlusPlus, + + Triage, + #[serde(rename = "Triage+")] + TriagePlus, + #[serde(rename = "Triage++")] + TriagePlusPlus, +} + +pub enum ItemEffect { + Skill, + Spec, +} + +impl Item { + pub fn colours(&self, count: &mut Colours) { + let combos = get_combos(); + let combo = combos.iter().find(|c| c.item == *self); + match combo { + Some(c) => c.components.iter().for_each(|unit| match unit { + Item::Red => count.red += 1, + Item::Blue => count.blue += 1, + Item::Green => count.green += 1, + _ => { + let mut combo_count = Colours::new(); + unit.colours(&mut combo_count); + count.red += combo_count.red; + count.blue += combo_count.blue; + count.green += combo_count.green; + } + }), + None => (), + } + } + + pub fn components(&self) -> Vec { + let combos = get_combos(); + let combo = combos.iter().find(|c| c.item == *self); + + match combo { + Some(c) => c.components.iter().flat_map(|c| c.components()).collect::>(), + None => vec![*self], + } + } + + pub fn cost(&self) -> usize { + match self { + Item::Red => 1, + Item::Green => 1, + Item::Blue => 1, + + Item::Attack => 2, + Item::Block => 2, + Item::Buff => 2, + Item::Debuff => 2, + Item::Stun => 2, + + Item::Power => 3, + Item::Life => 3, + Item::Speed => 3, + + _ => { + let combos = get_combos(); + let combo = combos.iter().find(|c| c.item == *self) + .unwrap_or_else(|| panic!("unable to find components for {:?}", self)); + return combo.components.iter().fold(0, |acc, c| { + match c { + Item::Attack | + Item::Block | + Item::Buff | + Item::Debuff | + Item::Stun => acc, + Item::Power | + Item::Life | + Item::Speed => acc + 1, + _ => acc + c.cost(), + } + }); + }, + } + } + + pub fn base_speed(&self) -> u64 { + match self { + Item::Attack => 1, + Item::Stun => 2, + Item::Block => 3, + Item::Buff | + Item::Debuff => 4, + Item::Blue => 1, + Item::Green => 2, + Item::Red => 3, + _ => 0, + } + } + + pub fn speed(&self) -> u64 { + match self { + Item::Attack | + Item::Stun | + Item::Block | + Item::Buff | + Item::Debuff => 24 + self.base_speed(), + _ => { + let combos = get_combos(); + let combo = combos.iter().find(|c| c.item == *self) + .unwrap_or_else(|| panic!("unable to find components for {:?}", self)); + + let mut colour_speed = 0; + let mut skill_speed = 0; + let mut component_speed = 0; + + combo.components.iter().for_each(|unit| { + colour_speed += match unit { + Item::Red | + Item::Green | + Item::Blue => unit.base_speed(), + _ => 0, + }; + skill_speed += match unit { + Item::Attack | + Item::Stun | + Item::Block | + Item::Buff | + Item::Debuff => unit.base_speed(), + _ => 0, + }; + if colour_speed == 0 && skill_speed == 0 { + component_speed = unit.speed(); + } + }); + if component_speed > 0 { return component_speed }; + return 24 + colour_speed * skill_speed + } + } + + } + + pub fn effect(&self) -> Option { + if let Some(_skill) = self.into_skill() { + return Some(ItemEffect::Skill); + } + if let Some(_spec) = self.into_spec() { + return Some(ItemEffect::Spec); + } + return None; + } + + pub fn into_skill(&self) -> Option { + match self { + Item::Absorb => Some(Skill::Absorb), + Item::AbsorbPlus => Some(Skill::AbsorbPlus), + Item::AbsorbPlusPlus => Some(Skill::AbsorbPlusPlus), + Item::Amplify => Some(Skill::Amplify), + Item::AmplifyPlus => Some(Skill::AmplifyPlus), + Item::AmplifyPlusPlus => Some(Skill::AmplifyPlusPlus), + Item::Attack => Some(Skill::Attack), + Item::Banish => Some(Skill::Banish), + Item::BanishPlus => Some(Skill::BanishPlus), + Item::BanishPlusPlus => Some(Skill::BanishPlusPlus), + Item::Bash => Some(Skill::Bash), + Item::BashPlus => Some(Skill::BashPlus), + Item::BashPlusPlus => Some(Skill::BashPlusPlus), + Item::Blast => Some(Skill::Blast), + Item::BlastPlus => Some(Skill::BlastPlus), + Item::BlastPlusPlus => Some(Skill::BlastPlusPlus), + Item::Block => Some(Skill::Block), + Item::Buff => Some(Skill::Buff), + Item::Chaos => Some(Skill::Chaos), + Item::ChaosPlus => Some(Skill::ChaosPlus), + Item::ChaosPlusPlus => Some(Skill::ChaosPlusPlus), + Item::Counter => Some(Skill::Counter), + Item::CounterPlus => Some(Skill::CounterPlus), + Item::CounterPlusPlus => Some(Skill::CounterPlusPlus), + Item::Curse => Some(Skill::Curse), + Item::CursePlus => Some(Skill::CursePlus), + Item::CursePlusPlus => Some(Skill::CursePlusPlus), + Item::Debuff => Some(Skill::Debuff), + Item::Decay => Some(Skill::Decay), + Item::DecayPlus => Some(Skill::DecayPlus), + Item::DecayPlusPlus => Some(Skill::DecayPlusPlus), + Item::Electrify => Some(Skill::Electrify), + Item::ElectrifyPlus => Some(Skill::ElectrifyPlus), + Item::ElectrifyPlusPlus => Some(Skill::ElectrifyPlusPlus), + Item::Haste => Some(Skill::Haste), + Item::HastePlus => Some(Skill::HastePlus), + Item::HastePlusPlus => Some(Skill::HastePlusPlus), + Item::Heal => Some(Skill::Heal), + Item::HealPlus => Some(Skill::HealPlus), + Item::HealPlusPlus => Some(Skill::HealPlusPlus), + Item::Hybrid => Some(Skill::Hybrid), + Item::HybridPlus => Some(Skill::HybridPlus), + Item::HybridPlusPlus => Some(Skill::HybridPlusPlus), + Item::Intercept => Some(Skill::Intercept), + Item::InterceptPlus => Some(Skill::InterceptPlus), + Item::InterceptPlusPlus => Some(Skill::InterceptPlusPlus), + Item::Invert => Some(Skill::Invert), + Item::InvertPlus => Some(Skill::InvertPlus), + Item::InvertPlusPlus => Some(Skill::InvertPlusPlus), + Item::Purge => Some(Skill::Purge), + Item::PurgePlus => Some(Skill::PurgePlus), + Item::PurgePlusPlus => Some(Skill::PurgePlusPlus), + Item::Purify => Some(Skill::Purify), + Item::PurifyPlus => Some(Skill::PurifyPlus), + Item::PurifyPlusPlus => Some(Skill::PurifyPlusPlus), + Item::Recharge => Some(Skill::Recharge), + Item::RechargePlus => Some(Skill::RechargePlus), + Item::RechargePlusPlus => Some(Skill::RechargePlusPlus), + Item::Reflect => Some(Skill::Reflect), + Item::ReflectPlus => Some(Skill::ReflectPlus), + Item::ReflectPlusPlus => Some(Skill::ReflectPlusPlus), + Item::Restrict => Some(Skill::Restrict), + Item::RestrictPlus => Some(Skill::RestrictPlus), + Item::RestrictPlusPlus => Some(Skill::RestrictPlusPlus), + Item::Ruin => Some(Skill::Ruin), + Item::RuinPlus => Some(Skill::RuinPlus), + Item::RuinPlusPlus => Some(Skill::RuinPlusPlus), + Item::Link => Some(Skill::Link), + Item::LinkPlus => Some(Skill::LinkPlus), + Item::LinkPlusPlus => Some(Skill::LinkPlusPlus), + Item::Silence => Some(Skill::Silence), + Item::SilencePlus => Some(Skill::SilencePlus), + Item::SilencePlusPlus => Some(Skill::SilencePlusPlus), + Item::Siphon => Some(Skill::Siphon), + Item::SiphonPlus => Some(Skill::SiphonPlus), + Item::SiphonPlusPlus => Some(Skill::SiphonPlusPlus), + Item::Slay => Some(Skill::Slay), + Item::SlayPlus => Some(Skill::SlayPlus), + Item::SlayPlusPlus => Some(Skill::SlayPlusPlus), + Item::Sleep => Some(Skill::Sleep), + Item::SleepPlus => Some(Skill::SleepPlus), + Item::SleepPlusPlus => Some(Skill::SleepPlusPlus), + Item::Strike => Some(Skill::Strike), + Item::StrikePlus => Some(Skill::StrikePlus), + Item::StrikePlusPlus => Some(Skill::StrikePlusPlus), + Item::Stun => Some(Skill::Stun), + Item::Sustain => Some(Skill::Sustain), + Item::SustainPlus => Some(Skill::SustainPlus), + Item::SustainPlusPlus => Some(Skill::SustainPlusPlus), + Item::Break => Some(Skill::Break), + Item::BreakPlus => Some(Skill::BreakPlus), + Item::BreakPlusPlus => Some(Skill::BreakPlusPlus), + Item::Triage => Some(Skill::Triage), + Item::TriagePlus => Some(Skill::TriagePlus), + Item::TriagePlusPlus => Some(Skill::TriagePlusPlus), + _ => None, + } + } + + pub fn into_spec(&self) -> Option { + match *self { + Item::Speed => Some(Spec::Speed), + Item::SpeedRR => Some(Spec::SpeedRR), + Item::SpeedBB => Some(Spec::SpeedBB), + Item::SpeedGG => Some(Spec::SpeedGG), + Item::SpeedRG => Some(Spec::SpeedRG), + Item::SpeedGB => Some(Spec::SpeedGB), + Item::SpeedRB => Some(Spec::SpeedRB), + + Item::SpeedRRPlus => Some(Spec::SpeedRRPlus), + Item::SpeedBBPlus => Some(Spec::SpeedBBPlus), + Item::SpeedGGPlus => Some(Spec::SpeedGGPlus), + Item::SpeedRGPlus => Some(Spec::SpeedRGPlus), + Item::SpeedGBPlus => Some(Spec::SpeedGBPlus), + Item::SpeedRBPlus => Some(Spec::SpeedRBPlus), + + Item::SpeedRRPlusPlus => Some(Spec::SpeedRRPlusPlus), + Item::SpeedBBPlusPlus => Some(Spec::SpeedBBPlusPlus), + Item::SpeedGGPlusPlus => Some(Spec::SpeedGGPlusPlus), + Item::SpeedRGPlusPlus => Some(Spec::SpeedRGPlusPlus), + Item::SpeedGBPlusPlus => Some(Spec::SpeedGBPlusPlus), + Item::SpeedRBPlusPlus => Some(Spec::SpeedRBPlusPlus), + + Item::Power => Some(Spec::Power), + Item::PowerRR => Some(Spec::PowerRR), + Item::PowerBB => Some(Spec::PowerBB), + Item::PowerGG => Some(Spec::PowerGG), + Item::PowerRG => Some(Spec::PowerRG), + Item::PowerGB => Some(Spec::PowerGB), + Item::PowerRB => Some(Spec::PowerRB), + Item::PowerRRPlus => Some(Spec::PowerRRPlus), + Item::PowerBBPlus => Some(Spec::PowerBBPlus), + Item::PowerGGPlus => Some(Spec::PowerGGPlus), + Item::PowerRGPlus => Some(Spec::PowerRGPlus), + Item::PowerGBPlus => Some(Spec::PowerGBPlus), + Item::PowerRBPlus => Some(Spec::PowerRBPlus), + Item::PowerRRPlusPlus => Some(Spec::PowerRRPlusPlus), + Item::PowerBBPlusPlus => Some(Spec::PowerBBPlusPlus), + Item::PowerGGPlusPlus => Some(Spec::PowerGGPlusPlus), + Item::PowerRGPlusPlus => Some(Spec::PowerRGPlusPlus), + Item::PowerGBPlusPlus => Some(Spec::PowerGBPlusPlus), + Item::PowerRBPlusPlus => Some(Spec::PowerRBPlusPlus), + + Item::Life => Some(Spec::Life), + Item::LifeRG => Some(Spec::LifeRG), + Item::LifeGB => Some(Spec::LifeGB), + Item::LifeRB => Some(Spec::LifeRB), + Item::LifeGG => Some(Spec::LifeGG), + Item::LifeRR => Some(Spec::LifeRR), + Item::LifeBB => Some(Spec::LifeBB), + Item::LifeRGPlus => Some(Spec::LifeRGPlus), + Item::LifeGBPlus => Some(Spec::LifeGBPlus), + Item::LifeRBPlus => Some(Spec::LifeRBPlus), + Item::LifeGGPlus => Some(Spec::LifeGGPlus), + Item::LifeRRPlus => Some(Spec::LifeRRPlus), + Item::LifeBBPlus => Some(Spec::LifeBBPlus), + Item::LifeRGPlusPlus => Some(Spec::LifeRGPlusPlus), + Item::LifeGBPlusPlus => Some(Spec::LifeGBPlusPlus), + Item::LifeRBPlusPlus => Some(Spec::LifeRBPlusPlus), + Item::LifeGGPlusPlus => Some(Spec::LifeGGPlusPlus), + Item::LifeRRPlusPlus => Some(Spec::LifeRRPlusPlus), + Item::LifeBBPlusPlus => Some(Spec::LifeBBPlusPlus), + + _ => None, + } + } + + pub fn into_colour(&self) -> Colour { + match *self { + Item::Red => Colour::Red, + Item::Green => Colour::Green, + Item::Blue => Colour::Blue, + _ => panic!("{:?} is not a colour", self), + } + } + + pub fn into_description(&self) -> String { + match self { + // colours + Item::Blue => format!("Combine two colours with a white base item to create a new combo. \n Slow speed, magical type. Deterrents and destruction."), + Item::Green => format!("Combine two colours with a white base item to create a new combo.\n Normal speed, healing type. Protection and trickery."), + Item::Red => format!("Combine two colours with a white base item to create a new combo. \n Fast speed, physical type. Chaos and momentum."), + + // base skills + Item::Attack => format!("Deal {:?}% RedPower as red damage.", + self.into_skill().unwrap().multiplier()), + Item::Block => format!("Reduce red damage and blue damage taken by {:?}%. Block lasts {:?}T", + 100 - self.into_skill().unwrap().effect()[0].get_multiplier(), + self.into_skill().unwrap().effect()[0].get_duration()), + + + Item::Stun => format!("Stun target construct for {:?}T.", + self.into_skill().unwrap().effect()[0].get_duration()), + Item::Buff => format!("Increase target construct RedPower BluePower SpeedStat by {:?}%. Buff lasts {:?}T", + self.into_skill().unwrap().effect()[0].get_multiplier() - 100, + self.into_skill().unwrap().effect()[0].get_duration()), + + Item::Debuff => format!("Slows the target reducing SpeedStat by {:?}%. Debuff lasts {:?}T", + 100 - self.into_skill().unwrap().effect()[0].get_multiplier(), + self.into_skill().unwrap().effect()[0].get_duration()), + // specs + // Base + Item::Power => format!("Increases all power stats by {:?}%. + Power determines the base damage and healing of your construct skills.", + self.into_spec().unwrap().values().base()), + Item::Life => format!("Increases construct GreenLife by {:?}. + When your construct reaches 0 GreenLife it is knocked out and cannot cast skills.", + self.into_spec().unwrap().values().base()), + Item::Speed => format!("Increases construct speed by {:?}%. + Speed SpeedStat determines the order in which skills resolve.", + self.into_spec().unwrap().values().base()), + + // Lifes Upgrades + Item::LifeGG | + Item::LifeGGPlus | + Item::LifeGGPlusPlus => format!("Increases construct GreenLife by {:?}. + If your team meets total colour thresholds the spec provides additional bonuses.", + self.into_spec().unwrap().values().base()), + + Item::LifeRR | + Item::LifeRRPlus | + Item::LifeRRPlusPlus => format!("Increases construct RedLife by {:?}. + If your team meets total colour thresholds the spec provides additional bonuses.", + self.into_spec().unwrap().values().base()), + + Item::LifeBB | + Item::LifeBBPlus | + Item::LifeBBPlusPlus => format!("Increases construct BlueLife by {:?}. + If your team meets total colour thresholds the spec provides additional bonuses.", + self.into_spec().unwrap().values().base()), + + Item::LifeRG | + Item::LifeRGPlus | + Item::LifeRGPlusPlus => format!("Increases construct RedLife and GreenLife by {:?}. + If your team meets total colour thresholds the spec provides additional bonuses.", + self.into_spec().unwrap().values().base()), + + Item::LifeGB | + Item::LifeGBPlus | + Item::LifeGBPlusPlus => format!("Increases construct GreenLife and BlueLife by {:?}. + If your team meets total colour thresholds the spec provides additional bonuses.", + self.into_spec().unwrap().values().base()), + + Item::LifeRB | + Item::LifeRBPlus | + Item::LifeRBPlusPlus => format!("Increases construct RedLife and BlueLife by {:?}. + If your team meets total colour thresholds the spec provides additional bonuses.", + self.into_spec().unwrap().values().base()), + + // Power Upgrades + Item::PowerRR | + Item::PowerRRPlus | + Item::PowerRRPlusPlus => format!("Increases construct RedPower by {:?}%. + If your team meets total colour thresholds the spec provides additional bonuses.", + self.into_spec().unwrap().values().base()), + Item::PowerBB | + Item::PowerBBPlus | + Item::PowerBBPlusPlus => format!("Increases construct BluePower by {:?}%. + If your team meets total colour thresholds the spec provides additional bonuses.", + self.into_spec().unwrap().values().base()), + + Item::PowerGG | + Item::PowerGGPlus | + Item::PowerGGPlusPlus => format!("Increases construct GreenPower by {:?}%. + If your team meets total colour thresholds the spec provides additional bonuses.", + self.into_spec().unwrap().values().base()), + Item::PowerRG | + Item::PowerRGPlus | + Item::PowerRGPlusPlus => format!("Increases construct GreenPower and RedPower by {:?}%. + If your team meets total colour thresholds the spec provides additional bonuses.", + self.into_spec().unwrap().values().base()), + Item::PowerGB | + Item::PowerGBPlus | + Item::PowerGBPlusPlus => format!("Increases construct GreenPower and BluePower by {:?}%. + If your team meets total colour thresholds the spec provides additional bonuses.", + self.into_spec().unwrap().values().base()), + Item::PowerRB | + Item::PowerRBPlus | + Item::PowerRBPlusPlus => format!("Increases construct RedPower and BluePower by {:?}%. + If your team meets total colour thresholds the spec provides additional bonuses.", + self.into_spec().unwrap().values().base()), + + // Speed Upgrades + Item::SpeedRR | + Item::SpeedBB | + Item::SpeedGG | + Item::SpeedRG | + Item::SpeedGB | + Item::SpeedRB | + Item::SpeedRRPlus | + Item::SpeedBBPlus | + Item::SpeedGGPlus | + Item::SpeedRGPlus | + Item::SpeedGBPlus | + Item::SpeedRBPlus | + Item::SpeedRRPlusPlus | + Item::SpeedBBPlusPlus | + Item::SpeedGGPlusPlus | + Item::SpeedRGPlusPlus | + Item::SpeedGBPlusPlus | + Item::SpeedRBPlusPlus => format!("Increases construct SpeedStat by {:?}%. + If your team meets total colour thresholds the spec provides additional bonuses.", + self.into_spec().unwrap().values().base()), + + // Skills <- need to move effect mulltipliers into skills + Item::Amplify| + Item::AmplifyPlus | + Item::AmplifyPlusPlus => format!("Increase RedPower BluePower by {:?}%. Lasts {:?}T.", + self.into_skill().unwrap().effect()[0].get_multiplier() - 100, + self.into_skill().unwrap().effect()[0].get_duration()), + + Item::Banish| + Item::BanishPlus | + Item::BanishPlusPlus => format!("Banish target for {:?}T. + Deal {:?}% target RedLife and BlueLife as red and blue damage respectively. + Banished constructs are immune to all skills and effects.", + self.into_skill().unwrap().effect()[0].get_duration(), + self.into_skill().unwrap().multiplier()), + + Item::Blast| + Item::BlastPlus | + Item::BlastPlusPlus => format!("Deals {:?}% BluePower as blue damage.", self.into_skill().unwrap().multiplier()), + + Item::Chaos| + Item::ChaosPlus | + Item::ChaosPlusPlus => format!( + "Hits twice for red and blue damage. Damage {:?}% RedPower and BluePower. + Randomly deals 0 to 30% more damage.", + self.into_skill().unwrap().multiplier()), + + Item::Sustain| + Item::SustainPlus | + Item::SustainPlusPlus => format!( + "Construct cannot be KO'd while active and provides immunity to disables. Lasts {:?}T. + Recharges target RedLife based on {:?}% RedPower.", + self.into_skill().unwrap().effect()[0].get_duration(), + self.into_skill().unwrap().multiplier()), + + Item::Electrify| + Item::ElectrifyPlus | + Item::ElectrifyPlusPlus => format!( + "Applies electrify for {:?}T. + If a construct with electrify takes direct damage they will apply an electrocute debuff to the caster. + Electrocute deals {:?}% BluePower as BlueDamage per turn for {:?}T.", + self.into_skill().unwrap().effect()[0].get_duration(), + self.into_skill().unwrap().effect()[0].get_skill().unwrap().effect()[0].get_skill().unwrap().multiplier(), + self.into_skill().unwrap().effect()[0].get_skill().unwrap().effect()[0].get_duration()), + + Item::Curse| + Item::CursePlus | + Item::CursePlusPlus => format!( + "Increases red and blue damage taken by {:?}%. Lasts {:?}T.", + self.into_skill().unwrap().effect()[0].get_multiplier() - 100, + self.into_skill().unwrap().effect()[0].get_duration()), + + Item::Decay| + Item::DecayPlus | + Item::DecayPlusPlus => format!( + "Reduces healing taken by {:?}% for {:?}T. + Deals blue damage {:?}% BluePower each turn for {:?}T.", + 100 - self.into_skill().unwrap().effect()[0].get_multiplier(), + self.into_skill().unwrap().effect()[0].get_duration(), + self.into_skill().unwrap().effect()[1].get_skill().unwrap().multiplier(), + self.into_skill().unwrap().effect()[1].get_duration()), + + Item::Absorb| + Item::AbsorbPlus | + Item::AbsorbPlusPlus => format!( + "Gain Absorb for {:?}T. Taking damage replaces Absorb with Absorption. + Absorption increases RedPower and BluePower based on damage taken. + Absorption lasts {:?}T. Recharges BlueLife based on {:?}% BluePower.", + self.into_skill().unwrap().effect()[0].get_duration(), + self.into_skill().unwrap().effect()[0].get_skill().unwrap().effect()[0].get_duration(), + self.into_skill().unwrap().multiplier()), + + Item::Haste| + Item::HastePlus | + Item::HastePlusPlus => format!( + "Haste increases SpeedStat by {:?}%. + Red Attack based skills will strike again dealing {:?}% SpeedStat as red damage. + Lasts {:?}T", + self.into_skill().unwrap().effect()[0].get_multiplier() - 100, + Skill::HasteStrike.multiplier(), + self.into_skill().unwrap().effect()[0].get_duration()), + + Item::Heal| + Item::HealPlus | + Item::HealPlusPlus => format!("Heals target for {:?}% GreenPower.", self.into_skill().unwrap().multiplier()), + + Item::Hybrid| + Item::HybridPlus | + Item::HybridPlusPlus => format!( + "Hybrid increases GreenPower by {:?}%. + Blue based Attack skills will blast again dealing {:?}% GreenPower as blue damage. + Lasts {:?}T.", + self.into_skill().unwrap().effect()[0].get_multiplier() - 100, + Skill::HybridBlast.multiplier(), + self.into_skill().unwrap().effect()[0].get_duration()), + + Item::Invert| + Item::InvertPlus | + Item::InvertPlusPlus => format!( + "Reverse healing/recharge into damage and damage into healing/recharge. + Any excess red or blue damage is converted into shield recharge after healing. + Lasts {:?}T.", + self.into_skill().unwrap().effect()[0].get_duration()), + + Item::Counter| + Item::CounterPlus | + Item::CounterPlusPlus => format!( + "Applies counter for {:?}T. + Red damage taken during counter will trigger a counter attack. + Counter attack deals {:?}% RedPower as red damage.", + self.into_skill().unwrap().effect()[0].get_duration(), + self.into_skill().unwrap().effect()[0].get_skill().unwrap().multiplier()), + + Item::Purge| + Item::PurgePlus | + Item::PurgePlusPlus => format!( + "Remove all effects from target construct. + Applies purge disabling target green skills for {:?}T.", + self.into_skill().unwrap().effect()[0].get_duration()), + + Item::Purify| + Item::PurifyPlus | + Item::PurifyPlusPlus => format!( + "Remove all effects and heals for {:?}% GreenPower per effect removed. + Applies Pure increasing healing taken by {:?}%.", + self.into_skill().unwrap().multiplier(), + self.into_skill().unwrap().effect()[0].get_multiplier() - 100), + + Item::Reflect| + Item::ReflectPlus | + Item::ReflectPlusPlus => format!( + "Reflect incoming blue skills to source. Lasts {:?}T. + Recharges target BlueLife based on {:?}% BluePower.", + self.into_skill().unwrap().effect()[0].get_duration(), + self.into_skill().unwrap().multiplier()), + + Item::Recharge| + Item::RechargePlus | + Item::RechargePlusPlus => format!( + "Recharge RedLife and BlueLife based on {:?}% RedPower and BluePower.", + self.into_skill().unwrap().multiplier()), + + Item::Ruin| + Item::RuinPlus | + Item::RuinPlusPlus => format!( + "Team wide skill. Stun each construct for {:?}T. + Deal {:?}% BluePower as blue damage to each construct.", + self.into_skill().unwrap().effect()[0].get_duration(), + self.into_skill().unwrap().multiplier()), + + Item::Link| + Item::LinkPlus | + Item::LinkPlusPlus => format!( + "Stun target for {:?}T. + Deal blue damage of {:?}% BluePower multiplied by number of effects on target.", + self.into_skill().unwrap().effect()[0].get_duration(), + self.into_skill().unwrap().multiplier()), + + Item::Silence| + Item::SilencePlus | + Item::SilencePlusPlus => format!( + "Disable the target from using blue skills for {:?}T and deals {:?}% BluePower as blue damage. + Deals 45% more Damage per blue skill on target.", + self.into_skill().unwrap().effect()[0].get_duration(), + self.into_skill().unwrap().multiplier()), + + Item::Slay| + Item::SlayPlus | + Item::SlayPlusPlus => format!( + "Deals {:?}% RedPower + {:?}% GreenPower as red damage. + Construct heals self for 50% of damage dealt to target construct GreenLife.", + self.into_skill().unwrap().multiplier(), + self.into_skill().unwrap().multiplier()), + + Item::Sleep| + Item::SleepPlus | + Item::SleepPlusPlus => format!( + "Stun for {:?}T and heal target for {:?}% GreenPower.", + self.into_skill().unwrap().effect()[0].get_duration(), + self.into_skill().unwrap().multiplier()), + + Item::Restrict| + Item::RestrictPlus | + Item::RestrictPlusPlus => format!( + "Disable the target from using red skills for {:?}T and deals {:?}% RedPower as red damage. + Deals 35% more damage per red skill on target.", + self.into_skill().unwrap().effect()[0].get_duration(), + self.into_skill().unwrap().multiplier()), + + Item::Bash| + Item::BashPlus | + Item::BashPlusPlus => format!( + "Bash the target increasing the cooldowns of target skills by 1T. Stuns target for {:?}T. + Deals {:?}% RedPower as red damage and 45% more damage per cooldown increased.", + self.into_skill().unwrap().effect()[0].get_duration(), + self.into_skill().unwrap().effect()[0].get_skill().unwrap().multiplier()), + + Item::Strike| + Item::StrikePlus | + Item::StrikePlusPlus => format!( + "Strike the target with speed dealing {:?}% RedPower as red damage.", + self.into_skill().unwrap().multiplier()), + + Item::Siphon| + Item::SiphonPlus | + Item::SiphonPlusPlus => format!( + "Deals {:?}% BluePower + {:?}% GreenPower as blue damage each turn. + Construct heals self for 100% of damage dealt to target construct GreenLife. + Lasts {:?}T.", + self.into_skill().unwrap().effect()[0].get_skill().unwrap().multiplier(), + self.into_skill().unwrap().effect()[0].get_skill().unwrap().multiplier(), + self.into_skill().unwrap().effect()[0].get_duration()), + + Item::Intercept| + Item::InterceptPlus | + Item::InterceptPlusPlus => format!( + "Intercept redirects skills against the team to target, lasts {:?}T. + Recharges RedLife for {:?} RedPower.", + self.into_skill().unwrap().effect()[0].get_duration(), + self.into_skill().unwrap().multiplier()), + + Item::Break| + Item::BreakPlus | + Item::BreakPlusPlus => format!( + "Stun the target for {:?}T and applies Vulnerable increasing red damage taken by {:?}% for {:?}T.", + self.into_skill().unwrap().effect()[0].get_duration(), + self.into_skill().unwrap().effect()[1].get_multiplier() - 100, + self.into_skill().unwrap().effect()[1].get_duration()), + + Item::Triage| + Item::TriagePlus | + Item::TriagePlusPlus => format!( + "Heals target for {:?}% GreenPower each turn. Lasts {:?}T.", + self.into_skill().unwrap().effect()[0].get_skill().unwrap().multiplier(), + self.into_skill().unwrap().effect()[0].get_duration()), + } + } + + // !!!!!! + // IF YOU CHANGE A COMBO + // BE SURE TO EDIT BUTTONS.JSX TOO + // !!!!!! + + fn combo(&self) -> Vec { + match self { + Item::Intercept => vec![Item::Buff, Item::Red, Item::Red], + Item::Triage => vec![Item::Buff, Item::Green, Item::Green], + Item::Absorb => vec![Item::Buff, Item::Blue, Item::Blue], + Item::Amplify => vec![Item::Buff, Item::Red, Item::Blue], + Item::Haste => vec![Item::Buff, Item::Red, Item::Green], + Item::Hybrid => vec![Item::Buff, Item::Green, Item::Blue], + Item::InterceptPlus => vec![Item::Intercept, Item::Intercept, Item::Intercept], + Item::InterceptPlusPlus => vec![Item::InterceptPlus, Item::InterceptPlus, Item::InterceptPlus], + Item::TriagePlus => vec![Item::Triage, Item::Triage, Item::Triage], + Item::TriagePlusPlus => vec![Item::TriagePlus, Item::TriagePlus, Item::TriagePlus], + Item::HastePlus => vec![Item::Haste, Item::Haste, Item::Haste], + Item::HastePlusPlus => vec![Item::HastePlus, Item::HastePlus, Item::HastePlus], + Item::HybridPlus => vec![Item::Hybrid, Item::Hybrid, Item::Hybrid], + Item::HybridPlusPlus => vec![Item::HybridPlus, Item::HybridPlus, Item::HybridPlus], + Item::AbsorbPlus => vec![Item::Absorb, Item::Absorb, Item::Absorb], + Item::AbsorbPlusPlus => vec![Item::AbsorbPlus, Item::AbsorbPlus, Item::AbsorbPlus], + Item::AmplifyPlus => vec![Item::Amplify, Item::Amplify, Item::Amplify], + Item::AmplifyPlusPlus => vec![Item::AmplifyPlus, Item::AmplifyPlus, Item::AmplifyPlus], + + Item::Purge => vec![Item::Debuff, Item::Green, Item::Green], // Needs flavour + Item::Invert => vec![Item::Debuff, Item::Red, Item::Green], + Item::Restrict => vec![Item::Debuff, Item::Red, Item::Red], + Item::Silence => vec![Item::Debuff, Item::Blue, Item::Blue], + Item::Curse => vec![Item::Debuff, Item::Red, Item::Blue], + Item::Decay => vec![Item::Debuff, Item::Green, Item::Blue], + Item::RestrictPlus => vec![Item::Restrict, Item::Restrict, Item::Restrict], + Item::RestrictPlusPlus => vec![Item::RestrictPlus, Item::RestrictPlus, Item::RestrictPlus], + Item::PurgePlus => vec![Item::Purge, Item::Purge, Item::Purge], // Needs flavour + Item::PurgePlusPlus => vec![Item::PurgePlus, Item::PurgePlus, Item::PurgePlus], // Needs flavour + Item::SilencePlus => vec![Item::Silence, Item::Silence, Item::Silence], + Item::SilencePlusPlus => vec![Item::SilencePlus, Item::SilencePlus, Item::SilencePlus], + Item::CursePlus => vec![Item::Curse, Item::Curse, Item::Curse], + Item::CursePlusPlus => vec![Item::CursePlus, Item::CursePlus, Item::CursePlus], + Item::DecayPlus => vec![Item::Decay, Item::Decay, Item::Decay], + Item::DecayPlusPlus => vec![Item::DecayPlus, Item::DecayPlus, Item::DecayPlus], + Item::InvertPlus => vec![Item::Invert, Item::Invert, Item::Invert], + Item::InvertPlusPlus => vec![Item::InvertPlus, Item::InvertPlus, Item::InvertPlus], + + Item::Counter => vec![Item::Block, Item::Red, Item::Red], + Item::Reflect => vec![Item::Block, Item::Green, Item::Blue], + Item::Purify => vec![Item::Block, Item::Green, Item::Green], + Item::Sustain => vec![Item::Block, Item::Red, Item::Green], + Item::Electrify => vec![Item::Block, Item::Blue, Item::Blue], + Item::Recharge => vec![Item::Block, Item::Red, Item::Blue], + Item::CounterPlus => vec![Item::Counter, Item::Counter, Item::Counter], + Item::CounterPlusPlus => vec![Item::CounterPlus, Item::CounterPlus, Item::CounterPlus], // Add red recharge + Item::PurifyPlus => vec![Item::Purify, Item::Purify, Item::Purify], + Item::PurifyPlusPlus => vec![Item::PurifyPlus, Item::PurifyPlus, Item::PurifyPlus], + Item::ElectrifyPlus => vec![Item::Electrify, Item::Electrify, Item::Electrify], + Item::ElectrifyPlusPlus => vec![Item::ElectrifyPlus, Item::ElectrifyPlus, Item::ElectrifyPlus], + Item::SustainPlus => vec![Item::Sustain, Item::Sustain, Item::Sustain], + Item::SustainPlusPlus => vec![Item::SustainPlus, Item::SustainPlus, Item::SustainPlus], + Item::ReflectPlus => vec![Item::Reflect, Item::Reflect, Item::Reflect], + Item::ReflectPlusPlus => vec![Item::ReflectPlus, Item::ReflectPlus, Item::ReflectPlus], + Item::RechargePlus => vec![Item::Recharge, Item::Recharge, Item::Recharge], + Item::RechargePlusPlus => vec![Item::RechargePlus, Item::RechargePlus, Item::RechargePlus], + + Item::Bash => vec![Item::Stun, Item::Red, Item::Red], + Item::Sleep => vec![Item::Stun, Item::Green, Item::Green], + Item::Ruin => vec![Item::Stun, Item::Blue, Item::Blue], + Item::Link => vec![Item::Stun, Item::Blue, Item::Green], + Item::Banish => vec![Item::Stun, Item::Red, Item::Blue], + Item::Break => vec![Item::Stun, Item::Red, Item::Green], + Item::BashPlus => vec![Item::Bash, Item::Bash, Item::Bash], + Item::BashPlusPlus => vec![Item::BashPlus, Item::BashPlus, Item::BashPlus], + Item::SleepPlus => vec![Item::Sleep, Item::Sleep, Item::Sleep], + Item::SleepPlusPlus => vec![Item::SleepPlus, Item::SleepPlus, Item::SleepPlus], + Item::RuinPlus => vec![Item::Ruin, Item::Ruin, Item::Ruin], + Item::RuinPlusPlus => vec![Item::RuinPlus, Item::RuinPlus, Item::RuinPlus], + Item::BreakPlus => vec![Item::Break, Item::Break, Item::Break], + Item::BreakPlusPlus => vec![Item::BreakPlus, Item::BreakPlus, Item::BreakPlus], + Item::LinkPlus => vec![Item::Link, Item::Link, Item::Link], + Item::LinkPlusPlus => vec![Item::LinkPlus, Item::LinkPlus, Item::LinkPlus], + Item::BanishPlus => vec![Item::Banish, Item::Banish, Item::Banish], + Item::BanishPlusPlus => vec![Item::BanishPlus, Item::BanishPlus, Item::BanishPlus], + + Item::Strike => vec![Item::Attack, Item::Red, Item::Red], + Item::Chaos => vec![Item::Attack, Item::Red, Item::Blue], + Item::Heal => vec![Item::Attack, Item::Green, Item::Green], + Item::Blast => vec![Item::Attack, Item::Blue, Item::Blue], + Item::Slay => vec![Item::Attack, Item::Red, Item::Green], + Item::Siphon => vec![Item::Attack, Item::Green, Item::Blue], + Item::StrikePlus => vec![Item::Strike, Item::Strike, Item::Strike], + Item::StrikePlusPlus => vec![Item::StrikePlus, Item::StrikePlus, Item::StrikePlus], + Item::HealPlus => vec![Item::Heal, Item::Heal, Item::Heal], + Item::HealPlusPlus => vec![Item::HealPlus, Item::HealPlus, Item::HealPlus], + Item::BlastPlus => vec![Item::Blast, Item::Blast, Item::Blast], + Item::BlastPlusPlus => vec![Item::BlastPlus, Item::BlastPlus, Item::BlastPlus], + Item::SlayPlus => vec![Item::Slay, Item::Slay, Item::Slay], + Item::SlayPlusPlus => vec![Item::SlayPlus, Item::SlayPlus, Item::SlayPlus], + Item::SiphonPlus => vec![Item::Siphon, Item::Siphon, Item::Siphon], + Item::SiphonPlusPlus => vec![Item::SiphonPlus, Item::SiphonPlus, Item::SiphonPlus], + Item::ChaosPlus => vec![Item::Chaos, Item::Chaos, Item::Chaos], + Item::ChaosPlusPlus => vec![Item::ChaosPlus, Item::ChaosPlus, Item::ChaosPlus], + + Item::PowerRR => vec![Item::Power, Item::Red, Item::Red], + Item::PowerGG => vec![Item::Power, Item::Green, Item::Green], + Item::PowerBB => vec![Item::Power, Item::Blue, Item::Blue], + Item::PowerRG => vec![Item::Power, Item::Red, Item::Green], + Item::PowerGB => vec![Item::Power, Item::Green, Item::Blue], + Item::PowerRB => vec![Item::Power, Item::Red, Item::Blue], + Item::PowerRRPlus => vec![Item::PowerRR, Item::PowerRR, Item::PowerRR], + Item::PowerGGPlus => vec![Item::PowerGG, Item::PowerGG, Item::PowerGG], + Item::PowerBBPlus => vec![Item::PowerBB, Item::PowerBB, Item::PowerBB], + Item::PowerRGPlus => vec![Item::PowerRG, Item::PowerRG, Item::PowerRG], + Item::PowerGBPlus => vec![Item::PowerGB, Item::PowerGB, Item::PowerGB], + Item::PowerRBPlus => vec![Item::PowerRB, Item::PowerRB, Item::PowerRB], + Item::PowerRRPlusPlus => vec![Item::PowerRRPlus, Item::PowerRRPlus, Item::PowerRRPlus], + Item::PowerGGPlusPlus => vec![Item::PowerGGPlus, Item::PowerGGPlus, Item::PowerGGPlus], + Item::PowerBBPlusPlus => vec![Item::PowerBBPlus, Item::PowerBBPlus, Item::PowerBBPlus], + Item::PowerRGPlusPlus => vec![Item::PowerRGPlus, Item::PowerRGPlus, Item::PowerRGPlus], + Item::PowerGBPlusPlus => vec![Item::PowerGBPlus, Item::PowerGBPlus, Item::PowerGBPlus], + Item::PowerRBPlusPlus => vec![Item::PowerRBPlus, Item::PowerRBPlus, Item::PowerRBPlus], + + Item::LifeRR => vec![Item::Life, Item::Red, Item::Red], + Item::LifeGG => vec![Item::Life, Item::Green, Item::Green], + Item::LifeBB => vec![Item::Life, Item::Blue, Item::Blue], + Item::LifeRG => vec![Item::Life, Item::Red, Item::Green], + Item::LifeGB => vec![Item::Life, Item::Green, Item::Blue], + Item::LifeRB => vec![Item::Life, Item::Red, Item::Blue], + Item::LifeRRPlus => vec![Item::LifeRR, Item::LifeRR, Item::LifeRR], + Item::LifeGGPlus => vec![Item::LifeGG, Item::LifeGG, Item::LifeGG], + Item::LifeBBPlus => vec![Item::LifeBB, Item::LifeBB, Item::LifeBB], + Item::LifeRGPlus => vec![Item::LifeRG, Item::LifeRG, Item::LifeRG], + Item::LifeGBPlus => vec![Item::LifeGB, Item::LifeGB, Item::LifeGB], + Item::LifeRBPlus => vec![Item::LifeRB, Item::LifeRB, Item::LifeRB], + Item::LifeRRPlusPlus => vec![Item::LifeRRPlus, Item::LifeRRPlus, Item::LifeRRPlus], + Item::LifeGGPlusPlus => vec![Item::LifeGGPlus, Item::LifeGGPlus, Item::LifeGGPlus], + Item::LifeBBPlusPlus => vec![Item::LifeBBPlus, Item::LifeBBPlus, Item::LifeBBPlus], + Item::LifeRGPlusPlus => vec![Item::LifeRGPlus, Item::LifeRGPlus, Item::LifeRGPlus], + Item::LifeGBPlusPlus => vec![Item::LifeGBPlus, Item::LifeGBPlus, Item::LifeGBPlus], + Item::LifeRBPlusPlus => vec![Item::LifeRBPlus, Item::LifeRBPlus, Item::LifeRBPlus], + + Item::SpeedRR => vec![Item::Speed, Item::Red, Item::Red], + Item::SpeedGG => vec![Item::Speed, Item::Green, Item::Green], + Item::SpeedBB => vec![Item::Speed, Item::Blue, Item::Blue], + Item::SpeedRG => vec![Item::Speed, Item::Red, Item::Green], + Item::SpeedGB => vec![Item::Speed, Item::Green, Item::Blue], + Item::SpeedRB => vec![Item::Speed, Item::Red, Item::Blue], + Item::SpeedRRPlus => vec![Item::SpeedRR, Item::SpeedRR, Item::SpeedRR], + Item::SpeedGGPlus => vec![Item::SpeedGG, Item::SpeedGG, Item::SpeedGG], + Item::SpeedBBPlus => vec![Item::SpeedBB, Item::SpeedBB, Item::SpeedBB], + Item::SpeedRGPlus => vec![Item::SpeedRG, Item::SpeedRG, Item::SpeedRG], + Item::SpeedGBPlus => vec![Item::SpeedGB, Item::SpeedGB, Item::SpeedGB], + Item::SpeedRBPlus => vec![Item::SpeedRB, Item::SpeedRB, Item::SpeedRB], + Item::SpeedRRPlusPlus => vec![Item::SpeedRRPlus, Item::SpeedRRPlus, Item::SpeedRRPlus], + Item::SpeedGGPlusPlus => vec![Item::SpeedGGPlus, Item::SpeedGGPlus, Item::SpeedGGPlus], + Item::SpeedBBPlusPlus => vec![Item::SpeedBBPlus, Item::SpeedBBPlus, Item::SpeedBBPlus], + Item::SpeedRGPlusPlus => vec![Item::SpeedRGPlus, Item::SpeedRGPlus, Item::SpeedRGPlus], + Item::SpeedGBPlusPlus => vec![Item::SpeedGBPlus, Item::SpeedGBPlus, Item::SpeedGBPlus], + Item::SpeedRBPlusPlus => vec![Item::SpeedRBPlus, Item::SpeedRBPlus, Item::SpeedRBPlus], + + _ => vec![*self], + } + } +} + +impl From for Item { + fn from(skill: Skill) -> Item { + match skill { + Skill::Absorb => Item::Absorb, + Skill::AbsorbPlus => Item::AbsorbPlus, + Skill::AbsorbPlusPlus => Item::AbsorbPlusPlus, + Skill::Amplify => Item::Amplify, + Skill::AmplifyPlus => Item::AmplifyPlus, + Skill::AmplifyPlusPlus => Item::AmplifyPlusPlus, + Skill::Attack => Item::Attack, + Skill::Banish => Item::Banish, + Skill::BanishPlus => Item::BanishPlus, + Skill::BanishPlusPlus => Item::BanishPlusPlus, + Skill::Bash => Item::Bash, + Skill::BashPlus => Item::BashPlus, + Skill::BashPlusPlus => Item::BashPlusPlus, + Skill::Blast => Item::Blast, + Skill::BlastPlus => Item::BlastPlus, + Skill::BlastPlusPlus => Item::BlastPlusPlus, + Skill::Block => Item::Block, + Skill::Buff => Item::Buff, + Skill::Chaos => Item::Chaos, + Skill::ChaosPlus => Item::ChaosPlus, + Skill::ChaosPlusPlus => Item::ChaosPlusPlus, + Skill::Counter => Item::Counter, + Skill::CounterPlus => Item::CounterPlus, + Skill::CounterPlusPlus => Item::CounterPlusPlus, + Skill::Curse => Item::Curse, + Skill::CursePlus => Item::CursePlus, + Skill::CursePlusPlus => Item::CursePlusPlus, + Skill::Debuff => Item::Debuff, + Skill::Decay => Item::Decay, + Skill::DecayPlus => Item::DecayPlus, + Skill::DecayPlusPlus => Item::DecayPlusPlus, + Skill::Electrify => Item::Electrify, + Skill::ElectrifyPlus => Item::ElectrifyPlus, + Skill::ElectrifyPlusPlus=> Item::ElectrifyPlusPlus, + Skill::Haste => Item::Haste, + Skill::HastePlus => Item::HastePlus, + Skill::HastePlusPlus => Item::HastePlusPlus, + Skill::Heal => Item::Heal, + Skill::HealPlus => Item::HealPlus, + Skill::HealPlusPlus => Item::HealPlusPlus, + Skill::Hybrid => Item::Hybrid, + Skill::HybridPlus => Item::HybridPlus, + Skill::HybridPlusPlus => Item::HybridPlusPlus, + Skill::Intercept => Item::Intercept, + Skill::InterceptPlus => Item::InterceptPlus, + Skill::InterceptPlusPlus=> Item::InterceptPlusPlus, + Skill::Invert => Item::Invert, + Skill::InvertPlus => Item::InvertPlus, + Skill::InvertPlusPlus => Item::InvertPlusPlus, + Skill::Purge => Item::Purge, + Skill::PurgePlus => Item::PurgePlus, + Skill::PurgePlusPlus => Item::PurgePlusPlus, + Skill::Purify => Item::Purify, + Skill::PurifyPlus => Item::PurifyPlus, + Skill::PurifyPlusPlus => Item::PurifyPlusPlus, + Skill::Recharge => Item::Recharge, + Skill::RechargePlus => Item::RechargePlus, + Skill::RechargePlusPlus => Item::RechargePlusPlus, + Skill::Reflect => Item::Reflect, + Skill::ReflectPlus => Item::ReflectPlus, + Skill::ReflectPlusPlus => Item::ReflectPlusPlus, + Skill::Restrict => Item::Restrict, + Skill::RestrictPlus => Item::RestrictPlus, + Skill::RestrictPlusPlus => Item::RestrictPlusPlus, + Skill::Ruin => Item::Ruin, + Skill::RuinPlus => Item::RuinPlus, + Skill::RuinPlusPlus => Item::RuinPlusPlus, + Skill::Link => Item::Link, + Skill::LinkPlus => Item::LinkPlus, + Skill::LinkPlusPlus => Item::LinkPlusPlus, + Skill::Silence => Item::Silence, + Skill::SilencePlus => Item::SilencePlus, + Skill::SilencePlusPlus => Item::SilencePlusPlus, + Skill::Siphon => Item::Siphon, + Skill::SiphonPlus => Item::SiphonPlus, + Skill::SiphonPlusPlus => Item::SiphonPlusPlus, + Skill::Slay => Item::Slay, + Skill::SlayPlus => Item::SlayPlus, + Skill::SlayPlusPlus => Item::SlayPlusPlus, + Skill::Sleep => Item::Sleep, + Skill::SleepPlus => Item::SleepPlus, + Skill::SleepPlusPlus => Item::SleepPlusPlus, + Skill::Strike => Item::Strike, + Skill::StrikePlus => Item::StrikePlus, + Skill::StrikePlusPlus => Item::StrikePlusPlus, + Skill::Stun => Item::Stun, + Skill::Sustain => Item::Sustain, + Skill::SustainPlus => Item::SustainPlus, + Skill::SustainPlusPlus => Item::SustainPlusPlus, + Skill::Break => Item::Break, + Skill::BreakPlus => Item::BreakPlus, + Skill::BreakPlusPlus => Item::BreakPlusPlus, + Skill::Triage => Item::Triage, + Skill::TriagePlus => Item::TriagePlus, + Skill::TriagePlusPlus => Item::TriagePlusPlus, + + // Convert subskills into parent skills + Skill::Electrocute => Item::Electrify, + Skill::ElectrocutePlus => Item::ElectrifyPlus, + Skill::ElectrocutePlusPlus => Item::ElectrifyPlusPlus, + Skill::ElectrocuteTick => Item::Electrify, + Skill::ElectrocuteTickPlus => Item::ElectrifyPlus, + Skill::ElectrocuteTickPlusPlus => Item::ElectrifyPlus, + Skill::DecayTick => Item::Decay, + Skill::DecayTickPlus => Item::DecayPlus, + Skill::DecayTickPlusPlus => Item::DecayPlusPlus, + Skill::Absorption => Item::Absorb, + Skill::AbsorptionPlus => Item::AbsorbPlus, + Skill::AbsorptionPlusPlus => Item::AbsorbPlusPlus, + Skill::HasteStrike => Item::Haste, + Skill::HybridBlast => Item::Hybrid, + Skill::CounterAttack => Item::Counter, + Skill::CounterAttackPlus => Item::CounterPlus, + Skill::CounterAttackPlusPlus => Item::CounterPlusPlus, + Skill::SiphonTick => Item::Siphon, + Skill::SiphonTickPlus => Item::SiphonPlus, + Skill::SiphonTickPlusPlus => Item::SiphonPlusPlus, + Skill::TriageTick => Item::Triage, + Skill::TriageTickPlus => Item::TriagePlus, + Skill::TriageTickPlusPlus => Item::TriagePlusPlus, + } + } +} + +impl From for Item { + fn from(spec: Spec) -> Item { + match spec { + Spec::Speed => Item::Speed, + Spec::SpeedRR => Item::SpeedRR, + Spec::SpeedBB => Item::SpeedBB, + Spec::SpeedGG => Item::SpeedGG, + Spec::SpeedRG => Item::SpeedRG, + Spec::SpeedGB => Item::SpeedGB, + Spec::SpeedRB => Item::SpeedRB, + + Spec::SpeedRRPlus => Item::SpeedRRPlus, + Spec::SpeedBBPlus => Item::SpeedBBPlus, + Spec::SpeedGGPlus => Item::SpeedGGPlus, + Spec::SpeedRGPlus => Item::SpeedRGPlus, + Spec::SpeedGBPlus => Item::SpeedGBPlus, + Spec::SpeedRBPlus => Item::SpeedRBPlus, + + Spec::SpeedRRPlusPlus => Item::SpeedRRPlusPlus, + Spec::SpeedBBPlusPlus => Item::SpeedBBPlusPlus, + Spec::SpeedGGPlusPlus => Item::SpeedGGPlusPlus, + Spec::SpeedRGPlusPlus => Item::SpeedRGPlusPlus, + Spec::SpeedGBPlusPlus => Item::SpeedGBPlusPlus, + Spec::SpeedRBPlusPlus => Item::SpeedRBPlusPlus, + + Spec::Power => Item::Power, + Spec::PowerRR => Item::PowerRR, + Spec::PowerBB => Item::PowerBB, + Spec::PowerGG => Item::PowerGG, + Spec::PowerRG => Item::PowerRG, + Spec::PowerGB => Item::PowerGB, + Spec::PowerRB => Item::PowerRB, + Spec::PowerRRPlus => Item::PowerRRPlus, + Spec::PowerBBPlus => Item::PowerBBPlus, + Spec::PowerGGPlus => Item::PowerGGPlus, + Spec::PowerRGPlus => Item::PowerRGPlus, + Spec::PowerGBPlus => Item::PowerGBPlus, + Spec::PowerRBPlus => Item::PowerRBPlus, + Spec::PowerRRPlusPlus => Item::PowerRRPlusPlus, + Spec::PowerBBPlusPlus => Item::PowerBBPlusPlus, + Spec::PowerGGPlusPlus => Item::PowerGGPlusPlus, + Spec::PowerRGPlusPlus => Item::PowerRGPlusPlus, + Spec::PowerGBPlusPlus => Item::PowerGBPlusPlus, + Spec::PowerRBPlusPlus => Item::PowerRBPlusPlus, + + Spec::Life => Item::Life, + Spec::LifeRG => Item::LifeRG, + Spec::LifeGB => Item::LifeGB, + Spec::LifeRB => Item::LifeRB, + Spec::LifeGG => Item::LifeGG, + Spec::LifeRR => Item::LifeRR, + Spec::LifeBB => Item::LifeBB, + Spec::LifeRGPlus => Item::LifeRGPlus, + Spec::LifeGBPlus => Item::LifeGBPlus, + Spec::LifeRBPlus => Item::LifeRBPlus, + Spec::LifeGGPlus => Item::LifeGGPlus, + Spec::LifeRRPlus => Item::LifeRRPlus, + Spec::LifeBBPlus => Item::LifeBBPlus, + Spec::LifeRGPlusPlus => Item::LifeRGPlusPlus, + Spec::LifeGBPlusPlus => Item::LifeGBPlusPlus, + Spec::LifeRBPlusPlus => Item::LifeRBPlusPlus, + Spec::LifeGGPlusPlus => Item::LifeGGPlusPlus, + Spec::LifeRRPlusPlus => Item::LifeRRPlusPlus, + Spec::LifeBBPlusPlus => Item::LifeBBPlusPlus, + + // _ => panic!("{:?} not implemented as a item", spec), + } + } +} + + +#[derive(Debug,Clone,Serialize,Deserialize)] +pub struct Combo { + pub item: Item, + pub components: Vec, +} + +pub fn get_combos() -> Vec { + let mut combinations = vec![ + Combo { components: Item::Intercept.combo(), item: Item::Intercept }, + Combo { components: Item::InterceptPlus.combo(), item: Item::InterceptPlus }, + Combo { components: Item::InterceptPlusPlus.combo(), item: Item::InterceptPlusPlus }, + Combo { components: Item::Triage.combo(), item: Item::Triage }, + Combo { components: Item::TriagePlus.combo(), item: Item::TriagePlus }, + Combo { components: Item::TriagePlusPlus.combo(), item: Item::TriagePlusPlus }, + Combo { components: Item::Absorb.combo(), item: Item::Absorb }, + Combo { components: Item::AbsorbPlus.combo(), item: Item::AbsorbPlus }, + Combo { components: Item::AbsorbPlusPlus.combo(), item: Item::AbsorbPlusPlus }, + Combo { components: Item::Haste.combo(), item: Item::Haste }, + Combo { components: Item::HastePlus.combo(), item: Item::HastePlus }, + Combo { components: Item::HastePlusPlus.combo(), item: Item::HastePlusPlus }, + Combo { components: Item::Hybrid.combo(), item: Item::Hybrid }, + Combo { components: Item::HybridPlus.combo(), item: Item::HybridPlus }, + Combo { components: Item::HybridPlusPlus.combo(), item: Item::HybridPlusPlus }, + Combo { components: Item::Amplify.combo(), item: Item::Amplify }, + Combo { components: Item::AmplifyPlus.combo(), item: Item::AmplifyPlus }, + Combo { components: Item::AmplifyPlusPlus.combo(), item: Item::AmplifyPlusPlus }, + + Combo { components: Item::Restrict.combo(), item: Item::Restrict }, + Combo { components: Item::RestrictPlus.combo(), item: Item::RestrictPlus }, + Combo { components: Item::RestrictPlusPlus.combo(), item: Item::RestrictPlusPlus }, + Combo { components: Item::Purge.combo(), item: Item::Purge }, // Needs flavour + Combo { components: Item::PurgePlus.combo(), item: Item::PurgePlus }, + Combo { components: Item::PurgePlusPlus.combo(), item: Item::PurgePlusPlus }, + + Combo { components: Item::Silence.combo(), item: Item::Silence }, + Combo { components: Item::SilencePlus.combo(), item: Item::SilencePlus }, + Combo { components: Item::SilencePlusPlus.combo(), item: Item::SilencePlusPlus }, + + + Combo { components: Item::Invert.combo(), item: Item::Invert }, + Combo { components: Item::InvertPlus.combo(), item: Item::InvertPlus }, + Combo { components: Item::InvertPlusPlus.combo(), item: Item::InvertPlusPlus }, + Combo { components: Item::Curse.combo(), item: Item::Curse }, + Combo { components: Item::CursePlus.combo(), item: Item::CursePlus }, + Combo { components: Item::CursePlusPlus.combo(), item: Item::CursePlusPlus }, + Combo { components: Item::Decay.combo(), item: Item::Decay }, + Combo { components: Item::DecayPlus.combo(), item: Item::DecayPlus }, + Combo { components: Item::DecayPlusPlus.combo(), item: Item::DecayPlusPlus }, + + Combo { components: Item::Counter.combo(), item: Item::Counter }, + Combo { components: Item::CounterPlus.combo(), item: Item::CounterPlus }, + Combo { components: Item::CounterPlusPlus.combo(), item: Item::CounterPlusPlus }, + Combo { components: Item::Purify.combo(), item: Item::Purify }, + Combo { components: Item::PurifyPlus.combo(), item: Item::PurifyPlus }, + Combo { components: Item::PurifyPlusPlus.combo(), item: Item::PurifyPlusPlus }, + + Combo { components: Item::Electrify.combo(), item: Item::Electrify }, + Combo { components: Item::ElectrifyPlus.combo(), item: Item::ElectrifyPlus }, + Combo { components: Item::ElectrifyPlusPlus.combo(), item: Item::ElectrifyPlusPlus }, + + Combo { components: Item::Sustain.combo(), item: Item::Sustain }, + Combo { components: Item::SustainPlus.combo(), item: Item::SustainPlus }, + Combo { components: Item::SustainPlusPlus.combo(), item: Item::SustainPlusPlus }, + Combo { components: Item::Reflect.combo(), item: Item::Reflect }, + Combo { components: Item::ReflectPlus.combo(), item: Item::ReflectPlus }, + Combo { components: Item::ReflectPlusPlus.combo(), item: Item::ReflectPlusPlus }, + + + Combo { components: Item::Recharge.combo(), item: Item::Recharge }, + Combo { components: Item::RechargePlus.combo(), item: Item::RechargePlus }, + Combo { components: Item::RechargePlusPlus.combo(), item: Item::RechargePlusPlus }, + + Combo { components: Item::Bash.combo(), item: Item::Bash }, + Combo { components: Item::BashPlus.combo(), item: Item::BashPlus }, + Combo { components: Item::BashPlusPlus.combo(), item: Item::BashPlusPlus }, + Combo { components: Item::Sleep.combo(), item: Item::Sleep }, + Combo { components: Item::SleepPlus.combo(), item: Item::SleepPlus }, + Combo { components: Item::SleepPlusPlus.combo(), item: Item::SleepPlusPlus }, + Combo { components: Item::Ruin.combo(), item: Item::Ruin }, + Combo { components: Item::RuinPlus.combo(), item: Item::RuinPlus }, + Combo { components: Item::RuinPlusPlus.combo(), item: Item::RuinPlusPlus }, + + Combo { components: Item::Break.combo(), item: Item::Break }, + Combo { components: Item::BreakPlus.combo(), item: Item::BreakPlus }, + Combo { components: Item::BreakPlusPlus.combo(), item: Item::BreakPlusPlus }, + Combo { components: Item::Link.combo(), item: Item::Link }, + Combo { components: Item::LinkPlus.combo(), item: Item::LinkPlus }, + Combo { components: Item::LinkPlusPlus.combo(), item: Item::LinkPlusPlus }, + Combo { components: Item::Banish.combo(), item: Item::Banish }, + Combo { components: Item::BanishPlus.combo(), item: Item::BanishPlus }, + Combo { components: Item::BanishPlusPlus.combo(), item: Item::BanishPlusPlus }, + + Combo { components: Item::Strike.combo(), item: Item::Strike }, + Combo { components: Item::StrikePlus.combo(), item: Item::StrikePlus }, + Combo { components: Item::StrikePlusPlus.combo(), item: Item::StrikePlusPlus }, + + Combo { components: Item::Heal.combo(), item: Item::Heal }, + Combo { components: Item::HealPlus.combo(), item: Item::HealPlus }, + Combo { components: Item::HealPlusPlus.combo(), item: Item::HealPlusPlus }, + Combo { components: Item::Blast.combo(), item: Item::Blast }, + Combo { components: Item::BlastPlus.combo(), item: Item::BlastPlus }, + Combo { components: Item::BlastPlusPlus.combo(), item: Item::BlastPlusPlus }, + Combo { components: Item::Slay.combo(), item: Item::Slay }, + Combo { components: Item::SlayPlus.combo(), item: Item::SlayPlus }, + Combo { components: Item::SlayPlusPlus.combo(), item: Item::SlayPlusPlus }, + Combo { components: Item::Siphon.combo(), item: Item::Siphon }, + Combo { components: Item::SiphonPlus.combo(), item: Item::SiphonPlus }, + Combo { components: Item::SiphonPlusPlus.combo(), item: Item::SiphonPlusPlus }, + Combo { components: Item::Chaos.combo(), item: Item::Chaos }, + Combo { components: Item::ChaosPlus.combo(), item: Item::ChaosPlus }, + Combo { components: Item::ChaosPlusPlus.combo(), item: Item::ChaosPlusPlus }, + + Combo { components: Item::PowerRR.combo(), item: Item::PowerRR }, + Combo { components: Item::PowerGG.combo(), item: Item::PowerGG }, + Combo { components: Item::PowerBB.combo(), item: Item::PowerBB }, + Combo { components: Item::PowerRG.combo(), item: Item::PowerRG }, + Combo { components: Item::PowerGB.combo(), item: Item::PowerGB }, + Combo { components: Item::PowerRB.combo(), item: Item::PowerRB }, + Combo { components: Item::PowerRRPlus.combo(), item: Item::PowerRRPlus }, + Combo { components: Item::PowerGGPlus.combo(), item: Item::PowerGGPlus }, + Combo { components: Item::PowerBBPlus.combo(), item: Item::PowerBBPlus }, + Combo { components: Item::PowerRGPlus.combo(), item: Item::PowerRGPlus }, + Combo { components: Item::PowerGBPlus.combo(), item: Item::PowerGBPlus }, + Combo { components: Item::PowerRBPlus.combo(), item: Item::PowerRBPlus }, + Combo { components: Item::PowerRRPlusPlus.combo(), item: Item::PowerRRPlusPlus }, + Combo { components: Item::PowerGGPlusPlus.combo(), item: Item::PowerGGPlusPlus }, + Combo { components: Item::PowerBBPlusPlus.combo(), item: Item::PowerBBPlusPlus }, + Combo { components: Item::PowerRGPlusPlus.combo(), item: Item::PowerRGPlusPlus }, + Combo { components: Item::PowerGBPlusPlus.combo(), item: Item::PowerGBPlusPlus }, + Combo { components: Item::PowerRBPlusPlus.combo(), item: Item::PowerRBPlusPlus }, + + Combo { components: Item::LifeRR.combo(), item: Item::LifeRR}, + Combo { components: Item::LifeGG.combo(), item: Item::LifeGG}, + Combo { components: Item::LifeBB.combo(), item: Item::LifeBB}, + Combo { components: Item::LifeRG.combo(), item: Item::LifeRG}, + Combo { components: Item::LifeGB.combo(), item: Item::LifeGB}, + Combo { components: Item::LifeRB.combo(), item: Item::LifeRB}, + Combo { components: Item::LifeRRPlus.combo(), item: Item::LifeRRPlus }, + Combo { components: Item::LifeGGPlus.combo(), item: Item::LifeGGPlus }, + Combo { components: Item::LifeBBPlus.combo(), item: Item::LifeBBPlus }, + Combo { components: Item::LifeRGPlus.combo(), item: Item::LifeRGPlus }, + Combo { components: Item::LifeGBPlus.combo(), item: Item::LifeGBPlus }, + Combo { components: Item::LifeRBPlus.combo(), item: Item::LifeRBPlus }, + Combo { components: Item::LifeRRPlusPlus.combo(), item: Item::LifeRRPlusPlus }, + Combo { components: Item::LifeGGPlusPlus.combo(), item: Item::LifeGGPlusPlus }, + Combo { components: Item::LifeBBPlusPlus.combo(), item: Item::LifeBBPlusPlus }, + Combo { components: Item::LifeRGPlusPlus.combo(), item: Item::LifeRGPlusPlus }, + Combo { components: Item::LifeGBPlusPlus.combo(), item: Item::LifeGBPlusPlus }, + Combo { components: Item::LifeRBPlusPlus.combo(), item: Item::LifeRBPlusPlus }, + + Combo { components: Item::SpeedRR.combo(), item: Item::SpeedRR}, + Combo { components: Item::SpeedGG.combo(), item: Item::SpeedGG}, + Combo { components: Item::SpeedBB.combo(), item: Item::SpeedBB}, + Combo { components: Item::SpeedRG.combo(), item: Item::SpeedRG}, + Combo { components: Item::SpeedGB.combo(), item: Item::SpeedGB}, + Combo { components: Item::SpeedRB.combo(), item: Item::SpeedRB}, + Combo { components: Item::SpeedRRPlus.combo(), item: Item::SpeedRRPlus }, + Combo { components: Item::SpeedGGPlus.combo(), item: Item::SpeedGGPlus }, + Combo { components: Item::SpeedBBPlus.combo(), item: Item::SpeedBBPlus }, + Combo { components: Item::SpeedRGPlus.combo(), item: Item::SpeedRGPlus }, + Combo { components: Item::SpeedGBPlus.combo(), item: Item::SpeedGBPlus }, + Combo { components: Item::SpeedRBPlus.combo(), item: Item::SpeedRBPlus }, + Combo { components: Item::SpeedRRPlusPlus.combo(), item: Item::SpeedRRPlusPlus }, + Combo { components: Item::SpeedGGPlusPlus.combo(), item: Item::SpeedGGPlusPlus }, + Combo { components: Item::SpeedBBPlusPlus.combo(), item: Item::SpeedBBPlusPlus }, + Combo { components: Item::SpeedRGPlusPlus.combo(), item: Item::SpeedRGPlusPlus }, + Combo { components: Item::SpeedGBPlusPlus.combo(), item: Item::SpeedGBPlusPlus }, + Combo { components: Item::SpeedRBPlusPlus.combo(), item: Item::SpeedRBPlusPlus }, + ]; + + combinations.iter_mut().for_each(|set| set.components.sort_unstable()); + + return combinations; +} + +#[derive(Debug,Clone,Serialize,Deserialize)] +pub struct ItemInfo { + pub item: Item, + pub cost: usize, + pub spec: bool, + pub values: Option, + pub skill: bool, + pub speed: Option, + pub cooldown: Cooldown, + pub description: String, +} + + +#[derive(Debug,Clone,Serialize,Deserialize)] +pub struct ItemInfoCtr { + pub combos: Vec, + pub items: Vec, +} + +pub fn item_info() -> ItemInfoCtr { + let combos = get_combos(); + let mut items = combos + .into_iter() + .flat_map(|mut c| { + c.components.push(c.item); + c.components + }) + .collect::>(); + + items.sort_unstable(); + items.dedup(); + + let items = items + .into_iter() + .map(|v| ItemInfo { + item: v, + cost: v.cost(), + spec: v.into_spec().is_some(), + values: match v.into_spec() { + Some(s) => Some(s.values()), + None => None + }, + skill: v.into_skill().is_some(), + description: v.into_description(), + speed: match v.into_skill() { + Some(s) => Some(s.speed()), + None => None + }, + cooldown: match v.into_skill() { + Some(s) => s.base_cd(), + None => None + }, + }) + .collect::>(); + + let combos = get_combos(); + + return ItemInfoCtr { + combos, + items, + }; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn item_components_test() { + assert_eq!(Item::Strike.components(), vec![Item::Red, Item::Red, Item::Attack]); + assert_eq!(Item::StrikePlus.components(), vec![ + Item::Red, Item::Red, Item::Attack, + Item::Red, Item::Red, Item::Attack, + Item::Red, Item::Red, Item::Attack, + ]); + assert_eq!(Item::StrikePlusPlus.components(), vec![ + Item::Red, Item::Red, Item::Attack, + Item::Red, Item::Red, Item::Attack, + Item::Red, Item::Red, Item::Attack, + + Item::Red, Item::Red, Item::Attack, + Item::Red, Item::Red, Item::Attack, + Item::Red, Item::Red, Item::Attack, + + Item::Red, Item::Red, Item::Attack, + Item::Red, Item::Red, Item::Attack, + Item::Red, Item::Red, Item::Attack, + ]); + + } + + #[test] + fn item_info_test() { + item_info(); + } +} \ No newline at end of file diff --git a/core/src/lib.rs b/core/src/lib.rs new file mode 100644 index 00000000..2fb53670 --- /dev/null +++ b/core/src/lib.rs @@ -0,0 +1,23 @@ +extern crate rand; +extern crate uuid; +extern crate bcrypt; +extern crate chrono; + +extern crate serde; +#[macro_use] extern crate serde_derive; +#[macro_use] extern crate failure; + +#[macro_use] extern crate log; + +pub mod construct; +pub mod effect; +pub mod game; +pub mod instance; +pub mod item; +pub mod mob; +pub mod names; +pub mod player; +pub mod skill; +pub mod spec; +pub mod util; +pub mod vbox; diff --git a/core/src/mob.rs b/core/src/mob.rs new file mode 100644 index 00000000..2cab954b --- /dev/null +++ b/core/src/mob.rs @@ -0,0 +1,30 @@ +use uuid::Uuid; + +use std::iter; + +use construct::{Construct}; +use names::{name}; +use player::{Player}; + +pub fn generate_mob() -> Construct { + let mob = Construct::new() + .named(&name()); + + return mob; +} + +pub fn instance_mobs(player_id: Uuid) -> Vec { + iter::repeat_with(|| + generate_mob() + .set_account(player_id)) + // .learn(Skill::Attack)) + .take(3) + .collect::>() +} + +pub fn bot_player() -> Player { + let bot_id = Uuid::new_v4(); + let constructs = instance_mobs(bot_id); + Player::new(bot_id, &name(), constructs).set_bot(true) +} + diff --git a/core/src/names.rs b/core/src/names.rs new file mode 100644 index 00000000..07cc6ff0 --- /dev/null +++ b/core/src/names.rs @@ -0,0 +1,138 @@ +use rand::prelude::*; +use rand::{thread_rng}; + +const FIRSTS: [&'static str; 53] = [ + "artificial", + "ambient", + "borean", + "brewing", + "bristling", + "compressed", + "ceramic", + "chromatic", + "concave", + "convex", + "distorted", + "deserted", + "emotive", + "emotionless", + "elliptical", + "extrasolar", + "fierce", + "fossilised", + "frozen", + "gravitational", + "jovian", + "inverted", + "leafy", + "lurking", + "limitless", + "magnetic", + "metallic", + "mossy", + "mighty", + "modulated", + "nocturnal", + "noisy", + "nutritious", + "powerful", + "obscure", + "organic", + "oxygenated", + "oscillating", + "ossified", + "orbiting", + "piscine", + "polar", + "pure", + "recalcitrant", + "rogue", + "sealed", + "subversive", + "subterranean", + "supercooled", + "subsonic", + "synthetic", + "terrestrial", + "weary", +]; + +const LASTS: [&'static str; 63] = [ + "artifact", + "assembly", + "antenna", + "alloy", + "carrier", + "carbon", + "console", + "construct", + "coordinates", + "craft", + "core", + "design", + "drone", + "distortion", + "detector", + "energy", + "entropy", + "exoplanet", + "foilage", + "forest", + "form", + "fossil", + "frequency", + "function", + "fusion", + "fission", + "information", + "insulator", + "layout", + "lifeform", + "liquid", + "landmass", + "lens", + "mass", + "mantle", + "magnetism", + "mechanism", + "mountain", + "nectar", + "nebula", + "oxide", + "orbit", + "pattern", + "plant", + "planet", + "poseidon", + "problem", + "receiver", + "replicant", + "river", + "satellite", + "scaffold", + "structure", + "shape", + "signal", + "synthesiser", + "system", + "tower", + "transmitter", + "traveller", + "vibration", + "warning", + "wildlife", +]; + +pub fn name() -> String { + let mut rng = thread_rng(); + + let first = rng.gen_range(0, FIRSTS.len() - 1); + let last = rng.gen_range(0, LASTS.len() - 1); + + let mut s = String::new(); + s.push_str(FIRSTS[first]); + s.push(' '); + s.push_str(LASTS[last]); + + s +} diff --git a/core/src/player.rs b/core/src/player.rs new file mode 100644 index 00000000..c87b8a97 --- /dev/null +++ b/core/src/player.rs @@ -0,0 +1,442 @@ +use std::collections::{HashMap}; + +use uuid::Uuid; +use rand::prelude::*; + +use failure::Error; +use failure::err_msg; + +use construct::{Construct, Colours}; +use vbox::{Vbox, ItemType, VboxIndices}; +use item::{Item, ItemEffect}; +use effect::{Effect}; + +const DISCARD_COST: usize = 2; + +#[derive(Debug,Copy,Clone,Serialize,Deserialize,Eq,PartialEq)] +pub enum Score { + Zero, + One, + Two, + Three, + Adv, + + Win, + Lose, +} + +impl Score { + pub fn add_win(self, _opp: &Score) -> Score { + match self { + Score::Zero => Score::One, + Score::One => Score::Two, + Score::Two => Score::Win, + // Tennis scoring + // Score::Three => match opp { + // Score::Adv => Score::Three, + // Score::Three => Score::Adv, + // _ => Score::Win, + // } + // Score::Adv => Score::Win, + + _ => panic!("faulty score increment {:?}", self), + } + } + + pub fn add_loss(self) -> Score { + match self { + // Score::Adv => Score::Three, + _ => self, + } + } + +} + +#[derive(Debug,Clone,Serialize,Deserialize)] +pub struct Player { + pub id: Uuid, + pub img: Option, + pub name: String, + pub vbox: Vbox, + pub constructs: Vec, + pub bot: bool, + pub ready: bool, + pub draw_offered: bool, + pub score: Score, +} + +impl Player { + pub fn new(account: Uuid, name: &String, constructs: Vec) -> Player { + Player { + id: account, + img: Some(account), + name: name.clone(), + vbox: Vbox::new(), + constructs, + bot: false, + ready: false, + draw_offered: false, + score: Score::Zero, + } + } + + pub fn redact(mut self, account: Uuid) -> Player { + // all g + if account == self.id { + return self; + } + + // remove vbox + self.vbox = Vbox::new(); + + // hide skills + for construct in self.constructs.iter_mut() { + construct.skills = vec![]; + construct.specs = vec![]; + } + + self + } + + pub fn set_bot(mut self, bot: bool) -> Player { + self.bot = bot; + self + } + + pub fn set_ready(&mut self, ready: bool) -> &mut Player { + self.ready = ready; + self + } + + pub fn forfeit(&mut self) -> &mut Player { + for construct in self.constructs.iter_mut() { + construct.force_ko(); + } + self + } + + pub fn set_win(&mut self) -> &mut Player { + self.score = Score::Win; + self + } + + pub fn set_lose(&mut self) -> &mut Player { + self.score = Score::Lose; + self + } + + pub fn construct_get(&mut self, id: Uuid) -> Result<&mut Construct, Error> { + self.constructs.iter_mut().find(|c| c.id == id).ok_or(err_msg("construct not found")) + } + + pub fn autobuy(&mut self) -> &mut Player { + let mut rng = thread_rng(); + + // skill buying phase + while self.constructs.iter().any(|c| c.skills.len() < 3) { + // find the construct with the smallest number of skills + let construct_id = match self.constructs.iter().min_by_key(|c| c.skills.len()) { + None => panic!("no constructs in autobuy"), + Some(c) => c.id, + }; + + let i = self.vbox.stash.iter() + .find(|(_i, v)| v.into_skill().is_some()) + .map(|(i, _v)| i.clone()); + + // got a skill in stash + if let Some(i) = i { + // AAAAAAAAAAAAAAAAAAAA + // there's a bad bug here where if this apply fails + // the item in question will be silently dropped + let item = self.vbox.stash.remove(&i).unwrap(); + self.vbox_apply(item, construct_id).ok(); + continue; + } + // need to buy one + else { + + // do we have any colours in store? + let colours = self.vbox.store[&ItemType::Colours].keys() + .cloned() + .collect::>(); + + // how about a base skill? + let base = match self.vbox.store[&ItemType::Skills].iter().next() { + Some(b) => Some(b.0.clone()), + None => None, + }; + + // if no: try to refill and start again + match colours.len() < 2 || base.is_none() { + true => match self.vbox_refill() { + Ok(_) => continue, + Err(_) => break, // give up + }, + false => { + let mut vbox_items = HashMap::new(); + vbox_items.insert(ItemType::Colours, colours); + vbox_items.insert(ItemType::Skills, vec![base.unwrap()]); + + match self.vbox_combine(vec![], Some(vbox_items)) { + Ok(_) => continue, + Err(_) => break, // give up + } + } + } + } + } + + // spec buying phase + while self.constructs.iter().any(|c| c.specs.len() < 3) { + // find the construct with the smallest number of skills + let construct_id = match self.constructs.iter().min_by_key(|c| c.specs.len()) { + None => panic!("no constructs in autobuy"), + Some(c) => c.id, + }; + + let i = self.vbox.stash.iter() + .find(|(_i, v)| v.into_spec().is_some()) + .map(|(i, _v)| i.clone()); + + // got a skill in stash + if let Some(i) = i { + // AAAAAAAAAAAAAAAAAAAA + // there's a bad bug here where if this apply fails + // the item in question will be silently dropped + let item = self.vbox.stash.remove(&i).unwrap(); + self.vbox_apply(item, construct_id).ok(); + continue; + } + // need to buy one + else { + // do we have any colours in store? + let colours = self.vbox.store[&ItemType::Colours].keys() + .cloned() + .collect::>(); + + // how about a base spec? + let base = match self.vbox.store[&ItemType::Specs].iter().next() { + Some(b) => Some(b.0.clone()), + None => None, + }; + + // if no: try to refill and start again + match colours.len() < 2 || base.is_none() { + true => match self.vbox_refill() { + Ok(_) => continue, + Err(_) => break, // give up + }, + false => { + let mut vbox_items = HashMap::new(); + vbox_items.insert(ItemType::Colours, colours); + vbox_items.insert(ItemType::Specs, vec![base.unwrap()]); + + match self.vbox_combine(vec![], Some(vbox_items)) { + Ok(_) => continue, + Err(_) => break, // give up + } + } + } + } + } + + // upgrading phase + // NYI + + return self; + } + + pub fn vbox_refill(&mut self) -> Result<&mut Player, Error> { + self.vbox.balance_sub(DISCARD_COST)?; + self.vbox.fill(); + Ok(self) + } + + pub fn bot_vbox_accept(&mut self, group: ItemType) -> Result<&mut Player, Error> { + let item = self.vbox.bot_buy(group)?; + self.vbox.stash_add(item, None)?; + Ok(self) + } + + pub fn vbox_buy(&mut self, group: ItemType, index: String, construct_id: Option) -> Result<&mut Player, Error> { + let item = self.vbox.buy(group, &index)?; + + match construct_id { + Some(id) => { self.vbox_apply(item, id)?; }, + None => { self.vbox.stash_add(item, None)?; }, + }; + + Ok(self) + } + + pub fn vbox_combine(&mut self, inv_indices: Vec, vbox_indices: VboxIndices) -> Result<&mut Player, Error> { + self.vbox.combine(inv_indices, vbox_indices)?; + Ok(self) + } + + pub fn vbox_refund(&mut self, index: String) -> Result<&mut Player, Error> { + self.vbox.refund(index)?; + Ok(self) + } + + pub fn vbox_equip(&mut self, index: String, construct_id: Uuid) -> Result<&mut Player, Error> { + let item = self.vbox.stash.remove(&index) + .ok_or(format_err!("no item at index {:?} {:}", self, &index))?; + + self.vbox_apply(item, construct_id) + } + + pub fn vbox_apply(&mut self, item: Item, construct_id: Uuid) -> Result<&mut Player, Error> { + match item.effect() { + Some(ItemEffect::Skill) => { + let skill = item.into_skill().ok_or(format_err!("item {:?} has no associated skill", item))?; + let construct = self.construct_get(construct_id)?; + // done here because i teach them a tonne of skills for tests + let max_skills = 3; + if construct.skills.len() >= max_skills { + return Err(format_err!("construct at max skills ({:?})", max_skills)); + } + + if construct.knows(skill) { + return Err(format_err!("construct already knows skill ({:?})" , skill)); + } + + construct.learn_mut(skill); + }, + Some(ItemEffect::Spec) => { + let spec = item.into_spec().ok_or(format_err!("item {:?} has no associated spec", item))?; + let construct = self.construct_get(construct_id)?; + construct.spec_add(spec)?; + + }, + None => return Err(err_msg("item has no effect on constructs")), + } + + // now the item has been applied + // recalculate the stats of the whole player + let player_colours = self.constructs.iter().fold(Colours::new(), |tc, c| { + Colours { + red: tc.red + c.colours.red, + green: tc.green + c.colours.green, + blue: tc.blue + c.colours.blue + } + }); + + for construct in self.constructs.iter_mut() { + construct.apply_modifiers(&player_colours); + } + + Ok(self) + } + + pub fn vbox_unequip(&mut self, target: Item, construct_id: Uuid, target_construct_id: Option) -> Result<&mut Player, Error> { + if self.vbox.stash.len() >= 9 && !target_construct_id.is_some() { + return Err(err_msg("too many items stash")); + } + + match target.effect() { + Some(ItemEffect::Skill) => { + let skill = target.into_skill().ok_or(format_err!("item {:?} has no associated skill", target))?; + let construct = self.construct_get(construct_id)?; + construct.forget(skill)?; + }, + Some(ItemEffect::Spec) => { + let spec = target.into_spec().ok_or(format_err!("item {:?} has no associated spec", target))?; + let construct = self.construct_get(construct_id)?; + construct.spec_remove(spec)?; + }, + None => return Err(err_msg("item has no effect on constructs")), + } + + // now the item has been applied + // recalculate the stats of the whole player + let player_colours = self.constructs.iter().fold(Colours::new(), |tc, c| { + Colours { + red: tc.red + c.colours.red, + green: tc.green + c.colours.green, + blue: tc.blue + c.colours.blue + } + }); + + for construct in self.constructs.iter_mut() { + construct.apply_modifiers(&player_colours); + } + + match target_construct_id { + Some(cid) => { self.vbox_apply(target, cid)?; }, + None => { self.vbox.stash_add(target, None)?; }, + }; + + Ok(self) + } + + // GAME METHODS + pub fn skills_required(&self) -> usize { + let required = self.constructs.iter() + .filter(|c| !c.is_ko()) + .filter(|c| c.available_skills().len() > 0) + .collect::>().len(); + // info!("{:} requires {:} skills this turn", self.id, required); + return required; + } + + pub fn intercepting(&self) -> Option<&Construct> { + self.constructs.iter() + .find(|c| c.affected(Effect::Intercept)) + } + + pub fn construct_by_id(&mut self, id: Uuid) -> Option<&mut Construct> { + self.constructs.iter_mut().find(|c| c.id == id) + } + +} + +#[cfg(test)] +mod tests { + use mob::instance_mobs; + use super::*; + + #[test] + fn player_bot_vbox_test() { + let player_account = Uuid::new_v4(); + let constructs = instance_mobs(player_account); + let mut player = Player::new(player_account, &"test".to_string(), constructs).set_bot(true); + + player.vbox.fill(); + player.autobuy(); + + assert!(player.constructs.iter().all(|c| c.skills.len() >= 1)); + } + + #[test] + fn player_score_test() { + let player_account = Uuid::new_v4(); + let constructs = instance_mobs(player_account); + let mut player = Player::new(player_account, &"test".to_string(), constructs).set_bot(true); + + player.score = player.score.add_win(&Score::Zero); + player.score = player.score.add_win(&Score::Zero); + player.score = player.score.add_win(&Score::Zero); + assert_eq!(player.score, Score::Win); // 40 / 0 + + // Bo7 tennis scoring + /*assert_eq!(player.score, Score::Three); // 40 / 0 + + player.score = player.score.add_loss(); // adv -> deuce + assert_eq!(player.score, Score::Three); + + player.score = player.score.add_loss(); // adv -> deuce + assert_eq!(player.score, Score::Three); + + player.score = player.score.add_win(&Score::Adv); // opp adv -> stays deuce + assert_eq!(player.score, Score::Three); + + player.score = player.score.add_win(&Score::Three); + assert_eq!(player.score, Score::Adv); + + player.score = player.score.add_win(&Score::Three); + assert_eq!(player.score, Score::Win);*/ + } + +} \ No newline at end of file diff --git a/core/src/skill.rs b/core/src/skill.rs new file mode 100644 index 00000000..13fdcf82 --- /dev/null +++ b/core/src/skill.rs @@ -0,0 +1,2188 @@ +use rand::{thread_rng, Rng}; +use uuid::Uuid; + +use util::{IntPct}; +use construct::{Construct, ConstructEffect, EffectMeta}; +use item::{Item}; + +use game::{Game}; +use effect::{Effect, Colour, Cooldown}; + +pub fn dev_resolve(a_id: Uuid, b_id: Uuid, skill: Skill) -> Resolutions { + let mut resolutions = vec![]; + + let mut a = Construct::new(); + a.id = a_id; + let mut b = Construct::new(); + b.id = b_id; + if skill.aoe() { // Send an aoe skill event for anims + resolutions.push(Resolution::new(&a, &b).event(Event::AoeSkill { skill }).stages(EventStages::StartEnd)); + } + return resolve_skill(skill, &mut a, &mut b, resolutions); +} + +pub fn resolve(cast: &Cast, game: &mut Game) -> Resolutions { + let mut resolutions = vec![]; + + let skill = cast.skill; + let source = game.construct_by_id(cast.source_construct_id).unwrap().clone(); + let targets = game.get_targets(cast.skill, &source, cast.target_construct_id); + + if skill.aoe() { // Send an aoe skill event for anims + resolutions.push(Resolution::new(&source, + &game.construct_by_id(cast.target_construct_id).unwrap().clone()).event(Event::AoeSkill { skill }).stages(EventStages::StartEnd)); + } + + for target_id in targets { + // we clone the current state of the target and source + // so we can modify them during the resolution + // no more than 1 mutable ref allowed on game + let mut source = game.construct_by_id(cast.source_construct_id).unwrap().clone(); + let mut target = game.construct_by_id(target_id).unwrap().clone(); + + // bail out on ticks that have been removed + if skill.is_tick() && target.effects.iter().find(|ce| match ce.tick { + Some(t) => t.id == cast.id, + None => false, + }).is_none() { + continue; + } + + resolutions = resolve_skill(cast.skill, &mut source, &mut target, resolutions); + + // save the changes to the game + game.update_construct(&mut source); + game.update_construct(&mut target); + + // do additional steps + resolutions = post_resolve(cast.skill, game, resolutions); + } + + return resolutions; +} + +pub fn resolve_skill(skill: Skill, source: &mut Construct, target: &mut Construct, mut resolutions: Vec) -> Resolutions { + if let Some(_disable) = source.disabled(skill) { + // resolutions.push(Resolution::new(source, target).event(Event::Disable { disable, skill }).stages(EventStages::PostOnly)); + return resolutions; + } + + if target.is_ko() { + // resolutions.push(Resolution::new(source, target).event(Event::TargetKo { skill }).stages(EventStages::PostOnly)); + return resolutions; + } + + if target.affected(Effect::Reflect) && skill.colours().contains(&Colour::Blue) && !skill.is_tick() { + // guard against overflow + if source.affected(Effect::Reflect) { + return resolutions; + } + resolutions.push(Resolution::new(source, target).event(Event::Reflection { skill })); + return resolve_skill(skill, &mut source.clone(), source, resolutions); + } + + if source.affected(Effect::Haste) { + match skill { + Skill::Slay | + Skill::SlayPlus | + Skill::SlayPlusPlus | + Skill::Chaos | + Skill::ChaosPlus | + Skill::ChaosPlusPlus | + Skill::Strike | + Skill::StrikePlus | + Skill::StrikePlusPlus => { + let amount = source.speed().pct(Skill::HasteStrike.multiplier()); + target.deal_red_damage(Skill::HasteStrike, amount) + .into_iter() + .for_each(|e| resolutions.push(Resolution::new(source, target).event(e))); + }, + _ => (), + } + } + + if source.affected(Effect::Hybrid) { + match skill { + Skill::Blast| + Skill::BlastPlus | + Skill::BlastPlusPlus | + Skill::Chaos | + Skill::ChaosPlus | + Skill::ChaosPlusPlus | + Skill::Siphon | + Skill::SiphonPlus | + Skill::SiphonPlusPlus => { + let amount = source.green_power().pct(Skill::HybridBlast.multiplier()); + target.deal_blue_damage(Skill::HybridBlast, amount) + .into_iter() + .for_each(|e| resolutions.push(Resolution::new(source, target).event(e))); + }, + _ => (), + } + } + + // match self.category() == EffectCategory::Red { + // true => { + // if let Some(evasion) = target.evade(*self) { + // resolutions.push(evasion); + // return Event; + // } + // }, + // false => (), + // } + + resolutions = match skill { + Skill::Amplify| + Skill::AmplifyPlus | + Skill::AmplifyPlusPlus => amplify(source, target, resolutions, skill), + + Skill::Banish| + Skill::BanishPlus | + Skill::BanishPlusPlus => banish(source, target, resolutions, skill), + + Skill::Bash| + Skill::BashPlus | + Skill::BashPlusPlus => bash(source, target, resolutions, skill), + + Skill::Blast| + Skill::BlastPlus | + Skill::BlastPlusPlus => blast(source, target, resolutions, skill), + + Skill::Chaos| + Skill::ChaosPlus | + Skill::ChaosPlusPlus => chaos(source, target, resolutions, skill), + + Skill::Sustain| + Skill::SustainPlus | + Skill::SustainPlusPlus => sustain(source, target, resolutions, skill), + + Skill::Electrify| + Skill::ElectrifyPlus | + Skill::ElectrifyPlusPlus => electrify(source, target, resolutions, skill), + Skill::ElectrocuteTick| + Skill::ElectrocuteTickPlus | + Skill::ElectrocuteTickPlusPlus => electrocute_tick(source, target, resolutions, skill), + + Skill::Curse| + Skill::CursePlus | + Skill::CursePlusPlus => curse(source, target, resolutions, skill), + + Skill::Decay| + Skill::DecayPlus | + Skill::DecayPlusPlus => decay(source, target, resolutions, skill), + Skill::DecayTick| + Skill::DecayTickPlus | + Skill::DecayTickPlusPlus => decay_tick(source, target, resolutions, skill), + + Skill::Haste| + Skill::HastePlus | + Skill::HastePlusPlus => haste(source, target, resolutions, skill), + + Skill::Heal| + Skill::HealPlus | + Skill::HealPlusPlus => heal(source, target, resolutions, skill), + + Skill::Absorb| + Skill::AbsorbPlus | + Skill::AbsorbPlusPlus => absorb(source, target, resolutions, skill), + + Skill::Hybrid| + Skill::HybridPlus | + Skill::HybridPlusPlus => hybrid(source, target, resolutions, skill), + + Skill::Invert| + Skill::InvertPlus | + Skill::InvertPlusPlus => invert(source, target, resolutions, skill), + + Skill::Counter| + Skill::CounterPlus | + Skill::CounterPlusPlus => counter(source, target, resolutions, skill), + + Skill::Purge| + Skill::PurgePlus | + Skill::PurgePlusPlus => purge(source, target, resolutions, skill), + + Skill::Purify| + Skill::PurifyPlus | + Skill::PurifyPlusPlus => purify(source, target, resolutions, skill), + + Skill::Recharge| + Skill::RechargePlus | + Skill::RechargePlusPlus => recharge(source, target, resolutions, skill), + + Skill::Reflect| + Skill::ReflectPlus | + Skill::ReflectPlusPlus => reflect(source, target, resolutions, skill), + + Skill::Ruin| + Skill::RuinPlus | + Skill::RuinPlusPlus => ruin(source, target, resolutions, skill), + + Skill::Link| + Skill::LinkPlus | + Skill::LinkPlusPlus => link(source, target, resolutions, skill), + + Skill::Silence| + Skill::SilencePlus | + Skill::SilencePlusPlus => silence(source, target, resolutions, skill), + + Skill::Siphon| + Skill::SiphonPlus | + Skill::SiphonPlusPlus => siphon(source, target, resolutions, skill), + Skill::SiphonTick| + Skill::SiphonTickPlus | + Skill::SiphonTickPlusPlus => siphon_tick(source, target, resolutions, skill), + + Skill::Slay| + Skill::SlayPlus | + Skill::SlayPlusPlus => slay(source, target, resolutions, skill), + + Skill::Sleep| + Skill::SleepPlus | + Skill::SleepPlusPlus => sleep(source, target, resolutions, skill), + + Skill::Restrict| + Skill::RestrictPlus | + Skill::RestrictPlusPlus => restrict(source, target, resolutions, skill), + + Skill::Strike| + Skill::StrikePlus | + Skill::StrikePlusPlus => strike(source, target, resolutions, skill), + + Skill::Intercept| + Skill::InterceptPlus | + Skill::InterceptPlusPlus => intercept(source, target, resolutions, skill), + + Skill::Break| + Skill::BreakPlus | + Skill::BreakPlusPlus => break_(source, target, resolutions, skill), + + Skill::Triage| + Skill::TriagePlus | + Skill::TriagePlusPlus => triage(source, target, resolutions, skill), + + Skill::TriageTick| + Skill::TriageTickPlus | + Skill::TriageTickPlusPlus => triage_tick(source, target, resolutions, skill), + + // Base Skills + Skill::Attack => attack(source, target, resolutions, skill), + Skill::Block => block(source, target, resolutions, skill), + Skill::Buff => buff(source, target, resolutions, skill), + Skill::Debuff => debuff(source, target, resolutions, skill), + Skill::Stun => stun(source, target, resolutions, skill), + + // Triggered + Skill::Electrocute | + Skill::ElectrocutePlus | + Skill::ElectrocutePlusPlus => panic!("should only trigger from electrify hit"), + Skill::HasteStrike => panic!("should only trigger from haste"), + Skill::Absorption| + Skill::AbsorptionPlus | + Skill::AbsorptionPlusPlus => panic!("should only trigger from absorb"), + Skill::HybridBlast => panic!("should only trigger from hybrid"), + Skill::CounterAttack| + Skill::CounterAttackPlus | + Skill::CounterAttackPlusPlus => panic!("should only trigger from counter"), + + + // Not used + }; + + return resolutions; +} + +fn post_resolve(_skill: Skill, game: &mut Game, mut resolutions: Resolutions) -> Resolutions { + for Resolution { source: event_source, target: event_target, event, stages: _ } in resolutions.clone() { + let mut source = game.construct_by_id(event_source.id).unwrap().clone(); + let mut target = game.construct_by_id(event_target.id).unwrap().clone(); + + match event { + Event::Damage { amount, skill, mitigation, colour: c } => { + if target.affected(Effect::Electric) && !skill.is_tick() { + let ConstructEffect { effect: _, duration: _, meta, tick: _ } = target.effects.iter() + .find(|e| e.effect == Effect::Electric).unwrap().clone(); + match meta { + Some(EffectMeta::Skill(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"), + }; + } + + if target.affected(Effect::Absorb) && !target.is_ko() { + let ConstructEffect { effect: _, duration: _, meta, tick: _ } = target.effects.iter() + .find(|e| e.effect == Effect::Absorb).unwrap().clone(); + match meta { + Some(EffectMeta::Skill(s)) => { + resolutions = absorption(&mut source, &mut target, resolutions, skill, amount + mitigation, s); + }, + _ => panic!("no absorb skill"), + }; + } + if c == Colour::Red { + if target.affected(Effect::Counter) && !target.is_ko() { + let ConstructEffect { effect: _, duration: _, meta, tick: _ } = target.effects.iter() + .find(|e| e.effect == Effect::Counter).unwrap().clone(); + match meta { + Some(EffectMeta::Skill(s)) => { + resolutions = counter_attack(&mut target, &mut source, resolutions, s); + }, + _ => panic!("no counter skill"), + }; + } + } + + if target.is_ko() && event_target.green == 0 { + // Make sure target ko is from this event + target.effects.clear(); + resolutions.push(Resolution::new(&source, &target).event(Event::Ko()).stages(EventStages::PostOnly)); + } + }, + _ => (), + }; + + + game.update_construct(&mut source); + game.update_construct(&mut target); + }; + + return resolutions; +} + + + +#[derive(Debug,Clone,Copy,PartialEq,Serialize,Deserialize)] +pub struct Cast { + pub id: Uuid, + pub source_player_id: Uuid, + pub source_construct_id: Uuid, + pub target_construct_id: Uuid, + pub skill: Skill, + pub speed: u64, +} + +impl Cast { + pub fn new(source_construct_id: Uuid, source_player_id: Uuid, target_construct_id: Uuid, skill: Skill) -> Cast { + return Cast { + id: Uuid::new_v4(), + source_construct_id, + source_player_id, + target_construct_id, + skill, + speed: 0, + }; + } + + pub fn new_tick(source: &mut Construct, target: &mut Construct, skill: Skill) -> Cast { + Cast { + id: Uuid::new_v4(), + source_construct_id: source.id, + source_player_id: source.account, + target_construct_id: target.id, + skill, + speed: source.skill_speed(skill), + } + } + + pub fn used_cooldown(&self) -> bool { + return self.skill.base_cd().is_some(); + } +} + +pub type Disable = Vec; +pub type Immunity = Vec; + +// used to show the progress of a construct +// while the resolutions are animating +#[derive(Debug,Clone,PartialEq,Serialize,Deserialize)] +pub struct EventConstruct { + pub id: Uuid, + pub red: u64, + pub green: u64, + pub blue: u64, +} + +#[derive(Debug,Clone,PartialEq,Serialize,Deserialize)] +pub enum EventStages { + #[serde(rename = "START_SKILL END_SKILL POST_SKILL")] + AllStages, // Anim Anim Anim + #[serde(rename = "START_SKILL END_SKILL")] + StartEnd, // Anim Anim Skip + #[serde(rename = "START_SKILL POST_SKILL")] + StartPost, // Anim Skip Anim + #[serde(rename = "START_SKILL")] + StartOnly, // Anim Skip Skip + #[serde(rename = "END_SKILL POST_SKILL")] + EndPost, // Skip Anim Anim + #[serde(rename = "END_SKILL")] + EndOnly, // Skip Anim Skip + #[serde(rename = "POST_SKILL")] + PostOnly, // Skip Skip Anim +} + +#[derive(Debug,Clone,PartialEq,Serialize,Deserialize)] +pub struct Resolution { + pub source: EventConstruct, + pub target: EventConstruct, + pub event: Event, + pub stages: EventStages, +} + +impl Resolution { + fn new(source: &Construct, target: &Construct) -> Resolution { + Resolution { + source: EventConstruct { + id: source.id, + red: source.red_life(), + green: source.green_life(), + blue: source.blue_life(), + }, + target: EventConstruct { + id: target.id, + red: target.red_life(), + green: target.green_life(), + blue: target.blue_life(), + }, + event: Event::Incomplete, + stages: EventStages::AllStages, + } + } + + fn event(mut self, e: Event) -> Resolution { + self.event = e; + self + } + + fn stages(mut self, s: EventStages) -> 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 + } + } +} + + +#[derive(Debug,Clone,PartialEq,Serialize,Deserialize)] +pub enum Event { + Disable { skill: Skill, disable: Disable }, + Immunity { skill: Skill, immunity: Immunity }, + Damage { skill: Skill, amount: u64, mitigation: u64, colour: Colour }, + Healing { skill: Skill, amount: u64, overhealing: u64 }, + Recharge { skill: Skill, red: u64, blue: u64 }, + Inversion { skill: Skill }, + Reflection { skill: Skill }, + AoeSkill { skill: Skill }, + Skill { skill: Skill }, + Effect { skill: Skill, effect: Effect, duration: u8, construct_effects: Vec }, + Removal { skill: Skill, effect: Option, construct_effects: Vec }, + TargetKo { skill: Skill }, + // skill not necessary but makes it neater as all events are arrays in js + Ko (), + Forfeit (), + Incomplete, + // not used + Evasion { skill: Skill, evasion_rating: u64 }, +} + +pub type Resolutions = Vec; + +#[derive(Debug,Clone,Copy,PartialEq,Serialize,Deserialize)] +pub enum Skill { + Attack, + Debuff, + Buff, + Block, // reduce damage + Stun, + + // Boost -- sounds nice + // Evade, // actively evade + // Nightmare, + // Sleep, + + Amplify, + #[serde(rename = "Amplify+")] + AmplifyPlus, + #[serde(rename = "Amplify++")] + AmplifyPlusPlus, + + Absorb, + #[serde(rename = "Absorb+")] + AbsorbPlus, + #[serde(rename = "Absorb++")] + AbsorbPlusPlus, + + Banish, + #[serde(rename = "Banish+")] + BanishPlus, + #[serde(rename = "Banish++")] + BanishPlusPlus, + + Bash, + #[serde(rename = "Bash+")] + BashPlus, + #[serde(rename = "Bash++")] + BashPlusPlus, + + Blast, + #[serde(rename = "Blast+")] + BlastPlus, + #[serde(rename = "Blast++")] + BlastPlusPlus, + + Chaos, + #[serde(rename = "Chaos+")] + ChaosPlus, + #[serde(rename = "Chaos++")] + ChaosPlusPlus, + + Sustain, + #[serde(rename = "Sustain+")] + SustainPlus, + #[serde(rename = "Sustain++")] + SustainPlusPlus, + + Electrify, + #[serde(rename = "Electrify+")] + ElectrifyPlus, + #[serde(rename = "Electrify++")] + ElectrifyPlusPlus, + + Curse, + #[serde(rename = "Curse+")] + CursePlus, + #[serde(rename = "Curse++")] + CursePlusPlus, + + Decay, + #[serde(rename = "Decay+")] + DecayPlus, + #[serde(rename = "Decay++")] + DecayPlusPlus, + + Haste, + #[serde(rename = "Haste+")] + HastePlus, + #[serde(rename = "Haste++")] + HastePlusPlus, + + Heal, + #[serde(rename = "Heal+")] + HealPlus, + #[serde(rename = "Heal++")] + HealPlusPlus, + + Hybrid, + #[serde(rename = "Hybrid+")] + HybridPlus, + #[serde(rename = "Hybrid++")] + HybridPlusPlus, + + Invert, + #[serde(rename = "Invert+")] + InvertPlus, + #[serde(rename = "Invert++")] + InvertPlusPlus, + + Counter, + #[serde(rename = "Counter+")] + CounterPlus, + #[serde(rename = "Counter++")] + CounterPlusPlus, + + Purge, + #[serde(rename = "Purge+")] + PurgePlus, + #[serde(rename = "Purge++")] + PurgePlusPlus, + + Purify, + #[serde(rename = "Purify+")] + PurifyPlus, + #[serde(rename = "Purify++")] + PurifyPlusPlus, + + Reflect, + #[serde(rename = "Reflect+")] + ReflectPlus, + #[serde(rename = "Reflect++")] + ReflectPlusPlus, + + Recharge, + #[serde(rename = "Recharge+")] + RechargePlus, + #[serde(rename = "Recharge++")] + RechargePlusPlus, + + Ruin, + #[serde(rename = "Ruin+")] + RuinPlus, + #[serde(rename = "Ruin++")] + RuinPlusPlus, + + Link, + #[serde(rename = "Link+")] + LinkPlus, + #[serde(rename = "Link++")] + LinkPlusPlus, + + Silence, + #[serde(rename = "Silence+")] + SilencePlus, + #[serde(rename = "Silence++")] + SilencePlusPlus, + + Slay, + #[serde(rename = "Slay+")] + SlayPlus, + #[serde(rename = "Slay++")] + SlayPlusPlus, + + Sleep, + #[serde(rename = "Sleep+")] + SleepPlus, + #[serde(rename = "Sleep++")] + SleepPlusPlus, + + Restrict, + #[serde(rename = "Restrict+")] + RestrictPlus, + #[serde(rename = "Restrict++")] + RestrictPlusPlus, + + Strike, + #[serde(rename = "Strike+")] + StrikePlus, + #[serde(rename = "Strike++")] + StrikePlusPlus, + + Siphon, + #[serde(rename = "Siphon+")] + SiphonPlus, + #[serde(rename = "Siphon++")] + SiphonPlusPlus, + + Intercept, + #[serde(rename = "Intercept+")] + InterceptPlus, + #[serde(rename = "Intercept++")] + InterceptPlusPlus, + + Break, + #[serde(rename = "Break+")] + BreakPlus, + #[serde(rename = "Break++")] + BreakPlusPlus, + + Triage, + #[serde(rename = "Triage+")] + TriagePlus, + #[serde(rename = "Triage++")] + TriagePlusPlus, + + Absorption, + #[serde(rename = "Absorption+")] + AbsorptionPlus, + #[serde(rename = "Absorption++")] + AbsorptionPlusPlus, + + CounterAttack, + #[serde(rename = "CounterAttack+")] + CounterAttackPlus, + #[serde(rename = "CounterAttack++")] + CounterAttackPlusPlus, + + Electrocute, + #[serde(rename = "Electrocute+")] + ElectrocutePlus, + #[serde(rename = "Electrocute++")] + ElectrocutePlusPlus, + ElectrocuteTick, + #[serde(rename = "ElectrocuteTick+")] + ElectrocuteTickPlus, + #[serde(rename = "ElectrocuteTick++")] + ElectrocuteTickPlusPlus, + + DecayTick, // dot + #[serde(rename = "DecayTick+")] + DecayTickPlus, + #[serde(rename = "DecayTick++")] + DecayTickPlusPlus, + + HasteStrike, + HybridBlast, + + SiphonTick, + #[serde(rename = "SiphonTick+")] + SiphonTickPlus, + #[serde(rename = "SiphonTick++")] + SiphonTickPlusPlus, + + TriageTick, + #[serde(rename = "TriageTick+")] + TriageTickPlus, + #[serde(rename = "TriageTick++")] + TriageTickPlusPlus, +} + +impl Skill { + pub fn multiplier(&self) -> u64 { + match self { + // Attack Base + Skill::Attack => 80, // Base + + Skill::Blast => 105, // BB + Skill::BlastPlus => 140, // BB + Skill::BlastPlusPlus => 200, // BB + + Skill::Chaos => 40, // BR + Skill::ChaosPlus => 65, // BR + Skill::ChaosPlusPlus => 90, // BR + + Skill::Heal => 125, //GG + Skill::HealPlus => 185, //GG + Skill::HealPlusPlus => 270, //GG + + Skill::SiphonTick => 25, // GB + Skill::SiphonTickPlus => 30, + Skill::SiphonTickPlusPlus => 40, + + Skill::Slay => 45, // RG + Skill::SlayPlus => 65, + Skill::SlayPlusPlus => 100, + + Skill::Strike => 90, //RR + Skill::StrikePlus => 140, + Skill::StrikePlusPlus => 200, + + // Block Base + Skill::ElectrocuteTick => 80, + Skill::ElectrocuteTickPlus => 100, + Skill::ElectrocuteTickPlusPlus => 130, + + Skill::CounterAttack => 120, + Skill::CounterAttackPlus => 160, + Skill::CounterAttackPlusPlus => 230, + + Skill::Purify => 45, //Green dmg (heal) + Skill::PurifyPlus => 70, + Skill::PurifyPlusPlus => 105, + + Skill::Reflect => 45, //Recharge blue life (heal) + Skill::ReflectPlus => 70, + Skill::ReflectPlusPlus => 100, + + Skill::Recharge => 70, //Recharge red and blue life (heal) + Skill::RechargePlus => 110, + Skill::RechargePlusPlus => 170, + + Skill::Sustain => 120, // Recharge red life (heal) + Skill::SustainPlus => 150, + Skill::SustainPlusPlus => 230, + + // Stun Base + Skill::Sleep => 200, //Green dmg (heal) + Skill::SleepPlus => 290, + Skill::SleepPlusPlus => 400, + + Skill::Banish => 40, //Green dmg (heal) + Skill::BanishPlus => 75, + Skill::BanishPlusPlus => 125, + + Skill::Bash => 45, + Skill::BashPlus => 65, + Skill::BashPlusPlus => 100, + + Skill::Link => 25, + Skill::LinkPlus => 40, + Skill::LinkPlusPlus => 70, + + Skill::Ruin => 40, + Skill::RuinPlus => 70, + Skill::RuinPlusPlus => 100, + + // Debuff Base + Skill::DecayTick => 33, + Skill::DecayTickPlus => 45, + Skill::DecayTickPlusPlus => 70, + + Skill::Silence => 55, // Deals more per blue skill on target + Skill::SilencePlus => 80, + Skill::SilencePlusPlus => 110, + + Skill::Restrict => 40, // Deals more per red skill on target + Skill::RestrictPlus => 65, + Skill::RestrictPlusPlus => 100, + + // Buff base + Skill::HybridBlast => 50, + + Skill::HasteStrike => 60, + + Skill::Absorb=> 95, + Skill::AbsorbPlus => 120, + Skill::AbsorbPlusPlus => 155, + + Skill::Intercept => 80, + Skill::InterceptPlus => 110, + Skill::InterceptPlusPlus => 150, + + Skill::TriageTick => 75, + Skill::TriageTickPlus => 110, + Skill::TriageTickPlusPlus => 140, + + _ => 100, + } + } + + pub fn effect(&self) -> Vec { + match self { + // Modifiers + Skill::Amplify => vec![ConstructEffect {effect: Effect::Amplify, duration: 2, + meta: Some(EffectMeta::Multiplier(150)), tick: None}], + Skill::AmplifyPlus => vec![ConstructEffect {effect: Effect::Amplify, duration: 3, + meta: Some(EffectMeta::Multiplier(175)), tick: None}], + Skill::AmplifyPlusPlus => vec![ConstructEffect {effect: Effect::Amplify, duration: 4, + meta: Some(EffectMeta::Multiplier(200)), tick: None}], + + Skill::Banish => vec![ConstructEffect {effect: Effect::Banish, duration: 2, meta: None, tick: None}], + Skill::BanishPlus => vec![ConstructEffect {effect: Effect::Banish, duration: 2, meta: None, tick: None}], + Skill::BanishPlusPlus => vec![ConstructEffect {effect: Effect::Banish, duration: 2, meta: None, tick: None}], + Skill::Block => vec![ConstructEffect {effect: Effect::Block, duration: 1, + meta: Some(EffectMeta::Multiplier(35)), tick: None}], + Skill::Buff => vec![ConstructEffect {effect: Effect::Buff, duration: 3, + meta: Some(EffectMeta::Multiplier(130)), tick: None }], + + Skill::Electrify => vec![ConstructEffect {effect: Effect::Electric, duration: 1, + meta: Some(EffectMeta::Skill(Skill::Electrocute)), tick: None}], + Skill::ElectrifyPlus => vec![ConstructEffect {effect: Effect::Electric, duration: 1, + meta: Some(EffectMeta::Skill(Skill::ElectrocutePlus)), tick: None}], + Skill::ElectrifyPlusPlus => vec![ConstructEffect {effect: Effect::Electric, duration: 1, + meta: Some(EffectMeta::Skill(Skill::ElectrocutePlusPlus)), tick: None}], + Skill::Electrocute => vec![ConstructEffect {effect: Effect::Electrocute, duration: 2, + meta: Some(EffectMeta::Skill(Skill::ElectrocuteTick)), tick: None}], + Skill::ElectrocutePlus => vec![ConstructEffect {effect: Effect::Electrocute, duration: 3, + meta: Some(EffectMeta::Skill(Skill::ElectrocuteTickPlus)), tick: None}], + Skill::ElectrocutePlusPlus => vec![ConstructEffect {effect: Effect::Electrocute, duration: 4, + meta: Some(EffectMeta::Skill(Skill::ElectrocuteTickPlusPlus)), tick: None}], + + Skill::Sustain => vec![ConstructEffect {effect: Effect::Sustain, duration: 1, meta: None, tick: None }], + Skill::SustainPlus => vec![ConstructEffect {effect: Effect::Sustain, duration: 1, meta: None, tick: None }], + Skill::SustainPlusPlus => vec![ConstructEffect {effect: Effect::Sustain, duration: 1, meta: None, tick: None }], + + Skill::Curse => vec![ConstructEffect {effect: Effect::Curse, duration: 2, + meta: Some(EffectMeta::Multiplier(150)), tick: None}], + Skill::CursePlus => vec![ConstructEffect {effect: Effect::Curse, duration: 2, + meta: Some(EffectMeta::Multiplier(200)), tick: None}], + Skill::CursePlusPlus => vec![ConstructEffect {effect: Effect::Curse, duration: 3, + meta: Some(EffectMeta::Multiplier(250)), tick: None}], + + Skill::Debuff => vec![ConstructEffect {effect: Effect::Slow, duration: 3, + meta: Some(EffectMeta::Multiplier(50)), tick: None }], + + Skill::Decay => vec![ConstructEffect {effect: Effect::Wither, duration: 3, + meta: Some(EffectMeta::Multiplier(50)), tick: None }, + ConstructEffect {effect: Effect::Decay, duration: 3, + meta: Some(EffectMeta::Skill(Skill::DecayTick)), tick: None}], + Skill::DecayPlus => vec![ConstructEffect {effect: Effect::Wither, duration: 3, + meta: Some(EffectMeta::Multiplier(35)), tick: None }, + ConstructEffect {effect: Effect::Decay, duration: 3, + meta: Some(EffectMeta::Skill(Skill::DecayTickPlus)), tick: None}], + Skill::DecayPlusPlus => vec![ConstructEffect {effect: Effect::Wither, duration: 4, + meta: Some(EffectMeta::Multiplier(20)), tick: None }, + ConstructEffect {effect: Effect::Decay, duration: 4, + meta: Some(EffectMeta::Skill(Skill::DecayTickPlusPlus)), tick: None}], + + Skill::Haste => vec![ConstructEffect {effect: Effect::Haste, duration: 3, + meta: Some(EffectMeta::Multiplier(150)), tick: None }], + Skill::HastePlus => vec![ConstructEffect {effect: Effect::Haste, duration: 4, + meta: Some(EffectMeta::Multiplier(175)), tick: None }], + Skill::HastePlusPlus => vec![ConstructEffect {effect: Effect::Haste, duration: 5, + meta: Some(EffectMeta::Multiplier(225)), tick: None }], + + Skill::Absorb => vec![ConstructEffect {effect: Effect::Absorb, duration: 1, + meta: Some(EffectMeta::Skill(Skill::Absorption)), tick: None}], + Skill::AbsorbPlus => vec![ConstructEffect {effect: Effect::Absorb, duration: 1, + meta: Some(EffectMeta::Skill(Skill::AbsorptionPlus)), tick: None}], + Skill::AbsorbPlusPlus => vec![ConstructEffect {effect: Effect::Absorb, duration: 1, + meta: Some(EffectMeta::Skill(Skill::AbsorptionPlusPlus)), tick: None}], + + Skill::Absorption => vec![ConstructEffect {effect: Effect::Absorption, duration: 3, meta: None, tick: None}], + Skill::AbsorptionPlus => vec![ConstructEffect {effect: Effect::Absorption, duration: 5, meta: None, tick: None}], + Skill::AbsorptionPlusPlus => vec![ConstructEffect {effect: Effect::Absorption, duration: 7, meta: None, tick: None}], + + Skill::Hybrid => vec![ConstructEffect {effect: Effect::Hybrid, duration: 3, + meta: Some(EffectMeta::Multiplier(150)), tick: None }], + Skill::HybridPlus => vec![ConstructEffect {effect: Effect::Hybrid, duration: 4, + meta: Some(EffectMeta::Multiplier(175)), tick: None }], + Skill::HybridPlusPlus => vec![ConstructEffect {effect: Effect::Hybrid, duration: 5, + meta: Some(EffectMeta::Multiplier(225)), tick: None }], + + Skill::Invert => vec![ConstructEffect {effect: Effect::Invert, duration: 2, meta: None, tick: None}], + Skill::InvertPlus => vec![ConstructEffect {effect: Effect::Invert, duration: 3, meta: None, tick: None}], + Skill::InvertPlusPlus => vec![ConstructEffect {effect: Effect::Invert, duration: 4, meta: None, tick: None}], + + Skill::Counter => vec![ConstructEffect {effect: Effect::Counter, duration: 1, + meta: Some(EffectMeta::Skill(Skill::CounterAttack)), tick: None}], + Skill::CounterPlus => vec![ConstructEffect {effect: Effect::Counter, duration: 1, + meta: Some(EffectMeta::Skill(Skill::CounterAttackPlus)), tick: None}], + Skill::CounterPlusPlus => vec![ConstructEffect {effect: Effect::Counter, duration: 1, + meta: Some(EffectMeta::Skill(Skill::CounterAttackPlusPlus)), tick: None}], + + Skill::Reflect => vec![ConstructEffect {effect: Effect::Reflect, duration: 1, meta: None, tick: None }], + Skill::ReflectPlus => vec![ConstructEffect {effect: Effect::Reflect, duration: 1, meta: None, tick: None }], + Skill::ReflectPlusPlus => vec![ConstructEffect {effect: Effect::Reflect, duration: 1, meta: None, tick: None }], + + Skill::Break => vec![ConstructEffect {effect: Effect::Stun, duration: 1, meta: None, tick: None}, + ConstructEffect {effect: Effect::Vulnerable, duration: 3, + meta: Some(EffectMeta::Multiplier(150)), tick: None}], + Skill::BreakPlus => vec![ConstructEffect {effect: Effect::Stun, duration: 1, meta: None, tick: None}, + ConstructEffect {effect: Effect::Vulnerable, duration: 4, + meta: Some(EffectMeta::Multiplier(200)), tick: None}], + Skill::BreakPlusPlus => vec![ConstructEffect {effect: Effect::Stun, duration: 2, meta: None, tick: None}, + ConstructEffect {effect: Effect::Vulnerable, duration: 4, + meta: Some(EffectMeta::Multiplier(250)), tick: None}], + + Skill::Ruin => vec![ConstructEffect {effect: Effect::Stun, duration: 1, meta: None, tick: None}], + Skill::RuinPlus => vec![ConstructEffect {effect: Effect::Stun, duration: 1, meta: None, tick: None}], + Skill::RuinPlusPlus => vec![ConstructEffect {effect: Effect::Stun, duration: 1, meta: None, tick: None}], + + Skill::Purge => vec![ConstructEffect {effect: Effect::Purge, duration: 2, meta: None, tick: None}], + Skill::PurgePlus => vec![ConstructEffect {effect: Effect::Purge, duration: 3, meta: None, tick: None}], + Skill::PurgePlusPlus => vec![ConstructEffect {effect: Effect::Purge, duration: 4, meta: None, tick: None}], + + Skill::Link => vec![ConstructEffect {effect: Effect::Stun, duration: 1, meta: None, tick: None}], + Skill::LinkPlus => vec![ConstructEffect {effect: Effect::Stun, duration: 1, meta: None, tick: None}], + Skill::LinkPlusPlus => vec![ConstructEffect {effect: Effect::Stun, duration: 1, meta: None, tick: None}], + + Skill::Silence => vec![ConstructEffect {effect: Effect::Silence, duration: 2, meta: None, tick: None}], + Skill::SilencePlus => vec![ConstructEffect {effect: Effect::Silence, duration: 2, meta: None, tick: None}], + Skill::SilencePlusPlus => vec![ConstructEffect {effect: Effect::Silence, duration: 2, meta: None, tick: None}], + + Skill::Siphon => vec![ConstructEffect {effect: Effect::Siphon, duration: 2, + meta: Some(EffectMeta::Skill(Skill::SiphonTick)), tick: None}], + Skill::SiphonPlus => vec![ConstructEffect {effect: Effect::Siphon, duration: 3, + meta: Some(EffectMeta::Skill(Skill::SiphonTickPlus)), tick: None}], + Skill::SiphonPlusPlus => vec![ConstructEffect {effect: Effect::Siphon, duration: 4, + meta: Some(EffectMeta::Skill(Skill::SiphonTickPlusPlus)), tick: None}], + + Skill::Sleep => vec![ConstructEffect {effect: Effect::Stun, duration: 2, meta: None, tick: None}], + Skill::SleepPlus => vec![ConstructEffect {effect: Effect::Stun, duration: 3, meta: None, tick: None}], + Skill::SleepPlusPlus => vec![ConstructEffect {effect: Effect::Stun, duration: 4, meta: None, tick: None}], + + Skill::Restrict => vec![ConstructEffect {effect: Effect::Restrict, duration: 2, meta: None, tick: None}], + Skill::RestrictPlus => vec![ConstructEffect {effect: Effect::Restrict, duration: 2, meta: None, tick: None}], + Skill::RestrictPlusPlus => vec![ConstructEffect {effect: Effect::Restrict, duration: 2, meta: None, tick: None}], + + Skill::Bash => vec![ConstructEffect {effect: Effect::Stun, duration: 2, + meta: Some(EffectMeta::Skill(Skill::Bash)), tick: None}], + Skill::BashPlus => vec![ConstructEffect {effect: Effect::Stun, duration: 2, + meta: Some(EffectMeta::Skill(Skill::BashPlus)), tick: None}], + Skill::BashPlusPlus => vec![ConstructEffect {effect: Effect::Stun, duration: 2, + meta: Some(EffectMeta::Skill(Skill::BashPlusPlus)), tick: None}], + Skill::Stun => vec![ConstructEffect {effect: Effect::Stun, duration: 2, meta: None, tick: None}], + + Skill::Intercept => vec![ConstructEffect {effect: Effect::Intercept, duration: 1, meta: None, tick: None}], + Skill::InterceptPlus => vec![ConstructEffect {effect: Effect::Intercept, duration: 1, meta: None, tick: None}], + Skill::InterceptPlusPlus => vec![ConstructEffect {effect: Effect::Intercept, duration: 1, meta: None, tick: None}], + + Skill::Triage => vec![ConstructEffect {effect: Effect::Triage, duration: 2, + meta: Some(EffectMeta::Skill(Skill::TriageTick)), tick: None}], + Skill::TriagePlus => vec![ConstructEffect {effect: Effect::Triage, duration: 3, + meta: Some(EffectMeta::Skill(Skill::TriageTickPlus)), tick: None}], + Skill::TriagePlusPlus => vec![ConstructEffect {effect: Effect::Triage, duration: 4, + meta: Some(EffectMeta::Skill(Skill::TriageTickPlusPlus)), tick: None}], + + Skill::Purify => vec![ConstructEffect { effect: Effect::Pure, duration: 2, + meta: Some(EffectMeta::Multiplier(150)), tick: None}], + Skill::PurifyPlus => vec![ConstructEffect { effect: Effect::Pure, duration: 2, + meta: Some(EffectMeta::Multiplier(175)), tick: None}], + Skill::PurifyPlusPlus => vec![ConstructEffect { effect: Effect::Pure, duration: 2, + meta: Some(EffectMeta::Multiplier(200)), tick: None}], + + _ => { + panic!("{:?} no skill effect", self); + }, + } + } + + pub fn base_cd(&self) -> Cooldown { + match self { + Skill::Attack => None, + Skill::Block => None, // reduce damage + Skill::Buff => None, + Skill::Debuff => Some(1), + Skill::Stun => Some(2), + + Skill::Strike=> None, + Skill::StrikePlus => None, + Skill::StrikePlusPlus => None, + + Skill::Counter| + Skill::CounterPlus | + Skill::CounterPlusPlus => None, // avoid all damage + + Skill::Restrict | + Skill::RestrictPlus | + Skill::RestrictPlusPlus => Some(2), + + Skill::Bash | + Skill::BashPlus | + Skill::BashPlusPlus => Some(2), + + Skill::Heal=> None, + Skill::HealPlus => None, + Skill::HealPlusPlus => None, + + Skill::Triage=> None, // hot + Skill::TriagePlus => None, // hot + Skill::TriagePlusPlus => None, // hot + + Skill::Break | // no damage stun, adds vulnerable + Skill::BreakPlus | + Skill::BreakPlusPlus => Some(1), + + Skill::Blast | + Skill::BlastPlus | + Skill::BlastPlusPlus => None, + + Skill::Chaos | + Skill::ChaosPlus | + Skill::ChaosPlusPlus => None, + + Skill::Amplify | + Skill::AmplifyPlus | + Skill::AmplifyPlusPlus => Some(1), + + Skill::Hybrid | + Skill::HybridPlus | + Skill::HybridPlusPlus => Some(1), + + Skill::Invert | + Skill::InvertPlus | + Skill::InvertPlusPlus => Some(2), + + Skill::Decay => None, // dot + Skill::DecayPlus => None, + Skill::DecayPlusPlus => None, + + Skill::Siphon| + Skill::SiphonPlus | + Skill::SiphonPlusPlus => None, + + Skill::Curse | + Skill::CursePlus | + Skill::CursePlusPlus => Some(1), + + Skill::Link | + Skill::LinkPlus | + Skill::LinkPlusPlus => Some(1), + + Skill::Silence | + Skill::SilencePlus | + Skill::SilencePlusPlus => Some(2), + + Skill::Purify | + Skill::PurifyPlus | + Skill::PurifyPlusPlus => None, + + Skill::Purge | + Skill::PurgePlus | + Skill::PurgePlusPlus => Some(1), + + Skill::Banish | + Skill::BanishPlus | + Skill::BanishPlusPlus => Some(1), + + Skill::Haste | + Skill::HastePlus | + Skill::HastePlusPlus => Some(1), + + Skill::Reflect | + Skill::ReflectPlus | + Skill::ReflectPlusPlus => None, + + Skill::Recharge | + Skill::RechargePlus | + Skill::RechargePlusPlus => None, + + Skill::Ruin | + Skill::RuinPlus | + Skill::RuinPlusPlus => Some(2), + + Skill::Slay=> None, + Skill::SlayPlus => None, + Skill::SlayPlusPlus => None, + + Skill::Sleep | + Skill::SleepPlus | + Skill::SleepPlusPlus => Some(2), + + Skill::Sustain | + Skill::SustainPlus | + Skill::SustainPlusPlus => Some(1), + + Skill::Intercept => Some(1), + Skill::InterceptPlus => Some(1), + Skill::InterceptPlusPlus => Some(1), + + Skill::Electrify | + Skill::ElectrifyPlus | + Skill::ElectrifyPlusPlus => None, + + Skill::Absorb | + Skill::AbsorbPlus | + Skill::AbsorbPlusPlus => Some(1), + + //----------- + // Never cast directly + //--------- + // Trigger + Skill::HybridBlast | + Skill::HasteStrike | + Skill::CounterAttack| + Skill::CounterAttackPlus | + Skill::CounterAttackPlusPlus | // counter + Skill::Electrocute| + Skill::ElectrocutePlus | + Skill::ElectrocutePlusPlus | + Skill::Absorption| + Skill::AbsorptionPlus | + Skill::AbsorptionPlusPlus | + // Ticks + Skill::ElectrocuteTick| + Skill::ElectrocuteTickPlus | + Skill::ElectrocuteTickPlusPlus | + Skill::DecayTick| + Skill::DecayTickPlus | + Skill::DecayTickPlusPlus | + Skill::SiphonTick| + Skill::SiphonTickPlus | + Skill::SiphonTickPlusPlus | + Skill::TriageTick| + Skill::TriageTickPlus | + Skill::TriageTickPlusPlus => None, + } + } + + pub fn ko_castable(&self) -> bool { + match self { + Skill::ElectrocuteTick | + Skill::ElectrocuteTickPlus | + Skill::ElectrocuteTickPlusPlus | + Skill::DecayTick | + Skill::DecayTickPlus | + Skill::DecayTickPlusPlus | + Skill::SiphonTick | + Skill::SiphonTickPlus | + Skill::SiphonTickPlusPlus | + + Skill::TriageTick | + Skill::TriageTickPlus | + Skill::TriageTickPlusPlus => true, + _ => false, + } + } + + pub fn is_tick(&self) -> bool { + match self { + Skill::ElectrocuteTick | + Skill::ElectrocuteTickPlus | + Skill::ElectrocuteTickPlusPlus | + Skill::DecayTick | + Skill::DecayTickPlus | + Skill::DecayTickPlusPlus | + Skill::SiphonTick | + Skill::SiphonTickPlus | + Skill::SiphonTickPlusPlus | + Skill::TriageTick | + Skill::TriageTickPlus | + Skill::TriageTickPlusPlus => true, + + _ => false, + } + } + + pub fn speed(&self) -> u64 { + match self { + Skill::SiphonTick | + Skill::SiphonTickPlus | + Skill::SiphonTickPlusPlus => Skill::Siphon.speed(), + + Skill::DecayTick | + Skill::DecayTickPlus | + Skill::DecayTickPlusPlus => Skill::Decay.speed(), + + Skill::TriageTick | + Skill::TriageTickPlus | + Skill::TriageTickPlusPlus => Skill::Triage.speed(), + + Skill::ElectrocuteTick | + Skill::ElectrocuteTickPlus | + Skill::ElectrocuteTickPlusPlus => Skill::Electrify.speed(), + + _ => Item::from(*self).speed(), + } + } + + pub fn aoe(&self) -> bool { + match self { + Skill::Ruin | + Skill::RuinPlus | + Skill::RuinPlusPlus => true, + _ => false, + } + } + + pub fn defensive(&self) -> bool { + match self { + Skill::Amplify| + Skill::AmplifyPlus | + Skill::AmplifyPlusPlus | + Skill::Block | + Skill::Sustain | + Skill::SustainPlus | + Skill::SustainPlusPlus | + Skill::Electrify | + Skill::ElectrifyPlus | + Skill::ElectrifyPlusPlus | + Skill::Haste | + Skill::HastePlus | + Skill::HastePlusPlus | + Skill::Heal | + Skill::HealPlus | + Skill::HealPlusPlus | + Skill::Absorb | + Skill::AbsorbPlus | + Skill::AbsorbPlusPlus | + Skill::Invert | + Skill::InvertPlus | + Skill::InvertPlusPlus | + Skill::Intercept | + Skill::InterceptPlus | + Skill::InterceptPlusPlus | + Skill::Counter | + Skill::CounterPlus | + Skill::CounterPlusPlus | + Skill::Purify | + Skill::PurifyPlus | + Skill::PurifyPlusPlus | + Skill::Recharge | + Skill::RechargePlus | + Skill::RechargePlusPlus | + Skill::Reflect | + Skill::ReflectPlus | + Skill::ReflectPlusPlus | + Skill::Triage | + Skill::TriagePlus | + Skill::TriagePlusPlus => true, + + _ => false, + } + } + + fn components(&self) -> Vec { + let mut components = Item::from(*self).components(); + components.sort_unstable(); + return components; + } + + pub fn colours(&self) -> Vec { + let mut components = self.components(); + let colour_items = [Item::Red, Item::Green, Item::Blue]; + components.dedup(); + return components.iter() + .filter(|i| colour_items.contains(i)) + .map(|i| i.into_colour()) + .collect::>(); + } + + fn base(&self) -> Skill { + let bases = [Item::Attack, Item::Stun, Item::Buff, Item::Debuff, Item::Block]; + match self.components() + .iter() + .find(|i| bases.contains(i)) { + Some(i) => i.into_skill().unwrap(), + None => panic!("{:?} has no base item", self), + } + } +} + +fn attack(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + let amount = source.red_power().pct(skill.multiplier()); + target.deal_red_damage(skill, amount) + .into_iter() + .for_each(|e| results.push(Resolution::new(source, target).event(e))); + + return results; +} + +fn strike(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + let amount = source.red_power().pct(skill.multiplier()); + target.deal_red_damage(skill, amount) + .into_iter() + .for_each(|e| results.push(Resolution::new(source, target).event(e))); + + return results; +} + +fn stun(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + skill.effect().into_iter() + .for_each(|e| (results.push(Resolution::new(source, target).event(target.add_effect(skill, e))))); + + return results; +} + +fn bash(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + skill.effect().into_iter() + .for_each(|e| (results.push(Resolution::new(source, target).event(target.add_effect(skill, e))))); + + if results.iter().any(|r| match r.event { + Event::Effect { effect, skill: effect_skill, duration: _, construct_effects: _ } + => effect == Effect::Stun && skill == effect_skill, + _ => false, + }) { + let mut cds = 0; + for cs in target.skills.iter_mut() { + if cs.skill.base_cd().is_some() { + cs.cd = match cs.cd { + None => Some(1), + Some(i) => Some(i + 1), + }; + + cds += 1; + } + } + + let amount = source.red_power().pct(skill.multiplier().pct(100 + 45u64.saturating_mul(cds))); + target.deal_red_damage(skill, amount) + .into_iter() + .for_each(|e| results.push(Resolution::new(source, target).event(e).stages(EventStages::PostOnly))); + } + + return results; +} + + +fn sleep(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + skill.effect().into_iter() + .for_each(|e| (results.push(Resolution::new(source, target).event(target.add_effect(skill, e))))); + + let amount = source.green_power().pct(skill.multiplier()); + target.deal_green_damage(skill, amount) + .into_iter() + .for_each(|e| results.push(Resolution::new(source, target).event(e).stages(EventStages::PostOnly))); + + return results; +} + +fn sustain(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + results.push(Resolution::new(source, target).event(target.add_effect(skill, skill.effect()[0]))); + + let red_amount = source.red_power().pct(skill.multiplier()); + target.recharge(skill, red_amount, 0) + .into_iter() + .for_each(|e| results.push(Resolution::new(source, target).event(e).stages(EventStages::PostOnly))); + + return results; +} + +fn intercept(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + let intercept = skill.effect()[0]; + results.push(Resolution::new(source, target).event(target.add_effect(skill, intercept))); + + let red_amount = source.red_power().pct(skill.multiplier()); + target.recharge(skill, red_amount, 0) + .into_iter() + .for_each(|e| results.push(Resolution::new(source, target).event(e).stages(EventStages::PostOnly))); + + return results; +} + +fn break_(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + let stun = skill.effect()[0]; + results.push(Resolution::new(source, target).event(target.add_effect(skill, stun))); + let vuln = skill.effect()[1]; + results.push(Resolution::new(source, target).event(target.add_effect(skill, vuln)).stages(EventStages::PostOnly)); + + return results; +} + +fn block(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + results.push(Resolution::new(source, target).event(target.add_effect(skill, skill.effect()[0]))); + return results; +} + +fn buff(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + results.push(Resolution::new(source, target) + .event(target.add_effect(skill, skill.effect()[0]))); + return results; +} + +fn counter(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + results.push(Resolution::new(source, target) + .event(target.add_effect(skill, skill.effect()[0]))); + + return results; +} + +fn counter_attack(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + let amount = source.red_power().pct(skill.multiplier()); + target.deal_red_damage(skill, amount) + .into_iter() + .for_each(|e| results.push(Resolution::new(source, target).event(e))); + + return results; +} + +fn restrict(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + skill.effect().into_iter() + .for_each(|e| (results.push(Resolution::new(source, target).event(target.add_effect(skill, e))))); + + let s_multi = target.skills + .iter() + .fold(100, |acc, cs| match cs.skill.colours().contains(&Colour::Red) { + true => acc + 35, + false => acc, + }); + + let amount = source.red_power().pct(skill.multiplier()).pct(s_multi); + target.deal_red_damage(skill, amount) + .into_iter() + .for_each(|e| results.push(Resolution::new(source, target).event(e).stages(EventStages::PostOnly))); + + + return results; +} + +fn slay(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + let amount = source.red_power().pct(skill.multiplier()) + source.green_power().pct(skill.multiplier()); + let slay_events = target.deal_red_damage(skill, amount); + + for e in slay_events { + match e { + Event::Damage { amount, mitigation: _, colour: _, skill: _ } => { + results.push(Resolution::new(source, target).event(e)); + let heal = source.deal_green_damage(skill, amount.pct(50)); + for h in heal { + results.push(Resolution::new(source, source).event(h).stages(EventStages::PostOnly)); + }; + }, + _ => results.push(Resolution::new(source, target).event(e)), + } + } + + return results; +} + +fn heal(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + let amount = source.green_power().pct(skill.multiplier()); + target.deal_green_damage(skill, amount) + .into_iter() + .for_each(|e| results.push(Resolution::new(source, target).event(e))); + return results; +} + +fn triage(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + let skip_tick = target.effects.iter().any(|e| { + match e.effect { + Effect::Triage => source.skill_speed(skill) <= e.tick.unwrap().speed, + _ => false, + } + }); + + let ConstructEffect { effect, duration, meta, tick: _ } = skill.effect()[0]; + let tick_skill = match meta { + Some(EffectMeta::Skill(s)) => s, + _ => panic!("no triage tick skill"), + }; + 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))); + + 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 { + let amount = source.green_power().pct(skill.multiplier()); + target.deal_green_damage(skill, amount) + .into_iter() + .for_each(|e| results.push(Resolution::new(source, target).event(e).stages(EventStages::EndPost))); + return results; +} + +fn chaos(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + let mut rng = thread_rng(); + let b_rng: u64 = rng.gen_range(100, 130); + let amount = source.blue_power().pct(skill.multiplier()).pct(b_rng); + target.deal_blue_damage(skill, amount) + .into_iter() + .for_each(|e| results.push(Resolution::new(source, target).event(e))); + let r_rng: u64 = rng.gen_range(100, 130); + let amount = source.red_power().pct(skill.multiplier()).pct(r_rng); + target.deal_red_damage(skill, amount) + .into_iter() + .for_each(|e| results.push(Resolution::new(source, target).event(e).stages(EventStages::PostOnly))); + return results; +} + +fn blast(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + let amount = source.blue_power().pct(skill.multiplier()); + target.deal_blue_damage(skill, amount) + .into_iter() + .for_each(|e| results.push(Resolution::new(source, target).event(e))); + return results; +} + +fn amplify(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + results.push(Resolution::new(source, target).event(target.add_effect(skill, skill.effect()[0]))); + return results;; +} + +fn haste(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + results.push(Resolution::new(source, target).event(target.add_effect(skill, skill.effect()[0]))); + return results;; +} + +fn debuff(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + results.push(Resolution::new(source, target).event(target.add_effect(skill, skill.effect()[0]))); + return results;; +} + +fn decay(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + + 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| { + match e.effect { + Effect::Decay => source.skill_speed(skill) <= e.tick.unwrap().speed, + _ => false, + } + }); + let ConstructEffect { effect, duration, meta, tick: _ } = skill.effect()[1]; + let tick_skill = match meta { + Some(EffectMeta::Skill(s)) => s, + _ => panic!("no decay tick skill"), + }; + let decay = ConstructEffect::new(effect, duration).set_tick(Cast::new_tick(source, target, tick_skill)); + results.push(Resolution::new(source, target) + .event(target.add_effect(skill, decay)) + .stages(EventStages::PostOnly)); + + 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 { + let amount = source.blue_power().pct(skill.multiplier()); + target.deal_blue_damage(skill, amount) + .into_iter() + .for_each(|e| results.push(Resolution::new(source, target).event(e).stages(EventStages::EndPost))); + return results; +} + +// electrify is the buff effect +// when attacked it runs electrocute and applies a debuff +fn electrify(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + let electrify = skill.effect()[0]; + results.push(Resolution::new(source, target).event(target.add_effect(skill, electrify))); + return results;; +} + +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 { skill, effect: Some(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| { + match e.effect { + Effect::Electrocute => source.skill_speed(skill) <= e.tick.unwrap().speed, + _ => false, + } + }); + 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::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 { + let amount = source.blue_power().pct(skill.multiplier()); + target.deal_blue_damage(skill, amount) + .into_iter() + .for_each(|e| results.push(Resolution::new(source, target).event(e).stages(EventStages::EndPost))); + return results; +} + +fn ruin(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + let amount = source.blue_power().pct(skill.multiplier()); + target.deal_blue_damage(skill, amount) + .into_iter() + .for_each(|e| results.push(Resolution::new(source, target).event(e).stages(EventStages::PostOnly))); + + results.push(Resolution::new(source, target) + .event(target.add_effect(skill, skill.effect()[0])) + .stages(EventStages::PostOnly)); + return results;; +} + +fn absorb(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + results.push(Resolution::new(source, target).event(target.add_effect(skill, skill.effect()[0]))); + let blue_amount = source.blue_power().pct(skill.multiplier()); + target.recharge(skill, 0, blue_amount) + .into_iter() + .for_each(|e| results.push(Resolution::new(source, target).event(e).stages(EventStages::PostOnly))); + return results;; +} + +fn absorption(source: &mut Construct, target: &mut Construct, mut results: Resolutions, reflect_skill: Skill, amount: u64, skill: Skill) -> Resolutions { + let absorb = skill.effect()[0].set_meta(EffectMeta::AddedDamage(amount)); + + results.push(Resolution::new(source, target) + .event(target.add_effect(reflect_skill, absorb)) + .stages(EventStages::PostOnly)); + + let absorb_index = target.effects.iter().position(|e| e.effect == Effect::Absorb).expect("No absorb"); + let ce = target.effects.remove(absorb_index); + + results.push(Resolution::new(source, target) + .event(Event::Removal { skill, effect: Some(ce.effect), construct_effects: target.effects.clone() }) + .stages(EventStages::PostOnly)); + return results;; +} + +fn curse(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + results.push(Resolution::new(source, target).event(target.add_effect(skill, skill.effect()[0]))); + return results;; +} + +fn hybrid(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + results.push(Resolution::new(source, target).event(target.add_effect(skill, skill.effect()[0]))); + return results;; +} + +fn invert(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + results.push(Resolution::new(source, target).event(target.add_effect(skill, skill.effect()[0]))); + return results;; +} + +fn reflect(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + results.push(Resolution::new(source, target).event(target.add_effect(skill, skill.effect()[0]))); + + let blue_amount = source.blue_power().pct(skill.multiplier()); + target.recharge(skill, 0, blue_amount) + .into_iter() + .for_each(|e| results.push(Resolution::new(source, target).event(e).stages(EventStages::PostOnly))); + + return results;; +} + +fn recharge(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + results.push(Resolution::new(source, target).event(Event::Skill { skill }).stages(EventStages::StartEnd)); + let red_amount = source.red_power().pct(skill.multiplier()); + let blue_amount = source.blue_power().pct(skill.multiplier()); + target.recharge(skill, red_amount, blue_amount) + .into_iter() + .for_each(|e| results.push(Resolution::new(source, target).event(e).stages(EventStages::PostOnly))); + + return results; +} + +fn siphon(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + + let skip_tick = target.effects.iter().any(|e| { + match e.effect { + Effect::Siphon => source.skill_speed(skill) <= e.tick.unwrap().speed, + _ => false, + } + }); + let ConstructEffect { effect, duration, meta, tick: _ } = skill.effect()[0]; + let tick_skill = match meta { + Some(EffectMeta::Skill(s)) => s, + _ => panic!("no siphon tick skill"), + }; + 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))); + + 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 { + let amount = source.blue_power().pct(skill.multiplier()) + source.green_power().pct(skill.multiplier()); + let siphon_events = target.deal_blue_damage(skill, amount); + + for e in siphon_events { + match e { + Event::Damage { amount, mitigation: _, colour: _, skill: _ } => { + results.push(Resolution::new(source, target).event(e).stages(EventStages::EndPost)); + let heal = source.deal_green_damage(skill, amount); + for h in heal { + results.push(Resolution::new(source, source).event(h).stages(EventStages::PostOnly)); + }; + }, + _ => results.push(Resolution::new(source, target).event(e).stages(EventStages::EndPost)), + } + } + + return results; +} + +fn link(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + results.push(Resolution::new(source, target).event(target.add_effect(skill, skill.effect()[0]))); + + let amount = source.blue_power().pct(skill.multiplier().saturating_mul(target.effects.len() as u64)); + target.deal_blue_damage(skill, amount) + .into_iter() + .for_each(|e| results.push(Resolution::new(source, target).event(e).stages(EventStages::PostOnly))); + + return results; +} + +fn silence(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + results.push(Resolution::new(source, target).event(target.add_effect(skill, skill.effect()[0]))); + + let s_multi = target.skills + .iter() + .fold(100, |acc, cs| match cs.skill.colours().contains(&Colour::Blue) { + true => acc + 45, + false => acc, + }); + + let amount = source.blue_power().pct(skill.multiplier()).pct(s_multi); + target.deal_blue_damage(skill, amount) + .into_iter() + .for_each(|e| results.push(Resolution::new(source, target).event(e).stages(EventStages::PostOnly))); + + return results; +} + +fn purge(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + results.push(Resolution::new(source, target).event(Event::Skill { skill }).stages(EventStages::StartEnd)); + if target.effects.len() > 0 { + target.effects.clear(); + results.push(Resolution::new(source, target) + .event(Event::Removal { skill, effect: None, construct_effects: target.effects.clone() }) + .stages(EventStages::PostOnly)); + } + + let effect = skill.effect()[0]; + results.push(Resolution::new(source, target).event(target.add_effect(skill, effect)).stages(EventStages::PostOnly)); + + return results; +} + +fn purify(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + results.push(Resolution::new(source, target).event(Event::Skill { skill }).stages(EventStages::StartEnd)); + if target.effects.len() > 0 { + let amount = source.green_power().pct(skill.multiplier().saturating_mul(target.effects.len() as u64)); + target.effects.clear(); + results.push(Resolution::new(source, target) + .event(Event::Removal { skill, effect: None, construct_effects: target.effects.clone() }) + .stages(EventStages::PostOnly)); + target.deal_green_damage(skill, amount) + .into_iter() + .for_each(|e| results.push(Resolution::new(source, target).event(e).stages(EventStages::PostOnly))); + } + let effect = skill.effect()[0]; + results.push(Resolution::new(source, target).event(target.add_effect(skill, effect)).stages(EventStages::PostOnly)); + + return results; +} + +fn banish(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { + results.push(Resolution::new(source, target).event(Event::Skill { skill }).stages(EventStages::StartEnd)); + + let red_damage = target.red_life().pct(skill.multiplier()); + let blue_damage = target.blue_life().pct(skill.multiplier()); + + if red_damage > 0 { + target.deal_red_damage(skill, red_damage) + .into_iter() + .for_each(|e| results.push(Resolution::new(source, target).event(e).stages(EventStages::PostOnly))); + } + + if blue_damage > 0 { + target.deal_blue_damage(skill, blue_damage) + .into_iter() + .for_each(|e| results.push(Resolution::new(source, target).event(e).stages(EventStages::PostOnly))); + } + + results.push(Resolution::new(source, target).event(target.add_effect(skill, skill.effect()[0])).stages(EventStages::PostOnly)); + return results; +} + +#[cfg(test)] +mod tests { + use skill::*; + + #[test] + fn heal_test() { + let mut x = Construct::new() + .named(&"muji".to_string()) + .learn(Skill::Heal); + + let mut y = Construct::new() + .named(&"camel".to_string()) + .learn(Skill::Heal); + + x.deal_red_damage(Skill::Attack, 5); + + heal(&mut y, &mut x, vec![], Skill::Heal); + } + + #[test] + fn decay_test() { + let mut x = Construct::new() + .named(&"muji".to_string()); + + let mut y = Construct::new() + .named(&"camel".to_string()); + + decay(&mut x, &mut y, vec![], Skill::Decay); + + assert!(y.effects.iter().any(|e| e.effect == Effect::Decay)); + + y.reduce_effect_durations(); + let _decay = y.effects.iter().find(|e| e.effect == Effect::Decay); + // assert!(y.green_life() == y.green_life().saturating_sub(decay.unwrap().tick.unwrap().amount)); + } + + #[test] + fn block_test() { + let mut x = Construct::new() + .named(&"muji".to_string()); + + let mut y = Construct::new() + .named(&"camel".to_string()); + + // ensure it doesn't have 0 pd + x.red_power.force(100); + y.green_life.force(500); + + block(&mut y.clone(), &mut y, vec![], Skill::Block); + assert!(y.effects.iter().any(|e| e.effect == Effect::Block)); + + let mut results = attack(&mut x, &mut y, vec![], Skill::Attack); + + let Resolution { source: _, target: _, event, stages: _ } = results.remove(0); + match event { + Event::Damage { amount, mitigation: _, colour: _, skill: _ } => + assert!(amount < x.red_power().pct(Skill::Attack.multiplier())), + _ => panic!("not damage"), + }; + } + + #[test] + fn sustain_test() { + let mut x = Construct::new() + .named(&"muji".to_string()); + + let mut y = Construct::new() + .named(&"camel".to_string()); + + x.red_power.force(10000000000000); // multiplication of int max will cause overflow + y.green_life.force(1024); // make tests more flexible if we change stats + + sustain(&mut y.clone(), &mut y, vec![], Skill::Sustain); + assert!(y.affected(Effect::Sustain)); + + let mut results = ruin(&mut x, &mut y, vec![], Skill::Ruin); + let Resolution { source: _, target: _, event, stages: _ } = results.remove(0); + match event { + Event::Immunity { skill: _, immunity } => assert!(immunity.contains(&Effect::Sustain)), + _ => panic!("not immune cluthc"), + }; + + let mut results = attack(&mut x, &mut y, vec![], Skill::Attack); + assert!(y.green_life() == 1); + + let Resolution { source: _, target: _, event, stages: _ } = results.remove(0); + match event { + Event::Damage { amount, mitigation: _, colour: _, skill: _ } => assert_eq!(amount, 1023), + _ => panic!("not damage"), + }; + } + + #[test] + fn invert_test() { + let mut x = Construct::new() + .named(&"muji".to_string()); + + let mut y = Construct::new() + .named(&"camel".to_string()); + + // give red shield but reduce to 0 + y.red_life.force(64); + y.red_life.reduce(64); + x.red_power.force(512); + invert(&mut y.clone(), &mut y, vec![], Skill::Invert); + assert!(y.affected(Effect::Invert)); + + // heal should deal green damage + heal(&mut x, &mut y, vec![], Skill::Heal); + assert!(y.green_life() < 1024); + + // attack should heal and recharge red shield + let mut results = attack(&mut x, &mut y, vec![], Skill::Attack); + + // match results.remove(0).event { + // Event::Inversion { skill } => assert_eq!(skill, Skill::Attack), + // _ => panic!("not inversion"), + //}; + + match results.remove(0).event { + Event::Healing { skill: _, overhealing: _, amount } => assert!(amount > 0), + _ => panic!("not healing from inversion"), + }; + + match results.remove(0).event { + Event::Recharge { skill: _, red, blue: _ } => assert!(red > 0), + _ => panic!("not recharge from inversion"), + }; + } + + #[test] + fn reflect_test() { + let mut x = Construct::new() + .named(&"muji".to_string()); + + let mut y = Construct::new() + .named(&"camel".to_string()); + + reflect(&mut y.clone(), &mut y, vec![], Skill::Reflect); + assert!(y.affected(Effect::Reflect)); + + let mut results = vec![]; + results = resolve_skill(Skill::Blast, &mut x, &mut y, results); + + assert!(x.green_life() < 1024); + + let Resolution { source: _, target: _, event, stages: _ } = results.remove(0); + match event { + Event::Reflection { skill } => assert_eq!(skill, Skill::Blast), + _ => panic!("not reflection"), + }; + + let Resolution { source: _, target: _, event, stages: _ } = results.remove(0); + match event { + Event::Damage { amount, mitigation: _, colour: _, skill: _ } => assert!(amount > 0), + _ => panic!("not damage"), + }; + } + + #[test] + fn siphon_test() { + let mut x = Construct::new() + .named(&"muji".to_string()); + + let mut y = Construct::new() + .named(&"camel".to_string()); + + x.blue_power.force(256); + x.green_power.force(220); + x.green_life.force(1024); + y.blue_life.force(0); + x.green_life.reduce(512); + + let mut results = resolve_skill(Skill::Siphon, &mut x, &mut y, vec![]); + + assert!(y.affected(Effect::Siphon)); + assert!(x.green_life() == (512 + 256.pct(Skill::SiphonTick.multiplier()) + 220.pct(Skill::SiphonTick.multiplier()))); + + let Resolution { source: _, target: _, event, stages: _ } = results.remove(0); + match event { + Event::Effect { effect, skill: _, duration: _, construct_effects: _ } => assert_eq!(effect, Effect::Siphon), + _ => panic!("not siphon"), + }; + + let Resolution { source: _, target: _, event, stages: _ } = results.remove(0); + match event { + Event::Damage { amount, skill: _, mitigation: _, colour: _} => assert_eq!(amount, 256.pct(Skill::SiphonTick.multiplier()) + + 220.pct(Skill::SiphonTick.multiplier())), + _ => panic!("not damage siphon"), + }; + + let Resolution { source: _, target, event, stages: _ } = results.remove(0); + match event { + Event::Healing { amount, skill: _, overhealing: _ } => { + assert_eq!(amount, 256.pct(Skill::SiphonTick.multiplier()) + 220.pct(Skill::SiphonTick.multiplier())); + assert_eq!(target.id, x.id); + }, + _ => panic!("not healing"), + }; + } + + #[test] + fn triage_test() { + let mut x = Construct::new() + .named(&"muji".to_string()); + + let mut y = Construct::new() + .named(&"pretaliation".to_string()); + + // ensure it doesn't have 0 sd + x.blue_power.force(50); + + // remove all mitigation + y.red_life.force(0); + y.blue_life.force(0); + + y.deal_red_damage(Skill::Attack, 5); + let prev_hp = y.green_life(); + + triage(&mut x, &mut y, vec![], Skill::Triage); + + assert!(y.effects.iter().any(|e| e.effect == Effect::Triage)); + assert!(y.green_life() > prev_hp); + } + + #[test] + fn recharge_test() { + let mut x = Construct::new() + .named(&"muji".to_string()); + + let mut y = Construct::new() + .named(&"pretaliation".to_string()); + + y.red_life.force(50); + y.blue_life.force(50); + + y.deal_red_damage(Skill::Attack, 5); + y.deal_blue_damage(Skill::Blast, 5); + + let mut results = recharge(&mut x, &mut y, vec![], Skill::Recharge); + + results.remove(0); + let Resolution { source: _, target: _, event, stages: _ } = results.remove(0); + match event { + Event::Recharge { red, blue, skill: _ } => { + assert!(red == 5); + assert!(blue == 5); + } + _ => panic!("result was not recharge"), + } + } + + + #[test] + fn silence_test() { + let mut x = Construct::new() + .named(&"muji".to_string()); + + silence(&mut x.clone(), &mut x, vec![], Skill::Silence); + assert!(x.effects.iter().any(|e| e.effect == Effect::Silence)); + assert!(x.disabled(Skill::Silence).is_some()); + } + + #[test] + fn amplify_test() { + let mut x = Construct::new() + .named(&"muji".to_string()); + + x.blue_power.force(50); + + amplify(&mut x.clone(), &mut x, vec![], Skill::Amplify); + assert!(x.effects.iter().any(|e| e.effect == Effect::Amplify)); + assert_eq!(x.blue_power(), 75); + } + + #[test] + fn purify_test() { + let mut x = Construct::new() + .named(&"muji".to_string()); + + decay(&mut x.clone(), &mut x, vec![], Skill::Decay); + assert!(x.effects.iter().any(|e| e.effect == Effect::Decay)); + + purify(&mut x.clone(), &mut x, vec![], Skill::Purify); + assert!(!x.effects.iter().any(|e| e.effect == Effect::Decay)); + } + + #[test] + fn bash_test() { + let mut x = Construct::new() + .named(&"muji".to_string()); + + let mut y = Construct::new() + .named(&"pretaliation".to_string()) + .learn(Skill::Stun); + + let stun_cd = y.skills.iter().find(|cs| cs.skill == Skill::Stun).unwrap().cd.unwrap(); + + bash(&mut x, &mut y, vec![], Skill::Bash); + assert!(!x.effects.iter().any(|e| e.effect == Effect::Stun)); + assert!(y.skills.iter().any(|cs| cs.skill == Skill::Stun && cs.cd.unwrap() == stun_cd + 1)); + } + + #[test] + fn purge_test() { + let mut x = Construct::new() + .named(&"muji".to_string()); + + let mut y = Construct::new() + .named(&"pretaliation".to_string()) + .learn(Skill::Heal) + .learn(Skill::HealPlus); + + purge(&mut x, &mut y, vec![], Skill::Purge); + // 2 turns at lvl 1 + assert!(y.effects.iter().any(|e| e.effect == Effect::Purge && e.duration == 2)); + assert!(y.disabled(Skill::Heal).is_some()); + } +} diff --git a/core/src/spec.rs b/core/src/spec.rs new file mode 100644 index 00000000..736eaba1 --- /dev/null +++ b/core/src/spec.rs @@ -0,0 +1,734 @@ +use construct::{Stat, Colours}; +use util::{IntPct}; + +#[derive(Debug,Clone,Serialize,Deserialize)] +pub struct SpecBonus { + pub req: Colours, + pub bonus: u64, +} + +impl SpecBonus { + pub fn get_bonus(&self, c: &Colours) -> u64 { + if c.red >= self.req.red && c.blue >= self.req.blue && c.green >= self.req.green { + return self.bonus; + } + return 0; + } +} + +#[derive(Debug,Clone,Serialize,Deserialize)] +pub struct SpecValues { + pub base: u64, + pub bonuses: Vec, +} + +impl SpecValues { + pub fn max_value (&self, c: &Colours) -> u64 { + self.bonuses.iter().fold(self.base, |acc, s| acc + s.get_bonus(c)) + } + + pub fn base (self) -> u64 { + self.base + } +} + + +#[derive(Debug,Copy,Clone,Serialize,Deserialize,PartialEq,PartialOrd,Ord,Eq)] +pub enum Spec { + Speed, + SpeedRR, + SpeedBB, + SpeedGG, + SpeedRG, + SpeedGB, + SpeedRB, + + SpeedRRPlus, + SpeedBBPlus, + SpeedGGPlus, + SpeedRGPlus, + SpeedGBPlus, + SpeedRBPlus, + + SpeedRRPlusPlus, + SpeedBBPlusPlus, + SpeedGGPlusPlus, + SpeedRGPlusPlus, + SpeedGBPlusPlus, + SpeedRBPlusPlus, + + Life, + LifeGG, + LifeRR, + LifeBB, + LifeRG, + LifeGB, + LifeRB, + LifeGGPlus, + LifeRRPlus, + LifeBBPlus, + LifeRGPlus, + LifeGBPlus, + LifeRBPlus, + LifeGGPlusPlus, + LifeRRPlusPlus, + LifeBBPlusPlus, + LifeRGPlusPlus, + LifeGBPlusPlus, + LifeRBPlusPlus, + + Power, + PowerRR, + PowerGG, + PowerBB, + PowerRG, + PowerGB, + PowerRB, + PowerRRPlus, + PowerGGPlus, + PowerBBPlus, + PowerRGPlus, + PowerGBPlus, + PowerRBPlus, + PowerRRPlusPlus, + PowerGGPlusPlus, + PowerBBPlusPlus, + PowerRGPlusPlus, + PowerGBPlusPlus, + PowerRBPlusPlus, + +} + +impl Spec { + pub fn affects(&self) -> Vec { + match *self { + Spec::Power => vec![Stat::BluePower, Stat::RedPower, Stat::GreenPower], + Spec::PowerRR => vec![Stat::RedPower], + Spec::PowerGG => vec![Stat::GreenPower], + Spec::PowerBB => vec![Stat::BluePower], + Spec::PowerRG => vec![Stat::GreenPower, Stat::RedPower], + Spec::PowerGB => vec![Stat::GreenPower, Stat::BluePower], + Spec::PowerRB => vec![Stat::RedPower, Stat::BluePower], + Spec::PowerRRPlus => vec![Stat::RedPower], + Spec::PowerGGPlus => vec![Stat::GreenPower], + Spec::PowerBBPlus => vec![Stat::BluePower], + Spec::PowerRGPlus => vec![Stat::GreenPower, Stat::RedPower], + Spec::PowerGBPlus => vec![Stat::GreenPower, Stat::BluePower], + Spec::PowerRBPlus => vec![Stat::RedPower, Stat::BluePower], + Spec::PowerRRPlusPlus => vec![Stat::RedPower], + Spec::PowerGGPlusPlus => vec![Stat::GreenPower], + Spec::PowerBBPlusPlus => vec![Stat::BluePower], + Spec::PowerRGPlusPlus => vec![Stat::GreenPower, Stat::RedPower], + Spec::PowerGBPlusPlus => vec![Stat::GreenPower, Stat::BluePower], + Spec::PowerRBPlusPlus => vec![Stat::RedPower, Stat::BluePower], + + Spec::Speed => vec![Stat::Speed], + Spec::SpeedRR => vec![Stat::Speed], + Spec::SpeedBB => vec![Stat::Speed], + Spec::SpeedGG => vec![Stat::Speed], + Spec::SpeedRG => vec![Stat::Speed], + Spec::SpeedGB => vec![Stat::Speed], + Spec::SpeedRB => vec![Stat::Speed], + Spec::SpeedRRPlus => vec![Stat::Speed], + Spec::SpeedBBPlus => vec![Stat::Speed], + Spec::SpeedGGPlus => vec![Stat::Speed], + Spec::SpeedRGPlus => vec![Stat::Speed], + Spec::SpeedGBPlus => vec![Stat::Speed], + Spec::SpeedRBPlus => vec![Stat::Speed], + Spec::SpeedRRPlusPlus => vec![Stat::Speed], + Spec::SpeedBBPlusPlus => vec![Stat::Speed], + Spec::SpeedGGPlusPlus => vec![Stat::Speed], + Spec::SpeedRGPlusPlus => vec![Stat::Speed], + Spec::SpeedGBPlusPlus => vec![Stat::Speed], + Spec::SpeedRBPlusPlus => vec![Stat::Speed], + + Spec::Life => vec![Stat::GreenLife], + Spec::LifeRR => vec![Stat::RedLife], + Spec::LifeBB => vec![Stat::BlueLife], + Spec::LifeGG => vec![Stat::GreenLife], + Spec::LifeRG => vec![Stat::GreenLife, Stat::RedLife], + Spec::LifeGB => vec![Stat::GreenLife, Stat::BlueLife], + Spec::LifeRB => vec![Stat::BlueLife, Stat::RedLife], + Spec::LifeRRPlus => vec![Stat::RedLife], + Spec::LifeBBPlus => vec![Stat::BlueLife], + Spec::LifeGGPlus => vec![Stat::GreenLife], + Spec::LifeRGPlus => vec![Stat::GreenLife, Stat::RedLife], + Spec::LifeGBPlus => vec![Stat::GreenLife, Stat::BlueLife], + Spec::LifeRBPlus => vec![Stat::BlueLife, Stat::RedLife], + Spec::LifeRRPlusPlus => vec![Stat::RedLife], + Spec::LifeBBPlusPlus => vec![Stat::BlueLife], + Spec::LifeGGPlusPlus => vec![Stat::GreenLife], + Spec::LifeRGPlusPlus => vec![Stat::GreenLife, Stat::RedLife], + Spec::LifeGBPlusPlus => vec![Stat::GreenLife, Stat::BlueLife], + Spec::LifeRBPlusPlus => vec![Stat::BlueLife, Stat::RedLife], + } + } + + pub fn values(&self) -> SpecValues { + match *self { + Spec::Power => SpecValues { + base: 10, + bonuses: vec![] + }, + + Spec::PowerRR=> SpecValues { + base: 25, + bonuses: vec![ + SpecBonus { req: Colours { red: 5, green: 0, blue: 0 }, bonus: 10 }, + SpecBonus { req: Colours { red: 10, green: 0, blue: 0 }, bonus: 15 }, + SpecBonus { req: Colours { red: 20, green: 0, blue: 0 }, bonus: 20 } + ], + }, + + Spec::PowerGG=> SpecValues { + base: 25, + bonuses: vec![ + SpecBonus { req: Colours { red: 0, green: 5, blue: 0 }, bonus: 10 }, + SpecBonus { req: Colours { red: 0, green: 10, blue: 0 }, bonus: 15 }, + SpecBonus { req: Colours { red: 0, green: 20, blue: 0 }, bonus: 20 } + ], + }, + + Spec::PowerBB=> SpecValues { + base: 25, + bonuses: vec![ + SpecBonus { req: Colours { red: 0, green: 0, blue: 5 }, bonus: 10 }, + SpecBonus { req: Colours { red: 0, green: 0, blue: 10 }, bonus: 15 }, + SpecBonus { req: Colours { red: 0, green: 0, blue: 20 }, bonus: 20 } + ], + }, + + Spec::PowerRG=> SpecValues { + base: 20, + bonuses: vec![ + SpecBonus { req: Colours { red: 2, green: 2, blue: 0 }, bonus: 5 }, + SpecBonus { req: Colours { red: 5, green: 5, blue: 0 }, bonus: 10 }, + SpecBonus { req: Colours { red: 10, green: 10, blue: 0 }, bonus: 15 } + ], + }, + + Spec::PowerGB=> SpecValues { + base: 20, + bonuses: vec![ + SpecBonus { req: Colours { red: 0, green: 2, blue: 2 }, bonus: 5 }, + SpecBonus { req: Colours { red: 0, green: 5, blue: 5 }, bonus: 10 }, + SpecBonus { req: Colours { red: 0, green: 10, blue: 10 }, bonus: 15 } + ], + }, + + Spec::PowerRB=> SpecValues { + base: 20, + bonuses: vec![ + SpecBonus { req: Colours { red: 2, green: 0, blue: 2 }, bonus: 5 }, + SpecBonus { req: Colours { red: 5, green: 0, blue: 5 }, bonus: 10 }, + SpecBonus { req: Colours { red: 10, green: 0, blue: 10 }, bonus: 15 } + ], + }, + + Spec::PowerRRPlus => SpecValues { + base: 45, + bonuses: vec![ + SpecBonus { req: Colours { red: 5, green: 0, blue: 0 }, bonus: 15 }, + SpecBonus { req: Colours { red: 10, green: 0, blue: 0 }, bonus: 25 }, + SpecBonus { req: Colours { red: 20, green: 0, blue: 0 }, bonus: 35 } + ], + }, + + Spec::PowerGGPlus => SpecValues { + base: 45, + bonuses: vec![ + SpecBonus { req: Colours { red: 0, green: 5, blue: 0 }, bonus: 15 }, + SpecBonus { req: Colours { red: 0, green: 10, blue: 0 }, bonus: 25 }, + SpecBonus { req: Colours { red: 0, green: 20, blue: 0 }, bonus: 35 } + ], + }, + + Spec::PowerBBPlus => SpecValues { + base: 45, + bonuses: vec![ + SpecBonus { req: Colours { red: 0, green: 0, blue: 5 }, bonus: 15 }, + SpecBonus { req: Colours { red: 0, green: 0, blue: 10 }, bonus: 25 }, + SpecBonus { req: Colours { red: 0, green: 0, blue: 20 }, bonus: 35 } + ], + }, + + Spec::PowerRGPlus => SpecValues { + base: 35, + bonuses: vec![ + SpecBonus { req: Colours { red: 2, green: 2, blue: 0 }, bonus: 10 }, + SpecBonus { req: Colours { red: 5, green: 5, blue: 0 }, bonus: 20 }, + SpecBonus { req: Colours { red: 10, green: 10, blue: 0 }, bonus: 25 } + ], + }, + + Spec::PowerGBPlus => SpecValues { + base: 35, + bonuses: vec![ + SpecBonus { req: Colours { red: 0, green: 2, blue: 2 }, bonus: 10 }, + SpecBonus { req: Colours { red: 0, green: 5, blue: 5 }, bonus: 20 }, + SpecBonus { req: Colours { red: 0, green: 10, blue: 10 }, bonus: 25 } + ], + }, + + Spec::PowerRBPlus => SpecValues { + base: 35, + bonuses: vec![ + SpecBonus { req: Colours { red: 2, green: 0, blue: 2 }, bonus: 10 }, + SpecBonus { req: Colours { red: 5, green: 0, blue: 5 }, bonus: 20 }, + SpecBonus { req: Colours { red: 10, green: 0, blue: 10 }, bonus: 25 } + ], + }, + Spec::PowerRRPlusPlus => SpecValues { + base: 80, + bonuses: vec![ + SpecBonus { req: Colours { red: 5, green: 0, blue: 0 }, bonus: 25 }, + SpecBonus { req: Colours { red: 10, green: 0, blue: 0 }, bonus: 45 }, + SpecBonus { req: Colours { red: 20, green: 0, blue: 0 }, bonus: 60 } + ], + }, + + Spec::PowerGGPlusPlus => SpecValues { + base: 80, + bonuses: vec![ + SpecBonus { req: Colours { red: 0, green: 5, blue: 0 }, bonus: 25 }, + SpecBonus { req: Colours { red: 0, green: 10, blue: 0 }, bonus: 45 }, + SpecBonus { req: Colours { red: 0, green: 20, blue: 0 }, bonus: 60 } + ], + }, + + Spec::PowerBBPlusPlus => SpecValues { + base: 80, + bonuses: vec![ + SpecBonus { req: Colours { red: 0, green: 0, blue: 5 }, bonus: 25 }, + SpecBonus { req: Colours { red: 0, green: 0, blue: 10 }, bonus: 45 }, + SpecBonus { req: Colours { red: 0, green: 0, blue: 20 }, bonus: 60 } + ], + }, + + Spec::PowerRGPlusPlus => SpecValues { + base: 60, + bonuses: vec![ + SpecBonus { req: Colours { red: 2, green: 2, blue: 0 }, bonus: 20 }, + SpecBonus { req: Colours { red: 5, green: 5, blue: 0 }, bonus: 30 }, + SpecBonus { req: Colours { red: 10, green: 10, blue: 0 }, bonus: 45 } + ], + }, + + Spec::PowerGBPlusPlus => SpecValues { + base: 60, + bonuses: vec![ + SpecBonus { req: Colours { red: 0, green: 2, blue: 2 }, bonus: 20 }, + SpecBonus { req: Colours { red: 0, green: 5, blue: 5 }, bonus: 30 }, + SpecBonus { req: Colours { red: 0, green: 10, blue: 10 }, bonus: 45 } + ], + }, + + Spec::PowerRBPlusPlus => SpecValues { + base: 60, + bonuses: vec![ + SpecBonus { req: Colours { red: 2, green: 0, blue: 2 }, bonus: 20 }, + SpecBonus { req: Colours { red: 5, green: 0, blue: 5 }, bonus: 30 }, + SpecBonus { req: Colours { red: 10, green: 0, blue: 10 }, bonus: 45 } + ], + }, + + Spec::Speed => SpecValues { + base: 40, + bonuses: vec![] + }, + + Spec::SpeedRR=> SpecValues { + base: 80, + bonuses: vec![ + SpecBonus { req: Colours { red: 5, green: 0, blue: 0 }, bonus: 80 }, + SpecBonus { req: Colours { red: 10, green: 0, blue: 0 }, bonus: 80 }, + SpecBonus { req: Colours { red: 20, green: 0, blue: 0 }, bonus: 80 } + ], + }, + + Spec::SpeedGG=> SpecValues { + base: 80, + bonuses: vec![ + SpecBonus { req: Colours { red: 0, green: 5, blue: 0 }, bonus: 80 }, + SpecBonus { req: Colours { red: 0, green: 10, blue: 0 }, bonus: 80 }, + SpecBonus { req: Colours { red: 0, green: 20, blue: 0 }, bonus: 80 } + ], + }, + + Spec::SpeedBB=> SpecValues { + base: 80, + bonuses: vec![ + SpecBonus { req: Colours { red: 0, green: 0, blue: 5 }, bonus: 80 }, + SpecBonus { req: Colours { red: 0, green: 0, blue: 10 }, bonus: 80 }, + SpecBonus { req: Colours { red: 0, green: 0, blue: 20 }, bonus: 80 } + ], + }, + + Spec::SpeedRG=> SpecValues { + base: 60, + bonuses: vec![ + SpecBonus { req: Colours { red: 2, green: 2, blue: 0 }, bonus: 60 }, + SpecBonus { req: Colours { red: 5, green: 5, blue: 0 }, bonus: 60 }, + SpecBonus { req: Colours { red: 10, green: 10, blue: 0 }, bonus: 60 } + ], + }, + + Spec::SpeedGB=> SpecValues { + base: 60, + bonuses: vec![ + SpecBonus { req: Colours { red: 0, green: 2, blue: 2 }, bonus: 60 }, + SpecBonus { req: Colours { red: 0, green: 5, blue: 5 }, bonus: 60 }, + SpecBonus { req: Colours { red: 0, green: 10, blue: 10 }, bonus: 60 } + ], + }, + + Spec::SpeedRB=> SpecValues { + base: 60, + bonuses: vec![ + SpecBonus { req: Colours { red: 2, green: 0, blue: 2 }, bonus: 60 }, + SpecBonus { req: Colours { red: 5, green: 0, blue: 5 }, bonus: 60 }, + SpecBonus { req: Colours { red: 10, green: 0, blue: 10 }, bonus: 60 } + ], + }, + + Spec::SpeedRRPlus => SpecValues { + base: 120, + bonuses: vec![ + SpecBonus { req: Colours { red: 5, green: 0, blue: 0 }, bonus: 120 }, + SpecBonus { req: Colours { red: 10, green: 0, blue: 0 }, bonus: 120 }, + SpecBonus { req: Colours { red: 20, green: 0, blue: 0 }, bonus: 120 } + ], + }, + + Spec::SpeedGGPlus => SpecValues { + base: 120, + bonuses: vec![ + SpecBonus { req: Colours { red: 0, green: 5, blue: 0 }, bonus: 120 }, + SpecBonus { req: Colours { red: 0, green: 10, blue: 0 }, bonus: 120 }, + SpecBonus { req: Colours { red: 0, green: 20, blue: 0 }, bonus: 120 } + ], + }, + + Spec::SpeedBBPlus => SpecValues { + base: 120, + bonuses: vec![ + SpecBonus { req: Colours { red: 0, green: 0, blue: 5 }, bonus: 120 }, + SpecBonus { req: Colours { red: 0, green: 0, blue: 10 }, bonus: 120 }, + SpecBonus { req: Colours { red: 0, green: 0, blue: 20 }, bonus: 120 } + ], + }, + + Spec::SpeedRGPlus => SpecValues { + base: 80, + bonuses: vec![ + SpecBonus { req: Colours { red: 2, green: 2, blue: 0 }, bonus: 80 }, + SpecBonus { req: Colours { red: 5, green: 5, blue: 0 }, bonus: 80 }, + SpecBonus { req: Colours { red: 10, green: 10, blue: 0 }, bonus: 80 } + ], + }, + + Spec::SpeedGBPlus => SpecValues { + base: 80, + bonuses: vec![ + SpecBonus { req: Colours { red: 0, green: 2, blue: 2 }, bonus: 80 }, + SpecBonus { req: Colours { red: 0, green: 5, blue: 5 }, bonus: 80 }, + SpecBonus { req: Colours { red: 0, green: 10, blue: 10 }, bonus: 80 } + ], + }, + + Spec::SpeedRBPlus => SpecValues { + base: 80, + bonuses: vec![ + SpecBonus { req: Colours { red: 2, green: 0, blue: 2 }, bonus: 80 }, + SpecBonus { req: Colours { red: 5, green: 0, blue: 5 }, bonus: 80 }, + SpecBonus { req: Colours { red: 10, green: 0, blue: 10 }, bonus: 80 } + ], + }, + + Spec::SpeedRRPlusPlus => SpecValues { + base: 160, + bonuses: vec![ + SpecBonus { req: Colours { red: 5, green: 0, blue: 0 }, bonus: 160 }, + SpecBonus { req: Colours { red: 10, green: 0, blue: 0 }, bonus: 160 }, + SpecBonus { req: Colours { red: 20, green: 0, blue: 0 }, bonus: 160 } + ], + }, + + Spec::SpeedGGPlusPlus => SpecValues { + base: 160, + bonuses: vec![ + SpecBonus { req: Colours { red: 0, green: 5, blue: 0 }, bonus: 160 }, + SpecBonus { req: Colours { red: 0, green: 10, blue: 0 }, bonus: 160 }, + SpecBonus { req: Colours { red: 0, green: 20, blue: 0 }, bonus: 160 } + ], + }, + + Spec::SpeedBBPlusPlus => SpecValues { + base: 160, + bonuses: vec![ + SpecBonus { req: Colours { red: 0, green: 0, blue: 5 }, bonus: 160 }, + SpecBonus { req: Colours { red: 0, green: 0, blue: 10 }, bonus: 160 }, + SpecBonus { req: Colours { red: 0, green: 0, blue: 20 }, bonus: 160 } + ], + }, + + Spec::SpeedRGPlusPlus => SpecValues { + base: 120, + bonuses: vec![ + SpecBonus { req: Colours { red: 2, green: 2, blue: 0 }, bonus: 120 }, + SpecBonus { req: Colours { red: 5, green: 5, blue: 0 }, bonus: 120 }, + SpecBonus { req: Colours { red: 10, green: 10, blue: 0 }, bonus: 120 } + ], + }, + + Spec::SpeedGBPlusPlus => SpecValues { + base: 120, + bonuses: vec![ + SpecBonus { req: Colours { red: 0, green: 2, blue: 2 }, bonus: 120 }, + SpecBonus { req: Colours { red: 0, green: 5, blue: 5 }, bonus: 120 }, + SpecBonus { req: Colours { red: 0, green: 10, blue: 10 }, bonus: 120 } + ], + }, + + Spec::SpeedRBPlusPlus => SpecValues { + base: 120, + bonuses: vec![ + SpecBonus { req: Colours { red: 2, green: 0, blue: 2 }, bonus: 120 }, + SpecBonus { req: Colours { red: 5, green: 0, blue: 5 }, bonus: 120 }, + SpecBonus { req: Colours { red: 10, green: 0, blue: 10 }, bonus: 120 } + ], + }, + + Spec::Life => SpecValues { + base: 125, + bonuses: vec![]}, + + Spec::LifeRR=> SpecValues { + base: 275, + bonuses: vec![ + SpecBonus { req: Colours { red: 5, green: 0, blue: 0 }, bonus: 75 }, + SpecBonus { req: Colours { red: 10, green: 0, blue: 0 }, bonus: 125 }, + SpecBonus { req: Colours { red: 20, green: 0, blue: 0 }, bonus: 175 } + ], + }, + + Spec::LifeGG=> SpecValues { + base: 225, + bonuses: vec![ + SpecBonus { req: Colours { red: 0, green: 5, blue: 0 }, bonus: 50 }, + SpecBonus { req: Colours { red: 0, green: 10, blue: 0 }, bonus: 75 }, + SpecBonus { req: Colours { red: 0, green: 20, blue: 0 }, bonus: 125 } + ], + }, + + Spec::LifeBB=> SpecValues { + base: 275, + bonuses: vec![ + SpecBonus { req: Colours { red: 0, green: 0, blue: 5 }, bonus: 75 }, + SpecBonus { req: Colours { red: 0, green: 0, blue: 10 }, bonus: 125 }, + SpecBonus { req: Colours { red: 0, green: 0, blue: 20 }, bonus: 175 } + ], + }, + + Spec::LifeRG=> SpecValues { + base: 125, + bonuses: vec![ + SpecBonus { req: Colours { red: 2, green: 2, blue: 0 }, bonus: 50 }, + SpecBonus { req: Colours { red: 5, green: 5, blue: 0 }, bonus: 75 }, + SpecBonus { req: Colours { red: 10, green: 10, blue: 0 }, bonus: 125 } + ], + }, + + Spec::LifeGB=> SpecValues { + base: 125, + bonuses: vec![ + SpecBonus { req: Colours { red: 0, green: 2, blue: 2 }, bonus: 50 }, + SpecBonus { req: Colours { red: 0, green: 5, blue: 5 }, bonus: 75 }, + SpecBonus { req: Colours { red: 0, green: 10, blue: 10 }, bonus: 125 } + ], + }, + + Spec::LifeRB=> SpecValues { + base: 175, + bonuses: vec![ + SpecBonus { req: Colours { red: 2, green: 0, blue: 2 }, bonus: 50 }, + SpecBonus { req: Colours { red: 5, green: 0, blue: 5 }, bonus: 75 }, + SpecBonus { req: Colours { red: 10, green: 0, blue: 10 }, bonus: 125 } + ], + }, + + Spec::LifeRRPlus => SpecValues { + base: 500, + bonuses: vec![ + SpecBonus { req: Colours { red: 5, green: 0, blue: 0 }, bonus: 125 }, + SpecBonus { req: Colours { red: 10, green: 0, blue: 0 }, bonus: 225 }, + SpecBonus { req: Colours { red: 20, green: 0, blue: 0 }, bonus: 300 } + ], + }, + + Spec::LifeGGPlus => SpecValues { + base: 400, + bonuses: vec![ + SpecBonus { req: Colours { red: 0, green: 5, blue: 0 }, bonus: 90 }, + SpecBonus { req: Colours { red: 0, green: 10, blue: 0 }, bonus: 130 }, + SpecBonus { req: Colours { red: 0, green: 20, blue: 0 }, bonus: 225 } + ], + }, + + Spec::LifeBBPlus => SpecValues { + base: 500, + bonuses: vec![ + SpecBonus { req: Colours { red: 0, green: 0, blue: 5 }, bonus: 125 }, + SpecBonus { req: Colours { red: 0, green: 0, blue: 10 }, bonus: 225 }, + SpecBonus { req: Colours { red: 0, green: 0, blue: 20 }, bonus: 300 } + ], + }, + + Spec::LifeRGPlus => SpecValues { + base: 225, + bonuses: vec![ + SpecBonus { req: Colours { red: 2, green: 2, blue: 0 }, bonus: 100 }, + SpecBonus { req: Colours { red: 5, green: 5, blue: 0 }, bonus: 150 }, + SpecBonus { req: Colours { red: 10, green: 10, blue: 0 }, bonus: 225 } + ], + }, + + Spec::LifeGBPlus => SpecValues { + base: 225, + bonuses: vec![ + SpecBonus { req: Colours { red: 0, green: 2, blue: 2 }, bonus: 100 }, + SpecBonus { req: Colours { red: 0, green: 5, blue: 5 }, bonus: 150 }, + SpecBonus { req: Colours { red: 0, green: 10, blue: 10 }, bonus: 225 } + ], + }, + + Spec::LifeRBPlus => SpecValues { + base: 350, + bonuses: vec![ + SpecBonus { req: Colours { red: 2, green: 0, blue: 2 }, bonus: 100 }, + SpecBonus { req: Colours { red: 5, green: 0, blue: 5 }, bonus: 150 }, + SpecBonus { req: Colours { red: 10, green: 0, blue: 10 }, bonus: 225 } + ], + }, + Spec::LifeRRPlusPlus => SpecValues { + base: 875, + bonuses: vec![ + SpecBonus { req: Colours { red: 5, green: 0, blue: 0 }, bonus: 225 }, + SpecBonus { req: Colours { red: 10, green: 0, blue: 0 }, bonus: 400 }, + SpecBonus { req: Colours { red: 20, green: 0, blue: 0 }, bonus: 525 } + ], + }, + + Spec::LifeGGPlusPlus => SpecValues { + base: 475, + bonuses: vec![ + SpecBonus { req: Colours { red: 0, green: 5, blue: 0 }, bonus: 130 }, + SpecBonus { req: Colours { red: 0, green: 10, blue: 0 }, bonus: 225 }, + SpecBonus { req: Colours { red: 0, green: 20, blue: 0 }, bonus: 300 } + ], + }, + + Spec::LifeBBPlusPlus => SpecValues { + base: 875, + bonuses: vec![ + SpecBonus { req: Colours { red: 0, green: 0, blue: 5 }, bonus: 225 }, + SpecBonus { req: Colours { red: 0, green: 0, blue: 10 }, bonus: 400 }, + SpecBonus { req: Colours { red: 0, green: 0, blue: 20 }, bonus: 525 } + ], + }, + + Spec::LifeRGPlusPlus => SpecValues { + base: 400, + bonuses: vec![ + SpecBonus { req: Colours { red: 2, green: 2, blue: 0 }, bonus: 175 }, + SpecBonus { req: Colours { red: 5, green: 5, blue: 0 }, bonus: 275 }, + SpecBonus { req: Colours { red: 10, green: 10, blue: 0 }, bonus: 400 } + ], + }, + + Spec::LifeGBPlusPlus => SpecValues { + base: 625, + bonuses: vec![ + SpecBonus { req: Colours { red: 0, green: 2, blue: 2 }, bonus: 175 }, + SpecBonus { req: Colours { red: 0, green: 5, blue: 5 }, bonus: 275 }, + SpecBonus { req: Colours { red: 0, green: 10, blue: 10 }, bonus: 400 } + ], + }, + + Spec::LifeRBPlusPlus => SpecValues { + base: 400, + bonuses: vec![ + SpecBonus { req: Colours { red: 2, green: 0, blue: 2 }, bonus: 175 }, + SpecBonus { req: Colours { red: 5, green: 0, blue: 5 }, bonus: 275 }, + SpecBonus { req: Colours { red: 10, green: 0, blue: 10 }, bonus: 400 } + ], + }, + } + } + + pub fn apply(&self, modified: u64, base: u64, player_colours: &Colours) -> u64 { + match *self { + // Percentage multipliers based on base value + Spec::Power | + Spec::Speed => modified + base.pct(self.values().base), + Spec::PowerRR| + Spec::PowerGG| + Spec::PowerBB| + Spec::PowerRG| + Spec::PowerGB| + Spec::PowerRB| + Spec::PowerRRPlus | + Spec::PowerGGPlus | + Spec::PowerBBPlus | + Spec::PowerRGPlus | + Spec::PowerGBPlus | + Spec::PowerRBPlus | + Spec::PowerRRPlusPlus | + Spec::PowerGGPlusPlus | + Spec::PowerBBPlusPlus | + Spec::PowerRGPlusPlus | + Spec::PowerGBPlusPlus | + Spec::PowerRBPlusPlus | + + Spec::SpeedRR| + Spec::SpeedGG| + Spec::SpeedBB| + Spec::SpeedRG| + Spec::SpeedGB| + Spec::SpeedRB| + Spec::SpeedRRPlus | + Spec::SpeedGGPlus | + Spec::SpeedBBPlus | + Spec::SpeedRGPlus | + Spec::SpeedGBPlus | + Spec::SpeedRBPlus | + Spec::SpeedRRPlusPlus | + Spec::SpeedGGPlusPlus | + Spec::SpeedBBPlusPlus | + Spec::SpeedRGPlusPlus | + Spec::SpeedGBPlusPlus | + Spec::SpeedRBPlusPlus => modified + base.pct(self.values().max_value(player_colours)), + + // Flat bonus + Spec::Life => modified + self.values().base, + Spec::LifeRR| + Spec::LifeGG| + Spec::LifeBB| + Spec::LifeRG| + Spec::LifeGB| + Spec::LifeRB| + Spec::LifeRRPlus | + Spec::LifeGGPlus | + Spec::LifeBBPlus | + Spec::LifeRGPlus | + Spec::LifeGBPlus | + Spec::LifeRBPlus | + Spec::LifeRRPlusPlus | + Spec::LifeGGPlusPlus | + Spec::LifeBBPlusPlus | + Spec::LifeRGPlusPlus | + Spec::LifeGBPlusPlus | + Spec::LifeRBPlusPlus => modified + self.values().max_value(player_colours), + } + } +} diff --git a/core/src/util.rs b/core/src/util.rs new file mode 100644 index 00000000..bf4f3032 --- /dev/null +++ b/core/src/util.rs @@ -0,0 +1,40 @@ +// use net::Db; +// Db Commons +// use failure::Error; + +// pub fn startup(db: Db) -> Result<(), Error> { +// let tx = db.transaction()?; + +// info!("running startup fns"); + +// match tx.commit() { +// Ok(_) => { +// info!("startup processes completed"); +// Ok(()) +// }, +// Err(e) => Err(format_err!("failed to commit startup tx {:?}", e)), +// } +// } + +pub trait IntPct { + fn pct(self, pct: u64) -> u64; +} + +impl IntPct for u64 { + fn pct(self, pct: u64) -> u64 { + self * pct / 100 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn int_pct_test() { + assert_eq!(100.pct(110), 110); + assert_eq!(100.pct(50), 50); + assert_eq!(1.pct(200), 2); + assert_eq!(1.pct(50), 0); + } +} \ No newline at end of file diff --git a/core/src/vbox.rs b/core/src/vbox.rs new file mode 100644 index 00000000..71a37ce2 --- /dev/null +++ b/core/src/vbox.rs @@ -0,0 +1,305 @@ +use uuid::Uuid; + +use std::iter; +use std::collections::HashMap; + +// refunds +use rand::prelude::*; +use rand::{thread_rng}; +use rand::distributions::{WeightedIndex}; + +use failure::Error; +use failure::err_msg; + +use instance::{Instance}; +use construct::{Colours}; + +use item::*; + +pub type VboxIndices = Option>>; + +#[derive(Debug,Clone,Serialize,Deserialize)] +pub struct Vbox { + pub bits: usize, + pub store: HashMap>, + pub stash: HashMap, +} + +#[derive(Debug,Copy,Clone,Serialize,Deserialize,Hash,PartialEq,Eq)] +pub enum ItemType { + Colours, + Skills, + Specs, +} + +const STORE_COLOURS_CAPACITY: usize = 6; +const STORE_SKILLS_CAPACITY: usize = 3; +const STORE_SPECS_CAPACITY: usize = 3; +const STASH_CAPACITY: usize = 6; +const STARTING_ATTACK_COUNT: usize = 3; + +impl Vbox { + pub fn new() -> Vbox { + let mut colours: HashMap = HashMap::new(); + let mut skills: HashMap = HashMap::new(); + let mut specs: HashMap = HashMap::new(); + + let store = [ + (ItemType::Colours, colours), + (ItemType::Skills, skills), + (ItemType::Colours, specs), + ].iter().cloned().collect(); + + let mut stash = HashMap::new(); + for i in 0..STARTING_ATTACK_COUNT { + stash.insert(i.to_string(), Item::Attack); + } + + Vbox { + store, + stash, + bits: 30, + } + } + + pub fn balance_sub(&mut self, amount: usize) -> Result<&mut Vbox, Error> { + let new_balance = self.bits + .checked_sub(amount) + .ok_or(format_err!("insufficient balance: {:?}", self.bits))?; + + self.bits = new_balance; + + Ok(self) + } + + pub fn balance_add(&mut self, amount: usize) -> &mut Vbox { + self.bits = self.bits.saturating_add(amount); + self + } + + pub fn fill(&mut self) -> &mut Vbox { + let mut rng = thread_rng(); + + let colours = vec![ + (Item::Red, 1), + (Item::Green, 1), + (Item::Blue, 1), + ]; + let colour_dist = WeightedIndex::new(colours.iter().map(|item| item.1)).unwrap(); + + let skills = vec![ + (Item::Attack, 1), + (Item::Block, 1), + (Item::Buff, 1), + (Item::Debuff, 1), + (Item::Stun, 1), + ]; + let skill_dist = WeightedIndex::new(skills.iter().map(|item| item.1)).unwrap(); + + let specs = vec![ + (Item::Power, 1), + (Item::Life, 1), + (Item::Speed, 1), + ]; + let spec_dist = WeightedIndex::new(specs.iter().map(|item| item.1)).unwrap(); + + for item_type in [ItemType::Colours, ItemType::Skills, ItemType::Specs].iter() { + let (items, num, dist) = match item_type { + ItemType::Colours => (&colours, STORE_COLOURS_CAPACITY, &colour_dist), + ItemType::Skills => (&skills, STORE_SKILLS_CAPACITY, &skill_dist), + ItemType::Specs => (&specs, STORE_SPECS_CAPACITY, &spec_dist), + }; + + let drops = iter::repeat_with(|| items[dist.sample(&mut rng)].0) + .take(num) + .enumerate() + .map(|(i, item)| (i.to_string(), item)) + .collect::>(); + + self.store.insert(*item_type, drops); + } + + self + } + + pub fn buy(&mut self, item: ItemType, i: &String) -> Result { + // check item exists + let selection = self.store + .get_mut(&item).ok_or(format_err!("no item group {:?}", item))? + .remove(i).ok_or(format_err!("no item at index {:?} {:}", self, i))?; + + self.balance_sub(selection.cost())?; + + Ok(selection) + } + + pub fn stash_add(&mut self, item: Item, index: Option<&String>) -> Result { + if self.stash.len() >= STASH_CAPACITY { + return Err(err_msg("stash full")); + } + + if let Some(index) = index { + if self.stash.contains_key(index) { + return Err(format_err!("slot occupied {:?}", index)); + } + self.stash.insert(index.clone(), item); + return Ok(index.to_string()); + } + + for i in (0..STASH_CAPACITY).map(|i| i.to_string()) { + if !self.stash.contains_key(&i) { + self.stash.insert(i.clone(), item); + return Ok(i); + } + } + + return Err(err_msg("stash full")); + } + + pub fn bot_buy(&mut self, item: ItemType) -> Result { + let buy_index = self.store[&item] + .keys() + .next() + .ok_or(format_err!("no item in group {:?}", item))? + .clone(); + + self.buy(item, &buy_index) + } + + pub fn refund(&mut self, i: String) -> Result<&mut Vbox, Error> { + let refunded = self.stash.remove(&i) + .ok_or(format_err!("no item at index {:?} {:?}", self.stash, i))?; + + let refund = refunded.cost(); + // info!("refunding {:?} for {:?}", refund, refunded); + self.balance_add(refund); + Ok(self) + } + + pub fn combine(&mut self, stash_indices: Vec, store_indices: Option>>) -> Result<&mut Vbox, Error> { + // find base item for index to insert into + let base_index = stash_indices.iter() + .find(|i| match self.stash.get(i.clone()) { + Some(item) => item.into_skill().is_some(), + None => false, + }); + + let mut input = stash_indices + .iter() + .map(|i| self.stash.remove(i) + .ok_or(format_err!("no item at index {:?} {:?}", self.stash, i))) + .collect::, Error>>()?; + + if let Some(store_indices) = store_indices { + let mut purchased = store_indices.iter() + .map(|(g, list)| + list.iter() + .map(|i| self.buy(*g, i)) + .collect::, Error>>() + ) + .collect::>, Error>>()? + .into_iter() + .flatten() + .collect(); + + input.append(&mut purchased); + } + + // sort the input to align with the combinations + // combos are sorted when created + input.sort_unstable(); + let combos = get_combos(); + let combo = combos.iter().find(|c| c.components == input).ok_or(err_msg("not a combo"))?; + + self.stash_add(combo.item, base_index)?; + + Ok(self) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn combine_test() { + let mut vbox = Vbox::new(); + vbox.stash.insert(0.to_string(), Item::Attack); + vbox.stash.insert(1.to_string(), Item::Green); + vbox.stash.insert(2.to_string(), Item::Green); + vbox.combine(vec![0.to_string(), 1.to_string(), 2.to_string()], None).unwrap(); + assert_eq!(vbox.stash["0"], Item::Heal); + } + + #[test] + fn buy_test() { + let mut vbox = Vbox::new(); + vbox.fill(); + + // cannot rebuy same + vbox.buy(ItemType::Skills, &0.to_string()).unwrap(); + assert!(vbox.store[&ItemType::Skills].get(&0.to_string()).is_none()); + assert!(vbox.buy(ItemType::Skills, &0.to_string()).is_err()); + } + + #[test] + fn capacity_test() { + let mut vbox = Vbox::new(); + vbox.fill(); + vbox.stash_add(Item::Red, None).unwrap(); + vbox.stash_add(Item::Red, None).unwrap(); + vbox.stash_add(Item::Red, None).unwrap(); + assert!(vbox.stash_add(Item::Red, None).is_err()); + } + + #[test] + fn store_and_stash_combine_test() { + let mut vbox = Vbox::new(); + vbox.fill(); + + let mut skill_combine_args = HashMap::new(); + skill_combine_args.insert(ItemType::Colours, vec![0.to_string(), 1.to_string()]); + skill_combine_args.insert(ItemType::Skills, vec![0.to_string()]); + + let mut spec_combine_args = HashMap::new(); + spec_combine_args.insert(ItemType::Colours, vec![2.to_string(), 3.to_string()]); + spec_combine_args.insert(ItemType::Specs, vec![0.to_string()]); + + vbox.combine(vec![], Some(skill_combine_args)).unwrap(); + vbox.combine(vec![], Some(spec_combine_args)).unwrap(); + } + + #[test] + fn combos_test() { + let mut input = vec![Item::Green, Item::Attack, Item::Green]; + let combos = get_combos(); + + // sort input so they align + input.sort_unstable(); + + let combo = combos.iter().find(|c| c.components == input); + assert!(combo.is_some()); + } + + #[test] + fn refund_test() { + let mut vbox = Vbox::new(); + vbox.stash.insert(0.to_string(), Item::Strike); + vbox.refund(0.to_string()).unwrap(); + assert_eq!(vbox.bits, 32); + } + + #[test] + fn colours_count_test() { + let strike = Item::Strike; + + let mut count = Colours::new(); + strike.colours(&mut count); + assert_eq!(count.red, 2); + } + + // #[test] + // fn item_info_test() { + // info!("{:#?}", item_info()); + // } +} \ No newline at end of file diff --git a/server/src/game.rs b/server/src/game.rs index ddc701e4..8d3ed833 100644 --- a/server/src/game.rs +++ b/server/src/game.rs @@ -17,7 +17,7 @@ use account::Account; use pg::Db; use construct::{Construct}; -use skill::{Skill, Cast, Resolution, Event, resolution_steps}; +use skill::{Skill, Cast, Resolution, Event, resolve}; use effect::{Effect}; use player::{Player}; use instance::{TimeControl, instance_game_finished}; @@ -217,6 +217,7 @@ impl Game { fn pve_add_skills(&mut self) -> &mut Game { let mut pve_skills = vec![]; + let mut rng = thread_rng(); for mobs in self.players .iter() @@ -228,8 +229,6 @@ impl Game { // info!("{:?} {:?}", mob.name, skill); match skill { Some(s) => { - let mut rng = thread_rng(); - // the mut marks it as being able to be called // more than once let mut find_target = || { @@ -479,7 +478,7 @@ impl Game { while let Some(cast) = self.stack.pop() { // info!("{:} casts ", cast); - let mut resolutions = resolution_steps(&cast, &mut self); + let mut resolutions = resolve(&cast, &mut self); r_animation_ms = resolutions.iter().fold(r_animation_ms, |acc, r| acc + r.clone().get_delay()); @@ -807,91 +806,6 @@ pub fn game_delete(tx: &mut Transaction, id: Uuid) -> Result<(), Error> { return Ok(()); } -// pub fn game_global_startup(tx: &mut Transaction) -> Result<(), Error> { -// if game_global_get(tx).is_ok() { -// info!("global mm game exists"); -// return Ok(()); -// } - -// let mut game = Game::new(); - -// game -// .set_player_num(2) -// .set_player_constructs(3) -// .set_mode(GameMode::Pvp); - -// game_write(tx, &game)?; - -// let query = " -// INSERT INTO matchmaking (id, game) -// VALUES ($1, $2) -// RETURNING id; -// "; - -// let result = tx -// .query(query, &[&Uuid::nil(), &game.id])?; - -// result.iter().next().ok_or(format_err!("no game written"))?; - -// info!("{:} wrote global mm startup", game.id); - -// return Ok(()); -// } - -// pub fn game_global_set(tx: &mut Transaction, game: &Game) -> Result<(), Error> { -// let query = " -// UPDATE matchmaking -// SET game = $1 -// WHERE id = $2 -// RETURNING id, game; -// "; - -// let result = tx -// .query(query, &[&game.id, &Uuid::nil()])?; - -// result.iter() -// .next() -// .ok_or(err_msg("could not set global game mm"))?; - -// return Ok(()); -// } - -// pub fn game_global_get(tx: &mut Transaction) -> Result { -// let query = " -// SELECT * from games -// WHERE id = ( -// SELECT game -// FROM matchmaking -// WHERE id = $1 -// ); -// "; - -// let delete_query = " -// DELETE from matchmaking; -// "; - -// let result = tx -// .query(query, &[&Uuid::nil()])?; - -// let returned = match result.iter().next() { -// Some(row) => row, -// None => return Err(err_msg("game not found")), -// }; - -// // tells from_slice to cast into a construct -// let game_bytes: Vec = returned.get("data"); -// let game = match from_slice::(&game_bytes) { -// Ok(g) => g, -// Err(_) => { -// tx.query(delete_query, &[])?; -// return Err(err_msg("matchmaking game was invalid")) -// } -// }; - -// return Ok(game); -// } - - pub fn game_update(tx: &mut Transaction, game: &Game) -> Result<(), Error> { let game_bytes = to_vec(&game)?; @@ -964,13 +878,7 @@ pub fn game_concede(tx: &mut Transaction, account: &Account, game_id: Uuid) -> R pub fn game_skill_clear(tx: &mut Transaction, account: &Account, game_id: Uuid) -> Result { let mut game = game_get(tx, game_id)?; - game.clear_skill(account.id)?; - - if game.skill_phase_finished() { - game = game.resolve_phase_start(); - } - game_update(tx, &game)?; Ok(game) diff --git a/server/src/skill.rs b/server/src/skill.rs index d94cdc96..13fdcf82 100644 --- a/server/src/skill.rs +++ b/server/src/skill.rs @@ -18,18 +18,12 @@ pub fn dev_resolve(a_id: Uuid, b_id: Uuid, skill: Skill) -> Resolutions { if skill.aoe() { // Send an aoe skill event for anims resolutions.push(Resolution::new(&a, &b).event(Event::AoeSkill { skill }).stages(EventStages::StartEnd)); } - return resolve(skill, &mut a, &mut b, resolutions); + return resolve_skill(skill, &mut a, &mut b, resolutions); } -pub fn resolution_steps(cast: &Cast, game: &mut Game) -> Resolutions { +pub fn resolve(cast: &Cast, game: &mut Game) -> Resolutions { let mut resolutions = vec![]; - resolutions = pre_resolve(cast, game, resolutions); - - return resolutions; -} - -pub fn pre_resolve(cast: &Cast, game: &mut Game, mut resolutions: Resolutions) -> Resolutions { let skill = cast.skill; let source = game.construct_by_id(cast.source_construct_id).unwrap().clone(); let targets = game.get_targets(cast.skill, &source, cast.target_construct_id); @@ -54,7 +48,7 @@ pub fn pre_resolve(cast: &Cast, game: &mut Game, mut resolutions: Resolutions) - continue; } - resolutions = resolve(cast.skill, &mut source, &mut target, resolutions); + resolutions = resolve_skill(cast.skill, &mut source, &mut target, resolutions); // save the changes to the game game.update_construct(&mut source); @@ -67,7 +61,7 @@ pub fn pre_resolve(cast: &Cast, game: &mut Game, mut resolutions: Resolutions) - return resolutions; } -pub fn resolve(skill: Skill, source: &mut Construct, target: &mut Construct, mut resolutions: Vec) -> Resolutions { +pub fn resolve_skill(skill: Skill, source: &mut Construct, target: &mut Construct, mut resolutions: Vec) -> Resolutions { if let Some(_disable) = source.disabled(skill) { // resolutions.push(Resolution::new(source, target).event(Event::Disable { disable, skill }).stages(EventStages::PostOnly)); return resolutions; @@ -84,7 +78,7 @@ pub fn resolve(skill: Skill, source: &mut Construct, target: &mut Construct, mut return resolutions; } resolutions.push(Resolution::new(source, target).event(Event::Reflection { skill })); - return resolve(skill, &mut source.clone(), source, resolutions); + return resolve_skill(skill, &mut source.clone(), source, resolutions); } if source.affected(Effect::Haste) { @@ -175,14 +169,14 @@ pub fn resolve(skill: Skill, source: &mut Construct, target: &mut Construct, mut Skill::Decay| Skill::DecayPlus | - Skill::DecayPlusPlus => decay(source, target, resolutions, skill), // dot + Skill::DecayPlusPlus => decay(source, target, resolutions, skill), Skill::DecayTick| Skill::DecayTickPlus | - Skill::DecayTickPlusPlus => decay_tick(source, target, resolutions, skill), // dot + Skill::DecayTickPlusPlus => decay_tick(source, target, resolutions, skill), Skill::Haste| Skill::HastePlus | - Skill::HastePlusPlus => haste(source, target, resolutions, skill), // speed slow + Skill::HastePlusPlus => haste(source, target, resolutions, skill), Skill::Heal| Skill::HealPlus | @@ -206,7 +200,7 @@ pub fn resolve(skill: Skill, source: &mut Construct, target: &mut Construct, mut Skill::Purge| Skill::PurgePlus | - Skill::PurgePlusPlus => purge(source, target, resolutions, skill), // dispel all buffs + Skill::PurgePlusPlus => purge(source, target, resolutions, skill), Skill::Purify| Skill::PurifyPlus | @@ -226,26 +220,26 @@ pub fn resolve(skill: Skill, source: &mut Construct, target: &mut Construct, mut Skill::Link| Skill::LinkPlus | - Skill::LinkPlusPlus => link(source, target, resolutions, skill), // target is immune to magic damage and fx + Skill::LinkPlusPlus => link(source, target, resolutions, skill), Skill::Silence| Skill::SilencePlus | - Skill::SilencePlusPlus => silence(source, target, resolutions, skill), // target cannot cast spells + Skill::SilencePlusPlus => silence(source, target, resolutions, skill), Skill::Siphon| Skill::SiphonPlus | - Skill::SiphonPlusPlus => siphon(source, target, resolutions, skill), // dot + Skill::SiphonPlusPlus => siphon(source, target, resolutions, skill), Skill::SiphonTick| Skill::SiphonTickPlus | - Skill::SiphonTickPlusPlus => siphon_tick(source, target, resolutions, skill), // dot + Skill::SiphonTickPlusPlus => siphon_tick(source, target, resolutions, skill), Skill::Slay| Skill::SlayPlus | - Skill::SlayPlusPlus => slay(source, target, resolutions, skill), // hybrid dmg self heal + Skill::SlayPlusPlus => slay(source, target, resolutions, skill), Skill::Sleep| Skill::SleepPlus | - Skill::SleepPlusPlus => sleep(source, target, resolutions, skill), // heal stun + Skill::SleepPlusPlus => sleep(source, target, resolutions, skill), Skill::Restrict| Skill::RestrictPlus | @@ -261,24 +255,24 @@ pub fn resolve(skill: Skill, source: &mut Construct, target: &mut Construct, mut Skill::Break| Skill::BreakPlus | - Skill::BreakPlusPlus => break_(source, target, resolutions, skill), // no damage stun, adds vulnerable + Skill::BreakPlusPlus => break_(source, target, resolutions, skill), Skill::Triage| Skill::TriagePlus | - Skill::TriagePlusPlus => triage(source, target, resolutions, skill), // hot + Skill::TriagePlusPlus => triage(source, target, resolutions, skill), Skill::TriageTick| Skill::TriageTickPlus | - Skill::TriageTickPlusPlus => triage_tick(source, target, resolutions, skill), // hot + Skill::TriageTickPlusPlus => triage_tick(source, target, resolutions, skill), // Base Skills Skill::Attack => attack(source, target, resolutions, skill), Skill::Block => block(source, target, resolutions, skill), Skill::Buff => buff(source, target, resolutions, skill), - Skill::Debuff => debuff(source, target, resolutions, skill), // speed slow + Skill::Debuff => debuff(source, target, resolutions, skill), Skill::Stun => stun(source, target, resolutions, skill), - //Triggered + // Triggered Skill::Electrocute | Skill::ElectrocutePlus | Skill::ElectrocutePlusPlus => panic!("should only trigger from electrify hit"), @@ -1267,8 +1261,6 @@ impl Skill { } pub fn defensive(&self) -> bool { - let mut rng = thread_rng(); - match self { Skill::Amplify| Skill::AmplifyPlus | @@ -1311,10 +1303,6 @@ impl Skill { Skill::TriagePlus | Skill::TriagePlusPlus => true, - Skill::Banish | - Skill::BanishPlus | - Skill::BanishPlusPlus => rng.gen_bool(0.5), - _ => false, } } @@ -2021,7 +2009,7 @@ mod tests { assert!(y.affected(Effect::Reflect)); let mut results = vec![]; - results = resolve(Skill::Blast, &mut x, &mut y, results); + results = resolve_skill(Skill::Blast, &mut x, &mut y, results); assert!(x.green_life() < 1024); @@ -2052,7 +2040,7 @@ mod tests { y.blue_life.force(0); x.green_life.reduce(512); - let mut results = resolve(Skill::Siphon, &mut x, &mut y, vec![]); + let mut results = resolve_skill(Skill::Siphon, &mut x, &mut y, vec![]); assert!(y.affected(Effect::Siphon)); assert!(x.green_life() == (512 + 256.pct(Skill::SiphonTick.multiplier()) + 220.pct(Skill::SiphonTick.multiplier())));