12 void DefenderWindow::configure_console() {
13 console.attach(*
this, asset_root +
"/data/font.ttf", HUD_FONT_SIZE);
14 console.setSpriteYOriginTopLeft(
true);
15 console.setPrompt(
"defender> ");
17 console.printLine(
"Press F3 to open/close the console.");
18 console.printLine(
"Type 'help' for Defender commands.");
19 log_game(
"Console attached.");
20 log_game(
"Defender initialized.");
21 console.setCommandCallback([
this](mxvk::VK_Window &,
const std::vector<std::string> &args, std::ostream &out) {
return handle_console_command(args, out); });
24 bool DefenderWindow::handle_console_command(
const std::vector<std::string> &args, std::ostream &out) {
29 const std::string &cmd = args.front();
31 out <<
"Defender commands:\n"
32 <<
" clear Clear console output\n"
33 <<
" echo <text> Print text to the console\n"
34 <<
" status Print current game state\n"
35 <<
" controls Print keyboard and controller bindings\n"
36 <<
" restart Restart the game and countdown\n"
37 <<
" intro Return to the intro screen\n"
38 <<
" play Start playing immediately\n"
39 <<
" gameover Force game over\n"
40 <<
" lives <count> Set remaining lives\n"
41 <<
" score <points> Set score\n"
42 <<
" killship Destroy the ship once\n"
43 <<
" clear_enemies Remove active UFOs and asteroids\n"
44 <<
" spawn_ufo Spawn one UFO near the camera\n"
45 <<
" spawn_asteroid [scale] Spawn one asteroid near the camera\n"
46 <<
" about Print program banner\n"
47 <<
" quit / exit Close the window\n";
52 for (std::size_t i = 1; i < args.size(); ++i) {
61 if (cmd ==
"controls") {
62 out <<
"Keyboard: Z thrust, D boost, X reverse, Up/W and Down move, Space fires, A/S roll.\n"
63 <<
"Controller: Left stick or D-pad moves, Right Trigger thrusts, East boosts, Left Trigger or West reverses,\n"
64 <<
" South fires, Left/Right Shoulder rolls, Start skips/restarts, Back quits.";
68 if (cmd ==
"status") {
69 out <<
"Mode: " << mode_name() <<
'\n' <<
"Score: " << score <<
'\n' <<
"Lives: " << lives <<
'\n' <<
"Level: " << level <<
'\n' <<
"Game over: " << (game_over ?
"yes" :
"no") <<
'\n' <<
"Ship respawning: " << (ship_respawning ?
"yes" :
"no") <<
'\n' <<
"Controller: " << (controller.active() ? controller.name() :
"Disconnected") <<
'\n' <<
"Ship position: " << format_vec3(ship.position) <<
'\n' <<
"Active UFOs: " << active_ufo_count() <<
'\n' <<
"Active asteroids: " << active_asteroid_count() <<
'\n' <<
"Projectiles: " << active_projectile_count() <<
'\n';
73 if (cmd ==
"restart") {
75 log_game(
"Game restarted from console.");
76 out <<
"Game restarted.";
85 ship_respawning =
false;
86 reset_ship_to_origin();
88 log_game(
"Returned to intro screen from console.");
89 out <<
"Intro screen active.";
96 ship_respawning =
false;
102 log_game(
"Play mode activated from console.");
107 if (cmd ==
"gameover") {
110 ship_respawning =
false;
111 ship.visible =
false;
112 ship.velocity = glm::vec3(0.0f);
113 ship.current_speed = 0.0f;
115 log_game(
"Game over forced from console.", SDL_Color{255, 120, 80, 255});
116 out <<
"Game over forced.";
120 if (cmd ==
"lives") {
122 if (!parse_int_arg(args, 1,
"lives", value, out)) {
125 lives = std::clamp(value, 0, 99);
130 log_game(std::format(
"Lives set to {} from console.", lives));
131 out <<
"Lives set to " << lives <<
'.';
135 if (cmd ==
"score") {
137 if (!parse_int_arg(args, 1,
"score", value, out)) {
140 score = std::max(0, value);
141 log_game(std::format(
"Score set to {} from console.", score));
142 out <<
"Score set to " << score <<
'.';
146 if (cmd ==
"killship") {
148 out <<
"Ship is already destroyed.";
151 spawn_ship_explosion(ship.position);
153 log_game(
"Ship destroyed from console.", SDL_Color{255, 120, 80, 255});
154 out <<
"Ship destroyed.";
158 if (cmd ==
"clear_enemies") {
159 const int cleared_ufos = active_ufo_count();
160 const int cleared_asteroids = active_asteroid_count();
161 for (
auto &ufo : ufos) {
165 for (
auto &asteroid : asteroids) {
166 asteroid.active =
false;
169 log_game(std::format(
"Cleared {} UFO(s) and {} asteroid(s) from console.", cleared_ufos, cleared_asteroids));
170 out <<
"Enemies cleared.";
174 if (cmd ==
"spawn_ufo") {
175 Ufo *ufo = find_inactive_ufo();
176 if (ufo ==
nullptr) {
177 out <<
"No free UFO slots.";
181 log_game(std::format(
"UFO spawned from console at {}.", format_vec3(ufo->position)));
182 out <<
"UFO spawned.";
186 if (cmd ==
"spawn_asteroid") {
188 if (args.size() > 1 && !parse_float_arg(args, 1,
"scale", scale, out)) {
191 Asteroid *asteroid = find_inactive_asteroid();
192 if (asteroid ==
nullptr) {
193 out <<
"No free asteroid slots.";
196 respawn_asteroid(*asteroid);
197 asteroid->scale = std::clamp(scale, 0.25f, 4.0f);
198 asteroid->collision_radius = asteroid->scale * 1.25f;
199 log_game(std::format(
"Asteroid spawned from console at {} scale {:.2f}.", format_vec3(asteroid->position), asteroid->scale));
200 out <<
"Asteroid spawned.";
204 if (cmd ==
"about") {
205 out <<
"defender: MXVK side-scrolling starfield shooter.\n";
209 if (cmd ==
"quit" || cmd ==
"exit") {
210 log_game(
"Exit requested from console.");
211 out <<
"Closing window...";
219 void DefenderWindow::log_game(
const std::string &message, SDL_Color color) {
220 if (!console_ready) {
223 console.printLine(
"[game] " + message, color);
226 [[nodiscard]]
const char *DefenderWindow::mode_name()
const {
231 return "intro_fade_in";
240 [[nodiscard]] std::string DefenderWindow::format_vec3(
const glm::vec3 &value) {
return std::format(
"({:.1f}, {:.1f}, {:.1f})", value.x, value.y, value.z); }
242 bool DefenderWindow::parse_int_arg(
const std::vector<std::string> &args, std::size_t index,
const char *name,
int &value, std::ostream &out)
const {
243 if (args.size() <= index) {
244 out <<
"Missing " << name <<
" value.";
248 std::size_t parsed = 0;
249 const int parsed_value = std::stoi(args[index], &parsed);
250 if (parsed != args[index].size()) {
251 out <<
"Invalid " << name <<
" value: " << args[index];
254 value = parsed_value;
257 out <<
"Invalid " << name <<
" value: " << args[index];
262 bool DefenderWindow::parse_float_arg(
const std::vector<std::string> &args, std::size_t index,
const char *name,
float &value, std::ostream &out)
const {
263 if (args.size() <= index) {
264 out <<
"Missing " << name <<
" value.";
268 std::size_t parsed = 0;
269 const float parsed_value = std::stof(args[index], &parsed);
270 if (parsed != args[index].size()) {
271 out <<
"Invalid " << name <<
" value: " << args[index];
274 value = parsed_value;
277 out <<
"Invalid " << name <<
" value: " << args[index];
282 [[nodiscard]]
int DefenderWindow::active_ufo_count()
const {
284 for (
const auto &ufo : ufos) {
292 [[nodiscard]]
int DefenderWindow::active_asteroid_count()
const {
294 for (
const auto &asteroid : asteroids) {
302 [[nodiscard]]
int DefenderWindow::active_projectile_count()
const {
304 for (
const auto &projectile : projectiles) {
305 if (projectile.active) {
312 Ufo *DefenderWindow::find_inactive_ufo() {
313 for (
auto &ufo : ufos) {
321 Asteroid *DefenderWindow::find_inactive_asteroid() {
322 for (
auto &asteroid : asteroids) {
330 void DefenderWindow::clear_input_state() {
331 reverse_pressed =
false;
332 propulsion_pressed =
false;
334 down_pressed =
false;
335 fire_pressed =
false;
336 roll_left_pressed =
false;
337 roll_right_pressed =
false;
338 boost_pressed =
false;
339 controller_propulsion_pressed =
false;
340 controller_boost_pressed =
false;
341 controller_roll_left_pressed =
false;
342 controller_roll_right_pressed =
false;
343 controller_reverse_trigger_pressed =
false;
344 controller_vertical_input = 0.0f;
347 bool DefenderWindow::open_controller() {
349 if (controller.open(index)) {
350 log_game(
"Controller connected: " + controller.name());
357 void DefenderWindow::sync_controller_connection() {
358 if (!controller.active()) {
363 float DefenderWindow::controller_axis(SDL_GamepadAxis axis)
const {
364 if (!controller.active()) {
368 const Sint16 raw_value = controller.getAxis(axis);
369 const float magnitude =
static_cast<float>(std::abs(
static_cast<int>(raw_value)));
375 return raw_value < 0 ? -normalized : normalized;
378 void DefenderWindow::update_controller_input() {
379 if (!controller.active()) {
380 controller_propulsion_pressed =
false;
381 controller_boost_pressed =
false;
382 controller_roll_left_pressed =
false;
383 controller_roll_right_pressed =
false;
384 controller_reverse_trigger_pressed =
false;
385 controller_vertical_input = 0.0f;
389 controller_vertical_input = -controller_axis(SDL_GAMEPAD_AXIS_LEFTY);
390 if (controller.getButton(SDL_GAMEPAD_BUTTON_DPAD_UP)) {
391 controller_vertical_input = 1.0f;
392 }
else if (controller.getButton(SDL_GAMEPAD_BUTTON_DPAD_DOWN)) {
393 controller_vertical_input = -1.0f;
396 controller_propulsion_pressed = controller.getAxis(SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) >
CONTROLLER_DEAD_ZONE;
397 controller_boost_pressed = controller.getButton(SDL_GAMEPAD_BUTTON_EAST);
398 controller_roll_left_pressed = controller.getButton(SDL_GAMEPAD_BUTTON_LEFT_SHOULDER);
399 controller_roll_right_pressed = controller.getButton(SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER);
401 const bool reverse_trigger_pressed = controller.getAxis(SDL_GAMEPAD_AXIS_LEFT_TRIGGER) >
CONTROLLER_DEAD_ZONE;
402 if (reverse_trigger_pressed && !controller_reverse_trigger_pressed && mode ==
GameMode::Playing && !game_over && !ship_respawning) {
403 reverse_pressed =
true;
405 controller_reverse_trigger_pressed = reverse_trigger_pressed;
static int joysticks()
Return the number of connected joysticks/controllers.
void exit()
Request loop termination.
constexpr float CONTROLLER_AXIS_MAX
constexpr Sint16 CONTROLLER_DEAD_ZONE
float random_float(float min_value, float max_value)
Generates a uniformly distributed floating-point value.