This makes menus display more correctly, and also fixes trees and other objects on the main map, although it messes up bounds clipping (sigh).
229 lines
4.9 KiB
Go
229 lines
4.9 KiB
Go
package main
|
|
|
|
import (
|
|
"flag"
|
|
"image"
|
|
"log"
|
|
"math"
|
|
"os"
|
|
"sort"
|
|
|
|
"github.com/hajimehoshi/ebiten"
|
|
|
|
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
|
"code.ur.gs/lupine/ordoor/internal/ui"
|
|
)
|
|
|
|
var (
|
|
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
|
|
gameMap = flag.String("map", "", "Name of a map, e.g., Chapter01")
|
|
)
|
|
|
|
type env struct {
|
|
assets *assetstore.AssetStore
|
|
area *assetstore.Map
|
|
|
|
step int
|
|
state state
|
|
lastState state
|
|
}
|
|
|
|
type state struct {
|
|
zoom float64
|
|
origin image.Point
|
|
zIdx int
|
|
}
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
|
|
if *gamePath == "" || *gameMap == "" {
|
|
flag.Usage()
|
|
os.Exit(1)
|
|
}
|
|
|
|
assets, err := assetstore.New(*gamePath)
|
|
if err != nil {
|
|
log.Fatalf("Failed to scan root directory %v: %v", *gamePath, err)
|
|
}
|
|
|
|
area, err := assets.Map(*gameMap)
|
|
if err != nil {
|
|
log.Fatalf("Failed to load map %v: %v", *gameMap, err)
|
|
}
|
|
|
|
state := state{
|
|
zoom: 1.0,
|
|
origin: image.Point{0, 3000}, // FIXME: haxxx
|
|
zIdx: 1,
|
|
}
|
|
env := &env{
|
|
area: area,
|
|
assets: assets,
|
|
state: state,
|
|
lastState: state,
|
|
}
|
|
|
|
win, err := ui.NewWindow("View Map " + *gameMap)
|
|
if err != nil {
|
|
log.Fatal("Couldn't create window: %v", err)
|
|
}
|
|
|
|
// TODO: click to view cell data
|
|
|
|
win.OnKeyUp(ebiten.KeyLeft, env.changeOrigin(-64, +0))
|
|
win.OnKeyUp(ebiten.KeyRight, env.changeOrigin(+64, +0))
|
|
win.OnKeyUp(ebiten.KeyUp, env.changeOrigin(+0, -64))
|
|
win.OnKeyUp(ebiten.KeyDown, env.changeOrigin(+0, +64))
|
|
win.OnMouseWheel(env.changeZoom)
|
|
|
|
for i := 0; i < 6; i++ {
|
|
win.OnKeyUp(ebiten.Key1+ebiten.Key(i), env.setZIdx(i+1))
|
|
}
|
|
|
|
if err := win.Run(env.Update, env.Draw); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func (e *env) Update() error {
|
|
if e.step == 0 || e.lastState != e.state {
|
|
log.Printf("zoom=%.2f zIdx=%v camPos=%#v", e.state.zoom, e.state.zIdx, e.state.origin)
|
|
}
|
|
|
|
e.lastState = e.state
|
|
e.step += 1
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *env) Draw(screen *ebiten.Image) error {
|
|
// Bounds clipping
|
|
// http://www.java-gaming.org/index.php?topic=24922.0
|
|
// https://stackoverflow.com/questions/892811/drawing-isometric-game-worlds
|
|
// https://gamedev.stackexchange.com/questions/25896/how-do-i-find-which-isometric-tiles-are-inside-the-cameras-current-view
|
|
|
|
sw, sh := screen.Size()
|
|
|
|
topLeft := pixToCell(e.state.origin)
|
|
topLeft.X -= 1 // Otherwise we miss half a cell on alternate rows on the left
|
|
|
|
bottomRight := pixToCell(image.Pt(e.state.origin.X+sw, e.state.origin.Y+sh))
|
|
|
|
// X+Y is constant for all tiles in a column
|
|
// X-Y is constant for all tiles in a row
|
|
// However, the drawing order is odd unless we reorder explicitly.
|
|
toDraw := []image.Point{}
|
|
for a := topLeft.X + topLeft.Y; a <= bottomRight.X+bottomRight.Y; a++ {
|
|
for b := topLeft.X - topLeft.Y; b <= bottomRight.X-bottomRight.Y; b++ {
|
|
if b&1 != a&1 {
|
|
continue
|
|
}
|
|
|
|
pt := image.Pt((a+b)/2, (a-b)/2)
|
|
|
|
if !pt.In(e.area.Rect) {
|
|
continue
|
|
}
|
|
toDraw = append(toDraw, pt)
|
|
}
|
|
}
|
|
|
|
sort.Slice(toDraw, func(i, j int) bool {
|
|
iPix := cellToPix(toDraw[i])
|
|
jPix := cellToPix(toDraw[j])
|
|
|
|
if iPix.Y < jPix.Y {
|
|
return true
|
|
}
|
|
|
|
if iPix.Y == jPix.Y {
|
|
return iPix.X < jPix.X
|
|
}
|
|
|
|
return false
|
|
})
|
|
|
|
for _, pt := range toDraw {
|
|
for z := 0; z <= e.state.zIdx; z++ {
|
|
if err := e.renderCell(pt.X, pt.Y, z, screen); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *env) renderCell(x, y, z int, screen *ebiten.Image) error {
|
|
sprites, err := e.area.SpritesForCell(x, y, z)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
iso := ebiten.GeoM{}
|
|
iso.Translate(-float64(e.state.origin.X), -float64(e.state.origin.Y))
|
|
|
|
pix := cellToPix(image.Pt(x, y))
|
|
iso.Translate(float64(pix.X), float64(pix.Y))
|
|
|
|
if e.step%30 == 0 {
|
|
// log.Printf("x=%v y=%v z=%v", pix.X-e.state.origin.X, pix.Y-e.state.origin.Y, z)
|
|
}
|
|
|
|
// Taking the Z index away *seems* to draw the object in the correct place.
|
|
// FIXME: There are some artifacts, investigate more
|
|
iso.Translate(0.0, -float64(z*48.0)) // offset for Z index
|
|
|
|
// TODO: iso.Scale(e.state.zoom, e.state.zoom) // apply current zoom factor
|
|
|
|
for _, spr := range sprites {
|
|
iso.Translate(float64(spr.XOffset), float64(spr.YOffset))
|
|
if err := screen.DrawImage(spr.Image, &ebiten.DrawImageOptions{GeoM: iso}); err != nil {
|
|
return err
|
|
}
|
|
iso.Translate(float64(-spr.XOffset), float64(-spr.YOffset))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *env) changeOrigin(byX, byY int) func() {
|
|
return func() {
|
|
e.state.origin.X += byX
|
|
e.state.origin.Y += byY
|
|
}
|
|
}
|
|
|
|
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) setZIdx(to int) func() {
|
|
return func() {
|
|
e.state.zIdx = to
|
|
}
|
|
}
|
|
|
|
const (
|
|
cellWidth = 64
|
|
cellHeight = 64
|
|
)
|
|
|
|
// Doesn't take the camera or Z level into account
|
|
func cellToPix(pt image.Point) image.Point {
|
|
return image.Pt(
|
|
(pt.X-pt.Y)*cellWidth,
|
|
(pt.X+pt.Y)*cellHeight/2,
|
|
)
|
|
}
|
|
|
|
// Doesn't take the camera or Z level into account
|
|
func pixToCell(pt image.Point) image.Point {
|
|
return image.Pt(
|
|
pt.Y/cellHeight+pt.X/(cellWidth*2),
|
|
pt.Y/cellHeight-pt.X/(cellWidth*2),
|
|
)
|
|
}
|