MXVK Vulkan Framework 0.24.0
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
defender_console.cpp
Go to the documentation of this file.
1#include "defender_window.hpp"
2
3#include <algorithm>
4#include <cmath>
5#include <format>
6#include <ostream>
7#include <string>
8#include <vector>
9
10namespace defender {
11
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> ");
16 console_ready = true;
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) {
22 return handle_console_command(args, out);
23 });
24 }
25
26 bool DefenderWindow::handle_console_command(const std::vector<std::string> &args, std::ostream &out) {
27 if (args.empty()) {
28 return true;
29 }
30
31 const std::string &cmd = args.front();
32 if (cmd == "help") {
33 out << "Defender commands:\n"
34 << " clear Clear console output\n"
35 << " echo <text> Print text to the console\n"
36 << " status Print current game state\n"
37 << " controls Print keyboard and controller bindings\n"
38 << " restart Restart the game and countdown\n"
39 << " intro Return to the intro screen\n"
40 << " play Start playing immediately\n"
41 << " gameover Force game over\n"
42 << " lives <count> Set remaining lives\n"
43 << " score <points> Set score\n"
44 << " killship Destroy the ship once\n"
45 << " clear_enemies Remove active UFOs and asteroids\n"
46 << " spawn_ufo Spawn one UFO near the camera\n"
47 << " spawn_asteroid [scale] Spawn one asteroid near the camera\n"
48 << " about Print program banner\n"
49 << " quit / exit Close the window\n";
50 return true;
51 }
52
53 if (cmd == "echo") {
54 for (std::size_t i = 1; i < args.size(); ++i) {
55 if (i > 1) {
56 out << ' ';
57 }
58 out << args[i];
59 }
60 return true;
61 }
62
63 if (cmd == "controls") {
64 out << "Keyboard: Z thrust, D boost, X reverse, Up/W and Down move, Space fires, A/S roll.\n"
65 << "Controller: Left stick or D-pad moves, Right Trigger thrusts, East boosts, Left Trigger or West reverses,\n"
66 << " South fires, Left/Right Shoulder rolls, Start skips/restarts, Back quits.";
67 return true;
68 }
69
70 if (cmd == "status") {
71 out << "Mode: " << mode_name() << '\n'
72 << "Score: " << score << '\n'
73 << "Lives: " << lives << '\n'
74 << "Level: " << level << '\n'
75 << "Game over: " << (game_over ? "yes" : "no") << '\n'
76 << "Ship respawning: " << (ship_respawning ? "yes" : "no") << '\n'
77 << "Controller: " << (controller.active() ? controller.name() : "Disconnected") << '\n'
78 << "Ship position: " << format_vec3(ship.position) << '\n'
79 << "Active UFOs: " << active_ufo_count() << '\n'
80 << "Active asteroids: " << active_asteroid_count() << '\n'
81 << "Projectiles: " << active_projectile_count() << '\n';
82 return true;
83 }
84
85 if (cmd == "restart") {
86 restart_game();
87 log_game("Game restarted from console.");
88 out << "Game restarted.";
89 return true;
90 }
91
92 if (cmd == "intro") {
93 clear_projectiles();
94 clear_particles();
95 clear_input_state();
96 game_over = false;
97 ship_respawning = false;
98 reset_ship_to_origin();
99 reset_intro_screen();
100 log_game("Returned to intro screen from console.");
101 out << "Intro screen active.";
102 return true;
103 }
104
105 if (cmd == "play") {
106 clear_input_state();
107 game_over = false;
108 ship_respawning = false;
109 mode = GameMode::Playing;
110 ship.visible = true;
111 if (!level_active) {
112 start_level();
113 }
114 log_game("Play mode activated from console.");
115 out << "Playing.";
116 return true;
117 }
118
119 if (cmd == "gameover") {
120 game_over = true;
121 mode = GameMode::Playing;
122 ship_respawning = false;
123 ship.visible = false;
124 ship.velocity = glm::vec3(0.0f);
125 ship.current_speed = 0.0f;
126 clear_projectiles();
127 log_game("Game over forced from console.", SDL_Color{255, 120, 80, 255});
128 out << "Game over forced.";
129 return true;
130 }
131
132 if (cmd == "lives") {
133 int value = 0;
134 if (!parse_int_arg(args, 1, "lives", value, out)) {
135 return true;
136 }
137 lives = std::clamp(value, 0, 99);
138 if (lives > 0) {
139 game_over = false;
140 ship.visible = true;
141 }
142 log_game(std::format("Lives set to {} from console.", lives));
143 out << "Lives set to " << lives << '.';
144 return true;
145 }
146
147 if (cmd == "score") {
148 int value = 0;
149 if (!parse_int_arg(args, 1, "score", value, out)) {
150 return true;
151 }
152 score = std::max(0, value);
153 log_game(std::format("Score set to {} from console.", score));
154 out << "Score set to " << score << '.';
155 return true;
156 }
157
158 if (cmd == "killship") {
159 if (game_over) {
160 out << "Ship is already destroyed.";
161 return true;
162 }
163 spawn_ship_explosion(ship.position);
164 lose_life();
165 log_game("Ship destroyed from console.", SDL_Color{255, 120, 80, 255});
166 out << "Ship destroyed.";
167 return true;
168 }
169
170 if (cmd == "clear_enemies") {
171 const int cleared_ufos = active_ufo_count();
172 const int cleared_asteroids = active_asteroid_count();
173 for (auto &ufo : ufos) {
174 ufo.active = false;
175 ufo.respawn_timer = space::random_float(0.8f, 3.0f);
176 }
177 for (auto &asteroid : asteroids) {
178 asteroid.active = false;
179 asteroid.respawn_timer = space::random_float(0.8f, 3.0f);
180 }
181 log_game(std::format("Cleared {} UFO(s) and {} asteroid(s) from console.", cleared_ufos, cleared_asteroids));
182 out << "Enemies cleared.";
183 return true;
184 }
185
186 if (cmd == "spawn_ufo") {
187 Ufo *ufo = find_inactive_ufo();
188 if (ufo == nullptr) {
189 out << "No free UFO slots.";
190 return true;
191 }
192 respawn_ufo(*ufo);
193 log_game(std::format("UFO spawned from console at {}.", format_vec3(ufo->position)));
194 out << "UFO spawned.";
195 return true;
196 }
197
198 if (cmd == "spawn_asteroid") {
199 float scale = space::random_float(0.72f, 2.15f);
200 if (args.size() > 1 && !parse_float_arg(args, 1, "scale", scale, out)) {
201 return true;
202 }
203 Asteroid *asteroid = find_inactive_asteroid();
204 if (asteroid == nullptr) {
205 out << "No free asteroid slots.";
206 return true;
207 }
208 respawn_asteroid(*asteroid);
209 asteroid->scale = std::clamp(scale, 0.25f, 4.0f);
210 asteroid->collision_radius = asteroid->scale * 1.25f;
211 log_game(std::format("Asteroid spawned from console at {} scale {:.2f}.", format_vec3(asteroid->position), asteroid->scale));
212 out << "Asteroid spawned.";
213 return true;
214 }
215
216 if (cmd == "about") {
217 out << "defender: MXVK side-scrolling starfield shooter.\n";
218 return true;
219 }
220
221 if (cmd == "quit" || cmd == "exit") {
222 log_game("Exit requested from console.");
223 out << "Closing window...";
224 exit();
225 return true;
226 }
227
228 return false;
229 }
230
231 void DefenderWindow::log_game(const std::string &message, SDL_Color color) {
232 if (!console_ready) {
233 return;
234 }
235 console.printLine("[game] " + message, color);
236 }
237
238 [[nodiscard]] const char *DefenderWindow::mode_name() const {
239 switch (mode) {
240 case GameMode::Intro:
241 return "intro";
243 return "intro_fade_in";
245 return "countdown";
247 return "playing";
248 }
249 return "unknown";
250 }
251
252 [[nodiscard]] std::string DefenderWindow::format_vec3(const glm::vec3 &value) {
253 return std::format("({:.1f}, {:.1f}, {:.1f})", value.x, value.y, value.z);
254 }
255
256 bool DefenderWindow::parse_int_arg(const std::vector<std::string> &args, std::size_t index, const char *name, int &value, std::ostream &out) const {
257 if (args.size() <= index) {
258 out << "Missing " << name << " value.";
259 return false;
260 }
261 try {
262 std::size_t parsed = 0;
263 const int parsed_value = std::stoi(args[index], &parsed);
264 if (parsed != args[index].size()) {
265 out << "Invalid " << name << " value: " << args[index];
266 return false;
267 }
268 value = parsed_value;
269 return true;
270 } catch (...) {
271 out << "Invalid " << name << " value: " << args[index];
272 return false;
273 }
274 }
275
276 bool DefenderWindow::parse_float_arg(const std::vector<std::string> &args, std::size_t index, const char *name, float &value, std::ostream &out) const {
277 if (args.size() <= index) {
278 out << "Missing " << name << " value.";
279 return false;
280 }
281 try {
282 std::size_t parsed = 0;
283 const float parsed_value = std::stof(args[index], &parsed);
284 if (parsed != args[index].size()) {
285 out << "Invalid " << name << " value: " << args[index];
286 return false;
287 }
288 value = parsed_value;
289 return true;
290 } catch (...) {
291 out << "Invalid " << name << " value: " << args[index];
292 return false;
293 }
294 }
295
296 [[nodiscard]] int DefenderWindow::active_ufo_count() const {
297 int count = 0;
298 for (const auto &ufo : ufos) {
299 if (ufo.active) {
300 ++count;
301 }
302 }
303 return count;
304 }
305
306 [[nodiscard]] int DefenderWindow::active_asteroid_count() const {
307 int count = 0;
308 for (const auto &asteroid : asteroids) {
309 if (asteroid.active) {
310 ++count;
311 }
312 }
313 return count;
314 }
315
316 [[nodiscard]] int DefenderWindow::active_projectile_count() const {
317 int count = 0;
318 for (const auto &projectile : projectiles) {
319 if (projectile.active) {
320 ++count;
321 }
322 }
323 return count;
324 }
325
326 Ufo *DefenderWindow::find_inactive_ufo() {
327 for (auto &ufo : ufos) {
328 if (!ufo.active) {
329 return &ufo;
330 }
331 }
332 return nullptr;
333 }
334
335 Asteroid *DefenderWindow::find_inactive_asteroid() {
336 for (auto &asteroid : asteroids) {
337 if (!asteroid.active) {
338 return &asteroid;
339 }
340 }
341 return nullptr;
342 }
343
344 void DefenderWindow::clear_input_state() {
345 reverse_pressed = false;
346 propulsion_pressed = false;
347 up_pressed = false;
348 down_pressed = false;
349 fire_pressed = false;
350 roll_left_pressed = false;
351 roll_right_pressed = false;
352 boost_pressed = false;
353 controller_propulsion_pressed = false;
354 controller_boost_pressed = false;
355 controller_roll_left_pressed = false;
356 controller_roll_right_pressed = false;
357 controller_reverse_trigger_pressed = false;
358 controller_vertical_input = 0.0f;
359 }
360
361 bool DefenderWindow::open_controller() {
362 for (int index = 0; index < mxvk::VK_Controller::joysticks(); ++index) {
363 if (controller.open(index)) {
364 log_game("Controller connected: " + controller.name());
365 return true;
366 }
367 }
368 return false;
369 }
370
371 void DefenderWindow::sync_controller_connection() {
372 if (!controller.active()) {
373 open_controller();
374 }
375 }
376
377 float DefenderWindow::controller_axis(SDL_GamepadAxis axis) const {
378 if (!controller.active()) {
379 return 0.0f;
380 }
381
382 const Sint16 raw_value = controller.getAxis(axis);
383 const float magnitude = static_cast<float>(std::abs(static_cast<int>(raw_value)));
384 if (magnitude <= static_cast<float>(CONTROLLER_DEAD_ZONE)) {
385 return 0.0f;
386 }
387
388 const float normalized = std::clamp((magnitude - static_cast<float>(CONTROLLER_DEAD_ZONE)) /
389 (CONTROLLER_AXIS_MAX - static_cast<float>(CONTROLLER_DEAD_ZONE)),
390 0.0f,
391 1.0f);
392 return raw_value < 0 ? -normalized : normalized;
393 }
394
395 void DefenderWindow::update_controller_input() {
396 if (!controller.active()) {
397 controller_propulsion_pressed = false;
398 controller_boost_pressed = false;
399 controller_roll_left_pressed = false;
400 controller_roll_right_pressed = false;
401 controller_reverse_trigger_pressed = false;
402 controller_vertical_input = 0.0f;
403 return;
404 }
405
406 controller_vertical_input = -controller_axis(SDL_GAMEPAD_AXIS_LEFTY);
407 if (controller.getButton(SDL_GAMEPAD_BUTTON_DPAD_UP)) {
408 controller_vertical_input = 1.0f;
409 } else if (controller.getButton(SDL_GAMEPAD_BUTTON_DPAD_DOWN)) {
410 controller_vertical_input = -1.0f;
411 }
412
413 controller_propulsion_pressed = controller.getAxis(SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > CONTROLLER_DEAD_ZONE;
414 controller_boost_pressed = controller.getButton(SDL_GAMEPAD_BUTTON_EAST);
415 controller_roll_left_pressed = controller.getButton(SDL_GAMEPAD_BUTTON_LEFT_SHOULDER);
416 controller_roll_right_pressed = controller.getButton(SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER);
417
418 const bool reverse_trigger_pressed = controller.getAxis(SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > CONTROLLER_DEAD_ZONE;
419 if (reverse_trigger_pressed && !controller_reverse_trigger_pressed && mode == GameMode::Playing && !game_over && !ship_respawning) {
420 reverse_pressed = true;
421 }
422 controller_reverse_trigger_pressed = reverse_trigger_pressed;
423 }
424
425} // namespace defender
static int joysticks()
Return the number of connected joysticks/controllers.
void exit()
Request loop termination.
Definition mxvk.cpp:1126
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.
bool active
Definition space.cpp:69