diff --git a/.gitignore b/.gitignore index a9901b8..20486e9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ /loader /orig /view-map +/view-minimap /view-set diff --git a/Makefile b/Makefile index 43b7bad..9bb2fa8 100644 --- a/Makefile +++ b/Makefile @@ -1,17 +1,21 @@ srcfiles = $(shell find . -iname *.go) -all: loader view-map +all: loader view-map view-minimap view-set loader: $(srcfiles) go build -o loader ur.gs/chaos-gate/cmd/loader + view-map: $(srcfiles) go build -o view-map ur.gs/chaos-gate/cmd/view-map +view-minimap: $(srcfiles) + go build -o view-minimap ur.gs/chaos-gate/cmd/view-minimap + view-set: $(srcfiles) go build -o view-set ur.gs/chaos-gate/cmd/view-set clean: - rm -f loader view-map view-set + rm -f loader view-map view-minimap view-set .PHONY: all clean diff --git a/cmd/view-map/main.go b/cmd/view-map/main.go index 8cd09a4..6465cfa 100644 --- a/cmd/view-map/main.go +++ b/cmd/view-map/main.go @@ -6,13 +6,13 @@ import ( "math" "os" "path/filepath" - "time" "github.com/faiface/pixel" - "github.com/faiface/pixel/imdraw" "github.com/faiface/pixel/pixelgl" "golang.org/x/image/colornames" + "ur.gs/chaos-gate/internal/conv" + "ur.gs/chaos-gate/internal/data" "ur.gs/chaos-gate/internal/maps" "ur.gs/chaos-gate/internal/sets" "ur.gs/chaos-gate/internal/ui" @@ -27,21 +27,20 @@ var ( type env struct { gameMap *maps.GameMap set *sets.MapSet + objects map[string]*conv.Object } -type runState struct { +type state struct { env *env - autoUpdate bool - started time.Time + step int cam pixel.Matrix camPos pixel.Vec zoom float64 - zIdx int - cellIdx int + zIdx int } func main() { @@ -64,7 +63,19 @@ func main() { log.Fatalf("Couldn't load set file %s: %v", setFile, err) } - env := &env{gameMap: gameMap, set: mapSet} + objects := make(map[string]*conv.Object) + + for _, name := range mapSet.Palette { + objFile := filepath.Join(*gamePath, "Obj", name+".obj") + obj, err := data.LoadObject(objFile) + if err != nil { + log.Fatalf("Failed to load %s: %v", name, err) + } + + objects[name] = conv.ConvertObject(obj, name) + } + + env := &env{gameMap: gameMap, set: mapSet, objects: objects} // The main thread now belongs to pixelgl pixelgl.Run(env.run) @@ -77,202 +88,138 @@ func (e *env) run() { } pWin := win.PixelWindow - state := &runState{ - env: e, - autoUpdate: true, - camPos: pixel.V(0, float64(-pWin.Bounds().Size().Y)), - zoom: 8.0, + state := &state{ + env: e, + // camPos: pixel.V(0, float64(-pWin.Bounds().Size().Y)), + // camPos: pixel.V(float64(3700), float64(0)), + zoom: 1.0, } win.Run(func() { oldState := *state - state = runStep(pWin, state) + state = state.runStep(pWin) - if oldState != *state { - log.Printf("z=%d cellIdx=%d", state.zIdx, state.cellIdx) - present(pWin, state) + if oldState != *state || oldState.step == 0 { + log.Printf("zoom=%.2f zIdx=%v camPos=%#v", state.zoom, state.zIdx, state.camPos) + state.present(pWin) } + + state.step += 1 }) } -// Converts pixel coordinates to cell coordinates -func vecToCell(vec pixel.Vec) (int, int) { - x := int(vec.X) - y := int(vec.Y) - - if x < 0 { - x = 0 +func (e *env) getSprite(palette []string, ref maps.ObjRef) (*conv.Sprite, *conv.Object) { + name := palette[ref.Index()] + obj := e.objects[name] + if obj == nil { + log.Printf("Failed to find surface sprite %#v -> %q", ref, name) + return nil, nil } - if x > maps.MaxWidth-1 { - x = maps.MaxWidth - 1 + if ref.Frame() >= len(obj.Sprites) { + log.Printf("Out-of-index sprite %v requested for %v", ref.Frame(), name) + return nil, obj } - if y < 0 { - y = 0 - } - - if y > maps.MaxLength-1 { - y = maps.MaxLength - 1 - } - - return x, y + return &obj.Sprites[ref.Frame()], obj } -func cellToVec(x, y int) pixel.Rect { - min := pixel.Vec{X: float64(x), Y: float64(y)} - max := pixel.Vec{X: min.X + 1, Y: min.Y + 1} - return pixel.Rect{Min: min, Max: max} -} +var ( + cellWidth = 128 // I think, anyway + cellHeight = 64 +) -func present(win *pixelgl.Window, state *runState) { - gameMap := state.env.gameMap - imd := imdraw.New(nil) +// TODO: build all the sprites in the set into a single spritesheet so we can +// use pixel.Batch +func (s *state) present(pWin *pixelgl.Window) { + gameMap := s.env.gameMap - for y := gameMap.MinLength; y < gameMap.MaxLength; y++ { - for x := gameMap.MinWidth; x < gameMap.MaxWidth; x++ { - rect := cellToVec(int(x), int(y)) - cell := gameMap.Cells.At(int(x), int(y), int(state.zIdx)) + pWin.Clear(colornames.Black) - // TODO: represent the state of the cell *sensibly*, using colour. - // Need to understand the contents better first, so for now optimize - // for exploration - imd.Color = makeColour(&cell, state.cellIdx) - imd.Push(rect.Min, rect.Max) - imd.Rectangle(0.0) - } - } - - // Draw the boundary - rect := pixel.R( - float64(gameMap.MinWidth)-0.5, float64(gameMap.MinLength)-0.5, - float64(gameMap.MaxWidth)+0.5, float64(gameMap.MaxLength)+0.5, - ) - - imd.Color = pixel.RGB(255, 0, 0) - imd.EndShape = imdraw.SharpEndShape - imd.Push(rect.Min, rect.Max) - imd.Rectangle(1.0) - - center := win.Bounds().Center() + center := pWin.Bounds().Center() cam := pixel.IM - cam = cam.ScaledXY(pixel.ZV, pixel.Vec{1.0, -1.0}) // invert the Y axis - cam = cam.Scaled(pixel.ZV, state.zoom) // apply current zoom factor - cam = cam.Moved(center.Sub(state.camPos)) // Make it central - cam = cam.Rotated(center.Sub(state.camPos), -0.785) // Apply isometric angle - state.cam = cam + // cam = cam.ScaledXY(center, pixel.Vec{1.0, -1.0}) // invert the Y axis + cam = cam.Scaled(pixel.ZV, s.zoom) // apply current zoom factor + cam = cam.Moved(center.Sub(s.camPos)) // Make it central + // cam = cam.Rotated(center.Sub(s.camPos), -0.785) // Apply isometric angle + s.cam = cam + pWin.SetMatrix(s.cam) - win.SetMatrix(state.cam) - win.Clear(colornames.Black) - imd.Draw(win) + // TODO: bounds clipping + z := int(s.zIdx) + for y := int(gameMap.MinLength); y < int(gameMap.MaxLength); y++ { + for x := int(gameMap.MinWidth); x < int(gameMap.MaxWidth); x++ { + + cell := gameMap.Cells.At(x, y, z) + + // TODO: optimize drawing, etc + surfaceSprite, surfaceObj := s.env.getSprite(s.env.set.SurfacePalette, cell.Surface) + if surfaceSprite == nil { + log.Printf("no surfaceSprite") + continue + } + + if surfaceSprite.Width != cellWidth { + log.Printf("WARN: Surface sprite has wrong width: %v", surfaceSprite.Width) + } + + yPos := (y - int(gameMap.MinLength)) * cellHeight + xPos := (x - int(gameMap.MinWidth)) * cellWidth + + // Tiles should be flush to each other. Offset odd-numbered tiles up + // and right to get the effect + if y%2 == 1 { + yPos -= cellHeight * 2 + xPos += (cellWidth / 2) + } + + pic := surfaceSprite.Pic + spr := pixel.NewSprite(pic, pic.Bounds()) + log.Printf( + "cell(%v,%v,%v): %s %d: pix(%v,%v - %v,%v)", + x, y, z, surfaceObj.Name, cell.Surface.Index(), + xPos, yPos, xPos+surfaceSprite.Width, yPos+surfaceSprite.Height, + ) + spr.Draw(pWin, pixel.IM.Moved(pixel.V(float64(xPos), float64(yPos)))) + } + } } -func makeColour(cell *maps.Cell, colIdx int) pixel.RGBA { - var scale func(float64) float64 +func (s *state) runStep(pWin *pixelgl.Window) *state { + newState := *s + newState.handleKeys(pWin) - mult := func(factor float64) func(float64) float64 { - return func(in float64) float64 { return in * factor } - } - - // Different columns do better with different levels of greyscale. - - switch colIdx { - case 0: - scale = mult(0.004) - case 1: - scale = mult(0.1) - case 2: - scale = mult(1.0) - case 3: - scale = mult(0.1) - case 4: - scale = func(in float64) float64 { return mult(0.01)(in - 100) } - case 10: - scale = func(in float64) float64 { return mult(0.01)(in - 100) } - case 12: - scale = mult(0.004) - case 13: - scale = mult(0.004) - case 14: - scale = mult(0.004) - case 15: - scale = mult(1.0) - default: - scale = mult(0.01) // close to maximum resolution, low-value fields will be lost - } - - col := scale(float64(cell.At(colIdx))) - return pixel.RGB(col, col, col) + return &newState } -func runStep(win *pixelgl.Window, state *runState) *runState { - nextState := *state +func (s *state) handleKeys(pWin *pixelgl.Window) { + if pWin.Pressed(pixelgl.KeyLeft) { + s.camPos.X -= 64 + } - // Enable / disable auto-update with the enter key - if win.JustPressed(pixelgl.KeyEnter) { - nextState.autoUpdate = !state.autoUpdate - log.Printf("autoUpdate=%v", nextState.autoUpdate) + if pWin.Pressed(pixelgl.KeyRight) { + s.camPos.X += 64 + } - if nextState.autoUpdate { - nextState.started = time.Now() - } - } - - // Automatically cycle every second when auto-update is on - if nextState.autoUpdate && time.Now().Sub(state.started) > 500*time.Millisecond { - nextState.cellIdx = nextState.cellIdx + 1 - if nextState.cellIdx >= maps.CellSize { - nextState.cellIdx = 0 - nextState.zIdx = nextState.zIdx + 1 - } - - if nextState.zIdx >= maps.MaxHeight { - nextState.zIdx = 0 - } - - nextState.started = time.Now() - } - - if win.Pressed(pixelgl.KeyLeft) { - nextState.camPos.X -= 4 - } - - if win.Pressed(pixelgl.KeyRight) { - nextState.camPos.X += 4 - } - - if win.Pressed(pixelgl.KeyDown) { - nextState.camPos.Y -= 4 - } - - if win.Pressed(pixelgl.KeyUp) { - nextState.camPos.Y += 4 - } - - for i := 0; i <= 6; i++ { - if win.JustPressed(pixelgl.Key1 + pixelgl.Button(i)) { - nextState.zIdx = i - } - } - - // Decrease the cell index - if win.JustPressed(pixelgl.KeyMinus) { - if nextState.cellIdx > 0 { - nextState.cellIdx -= 1 - } - } - - // Increase the cell index - if win.JustPressed(pixelgl.KeyEqual) { - if nextState.cellIdx < maps.CellSize-1 { - nextState.cellIdx += 1 + if pWin.Pressed(pixelgl.KeyDown) { + s.camPos.Y -= 64 + } + + if pWin.Pressed(pixelgl.KeyUp) { + s.camPos.Y += 64 + } + + for i := 1; i <= 7; i++ { + if pWin.JustPressed(pixelgl.Key0 + pixelgl.Button(i)) { + s.zIdx = i - 1 } } + /* TODO: restore this // Show details of clicked-on cell in termal - if win.JustPressed(pixelgl.MouseButtonLeft) { - vec := state.cam.Unproject(win.MousePosition()) + if pWin.JustPressed(pixelgl.MouseButtonLeft) { + vec := s.cam.Unproject(pWin.MousePosition()) x, y := vecToCell(vec) log.Printf("%#v -> %d,%d", vec, x, y) cell := state.env.gameMap.Cells.At(x, y, state.zIdx) @@ -284,12 +231,8 @@ func runStep(win *pixelgl.Window, state *runState) *runState { ) log.Printf("CellIdx%d=%d. Full cell data: %#v", state.cellIdx, cell.At(state.cellIdx), cell) } + */ // Zoom in and out with the mouse wheel - nextState.zoom *= math.Pow(1.2, win.MouseScroll().Y) - if nextState.zoom != state.zoom { - log.Printf("zoom=%.2f", nextState.zoom) - } - - return &nextState + s.zoom *= math.Pow(1.2, pWin.MouseScroll().Y) } diff --git a/cmd/view-minimap/main.go b/cmd/view-minimap/main.go new file mode 100644 index 0000000..8cd09a4 --- /dev/null +++ b/cmd/view-minimap/main.go @@ -0,0 +1,295 @@ +package main + +import ( + "flag" + "log" + "math" + "os" + "path/filepath" + "time" + + "github.com/faiface/pixel" + "github.com/faiface/pixel/imdraw" + "github.com/faiface/pixel/pixelgl" + "golang.org/x/image/colornames" + + "ur.gs/chaos-gate/internal/maps" + "ur.gs/chaos-gate/internal/sets" + "ur.gs/chaos-gate/internal/ui" +) + +var ( + gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation") + mapFile = flag.String("map", "", "Prefix path to a .map file, e.g. ./orig/Maps/Chapter01.MAP") + txtFile = flag.String("txt", "", "Prefix path to a .txt file, e.g. ./orig/Maps/Chapter01.txt") +) + +type env struct { + gameMap *maps.GameMap + set *sets.MapSet +} + +type runState struct { + env *env + + autoUpdate bool + started time.Time + + cam pixel.Matrix + camPos pixel.Vec + + zoom float64 + + zIdx int + cellIdx int +} + +func main() { + flag.Parse() + + if *gamePath == "" || *mapFile == "" || *txtFile == "" { + flag.Usage() + os.Exit(1) + } + + gameMap, err := maps.LoadGameMapByFiles(*mapFile, *txtFile) + if err != nil { + log.Fatalf("Couldn't load map file: %v", err) + } + + setFile := filepath.Join(*gamePath, "Sets", gameMap.MapSetFilename()) + log.Println(setFile) + mapSet, err := sets.LoadSet(setFile) + if err != nil { + log.Fatalf("Couldn't load set file %s: %v", setFile, err) + } + + env := &env{gameMap: gameMap, set: mapSet} + + // The main thread now belongs to pixelgl + pixelgl.Run(env.run) +} + +func (e *env) run() { + win, err := ui.NewWindow("View Map " + *mapFile) + if err != nil { + log.Fatal("Couldn't create window: %v", err) + } + + pWin := win.PixelWindow + state := &runState{ + env: e, + autoUpdate: true, + camPos: pixel.V(0, float64(-pWin.Bounds().Size().Y)), + zoom: 8.0, + } + + win.Run(func() { + oldState := *state + state = runStep(pWin, state) + + if oldState != *state { + log.Printf("z=%d cellIdx=%d", state.zIdx, state.cellIdx) + present(pWin, state) + } + }) +} + +// Converts pixel coordinates to cell coordinates +func vecToCell(vec pixel.Vec) (int, int) { + x := int(vec.X) + y := int(vec.Y) + + if x < 0 { + x = 0 + } + + if x > maps.MaxWidth-1 { + x = maps.MaxWidth - 1 + } + + if y < 0 { + y = 0 + } + + if y > maps.MaxLength-1 { + y = maps.MaxLength - 1 + } + + return x, y +} + +func cellToVec(x, y int) pixel.Rect { + min := pixel.Vec{X: float64(x), Y: float64(y)} + max := pixel.Vec{X: min.X + 1, Y: min.Y + 1} + return pixel.Rect{Min: min, Max: max} +} + +func present(win *pixelgl.Window, state *runState) { + gameMap := state.env.gameMap + imd := imdraw.New(nil) + + for y := gameMap.MinLength; y < gameMap.MaxLength; y++ { + for x := gameMap.MinWidth; x < gameMap.MaxWidth; x++ { + rect := cellToVec(int(x), int(y)) + cell := gameMap.Cells.At(int(x), int(y), int(state.zIdx)) + + // TODO: represent the state of the cell *sensibly*, using colour. + // Need to understand the contents better first, so for now optimize + // for exploration + imd.Color = makeColour(&cell, state.cellIdx) + imd.Push(rect.Min, rect.Max) + imd.Rectangle(0.0) + } + } + + // Draw the boundary + rect := pixel.R( + float64(gameMap.MinWidth)-0.5, float64(gameMap.MinLength)-0.5, + float64(gameMap.MaxWidth)+0.5, float64(gameMap.MaxLength)+0.5, + ) + + imd.Color = pixel.RGB(255, 0, 0) + imd.EndShape = imdraw.SharpEndShape + imd.Push(rect.Min, rect.Max) + imd.Rectangle(1.0) + + center := win.Bounds().Center() + + cam := pixel.IM + cam = cam.ScaledXY(pixel.ZV, pixel.Vec{1.0, -1.0}) // invert the Y axis + cam = cam.Scaled(pixel.ZV, state.zoom) // apply current zoom factor + cam = cam.Moved(center.Sub(state.camPos)) // Make it central + cam = cam.Rotated(center.Sub(state.camPos), -0.785) // Apply isometric angle + state.cam = cam + + win.SetMatrix(state.cam) + win.Clear(colornames.Black) + imd.Draw(win) +} + +func makeColour(cell *maps.Cell, colIdx int) pixel.RGBA { + var scale func(float64) float64 + + mult := func(factor float64) func(float64) float64 { + return func(in float64) float64 { return in * factor } + } + + // Different columns do better with different levels of greyscale. + + switch colIdx { + case 0: + scale = mult(0.004) + case 1: + scale = mult(0.1) + case 2: + scale = mult(1.0) + case 3: + scale = mult(0.1) + case 4: + scale = func(in float64) float64 { return mult(0.01)(in - 100) } + case 10: + scale = func(in float64) float64 { return mult(0.01)(in - 100) } + case 12: + scale = mult(0.004) + case 13: + scale = mult(0.004) + case 14: + scale = mult(0.004) + case 15: + scale = mult(1.0) + default: + scale = mult(0.01) // close to maximum resolution, low-value fields will be lost + } + + col := scale(float64(cell.At(colIdx))) + return pixel.RGB(col, col, col) +} + +func runStep(win *pixelgl.Window, state *runState) *runState { + nextState := *state + + // Enable / disable auto-update with the enter key + if win.JustPressed(pixelgl.KeyEnter) { + nextState.autoUpdate = !state.autoUpdate + log.Printf("autoUpdate=%v", nextState.autoUpdate) + + if nextState.autoUpdate { + nextState.started = time.Now() + } + } + + // Automatically cycle every second when auto-update is on + if nextState.autoUpdate && time.Now().Sub(state.started) > 500*time.Millisecond { + nextState.cellIdx = nextState.cellIdx + 1 + if nextState.cellIdx >= maps.CellSize { + nextState.cellIdx = 0 + nextState.zIdx = nextState.zIdx + 1 + } + + if nextState.zIdx >= maps.MaxHeight { + nextState.zIdx = 0 + } + + nextState.started = time.Now() + } + + if win.Pressed(pixelgl.KeyLeft) { + nextState.camPos.X -= 4 + } + + if win.Pressed(pixelgl.KeyRight) { + nextState.camPos.X += 4 + } + + if win.Pressed(pixelgl.KeyDown) { + nextState.camPos.Y -= 4 + } + + if win.Pressed(pixelgl.KeyUp) { + nextState.camPos.Y += 4 + } + + for i := 0; i <= 6; i++ { + if win.JustPressed(pixelgl.Key1 + pixelgl.Button(i)) { + nextState.zIdx = i + } + } + + // Decrease the cell index + if win.JustPressed(pixelgl.KeyMinus) { + if nextState.cellIdx > 0 { + nextState.cellIdx -= 1 + } + } + + // Increase the cell index + if win.JustPressed(pixelgl.KeyEqual) { + if nextState.cellIdx < maps.CellSize-1 { + nextState.cellIdx += 1 + } + } + + // Show details of clicked-on cell in termal + if win.JustPressed(pixelgl.MouseButtonLeft) { + vec := state.cam.Unproject(win.MousePosition()) + x, y := vecToCell(vec) + log.Printf("%#v -> %d,%d", vec, x, y) + cell := state.env.gameMap.Cells.At(x, y, state.zIdx) + log.Printf( + "x=%d y=%d z=%d SurfaceTile=%d (%s) SurfaceFrame=%d SquadRelated=%d", + x, y, state.zIdx, + cell.Surface.Index(), state.env.set.SurfacePalette[int(cell.Surface.Index())], cell.Surface.Frame(), + cell.SquadRelated, + ) + log.Printf("CellIdx%d=%d. Full cell data: %#v", state.cellIdx, cell.At(state.cellIdx), cell) + } + + // Zoom in and out with the mouse wheel + nextState.zoom *= math.Pow(1.2, win.MouseScroll().Y) + if nextState.zoom != state.zoom { + log.Printf("zoom=%.2f", nextState.zoom) + } + + return &nextState +} diff --git a/cmd/view-set/main.go b/cmd/view-set/main.go index 55c2a85..403f34b 100644 --- a/cmd/view-set/main.go +++ b/cmd/view-set/main.go @@ -100,10 +100,10 @@ func (e *env) run() { state.spriteIdx, state.zoom, ) - state.step += 1 - state.present(pWin) } + + state.step += 1 }) } diff --git a/internal/conv/object.go b/internal/conv/object.go index 90ce43e..345a4c1 100644 --- a/internal/conv/object.go +++ b/internal/conv/object.go @@ -43,6 +43,8 @@ func ConvertObject(rawObj *data.Object, name string) *Object { return out } +var transparent = color.RGBA{0, 0, 0, 0} + // WIP. Try to convert the pixeldata into a picture. func spriteToPic(sprite *data.Sprite) *pixel.PictureData { pic := pixel.MakePictureData(pixel.R(float64(0), float64(0), float64(sprite.Width), float64(sprite.Height))) @@ -77,9 +79,14 @@ func spriteToPic(sprite *data.Sprite) *pixel.PictureData { // Ignore the record separator rowData = rowData[0 : len(rowData)-1] - leftPad := (int(sprite.Width) - len(rowData)) / 2 + // Set all bytes to be transparent by default + for allX := 0; allX < int(sprite.Width); allX++ { + idx := pic.Index(pixel.V(float64(allX), float64(y))) + pic.Pix[idx] = transparent + } + for x, b := range rowData { idx := pic.Index(pixel.V(float64(leftPad+x), float64(y))) r, g, b, a := data.ColorPalette[int(b)].RGBA()