diff --git a/client/assets/c1.svg b/client/assets/c1.svg
new file mode 100644
index 00000000..42347312
--- /dev/null
+++ b/client/assets/c1.svg
@@ -0,0 +1,18 @@
+
+
+
+
diff --git a/client/assets/c2.svg b/client/assets/c2.svg
new file mode 100644
index 00000000..d816a009
--- /dev/null
+++ b/client/assets/c2.svg
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/client/assets/c3.svg b/client/assets/c3.svg
new file mode 100644
index 00000000..0092ae89
--- /dev/null
+++ b/client/assets/c3.svg
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/client/src/scenes/menu.navigation.js b/client/src/scenes/menu.navigation.js
index 0fef15cd..234cc270 100644
--- a/client/src/scenes/menu.navigation.js
+++ b/client/src/scenes/menu.navigation.js
@@ -67,7 +67,6 @@ class MenuNavigation extends Phaser.Scene {
pveText.destroy();
pvp.destroy();
pvpText.destroy();
- ws.sendGameJoinableList();
});
const cancel = this.add
diff --git a/client/src/socket.js b/client/src/socket.js
index 13b21d01..249a183f 100644
--- a/client/src/socket.js
+++ b/client/src/socket.js
@@ -77,10 +77,6 @@ function createSocket(events) {
send({ method: 'game_join', params: { game_id: gameId, cryp_ids: crypIds } });
}
- function sendGameJoinableList() {
- send({ method: 'game_joinable_list', params: { } });
- }
-
function sendSpecForget(id, spec) {
send({ method: 'cryp_unspec', params: { id, spec } });
}
@@ -105,18 +101,10 @@ function createSocket(events) {
send({ method: 'player_vbox_combine', params: { instance_id: instanceId, indices } });
}
-
function sendVboxDrop(instanceId, index) {
send({ method: 'player_vbox_drop', params: { instance_id: instanceId, index } });
}
-
- function sendPressR() {
- send({ method: 'press_r', params: { } });
- }
-
- window.pressR = sendPressR;
-
function sendGameSkill(gameId, crypId, targetCrypId, skill) {
send({
method: 'game_skill',
@@ -154,7 +142,6 @@ function createSocket(events) {
account = login;
events.setAccount(login);
sendAccountCryps();
- // sendGameJoinableList();
}
function accountCryps(response) {
@@ -167,11 +154,6 @@ function createSocket(events) {
events.setGame(game);
}
- function gameJoinableList(response) {
- const [structName, gameList] = response;
- events.setGameList(gameList);
- }
-
function crypSpawn(response) {
const [structName, cryp] = response;
}
@@ -202,7 +184,6 @@ function createSocket(events) {
cryp_learn: () => true,
game_pve: gamePve,
game_state: gameState,
- game_joinable_list: gameJoinableList,
account_login: accountLogin,
account_create: accountLogin,
account_cryps: accountCryps,
@@ -290,7 +271,6 @@ function createSocket(events) {
sendGamePve,
sendGamePvp,
sendGameJoin,
- sendGameJoinableList,
sendGameSkill,
sendGameTarget,
sendCrypSpawn,
diff --git a/server/src/game.rs b/server/src/game.rs
index 8f4ca467..1f6e115f 100644
--- a/server/src/game.rs
+++ b/server/src/game.rs
@@ -185,10 +185,6 @@ impl Game {
self
}
- fn joinable(&self) -> bool {
- self.phase == Phase::Start
- }
-
fn can_start(&self) -> bool {
return self.teams.len() == self.team_num
&& self.teams.iter().all(|t| t.cryps.len() == self.team_size)
@@ -607,13 +603,13 @@ pub fn game_write(game: &Game, tx: &mut Transaction) -> Result<(), Error> {
let game_bytes = to_vec(&game)?;
let query = "
- INSERT INTO games (id, joinable, data)
- VALUES ($1, $2, $3)
+ INSERT INTO games (id, data)
+ VALUES ($1, $2)
RETURNING id;
";
let result = tx
- .query(query, &[&game.id, &game.joinable(), &game_bytes])?;
+ .query(query, &[&game.id, &game_bytes])?;
result.iter().next().ok_or(format_err!("no game written"))?;
@@ -653,13 +649,13 @@ pub fn game_update(game: &Game, tx: &mut Transaction) -> Result<(), Error> {
let query = "
UPDATE games
- SET data = $1, joinable = $2
- WHERE id = $3
+ SET data = $1
+ WHERE id = $2
RETURNING id, data;
";
let result = tx
- .query(query, &[&game_bytes, &game.joinable(), &game.id])?;
+ .query(query, &[&game_bytes, &game.id])?;
result.iter().next().ok_or(format_err!("game {:?} could not be written", game))?;
diff --git a/server/src/game_target_phase.rs b/server/src/game_target_phase.rs
deleted file mode 100644
index 83412b1d..00000000
--- a/server/src/game_target_phase.rs
+++ /dev/null
@@ -1,1281 +0,0 @@
-use uuid::Uuid;
-use rand::prelude::*;
-use rand::distributions::Alphanumeric;
-
-use std::iter;
-
-// Db Commons
-use serde_cbor::{from_slice, to_vec};
-use postgres::transaction::Transaction;
-use failure::Error;
-use failure::err_msg;
-
-use account::Account;
-use rpc::{GameStateParams, GameSkillParams, GamePveParams, GamePvpParams, GameTargetParams, GameJoinParams};
-use cryp::{Cryp, cryp_get};
-use skill::{Skill, Cast, ResolutionResult};
-use item::{item_drop};
-use zone::{node_finish};
-
-pub type Log = Vec;
-
-#[derive(Debug,Clone,Serialize,Deserialize)]
-pub enum GameMode {
- Boss,
- Normal,
-}
-
-#[derive(Debug,Clone,Serialize,Deserialize)]
-pub struct Team {
- pub id: Uuid,
- cryps: Vec,
-}
-
-impl Team {
- pub fn new(account: Uuid) -> Team {
- return Team {
- id: account,
- cryps: vec![],
- };
- }
-
- fn skills_required(&self) -> usize {
- let required = self.cryps.iter()
- .filter(|c| !c.is_ko())
- .filter(|c| c.available_skills().len() > 0)
- .collect::>().len();
- // println!("{:} requires {:} skills this turn", self.id, required);
- return required;
- }
-
- pub fn set_cryps(&mut self, cryps: Vec) -> &mut Team {
- self.cryps = cryps;
- self
- }
-
- pub fn cryp_by_id(&mut self, id: Uuid) -> Option<&mut Cryp> {
- self.cryps.iter_mut().find(|c| c.id == id)
- }
-}
-
-#[derive(Debug,Clone,Copy,PartialEq,Serialize,Deserialize)]
-pub enum Phase {
- Start,
- Skill,
- Target,
- Resolve,
- Finish,
-}
-
-#[derive(Debug,Clone,Serialize,Deserialize)]
-pub struct Game {
- pub id: Uuid,
- pub team_size: usize,
- pub team_num: usize,
- pub teams: Vec,
- pub is_pve: bool,
- pub phase: Phase,
- pub stack: Vec,
- pub resolved: Vec,
- pub log: Vec,
- pub zone: Option<(Uuid, u32)>,
-}
-
-impl Game {
- pub fn new() -> Game {
- return Game {
- id: Uuid::new_v4(),
- team_size: 0,
- team_num: 0,
- teams: vec![],
- is_pve: true,
- phase: Phase::Start,
- stack: vec![],
- resolved: vec![],
- log: vec![],
- zone: None,
- };
- }
-
- pub fn set_team_num(&mut self, size: usize) -> &mut Game {
- self.team_num = size;
- self
- }
-
- pub fn set_team_size(&mut self, size: usize) -> &mut Game {
- self.team_size = size;
- self
- }
-
- pub fn set_pve(&mut self, pve: bool) -> &mut Game {
- self.is_pve = pve;
- self
- }
-
- pub fn set_zone(&mut self, id: Uuid, node: u32) -> &mut Game {
- self.zone = Some((id, node));
- self
- }
-
- fn already_joined(&self, team_id: Uuid) -> bool {
- self.teams.iter().any(|t| t.id == team_id)
- }
-
- pub fn team_add(&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)
- }
-
- // handle missing team properly
- fn team_by_id(&mut self, id: Uuid) -> &mut Team {
- match self.teams.iter_mut().find(|t| t.id == id) {
- Some(t) => t,
- None => panic!("id not in game {:}", id),
- }
- }
-
- fn cryp_by_id(&mut self, id: Uuid) -> Option<&mut Cryp> {
- match self.teams.iter_mut().find(|t| t.cryps.iter().any(|c| c.id == id)) {
- Some(team) => {
- return team.cryps.iter_mut().find(|c| c.id == id);
- },
- None => panic!("cryp not in game"),
- };
- }
-
- fn all_cryps(&self) -> Vec {
- self.teams.clone()
- .into_iter()
- .flat_map(
- |t| t.cryps
- .into_iter())
- .collect::>()
- }
-
- fn update_cryp(&mut self, cryp: &mut Cryp) -> &mut Game {
- match self.teams.iter_mut().find(|t| t.cryps.iter().any(|c| c.id == cryp.id)) {
- Some(team) => {
- let index = team.cryps.iter().position(|t| t.id == cryp.id).unwrap();
- team.cryps.remove(index);
- team.cryps.push(cryp.clone());
- },
- None => panic!("cryp not in game"),
- };
-
- self
- }
-
- fn joinable(&self) -> bool {
- self.phase == Phase::Start
- }
-
- fn can_start(&self) -> bool {
- return self.teams.len() == self.team_num
- && self.teams.iter().all(|t| t.cryps.len() == self.team_size)
- }
-
- 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 {
- 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.stack.clear();
-
- if self.is_pve {
- self.pve_add_skills();
- }
-
- self
- }
-
- fn pve_add_skills(&mut self) -> &mut Game {
- {
- let mob_team_id = Uuid::nil();
- let mobs = self.team_by_id(mob_team_id).clone();
-
- // TODO attack multiple players based on some criteria
- let player_team_id = self.teams.iter().find(|t| t.id != mob_team_id).unwrap().id;
- for mob in &mobs.cryps {
- // doesn't matter if the cryp can't cast
- self.add_skill(mob_team_id, mob.id, Some(player_team_id), Skill::Attack).ok();
- }
- }
-
- self
- }
-
- 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"));
- }
-
- {
- let cryp = match self.cryp_by_id(source_cryp_id) {
- Some(c) => c,
- None => return Err(err_msg("cryp not in team")),
- };
-
- if cryp.is_ko() {
- return Err(err_msg("cryp is ko"));
- }
-
- // check the cryp has the skill
- if !cryp.knows(skill) {
- return Err(err_msg("cryp does not have that skill"));
- }
-
- if cryp.skill_on_cd(skill).is_some() {
- return Err(err_msg("abiltity on cooldown"));
- }
-
- // if skill.self_targeting() && target_team_id.is_some() {
- // return Err(err_msg("skill is self targeting"));
- // }
-
- if !skill.self_targeting() && target_team_id.is_none() {
- return Err(err_msg("skill requires a target"));
- }
-
- // check here as well so uncastable spells don't go on the stack
- let check = cryp.disabled(skill);
- if check.disabled {
- return Err(err_msg("cryp cannot cast that skill"));
- }
- }
-
-
- // replace cryp skill
- if let Some(s) = self.stack.iter_mut().position(|s| s.source_cryp_id == source_cryp_id) {
- self.stack.remove(s);
- }
-
- 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);
- }
-
- fn skill_phase_finished(&self) -> bool {
- self.teams.iter()
- // for every team
- .all(|t| self.stack.iter()
- // the number of skills they have cast
- .filter(|s| s.source_team_id == t.id).collect::>()
- // should equal the number required this turn
- .len() == t.skills_required()
- )
- }
-
- 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");
- }
-
- self.phase = Phase::Target;
-
- if self.is_pve {
- self.pve_add_targets();
- }
-
- // all cryps are stunned or otherwise inactive
- if self.target_phase_finished() {
- self.resolve_phase_start();
- }
-
- self
- }
-
- fn pve_add_targets(&mut self) -> &mut Game {
- {
- let mob_team_id = Uuid::nil();
- let mobs = self.team_by_id(mob_team_id).clone();
-
- // TODO attack multiple players based on some criteria
- for (i, incoming_skill_id) in self.stack.clone().iter()
- .filter(|s| s.target_cryp_id.is_none() && s.target_team_id == mob_team_id)
- .enumerate()
- .map(|(i, s)| (i, s.id)) {
- let targets = mobs.cryps
- .iter()
- .filter(|c| self.cryp_targetable(mob_team_id, c.id).is_ok())
- .collect::>();
-
- if targets.len() == 0 {
- panic!("could not find a targetable pve cryp");
- }
-
- let target_id = targets[i % targets.len()].id;
- self.add_target(mob_team_id, target_id, incoming_skill_id).unwrap();
- }
- }
-
- self
- }
-
- // each cryp can be the target of
- // incomingSkills / activeCryps rounded up
- // maybe a problem with friendly / self targeting skills
- fn cryp_targetable(&mut self, team_id: Uuid, cryp_id: Uuid) -> Result<(), Error> {
- // whose team is this?
- let team = self.teams.iter()
- .find(|t| t.id == team_id)
- .ok_or(err_msg("team not found"))?;
-
- // is the target in the team?
- let cryp = team.cryps.iter()
- .find(|c| c.id == cryp_id)
- .ok_or(err_msg("cryp not in team"))?;
-
- if cryp.is_ko() {
- return Err(err_msg("you cannot target ko cryps"));
- }
-
- // let incoming = self.stack.iter()
- // .filter(|i| i.target_team_id == team.id)
- // .count();
-
- // let incoming = incoming as u32 as f64;
-
- // let active_cryps = team.cryps.iter()
- // .filter(|c| !c.is_ko())
- // .count();
-
- // let active_cryps = active_cryps as u32 as f64;
- // let max_targets = (incoming / active_cryps).ceil();
-
- // // println!("targets {:} / {:} = {:}", incoming, active_cryps, max_targets);
-
- // let targeted = self.stack.iter()
- // .filter(|s| s.target_cryp_id.is_some())
- // .filter(|s| s.target_cryp_id.unwrap() == cryp_id)
- // .count();
-
- // if targeted >= max_targets as usize {
- // return Err(format_err!("cryp target of maximum number of skills ({:})", max_targets));
- // }
-
- return Ok(());
- }
-
- // targets can only be added by the owner of the team
- fn add_target(&mut self, team_id: Uuid, cryp_id: Uuid, skill_id: Uuid) -> Result<&mut Cast, Error> {
- if self.phase != Phase::Target {
- return Err(err_msg("game not in target phase"));
- }
-
- self.cryp_targetable(team_id, cryp_id)?;
-
- // set the target
- let cast = match self.stack.iter_mut().find(|s| s.id == skill_id) {
- Some(c) => c,
- None => return Err(err_msg("skill_id not found")),
- };
-
- if cast.skill.self_targeting() {
- return Err(err_msg("skill is self targeting"));
- }
-
- if cast.target_team_id != team_id {
- return Err(err_msg("you cannot target that skill"));
- }
-
- Ok(cast.set_target(cryp_id))
- }
-
- fn target_phase_finished(&self) -> bool {
- self.stack.iter().all(|s| s.target_cryp_id.is_some())
- }
-
- // requires no input
- // just do it
- 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::Resolve;
- self.log.push("".to_string());
-
- self.resolve_skills()
- }
-
- fn log_resolution(&mut self, source: &mut Cryp, target: &mut Cryp, cast: &Cast) -> &mut Game {
- match cast.resolution.disable.disabled {
- true => {
- self.log.push(format!("{:} {:?} {:} disabled {:?}", source.name, cast.skill, target.name, cast.resolution.disable.effects));
- return self;
- },
- false => (),
- };
-
- for result in cast.resolution.results.iter() {
- match result {
- ResolutionResult::Damage { amount, category: _, immunity } => {
- match immunity.immune {
- true => self.log.push(format!("{:} {:?} {:} immune {:?}", source.name, cast.skill, target.name, immunity.effects)),
- false => self.log.push(format!("{:} {:?} {:} {:}", source.name, cast.skill, target.name, amount)),
- }
- },
- ResolutionResult::Healing { amount, overhealing, category: _, immunity } => {
- match immunity.immune {
- true => self.log.push(format!("{:} {:?} {:} immune {:?}", source.name, cast.skill, target.name, immunity.effects)),
- false => self.log.push(format!("{:} {:?} {:} {:} ({:}OH)", source.name, cast.skill, target.name, amount, overhealing)),
- }
- },
- ResolutionResult::Effect { effect, duration, immunity } => {
- match immunity.immune {
- true => self.log.push(format!("{:} {:?} {:} immune {:?}", source.name, cast.skill, target.name, immunity.effects)),
- false => self.log.push(format!("{:} {:?} {:} {:?} {:}T", source.name, cast.skill, target.name, effect, duration)),
- }
- },
- ResolutionResult::Removal { effect, immunity } => {
- match immunity.immune {
- true => self.log.push(format!("{:} {:?} {:} immune {:?}", source.name, cast.skill, target.name, immunity.effects)),
- false => self.log.push(format!("{:?} removed {:} {:?}", source.name, target.name, effect)),
- }
- },
- }
- }
-
- self
- }
-
- fn resolve_skills(&mut self) -> &mut Game {
- if self.phase != Phase::Resolve {
- panic!("game not in Resolve phase");
- }
-
- // find their statuses with ticks
- let mut ticks = self.all_cryps()
- .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_unstable_by_key(|s| s.skill.speed());
- self.stack.reverse();
-
- // update the stack with the resolved skills
- self.stack = self.stack.clone().iter_mut().map(|skill| {
- // println!("{:} resolving ", skill);
- 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();
-
- skill.set_resolution(&mut source, &mut target);
-
- self.log_resolution(&mut source, &mut target, skill);
-
- self.resolved.push(skill.clone());
-
- if target.is_ko() && !target.ko_logged {
- self.log.push(format!("{:} KO", target.name));
- target.effects.clear();
- target.ko_logged = true;
- }
-
- if source.is_ko() && !source.ko_logged {
- self.log.push(format!("{:} KO", source.name));
- source.effects.clear();
- source.ko_logged = true;
- }
-
- self.update_cryp(&mut source);
- self.update_cryp(&mut target);
-
- return skill.clone();
- }).collect::>();
-
- // now Resolve has all been assigned
- // handle cooldowns and statuses
- self.progress_durations();
-
- if self.finished() {
- return self.finish()
- }
-
- self.skill_phase_start()
- }
-
- fn progress_durations(&mut self) -> &mut Game {
- for mut cryp in self.all_cryps() {
- // println!("progressing durations for {:}", cryp.name);
-
- if cryp.is_ko() {
- continue;
- }
-
- // only reduce cooldowns if no cd was used
- // have to borrow self for the skill check
- {
- if let Some(skill) = self.stack.iter_mut().find(|s| s.source_cryp_id == cryp.id) {
- if skill.used_cooldown() {
- cryp.skill_set_cd(skill.skill);
- } else {
- cryp.reduce_cooldowns();
- }
- } else {
- cryp.reduce_cooldowns();
- }
- }
-
- // always reduce durations
- cryp.reduce_effect_durations(&mut self.log);
- self.update_cryp(&mut cryp);
- }
-
- self
- }
-
- fn finished(&self) -> bool {
- self.teams.iter().any(|t| t.cryps.iter().all(|c| c.is_ko()))
- }
-
- pub fn winner(&self) -> Option<&Team> {
- self.teams.iter().find(|t| t.cryps.iter().any(|c| !c.is_ko()))
- }
-
- fn finish(&mut self) -> &mut Game {
- self.phase = Phase::Finish;
- self.log.push(format!("Game finished."));
- self.stack.clear();
-
- {
- let winner = self.teams.iter().find(|t| t.cryps.iter().any(|c| !c.is_ko()));
- match winner {
- Some(w) => self.log.push(format!("Winner: {:}", w.id)),
- None => self.log.push(format!("Game was drawn.")),
- };
- }
-
- self
- }
-}
-
-pub fn game_skill(params: GameSkillParams, tx: &mut Transaction, account: &Account) -> Result {
- let query = "
- SELECT *
- FROM games
- WHERE id = $1
- ";
-
- let result = tx
- .query(query, &[¶ms.game_id])?;
-
- let returned = match result.iter().next() {
- Some(row) => row,
- None => return Err(err_msg("game not found")),
- };
-
- // tells from_slice to cast into a cryp
- let game_bytes: Vec = returned.get("data");
- let mut game = from_slice::(&game_bytes)?;
-
- if game.phase != Phase::Skill {
- return Err(err_msg("game not in skill phase"))
- }
-
- game.add_skill(account.id, params.cryp_id, params.target_team_id, params.skill)?;
-
- if game.skill_phase_finished() {
- game.target_phase_start();
- }
-
- game_update(&game, tx)?;
-
- Ok(game)
-}
-
-pub fn game_target(params: GameTargetParams, tx: &mut Transaction, account: &Account) -> Result {
- let query = "
- SELECT *
- FROM games
- WHERE id = $1
- ";
-
- let result = tx
- .query(query, &[¶ms.game_id])?;
-
- let returned = match result.iter().next() {
- Some(row) => row,
- None => return Err(err_msg("game not found")),
- };
-
- // tells from_slice to cast into a cryp
- let game_bytes: Vec = returned.get("data");
- let mut game = from_slice::(&game_bytes)?;
-
- game.add_target(account.id, params.cryp_id, params.skill_id)?;
-
- if game.target_phase_finished() {
- game.resolve_phase_start();
- }
-
- game_update(&game, tx)?;
-
- Ok(game)
-}
-
-pub fn game_write(game: &Game, tx: &mut Transaction) -> Result<(), Error> {
- let game_bytes = to_vec(&game)?;
-
- let query = "
- INSERT INTO games (id, joinable, data)
- VALUES ($1, $2, $3)
- RETURNING id;
- ";
-
- let result = tx
- .query(query, &[&game.id, &game.joinable(), &game_bytes])?;
-
- result.iter().next().ok_or(format_err!("no game written"))?;
-
- // println!("{:} wrote game", game.id);
-
- return Ok(());
-}
-
-pub fn game_state(params: GameStateParams, tx: &mut Transaction, _account: &Account) -> Result {
- return game_get(tx, params.id)
-}
-
-pub fn game_get(tx: &mut Transaction, id: Uuid) -> Result {
- let query = "
- SELECT *
- FROM games
- WHERE id = $1
- ";
-
- let result = tx
- .query(query, &[&id])?;
-
- let returned = match result.iter().next() {
- Some(row) => row,
- None => return Err(err_msg("game not found")),
- };
-
- // tells from_slice to cast into a cryp
- let game_bytes: Vec = returned.get("data");
- let game = from_slice::(&game_bytes)?;
-
- return Ok(game);
-}
-
-pub fn players_write(account: &Account, game_id: Uuid, tx: &mut Transaction) -> Result<(), Error> {
- // pve
- let id = Uuid::new_v4();
-
- let query = "
- INSERT INTO players (id, game, account)
- VALUES ($1, $2, $3)
- RETURNING id, account;
- ";
-
- let result = tx
- .query(query, &[&id, &game_id, &account.id])?;
-
- let _returned = result.iter().next().expect("no row written");
-
- println!("wrote player {:} joined game: {:}", account.name, game_id);
-
- return Ok(());
-}
-
-pub fn game_update(game: &Game, tx: &mut Transaction) -> Result<(), Error> {
- let game_bytes = to_vec(&game)?;
-
- let query = "
- UPDATE games
- SET data = $1, joinable = $2
- WHERE id = $3
- RETURNING id, data;
- ";
-
- let result = tx
- .query(query, &[&game_bytes, &game.joinable(), &game.id])?;
-
- result.iter().next().ok_or(format_err!("game {:?} could not be written", game))?;
-
- if game.finished() {
- if let Some(t) = game.winner() {
- if !t.id.is_nil() {
- item_drop(tx, t.id)?;
- }
- }
-
- // check for zone update
- if let Some((z, i)) = game.zone {
- node_finish(game, z, i, tx)?;
- }
-
- }
-
- return Ok(());
-}
-
-fn generate_mob(lvl: u8) -> Cryp {
- let mut rng = thread_rng();
-
- let name: String = iter::repeat(())
- .map(|()| rng.sample(Alphanumeric))
- .take(8)
- .collect();
-
- // rng panics on min == max
- // let mob_lvl: u8 = match lvl {
- // 1 => 1,
- // _ => rng.gen_range(lvl.saturating_sub(2), lvl)
- // };
-
- return Cryp::new()
- .named(&name)
- .level(lvl)
- .create();
-
-}
-
-fn generate_mob_team(mode: GameMode, cryps: &Vec) -> Team {
- let mut mob_team = Team::new(Uuid::nil());
-
- // Default settings
- let mut team_size = 1;
-
- // Modify the NPC cryps for game mode settings
- let mob_lvl = match mode {
- GameMode::Normal => {
- team_size = cryps.len();
- cryps.iter().max_by_key(|c| c.lvl).unwrap().lvl
- },
- GameMode::Boss => cryps.iter().max_by_key(|c| c.lvl).unwrap().lvl + 2,
- };
-
- // Generate and return the NPC team based on settings
- let mobs = iter::repeat_with(|| generate_mob(mob_lvl).set_account(Uuid::nil()))
- .take(team_size)
- .collect::>();
- mob_team.set_cryps(mobs);
-
- return mob_team;
-
-}
-
-pub fn game_pve_new(cryp_ids: Vec, mode: GameMode, tx: &mut Transaction, account: &Account) -> Result {
- let cryps = cryp_ids
- .iter()
- .map(|id| cryp_get(tx, *id, account.id))
- .collect::, Error>>()?;
-
- if cryps.len() > 3 {
- return Err(err_msg("team size too large (3 max)"));
- }
-
- // create the game
- let mut game = Game::new();
- // let game_id = game.id;
-
- game
- .set_pve(true)
- .set_team_num(2)
- .set_team_size(cryps.len());
-
- // create the mob team
- let mob_team = generate_mob_team(mode, &cryps);
-
- // add the players
- let mut plr_team = Team::new(account.id);
- plr_team
- .set_cryps(cryps);
-
-
- game
- .team_add(plr_team)?
- .team_add(mob_team)?;
-
- game.start();
-
- return Ok(game);
-}
-
-pub fn game_pve(params: GamePveParams, tx: &mut Transaction, account: &Account) -> Result {
- let game = game_pve_new(params.cryp_ids, params.mode, tx, account)?;
-
- // persist
- game_write(&game, tx)?;
-
- Ok(game)
-}
-
-pub fn game_pvp(params: GamePvpParams, tx: &mut Transaction, account: &Account) -> Result {
- let cryps = params.cryp_ids
- .iter()
- .map(|id| cryp_get(tx, *id, account.id))
- .collect::, Error>>()?;
-
- // create the game
- let mut game = Game::new();
- let game_id = game.id;
-
- game
- .set_pve(false)
- .set_team_num(2)
- .set_team_size(cryps.len());
-
- // create the initiators team
- let mut team = Team::new(account.id);
- team.set_cryps(cryps);
-
- game.team_add(team)?;
-
- // persist
- game_write(&game, tx)?;
- players_write(account, game_id, tx)?;
-
- Ok(game)
-}
-
-pub fn game_join(params: GameJoinParams, tx: &mut Transaction, account: &Account) -> Result {
- let mut game = game_get(tx, params.game_id)?;
-
- // rejoining a game from the FE list
- if game.already_joined(account.id) {
- return Ok(game);
- }
-
- // ok actually adding a new team
- let game_id = game.id;
-
- let cryps = params.cryp_ids
- .iter()
- .map(|id| cryp_get(tx, *id, account.id))
- .collect::, Error>>()?;
-
- if cryps.len() != game.team_size {
- return Err(format_err!("incorrect team size. ({:})", game.team_size));
- }
-
- let mut team = Team::new(account.id);
- team.set_cryps(cryps);
- game.team_add(team)?;
-
- if game.can_start() {
- game.start();
- }
-
- game_update(&game, tx)?;
- players_write(account, game_id, tx)?;
-
- Ok(game)
-}
-
-pub fn game_joinable_list(tx: &mut Transaction, _account: &Account) -> Result, Error> {
- let query = "
- SELECT games.data
- FROM games
- WHERE joinable;
- ";
-
- let result = tx
- .query(query, &[])?;
-
- let games: Result, _> = result.iter().map(|row| {
- let cryp_bytes: Vec = row.get(0);
- from_slice::(&cryp_bytes)
- }).collect();
-
- // catch any errors
- if games.is_err() {
- return Err(err_msg("could not deserialize a game"));
- }
-
- // now unwrap is safe
- return Ok(games.unwrap());
-}
-
-
-#[cfg(test)]
-mod tests {
- use game::*;
- use cryp::*;
-
- fn create_test_game() -> Game {
- let x = Cryp::new()
- .named(&"pronounced \"creeep\"".to_string())
- .level(8)
- .learn(Skill::TestStun)
- .learn(Skill::TestTouch)
- .learn(Skill::TestBlock)
- .learn(Skill::TestParry)
- .learn(Skill::TestSiphon)
- .learn(Skill::Empower)
- .learn(Skill::Block)
- .create();
-
- let y = Cryp::new()
- .named(&"lemongrass tea".to_string())
- .level(8)
- .learn(Skill::TestStun)
- .learn(Skill::TestTouch)
- .learn(Skill::TestBlock)
- .learn(Skill::TestParry)
- .learn(Skill::TestSiphon)
- .learn(Skill::Empower)
- .learn(Skill::Block)
- .create();
-
- let mut game = Game::new();
-
- game
- .set_team_num(2)
- .set_team_size(1)
- .set_pve(false);
-
- let x_team_id = Uuid::new_v4();
- let mut x_team = Team::new(x_team_id);
- x_team
- .set_cryps(vec![x]);
-
- let y_team_id = Uuid::new_v4();
- let mut y_team = Team::new(y_team_id);
- y_team
- .set_cryps(vec![y]);
-
- game
- .team_add(x_team).unwrap()
- .team_add(y_team).unwrap();
-
- assert!(game.can_start());
-
- game.start();
-
- return game;
- }
-
- fn create_2v2_test_game() -> Game {
- let i = Cryp::new()
- .named(&"pretaliate".to_string())
- .level(8)
- .learn(Skill::TestTouch)
- .create();
-
- let j = Cryp::new()
- .named(&"poy sian".to_string())
- .level(8)
- .learn(Skill::TestTouch)
- .create();
-
- let x = Cryp::new()
- .named(&"pronounced \"creeep\"".to_string())
- .level(8)
- .learn(Skill::TestTouch)
- .create();
-
- let y = Cryp::new()
- .named(&"lemongrass tea".to_string())
- .level(8)
- .learn(Skill::TestTouch)
- .create();
-
- let mut game = Game::new();
-
- game
- .set_team_num(2)
- .set_team_size(2)
- .set_pve(false);
-
- let i_team_id = Uuid::new_v4();
- let mut i_team = Team::new(i_team_id);
- i_team
- .set_cryps(vec![i,j]);
-
- let x_team_id = Uuid::new_v4();
- let mut x_team = Team::new(x_team_id);
- x_team
- .set_cryps(vec![x,y]);
-
- game
- .team_add(i_team).unwrap()
- .team_add(x_team).unwrap();
-
- assert!(game.can_start());
-
- game.start();
-
- return game;
- }
-
- #[test]
- fn phase_test() {
- let mut game = create_test_game();
-
- let x_team = game.teams[0].clone();
- let y_team = game.teams[1].clone();
-
- let x_cryp = x_team.cryps[0].clone();
- let y_cryp = y_team.cryps[0].clone();
-
- let x_attack_id = game.add_skill(x_team.id, x_cryp.id, Some(y_team.id), Skill::Attack).unwrap();
- let y_attack_id = game.add_skill(y_team.id, y_cryp.id, Some(x_team.id), Skill::Attack).unwrap();
-
- assert!(game.skill_phase_finished());
-
- game.target_phase_start();
-
- game.add_target(x_team.id, x_cryp.id, y_attack_id).unwrap();
- game.add_target(y_team.id, y_cryp.id, x_attack_id).unwrap();
-
- assert!(game.target_phase_finished());
-
- game.resolve_phase_start();
-
- assert!([Phase::Skill, Phase::Finish].contains(&game.phase));
-
- return;
- }
-
- #[test]
- fn stun_test() {
- let mut game = create_test_game();
-
- let x_team = game.teams[0].clone();
- let y_team = game.teams[1].clone();
-
- let x_cryp = x_team.cryps[0].clone();
- let y_cryp = y_team.cryps[0].clone();
-
- let x_stun_id = game.add_skill(x_team.id, x_cryp.id, Some(y_team.id), Skill::TestStun).unwrap();
- let y_attack_id = game.add_skill(y_team.id, y_cryp.id, Some(x_team.id), Skill::TestTouch).unwrap();
-
- assert!(game.skill_phase_finished());
- game.target_phase_start();
-
- 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();
-
- assert!(game.target_phase_finished());
- game.resolve_phase_start();
-
- // should auto progress back to skill phase
- assert!(game.phase == Phase::Skill);
-
- assert!(game.team_by_id(y_team.id).cryps[0].is_stunned());
- assert!(game.team_by_id(y_team.id).skills_required() == 0);
- }
-
- #[test]
- fn ko_resolution_test() {
- let mut game = create_test_game();
-
- let x_team = game.teams[0].clone();
- let y_team = game.teams[1].clone();
-
- let x_cryp = x_team.cryps[0].clone();
- let y_cryp = y_team.cryps[0].clone();
-
- game.team_by_id(y_team.id).cryp_by_id(y_cryp.id).unwrap().red_damage.set(u64::max_value());
-
- let x_stun_id = game.add_skill(x_team.id, x_cryp.id, Some(y_team.id), Skill::TestStun).unwrap();
- let y_attack_id = game.add_skill(y_team.id, y_cryp.id, Some(x_team.id), Skill::Attack).unwrap();
-
- assert!(game.skill_phase_finished());
- game.target_phase_start();
-
- 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();
-
- assert!(game.target_phase_finished());
- game.resolve_phase_start();
-
- // resolution should have been prevented by KO
- // println!("{:#?}", game);
- assert!(!game.team_by_id(y_team.id).cryps[0].is_stunned());
- assert!(game.phase == Phase::Finish);
- }
-
- #[test]
- fn cooldown_test() {
- let mut game = create_test_game();
-
- let x_team = game.teams[0].clone();
- let y_team = game.teams[1].clone();
-
- let x_cryp = x_team.cryps[0].clone();
- let y_cryp = y_team.cryps[0].clone();
-
- let x_stun_id = game.add_skill(x_team.id, x_cryp.id, Some(y_team.id), Skill::TestTouch).unwrap();
- let y_attack_id = game.add_skill(y_team.id, y_cryp.id, Some(x_team.id), Skill::TestTouch).unwrap();
-
- game.target_phase_start();
- 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.resolve_phase_start();
-
- // should auto progress back to skill phase
- assert!(game.phase == Phase::Skill);
-
-
- // after 1 turn block should be off cooldown
- assert!(game.team_by_id(y_team.id).cryps[0].skill_on_cd(Skill::Block).is_none());
- assert!(game.team_by_id(y_team.id).cryps[0].skill_on_cd(Skill::Empower).is_some());
- assert!(game.team_by_id(x_team.id).cryps[0].skill_on_cd(Skill::Block).is_none());
-
- // second round
- // now we block and it should go back on cd
- let _x_block_id = game.add_skill(x_team.id, x_cryp.id, None, Skill::Block).unwrap();
- let y_touch_id = game.add_skill(y_team.id, y_cryp.id, Some(x_team.id), Skill::TestTouch).unwrap();
-
- game.target_phase_start();
-
- game.add_target(x_team.id, x_cryp.id, y_touch_id).unwrap();
-
- game.resolve_phase_start();
-
- assert!(game.team_by_id(x_team.id).cryps[0].skill_on_cd(Skill::Block).is_some());
- assert!(game.team_by_id(y_team.id).cryps[0].skill_on_cd(Skill::Empower).is_none());
- }
-
- #[test]
- fn parry_test() {
- let mut game = create_test_game();
-
- let x_team = game.teams[0].clone();
- let y_team = game.teams[1].clone();
-
- let x_cryp = x_team.cryps[0].clone();
- let y_cryp = y_team.cryps[0].clone();
-
- let _x_block_id = game.add_skill(x_team.id, x_cryp.id, None, Skill::TestParry).unwrap();
- let y_attack_id = game.add_skill(y_team.id, y_cryp.id, Some(x_team.id), Skill::TestStun).unwrap();
-
- game.target_phase_start();
-
- // ensure you can't target another team's skills
- assert!(game.add_target(x_team.id, y_cryp.id, y_attack_id).is_err());
-
- game.add_target(x_team.id, x_cryp.id, y_attack_id).unwrap();
-
- game.resolve_phase_start();
-
- // should not be stunned because of parry
- assert!(game.team_by_id(x_team.id).cryps[0].is_stunned() == false);
- }
-
- #[test]
- fn siphon_test() {
- let mut game = create_test_game();
-
- let x_team = game.teams[0].clone();
- let y_team = game.teams[1].clone();
-
- let x_cryp = x_team.cryps[0].clone();
- let y_cryp = y_team.cryps[0].clone();
-
- let x_siphon_id = game.add_skill(x_team.id, x_cryp.id, Some(y_team.id), Skill::TestSiphon).unwrap();
- let y_touch_id = game.add_skill(y_team.id, y_cryp.id, Some(x_team.id), Skill::TestTouch).unwrap();
-
- game.target_phase_start();
-
- game.add_target(x_team.id, x_cryp.id, y_touch_id).unwrap();
- game.add_target(y_team.id, y_cryp.id, x_siphon_id).unwrap();
-
- game.resolve_phase_start();
-
- game.add_skill(x_team.id, x_cryp.id, None, Skill::TestBlock).unwrap();
- game.add_skill(y_team.id, y_cryp.id, None, Skill::TestBlock).unwrap();
-
- game.target_phase_start();
-
- assert!(game.resolved.iter().any(|r| r.skill == Skill::SiphonTick));
- }
-
- #[test]
- fn ko_pve_test() {
- let mut game = create_2v2_test_game();
-
- let i_team = game.teams[0].clone();
- let x_team = game.teams[1].clone();
-
- let i_cryp = i_team.cryps[0].clone();
- let j_cryp = i_team.cryps[1].clone();
- let x_cryp = x_team.cryps[0].clone();
- let y_cryp = x_team.cryps[1].clone();
-
- let i_attack_id = game.add_skill(i_team.id, i_cryp.id, Some(x_team.id), Skill::TestTouch).unwrap();
- let j_attack_id = game.add_skill(i_team.id, j_cryp.id, Some(x_team.id), Skill::TestTouch).unwrap();
- let x_attack_id = game.add_skill(x_team.id, x_cryp.id, Some(i_team.id), Skill::TestTouch).unwrap();
- let y_attack_id = game.add_skill(x_team.id, y_cryp.id, Some(i_team.id), Skill::TestTouch).unwrap();
-
-
- assert!(game.skill_phase_finished());
- game.target_phase_start();
-
- game.add_target(i_team.id, i_cryp.id, x_attack_id).unwrap();
- game.add_target(i_team.id, j_cryp.id, y_attack_id).unwrap();
-
- game.add_target(x_team.id, x_cryp.id, i_attack_id).unwrap();
- game.add_target(x_team.id, y_cryp.id, j_attack_id).unwrap();
-
- assert!(game.target_phase_finished());
-
- game.resolve_phase_start();
-
- assert!([Phase::Skill, Phase::Finish].contains(&game.phase));
-
- // kill a cryp
- game.team_by_id(i_team.id).cryp_by_id(i_cryp.id).unwrap().hp.reduce(u64::max_value());
-
- // add some more skills
- let j_attack_id = game.add_skill(i_team.id, j_cryp.id, Some(x_team.id), Skill::TestTouch).unwrap();
- let x_attack_id = game.add_skill(x_team.id, x_cryp.id, Some(i_team.id), Skill::TestTouch).unwrap();
- let y_attack_id = game.add_skill(x_team.id, y_cryp.id, Some(i_team.id), Skill::TestTouch).unwrap();
-
- assert!(game.skill_phase_finished());
- game.target_phase_start();
-
- assert!(game.team_by_id(i_team.id).skills_required() == 1);
- assert!(game.cryp_targetable(i_team.id, i_cryp.id).is_err());
- assert!(game.cryp_targetable(i_team.id, j_cryp.id).is_ok());
-
- game.add_target(i_team.id, j_cryp.id, x_attack_id).unwrap();
- game.add_target(i_team.id, j_cryp.id, y_attack_id).unwrap();
-
- game.add_target(x_team.id, x_cryp.id, j_attack_id).unwrap();
- game.add_target(x_team.id, y_cryp.id, j_attack_id).unwrap();
-
- assert!(game.target_phase_finished());
- return;
- }
-}
diff --git a/server/src/instance.rs b/server/src/instance.rs
index 34dc61ca..1c67786a 100644
--- a/server/src/instance.rs
+++ b/server/src/instance.rs
@@ -106,7 +106,7 @@ pub fn instance_get_open(tx: &mut Transaction) -> Result {
let query = "
SELECT *
FROM instance
- AND open = true;
+ WHERE open = true;
";
let result = tx
diff --git a/server/src/player.rs b/server/src/player.rs
index 1fb19389..54d6aa6b 100644
--- a/server/src/player.rs
+++ b/server/src/player.rs
@@ -8,10 +8,9 @@ use failure::Error;
use failure::err_msg;
use account::Account;
-use instance::Instance;
-use cryp::{Cryp};
+use cryp::{Cryp, cryp_get};
use vbox::{Vbox};
-use rpc::{PlayerStateParams};
+use rpc::{PlayerStateParams, PlayerCrypsSetParams};
#[derive(Debug,Clone,Serialize,Deserialize)]
pub struct Score {
@@ -106,3 +105,24 @@ pub fn player_update(tx: &mut Transaction, player: Player) -> Result Result {
player_get(tx, account.id, params.instance_id)
}
+
+pub fn player_cryps_set(params: PlayerCrypsSetParams, tx: &mut Transaction, account: &Account) -> Result {
+ if params.instance_id != Uuid::nil() {
+ return Err(err_msg("only the global team can be replaced"));
+ }
+
+ if params.cryp_ids.len() != 3 {
+ return Err(err_msg("team size is 3"));
+ }
+
+ let mut player = player_get(tx, account.id, params.instance_id)?;
+
+ let cryps = params.cryp_ids
+ .iter()
+ .map(|id| cryp_get(tx, *id, account.id))
+ .collect::, Error>>()?;
+
+ player.cryps = cryps;
+
+ player_update(tx, player)
+}
diff --git a/server/src/rpc.rs b/server/src/rpc.rs
index c1ebc729..c8e8a4b0 100644
--- a/server/src/rpc.rs
+++ b/server/src/rpc.rs
@@ -21,7 +21,7 @@ use account::{Account, account_create, account_login, account_from_token, accoun
use skill::{Skill};
use zone::{Zone, zone_create, zone_join, zone_close};
use spec::{Spec};
-use player::{player_state, player_create, Player};
+use player::{player_state, player_create, player_cryps_set, Player};
use instance::{instance_join};
use vbox::{vbox_accept, vbox_apply, vbox_discard, vbox_combine, vbox_drop};
@@ -91,7 +91,10 @@ impl Rpc {
return response;
},
- Err(_e) => Err(err_msg("invalid message")),
+ Err(e) => {
+ println!("{:?}", e);
+ Err(err_msg("invalid message"))
+ },
}
}
@@ -114,7 +117,7 @@ impl Rpc {
return Ok(game_response);
}
- fn game_pve(data: Vec, tx: &mut Transaction, account: Account, client: &mut WebSocket) -> Result {
+ fn game_pve(data: Vec, tx: &mut Transaction, account: Account, _client: &mut WebSocket) -> Result {
let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?;
let game_response = RpcResponse {
@@ -122,11 +125,6 @@ impl Rpc {
params: RpcResult::GameState(game_pve(msg.params, tx, &account)?)
};
- Rpc::send_msg(client, RpcResponse {
- method: "account_cryps".to_string(),
- params: RpcResult::CrypList(account_cryps(tx, &account)?)
- })?;
-
return Ok(game_response);
}
@@ -272,7 +270,6 @@ impl Rpc {
return Ok(response);
}
-
fn player_state(data: Vec, tx: &mut Transaction, account: Account, _client: &mut WebSocket) -> Result {
let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?;
@@ -284,6 +281,17 @@ impl Rpc {
return Ok(response);
}
+ fn player_cryps_set(data: Vec, tx: &mut Transaction, account: Account, _client: &mut WebSocket) -> Result {
+ let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?;
+
+ let response = RpcResponse {
+ method: "player_state".to_string(),
+ params: RpcResult::PlayerState(player_cryps_set(msg.params, tx, &account)?)
+ };
+
+ return Ok(response);
+ }
+
fn player_vbox_accept(data: Vec, tx: &mut Transaction, account: Account, _client: &mut WebSocket) -> Result {
let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?;
@@ -362,7 +370,6 @@ pub enum RpcResult {
Account(Account),
CrypList(Vec),
GameState(Game),
- GameJoinableList(Vec),
ZoneState(Zone),
ZoneClose(()),
@@ -540,6 +547,18 @@ pub struct PlayerStateParams {
pub instance_id: Uuid,
}
+#[derive(Debug,Clone,Serialize,Deserialize)]
+struct PlayerCrypsSetMsg {
+ method: String,
+ params: PlayerCrypsSetParams,
+}
+
+#[derive(Debug,Clone,Serialize,Deserialize)]
+pub struct PlayerCrypsSetParams {
+ pub instance_id: Uuid,
+ pub cryp_ids: Vec,
+}
+
#[derive(Debug,Clone,Serialize,Deserialize)]
struct VboxAcceptMsg {
method: String,
diff --git a/server/src/vbox.rs b/server/src/vbox.rs
index 05674889..c487fd34 100644
--- a/server/src/vbox.rs
+++ b/server/src/vbox.rs
@@ -50,7 +50,7 @@ pub enum Var {
Strangle,
Strike,
Siphon,
- Survival,
+ Clutch,
Taunt,
Throw,
Toxic,
@@ -91,7 +91,7 @@ impl Var {
Var::Snare => Ok(Skill::Snare),
Var::Strangle => Ok(Skill::Strangle),
// Var::Strike => Ok(Skill::Strike),
- // Var::Survival => Ok(Skill::Survival),
+ // Var::Clutch => Ok(Skill::Clutch),
// Var::Taunt => Ok(Skill::Taunt),
Var::Throw => Ok(Skill::Throw),
// Var::Toxic => Ok(Skill::Toxic),
@@ -247,7 +247,7 @@ impl Vbox {
ColourCode::RR => Var::Empower,
ColourCode::GG => Var::Triage,
ColourCode::BB => Var::Amplify,
- ColourCode::RG => Var::Survival,
+ ColourCode::RG => Var::Clutch,
ColourCode::GB => return Err(err_msg("unhandled skill combo")),
ColourCode::BR => Var::Haste,
},