diff options
Diffstat (limited to '')
| -rw-r--r-- | src/game.rs | 84 |
1 files changed, 71 insertions, 13 deletions
diff --git a/src/game.rs b/src/game.rs index 80ee0c6..ca30207 100644 --- a/src/game.rs +++ b/src/game.rs | |||
| @@ -608,6 +608,7 @@ impl Player { | |||
| 608 | const MOVE_DURATION: f64 = 0.18; // seconds to walk one tile | 608 | const MOVE_DURATION: f64 = 0.18; // seconds to walk one tile |
| 609 | const TURN_DURATION: f64 = 0.12; // seconds for a 90° turn | 609 | const TURN_DURATION: f64 = 0.12; // seconds for a 90° turn |
| 610 | const ENEMY_ANIM_DURATION: f64 = 0.10; // seconds for enemy movement | 610 | const ENEMY_ANIM_DURATION: f64 = 0.10; // seconds for enemy movement |
| 611 | const ATTACK_DURATION: f64 = 0.12; // seconds for attack lunge | ||
| 611 | 612 | ||
| 612 | pub const MAX_HP: u32 = 5; | 613 | pub const MAX_HP: u32 = 5; |
| 613 | pub const MAX_SANITY: u32 = 100; | 614 | pub const MAX_SANITY: u32 = 100; |
| @@ -659,6 +660,19 @@ pub struct Game { | |||
| 659 | 660 | ||
| 660 | // ---- animation: enemies ---- | 661 | // ---- animation: enemies ---- |
| 661 | enemy_anim_t: f64, | 662 | enemy_anim_t: f64, |
| 663 | |||
| 664 | // ---- animation: attack ---- | ||
| 665 | pub attacking: bool, | ||
| 666 | pub attack_t: f64, | ||
| 667 | attack_base_x: f64, | ||
| 668 | attack_base_y: f64, | ||
| 669 | |||
| 670 | // ---- screen flash ---- | ||
| 671 | pub flash_color: u32, | ||
| 672 | pub flash_alpha: f64, | ||
| 673 | |||
| 674 | // ---- controls overlay ---- | ||
| 675 | pub show_controls: bool, | ||
| 662 | } | 676 | } |
| 663 | 677 | ||
| 664 | fn smoothstep(t: f64) -> f64 { | 678 | fn smoothstep(t: f64) -> f64 { |
| @@ -703,6 +717,13 @@ impl Game { | |||
| 703 | turn_delta: 0.0, | 717 | turn_delta: 0.0, |
| 704 | turn_t: 0.0, | 718 | turn_t: 0.0, |
| 705 | enemy_anim_t: 0.0, | 719 | enemy_anim_t: 0.0, |
| 720 | attacking: false, | ||
| 721 | attack_t: 0.0, | ||
| 722 | attack_base_x: 0.0, | ||
| 723 | attack_base_y: 0.0, | ||
| 724 | flash_color: 0, | ||
| 725 | flash_alpha: 0.0, | ||
| 726 | show_controls: true, | ||
| 706 | } | 727 | } |
| 707 | } | 728 | } |
| 708 | 729 | ||
| @@ -717,6 +738,11 @@ impl Game { | |||
| 717 | /// Returns true when the current animation phase finishes and we should | 738 | /// Returns true when the current animation phase finishes and we should |
| 718 | /// advance the state machine. | 739 | /// advance the state machine. |
| 719 | pub fn update_animation(&mut self, dt: f64) -> bool { | 740 | pub fn update_animation(&mut self, dt: f64) -> bool { |
| 741 | // Flash always fades, regardless of state. | ||
| 742 | if self.flash_alpha > 0.0 { | ||
| 743 | self.flash_alpha = (self.flash_alpha - dt * 4.0).max(0.0); | ||
| 744 | } | ||
| 745 | |||
| 720 | // Turn animation runs independently — turning never consumes a turn. | 746 | // Turn animation runs independently — turning never consumes a turn. |
| 721 | if self.turning { | 747 | if self.turning { |
| 722 | self.turn_t += dt / TURN_DURATION; | 748 | self.turn_t += dt / TURN_DURATION; |
| @@ -751,6 +777,28 @@ impl Game { | |||
| 751 | } | 777 | } |
| 752 | } | 778 | } |
| 753 | 779 | ||
| 780 | // Animate attack lunge. | ||
| 781 | if self.attacking { | ||
| 782 | self.attack_t += dt / ATTACK_DURATION; | ||
| 783 | if self.attack_t >= 1.0 { | ||
| 784 | self.player.x = self.attack_base_x; | ||
| 785 | self.player.y = self.attack_base_y; | ||
| 786 | self.attacking = false; | ||
| 787 | } else { | ||
| 788 | // Lunge forward then back. | ||
| 789 | let lunge = if self.attack_t < 0.5 { | ||
| 790 | smoothstep(self.attack_t * 2.0) * 0.25 | ||
| 791 | } else { | ||
| 792 | (1.0 - smoothstep((self.attack_t - 0.5) * 2.0)) * 0.25 | ||
| 793 | }; | ||
| 794 | let fx = self.player.angle.cos(); | ||
| 795 | let fy = self.player.angle.sin(); | ||
| 796 | self.player.x = self.attack_base_x + fx * lunge; | ||
| 797 | self.player.y = self.attack_base_y + fy * lunge; | ||
| 798 | done = false; | ||
| 799 | } | ||
| 800 | } | ||
| 801 | |||
| 754 | if done { | 802 | if done { |
| 755 | self.state = GameState::ResolvingTurn; | 803 | self.state = GameState::ResolvingTurn; |
| 756 | self.resolve_turn(); | 804 | self.resolve_turn(); |
| @@ -921,12 +969,10 @@ impl Game { | |||
| 921 | let new_hp = hp.saturating_sub(total_dmg); | 969 | let new_hp = hp.saturating_sub(total_dmg); |
| 922 | 970 | ||
| 923 | if new_hp == 0 { | 971 | if new_hp == 0 { |
| 924 | // Enemy dies. | ||
| 925 | self.maze.take_entity(fx, fy); | 972 | self.maze.take_entity(fx, fy); |
| 926 | self.kills += 1; | 973 | self.kills += 1; |
| 927 | self.set_message(&format!("You slay the enemy! ({} dmg)", total_dmg)); | 974 | self.set_message(&format!("You slay the enemy! ({} dmg)", total_dmg)); |
| 928 | } else { | 975 | } else { |
| 929 | // Enemy survives and retaliates. | ||
| 930 | self.maze.entity_at_mut(fx, fy).map(|e| { | 976 | self.maze.entity_at_mut(fx, fy).map(|e| { |
| 931 | if let Entity::Enemy { hp: ref mut h, .. } = e { | 977 | if let Entity::Enemy { hp: ref mut h, .. } = e { |
| 932 | *h = new_hp; | 978 | *h = new_hp; |
| @@ -941,7 +987,9 @@ impl Game { | |||
| 941 | self.state = GameState::GameOver; | 987 | self.state = GameState::GameOver; |
| 942 | } | 988 | } |
| 943 | } | 989 | } |
| 944 | self.advance_turn(); | 990 | self.flash_color = 0xFF_CC3333; |
| 991 | self.flash_alpha = 0.4; | ||
| 992 | self.begin_attack_animation(); | ||
| 945 | true | 993 | true |
| 946 | } | 994 | } |
| 947 | 995 | ||
| @@ -950,11 +998,9 @@ impl Game { | |||
| 950 | let new_hp = hp.saturating_sub(1); | 998 | let new_hp = hp.saturating_sub(1); |
| 951 | 999 | ||
| 952 | if !persistent || new_hp == 0 { | 1000 | if !persistent || new_hp == 0 { |
| 953 | // Vanishes. | ||
| 954 | self.maze.take_entity(fx, fy); | 1001 | self.maze.take_entity(fx, fy); |
| 955 | self.set_message("The hallucination dissipates."); | 1002 | self.set_message("The hallucination dissipates."); |
| 956 | } else { | 1003 | } else { |
| 957 | // Persistent — doesn't vanish yet. | ||
| 958 | self.maze.entity_at_mut(fx, fy).map(|e| { | 1004 | self.maze.entity_at_mut(fx, fy).map(|e| { |
| 959 | if let Entity::Hallucination { hp: ref mut h, .. } = e { | 1005 | if let Entity::Hallucination { hp: ref mut h, .. } = e { |
| 960 | *h = new_hp; | 1006 | *h = new_hp; |
| @@ -962,18 +1008,30 @@ impl Game { | |||
| 962 | }); | 1008 | }); |
| 963 | self.set_message("You strike the apparition - it remains."); | 1009 | self.set_message("You strike the apparition - it remains."); |
| 964 | } | 1010 | } |
| 965 | self.advance_turn(); | 1011 | self.flash_color = 0xFF_8833CC; |
| 1012 | self.flash_alpha = 0.35; | ||
| 1013 | self.begin_attack_animation(); | ||
| 966 | true | 1014 | true |
| 967 | } | 1015 | } |
| 968 | 1016 | ||
| 969 | _ => { | 1017 | _ => { |
| 970 | self.set_message("You swing at the air."); | 1018 | self.set_message("You swing at the air."); |
| 971 | self.advance_turn(); | 1019 | self.flash_color = 0xFF_FFFFFF; |
| 1020 | self.flash_alpha = 0.05; | ||
| 1021 | self.begin_attack_animation(); | ||
| 972 | true | 1022 | true |
| 973 | } | 1023 | } |
| 974 | } | 1024 | } |
| 975 | } | 1025 | } |
| 976 | 1026 | ||
| 1027 | fn begin_attack_animation(&mut self) { | ||
| 1028 | self.attack_base_x = self.player.x; | ||
| 1029 | self.attack_base_y = self.player.y; | ||
| 1030 | self.attacking = true; | ||
| 1031 | self.attack_t = 0.0; | ||
| 1032 | self.state = GameState::AnimatingPlayer; | ||
| 1033 | } | ||
| 1034 | |||
| 977 | // ---- inventory -------------------------------------------------------- | 1035 | // ---- inventory -------------------------------------------------------- |
| 978 | 1036 | ||
| 979 | /// Use the currently selected inventory item (Z key). | 1037 | /// Use the currently selected inventory item (Z key). |
| @@ -1107,7 +1165,10 @@ impl Game { | |||
| 1107 | 1165 | ||
| 1108 | /// Apply all per-turn effects (weakness, heartbeat, HP drain, message decay). | 1166 | /// Apply all per-turn effects (weakness, heartbeat, HP drain, message decay). |
| 1109 | fn resolve_turn(&mut self) { | 1167 | fn resolve_turn(&mut self) { |
| 1110 | self.weakness = (self.weakness + 1).min(MAX_WEAKNESS); | 1168 | // Weakness grows every 3 turns, not every turn. |
| 1169 | if self.heartbeat_count % 3 == 0 { | ||
| 1170 | self.weakness = (self.weakness + 1).min(MAX_WEAKNESS); | ||
| 1171 | } | ||
| 1111 | self.heartbeat_count += 1; | 1172 | self.heartbeat_count += 1; |
| 1112 | 1173 | ||
| 1113 | // Message decay. | 1174 | // Message decay. |
| @@ -1118,14 +1179,11 @@ impl Game { | |||
| 1118 | } | 1179 | } |
| 1119 | } | 1180 | } |
| 1120 | 1181 | ||
| 1121 | // HP drain at max weakness. | 1182 | // HP drain at max weakness — stops at 1, never kills. |
| 1122 | if self.weakness >= MAX_WEAKNESS { | 1183 | if self.weakness >= MAX_WEAKNESS { |
| 1123 | if self.hp > 0 { | 1184 | if self.hp > 1 { |
| 1124 | self.hp -= 1; | 1185 | self.hp -= 1; |
| 1125 | } | 1186 | } |
| 1126 | if self.hp == 0 { | ||
| 1127 | self.state = GameState::GameOver; | ||
| 1128 | } | ||
| 1129 | } | 1187 | } |
| 1130 | } | 1188 | } |
| 1131 | 1189 | ||
