接下來要讓戰鬥增加一些難度,主要是對角色有一些限制,還有在玩的時候可能會遇到一些異常狀態。所以想到的是角色不能無止盡地揮劍,在異常狀態下會有持續的壓力。不過也加入了一下東西在地圖上,放些資源讓玩家去權衡取捨。
這個章節預計導入三個核心要素:
src/components/stats.rs
新增 Stamina
:
#[derive(Component, Debug, Clone)]
pub struct Stamina {
pub current: f32,
pub max: f32,
pub regen_per_second: f32,
}
提供 spend()
、regen()
、refill()
等方法,呼叫端完全不用碰欄位。相關數值集中在 src/constants.rs
:
pub const PLAYER_MAX_STAMINA: f32 = 100.0;
pub const PLAYER_STAMINA_REGEN_PER_SECOND: f32 = 25.0;
pub const PLAYER_ATTACK_STAMINA_COST: f32 = 35.0;
玩家在 spawn_player
(src/systems/setup.rs
)時就帶著滿耐力,保證開場即可揮第一刀。
src/systems/attack.rs
的 attack_input_system
現在會先檢查耐力:
if !stamina.spend(PLAYER_ATTACK_STAMINA_COST) {
info!("耐力不足,攻擊動作取消。");
return;
}
也就是說,只要耐力不足,攻擊事件就不會送出去,武器也不會被重置計時,徹底避免空白鍵連發刷怪這個無腦攻擊。
player_stamina_regen_system
放在 src/systems/player_status.rs
,只要鬆開空白鍵(沒有持續攻擊),每秒回復 PLAYER_STAMINA_REGEN_PER_SECOND
。這讓戰鬥節奏變成「找到破綻 → 爆發 → 拉開距離喘氣」,比起無腦輸出更貼近魂系節奏,也讓地圖上的補給品有存在價值。
同一個檔案(stats.rs
)也加入 Poisoned
:
#[derive(Component, Debug)]
pub struct Poisoned {
pub tick_timer: Timer,
pub damage_per_tick: i32,
}
常數定義在 constants.rs
:
pub const PLAYER_POISON_TICK_SECONDS: f32 = 1.25;
pub const PLAYER_POISON_TICK_DAMAGE: i32 = 3;
player_poison_tick_system
每 1.25 秒觸發一次,處理邏輯集中在 src/systems/player_status.rs
:
if !poisoned.tick_timer.tick(time.delta()).just_finished() {
return;
}
let damage = poisoned.damage_per_tick;
health.current = (health.current - damage).max(0);
info!("中毒造成 {} 點傷害,玩家剩餘 HP: {}", damage, health.current);
這段程式碼是用上一篇的 PlayerDamagedEvent
,因此受傷動畫和 UI 已經自動支援。
在 player_respawn_system
(src/systems/health.rs
)中,復活時會補滿血、補滿耐力並移除 Poisoned
,避免帶著負面狀態重生。
另一方面,如果玩家手上有解毒藥水或使用 Debug 快捷鍵,也可以即時移除 Component,詳細在後面的拾取系統介紹。
系統邏輯更新後,資訊也要跟上。所以在 src/systems/ui.rs
大幅調整:
PlayerStaminaUiRoot
放在血條下方,綠色長條即時顯示耐力占比。PlayerStatusText
顯示 ASCII 訊息:
POISON: HP -3 per tick | Use [G] antidote to cleanse
。Space drains stamina, release to recover | [T] refill [Y] apply poison
。update_player_stats_panel
追加 stamina line 以及 STATUS
欄位,讓測試人員可以直接看到目前是否中毒、攻擊一次耗掉多少耐力。整個 UI 需要把提示換成 ASCII,否則會有字型缺字的情況。
player_status_debug_shortcuts_system
(src/systems/player_status.rs
)提供三個常用指令:
T
:stamina.refill()
,模擬喝耐力藥水。G
:移除 Poisoned
,當成解毒藥水。Y
:如果目前沒有中毒,就加上 Poisoned::new(...)
。當玩家已經中毒時再按一次會提示「請先使用解毒藥水」。這組指令能搭配 UI 立刻驗證各種狀態,方便在遊戲中測試,但最後這個要記得拿掉。
模擬中毒以及移除中毒狀態。
有了耐力與中毒,接下來就是補給系統。把三種藥水的圖片放進 assets/items/potions/
:
health.png
:紅藥水補 40 HP。stamina.png
:綠藥水補 60 Stamina。toxic.png
:灰藥水解除 Poisoned。(藍色回魔留給未來的 MP 系統。)
src/components/items.rs
定義 Pickup
與 PickupEffect
,src/plugins/items.rs
插入新的 ItemPlugin
,在 PostStartup
階段呼叫 spawn_random_pickups
,並在 Update
階段執行 player_pickup_detection_system
。
spawn_random_pickups
(src/systems/items.rs
)會:
RoomTile
,挑出室內與戶外地板。ITEM_RANDOM_PICKUP_COUNT
(現在是 6 個)。AssetServer
載入。ITEM_PICKUP_Z_OFFSET
讓道具懸浮一點點,看起來比較像在地上。常數全部寫在 constants.rs
,修改起來很直觀。
player_pickup_detection_system
每禎檢查玩家與所有 Pickup
的距離,小於 ITEM_PICKUP_DISTANCE
就觸發效果並 despawn:
match &pickup.effect {
PickupEffect::Heal(amount) => {
let before = health.current;
health.current = (health.current + amount).min(health.max);
info!("拾取紅色藥水:HP {} -> {}", before, health.current);
}
PickupEffect::RestoreStamina(amount) => {
if let Some(stamina_ref) = stamina.as_mut() {
let before = stamina_ref.current;
stamina_ref.current = (stamina_ref.current + amount).min(stamina_ref.max);
info!("拾取綠色藥水:耐力 {:.1} -> {:.1}", before, stamina_ref.current);
}
}
PickupEffect::CurePoison => {
if poison_state.is_some() {
commands.entity(player_entity).remove::<Poisoned>();
info!("拾取解毒藥水:中毒狀態已解除");
} else {
info!("拾取解毒藥水:目前沒有中毒狀態");
}
}
}
使用者完全不用按鍵,「走過去就撿」;同時 log 會顯示前後差值,方便調整補量。之後如果要導入背包,也可以把 despawn 改成發送事件。
中毒狀態下撿到藥水解除狀態
有了耐力、異常狀態與補給,戰鬥步調和資源管理算是建立起來。接下來的重點是讓遊戲進入真正的「探索 → 戰鬥 → 恢復 → 前進」迴圈,玩家現在必須注意資源、規劃路線,也有更多可視化回饋。接下來只要把掉落、寶箱、裝備串起來,就能構成完整的 Rogue-lite 框架。
今日程式碼同步至 repo