package flow
import (
"github.com/unitoftime/ecs"
)
// type Stage uint8
// const (
// StageStartup Stage = iota
// StagePreFixedUpdate
// StageFixedUpdate
// StagePostFixedUpdate
// StageUpdate
// )
type App struct {
world *ecs.World
scheduler *ecs.Scheduler
}
func NewApp() *App {
world := ecs.NewWorld()
scheduler := ecs.NewScheduler(world)
ecs.PutResource(world, scheduler)
// scheduler.SetFixedTimeStep(4 * time.Millisecond)
app := &App{
world: world,
scheduler: scheduler,
}
return app
}
func (a *App) Run() {
// fmt.Printf("%+v", a.startupSystems)
// fmt.Printf("%+v", a.scheduler)
a.scheduler.Run()
}
type Plugin interface {
Initialize(world *ecs.World)
}
func (a *App) AddPlugin(plugin Plugin) {
plugin.Initialize(a.world)
}
func (a *App) AddSystems(stage ecs.Stage, systems ...ecs.SystemBuilder) {
a.scheduler.AddSystems(stage, systems...)
// for _, sys := range systems {
// system := sys.Build(a.world)
// switch stage {
// case StageStartup:
// a.startupSystems = append(a.startupSystems, system)
// case StageFixedUpdate:
// a.scheduler.AppendPhysics(system)
// case StageUpdate:
// a.scheduler.AppendRender(system)
// }
// }
}
// func (a *App) AddSystems2(stage Stage, systems ...ecs.System) {
// for _, system := range systems {
// switch stage {
// case StageStartup:
// a.startupSystems = append(a.startupSystems, system)
// case StageFixedUpdate:
// a.scheduler.AppendPhysics(system)
// case StageUpdate:
// a.scheduler.AppendRender(system)
// }
// }
// }
// func (a *App) AddSystems(stage Stage, systems ...func(*ecs.World) ecs.System) {
// for _, sys := range systems {
// system := sys(a.world)
// switch stage {
// case StageStartup:
// a.startupSystems = append(a.startupSystems, system)
// case StageFixedUpdate:
// a.scheduler.AppendPhysics(system)
// case StageUpdate:
// a.scheduler.AppendRender(system)
// }
// }
// }
// func (a *App) SetSystems(stage Stage, systems ...func(*ecs.World) ecs.System) {
// for _, sys := range systems {
// system := sys(a.world)
// switch stage {
// case StageStartup:
// a.startupSystems = append(a.startupSystems, system)
// case StageFixedUpdate:
// a.scheduler.AppendPhysics(system)
// case StageUpdate:
// a.scheduler.AppendRender(system)
// }
// }
// }
// // func AddSystems1[A ecs.Initializer](a *App, stage Stage, lambda func(time.Duration, A)) {
// // system := ecs.NewSystem1(a.world, lambda)
// // switch stage {
// // case StageStartup:
// // a.startupSystems = append(a.startupSystems, system)
// // case StageFixedUpdate:
// // a.scheduler.AppendPhysics(system)
// // case StageUpdate:
// // a.scheduler.AppendRender(system)
// // }
// // }
// func AddResource[T any](a *App, t *T) {
// ecs.PutResource(a.world, t)
// }
// func System1[A ecs.Initializer](sysFunc func(time.Duration, A)) func(*ecs.World) ecs.System {
// return func(world *ecs.World) ecs.System {
// ecs.NewSystem1(world, sysFunc)
// }
// }
// func System[A, B ecs.Initializer](sysFunc func(time.Duration, A, B)) func(*ecs.World) ecs.System {
// return func(world *ecs.World) ecs.System {
// ecs.NewSystem2(world, sysFunc)
// }
// }
package serde
import (
"errors"
"fmt"
"reflect"
"github.com/unitoftime/gotiny"
)
func Register[T any](value T) {
// gob.Register(value)
gotiny.Register(value)
}
func RegisterName[T any](name string, value T) {
// gob.RegisterName(name, value)
gotiny.RegisterName(name, reflect.TypeOf(value))
}
func Marshal[T any](t T) (data []byte, err error) {
defer func() {
// Warning: Defer can only set named return parameters
if r := recover(); r != nil {
err = extractError(r)
}
}()
data = gotiny.Marshal(&t)
return data, err
}
func Unmarshal[T any](dat []byte) (t T, err error) {
defer func() {
// Warning: Defer can only set named return parameters
if r := recover(); r != nil {
err = extractError(r)
}
}()
gotiny.Unmarshal(dat, &t)
return t, err
}
func extractError(r any) error {
switch x := r.(type) {
case string:
return errors.New(x)
case error:
return x
default:
return errors.New(fmt.Sprintf("unknown panic type: %t", x))
}
}
// func Marshal[T any](t T) ([]byte, error) {
// var dat bytes.Buffer
// // enc := gob.NewEncoder(&dat)
// enc := stablegob.NewEncoder(&dat)
// err := enc.Encode(t)
// if err != nil {
// return nil, err
// }
// return dat.Bytes(), nil
// }
// func Unmarshal[T any](dat []byte) (T, error) {
// dec := stablegob.NewDecoder(bytes.NewReader(dat))
// // dec := gob.NewDecoder(bytes.NewReader(dat))
// var t T
// err := dec.Decode(&t)
// if err != nil {
// return t, err
// }
// return t, err
// }
// func Register[T any](value T) {
// register(value)
// }
// func RegisterName[T any](name string, value T) {
// registerName(name, value)
// }
// func Marshal[T any](t T) ([]byte, error) {
// m := Encode(t)
// return json.Marshal(m)
// }
// func Unmarshal[T any](dat []byte) (T, error) {
// var t T
// m := make(map[string]any)
// err := json.Unmarshal(dat, &m)
// if err != nil {
// return t, err
// }
// Decode(&t, m)
// return t, err
// }
package asset
import (
"errors"
"fmt"
"io"
"io/fs"
"net/http"
"net/url"
"os"
"path"
"strings"
"sync"
"sync/atomic"
"time"
)
// TODO: if I wanted this to be more "ecs-like" I would make a resource per asset type then use some kind of integer handle (ie `type Handle[T] uint32` or something). Then I use that handle to index into the asset type resource (ie `assets.Get(handle)` and `assets := ecs.GetResource[T](world)`)
// TODO: Finalizers on handles to deallocate assets that are no longer used?
type Handle[T any] struct {
ptr atomic.Pointer[T]
Name string
err error
doneChan chan struct{}
done atomic.Bool
modTime time.Time
generation atomic.Int32
}
func newHandle[T any](name string) *Handle[T] {
return &Handle[T]{
Name: name,
doneChan: make(chan struct{}),
}
}
func (h *Handle[T]) Gen() int {
return int(h.generation.Load())
}
func (h *Handle[T]) SetErr(err error) {
h.err = err
h.done.Store(true)
h.generation.Add(1)
}
func (h *Handle[T]) Set(val *T) {
h.err = nil
h.ptr.Store(val)
h.done.Store(true)
h.generation.Add(1) // Note: Do this last so that everything else is in place when we swap generations
}
func (h *Handle[T]) Get() (*T, error) {
h.Wait()
return h.ptr.Load(), h.err
}
func (h *Handle[T]) Err() error {
return h.err
}
// Returns true if the asset is done loading
// At this point either an error or the asset will be available
func (h *Handle[T]) Done() bool {
return h.done.Load()
}
// Blocks until the handle, or an error is set
func (h *Handle[T]) Wait() {
<-h.doneChan
}
type assetHandler interface{}
type Loader[T any] interface {
Ext() []string
Load(*Server, []byte) (*T, error)
Store(*Server, *T) ([]byte, error)
}
type Filesystem struct {
path string
fs fs.FS // TODO: Maybe use: https://pkg.go.dev/github.com/ungerik/go-fs
prefix string // dynamically added when registered
}
func NewFilesystem(path string, fsys fs.FS) Filesystem {
return Filesystem{path, fsys, ""}
}
func (fsys *Filesystem) getModTime(fpath string) (time.Time, error) {
// TODO: Wont work for networked files
file, err := fsys.fs.Open(fpath)
if err != nil {
return time.Time{}, err
}
info, err := file.Stat()
if err != nil {
return time.Time{}, err
}
return info.ModTime(), nil
}
type Server struct {
// fsPath string
// filesystem fs.FS // TODO: Maybe use: https://pkg.go.dev/github.com/ungerik/go-fs
mu sync.Mutex
fsMap map[string]Filesystem // Maps a prefix to a filesystem
extToLoader map[string]any // Map file extension strings to the loader that loads them
nameToHandle map[string]assetHandler // Map the full filepath name to the asset handle
}
// func NewServerFromPath(fsPath string) *Server {
// filesystem := os.DirFS(fsPath)
// return &Server{
// // fsPath: fsPath,
// // filesystem: filesystem,
// extToLoader: make(map[string]any),
// nameToHandle: make(map[string]assetHandler),
// }
// }
// func NewServer(filesystem fs.FS) *Server {
// return &Server{
// filesystem: filesystem,
// extToLoader: make(map[string]any),
// nameToHandle: make(map[string]assetHandler),
// }
// }
func NewServer() *Server {
return &Server{
fsMap: make(map[string]Filesystem), // TODO: Would be faster to be a prefix tree
extToLoader: make(map[string]any),
nameToHandle: make(map[string]assetHandler),
}
}
func (s *Server) RegisterFilesystem(prefix string, fs Filesystem) {
s.mu.Lock()
defer s.mu.Unlock()
_, ok := s.fsMap[prefix]
if ok {
panic("flow:asset: failed to register filesystem, prefix already registered")
}
fs.prefix = prefix
s.fsMap[prefix] = fs
}
func getScheme(path string) string {
u, err := url.Parse(path)
if err != nil {
return ""
}
return u.Scheme
}
func (s *Server) getFilesystem(fpath string) (Filesystem, string, bool) {
for prefix, fs := range s.fsMap {
if !strings.HasPrefix(fpath, prefix) {
continue
}
return fs, strings.TrimPrefix(fpath, prefix), true
}
return Filesystem{}, "", false
}
func (s *Server) getModTime(fpath string) (time.Time, error) {
fsys, trimmedPath, ok := s.getFilesystem(fpath)
if !ok {
return time.Time{}, fmt.Errorf("Couldnt find file prefix: %s", fpath)
}
file, err := fsys.fs.Open(trimmedPath)
if err != nil {
return time.Time{}, err
}
defer file.Close()
info, err := file.Stat()
if err != nil {
return time.Time{}, err
}
return info.ModTime(), nil
// // TODO: Wont work for networked files
// file, err := s.filesystem.Open(fpath)
// if err != nil { return time.Time{}, err }
// info, err := file.Stat()
// if err != nil { return time.Time{}, err }
// return info.ModTime(), nil
}
func (s *Server) ReadRaw(fpath string) ([]byte, time.Time, error) {
scheme := getScheme(fpath)
var rc io.ReadCloser
var err error
var modTime time.Time
if scheme == "https" || scheme == "http" {
rc, err = s.getHttp(fpath)
} else {
rc, modTime, err = s.getFile(fpath)
}
if err != nil {
return nil, modTime, err
}
defer rc.Close()
dat, err := io.ReadAll(rc)
return dat, modTime, err
}
func (s *Server) getFile(fpath string) (io.ReadCloser, time.Time, error) {
fsys, trimmedPath, ok := s.getFilesystem(fpath)
if !ok {
return nil, time.Time{}, fmt.Errorf("Couldnt find file prefix: %s", fpath)
}
// TODO: I'd prefer this to be more explicit
// If the FS is nil, then try a web-based filesystem
if fsys.fs == nil {
httpPath, err := url.JoinPath(fsys.path, trimmedPath)
if err != nil {
return nil, time.Time{}, err
}
rc, err := s.getHttp(httpPath)
return rc, time.Time{}, err
}
file, err := fsys.fs.Open(trimmedPath)
if err != nil {
return nil, time.Time{}, err
}
info, err := file.Stat()
if err != nil {
return nil, time.Time{}, err
}
return file, info.ModTime(), nil
// file, err := s.filesystem.Open(fpath)
// if err != nil { return nil, time.Time{}, err }
// info, err := file.Stat()
// if err != nil { return nil, time.Time{}, err }
// return file, info.ModTime(), nil
}
func (s *Server) getHttp(fpath string) (io.ReadCloser, error) {
resp, err := http.Get(fpath)
if err != nil {
return nil, err
}
httpSuccess := (resp.StatusCode >= 200 && resp.StatusCode <= 299)
if httpSuccess || resp.StatusCode == http.StatusNotModified {
return resp.Body, nil
}
return nil, errors.New(fmt.Sprintf("unable to fetch http status code: %d", resp.StatusCode))
}
func (s *Server) WriteRaw(fpath string, dat []byte) error {
fsys, trimmedPath, ok := s.getFilesystem(fpath)
if !ok {
return fmt.Errorf("Couldnt find file prefix: %s", fpath)
}
fullFilepath := path.Join(fsys.path, trimmedPath)
// Build entire filepath
err := os.MkdirAll(path.Dir(fullFilepath), 0750)
if err != nil {
return err
}
// TODO: verify file is writable.
return os.WriteFile(fullFilepath, dat, 0755)
// // file, err := s.filesystem.Open(fpath)
// // if err != nil {
// // return nil, err
// // }
// // defer file.Close()
// fullFilepath := filepath.Join(s.fsPath, fpath)
// // Build entire filepath
// err := os.MkdirAll(filepath.Dir(fullFilepath), 0750)
// if err != nil {
// return err
// }
// // TODO: verify file is writable.
// return os.WriteFile(fullFilepath, dat, 0755)
}
func Register[T any](s *Server, loader Loader[T]) {
extensions := loader.Ext()
for _, ext := range extensions {
_, exists := s.extToLoader[ext]
if exists {
panic(fmt.Sprintf("duplicate loader registration: %s", ext))
}
s.extToLoader[ext] = loader
}
}
// TODO: Extension filters?
// TODO: Should this return a single handle that gives us access to subhandles in the directory?
// Loads a directory that contains the same asset type. Returns a slice filled with all asset handles. Does not search recursively
func LoadDir[T any](server *Server, fpath string, recursive bool) []*Handle[T] {
fsys, trimmedPath, ok := server.getFilesystem(fpath)
if !ok {
return nil // TODO!!! : You're just snuffing an error here, which obviously isn't good
}
fpath = path.Clean(trimmedPath)
dirEntries, err := fs.ReadDir(fsys.fs, fpath)
if err != nil {
return nil // TODO!!! : You're just snuffing an error here, which obviously isn't good
}
ret := make([]*Handle[T], 0, len(dirEntries))
for _, e := range dirEntries {
if e.IsDir() {
if !recursive {
continue
}
dirPath := path.Join(fsys.prefix, fpath, e.Name())
dirHandles := LoadDir[T](server, dirPath, recursive)
ret = append(ret, dirHandles...)
continue
}
handle := Load[T](server, path.Join(fsys.prefix, fpath, e.Name()))
ret = append(ret, handle)
}
return ret
}
// Gets the handle, returns true if the handle has already started loading
func getHandle[T any](server *Server, name string) (*Handle[T], bool) {
server.mu.Lock()
defer server.mu.Unlock()
// Check if already loaded
anyHandle, ok := server.nameToHandle[name]
if ok {
handle := anyHandle.(*Handle[T])
return handle, true
}
handle := newHandle[T](name)
server.nameToHandle[name] = handle
return handle, false
}
// Loads a single file
func Load[T any](server *Server, name string) *Handle[T] {
handle, loaded := getHandle[T](server, name)
if loaded {
return handle
}
// Find a loader for it
ext := getExtension(name)
anyLoader, ok := server.extToLoader[ext]
if !ok {
handle.SetErr(fmt.Errorf("could not find loader for extension: %s (%s)", ext, name))
close(handle.doneChan)
return handle
}
loader, ok := anyLoader.(Loader[T])
if !ok {
handle.SetErr(fmt.Errorf("wrong type for registered loader on extension: %s", ext))
close(handle.doneChan)
return handle
}
go func() {
// TODO: Recover?
defer func() {
handle.done.Store(true)
close(handle.doneChan)
}()
data, modTime, err := server.ReadRaw(name)
if err != nil {
handle.err = err
return
}
handle.modTime = modTime // TODO: Data race here if reload is called simultaneously with load
val, err := loader.Load(server, data)
if err != nil {
handle.err = err
return
}
handle.Set(val)
}()
// Success
return handle
}
// Loads a single file
func Reload[T any](server *Server, handle *Handle[T]) {
if !handle.Done() {
return
} // If its still loading, then don't try to reload
name := handle.Name
// Find a loader for it
ext := getExtension(name)
anyLoader, ok := server.extToLoader[ext]
if !ok {
panic(fmt.Sprintf("could not find loader for extension: %s", ext))
}
loader, ok := anyLoader.(Loader[T])
if !ok {
panic(fmt.Sprintf("wrong type for registered loader on extension: %s", ext))
}
go func() {
// TODO: Recover?
modTime, err := server.getModTime(name)
if err != nil {
handle.err = err
return
}
if handle.modTime.Equal(modTime) {
// Same file, don't reload
return
}
data, modTime, err := server.ReadRaw(name)
if err != nil {
handle.err = err
return
}
handle.modTime = modTime
val, err := loader.Load(server, data)
if err != nil {
handle.err = err
return
}
handle.Set(val)
}()
}
// Writes the asset handle back to the file
func Store[T any](server *Server, handle *Handle[T]) error {
name := handle.Name
// Find a loader for it
ext := getExtension(name)
anyLoader, ok := server.extToLoader[ext]
if !ok {
panic(fmt.Sprintf("could not find loader for extension: %s", ext))
}
loader, ok := anyLoader.(Loader[T])
if !ok {
panic(fmt.Sprintf("wrong type for registered loader on extension: %s", ext))
}
val, _ := handle.Get()
// Note: We skip error checking here b/c we really dont care if there was an error loading. All we want to do is write data to a file. Hence val shouldn't be nil
if val == nil {
return fmt.Errorf("handle data can't be nil when storing")
}
// if err != nil {
// return err
// }
dat, err := loader.Store(server, val)
if err != nil {
return err
}
return server.WriteRaw(handle.Name, dat)
}
func getExtension(name string) string {
idx := -1
for i := len(name) - 1; i >= 0; i-- {
if name[i] == '/' {
break
}
if name[i] == '.' {
idx = i
}
}
if idx > 0 {
return name[idx:]
}
return ""
// Note: Does't properly cut slashes
// _, ext, found := strings.Cut(name, ".")
// if !found {
// ext = name
// }
// Note: Only returns the very final extension
// ext := filepath.Ext(name)
}
// func LoadAsset[T any](server *Server, name string) *Handle[T] {
// handle := newHandle[T](name)
// }
// Note: This is more like how bevy works
// type UntypedHandle uint64
// type Handle[T any] struct {
// UntypedHandle
// }
// type Asset struct {
// Error error
// Value any
// }
// type Loader interface {
// Ext() []string
// Load(data []byte) (any, error)
// }
// type Server struct {
// load *Load
// extToLoader map[string]Loader
// nameToHandle map[string]UntypedHandle
// assets []Asset
// }
// func NewServer(load *Load) *Server {
// return &Server{
// load: load,
// extToLoader: make(map[string]Loader),
// nameToHandle: make(map[string]UntypedHandle),
// assets: make([]Asset, 0),
// }
// }
// func (s *Server) Register(loader Loader) {
// extensions := loader.Ext()
// for _, ext := range extensions {
// _, exists := s.extToLoader[ext]
// if exists {
// panic(fmt.Sprintf("duplicate loader registration: %s", ext))
// }
// s.extToLoader[ext] = loader
// }
// }
// func (s *Server) addAsset(name string) (*Asset, UntypedHandle) {
// s.assets = append(s.assets, Asset{})
// handle := UntypedHandle(len(s.assets) - 1)
// s.nameToHandle[name] = handle
// return &s.assets[handle], handle
// }
// func (s *Server) LoadUntyped(name string) UntypedHandle {
// // Check if already loaded
// handle, ok := s.nameToHandle[name]
// if ok {
// return handle
// }
// // Find a loader for it
// _, ext, found := strings.Cut(name, ".")
// if !found {
// ext = name
// }
// loader, ok := s.extToLoader["."+ext]
// if !ok {
// panic(fmt.Sprintf("could not find loader for extension: %s", ext))
// }
// asset, handle := s.addAsset(name)
// // TODO: load dynamically (maybe chan?)
// data, err := s.load.Data(name)
// if err != nil {
// asset.Error = err
// return handle
// }
// loadedVal, err := loader.Load(data)
// if err != nil {
// asset.Error = err
// return handle
// }
// // Success
// asset.Value = loadedVal
// return handle
// }
// func (s *Server) Get(handle UntypedHandle) (any, error) {
// asset := s.assets[handle]
// return asset.Value, asset.Error
// }
// func LoadAsset[T any](server *Server, name string) Handle[T] {
// uHandle := server.LoadUntyped(name)
// return Handle[T]{uHandle}
// }
// func GetAsset[T any](server *Server, handle Handle[T]) (T, error) {
// asset, err := server.Get(handle.UntypedHandle)
// if err != nil {
// var t T
// return t, err
// }
// asset.
// }
package audio
import (
"time"
"github.com/unitoftime/beep"
"github.com/unitoftime/beep/effects"
"github.com/unitoftime/beep/speaker"
)
// type Settings struct {
// Loop bool
// // Playback mode: once, loop, despawn (entity once source completes), remove (remove audio components once sound completes
// // Volume float64
// // Speed float64
// // Paused bool
// }
type Channel struct {
mixer *beep.Mixer
ctrl *beep.Ctrl
volume *effects.Volume
}
func NewChannel() *Channel {
mixer := &beep.Mixer{}
ctrl := &beep.Ctrl{
Streamer: mixer,
Paused: false,
}
volume := &effects.Volume{
Streamer: ctrl,
Base: 2,
Volume: 0,
Silent: false,
}
return &Channel{
mixer: mixer,
ctrl: ctrl,
volume: volume,
}
}
func (c *Channel) Add(channels ...*Channel) {
if c == nil {
return
}
for _, channel := range channels {
if channel == nil {
return
}
// TODO: Prevent the same channel from being added multiple times?
c.add(channel.volume)
}
}
// Get the number of sources currently playing
func (c *Channel) NumSources() int {
if c == nil {
return 0
}
speaker.Lock()
length := c.mixer.Len()
speaker.Unlock()
return length
}
func (c *Channel) add(streamer beep.Streamer) {
if streamer == nil {
return
}
speaker.Lock()
c.mixer.Add(streamer)
speaker.Unlock()
}
func (c *Channel) PlayOnly(src *Source, loop bool) {
if c == nil {
return
}
if src == nil {
return
}
go func() {
speaker.Lock()
c.mixer.Clear()
speaker.Unlock()
streamer, err := src.Streamer()
if err != nil {
return
} // TODO: Snuffed error message
if !loop {
c.add(streamer)
return
}
// Note: -1 indicates to loop forever
looper := beep.Loop(-1, streamer)
c.add(looper)
}()
}
// func (c *Channel) PlayStreamer(streamer beep.Streamer) {
// if c == nil { return }
// go func() {
// c.add(streamer)
// }()
// }
func (c *Channel) Play(src *Source) {
if c == nil {
return
}
if src == nil {
return
}
// TODO: You need to pass these via a channel/queue to execute on some other thread. The speaker locks for miliseconds at a time
go func() {
streamer, err := src.Streamer()
if err != nil {
return
} // TODO: Snuffed error message
c.add(streamer)
}()
}
// func (c *Channel) Paused() bool {
// if c == nil { return false }
// return c.ctrl.Paused
// }
// func (c *Channel) Pause() {
// if c == nil { return }
// speaker.Lock()
// c.ctrl.Paused = true
// speaker.Unlock()
// }
// func (c *Channel) Unpause() {
// if c == nil { return }
// speaker.Lock()
// c.ctrl.Paused = false
// speaker.Unlock()
// }
func (c *Channel) SetMute(val bool) {
if c == nil {
return
}
speaker.Lock()
c.volume.Silent = val
speaker.Unlock()
}
func (c *Channel) SetVolume(val float64) {
if c == nil {
return
}
speaker.Lock()
c.volume.Volume = val
speaker.Unlock()
}
func (c *Channel) Mute() {
if c == nil {
return
}
speaker.Lock()
c.volume.Silent = true
speaker.Unlock()
}
func (c *Channel) Unmute() {
if c == nil {
return
}
speaker.Lock()
c.volume.Silent = false
speaker.Unlock()
}
func (c *Channel) Muted() bool {
if c == nil {
return false
}
return c.volume.Silent
}
func (c *Channel) AddVolume(val float64) {
if c == nil {
return
}
speaker.Lock()
c.volume.Volume += val
speaker.Unlock()
}
func (c *Channel) Volume() float64 {
if c == nil {
return 0
}
return c.volume.Volume
}
// fmpeg -i input.mp3 -c:a libvorbis -q:a 0 -b:a 44100 output.ogg
var defaultSampleRate = beep.SampleRate(44100)
var MasterChannel *Channel
func Initialize() error {
err := speaker.Init(defaultSampleRate,
4*defaultSampleRate.N(time.Second/60)) // Buffer length of 4 * (1/60) of a second
if err != nil {
return err
}
MasterChannel = NewChannel()
speaker.Play(MasterChannel.volume)
return nil
}
package audio
import (
"bytes"
"errors"
"io"
"github.com/unitoftime/beep"
"github.com/unitoftime/beep/vorbis"
"github.com/unitoftime/flow/asset"
)
func newSource(data []byte) *Source {
return &Source{
data: data,
}
}
type Source struct {
data []byte
buffer *beep.Buffer // TODO: Would be nice to buffer short sound effects
}
// Buffers the audio source into a pre-decoded audio buffer.
// Useful for short sound effects where you play them frequently and they dont take much memory.
// This will reduce CPU usage and increase memory usage
func (s *Source) Buffer() {
if s == nil {
return
}
if s.buffer != nil {
return
} // Skip if we've already buffered
reader := bytes.NewReader(s.data)
streamer, format, err := vorbis.Decode(fakeCloser{reader})
if err != nil {
return // TODO: How to handle this error?
}
buffer := beep.NewBuffer(format)
buffer.Append(streamer)
s.buffer = buffer
}
// Returns an audio streamer for the audio source
func (s *Source) Streamer() (beep.StreamSeeker, error) {
// If we've buffered this audio source, then use that
if s.buffer != nil {
return s.buffer.Streamer(0, s.buffer.Len()), nil
}
// Else create a decoder for the audio stream
reader := bytes.NewReader(s.data)
streamer, _, err := vorbis.Decode(fakeCloser{reader})
if err != nil {
return nil, errors.New("unable to decode streamer as vorbis")
}
return streamer, nil
}
type AssetLoader struct {
// TODO: Target Sample Rate
}
func (l AssetLoader) Ext() []string {
return []string{".ogg"} //, ".wav"} // TODO: //, "opus", "mp3"}
}
func (l AssetLoader) Load(server *asset.Server, data []byte) (*Source, error) {
source := newSource(data)
// // Note: This just verifies the data can be turned into a streamer. TODO: maybe wastes some allocations
// _, err := source.Streamer()
// if err != nil {
// return nil, err
// }
// TODO: Would be nice to have streaming connections
// TODO: Would be nice to support other formats
return source, nil
}
func (l AssetLoader) Store(server *asset.Server, audio *Source) ([]byte, error) {
return nil, errors.New("audio files do not support writeback")
}
type fakeCloser struct {
// stop bool // TODO: Do I need closing functionality? Maybe to block reading if I individually close a streamer?
io.ReadSeeker // Note: This must be an io.ReadSeeker because decoders require the Seeker to be implemented for `Seek` operations to work
}
func (c fakeCloser) Close() error {
// stop = true
return nil
}
// Pre-Decoding:
// func loadVorbis(reader io.Reader) (*beep.Buffer, error) {
// streamer, format, err := vorbis.Decode(fakeCloser{reader})
// if err != nil {
// return nil, err
// }
// // TODO: Verify/resample the sampling rate? https://pkg.go.dev/github.com/faiface/beep?utm_source=godoc#ResampleRatio
// // Like: resampled := beep.Resample(4, format.SampleRate, sr, streamer)
// buffer := beep.NewBuffer(format)
// buffer.Append(streamer)
// // // TODO: Would be better if we could just continually buffer this at a larger distance than every streamer. Kind of like how beep.Speaker buffers data. That would help us to be able to listen to long songs more quickly. I'm not sure how that works with looping though...
// // takeAmount := 512
// // for {
// // startLen := buffer.Len()
// // buffer.Append(beep.Take(takeAmount, streamer))
// // endLen := buffer.Len()
// // if startLen == endLen {
// // break
// // }
// // time.Sleep(1 * time.Nanosecond) // Kind of a yield for wasm processing so the thread doesn't lock while we process this whole audio file
// // }
// return buffer, err
// }
// func loadWav(reader io.Reader) (beep.StreamSeekCloser, error) {
// streamer, _, err := wav.Decode(fakeCloser{reader})
// // TODO: Verify/resample the sampling rate? https://pkg.go.dev/github.com/faiface/beep?utm_source=godoc#ResampleRatio
// // Like: resampled := beep.Resample(4, format.SampleRate, sr, streamer)
// return streamer, err
// }
//go:build !js
package browser
import (
"github.com/pkg/browser"
)
func Open(url string, _ OpenType) error {
return browser.OpenURL(url)
}
package ds
import (
"iter"
)
type arrayMapData[K comparable, V any] struct {
key K
value V
}
func newArrayMapData[K comparable, V any](k K, v V) arrayMapData[K, V] {
return arrayMapData[K, V]{
key: k,
value: v,
}
}
// Acts like a map, but is backed by an array. Can provide better iteration speed at the cost of slower lookups
type ArrayMap[K comparable, V any] struct {
slice []arrayMapData[K, V] // TODO: Maybe faster with two slices? one for key, another for value?
}
func NewArrayMap[K comparable, V any]() ArrayMap[K, V] {
return ArrayMap[K, V]{
slice: make([]arrayMapData[K, V], 0),
}
}
func (m *ArrayMap[K, V]) append(key K, val V) {
m.slice = append(m.slice, newArrayMapData(key, val))
}
// returns the index of the value, or -1 if the key does not exist
func (m *ArrayMap[K, V]) find(key K) int {
for i := range m.slice {
if m.slice[i].key != key {
continue // Skip: Wrong key
}
return i
}
return -1
}
func (m *ArrayMap[K, V]) Put(key K, val V) {
idx := m.find(key)
if idx < 0 {
// Can't find key, so just append
m.append(key, val)
} else {
// Else, just update the current key
m.slice[idx].value = val
}
}
func (m *ArrayMap[K, V]) Get(key K) (V, bool) {
idx := m.find(key)
if idx < 0 {
var v V
return v, false
} else {
return m.slice[idx].value, true
}
}
// Delete a specific index. Note this will move the last index into the hole
func (m *ArrayMap[K, V]) Delete(key K) {
idx := m.find(key)
if idx < 0 {
return // Nothing to do, does not exist
}
m.slice[idx] = m.slice[len(m.slice)-1]
m.slice = m.slice[:len(m.slice)-1]
}
// Clear the entire slice
func (m *ArrayMap[K, V]) Clear() {
m.slice = m.slice[:0]
}
// Iterate through the entire slice
func (m *ArrayMap[K, V]) All() iter.Seq2[K, V] {
return func(yield func(K, V) bool) {
for _, d := range m.slice {
if !yield(d.key, d.value) {
break
}
}
}
}
package ds
import (
"iter"
"golang.org/x/exp/constraints"
)
// Acts like a map, but backed by an integer indexed slice instead of a map
// This is mostly for use cases where you have a small list of increasing numbers that you want to store in an array, but you dont want to worry about ensuring the bounds are always correct
// Note: If you put a huge key in here, the slice will allocate a ton of space.
type IndexMap[K constraints.Integer, V any] struct {
// TODO: set slice should just be a bitmask
set []bool // Tracks whether or not the data at a location is set or empty
slice []V // Tracks the data
}
func NewIndexMap[K constraints.Integer, V any]() IndexMap[K, V] {
return IndexMap[K, V]{
set: make([]bool, 0),
slice: make([]V, 0),
}
}
func (m *IndexMap[K, V]) grow(idx K) {
requiredLength := idx + 1
growAmount := requiredLength - K(len(m.set))
if growAmount <= 0 {
return // No need to grow if the sliceIdx is already in bounds
}
m.set = append(m.set, make([]bool, growAmount)...)
m.slice = append(m.slice, make([]V, growAmount)...)
}
func (m *IndexMap[K, V]) Put(idx K, val V) {
if idx < 0 {
return
}
m.grow(idx) // Ensure index is within bounds
m.set[idx] = true
m.slice[idx] = val
}
func (m *IndexMap[K, V]) Get(idx K) (V, bool) {
if idx < 0 || idx >= K(len(m.set)) {
var v V
return v, false
}
return m.slice[idx], m.set[idx]
}
// Delete a specific index
func (m *IndexMap[K, V]) Delete(idx K) {
m.set[idx] = false
}
// Clear the entire slice
func (m *IndexMap[K, V]) Clear() {
m.slice = m.slice[:0]
}
// Iterate through the entire slice
func (m *IndexMap[K, V]) All() iter.Seq2[K, V] {
return func(yield func(K, V) bool) {
for i, v := range m.slice {
// Ensure that the map key is set
if !m.set[i] {
continue
}
if !yield(K(i), v) {
break
}
}
}
}
package ds
import (
"iter"
)
type ArrayConstraint[T any] interface {
[1]T | [2]T | [3]T | [4]T | [5]T | [6]T | [7]T | [8]T | [9]T | [10]T | [11]T | [12]T | [13]T | [14]T | [15]T | [16]T
}
// A mini slice that allocates the first N elements into an array and then heap allocates the remaining into a traditional slice
type MiniSlice[A ArrayConstraint[T], T comparable] struct {
Array A
Slice []T
nextArrayIdx uint8
}
func (s *MiniSlice[A, T]) getIdx(idx int) (innerIdx int, array bool) {
arrayLen := len(s.Array)
if idx >= arrayLen {
innerIdx := idx - arrayLen
return innerIdx, false
}
return idx, true
}
func (s *MiniSlice[A, T]) Get(idx int) T {
innerIdx, isArray := s.getIdx(idx)
if isArray {
return s.Array[innerIdx]
} else {
return s.Slice[innerIdx]
}
}
func (s *MiniSlice[A, T]) Set(idx int, val T) {
innerIdx, isArray := s.getIdx(idx)
if isArray {
s.Array[innerIdx] = val
} else {
s.Slice[innerIdx] = val
}
}
func (s *MiniSlice[A, T]) Append(val T) {
arrayLen := len(s.Array)
if int(s.nextArrayIdx) >= arrayLen {
// If nextArrayIndex is outside to the fixed-sized array, then just start appending to the slice
s.Slice = append(s.Slice, val)
return
}
// Else, append to the fixed array and track the index
s.Array[s.nextArrayIdx] = val
s.nextArrayIdx++
}
// Find and return the index of the first element, else return -1
func (s *MiniSlice[A, T]) Find(searchVal T) int {
for i, val := range s.All() {
if searchVal == val {
return i
}
}
return -1
}
// Removes the element at the supplied index, swapping the element that was at the last index to
// the supplied index
func (s *MiniSlice[A, T]) Delete(idx int) {
if idx < 0 {
return
}
if idx > s.Len() {
return
}
lastVal := s.Get(s.Len() - 1)
s.Set(idx, lastVal)
s.SliceLast()
}
// Slices the last element
func (s *MiniSlice[A, T]) SliceLast() {
innerIdx, isArray := s.getIdx(s.Len() - 1)
if isArray {
s.nextArrayIdx--
} else {
s.Slice = s.Slice[:innerIdx]
}
}
// // Returns the last index, or returns -1 if empty
// func (s *MiniSlice[A, T]) Last() int {
// // If last element is on slice
// if len(s.Slice) > 0 {
// return len(s.Slice)-1
// // val := s.Slice[lastIdx]
// // s.Slice = s.Slice[:lastIdx]
// // return val
// }
// // Else last element is on array
// return int(s.nextArrayIdx) - 1
// }
// Returns the number of elements in the slice
func (s *MiniSlice[A, T]) Len() int {
return int(s.nextArrayIdx) + len(s.Slice)
}
// Clears the array, so the next append will start at index 0
func (s *MiniSlice[A, T]) Clear() {
s.nextArrayIdx = 0
s.Slice = s.Slice[:0]
}
// Iterates the slice from 0 to the last element added
func (s *MiniSlice[A, T]) All() iter.Seq2[int, T] {
return func(yield func(int, T) bool) {
arrayEnd := int(s.nextArrayIdx)
for i := 0; i < arrayEnd; i++ {
if !yield(i, s.Array[i]) {
return
}
}
for i := range s.Slice {
if !yield(i+arrayEnd, s.Slice[i]) {
return
}
}
}
}
package ds
type Opt[T any] struct {
Val T
Has bool
}
func Optional[T any](t T) Opt[T] {
return Opt[T]{
Val: t,
Has: true,
}
}
func (o *Opt[T]) Get() (T, bool) {
return o.Val, o.Has
}
func (o *Opt[T]) GetOrDefault(def T) T {
if !o.Has {
return def
}
return o.Val
}
func (o *Opt[T]) Set(newVal T) {
o.Has = true
o.Val = newVal
}
func (o *Opt[T]) Clear() {
o.Has = false
var t T
o.Val = t
}
package ds
import (
"github.com/zergon321/mempool"
)
type erasable[T any] struct {
val T
}
func newErasable[T any](v T) erasable[T] {
return erasable[T]{v}
}
func (s erasable[T]) Erase() error {
return nil
}
type SlicePool[T any] struct {
inner *mempool.Pool[erasable[[]T]]
}
func NewSlicePool[T any](defaultSliceSize int) SlicePool[T] {
inner, err := mempool.NewPool(func() erasable[[]T] {
return newErasable(make([]T, 0, defaultSliceSize))
})
if err != nil {
panic(err) // TODO: The only failure case should be caused by options
}
return SlicePool[T]{
inner: inner,
}
}
func (p SlicePool[T]) Put(slice []T) {
slice = slice[:0] // Erase
err := p.inner.Put(newErasable(slice))
if err != nil {
panic(err)
}
}
func (p SlicePool[T]) Get() []T {
s := p.inner.Get()
return s.val
}
type MapPool[K comparable, V any] struct {
inner *mempool.Pool[erasable[map[K]V]]
}
func NewMapPool[K comparable, V any](defaultSize int) MapPool[K, V] {
inner, err := mempool.NewPool(func() erasable[map[K]V] {
return newErasable(make(map[K]V, defaultSize))
})
if err != nil {
panic(err)
}
return MapPool[K, V]{
inner: inner,
}
}
func (p MapPool[K, V]) Put(m map[K]V) {
// Erase
for k := range m {
delete(m, k)
}
err := p.inner.Put(newErasable(m))
if err != nil {
panic(err)
}
}
func (p MapPool[K, V]) Get() map[K]V {
s := p.inner.Get()
return s.val
}
func (p MapPool[K, V]) Clone(og map[K]V) map[K]V {
m := p.Get()
for k, v := range og {
m[k] = v
}
return m
}
package ds
import (
"container/heap"
)
type mapData[K comparable, T any] struct {
key K
val T
}
func newMapData[K comparable, T any](key K, val T) mapData[K, T] {
return mapData[K, T]{
key: key,
val: val,
}
}
// Note: Higher value is pulled out first
type PriorityMap[K comparable, T any] struct {
lookup map[K]*Item[mapData[K, T]]
queue *PriorityQueue[mapData[K, T]]
}
func NewPriorityMap[K comparable, T any]() *PriorityMap[K, T] {
return &PriorityMap[K, T]{
lookup: make(map[K]*Item[mapData[K, T]]),
queue: NewPriorityQueue[mapData[K, T]](),
}
}
// Adds the item, overwriting the old one if needed
func (q *PriorityMap[K, T]) Put(key K, val T, priority int) {
item, ok := q.lookup[key]
if ok {
item.Priority = priority
item.Value.val = val
q.queue.Update(item)
} else {
item := NewItem(newMapData(key, val), priority)
q.lookup[key] = item
q.queue.Push(item)
}
}
// Gets the item, doesnt remove it
func (q *PriorityMap[K, T]) Get(key K) (T, int, bool) {
item, ok := q.lookup[key]
if ok {
return item.Value.val, item.Priority, true
}
var t T
return t, 0, false
}
// Gets the value and removes it from the queue
func (q *PriorityMap[K, T]) Remove(key K) (T, bool) {
item, ok := q.lookup[key]
if ok {
q.queue.Remove(item)
delete(q.lookup, key)
return item.Value.val, true
}
var t T
return t, false
}
func (q *PriorityMap[K, T]) Len() int {
return q.queue.Len()
}
// Pops the highest priority item
func (q *PriorityMap[K, T]) Pop() (K, T, int, bool) {
if q.queue.Len() <= 0 {
var key K
var t T
return key, t, 0, false
}
item := q.queue.Pop()
delete(q.lookup, item.Value.key)
return item.Value.key, item.Value.val, item.Priority, true
}
// Removes all items from the queue
func (q *PriorityMap[K, T]) Clear() {
clear(q.lookup)
q.queue.Clear()
}
//--------------------------------------------------------------------------------
type PriorityQueue[T any] struct {
heap heapQueue[T]
}
// Note: Higher value is pulled out first
func NewPriorityQueue[T any]() *PriorityQueue[T] {
pq := &PriorityQueue[T]{
heap: make(heapQueue[T], 0),
}
heap.Init(&pq.heap)
return pq
}
func (pq *PriorityQueue[T]) Len() int {
return pq.heap.Len()
}
func (pq *PriorityQueue[T]) Push(item *Item[T]) {
heap.Push(&pq.heap, item)
}
func (pq *PriorityQueue[T]) Pop() *Item[T] {
item := heap.Pop(&pq.heap).(*Item[T])
return item
}
func (pq *PriorityQueue[T]) Update(item *Item[T]) {
heap.Fix(&pq.heap, item.index)
}
func (pq *PriorityQueue[T]) Remove(item *Item[T]) {
heap.Remove(&pq.heap, item.index)
}
func (pq *PriorityQueue[T]) Clear() {
pq.heap.Clear()
}
// An Item is something we manage in a priority queue.
type Item[T any] struct {
// The index is needed by update and is maintained by the heap.Interface methods.
index int // The index of the item in the heap.
Priority int // The priority of the item in the queue.
Value T // The value of the item; arbitrary.
}
func NewItem[T any](value T, priority int) *Item[T] {
return &Item[T]{
Value: value,
Priority: priority,
}
}
// TODO: You could make this faster by replacing this with something hand-made
// A heap implements heap.Interface and holds Items.
type heapQueue[T any] []*Item[T]
func (pq heapQueue[T]) Len() int { return len(pq) }
func (pq heapQueue[T]) Less(i, j int) bool {
return pq[i].Priority > pq[j].Priority
}
func (pq heapQueue[T]) Swap(i, j int) {
pq[i], pq[j] = pq[j], pq[i]
pq[i].index = i
pq[j].index = j
}
func (pq *heapQueue[T]) Push(x any) {
n := len(*pq)
item := x.(*Item[T])
item.index = n
*pq = append(*pq, item)
}
func (pq *heapQueue[T]) Pop() any {
old := *pq
n := len(old)
item := old[n-1]
old[n-1] = nil // avoid memory leak
item.index = -1 // for safety
*pq = old[0 : n-1]
return item
}
func (pq *heapQueue[T]) Clear() {
*pq = (*pq)[:0]
}
package ds
type Queue[T any] struct {
Buffer []T
ReadIdx int
WriteIdx int
fixed bool
}
func NewQueue[T any](length int) *Queue[T] {
if length <= 0 {
length = 1
}
return &Queue[T]{
Buffer: make([]T, length),
ReadIdx: 0,
WriteIdx: 0,
}
}
func (q *Queue[T]) GrowDouble() {
newQueue := NewQueue[T](2 * len(q.Buffer))
for {
// TODO: Probably faster ways to do this with copy()
v, ok := q.Remove()
if !ok {
break
}
newQueue.Add(v)
}
q.Buffer = newQueue.Buffer
q.ReadIdx = newQueue.ReadIdx
q.WriteIdx = newQueue.WriteIdx
}
func NewFixedQueue[T any](length int) *Queue[T] {
q := NewQueue[T](length)
q.fixed = true
return q
}
func (q *Queue[T]) Len() int {
l := len(q.Buffer)
firstIdx := q.ReadIdx
lastIdx := q.WriteIdx
if lastIdx < firstIdx {
lastIdx += l
}
return lastIdx - firstIdx
}
func (q *Queue[T]) Add(t T) {
if (q.WriteIdx+1)%len(q.Buffer) == q.ReadIdx {
if q.fixed {
panic("QUEUE IS FULL!")
}
// If queue size isn't fixed, then double the size
q.GrowDouble()
}
q.Buffer[q.WriteIdx] = t
q.WriteIdx = (q.WriteIdx + 1) % len(q.Buffer)
}
func (q *Queue[T]) Peek() (T, bool) {
if q.ReadIdx == q.WriteIdx {
var ret T
return ret, false
}
return q.Buffer[q.ReadIdx], true
}
func (q *Queue[T]) PeekLast() (T, bool) {
if q.ReadIdx == q.WriteIdx {
var ret T
return ret, false
}
idx := (q.WriteIdx + len(q.Buffer) - 1) % len(q.Buffer)
return q.Buffer[idx], true
}
func (q *Queue[T]) Remove() (T, bool) {
if q.ReadIdx == q.WriteIdx {
var ret T
return ret, false
}
val := q.Buffer[q.ReadIdx]
q.ReadIdx = (q.ReadIdx + 1) % len(q.Buffer)
return val, true
}
// func (n *NextTransform) Map(fn func(t ServerTransform)) {
// if n.ReadIdx == n.WriteIdx {
// return // Empty
// }
// l := len(n.Transforms)
// firstIdx := n.ReadIdx
// // lastIdx := n.WriteIdx
// lastIdx := (n.WriteIdx + len(n.Transforms) - 1) % len(n.Transforms)
// cnt := 0
// // TODO - this might be simpler in two loops?
// for i := firstIdx; i != lastIdx; i=(i + 1) % l {
// fn(n.Transforms[i])
// cnt++
// }
// // log.Print("Mapped: ", cnt)
// }
package ds
type RingBuffer[T any] struct {
idx int
readIdx int
buffer []T
}
func NewRingBuffer[T any](length int) *RingBuffer[T] {
return &RingBuffer[T]{
idx: 0,
buffer: make([]T, length),
}
}
func (b *RingBuffer[T]) Cap() int {
return len(b.buffer)
}
func (b *RingBuffer[T]) Len() int {
l := len(b.buffer)
firstIdx := b.readIdx
lastIdx := b.idx
if lastIdx < firstIdx {
lastIdx += l
}
return lastIdx - firstIdx
}
func (b *RingBuffer[T]) Add(t T) {
b.buffer[b.idx] = t
b.idx = (b.idx + 1) % len(b.buffer)
if b.idx == b.readIdx {
// If we just added one and the read index matches the write index, then we know that we are overwriting unread elements. So just shift the read index by one (as if we just read the one that was written)
b.readIdx = (b.readIdx + 1) % len(b.buffer)
}
}
// Returns the last element and false if the buffer is emptied
func (b *RingBuffer[T]) Remove() (T, bool) {
ret := b.buffer[b.readIdx]
newReadIdx := (b.readIdx + 1) % len(b.buffer)
if newReadIdx == b.idx {
// If the next index to read from is the current write index, then we know we've read the whole buffer. In this case don't increment the read index, this will cause the next Remove function to read the same value.
return ret, false
} else {
// Else we do want to progress the index
b.readIdx = newReadIdx
}
return ret, true
}
// TODO - Maybe convert this to an iterator
func (b *RingBuffer[T]) Buffer() []T {
ret := make([]T, len(b.buffer))
firstSliceLen := len(b.buffer) - b.idx
copy(ret[:firstSliceLen], b.buffer[b.idx:len(b.buffer)])
copy(ret[firstSliceLen:], b.buffer[0:b.idx])
return ret
}
package ds
// Safely adds the value at the slice index provided
func GrowAdd[K any](slice []K, idx int, val K) []K {
requiredLength := idx + 1
growAmount := requiredLength - len(slice)
if growAmount <= 0 {
slice[idx] = val
return slice // No need to grow if the sliceIdx is already in bounds
}
slice = append(slice, make([]K, growAmount)...)
slice[idx] = val
return slice
}
// Safely gets and returns a value from a slice, and a boolen to indicate boundscheck
func SafeGet[K any](slice []K, idx int) (K, bool) {
if idx < 0 || idx >= len(slice) {
var k K
return k, false
}
return slice[idx], true
}
package ds
type Stack[T any] struct {
Buffer []T
}
func NewStack[T any]() *Stack[T] {
return &Stack[T]{
Buffer: make([]T, 0), // TODO: configurable?
}
}
func (s *Stack[T]) Len() int {
return len(s.Buffer)
}
func (s *Stack[T]) Add(t T) {
s.Buffer = append(s.Buffer, t)
}
// func (s *Stack[T]) Peek() (T, bool) {
// if q.ReadIdx == q.WriteIdx {
// var ret T
// return ret, false
// }
// return q.Buffer[q.ReadIdx], true
// }
//
// func (s *Stack[T]) PeekLast() (T, bool) {
// if q.ReadIdx == q.WriteIdx {
// var ret T
// return ret, false
// }
// idx := (q.WriteIdx + len(q.Buffer) - 1) % len(q.Buffer)
// return q.Buffer[idx], true
// }
func (s *Stack[T]) Remove() (T, bool) {
if len(s.Buffer) == 0 {
var ret T
return ret, false
}
last := len(s.Buffer) - 1
val := s.Buffer[last]
s.Buffer = s.Buffer[:last]
return val, true
}
package main
import (
"time"
"github.com/unitoftime/flow/examples/hot/plugin"
"github.com/unitoftime/flow/hot"
)
// Steps:
// 1. run rebuild.sh to build the initial plugin *.so file
// 2. Run main.go
// 3. Change something in the HelloWorld function in ./plugin/plugin.go
// 4. run rebuild.sh to rebuild the plugin *.so file. the main() function below will detect, reload the symbol and run it once
// Note: You need to run rebuild.sh to build the plugin *.so file
func main() {
// You can directly import the plugin package to use it immediately
plugin.HelloWorld()
// This will search the provided directory for .so files and try to load them
p := hot.NewPlugin("./plugin/build/lib/")
for {
time.Sleep(1 * time.Second)
if !p.Check() {
continue
} // When this becomes true, it means a new plugin is loaded
// With our new plugin, we can lookup our symbol `HelloWorld`
sym, err := p.Lookup("HelloWorld")
if err != nil {
panic(err)
}
hello := sym.(func())
// Then we can call our Looked up symbol
hello()
}
}
package plugin
import (
"fmt"
)
// Note: Here you can change one function to the other while main.go is running.
func HelloWorld() {
fmt.Println("Hello World")
}
// func HelloWorld() {
// fmt.Println("Goodbye Moon")
// }
package main
import (
"image/color"
"math"
"time"
"github.com/unitoftime/ecs"
"github.com/unitoftime/flow"
"github.com/unitoftime/flow/render"
"github.com/unitoftime/flow/transform"
"github.com/unitoftime/glitch"
)
// Ideas
// - ecs.PutResource -> world.AddResource(thing) but then have thing implement some interface
func main() {
glitch.Run(run)
}
func run() {
app := flow.NewApp()
app.AddPlugin(render.DefaultPlugin{})
app.AddPlugin(transform.DefaultPlugin{})
app.AddSystems(ecs.StageStartup,
ecs.NewSystem1(setup),
)
app.AddSystems(ecs.StageFixedUpdate,
ecs.NewSystem1(rotate),
)
app.AddSystems(ecs.StageUpdate,
ecs.NewSystem1(printStuff),
ecs.NewSystem2(escapeExit),
)
app.Run()
}
func setup(dt time.Duration, commands *ecs.CommandQueue) {
// TODO: I'd like to rewrite this to be internally managed, but for now you must manually call Execute()
defer commands.Execute()
texture := glitch.NewRGBATexture(128, 128, color.RGBA{0xFF, 0xFF, 0xFF, 0xFF}, false)
sprite := glitch.NewSprite(texture, texture.Bounds())
commands.SpawnEmpty().
Insert(render.Sprite{sprite}).
Insert(render.Visibility{}).
Insert(render.CalculatedVisibility{}).
Insert(render.Target{}).
Insert(transform.Local{transform.Default()}).
Insert(transform.Global{transform.Default()})
}
func rotate(dt time.Duration, query *ecs.View1[transform.Local]) {
query.MapId(func(id ecs.Id, lt *transform.Local) {
lt.Rot += math.Pi * dt.Seconds()
// angle := math.Pi * dt.Seconds()
// lt.Rot.RotateZ(angle)
})
}
func printStuff(dt time.Duration, query *ecs.View1[transform.Local]) {
// query.MapId(func(id ecs.Id, lt *transform.Local) {
// fmt.Println(id, lt)
// })
}
func escapeExit(dt time.Duration, scheduler *ecs.Scheduler, query *ecs.View1[render.Window]) {
query.MapId(func(_ ecs.Id, win *render.Window) {
if win.JustPressed(glitch.KeyEscape) {
win.Close()
scheduler.SetQuit(true)
}
})
}
package glm
import (
"image/color"
"math"
)
var (
White = RGBA{1, 1, 1, 1}
Black = RGBA{0, 0, 0, 1}
Transparent = RGBA{0, 0, 0, 0}
)
// Premultipled RGBA value scaled from [0, 1.0]
type RGBA struct {
R, G, B, A float64
}
// TODO - conversion from golang colors
func FromUint8(r, g, b, a uint8) RGBA {
return RGBA{
float64(r) / float64(math.MaxUint8),
float64(g) / float64(math.MaxUint8),
float64(b) / float64(math.MaxUint8),
float64(a) / float64(math.MaxUint8),
}
}
func (c RGBA) ToUint8() color.NRGBA {
return color.NRGBA{
R: uint8(math.Round(c.R * float64(math.MaxUint8))),
G: uint8(math.Round(c.G * float64(math.MaxUint8))),
B: uint8(math.Round(c.B * float64(math.MaxUint8))),
A: uint8(math.Round(c.A * float64(math.MaxUint8))),
}
}
func HexColor(col uint64, alpha uint8) RGBA {
return FromNRGBA(color.NRGBA{
R: uint8((col >> 16) & 0xff),
G: uint8((col >> 8) & 0xff),
B: uint8(col & 0xff),
A: alpha,
})
}
func Alpha(a float64) RGBA {
return RGBA{a, a, a, a}
}
func Greyscale(g float64) RGBA {
return RGBA{g, g, g, 1.0}
}
func FromStraightRGBA(r, g, b float64, a float64) RGBA {
return RGBA{r * a, g * a, b * a, a}
}
func FromNRGBA(c color.NRGBA) RGBA {
r, g, b, a := c.RGBA()
return RGBA{
float64(r) / float64(math.MaxUint16),
float64(g) / float64(math.MaxUint16),
float64(b) / float64(math.MaxUint16),
float64(a) / float64(math.MaxUint16),
}
}
func FromRGBA(c color.RGBA) RGBA {
return FromUint8(c.R, c.G, c.B, c.A)
}
func FromColor(c color.Color) RGBA {
r, g, b, a := c.RGBA()
return RGBA{
float64(r) / float64(math.MaxUint16),
float64(g) / float64(math.MaxUint16),
float64(b) / float64(math.MaxUint16),
float64(a) / float64(math.MaxUint16),
}
}
func (c1 RGBA) Mult(c2 RGBA) RGBA {
return RGBA{
c1.R * c2.R,
c1.G * c2.G,
c1.B * c2.B,
c1.A * c2.A,
}
}
func (c1 RGBA) Add(c2 RGBA) RGBA {
return RGBA{
c1.R + c2.R,
c1.G + c2.G,
c1.B + c2.B,
c1.A + c2.A,
}
}
func (c RGBA) Clamp() RGBA {
return RGBA{
R: Clamp(0, 1, c.R),
G: Clamp(0, 1, c.G),
B: Clamp(0, 1, c.B),
A: Clamp(0, 1, c.A),
}
}
func (c1 RGBA) Avg(c2 RGBA) RGBA {
return c1.Add(c2).Mult(RGBA{0.5, 0.5, 0.5, 0.5})
}
func (c RGBA) Desaturate(val float64) RGBA {
// https://stackoverflow.com/questions/70966873/algorithm-to-desaturate-rgb-color
i := (c.R + c.G + c.B) / 3
dr := i - c.R
dg := i - c.G
db := i - c.B
return RGBA{
c.R + (dr * val),
c.G + (dg * val),
c.B + (db * val),
c.A,
}
}
package glm
import "cmp"
func Clamp[T cmp.Ordered](low, high, val T) T {
return min(high, max(low, val))
}
package glm
type IVec2 struct {
X, Y int
}
func (v IVec2) Add(v2 IVec2) IVec2 {
return IVec2{v.X + v2.X, v.Y + v2.Y}
}
func (v IVec2) Sub(v2 IVec2) IVec2 {
return IVec2{v.X - v2.X, v.Y - v2.Y}
}
package glm
type Line2 struct {
A, B Vec2
}
// Returns true if the two lines intersect
func (l1 Line2) Intersects(l2 Line2) bool {
// Adapted from: https://www.gorillasun.de/blog/an-algorithm-for-polygon-intersections/
x1 := l1.A.X
y1 := l1.A.Y
x2 := l1.B.X
y2 := l1.B.Y
x3 := l2.A.X
y3 := l2.A.Y
x4 := l2.B.X
y4 := l2.B.Y
// Ensure no line is 0 length
if (x1 == x2 && y1 == y2) || (x3 == x4 && y3 == y4) {
return false
}
den := ((y4-y3)*(x2-x1) - (x4-x3)*(y2-y1))
// Lines are parallel
if den == 0 {
return false
}
ua := ((x4-x3)*(y1-y3) - (y4-y3)*(x1-x3)) / den
ub := ((x2-x1)*(y1-y3) - (y2-y1)*(x1-x3)) / den
// is the intersection along the segments
if ua < 0 || ua > 1 || ub < 0 || ub > 1 {
return false
}
return true
// // Return a object with the x and y coordinates of the intersection
// x := x1 + ua * (x2 - x1)
// y := y1 + ua * (y2 - y1)
// return {x, y}
}
package glm
import (
"github.com/go-gl/mathgl/mgl64"
)
type Vec3 struct {
X, Y, Z float64
}
type Vec4 struct {
X, Y, Z, W float64
}
type Box struct {
Min, Max Vec3
}
type Mat2 [4]float64
type Mat3 [9]float64
type Mat4 [16]float64
// This is in column major order
var Mat3Ident Mat3 = Mat3{
1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
0.0, 0.0, 1.0,
}
// This is in column major order
var Mat4Ident Mat4 = Mat4{
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0,
}
func IM4() *Mat4 {
return &Mat4{
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0,
}
}
func (m *Mat3) Translate(x, y float64) *Mat3 {
m[i3_2_0] = m[i3_2_0] + x
m[i3_2_1] = m[i3_2_1] + y
return m
}
// Note: Scales around 0,0
func (m *Mat4) Scale(x, y, z float64) *Mat4 {
m[i4_0_0] = m[i4_0_0] * x
m[i4_1_0] = m[i4_1_0] * x
m[i4_2_0] = m[i4_2_0] * x
m[i4_3_0] = m[i4_3_0] * x
m[i4_0_1] = m[i4_0_1] * y
m[i4_1_1] = m[i4_1_1] * y
m[i4_2_1] = m[i4_2_1] * y
m[i4_3_1] = m[i4_3_1] * y
m[i4_0_2] = m[i4_0_2] * z
m[i4_1_2] = m[i4_1_2] * z
m[i4_2_2] = m[i4_2_2] * z
m[i4_3_2] = m[i4_3_2] * z
return m
}
func (m *Mat4) Translate(x, y, z float64) *Mat4 {
m[i4_3_0] = m[i4_3_0] + x
m[i4_3_1] = m[i4_3_1] + y
m[i4_3_2] = m[i4_3_2] + z
return m
}
func (m *Mat4) GetTranslation() Vec3 {
return Vec3{m[i4_3_0], m[i4_3_1], m[i4_3_2]}
}
// Rotate around the Z axis
func (m *Mat4) RotateZ(angle float64) *Mat4 {
return m.Rotate(angle, Vec3{0, 0, 1})
}
// // https://github.com/go-gl/mathgl/blob/v1.0.0/mgl32/transform.go#L159
// func (m *Mat4) Rotate(angle float64, axis Vec3) *Mat4 {
// // // quat := mgl32.Mat4ToQuat(mgl32.Mat4(*m))
// // // return &retMat
// // rotation := Mat4(mgl64.HomogRotate3D(angle, mgl64.Vec3{axis.X, axis.Y, axis.Z}))
// // // retMat := Mat4(mgl32.Mat4(*m).)
// // // return &retMat
// // mNew := m.Mul(&rotation)
// // *m = *mNew
// // return m
// rotation := Mat4(mgl64.HomogRotate3D(angle, mgl64.Vec3{axis.X, axis.Y, axis.Z}))
// mNew := rotation.Mul(m)
// *m = *mNew
// return m
// }
func (m *Mat4) RotateQuat(quat Quat) *Mat4 {
rotation := quat.Mat4()
mNew := rotation.Mul(m)
*m = *mNew
return m
}
func (m *Mat4) Rotate(angle float64, axis Vec3) *Mat4 {
quat := QuatRotate(angle, axis)
return m.RotateQuat(quat)
}
// Note: This modifies in place
func (m *Mat4) Mul(n *Mat4) *Mat4 {
// This is in column major order
*m = Mat4{
// return &Mat4{
// Column 0
m[i4_0_0]*n[i4_0_0] + m[i4_1_0]*n[i4_0_1] + m[i4_2_0]*n[i4_0_2] + m[i4_3_0]*n[i4_0_3],
m[i4_0_1]*n[i4_0_0] + m[i4_1_1]*n[i4_0_1] + m[i4_2_1]*n[i4_0_2] + m[i4_3_1]*n[i4_0_3],
m[i4_0_2]*n[i4_0_0] + m[i4_1_2]*n[i4_0_1] + m[i4_2_2]*n[i4_0_2] + m[i4_3_2]*n[i4_0_3],
m[i4_0_3]*n[i4_0_0] + m[i4_1_3]*n[i4_0_1] + m[i4_2_3]*n[i4_0_2] + m[i4_3_3]*n[i4_0_3],
// Column 1
m[i4_0_0]*n[i4_1_0] + m[i4_1_0]*n[i4_1_1] + m[i4_2_0]*n[i4_1_2] + m[i4_3_0]*n[i4_1_3],
m[i4_0_1]*n[i4_1_0] + m[i4_1_1]*n[i4_1_1] + m[i4_2_1]*n[i4_1_2] + m[i4_3_1]*n[i4_1_3],
m[i4_0_2]*n[i4_1_0] + m[i4_1_2]*n[i4_1_1] + m[i4_2_2]*n[i4_1_2] + m[i4_3_2]*n[i4_1_3],
m[i4_0_3]*n[i4_1_0] + m[i4_1_3]*n[i4_1_1] + m[i4_2_3]*n[i4_1_2] + m[i4_3_3]*n[i4_1_3],
// Column 2
m[i4_0_0]*n[i4_2_0] + m[i4_1_0]*n[i4_2_1] + m[i4_2_0]*n[i4_2_2] + m[i4_3_0]*n[i4_2_3],
m[i4_0_1]*n[i4_2_0] + m[i4_1_1]*n[i4_2_1] + m[i4_2_1]*n[i4_2_2] + m[i4_3_1]*n[i4_2_3],
m[i4_0_2]*n[i4_2_0] + m[i4_1_2]*n[i4_2_1] + m[i4_2_2]*n[i4_2_2] + m[i4_3_2]*n[i4_2_3],
m[i4_0_3]*n[i4_2_0] + m[i4_1_3]*n[i4_2_1] + m[i4_2_3]*n[i4_2_2] + m[i4_3_3]*n[i4_2_3],
// Column 3
m[i4_0_0]*n[i4_3_0] + m[i4_1_0]*n[i4_3_1] + m[i4_2_0]*n[i4_3_2] + m[i4_3_0]*n[i4_3_3],
m[i4_0_1]*n[i4_3_0] + m[i4_1_1]*n[i4_3_1] + m[i4_2_1]*n[i4_3_2] + m[i4_3_1]*n[i4_3_3],
m[i4_0_2]*n[i4_3_0] + m[i4_1_2]*n[i4_3_1] + m[i4_2_2]*n[i4_3_2] + m[i4_3_2]*n[i4_3_3],
m[i4_0_3]*n[i4_3_0] + m[i4_1_3]*n[i4_3_1] + m[i4_2_3]*n[i4_3_2] + m[i4_3_3]*n[i4_3_3],
}
return m
}
// Matrix Indices
const (
// 4x4 - x_y
i4_0_0 = 0
i4_0_1 = 1
i4_0_2 = 2
i4_0_3 = 3
i4_1_0 = 4
i4_1_1 = 5
i4_1_2 = 6
i4_1_3 = 7
i4_2_0 = 8
i4_2_1 = 9
i4_2_2 = 10
i4_2_3 = 11
i4_3_0 = 12
i4_3_1 = 13
i4_3_2 = 14
i4_3_3 = 15
// 3x3 - x_y
i3_0_0 = 0
i3_0_1 = 1
i3_0_2 = 2
i3_1_0 = 3
i3_1_1 = 4
i3_1_2 = 5
i3_2_0 = 6
i3_2_1 = 7
i3_2_2 = 8
)
func (b Box) Rect() Rect {
return Rect{
Min: Vec2{b.Min.X, b.Min.Y},
Max: Vec2{b.Max.X, b.Max.Y},
}
}
func (a Box) Union(b Box) Box {
x1, _ := minMax(a.Min.X, b.Min.X)
_, x2 := minMax(a.Max.X, b.Max.X)
y1, _ := minMax(a.Min.Y, b.Min.Y)
_, y2 := minMax(a.Max.Y, b.Max.Y)
z1, _ := minMax(a.Min.Z, b.Min.Z)
_, z2 := minMax(a.Max.Z, b.Max.Z)
return Box{
Min: Vec3{x1, y1, z1},
Max: Vec3{x2, y2, z2},
}
}
// TODO: This is the wrong input matrix type
func (b Box) Apply(mat Mat4) Box {
return Box{
Min: mat.Apply(b.Min),
Max: mat.Apply(b.Max),
}
}
func (m *Mat4) Apply(v Vec3) Vec3 {
return Vec3{
m[i4_0_0]*v.X + m[i4_1_0]*v.Y + m[i4_2_0]*v.Z + m[i4_3_0], // w = 1.0
m[i4_0_1]*v.X + m[i4_1_1]*v.Y + m[i4_2_1]*v.Z + m[i4_3_1], // w = 1.0
m[i4_0_2]*v.X + m[i4_1_2]*v.Y + m[i4_2_2]*v.Z + m[i4_3_2], // w = 1.0
}
}
func (m *Mat3) Apply(v Vec2) Vec2 {
return Vec2{
m[i3_0_0]*v.X + m[i3_1_0]*v.Y + m[i3_2_0],
m[i3_0_1]*v.X + m[i3_1_1]*v.Y + m[i3_2_1],
}
}
// Note: Returns a new Mat4
func (m *Mat4) Inv() *Mat4 {
retMat := Mat4(mgl64.Mat4(*m).Inv())
return &retMat
}
func (m *Mat4) Transpose() *Mat4 {
retMat := Mat4(mgl64.Mat4(*m).Transpose())
return &retMat
}
package glm
import (
"github.com/go-gl/mathgl/mgl64"
)
type Quat struct {
// x, y, z, w float64
inner mgl64.Quat
}
func IQuat() Quat {
return Quat{mgl64.QuatIdent()}
}
func QuatRotate(angle float64, axis Vec3) Quat {
quat := mgl64.QuatRotate(angle, mgl64.Vec3{axis.X, axis.Y, axis.Z})
return Quat{quat}
}
func QuatZ(angle float64) Quat {
return QuatRotate(angle, Vec3{0, 0, 1})
}
func (q *Quat) Equals(q2 Quat) bool {
return q.inner.OrientationEqual(q2.inner)
}
func (q *Quat) RotateQuat(rQuat Quat) *Quat {
q.inner = q.inner.Mul(rQuat.inner)
return q
}
func (q *Quat) RotateX(angle float64) *Quat {
return q.Rotate(angle, Vec3{1, 0, 0})
}
func (q *Quat) RotateY(angle float64) *Quat {
return q.Rotate(angle, Vec3{0, 1, 0})
}
func (q *Quat) RotateZ(angle float64) *Quat {
return q.Rotate(angle, Vec3{0, 0, 1})
}
func (q *Quat) Rotate(angle float64, axis Vec3) *Quat {
rQuat := QuatRotate(angle, axis)
return q.RotateQuat(rQuat)
}
func (q *Quat) Mat4() Mat4 {
return Mat4(q.inner.Mat4())
}
package glm
import "math"
type Rect struct {
Min, Max Vec2
}
func R(minX, minY, maxX, maxY float64) Rect {
// TODO - guarantee min is less than max
return Rect{
Min: Vec2{minX, minY},
Max: Vec2{maxX, maxY},
}
}
// Creates a centered rect
func CR(radius float64) Rect {
// TODO - guarantee min is less than max
return Rect{
Min: Vec2{-radius, -radius},
Max: Vec2{radius, radius},
}
}
// Returns the top left point
func (r Rect) TL() Vec2 {
return Vec2{r.Min.X, r.Max.Y}
}
// Returns the bottom right point
func (r Rect) BR() Vec2 {
return Vec2{r.Max.X, r.Min.Y}
}
// Returns a box that holds this rect. The Z axis is 0
func (r Rect) Box() Box {
return r.ToBox()
}
func (r Rect) ToBox() Box {
return Box{
Min: Vec3{r.Min.X, r.Min.Y, 0},
Max: Vec3{r.Max.X, r.Max.Y, 0},
}
}
func (r Rect) W() float64 {
return r.Max.X - r.Min.X
}
func (r Rect) H() float64 {
return r.Max.Y - r.Min.Y
}
func (r Rect) Center() Vec2 {
return Vec2{r.Min.X + (r.W() / 2), r.Min.Y + (r.H() / 2)}
}
func (r Rect) Size() Vec2 {
return Vec2{r.W(), r.H()}
}
// func (r Rect) CenterAt(v Vec2) Rect {
// return r.Moved(r.Center().Scaled(-1)).Moved(v)
// }
func (r Rect) WithCenter(v Vec2) Rect {
w := r.W() / 2
h := r.H() / 2
return R(v.X-w, v.Y-h, v.X+w, v.Y+h)
}
// TODO: Should I make a pointer version of this that handles the nil case too?
// Returns the smallest rect which contains both input rects
func (r Rect) Union(s Rect) Rect {
r = r.Norm()
s = s.Norm()
x1 := min(r.Min.X, s.Min.X)
x2 := max(r.Max.X, s.Max.X)
y1 := min(r.Min.Y, s.Min.Y)
y2 := max(r.Max.Y, s.Max.Y)
return R(x1, y1, x2, y2)
}
func (r Rect) Moved(v Vec2) Rect {
return Rect{
Min: r.Min.Add(v),
Max: r.Max.Add(v),
}
}
// Calculates the scale required to fit rect r inside r2
func (r Rect) FitScale(r2 Rect) float64 {
scaleX := r2.W() / r.W()
scaleY := r2.H() / r.H()
min := min(scaleX, scaleY)
return min
}
// Calculates the scale required to fill rect r inside r2
func (r Rect) FillScale(r2 Rect) float64 {
scaleX := r2.W() / r.W()
scaleY := r2.H() / r.H()
min := max(scaleX, scaleY)
return min
}
// Fits rect 'r' into another rect 'r2' with same center but only integer scaled
func (r Rect) FitInt(r2 Rect) Rect {
scale := math.Floor(r.FitScale(r2))
return r.Scaled(scale).WithCenter(r2.Center())
}
// Fits rect 'r' into another rect 'r2' with same center
func (r Rect) Fit(r2 Rect) Rect {
return r.Scaled(r.FitScale(r2)).WithCenter(r2.Center())
}
// Scales rect r uniformly to fit inside rect r2
// TODO This only scales around {0, 0}
func (r Rect) ScaledToFit(r2 Rect) Rect {
return r.Scaled(r.FitScale(r2))
}
// Returns the largest square that fits inside the rectangle
func (r Rect) SubSquare() Rect {
w := r.W()
h := r.H()
min := min(w, h)
m2 := min / 2
return R(-m2, -m2, m2, m2).Moved(r.Center())
}
func (r Rect) CenterScaled(scale float64) Rect {
c := r.Center()
w := r.W() * scale / 2.0
h := r.H() * scale / 2.0
return R(c.X-w, c.Y-h, c.X+w, c.Y+h)
}
func (r Rect) CenterScaledXY(scaleX, scaleY float64) Rect {
c := r.Center()
w := r.W() * scaleX / 2.0
h := r.H() * scaleY / 2.0
return R(c.X-w, c.Y-h, c.X+w, c.Y+h)
}
// Note: This scales around the center
// func (r Rect) ScaledXY(scale Vec2) Rect {
// c := r.Center()
// w := r.W() * scale.X / 2.0
// h := r.H() * scale.Y / 2.0
// return R(c.X - w, c.Y - h, c.X + w, c.Y + h)
// }
// TODO: I need to deprecate this. This currently just indepentently scales the min and max point which is only useful if the center, min, or max is on (0, 0)
func (r Rect) Scaled(scale float64) Rect {
// center := r.Center()
// r = r.Moved(center.Scaled(-1))
r = Rect{
Min: r.Min.Scaled(scale),
Max: r.Max.Scaled(scale),
}
// r = r.Moved(center)
return r
}
func (r Rect) ScaledXY(scale Vec2) Rect {
r = Rect{
Min: r.Min.ScaledXY(scale),
Max: r.Max.ScaledXY(scale),
}
return r
}
func (r Rect) Norm() Rect {
x1, x2 := minMax(r.Min.X, r.Max.X)
y1, y2 := minMax(r.Min.Y, r.Max.Y)
return R(x1, y1, x2, y2)
}
// Returns true if the point is inside (not on the edge of) the rect
func (r Rect) Contains(pos Vec2) bool {
return pos.X > r.Min.X && pos.X < r.Max.X && pos.Y > r.Min.Y && pos.Y < r.Max.Y
}
// Returns true if the point is inside or on the edge of the rect
func (r Rect) OverlapsPoint(pos Vec3) bool {
return pos.X >= r.Min.X && pos.X <= r.Max.X && pos.Y >= r.Min.Y && pos.Y <= r.Max.Y
}
// func (r Rect) Contains(x, y float64) bool {
// return x > r.Min.X && x < r.Max.X && y > r.Min.Y && y < r.Max.Y
// }
func (r Rect) Intersects(r2 Rect) bool {
return (r.Min.X <= r2.Max.X &&
r.Max.X >= r2.Min.X &&
r.Min.Y <= r2.Max.Y &&
r.Max.Y >= r2.Min.Y)
}
// Layous out 'n' rectangles horizontally with specified padding between them and returns that rect
// The returned rectangle has a min point of 0,0
func (r Rect) LayoutHorizontal(n int, padding float64) Rect {
return R(
0,
0,
float64(n)*r.W()+float64(n-1)*padding,
r.H(),
)
}
func (r *Rect) CutLeft(amount float64) Rect {
cutRect := *r
cutRect.Max.X = cutRect.Min.X + amount
r.Min.X += amount
return cutRect
}
func (r *Rect) CutRight(amount float64) Rect {
cutRect := *r
cutRect.Min.X = cutRect.Max.X - amount
r.Max.X -= amount
return cutRect
}
func (r *Rect) CutBottom(amount float64) Rect {
cutRect := *r
cutRect.Max.Y = cutRect.Min.Y + amount
r.Min.Y += amount
return cutRect
}
func (r *Rect) CutTop(amount float64) Rect {
cutRect := *r
cutRect.Min.Y = cutRect.Max.Y - amount
r.Max.Y -= amount
return cutRect
}
// Returns the left half of the rectangle. Doesn't modify the original rectangle
func (r Rect) LeftHalf() Rect {
return r.CutLeft(0.5 * r.W())
}
// Returns the right half of the rectangle. Doesn't modify the original rectangle
func (r Rect) RightHalf() Rect {
return r.CutRight(0.5 * r.W())
}
// Returns the top half of the rectangle. Doesn't modify the original rectangle
func (r Rect) TopHalf() Rect {
return r.CutTop(0.5 * r.H())
}
// Returns the bottom half of the rectangle. Doesn't modify the original rectangle
func (r Rect) BottomHalf() Rect {
return r.CutBottom(0.5 * r.H())
}
// Returns a centered square with height and width set by amount
func (r Rect) SliceSquare(amount float64) Rect {
r = r.SliceHorizontal(amount)
r = r.SliceVertical(amount)
return r
}
// Returns a centered horizontal sliver with height set by amount
func (r Rect) SliceHorizontal(amount float64) Rect {
r.CutTop((r.H() - amount) / 2)
return r.CutTop(amount)
}
// Returns a centered vertical sliver with width set by amount
func (r Rect) SliceVertical(amount float64) Rect {
r.CutRight((r.W() - amount) / 2)
return r.CutRight(amount)
}
func (r Rect) Snap() Rect {
r.Min = r.Min.Snap()
r.Max = r.Max.Snap()
return r
}
// Adds padding to a rectangle consistently on every edge
func (r Rect) PadAll(padding float64) Rect {
return r.Pad(R(padding, padding, padding, padding))
}
// Adds padding to a rectangle on the X axis
func (r Rect) PadX(padding float64) Rect {
return r.Pad(R(padding, 0, padding, 0))
}
// Adds padding to a rectangle on the Y Axis
func (r Rect) PadY(padding float64) Rect {
return r.Pad(R(0, padding, 0, padding))
}
// Adds padding to the left side of a rectangle
func (r Rect) PadLeft(padding float64) Rect {
return r.Pad(R(padding, 0, 0, 0))
}
// Adds padding to the right side of a rectangle
func (r Rect) PadRight(padding float64) Rect {
return r.Pad(R(0, 0, padding, 0))
}
// Adds padding to the top side of a rectangle
func (r Rect) PadTop(padding float64) Rect {
return r.Pad(R(0, 0, 0, padding))
}
// Adds padding to the bottom side of a rectangle
func (r Rect) PadBottom(padding float64) Rect {
return r.Pad(R(0, padding, 0, 0))
}
// Adds padding to a rectangle (pads inward if padding is negative)
func (r Rect) Pad(pad Rect) Rect {
return R(r.Min.X-pad.Min.X, r.Min.Y-pad.Min.Y, r.Max.X+pad.Max.X, r.Max.Y+pad.Max.Y)
}
// Removes padding from a rectangle (pads outward if padding is negative). Essentially calls pad but with negative values
func (r Rect) Unpad(pad Rect) Rect {
return r.Pad(pad.Scaled(-1))
}
func (r Rect) Point(x, y float64) Vec2 {
anchorPoint := Vec2{
X: r.Min.X + (x * r.W()),
Y: r.Min.Y + (y * r.H()),
}
return anchorPoint
}
// Takes r2 and places it in r based on the alignment
func (r Rect) Anchor(r2 Rect, anchor Vec2) Rect {
return r.AnchorFull(r2, anchor, anchor)
}
// Anchors r2 to r1 based on two anchors, one for r and one for r2
func (r Rect) AnchorFull(r2 Rect, anchor, pivot Vec2) Rect {
r2 = r2.AnchorZero()
anchorPoint := Vec2{r.Min.X + (anchor.X * r.W()), r.Min.Y + (anchor.Y * r.H())}
pivotPoint := Vec2{r2.Min.X + (pivot.X * r2.W()), r2.Min.Y + (pivot.Y * r2.H())}
a := Vec2{anchorPoint.X - pivotPoint.X, anchorPoint.Y - pivotPoint.Y}
return R(a.X, a.Y, a.X+r2.W(), a.Y+r2.H()).Norm()
}
// Takes r2 and places it in r based on the alignment
// Warning: Assumes r2 min point is 0, 0
func (r Rect) AnchorOLD(r2 Rect, anchor Vec2) Rect {
// Anchor point is the position in r that we are anchoring to
anchorPoint := Vec2{r.Min.X + (anchor.X * r.W()), r.Min.Y + (anchor.Y * r.H())}
pivotPoint := Vec2{r2.Min.X + (anchor.X * r2.W()), r2.Min.Y + (anchor.Y * r2.H())}
// fmt.Println("Anchor:", anchorPoint)
// fmt.Println("Pivot:", pivotPoint)
a := Vec2{anchorPoint.X - pivotPoint.X, anchorPoint.Y - pivotPoint.Y}
return R(a.X, a.Y, a.X+r2.W(), a.Y+r2.H()).Norm()
}
// Anchors r2 to r1 based on two anchors, one for r and one for r2
// Warning: Assumes r2 min point is 0, 0
func (r Rect) FullAnchorOLD(r2 Rect, anchor, pivot Vec2) Rect {
anchorPoint := Vec2{r.Min.X + (anchor.X * r.W()), r.Min.Y + (anchor.Y * r.H())}
pivotPoint := Vec2{r2.Min.X + (pivot.X * r2.W()), r2.Min.Y + (pivot.Y * r2.H())}
a := Vec2{anchorPoint.X - pivotPoint.X, anchorPoint.Y - pivotPoint.Y}
return R(a.X, a.Y, a.X+r2.W(), a.Y+r2.H()).Norm()
}
// Anchors the minimum point of the rectangle to 0,0
func (r Rect) AnchorZero() Rect {
r.Max = r.Max.Sub(r.Min)
r.Min = Vec2{}
return r
}
// Move the min point of the rect to a certain position
func (r Rect) MoveMin(pos Vec2) Rect {
dv := r.Min.Sub(pos)
return r.Moved(dv)
}
func lerp(a, b float64, t float64) float64 {
m := b - a // Slope = Rise over run | Note: Run = (1 - 0)
y := (m * t) + a
return y
}
// returns the min, max of the two numbers
func minMax(a, b float64) (float64, float64) {
if a > b {
return b, a
}
return a, b
}
func (r Rect) RectDraw(r2 Rect) Mat4 {
srcCenter := r.Center()
dstCenter := r2.Center()
mat := Mat4Ident
mat.
Translate(-srcCenter.X, -srcCenter.Y, 0).
Scale(r2.W()/r.W(), r2.H()/r.H(), 1).
Translate(dstCenter.X, dstCenter.Y, 0)
return mat
}
package glm
import "math"
type Vec2 struct {
X, Y float64
}
func V2(x, y float64) Vec2 {
return Vec2{x, y}
}
func (v Vec2) Vec3() Vec3 {
return Vec3{v.X, v.Y, 0}
}
func (v Vec2) Add(v2 Vec2) Vec2 {
return V2(v.X+v2.X, v.Y+v2.Y)
}
func (v Vec2) Sub(v2 Vec2) Vec2 {
return V2(v.X-v2.X, v.Y-v2.Y)
}
func (v Vec2) Norm() Vec2 {
len := v.Len()
if len == 0 {
return Vec2{}
}
return V2(v.X/len, v.Y/len)
}
func (v Vec2) Dist(u Vec2) float64 {
return v.Sub(u).Len()
}
func (v Vec2) DistSq(u Vec2) float64 {
return v.Sub(u).LenSq()
}
func (v Vec2) Dot(u Vec2) float64 {
return (v.X * u.X) + (v.Y * u.Y)
}
// Returns the length of the vector
func (v Vec2) Len() float64 {
return math.Sqrt((v.X * v.X) + (v.Y * v.Y))
}
// Returns the length of the vector squared. Note this is slightly faster b/c it doesn't take the square root
func (v Vec2) LenSq() float64 {
return (v.X * v.X) + (v.Y * v.Y)
}
func (v Vec2) Scaled(s float64) Vec2 {
return V2(s*v.X, s*v.Y)
}
// TODO: Same thing as mult
func (v Vec2) ScaledXY(s Vec2) Vec2 {
return Vec2{v.X * s.X, v.Y * s.Y}
}
func (v Vec2) Mult(s Vec2) Vec2 {
return Vec2{v.X * s.X, v.Y * s.Y}
}
func (v Vec2) Div(s Vec2) Vec2 {
return Vec2{v.X / s.X, v.Y / s.Y}
}
func (v Vec2) Rotated(radians float64) Vec2 {
sin := math.Sin(radians)
cos := math.Cos(radians)
return V2(
v.X*cos-v.Y*sin,
v.X*sin+v.Y*cos,
)
}
func (v Vec2) Angle() float64 {
return math.Atan2(v.Y, v.X)
}
// Finds the angle between two vectors
func Angle(a, b Vec2) float64 {
angle := a.Angle() - b.Angle()
if angle > math.Pi {
angle -= 2 * math.Pi
} else if angle <= -math.Pi {
angle += 2 * math.Pi
}
return angle
}
func (v Vec2) Snap() Vec2 {
return Vec2{
math.Round(v.X),
math.Round(v.Y),
}
}
package glm
import "math"
func (v Vec3) Add(u Vec3) Vec3 {
return Vec3{v.X + u.X, v.Y + u.Y, v.Z + u.Z}
}
func (v Vec3) Sub(u Vec3) Vec3 {
return Vec3{v.X - u.X, v.Y - u.Y, v.Z - u.Z}
}
// Finds the dot product of two vectors
func (v Vec3) Dot(u Vec3) float64 {
return (v.X * u.X) + (v.Y * u.Y) + (v.Z * u.Z)
}
// Finds the angle between two vectors
// TODO: Is this correct?
func (v Vec3) Angle(u Vec3) float64 {
return math.Acos(v.Dot(u) / (v.Len() * u.Len()))
}
func (v Vec3) Theta() float64 {
return math.Atan2(v.Y, v.X)
}
// Rotates the vector by theta on the XY 2d plane
func (v Vec3) Rotate2D(theta float64) Vec3 {
t := theta
x := v.X
y := v.Y
x1 := x*math.Cos(t) - y*math.Sin(t)
y1 := x*math.Sin(t) + y*math.Cos(t)
return Vec3{x1, y1, v.Z}
}
func (v Vec3) Len() float64 {
// return float32(math.Hypot(float64(v.X), float64(v.Y)))
a := v.X
b := v.Y
c := v.Z
return math.Sqrt((a * a) + (b * b) + (c * c))
}
func (v Vec3) Vec2() Vec2 {
return Vec2{v.X, v.Y}
}
func (v Vec3) Unit() Vec3 {
len := v.Len()
return Vec3{v.X / len, v.Y / len, v.Z / len}
}
func (v Vec3) Scaled(x, y, z float64) Vec3 {
v.X *= x
v.Y *= y
v.Z *= z
return v
}
func (v Vec3) Mult(s Vec3) Vec3 {
return Vec3{
X: v.X * s.X,
Y: v.Y * s.Y,
Z: v.Z * s.Z,
}
return v
}
//go:build !js
package hot
import (
"errors"
"fmt"
"os"
"plugin"
"strings"
"sync"
"time"
)
//--------------------------------------------------------------------------------
// Notes
//--------------------------------------------------------------------------------
// 1. https://github.com/golang/go/issues/19004#issuecomment-288923294
// 2. https://github.com/edwingeng/hotswap/tree/main
// 3. https://stackoverflow.com/questions/70033200/plugin-already-loaded
// 4. https://segmentfault.com/a/1190000042104429/en#item-4
// 5. https://github.com/golang/go/issues/68349#issuecomment-2217718002
// 6. Helps checking to make sure the plugin and main binary match: `go version -m plugin.so` and `go version -m binary.bin`
// 7. Maybe helps (never used it): `go clean -cache`
// 8. Maybe helps (unsure the cases where it helps): `trimpath`
// 9. Didn't work: `go build -ldflags "-pluginpath=plugin/hot-$(date +%s)" -buildmode=plugin -o hotload.so hotload.go`
// 10. Might help with checking binary versions: `readelf -aW ../driver/plugin1.so | grep PluginFunc`
//--------------------------------------------------------------------------------
// Tips
//--------------------------------------------------------------------------------
// 1. If you reference to a package outside of the plugin directory: Changing the location of any global def (like a function definition) will break the plugin reloading bc it changes the version of the original package
// 2. If you split plugin into several small plugins: it makes it hard to have one plugin depend on another
// 3. If you make one big mega plugin, it would increase compile times (probably), but is easy to reference several things inside the plugin
//--------------------------------------------------------------------------------
var cache map[string]*Plugin
// rm -f ../plugin/*.so && VAR=$RANDOM && echo $VAR && rm -rf ./build/* && mkdir ./build/tmp$VAR && cp reloader.go ./build/tmp$VAR && go build -buildmode=plugin -o ../plugin/tmp$VAR.so ./build/tmp$VAR
type Plugin struct {
path string
internal *plugin.Plugin
startOnce sync.Once
// gen uint64
refresh chan struct{}
currentPlugin string
}
func NewPlugin(path string) *Plugin {
if cache == nil {
cache = make(map[string]*Plugin)
}
p, ok := cache[path]
if ok {
return p
}
newPlugin := Plugin{
path: path,
refresh: make(chan struct{}),
}
cache[path] = &newPlugin
return &newPlugin
}
// func (p Plugin) Generation() uint64 {
// return p.gen
// }
// func (p *Plugin) Refresh() chan struct{} {
// return p.refresh
// }
func (p *Plugin) Lookup(symName string) (any, error) {
if p.internal == nil {
return nil, errors.New("plugin not yet loaded")
}
val, err := p.internal.Lookup(symName)
return val, err
}
// Check to see if there is a new plugin to load
// Returns true if there is a new one
func (p *Plugin) Check() bool {
entries, err := os.ReadDir(p.path)
if err != nil {
panic(err)
}
nextPlugin := ""
for _, e := range entries {
if strings.HasSuffix(e.Name(), ".so") {
nextPlugin = p.path + e.Name()
break
}
}
if nextPlugin == "" {
return false // Nothing new
}
samePlugin := nextPlugin == p.currentPlugin
if samePlugin {
return false
}
p.currentPlugin = nextPlugin
fmt.Println("Found New Plugin:", nextPlugin)
// var iPlugin *plugin.Plugin
// func() {
// defer func() {
// if r := recover(); r != nil {
// fmt.Println("RECOVERED:", r)
// }
// }()
// time.Sleep(100 * time.Millisecond)
// // Note: I have to sleep here to ensure that all of the glitch CGO calls have completed for the frame. 100ms is arbitrary, and is unecessary if you dont make CGO calls.
// var err error
// iPlugin, err = plugin.Open(p.currentPlugin)
// if err != nil {
// fmt.Println("Error Loading Plugin:", err)
// }
// }()
// Note: I have to sleep here to ensure that all of the glitch CGO calls have completed for the frame. 100ms is arbitrary, and is unecessary if you dont make CGO calls.
time.Sleep(100 * time.Millisecond)
iPlugin, err := plugin.Open(p.currentPlugin)
if err != nil {
fmt.Println("Error Loading Plugin:", err)
return false
}
if iPlugin == nil {
fmt.Println("Error Loading Plugin")
return false
}
fmt.Println("Successfully Loaded Plugin:", p.currentPlugin)
p.internal = iPlugin
return true
}
// Old idea:
// - Problem - can't synchronize with CGO execution which can cause SIGBUS: bus errors
// // Starts a watcher process in the background
// func (p *Plugin) Start() {
// p.startOnce.Do(func() {
// p.start()
// })
// }
// func (p *Plugin) start() {
// path := p.path
// sleepDur := 100 * time.Millisecond
// go func() {
// nextPlugin := ""
// for {
// time.Sleep(sleepDur)
// entries, err := os.ReadDir(path)
// if err != nil { panic(err) }
// for _, e := range entries {
// if strings.HasSuffix(e.Name(), ".so") {
// nextPlugin = path + e.Name()
// break
// }
// }
// reload := nextPlugin != p.currentPlugin
// if !reload {
// // fmt.Println(".")
// continue
// }
// fmt.Println("Found New Plugin:", nextPlugin)
// // lastPlugin := currentPlugin
// p.currentPlugin = nextPlugin
// var iPlugin *plugin.Plugin
// func() {
// defer func() {
// if r := recover(); r != nil {
// fmt.Println("RECOVERED:", r)
// }
// }()
// var err error
// iPlugin, err = plugin.Open(p.currentPlugin)
// if err != nil {
// panic(err)
// // fmt.Println("Error Loading Plugin:", currentPlugin, err)
// // // fmt.Println("Plugin already loaded(last, curr):", lastPlugin, currentPlugin)
// // continue
// }
// }()
// if iPlugin == nil {
// fmt.Println("Error Loading Plugin")
// continue
// }
// fmt.Println("Successfully Loaded Plugin:", p.currentPlugin)
// p.internal = iPlugin
// // p.gen++
// p.refresh <- struct{}{}
// }
// }()
// }
package interp
import (
"math"
"time"
"github.com/ungerik/go3d/float64/bezier2"
"github.com/ungerik/go3d/float64/vec2"
"github.com/unitoftime/flow/glm"
"golang.org/x/exp/constraints"
)
// TODO: use https://easings.net/
// Note: https://cubic-bezier.com
// This will calculate
func DynamicValue(val float64, fixedTime, dt time.Duration) float64 {
// interpVal := val * dt.Seconds() / (16 * time.Millisecond).Seconds()
interpVal := (val / fixedTime.Seconds()) * dt.Seconds()
if interpVal > 1.0 {
return 1.0
} else if interpVal < 0 {
return 0.0
}
return interpVal
}
var Linear Lerp = Lerp{}
var EaseOut Bezier = Bezier{
bezier2.T{
vec2.T{0.0, 0.0},
vec2.T{1.0, 0.0},
vec2.T{1.0, 0.0},
vec2.T{1.0, 1.0},
},
}
var EaseIn Bezier = Bezier{
bezier2.T{
vec2.T{0.0, 0.0},
vec2.T{0.0, 1.0},
vec2.T{0.0, 1.0},
vec2.T{1.0, 1.0},
},
}
var EaseInOut Bezier = Bezier{
bezier2.T{
vec2.T{0.0, 0.0},
vec2.T{1.0, 0.0},
vec2.T{0.0, 1.0},
vec2.T{1.0, 1.0},
},
}
var BezLerp Bezier = Bezier{
bezier2.T{
vec2.T{0.0, 0.0},
vec2.T{0.0, 0.0},
vec2.T{1.0, 1.0},
vec2.T{1.0, 1.0},
},
}
var BezFlash Bezier = Bezier{
bezier2.T{
vec2.T{0.0, 0.0},
vec2.T{0.1, 1.0},
vec2.T{0.2, 0.5},
vec2.T{0.0, 0.0},
},
}
func NewBezier(a, b, c, d glm.Vec2) Bezier {
return Bezier{
bezier2.T{
vec2.T{a.X, a.Y},
vec2.T{b.X, b.Y},
vec2.T{c.X, c.Y},
vec2.T{d.X, d.Y},
},
}
}
// Note: https://www.w3schools.com/cssref/func_cubic-bezier.php#:~:text=P0%20is%20(0%2C%200),transition%2Dtiming%2Dfunction%20property.
// Essentially: First point is (0, 0), last point is (1, 1) and you can define the two points in the middle
func NewCubicBezier(b, c glm.Vec2) Bezier {
return Bezier{
bezier2.T{
vec2.T{0, 0},
vec2.T{b.X, b.Y},
vec2.T{c.X, c.Y},
vec2.T{1, 1},
},
}
}
func Step(divRatio float64, a, b Interp) StepF {
return StepF{
a: a,
b: b,
divRatio: divRatio,
}
}
type StepF struct {
a, b Interp
divRatio float64
}
func (i StepF) get(t float64) (Interp, float64) {
if t < i.divRatio {
newT := t / (i.divRatio - 0)
return i.a, newT
}
newT := t / (1 - i.divRatio)
return i.b, newT
}
func (i StepF) Float64(a, b float64, t float64) float64 {
itrp, val := i.get(t)
return itrp.Float64(a, b, val)
}
func (i StepF) Float32(a, b float32, t float64) float32 {
itrp, val := i.get(t)
return itrp.Float32(a, b, val)
}
func (i StepF) Uint8(a, b uint8, t float64) uint8 {
itrp, val := i.get(t)
return itrp.Uint8(a, b, val)
}
func (i StepF) Vec2(a, b glm.Vec2, t float64) glm.Vec2 {
itrp, val := i.get(t)
return itrp.Vec2(a, b, val)
}
func Const(val float64) Bezier {
a := vec2.T{0, val}
return Bezier{
bezier2.T{a, a, a, a},
}
}
// var Sinusoid *Equation = &Equation{
// Func: SinFunc{},
// }
// https://cubic-bezier.com/#.22,1,.36,1
var EaseOutQuint Bezier = Bezier{
bezier2.T{
vec2.T{0.0, 0.0},
vec2.T{0.22, 1.0},
vec2.T{0.36, 1.0},
vec2.T{1.0, 1.0},
},
}
var EaseTest Bezier = Bezier{
bezier2.T{
vec2.T{0.0, 0.0},
vec2.T{0.0, 1.0},
vec2.T{0.0, 2.0},
vec2.T{1.0, 1.0},
},
}
type Interp interface {
Uint8(uint8, uint8, float64) uint8
Float32(float32, float32, float64) float32
Float64(float64, float64, float64) float64
Vec2(glm.Vec2, glm.Vec2, float64) glm.Vec2
}
type Lerp struct {
}
func (i Lerp) Float64(a, b float64, t float64) float64 {
m := b - a // Slope = Rise over run | Note: Run = (1 - 0)
y := (m * t) + a
return y
}
func (i Lerp) Float32(a, b float32, t float64) float32 {
m := b - a // Slope = Rise over run | Note: Run = (1 - 0)
y := (m * float32(t)) + a
return y
}
func (i Lerp) Uint8(a, b uint8, t float64) uint8 {
return uint8(i.Float64(float64(a), float64(b), t))
}
func (i Lerp) Vec2(a, b glm.Vec2, t float64) glm.Vec2 {
ret := glm.Vec2{
i.Float64(a.X, b.X, t),
i.Float64(a.Y, b.Y, t),
}
return ret
}
// Linearly interpolates integers, rounding them to the nearest
func Int[T constraints.Integer](a, b T, t float64) T {
return T(math.Round(Linear.Float64(float64(a), float64(b), t)))
}
// Linearly interpolates floats
func Float[T constraints.Float](a, b T, t float64) T {
return T(Linear.Float64(float64(a), float64(b), t))
}
type Bezier struct {
bezier2.T
}
func (i Bezier) Float64(a, b float64, t float64) float64 {
iValue := i.T.Point(t)
return Linear.Float64(a, b, iValue[1])
}
func (i Bezier) Float32(a, b float32, t float64) float32 {
iValue := i.T.Point(t)
return Linear.Float32(a, b, iValue[1])
}
func (i Bezier) Uint8(a, b uint8, t float64) uint8 {
iValue := i.T.Point(t)
return Linear.Uint8(a, b, iValue[1])
}
func (i Bezier) Vec2(a, b glm.Vec2, t float64) glm.Vec2 {
iValue := i.T.Point(t)
return Linear.Vec2(a, b, iValue[1])
}
type Sine struct{}
func (i Sine) Float64(a, b float64, t float64) float64 {
iValue := math.Sin(t * math.Pi)
return Linear.Float64(a, b, iValue)
}
func (i Sine) Float32(a, b float32, t float64) float32 {
iValue := math.Sin(t * math.Pi)
return Linear.Float32(a, b, iValue)
}
func (i Sine) Uint8(a, b uint8, t float64) uint8 {
iValue := math.Sin(t * math.Pi)
return Linear.Uint8(a, b, iValue)
}
func (i Sine) Vec2(a, b glm.Vec2, t float64) glm.Vec2 {
iValue := math.Sin(t * math.Pi)
return Linear.Vec2(a, b, iValue)
}
type Equation struct {
Func Function
}
func (i *Equation) Float64(a, b float64, t float64) float64 {
iValue := i.Func.Interp(t)
return Linear.Float64(a, b, iValue)
}
func (i *Equation) Float32(a, b float32, t float64) float32 {
iValue := i.Func.Interp(t)
return Linear.Float32(a, b, iValue)
}
func (i *Equation) Uint8(a, b uint8, t float64) uint8 {
iValue := i.Func.Interp(t)
return Linear.Uint8(a, b, iValue)
}
func (i *Equation) Vec2(a, b glm.Vec2, t float64) glm.Vec2 {
iValue := i.Func.Interp(t)
return Linear.Vec2(a, b, iValue)
}
type Function interface {
Interp(t float64) float64
}
type SinFunc struct {
Radius float64
Freq float64
ShiftY float64
}
func (s SinFunc) Interp(t float64) float64 {
return s.Radius * (s.ShiftY + math.Sin(t*s.Freq))
}
type CosFunc struct {
Radius float64
Freq float64
ShiftY float64
}
func (s CosFunc) Interp(t float64) float64 {
return s.Radius * (s.ShiftY + math.Cos(t*s.Freq))
}
type BezFunc struct {
Radius float64
Dur float64
Bezier Bezier
}
func (f BezFunc) Interp(t float64) float64 {
if f.Dur == 0 {
return f.Radius * f.Bezier.Float64(0, 1, t)
} else {
// else normalize by the dur
if t >= f.Dur {
return f.Radius
}
norm := t / f.Dur
return f.Radius * f.Bezier.Float64(0, 1, norm)
}
}
type LineFunc struct {
Slope float64
Intercept float64 // The Y intercept
}
func (f LineFunc) Interp(t float64) float64 {
return f.Slope*t + f.Intercept
}
type AddFunc struct {
A, B Function
}
func (f AddFunc) Interp(t float64) float64 {
return f.A.Interp(t) + f.B.Interp(t)
}
type MultFunc struct {
A, B Function
}
func (f MultFunc) Interp(t float64) float64 {
return f.A.Interp(t) * f.B.Interp(t)
}
func Color(lerp Interp, start, finish glm.RGBA, val float64) glm.RGBA {
return glm.RGBA{
R: lerp.Float64(start.R, finish.R, val),
G: lerp.Float64(start.G, finish.G, val),
B: lerp.Float64(start.B, finish.B, val),
A: lerp.Float64(start.A, finish.A, val),
}
}
// Code generated by cod; DO NOT EDIT.
package particle
import (
"github.com/unitoftime/ecs"
)
var ColorComp = ecs.NewComp[Color]()
func (c Color) CompId() ecs.CompId {
return ColorComp.CompId()
}
func (c Color) CompWrite(w ecs.W) {
ColorComp.WriteVal(w, c)
}
var SizeComp = ecs.NewComp[Size]()
func (c Size) CompId() ecs.CompId {
return SizeComp.CompId()
}
func (c Size) CompWrite(w ecs.W) {
SizeComp.WriteVal(w, c)
}
package particle
import (
"math"
"math/rand"
"time"
"github.com/unitoftime/ecs"
"github.com/unitoftime/flow/glm"
"github.com/unitoftime/flow/interp"
"github.com/unitoftime/flow/pgen"
)
// + Position
// + Velocity
// - Acceleration
// + Size
// + Color
// + Type
// TODO
// - Rotation
// - Special Emitter
// func init() {
// gob.Register(ConstantPositioner{})
// gob.Register(CopyPositioner{})
// gob.Register(RingPositioner{})
// gob.Register(AnglePositioner{})
// gob.Register(PathPositioner{})
// gob.Register(PhysicsUpdater{})
// }
// TODO - Move to a math package once generics comes out
func Clamp(min, max, val float64) float64 {
if val < min {
return min
} else if val > max {
return max
}
return val
}
// --------------------------------------------------------------------------------------------------
// - Positioners
// --------------------------------------------------------------------------------------------------
type Lifetime struct {
Total time.Duration
Remaining time.Duration
}
func NewLifetime(total time.Duration) Lifetime {
return Lifetime{
Total: total,
Remaining: total,
}
}
func (l *Lifetime) Ratio() float64 {
if l.Total == 0 {
return 0
}
return 1 - Clamp(0, 1.0, l.Remaining.Seconds()/l.Total.Seconds())
}
//cod:component
type Color struct {
Interp interp.Interp
Start, End glm.RGBA
}
func (c *Color) Get(ratio float64) glm.RGBA {
ratio = Clamp(0, 1.0, ratio)
color := glm.RGBA{
c.Interp.Float64(c.Start.R, c.End.R, ratio),
c.Interp.Float64(c.Start.G, c.End.G, ratio),
c.Interp.Float64(c.Start.B, c.End.B, ratio),
c.Interp.Float64(c.Start.A, c.End.A, ratio),
}
return color
}
type Float64Getter interface {
Get() float64
}
type Vec2Getter interface {
Get() glm.Vec2
}
type StaticVec2 struct {
Vec glm.Vec2
}
func (r StaticVec2) Get() glm.Vec2 {
return r.Vec
}
type RandomSquareVec2 struct {
Min, Max float64
}
func (r RandomSquareVec2) Get() glm.Vec2 {
rng := pgen.Range[float64]{r.Min, r.Max}
v := rng.Get()
return glm.Vec2{v, v}
}
type RandomVec2 struct {
RangeX pgen.Range[float64]
RangeY pgen.Range[float64]
}
func (r RandomVec2) Get() glm.Vec2 {
return glm.Vec2{r.RangeX.Get(), r.RangeY.Get()}
}
type SizeBuilder interface {
Get() Size
}
type StartEndSize struct {
Interp interp.Interp
Start Vec2Getter
End Vec2Getter
}
func (s StartEndSize) Get() Size {
return Size{
Interp: s.Interp,
Start: s.Start.Get(),
End: s.End.Get(),
}
}
type StartOffsetSize struct {
Interp interp.Interp
Start Vec2Getter
Offset Vec2Getter
}
func (s StartOffsetSize) Get() Size {
start := s.Start.Get()
offset := s.Offset.Get()
return Size{
Interp: s.Interp,
Start: start,
End: start.Add(offset),
}
}
type RGBAGetter interface {
Get() glm.RGBA
}
type StaticColor struct {
Col glm.RGBA
}
func (r StaticColor) Get() glm.RGBA {
return r.Col
}
type RandomVariableColor struct {
Col glm.RGBA
Mult glm.RGBA
}
func (r RandomVariableColor) Get() glm.RGBA {
multCol := glm.RGBA{
R: (pgen.CenteredFloat64(r.Mult.R)),
G: (pgen.CenteredFloat64(r.Mult.G)),
B: (pgen.CenteredFloat64(r.Mult.B)),
A: 1.0,
}
col := r.Col.Add(multCol)
return col
// alpha := 1 + pgen.CenteredFloat64(r.Mult.A)
// multCol := glm.RGBA{
// R: (1 + pgen.CenteredFloat64(r.Mult.R)) * alpha,
// G: (1 + pgen.CenteredFloat64(r.Mult.G)) * alpha,
// B: (1 + pgen.CenteredFloat64(r.Mult.B)) * alpha,
// A: alpha,
// }
// col := r.Col.Mult(multCol)
// return col
}
type RandomValueColor struct {
Col glm.RGBA
ValueRadius float64
}
func (r RandomValueColor) Get() glm.RGBA {
valueRadius := pgen.CenteredFloat64(r.ValueRadius)
greyscale := glm.Greyscale(1 + valueRadius)
col := r.Col.Mult(greyscale).Clamp()
return col
}
type ColorBuilder interface {
Get() Color
}
type StartOffsetColor struct {
Interp interp.Interp
Start RGBAGetter
Offset RGBAGetter
}
func (s StartOffsetColor) Get() Color {
start := s.Start.Get()
offset := s.Offset.Get()
end := glm.RGBA{
R: start.R + offset.R,
G: start.G + offset.G,
B: start.B + offset.B,
A: start.A + offset.A,
}
return Color{
Interp: s.Interp,
Start: start,
End: end,
}
}
type StartEndColor struct {
Interp interp.Interp
Start RGBAGetter
End RGBAGetter
}
func (s StartEndColor) Get() Color {
start := s.Start.Get()
end := s.End.Get()
return Color{
Interp: s.Interp,
Start: start,
End: end,
}
}
//cod:component
type Size struct {
Interp interp.Interp
Start, End glm.Vec2
}
// func NewSize(interpolation interp.Interp, start, end glm.Vec2) Size {
// return Size{
// Interp: interpolation,
// Start: start,
// End: end,
// }
// }
func (s *Size) Get(ratio float64) glm.Vec2 {
ratio = Clamp(0, 1.0, ratio)
size := glm.Vec2{
s.Interp.Float64(s.Start.X, s.End.X, ratio),
s.Interp.Float64(s.Start.Y, s.End.Y, ratio),
}
return size
}
// --------------------------------------------------------------------------------------------------
// - ComponentFactory
// --------------------------------------------------------------------------------------------------
type PrefabBuilder interface {
Add(*ecs.Entity)
}
// type RingBuilder struct {
// AngleRange glm.Vec2
// RadiusRange glm.Vec2
// }
// func (p *RingBuilder) Add(prefab *ecs.Entity) {
// angle := interp.Linear.Float64(p.AngleRange.X, p.AngleRange.Y, rand.Float64())
// radius := interp.Linear.Float64(p.RadiusRange.X, p.RadiusRange.Y, rand.Float64())
// // vec := vec2.UnitX
// vec := glm.Vec2{1, 0}
// vec.Scaled(radius).Rotated(angle)
// prefab.Add(ecs.C(glm.Pos{vec.X, vec.Y}))
// }
// // type AngleBuilder struct {
// // Scale float64
// // }
// // func (p *AngleBuilder) Add(prefab ecs.Entity) {
// // transform := prefab.Read(glm.Transform{}).(glm.Transform)
// // pos := vec2.T{transform.X, transform.Y}
// // prefab.Write(glm.Rigidbody{
// // Mass: 1,
// // Velocity: pos.Normalize().Scaled(p.Scale),
// // })
// // }
// // TODO - Should I just build this into the emitter?
// type LifetimeBuilder struct {
// Range glm.Vec2 // Specified in seconds
// }
// func (b *LifetimeBuilder) Add(prefab *ecs.Entity) {
// seconds := interp.Linear.Float64(b.Range.X, b.Range.Y, rand.Float64())
// prefab.Add(ecs.C(
// NewLifetime(time.Duration(seconds * 1000) * time.Millisecond),
// ))
// }
// type TransformBuilder struct {
// PosPositioner Vec2Positioner
// }
// func (p *TransformBuilder) Add(prefab *ecs.Entity) {
// pos := p.PosPositioner.Vec2(glm.Vec2{})
// prefab.Add(ecs.C(glm.Pos{pos.X, pos.Y}))
// }
// type RigidbodyBuilder struct {
// Mass float64
// VelPositioner Vec2Positioner
// }
// func (b *RigidbodyBuilder) Add(prefab *ecs.Entity) {
// // transform := prefab.Read(glm.Transform{}).(glm.Transform)
// // transform, _ := ecs.ReadFromEntity[glm.Transform](prefab)
// // pos := glm.Vec2{transform.X, transform.Y}
// pos, _ := ecs.ReadFromEntity[glm.Pos](prefab)
// vel := b.VelPositioner.Vec2(glm.Vec2(pos))
// prefab.Add(ecs.C(glm.Rigidbody{
// Mass: b.Mass,
// Velocity: vel,
// }))
// }
// type ConstantBuilder struct {
// Component interface{}
// }
// func (b *RigidbodyBuilder) Add(prefab ecs.Entity) {
// }
// --------------------------------------------------------------------------------------------------
// - Positioners
// --------------------------------------------------------------------------------------------------
type Float64Positioner interface {
Float64(count int, A float64) float64
}
type ConstFloat64Positioner struct {
Val float64
}
func (p ConstFloat64Positioner) Float64(count int, v float64) float64 {
return v + p.Val
}
type RandomFloat64Positioner struct {
Min, Max float64
}
func (p RandomFloat64Positioner) Float64(count int, v float64) float64 {
w := p.Max - p.Min
return v + p.Min + (w * rand.Float64())
// return v + (2 * math.Pi * rand.Float64())
}
type Vec2Positioner interface {
Vec2(count int, A glm.Vec2) glm.Vec2
}
type ConstantPositioner struct {
X, Y float64
}
func (p ConstantPositioner) Vec2(count int, A glm.Vec2) glm.Vec2 {
return glm.Vec2{p.X, p.Y}
}
type GeometricPositioner struct {
Scale glm.Vec2 // X is min, Y is max
Offset float64
DistanceOffset float64
DistanceMod int
DistanceRem int
}
func (p GeometricPositioner) Vec2(count int, A glm.Vec2) glm.Vec2 {
theta := 2 * math.Pi * float64(count) * p.Offset
// w := p.Scale.Y - p.Scale.X
// scale := (w * rand.Float64()) + p.Scale.X
modCount := count
if p.DistanceMod != 0 {
modCount = count % p.DistanceMod
}
if p.DistanceRem != 0 {
modCount = count / p.DistanceRem
}
scale := float64(modCount) * p.DistanceOffset
x := scale * math.Cos(theta)
y := scale * math.Sin(theta)
return glm.Vec2{x, y}.Add(A)
}
type CirclePositioner struct {
Scale glm.Vec2 // X is min, Y is max
}
func (p CirclePositioner) Vec2(count int, A glm.Vec2) glm.Vec2 {
theta := 2 * math.Pi * rand.Float64()
w := p.Scale.Y - p.Scale.X
scale := (w * rand.Float64()) + p.Scale.X
x := scale * math.Cos(theta)
y := scale * math.Sin(theta)
return glm.Vec2{x, y}.Add(A)
}
type RectPositioner struct {
Min, Max glm.Vec2 // TODO - rectangle passed in
}
func (p RectPositioner) Vec2(count int, A glm.Vec2) glm.Vec2 {
w := p.Max.X - p.Min.X
h := p.Max.Y - p.Min.Y
x := w*rand.Float64() + p.Min.X
y := h*rand.Float64() + p.Min.Y
return glm.Vec2{x, y}
}
type CopyPositioner struct {
Scale float64
}
func (p *CopyPositioner) Vec2(count int, A glm.Vec2) glm.Vec2 {
return A.Scaled(p.Scale)
}
type AnglePositioner struct {
Scale float64
}
func (p *AnglePositioner) Vec2(count int, A glm.Vec2) glm.Vec2 {
return A.Norm().Scaled(p.Scale)
}
type RingPositioner struct {
AngleRange glm.Vec2
RadiusRange glm.Vec2
}
func (p RingPositioner) Vec2(count int, A glm.Vec2) glm.Vec2 {
angle := interp.Linear.Float64(p.AngleRange.X, p.AngleRange.Y, rand.Float64())
radius := interp.Linear.Float64(p.RadiusRange.X, p.RadiusRange.Y, rand.Float64())
vec := glm.Vec2{1, 0}
vec = vec.Scaled(radius).Rotated(angle)
return vec
}
// type FirePositioner struct {
// }
// func (p *FirePositioner) Vec2(A glm.Vec2) glm.Vec2 {
// return glm.Vec2{-A.X, 5}
// }
// An emitter is used to spawn particles in a certain way
type Emitter struct {
// Max int
Rate float64 // Spawn how many per frame
period int
OneShot bool
Loop bool
Probability float64
// Duration time.Duration
// Type ParticleType
Prefab *ecs.Entity
// SizeCurve, ColorCurve interp.Interp
// Timer timer.Timer
Builders []PrefabBuilder
// PosPositioner Vec2Positioner
// VelPositioner Vec2Positioner
// SizePositioner Vec2Positioner
// PosBuilder PrefabBuilder
// RbBuilder PrefabBuilder
// RedPositioner Vec2Positioner
// GreenPositioner Vec2Positioner
// BluePositioner Vec2Positioner
// AlphaPositioner Vec2Positioner
}
// func (e *Emitter) Update(world *ecs.World, position glm.Vec2, dt time.Duration) {
// count := 0
// // if e.OneShot {
// // count = e.Max
// // } else {
// // particles.EmissionCounter += dt
// // numParticles := math.Floor(particles.EmissionCounter.Seconds() * e.Rate)
// // particles.EmissionCounter -= time.Duration(math.Floor((numParticles / e.Rate) * 1e9)) * time.Nanosecond
// // count = int(numParticles)
// // }
// if e.Rate == 0 {
// return // Exit early if rate is set to 0
// }
// // 1/rate is the period, scaled to ms and then converted to duration
// // period := time.Duration(1000 * (1 / e.Rate)) * time.Millisecond
// period := int(1 / e.Rate)
// if period < 1.0 {
// count = int(e.Rate)
// } else {
// if e.period < 0 {
// e.period = period
// count = 1
// }
// e.period--
// }
// for i := 0; i < count; i++ {
// randP := rand.Float64()
// if randP < e.Probability {
// ok := e.Spawn(glm.Vec2{position.X, position.Y}, world)
// if !ok { break }
// }
// }
// // TODO - needs to be configurable
// // particles.Accel = ecs.Accelerator{pixel.V(position.X, position.Y)}
// }
// func (e *Emitter) Spawn(entPos glm.Vec2, world *ecs.World) bool {
// // If we don't loop, then only emit a Total equal to Max
// // if !e.Loop {
// // if p.Total > p.Max {
// // return false
// // }
// // }
// // Don't spawn if we're already full
// // if len(p.list) >= e.Max {
// // return false
// // }
// for i := range e.Builders {
// e.Builders[i].Add(e.Prefab)
// }
// // transform := e.Prefab.Read(glm.Transform{}).(glm.Transform)
// // transform, _ := ecs.ReadFromEntity[glm.Transform](e.Prefab)
// // transform.X += entPos.X
// // transform.Y += entPos.Y
// // e.Prefab.Add(ecs.C(transform))
// pos, _ := ecs.ReadFromEntity[glm.Pos](e.Prefab)
// pos.X += entPos.X
// pos.Y += entPos.Y
// e.Prefab.Add(ecs.C(pos))
// // sizes := e.SizePositioner.Vec2(vec2.Zero)
// id := world.NewId()
// e.Prefab.Write(world, id)
// return true
// }
// type ParticleType uint8
// type Particle struct {
// Position glm.Vec2
// Velocity glm.Vec2
// // Interpolation Values
// Size glm.Vec2
// Red glm.Vec2
// Green glm.Vec2
// Blue glm.Vec2
// Alpha glm.Vec2
// Type ParticleType
// MaxLife, Life time.Duration
// ratio float64 // Life ratio 0 = Full Life | 1 = No Life
// }
// func (p *Particle) GetSize(curve interp.Interp) float64 {
// // return Lerp(p.Size, p.ratio)
// // iValue := curve.Point(p.ratio)
// // return Lerp(p.Size, iValue.Y)
// return curve.Float64(p.Size.X, p.Size.Y, p.ratio)
// }
// func (p *Particle) GetColor(curve interp.Interp) color.NRGBA {
// // color := color.NRGBA{
// // uint8(Lerp(p.Red, p.ratio)),
// // uint8(Lerp(p.Green, p.ratio)),
// // uint8(Lerp(p.Blue, p.ratio)),
// // uint8(Lerp(p.Alpha, p.ratio)),
// // }
// // iValue := curve.Point(p.ratio)
// // color := color.NRGBA{
// // uint8(Lerp(p.Red, iValue.Y)),
// // uint8(Lerp(p.Green, iValue.Y)),
// // uint8(Lerp(p.Blue, iValue.Y)),
// // uint8(Lerp(p.Alpha, iValue.Y)),
// // }
// color := color.NRGBA{
// uint8(curve.Float64(p.Red.X, p.Red.Y, p.ratio)),
// uint8(curve.Float64(p.Green.X, p.Green.Y, p.ratio)),
// uint8(curve.Float64(p.Blue.X, p.Blue.Y, p.ratio)),
// uint8(curve.Float64(p.Alpha.X, p.Alpha.Y, p.ratio)),
// }
// return color
// }
// type Accelerator struct {
// Position glm.Vec2
// }
// func (a *Accelerator) GetAcceleration(p *Particle) glm.Vec2 {
// vec := vec2.Sub(&a.Position, &p.Position)
// return vec.Scaled(0.01)
// }
// type ParticleUpdater interface {
// Update(*Particle, time.Duration)
// }
// type PhysicsUpdater struct {
// // Acceleration Type (if we want)
// }
// func (u PhysicsUpdater) Update(p *Particle, dt time.Duration) {
// delta := p.Velocity.Scaled(dt.Seconds())
// p.Position.Add(&delta)
// }
// type PathUpdater struct {
// Path []glm.Vec2
// }
// func (u PathUpdater) Update(p *Particle, dt time.Duration) {
// // delta := p.Velocity.Scaled(dt.Seconds())
// // p.Position.Add(&delta)
// }
// type Group struct {
// Max int
// Total int
// EmissionCounter time.Duration
// Updater ParticleUpdater
// PosPositioner Vec2Positioner
// SizeCurve interp.Interp
// ColorCurve interp.Interp
// list []Particle
// }
// func NewGroup(initSize int, updater ParticleUpdater, sizeCurve, colorCurve interp.Interp) Group {
// return Group{
// Max: initSize,
// Updater: updater,
// SizeCurve: sizeCurve,
// ColorCurve: colorCurve,
// list: make([]Particle, initSize),
// }
// }
// func (g *Group) List() []Particle {
// return g.list
// }
// func (g *Group) Update(dt time.Duration) {
// for i := range g.list {
// g.list[i].Life -= dt
// g.Updater.Update(&g.list[i], dt)
// }
// // This loop removes p whose life has expired
// i := 0
// for {
// if i >= len(g.list) { break }
// if g.list[i].Life <= 0 {
// // If our life is over then we get removed
// // Move last element to this position
// g.list[i] = g.list[len(g.list)-1]
// g.list = g.list[:len(g.list)-1]
// } else {
// i++
// }
// }
// }
// type PathPositioner struct {
// Path []glm.Vec2
// Lengths []float64
// TotalLength float64
// Variation glm.Vec2
// }
// func RandomPath(start, end glm.Vec2, n int, pathVariation float64, variation glm.Vec2) *PathPositioner {
// path := make([]glm.Vec2, n)
// // iValues := make(float64, n)
// // iValues.X = 0 // Interpolate to start point
// // iValues[len(iValues)-1] = 1.0 // Interpolate to end point
// path.X = start
// path[len(path)-1] = end
// nVec := vec2.Sub(&end, &start)
// latVec := nVec.Normalize().Rotate90DegLeft()
// for i := 1; i < n-2; i++ {
// interpVec := vec2.Interpolate(&start, &end, rand.Float64())
// rnd := 2 * (rand.Float64() - 0.5) * pathVariation
// lateral := latVec.Scaled(rnd)
// path[i] = vec2.Add(&interpVec, &lateral)
// }
// sort.Slice(path, func(i, j int) bool {
// ii := vec2.Sub(&start, &path[i])
// jj := vec2.Sub(&start, &path[j])
// return ii.LengthSqr() < jj.LengthSqr()
// })
// return StraightPath(path, variation)
// }
// func StraightPath(path []glm.Vec2, variation glm.Vec2) *PathPositioner {
// lengths := make([]float64, 0, len(path)-1)
// totalLength := 0.0
// for i := 0; i < len(path)-1; i++ {
// v := vec2.Sub(&path[i+1], &path[i])
// d := v.Length()
// lengths = append(lengths, d)
// totalLength += d
// }
// return &PathPositioner{
// Path: path,
// Lengths: lengths,
// TotalLength: totalLength,
// Variation: variation,
// }
// }
// func (p *PathPositioner) Vec2(A glm.Vec2) glm.Vec2 {
// rnd := rand.Float64() * p.TotalLength
// // Find a random interpolation value along the TotalLength
// // index -> index of the path point before our interp value
// // rnd -> Becomes the inner path's interp value
// index := 0
// for i,l := range p.Lengths {
// if rnd < l {
// index = i
// break
// } else {
// rnd -= l
// }
// }
// v := vec2.Sub(&p.Path[index+1], &p.Path[index])
// rndVal := v.Normalize().Scale(rnd)
// point := vec2.Add(&p.Path[index], rndVal)
// variation := glm.Vec2{
// p.Variation.X * (rand.Float64() * 2 - 1),
// p.Variation.Y * (rand.Float64() * 2 - 1),
// }
// point.Add(&variation)
// return point
// }
package pgen
import (
"fmt"
"math"
"math/rand"
"github.com/unitoftime/flow/ds"
"github.com/unitoftime/flow/glm"
"github.com/unitoftime/flow/tile"
)
// type RoomDagNode struct {
// Data string
// }
// TODO: Dump string labels and use int labels?
type RoomDag struct {
Nodes []string // Holds the nodes in the order that they were added
NodeMap map[string]int // holds the rank of the node (to prevent cycles)
Edges map[string][]string // This is the dag
rank int
}
func NewRoomDag() *RoomDag {
return &RoomDag{
Nodes: make([]string, 0),
NodeMap: make(map[string]int),
Edges: make(map[string][]string),
}
}
func (d *RoomDag) AddNode(label string) bool {
_, ok := d.NodeMap[label]
if ok {
return false
} // already exists
d.NodeMap[label] = d.rank
d.Nodes = append(d.Nodes, label)
d.Edges[label] = make([]string, 0)
d.rank++
return true
}
func (d *RoomDag) AddEdge(from, to string) {
fromNode, ok := d.NodeMap[from]
if !ok {
return
} // TODO: false?
toNode, ok := d.NodeMap[to]
if !ok {
return
} // TODO: false?
// Only add edges from lower to higher ranks
if fromNode < toNode {
d.Edges[from] = append(d.Edges[from], to)
} else {
d.Edges[to] = append(d.Edges[to], from)
}
}
// Returns true if the node is a leaf node (ie has no outward facing edges)
func (d *RoomDag) IsLeafNode(label string) bool {
edges, ok := d.Edges[label]
if !ok {
return true
}
return len(edges) <= 0
}
// Returns true if the node is a leaf node (ie has no outward facing edges)
func (d *RoomDag) GetLeafNodes() []string {
ret := make([]string, 0)
for _, node := range d.Nodes {
if d.IsLeafNode(node) {
ret = append(ret, node)
}
}
return ret
}
// Counts and returns the number of edges needed to traverse to get from one node to another
func (d *RoomDag) Distance(from, to string) int {
distance := make(map[string]int)
queue := ds.NewQueue[string](0) // TODO: dynamically resized
queue.Add(from)
for {
currentNode, ok := queue.Remove()
if !ok {
break
}
currentNodeDistance := distance[currentNode]
if currentNode == to {
return currentNodeDistance
}
children := d.Edges[currentNode]
for _, child := range children {
_, exists := distance[child]
if !exists {
distance[child] = currentNodeDistance + 1
queue.Add(child)
}
}
}
return -1 // Could not find a path from -> to
}
func (d *RoomDag) HasEdgeEitherDirection(from, to string) bool {
return d.HasEdge(from, to) || d.HasEdge(to, from)
}
func (d *RoomDag) HasEdge(from, to string) bool {
edges := d.Edges[from]
for i := range edges {
if to == edges[i] {
return true
}
}
return false
}
// returns the node labels in topological order
func (d *RoomDag) TopologicalSort(start string) []string {
visit := make(map[string]bool)
queue := ds.NewQueue[string](0) // TODO: dynamically resized
queue.Add(start)
ret := make([]string, 0, len(d.Nodes))
for {
currentNode, ok := queue.Remove()
if !ok {
break
}
visited := visit[currentNode]
if visited {
continue
}
visit[currentNode] = true
ret = append(ret, currentNode)
children := d.Edges[currentNode]
for _, child := range children {
queue.Add(child)
}
}
return ret
}
func (d *RoomDag) RandomLabel(rng *rand.Rand) string {
if len(d.Nodes) <= 0 {
return ""
}
idx := rng.Intn(len(d.Nodes))
return d.Nodes[idx]
}
func (d *RoomDag) RandomSortedLabel(startLabel string, rng *rand.Rand, topoAfter, topoBeforeEnd int) string {
if len(d.Nodes) <= 0 {
return ""
}
nodes := d.TopologicalSort(startLabel)
after := topoAfter
if after >= len(nodes) {
after = len(nodes) - 1
}
before := len(nodes) - topoBeforeEnd
if before >= len(nodes) {
before = len(nodes) - 1
}
rngRange := Range[int]{after, before}
idx := rngRange.SeededGet(rng)
// Fallback
if idx < 0 || idx >= len(nodes) {
return d.RandomLabel(rng)
}
return nodes[idx]
}
// func GenerateRandomGridWalkDag(num int, numWalks int) *RoomDag {
// dag := NewRoomDag()
// pos := make([]tile.TilePosition, numWalks)
// lastLabel := make([]string, numWalks)
// for len(dag.Nodes) < num {
// for i := range pos {
// label := fmt.Sprintf("%d_%d", pos[i].X, pos[i].Y)
// _, exists := dag.NodeMap[label]
// if !exists {
// dag.AddNode(label)
// }
// dir := rand.Intn(4)
// if dir == 0 {
// pos[i].X++
// } else if dir == 1 {
// pos[i].X--
// } else if dir == 2 {
// pos[i].Y++
// } else {
// pos[i].Y--
// }
// if lastLabel[i] != "" {
// dag.AddEdge(lastLabel[i], label)
// }
// lastLabel[i] = label
// }
// }
// return dag
// }
func GenerateRandomGridWalkDag2(rng *rand.Rand, numRooms int, numWalks int) (*RoomDag, map[string]tile.TilePosition) {
if numWalks <= 0 {
numWalks = 1 // This is just a minimum
}
dag := NewRoomDag()
roomPos := make(map[string]tile.TilePosition)
curWalkPos := make([]tile.TilePosition, numWalks)
lastLabel := make([]string, numWalks)
for len(dag.Nodes) < numRooms {
for i := range curWalkPos {
label := fmt.Sprintf("%d_%d", curWalkPos[i].X, curWalkPos[i].Y)
_, exists := dag.NodeMap[label]
if !exists {
dag.AddNode(label)
roomPos[label] = curWalkPos[i]
}
dir := rng.Intn(4)
if dir == 0 {
curWalkPos[i].X++
} else if dir == 1 {
curWalkPos[i].X--
} else if dir == 2 {
curWalkPos[i].Y++
} else {
curWalkPos[i].Y--
}
if lastLabel[i] != "" {
dag.AddEdge(lastLabel[i], label)
}
lastLabel[i] = label
}
}
return dag, roomPos
}
func GenerateRandomGridWalkDagNoOverlap(rng *rand.Rand, numRooms int, numWalks int) (*RoomDag, map[string]tile.TilePosition) {
if numWalks <= 0 {
numWalks = 1 // This is just a minimum
}
dag := NewRoomDag()
roomPos := make(map[string]tile.TilePosition)
curWalkPos := make([]tile.TilePosition, numWalks)
lastLabel := make([]string, numWalks)
for len(dag.Nodes) < numRooms {
for i := range curWalkPos {
label := fmt.Sprintf("%d_%d", curWalkPos[i].X, curWalkPos[i].Y)
_, exists := dag.NodeMap[label]
if !exists {
dag.AddNode(label)
roomPos[label] = curWalkPos[i]
}
nextPos := curWalkPos[i]
for jj := 0; jj < 10; jj++ {
nextPos = curWalkPos[i]
dir := rng.Intn(4)
if dir == 0 {
nextPos.X++
} else if dir == 1 {
nextPos.X--
} else if dir == 2 {
nextPos.Y++
} else {
nextPos.Y--
}
label := fmt.Sprintf("%d_%d", nextPos.X, nextPos.Y)
_, exists := dag.NodeMap[label]
if !exists {
break
}
}
curWalkPos[i] = nextPos
if lastLabel[i] != "" {
dag.AddEdge(lastLabel[i], label)
}
lastLabel[i] = label
}
}
return dag, roomPos
}
func addDagNodeData(dag *RoomDag, roomPos map[string]tile.TilePosition, pos tile.Position, lastLabel string) string {
label := fmt.Sprintf("%d_%d", pos.X, pos.Y)
_, exists := dag.NodeMap[label]
if !exists {
dag.AddNode(label)
roomPos[label] = pos
}
if lastLabel != "" {
dag.AddEdge(lastLabel, label)
}
return label
}
func BlankWalkDag() (*RoomDag, map[string]tile.TilePosition) {
dag := NewRoomDag()
roomPos := make(map[string]tile.TilePosition)
return dag, roomPos
}
func AddWalk(dag *RoomDag, roomPos map[string]tile.TilePosition, rng *rand.Rand, startPos tile.Position, walkLength int, walkNorthOnly bool) {
lastLabel := addDagNodeData(dag, roomPos, startPos, "")
lastWalkPos := startPos // The last position we added
for walkCounter := 0; walkCounter < walkLength; walkCounter++ {
nextPos := lastWalkPos
for i := 0; i < 10; i++ { // TODO: Hardcoded 10 attempts
nextPos = lastWalkPos
if walkNorthOnly {
nextPos.Y++
} else {
dir := rng.Intn(4)
if dir == 0 {
nextPos.X++
} else if dir == 1 {
nextPos.X--
} else if dir == 2 {
nextPos.Y++
} else {
nextPos.Y--
}
}
label := fmt.Sprintf("%d_%d", nextPos.X, nextPos.Y)
_, exists := dag.NodeMap[label]
if !exists {
break
}
}
lastLabel = addDagNodeData(dag, roomPos, nextPos, lastLabel)
lastWalkPos = nextPos
}
}
// Calculates the depth of each room placement based on the dag
func CalculateRoomDepths(dag *RoomDag, placements map[string]RoomPlacement, start string) {
queue := ds.NewQueue[string](len(placements))
queue.Add(start)
visit := make(map[string]bool)
for {
currentNode, ok := queue.Remove()
if !ok {
return
} // Done when there's nothing in the queue
currentPlacement, ok := placements[currentNode]
if !ok {
panic("MUST BE SET")
}
currentDepth := currentPlacement.Depth
children := dag.Edges[currentNode]
for _, child := range children {
visited := visit[child]
if visited {
continue
}
visit[child] = true
childPlacement := placements[child]
childPlacement.Depth = currentDepth + 1
placements[child] = childPlacement
queue.Add(child)
}
}
}
// func GenerateRoomDag(num int, edgeProbability float64, maxEdges int) *RoomDag {
// dag := NewRoomDag()
// for i := 0; i < num; i++ {
// dag.AddNode(fmt.Sprintf("%d", i))
// }
// for i := 0; i < num; i++ {
// for j := i+1; j < num; j++ {
// numIEdges := len(dag.Edges[fmt.Sprintf("%d", i)])
// if numIEdges >= maxEdges { break }
// if rand.Float64() < edgeProbability {
// dag.AddEdge(fmt.Sprintf("%d", i), fmt.Sprintf("%d", j))
// }
// }
// }
// return dag
// }
type RoomPlacement struct {
// MapDef *MapDefinition // TODO: generic
Rect tile.Rect
GoalGap float64
Static bool
Repel float64
Attract float64
Depth int
Mass float64
Placed bool
}
type ForceBasedRelaxer struct {
// targetGap int
rng *rand.Rand
dag *RoomDag
placements map[string]RoomPlacement
repel float64
grav float64
}
func NewForceBasedRelaxer(rng *rand.Rand, dag *RoomDag, placements map[string]RoomPlacement, startingRepel float64, startingGrav float64) *ForceBasedRelaxer {
return &ForceBasedRelaxer{
rng: rng,
dag: dag,
placements: placements,
repel: startingRepel,
grav: startingGrav,
}
}
func (r *ForceBasedRelaxer) Iterate() {
for node, edges := range r.dag.Edges {
for _, e := range edges {
if HasEdgeIntersections(r.dag, r.placements, node, e) {
room := r.placements[node]
room.Attract = 1.2
room.Repel += 1
r.placements[node] = room
} else {
// r := r.placements[node]
// r.Attract = 1.0
// r.Repel = float64(r.Rect.W() * r.Rect.H())
// rooms[node] = r
}
}
}
for label, p := range r.placements {
if HasRectIntersections(r.placements, label) {
// p.GoalGap++
p.Repel += 1
r.placements[label] = p
} else {
// p.GoalGap--
// rooms[label] = p
}
}
r.repel--
r.grav = 0.1
Wiggle(r.dag, r.placements, r.repel, r.grav, 1)
}
// Returns true if there were no intersections
func PSLDStep(rng *rand.Rand, dag *RoomDag, place map[string]RoomPlacement) bool {
for node, edges := range dag.Edges {
for _, e := range edges {
if HasEdgeIntersections(dag, place, node, e) {
// Move
p1, ok := place[node]
if !ok {
panic("AAA")
}
if p1.Static {
continue
}
p2, ok := place[e]
if !ok {
panic("AAA")
}
// We will find the vector from p1 to p2, then move both of them by that amount
delta := p2.Rect.Center().Sub(p1.Rect.Center())
p1.Rect = p1.Rect.Moved(delta)
// p2.Rect = p2.Rect.Moved(delta)
place[node] = p1
place[e] = p2
return false
}
}
}
return true
}
func PSLD(rng *rand.Rand, dag *RoomDag, place map[string]RoomPlacement) {
for {
noIntersections := PSLDStep(rng, dag, place)
if noIntersections {
break
}
}
}
type GridLayout struct {
rng *rand.Rand
queue *ds.Queue[string]
dag *RoomDag
place map[string]RoomPlacement
nodeList []string
topoIdx int
topoMoved map[string]bool
tolerance int
}
func NewGridLayout(rng *rand.Rand, dag *RoomDag, place map[string]RoomPlacement, start string, tolerance int) *GridLayout {
queue := ds.NewQueue[string](len(place)) // TODO: dynamically resized
queue.Add(start)
return &GridLayout{
rng: rng,
queue: queue,
dag: dag,
place: place,
nodeList: dag.TopologicalSort(start),
topoMoved: make(map[string]bool),
tolerance: tolerance,
}
}
func (l *GridLayout) LayoutGrid(roomPos map[string]tile.TilePosition) {
gridSize := 1
for {
for k, room := range l.place {
pos, ok := roomPos[k]
if !ok {
panic("AAA")
}
rect := room.Rect.WithCenter(tile.TilePosition{pos.X * gridSize, pos.Y * gridSize})
room.Rect = rect
l.place[k] = room
}
if !HasAnyRectIntersections(l.place) {
break
}
gridSize++
}
}
func (l *GridLayout) Expand() bool {
// Idea: try and move expand everything outwards until you have no more collisions
// Goal: to minimize edge lengths and keep things generally grid-like (ie prioritize non diagonals)
for _, parentNode := range l.nodeList {
if !HasRectIntersections(l.place, parentNode) {
continue
}
parent, ok := l.place[parentNode]
if !ok {
panic("AAA")
}
pos := parent.Rect.Center()
pos.X = -pos.X
pos.Y = -pos.Y
parent.Rect = l.MoveTowards(parent.Rect, pos, 1)
l.place[parentNode] = parent
}
return false
}
func (l *GridLayout) IterateGravity() bool {
// Idea: try and move everything in by a small amount, if you start overlapping, then move back
// Goal: to minimize edge lengths and keep things generally grid-like (ie prioritize non diagonals)
moved := make(map[string]bool)
done := true
for _, parentNode := range l.nodeList {
// parent, ok := l.place[parentNode]
// if !ok { panic("AAA") }
edges, ok := l.dag.Edges[parentNode]
if !ok {
panic("AAA")
}
for _, child := range edges {
alreadyMoved := moved[child]
if alreadyMoved {
continue
}
moved[child] = true
p, ok := l.place[child]
if !ok {
panic("AAA")
}
origCenter := p.Rect.Center()
// pos := parent.Rect.Center()
p.Rect = l.MoveTowards(p.Rect, origCenter, 1)
l.place[child] = p
// if HasRectIntersections(l.place, child) || AnyEdgesIntersect(l.dag, l.place) {
if HasRectIntersections(l.place, child) || !l.AllEdgesAxisAligned(l.tolerance) {
p.Rect = p.Rect.WithCenter(origCenter)
l.place[child] = p
} else {
done = false // If we ever moved something then we aren't done
}
}
}
return done
}
// TODO: tolerance?
func (l *GridLayout) AllEdgesAxisAligned(tolerance int) bool {
for _, n := range l.nodeList {
edges, ok := l.dag.Edges[n]
if !ok {
panic("AAA")
}
a := l.place[n]
r1 := a.Rect
for _, e := range edges {
b := l.place[e]
r2 := b.Rect
if !AxisAligned(tolerance, r1, r2) {
return false
}
}
}
return true
}
func AxisAligned(tol int, a, b tile.Rect) bool {
// align from center, adjusted for room width
minXRadius := math.Min(float64(a.W())/2, float64(b.W())/2)
tolX := int(minXRadius) - tol
xOverlap := int(math.Abs(float64(a.Center().X - b.Center().X)))
if xOverlap < tolX {
return true
}
minYRadius := math.Min(float64(a.H())/2, float64(b.H())/2)
tolY := int(minYRadius) - tol
yOverlap := int(math.Abs(float64(a.Center().Y - b.Center().Y)))
if yOverlap < tolY {
return true
}
return false
// // align from center
// xOverlap := int(math.Abs(float64(a.Center().X - b.Center().X)))
// if xOverlap < tol { return true }
// yOverlap := int(math.Abs(float64(a.Center().Y - b.Center().Y)))
// if yOverlap < tol { return true }
// return false
// // align with overlap
// xOverlap := (tol <= b.Max.X-a.Min.X) && (a.Max.X-b.Min.X >= tol)
// if xOverlap { return true }
// yOverlap := (tol <= b.Max.Y-a.Min.Y) && (a.Max.Y-b.Min.Y >= tol)
// if yOverlap { return true }
// return false
}
func (l *GridLayout) IterateTowardsParent() bool {
// Idea: try and move everything in by a small amount, if you start overlapping, then move back
// Goal: to minimize edge lengths and keep things generally grid-like (ie prioritize non diagonals)
// TODO: Try moving halfway each time?
moved := make(map[string]bool)
movedAmount := make(map[string]tile.TilePosition) // track the movement amount of each node so they can compound
done := true
for _, parentNode := range l.nodeList {
parent, ok := l.place[parentNode]
if !ok {
panic("AAA")
}
// parentMove, ok := movedAmount[parentNode]
// if !ok { panic("AAA") }
edges, ok := l.dag.Edges[parentNode]
if !ok {
panic("AAA")
}
for _, child := range edges {
alreadyMoved := moved[child]
if alreadyMoved {
continue
}
moved[child] = true
p, ok := l.place[child]
if !ok {
panic("AAA")
}
origCenter := p.Rect.Center()
// p.Rect = l.MoveTowards(p.Rect, tile.TilePosition{}, 1)
// l.place[child] = p
// if HasRectIntersections(l.place, child) || AnyEdgesIntersect(l.dag, l.place) {
// p.Rect = p.Rect.WithCenter(origCenter)
// l.place[child] = p
// }
// Move x direction
{
origCenter = p.Rect.Center()
relCenter := origCenter.Sub(parent.Rect.Center()) // Relative to parent
relCenter.Y = 0
p.Rect = l.MoveTowards(p.Rect, relCenter, 1)
// p.Rect = p.Rect.Moved(parentMove)
l.place[child] = p
// if HasRectIntersections(l.place, child) || AnyEdgesIntersect(l.dag, l.place) {
if HasRectIntersections(l.place, child) || !l.AllEdgesAxisAligned(l.tolerance) {
p.Rect = p.Rect.WithCenter(origCenter)
l.place[child] = p
movedAmount[child] = tile.TilePosition{}
} else {
done = false
}
movedAmount[child] = p.Rect.Center().Sub(origCenter)
}
// Move y direction
{
origCenter = p.Rect.Center()
relCenter := origCenter.Sub(parent.Rect.Center()) // Relative to parent
relCenter.X = 0
p.Rect = l.MoveTowards(p.Rect, relCenter, 1)
// p.Rect = p.Rect.Moved(parentMove)
l.place[child] = p
// if HasRectIntersections(l.place, child) || AnyEdgesIntersect(l.dag, l.place) {
if HasRectIntersections(l.place, child) || !l.AllEdgesAxisAligned(l.tolerance) {
p.Rect = p.Rect.WithCenter(origCenter)
l.place[child] = p
movedAmount[child] = tile.TilePosition{}
} else {
done = false
}
movedAmount[child] = p.Rect.Center().Sub(origCenter)
}
}
}
return done
}
func (l *GridLayout) Iterate() bool {
// Idea: Pick a random node and optimize all of its edges,
// Goal: to minimize edge lengths and keep things generally grid-like (ie prioritize non diagonals)
// TODO: maybe pick longest edge?
// rngIndex := l.rng.Intn(len(l.nodeList))
parentNode := l.nodeList[l.topoIdx]
parent, ok := l.place[parentNode]
if !ok {
panic("AAA")
}
edges, ok := l.dag.Edges[parentNode]
if !ok {
panic("AAA")
}
for _, child := range edges {
alreadyMoved := l.topoMoved[child]
if alreadyMoved {
continue
}
p, ok := l.place[child]
if !ok {
panic("AAA")
}
for {
l.topoMoved[child] = true
origCenter := p.Rect.Center()
relCenter := origCenter.Sub(parent.Rect.Center()) // Relative to parent
// fmt.Println(p.Rect, origCenter, relCenter)
p.Rect = l.MoveTowards(p.Rect, relCenter, 1)
// fmt.Println(p.Rect)
l.place[child] = p
if HasRectIntersections(l.place, child) || AnyEdgesIntersect(l.dag, l.place) {
// fmt.Println("INTERSECT")
p.Rect = p.Rect.WithCenter(origCenter)
l.place[child] = p
break
}
}
}
l.topoIdx = (l.topoIdx + 1) % len(l.nodeList)
return false
}
func (l *GridLayout) MoveTowards(rect tile.Rect, pos tile.TilePosition, dist int) tile.Rect {
move := tile.TilePosition{}
magX := pos.X
if magX < 0 {
magX = -magX
}
magY := pos.Y
if magY < 0 {
magY = -magY
}
if magX > magY {
if pos.X > 0 {
move.X = -1
} else if pos.X < 0 {
move.X = +1
}
} else {
if pos.Y > 0 {
move.Y = -1
} else if pos.Y < 0 {
move.Y = +1
}
}
return rect.Moved(move)
}
// type HeirarchicalLayout struct {
// rng *rand.Rand
// queue *ds.Queue[string]
// dag *RoomDag
// place map[string]RoomPlacement
// depthGroup [][]string
// }
// func NewHeirarchicalLayout(rng *rand.Rand, dag *RoomDag, place map[string]RoomPlacement, start string) *HeirarchicalLayout {
// queue := ds.NewQueue[string](len(place)) // TODO: dynamically resized
// queue.Add(start)
// return &HeirarchicalLayout{
// rng: rng,
// queue: queue,
// dag: dag,
// place: place,
// }
// }
// func (l *HeirarchicalLayout) GroupLayers(start string) {
// visit := make(map[string]bool)
// queue := ds.NewQueue[string](len(place)) // TODO: dynamically resized
// queue.Add(start)
// for {
// currentNode, ok := queue.Remove()
// if !ok { break }
// currentPlacement, ok := d.place[currentNode]
// if !ok { panic("MUST BE SET") }
// currentDepth := currentPlacement.Depth
// children := d.dag.Edges[currentNode]
// for _, child := range children {
// visited := visit[child]
// if visited { continue }
// visit[child] = true
// // Set the depth of the child
// room := d.place[child]
// room.Depth = currentDepth + 1
// d.place[child] = room
// // Add the child to the relevant depthGroup
// if room.Depth >= len(d.depthGroup) {
// d.depthGroup = append(d.depthGroup, make([]string, 0))
// }
// group := d.depthGroup[room.Depth]
// group = append(group, child)
// d.depthGroup[room.Depth] = group
// queue.Add(child)
// }
// }
// }
// func (l *HeirarchicalLayout) Iterate() bool {
// for _, label := range l.depthGroup {
// }
// }
type DepthFirstLayout struct {
rng *rand.Rand
stack *ds.Stack[string]
dag *RoomDag
place map[string]RoomPlacement
dist float64
}
func NewDepthFirstLayout(rng *rand.Rand, dag *RoomDag, place map[string]RoomPlacement, start string, distance float64) *DepthFirstLayout {
stack := ds.NewStack[string]()
stack.Add(start)
return &DepthFirstLayout{
rng: rng,
stack: stack,
dag: dag,
place: place,
dist: distance,
}
}
func (d *DepthFirstLayout) Reset() {
for k, p := range d.place {
if !p.Static {
p.Placed = false
d.place[k] = p
}
}
}
func (d *DepthFirstLayout) CutCrossed() bool {
cutList := make([]string, 0)
for k, _ := range d.place {
if NodeHasEdgeIntersections(d.dag, d.place, k) {
// if NodeHasEdgeIntersectionsWithMoreShallowEdge(d.dag, d.place, k) {
cutList = append(cutList, k)
}
}
if len(cutList) <= 0 {
return false
}
for _, k := range cutList {
d.CutBelow(k)
// d.Cut(k)
d.stack.Add(k)
}
// // TODO: Add them all. a bit hacky
// for k := range d.place {
// d.stack.Add(k)
// }
return true
}
func (d *DepthFirstLayout) Cut(label string) {
p, ok := d.place[label]
if !ok {
return
}
// fmt.Println("Cut: ", label)
p.Placed = false
d.place[label] = p
}
func (d *DepthFirstLayout) CutBelow(label string) {
// fmt.Println("CutBelow: ", label)
visit := make(map[string]bool)
stack := ds.NewStack[string]()
stack.Add(label)
for {
curr, ok := stack.Remove()
if !ok {
break
}
visited := visit[curr]
if visited {
continue
}
visit[curr] = true
children := d.dag.Edges[curr]
for _, child := range children {
// fmt.Println("Cut: ", curr)
p, ok := d.place[child]
if !ok {
continue
}
p.Placed = false
d.place[child] = p
stack.Add(child)
}
}
}
// Returns true to indicate it hasn't finished
func (d *DepthFirstLayout) Iterate() bool {
currentNode, ok := d.stack.Remove()
if !ok {
return false
}
children := d.dag.Edges[currentNode]
for _, child := range children {
room := d.place[child]
if room.Placed {
continue
}
currentPlacement, ok := d.place[currentNode]
if !ok {
panic("MUST BE SET")
}
currentRect := currentPlacement.Rect
currentDepth := currentPlacement.Depth
for i := 0; i < 50; i++ {
// // Average Position
// squareRadius := int(d.dist)
// pos := FindNodeNeighborAveragePosition(d.dag, d.place, child)
// moveX := pos.X + d.rng.Intn(2 * squareRadius) - squareRadius
// moveY := pos.Y + d.rng.Intn(2 * squareRadius) - squareRadius
// room.Rect = room.Rect.WithCenter(tile.TilePosition{moveX, moveY})
// room.Depth = currentDepth + 1
// Random square placement
squareRadius := int(d.dist)
moveX := currentRect.Center().X + d.rng.Intn(2*squareRadius) - squareRadius
moveY := currentRect.Center().Y + d.rng.Intn(2*squareRadius) - squareRadius
room.Rect = room.Rect.WithCenter(tile.TilePosition{moveX, moveY})
room.Depth = currentDepth + 1
room.Placed = true
d.place[child] = room
repel := 90000.0
grav := 0.0
Wiggle(d.dag, d.place, repel, grav, 10)
if !NodeHasEdgeIntersections(d.dag, d.place, child) {
break
}
}
d.stack.Add(child)
}
return true
}
type BreadthFirstLayout struct {
rng *rand.Rand
queue *ds.Queue[string]
dag *RoomDag
place map[string]RoomPlacement
dist float64
}
func NewBreadthFirstLayout(rng *rand.Rand, dag *RoomDag, place map[string]RoomPlacement, start string, distance float64) *BreadthFirstLayout {
queue := ds.NewQueue[string](len(place)) // TODO: dynamically resized
queue.Add(start)
return &BreadthFirstLayout{
rng: rng,
queue: queue,
dag: dag,
place: place,
dist: distance,
}
}
func (d *BreadthFirstLayout) Reset() {
for k, p := range d.place {
if !p.Static {
p.Placed = false
d.place[k] = p
}
}
}
// Returns true to indicate it hasn't finished
func (d *BreadthFirstLayout) Iterate() bool {
currentNode, ok := d.queue.Remove()
if !ok {
return false
}
children := d.dag.Edges[currentNode]
for _, child := range children {
room := d.place[child]
if room.Placed {
continue
}
currentPlacement, ok := d.place[currentNode]
if !ok {
panic("MUST BE SET")
}
currentRect := currentPlacement.Rect
currentDepth := currentPlacement.Depth
for i := 0; i < 50; i++ {
// Random square placement
squareRadius := int(d.dist)
moveX := currentRect.Center().X + d.rng.Intn(2*squareRadius) - squareRadius
moveY := currentRect.Center().Y + d.rng.Intn(2*squareRadius) - squareRadius
room.Rect = room.Rect.WithCenter(tile.TilePosition{moveX, moveY})
room.Depth = currentDepth + 1
room.Placed = true
repel := 50000.0
grav := 0.0
Wiggle(d.dag, d.place, repel, grav, 20)
d.place[child] = room
if !NodeHasEdgeIntersections(d.dag, d.place, child) {
break
}
}
d.queue.Add(child)
}
return true
}
func PlaceDepthFirst(rng *rand.Rand, dag *RoomDag, rooms map[string]RoomPlacement, start string, distance float64) {
stack := ds.NewStack[string]()
stack.Add(start)
fan := 0
fanX := []int{0, 1, 1, 1}
fanY := []int{1, 1, 1, -1}
placed := make(map[string]bool)
for {
currentNode, ok := stack.Remove()
if !ok {
break
}
children := dag.Edges[currentNode]
for _, child := range children {
_, alreadyPlaced := placed[child]
if alreadyPlaced {
continue
}
currentPlacement, ok := rooms[currentNode]
if !ok {
panic("MUST BE SET")
}
currentRect := currentPlacement.Rect
currentDepth := currentPlacement.Depth
squareRadius := int(distance)
// squareRadius := int(distance / float64(2 * (currentDepth + 1)))
// moveX := currentRect.Center().X + rng.Intn(2 * squareRadius) - squareRadius
// moveY := currentRect.Center().Y + rng.Intn(2 * squareRadius) - squareRadius
fan = (fan + 1) % len(fanX)
mX := fanX[fan]
mY := fanY[fan]
moveX := (mX * squareRadius) + currentRect.Center().X
moveY := (mY * squareRadius) + currentRect.Center().Y
room := rooms[child]
room.Rect = room.Rect.WithCenter(tile.TilePosition{moveX, moveY})
room.Depth = currentDepth + 1
// TODO: Recalc gap goal?
rooms[child] = room
// // Random square placement
// // TODO: hardcoded. Could I favor non-diagonals here?
// squareRadius := int(distance / float64(2 * (currentDepth + 1)))
// // moveX := currentRect.Center().X + rng.Intn(2 * squareRadius) - squareRadius
// // moveY := currentRect.Center().Y + rng.Intn(2 * squareRadius) - squareRadius
// mX := rng.Intn(3) - 1
// mY := rng.Intn(3) - 1
// moveX := (mX * squareRadius) + currentRect.Center().X
// moveY := (mY * squareRadius) + currentRect.Center().Y
// room := rooms[child]
// room.Rect = room.Rect.WithCenter(tile.TilePosition{moveX, moveY})
// room.Depth = currentDepth + 1
// // TODO: Recalc gap goal?
// rooms[child] = room
// // Random circle placement
// rngAngle := rng.Float64() * math.Pi / 4
// posAngle := currentRect.Center().Angle()
// moveX := int(distance * math.Cos(rngAngle)) + currentRect.Center().X
// moveY := int(distance * math.Sin(rngAngle)) + currentRect.Center().Y
// room := rooms[child]
// room.Rect = room.Rect.WithCenter(tile.TilePosition{moveX, moveY})
// room.Depth = currentDepth + 1
// fmt.Println(child, tile.TilePosition{moveX, moveY}, room.Rect, room.Depth)
// // TODO: Recalc gap goal?
// rooms[child] = room
repel := 1000.0
grav := 0.0
Wiggle(dag, rooms, repel, grav, 100)
placed[child] = true
stack.Add(child)
}
}
}
func PlaceBreadthFirst(rng *rand.Rand, dag *RoomDag, rooms map[string]RoomPlacement, start string, distance float64) {
childQueue := ds.NewQueue[string](len(rooms)) // TODO: probably doesnt need to be this big, would be nice if we could have a dynamically resizable queue
childQueue.Add(start)
placed := make(map[string]bool)
for {
currentNode, ok := childQueue.Remove()
if !ok {
break
}
// currentPlacement, ok := rooms[currentNode]
// if !ok { panic("MUST BE SET") }
// currentRect := currentPlacement.Rect
// currentDepth := currentPlacement.Depth
children := dag.Edges[currentNode]
for _, child := range children {
_, alreadyPlaced := placed[child]
if alreadyPlaced {
continue
}
currentPlacement, ok := rooms[currentNode]
if !ok {
panic("MUST BE SET")
}
currentRect := currentPlacement.Rect
currentDepth := currentPlacement.Depth
// Random square placement
squareRadius := int(distance) // TODO: hardcoded. Could I favor non-diagonals here?
// moveX := currentRect.Center().X + rng.Intn(2 * squareRadius) - squareRadius
// moveY := currentRect.Center().Y + rng.Intn(2 * squareRadius) - squareRadius
mX := rng.Intn(3) - 1
mY := rng.Intn(3) - 1
moveX := (mX * squareRadius) + currentRect.Center().X
moveY := (mY * squareRadius) + currentRect.Center().Y
room := rooms[child]
room.Rect = room.Rect.WithCenter(tile.TilePosition{moveX, moveY})
room.Depth = currentDepth + 1
// TODO: Recalc gap goal?
rooms[child] = room
// // Random circle placement
// rngAngle := rng.Float64() * math.Pi / 4
// posAngle := currentRect.Center().Angle()
// moveX := int(distance * math.Cos(rngAngle)) + currentRect.Center().X
// moveY := int(distance * math.Sin(rngAngle)) + currentRect.Center().Y
// room := rooms[child]
// room.Rect = room.Rect.WithCenter(tile.TilePosition{moveX, moveY})
// room.Depth = currentDepth + 1
// fmt.Println(child, tile.TilePosition{moveX, moveY}, room.Rect, room.Depth)
// // TODO: Recalc gap goal?
// rooms[child] = room
placed[child] = true
childQueue.Add(child)
// for i := 0; i < 1; i++ {
// PSLDStep(rng, dag, rooms)
// }
// repel := 1000.0
// grav := 0.0
// Wiggle(dag, rooms, repel, grav, 100)
}
// // All children are added, wiggle everything
// repel := 100.0
// grav := 1.0
// Wiggle(dag, rooms, repel, grav, 10)
// bailoutMax--
// if bailoutMax <= 0 { break }
}
}
func Untangle(dag *RoomDag, rooms map[string]RoomPlacement, repelMultiplier, gravityConstant float64, iterations int) {
for i := 0; i < iterations; i++ {
for node, edges := range dag.Edges {
for _, e := range edges {
if HasEdgeIntersections(dag, rooms, node, e) {
r := rooms[node]
r.Attract = 1.2
r.Repel += 1
rooms[node] = r
// noIntersections = false
} else {
r := rooms[node]
// r.Attract = 1.0
// r.Repel = float64(r.Rect.W() * r.Rect.H())
rooms[node] = r
}
}
}
stable := Wiggle(dag, rooms, repelMultiplier, gravityConstant, 1)
if stable {
break
}
}
}
func Wiggle(dag *RoomDag, rooms map[string]RoomPlacement, repelMultiplier, gravityConstant float64, iterations int) bool {
// TODO: everything inside of here probably needs to be fixed point, or integer based
forces := make(map[string]glm.Vec2)
// repelMultiplier = 100.0
// repelConstant := 50000.0
// gravityConstant := 0.0001
// gravityConstant := 0.0
attractConstant := 0.2
mass := 4.0
stable := true
// TODO: You gotta make this loop deterministic
// https://editor.p5js.org/JeromePaddick/sketches/bjA_UOPip
for i := 0; i < iterations; i++ {
// Calculate Forces
for key, placement := range rooms {
if placement.Static {
continue
} // Dont add forces to static placements
if !placement.Placed {
continue
} // Skip if not yet placed
force := glm.Vec2{}
for key2, placement2 := range rooms {
if key == key2 {
continue
} // Skip if same
if !placement2.Placed {
continue
} // Skip if not yet placed
deltaTile := placement2.Rect.Center().Sub(placement.Rect.Center())
delta := glm.Vec2{float64(deltaTile.X), float64(deltaTile.Y)}
// dist := float64(tile.ManhattanDistance(placement.Rect.Center(), placement2.Rect.Center()))
dist := delta.Len()
// Connections
if dag.HasEdgeEitherDirection(key2, key) {
attractVal := attractConstant * placement.Attract * placement2.Attract
mag := attractVal * (dist - (placement.GoalGap + placement2.GoalGap))
if mag > 100 {
mag = 100
}
vec := delta.Norm().Scaled(mag)
vec.X *= 4
force = force.Add(vec)
// fmt.Println("Conn: ", vec)
} else {
if dist < 100 {
dist = 100
}
// Repulsions
// mag := -1 * repelConstant / (dist * dist)
repelVal := repelMultiplier * placement.Repel * placement2.Repel
mag := -1 * repelVal / (dist * dist * dist * dist)
vec := delta.Norm().Scaled(mag)
force = force.Add(vec)
// fmt.Println("Repel: ", vec)
}
}
// Add a gravity
pos := glm.Vec2{
float64(placement.Rect.Center().X),
float64(placement.Rect.Center().Y),
}
// force = force.Add(pos.Scaled(-gravityConstant / (10 * float64(placement.Depth + 1))))
force = force.Add(pos.Scaled(-gravityConstant))
// fmt.Println("Grav: ", pos.Scaled(-gravityConstant))
// // Add very weak inverse gravity
// {
// ceil := 1000.0
// mag := pos.Len()
// if mag > ceil { mag = ceil }
// force = force.Add(pos.Norm().Scaled(mag))
// }
// // Add a upward Y force
// {
// vec := glm.Vec{
// 0.0,
// -100.0,
// }
// force = force.Add(vec)
// }
forces[key] = force
}
// Apply Forces
for k, f := range forces {
placement, ok := rooms[k]
if !ok {
panic("AAAA")
}
tileMove := tile.TilePosition{
int(math.Round(f.X / mass)),
int(math.Round(f.Y / mass)),
}
if (tileMove == tile.TilePosition{}) {
continue
} // Skip if there is no movement
stable = false
placement.Rect = placement.Rect.Moved(tileMove)
rooms[k] = placement
}
}
return stable
}
// func (g roomAndHallwayDungeon) pickRoom(rooms []string) *MapDefinition {
// // Random block
// idx := g.rng.Intn(len(rooms))
// name := rooms[idx]
// mapDef, err := LoadMap(name)
// if err != nil {
// panic(err)
// }
// mapDef.RecalculateBounds() // TODO: Move this to editor so we dont have to do it during dungeon generation time
// return mapDef
// }
// func (g roomAndHallwayDungeon) findNonCollidingPosition(rects map[string]RoomPlacement, last tile.Rect, newRect tile.Rect) tile.Rect {
// expansion := 12
// combinedWidth := (last.W() + newRect.W()) / 2
// combinedHeight := (last.H() + newRect.H()) / 2
// for {
// newPos := last.Center()
// dir := g.rng.Intn(4)
// posneg := g.rng.Intn(2)
// if dir == 0 {
// newPos.X += combinedWidth + expansion
// if posneg == 0 {
// newPos.Y += combinedHeight / 2
// } else {
// newPos.Y -= combinedHeight / 2
// }
// } else if dir == 1 {
// newPos.X -= (combinedWidth + expansion)
// if posneg == 0 {
// newPos.Y += combinedHeight / 2
// } else {
// newPos.Y -= combinedHeight / 2
// }
// } else if dir == 2 {
// newPos.Y += combinedHeight + expansion
// if posneg == 0 {
// newPos.X += combinedWidth / 2
// } else {
// newPos.X -= combinedWidth / 2
// }
// } else if dir == 3 {
// newPos.Y -= (combinedHeight + expansion)
// if posneg == 0 {
// newPos.X += combinedWidth / 2
// } else {
// newPos.X -= combinedWidth / 2
// }
// }
// rect := newRect.WithCenter(newPos)
// if !hasRectIntersections(rects, rect) {
// return rect
// }
// }
// }
func NodeHasEdgeIntersectionsWithMoreShallowEdge(dag *RoomDag, placements map[string]RoomPlacement, label string) bool {
edges, ok := dag.Edges[label]
if !ok {
panic("AAA")
}
p1, ok := placements[label]
if !ok {
return false
}
for _, e := range edges {
p2, ok := placements[e]
if !ok {
continue
}
if p1.Depth < p2.Depth {
continue
} // skip if we are more shallow
if HasEdgeIntersections(dag, placements, label, e) {
return true
}
}
return false
}
func AnyEdgesIntersect(dag *RoomDag, rects map[string]RoomPlacement) bool {
for node, edges := range dag.Edges {
for _, e := range edges {
if HasEdgeIntersections(dag, rects, node, e) {
return true
}
}
}
return false
}
func NodeHasEdgeIntersections(dag *RoomDag, rects map[string]RoomPlacement, label string) bool {
edges, ok := dag.Edges[label]
if !ok {
panic("AAA")
}
for _, e := range edges {
if HasEdgeIntersections(dag, rects, label, e) {
return true
}
}
return false
}
// def ccw(A,B,C):
// return (C.y-A.y) * (B.x-A.x) > (B.y-A.y) * (C.x-A.x)
// # Return true if line segments AB and CD intersect
// def intersect(A,B,C,D):
//
// return ccw(A,C,D) != ccw(B,C,D) and ccw(A,B,C) != ccw(A,B,D)
//
// https://stackoverflow.com/questions/3838329/how-can-i-check-if-two-segments-intersect
func ccw(a, b, c tile.TilePosition) bool {
return (c.Y-a.Y)*(b.X-a.X) >= (b.Y-a.Y)*(c.X-a.X)
}
// Returns true if line ab intersects line cd
func intersect(a, b, c, d tile.TilePosition) bool {
return (ccw(a, c, d) != ccw(b, c, d)) && (ccw(a, b, c) != ccw(a, b, d))
}
func HasEdgeIntersections(dag *RoomDag, rects map[string]RoomPlacement, label1, label2 string) bool {
// Line ab is going to go from label1 to label2
rA, ok := rects[label1]
if !ok {
return false
}
rB, ok := rects[label2]
if !ok {
return false
}
if !rA.Placed {
return false
} // Skip if not placed
if !rB.Placed {
return false
} // Skip if not placed
a := rA.Rect.Center()
b := rB.Rect.Center()
// We will skip all edges that are connect to labels that are the same as ours
for k, edges := range dag.Edges {
if k == label1 {
continue
}
if k == label2 {
continue
}
for _, e := range edges {
if e == label1 {
continue
}
if e == label2 {
continue
}
rC, ok := rects[k]
if !ok {
continue
}
rD, ok := rects[e]
if !ok {
continue
}
if !rC.Placed {
continue
} // Skip if not placed
if !rD.Placed {
continue
} // Skip if not placed
c := rC.Rect.Center()
d := rD.Rect.Center()
if intersect(a, b, c, d) {
return true
}
}
}
return false
}
func HasAnyRectIntersections(rects map[string]RoomPlacement) bool {
for label := range rects {
if HasRectIntersections(rects, label) {
return true
}
}
return false
}
func HasRectIntersections(rects map[string]RoomPlacement, label string) bool {
src, ok := rects[label]
if !ok {
return false
}
for key, r := range rects {
if key == label {
continue
} // skip self
if !r.Placed {
continue
} // skip if not placed
if r.Rect.Intersects(src.Rect) {
return true
}
}
return false
}
func FindNodeNeighborAveragePosition(dag *RoomDag, place map[string]RoomPlacement, label string) tile.TilePosition {
total := tile.TilePosition{}
count := 0
for key, p := range place {
if key == label {
continue
} // Skip self
if !p.Placed {
continue
} // skip unplaced rooms
if dag.HasEdgeEitherDirection(key, label) {
total = total.Add(p.Rect.Center())
count++
}
}
if count == 0 {
return total
}
return total.Div(count)
}
package pgen
import (
"math"
"github.com/ojrac/opensimplex-go"
)
type Octave struct {
Freq, Scale float64
}
type NoiseMap struct {
seed int64
noise opensimplex.Noise
octaves []Octave
exponent float64
}
func NewNoiseMap(seed int64, octaves []Octave, exponent float64) *NoiseMap {
// TODO - ensure that sum of all octave amplitudes equals 1!
return &NoiseMap{
seed: seed,
noise: opensimplex.NewNormalized(seed),
octaves: octaves,
exponent: exponent,
}
}
func (n *NoiseMap) Get(x, y int) float64 {
ret := 0.0
for i := range n.octaves {
xNoise := n.octaves[i].Freq * float64(x)
yNoise := n.octaves[i].Freq * float64(y)
ret += n.octaves[i].Scale * n.noise.Eval2(xNoise, yNoise)
}
// Exit early if the exponent is just 1.0
if n.exponent == 1.0 {
return ret
}
ret = math.Pow(ret, n.exponent)
return ret
}
package pgen
import (
"github.com/ungerik/go3d/float64/vec2"
"math/rand"
"sort"
)
func Path(start, end vec2.T, n int, variation float64) []vec2.T {
path := make([]vec2.T, n)
path[0] = start
path[len(path)-1] = end
nVec := vec2.Sub(&end, &start)
latVec := nVec.Normalize().Rotate90DegLeft()
for i := 1; i < n-2; i++ {
interpVec := vec2.Interpolate(&start, &end, rand.Float64())
rnd := 2 * (rand.Float64() - 0.5) * variation
lateral := latVec.Scaled(rnd)
path[i] = vec2.Add(&interpVec, &lateral)
}
sort.Slice(path, func(i, j int) bool {
ii := vec2.Sub(&start, &path[i])
jj := vec2.Sub(&start, &path[j])
return ii.LengthSqr() < jj.LengthSqr()
})
return path
}
package pgen
import (
"math/rand"
"slices"
"github.com/unitoftime/flow/glm"
"golang.org/x/exp/constraints"
)
// Returns a random float with provided radius, centered at 0
func CenteredFloat64(radius float64) float64 {
if radius == 0 {
return 0
}
rngVal := 2 * (rand.Float64() - 0.5)
return rngVal * radius
}
type castable interface {
constraints.Integer | constraints.Float
}
type Range[T castable] struct {
Min, Max T // Min inclusive, Max exclusive
}
func (r Range[T]) Get() T {
width := float64(r.Max) - float64(r.Min)
return T((rand.Float64() * width) + float64(r.Min))
}
func (r Range[T]) SeededGet(rng *rand.Rand) T {
width := float64(r.Max) - float64(r.Min)
return T((rng.Float64() * width) + float64(r.Min))
}
func (r Range[T]) RollMultipleUnique(rng *rand.Rand, n int) []T {
if n == 0 { return []T{} }
list := make([]T, 0, n)
for range 50 {
item := r.SeededGet(rng)
if slices.Contains(list, item) {
continue // Skip: We already have this modifier
}
list = append(list, item)
if len(list) >= n {
break // Exit, we have finished the list
}
}
return list
}
// Pick a random item out of a list
func GetList[T any](list []T) (T, bool) {
if len(list) <= 0 {
var t T
return t, false
}
return list[rand.Intn(len(list))], true
}
// Returns a random element of the list, based on the provided rng
func SeededList[T any](rng *rand.Rand, list []T) T {
return list[rng.Intn(len(list))]
}
// Rolls the provided chance out of 100
func Percent(chance int) bool {
if chance <= 0 {
return false
}
return rand.Intn(100) < chance
}
// func ListItem[T any](rng *rand.Rand, list []T) (T, bool) {
// if len(list) <= 0 {
// var t T
// return t, false
// }
// return list[rng.Intn(len(list))], true
// }
// TODO: Should I separate int from float?
// type RngRange[T constraints.Integer]struct{
// Min, Max T
// }
// func NewRngRange[T constraints.Integer](min, max T) RngRange[T] {
// return RngRange[T]{min, max}
// }
// func (r RngRange[T]) Roll() T {
// delta := r.Max - r.Min
// if delta <= 0 {
// return r.Min
// }
// return T(rand.Intn(int(delta))) + r.Min
// }
func RandomPositionInRect(r glm.Rect) glm.Vec2 {
randX := Range[float64]{r.Min.X, r.Max.X}.Get()
randY := Range[float64]{r.Min.Y, r.Max.Y}.Get()
return glm.Vec2{randX, randY}
}
//--------------------------------------------------------------------------------
// - Tables
//--------------------------------------------------------------------------------
type Item[T any] struct {
Weight int
Item T
}
func NewItem[T any](weight int, item T) Item[T] {
return Item[T]{
Weight: weight,
Item: item,
}
}
type Table[T any] struct {
Total int
Items []Item[T]
}
func NewTable[T any](items ...Item[T]) *Table[T] {
// TODO - Seeding?
t := &Table[T]{
Items: items, // TODO - maybe sort this. it might make the search a little faster?
}
t.regenerate()
return t
}
func NewUniformTable[T any](items ...T) *Table[T] {
weightedItems := make([]Item[T], len(items))
for i := range items {
weightedItems[i] = NewItem(1, items[i])
}
return NewTable(weightedItems...)
}
func (t *Table[T]) regenerate() {
total := 0
for i := range t.Items {
if t.Items[i].Weight <= 0 {
continue
} // Skip if the weight of this item is <= 0
total += t.Items[i].Weight
}
t.Total = total
}
func (t *Table[T]) getIndex() int {
if t.Total == 0 {
t.regenerate()
}
roll := rand.Intn(t.Total)
// Essentially we just loop forward incrementing the `current` value. and once we pass it, we know that we are in that current section of the distribution.
current := 0
for i := range t.Items {
current += t.Items[i].Weight
if roll < current {
return i
}
}
// Else just return the first item, something went wrong with the search
// TODO: is this okay? Or should I return a bool and handle it further up?
return 0
}
// Returns the item rolled
func (t *Table[T]) Get() T {
index := t.getIndex()
// TODO: is there a way to write this so it never fails?
return t.Items[index].Item
}
// TODO: Needs testing
// // Returns the item rolled and removes it from the table
// func (t *Table[T]) GetAndRemove() (T, bool) {
// var ret T
// if len(t.Items) <= 0 {
// return ret, false
// }
// if t.Total == 0 {
// t.regenerate()
// }
// roll := rand.Intn(t.Total)
// // Essentially we just loop forward incrementing the `current` value. and once we pass it, we know that we are in that current section of the distribution.
// current := 0
// idx := -1
// for i := range t.Items {
// current += t.Items[i].Weight
// if roll < current {
// idx = i
// }
// }
// if idx < 0 {
// // If we couldn't find the index for some reason, then it fails
// return ret, false
// }
// // Get Item
// ret = t.Items[idx].Item
// // Remove Item and regenerate
// t.Items[idx] = t.Items[len(t.Items)-1]
// t.Items = t.Items[:len(t.Items)-1]
// t.regenerate()
// return ret, true
// }
// Returns returns count unique items, if there are less items in the loot table, only returns what is available to satisfy the uniqueness
func (t *Table[T]) GetUnique(count int) []T {
if count <= 0 {
return []T{}
}
ret := make([]T, 0, count)
// If there are less items than we are requesting, then just return them all
if count >= len(t.Items) {
for i := range t.Items {
ret = append(ret, t.Items[i].Item)
}
return ret
}
indexes := make([]int, 0, count)
for i := 0; i < count; i++ {
idx := t.getIndex()
if slices.Contains(indexes, idx) {
// If we have already found this index, try again
i-- // Note: You are guaranteed that count < len(t.Items) so this will exit
continue
}
indexes = append(indexes, idx)
}
for _, idx := range indexes {
ret = append(ret, t.Items[idx].Item)
}
return ret
}
package phy2
import (
"math"
"github.com/unitoftime/ecs"
"github.com/unitoftime/flow/glm"
)
type CollisionLayer uint8
func (c CollisionLayer) Mask(layer CollisionLayer) bool {
return (c & layer) > 0 // One layer must overlap for the layermask to work
}
// This tracks the list of current collisions
type ColliderCache struct {
Current []ecs.Id
Last []ecs.Id
NewCollisions []ecs.Id // This list contains all new collisions
}
func NewColliderCache() ColliderCache {
return ColliderCache{
Current: make([]ecs.Id, 0),
Last: make([]ecs.Id, 0),
NewCollisions: make([]ecs.Id, 0),
}
}
func (c *ColliderCache) Add(id ecs.Id) {
c.Current = append(c.Current, id)
for i := range c.Last {
if c.Last[i] == id {
return
} // Exit early, because this one was in the last frame
}
// Else if we get here, then the id wasn't in the last frame list
c.NewCollisions = append(c.NewCollisions, id)
}
func (c *ColliderCache) Clear() {
last := c.Last
current := c.Current
c.Last = current
c.Current = last[:0]
c.NewCollisions = c.NewCollisions[:0]
}
var colliderCacheComp = ecs.Comp(ColliderCache{})
func (c ColliderCache) CompId() ecs.CompId {
return colliderCacheComp.CompId()
}
func (c ColliderCache) CompWrite(w ecs.W) {
colliderCacheComp.WriteVal(w, c)
}
type CircleCollider struct {
CenterX, CenterY float64 // TODO - right now this holds the entire position of the circle (relative to world space). You might consider stripping that out though
Radius float64
HitLayer CollisionLayer
Layer CollisionLayer
// Disabled bool // If set true, this collider won't collide with anything
}
func NewCircleCollider(radius float64) CircleCollider {
return CircleCollider{
Radius: radius,
}
}
func (c *CircleCollider) LayerMask(layer CollisionLayer) bool {
return (c.HitLayer & layer) > 0 // One layer must overlap for the layermask to work
}
// func (c *CircleCollider) Position() Pos {
// return Pos{c.CenterX, c.CenterY}
// }
// func (c *CircleCollider) SetPosition(pos Pos) {
// c.CenterX = pos.X
// c.CenterY = pos.Y
// }
func (c *CircleCollider) Bounds() glm.Rect {
return glm.Rect{
Min: glm.Vec2{c.CenterX - c.Radius, c.CenterY - c.Radius},
Max: glm.Vec2{c.CenterX + c.Radius, c.CenterY + c.Radius},
}
}
func (c *CircleCollider) Contains(yProjection float64, pos glm.Vec2) bool {
// dx := transform.X - c.CenterX
// dy := transform.Y - c.CenterY
// dist := math.Hypot(dx, yProjection * dy)
// return dist < c.Radius
dx := pos.X - c.CenterX
dy := pos.Y - c.CenterY
dist := math.Hypot(dx, yProjection*dy)
return dist < c.Radius
}
// TODO - Maybe pass position based delta into collider?
// func (c *CircleCollider) Collides(yProjection float64, c2 *CircleCollider) bool {
// return !c.Disabled && !c2.Disabled && c.Overlaps(yProjection, c2)
// }
func (c *CircleCollider) Overlaps(yProjection float64, c2 *CircleCollider) bool {
dx := c2.CenterX - c.CenterX
dy := c2.CenterY - c.CenterY
dist := math.Hypot(dx, yProjection*dy)
return dist < (c.Radius + c2.Radius)
}
type HashPosition struct {
X, Y int32
}
type SpatialBucket struct {
position HashPosition
ids []ecs.Id
}
func NewSpatialBucket(hashPos HashPosition) *SpatialBucket {
return &SpatialBucket{
position: hashPos,
ids: make([]ecs.Id, 0),
}
}
// This holds a spatial hash of objects placed inside
type SpatialHash struct {
bucketSize float64
buckets map[HashPosition]*SpatialBucket
}
// TODO - pass in world dimensions?
// TODO - 2d bucket sizes?
func NewSpatialHash(bucketSize float64) *SpatialHash {
return &SpatialHash{
bucketSize: bucketSize,
buckets: make(map[HashPosition]*SpatialBucket),
}
}
func (s *SpatialHash) AddCircle(id ecs.Id, circle *CircleCollider) {
hashPos := s.ToHashPosition(circle.CenterX, circle.CenterY)
bucket, ok := s.buckets[hashPos]
if !ok {
bucket = NewSpatialBucket(hashPos)
s.buckets[hashPos] = bucket
}
bucket.ids = append(bucket.ids, id)
}
func (s *SpatialHash) ToHashPosition(x, y float64) HashPosition {
bucketX := int32(math.Floor(x / s.bucketSize))
bucketY := int32(math.Floor(y / s.bucketSize))
return HashPosition{bucketX, bucketY}
}
package phy2
import (
"math"
"github.com/unitoftime/ecs"
"github.com/unitoftime/flow/glm"
)
// //cod:struct
// type Pos Vec2
// func (v Pos) Add(v2 Pos) Pos {
// return Pos(Vec2(v).Add(Vec2(v2)))
// }
// func (v Pos) Sub(v2 Pos) Pos {
// return Pos(Vec2(v).Sub(Vec2(v2)))
// }
// func (v Pos) Norm() Pos {
// return Pos(Vec2(v).Norm())
// }
// func (v Pos) Dot(u Pos) float64 {
// return Vec2(v).Dot(Vec2(u))
// }
// func (v Pos) Dist(u Pos) float64 {
// return Vec2(v).Dist(Vec2(u))
// }
// func (v Pos) Len() float64 {
// return Vec2(v).Len()
// }
// func (v Pos) Scaled(s float64) Pos {
// return Pos(Vec2(v).Scaled(s))
// }
// func (v Pos) Rotated(radians float64) Pos {
// return Pos(Vec2(v).Rotated(radians))
// }
// func (v Pos) Angle() float64 {
// return math.Atan2(v.Y, v.X)
// }
type Vel glm.Vec2
var velComp = ecs.Comp(Vel{})
func (c Vel) CompId() ecs.CompId {
return velComp.CompId()
}
func (c Vel) CompWrite(w ecs.W) {
velComp.WriteVal(w, c)
}
func (v Vel) Add(v2 Vel) Vel {
return Vel(glm.Vec2(v).Add(glm.Vec2(v2)))
}
func (v Vel) Sub(v2 Vel) Vel {
return Vel(glm.Vec2(v).Sub(glm.Vec2(v2)))
}
func (v Vel) Norm() Vel {
return Vel(glm.Vec2(v).Norm())
}
func (v Vel) Dot(u Vel) float64 {
return glm.Vec2(v).Dot(glm.Vec2(u))
}
func (v Vel) Len() float64 {
return glm.Vec2(v).Len()
}
func (v Vel) Scaled(s float64) Vel {
return Vel(glm.Vec2(v).Scaled(s))
}
func (v Vel) Rotated(radians float64) Vel {
return Vel(glm.Vec2(v).Rotated(radians))
}
func (v Vel) Angle() float64 {
return math.Atan2(v.Y, v.X)
}
// //cod:struct
// type Scale glm.Vec2
// //cod:struct
// type Rotation float64
// //cod:struct
// type Rigidbody struct {
// Mass float64
// Velocity glm.Vec2
// }
// // Applies rigidbody physics
// func RigidbodyPhysics(world *ecs.World, dt time.Duration) {
// ecs.Map2(world, func(id ecs.Id, transform *Transform, rigidbody *Rigidbody) {
// newTransform := Vec2{transform.X, transform.Y}
// delta := rigidbody.Velocity.Scaled(dt.Seconds())
// newTransform = newTransform.Add(delta)
// transform.X = newTransform.X
// transform.Y = newTransform.Y
// })
// }
package render
import (
"time"
"github.com/unitoftime/flow/glm"
"github.com/unitoftime/glitch"
)
var globalTimer time.Duration
func UpdateGlobalAnimationTimer(dt time.Duration) {
globalTimer += dt
}
// TODO - it might make more sense to make this like an aseprite wrapper object that has layers, frames, tags, etc
// This is an animation frame
type Frame struct {
Sprite *glitch.Sprite
// Origin phy2.Vec
Dur time.Duration
mount map[string]glm.Vec2 // TODO - this is just kind of arbitrary data for my mountpoint system
}
func NewFrame(sprite *glitch.Sprite, dur time.Duration) Frame {
return Frame{
Sprite: sprite,
Dur: dur,
mount: make(map[string]glm.Vec2),
}
}
func (f Frame) Bounds() glitch.Rect {
return f.Sprite.Bounds()
}
func (f *Frame) SetMount(name string, point glm.Vec2) {
f.mount[name] = point
}
func (f *Frame) Mount(name string) glm.Vec2 {
pos, ok := f.mount[name]
if !ok {
return glm.Vec2{}
}
return pos
}
//cod:component
type Animation struct {
frameIdx int
remainingDur time.Duration
frames map[string][]Frame // This is the map of all animations and their associated frames
animName string
curAnim []Frame // This is the current animation frames that we are operating on
totalAnimTime time.Duration // This is the total amount of time for the current animation (speed adjusted)
done bool
Loop bool
speed float64 // This is used to scale the duration of the animation evenly so that the animation can fit a certain time duration
// MirrorX bool // TODO
MirrorY bool // Mirror around the Y axis
AlignAnimation bool
hasUpdatedOnce bool
}
func NewAnimation(startingAnim string, frames map[string][]Frame) Animation {
anim := Animation{
frames: frames,
// Color: color.NRGBA{255, 255, 255, 255},
// Scale: glitch.Vec2{1, 1},
Loop: true,
speed: 1.0,
}
if startingAnim == "" {
// Just set some random animation if unset
anim.randomAnimation()
} else {
anim.SetAnimation(startingAnim)
}
return anim
}
// func (a *Animation) SetTranslation(pos glitch.Vec3) {
// a.translation = pos
// }
func (a *Animation) randomAnimation() {
for name := range a.frames {
a.SetAnimation(name)
break
}
}
func (a *Animation) calculateTotalAnimTime() {
totalAnimTime := 0 * time.Second
for _, frame := range a.curAnim {
totalAnimTime += frame.Dur
}
a.totalAnimTime = totalAnimTime
}
func (a *Animation) SetAnimationWithDuration(name string, dur time.Duration) {
a.SetAnimation(name)
a.speed = a.totalAnimTime.Seconds() / dur.Seconds()
a.totalAnimTime = dur
}
func (a *Animation) HasAnimation(name string) bool {
_, exists := a.frames[name]
return exists
}
func (a *Animation) GetAnimationName() string {
return a.animName
}
func (a *Animation) SetAnimation(name string) {
if name == a.animName {
return
} // Skip if we aren't actually changing the animation
newAnim, ok := a.frames[name]
if !ok {
if a.animName == "" {
a.randomAnimation()
}
return
}
a.animName = name
a.curAnim = newAnim
a.SetFrame(0)
a.speed = 1.0
a.hasUpdatedOnce = false
a.done = false
a.calculateTotalAnimTime()
}
func (a *Animation) NextFrame() {
a.SetFrame(a.frameIdx + 1)
}
// Returns true when the current animation is done, else returns false
// Always returns false if the animation loops
func (a *Animation) Done() bool {
if a.Loop {
return false
}
return a.done
}
func (a *Animation) SetFrame(idx int) {
if len(a.curAnim) <= 0 {
return
} // Cant set the frame if the animation is zero length
if a.Loop {
// If the idx is passed the animation, then loop it
a.frameIdx = idx % len(a.curAnim)
frame := a.curAnim[a.frameIdx]
a.remainingDur = frame.Dur
} else {
// If the idx is passed the animation, snap to the last frame
if idx >= len(a.curAnim) {
a.done = true
idx = len(a.curAnim) - 1
}
a.frameIdx = idx
frame := a.curAnim[a.frameIdx]
a.remainingDur = frame.Dur
}
}
func (a *Animation) GetFrame() Frame {
if len(a.curAnim) <= 0 {
return Frame{}
}
idx := a.frameIdx % len(a.curAnim)
return a.curAnim[idx]
}
// Steps the animation forward by dt amount of time
// Returns true if the animation frame has changed, else returns false
func (anim *Animation) Update(dt time.Duration) bool {
if anim.AlignAnimation {
remainder := (globalTimer % anim.totalAnimTime)
idx := 0
for {
frameTime := time.Duration(1_000_000_000 * anim.curAnim[idx].Dur.Seconds() / anim.speed)
if remainder < frameTime {
ret := (anim.frameIdx != idx) // If the frame changed, return true
anim.SetFrame(idx)
if !anim.hasUpdatedOnce {
anim.hasUpdatedOnce = true
return true
}
return ret
}
remainder -= frameTime
idx++
}
}
adjustedDt := time.Duration(1_000_000_000 * anim.speed * dt.Seconds())
anim.remainingDur -= adjustedDt
if anim.remainingDur < 0 {
// Change to a new animation frame
anim.NextFrame()
return true
}
if !anim.hasUpdatedOnce {
anim.hasUpdatedOnce = true
return true
}
return false
}
// // Draws the animation to the render pass
// func (anim *Animation) Draw(target glitch.BatchTarget, pos Pos) {
// frame := anim.curAnim[anim.frameIdx]
// // frame.sprite.SetTranslation(anim.translation)
// mat := glitch.Mat4Ident
// // mat.Translate(float32(frame.Origin.X), float32(frame.Origin.Y), 0)
// mat.Scale(anim.Scale[0], anim.Scale[1], 1.0)
// if anim.MirrorY {
// mat.Scale(-1.0, 1.0, 1.0)
// }
// mat.Rotate(anim.Rotation, glitch.Vec3{0, 0, 1})
// mat.Translate(pos.X, pos.Y, 0)
// // TODO - I think there's some mistakes here with premultiplied vs non premultiplied alpha
// col := glitch.RGBA{anim.Color.R/255.0, (anim.Color.G)/255.0, (anim.Color.B)/255.0, (anim.Color.A)/255.0}
// frame.Sprite.DrawColorMask(target, mat, col)
// }
package render
import (
"github.com/unitoftime/ecs"
"github.com/unitoftime/flow/glm"
"github.com/unitoftime/flow/spatial"
"github.com/unitoftime/glitch"
)
// Bevy Reference
// https://docs.rs/bevy_core_pipeline/0.15.1/src/bevy_core_pipeline/core_2d/camera_2d.rs.html#34
// TODO: Make a cod macro to generate stuff like this? ie use required components instead of manually setting up a bundle
type Camera2DBundle struct {
target RenderTarget
}
func (c Camera2DBundle) CompWrite(w ecs.W) {
cam := NewCamera2D(glm.CR(0), 0, 0)
target := Target{
draw: c.target,
batch: c.target,
}
visionList := NewVisionList()
cam.CompWrite(w)
target.CompWrite(w)
visionList.CompWrite(w)
}
//cod:component
type Camera struct {
Camera glitch.CameraOrtho
Position glm.Vec2
Zoom float64
bounds glm.Rect
}
func NewCamera2D(bounds glm.Rect, x, y float64) Camera {
orthoCam := glitch.NewCameraOrtho()
return Camera{
Camera: *orthoCam,
Position: glm.Vec2{x, y},
Zoom: 1.0,
bounds: bounds,
}
}
func NewCamera(bounds glm.Rect, x, y float64) *Camera {
cam := NewCamera2D(bounds, x, y)
return &cam
}
func (c *Camera) Update(bounds glm.Rect) {
// // Snap camera
// c.Position[0] = float32(math.Round(float64(c.Position[0])))
// c.Position[1] = float32(math.Round(float64(c.Position[1])))
c.bounds = bounds
// TODO - Note: This is just to center point (0, 0), this should be selected some other way
screenCenter := bounds.Center()
c.Camera.SetOrtho2D(bounds)
movePos := glm.Vec2{c.Position.X, c.Position.Y}.Sub(screenCenter)
c.Camera.SetView2D(movePos.X, movePos.Y, c.Zoom, c.Zoom)
}
func (c *Camera) Project(point glitch.Vec3) glitch.Vec3 {
return c.Camera.Project(point)
}
func (c *Camera) Unproject(point glitch.Vec3) glitch.Vec3 {
return c.Camera.Unproject(point)
}
func (c *Camera) Bounds() glm.Rect {
return c.bounds
}
func (c *Camera) WorldSpaceRect() glm.Rect {
box := c.bounds.ToBox()
min := c.Unproject(box.Min)
max := c.Unproject(box.Max)
return glm.R(min.X, min.Y, max.X, max.Y)
}
func (c *Camera) WorldSpaceShape() spatial.Shape {
cameraMatrix := c.Camera.GetInverseMat4()
return spatial.Rect(c.bounds, cameraMatrix)
}
// type Camera struct {
// win *pixelgl.Window
// Position pixel.Vec
// Zoom float64
// mat pixel.Matrix
// }
// func NewCamera(win *pixelgl.Window, x, y float64) *Camera {
// return &Camera{
// win: win,
// Position: pixel.V(x, y),
// Zoom: 1.0,
// mat: pixel.IM,
// }
// }
// func (c *Camera) Update() {
// screenCenter := c.win.Bounds().Center()
// movePos := pixel.V(-c.Position.X, -c.Position.Y).Add(screenCenter)
// c.mat = pixel.IM.Moved(movePos).Scaled(screenCenter, c.Zoom)
// }
// func (c *Camera) Mat() pixel.Matrix {
// return c.mat
// }
// Code generated by cod; DO NOT EDIT.
package render
import (
"github.com/unitoftime/ecs"
)
var AnimationComp = ecs.NewComp[Animation]()
func (c Animation) CompId() ecs.CompId {
return AnimationComp.CompId()
}
func (c Animation) CompWrite(w ecs.W) {
AnimationComp.WriteVal(w, c)
}
var CalculatedVisibilityComp = ecs.NewComp[CalculatedVisibility]()
func (c CalculatedVisibility) CompId() ecs.CompId {
return CalculatedVisibilityComp.CompId()
}
func (c CalculatedVisibility) CompWrite(w ecs.W) {
CalculatedVisibilityComp.WriteVal(w, c)
}
var CameraComp = ecs.NewComp[Camera]()
func (c Camera) CompId() ecs.CompId {
return CameraComp.CompId()
}
func (c Camera) CompWrite(w ecs.W) {
CameraComp.WriteVal(w, c)
}
var SpriteComp = ecs.NewComp[Sprite]()
func (c Sprite) CompId() ecs.CompId {
return SpriteComp.CompId()
}
func (c Sprite) CompWrite(w ecs.W) {
SpriteComp.WriteVal(w, c)
}
var TargetComp = ecs.NewComp[Target]()
func (c Target) CompId() ecs.CompId {
return TargetComp.CompId()
}
func (c Target) CompWrite(w ecs.W) {
TargetComp.WriteVal(w, c)
}
var TransformComp = ecs.NewComp[Transform]()
func (c Transform) CompId() ecs.CompId {
return TransformComp.CompId()
}
func (c Transform) CompWrite(w ecs.W) {
TransformComp.WriteVal(w, c)
}
var VisibilityComp = ecs.NewComp[Visibility]()
func (c Visibility) CompId() ecs.CompId {
return VisibilityComp.CompId()
}
func (c Visibility) CompWrite(w ecs.W) {
VisibilityComp.WriteVal(w, c)
}
var VisionListComp = ecs.NewComp[VisionList]()
func (c VisionList) CompId() ecs.CompId {
return VisionListComp.CompId()
}
func (c VisionList) CompWrite(w ecs.W) {
VisionListComp.WriteVal(w, c)
}
var WindowComp = ecs.NewComp[Window]()
func (c Window) CompId() ecs.CompId {
return WindowComp.CompId()
}
func (c Window) CompWrite(w ecs.W) {
WindowComp.WriteVal(w, c)
}
package render
import (
"time"
"github.com/unitoftime/ecs"
"github.com/unitoftime/flow/glm"
"github.com/unitoftime/glitch"
)
type DefaultPlugin struct {
}
func (p DefaultPlugin) Initialize(world *ecs.World) {
scheduler := ecs.GetResource[ecs.Scheduler](world)
rl := NewRenderPassList()
ecs.PutResource(world, &rl)
scheduler.AddSystems(ecs.StageStartup,
ecs.NewSystem1(SetupRenderingSystem),
)
scheduler.AddSystems(ecs.StageUpdate,
ecs.NewSystem1(UpdateCameraSystem),
ecs.NewSystem2(CalculateVisibilitySystem),
ecs.NewSystem2(CalculateRenderPasses),
ecs.NewSystem2(ExecuteRenderPass),
ecs.NewSystem1(RenderingFlushSystem),
)
}
//cod:component
type VisionList struct {
List []ecs.Id
}
func NewVisionList() VisionList {
return VisionList{
List: make([]ecs.Id, 0),
}
}
func (vl *VisionList) Add(id ecs.Id) {
vl.List = append(vl.List, id)
}
func (vl *VisionList) Clear() {
vl.List = vl.List[:0]
}
//cod:component
type Window struct {
*glitch.Window
}
//cod:component
type Sprite struct {
Sprite *glitch.Sprite
}
func (s Sprite) Bounds() glm.Rect {
return s.Sprite.Bounds()
}
//cod:component
type Target struct {
draw RenderTarget
batch glitch.BatchTarget // TODO: Indexed?
}
//cod:component
type Visibility struct {
Hide bool // If set true, the entity will always be calculated as invisible
Calculated bool
}
//cod:component
type CalculatedVisibility struct {
Visible bool // If set true, the entity is visible
}
func SetupRenderingSystem(dt time.Duration, commands *ecs.CommandQueue) {
defer commands.Execute() // TODO: Remove
win, err := glitch.NewWindow(1920, 1080, "TODO", glitch.WindowConfig{
Vsync: true,
})
if err != nil {
panic(err)
}
commands.SpawnEmpty().
Insert(Window{win})
commands.SpawnEmpty().
Insert(Camera2DBundle{win})
}
package render
import (
"github.com/unitoftime/glitch"
)
type Cursor struct {
Dragging bool
DragStart glitch.Vec2
}
var cursor Cursor
func MouseDrag(win *glitch.Window, camera *Camera, dragButton glitch.Key) {
mX, mY := win.MousePosition()
if win.JustPressed(dragButton) {
cursor.DragStart = glitch.Vec2{mX, mY}
}
if win.Pressed(dragButton) {
// this is the right ratio, but you have to pass this in someway maybe camera knows about FB scaling? Idk
// camera.Position[0] += (cursor.DragStart[0] - mX) / float32(camera.Zoom) * (camera.bounds.W() / 1920.0)
// camera.Position[1] += (cursor.DragStart[1] - mY) / float32(camera.Zoom) * (camera.bounds.W() / 1920.0)
camera.Position.X += (cursor.DragStart.X - mX) / camera.Zoom
camera.Position.Y += (cursor.DragStart.Y - mY) / camera.Zoom
cursor.DragStart = glitch.Vec2{mX, mY}
cursor.Dragging = true
}
if !win.Pressed(dragButton) {
cursor.Dragging = false
}
}
package render
import (
"time"
"github.com/unitoftime/ecs"
"github.com/unitoftime/flow/glm"
"github.com/unitoftime/flow/spatial"
"github.com/unitoftime/flow/transform"
"github.com/unitoftime/glitch"
)
// Systems:
// - Update cameras
// - Calculate vision list for each camera
// - (Optimization) - If a lot of render passes, you might build a spatial map
// Clear, Draw, Next
// - Calculate all of the render passes that need to be executed
// - Calculate the visible entities for each render pass
// - (Optimization) - If a lot of render passes, you might build a spatial map
// - Loop through render passes and execute them
// Clear a target
// Add commands to something
// Flush to that target
// Go next
// Render Pass Structure
// - RenderPass
// |-- Render Target
// |-- Camera
// |-- Visible Entity
// |-- Visible Entity
// |-- Camera
// |-- Visible Entity
// |-- Visible Entity
func UpdateCameraSystem(dt time.Duration, query *ecs.View2[Camera, Target]) {
query.MapId(func(id ecs.Id, camera *Camera, target *Target) {
camera.Update(target.draw.Bounds())
})
}
func CalculateVisibilitySystem(dt time.Duration, camQuery *ecs.View2[Camera, VisionList], query *ecs.View3[transform.Global, Sprite, Visibility]) {
// For each camera, calculate it's frustrum, clear its vision list, and add every visible sprite
camQuery.MapId(func(_ ecs.Id, camera *Camera, visionList *VisionList) {
winShape := camera.WorldSpaceShape()
visionList.Clear()
query.MapId(func(id ecs.Id, gt *transform.Global, sprite *Sprite, vis *Visibility) {
vis.Calculated = false
if vis.Hide {
return
}
mat := gt.Mat4()
shape := spatial.Rect(sprite.Bounds(), mat)
vis.Calculated = shape.Intersects(winShape)
if vis.Calculated {
visionList.Add(id)
}
})
})
// TODO: Optimization if there are a lot of cameras: Preconfigure a spatial hash
// chunkSize := [2]int{1024/8, 1024/8}
// visionMap := spatial.NewHashmap[ecs.Id](chunkSize, 8)
// query.MapId(func(id ecs.Id, gt *transform.Global, sprite *Sprite, vis *Visibility, calcVis *CalculatedVisibility) {
// mat := gt.Mat4()
// shape := spatial.Rect(sprite.sprite.Bounds(), mat)
// visionMap.Add(shape, id)
// })
}
type RenderPassList struct {
List []RenderPass
}
func NewRenderPassList() RenderPassList {
return RenderPassList{
List: make([]RenderPass, 0),
}
}
func (l *RenderPassList) Add(p RenderPass) {
l.List = append(l.List, p)
}
func (l *RenderPassList) Clear() {
l.List = l.List[:0]
}
type RenderTarget interface {
glitch.Target
glitch.BatchTarget
Bounds() glm.Rect
}
type RenderPass struct {
batchTarget glitch.BatchTarget
drawTarget RenderTarget
clearColor glm.RGBA
camera Camera // TODO: I feel like there could/should support multiple?
visionList VisionList
}
func CalculateRenderPasses(dt time.Duration, passes *RenderPassList, query *ecs.View3[Camera, Target, VisionList]) {
passes.Clear()
query.MapId(func(id ecs.Id, camera *Camera, target *Target, visionList *VisionList) {
passes.Add(RenderPass{
drawTarget: target.draw,
batchTarget: target.batch,
camera: *camera,
visionList: *visionList,
})
})
// TODO: Sort somehow? Priority? Order added?
}
func ExecuteRenderPass(dt time.Duration, passes *RenderPassList, query *ecs.View2[transform.Global, Sprite]) {
for _, pass := range passes.List {
glitch.Clear(pass.drawTarget, pass.clearColor)
camera := &pass.camera.Camera
glitch.SetCamera(camera)
for _, id := range pass.visionList.List {
gt, sprite := query.Read(id)
if gt == nil {
continue
}
if sprite == nil {
continue
}
mat := gt.Mat4()
sprite.Sprite.DrawColorMask(pass.batchTarget, mat, glm.White)
}
}
}
func RenderingFlushSystem(dt time.Duration, query *ecs.View1[Window]) {
query.MapId(func(_ ecs.Id, win *Window) {
win.Update()
})
}
package render
import (
"github.com/unitoftime/flow/glm"
"github.com/unitoftime/flow/transform"
)
//cod:component
type Transform struct {
transform.Transform
Height float64 // TODO: Remove
}
func TransformFromPos(pos glm.Vec2) Transform {
return Transform{
Transform: transform.Transform{
Pos: pos,
Rot: 0,
Scale: glm.Vec2{1, 1},
},
}
}
// // Represents multiple sprites
// type MultiSprite struct {
// Sprites []Sprite
// }
// func NewMultiSprite(sprites ...Sprite) MultiSprite {
// m := MultiSprite{
// Sprites: make([]Sprite, len(sprites)),
// }
// for i := range sprites {
// m.Sprites[i] = sprites[i]
// }
// return m
// }
// type Sprite struct {
// *glitch.Sprite
// // Color color.NRGBA // TODO - performance on interfaces vs structs?
// Color glitch.RGBA
// Rotation float64
// Scale glitch.Vec2
// Layer int8
// }
// func NewSprite(sprite *glitch.Sprite) Sprite {
// return Sprite{
// Sprite: sprite,
// // Color: color.NRGBA{255, 255, 255, 255},
// Color: glitch.White,
// Scale: glitch.Vec2{1, 1},
// // Layer: glitch.DefaultLayer,
// }
// }
// func (sprite *Sprite) Draw(pass *glitch.RenderPass, pos *Pos) {
// mat := glitch.Mat4Ident
// mat.Scale(sprite.Scale[0], sprite.Scale[1], 1.0).
// Rotate(sprite.Rotation, glitch.Vec3{0, 0, 1}).
// Translate(pos.X, pos.Y, 0)
// // TODO - I think there's some mistakes here with premultiplied vs non premultiplied alpha
// col := glitch.RGBA{sprite.Color.R/255.0, sprite.Color.G/255.0, sprite.Color.B/255.0, sprite.Color.A/255.0}
// pass.SetLayer(sprite.Layer)
// sprite.DrawColorMask(pass, mat, col)
// }
// type Keybinds struct {
// Up, Down, Left, Right glitch.Key
// }
// // Note: val should probably be between 0 and 1
// func Interpolate(A, B glitch.Vec2, lowerBound, upperBound float32) glitch.Vec2 {
// delta := B.Sub(A)
// dMag := delta.Len()
// interpValue := float32(0.0)
// if dMag > upperBound {
// interpValue = 1.0
// } else if dMag > lowerBound {
// // y - y1 = m(x - x1)
// slope := 1/(upperBound - lowerBound)
// interpValue = slope * (dMag - lowerBound) + 0
// }
// deltaScaled := delta.Scaled(interpValue)
// return A.Add(deltaScaled)
// }
// // TODO - how to do optional components? with some default val?
// func DrawSprites(pass *glitch.RenderPass, world *ecs.World) {
// ecs.Map2(world, func(id ecs.Id, sprite *Sprite, t *phy2.Transform) {
// mat := glitch.Mat4Ident
// mat.Scale(sprite.Scale[0], sprite.Scale[1], 1.0).Translate(float32(t.X), float32(t.Y + t.Height), 0)
// // TODO - I think there's some mistakes here with premultiplied vs non premultiplied alpha
// col := glitch.RGBA{float32(sprite.Color.R)/255.0, float32(sprite.Color.G)/255.0, float32(sprite.Color.B)/255.0, float32(sprite.Color.A)/255.0}
// pass.SetLayer(sprite.Layer)
// sprite.DrawColorMask(pass, mat, col)
// })
// }
// func DrawMultiSprites(pass *glitch.RenderPass, world *ecs.World) {
// ecs.Map2(world, func(id ecs.Id, mSprite *MultiSprite, t *phy2.Transform) {
// for _, sprite := range mSprite.Sprites {
// mat := glitch.Mat4Ident
// mat.Scale(sprite.Scale[0], sprite.Scale[1], 1.0).Translate(float32(t.X), float32(t.Y + t.Height), 0)
// // TODO - I think there's some mistakes here with premultiplied vs non premultiplied alpha
// col := glitch.RGBA{float32(sprite.Color.R)/255.0, float32(sprite.Color.G)/255.0, float32(sprite.Color.B)/255.0, float32(sprite.Color.A)/255.0}
// pass.SetLayer(sprite.Layer)
// sprite.DrawColorMask(pass, mat, col)
// }
// })
// }
// func CaptureInput(win *glitch.Window, world *ecs.World) {
// // TODO - technically this should only run for the player Ids?
// ecs.Map2(world, func(id ecs.Id, keybinds *Keybinds, input *phy2.Input) {
// input.Left = false
// input.Right = false
// input.Up = false
// input.Down = false
// if win.Pressed(keybinds.Left) {
// input.Left = true
// }
// if win.Pressed(keybinds.Right) {
// input.Right = true
// }
// if win.Pressed(keybinds.Up) {
// input.Up = true
// }
// if win.Pressed(keybinds.Down) {
// input.Down = true
// }
// })
// }
package render
import (
"github.com/unitoftime/glitch"
"github.com/unitoftime/flow/tile"
)
type TileDraw struct {
Sprite *glitch.Sprite
Depth float64
}
type Chunkmap[T any] struct {
chunkmap *tile.Chunkmap[T]
tilemapRender *TilemapRender[T]
chunks map[tile.ChunkPosition]*glitch.Batch
}
func NewChunkmap[T any](chunkmap *tile.Chunkmap[T], tileToSprite func(t T) []TileDraw) *Chunkmap[T] {
return &Chunkmap[T]{
chunkmap: chunkmap,
tilemapRender: NewTilemapRender[T](tileToSprite),
chunks: make(map[tile.ChunkPosition]*glitch.Batch),
}
}
// Returns the request chunk batch
func (c *Chunkmap[T]) GetChunk(chunkPos tile.ChunkPosition) *glitch.Batch {
batch, ok := c.chunks[chunkPos]
if !ok {
c.RebatchChunk(chunkPos)
}
batch, ok = c.chunks[chunkPos]
if !ok {
panic("Programmer bug")
}
return batch
}
// Rebatches a specific chunk (ie signal that the chunk has changed and needs to be rebatched
func (c *Chunkmap[T]) RebatchChunk(chunkPos tile.ChunkPosition) {
batch, ok := c.chunks[chunkPos]
if !ok {
batch = glitch.NewBatch()
}
chunk, ok := c.chunkmap.GetChunk(chunkPos)
if ok {
// Chunk exists, rebatch it
batch.Clear()
c.tilemapRender.Draw(chunk, batch)
} else {
// Chunk doesn't exist, so just store an empty batch there
batch.Clear()
}
c.chunks[chunkPos] = batch
}
type TilemapRender[T any] struct {
tileToSprite func(t T) []TileDraw
// tileToSprite map[tile.TileType]*glitch.Sprite
}
func NewTilemapRender[T any](tileToSprite func(t T) []TileDraw) *TilemapRender[T] {
// func NewTilemapRender(tileToSprite map[tile.TileType]*glitch.Sprite) *TilemapRender {
// Note: Assumes that all sprites share the same spritesheet
return &TilemapRender[T]{
tileToSprite: tileToSprite,
}
}
func (r *TilemapRender[T]) Draw(tmap *tile.Chunk[T], batch *glitch.Batch) {
for x := 0; x < tmap.Width(); x++ {
for y := tmap.Height(); y >= 0; y-- {
t, ok := tmap.Get(tile.TilePosition{x, y})
if !ok {
continue
}
// pos := r.Math.Position(x, y, t.TileSize)
xPos, yPos := tmap.TileToPosition(tile.TilePosition{x, y})
pos := glitch.Vec2{xPos, yPos} // .Add(glitch.Vec2{8, 8})
// TODO!!! - This should get captured in maybe some extra offset function?
// pos[1] += t.Height * float32(tmap.TileSize[1])
// Normal grid
// pos := glitch.Vec2{float32(x * t.TileSize[0]), float32(y * t.TileSize[1])}
// Isometric grid
// pos := glitch.Vec2{
// // If y goes up, then xPos must go downward a bit
// -float32((x * t.TileSize[0] / 2) - (y * t.TileSize[0] / 2)),
// // If x goes up, then yPos must go up a bit as well
// -float32((y * t.TileSize[1] / 2) + (x * t.TileSize[1] / 2))}
tileDraw := r.tileToSprite(t)
for _, d := range tileDraw {
if d.Sprite == nil {
continue // Skip if the sprite is nil
}
mat := glitch.Mat4Ident
mat.Translate(pos.X, pos.Y, d.Depth)
d.Sprite.Draw(batch, mat)
}
}
}
}
package spatial
import (
"math"
"slices"
"github.com/unitoftime/flow/glm"
)
type arrayMap[T any] struct {
// (+x, +y) => 0
// (+x, -y) => 2
// (-x, +y) => 1
// (-x, -y) => 3
quad [][][]*T
}
func newArrayMap[T any](size int) *arrayMap[T] {
size = size / 2 // Note: We cut in half b/c we use 4 quadrants
m := &arrayMap[T]{
quad: make([][][]*T, size),
}
// Create 4 quadrants
for range 4 {
slice := make([][]*T, size)
for i := range slice {
slice[i] = make([]*T, size)
}
m.quad = append(m.quad, slice)
}
return m
}
func (m *arrayMap[T]) safePut(slice [][]*T, x, y int, t *T) [][]*T {
if x < len(slice) {
if y < len(slice[x]) {
slice[x][y] = t
return slice
} else {
slice[x] = slices.Grow(slice[x], y-len(slice[x])+1)
slice[x] = slice[x][:y+1]
slice[x][y] = t
return slice
}
}
slice = slices.Grow(slice, x-len(slice)+1)
slice = slice[:x+1]
slice[x] = slices.Grow(slice[x], y-len(slice[x])+1)
slice[x] = slice[x][:y+1]
slice[x][y] = t
return slice
}
// Returns the qudrant index, the X index, and the Y Index
func (m *arrayMap[T]) getQuadIndexes(x, y int) (int, int, int) {
// Calculates the following Quadrants:
// (+x, +y) => 0
// (+x, -y) => 2
// (-x, +y) => 1
// (-x, -y) => 3
idx := 0
if x < 0 {
idx += 1
x = -x
}
if y < 0 {
idx += 2
y = -y
}
return idx, x, y
}
func (m *arrayMap[T]) Put(x, y int, t *T) {
idx, xIdx, yIdx := m.getQuadIndexes(x, y)
m.quad[idx] = m.safePut(m.quad[idx], xIdx, yIdx, t)
}
func (m *arrayMap[T]) safeGet(slice [][]*T, x, y int) (*T, bool) {
if x >= len(slice) {
return nil, false
}
if y >= len(slice[x]) {
return nil, false
}
isNil := (slice[x][y] == nil)
return slice[x][y], !isNil
}
func (m *arrayMap[T]) Get(x, y int) (*T, bool) {
idx, xIdx, yIdx := m.getQuadIndexes(x, y)
return m.safeGet(m.quad[idx], xIdx, yIdx)
}
func (m *arrayMap[T]) ForEachValue(lambda func(t *T)) {
for idx := range m.quad {
for x := range m.quad[idx] {
for y := range m.quad[idx][x] {
val := m.quad[idx][x][y]
if val == nil {
continue
}
lambda(val)
}
}
}
}
type Index struct {
X, Y int
}
type BucketItem[T comparable] struct {
shape Shape
item T
}
type Bucket[T comparable] struct {
List []BucketItem[T]
}
func NewBucket[T comparable]() *Bucket[T] {
return &Bucket[T]{
List: make([]BucketItem[T], 0),
}
}
func (b *Bucket[T]) Add(shape Shape, val T) {
b.List = append(b.List, BucketItem[T]{
shape: shape,
item: val,
})
}
func (b *Bucket[T]) Remove(val T) {
b.List = slices.DeleteFunc(b.List, func(a BucketItem[T]) bool {
return (a.item == val)
})
}
func (b *Bucket[T]) Clear() {
b.List = b.List[:0]
}
func (b *Bucket[T]) Check(colSet *CollisionSet[T], shape Shape) {
for i := range b.List {
if shape.Intersects(b.List[i].shape) {
colSet.Add(b.List[i].item)
}
}
}
func (b *Bucket[T]) Collides(shape Shape) bool {
for i := range b.List {
if shape.Intersects(b.List[i].shape) {
return true
}
}
return false
}
func (b *Bucket[T]) FindClosest(shape Shape) (BucketItem[T], bool) {
center := shape.Bounds.Center()
distSquared := math.MaxFloat64
ret := BucketItem[T]{}
set := false
for i := range b.List {
if shape.Intersects(b.List[i].shape) {
ds := center.DistSq(b.List[i].shape.Bounds.Center())
if ds < distSquared {
distSquared = ds
ret = b.List[i]
set = true
}
}
}
return ret, set
}
// --------------------------------------------------------------------------------
type PositionHasher struct {
size [2]int
sizeOver2 [2]int
div [2]int
}
func NewPositionHasher(size [2]int) PositionHasher {
divX := int(math.Log2(float64(size[0])))
divY := int(math.Log2(float64(size[1])))
if (1<<divX) != size[0] || (1<<divY) != size[1] {
panic("Spatial maps must have a chunksize that is a power of 2!")
}
return PositionHasher{
size: size,
sizeOver2: [2]int{size[0] / 2, size[1] / 2},
div: [2]int{divX, divY},
}
}
func (h *PositionHasher) PositionToIndex(pos glm.Vec2) Index {
x := pos.X
y := pos.Y
xPos := (int(x)) >> h.div[0]
yPos := (int(y)) >> h.div[1]
// TODO: I dont think I need this b/c of how shift right division works (negatives stay negative)
// // Adjust for negatives
// if x < float64(-h.sizeOver2[0]) {
// xPos -= 1
// }
// if y < float64(-h.sizeOver2[1]) {
// yPos -= 1
// }
return Index{xPos, yPos}
}
// --------------------------------------------------------------------------------
// TODO: rename? ColliderMap?
type Hashmap[T comparable] struct {
PositionHasher
allBuckets []*Bucket[T]
Bucket *arrayMap[Bucket[T]]
}
func NewHashmap[T comparable](chunksize [2]int, startingSize int) *Hashmap[T] {
return &Hashmap[T]{
PositionHasher: NewPositionHasher(chunksize),
allBuckets: make([]*Bucket[T], 0, 1024),
Bucket: newArrayMap[Bucket[T]](startingSize),
}
}
func (h *Hashmap[T]) Clear() {
for i := range h.allBuckets {
h.allBuckets[i].Clear()
}
}
func (h *Hashmap[T]) GetBucket(index Index) *Bucket[T] {
bucket, ok := h.Bucket.Get(index.X, index.Y)
if !ok {
bucket = NewBucket[T]()
h.allBuckets = append(h.allBuckets, bucket)
h.Bucket.Put(index.X, index.Y, bucket)
}
return bucket
}
func (h *Hashmap[T]) Add(shape Shape, val T) {
min := h.PositionToIndex(shape.Bounds.Min)
max := h.PositionToIndex(shape.Bounds.Max)
// Early case if it's only one bucket
if min == max {
bucket := h.GetBucket(min)
bucket.Add(shape, val)
return
}
for x := min.X; x <= max.X; x++ {
for y := min.Y; y <= max.Y; y++ {
bucket := h.GetBucket(Index{x, y})
bucket.Add(shape, val)
}
}
}
// Warning: This is a relatively slow operation
func (h *Hashmap[T]) Remove(val T) {
// Just try and remove the id from all buckets
for i := range h.allBuckets {
h.allBuckets[i].Remove(val)
}
}
// Finds collisions and adds them directly into your collision set
func (h *Hashmap[T]) Check(colSet *CollisionSet[T], shape Shape) {
// shape := AABB(rect)
min := h.PositionToIndex(shape.Bounds.Min)
max := h.PositionToIndex(shape.Bounds.Max)
for x := min.X; x <= max.X; x++ {
for y := min.Y; y <= max.Y; y++ {
isBorderChunk := (x == min.X || x == max.X) || (y == min.Y || y == max.Y)
// bucket := h.GetBucket(Index{x, y})
bucket, ok := h.Bucket.Get(x, y)
if !ok {
continue
}
if isBorderChunk {
// For border chunks, we need to do narrow phase too
bucket.Check(colSet, shape)
} else {
// If the shape is an AABB it means that everything inside inner chunks must collide
// so we can just add everything from the bucket (much faster)
if shape.Type == ShapeAABB {
for i := range bucket.List {
colSet.Add(bucket.List[i].item)
}
} else {
// Else we need to do the slower, manual check of each one
bucket.Check(colSet, shape)
}
}
}
}
}
// Returns true if the rect collides with anything in the hashmap
func (h *Hashmap[T]) Collides(rect glm.Rect) bool {
shape := AABB(rect)
min := h.PositionToIndex(shape.Bounds.Min)
max := h.PositionToIndex(shape.Bounds.Max)
for x := min.X; x <= max.X; x++ {
for y := min.Y; y <= max.Y; y++ {
isBorderChunk := (x == min.X || x == max.X) || (y == min.Y || y == max.Y)
// bucket := h.GetBucket(Index{x, y})
bucket, ok := h.Bucket.Get(x, y)
if !ok {
continue
}
if isBorderChunk {
// For border chunks, we need to do narrow phase too
if bucket.Collides(shape) {
return true
}
} else {
// For inner chunks, we can just assume everything collides
if len(bucket.List) > 0 {
return true
}
}
}
}
return false
}
func (h *Hashmap[T]) FindClosest(rect glm.Rect) (T, bool) {
shape := AABB(rect)
min := h.PositionToIndex(shape.Bounds.Min)
max := h.PositionToIndex(shape.Bounds.Max)
center := shape.Bounds.Center()
distSquared := math.MaxFloat64
var ret T
set := false
for x := min.X; x <= max.X; x++ {
for y := min.Y; y <= max.Y; y++ {
bucket, ok := h.Bucket.Get(x, y)
if !ok {
continue
}
// For border chunks, we need to do narrow phase too
closest, ok := bucket.FindClosest(shape)
if ok {
ds := center.DistSq(closest.shape.Bounds.Center())
if ds < distSquared {
distSquared = ds
ret = closest.item
set = true
}
}
}
}
return ret, set
}
// Adds the collisions directly into your collision set. This one doesnt' do any narrow phase detection. It returns all objects that collide with the same chunk
func (h *Hashmap[T]) BroadCheck(colSet CollisionSet[T], rect glm.Rect) {
shape := AABB(rect)
min := h.PositionToIndex(shape.Bounds.Min)
max := h.PositionToIndex(shape.Bounds.Max)
for x := min.X; x <= max.X; x++ {
for y := min.Y; y <= max.Y; y++ {
bucket := h.GetBucket(Index{x, y})
for i := range bucket.List {
colSet.Add(bucket.List[i].item)
}
}
}
}
// TODO - there's probably more efficient ways to deduplicate than a map here?
// type CollisionSet[T comparable] map[T]struct{}
// func NewCollisionSet[T comparable](cap int) CollisionSet[T] {
// return make(CollisionSet[T], cap)
// }
// func (s CollisionSet[T]) Add(t T) {
// s[t] = struct{}{}
// }
// func (s CollisionSet[T]) Clear() {
// // clear(s) // TODO: Is this slow?
// // Clearing Optimization: https://go.dev/doc/go1.11#performance-compiler
// for k := range s {
// delete(s, k)
// }
// }
type CollisionSet[T comparable] struct {
List []T
}
func NewCollisionSet[T comparable](cap int) *CollisionSet[T] {
return &CollisionSet[T]{
List: make([]T, cap),
}
}
func (s *CollisionSet[T]) Add(t T) {
for i := range s.List {
if s.List[i] == t {
return // Already added
}
}
s.List = append(s.List, t)
}
func (s *CollisionSet[T]) Clear() {
s.List = s.List[:0]
}
package spatial
import (
"slices"
"github.com/unitoftime/flow/glm"
)
type PointBucketItem[T comparable] struct {
point glm.Vec2
item T
}
type PointBucket[T comparable] struct {
List []PointBucketItem[T]
}
func NewPointBucket[T comparable]() *PointBucket[T] {
return &PointBucket[T]{
List: make([]PointBucketItem[T], 0),
}
}
func (b *PointBucket[T]) Add(point glm.Vec2, val T) {
b.List = append(b.List, PointBucketItem[T]{
point: point,
item: val,
})
}
func (b *PointBucket[T]) Remove(point glm.Vec2, val T) {
itemToRemove := PointBucketItem[T]{
point: point,
item: val,
}
indexToRemove := slices.Index(b.List, itemToRemove)
if indexToRemove < 0 {
return
} // Nothing to remove
b.RemoveIndex(indexToRemove)
}
func (b *PointBucket[T]) RemoveIndex(idx int) {
lastIdx := len(b.List) - 1
b.List[idx] = b.List[lastIdx]
b.List = b.List[:lastIdx]
}
// Remove every point that collides with the bucket
func (b *PointBucket[T]) RemoveCollides(bounds glm.Rect) {
for i := 0; i < len(b.List); i++ {
if !bounds.Contains(b.List[i].point) {
continue
} // skip if doesnt collide
b.RemoveIndex(i)
i--
}
}
func (b *PointBucket[T]) Clear() {
b.List = b.List[:0]
}
// TODO: rename? ColliderMap?
type Pointmap[T comparable] struct {
PositionHasher
Bucket *arrayMap[PointBucket[T]]
allBuckets []*PointBucket[T]
}
func NewPointmap[T comparable](chunksize [2]int, startingSize int) *Pointmap[T] {
return &Pointmap[T]{
allBuckets: make([]*PointBucket[T], 0, 1024),
Bucket: newArrayMap[PointBucket[T]](startingSize),
PositionHasher: NewPositionHasher(chunksize),
}
}
func (h *Pointmap[T]) Clear() {
for i := range h.allBuckets {
h.allBuckets[i].Clear()
}
}
func (h *Pointmap[T]) GetBucket(index Index) *PointBucket[T] {
bucket, ok := h.Bucket.Get(index.X, index.Y)
if !ok {
bucket = NewPointBucket[T]()
h.allBuckets = append(h.allBuckets, bucket)
h.Bucket.Put(index.X, index.Y, bucket)
}
return bucket
}
func (h *Pointmap[T]) Add(pos glm.Vec2, val T) {
idx := h.PositionToIndex(pos)
bucket := h.GetBucket(idx)
bucket.Add(pos, val)
}
func (h *Pointmap[T]) Remove(pos glm.Vec2, val T) {
idx := h.PositionToIndex(pos)
bucket := h.GetBucket(idx)
bucket.Remove(pos, val)
}
// TODO: Right now this does a broad phased check
// Adds the collisions directly into your collision list. Items are deduplicated by nature of them only existing once in this Pointmap. (ie if you add multiple of the same thing, you might get multiple out)
func (h *Pointmap[T]) BroadCheck(list []T, bounds glm.Rect) []T {
min := h.PositionToIndex(bounds.Min)
max := h.PositionToIndex(bounds.Max)
// TODO: Might be nice if this spirals from inside to outside, that way its roughly sorted by distance?
for x := min.X; x <= max.X; x++ {
for y := min.Y; y <= max.Y; y++ {
bucket, ok := h.Bucket.Get(x, y)
if !ok {
continue
}
// bucket := h.GetBucket(Index{x, y})
for i := range bucket.List {
list = append(list, bucket.List[i].item)
}
}
}
return list
}
// TODO: I think I'd rather the default for this be called "Check" then have the other be called CheckBroad or something
func (h *Pointmap[T]) NarrowCheck(list []T, bounds glm.Rect) []T {
min := h.PositionToIndex(bounds.Min)
max := h.PositionToIndex(bounds.Max)
// TODO: Might be nice if this spirals from inside to outside, that way its roughly sorted by distance?
for x := min.X; x <= max.X; x++ {
for y := min.Y; y <= max.Y; y++ {
bucket, ok := h.Bucket.Get(x, y)
if !ok {
continue
}
for i := range bucket.List {
if bounds.Contains(bucket.List[i].point) {
list = append(list, bucket.List[i].item)
}
}
}
}
return list
}
// TODO: This only does a broadphase check. no narrow phase
// Returns true if the bounds collides with anything
func (h *Pointmap[T]) Collides(bounds glm.Rect) bool {
min := h.PositionToIndex(bounds.Min)
max := h.PositionToIndex(bounds.Max)
// TODO: Might be nice if this spirals from inside to outside, that way its roughly sorted by distance?
for x := min.X; x <= max.X; x++ {
for y := min.Y; y <= max.Y; y++ {
bucket, ok := h.Bucket.Get(x, y)
if !ok {
continue
}
if len(bucket.List) > 0 {
return true
}
}
}
return false
}
// Remove every point that collides with the supplied bounds
func (h *Pointmap[T]) RemoveCollides(bounds glm.Rect) {
min := h.PositionToIndex(bounds.Min)
max := h.PositionToIndex(bounds.Max)
// TODO: Might be nice if this spirals from inside to outside, that way its roughly sorted by distance?
for x := min.X; x <= max.X; x++ {
for y := min.Y; y <= max.Y; y++ {
bucket, ok := h.Bucket.Get(x, y)
if !ok {
continue
}
bucket.RemoveCollides(bounds)
}
}
}
package spatial
import (
"github.com/unitoftime/flow/glm"
)
type ShapeType uint8
// TODO: Tagged Union?
const (
ShapeAABB ShapeType = iota // A rectangle, not rotated nor scaled
ShapeRect // Can be rotated or scaled
// ShapeCircle // TODO: A circle
// ShapeRing // TODO: A ring (Circle with excluded middle segment
// ShapeEllipses // TODO: Maybe combine with circle?
// ShapePolygon // TODO: An arbitrary convex polygon (Question: How to support arbitrary length points)
)
type Shape struct {
Type ShapeType
Bounds glm.Rect // The bounding AABB
Vectors [4]glm.Vec2 // Vector data which is stored differently depending on the shape type
}
func AABB(rect glm.Rect) Shape {
return Shape{
Type: ShapeAABB,
Bounds: rect,
Vectors: [4]glm.Vec2{
rect.Min,
rect.TL(),
rect.Max,
rect.BR(),
},
}
}
func Rect(r glm.Rect, mat glm.Mat4) Shape {
tl := mat.Apply(glm.Vec3{r.Min.X, r.Max.Y, 0}).Vec2()
br := mat.Apply(glm.Vec3{r.Max.X, r.Min.Y, 0}).Vec2()
bl := mat.Apply(r.Min.Vec3()).Vec2()
tr := mat.Apply(r.Max.Vec3()).Vec2()
xMin := min(tl.X, br.X, bl.X, tr.X)
yMin := min(tl.Y, br.Y, bl.Y, tr.Y)
xMax := max(tl.X, br.X, bl.X, tr.X)
yMax := max(tl.Y, br.Y, bl.Y, tr.Y)
bounds := glm.R(xMin, yMin, xMax, yMax)
return Shape{
Type: ShapeRect,
Bounds: bounds,
Vectors: [4]glm.Vec2{
bl, tl, tr, br,
},
}
}
func (s Shape) Intersects(s2 Shape) bool {
switch s.Type {
case ShapeAABB:
{
switch s2.Type {
case ShapeAABB:
return s.Bounds.Intersects(s2.Bounds)
case ShapeRect:
if !s.Bounds.Intersects(s2.Bounds) {
return false // Bounding box must intersect
}
return polygonIntersectionCheck(s.Vectors[:], s2.Vectors[:])
}
}
case ShapeRect:
{
switch s2.Type {
case ShapeAABB:
if !s.Bounds.Intersects(s2.Bounds) {
return false // Bounding box must intersect
}
return polygonIntersectionCheck(s.Vectors[:], s2.Vectors[:])
case ShapeRect:
if !s.Bounds.Intersects(s2.Bounds) {
return false // Bounding box must intersect
}
return polygonIntersectionCheck(s.Vectors[:], s2.Vectors[:])
}
}
}
// Invalid case
return false
}
func polygonIntersectionCheck(a, b []glm.Vec2) bool {
// First check if at least one point is inside. we need to do this to handle the case where one polygon is entirely inside the other
for i := range a {
if polygonContainsPoint(b, a[i]) {
return true
}
}
for i := range b {
if polygonContainsPoint(a, b[i]) {
return true
}
}
// Else no points are inside, so we test all edge pairs
lenA := len(a)
lenB := len(b)
var l1, l2 glm.Line2
for i := range a {
l1.A = a[i]
l1.B = a[(i+1)%lenA]
for j := range b {
l2.A = b[j]
l2.B = b[(j+1)%lenB]
if l1.Intersects(l2) {
return true
}
}
}
return false
}
// func polygonContainsPoint(poly []glm.Vec2, point glm.Vec2) bool {
// count := 0
// length := len(poly)
// for i := range poly {
// a := poly[i]
// b := poly[(i+1) % length]
// if (point.Y < a.Y) != (point.Y < b.Y) && point.X < a.X + ((point.Y - a.Y)/(b.Y - a.Y)) * (b.X - a.X) {
// count++
// }
// }
// return (count % 2) == 1
// }
func polygonContainsPoint(poly []glm.Vec2, point glm.Vec2) bool {
// Ref: https://stackoverflow.com/questions/22521982/check-if-point-is-inside-a-polygon
// https://wrf.ecse.rpi.edu/Research/Short_Notes/pnpoly.html
x := point.X
y := point.Y
length := len(poly)
var inside = false
for i := range poly {
a := poly[i]
b := poly[(i+1)%length]
xi := a.X
yi := a.Y
xj := b.X
yj := b.Y
intersect := ((yi > y) != (yj > y)) && (x < (xj-xi)*(y-yi)/(yj-yi)+xi)
if intersect {
inside = !inside
}
}
return inside
}
// Note: Originally I was doing this, but it actually takes way more projections than I thought. So I opted to just do a generic line intersection test for all edges of both rectangles. Even though this one may be faster. Maybe I"ll add it back in the future
// TODO: This was almost done. I think all that remained was that I need to
// 1. Repeat for both axes and make sure all segments overlap
// Reference: https://math.stackexchange.com/questions/1278665/how-to-check-if-two-rectangles-intersect-rectangles-can-be-rotated
// 1. calc new axis
// 2. project b points onto new axis
// 3. Check if any point overlaps 'rect A' ranges
// 2. (If only one is rotated) The axis and projection calculations
// func (a RectShape) Intersects(b RectShape) bool {
// // return a.Bounds.Intersects(b.Bounds)
// // 1. calc new axis
// axisXVec := a.BR.Sub(a.BL)
// axisYVec := a.TL.Sub(a.BL)
// axisX := axisXVec.Norm()
// axisY := axisYVec.Norm()
// fmt.Println("axisX", axisX)
// fmt.Println("axisY", axisY)
// // 2. project b points onto new axis
// newBL := b.BL.Sub(a.BL)
// newTL := b.TL.Sub(a.BL)
// newTR := b.TR.Sub(a.BL)
// newBR := b.BR.Sub(a.BL)
// // X
// projBL_X := newBL.Dot(axisX)
// projTL_X := newTL.Dot(axisX)
// projTR_X := newTR.Dot(axisX)
// projBR_X := newBR.Dot(axisX)
// // Y
// projBL_Y := newBL.Dot(axisY)
// projTL_Y := newTL.Dot(axisY)
// projTR_Y := newTR.Dot(axisY)
// projBR_Y := newBR.Dot(axisY)
// fmt.Println("projX", projBL_X, projTL_X, projTR_X, projBR_X)
// fmt.Println("projY", projBL_Y, projTL_Y, projTR_Y, projBR_Y)
// // 3. Check if any point overlaps 'rect A' ranges
// rangeX := axisXVec.Len()
// rangeY := axisYVec.Len()
// fmt.Println("rangeX:", rangeX)
// fmt.Println("rangeY:", rangeY)
// // This is recalculated around the new Axis
// rectA := glm.R(0, 0, rangeX, rangeY)
// // TODO: It might be faster to reorganize the search to do one axis at a time? Idk
// // Check Rect B point BL
// if rectA.Contains(glm.Vec2{projBL_X, projBL_Y}) {
// return true
// }
// // Check Rect B point TL
// if rectA.Contains(glm.Vec2{projTL_X, projTL_Y}) {
// return true
// }
// // Check Rect B point TR
// if rectA.Contains(glm.Vec2{projTR_X, projTR_Y}) {
// return true
// }
// // Check Rect B point BR
// if rectA.Contains(glm.Vec2{projBR_X, projBR_Y}) {
// return true
// }
// return false
// }
package storage
import (
"encoding/json"
"maps"
"github.com/mitchellh/mapstructure"
)
func serialize(val any) ([]byte, error) {
valMap := make(map[string]any)
err := mapstructure.Decode(val, &valMap)
if err != nil {
return nil, err
}
buf, err := json.Marshal(valMap)
if err != nil {
return nil, err
}
return buf, nil
}
func deserialize[T any](dat []byte) (*T, error) {
var ret T
err := json.Unmarshal(dat, &ret)
if err != nil {
return nil, err
}
return &ret, nil
}
func deserializeWithDefault[T any](dat []byte, def T) (*T, error) {
defaultMap := make(map[string]any)
err := mapstructure.Decode(def, &defaultMap)
if err != nil {
return nil, err
}
decodedMap := make(map[string]any)
err = json.Unmarshal(dat, &decodedMap)
if err != nil {
return nil, err
}
maps.Copy(defaultMap, decodedMap)
var ret T
err = mapstructure.Decode(decodedMap, &ret)
if err != nil {
return nil, err
}
return &ret, nil
}
//go:build !js
package storage
import (
"os"
"path/filepath"
"runtime"
"runtime/pprof"
)
var storageRoot = ""
// Sets the root directory for local file storage
func SetStorageRoot(root string) error {
err := os.MkdirAll(root, 0700)
if err != nil {
return err
}
storageRoot = root
return nil
}
// Gets a copy of the item out of storage and returns a pointer to it. Else returns nil
// If there is no item we will return nil
// If there is an error getting or deserializing the item we will return (nil, error)
func GetItem[T any](key string) (*T, error) {
key = filepath.Join(storageRoot, key)
dat, err := os.ReadFile(key)
if err != nil {
return nil, err
}
return deserialize[T](dat)
}
func GetItemWithDefault[T any](key string, def T) (*T, error) {
key = filepath.Join(storageRoot, key)
dat, err := os.ReadFile(key)
if err != nil {
return nil, err
}
return deserializeWithDefault(dat, def)
}
func SetItem(key string, val any) error {
key = filepath.Join(storageRoot, key)
buf, err := serialize(val)
if err != nil {
return err
}
// Perm: Read/Write owner, nothing for anyone else
err = os.WriteFile(key, buf, 0600)
return err
}
func GetQueryString(key string) ([]string, error) {
return []string{}, nil
}
func WriteMemoryProfile(file string) error {
file = filepath.Join(storageRoot, file)
f, err := os.Create(file)
if err != nil {
return err
}
defer f.Close() // error handling omitted for example
runtime.GC() // get up-to-date statistics
if err := pprof.WriteHeapProfile(f); err != nil {
return err
}
return nil
}
func WriteCpuProfile(file string) (func(), error) {
file = filepath.Join(storageRoot, file)
f, err := os.Create(file)
if err != nil {
return func() {}, err
}
if err := pprof.StartCPUProfile(f); err != nil {
return func() {}, err
}
finisher := func() {
defer f.Close() // error handling omitted for example
pprof.StopCPUProfile()
}
return finisher, nil
}
package autotile
import (
"math"
"math/rand"
"github.com/unitoftime/flow/pgen"
"github.com/unitoftime/flow/tile"
)
type Pattern uint8
const (
Top Pattern = 0b00000001
Right = 0b00000010
Bottom = 0b00000100
Left = 0b00001000
)
func (p Pattern) Top() bool {
return (p & Top) == Top
}
func (p Pattern) Right() bool {
return (p & Right) == Right
}
func (p Pattern) Bottom() bool {
return (p & Bottom) == Bottom
}
func (p Pattern) Left() bool {
return (p & Left) == Left
}
type Tilemap[T any] interface {
GetTile(pos tile.TilePosition) (T, bool)
}
type Rule[T any] interface {
Execute(Tilemap[T], tile.TilePosition) int
ExecuteFull(T, T, T, T, T, T, T, T, T) int
}
// type RawEightRule[T any] struct {
// Top []T
// Bottom []T
// Left []T
// Right []T
// }
type BlobmapRule[T any] struct {
MatchCenter func(a, b T) bool
Match func(a, b T) bool
}
func (rule BlobmapRule[T]) ExecuteFull(m, t, b, l, r, tl, tr, bl, br T) int {
if rule.MatchCenter != nil {
if !rule.MatchCenter(m, m) {
return -1
}
} else {
if !rule.Match(m, m) {
return -1
}
}
pattern := PackedBlobmapNumber(
rule.Match(m, t),
rule.Match(m, b),
rule.Match(m, l),
rule.Match(m, r),
rule.Match(m, tl),
rule.Match(m, tr),
rule.Match(m, bl),
rule.Match(m, br),
)
return int(pattern)
}
func (rule BlobmapRule[T]) Execute(tilemap Tilemap[T], pos tile.TilePosition) int {
tile, ok := tilemap.GetTile(pos)
if !ok {
return -1
}
if !rule.Match(tile, tile) {
return -1
}
t, b, l, r, tl, tr, bl, br := getEightNeighbors(tilemap, pos)
pattern := PackedBlobmapNumber(
rule.Match(tile, t),
rule.Match(tile, b),
rule.Match(tile, l),
rule.Match(tile, r),
rule.Match(tile, tl),
rule.Match(tile, tr),
rule.Match(tile, bl),
rule.Match(tile, br),
)
return int(pattern)
}
type PipemapRule[T any] struct {
Match func(a, b T) bool
}
func (rule PipemapRule[T]) ExecuteFull(m, t, b, l, r, tl, tr, bl, br T) int {
if !rule.Match(m, m) {
return -1
}
pattern := PackedPipemapNumber(
rule.Match(m, t),
rule.Match(m, b),
rule.Match(m, l),
rule.Match(m, r),
)
return int(pattern)
}
func (rule PipemapRule[T]) Execute(tilemap Tilemap[T], pos tile.TilePosition) int {
tile, ok := tilemap.GetTile(pos)
if !ok {
return -1
}
if !rule.Match(tile, tile) {
return -1
}
t, b, l, r := getEdgeNeighbors(tilemap, pos)
pattern := PackedPipemapNumber(
rule.Match(tile, t),
rule.Match(tile, b),
rule.Match(tile, l),
rule.Match(tile, r),
)
return int(pattern)
}
type LambdaRule[T any] struct {
Func func(Pattern) int
Match func(T, T) bool
}
func (rule LambdaRule[T]) ExecuteFull(m, t, b, l, r, tl, tr, bl, br T) int {
if !rule.Match(m, m) {
return -1
}
pattern := PackedRawEightNumber(
rule.Match(m, t),
rule.Match(m, b),
rule.Match(m, l),
rule.Match(m, r),
rule.Match(m, tl),
rule.Match(m, tr),
rule.Match(m, bl),
rule.Match(m, br),
)
return rule.Func(Pattern(pattern))
}
func (rule LambdaRule[T]) Execute(tilemap Tilemap[T], pos tile.TilePosition) int {
tile, ok := tilemap.GetTile(pos)
if !ok {
return -1
}
if !rule.Match(tile, tile) {
return -1
}
t, b, l, r, tl, tr, bl, br := getEightNeighbors(tilemap, pos)
pattern := PackedRawEightNumber(
rule.Match(tile, t),
rule.Match(tile, b),
rule.Match(tile, l),
rule.Match(tile, r),
rule.Match(tile, tl),
rule.Match(tile, tr),
rule.Match(tile, bl),
rule.Match(tile, br),
)
return rule.Func(Pattern(pattern))
}
// type DualGridRule[T any] struct {
// Match func(a, b T) bool
// }
// func (rule DualGridRule[T]) Execute(tilemap Tilemap[T], pos tile.TilePosition) int {
// tile, ok := tilemap.GetTile(pos)
// if !ok {
// return -1
// }
// if !rule.Match(tile, tile) {
// return -1
// }
// t, b, l, r := getEdgeNeighbors(tilemap, pos)
// pattern := PackedPipemapNumber(
// rule.Match(tile, t),
// rule.Match(tile, b),
// rule.Match(tile, l),
// rule.Match(tile, r),
// )
// return int(pattern)
// }
type Set[T, S any] struct {
// mapping map[uint8][]int
// Rule func(Pattern)int
Rule Rule[T]
Tiles [][]S
VariantNoise *pgen.NoiseMap
}
func (s *Set[T, S]) GetFull(pos tile.Position, m, t, b, l, r, tl, tr, bl, br T) (S, bool) {
variant := s.Rule.ExecuteFull(m, t, b, l, r, tl, tr, bl, br)
if variant < 0 {
var ret S
return ret, false
}
// idx := rand.Intn(len(s.Tiles[variant]))
idx := 0
if s.VariantNoise == nil {
idx = rand.Intn(len(s.Tiles[variant]))
} else {
noise := s.VariantNoise.Get(pos.X, pos.Y)
idx = int(math.Floor(noise * float64(len(s.Tiles[variant]))))
}
return s.Tiles[variant][idx], true
}
func (s *Set[T, S]) Get(tilemap Tilemap[T], pos tile.TilePosition) (S, bool) {
variant := s.Rule.Execute(tilemap, pos)
if variant < 0 {
var ret S
return ret, false
}
idx := 0
if s.VariantNoise == nil {
idx = rand.Intn(len(s.Tiles[variant]))
} else {
noise := s.VariantNoise.Get(pos.X, pos.Y)
idx = int(math.Floor(noise * float64(len(s.Tiles[variant]))))
}
return s.Tiles[variant][idx], true
}
// func (s *Set[T]) Get(val Pattern) T {
// variant := s.Rule(val)
// idx := rand.Intn(len(s.Tiles[variant]))
// return s.Tiles[variant][idx]
// }
func getEdgeNeighbors[T any](tilemap Tilemap[T], pos tile.TilePosition) (T, T, T, T) {
t, _ := tilemap.GetTile(tile.TilePosition{pos.X, pos.Y + 1})
b, _ := tilemap.GetTile(tile.TilePosition{pos.X, pos.Y - 1})
l, _ := tilemap.GetTile(tile.TilePosition{pos.X - 1, pos.Y})
r, _ := tilemap.GetTile(tile.TilePosition{pos.X + 1, pos.Y})
return t, b, l, r
}
func getEightNeighbors[T any](tilemap Tilemap[T], pos tile.TilePosition) (T, T, T, T, T, T, T, T) {
t, _ := tilemap.GetTile(tile.TilePosition{pos.X, pos.Y + 1})
b, _ := tilemap.GetTile(tile.TilePosition{pos.X, pos.Y - 1})
l, _ := tilemap.GetTile(tile.TilePosition{pos.X - 1, pos.Y})
r, _ := tilemap.GetTile(tile.TilePosition{pos.X + 1, pos.Y})
tl, _ := tilemap.GetTile(tile.TilePosition{pos.X - 1, pos.Y + 1})
tr, _ := tilemap.GetTile(tile.TilePosition{pos.X + 1, pos.Y + 1})
bl, _ := tilemap.GetTile(tile.TilePosition{pos.X - 1, pos.Y - 1})
br, _ := tilemap.GetTile(tile.TilePosition{pos.X + 1, pos.Y - 1})
return t, b, l, r, tl, tr, bl, br
}
func PackedRawEightNumber(t, b, l, r, tl, tr, bl, br bool) uint8 {
total := uint8(0)
if t {
total += (1 << 0)
}
if r {
total += (1 << 1)
}
if b {
total += (1 << 2)
}
if l {
total += (1 << 3)
}
if tr {
total += (1 << 4)
}
if tl {
total += (1 << 5)
}
if br {
total += (1 << 6)
}
if bl {
total += (1 << 7)
}
return total
}
package autotile
// Inspired By: http://www.cr31.co.uk/stagecast/wang/1sideedge.html
// Follows the tile numbering that godot uses. Numbered in reading order, starting at 0.
// Godot: https://docs.godotengine.org/en/stable/tutorials/2d/using_tilemaps.html
func PackedBlobmapNumber(t, b, l, r, tl, tr, bl, br bool) uint8 {
wang := WangBlobmapNumber(t, b, l, r, tl, tr, bl, br)
// Default blank
ret := 22
switch wang {
case 0:
ret = 22
case 1:
ret = 24
case 4:
ret = 37
case 16:
ret = 0
case 64:
ret = 39
case 5:
ret = 25
case 20:
ret = 1
case 80:
ret = 3
case 65:
ret = 27
case 7:
ret = 44
case 28:
ret = 8
case 112:
ret = 11
case 193:
ret = 47
case 17:
ret = 12
case 68:
ret = 38
case 21:
ret = 13
case 84:
ret = 2
case 81:
ret = 15
case 69:
ret = 26
case 23:
ret = 28
case 92:
ret = 5
case 113:
ret = 19
case 197:
ret = 42
case 29:
ret = 16
case 116:
ret = 6
case 209:
ret = 31
case 71:
ret = 41
case 31:
ret = 20
case 124:
ret = 10
case 241:
ret = 35
case 199:
ret = 45
case 85:
ret = 14
case 87:
ret = 7
case 93:
ret = 43
case 117:
ret = 40
case 219:
ret = 4
case 95:
ret = 32
case 125:
ret = 9
case 245:
ret = 23
case 215:
ret = 46
case 119:
ret = 21
case 221:
ret = 34
case 127:
ret = 17
case 253:
ret = 18
case 247:
ret = 30
case 223:
ret = 29
case 213:
ret = 4
case 255:
ret = 33
}
return uint8(ret)
}
// This function computes the wang tilenumber of a tile based on the tiles around it
func WangBlobmapNumber(t, b, l, r, tl, tr, bl, br bool) uint8 {
// If surrounding edges aren't set, then corners must be false
if !(t && l) {
tl = false
}
if !(t && r) {
tr = false
}
if !(b && l) {
bl = false
}
if !(b && r) {
br = false
}
total := uint8(0)
if t {
total += (1 << 0)
}
if tr {
total += (1 << 1)
}
if r {
total += (1 << 2)
}
if br {
total += (1 << 3)
}
if b {
total += (1 << 4)
}
if bl {
total += (1 << 5)
}
if l {
total += (1 << 6)
}
if tl {
total += (1 << 7)
}
return total
}
func PackedPipemapNumber(t, b, l, r bool) uint8 {
total := uint8(0)
if t {
total += (1 << 0)
}
if r {
total += (1 << 1)
}
if b {
total += (1 << 2)
}
if l {
total += (1 << 3)
}
return total
}
package tile
import (
"github.com/unitoftime/flow/glm"
"github.com/unitoftime/flow/phy2"
)
// TODO
// tile.Chunk?
// tile.Chunkmap?
// chunk.Chunk?
// chunk.Map?
type Chunk[T any] struct {
TileSize [2]int // In pixels
tiles [][]T
math FlatRectMath
Offset glm.Vec2 // In world space positioning
TileOffset Position
}
func NewChunk[T any](tiles [][]T, tileSize [2]int, math FlatRectMath) *Chunk[T] {
return &Chunk[T]{
TileSize: tileSize,
tiles: tiles,
math: math,
Offset: glm.Vec2{},
}
}
// This returns the underlying array, not a copy
// TODO - should I just make tiles public?
func (t *Chunk[T]) Tiles() [][]T {
return t.tiles
}
func (t *Chunk[T]) Width() int {
return len(t.tiles)
}
func (t *Chunk[T]) Height() int {
// TODO - Assumes the tilemap is a square and is larger than size 0
return len(t.tiles[0])
}
// func (t *Chunk[T]) Bounds() Rect {
// min := t.PositionToTile(0, 0)
// return R(
// min.X,
// min.Y,
// min.X + t.Width(),
// min.Y + t.Height(),
// )
// }
func (t *Chunk[T]) Get(pos TilePosition) (T, bool) {
if pos.X < 0 || pos.X >= len(t.tiles) || pos.Y < 0 || pos.Y >= len(t.tiles[pos.X]) {
var ret T
return ret, false
}
return t.tiles[pos.X][pos.Y], true
}
func (t *Chunk[T]) unsafeGet(pos TilePosition) T {
return t.tiles[pos.X][pos.Y]
}
func (t *Chunk[T]) Set(pos TilePosition, tile T) bool {
if pos.X < 0 || pos.X >= len(t.tiles) || pos.Y < 0 || pos.Y >= len(t.tiles[pos.X]) {
return false
}
t.tiles[pos.X][pos.Y] = tile
return true
}
func (t *Chunk[T]) TileToPosition(tilePos TilePosition) (float64, float64) {
x, y := t.math.Position(tilePos.X, tilePos.Y)
return (x + t.Offset.X), (y + t.Offset.Y)
}
func (t *Chunk[T]) PositionToTile(x, y float64) TilePosition {
x -= t.Offset.X
y -= t.Offset.Y
tX, tY := t.math.PositionToTile(x, y)
return TilePosition{tX, tY}
}
func (t *Chunk[T]) GetEdgeNeighbors(x, y int) []TilePosition {
return []TilePosition{
TilePosition{x + 1, y},
TilePosition{x - 1, y},
TilePosition{x, y + 1},
TilePosition{x, y - 1},
}
}
// TODO - this might not work for pointy-top tilemaps
func (t *Chunk[T]) BoundsAt(pos TilePosition) (float64, float64, float64, float64) {
x, y := t.TileToPosition(pos)
return float64(x) - float64(t.TileSize[0]/2), float64(y) - float64(t.TileSize[1]/2), float64(x) + float64(t.TileSize[0]/2), float64(y) + float64(t.TileSize[1]/2)
}
// Adds an entity to the chunk
// func (t *Chunk[T]) AddEntity(id ecs.Id, collider *Collider, pos *glm.Pos) {
// tilePos := t.PositionToTile(float32(pos.X), float32(pos.Y))
// for x := tilePos.X; x < tilePos.X + collider.Width; x++ {
// for y := tilePos.Y; y < tilePos.Y + collider.Height; y++ {
// // TODO - Just using this as a bounds check
// tile, ok := t.Get(TilePosition{x, y})
// if ok {
// t.tiles[x][y].Entity = id // Store the entity
// }
// }
// }
// }
// func (t *Chunk[T]) ClearEntities() {
// // Clear Entities
// for x := range t.tiles {
// for y := range t.tiles[x] {
// t.tiles[x][y].Entity = ecs.InvalidEntity
// }
// }
// }
// // Recalculates all of the entities that are on tiles based on tile colliders
// func (t *Chunk[T]) RecalculateEntities(world *ecs.World) {
// t.ClearEntities()
// // Recompute all entities with TileColliders
// ecs.Map2(world, func(id ecs.Id, collider *Collider, pos *glm.Pos) {
// tilePos := t.PositionToTile(float32(pos.X), float32(pos.Y))
// for x := tilePos.X; x < tilePos.X + collider.Width; x++ {
// for y := tilePos.Y; y < tilePos.Y + collider.Height; y++ {
// t.tiles[x][y].Entity = id // Store the entity
// }
// }
// })
// }
// Returns a list of tiles that are overlapping the collider at a position
func (t *Chunk[T]) GetOverlappingTiles(x, y float64, collider *phy2.CircleCollider) []TilePosition {
minX := x - collider.Radius
maxX := x + collider.Radius
minY := y - collider.Radius
maxY := y + collider.Radius
min := t.PositionToTile(minX, minY)
max := t.PositionToTile(maxX, maxY)
ret := make([]TilePosition, 0)
for tx := min.X; tx <= max.X; tx++ {
for ty := min.Y; ty <= max.Y; ty++ {
ret = append(ret, TilePosition{tx, ty})
}
}
return ret
}
package tile
import (
// "fmt"
"iter"
"github.com/unitoftime/flow/glm"
"github.com/unitoftime/flow/phy2"
"github.com/unitoftime/intmap"
"github.com/zyedidia/generic/queue"
)
// type ChunkLoader[T any] interface {
// LoadChunk(chunkPos ChunkPosition) ([][]T, error)
// SaveChunk(chunkmap *Chunkmap[T], chunkPos ChunkPosition) error
// }
type ChunkPosition struct {
X, Y int16
}
func (c *ChunkPosition) hash() uint32 {
return (uint32(uint16(c.X)) << 16) | uint32(uint16(c.Y))
}
func fromHash(hash uint32) ChunkPosition {
return ChunkPosition{
X: int16(uint16((hash >> 16) & 0xFFFF)),
Y: int16(uint16(hash & 0xFFFF)),
}
}
type Chunkmap[T any] struct {
ChunkMath
// chunks map[ChunkPosition]*Chunk[T]
chunks *intmap.Map[uint32, *Chunk[T]]
}
func NewChunkmap[T any](math ChunkMath) *Chunkmap[T] {
return &Chunkmap[T]{
ChunkMath: math,
// chunks: make(map[ChunkPosition]*Chunk[T]),
chunks: intmap.New[uint32, *Chunk[T]](0),
}
}
// func(c *Chunkmap[T]) SetLoader(loader ChunkLoader[T]) *Chunkmap[T] {
// c.loader = loader
// return c
// }
// TODO - It might be cool to have a function which returns a rectangle of chunks as a list (To automatically cull out sections we don't want)
func (c *Chunkmap[T]) GetAllChunks() []*Chunk[T] {
ret := make([]*Chunk[T], 0, c.NumChunks())
// for _, chunk := range c.chunks {
c.chunks.ForEach(func(_ uint32, chunk *Chunk[T]) {
ret = append(ret, chunk)
})
return ret
}
func (c *Chunkmap[T]) GetAllChunkPositions() []ChunkPosition {
ret := make([]ChunkPosition, 0, c.NumChunks())
c.chunks.ForEach(func(chunkHash uint32, chunk *Chunk[T]) {
chunkPos := fromHash(chunkHash)
// for chunkPos := range c.chunks {
ret = append(ret, chunkPos)
})
return ret
}
func (c *Chunkmap[T]) Bounds() Rect {
var bounds Rect
i := 0
c.chunks.ForEach(func(chunkHash uint32, chunk *Chunk[T]) {
chunkPos := fromHash(chunkHash)
// for chunkPos := range c.chunks {
if i == 0 {
bounds = c.GetChunkTileRect(chunkPos)
} else {
bounds = bounds.Union(c.GetChunkTileRect(chunkPos))
}
i++
})
return bounds
// var bounds Rect
// i := 0
// for _, chunk := range c.chunks {
// if i == 0 {
// bounds = chunk.Bounds()
// } else {
// bounds = bounds.Union(chunk.Bounds())
// }
// i++
// }
// return bounds
}
func (c *Chunkmap[T]) GetChunk(chunkPos ChunkPosition) (*Chunk[T], bool) {
chunk, ok := c.chunks.Get(chunkPos.hash())
if ok {
return chunk, true
}
return nil, false
}
// This generates a chunk based on the passed in expansionLambda
func (c *Chunkmap[T]) GenerateChunk(chunkPos ChunkPosition, expansionLambda func(x, y int) T) *Chunk[T] {
chunk, ok := c.GetChunk(chunkPos)
if ok {
return chunk // Return the chunk and don't create, if the chunk is already made
}
tileOffset := c.ChunkToTile(chunkPos)
tiles := make([][]T, c.ChunkMath.chunkmath.size[0], c.ChunkMath.chunkmath.size[1])
for x := range tiles {
tiles[x] = make([]T, c.ChunkMath.chunkmath.size[0], c.ChunkMath.chunkmath.size[1])
for y := range tiles[x] {
// fmt.Println(x, y, tileOffset.X, tileOffset.Y)
if expansionLambda != nil {
tiles[x][y] = expansionLambda(x+tileOffset.X, y+tileOffset.Y)
}
}
}
return c.AddChunk(chunkPos, tiles)
}
// func (c *Chunkmap[T]) SaveChunk(chunkPos ChunkPosition) error {
// if c.loader == nil {
// return fmt.Errorf("Chunkmap loader is nil")
// }
// // TODO - I feel like I need some way to dump a chunk out of memory. like, SaveCHunk(...), then RemoveFromMemory(...) - OR - PersistChunk (...) which just does both of those
// return c.loader.SaveChunk(c, chunkPos)
// }
func (c *Chunkmap[T]) AddChunk(chunkPos ChunkPosition, tiles [][]T) *Chunk[T] {
chunk := NewChunk[T](tiles, c.ChunkMath.tilemath.size, c.ChunkMath.tilemath)
// offX, offY := c.ChunkMath.math.Position(int(chunkPos.X), int(chunkPos.Y),
// [2]int{c.ChunkMath.tileSize[0]*c.ChunkMath.chunkSize[0], c.ChunkMath.tileSize[1]*c.ChunkMath.chunkSize[1]})
// chunk.Offset.X = float64(offX)
// chunk.Offset.Y = float64(offY)
chunk.Offset = c.ChunkMath.ToPosition(chunkPos)
chunk.TileOffset = c.ChunkToTile(chunkPos)
// Write back
// c.chunks[chunkPos] = chunk
c.chunks.Put(chunkPos.hash(), chunk)
return chunk
}
func (c *Chunkmap[T]) NumChunks() int {
return c.chunks.Len()
// return len(c.chunks)
}
func (c *Chunkmap[T]) GetTile(pos TilePosition) (T, bool) {
chunkPos := c.TileToChunk(pos)
chunk, ok := c.GetChunk(chunkPos)
if !ok {
var ret T
return ret, false
}
localTilePos := TilePosition{pos.X - chunk.TileOffset.X, pos.Y - chunk.TileOffset.Y}
return chunk.unsafeGet(localTilePos), true
}
// Adds a tile at the position, if the chunk doesnt exist, then it will be created
func (c *Chunkmap[T]) AddTile(pos TilePosition, tile T) {
success := c.SetTile(pos, tile)
if !success {
// TODO: in my game, I have the default T value being an empty tile, but it might be nice to modify Chunkmap struct to have a 'defaultTile' or something that people can use to fill blank spaces
chunkPos := c.TileToChunk(pos)
c.GenerateChunk(chunkPos, nil)
success := c.SetTile(pos, tile)
if !success {
panic("programmer error")
}
}
}
// Tries to set the tile, returns false if the chunk does not exist
func (c *Chunkmap[T]) SetTile(pos TilePosition, tile T) bool {
chunkPos := c.TileToChunk(pos)
chunk, ok := c.GetChunk(chunkPos)
if !ok {
return false
}
tileOffset := c.ChunkToTile(chunkPos)
localTilePos := TilePosition{pos.X - tileOffset.X, pos.Y - tileOffset.Y}
// fmt.Println("chunk.Get:", chunkPos, pos, localTilePos)
ok = chunk.Set(localTilePos, tile)
if !ok {
panic("Programmer error")
}
return true
}
func (c *Chunkmap[T]) GetEightNeighbors(pos TilePosition) (T, T, T, T, T, T, T, T) {
t, _ := c.GetTile(Position{pos.X, pos.Y + 1})
b, _ := c.GetTile(Position{pos.X, pos.Y - 1})
l, _ := c.GetTile(Position{pos.X - 1, pos.Y})
r, _ := c.GetTile(Position{pos.X + 1, pos.Y})
tl, _ := c.GetTile(Position{pos.X - 1, pos.Y + 1})
tr, _ := c.GetTile(Position{pos.X + 1, pos.Y + 1})
bl, _ := c.GetTile(Position{pos.X - 1, pos.Y - 1})
br, _ := c.GetTile(Position{pos.X + 1, pos.Y - 1})
return t, b, l, r, tl, tr, bl, br
}
func (c *Chunkmap[T]) GetNeighborsAtDistance(tilePos TilePosition, dist int) []TilePosition {
distance := make(map[TilePosition]int)
q := queue.New[TilePosition]()
q.Enqueue(tilePos)
for !q.Empty() {
current := q.Dequeue()
d := distance[current]
if d >= dist {
continue
} // Don't need to search past our limit
neighbors := c.GetEdgeNeighbors(current.X, current.Y)
for _, next := range neighbors {
_, ok := c.GetTile(next)
if !ok {
continue
} // Skip as neighbor doesn't actually exist (ie could be OOB)
// If we haven't already walked over this neighbor, then enqueue it and add it to our path
_, exists := distance[next]
if !exists {
q.Enqueue(next)
distance[next] = 1 + distance[current]
}
}
}
// Pull out all of the tiles that are at the correct distance
ret := make([]TilePosition, 0)
for pos, d := range distance {
if d != dist {
continue
} // Don't return if distance isn't corect
ret = append(ret, pos)
}
return ret
}
func (c *Chunkmap[T]) BreadthFirstSearch(tilePos TilePosition, valid func(t T) bool) []TilePosition {
distance := make(map[TilePosition]int)
q := queue.New[TilePosition]()
q.Enqueue(tilePos)
for !q.Empty() {
current := q.Dequeue()
neighbors := c.GetEdgeNeighbors(current.X, current.Y)
for _, next := range neighbors {
t, ok := c.GetTile(next)
if !ok {
continue
} // Skip as neighbor doesn't actually exist (ie could be OOB)
if !valid(t) {
continue
} // Skip if the tile isn't valid
// If we haven't already walked over this neighbor, then enqueue it and add it to our path
_, exists := distance[next]
if !exists {
q.Enqueue(next)
distance[next] = 1 + distance[current]
}
}
}
// Pull out all of the tiles that are at the correct distance
ret := make([]TilePosition, 0)
for pos := range distance {
ret = append(ret, pos)
}
return ret
}
func (c *Chunkmap[T]) IterBreadthFirst(tilePos TilePosition, valid func(t T) bool) iter.Seq[Position] {
return func(yield func(Position) bool) {
distance := make(map[TilePosition]int)
q := queue.New[TilePosition]()
q.Enqueue(tilePos)
for !q.Empty() {
current := q.Dequeue()
if !yield(current) {
break
}
neighbors := c.GetEdgeNeighbors(current.X, current.Y)
for _, next := range neighbors {
t, ok := c.GetTile(next)
if !ok {
continue
} // Skip as neighbor doesn't actually exist (ie could be OOB)
if !valid(t) {
continue
} // Skip if the tile isn't valid
// If we haven't already walked over this neighbor, then enqueue it and add it to our path
_, exists := distance[next]
if !exists {
q.Enqueue(next)
distance[next] = 1 + distance[current]
}
}
}
}
}
func (c *Chunkmap[T]) GetPerimeter() map[ChunkPosition]bool {
perimeter := make(map[ChunkPosition]bool) // List of chunkPositions that are the perimeter
processed := make(map[ChunkPosition]bool) // List of chunkPositions that we've already processed
// Just start at some random chunkPosition (whichever is first)
var start ChunkPosition
// TODO: originally this function would just use the first in the for loop. but we cant break out of a lambda func
c.chunks.ForEach(func(chunkHash uint32, chunk *Chunk[T]) {
chunkPos := fromHash(chunkHash)
// for chunkPos := range c.chunks {
start = chunkPos
// break
})
q := queue.New[ChunkPosition]()
q.Enqueue(start)
for !q.Empty() {
current := q.Dequeue()
neighbors := c.GetChunkEdgeNeighbors(current)
for _, next := range neighbors {
_, ok := c.GetChunk(next)
if ok {
// If the chunk's neighbor exists, then add it and keep processing
_, alreadyProcessed := processed[next]
if !alreadyProcessed {
q.Enqueue(next)
processed[next] = true
}
continue
}
perimeter[next] = true
}
}
return perimeter
}
func (c *Chunkmap[T]) CalculateBlobmapVariant(pos TilePosition, same func(a T, b T) bool) uint8 {
tile, ok := c.GetTile(pos)
if !ok {
return 0
}
t, _ := c.GetTile(TilePosition{pos.X, pos.Y + 1})
b, _ := c.GetTile(TilePosition{pos.X, pos.Y - 1})
l, _ := c.GetTile(TilePosition{pos.X - 1, pos.Y})
r, _ := c.GetTile(TilePosition{pos.X + 1, pos.Y})
tl, _ := c.GetTile(TilePosition{pos.X - 1, pos.Y + 1})
tr, _ := c.GetTile(TilePosition{pos.X + 1, pos.Y + 1})
bl, _ := c.GetTile(TilePosition{pos.X - 1, pos.Y - 1})
br, _ := c.GetTile(TilePosition{pos.X + 1, pos.Y - 1})
return PackedBlobmapNumber(
same(tile, t),
same(tile, b),
same(tile, l),
same(tile, r),
same(tile, tl),
same(tile, tr),
same(tile, bl),
same(tile, br),
)
}
func (c *Chunkmap[T]) CalculatePipemapVariant(pos TilePosition, same func(a T, b T) bool) uint8 {
tile, ok := c.GetTile(pos)
if !ok {
return 0
}
t, _ := c.GetTile(TilePosition{pos.X, pos.Y + 1})
b, _ := c.GetTile(TilePosition{pos.X, pos.Y - 1})
l, _ := c.GetTile(TilePosition{pos.X - 1, pos.Y})
r, _ := c.GetTile(TilePosition{pos.X + 1, pos.Y})
return PackedPipemapNumber(
same(tile, t),
same(tile, b),
same(tile, l),
same(tile, r),
)
}
func (c *Chunkmap[T]) CalculateRawEightVariant(pos TilePosition, same func(a T, b T) bool) uint8 {
tile, ok := c.GetTile(pos)
if !ok {
return 0
}
t, _ := c.GetTile(TilePosition{pos.X, pos.Y + 1})
b, _ := c.GetTile(TilePosition{pos.X, pos.Y - 1})
l, _ := c.GetTile(TilePosition{pos.X - 1, pos.Y})
r, _ := c.GetTile(TilePosition{pos.X + 1, pos.Y})
tl, _ := c.GetTile(TilePosition{pos.X - 1, pos.Y + 1})
tr, _ := c.GetTile(TilePosition{pos.X + 1, pos.Y + 1})
bl, _ := c.GetTile(TilePosition{pos.X - 1, pos.Y - 1})
br, _ := c.GetTile(TilePosition{pos.X + 1, pos.Y - 1})
return PackedRawEightNumber(
same(tile, t),
same(tile, b),
same(tile, l),
same(tile, r),
same(tile, tl),
same(tile, tr),
same(tile, bl),
same(tile, br),
)
}
// --------------------------------------------------------------------------------
// - Math functions
// --------------------------------------------------------------------------------
type ChunkMath struct {
// chunkSize [2]int
// chunkSizeOver2 [2]int
// chunkDiv [2]int
// tileSize [2]int
// tileSizeOver2 [2]int
// tileDiv [2]int
// math FlatRectMath
globalmath FlatRectMath
chunkmath FlatRectMath
tilemath FlatRectMath
}
func NewChunkmath(chunkSize int, tileSize int) ChunkMath {
return ChunkMath{
globalmath: NewFlatRectMath([2]int{tileSize * chunkSize, tileSize * chunkSize}),
chunkmath: NewFlatRectMath([2]int{chunkSize, chunkSize}),
tilemath: NewFlatRectMath([2]int{tileSize, tileSize}),
}
// chunkDiv := int(math.Log2(float64(chunkSize)))
// if (1 << chunkDiv) != chunkSize {
// panic("Chunk maps must have a chunksize that is a power of 2!")
// }
// tileDiv := int(math.Log2(float64(tileSize)))
// if (1 << tileDiv) != tileSize {
// panic("Chunk maps must have a tileSize that is a power of 2!")
// }
// return ChunkMath{
// chunkSize: [2]int{chunkSize, chunkSize},
// chunkDiv: [2]int{chunkDiv, chunkDiv},
// chunkSizeOver2: [2]int{chunkSize/2, chunkSize/2},
// tileSize: [2]int{tileSize, tileSize},
// tileSizeOver2: [2]int{tileSize/2, tileSize/2},
// tileDiv: [2]int{tileDiv, tileDiv},
// math: FlatRectMath{},
// }
}
// Returns the worldspace position of a chunk
func (c *ChunkMath) ToPosition(chunkPos ChunkPosition) glm.Vec2 {
offX, offY := c.globalmath.Position(int(chunkPos.X), int(chunkPos.Y))
// offX, offY := c.math.Position(int(chunkPos.X), int(chunkPos.Y),
// [2]int{c.tileSize[0]*c.chunkSize[0], c.tileSize[1]*c.chunkSize[1]})
offset := glm.Vec2{
X: float64(offX),
// Y: float64(offY) - (0.5 * float64(c.chunkSize[1]) * float64(c.tileSize[1])) + float64(c.tileSize[1]/2),
// Y: float64(offY) - float64(c.chunkSizeOver2[1] * c.tileSize[1]) + float64(c.tileSizeOver2[1]),
Y: float64(offY) - float64(c.chunkmath.sizeOver2[1]*c.tilemath.size[1]) + float64(c.tilemath.sizeOver2[1]),
}
return offset
}
// Note: untested
func (c *ChunkMath) TileToChunkLocalPosition(tilePos Position) glm.Vec2 {
chunkPos := c.TileToChunk(tilePos)
offsetPos := c.ToPosition(chunkPos)
pos := c.TileToPosition(tilePos)
return pos.Sub(offsetPos)
}
func (c *ChunkMath) PositionToChunk(x, y float64) ChunkPosition {
return c.TileToChunk(c.PositionToTile(x, y))
}
func (c *ChunkMath) TileToChunk(tilePos TilePosition) ChunkPosition {
xPos := tilePos.X >> c.chunkmath.div[0]
yPos := tilePos.Y >> c.chunkmath.div[1]
return ChunkPosition{int16(xPos), int16(yPos)}
// xPos := (int(tilePos.X) + c.chunkmath.sizeOver2[0]) >> c.chunkmath.div[0]
// yPos := (int(tilePos.Y) + c.chunkmath.sizeOver2[1]) >> c.chunkmath.div[1]
// // Adjust for negatives
// if tilePos.X < -c.chunkmath.sizeOver2[0] {
// xPos -= 1
// }
// if tilePos.Y < -c.chunkmath.sizeOver2[1] {
// yPos -= 1
// }
// return ChunkPosition{int16(xPos), int16(yPos)}
// if tilePos.X < 0 {
// tilePos.X -= (c.chunkmath.size[0] - 1)
// }
// if tilePos.Y < 0 {
// tilePos.Y -= (c.chunkmath.size[1] - 1)
// }
// chunkX := tilePos.X / c.chunkmath.size[0]
// chunkY := tilePos.Y / c.chunkmath.size[1]
// // chunkX := tilePos.X >> c.chunkmath.div[0]
// // chunkY := tilePos.Y >> c.chunkmath.div[1]
// return ChunkPosition{int16(chunkX), int16(chunkY)}
}
// Returns the center tile of a chunk
func (c *ChunkMath) ChunkToTile(chunkPos ChunkPosition) TilePosition {
tileX := int(chunkPos.X) * c.chunkmath.size[0]
tileY := int(chunkPos.Y) * c.chunkmath.size[1]
tilePos := TilePosition{tileX, tileY}
return tilePos
}
func (c *ChunkMath) TileToPosition(tilePos TilePosition) glm.Vec2 {
x, y := c.tilemath.Position(tilePos.X, tilePos.Y)
return glm.Vec2{x, y}
}
func (c *ChunkMath) PositionToTile(x, y float64) TilePosition {
tX, tY := c.tilemath.PositionToTile(x, y)
return TilePosition{tX, tY}
}
func (c *ChunkMath) PositionToTile2(pos glm.Vec2) TilePosition {
tX, tY := c.tilemath.PositionToTile(pos.X, pos.Y)
return TilePosition{tX, tY}
}
func (c *ChunkMath) GetChunkTileRect(chunkPos ChunkPosition) Rect {
center := c.ChunkToTile(chunkPos)
return R(
center.X,
center.Y,
center.X+(c.chunkmath.size[0])-1,
center.Y+(c.chunkmath.size[1])-1,
)
}
// // Returns the worldspace position of a chunk
// func (c *ChunkMath) ToPosition(chunkPos ChunkPosition) glm.Vec22 {
// offX, offY := c.math.Position(int(chunkPos.X), int(chunkPos.Y),
// [2]int{c.tileSize[0]*c.chunkSize[0], c.tileSize[1]*c.chunkSize[1]})
// offset := glm.Vec22{
// X: float64(offX),
// // Y: float64(offY) - (0.5 * float64(c.chunkSize[1]) * float64(c.tileSize[1])) + float64(c.tileSize[1]/2),
// Y: float64(offY) - float64(c.chunkSizeOver2[1] * c.tileSize[1]) + float64(c.tileSizeOver2[1]),
// }
// return offset
// }
// //Note: untested
// func (c *ChunkMath) TileToChunkLocalPosition(tilePos Position) glm.Pos {
// chunkPos := c.TileToChunk(tilePos)
// offsetPos := c.ToPosition(chunkPos)
// pos := c.TileToPosition(tilePos)
// return pos.Sub(glm.Pos(offsetPos))
// }
// func (c *ChunkMath) PositionToChunk(x, y float64) ChunkPosition {
// return c.TileToChunk(c.PositionToTile(x, y))
// }
// func (c *ChunkMath) TileToChunk(tilePos TilePosition) ChunkPosition {
// if tilePos.X < 0 {
// tilePos.X -= (c.chunkSize[0] - 1)
// }
// if tilePos.Y < 0 {
// tilePos.Y -= (c.chunkSize[1] - 1)
// }
// chunkX := tilePos.X / c.chunkSize[0]
// chunkY := tilePos.Y / c.chunkSize[1]
// return ChunkPosition{int16(chunkX), int16(chunkY)}
// }
// // Returns the center tile of a chunk
// func (c *ChunkMath) ChunkToTile(chunkPos ChunkPosition) TilePosition {
// tileX := int(chunkPos.X) * c.chunkSize[0]
// tileY := int(chunkPos.Y) * c.chunkSize[1]
// tilePos := TilePosition{tileX, tileY}
// return tilePos
// }
// func (c *ChunkMath) TileToPosition(tilePos TilePosition) glm.Pos {
// x, y := c.math.Position(tilePos.X, tilePos.Y, c.tileSize)
// return glm.Pos{x, y}
// }
// func (c *ChunkMath) PositionToTile(x, y float64) TilePosition {
// tX, tY := c.math.PositionToTile(x, y, c.tileSize)
// return TilePosition{tX, tY}
// }
// func (c *ChunkMath) PositionToTile2(pos glm.Vec2) TilePosition {
// tX, tY := c.math.PositionToTile(pos.X, pos.Y, c.tileSize)
// return TilePosition{tX, tY}
// }
// func (c *ChunkMath) GetChunkTileRect(chunkPos ChunkPosition) Rect {
// center := c.ChunkToTile(chunkPos)
// return R(
// center.X,
// center.Y,
// center.X + (c.chunkSize[0]) - 1,
// center.Y + (c.chunkSize[1]) - 1,
// )
// }
func (c *ChunkMath) WorldToTileRect(r glm.Rect) Rect {
min := c.PositionToTile2(r.Min)
max := c.PositionToTile2(r.Max)
return Rect{min, max}
}
// Returns a rect including all of the tiles.
// Centered on edge tiles
func (c *ChunkMath) RectToWorldRect(r Rect) glm.Rect {
min := c.TileToPosition(r.Min)
max := c.TileToPosition(r.Max)
return glm.Rect{glm.Vec2(min), glm.Vec2(max)}
}
func (c *ChunkMath) GetEdgeNeighbors(x, y int) []TilePosition {
return []TilePosition{
TilePosition{x + 1, y},
TilePosition{x - 1, y},
TilePosition{x, y + 1},
TilePosition{x, y - 1},
}
}
func (c *ChunkMath) GetNeighbors(pos TilePosition) []TilePosition {
x := pos.X
y := pos.Y
return []TilePosition{
// Edges
TilePosition{x + 1, y},
TilePosition{x - 1, y},
TilePosition{x, y + 1},
TilePosition{x, y - 1},
// Corners
TilePosition{x - 1, y - 1},
TilePosition{x - 1, y + 1},
TilePosition{x + 1, y - 1},
TilePosition{x + 1, y + 1},
}
}
func (c *ChunkMath) GetChunkEdgeNeighbors(pos ChunkPosition) []ChunkPosition {
return []ChunkPosition{
{pos.X + 1, pos.Y},
{pos.X - 1, pos.Y},
{pos.X, pos.Y + 1},
{pos.X, pos.Y - 1},
}
}
func (c *ChunkMath) GetChunkNeighbors(pos ChunkPosition) []ChunkPosition {
return []ChunkPosition{
// Edge
{pos.X + 1, pos.Y},
{pos.X - 1, pos.Y},
{pos.X, pos.Y + 1},
{pos.X, pos.Y - 1},
// Corners
{pos.X + 1, pos.Y + 1},
{pos.X - 1, pos.Y + 1},
{pos.X - 1, pos.Y - 1},
{pos.X + 1, pos.Y - 1},
}
}
// Returns a list of tiles that are overlapping the collider at a position
func (t *ChunkMath) GetOverlappingTiles(x, y float64, collider *phy2.CircleCollider) []TilePosition {
minX := x - collider.Radius
maxX := x + collider.Radius
minY := y - collider.Radius
maxY := y + collider.Radius
min := t.PositionToTile(minX, minY)
max := t.PositionToTile(maxX, maxY)
ret := make([]TilePosition, 0)
for tx := min.X; tx <= max.X; tx++ {
for ty := min.Y; ty <= max.Y; ty++ {
ret = append(ret, TilePosition{tx, ty})
}
}
return ret
}
func (t *ChunkMath) GetOverlappingTiles2(ret []TilePosition, x, y float64, radius float64) []TilePosition {
minX := x - float64(radius)
maxX := x + float64(radius)
minY := y - float64(radius)
maxY := y + float64(radius)
min := t.PositionToTile(minX, minY)
max := t.PositionToTile(maxX, maxY)
ret = ret[:0]
for tx := min.X; tx <= max.X; tx++ {
for ty := min.Y; ty <= max.Y; ty++ {
ret = append(ret, TilePosition{tx, ty})
}
}
return ret
}
package tile
import (
"math"
)
type Math interface {
Position(x, y int, size [2]int) (float64, float64)
PositionToTile(x, y float64, size [2]int) (int, int)
}
type FlatRectMath struct {
size [2]int
sizeOver2 [2]int
div [2]int
}
func NewFlatRectMath(size [2]int) FlatRectMath {
divX := int(math.Log2(float64(size[0])))
divY := int(math.Log2(float64(size[1])))
if (1<<divX) != size[0] || (1<<divY) != size[1] {
panic("Tile maps must have a chunksize and tilesize that is a power of 2!")
}
return FlatRectMath{
size: size,
sizeOver2: [2]int{size[0] / 2, size[1] / 2},
div: [2]int{divX, divY},
}
}
func (m FlatRectMath) Position(x, y int) (float64, float64) {
return float64(x * m.size[0]), float64(y * m.size[1])
}
func (m FlatRectMath) PositionToTile(x, y float64) (int, int) {
// xPos := (int(x) + (m.sizeOver2[0])) / size[0]
// yPos := (int(y) + (m.sizeOver2[1])) / size[1]
xPos := (int(x) + m.sizeOver2[0]) >> m.div[0]
yPos := (int(y) + m.sizeOver2[1]) >> m.div[1]
// xPos := (int(x) + m.sizeOver2[0]) >> m.div[0]
// yPos := (int(y) + m.sizeOver2[1]) >> m.div[1]
// // Adjust for negatives
// if x < float64(-m.sizeOver2[0]) {
// xPos -= 1
// }
// if y < float64(-m.sizeOver2[1]) {
// yPos -= 1
// }
return xPos, yPos
// return (int(x) + (size[0]/2)) / size[0],
// (int(y) + (size[1]/2))/ size[1]
}
// func (t FlatRectMath) Position(x, y int, size [2]int) (float64, float64) {
// return float64(x * size[0]), float64(y * size[1])
// }
// func (t FlatRectMath) PositionToTile(x, y float64, size [2]int) (int, int) {
// so2x := size[0] >> 1 //Note: Same as: / 2
// so2y := size[1] >> 1 // Note: Same as: / 2
// xPos := (int(x) + (so2x)) / size[0]
// yPos := (int(y) + (so2y)) / size[1]
// // Adjust for negatives
// if x < float64(-so2x) {
// xPos -= 1
// }
// if y < float64(-so2y) {
// yPos -= 1
// }
// return xPos, yPos
// // return (int(x) + (size[0]/2)) / size[0],
// // (int(y) + (size[1]/2))/ size[1]
// }
type PointyRectMath struct{}
func (t PointyRectMath) Position(x, y int, size [2]int) (float64, float64) {
// If y goes up, then xPos must go downward a bit
return -float64((x * size[0] / 2) - (y * size[0] / 2)),
// If x goes up, then yPos must go up a bit as well
-float64((y * size[1] / 2) + (x * size[1] / 2))
}
func (t PointyRectMath) PositionToTile(x, y float64, size [2]int) (int, int) {
dx := float64(size[0]) / 2.0
dy := float64(size[1]) / 2.0
tx := -((x / dx) + (y / dy)) / 2
ty := -((y / dy) + tx)
return int(math.Round(float64(tx))), int(math.Round(float64(ty)))
}
package tile
import (
"iter"
"github.com/unitoftime/flow/glm"
"github.com/unitoftime/flow/phy2"
)
// type TileType uint8
// type Tile struct {
// Type TileType
// Height float32
// // TODO - Should entity inclusion be held somewhere else? What if two entities occupy the same tile?
// Entity ecs.Id // This holds the entity Id of the object that is placed here
// }
type TilePosition = Position
type Position struct {
X, Y int
}
func (t Position) Add(v Position) Position {
return Position{
t.X + v.X,
t.Y + v.Y,
}
}
func (t Position) Sub(v Position) Position {
return Position{
t.X - v.X,
t.Y - v.Y,
}
}
func (t Position) Div(val int) Position {
return Position{
t.X / val,
t.Y / val,
}
}
func (t Position) Maximum() int {
x := t.X
y := t.Y
if x < 0 {
x = -x
}
if y < 0 {
y = -y
}
if x > y {
return x
}
return y
}
func (t Position) Manhattan() int {
x := t.X
y := t.Y
if x < 0 {
x = -x
}
if y < 0 {
y = -y
}
return x + y
}
func ManhattanDistance(a, b Position) int {
dx := a.X - b.X
dy := a.Y - b.Y
if dx < 0 {
dx = -dx
}
if dy < 0 {
dy = -dy
}
return dx + dy
}
type Rect struct {
Min, Max Position
}
func R(minX, minY, maxX, maxY int) Rect {
return Rect{
Position{minX, minY},
Position{maxX, maxY},
}
}
func (r Rect) WithCenter(v Position) Rect {
c := r.Center()
zRect := r.Moved(Position{
X: -c.X,
Y: -c.Y,
})
return zRect.Moved(v)
}
func (r Rect) Area() int {
return r.W() * r.H()
}
func (r Rect) W() int {
return r.Max.X - r.Min.X
}
func (r Rect) H() int {
return r.Max.Y - r.Min.Y
}
func (r Rect) Center() Position {
return Position{r.Min.X + (r.W() / 2), r.Min.Y + (r.H() / 2)}
}
func (r Rect) Moved(v Position) Rect {
return Rect{
Min: r.Min.Add(v),
Max: r.Max.Add(v),
}
}
func (r Rect) Contains(pos Position) bool {
return pos.X <= r.Max.X && pos.X >= r.Min.X && pos.Y <= r.Max.Y && pos.Y >= r.Min.Y
}
func (r Rect) Intersects(r2 Rect) bool {
return (r.Min.X <= r2.Max.X &&
r.Max.X >= r2.Min.X &&
r.Min.Y <= r2.Max.Y &&
r.Max.Y >= r2.Min.Y)
}
func (r Rect) Norm() Rect {
x1, x2 := minMax(r.Min.X, r.Max.X)
y1, y2 := minMax(r.Min.Y, r.Max.Y)
return R(x1, y1, x2, y2)
}
func (r Rect) Union(s Rect) Rect {
r = r.Norm()
s = s.Norm()
x1, _ := minMax(r.Min.X, s.Min.X)
_, x2 := minMax(r.Max.X, s.Max.X)
y1, _ := minMax(r.Min.Y, s.Min.Y)
_, y2 := minMax(r.Max.Y, s.Max.Y)
return R(x1, y1, x2, y2)
}
func minMax(a, b int) (int, int) {
if a > b {
return b, a
}
return a, b
}
func (r Rect) PadAll(pad int) Rect {
return R(r.Min.X-pad, r.Min.Y-pad, r.Max.X+pad, r.Max.Y+pad)
}
func (r Rect) UnpadAll(pad int) Rect {
return r.PadAll(-pad)
}
func (r Rect) Iter() iter.Seq[Position] {
return func(yield func(Position) bool) {
for x := r.Min.X; x <= r.Max.X; x++ {
for y := r.Min.Y; y <= r.Max.Y; y++ {
if !yield(Position{x, y}) {
return // Exit the iteration
}
}
}
}
}
// //cod:struct
// type Collider struct {
// Width, Height int // Size of the collider in terms of tiles
// }
type Tilemap[T any] struct {
TileSize [2]int // In pixels
tiles [][]T
math Math
Offset glm.Vec2 // In world space positioning
}
func New[T any](tiles [][]T, tileSize [2]int, math Math) *Tilemap[T] {
return &Tilemap[T]{
TileSize: tileSize,
tiles: tiles,
math: math,
Offset: glm.Vec2{},
}
}
// This returns the underlying array, not a copy
// TODO - should I just make tiles public?
func (t *Tilemap[T]) Tiles() [][]T {
return t.tiles
}
func (t *Tilemap[T]) Width() int {
return len(t.tiles)
}
func (t *Tilemap[T]) Height() int {
// TODO - Assumes the tilemap is a square and is larger than size 0
return len(t.tiles[0])
}
func (t *Tilemap[T]) GetTile(pos Position) (T, bool) {
if pos.X < 0 || pos.X >= len(t.tiles) || pos.Y < 0 || pos.Y >= len(t.tiles[pos.X]) {
var ret T
return ret, false
}
return t.tiles[pos.X][pos.Y], true
}
func (t *Tilemap[T]) SetTile(pos Position, tile T) bool {
if pos.X < 0 || pos.X >= len(t.tiles) || pos.Y < 0 || pos.Y >= len(t.tiles[pos.X]) {
return false
}
t.tiles[pos.X][pos.Y] = tile
return true
}
func (t *Tilemap[T]) TileToPosition(tilePos Position) (float64, float64) {
x, y := t.math.Position(tilePos.X, tilePos.Y, t.TileSize)
return (x + float64(t.Offset.X)), (y + float64(t.Offset.Y))
}
func (t *Tilemap[T]) PositionToTile(x, y float64) Position {
x -= t.Offset.X
y -= t.Offset.Y
tX, tY := t.math.PositionToTile(x, y, t.TileSize)
return Position{tX, tY}
}
func (t *Tilemap[T]) GetEdgeNeighbors(x, y int) []Position {
return []Position{
Position{x + 1, y},
Position{x - 1, y},
Position{x, y + 1},
Position{x, y - 1},
}
}
// TODO - this might not work for pointy-top tilemaps
func (t *Tilemap[T]) BoundsAt(pos Position) (float64, float64, float64, float64) {
x, y := t.TileToPosition(pos)
return float64(x) - float64(t.TileSize[0]/2), float64(y) - float64(t.TileSize[1]/2), float64(x) + float64(t.TileSize[0]/2), float64(y) + float64(t.TileSize[1]/2)
}
// Returns a list of tiles that are overlapping the collider at a position
func (t *Tilemap[T]) GetOverlappingTiles(x, y float64, collider *phy2.CircleCollider) []Position {
minX := x - collider.Radius
maxX := x + collider.Radius
minY := y - collider.Radius
maxY := y + collider.Radius
min := t.PositionToTile(minX, minY)
max := t.PositionToTile(maxX, maxY)
ret := make([]Position, 0)
for tx := min.X; tx <= max.X; tx++ {
for ty := min.Y; ty <= max.Y; ty++ {
ret = append(ret, Position{tx, ty})
}
}
return ret
}
package tile
// Inspired By: http://www.cr31.co.uk/stagecast/wang/1sideedge.html
// Follows the tile numbering that godot uses. Numbered in reading order, starting at 0.
// Godot: https://docs.godotengine.org/en/stable/tutorials/2d/using_tilemaps.html
func PackedBlobmapNumber(t, b, l, r, tl, tr, bl, br bool) uint8 {
wang := WangBlobmapNumber(t, b, l, r, tl, tr, bl, br)
// Default blank
ret := 22
switch wang {
case 0:
ret = 22
case 1:
ret = 24
case 4:
ret = 37
case 16:
ret = 0
case 64:
ret = 39
case 5:
ret = 25
case 20:
ret = 1
case 80:
ret = 3
case 65:
ret = 27
case 7:
ret = 44
case 28:
ret = 8
case 112:
ret = 11
case 193:
ret = 47
case 17:
ret = 12
case 68:
ret = 38
case 21:
ret = 13
case 84:
ret = 2
case 81:
ret = 15
case 69:
ret = 26
case 23:
ret = 28
case 92:
ret = 5
case 113:
ret = 19
case 197:
ret = 42
case 29:
ret = 16
case 116:
ret = 6
case 209:
ret = 31
case 71:
ret = 41
case 31:
ret = 20
case 124:
ret = 10
case 241:
ret = 35
case 199:
ret = 45
case 85:
ret = 14
case 87:
ret = 7
case 93:
ret = 43
case 117:
ret = 40
case 219:
ret = 4
case 95:
ret = 32
case 125:
ret = 9
case 245:
ret = 23
case 215:
ret = 46
case 119:
ret = 21
case 221:
ret = 34
case 127:
ret = 17
case 253:
ret = 18
case 247:
ret = 30
case 223:
ret = 29
case 255:
ret = 33
}
return uint8(ret)
}
// This function computes the wang tilenumber of a tile based on the tiles around it
func WangBlobmapNumber(t, b, l, r, tl, tr, bl, br bool) uint8 {
// If surrounding edges aren't set, then corners must be false
if !(t && l) {
tl = false
}
if !(t && r) {
tr = false
}
if !(b && l) {
bl = false
}
if !(b && r) {
br = false
}
total := uint8(0)
if t {
total += (1 << 0)
}
if tr {
total += (1 << 1)
}
if r {
total += (1 << 2)
}
if br {
total += (1 << 3)
}
if b {
total += (1 << 4)
}
if bl {
total += (1 << 5)
}
if l {
total += (1 << 6)
}
if tl {
total += (1 << 7)
}
return total
}
// Same thing as blobmap but only includes the edge neighbors (and not corners)
func PackedPipemapNumber(t, b, l, r bool) uint8 {
total := uint8(0)
if t {
total += (1 << 0)
}
if r {
total += (1 << 1)
}
if b {
total += (1 << 2)
}
if l {
total += (1 << 3)
}
return total
}
func PackedRawEightNumber(t, b, l, r, tl, tr, bl, br bool) uint8 {
total := uint8(0)
if t {
total += (1 << 0)
}
if r {
total += (1 << 1)
}
if b {
total += (1 << 2)
}
if l {
total += (1 << 3)
}
if tr {
total += (1 << 4)
}
if tl {
total += (1 << 5)
}
if br {
total += (1 << 6)
}
if bl {
total += (1 << 7)
}
return total
}
package timer
import (
"math/rand"
"time"
)
type Timer struct {
Interval time.Duration // How long the interval is
Remaining time.Duration // Remaining time
Probability float64 // From [0 to 1]
// Func func() // Anonymous function to execute
}
func New(interval time.Duration, probability float64) Timer {
if probability > 1.0 {
probability = 1.0
} else if probability < 0 {
probability = 0.0
}
return Timer{
Interval: interval,
Remaining: interval,
Probability: probability,
}
}
func (t *Timer) Update(dt time.Duration) bool {
if t.Interval == 0 {
return false
}
ret := false
if t.Remaining <= 0 {
// Else if the l is done, then roll the dice to see if we evaluate it
random := rand.Float64()
if random < t.Probability {
// t.Func()
ret = true
}
t.Reset()
} else {
// If the l is not done, then count it down
t.Remaining -= dt
}
return ret
}
func (t *Timer) Reset() {
t.Remaining = t.Interval
}
package main
import (
"flag"
"log"
"net/http"
)
func main() {
port := flag.String("p", "8081", "port to serve on")
directory := flag.String("d", ".", "the directory of static file to host")
flag.Parse()
http.Handle("/", http.FileServer(http.Dir(*directory)))
log.Printf("Serving %s on HTTP port: %s\n", *directory, *port)
log.Fatal(http.ListenAndServe(":"+*port, nil))
}
// Code generated by cod; DO NOT EDIT.
package transform
import (
"github.com/unitoftime/ecs"
)
var ChildrenComp = ecs.NewComp[Children]()
func (c Children) CompId() ecs.CompId {
return ChildrenComp.CompId()
}
func (c Children) CompWrite(w ecs.W) {
ChildrenComp.WriteVal(w, c)
}
var GlobalComp = ecs.NewComp[Global]()
func (c Global) CompId() ecs.CompId {
return GlobalComp.CompId()
}
func (c Global) CompWrite(w ecs.W) {
GlobalComp.WriteVal(w, c)
}
var LocalComp = ecs.NewComp[Local]()
func (c Local) CompId() ecs.CompId {
return LocalComp.CompId()
}
func (c Local) CompWrite(w ecs.W) {
LocalComp.WriteVal(w, c)
}
var ParentComp = ecs.NewComp[Parent]()
func (c Parent) CompId() ecs.CompId {
return ParentComp.CompId()
}
func (c Parent) CompWrite(w ecs.W) {
ParentComp.WriteVal(w, c)
}
package transform
import (
"time"
"github.com/unitoftime/ecs"
"github.com/unitoftime/flow/ds"
"github.com/unitoftime/flow/glm"
)
// Todo List
// 1. Migrate to 3D transforms
// 2. Revisit optimizations for heirarchy resolutions
type DefaultPlugin struct {
}
func (p DefaultPlugin) Initialize(world *ecs.World) {
scheduler := ecs.GetResource[ecs.Scheduler](world)
// TODO: This should be added to a better stage
scheduler.AddSystems(ecs.StageFixedUpdate, ResolveHeirarchySystem(world))
}
type Transform3D struct {
Pos glm.Vec3
Rot glm.Quat
Scale glm.Vec3
}
func Default3D() Transform3D {
return Transform3D{
Rot: glm.IQuat(),
Scale: glm.Vec3{1, 1, 1},
}
}
func FromPos3D(pos glm.Vec3) Transform3D {
return Transform3D{
Pos: pos,
Rot: glm.IQuat(),
Scale: glm.Vec3{1, 1, 1},
}
}
func (t Transform3D) Mat4() glm.Mat4 {
mat := glm.Mat4Ident
mat.
Scale(t.Scale.X, t.Scale.Y, 1).
RotateQuat(t.Rot).
// Rotate(t.Rot, glm.Vec3{0, 0, 1}).
Translate(t.Pos.X, t.Pos.Y, 0)
return mat
}
// // Returns the transform, but with the vector space moved to the parent transform
// func (t Transform3D) Globalize(parent Global) Global {
// childGlobal := t
// parentMat := parent.Mat4()
// dstPos := parentMat.Apply(t.Pos)
// childGlobal.Pos = dstPos
// childGlobal.Rot = *parent.Rot.RotateQuat(t.Rot) // TODO: is this backwards?
// childGlobal.Scale = t.Scale.Mult(parent.Scale)
// return Global{childGlobal}
// }
//--------------------------------------------------------------------------------
func Default() Transform {
return Transform{
Rot: 0,
Scale: glm.Vec2{1, 1},
}
}
func FromPos(pos glm.Vec2) Transform {
return Transform{
Pos: pos,
Rot: 0,
Scale: glm.Vec2{1, 1},
}
}
//cod:component
type Local struct {
Transform
}
type Transform struct {
Pos glm.Vec2
Rot float64
Scale glm.Vec2
}
func (t Transform) Mat4() glm.Mat4 {
mat := glm.Mat4Ident
mat.
Scale(t.Scale.X, t.Scale.Y, 1).
Rotate(t.Rot, glm.Vec3{0, 0, 1}).
Translate(t.Pos.X, t.Pos.Y, 0)
return mat
}
// Returns the transform, but with the vector space moved to the parent transform
func (t Transform) Globalize(parent Global) Global {
childGlobal := t
// try 2 - manually calculate
parentMat := parent.Mat4()
dstPos := parentMat.Apply(t.Pos.Vec3())
childGlobal.Pos = dstPos.Vec2()
childGlobal.Rot = parent.Rot + t.Rot
childGlobal.Scale = t.Scale.Mult(parent.Scale)
// // try 3 - multiply mats and pull out values
// Notes: https://math.stackexchange.com/questions/237369/given-this-transformation-matrix-how-do-i-decompose-it-into-translation-rotati
// parentMat := parentGlobal.Mat4()
// childMat := childLocal.Mat4()
// parentMat.Mul(&childMat)
// childGlobal.Pos = parentMat.GetTranslation().Vec2()
// childGlobal.Rot = parentMat.GetRotation().Z
// childGlobal.Scale = parentMat.GetScale().Vec2()
return Global{childGlobal}
}
// Returns the transform, but with the vector space moved to the parent transform
func (parent Transform) Localize(childGlobal Global) Local {
// try 2 - manually calculate
parentMat := parent.Mat4()
dstPos := parentMat.Inv().Apply(childGlobal.Pos.Vec3())
childGlobal.Pos = dstPos.Vec2()
childGlobal.Rot = childGlobal.Rot - parent.Rot
childGlobal.Scale = childGlobal.Scale.Div(parent.Scale)
return Local{childGlobal.Transform}
}
//cod:component
type Global struct {
Transform
}
// func (t GlobalTransform) Mat4() glm.Mat4 {
// mat := glm.Mat4Ident
// mat.
// Scale(t.Scale.X, t.Scale.Y, 1).
// Rotate(t.Rot, glm.Vec3{0, 0, 1}).
// Translate(t.Pos.X, t.Pos.Y, 0)
// return mat
// }
//cod:component
type Parent struct {
Id ecs.Id
}
// 3. You could reorganize so that parent's know their transform children, and recursively calculate each child's GlobalTransform
//
//cod:component
type Children struct {
// List []ecs.Id
MiniSlice ds.MiniSlice[[8]ecs.Id, ecs.Id]
}
// func (c *Children) Add(id ecs.Id) {
// c.List = append(c.List, id)
// }
// func (c *Children) Remove(id ecs.Id) {
// idx := slices.Index(c.List, id)
// if idx < 0 { return } // Skip: Doesnt exist
// c.List[idx] = c.List[len(c.List) - 1]
// c.List = c.List[:len(c.List) - 1]
// }
// func (c *Children) Clear() {
// if c.List == nil { return }
// c.List = c.List[:0]
// }
func (c *Children) Add(id ecs.Id) {
c.MiniSlice.Append(id)
}
func (c *Children) Remove(id ecs.Id) {
idx := c.MiniSlice.Find(id)
if idx < 0 {
return
} // Skip: Doesnt exist
c.MiniSlice.Delete(idx)
}
func (c *Children) Clear() {
c.MiniSlice.Clear()
}
// Recursively goes through the transform heirarchy and calculates entity GlobalTransform based on their Parent's GlobalTransform and their local Transform.
func ResolveHeirarchySystem(world *ecs.World) ecs.System {
queryTopLevel := ecs.Query3[Children, Local, Global](world,
ecs.Without(Parent{}),
ecs.Optional(Children{}, Local{}),
// Note: Some entities are "top level" and dont have a local transform, so we will just use their global transform as is. I should move away from this model
// ^^^^^
// TODO: Fix hack where Local{} is optional
)
query := ecs.Query3[Children, Local, Global](world)
return ecs.NewSystem(func(dt time.Duration) {
queryTopLevel.MapId(func(id ecs.Id, children *Children, local *Local, global *Global) {
// Resolve the top level transform (No movement, rotation, or scale)
if local != nil {
global.Transform = local.Transform
}
if children == nil {
return
}
resolveTransform(query, children, global)
})
})
}
// TODO: This might loop forever if you have malformed heirarchies where there are cycles. I dont have any prevention for that right now.
func resolveTransform(
query *ecs.View3[Children, Local, Global],
children *Children, // TODO: Pointer here?
parentGlobal *Global, // TODO: Pointer here?
) {
// for i := range children.List {
// nextChildren, childLocal, childGlobal := query.Read(children.List[i])
// if childGlobal == nil { continue } // If child has no transform, skip
// if childLocal == nil {
// // If child has no local transform. Assume it is identity. And just copy parentGlobal
// childGlobal.Pos = parentGlobal.Pos
// childGlobal.Rot = parentGlobal.Rot
// childGlobal.Scale = parentGlobal.Scale
// } else {
// *childGlobal = childLocal.Globalize(*parentGlobal)
// }
// if nextChildren == nil { continue } // Dont recurse: This child doesn't have a TransformChildren component
// if len(nextChildren.List) > 0 {
// resolveTransform(query, nextChildren, childGlobal)
// }
// }
for _, childId := range children.MiniSlice.All() {
nextChildren, childLocal, childGlobal := query.Read(childId)
if childGlobal == nil {
continue // If child has no transform, skip
}
if childLocal == nil {
// If child has no local transform. Assume it is identity. And just copy parentGlobal
childGlobal.Pos = parentGlobal.Pos
childGlobal.Rot = parentGlobal.Rot
childGlobal.Scale = parentGlobal.Scale
} else {
*childGlobal = childLocal.Globalize(*parentGlobal)
}
if nextChildren == nil {
continue // Dont recurse: This child doesn't have a TransformChildren component
}
if nextChildren.MiniSlice.Len() > 0 {
resolveTransform(query, nextChildren, childGlobal)
}
}
}
// Note: Originally you added this just because you wanted transform parenting with projectiles
// TODO: Potential optimization ideas:
// 1. You can sort the archetype storage by ECS ID, then make sure all children have higher IDs than parents
// 2. Read just the ECS IDs and do query.Read operations, rather than MapId
// Level Based idea: Iterate levels. I think I'd prefer to do it recursively through children
// type TransformParent struct {
// Level uint8
// Id ecs.Id
// }
// func ResolveTransformHeirarchySystem(game *Game) ecs.System {
// query := ecs.Query3[TransformParent, Transform, GlobalTransform](game.World)
// queryTopLevel := ecs.Query2[Transform, GlobalTransform](game.World,
// ecs.Without(TransformParent{}))
// return ecs.NewSystem(func(dt time.Duration) {
// queryTopLevel.MapId(func(id ecs.Id, local *Transform, global *GlobalTransform) {
// global.Position = local.Position
// })
// currentLevel := uint8(0)
// for {
// noneFound := true
// query.MapId(func(id ecs.Id, parent *TransformParent, local *Transform, global *GlobalTransform) {
// if parent.Level == currentLevel { return } // Skip everything not on our current level
// noneFound = false
// _, _, parentGlobal := query.Read(parent.Id)
// global.Position = local.Position.Add(parentGlobal.Position)
// })
// currentLevel++
// if noneFound {
// break
// }
// if currentLevel == math.MaxUint8 {
// break
// }
// }
// })
// }
package main
import (
"bytes"
"errors"
"image"
_ "image/png"
"github.com/unitoftime/flow/asset"
"github.com/unitoftime/glitch"
)
type SpriteAssetLoader struct {
}
func (l SpriteAssetLoader) Ext() []string {
return []string{".png"}
}
func (l SpriteAssetLoader) Load(server *asset.Server, data []byte) (*glitch.Sprite, error) {
smooth := true
img, _, err := image.Decode(bytes.NewBuffer(data))
if err != nil {
return nil, err
}
texture := glitch.NewTexture(img, smooth)
return glitch.NewSprite(texture, texture.Bounds()), nil
}
func (l SpriteAssetLoader) Store(server *asset.Server, sprite *glitch.Sprite) ([]byte, error) {
return nil, errors.New("sprites do not support writeback")
}
package glitchasset
import (
"encoding/json"
"errors"
"fmt"
"image"
_ "image/png"
"io/fs"
"io/ioutil"
"path"
"time"
"gopkg.in/yaml.v3"
"github.com/golang/freetype/truetype"
"golang.org/x/image/font"
"github.com/unitoftime/flow/glm"
"github.com/unitoftime/glitch"
"github.com/unitoftime/packer"
)
type Load struct {
filesystem fs.FS
}
func NewLoad(filesystem fs.FS) *Load {
return &Load{filesystem}
}
func (load *Load) Open(filepath string) (fs.File, error) {
return load.filesystem.Open(filepath)
}
func (load *Load) Data(filepath string) ([]byte, error) {
file, err := load.filesystem.Open(filepath)
if err != nil {
return nil, err
}
defer file.Close()
return ioutil.ReadAll(file)
}
func (load *Load) Font(filepath string, size float64) (font.Face, error) {
file, err := load.filesystem.Open(filepath)
if err != nil {
return nil, err
}
defer file.Close()
fontData, err := ioutil.ReadAll(file)
if err != nil {
return nil, err
}
font, err := truetype.Parse(fontData)
if err != nil {
return nil, err
}
fontFace := truetype.NewFace(font, &truetype.Options{
Size: size,
})
return fontFace, nil
}
func (load *Load) Image(filepath string) (image.Image, error) {
file, err := load.filesystem.Open(filepath)
if err != nil {
return nil, err
}
defer file.Close()
img, _, err := image.Decode(file)
if err != nil {
return nil, err
}
return img, nil
}
func (load *Load) Texture(filepath string, smooth bool) (*glitch.Texture, error) {
img, err := load.Image(filepath)
if err != nil {
return nil, err
}
texture := glitch.NewTexture(img, smooth)
return texture, nil
}
// Loads a single sprite from a filepath of an image
func (load *Load) Sprite(filepath string, smooth bool) (*glitch.Sprite, error) {
texture, err := load.Texture(filepath, smooth)
if err != nil {
return nil, err
}
return glitch.NewSprite(texture, texture.Bounds()), nil
}
func (load *Load) Json(filepath string, dat interface{}) error {
file, err := load.filesystem.Open(filepath)
if err != nil {
return err
}
defer file.Close()
jsonData, err := ioutil.ReadAll(file)
if err != nil {
return err
}
return json.Unmarshal(jsonData, dat)
}
func (load *Load) Yaml(filepath string, dat interface{}) error {
file, err := load.filesystem.Open(filepath)
if err != nil {
return err
}
defer file.Close()
yamlData, err := ioutil.ReadAll(file)
if err != nil {
return err
}
return yaml.Unmarshal(yamlData, dat)
}
// TODO - move Aseprite stuff to another package?
type AseSheet struct {
Frames []AseFrame `json:frames`
Meta AseMeta
}
type AseFrame struct {
Filename string `json:filename`
//Frame todo
Duration int `json:duration`
}
type AseMeta struct {
FrameTags []AseFrameTag `json:frameTags`
}
type AseFrameTag struct {
Name string `json:name`
From int `json:from`
To int `json:to`
Direction string `json:direction`
}
// Loads an aseprite spritesheet.
func (load *Load) AseSheet(filepath string) (*AseSheet, error) {
dat := AseSheet{}
err := load.Json(filepath, &dat)
if err != nil {
return nil, err
}
return &dat, nil
}
// TODO - Assumes that all animations share the same spritesheet
type Animation struct {
spritesheet *Spritesheet
Frames map[string][]AnimationFrame
}
type AnimationFrame struct {
Name string
Sprite *glitch.Sprite
Duration time.Duration
MirrorY bool
}
// TODO - Assumptions: frame name is <filename>-<framenumber>.png (Aseprite doesn't export the file name. But you could maybe repack their spritesheet into your own)
func (load *Load) AseAnimation(spritesheet *Spritesheet, filepath string) (*Animation, error) {
base := path.Base(filepath)
baseNoExt := base[:len(base)-len(path.Ext(base))]
aseSheet, err := load.AseSheet(filepath)
if err != nil {
return nil, err
}
anim := Animation{
spritesheet: spritesheet,
Frames: make(map[string][]AnimationFrame),
}
for _, frameTag := range aseSheet.Meta.FrameTags {
// TODO - implement other directions
if frameTag.Direction != "forward" {
panic("NonForward frametag not supported!")
}
frames := make([]AnimationFrame, 0)
for i := frameTag.From; i <= frameTag.To; i++ {
spriteName := fmt.Sprintf("%s-%d.png", baseNoExt, i)
sprite, err := spritesheet.Get(spriteName)
if err != nil {
return nil, err
}
frames = append(frames, AnimationFrame{
Name: spriteName,
Sprite: sprite,
Duration: time.Duration(aseSheet.Frames[i].Duration) * time.Millisecond,
MirrorY: false,
})
}
anim.Frames[frameTag.Name] = frames
}
return &anim, nil
}
func (load *Load) Mountpoints(filepath string) (packer.MountFrames, error) {
mountFrames := packer.MountFrames{}
err := load.Json(filepath, &mountFrames)
if err != nil {
return packer.MountFrames{}, err
}
return mountFrames, nil
}
func (load *Load) Spritesheet(filepath string, smooth bool) (*Spritesheet, error) {
//Load the Json
serializedSpritesheet := packer.SerializedSpritesheet{}
err := load.Json(filepath, &serializedSpritesheet)
if err != nil {
return nil, err
}
imageFilepath := path.Join(path.Dir(filepath), serializedSpritesheet.ImageName)
// Load the image
img, err := load.Image(imageFilepath)
if err != nil {
return nil, err
}
// pic := pixel.PictureDataFromImage(img)
texture := glitch.NewTexture(img, smooth)
// Create the spritesheet object
// bounds := texture.Bounds()
lookup := make(map[string]*glitch.Sprite)
for k, v := range serializedSpritesheet.Frames {
rect := glm.R(
v.Frame.X,
v.Frame.Y,
v.Frame.X+v.Frame.W,
v.Frame.Y+v.Frame.H).Norm()
// rect := glitch.R(
// float32(v.Frame.X),
// float32(float64(bounds.H()) - v.Frame.Y),
// float32(v.Frame.X + v.Frame.W),
// float32(float64(bounds.W()) - (v.Frame.Y + v.Frame.H))).Norm()
lookup[k] = glitch.NewSprite(texture, rect)
}
return NewSpritesheet(texture, lookup), nil
}
type Spritesheet struct {
texture *glitch.Texture
lookup map[string]*glitch.Sprite
}
func NewSpritesheet(tex *glitch.Texture, lookup map[string]*glitch.Sprite) *Spritesheet {
return &Spritesheet{
texture: tex,
lookup: lookup,
}
}
func (s *Spritesheet) Get(name string) (*glitch.Sprite, error) {
sprite, ok := s.lookup[name]
if !ok {
return nil, errors.New(fmt.Sprintf("Invalid sprite name: %s", name))
}
return sprite, nil
}
// https://www.aseprite.org/docs/slices/#:~:text=With%20the%20Slice%20tool,some%20extra%20user%20defined%20information.
func (s *Spritesheet) GetNinePanel(name string, border glm.Rect) (*glitch.NinePanelSprite, error) {
sprite, ok := s.lookup[name]
if !ok {
return nil, errors.New(fmt.Sprintf("Invalid sprite name: %s", name))
}
return glitch.SpriteToNinePanel(sprite, border), nil
}
func (s *Spritesheet) Picture() *glitch.Texture {
return s.texture
}
// // Gets multiple frames with the same prefix name. Indexing starts at 0
// func (s *Spritesheet) GetFrames(name, ext string, length int) ([]*glitch.Sprite, error) {
// ret := make([]*glitch.Sprite, length)
// for i := range names {
// sprite, err := s.Get(fmt.Sprintf("%s%d%s", name, i, ext))
// if err != nil {
// return nil, err
// }
// ret[i] = sprite
// }
// return ret, nil
// }