Add keyboard play: arrows/WASD/hjkl focus, Enter/Space flips
Focus ring appears on the first key press and hides when the mouse moves. Enter also triggers Play Again on the win panel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8214fda8f6
commit
685182b25d
4 files changed
+74
-9
No files matched your search
+10
-6
@@ -229,18 +229,17 @@ smoothstep :: proc(t: f32) -> f32 {
|
|||||||
return t * t * (3 - 2 * t)
|
return t * t * (3 - 2 * t)
|
||||||
}
|
}
|
||||||
|
|
||||||
draw :: proc(card: ^Card, theme: themes.Theme, clickable: bool) {
|
draw :: proc(card: ^Card, theme: themes.Theme, clickable: bool, focused := false) {
|
||||||
eased := smoothstep(card.flip_t)
|
eased := smoothstep(card.flip_t)
|
||||||
squash := abs(math.cos(eased * math.PI)) // 1 → 0 → 1 across the flip
|
squash := abs(math.cos(eased * math.PI)) // 1 → 0 → 1 across the flip
|
||||||
showing_face := eased >= 0.5
|
showing_face := eased >= 0.5
|
||||||
|
|
||||||
// Cards lift as they turn, and a little on hover
|
// Cards lift as they turn, and a little on hover
|
||||||
lift := math.sin(eased * math.PI) * 6
|
lift := math.sin(eased * math.PI) * 6
|
||||||
hovered := clickable && is_hovered(card)
|
mouse_over := clickable && is_hovered(card)
|
||||||
if hovered {
|
if mouse_over { ui.set_hovered() }
|
||||||
lift += HOVER_LIFT
|
hovered := mouse_over || (clickable && focused)
|
||||||
ui.set_hovered()
|
if hovered { lift += HOVER_LIFT }
|
||||||
}
|
|
||||||
|
|
||||||
scale: f32 = 1
|
scale: f32 = 1
|
||||||
if card.pulse_t > 0 {
|
if card.pulse_t > 0 {
|
||||||
@@ -273,6 +272,11 @@ draw :: proc(card: ^Card, theme: themes.Theme, clickable: bool) {
|
|||||||
dest := raylib.Rectangle{center.x, center.y, width * squash, height}
|
dest := raylib.Rectangle{center.x, center.y, width * squash, height}
|
||||||
raylib.DrawTexturePro(texture, source, dest, {dest.width / 2, dest.height / 2}, 0, tint)
|
raylib.DrawTexturePro(texture, source, dest, {dest.width / 2, dest.height / 2}, 0, tint)
|
||||||
|
|
||||||
|
if focused {
|
||||||
|
ring := raylib.Rectangle{center.x - width * squash / 2 - 6, center.y - height / 2 - 6, width * squash + 12, height + 12}
|
||||||
|
raylib.DrawRectangleRoundedLinesEx(ring, ROUNDNESS, SEGMENTS, 3, raylib.Fade(theme.ink, 0.75))
|
||||||
|
}
|
||||||
|
|
||||||
if card.matched && card.flip_t == 1 {
|
if card.matched && card.flip_t == 1 {
|
||||||
outline := raylib.Rectangle{center.x - width / 2 - 3, center.y - height / 2 - 3, width + 6, height + 6}
|
outline := raylib.Rectangle{center.x - width / 2 - 3, center.y - height / 2 - 3, width + 6, height + 6}
|
||||||
raylib.DrawRectangleRoundedLinesEx(outline, ROUNDNESS, SEGMENTS, 3, theme.accent)
|
raylib.DrawRectangleRoundedLinesEx(outline, ROUNDNESS, SEGMENTS, 3, theme.accent)
|
||||||
|
|||||||
+42
-2
@@ -56,6 +56,8 @@ elapsed: f32
|
|||||||
resolve_timer: f32
|
resolve_timer: f32
|
||||||
win_t: f32 // fades in the win panel
|
win_t: f32 // fades in the win panel
|
||||||
restart_requested: bool
|
restart_requested: bool
|
||||||
|
focus: int // card selected with the keyboard
|
||||||
|
keyboard_active: bool // show the focus ring (hidden again when the mouse moves)
|
||||||
|
|
||||||
// Deals a new game and renders the card faces. Needs the window to exist.
|
// Deals a new game and renders the card faces. Needs the window to exist.
|
||||||
start :: proc(deck_id: decks.Deck_Id, game_difficulty: Difficulty) {
|
start :: proc(deck_id: decks.Deck_Id, game_difficulty: Difficulty) {
|
||||||
@@ -112,6 +114,7 @@ deal :: proc(deck_id: decks.Deck_Id, game_difficulty: Difficulty) {
|
|||||||
resolve_timer = 0
|
resolve_timer = 0
|
||||||
win_t = 0
|
win_t = 0
|
||||||
restart_requested = false
|
restart_requested = false
|
||||||
|
focus = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
active_cards :: proc() -> []card.Card {
|
active_cards :: proc() -> []card.Card {
|
||||||
@@ -146,6 +149,42 @@ select_card :: proc(i: int) -> bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Moves the keyboard focus within the grid, clamped at the edges
|
||||||
|
move_focus :: proc(dx, dy: int) {
|
||||||
|
col := clamp(focus % layout.cols + dx, 0, layout.cols - 1)
|
||||||
|
row := clamp(focus / layout.cols + dy, 0, layout.rows - 1)
|
||||||
|
focus = row * layout.cols + col
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private)
|
||||||
|
key_pressed :: proc(keys: ..raylib.KeyboardKey) -> bool {
|
||||||
|
for key in keys {
|
||||||
|
if raylib.IsKeyPressed(key) || raylib.IsKeyPressedRepeat(key) { return true }
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arrows, WASD or vim keys move the focus; Enter or Space flips the focused card
|
||||||
|
@(private)
|
||||||
|
handle_keyboard :: proc() {
|
||||||
|
if raylib.GetMouseDelta() != {} { keyboard_active = false }
|
||||||
|
|
||||||
|
dx, dy := 0, 0
|
||||||
|
if key_pressed(.LEFT, .A, .H) { dx -= 1 }
|
||||||
|
if key_pressed(.RIGHT, .D, .L) { dx += 1 }
|
||||||
|
if key_pressed(.UP, .W, .K) { dy -= 1 }
|
||||||
|
if key_pressed(.DOWN, .S, .J) { dy += 1 }
|
||||||
|
if dx != 0 || dy != 0 {
|
||||||
|
// The first key press only reveals the focus ring
|
||||||
|
if keyboard_active { move_focus(dx, dy) }
|
||||||
|
keyboard_active = true
|
||||||
|
}
|
||||||
|
if raylib.IsKeyPressed(.ENTER) || raylib.IsKeyPressed(.SPACE) {
|
||||||
|
keyboard_active = true
|
||||||
|
select_card(focus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@(private)
|
@(private)
|
||||||
resolve :: proc() {
|
resolve :: proc() {
|
||||||
a, b := &cards[first], &cards[second]
|
a, b := &cards[first], &cards[second]
|
||||||
@@ -180,6 +219,7 @@ update :: proc(dt: f32) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for &c in active_cards() { card.update(&c, dt) }
|
for &c in active_cards() { card.update(&c, dt) }
|
||||||
|
if state != .OVER { handle_keyboard() }
|
||||||
|
|
||||||
switch state {
|
switch state {
|
||||||
case .WAITING, .FIRST:
|
case .WAITING, .FIRST:
|
||||||
@@ -221,7 +261,7 @@ draw :: proc() -> themes.Screen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for &c, i in active_cards() {
|
for &c, i in active_cards() {
|
||||||
card.draw(&c, theme, can_pick(i))
|
card.draw(&c, theme, can_pick(i), focused = keyboard_active && i == focus && state != .OVER)
|
||||||
}
|
}
|
||||||
|
|
||||||
if state == .OVER {
|
if state == .OVER {
|
||||||
@@ -256,7 +296,7 @@ draw_win_panel :: proc() -> Maybe(themes.Screen) {
|
|||||||
if win_t < 1 { return nil }
|
if win_t < 1 { return nil }
|
||||||
button_w: f32 = 200
|
button_w: f32 = 200
|
||||||
button_y := panel.y + panel_h - 100
|
button_y := panel.y + panel_h - 100
|
||||||
if ui.button({cx - button_w - 10, button_y, button_w, 60}, "Play Again", theme.primary) {
|
if ui.button({cx - button_w - 10, button_y, button_w, 60}, "Play Again", theme.primary, hotkey = .ENTER) {
|
||||||
restart_requested = true
|
restart_requested = true
|
||||||
}
|
}
|
||||||
if ui.button({cx + 10, button_y, button_w, 60}, "Menu", theme.ink) {
|
if ui.button({cx + 10, button_y, button_w, 60}, "Menu", theme.ink) {
|
||||||
|
|||||||
@@ -60,7 +60,8 @@ main_menu :: proc() -> Maybe(decks.Language) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.draw_text_centered("1-3 pick a language · Esc goes back, and quits from here", themes.SCREEN_WIDTH / 2, 660, 18, raylib.Fade(themes.DARK_GRAY, 0.8))
|
ui.draw_text_centered("1-3 pick a language · In game: arrows, WASD or hjkl to move, Enter/Space to flip · Esc back / quit",
|
||||||
|
themes.SCREEN_WIDTH / 2, 660, 18, raylib.Fade(themes.DARK_GRAY, 0.8))
|
||||||
return picked
|
return picked
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -270,3 +270,23 @@ card_pulse_counts_down :: proc(t: ^testing.T) {
|
|||||||
card.update(&c, 1)
|
card.update(&c, 1)
|
||||||
testing.expect_value(t, c.pulse_t, 0)
|
testing.expect_value(t, c.pulse_t, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
keyboard_focus_stays_inside_grid :: proc(t: ^testing.T) {
|
||||||
|
memory.deal(.HIRAGANA, .NORMAL) // 4 rows x 6 columns
|
||||||
|
testing.expect_value(t, memory.focus, 0)
|
||||||
|
|
||||||
|
memory.move_focus(-1, -1) // already top-left
|
||||||
|
testing.expect_value(t, memory.focus, 0)
|
||||||
|
memory.move_focus(1, 0)
|
||||||
|
testing.expect_value(t, memory.focus, 1)
|
||||||
|
memory.move_focus(0, 1)
|
||||||
|
testing.expect_value(t, memory.focus, 7)
|
||||||
|
memory.move_focus(100, 100) // clamps to bottom-right
|
||||||
|
testing.expect_value(t, memory.focus, memory.card_count - 1)
|
||||||
|
|
||||||
|
memory.deal(.HIRAGANA, .HARD) // dealing resets focus
|
||||||
|
testing.expect_value(t, memory.focus, 0)
|
||||||
|
memory.move_focus(0, 3)
|
||||||
|
testing.expect_value(t, memory.focus, 24) // 4 rows x 8 columns
|
||||||
|
}
|
||||||
Reference in new issue
Block a user