summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-07-11 15:30:04 +0200
committersalmonik <salmonik@tuta.io>2026-07-11 15:30:04 +0200
commit723c2fe0982f80438b75098fa79d1933efc13d0a (patch)
tree78bb8c9089eb351f4889affd74eb0d4de65f0a67
parentend game screen, fix occlusion (diff)
downloaddungeon-723c2fe0982f80438b75098fa79d1933efc13d0a.tar.gz
dungeon-723c2fe0982f80438b75098fa79d1933efc13d0a.zip
add enemy AI: chase, patrol, attack, and movement animation
-rw-r--r--src/game.rs362
-rw-r--r--src/renderer.rs117
2 files changed, 420 insertions, 59 deletions
diff --git a/src/game.rs b/src/game.rs
index f66ca40..992023e 100644
--- a/src/game.rs
+++ b/src/game.rs
@@ -614,6 +614,16 @@ const TURN_DURATION: f64 = 0.12; // seconds for a 90° turn
614const ENEMY_ANIM_DURATION: f64 = 0.10; // seconds for enemy movement 614const ENEMY_ANIM_DURATION: f64 = 0.10; // seconds for enemy movement
615const ATTACK_DURATION: f64 = 0.12; // seconds for attack lunge 615const ATTACK_DURATION: f64 = 0.12; // seconds for attack lunge
616 616
617/// An enemy movement queued for animation.
618#[derive(Clone)]
619struct EnemyMove {
620 from_x: usize,
621 from_y: usize,
622 to_x: usize,
623 to_y: usize,
624 entity: Entity,
625}
626
617pub const MAX_HP: u32 = 5; 627pub const MAX_HP: u32 = 5;
618pub const MAX_SANITY: u32 = 100; 628pub const MAX_SANITY: u32 = 100;
619pub const MAX_WEAKNESS: u32 = 100; 629pub const MAX_WEAKNESS: u32 = 100;
@@ -664,6 +674,10 @@ pub struct Game {
664 674
665 // ---- animation: enemies ---- 675 // ---- animation: enemies ----
666 enemy_anim_t: f64, 676 enemy_anim_t: f64,
677 enemy_moves: Vec<EnemyMove>,
678
679 /// Enemies that already retaliated this turn (skip their independent attack).
680 acted_this_turn: Vec<(usize, usize)>,
667 681
668 // ---- animation: attack ---- 682 // ---- animation: attack ----
669 pub attacking: bool, 683 pub attacking: bool,
@@ -724,6 +738,8 @@ impl Game {
724 turn_delta: 0.0, 738 turn_delta: 0.0,
725 turn_t: 0.0, 739 turn_t: 0.0,
726 enemy_anim_t: 0.0, 740 enemy_anim_t: 0.0,
741 enemy_moves: Vec::new(),
742 acted_this_turn: Vec::new(),
727 attacking: false, 743 attacking: false,
728 attack_t: 0.0, 744 attack_t: 0.0,
729 attack_base_x: 0.0, 745 attack_base_x: 0.0,
@@ -808,10 +824,19 @@ impl Game {
808 } 824 }
809 825
810 if done { 826 if done {
811 self.state = GameState::ResolvingTurn; 827 let enemies_moved = self.compute_enemy_turns();
812 self.resolve_turn(); 828 if self.state == GameState::GameOver {
813 if self.state != GameState::GameOver { 829 return true;
814 self.state = GameState::WaitingForInput; 830 }
831 if enemies_moved {
832 self.state = GameState::AnimatingEnemies;
833 self.enemy_anim_t = 0.0;
834 } else {
835 self.state = GameState::ResolvingTurn;
836 self.resolve_turn();
837 if self.state != GameState::GameOver {
838 self.state = GameState::WaitingForInput;
839 }
815 } 840 }
816 return true; 841 return true;
817 } 842 }
@@ -821,6 +846,7 @@ impl Game {
821 self.enemy_anim_t += dt; 846 self.enemy_anim_t += dt;
822 if self.enemy_anim_t >= ENEMY_ANIM_DURATION { 847 if self.enemy_anim_t >= ENEMY_ANIM_DURATION {
823 self.enemy_anim_t = 0.0; 848 self.enemy_anim_t = 0.0;
849 self.apply_enemy_moves();
824 self.state = GameState::ResolvingTurn; 850 self.state = GameState::ResolvingTurn;
825 self.resolve_turn(); 851 self.resolve_turn();
826 if self.state != GameState::GameOver { 852 if self.state != GameState::GameOver {
@@ -866,6 +892,7 @@ impl Game {
866 // Blocked by real enemy — waste the turn, take damage. 892 // Blocked by real enemy — waste the turn, take damage.
867 self.set_message("Something is blocking your way."); 893 self.set_message("Something is blocking your way.");
868 self.hp = self.hp.saturating_sub(damage); 894 self.hp = self.hp.saturating_sub(damage);
895 self.acted_this_turn.push((tx, ty));
869 self.advance_turn(); 896 self.advance_turn();
870 if self.hp == 0 { 897 if self.hp == 0 {
871 self.state = GameState::GameOver; 898 self.state = GameState::GameOver;
@@ -996,6 +1023,7 @@ impl Game {
996 } 1023 }
997 }); 1024 });
998 self.hp = self.hp.saturating_sub(damage); 1025 self.hp = self.hp.saturating_sub(damage);
1026 self.acted_this_turn.push((fx, fy));
999 self.set_message(&format!( 1027 self.set_message(&format!(
1000 "You hit the enemy for {} dmg. It strikes back!", 1028 "You hit the enemy for {} dmg. It strikes back!",
1001 total_dmg 1029 total_dmg
@@ -1175,15 +1203,27 @@ impl Game {
1175 1203
1176 /// Advance the turn counter and state without animation. 1204 /// Advance the turn counter and state without animation.
1177 fn advance_turn(&mut self) { 1205 fn advance_turn(&mut self) {
1178 self.state = GameState::ResolvingTurn; 1206 let enemies_moved = self.compute_enemy_turns();
1179 self.resolve_turn(); 1207 if self.state == GameState::GameOver {
1180 if self.state != GameState::GameOver { 1208 return;
1181 self.state = GameState::WaitingForInput; 1209 }
1210 if enemies_moved {
1211 self.state = GameState::AnimatingEnemies;
1212 self.enemy_anim_t = 0.0;
1213 } else {
1214 self.state = GameState::ResolvingTurn;
1215 self.resolve_turn();
1216 if self.state != GameState::GameOver {
1217 self.state = GameState::WaitingForInput;
1218 }
1182 } 1219 }
1183 } 1220 }
1184 1221
1185 /// Apply all per-turn effects (weakness, heartbeat, HP drain, message decay). 1222 /// Apply all per-turn effects (weakness, heartbeat, HP drain, message decay).
1186 fn resolve_turn(&mut self) { 1223 fn resolve_turn(&mut self) {
1224 // Clear acted-this-turn tracking (enemy AI already consumed it).
1225 self.acted_this_turn.clear();
1226
1187 // Weakness grows every 3 turns, not every turn. 1227 // Weakness grows every 3 turns, not every turn.
1188 if self.heartbeat_count % 3 == 0 { 1228 if self.heartbeat_count % 3 == 0 {
1189 self.weakness = (self.weakness + 1).min(MAX_WEAKNESS); 1229 self.weakness = (self.weakness + 1).min(MAX_WEAKNESS);
@@ -1211,9 +1251,20 @@ impl Game {
1211 if !self.can_act() { 1251 if !self.can_act() {
1212 return; 1252 return;
1213 } 1253 }
1214 self.state = GameState::AnimatingEnemies; 1254 let enemies_moved = self.compute_enemy_turns();
1215 self.enemy_anim_t = 0.0; 1255 if self.state == GameState::GameOver {
1216 // TODO Phase 7: enemy movement happens here 1256 return;
1257 }
1258 if enemies_moved {
1259 self.state = GameState::AnimatingEnemies;
1260 self.enemy_anim_t = 0.0;
1261 } else {
1262 self.state = GameState::ResolvingTurn;
1263 self.resolve_turn();
1264 if self.state != GameState::GameOver {
1265 self.state = GameState::WaitingForInput;
1266 }
1267 }
1217 } 1268 }
1218 1269
1219 /// Set a HUD message that persists for 3 turns. 1270 /// Set a HUD message that persists for 3 turns.
@@ -1225,6 +1276,295 @@ impl Game {
1225 // ---- cardinal-direction helpers ---- 1276 // ---- cardinal-direction helpers ----
1226 1277
1227 /// Which grid direction the player faces: (dx, dy) ∈ {±1, 0}. 1278 /// Which grid direction the player faces: (dx, dy) ∈ {±1, 0}.
1279 // ---- enemy AI ---------------------------------------------------------
1280
1281 /// DDA line-of-sight check from grid cell (ex,ey) to player position (px,py).
1282 fn has_line_of_sight(&self, ex: usize, ey: usize, px: f64, py: f64) -> bool {
1283 let sx = ex as f64 + 0.5;
1284 let sy = ey as f64 + 0.5;
1285 let dx = px - sx;
1286 let dy = py - sy;
1287 let dist = (dx * dx + dy * dy).sqrt();
1288 if dist < 0.01 {
1289 return true;
1290 }
1291 // Walk the grid line; step size is half a cell for accuracy.
1292 let steps = (dist * 2.0).ceil() as usize;
1293 for i in 1..steps {
1294 let t = i as f64 / steps as f64;
1295 let cx = (sx + dx * t) as usize;
1296 let cy = (sy + dy * t) as usize;
1297 // Reached player cell — LOS confirmed.
1298 if cy == py as usize && cx == px as usize {
1299 return true;
1300 }
1301 if self.maze.is_wall(cy, cx) {
1302 return false;
1303 }
1304 }
1305 true
1306 }
1307
1308 /// Try to move the enemy at (ex,ey) one tile toward the player.
1309 /// Queues the move in `enemy_moves` and removes the entity from the grid.
1310 /// `occupied` tracks cells that are already taken (by other enemy moves or static entities).
1311 fn try_move_enemy_toward(&mut self, ex: usize, ey: usize, occupied: &mut Vec<bool>) {
1312 let px = self.player.x.floor() as isize;
1313 let py = self.player.y.floor() as isize;
1314
1315 // Four cardinal neighbours, sorted by Manhattan distance to player.
1316 let dirs: [(isize, isize); 4] = [(1, 0), (-1, 0), (0, 1), (0, -1)];
1317 let mut best: Option<(usize, usize, isize)> = None;
1318
1319 for (dx, dy) in dirs {
1320 let nx = ex as isize + dx;
1321 let ny = ey as isize + dy;
1322 if nx < 0 || ny < 0 {
1323 continue;
1324 }
1325 let nx = nx as usize;
1326 let ny = ny as usize;
1327 if nx >= self.maze.width() || ny >= self.maze.height() {
1328 continue;
1329 }
1330 // Must be a passage and not occupied.
1331 if !self
1332 .maze
1333 .is_passage_at_float(nx as f64 + 0.5, ny as f64 + 0.5)
1334 {
1335 continue;
1336 }
1337 if occupied[ny * self.maze.width() + nx] {
1338 continue;
1339 }
1340 let dist = manhattan(nx as isize, ny as isize, px, py);
1341 let current = manhattan(ex as isize, ey as isize, px, py);
1342 if dist < current {
1343 match best {
1344 None => best = Some((nx, ny, dist)),
1345 Some((_, _, best_dist)) if dist < best_dist => best = Some((nx, ny, dist)),
1346 _ => {}
1347 }
1348 }
1349 }
1350
1351 if let Some((tx, ty, _)) = best {
1352 let entity = self.maze.take_entity(ex, ey).unwrap();
1353 occupied[ty * self.maze.width() + tx] = true;
1354 self.enemy_moves.push(EnemyMove {
1355 from_x: ex,
1356 from_y: ey,
1357 to_x: tx,
1358 to_y: ty,
1359 entity,
1360 });
1361 }
1362 }
1363
1364 /// Try to move the enemy at (ex,ey) to a random adjacent passage cell (patrol).
1365 fn try_patrol_enemy(&mut self, ex: usize, ey: usize, occupied: &mut Vec<bool>) {
1366 let mut rng = rand::rng();
1367 let dirs: [(isize, isize); 4] = [(1, 0), (-1, 0), (0, 1), (0, -1)];
1368 // Shuffle directions.
1369 let mut indices = [0usize, 1, 2, 3];
1370 for i in (1..4).rev() {
1371 let j = rng.random_range(0..=i);
1372 indices.swap(i, j);
1373 }
1374
1375 for &idx in &indices {
1376 let (dx, dy) = dirs[idx];
1377 let nx = ex as isize + dx;
1378 let ny = ey as isize + dy;
1379 if nx < 0 || ny < 0 {
1380 continue;
1381 }
1382 let nx = nx as usize;
1383 let ny = ny as usize;
1384 if nx >= self.maze.width() || ny >= self.maze.height() {
1385 continue;
1386 }
1387 if !self
1388 .maze
1389 .is_passage_at_float(nx as f64 + 0.5, ny as f64 + 0.5)
1390 {
1391 continue;
1392 }
1393 if occupied[ny * self.maze.width() + nx] {
1394 continue;
1395 }
1396 let entity = self.maze.take_entity(ex, ey).unwrap();
1397 occupied[ny * self.maze.width() + nx] = true;
1398 self.enemy_moves.push(EnemyMove {
1399 from_x: ex,
1400 from_y: ey,
1401 to_x: nx,
1402 to_y: ny,
1403 entity,
1404 });
1405 return;
1406 }
1407 }
1408
1409 /// Compute actions for every enemy/hallucination on the level.
1410 /// Attacks are applied immediately; moves are queued in `enemy_moves`.
1411 /// Returns true if any enemy moved (needs animation).
1412 fn compute_enemy_turns(&mut self) -> bool {
1413 self.enemy_moves.clear();
1414
1415 let w = self.maze.width();
1416 let h = self.maze.height();
1417
1418 // Occupied mask: cells that are taken (by static entities or queued moves).
1419 let mut occupied = vec![false; w * h];
1420 for y in 0..h {
1421 for x in 0..w {
1422 if self.maze.entity_at(x, y).is_some() {
1423 occupied[y * w + x] = true;
1424 }
1425 }
1426 }
1427 // Also mark the player's cell as occupied so enemies don't walk into it.
1428 let pcx_usize = self.player.x.floor() as usize;
1429 let pcy_usize = self.player.y.floor() as usize;
1430 if pcx_usize < w && pcy_usize < h {
1431 occupied[pcy_usize * w + pcx_usize] = true;
1432 }
1433
1434 // Gather all enemy/hallucination positions (snapshot before mutation).
1435 let mut enemy_positions: Vec<(usize, usize)> = Vec::new();
1436 for y in 0..h {
1437 for x in 0..w {
1438 match self.maze.entity_at(x, y) {
1439 Some(Entity::Enemy { .. }) | Some(Entity::Hallucination { .. }) => {
1440 enemy_positions.push((x, y));
1441 }
1442 _ => {}
1443 }
1444 }
1445 }
1446
1447 let px = self.player.x;
1448 let py = self.player.y;
1449 let pcx = px.floor() as isize;
1450 let pcy = py.floor() as isize;
1451 let is_patrol_turn = self.heartbeat_count % 3 == 0;
1452
1453 // Drain acted-this-turn list for fast lookups.
1454 let acted: Vec<(usize, usize)> = self.acted_this_turn.drain(..).collect();
1455
1456 for (ex, ey) in enemy_positions {
1457 // Entity may have been removed by a previous enemy's action.
1458 let entity = match self.maze.entity_at(ex, ey) {
1459 Some(e) => e.clone(),
1460 None => continue,
1461 };
1462
1463 let is_hallucination = matches!(entity, Entity::Hallucination { .. });
1464
1465 // ---- hallucination behaviour gating ----
1466 if is_hallucination {
1467 if self.sanity >= 40 {
1468 // Static: hallucinations don't move or attack.
1469 continue;
1470 }
1471 // Sanity 20-39%: patrol only.
1472 // Sanity < 20%: chase (handled below via has_line_of_sight).
1473 }
1474
1475 // ---- adjacent → attack ----
1476 if manhattan(ex as isize, ey as isize, pcx, pcy) == 1 {
1477 // Skip if this enemy already acted this turn (retaliated or blocked).
1478 if acted.contains(&(ex, ey)) {
1479 // Still let it patrol/move if applicable.
1480 } else {
1481 if is_hallucination {
1482 self.sanity = self.sanity.saturating_sub(5);
1483 self.set_message("A hallucination claws at your mind!");
1484 self.flash_color = 0xFF_8833CC;
1485 self.flash_alpha = 0.35;
1486 } else {
1487 let damage = match &entity {
1488 Entity::Enemy { damage, .. } => *damage,
1489 _ => 1,
1490 };
1491 self.hp = self.hp.saturating_sub(damage);
1492 self.set_message(&format!("An enemy strikes you for {} damage!", damage));
1493 self.flash_color = 0xFF_CC3333;
1494 self.flash_alpha = 0.4;
1495 if self.hp == 0 {
1496 self.state = GameState::GameOver;
1497 return false;
1498 }
1499 }
1500 continue;
1501 }
1502 }
1503
1504 // ---- line-of-sight → move toward player ----
1505 let has_los = self.has_line_of_sight(ex, ey, px, py);
1506
1507 if has_los {
1508 // Real enemies always chase; hallucinations chase only at sanity < 20%.
1509 if !is_hallucination {
1510 self.try_move_enemy_toward(ex, ey, &mut occupied);
1511 continue;
1512 } else if self.sanity < 20 {
1513 self.try_move_enemy_toward(ex, ey, &mut occupied);
1514 continue;
1515 }
1516 }
1517
1518 // ---- patrol ----
1519 if is_patrol_turn {
1520 self.try_patrol_enemy(ex, ey, &mut occupied);
1521 }
1522 }
1523
1524 !self.enemy_moves.is_empty()
1525 }
1526
1527 /// After enemy animation completes, place entities in their target cells.
1528 fn apply_enemy_moves(&mut self) {
1529 for m in self.enemy_moves.drain(..) {
1530 let idx = m.to_y * self.maze.width() + m.to_x;
1531 self.maze.entities[idx] = Some(m.entity);
1532 }
1533 }
1534
1535 /// Interpolation offset for an enemy during movement animation.
1536 /// Returns (dx, dy) in world units; (0, 0) when not animating.
1537 #[allow(dead_code)]
1538 pub fn enemy_offset(&self, cell_x: usize, cell_y: usize) -> (f64, f64) {
1539 if self.state != GameState::AnimatingEnemies {
1540 return (0.0, 0.0);
1541 }
1542 for m in &self.enemy_moves {
1543 if m.from_x == cell_x && m.from_y == cell_y {
1544 let t = smoothstep((self.enemy_anim_t / ENEMY_ANIM_DURATION).min(1.0));
1545 return (
1546 (m.to_x as f64 - m.from_x as f64) * t,
1547 (m.to_y as f64 - m.from_y as f64) * t,
1548 );
1549 }
1550 }
1551 (0.0, 0.0)
1552 }
1553
1554 /// Entities that should be rendered during enemy animation.
1555 /// Returns (world_x, world_y, entity) for mid-air enemies.
1556 pub fn enemy_render_entities(&self) -> Vec<(f64, f64, Entity)> {
1557 self.enemy_moves
1558 .iter()
1559 .map(|m| {
1560 let t = smoothstep((self.enemy_anim_t / ENEMY_ANIM_DURATION).min(1.0));
1561 let wx = m.from_x as f64 + 0.5 + (m.to_x as f64 - m.from_x as f64) * t;
1562 let wy = m.from_y as f64 + 0.5 + (m.to_y as f64 - m.from_y as f64) * t;
1563 (wx, wy, m.entity.clone())
1564 })
1565 .collect()
1566 }
1567
1228 fn facing_delta(&self) -> (isize, isize) { 1568 fn facing_delta(&self) -> (isize, isize) {
1229 let q = (self.player.angle / std::f64::consts::FRAC_PI_2).round() as i32 & 3; 1569 let q = (self.player.angle / std::f64::consts::FRAC_PI_2).round() as i32 & 3;
1230 match q { 1570 match q {
diff --git a/src/renderer.rs b/src/renderer.rs
index aa87b54..885e5de 100644
--- a/src/renderer.rs
+++ b/src/renderer.rs
@@ -74,6 +74,18 @@ pub struct Renderer {
74 floor_dist_lut: Vec<f64>, 74 floor_dist_lut: Vec<f64>,
75} 75}
76 76
77// ---- entity sprites ---------------------------------------------------
78
79/// Screen-space info for an entity sprite to render.
80struct Sprite {
81 sx: isize,
82 sy: isize,
83 scale: f64,
84 ch: char,
85 color: u32,
86 cam_z: f64,
87}
88
77impl Renderer { 89impl Renderer {
78 pub fn new(screen_w: usize, screen_h: usize) -> Self { 90 pub fn new(screen_w: usize, screen_h: usize) -> Self {
79 let half_h = screen_h as f64 / 2.0; 91 let half_h = screen_h as f64 / 2.0;
@@ -306,19 +318,9 @@ impl Renderer {
306 ) { 318 ) {
307 let mw = game.maze.width(); 319 let mw = game.maze.width();
308 let mh = game.maze.height(); 320 let mh = game.maze.height();
309
310 // Collect visible entities with their screen info.
311 struct Sprite {
312 sx: isize,
313 sy: isize,
314 scale: f64,
315 ch: char,
316 color: u32,
317 cam_z: f64,
318 }
319
320 let mut sprites: Vec<Sprite> = Vec::new(); 321 let mut sprites: Vec<Sprite> = Vec::new();
321 322
323 // Static entities on the grid.
322 for y in 0..mh { 324 for y in 0..mh {
323 for x in 0..mw { 325 for x in 0..mw {
324 let entity = match game.maze.entity_at(x, y) { 326 let entity = match game.maze.entity_at(x, y) {
@@ -329,46 +331,15 @@ impl Renderer {
329 let ex = x as f64 + 0.5; 331 let ex = x as f64 + 0.5;
330 let ey = y as f64 + 0.5; 332 let ey = y as f64 + 0.5;
331 333
332 let dx = ex - game.player.x; 334 self.push_entity_sprite(game, ex, ey, entity, fov, half_h, &mut sprites);
333 let dy = ey - game.player.y;
334
335 let fx = game.player.angle.cos();
336 let fy = game.player.angle.sin();
337 let rx = -fy;
338 let ry = fx;
339
340 let cam_z = dx * fx + dy * fy;
341 if cam_z <= 0.1 {
342 continue;
343 }
344
345 let cam_x = dx * rx + dy * ry;
346
347 let half_fov_tan = (fov / 2.0).tan();
348 let screen_x_norm = cam_x / (cam_z * half_fov_tan);
349 if screen_x_norm < -1.0 || screen_x_norm > 1.0 {
350 continue;
351 }
352
353 let sx = ((screen_x_norm + 1.0) * 0.5 * self.screen_w as f64) as isize;
354
355 let entity_h = (self.screen_h as f64 / cam_z * 0.4) as isize;
356 let sy = (half_h - entity_h as f64 * 0.3) as isize;
357
358 let scale = (entity_h.max(6) as f64 / LETTER_H as f64).max(0.5);
359 let (ch, color) = entity_char_and_color(entity);
360
361 sprites.push(Sprite {
362 sx,
363 sy,
364 scale,
365 ch,
366 color,
367 cam_z,
368 });
369 } 335 }
370 } 336 }
371 337
338 // Animating enemies (mid-move, not on the grid).
339 for (wx, wy, entity) in game.enemy_render_entities() {
340 self.push_entity_sprite(game, wx, wy, &entity, fov, half_h, &mut sprites);
341 }
342
372 // Sort farthest first so nearby entities draw on top. 343 // Sort farthest first so nearby entities draw on top.
373 sprites.sort_by(|a, b| b.cam_z.partial_cmp(&a.cam_z).unwrap()); 344 sprites.sort_by(|a, b| b.cam_z.partial_cmp(&a.cam_z).unwrap());
374 345
@@ -387,6 +358,56 @@ impl Renderer {
387 } 358 }
388 } 359 }
389 360
361 /// Project a world-space entity position and push a Sprite if on screen.
362 fn push_entity_sprite(
363 &self,
364 game: &Game,
365 ex: f64,
366 ey: f64,
367 entity: &Entity,
368 fov: f64,
369 half_h: f64,
370 sprites: &mut Vec<Sprite>,
371 ) {
372 let dx = ex - game.player.x;
373 let dy = ey - game.player.y;
374
375 let fx = game.player.angle.cos();
376 let fy = game.player.angle.sin();
377 let rx = -fy;
378 let ry = fx;
379
380 let cam_z = dx * fx + dy * fy;
381 if cam_z <= 0.1 {
382 return;
383 }
384
385 let cam_x = dx * rx + dy * ry;
386
387 let half_fov_tan = (fov / 2.0).tan();
388 let screen_x_norm = cam_x / (cam_z * half_fov_tan);
389 if screen_x_norm < -1.0 || screen_x_norm > 1.0 {
390 return;
391 }
392
393 let sx = ((screen_x_norm + 1.0) * 0.5 * self.screen_w as f64) as isize;
394
395 let entity_h = (self.screen_h as f64 / cam_z * 0.4) as isize;
396 let sy = (half_h - entity_h as f64 * 0.3) as isize;
397
398 let scale = (entity_h.max(6) as f64 / LETTER_H as f64).max(0.5);
399 let (ch, color) = entity_char_and_color(entity);
400
401 sprites.push(Sprite {
402 sx,
403 sy,
404 scale,
405 ch,
406 color,
407 cam_z,
408 });
409 }
410
390 fn draw_char_scaled( 411 fn draw_char_scaled(
391 &self, 412 &self,
392 buffer: &mut [u32], 413 buffer: &mut [u32],