package main import ( "flag" "log" "math" "os" "path/filepath" "github.com/faiface/pixel" "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" ) 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 objects map[string]*conv.Object } type state struct { env *env step int cam pixel.Matrix camPos pixel.Vec zoom float64 zIdx 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) } 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) } 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 := &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 = state.runStep(pWin) 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 }) } 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 ref.Frame() >= len(obj.Sprites) { log.Printf("Out-of-index sprite %v requested for %v", ref.Frame(), name) return nil, obj } return &obj.Sprites[ref.Frame()], obj } var ( cellWidth = 128 // I think, anyway cellHeight = 64 ) // 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 pWin.Clear(colornames.Black) center := pWin.Bounds().Center() cam := pixel.IM // 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) // 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 (s *state) runStep(pWin *pixelgl.Window) *state { newState := *s newState.handleKeys(pWin) return &newState } func (s *state) handleKeys(pWin *pixelgl.Window) { if pWin.Pressed(pixelgl.KeyLeft) { s.camPos.X -= 64 } if pWin.Pressed(pixelgl.KeyRight) { s.camPos.X += 64 } 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 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) 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 s.zoom *= math.Pow(1.2, pWin.MouseScroll().Y) }