Monster eye marks the camera on the minimap; middle double click resets the view angle; creatures by beds up to 100
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0b2d327791
commit
ac22351cdf
22 files changed
+147
-5
No files matched your search
+51
-1
@@ -94,6 +94,7 @@ pub struct SfxEvent {
|
||||
pub pos: Option<(f32, f32)>, // world position (for distance-based volume), None = UI
|
||||
}
|
||||
pub const MANA_MAX: f32 = 400.0;
|
||||
pub const MAX_CREATURES: usize = 100; // creatures of one master (the beds decide below that)
|
||||
pub const SPRING_MANA: f32 = 0.6; // mana per second of each claimed mana spring
|
||||
pub const SPRING_MAX: f32 = 150.0; // more mana maximum per claimed spring
|
||||
pub const CARRY_MAX: i32 = 300;
|
||||
@@ -1925,7 +1926,14 @@ impl Game {
|
||||
pub fn free_lair_aerie(&self) -> (usize, usize) {
|
||||
let flyers = self.ents.iter().filter(|e| e.alive && e.creature() && flies(e.kind)).count();
|
||||
let walkers = self.creature_count() - flyers;
|
||||
(self.counts[1].min(30).saturating_sub(walkers), self.counts[7].min(20).saturating_sub(flyers))
|
||||
// a bed per lair tile (aerie: per aerie tile), never more than MAX_CREATURES in all
|
||||
let room = MAX_CREATURES.saturating_sub(walkers + flyers);
|
||||
(self.counts[1].saturating_sub(walkers).min(room), self.counts[7].saturating_sub(flyers).min(room))
|
||||
}
|
||||
|
||||
// how many creatures this master can house: his beds, at most MAX_CREATURES
|
||||
pub fn creature_cap(&self) -> usize {
|
||||
(self.counts[1] + self.counts[7]).min(MAX_CREATURES)
|
||||
}
|
||||
|
||||
pub fn lair_factor(&self) -> f32 {
|
||||
@@ -1955,6 +1963,10 @@ impl Game {
|
||||
self.hint(1, "hint.no_farm");
|
||||
return;
|
||||
}
|
||||
if self.creature_count() >= MAX_CREATURES {
|
||||
self.hint(35, "hint.creature_max");
|
||||
return;
|
||||
}
|
||||
let (free_lair, free_aerie) = self.free_lair_aerie();
|
||||
if free_lair == 0 && free_aerie == 0 {
|
||||
self.hint(2, "hint.lair_full");
|
||||
@@ -6861,6 +6873,44 @@ mod tests {
|
||||
assert!(g.ents.iter().filter(|e| e.kind == EK::Chainbreaker).count() <= 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_beds_decide_how_many_creatures_come_but_never_more_than_100() {
|
||||
let mut g = Game::new(9, 96, 96);
|
||||
let (cx, cy) = g.map.core;
|
||||
// a lair of 40 beds: room for 40
|
||||
let mut n = 0;
|
||||
for y in cy + 4..cy + 20 {
|
||||
for x in cx - 6..cx + 8 {
|
||||
if n < 40 && inb(x, y) {
|
||||
let t = &mut g.map.t[idx(x, y)];
|
||||
t.kind = Kind::Lair;
|
||||
t.owner = Owner::Player;
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
g.recount();
|
||||
assert_eq!(g.creature_cap(), 40);
|
||||
assert_eq!(g.free_lair_aerie().0, 40, "more than the old 30");
|
||||
// a huge lair: the cap is 100
|
||||
for y in cy + 4..cy + 20 {
|
||||
for x in cx - 10..cx + 12 {
|
||||
if inb(x, y) && g.map.t[idx(x, y)].kind != Kind::Core {
|
||||
let t = &mut g.map.t[idx(x, y)];
|
||||
t.kind = Kind::Lair;
|
||||
t.owner = Owner::Player;
|
||||
}
|
||||
}
|
||||
}
|
||||
g.recount();
|
||||
assert!(g.counts[1] > 100);
|
||||
assert_eq!(g.creature_cap(), MAX_CREATURES);
|
||||
for k in 0..100 {
|
||||
g.spawn(EK::Kobold, false, cx as f32 + 0.5 + (k % 10) as f32, cy as f32 + 5.5 + (k / 10) as f32, 1);
|
||||
}
|
||||
assert_eq!(g.free_lair_aerie(), (0, 0), "no room for a 101st");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn creatures_never_make_their_bed_in_an_allys_lair() {
|
||||
let mut g = Game::new(11, 96, 128);
|
||||
|
||||
+12
-1
@@ -96,6 +96,7 @@ struct Input {
|
||||
rlast: (f32, f32),
|
||||
rdrag: bool,
|
||||
mlast: Option<(f32, f32)>, // middle button drag
|
||||
mclick: f64, // time of the last middle click (a double click resets the view)
|
||||
minimap_drag: bool,
|
||||
// touch
|
||||
tlast: Option<Vec2>, // last position of a one-finger pan (device pixels)
|
||||
@@ -902,7 +903,7 @@ async fn main() {
|
||||
let mut app = App { game, cam, settings, seed, status: None, recorded: false, client: MatchClient::new(), ws_id: 0, match_mode: false, coop: false, coop_editor: false, coop_run: None, offline: false, tutorial: false, fps: 0.0, renderer: platform::renderer(), frame_ms: 0.0, frame_hist: [0.0; FRAME_HIST], frame_hist_pos: 0, custom_campaign: None, custom_progress: None, custom_mission_index: None, custom_next_index: None, editor: None, editor_testing: false, editor_test_only: false, editor_public_pending: false, editor_loading: None, pending_briefing: None };
|
||||
menu.progress = platform::storage_get(PROGRESS_KEY).and_then(|s| serde_json::from_str(&s).ok()).unwrap_or_default();
|
||||
apply_game_settings(&mut app.game, &app.settings);
|
||||
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 };
|
||||
let mut inp = Input { ldown: None, rdown: None, rlast: (0.0, 0.0), rdrag: false, mlast: None, mclick: -1.0, minimap_drag: false, tlast: None, touch_world: false, inspect: None };
|
||||
let mut touch = TouchInput::new();
|
||||
let mut auto_touch = platform::coarse_pointer();
|
||||
let mut menu_swipe: Option<(f32, f32)> = None; // a swipe over a menu: last finger y, unused distance
|
||||
@@ -1329,6 +1330,16 @@ async fn main() {
|
||||
if is_key_down(key(Action::TiltDown)) {
|
||||
cam.pitch -= rot * 0.6;
|
||||
}
|
||||
// a double click with the middle button: the standard angle and turn (same place)
|
||||
if is_mouse_button_pressed(MouseButton::Middle) && !touch_mode {
|
||||
let now = get_time();
|
||||
if now - inp.mclick < 0.35 {
|
||||
cam.reset_view();
|
||||
inp.mclick = -1.0;
|
||||
} else {
|
||||
inp.mclick = now;
|
||||
}
|
||||
}
|
||||
if is_mouse_button_down(MouseButton::Middle) && !touch_mode {
|
||||
if let Some((lx, ly)) = inp.mlast {
|
||||
let inv = if keys.invert_mouse { -1.0 } else { 1.0 };
|
||||
|
||||
+10
-1
@@ -34,6 +34,9 @@ pub struct Cam {
|
||||
pub render_init: bool,
|
||||
}
|
||||
|
||||
pub const DEFAULT_YAW: f32 = 0.785;
|
||||
pub const DEFAULT_PITCH: f32 = 0.6155;
|
||||
|
||||
fn zero_f32() -> f32 {
|
||||
0.0
|
||||
}
|
||||
@@ -44,7 +47,13 @@ fn cam_uninit() -> bool {
|
||||
|
||||
impl Cam {
|
||||
pub fn new(tx: f32, tz: f32) -> Cam {
|
||||
Cam { tx, tz, yaw: 0.785, pitch: 0.6155, zoom: 18.0, rx: tx, rz: tz, render_init: true }
|
||||
Cam { tx, tz, yaw: DEFAULT_YAW, pitch: DEFAULT_PITCH, zoom: 18.0, rx: tx, rz: tz, render_init: true }
|
||||
}
|
||||
|
||||
// the standard view angle and turn; the position and zoom stay
|
||||
pub fn reset_view(&mut self) {
|
||||
self.yaw = DEFAULT_YAW;
|
||||
self.pitch = DEFAULT_PITCH;
|
||||
}
|
||||
|
||||
pub fn snap(&mut self) {
|
||||
|
||||
@@ -741,7 +741,7 @@ impl Ui {
|
||||
Gap,
|
||||
}
|
||||
let txt = |s: String, c: Color| Part::T(s, c);
|
||||
let cap = g.counts[1].min(30);
|
||||
let cap = g.creature_cap();
|
||||
let wave_col = if g.wave_timer < 30.0 { RED } else { TXT };
|
||||
let vol = if settings.muted { tr("hud.off") } else { format!("{}%", (settings.volume * 100.0).round() as i32) };
|
||||
let mut gold_seg = vec![txt(tr("hud.gold"), GOLD), Part::Gap, txt(format!("{}/{}", g.gold as i32, g.gold_cap() as i32), TXT)];
|
||||
@@ -1026,6 +1026,42 @@ impl Ui {
|
||||
draw_line(mrx + a.x * ms, mry + a.y * ms, mrx + b.x * ms, mry + b.y * ms, px, WHITE);
|
||||
}
|
||||
}
|
||||
// the camera: a monster's eye on the viewer's side of the footprint, looking in
|
||||
{
|
||||
let near = (corners[2] + corners[3]) * 0.5;
|
||||
let look = vec2(-cam.yaw.sin(), -cam.yaw.cos()); // from the eye towards the target (map units)
|
||||
let r = (4.0 * px).max(ms * 2.5);
|
||||
let (ex, ey) = (mrx + near.x * ms - look.x * r * 1.6, mry + near.y * ms - look.y * r * 1.6);
|
||||
let (ex, ey) = (ex.clamp(mrx + r, mrx + map_w - r), ey.clamp(mry + r, mry + map_h - r));
|
||||
let side = vec2(-look.y, look.x);
|
||||
// the lids: an almond across the view direction
|
||||
let lid = |t: f32, w: f32| vec2(ex, ey) + side * (t * r * 1.25) + look * w;
|
||||
let n = 8;
|
||||
let mut upper = Vec::new();
|
||||
let mut lower = Vec::new();
|
||||
for k in 0..=n {
|
||||
let t = k as f32 / n as f32 * 2.0 - 1.0;
|
||||
let h = (1.0 - t * t).max(0.0).sqrt() * r * 0.75;
|
||||
upper.push(lid(t, h));
|
||||
lower.push(lid(t, -h));
|
||||
}
|
||||
let c = vec2(ex, ey);
|
||||
let sclera = Color::new(0.95, 0.85, 0.35, 1.0);
|
||||
for k in 0..n {
|
||||
draw_triangle(c, upper[k], upper[k + 1], sclera);
|
||||
draw_triangle(c, lower[k], lower[k + 1], sclera);
|
||||
}
|
||||
// the iris looks towards the target, with a slit pupil across it
|
||||
let iris = c + look * (r * 0.2);
|
||||
draw_circle(iris.x, iris.y, r * 0.55, Color::new(0.85, 0.3, 0.08, 1.0));
|
||||
let a = iris + side * (r * 0.45);
|
||||
let b = iris - side * (r * 0.45);
|
||||
draw_line(a.x, a.y, b.x, b.y, (r * 0.22).max(px), Color::new(0.05, 0.0, 0.02, 1.0));
|
||||
for k in 0..n {
|
||||
draw_line(upper[k].x, upper[k].y, upper[k + 1].x, upper[k + 1].y, px.max(1.0), Color::new(0.2, 0.05, 0.08, 1.0));
|
||||
draw_line(lower[k].x, lower[k].y, lower[k + 1].x, lower[k + 1].y, px.max(1.0), Color::new(0.2, 0.05, 0.08, 1.0));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- mission goals (campaign)
|
||||
if let Some(m) = &g.mission {
|
||||
@@ -1327,7 +1363,7 @@ impl Ui {
|
||||
let controls: &[&str] = if touch_ui() {
|
||||
&["help.t_tap", "help.t_drag", "help.t_two", "help.t_long", "help.t_double", "help.t_tools"]
|
||||
} else {
|
||||
&["help.k_left", "help.k_right", "help.k_slap", "help.k_pan", "help.k_rot", "help.k_zoom", "help.k_tools", "help.k_misc", "help.k_misc2"]
|
||||
&["help.k_left", "help.k_right", "help.k_slap", "help.k_pan", "help.k_rot", "help.k_reset", "help.k_zoom", "help.k_tools", "help.k_misc", "help.k_misc2"]
|
||||
};
|
||||
let mut keys: Vec<(&str, Color)> = vec![("help.goal", GOLD), ("", TXT)];
|
||||
keys.extend(controls.iter().map(|k| (*k, TXT)));
|
||||
|
||||
Reference in new issue
Block a user