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/ordoor/internal/conv" "ur.gs/ordoor/internal/data" "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 objects map[string]*conv.Object sprites map[string][]*pixel.Sprite } type state struct { env *env step int cam pixel.Matrix camPos pixel.Vec zoom float64 rot 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, rot: 0.45, } pWin.SetSmooth(true) win.Run(func() { oldState := *state state = state.runStep(pWin) if oldState != *state || oldState.step == 0 { log.Printf("zoom=%.2f rot=%.4f zIdx=%v camPos=%#v", state.zoom, state.rot, 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 = 63 ) // 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(cam) // TODO: bounds clipping z := int(s.zIdx) for x := int(gameMap.MinWidth); x < int(gameMap.MaxWidth); x++ { for y := int(gameMap.MinLength); y < int(gameMap.MaxLength); y++ { cell := gameMap.Cells.At(x, y, z) surfaceSprite, obj := s.env.getSprite(s.env.set.SurfacePalette, cell.Surface) fX := float64(x) fY := float64(y) fWidth := float64(cellWidth) fLength := float64(cellHeight) // numCellsToOffset := math.Abs(float64(gameMap.Width() / 2) - fY) // xOffset := numCellsToOffset * (fWidth/2) //yOffset := 0.0 //numCellsToOffset * (fLength/2) xPos := (fX - fY) * fWidth / 2 // - xOffset yPos := fY * fLength // - yOffset // The rotation translates the rectangular coordinates to diamond // ones \o/ orig := pixel.V(xPos, yPos) rotated := orig rotated = rotated.Rotated(s.rot) // rotated = rotated.Rotated(0.464) // 26.565' // rotated = orig.Rotated(0.35) // rotated = rotated.Rotated(0.524) // 30' // rotated = rotated.Rotated(0.785) // 45' // rotated = rotated.Rotated(1.571) // 90' log.Printf( "cell(%v,%v): %s %d: %#v -> %#v", x, y, obj.Name, cell.Surface.Index(), orig, rotated, ) surfaceSprite.Spr.Draw(pWin, pixel.IM.Moved(rotated)) } } } 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 } } if pWin.Pressed(pixelgl.KeyMinus) { s.rot -= 0.001 } if pWin.Pressed(pixelgl.KeyEqual) { s.rot += 0.001 } /* 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) }