Files
2026-07-15 01:28:56 -04:00

141 lines
2.9 KiB
Go

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
}