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/ordoor/internal/maps" "ur.gs/ordoor/internal/sets" "ur.gs/ordoor/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) SurfaceSprite=%d SquadRelated=%d", x, y, state.zIdx, cell.Surface.Index(), state.env.set.SurfacePalette[int(cell.Surface.Index())], cell.Surface.Sprite(), 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 }