1
0
Fork 0
mirror of https://github.com/doukutsu-rs/doukutsu-rs synced 2025-03-23 18:39:20 +00:00

boss life bar, textscript opcodes and etc.

This commit is contained in:
Alula 2020-11-02 15:01:30 +01:00
parent 6964b8d167
commit 30f47b17eb
No known key found for this signature in database
GPG key ID: 3E00485503A1D8BA
11 changed files with 457 additions and 19 deletions

View file

@ -76,6 +76,7 @@ bitfield! {
pub alive, set_alive: 7; // 0x80
// engine specific flags
pub drs_boss, set_drs_boss: 14;
pub drs_destroyed, set_drs_destroyed: 15;
}

View file

@ -0,0 +1,122 @@
use crate::entity::GameEntity;
use crate::frame::Frame;
use crate::ggez::{Context, GameResult};
use crate::npc::NPCMap;
use crate::shared_game_state::SharedGameState;
use crate::common::Rect;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
enum BossLifeTarget {
None,
NPC(u16),
Boss,
}
pub struct BossLifeBar {
target: BossLifeTarget,
life: u16,
max_life: u16,
prev_life: u16,
counter: u16,
}
impl BossLifeBar {
pub fn new() -> BossLifeBar {
BossLifeBar {
target: BossLifeTarget::None,
life: 0,
max_life: 0,
prev_life: 0,
counter: 0,
}
}
pub fn set_npc_target(&mut self, npc_id: u16, npc_map: &NPCMap) {
if let Some(npc_cell) = npc_map.npcs.get(&npc_id) {
let npc = npc_cell.borrow();
self.target = BossLifeTarget::NPC(npc.id);
self.life = npc.life;
self.max_life = self.life;
self.prev_life = self.life;
}
}
pub fn set_boss_target(&mut self, npc_map: &NPCMap) {
self.target = BossLifeTarget::Boss;
self.life = npc_map.boss_map.parts[0].life;
self.max_life = self.life;
self.prev_life = self.life;
}
}
impl GameEntity<&NPCMap> for BossLifeBar {
fn tick(&mut self, state: &mut SharedGameState, npc_map: &NPCMap) -> GameResult<()> {
match self.target {
BossLifeTarget::NPC(npc_id) => {
if let Some(npc_cell) = npc_map.npcs.get(&npc_id) {
let npc = npc_cell.borrow();
self.life = npc.life;
}
}
BossLifeTarget::Boss => {
self.life = npc_map.boss_map.parts[0].life;
}
_ => {
return Ok(());
}
}
if self.life == 0 {
self.target = BossLifeTarget::None;
} else if self.prev_life > self.life {
self.counter += 1;
if self.counter > 30 {
self.prev_life = self.prev_life.saturating_sub(1);
}
} else {
self.counter = 0;
}
Ok(())
}
fn draw(&self, state: &mut SharedGameState, ctx: &mut Context, _frame: &Frame) -> GameResult<()> {
if self.max_life == 0 || self.target == BossLifeTarget::None {
return Ok(());
}
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "TextBox")?;
let box_length = 256;
let bar_length = box_length - 58;
let text_rect = Rect::new_size(0, 48, 32, 8);
let box_rect1 = Rect::new_size(0, 0, 244, 8);
let box_rect2 = Rect::new_size(0, 16, 244, 8);
let mut rect_prev_bar = Rect::new_size(0, 32, 232, 8);
let mut rect_life_bar = Rect::new_size(0, 24, 232, 8);
rect_prev_bar.right = ((self.prev_life as usize * bar_length) / self.max_life as usize).min(bar_length);
rect_life_bar.right = ((self.life as usize * bar_length) / self.max_life as usize).min(bar_length);
batch.add_rect(((state.canvas_size.0 - box_length as f32) / 2.0).floor(),
state.canvas_size.1 - 20.0, &box_rect1);
batch.add_rect(((state.canvas_size.0 - box_length as f32) / 2.0).floor(),
state.canvas_size.1 - 12.0, &box_rect2);
batch.add_rect(((state.canvas_size.0 - box_length as f32) / 2.0).floor(),
state.canvas_size.1 - 20.0, &box_rect1);
batch.add_rect(((state.canvas_size.0 - box_length as f32) / 2.0 + 40.0).floor(),
state.canvas_size.1 - 16.0, &rect_prev_bar);
batch.add_rect(((state.canvas_size.0 - box_length as f32) / 2.0 + 40.0).floor(),
state.canvas_size.1 - 16.0, &rect_life_bar);
batch.add_rect(((state.canvas_size.0 - box_length as f32) / 2.0 + 8.0).floor(),
state.canvas_size.1 - 16.0, &text_rect);
batch.draw(ctx)?;
Ok(())
}
}

1
src/components/mod.rs Normal file
View file

@ -0,0 +1 @@
pub mod boss_life_bar;

View file

@ -155,6 +155,10 @@ impl SpriteBatch {
impl graphics::Drawable for SpriteBatch {
fn draw(&self, ctx: &mut Context, param: DrawParam) -> GameResult {
if self.sprites.is_empty() {
return Ok(());
}
// Awkwardly we must update values on all sprites and such.
// Also awkwardly we have this chain of colors with differing priorities.
self.flush(ctx, &self.image)?;

View file

@ -27,6 +27,7 @@ use crate::ggez::conf::{Backend, WindowMode, WindowSetup};
use crate::ggez::event::{KeyCode, KeyMods};
use crate::ggez::graphics;
use crate::ggez::graphics::{Canvas, DrawParam, window};
use crate::ggez::graphics::glutin_ext::WindowUpdateExt;
use crate::ggez::input::keyboard;
use crate::ggez::mint::ColumnMatrix4;
use crate::ggez::nalgebra::Vector2;
@ -34,7 +35,6 @@ use crate::scene::loading_scene::LoadingScene;
use crate::scene::Scene;
use crate::shared_game_state::{SharedGameState, TimingMode};
use crate::ui::UI;
use crate::ggez::graphics::glutin_ext::WindowUpdateExt;
mod bmfont;
mod bmfont_renderer;
@ -42,6 +42,7 @@ mod builtin_fs;
mod bullet;
mod caret;
mod common;
mod components;
mod encoding;
mod engine_constants;
mod entity;

View file

@ -1,12 +1,14 @@
use crate::common::{Direction, Rect};
use crate::npc::boss::BossNPC;
use crate::npc::NPCMap;
use crate::player::Player;
use crate::shared_game_state::SharedGameState;
impl BossNPC {
pub(crate) fn tick_b02_balfrog(&mut self, state: &mut SharedGameState) {
pub(crate) fn tick_b02_balfrog(&mut self, state: &mut SharedGameState, player: &Player) {
match self.parts[0].action_num {
0 => {
self.hurt_sound[0] = 52;
self.parts[0].x = 6 * 16 * 0x200;
self.parts[0].y = 12 * 16 * 0x200;
self.parts[0].direction = Direction::Right;
@ -68,6 +70,182 @@ impl BossNPC {
self.parts[0].anim_num = 0;
}
}
100 | 101 => {
if self.parts[0].action_num == 100 {
self.parts[0].action_num = 101;
self.parts[0].action_counter = 0;
self.parts[0].anim_num = 1;
self.parts[0].vel_x = 0;
}
self.parts[0].action_counter += 1;
if self.parts[0].action_counter > 50 {
self.parts[0].action_num = 102;
self.parts[0].anim_counter = 0;
self.parts[0].anim_num = 2;
}
}
102 => {
self.parts[0].anim_counter += 1;
if self.parts[0].anim_counter > 10 {
self.parts[0].action_num = 103;
self.parts[0].anim_counter = 0;
self.parts[0].anim_num = 1;
}
}
103 => {
self.parts[0].anim_counter += 1;
if self.parts[0].anim_counter > 4 {
self.parts[0].action_num = 104;
self.parts[0].anim_num = 5;
self.parts[0].vel_x = self.parts[0].direction.vector_x() * 0x200;
self.parts[0].vel_y = -2 * 0x200;
self.parts[0].display_bounds.top = 64 * 0x200;
self.parts[0].display_bounds.bottom = 24 * 0x200;
state.sound_manager.play_sfx(25);
}
}
104 => {
if self.parts[0].direction == Direction::Left && self.parts[0].flags.hit_left_wall() {
self.parts[0].direction = Direction::Right;
self.parts[0].vel_x = 0x200;
}
if self.parts[0].direction == Direction::Right && self.parts[0].flags.hit_right_wall() {
self.parts[0].direction = Direction::Left;
self.parts[0].vel_x = -0x200;
}
if self.parts[0].flags.hit_bottom_wall() {
self.parts[0].action_num = 100;
self.parts[0].anim_num = 1;
self.parts[0].display_bounds.top = 48 * 0x200;
self.parts[0].display_bounds.bottom = 16 * 0x200;
if self.parts[0].direction == Direction::Left && self.parts[0].x < player.x {
self.parts[0].direction = Direction::Right;
self.parts[0].action_num = 110;
}
if self.parts[0].direction == Direction::Right && self.parts[0].x > player.x {
self.parts[0].direction = Direction::Left;
self.parts[0].action_num = 110;
}
let mut npc = NPCMap::create_npc(110, &state.npc_table);
npc.cond.set_alive(true);
npc.x = state.game_rng.range(4..16) as isize * 16 * 0x200;
npc.y = state.game_rng.range(0..4) as isize * 16 * 0x200;
npc.direction = if npc.x < player.x { Direction::Left } else { Direction::Right };
state.new_npcs.push(npc);
let mut npc = NPCMap::create_npc(4, &state.npc_table);
for _ in 0..4 {
npc.cond.set_alive(true);
npc.direction = Direction::Left;
npc.x = self.parts[0].x + state.game_rng.range(-12..12) as isize * 0x200;
npc.y = self.parts[0].y + state.game_rng.range(-12..12) as isize * 0x200;
npc.vel_x = state.game_rng.range(-0x155..0x155) as isize;
npc.vel_y = state.game_rng.range(-0x600..0) as isize;
state.new_npcs.push(npc);
}
state.quake_counter = 30;
state.sound_manager.play_sfx(26);
}
}
110 | 111 => {
if self.parts[0].action_num == 110 {
self.parts[0].anim_num = 1;
self.parts[0].action_num = 111;
self.parts[0].action_counter = 0;
}
self.parts[0].action_counter += 1;
self.parts[0].vel_x = self.parts[0].vel_x * 8 / 9;
if self.parts[0].action_counter > 50 {
self.parts[0].anim_num = 2;
self.parts[0].anim_counter = 0;
self.parts[0].action_num = 112;
}
}
112 => {
self.parts[0].anim_counter += 1;
if self.parts[0].anim_counter > 4 {
self.parts[0].action_num = 113;
self.parts[0].action_counter = 0;
self.parts[0].action_counter2 = 16;
self.parts[0].anim_num = 3;
self.parts[0].target_x = self.parts[0].life as isize;
self.parts[1].npc_flags.set_shootable(true);
}
}
113 => {
if self.parts[0].shock != 0 {
if self.parts[0].action_counter2 / 2 % 2 != 0 {
self.parts[0].anim_num = 4;
} else {
self.parts[0].anim_num = 3;
}
} else {
self.parts[0].action_counter2 = 0;
self.parts[0].anim_num = 3;
}
self.parts[0].vel_x = self.parts[0].vel_x * 10 / 11;
self.parts[0].action_counter += 1;
if self.parts[0].action_counter > 16 {
self.parts[0].action_counter = 0;
self.parts[0].action_counter2 = self.parts[0].action_counter2.saturating_sub(1);
let px = self.parts[0].x + self.parts[0].direction.vector_x() * 2 * 16 * 0x200;
let py = self.parts[0].y - 8 * 0x200 - player.y;
let deg = f64::atan2(py as f64, px as f64);
// todo rand
let mut npc = NPCMap::create_npc(108, &state.npc_table);
npc.cond.set_alive(true);
npc.x = px;
npc.y = py;
npc.vel_x = (deg.cos() * 512.0) as isize;
npc.vel_y = (deg.sin() * 512.0) as isize;
state.sound_manager.play_sfx(39);
if self.parts[0].action_counter2 == 0 || (self.parts[0].life as isize) < self.parts[0].target_x - 90 {
self.parts[0].action_num = 114;
self.parts[0].action_counter = 0;
self.parts[0].anim_num = 2;
self.parts[0].anim_counter = 0;
self.parts[1].npc_flags.set_shootable(false);
}
}
}
114 => {
self.parts[0].anim_counter += 1;
if self.parts[0].anim_counter > 10 {
self.parts[0].anim_num = 1;
self.parts[0].anim_counter = 0;
self.parts[1].action_counter2 += 1;
if self.parts[1].action_counter2 > 2 {
self.parts[1].action_counter2 = 0;
self.parts[0].action_num = 120;
} else {
self.parts[0].action_num = 100;
}
}
}
_ => {}
}

View file

@ -1,14 +1,14 @@
use std::cell::RefCell;
use std::collections::HashMap;
use crate::ggez::{GameResult, Context};
use crate::common::{Direction, interpolate_fix9_scale};
use crate::entity::GameEntity;
use crate::frame::Frame;
use crate::ggez::{Context, GameResult};
use crate::npc::NPC;
use crate::player::Player;
use crate::shared_game_state::SharedGameState;
use crate::stage::Stage;
use crate::entity::GameEntity;
use crate::frame::Frame;
use crate::common::{Direction, interpolate_fix9_scale};
pub mod balfrog;
pub mod ballos;
@ -23,13 +23,20 @@ pub mod undead_core;
pub struct BossNPC {
pub boss_type: u16,
pub parts: [NPC; 16],
pub hurt_sound: [u8; 16],
pub death_sound: [u8; 16],
}
impl BossNPC {
pub fn new() -> BossNPC {
let mut part = NPC::empty();
part.cond.set_drs_boss(true);
BossNPC {
boss_type: 0,
parts: [NPC::empty(); 16],
parts: [part; 16],
hurt_sound: [0; 16],
death_sound: [0; 16],
}
}
}
@ -38,7 +45,7 @@ impl GameEntity<(&mut Player, &HashMap<u16, RefCell<NPC>>, &mut Stage)> for Boss
fn tick(&mut self, state: &mut SharedGameState, (player, map, stage): (&mut Player, &HashMap<u16, RefCell<NPC>>, &mut Stage)) -> GameResult {
match self.boss_type {
1 => self.tick_b01_omega(),
2 => self.tick_b02_balfrog(state),
2 => self.tick_b02_balfrog(state, player),
3 => self.tick_b03_monster_x(),
4 => self.tick_b04_core(),
5 => self.tick_b05_ironhead(),

View file

@ -112,7 +112,7 @@ pub struct NPC {
pub anim_rect: Rect<usize>,
}
static PARTICLE_NPCS: [u16; 9] = [1, 4, 73, 84, 86, 87, 129, 199, 355];
static PARTICLE_NPCS: [u16; 10] = [1, 4, 73, 84, 86, 87, 108, 129, 199, 355];
impl NPC {
pub fn get_start_index(&self) -> u16 {
@ -314,13 +314,19 @@ impl PhysicalEntity for NPC {
fn vel_y(&self) -> isize { self.vel_y }
#[inline(always)]
fn hit_rect_size(&self) -> usize { if self.size >= 3 { 3 } else { 2 } }
fn hit_rect_size(&self) -> usize {
if self.size >= 3 {
if self.cond.drs_boss() { 4 } else { 3 }
} else {
2
}
}
#[inline(always)]
fn offset_x(&self) -> isize { if self.size >= 3 { -0x1000 } else { 0 } }
fn offset_x(&self) -> isize { if self.size >= 3 && !self.cond.drs_boss() { -0x1000 } else { 0 } }
#[inline(always)]
fn offset_y(&self) -> isize { if self.size >= 3 { -0x1000 } else { 0 } }
fn offset_y(&self) -> isize { if self.size >= 3 && !self.cond.drs_boss() { -0x1000 } else { 0 } }
#[inline(always)]
fn hit_bounds(&self) -> &Rect<usize> {

View file

@ -19,7 +19,7 @@ pub enum ControlMode {
IronHead,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
/// Cave Story+ player skins
pub enum PlayerAppearance {

View file

@ -4,6 +4,7 @@ use log::info;
use crate::bullet::BulletManager;
use crate::caret::CaretType;
use crate::common::{Direction, FadeDirection, FadeState, fix9_scale, Rect};
use crate::components::boss_life_bar::BossLifeBar;
use crate::entity::GameEntity;
use crate::frame::{Frame, UpdateTarget};
use crate::ggez::{Context, GameResult, graphics, timer};
@ -16,7 +17,7 @@ use crate::player::{Player, PlayerAppearance};
use crate::rng::RNG;
use crate::scene::Scene;
use crate::scene::title_scene::TitleScene;
use crate::shared_game_state::{SharedGameState, Season};
use crate::shared_game_state::{Season, SharedGameState};
use crate::stage::{BackgroundType, Stage};
use crate::text_script::{ConfirmSelection, ScriptMode, TextScriptExecutionState, TextScriptVM};
use crate::texture_set::SizedBatch;
@ -26,6 +27,7 @@ use crate::weapon::WeaponType;
pub struct GameScene {
pub tick: usize,
pub stage: Stage,
pub boss_life_bar: BossLifeBar,
pub frame: Frame,
pub player: Player,
pub inventory: Inventory,
@ -74,6 +76,7 @@ impl GameScene {
stage,
player: Player::new(state),
inventory: Inventory::new(),
boss_life_bar: BossLifeBar::new(),
frame: Frame {
x: 0,
y: 0,
@ -800,7 +803,7 @@ impl GameScene {
}
for bullet in self.bullet_manager.bullets.iter_mut() {
if bullet.damage < 1 {
if !bullet.cond.alive() || bullet.damage < 1 {
continue;
}
@ -823,7 +826,6 @@ impl GameScene {
}
if npc.npc_flags.shootable() {
log::info!("damage: {} {}", npc.life, -(bullet.damage.min(npc.life) as isize));
npc.life = npc.life.saturating_sub(bullet.damage);
if npc.life == 0 {
@ -843,7 +845,12 @@ impl GameScene {
if let Some(table_entry) = state.npc_table.get_entry(npc.npc_type) {
state.sound_manager.play_sfx(table_entry.hurt_sound);
}
npc.shock = 16;
for _ in 0..3 {
state.create_caret((bullet.x + npc.x) / 2, (bullet.y + npc.y) / 2, CaretType::HurtParticles, Direction::Left);
}
}
if npc.npc_flags.show_damage() {
@ -872,6 +879,95 @@ impl GameScene {
}
}
for i in 0..self.npc_map.boss_map.parts.len() {
let mut idx = i;
let (mut destroy_x, mut destroy_y, mut destroy_radius, mut destroy_count) = (0, 0, 0, 0);
let mut npc = unsafe { self.npc_map.boss_map.parts.get_unchecked_mut(i) };
if !npc.cond.alive() {
continue;
}
for bullet in self.bullet_manager.bullets.iter_mut() {
if !bullet.cond.alive() || bullet.damage < 1 {
continue;
}
let hit = (
npc.npc_flags.shootable()
&& (npc.x - npc.hit_bounds.right as isize) < (bullet.x + bullet.enemy_hit_width as isize)
&& (npc.x + npc.hit_bounds.right as isize) > (bullet.x - bullet.enemy_hit_width as isize)
&& (npc.y - npc.hit_bounds.top as isize) < (bullet.y + bullet.enemy_hit_height as isize)
&& (npc.y + npc.hit_bounds.bottom as isize) > (bullet.y - bullet.enemy_hit_height as isize)
) || (
npc.npc_flags.invulnerable()
&& (npc.x - npc.hit_bounds.right as isize) < (bullet.x + bullet.hit_bounds.right as isize)
&& (npc.x + npc.hit_bounds.right as isize) > (bullet.x - bullet.hit_bounds.left as isize)
&& (npc.y - npc.hit_bounds.top as isize) < (bullet.y + bullet.hit_bounds.bottom as isize)
&& (npc.y + npc.hit_bounds.bottom as isize) > (bullet.y - bullet.hit_bounds.top as isize)
);
if !hit {
continue;
}
if npc.npc_flags.shootable() {
if npc.cond.damage_boss() {
idx = 0;
npc = unsafe { self.npc_map.boss_map.parts.get_unchecked_mut(0) };
}
npc.life = npc.life.saturating_sub(bullet.damage);
if npc.life == 0 {
npc.life = npc.id;
if self.player.cond.alive() && npc.npc_flags.event_when_killed() {
state.control_flags.set_tick_world(true);
state.control_flags.set_interactions_disabled(true);
state.textscript_vm.start_script(npc.event_num);
} else {
state.sound_manager.play_sfx(self.npc_map.boss_map.death_sound[idx]);
destroy_x = npc.x;
destroy_y = npc.y;
destroy_radius = npc.display_bounds.right;
destroy_count = 4usize * (2usize).pow((npc.size as u32).saturating_sub(1));
npc.cond.set_alive(false);
}
} else {
if npc.shock < 14 {
for _ in 0..3 {
state.create_caret(bullet.x, bullet.y, CaretType::HurtParticles, Direction::Left);
}
state.sound_manager.play_sfx(self.npc_map.boss_map.hurt_sound[idx]);
}
npc.shock = 8;
npc = unsafe { self.npc_map.boss_map.parts.get_unchecked_mut(0) };
npc.shock = 8;
}
bullet.life = bullet.life.saturating_sub(1);
if bullet.life < 1 {
bullet.cond.set_alive(false);
}
} else if [13, 14, 15, 28, 29, 30].contains(&bullet.btype) {
bullet.life = bullet.life.saturating_sub(1);
} else if !bullet.weapon_flags.hit_right_slope() {
state.create_caret(bullet.x, bullet.y, CaretType::ProjectileDissipation, Direction::Right);
state.sound_manager.play_sfx(31);
bullet.life = 0;
continue;
}
}
if destroy_count != 0 {
self.npc_map.create_death_effect(destroy_x, destroy_y, destroy_radius, destroy_count, state);
}
}
if !dead_npcs.is_empty() {
let missile = self.inventory.has_weapon(WeaponType::MissileLauncher)
| self.inventory.has_weapon(WeaponType::SuperMissileLauncher);
@ -998,6 +1094,8 @@ impl GameScene {
} else {
self.life_bar_counter = 0;
}
self.boss_life_bar.tick(state, &self.npc_map)?;
}
Ok(())
@ -1141,7 +1239,7 @@ impl GameScene {
batch.add_rect(((x + ox) * 16 - self.frame.x / 0x200) as f32 - 2.0,
((y + oy) * 16 - self.frame.y / 0x200) as f32 - 2.0,
&caret_rect);
&caret_rect);
}
@ -1317,6 +1415,7 @@ impl Scene for GameScene {
if state.control_flags.control_enabled() {
self.draw_hud(state, ctx)?;
self.boss_life_bar.draw(state, ctx, &self.frame)?;
}
if state.textscript_vm.mode == ScriptMode::StageSelect {

View file

@ -1140,6 +1140,26 @@ impl TextScriptVM {
exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32);
}
OpCode::BSL => {
let event_num = read_cur_varint(&mut cursor)? as u16;
if event_num == 0 {
game_scene.boss_life_bar.set_boss_target(&game_scene.npc_map);
} else {
for npc_id in game_scene.npc_map.npc_ids.iter() {
if let Some(npc_cell) = game_scene.npc_map.npcs.get(npc_id) {
let npc = npc_cell.borrow();
if event_num == npc.event_num {
game_scene.boss_life_bar.set_npc_target(npc.id, &game_scene.npc_map);
break;
}
}
}
}
exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32);
}
OpCode::BOA => {
let action_num = read_cur_varint(&mut cursor)? as u16;
@ -1407,8 +1427,7 @@ impl TextScriptVM {
exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32);
}
// One operand codes
OpCode::BSL | OpCode::NUM |
OpCode::MPp | OpCode::SKm | OpCode::SKp |
OpCode::NUM | OpCode::MPp | OpCode::SKm | OpCode::SKp |
OpCode::UNJ | OpCode::MPJ | OpCode::XX1 | OpCode::SIL |
OpCode::SSS | OpCode::ACH | OpCode::S2MV => {
let par_a = read_cur_varint(&mut cursor)?;