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) => (
-
-
{phaseText(game.phase)}
-
log
+
)
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);
}
}