package main import ( "flag" "image" "image/color" "log" "math" "os" "path/filepath" "time" "github.com/hajimehoshi/ebiten" "code.ur.gs/lupine/ordoor/internal/maps" "code.ur.gs/lupine/ordoor/internal/sets" "code.ur.gs/lupine/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") 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 { gameMap *maps.GameMap set *sets.MapSet state state lastState state step int } type state struct { autoUpdate bool started time.Time origin image.Point 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) } state := state{ autoUpdate: true, zoom: 8.0, } env := &env{gameMap: gameMap, set: mapSet, state: state, lastState: state} win, err := ui.NewWindow(env, "View Map "+*mapFile, *winX, *winY) if err != nil { log.Fatal("Couldn't create window: %v", err) } win.OnKeyUp(ebiten.KeyEnter, env.toggleAutoUpdate) win.OnKeyUp(ebiten.KeyLeft, env.changeOrigin(+4, +0)) win.OnKeyUp(ebiten.KeyRight, env.changeOrigin(-4, +0)) win.OnKeyUp(ebiten.KeyUp, env.changeOrigin(+0, +4)) win.OnKeyUp(ebiten.KeyDown, env.changeOrigin(+0, -4)) win.OnKeyUp(ebiten.KeyMinus, env.changeCellIdx(-1)) win.OnKeyUp(ebiten.KeyEqual, env.changeCellIdx(+1)) for i := 0; i <= 6; i++ { win.OnKeyUp(ebiten.Key1+ebiten.Key(i), env.setZIdx(i)) } win.OnMouseWheel(env.changeZoom) if err := win.Run(); err != nil { log.Fatal(err) } } func (e *env) setZIdx(to int) func() { return func() { e.state.zIdx = to } } // Enable / disable auto-update func (e *env) toggleAutoUpdate() { e.state.autoUpdate = !e.state.autoUpdate if e.state.autoUpdate { e.state.started = time.Now() } } func (e *env) changeOrigin(byX, byY int) func() { return func() { e.state.origin.X += byX e.state.origin.Y += byY } } func (e *env) changeCellIdx(by int) func() { return func() { e.state.cellIdx += by if e.state.cellIdx < 0 { e.state.cellIdx = 0 } if e.state.cellIdx > maps.CellSize-1 { e.state.cellIdx = maps.CellSize - 1 } } } func (e *env) changeZoom(_, y float64) { // Zoom in and out with the mouse wheel e.state.zoom *= math.Pow(1.2, y) } func (e *env) Update(screenX, screenY int) error { // TODO: show details of clicked-on cell in terminal // Automatically cycle every 500ms when auto-update is on if e.state.autoUpdate && time.Now().Sub(e.state.started) > 500*time.Millisecond { e.state.cellIdx += 1 // bounds checking if e.state.cellIdx >= maps.CellSize { e.state.cellIdx = 0 e.state.zIdx += 1 } if e.state.zIdx >= maps.MaxHeight { e.state.zIdx = 0 } e.state.started = time.Now() } if e.step == 0 || e.lastState != e.state { log.Printf("z=%d cellIdx=%d origin=%#v", e.state.zIdx, e.state.cellIdx, e.state.origin) } e.step += 1 e.lastState = e.state return nil } func (e *env) Draw(screen *ebiten.Image) error { gameMap := e.gameMap imd, err := ebiten.NewImage( int(gameMap.MaxWidth), int(gameMap.MaxLength), ebiten.FilterDefault, ) if err != nil { return err } 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, int(e.state.zIdx)) imd.Set(x, y, makeColour(&cell, e.state.cellIdx)) } } // TODO: draw a boundary around the minimap 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 cam.Rotate(0.785) // Apply isometric angle return screen.DrawImage(imd, &ebiten.DrawImageOptions{GeoM: cam}) } // Converts pixel coordinates to cell coordinates func vecToCell(p image.Point) (int, int) { x := int(p.X) y := int(p.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) image.Rectangle { min := image.Point{X: x, Y: y} max := image.Point{X: min.X + 1, Y: min.Y + 1} return image.Rect(min.X, min.Y, max.X, max.Y) } func makeColour(cell *maps.Cell, colIdx int) color.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. // FIXME: this may not be translated correctly from pixel 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 := uint8(scale(float64(cell.At(colIdx)))) return color.RGBA{col, col, col, 255} }