package main import ( "flag" "fmt" "log" "math" "os" "path/filepath" "time" "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 batch *pixel.Batch } type state struct { env *env step int fpsTicker <-chan time.Time 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) } rawObjs := []*data.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) } obj.Name = name rawObjs = append(rawObjs, obj) } objects, spritesheet := conv.ConvertObjects(rawObjs) batch := pixel.NewBatch(&pixel.TrianglesData{}, spritesheet) env := &env{ gameMap: gameMap, set: mapSet, objects: conv.MapByName(objects), batch: batch, } // The main thread now belongs to pixelgl pixelgl.Run(env.run) } func (e *env) run() { title := "View Map " + *mapFile win, err := ui.NewWindow(title + " | FPS: ?") 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.785, step: -1, fpsTicker: time.Tick(time.Second), } pWin.SetSmooth(true) win.Run(func() { oldState := *state state = state.runStep(pWin) if oldState != *state || oldState.step == -1 { log.Printf("zoom=%.2f rot=%.4f zIdx=%v camPos=%#v", state.zoom, state.rot, state.zIdx, state.camPos) state.present(pWin) } state.step += 1 select { case <-state.fpsTicker: pWin.SetTitle(fmt.Sprintf("%s | FPS: %d", title, state.step)) state.step = 0 default: } }) } func (e *env) getSprite(palette []string, ref maps.ObjRef) (*conv.Sprite, error) { // There seems to be an active bit that hides many sins if !ref.IsActive() { return nil, nil } if ref.Index() >= len(palette) { return nil, fmt.Errorf("Palette too small: %v requested", ref.Index()) } name := palette[ref.Index()] obj := e.objects[name] if obj == nil { return nil, fmt.Errorf("Failed to find surface sprite %#v -> %q", ref, name) } if ref.Sprite() >= len(obj.Sprites) { return nil, fmt.Errorf("Out-of-index sprite %v requested for %v", ref.Sprite(), name) } return obj.Sprites[ref.Sprite()], nil } func (s *state) present(pWin *pixelgl.Window) { pWin.Clear(colornames.Black) s.env.batch.Clear() center := pWin.Bounds().Center() cam := pixel.IM cam = cam.ScaledXY(center, pixel.Vec{1.0, -1.0}) // invert the Y axis cam = cam.Scaled(center, s.zoom) // apply current zoom factor cam = cam.Moved(center.Sub(s.camPos)) // Make it central s.cam = cam pWin.SetMatrix(cam) // TODO: we should be able to perform bounds clipping on these minX := int(s.env.gameMap.MinWidth) maxX := int(s.env.gameMap.MaxWidth) minY := int(s.env.gameMap.MinLength) maxY := int(s.env.gameMap.MaxLength) minZ := 0 maxZ := int(s.zIdx) + 1 for z := minZ; z < maxZ; z++ { for y := minY; y < maxY; y++ { for x := minX; x < maxX; x++ { s.renderCell(x, y, z, s.env.batch) } } } s.env.batch.Draw(pWin) pWin.Update() } func (s *state) renderCell(x, y, z int, target pixel.Target) { var sprites []*conv.Sprite cell := s.env.gameMap.Cells.At(x, y, z) if spr, err := s.env.getSprite(s.env.set.Palette, cell.Surface); err != nil { log.Printf("%v %v %v surface: %v", x, y, z, err) } else if spr != nil { sprites = append(sprites, spr) } if spr, err := s.env.getSprite(s.env.set.Palette, cell.Center); err != nil { log.Printf("%v %v %v center: %v", x, y, z, err) } else if spr != nil { sprites = append(sprites, spr) } if spr, err := s.env.getSprite(s.env.set.Palette, cell.Left); err != nil { log.Printf("%v %v %v left: %v", x, y, z, err) } else if spr != nil { sprites = append(sprites, spr) } if spr, err := s.env.getSprite(s.env.set.Palette, cell.Right); err != nil { log.Printf("%v %v %v right: %v", x, y, z, err) } else if spr != nil { sprites = append(sprites, spr) } // Taking the Z index away *seems* to draw the object in the correct place. // FIXME: There are some artifacts, investigate more fX := float64(x) fY := float64(y) iso := s.cellToPix(pixel.V(fX, fY)) iso = iso.Add(pixel.Vec{0.0, -float64(z * 48.0)}) for _, sprite := range sprites { sprite.Spr.Draw(target, pixel.IM.Moved(iso)) } } var ( cellWidth = 64.0 cellHeight = 64.0 ) // Doesn't take the camera or Z level into account func (s *state) cellToPix(cell pixel.Vec) pixel.Vec { return pixel.V( (cell.X-cell.Y)*cellWidth, (cell.X+cell.Y)*cellHeight/2.0, ) } // Doesn't take the camera or Z level into account func (s *state) pixToCell(pix pixel.Vec) pixel.Vec { return pixel.V( pix.Y/cellHeight+pix.X/(cellWidth*2.0), pix.Y/cellHeight-pix.X/(cellWidth*2.0), ) } func (s *state) runStep(pWin *pixelgl.Window) *state { newState := *s newState.handleKeys(pWin) return &newState } func (s *state) handleKeys(pWin *pixelgl.Window) { // Do this first to avoid taking the below mutations into account // FIXME: this suggests we should pass the next state into here and // modify it instead if pWin.JustPressed(pixelgl.MouseButton1) { if s.zIdx != 0 { log.Printf("WARNING: z-index not yet taken into account") } log.Printf("cam: %#v", s.cam) pos := s.pixToCell(s.cam.Unproject(pWin.MousePosition())) log.Printf("X=%v Y=%v, zIdx=%v", pos.X, pos.Y, s.zIdx) cell := s.env.gameMap.Cells.At(int(pos.X), int(pos.Y), s.zIdx) log.Printf("Cell=%#v", cell) } 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 } // Zoom in and out with the mouse wheel s.zoom *= math.Pow(1.2, pWin.MouseScroll().Y) }