diff --git a/.gitignore b/.gitignore index e8a9b50..656ed88 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /loader /orig /palette-idx +/view-ani /view-font /view-obj /view-map diff --git a/Makefile b/Makefile index 75b8131..284ce27 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ srcfiles = Makefile go.mod $(shell find . -iname *.go) GOBUILD ?= go build -tags ebitengl -all: loader ordoor palette-idx view-font view-obj view-map view-menu view-minimap view-set +all: loader ordoor palette-idx view-ani view-font view-obj view-map view-menu view-minimap view-set loader: $(srcfiles) $(GOBUILD) -o loader ./cmd/loader @@ -10,6 +10,9 @@ loader: $(srcfiles) palette-idx: $(srcfiles) $(GOBUILD) -o palette-idx ./cmd/palette-idx +view-ani: $(srcfiles) + $(GOBUILD) -o view-ani ./cmd/view-ani + view-font: $(srcfiles) $(GOBUILD) -o view-font ./cmd/view-font @@ -32,6 +35,6 @@ ordoor: $(srcfiles) $(GOBUILD) -o ordoor ./cmd/ordoor clean: - rm -f loader ordoor view-obj view-map view-minimap view-set palette-idx view-font + rm -f loader ordoor view-ani view-obj view-map view-minimap view-set palette-idx view-font .PHONY: all clean diff --git a/cmd/view-ani/main.go b/cmd/view-ani/main.go new file mode 100644 index 0000000..6b86cf7 --- /dev/null +++ b/cmd/view-ani/main.go @@ -0,0 +1,164 @@ +package main + +import ( + "flag" + "image" + "log" + "math" + "os" + + "github.com/hajimehoshi/ebiten" + + "code.ur.gs/lupine/ordoor/internal/assetstore" + "code.ur.gs/lupine/ordoor/internal/ui" +) + +var ( + gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation") + groupIdx = flag.Int("group", 0, "Group index to start at") + recIdx = flag.Int("record", 0, "Record index to start at") + + winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension") + winY = flag.Int("win-y", 1024, "Pre-scaled window Y dimension") +) + +type env struct { + assets *assetstore.AssetStore + ani *assetstore.Animation + step int + + state state + lastState state +} + +type state struct { + groupIdx int + recIdx int + + zoom float64 + origin image.Point +} + +func main() { + flag.Parse() + + if *gamePath == "" { + flag.Usage() + os.Exit(1) + } + + assets, err := assetstore.New(*gamePath) + if err != nil { + log.Fatal("Failed to set up asset store: %v", err) + } + + state := state{ + zoom: 2.0, + origin: image.Point{0, 0}, + groupIdx: *groupIdx, + recIdx: *recIdx, + } + + env := &env{ + assets: assets, + state: state, + lastState: state, + } + + win, err := ui.NewWindow(env, "View Animations", *winX, *winY) + if err != nil { + log.Fatal(err) + } + + win.OnKeyUp(ebiten.KeyMinus, env.changeGroup(-1)) + win.OnKeyUp(ebiten.KeyEqual, env.changeGroup(+1)) + + win.OnKeyUp(ebiten.KeyComma, env.changeRec(-1)) + win.OnKeyUp(ebiten.KeyPeriod, env.changeRec(+1)) + + win.OnKeyUp(ebiten.KeyLeft, env.changeOrigin(-16, +0)) + win.OnKeyUp(ebiten.KeyRight, env.changeOrigin(+16, +0)) + win.OnKeyUp(ebiten.KeyUp, env.changeOrigin(+0, -16)) + win.OnKeyUp(ebiten.KeyDown, env.changeOrigin(+0, +16)) + win.OnMouseWheel(env.changeZoom) + + // The main thread now belongs to ebiten + if err := win.Run(); err != nil { + log.Fatal(err) + } +} + +func (e *env) Update(screenX, screenY int) error { + if e.step == 0 || e.lastState != e.state { + + ani, err := e.assets.Animation(e.state.groupIdx, e.state.recIdx) + if err != nil { + return err + } + e.ani = ani + + log.Printf( + "new state: group=%d rec=%d zoom=%.2f, origin=%+v", + e.state.groupIdx, + e.state.recIdx, + e.state.zoom, + e.state.origin, + ) + } + + // This should be the final action + e.step += 1 + e.lastState = e.state + + return nil +} + +func (e *env) Draw(screen *ebiten.Image) error { + cam := ebiten.GeoM{} + cam.Translate(float64(-e.state.origin.X), float64(-e.state.origin.Y)) // Move to origin + cam.Scale(e.state.zoom, e.state.zoom) // apply current zoom factor + + if len(e.ani.Frames) > 0 { + sprite := e.ani.Frames[e.step/4%len(e.ani.Frames)] + + return screen.DrawImage(sprite.Image, &ebiten.DrawImageOptions{GeoM: cam}) + } + + return nil +} + +func (e *env) changeGroup(by int) func() { + return func() { + e.state.groupIdx += by + + if e.state.groupIdx < 0 { + e.state.groupIdx = 0 + } + + // TODO: upper bounds checking + } +} + +func (e *env) changeRec(by int) func() { + return func() { + e.state.recIdx += by + + if e.state.recIdx < 0 { + e.state.recIdx = 0 + } + + // TODO: upper bounds checking + } +} + +func (e *env) changeOrigin(byX, byY int) func() { + return func() { + e.state.origin.X += byX + e.state.origin.Y += byY + } +} + +func (e *env) changeZoom(_, y float64) { + // Zoom in and out with the mouse wheel + e.state.zoom *= math.Pow(1.2, y) +} diff --git a/internal/assetstore/ani.go b/internal/assetstore/ani.go new file mode 100644 index 0000000..c9c4500 --- /dev/null +++ b/internal/assetstore/ani.go @@ -0,0 +1,78 @@ +package assetstore + +import ( + "code.ur.gs/lupine/ordoor/internal/idx" +) + +type Animation struct { + Frames []*Sprite +} + +func (a *AssetStore) AnimationsIndex() (*idx.Idx, error) { + if a.idx != nil { + return a.idx, nil + } + + filename, err := a.lookup("WarHammer", "idx", "Idx") + if err != nil { + return nil, err + } + + idx, err := idx.Load(filename) + if err != nil { + return nil, err + } + + a.idx = idx + return idx, nil +} + +func (a *AssetStore) AnimationsObject() (*Object, error) { + if a.aniObj != nil { + return a.aniObj, nil + } + + filename, err := a.lookup("WarHammer", "ani", "Anim") + if err != nil { + return nil, err + } + + obj, err := a.ObjectByPath(filename) + if err != nil { + return nil, err + } + + a.aniObj = obj + return obj, nil +} + +func (a *AssetStore) Animation(groupIdx, recIdx int) (*Animation, error) { + idx, err := a.AnimationsIndex() + if err != nil { + return nil, err + } + + obj, err := a.AnimationsObject() + if err != nil { + return nil, err + } + + group := idx.Groups[groupIdx] + if group.Spec.Count == 0 { + return &Animation{}, nil + } + + // rec := group.Records[recIdx] + det := group.Details[recIdx] + + first := int(group.Spec.SpriteIdx) + int(det.FirstSprite) + last := int(group.Spec.SpriteIdx) + int(det.LastSprite) + count := last - first + 1 + + sprites, err := obj.Sprites(first, count) + if err != nil { + return nil, err + } + + return &Animation{Frames: sprites}, nil +} diff --git a/internal/assetstore/assetstore.go b/internal/assetstore/assetstore.go index 3c730a2..227d625 100644 --- a/internal/assetstore/assetstore.go +++ b/internal/assetstore/assetstore.go @@ -8,6 +8,7 @@ import ( "strings" "code.ur.gs/lupine/ordoor/internal/data" + "code.ur.gs/lupine/ordoor/internal/idx" ) const ( @@ -34,17 +35,18 @@ type AssetStore struct { entries entryMap // These members are used to store things we've already loaded + aniObj *Object cursorObj *Object - - cursors map[CursorName]*Cursor - fonts map[string]*Font - generic *data.Generic - maps map[string]*Map - menus map[string]*Menu - objs map[string]*Object - sets map[string]*Set - sounds map[string]*Sound - strings *data.I18n + cursors map[CursorName]*Cursor + fonts map[string]*Font + generic *data.Generic + idx *idx.Idx + maps map[string]*Map + menus map[string]*Menu + objs map[string]*Object + sets map[string]*Set + sounds map[string]*Sound + strings *data.I18n } // New returns a new AssetStore @@ -88,10 +90,12 @@ func (a *AssetStore) Refresh() error { } // Refresh + a.aniObj = nil a.cursorObj = nil a.cursors = make(map[CursorName]*Cursor) a.entries = newEntryMap a.fonts = make(map[string]*Font) + a.idx = nil a.maps = make(map[string]*Map) a.menus = make(map[string]*Menu) a.objs = make(map[string]*Object) diff --git a/internal/assetstore/menu.go b/internal/assetstore/menu.go index 3d5d604..489e82f 100644 --- a/internal/assetstore/menu.go +++ b/internal/assetstore/menu.go @@ -41,18 +41,7 @@ func (m *Menu) Images(objIdx, start, count int) ([]*ebiten.Image, error) { } func (m *Menu) Sprites(objIdx, start, count int) ([]*Sprite, error) { - out := make([]*Sprite, count) - - for i := start; i < start+count; i++ { - sprite, err := m.Sprite(objIdx, i) - if err != nil { - return nil, err - } - - out[i-start] = sprite - } - - return out, nil + return m.objects[objIdx].Sprites(start, count) } func (m *Menu) Sprite(objIdx, idx int) (*Sprite, error) { diff --git a/internal/assetstore/object.go b/internal/assetstore/object.go index 44aac07..76cc72e 100644 --- a/internal/assetstore/object.go +++ b/internal/assetstore/object.go @@ -90,6 +90,21 @@ func (o *Object) LoadSprites() error { return nil } +func (o *Object) Sprites(start, count int) ([]*Sprite, error) { + out := make([]*Sprite, count) + + for i := start; i < start+count; i++ { + sprite, err := o.Sprite(i) + if err != nil { + return nil, err + } + + out[i-start] = sprite + } + + return out, nil +} + func (o *Object) Sprite(idx int) (*Sprite, error) { if sprite := o.sprites[idx]; sprite != nil { return sprite, nil