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
+1046
-425
No files matched your search
@@ -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])
|
||||
}
|
||||
Reference in new issue
Block a user