Co-op campaign matches and command markers: mission relay, allied masters, temporary flags
This commit is contained in:
1 parent
dc22e27184
commit
5928acf7f6
12 files changed
+428
-22
No files matched your search
@@ -618,6 +618,8 @@ net.error = {0}
|
||||
net.rooms = Offene Partien
|
||||
net.no_rooms = Keine offenen Partien.
|
||||
net.create = Partie erstellen
|
||||
net.coop_mission = Koop-Kampagne: Mission {0} ({1})
|
||||
net.coop_create = Koop-Kampagne
|
||||
net.join = Beitreten
|
||||
net.join_hint = Partiecode
|
||||
net.seats = Plätze
|
||||
|
||||
@@ -619,6 +619,8 @@ net.error = {0}
|
||||
net.rooms = Open matches
|
||||
net.no_rooms = No open matches.
|
||||
net.create = Create match
|
||||
net.coop_mission = Co-op campaign: mission {0} ({1})
|
||||
net.coop_create = Co-op campaign
|
||||
net.join = Join
|
||||
net.join_hint = Match code
|
||||
net.seats = Seats
|
||||
|
||||
+43
-5
@@ -6,7 +6,7 @@
|
||||
//
|
||||
// Lobby messages (JSON, one object per WebSocket frame):
|
||||
// {"t":"rooms"} -> list of open matches
|
||||
// {"t":"create","sides":[0,2,3,4]?,"map_w":96?,"map_h":128?,"difficulty":1?}
|
||||
// {"t":"create","sides":[0,2,3,4]?,"map_w":96?,"map_h":128?,"difficulty":1?,"mission":12?}
|
||||
// {"t":"join","code":"ABC123"}
|
||||
// {"t":"take_side","side":2}
|
||||
// {"t":"ready","ready":true}
|
||||
@@ -41,6 +41,7 @@ const CHAT_MAX: usize = 80;
|
||||
const MAX_PENDING: usize = 200;
|
||||
const MAX_ROOMS: usize = 64;
|
||||
const MASTER_SIDES: [u8; 4] = [0, 2, 3, 4];
|
||||
const MAX_MISSIONS: usize = 30; // keep this in step with the campaign mission count in the client
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Netplay {
|
||||
@@ -72,6 +73,7 @@ struct Room {
|
||||
map_w: usize,
|
||||
map_h: usize,
|
||||
difficulty: u8,
|
||||
mission: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -136,6 +138,13 @@ fn sides_of(v: Option<&Value>) -> Vec<u8> {
|
||||
out
|
||||
}
|
||||
|
||||
fn mission_of(v: Option<&Value>) -> Option<usize> {
|
||||
match v {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(x) => x.as_u64().map(|m| m as usize),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn netplay_ws(ws: WebSocketUpgrade, Query(q): Query<WsQuery>, State(st): State<AppState>) -> Response {
|
||||
ws.on_upgrade(move |socket| handle_socket(socket, st, q))
|
||||
}
|
||||
@@ -279,9 +288,14 @@ impl Netplay {
|
||||
let map_w = msg.get("map_w").and_then(|v| v.as_u64()).unwrap_or(96).clamp(72, 256) as usize;
|
||||
let map_h = msg.get("map_h").and_then(|v| v.as_u64()).unwrap_or(128).clamp(72, 256) as usize;
|
||||
let difficulty = msg.get("difficulty").and_then(|v| v.as_u64()).unwrap_or(1).clamp(0, 2) as u8;
|
||||
let mission = mission_of(msg.get("mission"));
|
||||
if mission.map_or(false, |m| m >= MAX_MISSIONS) {
|
||||
self.error(conn, "unknown campaign mission");
|
||||
return None;
|
||||
}
|
||||
let name = self.clients.get(&conn).map(|c| c.name.clone()).unwrap_or_else(|| "Guest".into());
|
||||
let seats = sides.iter().map(|s| (*s, Seat { conn: None, name: String::new(), ready: false })).collect::<HashMap<_, _>>();
|
||||
let room = Room { code: code.clone(), sides, seats, pending: HashMap::new(), turn: 0, started: false, paused: false, missing: false, seed: None, map_w, map_h, difficulty };
|
||||
let room = Room { code: code.clone(), sides, seats, pending: HashMap::new(), turn: 0, started: false, paused: false, missing: false, seed: None, map_w, map_h, difficulty, mission };
|
||||
self.rooms.insert(code.clone(), room);
|
||||
if let Some(c) = self.clients.get_mut(&conn) {
|
||||
c.room = Some(code.clone());
|
||||
@@ -422,7 +436,7 @@ impl Netplay {
|
||||
r.turn = 0;
|
||||
}
|
||||
let msg = match self.rooms.get(&code) {
|
||||
Some(r) => json!({"t":"start","seed":r.seed.unwrap_or(0),"map_w":r.map_w,"map_h":r.map_h,"difficulty":r.difficulty,"sides":r.sides}),
|
||||
Some(r) => json!({"t":"start","seed":r.seed.unwrap_or(0),"map_w":r.map_w,"map_h":r.map_h,"difficulty":r.difficulty,"sides":r.sides,"mission":r.mission}),
|
||||
None => return,
|
||||
};
|
||||
self.room_msg(conn, &code);
|
||||
@@ -648,7 +662,8 @@ impl Netplay {
|
||||
"map_w":r.map_w,
|
||||
"map_h":r.map_h,
|
||||
"difficulty":r.difficulty,
|
||||
"seed":r.seed
|
||||
"seed":r.seed,
|
||||
"mission":r.mission
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -658,7 +673,7 @@ impl Netplay {
|
||||
.values()
|
||||
.map(|r| {
|
||||
let players = r.seats.values().filter(|s| s.conn.is_some()).count();
|
||||
json!({"code":r.code,"players":players,"sides":r.sides,"started":r.started,"paused":r.paused})
|
||||
json!({"code":r.code,"players":players,"sides":r.sides,"started":r.started,"paused":r.paused,"mission":r.mission})
|
||||
})
|
||||
.collect();
|
||||
self.send(conn, json!({"t":"rooms","rooms":rooms}));
|
||||
@@ -720,6 +735,29 @@ mod tests {
|
||||
assert_eq!(sum["sum"].as_u64(), Some(12345678901234567890u64));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_room_remembers_a_coop_campaign_mission() {
|
||||
let mut net = Netplay::new();
|
||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
let red = net.connect(tx, Some("red".into()), None);
|
||||
let rooms = net.handle(red, json!({"t":"create","sides":[0,2],"mission":2}));
|
||||
let code = rooms[0].clone();
|
||||
let room = until(&mut rx, "room");
|
||||
assert_eq!(room["mission"].as_u64(), Some(2));
|
||||
|
||||
let (tx2, mut rx2) = mpsc::unbounded_channel();
|
||||
let green = net.connect(tx2, Some("green".into()), None);
|
||||
net.handle(green, json!({"t":"join","code":&code}));
|
||||
net.handle(red, json!({"t":"ready"}));
|
||||
net.handle(green, json!({"t":"ready"}));
|
||||
net.handle(red, json!({"t":"start"}));
|
||||
|
||||
let start_red = until(&mut rx, "start");
|
||||
let start_green = until(&mut rx2, "start");
|
||||
assert_eq!(start_red["mission"], start_green["mission"]);
|
||||
assert_eq!(start_red["mission"].as_u64(), Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_non_host_cannot_start_a_match() {
|
||||
let mut net = Netplay::new();
|
||||
|
||||
+57
-2
@@ -476,6 +476,15 @@ fn place_cave(g: &mut Game, d: &CaveDef, w: usize, h: usize) {
|
||||
|
||||
// Builds the game of mission i (carry: what the last mission handed over)
|
||||
pub fn start_mission(i: usize, carry: Option<&Carry>) -> Game {
|
||||
start_mission_mode(i, carry, false)
|
||||
}
|
||||
|
||||
// A co-op campaign mission: the same mission, but without rival masters on playable sides.
|
||||
pub fn start_coop_mission(i: usize, carry: Option<&Carry>) -> Game {
|
||||
start_mission_mode(i, carry, true)
|
||||
}
|
||||
|
||||
fn start_mission_mode(i: usize, carry: Option<&Carry>, coop: bool) -> Game {
|
||||
let all = missions();
|
||||
let d = &all[i.min(all.len() - 1)];
|
||||
let (w, h) = d.size;
|
||||
@@ -498,11 +507,13 @@ pub fn start_mission(i: usize, carry: Option<&Carry>) -> Game {
|
||||
for c in &d.caves {
|
||||
place_cave(&mut g, c, w, h);
|
||||
}
|
||||
if !coop {
|
||||
for &(rx, ry, diff, side) in &d.rivals {
|
||||
let x = ((rx * w as f32) as i32).clamp(10, w as i32 - 10);
|
||||
let y = ((ry * h as f32) as i32).clamp(10, h as i32 - 10);
|
||||
g.add_rival(side, x, y, diff);
|
||||
}
|
||||
}
|
||||
g.place_dwarf_caves(d.seed);
|
||||
g.add_wilds(d.seed, false);
|
||||
g.init_research(Some(i));
|
||||
@@ -514,9 +525,14 @@ pub fn start_mission(i: usize, carry: Option<&Carry>) -> Game {
|
||||
g.gate_on = heart;
|
||||
g.guard_treasures();
|
||||
g.place_all_torches();
|
||||
let goals: Vec<Goal> = if coop {
|
||||
d.goals.iter().filter(|&g| *g != Goal::Rivals).cloned().collect()
|
||||
} else {
|
||||
d.goals.clone()
|
||||
};
|
||||
g.mission = Some(MissionState {
|
||||
index: i,
|
||||
goals: d.goals.clone(),
|
||||
goals,
|
||||
tools: TOOL_UNLOCK.iter().map(|&u| u <= i).collect(),
|
||||
waves: d.waves,
|
||||
wave_limit: d.wave_limit,
|
||||
@@ -548,6 +564,30 @@ pub fn start_mission(i: usize, carry: Option<&Carry>) -> Game {
|
||||
g
|
||||
}
|
||||
|
||||
// missions that can be played in co-op: they fit the multiplayer master areas and do not
|
||||
// depend on rival masters as their only goal
|
||||
pub fn is_coop_mission(i: usize) -> bool {
|
||||
missions().get(i).map_or(false, |d| d.size.0 >= 72 && d.size.1 >= 72 && d.goals.iter().any(|&g| g != Goal::Rivals))
|
||||
}
|
||||
|
||||
pub fn coop_mission_indexes() -> Vec<usize> {
|
||||
missions()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, d)| d.size.0 >= 72 && d.size.1 >= 72 && d.goals.iter().any(|&g| g != Goal::Rivals))
|
||||
.map(|(i, _)| i)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn next_coop_mission(i: usize, dir: i32) -> usize {
|
||||
let list = coop_mission_indexes();
|
||||
if list.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
let pos = list.iter().position(|m| *m == i).unwrap_or(0);
|
||||
list[((pos as i32 + dir).rem_euclid(list.len() as i32)) as usize]
|
||||
}
|
||||
|
||||
// the strongest creatures and a fifth of the gold go along into the next mission
|
||||
pub fn carry_from(g: &Game) -> Carry {
|
||||
let mut c: Vec<&Ent> = g.ents.iter().filter(|e| e.alive && e.creature()).collect();
|
||||
@@ -635,6 +675,21 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coop_missions_drop_rivals_and_are_not_decided_at_once() {
|
||||
let list = coop_mission_indexes();
|
||||
assert!(list.len() > 3, "co-op campaign needs selectable missions");
|
||||
for &i in &list {
|
||||
assert!(is_coop_mission(i), "mission {} selectable in co-op", i);
|
||||
let g = start_coop_mission(i, None);
|
||||
assert!(g.rivals.is_empty(), "mission {} must not spawn rival masters", i);
|
||||
let m = g.mission.as_ref().unwrap();
|
||||
assert!(!m.goals.iter().any(|g| *g == Goal::Rivals), "mission {} keeps the rival goal", i);
|
||||
assert!(!m.goals.is_empty(), "mission {} has a playable goal after dropping rivals", i);
|
||||
assert!(!m.goals.iter().all(|&goal| g.goal_done(goal)), "mission {} is decided at the start", i);
|
||||
}
|
||||
}
|
||||
|
||||
// A simple scripted player: digs a corridor to the shard, bridges rivers on the way and
|
||||
// sends a few strong creatures along with the flag.
|
||||
fn play_to_shard(i: usize) -> (u8, f32) {
|
||||
@@ -648,7 +703,7 @@ mod tests {
|
||||
match t.kind {
|
||||
Kind::Rock | Kind::Core | Kind::Tower => None,
|
||||
Kind::Water | Kind::Lava => Some(3),
|
||||
k if t.solid() => Some(if t.reinforced { 6 } else { 2 }),
|
||||
_ if t.solid() => Some(if t.reinforced { 6 } else { 2 }),
|
||||
_ => Some(1),
|
||||
}
|
||||
}, |j| j == shard, false)
|
||||
|
||||
+61
-1
@@ -3,7 +3,7 @@
|
||||
// network match run exactly the same path. Commands are applied at step boundaries on every client,
|
||||
// in the order the server confirmed them.
|
||||
|
||||
use crate::game::{Game, TowerKind};
|
||||
use crate::game::{Game, TowerKind, MARK_ATTACK, MARK_BRIDGE, MARK_CANCEL, MARK_DIG, MARK_DROP, MARK_FLAG, MARK_GUARD, MARK_HEAL, MARK_LIGHTNING, MARK_PICKUP, MARK_ROOM, MARK_SELL, MARK_SLAP, MARK_SPELL, MARK_STEEL, MARK_SUMMON, MARK_TOWER, MARK_TRAP};
|
||||
use crate::spells::Spell;
|
||||
use crate::traps::TrapKind;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -103,6 +103,7 @@ impl Game {
|
||||
let seq = self.cmd_seq;
|
||||
self.cmd_seq = self.cmd_seq.wrapping_add(1);
|
||||
self.pending.push(Command { side: self.local_side, seq, act });
|
||||
self.mark_command(act, self.local_side);
|
||||
}
|
||||
|
||||
// one command of one master: that master's state is switched in, the action runs, the local
|
||||
@@ -113,6 +114,7 @@ impl Game {
|
||||
}
|
||||
self.switch_to(c.side);
|
||||
self.run_act(c.act);
|
||||
self.mark_command(c.act, c.side);
|
||||
let local = self.local_side;
|
||||
self.switch_to(local);
|
||||
}
|
||||
@@ -127,6 +129,39 @@ impl Game {
|
||||
}
|
||||
}
|
||||
|
||||
fn mark_command(&mut self, act: Act, side: u8) {
|
||||
let ((x, y), kind, dur) = match act {
|
||||
Act::Dig(r) => (rect_center(r), MARK_DIG, 4.0),
|
||||
Act::Room { r, .. } => (rect_center(r), MARK_ROOM, 5.0),
|
||||
Act::Sell(r) => (rect_center(r), MARK_SELL, 4.0),
|
||||
Act::Bridge(r) => (rect_center(r), MARK_BRIDGE, 4.0),
|
||||
Act::Steel(r) => (rect_center(r), MARK_STEEL, 4.0),
|
||||
Act::Tower { x, y, .. } => (point(x as f32, y as f32), MARK_TOWER, 6.0),
|
||||
Act::Trap { x, y, .. } => (point(x as f32, y as f32), MARK_TRAP, 6.0),
|
||||
Act::Cast { x, y, .. } => (point(x, y), MARK_SPELL, 5.0),
|
||||
Act::Lightning { x, y } => (point(x, y), MARK_LIGHTNING, 5.0),
|
||||
Act::Heal { x, y } => (point(x, y), MARK_HEAL, 5.0),
|
||||
Act::Summon { x, y } => (point(x, y), MARK_SUMMON, 6.0),
|
||||
Act::Flag { x, y } => (point(x as f32, y as f32), MARK_FLAG, 4.0),
|
||||
Act::Pickup { ent } => match self.find(ent) {
|
||||
Some(j) => (point(self.ents[j].x as f32, self.ents[j].y as f32), MARK_PICKUP, 3.5),
|
||||
None => (point(self.map.core.0 as f32, self.map.core.1 as f32), MARK_PICKUP, 3.5),
|
||||
},
|
||||
Act::Drop { x, y } => (point(x, y), MARK_DROP, 3.5),
|
||||
Act::Slap { ent } => match self.find(ent) {
|
||||
Some(j) => (point(self.ents[j].x as f32, self.ents[j].y as f32), MARK_SLAP, 4.0),
|
||||
None => (point(self.map.core.0 as f32, self.map.core.1 as f32), MARK_SLAP, 4.0),
|
||||
},
|
||||
Act::Attack { ent, x, y } => match ent.and_then(|id| self.find(id)) {
|
||||
Some(j) => (point(self.ents[j].x as f32, self.ents[j].y as f32), MARK_ATTACK, 10.0),
|
||||
None => (point(x, y), MARK_ATTACK, 10.0),
|
||||
},
|
||||
Act::Guard { x, y } => (point(x as f32, y as f32), MARK_GUARD, 10.0),
|
||||
Act::CancelOrders => (point(self.map.core.0 as f32, self.map.core.1 as f32), MARK_CANCEL, 4.0),
|
||||
};
|
||||
self.add_mark(x, y, kind, side, dur);
|
||||
}
|
||||
|
||||
fn run_act(&mut self, act: Act) {
|
||||
match act {
|
||||
Act::Dig(r) => {
|
||||
@@ -165,6 +200,17 @@ impl Game {
|
||||
}
|
||||
}
|
||||
|
||||
fn rect_center(r: Rect) -> (f32, f32) {
|
||||
(
|
||||
(r.x0 as f32 + r.x1 as f32 + 1.0) * 0.5,
|
||||
(r.y0 as f32 + r.y1 as f32 + 1.0) * 0.5,
|
||||
)
|
||||
}
|
||||
|
||||
fn point(x: f32, y: f32) -> (f32, f32) {
|
||||
(x.floor() + 0.5, y.floor() + 0.5)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -184,11 +230,25 @@ mod tests {
|
||||
let (x, y) = (cx + 6, cy); // outside the treasury ring: rock that can be marked
|
||||
g.queue(Act::Dig(Rect::new(x, y, x + 1, y + 1)));
|
||||
assert!(!g.map.t[idx(x, y)].marked, "queued, not applied yet");
|
||||
assert!(g.marks.iter().any(|m| m.kind == MARK_DIG), "queued command has a marker");
|
||||
g.update(STEP);
|
||||
assert!(g.map.t[idx(x, y)].marked, "applied at the step boundary");
|
||||
assert!(g.pending.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn commands_leave_visible_markers() {
|
||||
let mut g = game(12);
|
||||
let (cx, cy) = g.map.core;
|
||||
g.queue(Act::Dig(Rect::new(cx + 6, cy, cx + 7, cy + 1)));
|
||||
g.queue(Act::Guard { x: cx, y: cy });
|
||||
g.queue(Act::Attack { ent: None, x: (cx + 20) as f32, y: (cy + 20) as f32 });
|
||||
g.update(STEP);
|
||||
assert!(g.marks.iter().any(|m| m.kind == MARK_DIG && m.side == 0), "dig marker");
|
||||
assert!(g.marks.iter().any(|m| m.kind == MARK_GUARD && m.side == 0), "guard marker");
|
||||
assert!(g.marks.iter().any(|m| m.kind == MARK_ATTACK && m.side == 0), "attack marker");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_command_of_another_master_leaves_the_local_one_alone() {
|
||||
let mut g = game(8);
|
||||
|
||||
+74
-5
@@ -611,11 +611,43 @@ pub struct Game {
|
||||
#[serde(skip)]
|
||||
pub ai_difficulty: u8, // difficulty used when a left side becomes an AI rival
|
||||
#[serde(skip)]
|
||||
pub coop: bool, // co-op match: the playable master sides are on one side against the heroes
|
||||
#[serde(skip)]
|
||||
pub marks: Vec<Mark>, // short-lived markers for commands given by the masters
|
||||
#[serde(skip)]
|
||||
pub hints: bool, // show hint messages (setting)
|
||||
#[serde(skip, default = "hint_times")]
|
||||
hint_t: [f32; 32], // last time each hint slot was shown
|
||||
}
|
||||
|
||||
pub const MARK_DIG: u8 = 0;
|
||||
pub const MARK_ROOM: u8 = 1;
|
||||
pub const MARK_SELL: u8 = 2;
|
||||
pub const MARK_BRIDGE: u8 = 3;
|
||||
pub const MARK_STEEL: u8 = 4;
|
||||
pub const MARK_TOWER: u8 = 5;
|
||||
pub const MARK_TRAP: u8 = 6;
|
||||
pub const MARK_SPELL: u8 = 7;
|
||||
pub const MARK_LIGHTNING: u8 = 8;
|
||||
pub const MARK_HEAL: u8 = 9;
|
||||
pub const MARK_SUMMON: u8 = 10;
|
||||
pub const MARK_FLAG: u8 = 11;
|
||||
pub const MARK_PICKUP: u8 = 12;
|
||||
pub const MARK_DROP: u8 = 13;
|
||||
pub const MARK_SLAP: u8 = 14;
|
||||
pub const MARK_ATTACK: u8 = 15;
|
||||
pub const MARK_GUARD: u8 = 16;
|
||||
pub const MARK_CANCEL: u8 = 17;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Mark {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub kind: u8,
|
||||
pub side: u8,
|
||||
pub until: u64,
|
||||
}
|
||||
|
||||
fn hint_times() -> [f32; 32] {
|
||||
[-1000.0; 32]
|
||||
}
|
||||
@@ -794,6 +826,8 @@ impl Game {
|
||||
netplay: None,
|
||||
ai_sides: Vec::new(),
|
||||
ai_difficulty: 1,
|
||||
coop: false,
|
||||
marks: Vec::new(),
|
||||
hint_t: hint_times(),
|
||||
reserve: 0.0,
|
||||
incoming: Vec::new(),
|
||||
@@ -1237,6 +1271,9 @@ impl Game {
|
||||
std::mem::take(&mut self.pending)
|
||||
};
|
||||
self.apply_turn(&cmds, steps);
|
||||
if !self.marks.is_empty() {
|
||||
self.marks.retain(|m| self.sim_steps < m.until);
|
||||
}
|
||||
// what only the camera cares about runs in real time: it is not part of the shared state
|
||||
self.update_particles(dt);
|
||||
for f in self.fx.iter_mut() {
|
||||
@@ -1271,6 +1308,14 @@ impl Game {
|
||||
side == self.me_side || self.masters.iter().any(|m| m.side == side)
|
||||
}
|
||||
|
||||
fn is_hostile_master(&self, side: u8) -> bool {
|
||||
self.coop && Owner::MASTER_SIDES.contains(&self.me_side) && Owner::MASTER_SIDES.contains(&side)
|
||||
}
|
||||
|
||||
pub(crate) fn is_hostile(&self, e: &Ent) -> bool {
|
||||
e.alive && !e.held && e.captive == 0 && !e.chained && e.side() != self.me_side && !self.is_hostile_master(e.side())
|
||||
}
|
||||
|
||||
pub fn is_rival_side(&self, side: u8) -> bool {
|
||||
self.rivals.iter().any(|r| r.alive && r.side == side)
|
||||
}
|
||||
@@ -2316,8 +2361,9 @@ impl Game {
|
||||
let mut best = None;
|
||||
let mut bd = radius * radius;
|
||||
for o in &self.ents {
|
||||
// every other side is an enemy
|
||||
if !o.alive || o.held || o.side() == e.side() || o.captive != 0 || o.chained || o.invis > 0.0 {
|
||||
// every other side is an enemy; co-op master sides fight the heroes, not each other
|
||||
let coop_allies = self.coop && Owner::MASTER_SIDES.contains(&e.side()) && Owner::MASTER_SIDES.contains(&o.side());
|
||||
if !o.alive || o.held || o.side() == e.side() || coop_allies || o.captive != 0 || o.chained || o.invis > 0.0 {
|
||||
continue;
|
||||
}
|
||||
let d = (o.x - e.x).powi(2) + (o.y - e.y).powi(2);
|
||||
@@ -2789,8 +2835,9 @@ impl Game {
|
||||
self.contested.iter_mut().for_each(|c| *c = false);
|
||||
}
|
||||
for e in &self.ents {
|
||||
// any enemy of the player (heroes and the rivals' units) keeps ground from being claimed
|
||||
if !e.alive || e.held || e.captive != 0 || e.side() == 0 {
|
||||
// any enemy of the player (heroes and the rivals' units) keeps ground from being claimed.
|
||||
// In co-op matches, allied master sides do not block the player's own ground claims.
|
||||
if !e.alive || e.held || e.captive != 0 || e.side() == 0 || (self.coop && Owner::MASTER_SIDES.contains(&e.side())) {
|
||||
continue;
|
||||
}
|
||||
let (x, y) = (e.x as i32, e.y as i32);
|
||||
@@ -3788,6 +3835,21 @@ impl Game {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_mark(&mut self, x: f32, y: f32, kind: u8, side: u8, duration: f32) {
|
||||
if duration <= 0.0 || !x.is_finite() || !y.is_finite() {
|
||||
return;
|
||||
}
|
||||
let until = self.sim_steps + (duration / STEP).ceil() as u64;
|
||||
if let Some(m) = self.marks.iter_mut().find(|m| (m.x - x).abs() < 0.75 && (m.y - y).abs() < 0.75 && m.kind == kind && m.side == side) {
|
||||
m.until = m.until.max(until);
|
||||
return;
|
||||
}
|
||||
if self.marks.len() >= 80 {
|
||||
self.marks.remove(0);
|
||||
}
|
||||
self.marks.push(Mark { x, y, kind, side, until });
|
||||
}
|
||||
|
||||
fn creature_choose(&mut self, e: &mut Ent) {
|
||||
let me = e.id + 1;
|
||||
let cur = e.tile();
|
||||
@@ -4928,7 +4990,14 @@ impl Game {
|
||||
|
||||
// a unit the tower shoots at
|
||||
fn tower_enemy(&self, t: &Tower, e: &Ent) -> bool {
|
||||
e.captive == 0 && !e.chained && if t.hero { e.side() != 1 } else { e.side() != 0 }
|
||||
let target = if t.hero {
|
||||
e.side() != 1
|
||||
} else if self.coop {
|
||||
e.side() == 1
|
||||
} else {
|
||||
e.side() != 0
|
||||
};
|
||||
e.captive == 0 && !e.chained && target
|
||||
}
|
||||
|
||||
// a hero attacks a tower; false = tower gone or unreachable
|
||||
|
||||
+21
-2
@@ -75,6 +75,7 @@ pub struct StartInfo {
|
||||
pub difficulty: u8,
|
||||
pub sides: Vec<u8>,
|
||||
pub local_side: Option<u8>,
|
||||
pub mission: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
@@ -169,6 +170,14 @@ impl MatchClient {
|
||||
}
|
||||
|
||||
pub fn create(&mut self, sides: &[u8], map_w: usize, map_h: usize, difficulty: u8) {
|
||||
self.send_create(sides, map_w, map_h, difficulty, None);
|
||||
}
|
||||
|
||||
pub fn create_coop(&mut self, mission: usize, sides: &[u8], map_w: usize, map_h: usize, difficulty: u8) {
|
||||
self.send_create(sides, map_w, map_h, difficulty, Some(mission));
|
||||
}
|
||||
|
||||
fn send_create(&mut self, sides: &[u8], map_w: usize, map_h: usize, difficulty: u8, mission: Option<usize>) {
|
||||
if !self.connected {
|
||||
return;
|
||||
}
|
||||
@@ -176,7 +185,7 @@ impl MatchClient {
|
||||
self.status = None;
|
||||
self.chat.clear();
|
||||
self.chat_seen = 0;
|
||||
self.outbox.push_back(json!({"t":"create","sides":sides,"map_w":map_w,"map_h":map_h,"difficulty":difficulty}).to_string());
|
||||
self.outbox.push_back(json!({"t":"create","sides":sides,"map_w":map_w,"map_h":map_h,"difficulty":difficulty,"mission":mission}).to_string());
|
||||
}
|
||||
|
||||
pub fn join(&mut self, code: &str) {
|
||||
@@ -384,6 +393,7 @@ impl MatchClient {
|
||||
let map_h = v.get("map_h").and_then(|x| x.as_u64()).unwrap_or(128) as usize;
|
||||
let difficulty = v.get("difficulty").and_then(|x| x.as_u64()).unwrap_or(1) as u8;
|
||||
let sides = array_bytes(v.get("sides"));
|
||||
let mission = v.get("mission").and_then(|x| x.as_u64()).map(|x| x as usize);
|
||||
let local_side = self.room.as_ref().and_then(|r| r.seats.iter().find(|s| s.name == self.name && s.connected).map(|s| s.side));
|
||||
self.local_side = local_side;
|
||||
if let Some(r) = self.room.as_mut() {
|
||||
@@ -395,7 +405,7 @@ impl MatchClient {
|
||||
r.sides = sides.clone();
|
||||
}
|
||||
}
|
||||
self.start = Some(StartInfo { seed, map_w, map_h, difficulty, sides, local_side });
|
||||
self.start = Some(StartInfo { seed, map_w, map_h, difficulty, sides, local_side, mission });
|
||||
self.error = None;
|
||||
self.status = None;
|
||||
}
|
||||
@@ -499,6 +509,15 @@ mod tests {
|
||||
let start = client.take_start().expect("start info");
|
||||
assert_eq!(start.seed, 7);
|
||||
assert_eq!(start.local_side, Some(0));
|
||||
assert_eq!(start.mission, None);
|
||||
|
||||
client.create_coop(2, &[0, 2], 80, 72, 1);
|
||||
let coop = pop(&mut client);
|
||||
assert_eq!(coop["t"].as_str(), Some("create"));
|
||||
assert_eq!(coop["mission"].as_u64(), Some(2));
|
||||
client.feed(r#"{"t":"start","seed":8,"map_w":80,"map_h":72,"difficulty":1,"sides":[0,2],"mission":2}"#);
|
||||
let coop_start = client.take_start().expect("co-op start info");
|
||||
assert_eq!(coop_start.mission, Some(2));
|
||||
|
||||
client.send_commands(&[Command { side: 0, seq: 0, act: Act::Dig(Rect::new(1, 2, 3, 4)) }]);
|
||||
let req = pop(&mut client);
|
||||
|
||||
+53
-2
@@ -239,17 +239,29 @@ fn account_action(a: MenuAction, cloud: &mut Cloud, menu: &mut Menu, app: &mut A
|
||||
}
|
||||
MenuAction::MatchJoin(code) => app.client.join(&code),
|
||||
MenuAction::MatchCreate => app.client.create(&[0, 2, 3, 4], app.settings.map_w, app.settings.map_h, app.settings.difficulty),
|
||||
MenuAction::MatchCoopCreate(mission) => {
|
||||
let all = campaign::missions();
|
||||
if let Some(d) = all.get(mission) {
|
||||
app.client.create_coop(mission, &[0, 2, 3, 4], d.size.0, d.size.1, app.settings.difficulty);
|
||||
} else {
|
||||
menu.error = Some(i18n::tr("net.status_closed"));
|
||||
}
|
||||
}
|
||||
MenuAction::MatchTakeSide(side) => app.client.take_side(side),
|
||||
MenuAction::MatchReady(ready) => app.client.ready(ready),
|
||||
MenuAction::MatchStart => app.client.start(),
|
||||
MenuAction::MatchResume => app.client.resume(),
|
||||
MenuAction::MatchAbort => {
|
||||
app.client.abort();
|
||||
app.game.coop = false;
|
||||
app.match_mode = false;
|
||||
app.coop = false;
|
||||
}
|
||||
MenuAction::MatchLeave => {
|
||||
app.client.leave();
|
||||
app.game.coop = false;
|
||||
app.match_mode = false;
|
||||
app.coop = false;
|
||||
}
|
||||
MenuAction::MatchChat(text) => app.client.chat(&text, false),
|
||||
MenuAction::MatchEditField(i) => {
|
||||
@@ -380,6 +392,7 @@ struct App {
|
||||
client: MatchClient,
|
||||
ws_id: i32,
|
||||
match_mode: bool,
|
||||
coop: bool,
|
||||
}
|
||||
|
||||
impl App {
|
||||
@@ -402,6 +415,8 @@ impl App {
|
||||
store_settings(&self.settings);
|
||||
}
|
||||
self.game.hints = self.settings.hints;
|
||||
self.game.coop = false;
|
||||
self.coop = false;
|
||||
r3d.invalidate();
|
||||
menu.page = None;
|
||||
ui.show_help = false;
|
||||
@@ -482,12 +497,37 @@ impl App {
|
||||
self.ws_id = 0;
|
||||
}
|
||||
self.game.end_match();
|
||||
self.game.coop = false;
|
||||
self.match_mode = false;
|
||||
self.coop = false;
|
||||
self.client.on_disconnect();
|
||||
}
|
||||
|
||||
fn start_match_game(&mut self, info: StartInfo) -> bool {
|
||||
if info.map_w < 72 || info.map_h < 72 || !info.sides.contains(&0) {
|
||||
if !info.sides.contains(&0) {
|
||||
return false;
|
||||
}
|
||||
if let Some(mission) = info.mission {
|
||||
if !campaign::is_coop_mission(mission) {
|
||||
return false;
|
||||
}
|
||||
let local = info.local_side.unwrap_or(0);
|
||||
let mut game = campaign::start_coop_mission(mission, None);
|
||||
game.coop = true;
|
||||
if !game.start_match(&info.sides, local) {
|
||||
return false;
|
||||
}
|
||||
game.ai_difficulty = info.difficulty;
|
||||
game.hints = self.settings.hints;
|
||||
self.game = game;
|
||||
let (cx, cy) = self.game.map.core;
|
||||
self.cam = Cam::new(cx as f32 + 1.5, cy as f32 + 1.5);
|
||||
self.match_mode = true;
|
||||
self.coop = true;
|
||||
self.seed = info.seed.wrapping_mul(31) | 1;
|
||||
return true;
|
||||
}
|
||||
if info.map_w < 72 || info.map_h < 72 {
|
||||
return false;
|
||||
}
|
||||
self.game = Game::new(info.seed, info.map_w, info.map_h);
|
||||
@@ -495,12 +535,14 @@ impl App {
|
||||
if !self.game.start_match(&info.sides, local) {
|
||||
return false;
|
||||
}
|
||||
self.game.coop = false;
|
||||
self.game.ai_difficulty = info.difficulty;
|
||||
self.game.hints = self.settings.hints;
|
||||
self.game.init_fog();
|
||||
let (cx, cy) = self.game.map.core;
|
||||
self.cam = Cam::new(cx as f32 + 1.5, cy as f32 + 1.5);
|
||||
self.match_mode = true;
|
||||
self.coop = false;
|
||||
self.seed = info.seed.wrapping_mul(31) | 1;
|
||||
true
|
||||
}
|
||||
@@ -510,7 +552,11 @@ impl App {
|
||||
return;
|
||||
}
|
||||
for (_, cmds, ai) in self.client.take_turns() {
|
||||
if self.coop {
|
||||
self.game.ai_sides.clear();
|
||||
} else {
|
||||
self.game.sync_ai_sides(&ai, self.settings.difficulty);
|
||||
}
|
||||
self.game.receive_turn(&cmds);
|
||||
}
|
||||
for (_, turn, sum) in self.client.take_checks() {
|
||||
@@ -554,7 +600,7 @@ async fn main() {
|
||||
let seed = (miniquad::date::now() * 1000.0) as u32 | 1;
|
||||
// behind the title menu: the endless bridge battle
|
||||
let (game, cam) = Game::new_demo(seed);
|
||||
let mut app = App { game, cam, settings, seed, status: None, recorded: false, client: MatchClient::new(), ws_id: 0, match_mode: false };
|
||||
let mut app = App { game, cam, settings, seed, status: None, recorded: false, client: MatchClient::new(), ws_id: 0, match_mode: false, coop: false };
|
||||
menu.progress = platform::storage_get(PROGRESS_KEY).and_then(|s| serde_json::from_str(&s).ok()).unwrap_or_default();
|
||||
app.game.hints = app.settings.hints;
|
||||
let mut inp = Input { ldown: None, rdown: None, rlast: (0.0, 0.0), rdrag: false, mlast: None, minimap_drag: false, tlast: None, touch_world: false, inspect: None };
|
||||
@@ -1029,6 +1075,8 @@ async fn main() {
|
||||
MenuAction::StartMission(i) => {
|
||||
(app.game, app.cam) = mission_game(i, &menu.progress);
|
||||
app.game.hints = app.settings.hints;
|
||||
app.game.coop = false;
|
||||
app.coop = false;
|
||||
app.recorded = false;
|
||||
menu.page = None;
|
||||
ui.log.clear();
|
||||
@@ -1048,6 +1096,7 @@ async fn main() {
|
||||
| MenuAction::OpenMatch
|
||||
| MenuAction::MatchJoin(..)
|
||||
| MenuAction::MatchCreate
|
||||
| MenuAction::MatchCoopCreate(..)
|
||||
| MenuAction::MatchTakeSide(..)
|
||||
| MenuAction::MatchReady(..)
|
||||
| MenuAction::MatchStart
|
||||
@@ -1090,6 +1139,8 @@ async fn main() {
|
||||
app.seed = next_seed(app.seed);
|
||||
(app.game, app.cam) = new_game(app.seed, &app.settings);
|
||||
app.game.hints = app.settings.hints;
|
||||
app.game.coop = false;
|
||||
app.coop = false;
|
||||
}
|
||||
// end of a mission: next mission (won) or try again (lost)
|
||||
UiAction::EndNext => {
|
||||
|
||||
+35
-2
@@ -231,6 +231,9 @@ enum Btn {
|
||||
MatchJoin,
|
||||
MatchJoinRoom(usize),
|
||||
MatchCreate,
|
||||
MatchCoopPrev,
|
||||
MatchCoopNext,
|
||||
MatchCoopCreate,
|
||||
MatchTake(usize),
|
||||
MatchReady,
|
||||
MatchStart,
|
||||
@@ -307,6 +310,7 @@ pub enum MenuAction {
|
||||
OpenMatch,
|
||||
MatchJoin(String),
|
||||
MatchCreate,
|
||||
MatchCoopCreate(usize),
|
||||
MatchTakeSide(u8),
|
||||
MatchReady(bool),
|
||||
MatchStart,
|
||||
@@ -327,6 +331,7 @@ pub struct Menu {
|
||||
pub focus: usize, // field that gets the typed text
|
||||
pub error: Option<String>, // message on the login and account pages
|
||||
pub match_ui: MatchUi, // copied state of the multiplayer relay, shown on the Match page
|
||||
pub coop_mission: usize, // co-op campaign mission selected on the Match page
|
||||
pub in_match: bool, // a match is being played; the pause menu changes its shape
|
||||
confirm: Option<Btn>, // overwrite / delete waiting for a second click
|
||||
scroll: usize,
|
||||
@@ -353,6 +358,7 @@ impl Menu {
|
||||
focus: 0,
|
||||
error: None,
|
||||
match_ui: MatchUi::default(),
|
||||
coop_mission: crate::campaign::coop_mission_indexes().first().copied().unwrap_or(0),
|
||||
in_match: false,
|
||||
confirm: None,
|
||||
scroll: 0,
|
||||
@@ -616,6 +622,17 @@ impl Menu {
|
||||
None => MenuAction::Consumed,
|
||||
},
|
||||
Some(Btn::MatchCreate) => MenuAction::MatchCreate,
|
||||
Some(Btn::MatchCoopPrev) => {
|
||||
self.coop_mission = crate::campaign::next_coop_mission(self.coop_mission, -1);
|
||||
self.confirm = None;
|
||||
MenuAction::Consumed
|
||||
}
|
||||
Some(Btn::MatchCoopNext) => {
|
||||
self.coop_mission = crate::campaign::next_coop_mission(self.coop_mission, 1);
|
||||
self.confirm = None;
|
||||
MenuAction::Consumed
|
||||
}
|
||||
Some(Btn::MatchCoopCreate) => MenuAction::MatchCoopCreate(self.coop_mission),
|
||||
Some(Btn::MatchReady) => MenuAction::MatchReady(!self.match_ui.ready),
|
||||
Some(Btn::MatchStart) => MenuAction::MatchStart,
|
||||
Some(Btn::MatchResume) => MenuAction::MatchResume,
|
||||
@@ -1374,6 +1391,16 @@ impl Menu {
|
||||
yy += CH() * 1.6;
|
||||
}
|
||||
|
||||
if !ui.started && !ui.room_open() {
|
||||
if let Some(d) = missions().get(self.coop_mission) {
|
||||
let line = trf("net.coop_mission", &[(self.coop_mission + 1).to_string(), tr(&format!("camp.{}.title", d.key))]);
|
||||
let maxc = ((cols - 6) as f32 * CW() / (GW * SMALL())) as usize;
|
||||
for (_k, text) in wrap(&line, maxc).iter().take(2).enumerate() {
|
||||
gfx.text(text, ix, yy, SMALL(), GOLD);
|
||||
yy += GH * SMALL() * 1.35;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.field(gfx, ix, yy, w, &tr("net.join_hint"), 0, false);
|
||||
self.field(gfx, ix, yy + CH() * 2.4, w, &tr("net.chat"), 1, false);
|
||||
yy += CH() * 4.8;
|
||||
@@ -1382,6 +1409,11 @@ impl Menu {
|
||||
if !ui.started {
|
||||
items.push((Btn::MatchCreate, tr("net.create")));
|
||||
items.push((Btn::MatchJoin, tr("net.join")));
|
||||
if !ui.room_open() {
|
||||
items.push((Btn::MatchCoopPrev, "<".to_string()));
|
||||
items.push((Btn::MatchCoopCreate, tr("net.coop_create")));
|
||||
items.push((Btn::MatchCoopNext, ">".to_string()));
|
||||
}
|
||||
if ui.room_open() {
|
||||
items.push((Btn::MatchReady, tr(if ui.ready { "net.unready" } else { "net.ready" })));
|
||||
if ui.is_host {
|
||||
@@ -1402,12 +1434,13 @@ impl Menu {
|
||||
}
|
||||
items.push((Btn::Back, tr("menu.back")));
|
||||
let bw = 14.0 * CW();
|
||||
for (k, (b, label)) in items.iter().take(6).enumerate() {
|
||||
let shown = items.iter().take(9).count();
|
||||
for (k, (b, label)) in items.iter().take(shown).enumerate() {
|
||||
let r = Rect::new(ix + (k % 3) as f32 * (bw + CW()), yy + (k / 3) as f32 * CH() * 2.4, bw, CH() * 2.0);
|
||||
button(gfx, r, label, r.contains(vec2(mx, my)), FRAME);
|
||||
self.buttons.push((r, *b));
|
||||
}
|
||||
yy += CH() * 5.4;
|
||||
yy += CH() * 2.4 * ((shown + 2) / 3) as f32;
|
||||
|
||||
if !ui.rooms.is_empty() && !ui.started {
|
||||
gfx.text(&tr("net.rooms"), ix, yy, S(), GOLD);
|
||||
|
||||
+2
-2
@@ -14,8 +14,8 @@ fn ready(e: &Ent) -> bool {
|
||||
impl Game {
|
||||
// attack: the enemy (unit, or tower) at (wx, wy)
|
||||
pub fn order_attack(&mut self, picked: Option<usize>, wx: f32, wy: f32) {
|
||||
let unit = picked.filter(|&j| j < self.ents.len() && self.ents[j].alive && self.ents[j].side() != 0 && self.ents[j].captive == 0).or_else(|| {
|
||||
self.ents.iter().position(|e| e.alive && !e.held && e.side() != 0 && e.captive == 0 && !e.chained && (e.x - wx).powi(2) + (e.y - wy).powi(2) < 1.5 * 1.5)
|
||||
let unit = picked.filter(|&j| j < self.ents.len() && self.is_hostile(&self.ents[j])).or_else(|| {
|
||||
self.ents.iter().position(|e| self.is_hostile(e) && (e.x - wx).powi(2) + (e.y - wy).powi(2) < 1.5 * 1.5)
|
||||
});
|
||||
let tower = if unit.is_none() {
|
||||
self.towers.iter().find(|t| t.hero && (t.pos().0 - wx).powi(2) + (t.pos().1 - wy).powi(2) < 1.2 * 1.2).map(|t| (t.id, t.pos()))
|
||||
|
||||
@@ -1121,6 +1121,7 @@ impl R3d {
|
||||
if let Some(f) = g.flag {
|
||||
draw_flag(f, t);
|
||||
}
|
||||
draw_marks(g, t);
|
||||
draw_overlay(&g.map, ov, self.wall_sub);
|
||||
draw_entry_warnings(g, t, self.wall_sub);
|
||||
// translucent glows and effects last (they write depth, see glow_material)
|
||||
@@ -2165,6 +2166,82 @@ fn draw_flag(f: usize, t: f32) {
|
||||
draw_mesh(&Mesh { vertices: v, indices: vec![0, 1, 2], texture: None });
|
||||
}
|
||||
|
||||
fn mark_color(kind: u8) -> Color {
|
||||
match kind {
|
||||
MARK_DIG => Color::new(0.45, 0.95, 0.55, 1.0),
|
||||
MARK_ROOM => Color::new(0.35, 0.85, 1.0, 1.0),
|
||||
MARK_SELL => Color::new(0.95, 0.78, 0.22, 1.0),
|
||||
MARK_BRIDGE => Color::new(0.70, 0.48, 0.24, 1.0),
|
||||
MARK_STEEL => Color::new(0.78, 0.84, 0.92, 1.0),
|
||||
MARK_TOWER => Color::new(0.74, 0.42, 0.95, 1.0),
|
||||
MARK_TRAP => Color::new(1.0, 0.58, 0.18, 1.0),
|
||||
MARK_SPELL => Color::new(0.42, 0.88, 0.98, 1.0),
|
||||
MARK_LIGHTNING => Color::new(0.98, 0.94, 0.35, 1.0),
|
||||
MARK_HEAL => Color::new(0.48, 0.96, 0.66, 1.0),
|
||||
MARK_SUMMON => Color::new(0.95, 0.36, 0.24, 1.0),
|
||||
MARK_FLAG => Color::new(0.88, 0.18, 0.18, 1.0),
|
||||
MARK_PICKUP => Color::new(0.72, 0.76, 0.82, 1.0),
|
||||
MARK_DROP => Color::new(0.72, 0.76, 0.82, 1.0),
|
||||
MARK_SLAP => Color::new(1.0, 0.46, 0.24, 1.0),
|
||||
MARK_ATTACK => Color::new(0.94, 0.18, 0.20, 1.0),
|
||||
MARK_GUARD => Color::new(0.98, 0.82, 0.22, 1.0),
|
||||
MARK_CANCEL => Color::new(0.58, 0.64, 0.78, 1.0),
|
||||
_ => Color::new(0.88, 0.18, 0.18, 1.0),
|
||||
}
|
||||
}
|
||||
|
||||
fn flag_rgba(c: Color) -> [u8; 4] {
|
||||
[
|
||||
(c.r.clamp(0.0, 1.0) * 255.0) as u8,
|
||||
(c.g.clamp(0.0, 1.0) * 255.0) as u8,
|
||||
(c.b.clamp(0.0, 1.0) * 255.0) as u8,
|
||||
(c.a.clamp(0.0, 1.0) * 255.0) as u8,
|
||||
]
|
||||
}
|
||||
|
||||
fn mark_side_color(side: u8) -> Color {
|
||||
match side {
|
||||
0 => Color::new(1.0, 0.85, 0.45, 1.0),
|
||||
1 => Color::new(0.85, 0.55, 0.25, 1.0),
|
||||
2 => Color::new(0.35, 0.95, 0.40, 1.0),
|
||||
3 => Color::new(0.40, 0.60, 1.00, 1.0),
|
||||
4 => Color::new(0.85, 0.75, 0.30, 1.0),
|
||||
_ => Color::new(0.82, 0.84, 0.88, 1.0),
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_command_flag(x: f32, z: f32, cloth: Color, pole: Color, t: f32) {
|
||||
let base = vec3(x - 0.04, 0.0, z);
|
||||
draw_cube(base + vec3(0.0, 0.65, 0.0), vec3(0.035, 1.30, 0.035), None, Color::new(pole.r, pole.g, pole.b, pole.a));
|
||||
let wave = (t * 6.0).sin() * 0.07;
|
||||
let p1 = base + vec3(0.0, 1.28, 0.0);
|
||||
let p2 = base + vec3(0.48, 1.14 + wave, 0.08);
|
||||
let p3 = base + vec3(0.0, 0.92, 0.0);
|
||||
let col = flag_rgba(cloth);
|
||||
let v = vec![
|
||||
Vertex { position: p1, uv: Vec2::ZERO, color: col, normal: Vec4::Y },
|
||||
Vertex { position: p2, uv: Vec2::ZERO, color: col, normal: Vec4::Y },
|
||||
Vertex { position: p3, uv: Vec2::ZERO, color: col, normal: Vec4::Y },
|
||||
];
|
||||
draw_mesh(&Mesh { vertices: v, indices: vec![0, 1, 2], texture: None });
|
||||
}
|
||||
|
||||
fn draw_marks(g: &Game, t: f32) {
|
||||
if g.marks.is_empty() {
|
||||
return;
|
||||
}
|
||||
for m in &g.marks {
|
||||
let left = m.until.saturating_sub(g.sim_steps) as f32 * STEP;
|
||||
if left <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
let a = (left / 2.0).min(1.0).max(0.18);
|
||||
let base = mark_color(m.kind);
|
||||
let side = mark_side_color(m.side);
|
||||
draw_command_flag(m.x, m.y, Color::new(base.r, base.g, base.b, a), Color::new(side.r, side.g, side.b, a), t);
|
||||
}
|
||||
}
|
||||
|
||||
// projectiles with a look of their own (styles in particles.rs)
|
||||
fn draw_projectile(f: &Fx, p: f32, t: f32) {
|
||||
use crate::particles::*;
|
||||
|
||||
Reference in new issue
Block a user