141 lines
2.9 KiB
Go
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
|
|
}
|