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) {
22 return handle_console_command(args, out);
26 bool DefenderWindow::handle_console_command(
const std::vector<std::string> &args, std::ostream &out) {
31 const std::string &cmd = args.front();
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";
54 for (std::size_t i = 1; i < args.size(); ++i) {
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.";
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';
85 if (cmd ==
"restart") {
87 log_game(
"Game restarted from console.");
88 out <<
"Game restarted.";
97 ship_respawning =
false;
98 reset_ship_to_origin();
100 log_game(
"Returned to intro screen from console.");
101 out <<
"Intro screen active.";
108 ship_respawning =
false;
114 log_game(
"Play mode activated from console.");
119 if (cmd ==
"gameover") {
122 ship_respawning =
false;
123 ship.visible =
false;
124 ship.velocity = glm::vec3(0.0f);
125 ship.current_speed = 0.0f;
127 log_game(
"Game over forced from console.", SDL_Color{255, 120, 80, 255});
128 out <<
"Game over forced.";
132 if (cmd ==
"lives") {
134 if (!parse_int_arg(args, 1,
"lives", value, out)) {
137 lives = std::clamp(value, 0, 99);
142 log_game(std::format(
"Lives set to {} from console.", lives));
143 out <<
"Lives set to " << lives <<
'.';
147 if (cmd ==
"score") {
149 if (!parse_int_arg(args, 1,
"score", value, out)) {
152 score = std::max(0, value);
153 log_game(std::format(
"Score set to {} from console.", score));
154 out <<
"Score set to " << score <<
'.';
158 if (cmd ==
"killship") {
160 out <<
"Ship is already destroyed.";
163 spawn_ship_explosion(ship.position);
165 log_game(
"Ship destroyed from console.", SDL_Color{255, 120, 80, 255});
166 out <<
"Ship destroyed.";
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) {
177 for (
auto &asteroid : asteroids) {
178 asteroid.active =
false;
181 log_game(std::format(
"Cleared {} UFO(s) and {} asteroid(s) from console.", cleared_ufos, cleared_asteroids));
182 out <<
"Enemies cleared.";
186 if (cmd ==
"spawn_ufo") {
187 Ufo *ufo = find_inactive_ufo();
188 if (ufo ==
nullptr) {
189 out <<
"No free UFO slots.";
193 log_game(std::format(
"UFO spawned from console at {}.", format_vec3(ufo->position)));
194 out <<
"UFO spawned.";
198 if (cmd ==
"spawn_asteroid") {
200 if (args.size() > 1 && !parse_float_arg(args, 1,
"scale", scale, out)) {
203 Asteroid *asteroid = find_inactive_asteroid();
204 if (asteroid ==
nullptr) {
205 out <<
"No free asteroid slots.";
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.";
216 if (cmd ==
"about") {
217 out <<
"defender: MXVK side-scrolling starfield shooter.\n";
221 if (cmd ==
"quit" || cmd ==
"exit") {
222 log_game(
"Exit requested from console.");
223 out <<
"Closing window...";
231 void DefenderWindow::log_game(
const std::string &message, SDL_Color color) {
232 if (!console_ready) {
235 console.printLine(
"[game] " + message, color);
238 [[nodiscard]]
const char *DefenderWindow::mode_name()
const {
243 return "intro_fade_in";
252 [[nodiscard]] std::string DefenderWindow::format_vec3(
const glm::vec3 &value) {
253 return std::format(
"({:.1f}, {:.1f}, {:.1f})", value.x, value.y, value.z);
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.";
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];
268 value = parsed_value;
271 out <<
"Invalid " << name <<
" value: " << args[index];
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.";
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];
288 value = parsed_value;
291 out <<
"Invalid " << name <<
" value: " << args[index];
296 [[nodiscard]]
int DefenderWindow::active_ufo_count()
const {
298 for (
const auto &ufo : ufos) {
306 [[nodiscard]]
int DefenderWindow::active_asteroid_count()
const {
308 for (
const auto &asteroid : asteroids) {
316 [[nodiscard]]
int DefenderWindow::active_projectile_count()
const {
318 for (
const auto &projectile : projectiles) {
319 if (projectile.active) {
326 Ufo *DefenderWindow::find_inactive_ufo() {
327 for (
auto &ufo : ufos) {
335 Asteroid *DefenderWindow::find_inactive_asteroid() {
336 for (
auto &asteroid : asteroids) {
344 void DefenderWindow::clear_input_state() {
345 reverse_pressed =
false;
346 propulsion_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;
361 bool DefenderWindow::open_controller() {
363 if (controller.open(index)) {
364 log_game(
"Controller connected: " + controller.name());
371 void DefenderWindow::sync_controller_connection() {
372 if (!controller.active()) {
377 float DefenderWindow::controller_axis(SDL_GamepadAxis axis)
const {
378 if (!controller.active()) {
382 const Sint16 raw_value = controller.getAxis(axis);
383 const float magnitude =
static_cast<float>(std::abs(
static_cast<int>(raw_value)));
392 return raw_value < 0 ? -normalized : normalized;
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;
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;
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);
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;
422 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.