From 9d0750d1340133c46a9657f8180953b92bae1755 Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Mon, 20 Apr 2020 00:16:21 +0100 Subject: [PATCH] Scenario viewpoint, Z index management, and arrow controls --- cmd/view-map/main.go | 15 ++++-- internal/assetstore/map.go | 3 +- internal/flow/flow.go | 26 ++++++++++ internal/flow/main_game.go | 66 +++++++++++++++++++++--- internal/maps/maps.go | 2 +- internal/scenario/draw.go | 96 +++++++++++++++++++---------------- internal/scenario/manage.go | 13 +++++ internal/scenario/scenario.go | 2 + internal/ui/dialogues.go | 4 ++ internal/ui/window.go | 30 ++++++++--- 10 files changed, 194 insertions(+), 63 deletions(-) diff --git a/cmd/view-map/main.go b/cmd/view-map/main.go index 80d9bb5..48161c8 100644 --- a/cmd/view-map/main.go +++ b/cmd/view-map/main.go @@ -3,6 +3,7 @@ package main import ( "flag" "log" + "math" "os" "github.com/hajimehoshi/ebiten" @@ -51,16 +52,18 @@ func main() { log.Fatal("Couldn't create window: %v", err) } - 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)) + step := 32 + win.WhileKeyDown(ebiten.KeyLeft, env.changeOrigin(-step, +0)) + win.WhileKeyDown(ebiten.KeyRight, env.changeOrigin(+step, +0)) + win.WhileKeyDown(ebiten.KeyUp, env.changeOrigin(+0, -step)) + win.WhileKeyDown(ebiten.KeyDown, env.changeOrigin(+0, +step)) for i := 0; i <= 6; i++ { win.OnKeyUp(ebiten.Key1+ebiten.Key(i), env.setZIdx(i)) } win.OnMouseClick(env.showCellData) + win.OnMouseWheel(env.changeZoom) if err := win.Run(); err != nil { log.Fatal(err) @@ -82,6 +85,10 @@ func (e *env) changeOrigin(byX, byY int) func() { } } +func (e *env) changeZoom(_, byY float64) { + e.scenario.Zoom *= math.Pow(1.2, byY) +} + func (e *env) setZIdx(to int) func() { return func() { e.scenario.ZIdx = to diff --git a/internal/assetstore/map.go b/internal/assetstore/map.go index 0410d93..2450d08 100644 --- a/internal/assetstore/map.go +++ b/internal/assetstore/map.go @@ -1,6 +1,7 @@ package assetstore import ( + "fmt" "image" "log" @@ -95,7 +96,7 @@ func (m *Map) SpritesForCell(x, y, z int) ([]*Sprite, error) { obj, err := m.set.Object(ref.Index()) if err != nil { - return nil, err + return nil, fmt.Errorf("Failed to get object for %#+v: %v", ref, err) } sprite, err := obj.Sprite(ref.Sprite()) diff --git a/internal/flow/flow.go b/internal/flow/flow.go index bb51208..3ae1544 100644 --- a/internal/flow/flow.go +++ b/internal/flow/flow.go @@ -97,6 +97,24 @@ func (f *Flow) Update(screenX, screenY int) error { return f.exit } + // Keybindings for map control + // FIXME: this needs a big rethink + if f.current != nil && f.scenario != nil && !f.current.IsInDialogue() { + step := 32 + if ebiten.IsKeyPressed(ebiten.KeyLeft) { + f.scenario.Viewpoint.X -= step + } + if ebiten.IsKeyPressed(ebiten.KeyRight) { + f.scenario.Viewpoint.X += step + } + if ebiten.IsKeyPressed(ebiten.KeyUp) { + f.scenario.Viewpoint.Y -= step + } + if ebiten.IsKeyPressed(ebiten.KeyDown) { + f.scenario.Viewpoint.Y += step + } + } + if f.scenario != nil { if err := f.scenario.Update(screenX, screenY); err != nil { return err @@ -265,6 +283,14 @@ func (f *Flow) hideDialogue(driver driverName) func() { return f.drivers[driver].HideDialogue } +func (f *Flow) withScenario(then func()) func() { + return func() { + if f.scenario != nil { + then() + } + } +} + func (f *Flow) reset() { if f.exit != nil { return diff --git a/internal/flow/main_game.go b/internal/flow/main_game.go index b163320..eea2b3f 100644 --- a/internal/flow/main_game.go +++ b/internal/flow/main_game.go @@ -6,9 +6,9 @@ package flow func (f *Flow) linkMainGame() { f.linkMainGameActionMenu() f.linkMainGameInterfaceOptionsMenu() - // 5: Holding menu - // 6: View menu + f.linkMainGameViewMenu() + // 7: General character menu f.onClick(mainGame, "7.4", func() { // More button f.setActiveNow(mainGame, "7", false) @@ -26,10 +26,10 @@ func (f *Flow) linkMainGame() { // 11: Psyker spell dialogue // 12: Inventory dialogue f.onClick(mainGame, "12.21", f.hideDialogue(mainGame)) // Exit + // 13: exchange menu // 14: Map - // FIXME: Map should be right-aligned // 14.1: MAP_SPRITE // 14.2: Multiplier button (2x) f.onClick(mainGame, "14.3", f.setActive(mainGame, "14", false)) @@ -46,8 +46,6 @@ func (f *Flow) linkMainGame() { // and the dead space is filled by the "interface wing" sprites in the // background. Should we replicate this, or keep with the current scaling // behaviour? Which is better? - // - // FIXME: the menu bar should be at the bottom, not top, of the screen f.exit = maybeErr(mainGame, f.setActiveNow(mainGame, "15", false)) // Interface wing left f.exit = maybeErr(mainGame, f.setActiveNow(mainGame, "16", false)) // Interface wing right // 17: Grenade dialogue @@ -90,7 +88,10 @@ func (f *Flow) linkMainGameActionMenu() { func (f *Flow) linkMainGameInterfaceOptionsMenu() { // 4: Interface options menu f.onClick(mainGame, "4.1", f.setReturningDriver(mainGame, options)) // Options button - f.onClick(mainGame, "4.2", f.toggleActive(mainGame, "14")) // Map button + + // FIXME: map should be shown top-right, not top-left. We need to support 2x + // mode as well. + f.onClick(mainGame, "4.2", f.toggleActive(mainGame, "14")) // Map button // FIXME: mission objectives should be shown top-left, not centered f.onClick(mainGame, "4.3", f.toggleActive(mainGame, "18")) // Mission objectives @@ -100,3 +101,56 @@ func (f *Flow) linkMainGameInterfaceOptionsMenu() { // 4.6: Next enemy // 4.7: Total enemy text } + +func (f *Flow) linkMainGameViewMenu() { + // FIXME: all these buttons should show current state as well as have an + // effect + f.onClick(mainGame, "6.1", f.withScenario(func() { // View 100% + f.scenario.Zoom = 1.0 + })) + + f.onClick(mainGame, "6.2", f.withScenario(func() { // View 50% + f.scenario.Zoom = 0.5 + })) + + f.onClick(mainGame, "6.3", f.withScenario(func() { // View 25% + f.scenario.Zoom = 0.25 + })) + + f.onClick(mainGame, "6.4", f.withScenario(func() { // Z index up + f.scenario.ChangeZIdx(+1) + })) + + f.onClick(mainGame, "6.5", f.withScenario(func() { // Z index down + f.scenario.ChangeZIdx(-1) + })) + + f.onClick(mainGame, "6.6", f.withScenario(func() { // Z index 1 + f.scenario.ZIdx = 0 + })) + + f.onClick(mainGame, "6.7", f.withScenario(func() { // Z index 2 + f.scenario.ZIdx = 1 + })) + + f.onClick(mainGame, "6.8", f.withScenario(func() { // Z index 3 + f.scenario.ZIdx = 2 + })) + + f.onClick(mainGame, "6.9", f.withScenario(func() { // Z index 4 + f.scenario.ZIdx = 3 + })) + + f.onClick(mainGame, "6.10", f.withScenario(func() { // Z index 5 + f.scenario.ZIdx = 4 + })) + + f.onClick(mainGame, "6.11", f.withScenario(func() { // Z index 6 + f.scenario.ZIdx = 5 + })) + + f.onClick(mainGame, "6.12", f.withScenario(func() { // Z index 7 + f.scenario.ZIdx = 6 + })) + +} diff --git a/internal/maps/maps.go b/internal/maps/maps.go index 46c9598..68821ff 100644 --- a/internal/maps/maps.go +++ b/internal/maps/maps.go @@ -26,7 +26,7 @@ const ( CellSize = 16 // seems to be - cellDataOffset = 0x110 // tentatively + cellDataOffset = 0x110 // definitely cellCount = MaxHeight * MaxLength * MaxWidth ) diff --git a/internal/scenario/draw.go b/internal/scenario/draw.go index 4569d75..7c16d0b 100644 --- a/internal/scenario/draw.go +++ b/internal/scenario/draw.go @@ -22,16 +22,21 @@ type IsoPt struct { func (s *Scenario) Update(screenX, screenY int) error { s.tick += 1 - x, y := ebiten.CursorPosition() + geo := s.geoForCam() + geo.Translate(cellWidthHalf, 0) + geo.Scale(s.Zoom, s.Zoom) + geo.Invert() + + cX, cY := ebiten.CursorPosition() + x, y := geo.Apply(float64(cX), float64(cY)) + screenPos := CartPt{ - X: float64(s.Viewpoint.X + x), - Y: float64(s.Viewpoint.Y + y), + X: x, + Y: y, } - s.selectedCell = screenPos.ToISO() - - // TODO: zoom support will need a camera // FIXME: adjust for Z level + s.selectedCell = screenPos.ToISO() return nil } @@ -41,17 +46,18 @@ func (s *Scenario) Draw(screen *ebiten.Image) error { // 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 + // FIXME: we don't cope with zoom very neatly here sw, sh := screen.Size() topLeft := CartPt{ - X: float64(s.Viewpoint.X - 2*cellWidth), // Ensure all visible cells are rendered - Y: float64(s.Viewpoint.Y - 2*cellHeight), + X: float64(s.Viewpoint.X) - (2*cellWidth/s.Zoom), // Ensure all visible cells are rendered + Y: float64(s.Viewpoint.Y) - (2*cellHeight/s.Zoom), }.ToISO() bottomRight := CartPt{ - X: float64(s.Viewpoint.X + sw + 2*cellHeight), - Y: float64(s.Viewpoint.Y + sh + 5*cellHeight), // Z dimension requires it + X: float64(s.Viewpoint.X) + (float64(sw)/s.Zoom) + (2*cellHeight/s.Zoom), + Y: float64(s.Viewpoint.Y) + (float64(sh)/s.Zoom) + (5*cellHeight/s.Zoom), // Z dimension requires it }.ToISO() // X+Y is constant for all tiles in a column @@ -101,29 +107,53 @@ func (s *Scenario) Draw(screen *ebiten.Image) error { //log.Printf("%#+v", counter) // Finally, draw cursor chrome + // FIXME: it looks like we might need to do this in normal painting order... spr, err := s.specials.Sprite(0) if err != nil { return err } op := ebiten.DrawImageOptions{} - op.GeoM = s.geoForCoords(int(s.selectedCell.X), int(s.selectedCell.Y), 0) - op.GeoM.Translate(-cellWidthHalf, -cellHeightHalf) + geo := s.geoForCoords(int(s.selectedCell.X), int(s.selectedCell.Y), 0) + op.GeoM = geo + op.GeoM.Translate(-209, -332) + op.GeoM.Translate(float64(spr.Rect.Min.X), float64(spr.Rect.Min.Y)) + op.GeoM.Scale(s.Zoom, s.Zoom) if err := screen.DrawImage(spr.Image, &op); err != nil { return err } + + x1, y1 := geo.Apply(0, 0) + ebitenutil.DebugPrintAt( + screen, + fmt.Sprintf("(%d,%d)", int(s.selectedCell.X), int(s.selectedCell.Y)), + int(x1), + int(y1), + ) - sx, sy := op.GeoM.Apply(0, 0) - ebitenutil.DebugPrintAt(screen, fmt.Sprintf("(%.0f,%.0f)", s.selectedCell.X, s.selectedCell.Y), int(sx), int(sy)) + /* + // debug: draw a square around the selected cell + x2, y2 := geo.Apply(cellWidth, cellHeight) + ebitenutil.DrawLine(screen, x1, y1, x2, y1, colornames.Green) // top line + ebitenutil.DrawLine(screen, x1, y1, x1, y2, colornames.Green) // left line + ebitenutil.DrawLine(screen, x2, y1, x2, y2, colornames.Green) // right line + ebitenutil.DrawLine(screen, x1, y2, x2, y2, colornames.Green) // bottom line + */ return nil } -func (s *Scenario) geoForCoords(x, y, z int) ebiten.GeoM { +func (s *Scenario) geoForCam() ebiten.GeoM { geo := ebiten.GeoM{} geo.Translate(-float64(s.Viewpoint.X), -float64(s.Viewpoint.Y)) + return geo +} + +func (s *Scenario) geoForCoords(x, y, z int) ebiten.GeoM { + geo := s.geoForCam() + pix := IsoPt{X: float64(x), Y: float64(y)}.ToCart() geo.Translate(pix.X, pix.Y) @@ -157,6 +187,9 @@ func (s *Scenario) renderCell(x, y, z int, screen *ebiten.Image, counter map[str op.GeoM.Translate(float64(spr.Rect.Min.X), float64(spr.Rect.Min.Y)) + // Zoom has to come last + op.GeoM.Scale(s.Zoom, s.Zoom) + if err := screen.DrawImage(spr.Image, &op); err != nil { return err } @@ -166,11 +199,11 @@ func (s *Scenario) renderCell(x, y, z int, screen *ebiten.Image, counter map[str } const ( - cellWidth = 128 - cellHeight = 64 + cellWidth = 128.0 + cellHeight = 63.0 - cellWidthHalf = cellWidth / 2 - cellHeightHalf = cellHeight / 2 + cellWidthHalf = cellWidth / 2.0 + cellHeightHalf = cellHeight / 2.0 ) func (p CartPt) ToISO() IsoPt { @@ -186,28 +219,3 @@ func (p IsoPt) ToCart() CartPt { Y: (p.X + p.Y) * cellHeightHalf, } } - -/* -// Doesn't take the camera or Z level into account -func cellToPix(pt image.Point) image.Point { - return image.Pt( - (pt.X-pt.Y)*cellWidthHalf, - (pt.X+pt.Y)*cellHeightHalf, - ) -} - -// Doesn't take the camera or Z level into account -func pixToCell(pt image.Point) image.Point { - fX := pt.X - fY := pt.Y - return image.Pt( -// (pt.X / cellWidthHalf + pt.Y / cellHeightHalf) / 2, -// (pt.Y / cellHeightHalf - (pt.Y / cellWidthHalf)) / 2, -// int(fY/cellHeight+fX/(cellWidth*2)), -// int(fY/cellHeight-fX/(cellWidth*2)), - //int((fY / cellHeight) + (fX / cellWidth)), - //int((-fX / cellWidth) + (fY / cellHeight)), - - - ) -}*/ diff --git a/internal/scenario/manage.go b/internal/scenario/manage.go index 7dbd6d4..ed08380 100644 --- a/internal/scenario/manage.go +++ b/internal/scenario/manage.go @@ -13,3 +13,16 @@ func (s *Scenario) CellAtCursor() (maps.Cell, CellPoint) { cell := s.area.Cell(int(s.selectedCell.X), int(s.selectedCell.Y), 0) return cell, CellPoint{IsoPt: s.selectedCell, Z: 0} } + +func (s *Scenario) ChangeZIdx(by int) { + newZ := s.ZIdx + by + if newZ < 0 { + newZ = 0 + } + + if newZ > 6 { + newZ = 6 + } + + s.ZIdx = newZ +} diff --git a/internal/scenario/scenario.go b/internal/scenario/scenario.go index 2c37988..db0e06a 100644 --- a/internal/scenario/scenario.go +++ b/internal/scenario/scenario.go @@ -21,6 +21,7 @@ type Scenario struct { // Or have a separater Drawer for the Scenario? Viewpoint image.Point // Top-left of the screen ZIdx int // Currently-viewed Z index + Zoom float64 // Zoom level to set } func NewScenario(assets *assetstore.AssetStore, name string) (*Scenario, error) { @@ -43,6 +44,7 @@ func NewScenario(assets *assetstore.AssetStore, name string) (*Scenario, error) area: area, specials: specials, Viewpoint: image.Pt(0, 3000), // FIXME: haxxx + Zoom: 1.0, } return out, nil diff --git a/internal/ui/dialogues.go b/internal/ui/dialogues.go index 86e7dbe..5bf856a 100644 --- a/internal/ui/dialogues.go +++ b/internal/ui/dialogues.go @@ -14,6 +14,10 @@ func (d *Driver) Dialogues() []string { return out } +func (d *Driver) IsInDialogue() bool { + return d.activeDialogue != nil +} + func (d *Driver) ShowDialogue(locator string) error { for _, dialogue := range d.dialogues { if dialogue.Locator == locator { diff --git a/internal/ui/window.go b/internal/ui/window.go index b83ac23..c7387d9 100644 --- a/internal/ui/window.go +++ b/internal/ui/window.go @@ -37,6 +37,8 @@ type Window struct { MouseWheelHandler func(float64, float64) MouseClickHandler func() + WhileKeyDownHandlers map[ebiten.Key]func() + // Allow the "game" to be switched out at any time game Game @@ -54,13 +56,16 @@ func NewWindow(game Game, title string, xRes int, yRes int) (*Window, error) { ebiten.SetRunnableInBackground(true) return &Window{ - Title: title, + Title: title, + debug: true, + firstRun: true, + game: game, + xRes: xRes, + yRes: yRes, + + WhileKeyDownHandlers: make(map[ebiten.Key]func()), + KeyUpHandlers: make(map[ebiten.Key]func()), - debug: true, - firstRun: true, - game: game, - xRes: xRes, - yRes: yRes, }, nil } @@ -69,6 +74,10 @@ func (w *Window) OnKeyUp(key ebiten.Key, f func()) { w.KeyUpHandlers[key] = f } +func (w *Window) WhileKeyDown(key ebiten.Key, f func()) { + w.WhileKeyDownHandlers[key] = f +} + func (w *Window) OnMouseWheel(f func(x, y float64)) { w.MouseWheelHandler = f } @@ -119,7 +128,8 @@ func (w *Window) Update(screen *ebiten.Image) (outErr error) { return err } - // Process keys + // Process keys. + // FIXME: : should this happen before or after update? // TODO: efficient set operations for key, cb := range w.KeyUpHandlers { @@ -128,6 +138,12 @@ func (w *Window) Update(screen *ebiten.Image) (outErr error) { } } + for key, cb := range w.WhileKeyDownHandlers { + if ebiten.IsKeyPressed(key) { + cb() + } + } + if w.MouseWheelHandler != nil { x, y := ebiten.Wheel() if x != 0 || y != 0 {