You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

513 lines
17 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using Godot;
using RogueTank.Actors;
using RogueTank.Combat;
using RogueTank.Upgrades;
using RogueTank.Util;
using RogueTank.World;
namespace RogueTank;
public partial class Game : Node2D
{
private static readonly Vector2 ArenaHalfSize = new(250, 150);
// Scene roots
private Node2D _world = null!;
private Node2D _arena = null!;
private Node2D _actors = null!;
private Node2D _projectiles = null!;
private Node2D _pickups = null!;
private Camera2D _camera = null!;
// UI
private Label _hpLabel = null!;
private Label _levelLabel = null!;
private Label _coinLabel = null!;
private Label _waveLabel = null!;
private Label _hintLabel = null!;
private Control _pauseOverlay = null!;
private Button _resumeBtn = null!;
private Button _restartBtn = null!;
private Control _upgradeOverlay = null!;
private Button _up1 = null!;
private Button _up2 = null!;
private Button _up3 = null!;
private Control _gameOverOverlay = null!;
private Label _gameOverStats = null!;
private Button _gameOverRestartBtn = null!;
// Gameplay
private PlayerTank? _player;
private readonly PackedScene _bulletScene;
private readonly PackedScene _enemyScene;
private readonly PackedScene _pickupScene;
private readonly RandomNumberGenerator _rng = new();
private int _wave = 1;
private int _enemiesAlive;
private bool _paused;
private bool _choosingUpgrade;
private string _lastDebug = "";
private int _shotsFired;
private readonly List<UpgradeOption> _currentChoices = new();
public Game()
{
_bulletScene = MakeBulletScene();
_enemyScene = MakeEnemyScene();
_pickupScene = MakePickupScene();
}
public override void _Ready()
{
_rng.Randomize();
// IMPORTANT: "WhenPaused" will prevent the game from processing during normal gameplay.
// Keep the game running normally; only the UI should keep processing while paused.
ProcessMode = ProcessModeEnum.Pausable;
// Safety: ensure we start unpaused (some editor/debug states can leave the tree paused).
GetTree().Paused = false;
_world = GetNode<Node2D>("World");
_arena = GetNode<Node2D>("World/Arena");
_actors = GetNode<Node2D>("World/Actors");
_projectiles = GetNode<Node2D>("World/Projectiles");
_pickups = GetNode<Node2D>("World/Pickups");
_camera = GetNode<Camera2D>("World/Camera2D");
var canvas = GetNode<CanvasLayer>("CanvasLayer");
canvas.ProcessMode = ProcessModeEnum.WhenPaused;
GetNode<Control>("CanvasLayer/HUD").ProcessMode = ProcessModeEnum.WhenPaused;
_hpLabel = GetNode<Label>("CanvasLayer/HUD/TopBar/HBox/HpLabel");
_levelLabel = GetNode<Label>("CanvasLayer/HUD/TopBar/HBox/LevelLabel");
_coinLabel = GetNode<Label>("CanvasLayer/HUD/TopBar/HBox/CoinLabel");
_waveLabel = GetNode<Label>("CanvasLayer/HUD/TopBar/HBox/WaveLabel");
_hintLabel = GetNode<Label>("CanvasLayer/HUD/Hint");
_pauseOverlay = GetNode<Control>("CanvasLayer/HUD/PauseOverlay");
_resumeBtn = GetNode<Button>("CanvasLayer/HUD/PauseOverlay/PauseVBox/ResumeBtn");
_restartBtn = GetNode<Button>("CanvasLayer/HUD/PauseOverlay/PauseVBox/RestartBtn");
_upgradeOverlay = GetNode<Control>("CanvasLayer/HUD/UpgradeOverlay");
_up1 = GetNode<Button>("CanvasLayer/HUD/UpgradeOverlay/UpgradeVBox/UpgradeBtn1");
_up2 = GetNode<Button>("CanvasLayer/HUD/UpgradeOverlay/UpgradeVBox/UpgradeBtn2");
_up3 = GetNode<Button>("CanvasLayer/HUD/UpgradeOverlay/UpgradeVBox/UpgradeBtn3");
_gameOverOverlay = GetNode<Control>("CanvasLayer/HUD/GameOverOverlay");
_gameOverStats = GetNode<Label>("CanvasLayer/HUD/GameOverOverlay/GameOverVBox/GameOverStats");
_gameOverRestartBtn = GetNode<Button>("CanvasLayer/HUD/GameOverOverlay/GameOverVBox/GameOverRestartBtn");
_resumeBtn.Pressed += TogglePause;
_restartBtn.Pressed += Restart;
_gameOverRestartBtn.Pressed += Restart;
_up1.Pressed += () => ChooseUpgrade(0);
_up2.Pressed += () => ChooseUpgrade(1);
_up3.Pressed += () => ChooseUpgrade(2);
BuildArena();
SpawnPlayer();
StartWave(1);
UpdateHud();
GD.Print("RogueTank: C# Game._Ready OK");
_hintLabel.Text = "C# 已启动 | 如果键鼠无响应,请先点击游戏窗口获取焦点 | WASD移动 鼠标瞄准 左键射击 Esc暂停";
}
public override void _UnhandledInput(InputEvent @event)
{
if (@event.IsActionPressed("pause"))
{
TogglePause();
GetViewport().SetInputAsHandled();
}
}
public override void _Process(double delta)
{
if (_player == null || !IsInstanceValid(_player))
return;
_camera.GlobalPosition = _player.GlobalPosition;
// Lightweight input/paused debug (helps diagnose "can't move/shoot").
var v = _player.Velocity;
var inp = _player.DebugLastMoveInput;
var pos = _player.GlobalPosition;
var paused = GetTree().Paused;
var shoot = (InputMap.HasAction("shoot") && Input.IsActionPressed("shoot")) || Input.IsMouseButtonPressed(MouseButton.Left);
var dbg = $"Paused:{paused} In:{inp.X:0},{inp.Y:0} Vel:{v.X:0},{v.Y:0} Pos:{pos.X:0},{pos.Y:0} Shoot:{shoot} Shots:{_shotsFired} Bullets:{_projectiles.GetChildCount()}";
if (dbg != _lastDebug)
{
_lastDebug = dbg;
_hintLabel.Text = $"C# 已启动 | {dbg} | (先点窗口获取焦点) WASD移动 鼠标瞄准 左键射击 Esc暂停";
}
}
private void TogglePause()
{
if (_choosingUpgrade) return;
_paused = !_paused;
GetTree().Paused = _paused;
_pauseOverlay.Visible = _paused;
}
private void Restart()
{
GetTree().Paused = false;
GetTree().ReloadCurrentScene();
}
private void GameOver()
{
GetTree().Paused = true;
_pauseOverlay.Visible = false;
_upgradeOverlay.Visible = false;
_gameOverOverlay.Visible = true;
var coin = _player?.Coins ?? 0;
_gameOverStats.Text = $"Wave: {_wave} Coin: {coin}";
// Auto-restart after a short delay (timer runs while paused).
var t = GetTree().CreateTimer(1.2, processAlways: true);
t.Timeout += () =>
{
// If player already restarted, don't reload again.
if (!IsInstanceValid(_player))
Restart();
};
}
private void BuildArena()
{
_arena.QueueFreeChildren();
// Boundary walls as static bodies (simple rectangles).
MakeWall(new Vector2(0, -ArenaHalfSize.Y - 10), new Vector2(ArenaHalfSize.X * 2 + 60, 20));
MakeWall(new Vector2(0, ArenaHalfSize.Y + 10), new Vector2(ArenaHalfSize.X * 2 + 60, 20));
MakeWall(new Vector2(-ArenaHalfSize.X - 10, 0), new Vector2(20, ArenaHalfSize.Y * 2 + 60));
MakeWall(new Vector2(ArenaHalfSize.X + 10, 0), new Vector2(20, ArenaHalfSize.Y * 2 + 60));
// Random obstacles
for (int i = 0; i < 8; i++)
{
var pos = new Vector2(_rng.RandfRange(-ArenaHalfSize.X + 40, ArenaHalfSize.X - 40),
_rng.RandfRange(-ArenaHalfSize.Y + 30, ArenaHalfSize.Y - 30));
// Keep a clear spawn zone around the origin so the player isn't stuck inside walls.
if (pos.Length() < 80f)
{
i--;
continue;
}
var size = new Vector2(_rng.RandfRange(20, 60), _rng.RandfRange(14, 46));
MakeWall(pos, size, new Color(0.2f, 0.2f, 0.25f));
}
}
private void MakeWall(Vector2 pos, Vector2 size, Color? color = null)
{
var wall = new StaticBody2D { GlobalPosition = pos };
var col = new CollisionShape2D { Shape = new RectangleShape2D { Size = size } };
wall.AddChild(col);
var sprite = new Sprite2D
{
Texture = PixelArt.MakeSolidTexture(8, 8, color ?? new Color(0.16f, 0.16f, 0.18f)),
TextureFilter = CanvasItem.TextureFilterEnum.Nearest,
Centered = true,
Scale = size / 8f,
};
wall.AddChild(sprite);
_arena.AddChild(wall);
}
private void SpawnPlayer()
{
_player = new PlayerTank { GlobalPosition = Vector2.Zero };
_player.ProjectilesRoot = _projectiles;
_player.BulletScene = _bulletScene;
_player.PickupsRoot = _pickups;
_player.StatsChanged += UpdateHud;
_player.Died += GameOver;
_player.ShotFired += () => _shotsFired++;
_actors.AddChild(_player);
}
private void StartWave(int wave)
{
_wave = wave;
_waveLabel.Text = $"Wave: {_wave}";
var count = 6 + (_wave - 1) * 3;
_enemiesAlive = 0;
for (int i = 0; i < count; i++)
SpawnEnemy();
}
private void SpawnEnemy()
{
if (_player == null || !IsInstanceValid(_player))
return;
var e = _enemyScene.Instantiate<EnemyTank>();
var angle = _rng.RandfRange(0, Mathf.Tau);
var radius = _rng.RandfRange(120, 220);
e.GlobalPosition = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * radius;
e.Target = _player;
var hp = 26f + _wave * 6f;
e.MaxHp = hp;
e.MoveSpeed = 90f + _wave * 3f;
e.ContactDamage = 10f + _wave * 1.2f;
e.Died += OnEnemyDied;
_actors.AddChild(e);
_enemiesAlive++;
}
private void OnEnemyDied(EnemyTank who)
{
_enemiesAlive = Mathf.Max(0, _enemiesAlive - 1);
// Drops
Drop(Pickup.PickupType.Xp, _rng.RandiRange(4, 7), who.GlobalPosition);
if (_rng.Randf() < 0.55f)
Drop(Pickup.PickupType.Coin, _rng.RandiRange(1, 3), who.GlobalPosition + new Vector2(6, 0));
if (_rng.Randf() < 0.10f)
Drop(Pickup.PickupType.Heal, _rng.RandiRange(6, 12), who.GlobalPosition + new Vector2(-6, 0));
if (_player != null && IsInstanceValid(_player))
{
var leveled = _player.AddXp(_rng.RandiRange(1, 2)); // small bonus on kill
if (leveled && !_choosingUpgrade)
ShowUpgradeChoices("升级:等级提升");
}
if (_enemiesAlive <= 0 && !_choosingUpgrade)
ShowUpgradeChoices("通关奖励:选择一项升级");
}
private void Drop(Pickup.PickupType type, int amount, Vector2 at)
{
var p = _pickupScene.Instantiate<Pickup>();
p.Type = type;
p.Amount = amount;
p.GlobalPosition = at + new Vector2(_rng.RandfRange(-6, 6), _rng.RandfRange(-6, 6));
_pickups.AddChild(p);
}
private void ShowUpgradeChoices(string title)
{
if (_player == null || !IsInstanceValid(_player))
return;
_choosingUpgrade = true;
GetTree().Paused = true;
_pauseOverlay.Visible = false;
_upgradeOverlay.Visible = true;
var titleLabel = GetNode<Label>("CanvasLayer/HUD/UpgradeOverlay/UpgradeVBox/UpgradeTitle");
titleLabel.Text = title;
_currentChoices.Clear();
var choices = RollUpgrades(_player).Take(3).ToList();
_currentChoices.AddRange(choices);
ApplyToButton(_up1, choices.ElementAtOrDefault(0));
ApplyToButton(_up2, choices.ElementAtOrDefault(1));
ApplyToButton(_up3, choices.ElementAtOrDefault(2));
}
private void ApplyToButton(Button btn, UpgradeOption? opt)
{
if (opt == null)
{
btn.Disabled = true;
btn.Text = "---";
return;
}
btn.Disabled = false;
btn.Text = $"{opt.Title}\n{opt.Desc}";
}
private void ChooseUpgrade(int index)
{
if (index < 0 || index >= _currentChoices.Count) return;
var opt = _currentChoices[index];
opt.Apply();
_upgradeOverlay.Visible = false;
_choosingUpgrade = false;
GetTree().Paused = false;
UpdateHud();
if (_enemiesAlive <= 0)
{
// Next wave: rebuild arena a bit to simulate "随机关卡"
BuildArena();
StartWave(_wave + 1);
}
}
private IEnumerable<UpgradeOption> RollUpgrades(PlayerTank p)
{
// We allow repeats; keeps MVP simple.
var list = new List<UpgradeOption>
{
new()
{
Id = UpgradeId.MoveSpeed,
Title = "底盘升级:移速 +10%",
Desc = $"当前移速:{p.MoveSpeed:0}",
Apply = () => p.MoveSpeed *= 1.10f
},
new()
{
Id = UpgradeId.FireRate,
Title = "火炮升级:射速 +15%",
Desc = $"当前射速:{p.FireRate:0.0}/s",
Apply = () => p.FireRate *= 1.15f
},
new()
{
Id = UpgradeId.Damage,
Title = "火炮升级:伤害 +20%",
Desc = $"当前伤害:{p.Damage:0}",
Apply = () => p.Damage *= 1.20f
},
new()
{
Id = UpgradeId.MaxHp,
Title = "装甲升级:最大生命 +25",
Desc = $"当前生命:{p.Hp:0}/{p.MaxHp:0}",
Apply = () =>
{
p.MaxHp += 25f;
p.Heal(25f);
}
},
new()
{
Id = UpgradeId.Armor,
Title = "装甲升级:减伤 +1",
Desc = $"当前减伤:{p.Armor:0}",
Apply = () => p.Armor += 1f
},
new()
{
Id = UpgradeId.BulletSpeed,
Title = "弹种升级:弹速 +15%",
Desc = $"当前弹速:{p.BulletSpeed:0}",
Apply = () => p.BulletSpeed *= 1.15f
},
new()
{
Id = UpgradeId.BulletPierce,
Title = "弹种升级:穿透 +1",
Desc = $"当前穿透:{p.BulletPierce}",
Apply = () => p.BulletPierce += 1
},
new()
{
Id = UpgradeId.HealSmall,
Title = "维修:恢复 30 生命",
Desc = $"当前生命:{p.Hp:0}/{p.MaxHp:0}",
Apply = () => p.Heal(30f)
},
new()
{
Id = UpgradeId.Magnet,
Title = "辅助模块:拾取范围 +25%",
Desc = $"当前范围:{p.MagnetRadius:0}",
Apply = () => p.MagnetRadius *= 1.25f
}
};
// Random shuffle
return list.OrderBy(_ => _rng.Randf());
}
private void UpdateHud()
{
if (_player == null || !IsInstanceValid(_player))
return;
_hpLabel.Text = $"HP: {_player.Hp:0}/{_player.MaxHp:0}";
_levelLabel.Text = $"Lv {_player.Level} ({_player.Xp}/{_player.XpToNext})";
_coinLabel.Text = $"Coin: {_player.Coins}";
_waveLabel.Text = $"Wave: {_wave}";
}
// --- Runtime-built PackedScenes (keeps repo asset-light) ---
private static PackedScene MakeBulletScene()
{
var root = new Bullet();
root.Name = "Bullet";
var sprite = new Sprite2D
{
Texture = PixelArt.MakeBulletTexture(6, 2, new Color(1f, 0.95f, 0.65f)),
Centered = true,
TextureFilter = CanvasItem.TextureFilterEnum.Nearest
};
root.AddChild(sprite);
var col = new CollisionShape2D { Shape = new RectangleShape2D { Size = new Vector2(6, 2) } };
root.AddChild(col);
root.CollisionLayer = 1u << 2; // bullet
root.CollisionMask = (1u << 0) | (1u << 1); // walls + enemies
var ps = new PackedScene();
ps.Pack(root);
root.QueueFree();
return ps;
}
private static PackedScene MakeEnemyScene()
{
var root = new EnemyTank { Name = "Enemy" };
root.CollisionLayer = 1u << 1; // enemies
root.CollisionMask = (1u << 0) | (1u << 1) | (1u << 3); // walls + enemies + player
var ps = new PackedScene();
ps.Pack(root);
root.QueueFree();
return ps;
}
private static PackedScene MakePickupScene()
{
var root = new Pickup { Name = "Pickup" };
root.CollisionLayer = 1u << 4; // pickup
root.CollisionMask = (1u << 3) | (1u << 5); // player + magnet
var ps = new PackedScene();
ps.Pack(root);
root.QueueFree();
return ps;
}
}
internal static class NodeExtensions
{
public static void QueueFreeChildren(this Node node)
{
foreach (var c in node.GetChildren())
(c as Node)?.QueueFree();
}
}