v0.1.0 initial commit

This commit is contained in:
2026-07-15 01:28:56 -04:00
commit 874abee663
35 changed files with 3486 additions and 0 deletions
+91
View File
@@ -0,0 +1,91 @@
//go:build linux
package main
import (
"time"
rl "github.com/gen2brain/raylib-go/raylib"
"symph/audio"
"symph/game"
"symph/input"
raylibrender "symph/render/raylib"
)
const (
defaultScreenW = 800
defaultScreenH = 600
)
func main() {
rl.SetConfigFlags(rl.FlagWindowResizable | rl.FlagVsyncHint)
rl.InitWindow(defaultScreenW, defaultScreenH, "Symph")
defer rl.CloseWindow()
rl.SetTargetFPS(60)
state := game.New()
sfx := audio.NewMuter(audio.NewEngine())
defer sfx.Close()
renderer := raylibrender.NewRenderer(state)
for !rl.WindowShouldClose() {
now := time.Now()
// Modifier state is polled, not queued
ctrl := rl.IsKeyDown(rl.KeyLeftControl) || rl.IsKeyDown(rl.KeyRightControl)
for {
k := rl.GetKeyPressed()
if k == 0 {
break
}
if k == rl.KeyS && ctrl {
renderer.Muted = sfx.Toggle()
continue
}
if ik := keyFromRaylib(k); ik != input.KeyNone {
state.Dispatch(input.Translate(ik), now)
}
}
state.Update(now)
for _, ge := range state.DrainEvents() {
sfx.Play(audio.MapGameEvent(ge))
}
rl.BeginDrawing()
renderer.Draw(int32(rl.GetScreenWidth()), int32(rl.GetScreenHeight()), now)
rl.EndDrawing()
}
}
// keyFromRaylib translates a raylib keycode to a platform-neutral input.Key
func keyFromRaylib(k int32) input.Key {
switch k {
case rl.KeyH:
return input.KeyH
case rl.KeyJ:
return input.KeyJ
case rl.KeyK:
return input.KeyK
case rl.KeyL:
return input.KeyL
case rl.KeySemicolon:
return input.KeySemicolon
case rl.KeyA:
return input.KeyA
case rl.KeyS:
return input.KeyS
case rl.KeyD:
return input.KeyD
case rl.KeyF:
return input.KeyF
case rl.KeySpace:
return input.KeySpace
case rl.KeyEnter, rl.KeyKpEnter:
return input.KeyEnter
}
return input.KeyNone
}
+140
View File
@@ -0,0 +1,140 @@
package main
import (
"fmt"
"os"
"time"
"symph/audio"
"symph/game"
"symph/input"
"symph/parameter"
"symph/render"
"github.com/lixenwraith/terminal"
)
func main() {
// Terminal init
term := terminal.New()
if err := term.Init(); err != nil {
fmt.Fprintln(os.Stderr, "terminal init:", err)
os.Exit(1)
}
defer term.Fini()
// Game init
state := game.New()
// Audio init (silent fallback when backend unavailable)
sfx := audio.NewMuter(audio.NewEngine())
defer sfx.Close()
// Render init
width, height := term.Size()
renderer := render.NewRenderer(term, width, height, state)
minW, minH := render.MinSize()
// Ticker wakes the event loop for UI frames (~60fps)
// The engine strictly owns beat quantization via Update(); frontend just pushes time forward
ticker := time.NewTicker(parameter.GameRenderUpdate)
defer ticker.Stop()
go func() {
for range ticker.C {
term.PostEvent(terminal.Event{Type: terminal.EventKey, Key: terminal.KeyNone})
}
}()
renderer.Render(time.Now())
for {
ev := term.PollEvent()
switch ev.Type {
case terminal.EventClosed, terminal.EventError:
return
case terminal.EventKey:
// Quit on Escape, Ctrl+C, or 'q'
if ev.Key == terminal.KeyEscape || ev.Key == terminal.KeyCtrlC || ev.Rune == 'q' || ev.Rune == 'Q' {
return
}
// Toggle mute with Ctrl+S
if ev.Key == terminal.KeyCtrlS {
renderer.Muted = sfx.Toggle()
}
now := time.Now()
// Viewport gate. Below the minimum the field is unrenderable;
// freeze the simulation clock so no beats elapse behind the notice
if w, h := term.Size(); w < minW || h < minH {
state.Pause(now)
renderer.Render(now)
continue
}
state.Resume(now)
// Real key: translate and dispatch with strict temporal timestamp
if k := eventToKey(ev); k != input.KeyNone {
state.Dispatch(input.Translate(k), now)
}
// Engine time step, event drain, render cycle every iteration
// (Synthetic tick falls through to here naturally)
state.Update(now)
for _, ge := range state.DrainEvents() {
sfx.Play(audio.MapGameEvent(ge))
}
renderer.Render(now)
case terminal.EventResize:
renderer.Resize(ev.Width, ev.Height)
now := time.Now()
if ev.Width < minW || ev.Height < minH {
state.Pause(now)
} else {
state.Resume(now)
}
renderer.Render(time.Now())
}
}
}
// eventToKey translates a terminal key event to a platform-neutral input.Key
func eventToKey(ev terminal.Event) input.Key {
if ev.Key == terminal.KeyEnter {
return input.KeyEnter
}
switch ev.Rune {
case 'h', 'H':
return input.KeyH
case 'j', 'J':
return input.KeyJ
case 'k', 'K':
return input.KeyK
case 'l', 'L':
return input.KeyL
case ';':
return input.KeySemicolon
case 'a', 'A':
return input.KeyA
case 's', 'S':
return input.KeyS
case 'd', 'D':
return input.KeyD
case 'f', 'F':
return input.KeyF
case ' ':
return input.KeySpace
}
return input.KeyNone
}