v0.1.0 initial commit
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user