diff --git a/client/cryps.css b/client/cryps.css new file mode 100644 index 00000000..5055091d --- /dev/null +++ b/client/cryps.css @@ -0,0 +1,3 @@ +body { + background-color: #181818; +} diff --git a/client/index.js b/client/index.js index 0787daa8..6ad41876 100755 --- a/client/index.js +++ b/client/index.js @@ -1,2 +1,4 @@ +require('./cryps.css'); + // kick it off require('./src/main'); diff --git a/client/src/components/game.jsx b/client/src/components/game.jsx index f4c2d80e..5af4bf95 100755 --- a/client/src/components/game.jsx +++ b/client/src/components/game.jsx @@ -55,8 +55,8 @@ function GamePanel(props) { ); }); - const statuses = cryp.statuses.map((status, i) => ( -
{status} for {status.turns}T
+ const effects = cryp.effects.map((effect, i) => ( +
{effect} for {effect.turns}T
)); return ( @@ -84,7 +84,7 @@ function GamePanel(props) {
{cryp.xp} / {Math.pow(2, cryp.lvl + 1)} XP
- {statuses} + {effects} {skills} ); @@ -101,8 +101,8 @@ function GamePanel(props) { } function OpponentCrypCard(cryp) { - const statuses = cryp.statuses.map((status, i) => ( -
{status.status} for {status.turns}T
+ const effects = cryp.effects.map((effect, i) => ( +
{effect.effect} for {effect.turns}T
)); return ( @@ -127,7 +127,7 @@ function GamePanel(props) { - {statuses} + {effects} ); } @@ -155,6 +155,9 @@ function GamePanel(props) { return 'Game over'; } } + + const logs = game.log.reverse().map((l, i) => (
{l}
)); + return (
@@ -175,9 +178,8 @@ function GamePanel(props) {
-
-
{phaseText(game.phase)}
-
log
+
+
{logs}
) diff --git a/ops/package.json b/ops/package.json index 3ee5cdf7..6ac74db4 100755 --- a/ops/package.json +++ b/ops/package.json @@ -11,6 +11,9 @@ "author": "", "license": "UNLICENSED", "dependencies": { + "ascii-tree": "^0.3.0", + "cli-ascii-tree": "0.0.4", + "inquirer": "^6.2.0", "knex": "^0.15.2", "pg": "^7.4.3" } diff --git a/server/Cargo.toml b/server/Cargo.toml index d64ff304..725929a2 100755 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -12,6 +12,7 @@ serde_cbor = "0.9" tungstenite = "0.6" bcrypt = "0.2" +petgraph = "0.4" dotenv = "0.9.0" env_logger = "*" diff --git a/server/README.md b/server/README.md index 07b9e5a4..adab2a60 100755 --- a/server/README.md +++ b/server/README.md @@ -1,6 +1,6 @@ -# Cryps ("creeps") // Creeptography +# Cryps ("creeps") -## Setup +## Combat skill phase: 1.1 -> block (sp 10) -> on self @@ -20,3 +20,122 @@ resolve phase: 1.1 <- attack (no effect because of block) 2.2 <- attack (normal resolve) 1.1 <- hexed (no skills for the rest of this turn and next) + +## Dmg Chart + +| Physical | Magic | Modifiers | +| ------ | ------ | ------ | +| dmg | dmg | speed | +| evasion | resistance | cooldowns | +| reduction | absorption? | durations | + + +## Cryp Alignments + +Natural Selection +================ +Survival of the fittest / Strength of the Individual / Attack & Defense +----------------------------------------------------------------------- +they value individual strength and the ability to defend one's self. +having undergone natural selection they are combative by nature and feel threatened from all sides. +magic and advanced technology disturbs them as they are unable to understand it; +their response is to try and crush it and restore their place at the apex. +their fear is a manifestation of the emotions and prejudices they have grown in order to survive. + +* tactics and strategy + * rally + * physical damage + * rend / expose + * taunt +* martial arts and combat + * blocking + * evasion and redirection +* frenzy + +Machine Cult +===================== +Everything Connected / Speed & Efficiency +----------------------------------------- +members of the Machine Cult worship artificial machines of any sort, from simple spring powered devices to vast self-aware networks. +they value speed and efficiency above all else. +the individual has little significance for the machine cult, its members are rushing headlong into state of complete connectedness +they long to transcend beyond their physical limitations and become vaster and more powerful than the sum of each part + +rigid, often serving a very specific purpose, not as adaptable as the other alignments +they and their machinations do not think, they simply act. +no motivation / no emotions / no tricks; just action. + +* efficiency + * reduced cooldowns + * increased speed +* replication + * drones / tokens + +Non-Violence +=============== +Enhancement & Preservation +-------------------------- + +the philosophy of nonviolence teaches that the sanctity of life is above all else +its adherants are defensive and gracious, seeking to minimise the damage done by others and doing no direct harm themselves. +they seek to prevent damage in any way possible + +* healing + * hots + * direct healing +* defensive buffs + * protection from effects + * damage reduction + +Path to Destruction +=================== +Damage & Destruction +------------------------- + +cryps walking the path to destruction have forsaken themselves in order to gain ruinous power. +no price is too high, they gladly harm themselves and allies to amplify the destruction they wreak on everything around them +specialise in magical damage dealing + +* damage amplification +* nukes +* life leach +* life exchange +* poison +* aoe + + +The Tribunal +============= +Fuck Magic +------------------- + +The Tribunal has ruled that magic is an abomination. +Its members now scour the lands in search of magic, censoring its teaching, purging its effects and slaying the heretics who wield it. + +* Dispel, removal +* Silence +* Magic resistance +* Information gathering + * team composition + * available skills etc + +Universal Chaos +=============== +The only constant is change. +---------------------------- + +Cryps aligning themselves with the forces of chaos believe that constant change is the only truth in the universe. +They harness its power to manipulate physical reality as well as control and disrupt the flow of battle. +They blend between physical and astral forms, constantly shifting throughout time and space. + +* Banish +* Chaos damage +* Time control (reverse turn outcomes) +* increase cooldowns +* increase durations +* Slow +* damage redirection + +## Styles +* Aztec +* Yokai / ukiyo-e \ No newline at end of file diff --git a/server/WORKLOG.md b/server/WORKLOG.md index 9b68c58e..7366c9bd 100755 --- a/server/WORKLOG.md +++ b/server/WORKLOG.md @@ -8,6 +8,8 @@ * move rpc functions out * unwrap account for all functions except list +* handle unserializable cryps + * Global rolls * Stats @@ -57,6 +59,11 @@ * run nginx as not root +# Art Styles +* Aztec +* Pixel +* Industrial + # Mechanic Ideas teams 1v1 2v2 3v3 @@ -89,6 +96,7 @@ gem td style attr combinations techno artists for the soundtrack + slimey ghostly @@ -114,24 +122,3 @@ gem td style attr combinations * 18: Restrictions breed creativity * 19: Your audience is good at recognizing problems and bad at solving them * 20: All the lessons connect - - - -skill phase: -1.1 -> block (sp 10) -> on self -1.2 -> attack (sp 5) -> on team 2 - -2.1 -> hex (sp 3) -> on team 1 -2.2 -> attack (sp 5) -> on team 1 - -target phase: -team 2 targets 1.2 on 2.2 - -team 1 targets 2.1 on 1.1 -team 1 targets 2.2 on 1.1 - -resolve phase: -1.1 <- block -1.1 <- attack (no effect because of block) -2.2 <- attack (normal resolve) -1.1 <- hexed (no skills for the rest of this turn and next) diff --git a/server/src/cryp.rs b/server/src/cryp.rs index da8fef4c..a966ca11 100755 --- a/server/src/cryp.rs +++ b/server/src/cryp.rs @@ -9,7 +9,8 @@ use failure::err_msg; use account::Account; use rpc::{CrypSpawnParams}; -use skill::{Skill, Cooldown, Roll}; +use skill::{Skill, Cooldown, Effect, Tick}; +use game::{Log}; #[derive(Debug,Clone,Copy,PartialEq,Serialize,Deserialize)] pub struct CrypSkill { @@ -29,16 +30,17 @@ impl CrypSkill { } #[derive(Debug,Clone,Copy,PartialEq,Serialize,Deserialize)] -pub enum Status { - Stunned, - Silenced, - Blocking, +pub struct CrypEffect { + pub effect: Effect, + pub duration: u8, + pub tick: Option, } -#[derive(Debug,Clone,Copy,PartialEq,Serialize,Deserialize)] -pub struct CrypStatus { - status: Status, - duration: u8, +impl CrypEffect { + pub fn tick(&self, cryp: &mut Cryp, log: &mut Log) -> &CrypEffect { + self.effect.tick(self, cryp, log); + self + } } #[derive(Debug,Clone,Copy,PartialEq,Serialize,Deserialize)] @@ -46,6 +48,8 @@ pub enum Stat { Str, Agi, Int, + PhysicalDmg, + SpellPower, Hp, Stam, } @@ -57,30 +61,35 @@ pub struct CrypStat { } impl CrypStat { - fn set(&mut self, v: u64) -> &CrypStat { + pub fn set(&mut self, v: u64) -> &CrypStat { self.value = v; self } - pub fn reduce(&mut self, dmg: u64) -> &mut CrypStat { - self.value = self.value.saturating_sub(dmg); + pub fn reduce(&mut self, amt: u64) -> &mut CrypStat { + self.value = self.value.saturating_sub(amt); self } + + pub fn increase(&mut self, amt: u64) -> &mut CrypStat { + self.value = self.value.saturating_add(amt); + self + } + } #[derive(Debug,Clone,Serialize,Deserialize)] pub struct Cryp { pub id: Uuid, pub account: Uuid, - pub str: CrypStat, - pub agi: CrypStat, - pub int: CrypStat, - pub stam: CrypStat, + pub phys_dmg: CrypStat, + pub spell_dmg: CrypStat, + pub stamina: CrypStat, pub hp: CrypStat, pub xp: u64, pub lvl: u8, pub skills: Vec, - pub statuses: Vec, + pub effects: Vec, pub name: String, } @@ -95,15 +104,14 @@ impl Cryp { return Cryp { id, account: id, - str: CrypStat { value: 0, stat: Stat::Str }, - agi: CrypStat { value: 0, stat: Stat::Agi }, - int: CrypStat { value: 0, stat: Stat::Int }, - stam: CrypStat { value: 0, stat: Stat::Stam }, + phys_dmg: CrypStat { value: 0, stat: Stat::Str }, + spell_dmg: CrypStat { value: 0, stat: Stat::Int }, + stamina: CrypStat { value: 0, stat: Stat::Stam }, hp: CrypStat { value: 0, stat: Stat::Hp }, lvl: 0, xp: 0, skills: vec![CrypSkill::new(Skill::Attack)], - statuses: vec![], + effects: vec![], name: String::new() }; } @@ -149,13 +157,17 @@ impl Cryp { false => 2_u64.pow(self.lvl.into()), }; + let min = match self.lvl == 1 { + true => 2_u64, + false => 2_u64.pow(self.lvl.saturating_sub(1).into()), + }; + self.xp = max; - self.str.set(rng.gen_range(1, max)); - self.agi.set(rng.gen_range(1, max)); - self.int.set(rng.gen_range(1, max)); - self.stam.set(rng.gen_range(1, max)); - self.hp.set(self.stam.value); + self.phys_dmg.set(rng.gen_range(min, max)); + self.spell_dmg.set(rng.gen_range(min, max)); + self.stamina.set(rng.gen_range(min, max)); + self.hp.set(self.stamina.value); self } @@ -164,8 +176,12 @@ impl Cryp { self.hp.value == 0 } + pub fn immune(&self, skill: Skill) -> bool { + self.effects.iter().any(|e| e.effect.immune(skill)) + } + pub fn is_stunned(&self) -> bool { - self.statuses.iter().any(|s| s.status == Status::Stunned) + self.effects.iter().any(|s| s.effect == Effect::Stun) } pub fn available_skills(&self) -> Vec<&CrypSkill> { @@ -207,62 +223,27 @@ impl Cryp { self } - pub fn reduce_statuses(&mut self) -> &mut Cryp { - self.statuses = self.statuses.clone().into_iter().filter_map(|mut s| { - s.duration = s.duration.saturating_sub(1); + pub fn reduce_effect_durations(&mut self, log: &mut Log) -> &mut Cryp { + self.effects = self.effects.clone().into_iter().filter_map(|mut effect| { - if s.duration == 0 { + effect.tick(self, log); + effect.duration = effect.duration.saturating_sub(1); + + if effect.duration == 0 { return None; } - println!("reduced status {:?}", s); - return Some(s); - }).collect::>(); + println!("reduced effect {:?}", effect); + return Some(effect); + }).collect::>(); self } pub fn rez(&mut self) -> &mut Cryp { - self.hp.set(self.stam.value); + self.hp.set(self.stamina.value); self } - - pub fn roll(&self, skill: Skill) -> Roll { - let mut rng = thread_rng(); - let base: u64 = rng.gen(); - - let stat = skill.stat(self); - - let mut roll = Roll { base, result: base }; - - println!("{:?}'s stats", self.name); - println!("{:064b} <- finalised", roll.result); - roll.result = roll.result & stat.value; - - println!("{:064b} & <- attribute roll", stat.value); - println!("{:064b} = {:?}", roll.result, roll.result); - println!(""); - - return roll; - } - - pub fn stun(&mut self, _roll: Roll) -> &mut Cryp { - if !self.statuses.iter().any(|s| s.status == Status::Blocking) { - self.statuses.push(CrypStatus { status: Status::Stunned, duration: Skill::Stun.duration() }); - } - self - } - - pub fn attack(&mut self, roll: Roll) -> &mut Cryp { - self.hp.reduce(roll.result); - self - } - - pub fn block(&mut self, _roll: Roll) -> &mut Cryp { - self.statuses.push(CrypStatus { status: Status::Blocking, duration: Skill::Block.duration() }); - self - } - } pub fn cryp_get(tx: &mut Transaction, id: Uuid, account_id: Uuid) -> Result { @@ -350,13 +331,13 @@ mod tests { // pub fn assign_str(&mut self, opp: &Cryp, plr_t: &mut Turn, opp_t: &Turn) -> &mut Cryp { - // // let final_str = opp_t.str.result.saturating_sub(plr_t.agi.result); - // // let blocked = opp_t.str.result.saturating_sub(final_str); + // // let final_str = opp_t.phys_dmg.result.saturating_sub(plr_t.agi.result); + // // let blocked = opp_t.phys_dmg.result.saturating_sub(final_str); - // let final_str = opp_t.str.result & !plr_t.agi.result; - // let blocked = opp_t.str.result & plr_t.agi.result; + // let final_str = opp_t.phys_dmg.result & !plr_t.agi.result; + // let blocked = opp_t.phys_dmg.result & plr_t.agi.result; - // plr_t.log.push(format!("{:064b} <- attacking roll {:?}", opp_t.str.result, opp_t.str.result)); + // plr_t.log.push(format!("{:064b} <- attacking roll {:?}", opp_t.phys_dmg.result, opp_t.phys_dmg.result)); // // plr_t.log.push(format!("{:064b} <- blocking roll {:?}", plr_t.agi.result, plr_t.agi.result)); // plr_t.log.push(format!("{:064b} <- final str {:?} ({:?} blocked)", final_str, final_str, blocked)); diff --git a/server/src/game.rs b/server/src/game.rs index 728dd219..75e1bcb3 100755 --- a/server/src/game.rs +++ b/server/src/game.rs @@ -12,6 +12,8 @@ use rpc::{GameStateParams, GameSkillParams, GamePveParams, GamePvpParams, GameTa use cryp::{Cryp, cryp_get}; use skill::{Skill, Cast}; +pub type Log = Vec; + #[derive(Debug,Clone,Serialize,Deserialize)] pub struct Team { id: Uuid, @@ -47,7 +49,7 @@ pub enum Phase { Start, Skill, Target, - Damage, + Resolve, Finish, } @@ -94,11 +96,18 @@ impl Game { self } - // check team not already in fn add_team(&mut self, team: Team) -> Result<&mut Game, Error> { if self.teams.len() == self.team_num { return Err(err_msg("maximum number of teams")); } + + if self.teams.iter().any(|t| t.id == team.id) { + return Err(err_msg("team already in game")); + } + + let team_description = team.cryps.iter().map(|c| c.name.clone()).collect::>().join(", "); + self.log.push(format!("{:?} has joined the game.", team_description)); + self.teams.push(team); Ok(self) @@ -139,13 +148,17 @@ impl Game { } fn start(&mut self) -> &mut Game { + self.log.push("Game starting...".to_string()); + self.skill_phase_start(); self } fn skill_phase_start(&mut self) -> &mut Game { - if ![Phase::Start, Phase::Damage].contains(&self.phase) { - panic!("game not in damage or start phase"); + 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; @@ -175,8 +188,6 @@ impl Game { self } - // skills can target any team, but we have to check if the caller is the owner of the cryp - // and that the cryp has the skill they are trying to add fn add_skill(&mut self, team_id: Uuid, source_cryp_id: Uuid, target_team_id: Option, skill: Skill) -> Result { if self.phase != Phase::Skill { return Err(err_msg("game not in skill phase")); @@ -207,7 +218,7 @@ impl Game { // check here as well so uncastable spells don't go on the stack if !skill.castable(&cryp) { - return Err(err_msg("cryp cannot cast spell")); + return Err(err_msg("cryp cannot cast that skill")); } } @@ -218,9 +229,10 @@ impl Game { } let skill = Cast::new(source_cryp_id, team_id, target_team_id, skill); + let skill_id = skill.id; self.stack.push(skill); - return Ok(skill.id); + return Ok(skill_id); } fn skill_phase_finished(&self) -> bool { @@ -228,16 +240,16 @@ impl Game { // for every team .all(|t| self.stack.iter() // the number of skills they have cast - .filter(|s| s.source_team_id == t.id) - .collect::>() + .filter(|s| s.source_team_id == t.id).collect::>() // should equal the number required this turn .len() == t.skills_required() ) } - // move all skills into their target team's targets list fn target_phase_start(&mut self) -> &mut Game { assert!(self.skill_phase_finished()); + self.log.push("".to_string()); + if self.phase != Phase::Skill { panic!("game not in skill phase"); } @@ -250,7 +262,7 @@ impl Game { // all cryps are stunned or otherwise inactive if self.target_phase_finished() { - self.damage_phase_start(); + self.resolve_phase_start(); } self @@ -311,12 +323,14 @@ impl Game { // requires no input // just do it - fn damage_phase_start(&mut self) -> &mut Game { + fn resolve_phase_start(&mut self) -> &mut Game { if self.phase != Phase::Target { panic!("game not in target phase"); } + assert!(self.target_phase_finished()); - self.phase = Phase::Damage; + self.phase = Phase::Resolve; + self.log.push("".to_string()); self.resolve_skills(); @@ -330,8 +344,8 @@ impl Game { } fn resolve_skills(&mut self) -> &mut Game { - if self.phase != Phase::Damage { - panic!("game not in damage phase"); + if self.phase != Phase::Resolve { + panic!("game not in Resolve phase"); } self.stack.sort_unstable_by_key(|s| s.skill.speed()); @@ -342,16 +356,18 @@ impl Game { let mut source = self.cryp_by_id(skill.source_cryp_id).unwrap().clone(); let mut target = self.cryp_by_id(skill.target_cryp_id.unwrap()).unwrap().clone(); - let resolution = skill.resolve(&mut source, &mut target); - self.resolved.push(*resolution); + // self.log.push(format!("{:?} uses {:?} on {:?}", source.name, skill.skill, target.name)); + skill.set_resolution(&mut source, &mut target, &mut self.log); + self.resolved.push(skill.clone()); + self.update_cryp(&mut source); self.update_cryp(&mut target); - return *resolution; + return skill.clone(); }).collect::>(); - // now damage has all been assigned + // now Resolve has all been assigned // handle cooldowns and statuses self.progress_durations(); @@ -378,7 +394,7 @@ impl Game { } // always reduce durations - cryp.reduce_statuses(); + cryp.reduce_effect_durations(&mut self.log); self.update_cryp(&mut cryp); } @@ -455,7 +471,7 @@ pub fn game_target(params: GameTargetParams, tx: &mut Transaction, account: &Acc game.add_target(account.id, params.cryp_id, params.skill_id)?; if game.target_phase_finished() { - game.damage_phase_start(); + game.resolve_phase_start(); } game_update(&game, tx)?; @@ -753,7 +769,7 @@ mod tests { assert!(game.target_phase_finished()); - game.damage_phase_start(); + game.resolve_phase_start(); assert!([Phase::Skill, Phase::Finish].contains(&game.phase)); @@ -780,7 +796,7 @@ mod tests { game.add_target(y_team.id, y_cryp.id, x_stun_id).unwrap(); assert!(game.target_phase_finished()); - game.damage_phase_start(); + game.resolve_phase_start(); // should auto progress back to skill phase assert!(game.phase == Phase::Skill); @@ -807,7 +823,7 @@ mod tests { game.add_target(x_team.id, x_cryp.id, y_attack_id).unwrap(); game.add_target(y_team.id, y_cryp.id, x_stun_id).unwrap(); - game.damage_phase_start(); + game.resolve_phase_start(); // should auto progress back to skill phase assert!(game.phase == Phase::Skill); @@ -821,9 +837,6 @@ mod tests { let _y_block_id = game.add_skill(y_team.id, y_cryp.id, None, Skill::Block).unwrap(); game.target_phase_start(); - // game.add_target(x_team.id, x_cryp.id, y_block_id).unwrap(); - // game.add_target(y_team.id, y_cryp.id, x_block_id).unwrap(); - // game.damage_phase_start(); assert!(game.team_by_id(y_team.id).cryps[0].skill_on_cd(Skill::Block).is_some()); assert!(game.team_by_id(x_team.id).cryps[0].skill_on_cd(Skill::Block).is_some()); @@ -855,10 +868,10 @@ mod tests { game.add_target(x_team.id, x_cryp.id, y_attack_id).unwrap(); - game.damage_phase_start(); + game.resolve_phase_start(); // should not be stunned because of block assert!(game.team_by_id(x_team.id).cryps[0].is_stunned() == false); + println!("{:#?}", game.log); } - } diff --git a/server/src/main.rs b/server/src/main.rs index 7189ae3c..a52e5dfd 100755 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -5,6 +5,7 @@ extern crate env_logger; extern crate bcrypt; extern crate dotenv; +extern crate petgraph; extern crate postgres; extern crate r2d2; extern crate r2d2_postgres; @@ -21,6 +22,7 @@ mod cryp; mod game; mod net; mod skill; +mod passives; mod rpc; mod account; mod item; diff --git a/server/src/passives.rs b/server/src/passives.rs new file mode 100644 index 00000000..6ecc5d97 --- /dev/null +++ b/server/src/passives.rs @@ -0,0 +1,70 @@ +use petgraph::graph::{Graph, UnGraph, NodeIndex}; +use petgraph::dot::{Dot, Config}; + +#[derive(Debug,Clone,Copy,PartialEq,Eq,Hash,PartialOrd,Ord,Serialize,Deserialize)] +pub struct Passive { + id: &'static str, + allocated: bool, + effect: usize, +} + +impl Passive { + fn new(id: &'static str) -> Passive { + return Passive { + id, + allocated: false, + effect: 0, + }; + } +} + +pub fn create_passive_graph() -> UnGraph { + let mut gr = Graph::new_undirected(); + + let start = gr.add_node(Passive::new("START")); + let mut last; + let mut next; + + // Natural Selection nodes + next = gr.add_node(Passive::new("NS")); + gr.add_edge(start, next, ()); + last = next; + + next = gr.add_node(Passive::new("NSPD0000")); + gr.add_edge(last, next, ()); + last = next; + + next = gr.add_node(Passive::new("NSPD0001")); + gr.add_edge(last, next, ()); + last = next; + + next = gr.add_node(Passive::new("NSPD0002")); + gr.add_edge(last, next, ()); + last = next; + + next = gr.add_node(Passive::new("NSPD0003")); + gr.add_edge(last, next, ()); + last = next; + + next = gr.add_node(Passive::new("NSBLOCK")); + gr.add_edge(last, next, ()); + last = next; + + return gr; +} + +#[cfg(test)] +mod tests { + use passives::*; + + #[test] + fn create_graph() { + let _graph = create_passive_graph(); + // good shit; + // let nodes = graph.node_indices().collect::>(); + // println!("{:?}", nodes[0]); + // println!("{:?}", graph.node_weight(nodes[0])); + + // println!("{:?}", Dot::with_config(&graph, &[Config::EdgeNoLabel])); + } +} diff --git a/server/src/skill.rs b/server/src/skill.rs index 63e710f9..a378be64 100755 --- a/server/src/skill.rs +++ b/server/src/skill.rs @@ -1,23 +1,199 @@ -// use rand::prelude::*; +use rand::{thread_rng, Rng}; use uuid::Uuid; -use cryp::{Cryp, CrypSkill, CrypStat}; +use game::{Log}; +use cryp::{Cryp, CrypEffect}; -#[derive(Debug,Clone,Copy,PartialEq,Serialize,Deserialize)] -pub struct Roll { +#[derive(Debug,Clone,PartialEq,Serialize,Deserialize)] +pub struct Cast { + pub id: Uuid, + pub skill: Skill, + pub source_team_id: Uuid, + pub source_cryp_id: Uuid, + pub target_cryp_id: Option, + pub target_team_id: Uuid, + pub resolution: Resolution, +} + +impl Cast { + pub fn new(source_cryp_id: Uuid, source_team_id: Uuid, target_team_id: Option, skill: Skill) -> Cast { + + let (target_cryp_id, target_team_id) = match skill.self_targeting() { + true => (Some(source_cryp_id), source_team_id), + false => (None, target_team_id.unwrap()) + }; + + return Cast { + id: Uuid::new_v4(), + source_cryp_id, + source_team_id, + target_cryp_id, + target_team_id, + skill, + resolution: Resolution { base: 0, result: None }, + }; + } + + pub fn set_resolution(&mut self, cryp: &mut Cryp, target: &mut Cryp, log: &mut Log) -> &mut Cast { + self.resolution = self.skill.resolve(cryp, target, log); + self + } + + pub fn set_target(&mut self, cryp_id: Uuid) -> &mut Cast { + self.target_cryp_id = Some(cryp_id); + self + } + + pub fn used_cooldown(&self) -> bool { + return self.skill.cd().is_some(); + } +} + +#[derive(Debug,Clone,PartialEq,Serialize,Deserialize)] +pub struct Resolution { pub base: u64, - pub result: u64, + pub result: Option, } pub type Cooldown = Option; +#[derive(Debug,Clone,Copy,PartialEq,Serialize,Deserialize)] +pub enum Effect { + // physical + Stun, + Block, + Bleed, + Leech, + Airborne, + Untouchable, + Deadly, + Vulnerable, + Fury, + Evasion, + Blind, + Snare, + + // magic + Hex, + Banish, + Slow, + Haste, + Enslave, + Mesmerise, + Amplify, + + // magic immunity + Immune, + + // effects over time + Triage, + Decay, + Regen, + Degen, + + SpeedDrain, + SpeedIncrease, +} + +impl Effect { + pub fn immune(&self, skill: Skill) -> bool { + match self { + Effect::Block => match skill { + Skill::Stun | + Skill::Attack => true, + _ => false, + }, + _ => false, + } + } + + pub fn tick(&self, cryp_effect: &CrypEffect, target: &mut Cryp, log: &mut Log) -> &Effect { + match self { + Effect::Decay => decay_tick(target, cryp_effect, log), + Effect::Triage => triage_tick(target, cryp_effect, log), + _ => (), + } + + self + } + +} + +#[derive(Debug,Clone,Copy,PartialEq,Serialize,Deserialize)] +pub struct Tick { + amount: u64 +} + #[derive(Debug,Clone,Copy,PartialEq,Serialize,Deserialize)] pub enum Skill { Attack, - Block, - Heal, + + // ----------------- + // Nature + // ----------------- + Block, // reduce dmg + Parry, // avoid all dmg + Snare, + + Paralyse, + + Strangle, // physical dot and disable + Stun, - Dodge, + Evade, // actively evade + Evasion, // adds evasion to cryp + + + // ----------------- + // Technology + // ----------------- + Replicate, + Swarm, + Orbit, + Repair, + Scan, // track? + + // ----------------- + // Nonviolence + // ----------------- + Heal, + Triage, // hot + Throw, // no dmg stun, adds vulnerable + Charm, + Calm, + Rez, + + // ------------------- + // Destruction + // ------------------- + Blast, + Amplify, + Decay, // dot + Drain, + Curse, + Plague, // aoe dot + Ruin, // aoe + + // ----------------- + // Purity + // ----------------- + Precision, + Inspire, + Slay, + Shield, + Silence, + Inquiry, + Purify, + + // ----------------- + // Chaos + // ----------------- + Banish, + Hex, + Fear, + Taunt, + Pause, // speed slow + // used by tests, no cd, no dmg TestTouch, TestStun, @@ -28,10 +204,76 @@ impl Skill { pub fn cd(&self) -> Cooldown { match self { Skill::Attack => None, - Skill::Block => Some(1), - Skill::Dodge => Some(1), - Skill::Heal => Some(2), - Skill::Stun => Some(2), + + // ----------------- + // Nature + // ----------------- + Skill::Block => Some(1), // reduce dmg + Skill::Parry => Some(1), // avoid all dmg + Skill::Snare => Some(2), + + Skill::Paralyse => Some(3), + Skill::Strangle => Some(3), + + // Strangle + + Skill::Stun => Some(1), + Skill::Evade => Some(2), + Skill::Evasion => Some(3), // additional layer of dmg avoidance + + // ----------------- + // Technology + // ----------------- + Skill::Replicate => Some(1), + Skill::Swarm => Some(3), + Skill::Orbit => Some(2), + Skill::Repair => Some(1), + Skill::Scan => Some(2), // track? + + // ----------------- + // Preservation + // ----------------- + Skill::Heal => Some(1), + Skill::Triage => Some(1), // hot + Skill::Throw => Some(2), // no dmg stun, adds vulnerable + Skill::Charm => Some(2), + Skill::Calm => Some(2), + Skill::Rez => Some(4), + + // ----------------- + // Destruction + // ----------------- + Skill::Blast => Some(1), + Skill::Amplify => Some(2), + Skill::Decay => Some(1), // dot + Skill::Drain => Some(2), + Skill::Curse => Some(2), + Skill::Plague => Some(2), // aoe dot + Skill::Ruin => Some(3), // aoe + + // ----------------- + // Purity + // ----------------- + Skill::Precision => Some(1), + Skill::Inspire => Some(2), + Skill::Slay => Some(1), + Skill::Shield => Some(1), + Skill::Silence => Some(2), + Skill::Inquiry => Some(2), + Skill::Purify => Some(1), + + // ----------------- + // Chaos + // ----------------- + Skill::Banish => Some(2), + Skill::Hex => Some(1), + Skill::Fear => Some(1), + Skill::Taunt => Some(2), + Skill::Pause => Some(2), // speed slow + + // ----------------- + // Test + // ----------------- Skill::TestTouch => None, Skill::TestStun => None, Skill::TestBlock => None, @@ -40,38 +282,185 @@ impl Skill { pub fn speed(&self) -> u8 { match self { - Skill::Attack => 10, - Skill::Block => 5, - Skill::Dodge => 5, - Skill::Heal => 2, - Skill::Stun => 2, + Skill::Attack => 5, + + // ----------------- + // Nature + // ----------------- + Skill::Block => 10, // reduce dmg + Skill::Evade => 10, + Skill::Parry => 10, // avoid all dmg + Skill::Snare => 10, + + Skill::Paralyse => 5, + Skill::Strangle => 5, + + // Strangle + + Skill::Stun => 5, + Skill::Evasion => 3, // additional layer of dmg avoidance + + // ----------------- + // Technology + // ----------------- + Skill::Replicate => 1, + Skill::Swarm => 3, + Skill::Orbit => 2, + Skill::Repair => 1, + Skill::Scan => 2, // track? + + // ----------------- + // Preservation + // ----------------- + Skill::Heal => 1, + Skill::Triage => 1, // hot + Skill::Throw => 2, // no dmg stun, adds vulnerable + Skill::Charm => 2, + Skill::Calm => 2, + Skill::Rez => 4, + + // ----------------- + // Destruction + // ----------------- + Skill::Blast => 1, + Skill::Amplify => 2, + Skill::Decay => 1, // dot + Skill::Drain => 2, + Skill::Curse => 2, + Skill::Plague => 2, // aoe dot + Skill::Ruin => 3, // aoe + + // ----------------- + // Purity + // ----------------- + Skill::Precision => 1, + Skill::Inspire => 2, + Skill::Slay => 1, + Skill::Shield => 1, + Skill::Silence => 2, + Skill::Inquiry => 2, + Skill::Purify => 1, + + // ----------------- + // Chaos + // ----------------- + Skill::Banish => 2, + Skill::Hex => 1, + Skill::Fear => 1, + Skill::Taunt => 2, + Skill::Pause => 2, // speed slow + + // ----------------- + // Test + // ----------------- Skill::TestTouch => 10, - Skill::TestStun => 2, - Skill::TestBlock => 5, + Skill::TestStun => 5, + Skill::TestBlock => 10, } } - pub fn stat(&self, cryp: &Cryp) -> CrypStat { - match self { - Skill::Attack => cryp.str, - Skill::Block => cryp.str, - Skill::Stun => cryp.str, - Skill::Dodge => cryp.agi, - Skill::Heal => cryp.int, + pub fn resolve(&self, cryp: &mut Cryp, target: &mut Cryp, log: &mut Log) -> Resolution { + let mut rng = thread_rng(); + let base: u64 = rng.gen(); - // test skills - Skill::TestTouch => cryp.int, - Skill::TestStun => cryp.str, - Skill::TestBlock => cryp.str, - } + let res = Resolution { base, result: None }; + + // println!("{:?}'s stats", self.name); + // println!("{:064b} <- finalised", roll.result); + // roll.result = roll.result & stat.value; + + // println!("{:064b} & <- attribute roll", stat.value); + // println!("{:064b} = {:?}", roll.result, roll.result); + // println!(""); + + // return Some(roll); + + match self { + Skill::Attack => attack(cryp, target, log), + // ----------------- + // Nature + // ----------------- + Skill::Block => block(cryp, target, log), + Skill::Evade => panic!("nyi"), // + Skill::Parry => panic!("nyi"), // avoid all dmg + Skill::Snare => snare(cryp, target, log), // TODO prevent physical moves + + Skill::Paralyse => panic!("nyi"), // no physical moves + Skill::Strangle => panic!("nyi"), // no physical moves + + Skill::Stun => stun(cryp, target, log), + Skill::Evasion => panic!("nyi"), // additional layer of dmg avoidance + + // ----------------- + // Technology + // ----------------- + Skill::Replicate => panic!("nyi"), + Skill::Swarm => panic!("nyi"), + Skill::Orbit => panic!("nyi"), + Skill::Repair => panic!("nyi"), + Skill::Scan => panic!("nyi"), // track? + + // ----------------- + // Preservation + // ----------------- + Skill::Heal => heal(cryp, target, log), + Skill::Triage => triage(cryp, target, log), // hot + Skill::Throw => throw(cryp, target, log), // no dmg stun, adds vulnerable + Skill::Charm => panic!("nyi"), + Skill::Calm => panic!("nyi"), + Skill::Rez => panic!("nyi"), + + // ----------------- + // Destruction + // ----------------- + Skill::Blast => blast(cryp, target, log), + Skill::Amplify => amplify(cryp, target, log), // TODO increase magic dmg + Skill::Decay => decay(cryp, target, log), // dot + Skill::Drain => panic!("nyi"), + Skill::Curse => panic!("nyi"), + Skill::Plague => panic!("nyi"), // aoe dot + Skill::Ruin => panic!("nyi"), // aoe + + // ----------------- + // Purity + // ----------------- + Skill::Precision => panic!("nyi"), + Skill::Inspire => panic!("nyi"), + Skill::Slay => panic!("nyi"), + Skill::Shield => panic!("nyi"), + Skill::Silence => panic!("nyi"), + Skill::Inquiry => panic!("nyi"), + Skill::Purify => panic!("nyi"), + + // ----------------- + // Chaos + // ----------------- + Skill::Banish => banish(cryp, target, log), // TODO prevent all actions + Skill::Hex => hex(cryp, target, log), // todo prevent casting + Skill::Fear => panic!("nyi"), + Skill::Taunt => panic!("nyi"), + Skill::Pause => panic!("nyi"), // speed slow + + // ----------------- + // Test + // ----------------- + Skill::TestTouch => (), + Skill::TestStun => stun(cryp, target, log), + Skill::TestBlock => block(cryp, target, log), + }; + + return res; } pub fn duration(&self) -> u8 { match self { - Skill::Dodge => 1, + Skill::Evade => 1, Skill::Stun => 2, Skill::Block => 1, + Skill::Decay => 3, + Skill::Triage => 3, + Skill::TestBlock => 1, Skill::TestStun => 2, _ => panic!("{:?} does not have a duration", self), @@ -94,70 +483,197 @@ impl Skill { } } - -#[derive(Debug,Clone,Copy,PartialEq,Serialize,Deserialize)] -pub struct Cast { - pub id: Uuid, - pub skill: Skill, - pub source_team_id: Uuid, - pub source_cryp_id: Uuid, - pub target_cryp_id: Option, - pub target_team_id: Uuid, - pub roll: Option, +fn attack(cryp: &mut Cryp, target: &mut Cryp, log: &mut Log) { + log.push(format!("{:?} -> {:?} | Attack for {:?}", cryp.name, target.name, cryp.phys_dmg)); + target.hp.reduce(cryp.phys_dmg.value); } -impl Cast { - pub fn new(source_cryp_id: Uuid, source_team_id: Uuid, target_team_id: Option, skill: Skill) -> Cast { +fn stun(cryp: &mut Cryp, target: &mut Cryp, log: &mut Log) { + if !target.immune(Skill::Stun) { + let stun = CrypEffect { effect: Effect::Stun, duration: Skill::Stun.duration(), tick: None }; + target.effects.push(stun); + log.push(format!("{:?} -> {:?} | {:?} for {:?}T", cryp.name, target.name, stun.effect, stun.duration)); + } else { + log.push(format!("{:?} -> {:?} | {:?} immune", cryp.name, target.name, target.name)); + } +} - let (target_cryp_id, target_team_id) = match skill.self_targeting() { - true => (Some(source_cryp_id), source_team_id), - false => (None, target_team_id.unwrap()) - }; +fn throw(cryp: &mut Cryp, target: &mut Cryp, log: &mut Log) { + if !target.immune(Skill::Throw) { + let stun = CrypEffect { effect: Effect::Stun, duration: Skill::Stun.duration(), tick: None }; + let vulnerable = CrypEffect { effect: Effect::Vulnerable, duration: Skill::Stun.duration(), tick: None }; + target.effects.push(stun); + target.effects.push(vulnerable); + log.push(format!("{:?} -> {:?} | {:?} for {:?}T", cryp.name, target.name, stun.effect, stun.duration)); + log.push(format!("{:?} -> {:?} | {:?} for {:?}T", cryp.name, target.name, vulnerable.effect, vulnerable.duration)); + } else { + log.push(format!("{:?} -> {:?} | {:?} immune", cryp.name, target.name, target.name)); + } +} - return Cast { - id: Uuid::new_v4(), - source_cryp_id, - source_team_id, - target_cryp_id, - target_team_id, - skill, - roll: None, - }; + +fn block(_cryp: &mut Cryp, target: &mut Cryp, log: &mut Log) { + let effect = CrypEffect { effect: Effect::Block, duration: Skill::Block.duration(), tick: None }; + target.effects.push(effect); + log.push(format!("{:?} is {:?} for {:?}T", target.name, effect.effect, effect.duration)); +} + +fn snare(_cryp: &mut Cryp, target: &mut Cryp, log: &mut Log) { + let effect = CrypEffect { effect: Effect::Snare, duration: Skill::Snare.duration(), tick: None }; + target.effects.push(effect); + log.push(format!("{:?} is {:?} for {:?}T", target.name, effect.effect, effect.duration)); +} + +fn heal(cryp: &mut Cryp, target: &mut Cryp, log: &mut Log) { + let new_hp = *[ + target.hp.value.saturating_add(cryp.spell_dmg.value), + target.stamina.value + ].iter().min().unwrap(); + + let healing = new_hp.saturating_sub(target.hp.value); + let overhealing = target.hp.value.saturating_add(cryp.phys_dmg.value).saturating_sub(target.stamina.value); + target.hp.value = new_hp; + log.push(format!("{:?} -> {:?} | Heal for {:?} ({:?} OH)", cryp.name, target.name, healing, overhealing)); +} + +fn triage(cryp: &mut Cryp, target: &mut Cryp, log: &mut Log) { + let effect = CrypEffect { + effect: Effect::Triage, + duration: Skill::Triage.duration(), + tick: Some(Tick { amount: cryp.spell_dmg.value.wrapping_div(2) }) + }; + target.effects.push(effect); + log.push(format!("{:?} is {:?} for {:?}T", target.name, effect.effect, effect.duration)); +} + +fn triage_tick(target: &mut Cryp, effect: &CrypEffect, log: &mut Log) { + let tick = effect.tick.expect("no tick for triage"); + let new_hp = *[ + target.hp.value.saturating_add(tick.amount), + target.stamina.value + ].iter().min().unwrap(); + + let healing = new_hp.saturating_sub(target.hp.value); + let overhealing = target.hp.value + tick.amount - target.stamina.value; + log.push(format!("{:?} | Triage healing for {:?} ({:?} OH)", target.name, healing, overhealing)); + target.hp.value = new_hp; +} + +fn blast(cryp: &mut Cryp, target: &mut Cryp, log: &mut Log) { + let amount = cryp.spell_dmg.value; + log.push(format!("{:?} -> {:?} | Blast for {:?}", cryp.name, target.name, amount)); + target.hp.reduce(amount); +} + +fn amplify(_cryp: &mut Cryp, target: &mut Cryp, log: &mut Log) { + let effect = CrypEffect { effect: Effect::Amplify, duration: Skill::Amplify.duration(), tick: None }; + target.effects.push(effect); + log.push(format!("{:?} is {:?} for {:?}T", target.name, effect.effect, effect.duration)); +} + +fn decay(cryp: &mut Cryp, target: &mut Cryp, log: &mut Log) { + let effect = CrypEffect { + effect: Effect::Decay, + duration: Skill::Decay.duration(), + tick: Some(Tick { amount: cryp.spell_dmg.value.wrapping_div(2) }) + }; + target.effects.push(effect); + log.push(format!("{:?} is {:?} for {:?}T", target.name, effect.effect, effect.duration)); +} + +fn decay_tick(target: &mut Cryp, effect: &CrypEffect, log: &mut Log) { + let tick = effect.tick.expect("no tick for decay"); + target.hp.reduce(tick.amount); + log.push(format!("{:?} | Decay damage for {:?}", target.name, tick.amount)); +} + +fn hex(_cryp: &mut Cryp, target: &mut Cryp, log: &mut Log) { + let effect = CrypEffect { effect: Effect::Hex, duration: Skill::Hex.duration(), tick: None }; + target.effects.push(effect); + log.push(format!("{:?} is {:?} for {:?}T", target.name, effect.effect, effect.duration)); +} + +fn banish(_cryp: &mut Cryp, target: &mut Cryp, log: &mut Log) { + let effect = CrypEffect { effect: Effect::Banish, duration: Skill::Banish.duration(), tick: None }; + target.effects.push(effect); + log.push(format!("{:?} is {:?} for {:?}T", target.name, effect.effect, effect.duration)); +} + + +#[cfg(test)] +mod tests { + use skill::*; + + #[test] + fn heal_test() { + let mut x = Cryp::new() + .named(&"muji".to_string()) + .level(8) + .learn(Skill::Heal) + .create(); + + let mut y = Cryp::new() + .named(&"camel".to_string()) + .level(8) + .learn(Skill::Heal) + .create(); + + x.hp.reduce(5); + + let mut log = vec![]; + heal(&mut y, &mut x, &mut log); + + println!("{:?}", log); } - pub fn resolve(&mut self, cryp: &mut Cryp, target: &mut Cryp) -> &mut Cast { - let roll = cryp.roll(self.skill); + #[test] + fn decay_test() { + let mut x = Cryp::new() + .named(&"muji".to_string()) + .level(8) + .create(); - println!("{:?} -> {:?} -> {:?}", cryp.name, self.skill, target.name); + let mut y = Cryp::new() + .named(&"camel".to_string()) + .level(8) + .create(); - match self.skill { - // the real deal - Skill::Stun => target.stun(roll), - Skill::Attack => target.attack(roll), - Skill::Block => target.block(roll), - Skill::Heal => target, - Skill::Dodge => target, + let mut log = vec![]; + decay(&mut x, &mut y, &mut log); - // Test Skills - Skill::TestStun => target.stun(roll), - Skill::TestBlock => target.block(roll), - Skill::TestTouch => target, - }; + assert!(y.effects.iter().any(|e| e.effect == Effect::Decay)); - // println!("{:?} gettin clapped for {:?}", target.name, roll.result); - - self.roll = Some(roll); - self + y.reduce_effect_durations(&mut log); + let decay = y.effects.iter().find(|e| e.effect == Effect::Decay); + assert!(y.hp.value == y.stamina.value.saturating_sub(decay.unwrap().tick.unwrap().amount)); } - pub fn set_target(&mut self, cryp_id: Uuid) -> &mut Cast { - self.target_cryp_id = Some(cryp_id); - self - } + #[test] + fn triage_test() { + let mut x = Cryp::new() + .named(&"muji".to_string()) + .level(8) + .create(); - pub fn used_cooldown(self) -> bool { - let cs = CrypSkill::new(self.skill); - return cs.cd.is_some(); + let mut y = Cryp::new() + .named(&"pretaliation".to_string()) + .level(8) + .create(); + + let mut log = vec![]; + + // ensure it doesn't have 0 sd + x.spell_dmg.value = 50; + y.hp.reduce(5); + + let prev_hp = y.hp.value; + + triage(&mut x, &mut y, &mut log); + + assert!(y.effects.iter().any(|e| e.effect == Effect::Triage)); + + y.reduce_effect_durations(&mut log); + assert!(y.hp.value > prev_hp); } }