MXVK Vulkan Framework 0.35.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) { return handle_console_command(args, out); });
22 }
23
24 bool DefenderWindow::handle_console_command(const std::vector<std::string> &args, std::ostream &out) {
25 if (args.empty()) {
26 return true;
27 }
28
29 const std::string &cmd = args.front();
30 if (cmd == "help") {
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";
48 return true;
49 }
50
51 if (cmd == "echo") {
52 for (std::size_t i = 1; i < args.size(); ++i) {
53 if (i > 1) {
54 out << ' ';
55 }
56 out << args[i];
57 }
58 return true;
59 }
60
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.";
65 return true;
66 }
67
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';
70 return true;
71 }
72
73 if (cmd == "restart") {
74 restart_game();
75 log_game("Game restarted from console.");
76 out << "Game restarted.";
77 return true;
78 }
79
80 if (cmd == "intro") {
81 clear_projectiles();
82 clear_particles();
83 clear_input_state();
84 game_over = false;
85 ship_respawning = false;
86 reset_ship_to_origin();
87 reset_intro_screen();
88 log_game("Returned to intro screen from console.");
89 out << "Intro screen active.";
90 return true;
91 }
92
93 if (cmd == "play") {
94 clear_input_state();
95 game_over = false;
96 ship_respawning = false;
97 mode = GameMode::Playing;
98 ship.visible = true;
99 if (!level_active) {
100 start_level();
101 }
102 log_game("Play mode activated from console.");
103 out << "Playing.";
104 return true;
105 }
106
107 if (cmd == "gameover") {
108 game_over = true;
109 mode = GameMode::Playing;
110 ship_respawning = false;
111 ship.visible = false;
112 ship.velocity = glm::vec3(0.0f);
113 ship.current_speed = 0.0f;
114 clear_projectiles();
115 log_game("Game over forced from console.", SDL_Color{255, 120, 80, 255});
116 out << "Game over forced.";
117 return true;
118 }
119
120 if (cmd == "lives") {
121 int value = 0;
122 if (!parse_int_arg(args, 1, "lives", value, out)) {
123 return true;
124 }
125 lives = std::clamp(value, 0, 99);
126 if (lives > 0) {
127 game_over = false;
128 ship.visible = true;
129 }
130 log_game(std::format("Lives set to {} from console.", lives));
131 out << "Lives set to " << lives << '.';
132 return true;
133 }
134
135 if (cmd == "score") {
136 int value = 0;
137 if (!parse_int_arg(args, 1, "score", value, out)) {
138 return true;
139 }
140 score = std::max(0, value);
141 log_game(std::format("Score set to {} from console.", score));
142 out << "Score set to " << score << '.';
143 return true;
144 }
145
146 if (cmd == "killship") {
147 if (game_over) {
148 out << "Ship is already destroyed.";
149 return true;
150 }
151 spawn_ship_explosion(ship.position);
152 lose_life();
153 log_game("Ship destroyed from console.", SDL_Color{255, 120, 80, 255});
154 out << "Ship destroyed.";
155 return true;
156 }
157
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) {
162 ufo.active = false;
163 ufo.respawn_timer = space::random_float(0.8f, 3.0f);
164 }
165 for (auto &asteroid : asteroids) {
166 asteroid.active = false;
167 asteroid.respawn_timer = space::random_float(0.8f, 3.0f);
168 }
169 log_game(std::format("Cleared {} UFO(s) and {} asteroid(s) from console.", cleared_ufos, cleared_asteroids));
170 out << "Enemies cleared.";
171 return true;
172 }
173
174 if (cmd == "spawn_ufo") {
175 Ufo *ufo = find_inactive_ufo();
176 if (ufo == nullptr) {
177 out << "No free UFO slots.";
178 return true;
179 }
180 respawn_ufo(*ufo);
181 log_game(std::format("UFO spawned from console at {}.", format_vec3(ufo->position)));
182 out << "UFO spawned.";
183 return true;
184 }
185
186 if (cmd == "spawn_asteroid") {
187 float scale = space::random_float(0.72f, 2.15f);
188 if (args.size() > 1 && !parse_float_arg(args, 1, "scale", scale, out)) {
189 return true;
190 }
191 Asteroid *asteroid = find_inactive_asteroid();
192 if (asteroid == nullptr) {
193 out << "No free asteroid slots.";
194 return true;
195 }
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.";
201 return true;
202 }
203
204 if (cmd == "about") {
205 out << "defender: MXVK side-scrolling starfield shooter.\n";
206 return true;
207 }
208
209 if (cmd == "quit" || cmd == "exit") {
210 log_game("Exit requested from console.");
211 out << "Closing window...";
212 exit();
213 return true;
214 }
215
216 return false;
217 }
218
219 void DefenderWindow::log_game(const std::string &message, SDL_Color color) {
220 if (!console_ready) {
221 return;
222 }
223 console.printLine("[game] " + message, color);
224 }
225
226 [[nodiscard]] const char *DefenderWindow::mode_name() const {
227 switch (mode) {
228 case GameMode::Intro:
229 return "intro";
231 return "intro_fade_in";
233 return "countdown";
235 return "playing";
236 }
237 return "unknown";
238 }
239
240 [[nodiscard]] std::string DefenderWindow::format_vec3(const glm::vec3 &value) { return std::format("({:.1f}, {:.1f}, {:.1f})", value.x, value.y, value.z); }
241
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.";
245 return false;
246 }
247 try {
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];
252 return false;
253 }
254 value = parsed_value;
255 return true;
256 } catch (...) {
257 out << "Invalid " << name << " value: " << args[index];
258 return false;
259 }
260 }
261
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.";
265 return false;
266 }
267 try {
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];
272 return false;
273 }
274 value = parsed_value;
275 return true;
276 } catch (...) {
277 out << "Invalid " << name << " value: " << args[index];
278 return false;
279 }
280 }
281
282 [[nodiscard]] int DefenderWindow::active_ufo_count() const {
283 int count = 0;
284 for (const auto &ufo : ufos) {
285 if (ufo.active) {
286 ++count;
287 }
288 }
289 return count;
290 }
291
292 [[nodiscard]] int DefenderWindow::active_asteroid_count() const {
293 int count = 0;
294 for (const auto &asteroid : asteroids) {
295 if (asteroid.active) {
296 ++count;
297 }
298 }
299 return count;
300 }
301
302 [[nodiscard]] int DefenderWindow::active_projectile_count() const {
303 int count = 0;
304 for (const auto &projectile : projectiles) {
305 if (projectile.active) {
306 ++count;
307 }
308 }
309 return count;
310 }
311
312 Ufo *DefenderWindow::find_inactive_ufo() {
313 for (auto &ufo : ufos) {
314 if (!ufo.active) {
315 return &ufo;
316 }
317 }
318 return nullptr;
319 }
320
321 Asteroid *DefenderWindow::find_inactive_asteroid() {
322 for (auto &asteroid : asteroids) {
323 if (!asteroid.active) {
324 return &asteroid;
325 }
326 }
327 return nullptr;
328 }
329
330 void DefenderWindow::clear_input_state() {
331 reverse_pressed = false;
332 propulsion_pressed = false;
333 up_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;
345 }
346
347 bool DefenderWindow::open_controller() {
348 for (int index = 0; index < mxvk::VK_Controller::joysticks(); ++index) {
349 if (controller.open(index)) {
350 log_game("Controller connected: " + controller.name());
351 return true;
352 }
353 }
354 return false;
355 }
356
357 void DefenderWindow::sync_controller_connection() {
358 if (!controller.active()) {
359 open_controller();
360 }
361 }
362
363 float DefenderWindow::controller_axis(SDL_GamepadAxis axis) const {
364 if (!controller.active()) {
365 return 0.0f;
366 }
367
368 const Sint16 raw_value = controller.getAxis(axis);
369 const float magnitude = static_cast<float>(std::abs(static_cast<int>(raw_value)));
370 if (magnitude <= static_cast<float>(CONTROLLER_DEAD_ZONE)) {
371 return 0.0f;
372 }
373
374 const float normalized = std::clamp((magnitude - static_cast<float>(CONTROLLER_DEAD_ZONE)) / (CONTROLLER_AXIS_MAX - static_cast<float>(CONTROLLER_DEAD_ZONE)), 0.0f, 1.0f);
375 return raw_value < 0 ? -normalized : normalized;
376 }
377
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;
386 return;
387 }
388
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;
394 }
395
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);
400
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;
404 }
405 controller_reverse_trigger_pressed = reverse_trigger_pressed;
406 }
407
408} // namespace defender
static int joysticks()
Return the number of connected joysticks/controllers.
void exit()
Request loop termination.
Definition mxvk.cpp:1378
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