Restructure into themed screens with flag card backs, real font, flip animation and sound
- Replace the bogus DroidSansJapanese.ttf (an HTML page) with M PLUS Rounded 1c, loaded with LoadFontEx and explicit kana codepoints - Theme struct per language with flag-based card backs (Hinomaru, Taegukgi, Trairong) - Card art rendered to textures so the flip squashes the whole card - Synthesized flip/match/mismatch/win/click sounds (new audio package) - Shared ui package for text and buttons; Screen enum replaces i32 ids - Kana <-> romaji pairs drawn from all 46 basic hiragana Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5557de62a3
commit
5806f0498c
11 files changed
+1023
-402
No files matched your search
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,94 @@
|
|||||||
|
Copyright 2016 The Rounded M+ Project Authors.
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
|
Copyright 2016 The Rounded M+ Project Authors.
|
||||||
@@ -1,56 +1,77 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import raylib "vendor:raylib"
|
import raylib "vendor:raylib"
|
||||||
import themes "src/themes"
|
|
||||||
import screens "src/screens"
|
|
||||||
import memory "src/memory"
|
|
||||||
import assets "src/assets"
|
import assets "src/assets"
|
||||||
|
import audio "src/audio"
|
||||||
|
import card "src/card"
|
||||||
|
import memory "src/memory"
|
||||||
|
import screens "src/screens"
|
||||||
|
import themes "src/themes"
|
||||||
|
import ui "src/ui"
|
||||||
|
|
||||||
current_screen: i32 = themes.MAIN_MENU
|
// Where Escape takes you from each screen (nil = quit)
|
||||||
mouse_x: i32
|
parent_screen :: proc(screen: themes.Screen) -> Maybe(themes.Screen) {
|
||||||
mouse_y: i32
|
switch screen {
|
||||||
|
case .MAIN_MENU: return nil
|
||||||
|
case .JAPANESE_MEMORY: return .JAPANESE_MENU
|
||||||
|
case .JAPANESE_MENU, .KOREAN_MENU, .THAI_MENU: return .MAIN_MENU
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
main :: proc() {
|
main :: proc() {
|
||||||
raylib.InitWindow(themes.SCREEN_WIDTH, themes.SCREEN_HEIGHT, cstring("HanjaKanaMun"))
|
raylib.SetConfigFlags({.MSAA_4X_HINT, .VSYNC_HINT})
|
||||||
|
raylib.InitWindow(themes.SCREEN_WIDTH, themes.SCREEN_HEIGHT, "HanjaKanaMun")
|
||||||
defer raylib.CloseWindow()
|
defer raylib.CloseWindow()
|
||||||
raylib.SetExitKey(raylib.KeyboardKey.KEY_NULL)
|
raylib.SetExitKey(.KEY_NULL)
|
||||||
|
raylib.SetTargetFPS(60)
|
||||||
|
|
||||||
|
raylib.InitAudioDevice()
|
||||||
|
defer raylib.CloseAudioDevice()
|
||||||
|
audio.load()
|
||||||
|
defer audio.unload()
|
||||||
|
|
||||||
assets.load_fonts()
|
assets.load_fonts()
|
||||||
defer assets.unload_fonts()
|
defer assets.unload_fonts()
|
||||||
|
card.load_art()
|
||||||
|
defer card.unload_art()
|
||||||
|
defer memory.unload()
|
||||||
|
|
||||||
raylib.SetTargetFPS(60)
|
screen := themes.Screen.MAIN_MENU
|
||||||
memory.init_memory_game()
|
running := true
|
||||||
|
for running && !raylib.WindowShouldClose() {
|
||||||
|
dt := raylib.GetFrameTime()
|
||||||
|
|
||||||
for !raylib.WindowShouldClose() {
|
if raylib.IsKeyPressed(.ESCAPE) {
|
||||||
mouse_x = raylib.GetMouseX()
|
if parent, ok := parent_screen(screen).?; ok {
|
||||||
mouse_y = raylib.GetMouseY()
|
screen = parent
|
||||||
|
} else {
|
||||||
if raylib.IsKeyPressed(raylib.KeyboardKey.ESCAPE) {
|
running = false
|
||||||
current_screen = themes.MAIN_MENU
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if current_screen == themes.JAPANESE_MEMORY {
|
if screen == .JAPANESE_MEMORY {
|
||||||
delta_time := raylib.GetFrameTime()
|
memory.update(dt)
|
||||||
memory.update_memory_game(delta_time, mouse_x, mouse_y)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
raylib.BeginDrawing()
|
raylib.BeginDrawing()
|
||||||
|
ui.begin_frame()
|
||||||
if current_screen == themes.MAIN_MENU {
|
next := screen
|
||||||
current_screen = screens.draw_main_menu(mouse_x, mouse_y)
|
switch screen {
|
||||||
} else if current_screen == themes.JAPANESE_SCREEN {
|
case .MAIN_MENU: next = screens.main_menu()
|
||||||
current_screen = screens.draw_japanese_screen(mouse_x, mouse_y)
|
case .JAPANESE_MENU: next = screens.japanese_menu()
|
||||||
if current_screen == themes.JAPANESE_MEMORY {
|
case .JAPANESE_MEMORY: next = memory.draw()
|
||||||
memory.init_memory_game()
|
case .KOREAN_MENU: next = screens.korean_menu()
|
||||||
}
|
case .THAI_MENU: next = screens.thai_menu()
|
||||||
} else if current_screen == themes.JAPANESE_MEMORY {
|
|
||||||
current_screen = memory.draw_memory_game(mouse_x, mouse_y)
|
|
||||||
} else if current_screen == themes.KOREAN_SCREEN {
|
|
||||||
current_screen = screens.draw_korean_screen(mouse_x, mouse_y)
|
|
||||||
} else if current_screen == themes.THAI_SCREEN {
|
|
||||||
current_screen = screens.draw_thai_screen(mouse_x, mouse_y)
|
|
||||||
}
|
}
|
||||||
|
ui.end_frame()
|
||||||
raylib.EndDrawing()
|
raylib.EndDrawing()
|
||||||
|
|
||||||
|
// Start a fresh game when entering the memory screen (outside of drawing,
|
||||||
|
// because building card faces renders into textures)
|
||||||
|
if next == .JAPANESE_MEMORY && screen != .JAPANESE_MEMORY {
|
||||||
|
memory.start(themes.JAPAN)
|
||||||
|
}
|
||||||
|
screen = next
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+41
-6
@@ -2,17 +2,52 @@ package assets
|
|||||||
|
|
||||||
import raylib "vendor:raylib"
|
import raylib "vendor:raylib"
|
||||||
|
|
||||||
japanese_font: raylib.Font
|
REGULAR_FONT_PATH :: "assets/MPLUSRounded1c-Medium.ttf"
|
||||||
|
BOLD_FONT_PATH :: "assets/MPLUSRounded1c-ExtraBold.ttf"
|
||||||
|
|
||||||
|
// Glyphs are rasterized at this size and scaled down when drawn
|
||||||
|
FONT_BASE_SIZE :: 128
|
||||||
|
|
||||||
|
regular_font: raylib.Font
|
||||||
|
bold_font: raylib.Font
|
||||||
|
|
||||||
|
// raylib only bakes ASCII by default, so every non-ASCII glyph must be listed
|
||||||
|
@(private)
|
||||||
|
build_codepoints :: proc() -> [dynamic]rune {
|
||||||
|
codepoints: [dynamic]rune
|
||||||
|
for r in rune(0x20) ..= 0x7E { append(&codepoints, r) } // ASCII
|
||||||
|
for r in rune(0x3041) ..= 0x3096 { append(&codepoints, r) } // Hiragana
|
||||||
|
for r in rune(0x30A1) ..= 0x30FC { append(&codepoints, r) } // Katakana
|
||||||
|
for r in "日本語・·āō" { append(&codepoints, r) }
|
||||||
|
return codepoints
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private)
|
||||||
|
load_font :: proc(path: cstring, codepoints: []rune) -> raylib.Font {
|
||||||
|
font := raylib.LoadFontEx(path, FONT_BASE_SIZE, raw_data(codepoints), i32(len(codepoints)))
|
||||||
|
if !raylib.IsFontValid(font) || font.texture.id == 0 {
|
||||||
|
raylib.TraceLog(.WARNING, "ASSETS: Falling back to default font, could not load %s", path)
|
||||||
|
return raylib.GetFontDefault()
|
||||||
|
}
|
||||||
|
// Mipmaps keep small UI text smooth when the large atlas is scaled down
|
||||||
|
raylib.GenTextureMipmaps(&font.texture)
|
||||||
|
raylib.SetTextureFilter(font.texture, .TRILINEAR)
|
||||||
|
return font
|
||||||
|
}
|
||||||
|
|
||||||
load_fonts :: proc() {
|
load_fonts :: proc() {
|
||||||
// Use raylib's default font for now
|
codepoints := build_codepoints()
|
||||||
japanese_font = raylib.GetFontDefault()
|
defer delete(codepoints)
|
||||||
|
regular_font = load_font(REGULAR_FONT_PATH, codepoints[:])
|
||||||
|
bold_font = load_font(BOLD_FONT_PATH, codepoints[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
unload_fonts :: proc() {
|
unload_fonts :: proc() {
|
||||||
// Don't unload the default font
|
default_id := raylib.GetFontDefault().texture.id
|
||||||
|
if regular_font.texture.id != default_id { raylib.UnloadFont(regular_font) }
|
||||||
|
if bold_font.texture.id != default_id { raylib.UnloadFont(bold_font) }
|
||||||
}
|
}
|
||||||
|
|
||||||
get_font :: proc() -> raylib.Font {
|
get_font :: proc(bold := false) -> raylib.Font {
|
||||||
return japanese_font
|
return bold ? bold_font : regular_font
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
package audio
|
||||||
|
|
||||||
|
import "core:math"
|
||||||
|
import "core:math/rand"
|
||||||
|
import raylib "vendor:raylib"
|
||||||
|
|
||||||
|
// All sounds are synthesized at startup, so no audio files are needed
|
||||||
|
|
||||||
|
SAMPLE_RATE :: 44100
|
||||||
|
|
||||||
|
Sound_Effect :: enum {
|
||||||
|
FLIP,
|
||||||
|
MATCH,
|
||||||
|
MISMATCH,
|
||||||
|
WIN,
|
||||||
|
CLICK,
|
||||||
|
}
|
||||||
|
|
||||||
|
sounds: [Sound_Effect]raylib.Sound
|
||||||
|
ready: bool
|
||||||
|
|
||||||
|
@(private)
|
||||||
|
Tone :: struct {
|
||||||
|
start: f32, // seconds
|
||||||
|
freq: f32, // Hz
|
||||||
|
duration: f32, // seconds
|
||||||
|
amp: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sine with a little 2nd/3rd harmonic, soft attack and exponential decay: a small bell
|
||||||
|
@(private)
|
||||||
|
add_tone :: proc(samples: []f32, tone: Tone) {
|
||||||
|
first := int(tone.start * SAMPLE_RATE)
|
||||||
|
count := int(tone.duration * SAMPLE_RATE)
|
||||||
|
attack: f32 = 0.006
|
||||||
|
for i in 0 ..< count {
|
||||||
|
idx := first + i
|
||||||
|
if idx >= len(samples) { break }
|
||||||
|
t := f32(i) / SAMPLE_RATE
|
||||||
|
env := min(t / attack, 1) * math.exp(-5 * t / tone.duration)
|
||||||
|
phase := 2 * math.PI * tone.freq * t
|
||||||
|
s := math.sin(phase) + 0.25 * math.sin(2 * phase) + 0.08 * math.sin(3 * phase)
|
||||||
|
samples[idx] += tone.amp * env * s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Card "snap": low-passed noise with a fast decay over a short low thump
|
||||||
|
@(private)
|
||||||
|
add_flip :: proc(samples: []f32) {
|
||||||
|
lp: f32 = 0
|
||||||
|
for i in 0 ..< len(samples) {
|
||||||
|
t := f32(i) / SAMPLE_RATE
|
||||||
|
noise := rand.float32_range(-1, 1)
|
||||||
|
lp += 0.35 * (noise - lp)
|
||||||
|
swish := lp * math.exp(-t * 55) * min(t / 0.004, 1)
|
||||||
|
thump := math.sin(2 * math.PI * 150 * t) * math.exp(-t * 70)
|
||||||
|
samples[i] += 0.55 * swish + 0.35 * thump
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private)
|
||||||
|
make_sound :: proc(samples: []f32) -> raylib.Sound {
|
||||||
|
pcm := make([]i16, len(samples))
|
||||||
|
defer delete(pcm)
|
||||||
|
for s, i in samples {
|
||||||
|
pcm[i] = i16(math.clamp(s, -1, 1) * 32000)
|
||||||
|
}
|
||||||
|
wave := raylib.Wave{
|
||||||
|
frameCount = u32(len(pcm)),
|
||||||
|
sampleRate = SAMPLE_RATE,
|
||||||
|
sampleSize = 16,
|
||||||
|
channels = 1,
|
||||||
|
data = raw_data(pcm),
|
||||||
|
}
|
||||||
|
// LoadSoundFromWave copies the samples into its own buffer
|
||||||
|
return raylib.LoadSoundFromWave(wave)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private)
|
||||||
|
build :: proc(duration: f32, tones: []Tone, flip := false) -> raylib.Sound {
|
||||||
|
samples := make([]f32, int(duration * SAMPLE_RATE))
|
||||||
|
defer delete(samples)
|
||||||
|
if flip { add_flip(samples) }
|
||||||
|
for tone in tones { add_tone(samples, tone) }
|
||||||
|
return make_sound(samples)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call after raylib.InitAudioDevice
|
||||||
|
load :: proc() {
|
||||||
|
if !raylib.IsAudioDeviceReady() { return }
|
||||||
|
|
||||||
|
E5 :: 659.25
|
||||||
|
A5 :: 880.00
|
||||||
|
B5 :: 987.77
|
||||||
|
C5 :: 523.25
|
||||||
|
G5 :: 783.99
|
||||||
|
C6 :: 1046.50
|
||||||
|
E6 :: 1318.51
|
||||||
|
|
||||||
|
sounds[.FLIP] = build(0.12, {}, flip = true)
|
||||||
|
sounds[.MATCH] = build(0.6, {
|
||||||
|
{start = 0.00, freq = E5, duration = 0.35, amp = 0.30},
|
||||||
|
{start = 0.09, freq = B5, duration = 0.50, amp = 0.28},
|
||||||
|
})
|
||||||
|
sounds[.MISMATCH] = build(0.45, {
|
||||||
|
{start = 0.00, freq = 330, duration = 0.20, amp = 0.20},
|
||||||
|
{start = 0.11, freq = 262, duration = 0.30, amp = 0.20},
|
||||||
|
})
|
||||||
|
sounds[.WIN] = build(1.3, {
|
||||||
|
{start = 0.00, freq = C5, duration = 0.40, amp = 0.18},
|
||||||
|
{start = 0.11, freq = E5, duration = 0.40, amp = 0.18},
|
||||||
|
{start = 0.22, freq = G5, duration = 0.40, amp = 0.18},
|
||||||
|
{start = 0.33, freq = C6, duration = 0.90, amp = 0.2},
|
||||||
|
{start = 0.33, freq = E6, duration = 0.90, amp = 0.07},
|
||||||
|
})
|
||||||
|
sounds[.CLICK] = build(0.08, {{start = 0, freq = A5, duration = 0.06, amp = 0.15}})
|
||||||
|
|
||||||
|
raylib.SetSoundVolume(sounds[.FLIP], 0.7)
|
||||||
|
ready = true
|
||||||
|
}
|
||||||
|
|
||||||
|
unload :: proc() {
|
||||||
|
if !ready { return }
|
||||||
|
for s in sounds { raylib.UnloadSound(s) }
|
||||||
|
ready = false
|
||||||
|
}
|
||||||
|
|
||||||
|
play :: proc(effect: Sound_Effect) {
|
||||||
|
if !ready { return }
|
||||||
|
if effect == .FLIP {
|
||||||
|
// Slight pitch variation so repeated flips don't sound identical
|
||||||
|
raylib.SetSoundPitch(sounds[.FLIP], rand.float32_range(0.9, 1.12))
|
||||||
|
}
|
||||||
|
raylib.PlaySound(sounds[effect])
|
||||||
|
}
|
||||||
+246
-62
@@ -1,81 +1,265 @@
|
|||||||
package card
|
package card
|
||||||
|
|
||||||
|
import "core:math"
|
||||||
import raylib "vendor:raylib"
|
import raylib "vendor:raylib"
|
||||||
import themes "../themes"
|
import themes "../themes"
|
||||||
import assets "../assets"
|
import ui "../ui"
|
||||||
|
|
||||||
|
// Card art is rendered once into textures at 2x size and scaled down when drawn.
|
||||||
|
// This lets the flip animation squash the whole card (text included) horizontally.
|
||||||
|
ART_SCALE :: 2
|
||||||
|
ART_WIDTH :: themes.CARD_WIDTH * ART_SCALE
|
||||||
|
ART_HEIGHT :: themes.CARD_HEIGHT * ART_SCALE
|
||||||
|
ROUNDNESS :: 0.16
|
||||||
|
SEGMENTS :: 16
|
||||||
|
|
||||||
|
FLIP_SPEED :: 2.6 // flips per second
|
||||||
|
PULSE_DURATION :: 0.35 // seconds
|
||||||
|
HOVER_LIFT :: 4
|
||||||
|
|
||||||
Card :: struct {
|
Card :: struct {
|
||||||
x: i32,
|
rect: raylib.Rectangle, // resting position on screen
|
||||||
y: i32,
|
text: cstring,
|
||||||
character: cstring,
|
is_kana: bool,
|
||||||
is_face_up: bool,
|
pair_id: int,
|
||||||
is_matched: bool,
|
face_up: bool, // logical state; the animation follows it
|
||||||
pair_id: i32,
|
matched: bool,
|
||||||
flip_progress: f32, // 0 = face down, 1 = face up
|
flip_t: f32, // 0 = back showing, 1 = face showing
|
||||||
is_flipping: bool,
|
pulse_t: f32, // counts down after a match
|
||||||
flip_direction: bool, // true = to face up, false = to face down
|
face_art: raylib.RenderTexture2D,
|
||||||
}
|
}
|
||||||
|
|
||||||
draw_card :: proc(card: ^Card, mouse_x, mouse_y: i32, theme_color: raylib.Color) {
|
@(private)
|
||||||
draw_color := theme_color
|
back_art: [themes.Flag]raylib.RenderTexture2D
|
||||||
if card.is_matched {
|
|
||||||
draw_color = themes.LIGHT_GRAY
|
|
||||||
}
|
|
||||||
|
|
||||||
is_hovered := mouse_x >= card.x && mouse_x <= card.x + themes.CARD_WIDTH &&
|
// ---------------------------------------------------------------------------
|
||||||
mouse_y >= card.y && mouse_y <= card.y + themes.CARD_HEIGHT
|
// Art generation
|
||||||
if is_hovered && !card.is_face_up && !card.is_matched && !card.is_flipping {
|
// ---------------------------------------------------------------------------
|
||||||
draw_color = raylib.Color{
|
|
||||||
byte(f32(theme_color.r) * 0.8),
|
|
||||||
byte(f32(theme_color.g) * 0.8),
|
|
||||||
byte(f32(theme_color.b) * 0.8),
|
|
||||||
theme_color.a,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw card background with rounded corners
|
@(private)
|
||||||
raylib.DrawRectangleRounded(raylib.Rectangle{f32(card.x), f32(card.y), f32(themes.CARD_WIDTH), f32(themes.CARD_HEIGHT)}, 0.2, 10, draw_color)
|
art_rect :: proc(inset: f32 = 0) -> raylib.Rectangle {
|
||||||
raylib.DrawRectangleRoundedLines(raylib.Rectangle{f32(card.x), f32(card.y), f32(themes.CARD_WIDTH), f32(themes.CARD_HEIGHT)}, 0.2, 10, themes.BLACK)
|
return {inset, inset, ART_WIDTH - 2 * inset, ART_HEIGHT - 2 * inset}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle flip animation
|
@(private)
|
||||||
if card.is_flipping {
|
new_art_texture :: proc(edge_color: raylib.Color) -> raylib.RenderTexture2D {
|
||||||
// Draw the flip animation
|
rt := raylib.LoadRenderTexture(ART_WIDTH, ART_HEIGHT)
|
||||||
flip_angle := card.flip_progress * 180.0
|
raylib.SetTextureFilter(rt.texture, .BILINEAR)
|
||||||
if !card.flip_direction {
|
raylib.BeginTextureMode(rt)
|
||||||
flip_angle = 180.0 - flip_angle
|
// Transparent, but with the edge's RGB so filtering doesn't create dark fringes
|
||||||
}
|
raylib.ClearBackground({edge_color.r, edge_color.g, edge_color.b, 0})
|
||||||
|
raylib.EndTextureMode()
|
||||||
|
return rt
|
||||||
|
}
|
||||||
|
|
||||||
// Draw the card as a rectangle that's rotating
|
@(private)
|
||||||
center_x := f32(card.x + themes.CARD_WIDTH / 2)
|
draw_frame :: proc(fill, border: raylib.Color) {
|
||||||
center_y := f32(card.y + themes.CARD_HEIGHT / 2)
|
raylib.DrawRectangleRounded(art_rect(), ROUNDNESS, SEGMENTS, fill)
|
||||||
|
raylib.DrawRectangleRoundedLinesEx(art_rect(3), ROUNDNESS, SEGMENTS, 6, border)
|
||||||
|
}
|
||||||
|
|
||||||
// For simplicity, just show the character when flip_progress > 0.5
|
// Japan: Hinomaru, a crimson sun disc on white, with a double crimson border
|
||||||
show_face := card.flip_progress > 0.5
|
@(private)
|
||||||
if card.flip_direction {
|
draw_japan_back :: proc() {
|
||||||
show_face = card.flip_progress >= 0.5
|
draw_frame(themes.WHITE, themes.HINOMARU_RED)
|
||||||
|
raylib.DrawRectangleRoundedLinesEx(art_rect(16), ROUNDNESS, SEGMENTS, 2, themes.HINOMARU_RED)
|
||||||
|
center := raylib.Vector2{ART_WIDTH / 2, ART_HEIGHT / 2}
|
||||||
|
raylib.DrawCircleV(center, ART_WIDTH * 0.3, themes.HINOMARU_RED)
|
||||||
|
}
|
||||||
|
|
||||||
|
// One Taegukgi trigram: three bars, each either solid or broken in the middle
|
||||||
|
@(private)
|
||||||
|
draw_trigram :: proc(center: raylib.Vector2, angle_deg: f32, solid: [3]bool, scale: f32) {
|
||||||
|
length := 46 * scale
|
||||||
|
thick := 8 * scale
|
||||||
|
gap := 6 * scale
|
||||||
|
rad := angle_deg * math.RAD_PER_DEG
|
||||||
|
along := raylib.Vector2{math.cos(rad), math.sin(rad)} // bar direction
|
||||||
|
across := raylib.Vector2{-along.y, along.x} // stacking direction
|
||||||
|
for i in 0 ..< 3 {
|
||||||
|
bar_center := center + across * (f32(i - 1) * (thick + gap))
|
||||||
|
if solid[i] {
|
||||||
|
raylib.DrawRectanglePro({bar_center.x, bar_center.y, length, thick}, {length / 2, thick / 2}, angle_deg, themes.BLACK)
|
||||||
} else {
|
} else {
|
||||||
show_face = card.flip_progress < 0.5
|
half := (length - gap) / 2
|
||||||
|
for side in ([2]f32{-1, 1}) {
|
||||||
|
c := bar_center + along * (side * (half + gap) / 2)
|
||||||
|
raylib.DrawRectanglePro({c.x, c.y, half, thick}, {half / 2, thick / 2}, angle_deg, themes.BLACK)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if show_face || card.is_face_up || card.is_matched {
|
|
||||||
font := assets.get_font()
|
|
||||||
text_w := raylib.MeasureText(card.character, 40)
|
|
||||||
text_x := card.x + (themes.CARD_WIDTH - text_w) / 2
|
|
||||||
text_y := card.y + (themes.CARD_HEIGHT - 40) / 2 - 8
|
|
||||||
raylib.DrawTextEx(font, card.character, raylib.Vector2{f32(text_x + 1), f32(text_y + 1)}, 40.0, 0, themes.BLACK)
|
|
||||||
raylib.DrawTextEx(font, card.character, raylib.Vector2{f32(text_x), f32(text_y)}, 40.0, 0, themes.WHITE)
|
|
||||||
}
|
|
||||||
} else if card.is_face_up || card.is_matched {
|
|
||||||
font := assets.get_font()
|
|
||||||
text_w := raylib.MeasureText(card.character, 40)
|
|
||||||
text_x := card.x + (themes.CARD_WIDTH - text_w) / 2
|
|
||||||
text_y := card.y + (themes.CARD_HEIGHT - 40) / 2 - 8
|
|
||||||
raylib.DrawTextEx(font, card.character, raylib.Vector2{f32(text_x + 1), f32(text_y + 1)}, 40.0, 0, themes.BLACK)
|
|
||||||
raylib.DrawTextEx(font, card.character, raylib.Vector2{f32(text_x), f32(text_y)}, 40.0, 0, themes.WHITE)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is_card_clicked :: proc(card: ^Card, mouse_x, mouse_y: i32) -> bool {
|
// Korea: Taegukgi, a red/blue taeguk circle with the four black trigrams in the corners
|
||||||
return mouse_x >= card.x && mouse_x <= card.x + themes.CARD_WIDTH &&
|
@(private)
|
||||||
mouse_y >= card.y && mouse_y <= card.y + themes.CARD_HEIGHT
|
draw_korea_back :: proc() {
|
||||||
|
draw_frame(themes.WHITE, themes.TAEGUK_BLUE)
|
||||||
|
center := raylib.Vector2{ART_WIDTH / 2, ART_HEIGHT / 2}
|
||||||
|
radius: f32 = ART_WIDTH * 0.26
|
||||||
|
// The flag's taeguk is tilted along the diagonal of a 3:2 rectangle
|
||||||
|
tilt := math.atan2(f32(2), 3) * math.DEG_PER_RAD
|
||||||
|
raylib.DrawCircleSector(center, radius, 180 + tilt, 360 + tilt, 48, themes.TAEGUK_RED)
|
||||||
|
raylib.DrawCircleSector(center, radius, tilt, 180 + tilt, 48, themes.TAEGUK_BLUE)
|
||||||
|
offset := raylib.Vector2{math.cos(tilt * math.RAD_PER_DEG), math.sin(tilt * math.RAD_PER_DEG)} * (radius / 2)
|
||||||
|
// Red head toward the upper-left (geon ☰), blue head toward the lower-right (gon ☷)
|
||||||
|
raylib.DrawCircleV(center - offset, radius / 2, themes.TAEGUK_RED)
|
||||||
|
raylib.DrawCircleV(center + offset, radius / 2, themes.TAEGUK_BLUE)
|
||||||
|
|
||||||
|
scale: f32 = 0.8
|
||||||
|
dx: f32 = ART_WIDTH * 0.24
|
||||||
|
dy: f32 = ART_HEIGHT * 0.26
|
||||||
|
draw_trigram(center + {-dx, -dy}, -45, {true, true, true}, scale) // geon ☰
|
||||||
|
draw_trigram(center + {dx, -dy}, 45, {false, true, false}, scale) // gam ☵
|
||||||
|
draw_trigram(center + {-dx, dy}, 45, {true, false, true}, scale) // ri ☲
|
||||||
|
draw_trigram(center + {dx, dy}, -45, {false, false, false}, scale) // gon ☷
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thailand: Thong Trairong, red-white-blue-white-red stripes (blue twice as wide)
|
||||||
|
@(private)
|
||||||
|
draw_thailand_back :: proc() {
|
||||||
|
draw_frame(themes.WHITE, themes.THAI_BLUE)
|
||||||
|
panel := art_rect(18)
|
||||||
|
unit := panel.height / 6
|
||||||
|
stripes := [5]struct{ color: raylib.Color, units: f32 }{
|
||||||
|
{themes.THAI_RED, 1}, {themes.WHITE, 1}, {themes.THAI_BLUE, 2}, {themes.WHITE, 1}, {themes.THAI_RED, 1},
|
||||||
|
}
|
||||||
|
y := panel.y
|
||||||
|
for stripe in stripes {
|
||||||
|
raylib.DrawRectangleRec({panel.x, y, panel.width, unit * stripe.units}, stripe.color)
|
||||||
|
y += unit * stripe.units
|
||||||
|
}
|
||||||
|
raylib.DrawRectangleLinesEx(panel, 2, themes.THAI_BLUE)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builds the card back textures. Call once after the window is created.
|
||||||
|
load_art :: proc() {
|
||||||
|
edges := [themes.Flag]raylib.Color{.JAPAN = themes.HINOMARU_RED, .KOREA = themes.TAEGUK_BLUE, .THAILAND = themes.THAI_BLUE}
|
||||||
|
for flag in themes.Flag {
|
||||||
|
back_art[flag] = new_art_texture(edges[flag])
|
||||||
|
raylib.BeginTextureMode(back_art[flag])
|
||||||
|
switch flag {
|
||||||
|
case .JAPAN: draw_japan_back()
|
||||||
|
case .KOREA: draw_korea_back()
|
||||||
|
case .THAILAND: draw_thailand_back()
|
||||||
|
}
|
||||||
|
raylib.EndTextureMode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unload_art :: proc() {
|
||||||
|
for rt in back_art { raylib.UnloadRenderTexture(rt) }
|
||||||
|
}
|
||||||
|
|
||||||
|
get_back_art :: proc(flag: themes.Flag) -> raylib.Texture2D {
|
||||||
|
return back_art[flag].texture
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renders the card's face into its own texture
|
||||||
|
build_face :: proc(card: ^Card, theme: themes.Theme) {
|
||||||
|
unload_face(card)
|
||||||
|
card.face_art = new_art_texture(theme.primary)
|
||||||
|
raylib.BeginTextureMode(card.face_art)
|
||||||
|
draw_frame(theme.card_face, theme.primary)
|
||||||
|
raylib.DrawRectangleRoundedLinesEx(art_rect(16), ROUNDNESS, SEGMENTS, 2, raylib.Fade(theme.primary, 0.35))
|
||||||
|
|
||||||
|
size: f32 = card.is_kana ? 132 : 96
|
||||||
|
color := card.is_kana ? theme.ink : theme.primary
|
||||||
|
// Shrink long romaji (e.g. "TSU") to fit inside the card
|
||||||
|
max_width: f32 = ART_WIDTH * 0.78
|
||||||
|
width := ui.measure_text(card.text, size, bold = true).x
|
||||||
|
if width > max_width { size *= max_width / width }
|
||||||
|
ui.draw_text_in_rect(card.text, art_rect(), size, color, bold = true)
|
||||||
|
raylib.EndTextureMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
unload_face :: proc(card: ^Card) {
|
||||||
|
if card.face_art.id != 0 {
|
||||||
|
raylib.UnloadRenderTexture(card.face_art)
|
||||||
|
card.face_art = {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Behaviour
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
is_animating :: proc(card: ^Card) -> bool {
|
||||||
|
target: f32 = card.face_up ? 1 : 0
|
||||||
|
return card.flip_t != target
|
||||||
|
}
|
||||||
|
|
||||||
|
update :: proc(card: ^Card, dt: f32) {
|
||||||
|
target: f32 = card.face_up ? 1 : 0
|
||||||
|
step := dt * FLIP_SPEED
|
||||||
|
if card.flip_t < target {
|
||||||
|
card.flip_t = min(card.flip_t + step, target)
|
||||||
|
} else if card.flip_t > target {
|
||||||
|
card.flip_t = max(card.flip_t - step, target)
|
||||||
|
}
|
||||||
|
if card.pulse_t > 0 {
|
||||||
|
card.pulse_t = max(card.pulse_t - dt, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
start_pulse :: proc(card: ^Card) {
|
||||||
|
card.pulse_t = PULSE_DURATION
|
||||||
|
}
|
||||||
|
|
||||||
|
is_hovered :: proc(card: ^Card) -> bool {
|
||||||
|
return raylib.CheckCollisionPointRec(raylib.GetMousePosition(), card.rect)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private)
|
||||||
|
smoothstep :: proc(t: f32) -> f32 {
|
||||||
|
return t * t * (3 - 2 * t)
|
||||||
|
}
|
||||||
|
|
||||||
|
draw :: proc(card: ^Card, theme: themes.Theme, clickable: bool) {
|
||||||
|
eased := smoothstep(card.flip_t)
|
||||||
|
squash := abs(math.cos(eased * math.PI)) // 1 → 0 → 1 across the flip
|
||||||
|
showing_face := eased >= 0.5
|
||||||
|
|
||||||
|
// Cards lift as they turn, and a little on hover
|
||||||
|
lift := math.sin(eased * math.PI) * 6
|
||||||
|
hovered := clickable && is_hovered(card)
|
||||||
|
if hovered {
|
||||||
|
lift += HOVER_LIFT
|
||||||
|
ui.set_hovered()
|
||||||
|
}
|
||||||
|
|
||||||
|
scale: f32 = 1
|
||||||
|
if card.pulse_t > 0 {
|
||||||
|
progress := 1 - card.pulse_t / PULSE_DURATION
|
||||||
|
scale += 0.09 * math.sin(progress * math.PI)
|
||||||
|
}
|
||||||
|
|
||||||
|
width := card.rect.width * scale
|
||||||
|
height := card.rect.height * scale
|
||||||
|
center := raylib.Vector2{card.rect.x + card.rect.width / 2, card.rect.y + card.rect.height / 2 - lift}
|
||||||
|
|
||||||
|
// Shadow stays on the table and spreads as the card lifts
|
||||||
|
shadow := raylib.Rectangle{
|
||||||
|
center.x - width * squash / 2 + 2,
|
||||||
|
card.rect.y + card.rect.height * (1 - scale) / 2 + 4 + lift * 0.15,
|
||||||
|
max(width * squash, 2),
|
||||||
|
height,
|
||||||
|
}
|
||||||
|
raylib.DrawRectangleRounded(shadow, ROUNDNESS, SEGMENTS, raylib.Fade(themes.BLACK, 0.12 - lift * 0.004))
|
||||||
|
|
||||||
|
// Darken the card while it is edge-on, to fake lighting
|
||||||
|
shade := u8(255 - (1 - squash) * 70)
|
||||||
|
tint := raylib.Color{shade, shade, shade, 255}
|
||||||
|
if hovered && !showing_face {
|
||||||
|
tint = raylib.ColorBrightness(tint, 0.08)
|
||||||
|
}
|
||||||
|
|
||||||
|
texture := showing_face ? card.face_art.texture : back_art[theme.flag].texture
|
||||||
|
source := raylib.Rectangle{0, 0, f32(texture.width), -f32(texture.height)} // render textures are upside down
|
||||||
|
dest := raylib.Rectangle{center.x, center.y, width * squash, height}
|
||||||
|
raylib.DrawTexturePro(texture, source, dest, {dest.width / 2, dest.height / 2}, 0, tint)
|
||||||
|
|
||||||
|
if card.matched && card.flip_t == 1 {
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+199
-180
@@ -2,213 +2,232 @@ package memory
|
|||||||
|
|
||||||
import "core:math/rand"
|
import "core:math/rand"
|
||||||
import raylib "vendor:raylib"
|
import raylib "vendor:raylib"
|
||||||
import themes "../themes"
|
import audio "../audio"
|
||||||
import card "../card"
|
import card "../card"
|
||||||
import assets "../assets"
|
import themes "../themes"
|
||||||
|
import ui "../ui"
|
||||||
|
|
||||||
MemoryState :: enum {
|
PAIR_COUNT :: 12
|
||||||
WAITING,
|
CARD_COUNT :: PAIR_COUNT * 2
|
||||||
FIRST,
|
|
||||||
SECOND,
|
MATCH_DELAY :: 0.35 // seconds both cards stay up before a match is confirmed
|
||||||
|
MISMATCH_DELAY :: 0.9 // seconds before a wrong pair flips back
|
||||||
|
|
||||||
|
State :: enum {
|
||||||
|
WAITING, // no card selected
|
||||||
|
FIRST, // one card selected
|
||||||
|
RESOLVING, // two cards up, waiting before match/mismatch
|
||||||
OVER,
|
OVER,
|
||||||
}
|
}
|
||||||
|
|
||||||
memory_cards: [24]card.Card
|
Kana :: struct {
|
||||||
memory_state: MemoryState
|
kana: cstring,
|
||||||
memory_first: i32
|
romaji: cstring,
|
||||||
memory_second: i32
|
|
||||||
memory_matches: i32
|
|
||||||
memory_moves: i32
|
|
||||||
memory_time: f32
|
|
||||||
memory_flip: f32
|
|
||||||
|
|
||||||
init_memory_game :: proc() {
|
|
||||||
hiragana_chars: [12]cstring = {"あ", "い", "う", "え", "お", "か", "き", "く", "け", "こ", "さ", "し"}
|
|
||||||
romanji_chars: [12]cstring = {"A", "I", "U", "E", "O", "KA", "KI", "KU", "KE", "KO", "SA", "SHI"}
|
|
||||||
|
|
||||||
for i := 0; i < 12; i += 1 {
|
|
||||||
memory_cards[i*2] = card.Card{
|
|
||||||
x = 0, y = 0,
|
|
||||||
character = romanji_chars[i],
|
|
||||||
is_face_up = false,
|
|
||||||
is_matched = false,
|
|
||||||
pair_id = i32(i),
|
|
||||||
flip_progress = 0.0,
|
|
||||||
is_flipping = false,
|
|
||||||
flip_direction = false,
|
|
||||||
}
|
|
||||||
memory_cards[i*2+1] = card.Card{
|
|
||||||
x = 0, y = 0,
|
|
||||||
character = romanji_chars[i],
|
|
||||||
is_face_up = false,
|
|
||||||
is_matched = false,
|
|
||||||
pair_id = i32(i),
|
|
||||||
flip_progress = 0.0,
|
|
||||||
is_flipping = false,
|
|
||||||
flip_direction = false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rand.shuffle(memory_cards[:])
|
|
||||||
|
|
||||||
total_w := themes.GRID_COLS * (themes.CARD_WIDTH + themes.CARD_SPACING) - themes.CARD_SPACING
|
|
||||||
start_x := (themes.SCREEN_WIDTH - total_w) / 2
|
|
||||||
start_y := themes.GRID_TOP
|
|
||||||
|
|
||||||
for i := 0; i < 24; i += 1 {
|
|
||||||
row := i32(i) / themes.GRID_COLS
|
|
||||||
col := i32(i) % themes.GRID_COLS
|
|
||||||
memory_cards[i].x = start_x + col * (themes.CARD_WIDTH + themes.CARD_SPACING)
|
|
||||||
memory_cards[i].y = start_y + row * (themes.CARD_HEIGHT + themes.CARD_SPACING)
|
|
||||||
}
|
|
||||||
|
|
||||||
memory_state = MemoryState.WAITING
|
|
||||||
memory_first = -1
|
|
||||||
memory_second = -1
|
|
||||||
memory_matches = 0
|
|
||||||
memory_moves = 0
|
|
||||||
memory_time = 0.0
|
|
||||||
memory_flip = 0.0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
update_memory_game :: proc(delta_time: f32, mouse_x, mouse_y: i32) {
|
// The 46 basic hiragana (gojūon)
|
||||||
memory_time += delta_time
|
HIRAGANA :: [?]Kana{
|
||||||
|
{"あ", "a"}, {"い", "i"}, {"う", "u"}, {"え", "e"}, {"お", "o"},
|
||||||
|
{"か", "ka"}, {"き", "ki"}, {"く", "ku"}, {"け", "ke"}, {"こ", "ko"},
|
||||||
|
{"さ", "sa"}, {"し", "shi"}, {"す", "su"}, {"せ", "se"}, {"そ", "so"},
|
||||||
|
{"た", "ta"}, {"ち", "chi"}, {"つ", "tsu"}, {"て", "te"}, {"と", "to"},
|
||||||
|
{"な", "na"}, {"に", "ni"}, {"ぬ", "nu"}, {"ね", "ne"}, {"の", "no"},
|
||||||
|
{"は", "ha"}, {"ひ", "hi"}, {"ふ", "fu"}, {"へ", "he"}, {"ほ", "ho"},
|
||||||
|
{"ま", "ma"}, {"み", "mi"}, {"む", "mu"}, {"め", "me"}, {"も", "mo"},
|
||||||
|
{"や", "ya"}, {"ゆ", "yu"}, {"よ", "yo"},
|
||||||
|
{"ら", "ra"}, {"り", "ri"}, {"る", "ru"}, {"れ", "re"}, {"ろ", "ro"},
|
||||||
|
{"わ", "wa"}, {"を", "wo"}, {"ん", "n"},
|
||||||
|
}
|
||||||
|
|
||||||
if memory_state == MemoryState.SECOND {
|
cards: [CARD_COUNT]card.Card
|
||||||
memory_flip += delta_time
|
state: State
|
||||||
if memory_flip >= themes.FLIP_DELAY {
|
theme: themes.Theme
|
||||||
if memory_first >= 0 && memory_second >= 0 {
|
first: int = -1
|
||||||
if memory_cards[memory_first].pair_id == memory_cards[memory_second].pair_id {
|
second: int = -1
|
||||||
memory_cards[memory_first].is_matched = true
|
matches: int
|
||||||
memory_cards[memory_second].is_matched = true
|
moves: int
|
||||||
memory_matches += 1
|
elapsed: f32
|
||||||
} else {
|
resolve_timer: f32
|
||||||
memory_cards[memory_first].is_flipping = true
|
win_t: f32 // fades in the win panel
|
||||||
memory_cards[memory_first].flip_direction = false
|
restart_requested: bool
|
||||||
memory_cards[memory_first].flip_progress = 0.0
|
|
||||||
memory_cards[memory_second].is_flipping = true
|
start :: proc(game_theme: themes.Theme) {
|
||||||
memory_cards[memory_second].flip_direction = false
|
theme = game_theme
|
||||||
memory_cards[memory_second].flip_progress = 0.0
|
|
||||||
}
|
deck := HIRAGANA
|
||||||
}
|
rand.shuffle(deck[:])
|
||||||
memory_state = MemoryState.WAITING
|
for i in 0 ..< PAIR_COUNT {
|
||||||
memory_flip = 0.0
|
card.unload_face(&cards[i * 2])
|
||||||
memory_first = -1
|
card.unload_face(&cards[i * 2 + 1])
|
||||||
memory_second = -1
|
cards[i * 2] = card.Card{text = deck[i].kana, is_kana = true, pair_id = i}
|
||||||
|
cards[i * 2 + 1] = card.Card{text = deck[i].romaji, is_kana = false, pair_id = i}
|
||||||
|
}
|
||||||
|
rand.shuffle(cards[:])
|
||||||
|
|
||||||
|
total_w: f32 = themes.GRID_COLS * (themes.CARD_WIDTH + themes.CARD_SPACING) - themes.CARD_SPACING
|
||||||
|
start_x := (themes.SCREEN_WIDTH - total_w) / 2
|
||||||
|
for &c, i in cards {
|
||||||
|
row := f32(i / themes.GRID_COLS)
|
||||||
|
col := f32(i % themes.GRID_COLS)
|
||||||
|
c.rect = {
|
||||||
|
start_x + col * (themes.CARD_WIDTH + themes.CARD_SPACING),
|
||||||
|
themes.GRID_TOP + row * (themes.CARD_HEIGHT + themes.CARD_SPACING),
|
||||||
|
themes.CARD_WIDTH,
|
||||||
|
themes.CARD_HEIGHT,
|
||||||
}
|
}
|
||||||
|
card.build_face(&c, theme)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update flip animations
|
state = .WAITING
|
||||||
for i := 0; i < 24; i += 1 {
|
first = -1
|
||||||
if memory_cards[i].is_flipping {
|
second = -1
|
||||||
memory_cards[i].flip_progress += delta_time * 3.0 // Flip speed
|
matches = 0
|
||||||
if memory_cards[i].flip_progress >= 1.0 {
|
moves = 0
|
||||||
memory_cards[i].flip_progress = 1.0
|
elapsed = 0
|
||||||
memory_cards[i].is_flipping = false
|
resolve_timer = 0
|
||||||
memory_cards[i].is_face_up = memory_cards[i].flip_direction
|
win_t = 0
|
||||||
}
|
restart_requested = false
|
||||||
|
}
|
||||||
|
|
||||||
|
unload :: proc() {
|
||||||
|
for &c in cards { card.unload_face(&c) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private)
|
||||||
|
can_pick :: proc(i: int) -> bool {
|
||||||
|
c := &cards[i]
|
||||||
|
return (state == .WAITING || state == .FIRST) && !c.face_up && !c.matched
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private)
|
||||||
|
pick :: proc(i: int) {
|
||||||
|
cards[i].face_up = true
|
||||||
|
audio.play(.FLIP)
|
||||||
|
if state == .WAITING {
|
||||||
|
first = i
|
||||||
|
state = .FIRST
|
||||||
|
} else {
|
||||||
|
second = i
|
||||||
|
moves += 1
|
||||||
|
is_match := cards[first].pair_id == cards[second].pair_id
|
||||||
|
resolve_timer = is_match ? MATCH_DELAY : MISMATCH_DELAY
|
||||||
|
state = .RESOLVING
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private)
|
||||||
|
resolve :: proc() {
|
||||||
|
a, b := &cards[first], &cards[second]
|
||||||
|
if a.pair_id == b.pair_id {
|
||||||
|
a.matched = true
|
||||||
|
b.matched = true
|
||||||
|
card.start_pulse(a)
|
||||||
|
card.start_pulse(b)
|
||||||
|
matches += 1
|
||||||
|
if matches == PAIR_COUNT {
|
||||||
|
state = .OVER
|
||||||
|
audio.play(.WIN)
|
||||||
|
} else {
|
||||||
|
audio.play(.MATCH)
|
||||||
|
state = .WAITING
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
a.face_up = false
|
||||||
|
b.face_up = false
|
||||||
|
audio.play(.MISMATCH)
|
||||||
|
audio.play(.FLIP)
|
||||||
|
state = .WAITING
|
||||||
|
}
|
||||||
|
first = -1
|
||||||
|
second = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
update :: proc(dt: f32) {
|
||||||
|
if restart_requested {
|
||||||
|
start(theme)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if memory_state == MemoryState.WAITING || memory_state == MemoryState.FIRST {
|
for &c in cards { card.update(&c, dt) }
|
||||||
for i := 0; i < 24; i += 1 {
|
|
||||||
if !memory_cards[i].is_matched && !memory_cards[i].is_flipping &&
|
switch state {
|
||||||
!memory_cards[i].is_face_up && i32(i) != memory_first {
|
case .WAITING, .FIRST:
|
||||||
if card.is_card_clicked(&memory_cards[i], mouse_x, mouse_y) &&
|
elapsed += dt
|
||||||
raylib.IsMouseButtonPressed(raylib.MouseButton.LEFT) {
|
if raylib.IsMouseButtonPressed(.LEFT) {
|
||||||
memory_cards[i].is_flipping = true
|
for &c, i in cards {
|
||||||
memory_cards[i].flip_direction = true
|
if can_pick(i) && card.is_hovered(&c) {
|
||||||
memory_cards[i].flip_progress = 0.0
|
pick(i)
|
||||||
if memory_first == -1 {
|
|
||||||
memory_first = i32(i)
|
|
||||||
memory_state = MemoryState.FIRST
|
|
||||||
} else {
|
|
||||||
memory_second = i32(i)
|
|
||||||
memory_moves += 1
|
|
||||||
memory_state = MemoryState.SECOND
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
case .RESOLVING:
|
||||||
|
elapsed += dt
|
||||||
if memory_matches >= 12 {
|
// Wait for the second card to finish turning before starting the delay
|
||||||
memory_state = MemoryState.OVER
|
if !card.is_animating(&cards[second]) {
|
||||||
|
resolve_timer -= dt
|
||||||
|
if resolve_timer <= 0 { resolve() }
|
||||||
|
}
|
||||||
|
case .OVER:
|
||||||
|
win_t = min(win_t + dt * 2.5, 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
draw_memory_game :: proc(mouse_x, mouse_y: i32) -> i32 {
|
// Draws the game and returns the screen to show next frame
|
||||||
raylib.ClearBackground(themes.JAPAN_BACKGROUND)
|
draw :: proc() -> themes.Screen {
|
||||||
|
next := themes.Screen.JAPANESE_MEMORY
|
||||||
|
raylib.ClearBackground(theme.background)
|
||||||
|
|
||||||
font := assets.get_font()
|
ui.draw_title("Hiragana Memory", 24, theme.primary, 48)
|
||||||
title: cstring = "Memory Game - Japanese"
|
minutes := int(elapsed) / 60
|
||||||
title_size: i32 = 48
|
seconds := int(elapsed) % 60
|
||||||
title_w := raylib.MeasureText(title, title_size)
|
info := raylib.TextFormat("Moves %d · Pairs %d/%d · %d:%02d", moves, matches, PAIR_COUNT, minutes, seconds)
|
||||||
title_x := (themes.SCREEN_WIDTH - title_w) / 2
|
ui.draw_text_centered(info, themes.SCREEN_WIDTH / 2, 88, 24, theme.ink)
|
||||||
raylib.DrawTextEx(font, title, raylib.Vector2{f32(title_x + 3), 33}, f32(title_size), 0, themes.BLACK)
|
|
||||||
raylib.DrawTextEx(font, title, raylib.Vector2{f32(title_x), 30}, f32(title_size), 0, themes.JAPAN_PRIMARY)
|
|
||||||
|
|
||||||
info_text := raylib.TextFormat("Moves: %d | Time: %.1fs", memory_moves, memory_time)
|
if state != .OVER && ui.button({30, 28, 120, 48}, "Back", theme.primary, font_size = 22) {
|
||||||
info_w := raylib.MeasureText(info_text, themes.SUBTITLE_FONT_SIZE)
|
next = .JAPANESE_MENU
|
||||||
info_x := (themes.SCREEN_WIDTH - info_w) / 2
|
|
||||||
raylib.DrawTextEx(font, info_text, raylib.Vector2{f32(info_x), 92}, f32(themes.SUBTITLE_FONT_SIZE), 0, themes.DARK_GRAY)
|
|
||||||
|
|
||||||
for i := 0; i < 24; i += 1 {
|
|
||||||
card.draw_card(&memory_cards[i], mouse_x, mouse_y, themes.JAPAN_PRIMARY)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
back_w: i32 = 120
|
for &c, i in cards {
|
||||||
back_h: i32 = 50
|
card.draw(&c, theme, can_pick(i))
|
||||||
back_x: i32 = 30
|
|
||||||
back_y: i32 = 30
|
|
||||||
back_hovered := mouse_x >= back_x && mouse_x <= back_x + back_w &&
|
|
||||||
mouse_y >= back_y && mouse_y <= back_y + back_h
|
|
||||||
back_color := themes.JAPAN_PRIMARY
|
|
||||||
if back_hovered {
|
|
||||||
back_color = themes.JAPAN_ACCENT
|
|
||||||
}
|
|
||||||
raylib.DrawRectangle(back_x, back_y, back_w, back_h, back_color)
|
|
||||||
raylib.DrawRectangleLines(back_x, back_y, back_w, back_h, themes.BLACK)
|
|
||||||
back_text: cstring = "Back"
|
|
||||||
back_text_w := raylib.MeasureText(back_text, themes.BUTTON_FONT_SIZE)
|
|
||||||
back_text_x := back_x + (back_w - back_text_w) / 2
|
|
||||||
back_text_y := back_y + (back_h - themes.BUTTON_FONT_SIZE) / 2
|
|
||||||
raylib.DrawTextEx(font, back_text, raylib.Vector2{f32(back_text_x), f32(back_text_y)}, f32(themes.BUTTON_FONT_SIZE), 0, themes.WHITE)
|
|
||||||
|
|
||||||
if back_hovered && raylib.IsMouseButtonPressed(raylib.MouseButton.LEFT) {
|
|
||||||
return themes.JAPANESE_SCREEN
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if memory_state == MemoryState.OVER {
|
if state == .OVER {
|
||||||
raylib.DrawRectangle(0, 0, themes.SCREEN_WIDTH, themes.SCREEN_HEIGHT, raylib.Color{0, 0, 0, 128})
|
if action := draw_win_panel(); action != nil {
|
||||||
|
next = action.?
|
||||||
win_text: cstring = "Congratulations! You Win!"
|
|
||||||
win_w := raylib.MeasureText(win_text, 48)
|
|
||||||
win_x := (themes.SCREEN_WIDTH - win_w) / 2
|
|
||||||
raylib.DrawTextEx(font, win_text, raylib.Vector2{f32(win_x), f32(themes.SCREEN_HEIGHT / 2 - 60)}, 48, 0, themes.WHITE)
|
|
||||||
|
|
||||||
stats_text := raylib.TextFormat("Moves: %d | Time: %.1fs", memory_moves, memory_time)
|
|
||||||
stats_w := raylib.MeasureText(stats_text, 32)
|
|
||||||
stats_x := (themes.SCREEN_WIDTH - stats_w) / 2
|
|
||||||
raylib.DrawTextEx(font, stats_text, raylib.Vector2{f32(stats_x), f32(themes.SCREEN_HEIGHT / 2)}, 32, 0, themes.WHITE)
|
|
||||||
|
|
||||||
again_text: cstring = "Play Again"
|
|
||||||
again_w := raylib.MeasureText(again_text, themes.BUTTON_FONT_SIZE)
|
|
||||||
again_x := (themes.SCREEN_WIDTH - again_w) / 2
|
|
||||||
again_y := themes.SCREEN_HEIGHT / 2 + 60
|
|
||||||
again_hovered := mouse_x >= again_x && mouse_x <= again_x + again_w &&
|
|
||||||
mouse_y >= again_y && mouse_y <= again_y + themes.BUTTON_FONT_SIZE
|
|
||||||
|
|
||||||
again_color := themes.JAPAN_PRIMARY
|
|
||||||
if again_hovered {
|
|
||||||
again_color = themes.JAPAN_ACCENT
|
|
||||||
}
|
|
||||||
raylib.DrawTextEx(font, again_text, raylib.Vector2{f32(again_x), f32(again_y)}, f32(themes.BUTTON_FONT_SIZE), 0, again_color)
|
|
||||||
|
|
||||||
if again_hovered && raylib.IsMouseButtonPressed(raylib.MouseButton.LEFT) {
|
|
||||||
init_memory_game()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return themes.JAPANESE_MEMORY
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private)
|
||||||
|
draw_win_panel :: proc() -> Maybe(themes.Screen) {
|
||||||
|
raylib.DrawRectangle(0, 0, themes.SCREEN_WIDTH, themes.SCREEN_HEIGHT, raylib.Fade(themes.BLACK, 0.45 * win_t))
|
||||||
|
|
||||||
|
panel_w: f32 = 560
|
||||||
|
panel_h: f32 = 340
|
||||||
|
slide := (1 - win_t) * 40
|
||||||
|
panel := raylib.Rectangle{(themes.SCREEN_WIDTH - panel_w) / 2, (themes.SCREEN_HEIGHT - panel_h) / 2 + slide, panel_w, panel_h}
|
||||||
|
raylib.DrawRectangleRounded({panel.x, panel.y + 8, panel.width, panel.height}, 0.12, 16, raylib.Fade(themes.BLACK, 0.25 * win_t))
|
||||||
|
raylib.DrawRectangleRounded(panel, 0.12, 16, raylib.Fade(theme.card_face, win_t))
|
||||||
|
raylib.DrawRectangleRoundedLinesEx(panel, 0.12, 16, 4, raylib.Fade(theme.primary, win_t))
|
||||||
|
|
||||||
|
cx := panel.x + panel_w / 2
|
||||||
|
ui.draw_text_centered("おめでとう!", cx, panel.y + 34, 56, raylib.Fade(theme.primary, win_t), bold = true)
|
||||||
|
ui.draw_text_centered("Congratulations, all pairs found!", cx, panel.y + 112, 26, raylib.Fade(theme.ink, win_t))
|
||||||
|
minutes := int(elapsed) / 60
|
||||||
|
seconds := int(elapsed) % 60
|
||||||
|
stats := raylib.TextFormat("%d moves · %d:%02d", moves, minutes, seconds)
|
||||||
|
ui.draw_text_centered(stats, cx, panel.y + 156, 26, raylib.Fade(theme.ink, win_t), bold = true)
|
||||||
|
|
||||||
|
if win_t < 1 { return nil }
|
||||||
|
button_w: f32 = 200
|
||||||
|
button_y := panel.y + panel_h - 100
|
||||||
|
if ui.button({cx - button_w - 10, button_y, button_w, 60}, "Play Again", theme.primary) {
|
||||||
|
restart_requested = true
|
||||||
|
}
|
||||||
|
if ui.button({cx + 10, button_y, button_w, 60}, "Menu", theme.ink) {
|
||||||
|
return .JAPANESE_MENU
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
+85
-87
@@ -1,113 +1,111 @@
|
|||||||
package screens
|
package screens
|
||||||
|
|
||||||
|
import "core:math"
|
||||||
import raylib "vendor:raylib"
|
import raylib "vendor:raylib"
|
||||||
|
import card "../card"
|
||||||
import themes "../themes"
|
import themes "../themes"
|
||||||
import assets "../assets"
|
import ui "../ui"
|
||||||
|
|
||||||
draw_button :: proc(x, y, w, h: i32, text: cstring, base_color, hover_color: raylib.Color, mouse_x, mouse_y: i32) -> bool {
|
LANGUAGES :: [3]struct{ theme: themes.Theme, screen: themes.Screen }{
|
||||||
is_hovered := mouse_x >= x && mouse_x <= x + w && mouse_y >= y && mouse_y <= y + h
|
{themes.JAPAN, .JAPANESE_MENU},
|
||||||
color := base_color
|
{themes.KOREA, .KOREAN_MENU},
|
||||||
if is_hovered {
|
{themes.THAI, .THAI_MENU},
|
||||||
color = hover_color
|
|
||||||
}
|
|
||||||
raylib.DrawRectangle(x, y, w, h, color)
|
|
||||||
raylib.DrawRectangleLines(x, y, w, h, themes.BLACK)
|
|
||||||
font := assets.get_font()
|
|
||||||
text_w := raylib.MeasureText(text, themes.BUTTON_FONT_SIZE)
|
|
||||||
text_x := x + (w - text_w) / 2
|
|
||||||
text_y := y + (h - themes.BUTTON_FONT_SIZE) / 2
|
|
||||||
raylib.DrawTextEx(font, text, raylib.Vector2{f32(text_x), f32(text_y)}, f32(themes.BUTTON_FONT_SIZE), 0, themes.WHITE)
|
|
||||||
return is_hovered && raylib.IsMouseButtonPressed(raylib.MouseButton.LEFT)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
draw_title :: proc(text: cstring, y: i32, color: raylib.Color) {
|
// Draws a card back like a playing card lying on the table
|
||||||
font := assets.get_font()
|
@(private)
|
||||||
text_w := raylib.MeasureText(text, themes.TITLE_FONT_SIZE)
|
draw_card_art :: proc(flag: themes.Flag, rect: raylib.Rectangle, rotation: f32 = 0) {
|
||||||
x := (themes.SCREEN_WIDTH - text_w) / 2
|
texture := card.get_back_art(flag)
|
||||||
raylib.DrawTextEx(font, text, raylib.Vector2{f32(x + 4), f32(y + 4)}, f32(themes.TITLE_FONT_SIZE), 0, themes.BLACK)
|
source := raylib.Rectangle{0, 0, f32(texture.width), -f32(texture.height)}
|
||||||
raylib.DrawTextEx(font, text, raylib.Vector2{f32(x), f32(y)}, f32(themes.TITLE_FONT_SIZE), 0, color)
|
dest := raylib.Rectangle{rect.x + rect.width / 2, rect.y + rect.height / 2, rect.width, rect.height}
|
||||||
|
origin := raylib.Vector2{rect.width / 2, rect.height / 2}
|
||||||
|
shadow := dest
|
||||||
|
shadow.x += 3
|
||||||
|
shadow.y += 6
|
||||||
|
raylib.DrawRectanglePro(shadow, origin, rotation, raylib.Fade(themes.BLACK, 0.15))
|
||||||
|
raylib.DrawTexturePro(texture, source, dest, origin, rotation, themes.WHITE)
|
||||||
}
|
}
|
||||||
|
|
||||||
draw_centered_text :: proc(text: cstring, y: i32, font_size: i32, color: raylib.Color) {
|
main_menu :: proc() -> themes.Screen {
|
||||||
font := assets.get_font()
|
next := themes.Screen.MAIN_MENU
|
||||||
text_w := raylib.MeasureText(text, font_size)
|
raylib.ClearBackground(themes.MENU_BACKGROUND)
|
||||||
x := (themes.SCREEN_WIDTH - text_w) / 2
|
|
||||||
raylib.DrawTextEx(font, text, raylib.Vector2{f32(x + 4), f32(y + 4)}, f32(font_size), 0, themes.BLACK)
|
// Thin stripes along the top in each language's primary color
|
||||||
raylib.DrawTextEx(font, text, raylib.Vector2{f32(x), f32(y)}, f32(font_size), 0, color)
|
stripe_w: i32 = themes.SCREEN_WIDTH / 3
|
||||||
|
for lang, i in LANGUAGES {
|
||||||
|
raylib.DrawRectangle(i32(i) * stripe_w, 0, stripe_w, 8, lang.theme.primary)
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.draw_title("HanjaKanaMun", 70, themes.SUMI_INK, 84)
|
||||||
|
ui.draw_text_centered("Learn Japanese, Korean and Thai scripts", themes.SCREEN_WIDTH / 2, 170, themes.SUBTITLE_FONT_SIZE, themes.DARK_GRAY)
|
||||||
|
|
||||||
|
tile_w: f32 = 150
|
||||||
|
tile_h: f32 = 200
|
||||||
|
gap: f32 = 110
|
||||||
|
total := 3 * tile_w + 2 * gap
|
||||||
|
start_x := (themes.SCREEN_WIDTH - total) / 2
|
||||||
|
// Gentle bobbing so the menu feels alive
|
||||||
|
time := f32(raylib.GetTime())
|
||||||
|
for lang, i in LANGUAGES {
|
||||||
|
x := start_x + f32(i) * (tile_w + gap)
|
||||||
|
bob := math.sin(time * 1.6 + f32(i) * 1.1) * 4
|
||||||
|
rotation := f32(i - 1) * 4
|
||||||
|
draw_card_art(lang.theme.flag, {x, 250 + bob, tile_w, tile_h}, rotation)
|
||||||
|
button_rect := raylib.Rectangle{x + tile_w / 2 - themes.BUTTON_WIDTH / 2 + 30, 500, themes.BUTTON_WIDTH - 60, themes.BUTTON_HEIGHT}
|
||||||
|
if ui.button(button_rect, lang.theme.name, lang.theme.primary) {
|
||||||
|
next = lang.screen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.draw_text_centered("Esc to go back · Esc on this screen quits", themes.SCREEN_WIDTH / 2, 660, 18, raylib.Fade(themes.DARK_GRAY, 0.7))
|
||||||
|
return next
|
||||||
}
|
}
|
||||||
|
|
||||||
draw_main_menu :: proc(mouse_x, mouse_y: i32) -> i32 {
|
// Shared layout for a language's menu screen. Returns the clicked option index, -1 for back, or nil.
|
||||||
raylib.ClearBackground(themes.WHITE)
|
@(private)
|
||||||
|
language_menu :: proc(theme: themes.Theme, subtitle: cstring, options: []cstring) -> Maybe(int) {
|
||||||
|
raylib.ClearBackground(theme.background)
|
||||||
|
raylib.DrawRectangle(0, 0, themes.SCREEN_WIDTH, 8, theme.primary)
|
||||||
|
|
||||||
draw_title("HanjaKanaMun", 100, themes.JAPAN_PRIMARY)
|
ui.draw_title(theme.name, 70, theme.primary)
|
||||||
draw_centered_text("Learn Japanese, Korean, and Thai", 180, themes.SUBTITLE_FONT_SIZE, themes.DARK_GRAY)
|
ui.draw_text_centered(subtitle, themes.SCREEN_WIDTH / 2, 170, themes.SUBTITLE_FONT_SIZE, theme.ink)
|
||||||
|
|
||||||
button_w := themes.BUTTON_WIDTH
|
// A small fan of card backs
|
||||||
button_h := themes.BUTTON_HEIGHT
|
for i in 0 ..< 3 {
|
||||||
button_space: i32 = 40
|
offset := f32(i - 1)
|
||||||
total_w := button_w * 3 + button_space * 2
|
draw_card_art(theme.flag, {themes.SCREEN_WIDTH / 2 - 55 + offset * 70, 235 + abs(offset) * 10, 110, 146}, offset * 9)
|
||||||
start_x := (themes.SCREEN_WIDTH - total_w) / 2
|
|
||||||
start_y := themes.SCREEN_HEIGHT / 2 + 100
|
|
||||||
|
|
||||||
if draw_button(start_x, start_y, button_w, button_h, "Japanese", themes.JAPAN_PRIMARY, themes.JAPAN_ACCENT, mouse_x, mouse_y) {
|
|
||||||
return themes.JAPANESE_SCREEN
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if draw_button(start_x + button_w + button_space, start_y, button_w, button_h, "Korean", themes.KOREA_PRIMARY, themes.KOREA_ACCENT, mouse_x, mouse_y) {
|
result: Maybe(int)
|
||||||
return themes.KOREAN_SCREEN
|
y: f32 = 440
|
||||||
|
for option, i in options {
|
||||||
|
if ui.button({(themes.SCREEN_WIDTH - themes.BUTTON_WIDTH) / 2, y, themes.BUTTON_WIDTH, themes.BUTTON_HEIGHT}, option, theme.primary) {
|
||||||
|
result = i
|
||||||
|
}
|
||||||
|
y += themes.BUTTON_HEIGHT + 20
|
||||||
}
|
}
|
||||||
|
if ui.button({(themes.SCREEN_WIDTH - themes.BUTTON_WIDTH) / 2, y, themes.BUTTON_WIDTH, themes.BUTTON_HEIGHT}, "Back", theme.ink) {
|
||||||
if draw_button(start_x + i32(button_w) + i32(button_space) + i32(button_w) + i32(button_space), start_y, button_w, button_h, "Thai", themes.THAI_PRIMARY, themes.THAI_ACCENT, mouse_x, mouse_y) {
|
result = -1
|
||||||
return themes.THAI_SCREEN
|
|
||||||
}
|
}
|
||||||
|
return result
|
||||||
return themes.MAIN_MENU
|
|
||||||
}
|
}
|
||||||
|
|
||||||
draw_japanese_screen :: proc(mouse_x, mouse_y: i32) -> i32 {
|
japanese_menu :: proc() -> themes.Screen {
|
||||||
raylib.ClearBackground(themes.WHITE)
|
choice, clicked := language_menu(themes.JAPAN, "ひらがな · Hiragana", {"Memory Game"}).?
|
||||||
|
if !clicked { return .JAPANESE_MENU }
|
||||||
draw_title("Japanese", 100, themes.JAPAN_PRIMARY)
|
return choice == 0 ? .JAPANESE_MEMORY : .MAIN_MENU
|
||||||
draw_centered_text("Select a game to play", 180, themes.SUBTITLE_FONT_SIZE, themes.DARK_GRAY)
|
|
||||||
|
|
||||||
button_w := themes.BUTTON_WIDTH
|
|
||||||
button_h := themes.BUTTON_HEIGHT
|
|
||||||
button_space := 40
|
|
||||||
start_y := themes.SCREEN_HEIGHT / 2
|
|
||||||
start_x := (themes.SCREEN_WIDTH - button_w) / 2
|
|
||||||
|
|
||||||
if draw_button(start_x, start_y, button_w, button_h, "Memory Game", themes.JAPAN_PRIMARY, themes.JAPAN_ACCENT, mouse_x, mouse_y) {
|
|
||||||
return themes.JAPANESE_MEMORY
|
|
||||||
}
|
|
||||||
|
|
||||||
back_y := i32(start_y) + i32(button_h) + i32(button_space)
|
|
||||||
if draw_button(start_x, back_y, button_w, button_h, "Back", themes.JAPAN_PRIMARY, themes.JAPAN_ACCENT, mouse_x, mouse_y) {
|
|
||||||
return themes.MAIN_MENU
|
|
||||||
}
|
|
||||||
|
|
||||||
return themes.JAPANESE_SCREEN
|
|
||||||
}
|
}
|
||||||
|
|
||||||
draw_korean_screen :: proc(mouse_x, mouse_y: i32) -> i32 {
|
korean_menu :: proc() -> themes.Screen {
|
||||||
raylib.ClearBackground(themes.WHITE)
|
if _, clicked := language_menu(themes.KOREA, "Hangul · coming soon", {}).?; clicked {
|
||||||
draw_title("Korean", 100, themes.KOREA_PRIMARY)
|
return .MAIN_MENU
|
||||||
draw_centered_text("Coming Soon", 180, themes.SUBTITLE_FONT_SIZE, themes.DARK_GRAY)
|
|
||||||
|
|
||||||
if draw_button((themes.SCREEN_WIDTH - themes.BUTTON_WIDTH) / 2, themes.SCREEN_HEIGHT / 2, themes.BUTTON_WIDTH, themes.BUTTON_HEIGHT, "Back", themes.KOREA_PRIMARY, themes.KOREA_ACCENT, mouse_x, mouse_y) {
|
|
||||||
return themes.MAIN_MENU
|
|
||||||
}
|
}
|
||||||
|
return .KOREAN_MENU
|
||||||
return themes.KOREAN_SCREEN
|
|
||||||
}
|
}
|
||||||
|
|
||||||
draw_thai_screen :: proc(mouse_x, mouse_y: i32) -> i32 {
|
thai_menu :: proc() -> themes.Screen {
|
||||||
raylib.ClearBackground(themes.WHITE)
|
if _, clicked := language_menu(themes.THAI, "Thai script · coming soon", {}).?; clicked {
|
||||||
draw_title("Thai", 100, themes.THAI_PRIMARY)
|
return .MAIN_MENU
|
||||||
draw_centered_text("Coming Soon", 180, themes.SUBTITLE_FONT_SIZE, themes.DARK_GRAY)
|
|
||||||
|
|
||||||
if draw_button((themes.SCREEN_WIDTH - themes.BUTTON_WIDTH) / 2, themes.SCREEN_HEIGHT / 2, themes.BUTTON_WIDTH, themes.BUTTON_HEIGHT, "Back", themes.THAI_PRIMARY, themes.THAI_ACCENT, mouse_x, mouse_y) {
|
|
||||||
return themes.MAIN_MENU
|
|
||||||
}
|
}
|
||||||
|
return .THAI_MENU
|
||||||
return themes.THAI_SCREEN
|
|
||||||
}
|
}
|
||||||
+84
-33
@@ -2,40 +2,91 @@ package themes
|
|||||||
|
|
||||||
import raylib "vendor:raylib"
|
import raylib "vendor:raylib"
|
||||||
|
|
||||||
SCREEN_WIDTH: i32 = 1280
|
SCREEN_WIDTH :: 1280
|
||||||
SCREEN_HEIGHT: i32 = 720
|
SCREEN_HEIGHT :: 720
|
||||||
|
|
||||||
JAPAN_PRIMARY: raylib.Color = raylib.Color{230, 57, 70, 255}
|
Screen :: enum {
|
||||||
JAPAN_ACCENT: raylib.Color = raylib.Color{253, 184, 190, 255}
|
MAIN_MENU,
|
||||||
JAPAN_BACKGROUND: raylib.Color = raylib.Color{250, 245, 245, 255}
|
JAPANESE_MENU,
|
||||||
KOREA_PRIMARY: raylib.Color = raylib.Color{0, 71, 160, 255}
|
JAPANESE_MEMORY,
|
||||||
KOREA_ACCENT: raylib.Color = raylib.Color{180, 180, 255, 255}
|
KOREAN_MENU,
|
||||||
KOREA_BACKGROUND: raylib.Color = raylib.Color{245, 248, 255, 255}
|
THAI_MENU,
|
||||||
THAI_PRIMARY: raylib.Color = raylib.Color{165, 25, 49, 255}
|
}
|
||||||
THAI_ACCENT: raylib.Color = raylib.Color{255, 180, 200, 255}
|
|
||||||
THAI_BACKGROUND: raylib.Color = raylib.Color{255, 245, 248, 255}
|
|
||||||
WHITE: raylib.Color = raylib.Color{255, 255, 255, 255}
|
|
||||||
BLACK: raylib.Color = raylib.Color{0, 0, 0, 255}
|
|
||||||
DARK_GRAY: raylib.Color = raylib.Color{64, 64, 64, 255}
|
|
||||||
LIGHT_GRAY: raylib.Color = raylib.Color{200, 200, 200, 255}
|
|
||||||
CARD_BACK_COLOR: raylib.Color = raylib.Color{40, 40, 50, 255}
|
|
||||||
|
|
||||||
BUTTON_WIDTH: i32 = 240
|
// Which national flag the card backs are modelled on
|
||||||
BUTTON_HEIGHT: i32 = 60
|
Flag :: enum {
|
||||||
TITLE_FONT_SIZE: i32 = 72
|
JAPAN, // Nisshōki / Hinomaru
|
||||||
SUBTITLE_FONT_SIZE: i32 = 24
|
KOREA, // Taegukgi
|
||||||
BUTTON_FONT_SIZE: i32 = 24
|
THAILAND, // Thong Trairong
|
||||||
|
}
|
||||||
|
|
||||||
CARD_WIDTH: i32 = 90
|
Theme :: struct {
|
||||||
CARD_HEIGHT: i32 = 120
|
name: cstring,
|
||||||
CARD_SPACING: i32 = 12
|
flag: Flag,
|
||||||
GRID_TOP: i32 = 140
|
primary: raylib.Color, // main brand color (buttons, titles)
|
||||||
GRID_ROWS: i32 = 4
|
secondary: raylib.Color, // second flag color
|
||||||
GRID_COLS: i32 = 6
|
accent: raylib.Color, // highlights, matched outlines
|
||||||
FLIP_DELAY: f32 = 1.0
|
background: raylib.Color,
|
||||||
|
card_face: raylib.Color,
|
||||||
|
ink: raylib.Color, // body text
|
||||||
|
}
|
||||||
|
|
||||||
MAIN_MENU: i32 = 0
|
// Flag colors
|
||||||
JAPANESE_SCREEN: i32 = 1
|
HINOMARU_RED :: raylib.Color{188, 0, 45, 255}
|
||||||
JAPANESE_MEMORY: i32 = 2
|
TAEGUK_RED :: raylib.Color{205, 46, 58, 255}
|
||||||
KOREAN_SCREEN: i32 = 3
|
TAEGUK_BLUE :: raylib.Color{0, 71, 160, 255}
|
||||||
THAI_SCREEN: i32 = 4
|
THAI_RED :: raylib.Color{165, 25, 49, 255}
|
||||||
|
THAI_BLUE :: raylib.Color{45, 42, 74, 255}
|
||||||
|
|
||||||
|
WHITE :: raylib.Color{255, 255, 255, 255}
|
||||||
|
BLACK :: raylib.Color{0, 0, 0, 255}
|
||||||
|
SUMI_INK :: raylib.Color{35, 31, 32, 255}
|
||||||
|
DARK_GRAY :: raylib.Color{80, 76, 72, 255}
|
||||||
|
LIGHT_GRAY :: raylib.Color{200, 200, 200, 255}
|
||||||
|
MENU_BACKGROUND :: raylib.Color{248, 246, 242, 255}
|
||||||
|
|
||||||
|
JAPAN :: Theme{
|
||||||
|
name = "Japanese",
|
||||||
|
flag = .JAPAN,
|
||||||
|
primary = HINOMARU_RED,
|
||||||
|
secondary = WHITE,
|
||||||
|
accent = raylib.Color{212, 160, 23, 255}, // kin (gold)
|
||||||
|
background = raylib.Color{250, 246, 238, 255}, // washi paper
|
||||||
|
card_face = raylib.Color{255, 252, 245, 255},
|
||||||
|
ink = SUMI_INK,
|
||||||
|
}
|
||||||
|
|
||||||
|
KOREA :: Theme{
|
||||||
|
name = "Korean",
|
||||||
|
flag = .KOREA,
|
||||||
|
primary = TAEGUK_BLUE,
|
||||||
|
secondary = TAEGUK_RED,
|
||||||
|
accent = raylib.Color{30, 30, 30, 255}, // trigram black
|
||||||
|
background = raylib.Color{244, 246, 251, 255},
|
||||||
|
card_face = raylib.Color{255, 255, 255, 255},
|
||||||
|
ink = raylib.Color{22, 24, 35, 255},
|
||||||
|
}
|
||||||
|
|
||||||
|
THAI :: Theme{
|
||||||
|
name = "Thai",
|
||||||
|
flag = .THAILAND,
|
||||||
|
primary = THAI_RED,
|
||||||
|
secondary = THAI_BLUE,
|
||||||
|
accent = raylib.Color{218, 165, 32, 255}, // temple gold
|
||||||
|
background = raylib.Color{251, 246, 240, 255},
|
||||||
|
card_face = raylib.Color{255, 253, 248, 255},
|
||||||
|
ink = raylib.Color{30, 28, 45, 255},
|
||||||
|
}
|
||||||
|
|
||||||
|
BUTTON_WIDTH :: 240
|
||||||
|
BUTTON_HEIGHT :: 60
|
||||||
|
TITLE_FONT_SIZE :: 72
|
||||||
|
SUBTITLE_FONT_SIZE :: 26
|
||||||
|
BUTTON_FONT_SIZE :: 26
|
||||||
|
|
||||||
|
CARD_WIDTH :: 96
|
||||||
|
CARD_HEIGHT :: 128
|
||||||
|
CARD_SPACING :: 14
|
||||||
|
GRID_TOP :: 136
|
||||||
|
GRID_ROWS :: 4
|
||||||
|
GRID_COLS :: 6
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import raylib "vendor:raylib"
|
||||||
|
import assets "../assets"
|
||||||
|
import audio "../audio"
|
||||||
|
import themes "../themes"
|
||||||
|
|
||||||
|
TEXT_SPACING :: 1
|
||||||
|
|
||||||
|
@(private)
|
||||||
|
any_hovered: bool
|
||||||
|
|
||||||
|
// Call at the start of each frame's drawing
|
||||||
|
begin_frame :: proc() {
|
||||||
|
any_hovered = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call at the end of each frame's drawing
|
||||||
|
end_frame :: proc() {
|
||||||
|
raylib.SetMouseCursor(any_hovered ? .POINTING_HAND : .DEFAULT)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark that something clickable is under the mouse (shows the hand cursor)
|
||||||
|
set_hovered :: proc() {
|
||||||
|
any_hovered = true
|
||||||
|
}
|
||||||
|
|
||||||
|
measure_text :: proc(text: cstring, size: f32, bold := false) -> raylib.Vector2 {
|
||||||
|
return raylib.MeasureTextEx(assets.get_font(bold), text, size, TEXT_SPACING)
|
||||||
|
}
|
||||||
|
|
||||||
|
draw_text :: proc(text: cstring, pos: raylib.Vector2, size: f32, color: raylib.Color, bold := false) {
|
||||||
|
raylib.DrawTextEx(assets.get_font(bold), text, pos, size, TEXT_SPACING, color)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draws text horizontally centered on center_x with its top at y
|
||||||
|
draw_text_centered :: proc(text: cstring, center_x, y, size: f32, color: raylib.Color, bold := false, shadow := false) {
|
||||||
|
w := measure_text(text, size, bold).x
|
||||||
|
pos := raylib.Vector2{center_x - w / 2, y}
|
||||||
|
if shadow {
|
||||||
|
draw_text(text, pos + {0, size * 0.05}, size, raylib.Fade(themes.BLACK, 0.18), bold)
|
||||||
|
}
|
||||||
|
draw_text(text, pos, size, color, bold)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draws text centered in a rectangle
|
||||||
|
draw_text_in_rect :: proc(text: cstring, rect: raylib.Rectangle, size: f32, color: raylib.Color, bold := false) {
|
||||||
|
dims := measure_text(text, size, bold)
|
||||||
|
pos := raylib.Vector2{rect.x + (rect.width - dims.x) / 2, rect.y + (rect.height - dims.y) / 2}
|
||||||
|
draw_text(text, pos, size, color, bold)
|
||||||
|
}
|
||||||
|
|
||||||
|
draw_title :: proc(text: cstring, y: f32, color: raylib.Color, size: f32 = themes.TITLE_FONT_SIZE) {
|
||||||
|
draw_text_centered(text, themes.SCREEN_WIDTH / 2, y, size, color, bold = true, shadow = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A rounded button with hover, press and shadow states. Returns true when clicked.
|
||||||
|
button :: proc(rect: raylib.Rectangle, text: cstring, color: raylib.Color,
|
||||||
|
text_color := themes.WHITE, font_size: f32 = themes.BUTTON_FONT_SIZE) -> bool {
|
||||||
|
hovered := raylib.CheckCollisionPointRec(raylib.GetMousePosition(), rect)
|
||||||
|
pressed := hovered && raylib.IsMouseButtonDown(.LEFT)
|
||||||
|
if hovered { set_hovered() }
|
||||||
|
|
||||||
|
lift: f32 = 0
|
||||||
|
fill := color
|
||||||
|
if hovered {
|
||||||
|
lift = 2
|
||||||
|
fill = raylib.ColorBrightness(color, 0.12)
|
||||||
|
}
|
||||||
|
if pressed {
|
||||||
|
lift = -1
|
||||||
|
fill = raylib.ColorBrightness(color, -0.08)
|
||||||
|
}
|
||||||
|
|
||||||
|
shadow := raylib.Rectangle{rect.x, rect.y + 4, rect.width, rect.height}
|
||||||
|
raylib.DrawRectangleRounded(shadow, 0.4, 12, raylib.Fade(themes.BLACK, 0.18))
|
||||||
|
body := raylib.Rectangle{rect.x, rect.y - lift, rect.width, rect.height}
|
||||||
|
raylib.DrawRectangleRounded(body, 0.4, 12, fill)
|
||||||
|
draw_text_in_rect(text, body, font_size, text_color, bold = true)
|
||||||
|
|
||||||
|
clicked := hovered && raylib.IsMouseButtonPressed(.LEFT)
|
||||||
|
if clicked { audio.play(.CLICK) }
|
||||||
|
return clicked
|
||||||
|
}
|
||||||
Reference in new issue
Block a user