Compare commits

..
8 Commits
Author SHA1 Message Date
jasonhilder dda388868c Todo to add difficulty levels 2026-10-08 07:45:08 +02:00
jasonhilder fb7bfd6bf3 Event manager now handles the new parent field. 2026-10-08 07:44:49 +02:00
jasonhilder 5395225cc2 Scene rendering is better now with parent setup. 2026-10-08 07:44:31 +02:00
jasonhilder 200e05f2dc Improved scene manager history.
Added a parent field essentially making a linked list.
Allows for better menu traversal.
2026-10-08 07:43:58 +02:00
jasonhilder 404c320973 reset ball now clears trail effect. 2026-10-08 07:43:21 +02:00
jasonhilder d6c52cecda Setup serve direction after scoring 2026-10-08 07:43:00 +02:00
jasonhilder 25bd7a85a1 Better clean up funcs for managers. 2026-10-08 07:41:44 +02:00
jasonhilder 0f393af931 Moved over to vis with ctags. 2026-10-08 07:40:22 +02:00
10 changed files with 114534 additions and 6854 deletions
+68 -11
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -e set -eo pipefail
# Find the odin binary # Find the odin binary
export PATH="$HOME/.odin:$PATH" export PATH="$HOME/.odin:$PATH"
@@ -9,26 +9,72 @@ SRC_DIR="src"
OUT_DIR="out" OUT_DIR="out"
OUT_NAME="pong" OUT_NAME="pong"
# flags for every odin build; unix-style errors (file:line:col: msg) are what the vis error picker parses
ODIN_FLAGS=(-error-pos-style:unix)
# ---- ctags ------------------------------------------------------------
# tags = project tags (-t, vis runs it on every .odin save)
# + stdlib tags (-T, once, or after an Odin update), shared by all projects
TAGS_FILE="tags"
STD_TAGS="${XDG_CACHE_HOME:-$HOME/.cache}/odin-tags/std.tags"
# --languages=Odin keeps vendor's bundled C/JS out of the index;
# --fields=+n adds the line:N field the vis pickers jump to
CTAGS_OPTS=(--languages=Odin --fields=+n)
# -----------------------------------------------------------------------
BUILD_ONLY=0 BUILD_ONLY=0
OPTIMIZED=0 OPTIMIZED=0
WEB=0 WEB=0
CLEAN=0 CLEAN=0
DEBUGGER=0 DEBUGGER=0
VALGRIND=0 VALGRIND=0
TAGS=""
usage() { usage() {
echo "Usage: $0 [-b] [-o] [-w] [-d] [-v] [-c] [-h]" echo "Usage: $0 [-b] [-o] [-w] [-d] [-v] [-c] [-t] [-T] [-h]"
echo " -b build only, don't run" echo " -b build only, don't run"
echo " -o optimized release build (-o:speed, no debug info)" echo " -o optimized release build (-o:speed, no debug info)"
echo " -w web/WASM build (js_wasm32 target)" echo " -w web/WASM build (js_wasm32 target)"
echo " -d build then launch in debugger instead of running" echo " -d build then launch in debugger instead of running"
echo " -v build then run under Valgrind" echo " -v build then run under Valgrind"
echo " -c clean build artifacts" echo " -c clean build artifacts"
echo " -t re-index $SRC_DIR and merge it with the stdlib tags"
echo " -T index Odin core + vendor (once, or after an Odin update)"
echo " -h show this help" echo " -h show this help"
exit 1 exit 1
} }
while getopts "bowdvch" opt; do # stdlib tags, header lines stripped so the merged file keeps only one set
generate_std_tags() {
local root
root="$(odin root 2>/dev/null || echo "$HOME/.odin")"
root="${root%/}"
mkdir -p "$(dirname "$STD_TAGS")"
ctags -R -f - "${CTAGS_OPTS[@]}" "$root/core" "$root/vendor" | grep -v '^!_TAG_' > "$STD_TAGS.tmp"
mv -f "$STD_TAGS.tmp" "$STD_TAGS"
echo "Stdlib tags written -> $STD_TAGS"
}
# project tags merged with the stdlib tags; both inputs are byte-sorted
generate_tags() {
if [ ! -f "$STD_TAGS" ]; then
echo "No stdlib tags; run $0 -T first" >&2
exit 1
fi
# $$ keeps two overlapping on-save runs from sharing temp files
local src_tags="$TAGS_FILE.src.$$"
local merged="$TAGS_FILE.tmp.$$"
ctags -R -f "$src_tags" "${CTAGS_OPTS[@]}" "$SRC_DIR"
LC_ALL=C sort -m -u "$src_tags" "$STD_TAGS" > "$merged"
mv -f "$merged" "$TAGS_FILE"
rm -f "$src_tags"
}
while getopts "bowdvctTh" opt; do
case $opt in case $opt in
b) BUILD_ONLY=1 ;; b) BUILD_ONLY=1 ;;
o) OPTIMIZED=1 ;; o) OPTIMIZED=1 ;;
@@ -36,11 +82,18 @@ while getopts "bowdvch" opt; do
d) DEBUGGER=1 ;; d) DEBUGGER=1 ;;
v) VALGRIND=1 ;; v) VALGRIND=1 ;;
c) CLEAN=1 ;; c) CLEAN=1 ;;
t) TAGS=src ;;
T) TAGS=std ;;
h) usage ;; h) usage ;;
*) usage ;; *) usage ;;
esac esac
done done
case "$TAGS" in
src) generate_tags; exit 0 ;;
std) generate_std_tags; exit 0 ;;
esac
mkdir -p "$OUT_DIR" mkdir -p "$OUT_DIR"
if [ "$CLEAN" -eq 1 ]; then if [ "$CLEAN" -eq 1 ]; then
@@ -51,7 +104,7 @@ fi
if [ "$WEB" -eq 1 ]; then if [ "$WEB" -eq 1 ]; then
echo "Building web (WASM)..." echo "Building web (WASM)..."
odin build "$SRC_DIR" \ odin build "$SRC_DIR" "${ODIN_FLAGS[@]}" \
-target:js_wasm32 \ -target:js_wasm32 \
-out:"$OUT_DIR/$OUT_NAME.wasm" \ -out:"$OUT_DIR/$OUT_NAME.wasm" \
$( [ "$OPTIMIZED" -eq 1 ] && echo "-o:speed" || echo "-debug" ) $( [ "$OPTIMIZED" -eq 1 ] && echo "-o:speed" || echo "-debug" )
@@ -67,15 +120,16 @@ fi
if [ "$OPTIMIZED" -eq 1 ]; then if [ "$OPTIMIZED" -eq 1 ]; then
echo "Building optimized ($SRC_DIR -> $OUT_DIR/$OUT_NAME)..." echo "Building optimized ($SRC_DIR -> $OUT_DIR/$OUT_NAME)..."
odin build "$SRC_DIR" \ odin build "$SRC_DIR" "${ODIN_FLAGS[@]}" \
-out:"$OUT_DIR/$OUT_NAME" \ -out:"$OUT_DIR/$OUT_NAME" \
-o:speed u -o:speed \
-no-bounds-check -no-bounds-check
else else
echo "Building debug ($SRC_DIR -> $OUT_DIR/$OUT_NAME)..." echo "Building debug ($SRC_DIR -> $OUT_DIR/$OUT_NAME)..."
odin build "$SRC_DIR" \ odin build "$SRC_DIR" "${ODIN_FLAGS[@]}" \
-out:"$OUT_DIR/$OUT_NAME" \ -out:"$OUT_DIR/$OUT_NAME" \
-define:DEBUG_MODE=true -debug \
-define:DEBUG_MODE=true
fi fi
echo "Build complete." echo "Build complete."
@@ -87,6 +141,7 @@ if [ "$BUILD_ONLY" -eq 0 ]; then
lldb "$OUT_DIR/$OUT_NAME" lldb "$OUT_DIR/$OUT_NAME"
elif [ "$VALGRIND" -eq 1 ]; then elif [ "$VALGRIND" -eq 1 ]; then
VG_STATUS=0
valgrind \ valgrind \
--leak-check=full \ --leak-check=full \
--show-leak-kinds=definite,possible \ --show-leak-kinds=definite,possible \
@@ -94,11 +149,13 @@ if [ "$BUILD_ONLY" -eq 0 ]; then
--errors-for-leak-kinds=definite,possible \ --errors-for-leak-kinds=definite,possible \
--error-exitcode=1 \ --error-exitcode=1 \
--log-file="$OUT_DIR/valgrind.log" \ --log-file="$OUT_DIR/valgrind.log" \
"./$OUT_DIR/$OUT_NAME" "./$OUT_DIR/$OUT_NAME" || VG_STATUS=$?
echo echo
echo "Valgrind report written to $OUT_DIR/valgrind.log" echo "Valgrind report written to $OUT_DIR/valgrind.log"
[ "$VG_STATUS" -ne 0 ] && echo "Valgrind reported errors (exit $VG_STATUS)"
exit "$VG_STATUS"
else else
"./$OUT_DIR/$OUT_NAME" "./$OUT_DIR/$OUT_NAME"
fi fi
fi fi
-44
View File
@@ -1,44 +0,0 @@
{
"folders":
[
{
"path": "."
}
],
"build_systems":
[
{
"name": "Run Build Script",
"cmd": ["/bin/bash", "build.sh"],
"working_dir": "$project_path",
"file_regex": "^[ ]*File \"(...*?)\", line ([0-9]*)",
"variants":
[
{
"name": "Run with Arguments...",
"cmd": ["/bin/bash", "build.sh", "$select_args"]
},
{
"name": "Build Only (-b)",
"cmd": ["/bin/bash", "build.sh", "-b"]
},
{
"name": "Optimized (-o)",
"cmd": ["/bin/bash", "build.sh", "-o"]
},
{
"name": "Debug in LLDB (-d)",
"cmd": ["/bin/bash", "build.sh", "-d"]
},
{
"name": "Valgrind (-v)",
"cmd": ["/bin/bash", "build.sh", "-v"]
},
{
"name": "Clean (-c)",
"cmd": ["/bin/bash", "build.sh", "-c"]
}
]
}
]
}
File diff suppressed because one or more lines are too long
+11 -4
View File
@@ -21,9 +21,13 @@ init_ball :: proc() -> Entity {
} }
} }
serve_ball :: proc(ball: ^Entity) { serve_ball :: proc(ball: ^Entity, serve_dir: u8) {
dir_x: f32 = 0.0
// Randomize initial direction: -1.0 or 1.0 // Randomize initial direction: -1.0 or 1.0
dir_x := f32(rand.float32() > 0.5 ? 1 : -1) if serve_dir == 0 do dir_x = f32(rand.float32() > 0.5 ? 1 : -1)
if serve_dir == 1 do dir_x = 1.0
if serve_dir == 2 do dir_x = -1.0
dir_y := rand.choice([]f32{-0.1, 0.1}) // Slightly angled launch dir_y := rand.choice([]f32{-0.1, 0.1}) // Slightly angled launch
ball.vel = rl.Vector2Normalize({dir_x, dir_y}) * ball.speed ball.vel = rl.Vector2Normalize({dir_x, dir_y}) * ball.speed
} }
@@ -32,6 +36,8 @@ reset_ball :: proc(ball: ^Entity) {
ball.pos = rl.Vector2{(VIEWPORT_W / 2), (VIEWPORT_H / 2)} ball.pos = rl.Vector2{(VIEWPORT_W / 2), (VIEWPORT_H / 2)}
ball.vel = rl.Vector2{0,0} ball.vel = rl.Vector2{0,0}
ball.speed = 300 ball.speed = 300
ball.trail_positions = {}
ball.trail_count = 0
} }
update_ball :: proc(ball: ^Entity, level: ^Level, dt: f32) { update_ball :: proc(ball: ^Entity, level: ^Level, dt: f32) {
@@ -45,7 +51,7 @@ update_ball :: proc(ball: ^Entity, level: ^Level, dt: f32) {
level.serve_timer -= dt level.serve_timer -= dt
if level.serve_timer <= 0 { if level.serve_timer <= 0 {
serve_ball(ball) serve_ball(ball, level.serve_dir)
} }
return return
@@ -86,12 +92,14 @@ update_ball :: proc(ball: ^Entity, level: ^Level, dt: f32) {
increment_score(&level.score_player, level.score_win, true, ball) increment_score(&level.score_player, level.score_win, true, ball)
reset_ball(ball) reset_ball(ball)
level.serve_timer = level.wait_time level.serve_timer = level.wait_time
level.serve_dir = 1
} }
if ball.pos.x <= field_bounds.x + ball.size.x { if ball.pos.x <= field_bounds.x + ball.size.x {
increment_score(&level.score_enemy, level.score_win, false, ball) increment_score(&level.score_enemy, level.score_win, false, ball)
reset_ball(ball) reset_ball(ball)
level.serve_timer = level.wait_time level.serve_timer = level.wait_time
level.serve_dir = 2
} }
ball.pos += ball.vel * dt ball.pos += ball.vel * dt
@@ -113,7 +121,6 @@ increment_score :: proc(score_side: ^int, max_score: int, player: bool, ball: ^E
score_side^ += 1 score_side^ += 1
if score_side^ == max_score { if score_side^ == max_score {
emit_event(Game_Over_Data{player}) emit_event(Game_Over_Data{player})
ball.trail_count = 0
} }
} }
+1
View File
@@ -27,6 +27,7 @@ update_opponent :: proc(self: ^Entity, ball: Entity, bounds: rl.Rectangle, dt: f
// center of ball - center of paddle gives the target // center of ball - center of paddle gives the target
self.target_y = (ball.pos.y + (ball.size.y / 2)) - (self.size.y / 2) + error_offset self.target_y = (ball.pos.y + (ball.size.y / 2)) - (self.size.y / 2) + error_offset
// TODO: make this and the deadzone change with difficulty
self.reaction_timer = 0.15 self.reaction_timer = 0.15
} }
} else { } else {
+30 -3
View File
@@ -78,15 +78,38 @@ process_events :: proc(game: ^Game) {
case .Game_Pause: case .Game_Pause:
push_scene(game, make_pause_overlay()) push_scene(game, make_pause_overlay())
case .Game_Resume, .Game_Back: case .Game_Resume:
pop_scene(game) pop_scene(game)
case .Game_Settings: case .Game_Settings:
push_scene(game, make_settings_menu(&game.audio, RESOLUTIONS[game.res_index].label)) parent: Maybe(Scene_Type) = nil
if active := get_active_scene(game); active != nil {
parent = active.type
}
pop_scene(game)
push_scene(game, make_settings_menu(parent, &game.audio, RESOLUTIONS[game.res_index].label))
case .Game_Main_Menu: case .Game_Main_Menu:
set_root_scene(game, make_main_menu()) set_root_scene(game, make_main_menu())
audio_fade_out(&game.audio, duration = 1.0) audio_fade_out(&game.audio, duration = 1.0)
case .Game_Back:
// Read parent from active scene
if active := get_active_scene(game); active != nil {
if parent, ok := active.parent.?; ok {
pop_scene(game)
#partial switch parent {
case .Pause_Menu: push_scene(game, make_pause_overlay())
case .Main_Menu: push_scene(game, make_main_menu())
}
break
}
}
// still pop when no parent linked ie resuming Gameplay
pop_scene(game)
case .Game_Quit: case .Game_Quit:
game.should_close = true game.should_close = true
@@ -97,4 +120,8 @@ process_events :: proc(game: ^Game) {
} }
} }
} }
} }
event_manager_quit :: proc(game: ^Game) {
delete(GLOBAL_EVENTS)
}
+8 -10
View File
@@ -1,4 +1,4 @@
package game package game
import "core:strings" import "core:strings"
import "core:math/rand" import "core:math/rand"
@@ -137,18 +137,17 @@ game_render :: proc(game: ^Game) {
rl.ClearBackground(rl.BLACK) rl.ClearBackground(rl.BLACK)
rl.BeginTextureMode(game.viewport) rl.BeginTextureMode(game.viewport)
rl.DrawTexture(game.bg_texture, 0, 0, rl.WHITE) rl.DrawTexture(game.bg_texture, 0, 0, rl.WHITE)
// World/Scene objects rendered through 2D camera space // World/Scene objects rendered through 2D camera space
rl.BeginMode2D(game.camera) rl.BeginMode2D(game.camera)
// Render all active items on the stack from bottom to top
// @note: fine for now but probably not a good idea... for &scene in game.scene_stack {
// Render all scenes in the stack from bottom to top scene_render(&scene)
for i in 0 ..< len(game.scene_stack) {
scene_render(&game.scene_stack[i])
} }
rl.EndMode2D() rl.EndMode2D()
rl.EndTextureMode() rl.EndTextureMode()
render_game_to_window(game) render_game_to_window(game)
@@ -160,13 +159,12 @@ game_render :: proc(game: ^Game) {
game_quit :: proc(game: ^Game) { game_quit :: proc(game: ^Game) {
audio_manager_quit(&game.audio) audio_manager_quit(&game.audio)
scene_manager_quit(game)
event_manager_quit(game)
rl.UnloadShader(game.crt_shader) rl.UnloadShader(game.crt_shader)
rl.UnloadTexture(game.bg_texture) rl.UnloadTexture(game.bg_texture)
rl.UnloadRenderTexture(game.viewport) rl.UnloadRenderTexture(game.viewport)
delete(GLOBAL_EVENTS)
delete(game.scene_stack)
rl.CloseWindow() rl.CloseWindow()
} }
+2 -1
View File
@@ -21,6 +21,7 @@ Level :: struct {
player: Entity, player: Entity,
opponent: Entity, opponent: Entity,
ball: Entity, ball: Entity,
serve_dir: u8,
serve_timer: f32, serve_timer: f32,
wait_time: f32, wait_time: f32,
score_player: int, score_player: int,
@@ -55,7 +56,7 @@ init_level :: proc() -> Level {
ball = init_ball(), ball = init_ball(),
serve_timer = SERVE_WAIT_TIME, serve_timer = SERVE_WAIT_TIME,
wait_time = SERVE_WAIT_TIME, wait_time = SERVE_WAIT_TIME,
score_win = 1, score_win = 3,
score_player = 0, score_player = 0,
score_enemy = 0, score_enemy = 0,
particles = make([dynamic]Particle), particles = make([dynamic]Particle),
+21 -2
View File
@@ -16,6 +16,7 @@ Scene_Type :: enum {
Scene :: struct { Scene :: struct {
type: Scene_Type, type: Scene_Type,
parent: Maybe(Scene_Type),
level: Maybe(Level), level: Maybe(Level),
canvas: Maybe(Canvas), canvas: Maybe(Canvas),
winner_is_player: bool, winner_is_player: bool,
@@ -26,7 +27,7 @@ push_scene :: proc(game: ^Game, scene: Scene) {
} }
pop_scene :: proc(game: ^Game) { pop_scene :: proc(game: ^Game) {
if len(game.scene_stack) > 1 { if len(game.scene_stack) > 0 {
top_index := len(game.scene_stack) - 1 top_index := len(game.scene_stack) - 1
destroy_scene(&game.scene_stack[top_index]) destroy_scene(&game.scene_stack[top_index])
pop(&game.scene_stack) pop(&game.scene_stack)
@@ -101,6 +102,13 @@ scene_render :: proc(scene: ^Scene) {
} }
} }
scene_manager_quit :: proc(game: ^Game) {
for &scene in game.scene_stack {
destroy_scene(&scene)
}
delete(game.scene_stack)
}
// ------------------------------------------------------------------------------ // ------------------------------------------------------------------------------
// UI Layout & Styling Constants // UI Layout & Styling Constants
// ------------------------------------------------------------------------------ // ------------------------------------------------------------------------------
@@ -220,6 +228,7 @@ make_main_menu :: proc() -> Scene {
return Scene{ return Scene{
type = .Main_Menu, type = .Main_Menu,
parent = nil,
canvas = Canvas{ canvas = Canvas{
bounds = rl.Rectangle{0, 0, VIEWPORT_W, VIEWPORT_H}, bounds = rl.Rectangle{0, 0, VIEWPORT_W, VIEWPORT_H},
padding = UI_TITLE_PADDING, padding = UI_TITLE_PADDING,
@@ -289,6 +298,7 @@ make_pause_overlay :: proc() -> Scene {
return Scene{ return Scene{
type = .Pause_Menu, type = .Pause_Menu,
parent = nil,
canvas = Canvas{ canvas = Canvas{
bounds = rl.Rectangle{0, 0, VIEWPORT_W, VIEWPORT_H}, bounds = rl.Rectangle{0, 0, VIEWPORT_W, VIEWPORT_H},
padding = UI_TITLE_PADDING, padding = UI_TITLE_PADDING,
@@ -356,7 +366,7 @@ update_pause_menu :: proc(scene: ^Scene, input: Input_Event) {
// ------------------------------------------------------------------------------ // ------------------------------------------------------------------------------
// Settings Menu // Settings Menu
// ------------------------------------------------------------------------------ // ------------------------------------------------------------------------------
make_settings_menu :: proc(audio: ^Audio_Manager, current_res_label: cstring) -> Scene { make_settings_menu :: proc(parent: Maybe(Scene_Type), audio: ^Audio_Manager, current_res_label: cstring) -> Scene {
ui_items:[8]UI_Item = { ui_items:[8]UI_Item = {
0 = UI_Button{"Back"}, 0 = UI_Button{"Back"},
1 = UI_Selector{ 1 = UI_Selector{
@@ -377,6 +387,7 @@ make_settings_menu :: proc(audio: ^Audio_Manager, current_res_label: cstring) ->
return Scene{ return Scene{
type = .Settings_Menu, type = .Settings_Menu,
parent = parent,
canvas = Canvas{ canvas = Canvas{
bounds = rl.Rectangle{0, 0, VIEWPORT_W, VIEWPORT_H}, bounds = rl.Rectangle{0, 0, VIEWPORT_W, VIEWPORT_H},
padding = UI_TITLE_PADDING, padding = UI_TITLE_PADDING,
@@ -398,6 +409,12 @@ render_settings_menu :: proc(scene: Scene) {
menu, menu_ok := canvas.menu.? menu, menu_ok := canvas.menu.?
assert(menu_ok, "render_settings_menu: Canvas is missing a UI_Menu component") assert(menu_ok, "render_settings_menu: Canvas is missing a UI_Menu component")
if scene.parent != .Main_Menu {
// Draw dark overlay so gameplay/background stays visible behind Settings
rl.DrawRectangleRec(canvas.bounds, rl.Color{0, 0, 0, 200})
}
t_text: cstring = "SETTINGS" t_text: cstring = "SETTINGS"
t_width := rl.MeasureText(t_text, UI_TITLE_FONT_SIZE) t_width := rl.MeasureText(t_text, UI_TITLE_FONT_SIZE)
t_x := i32(canvas.bounds.width * 0.5) - (t_width / 2) t_x := i32(canvas.bounds.width * 0.5) - (t_width / 2)
@@ -465,6 +482,7 @@ make_game_over_overlay :: proc(winner_is_player: bool) -> Scene {
return Scene{ return Scene{
type = .Game_Over, type = .Game_Over,
parent = nil,
winner_is_player = winner_is_player, winner_is_player = winner_is_player,
canvas = Canvas{ canvas = Canvas{
bounds = rl.Rectangle{0, 0, VIEWPORT_W, VIEWPORT_H}, bounds = rl.Rectangle{0, 0, VIEWPORT_W, VIEWPORT_H},
@@ -530,6 +548,7 @@ update_game_over :: proc(scene: ^Scene, input: Input_Event) {
make_gameplay_scene :: proc() -> Scene { make_gameplay_scene :: proc() -> Scene {
return Scene{ return Scene{
type = .Gameplay, type = .Gameplay,
parent = nil,
level = init_level(), level = init_level(),
} }
} }
+114393
View File
File diff suppressed because it is too large Load Diff