Integrate view-map into ordoor
Now we can view scenario maps from the main game interface. We can't cancel out of a scenario yet, though.
This commit is contained in:
@@ -2,15 +2,13 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"image"
|
|
||||||
"log"
|
"log"
|
||||||
"math"
|
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/hajimehoshi/ebiten"
|
"github.com/hajimehoshi/ebiten"
|
||||||
|
|
||||||
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
||||||
|
"code.ur.gs/lupine/ordoor/internal/scenario"
|
||||||
"code.ur.gs/lupine/ordoor/internal/ui"
|
"code.ur.gs/lupine/ordoor/internal/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,18 +21,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type env struct {
|
type env struct {
|
||||||
assets *assetstore.AssetStore
|
scenario *scenario.Scenario
|
||||||
area *assetstore.Map
|
|
||||||
|
|
||||||
step int
|
|
||||||
state state
|
|
||||||
lastState state
|
|
||||||
}
|
|
||||||
|
|
||||||
type state struct {
|
|
||||||
zoom float64
|
|
||||||
origin image.Point
|
|
||||||
zIdx int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -50,26 +37,13 @@ func main() {
|
|||||||
log.Fatalf("Failed to scan root directory %v: %v", *gamePath, err)
|
log.Fatalf("Failed to scan root directory %v: %v", *gamePath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
area, err := assets.Map(*gameMap)
|
scenario, err := scenario.NewScenario(assets, *gameMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to load map %v: %v", *gameMap, err)
|
log.Fatalf("Failed to load scenario %v: %v", *gameMap, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eager load sprites
|
|
||||||
if err := area.LoadSprites(); err != nil {
|
|
||||||
log.Fatal("Eager-loading sprites failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
state := state{
|
|
||||||
zoom: 1.0,
|
|
||||||
origin: image.Point{0, 3000}, // FIXME: haxxx
|
|
||||||
zIdx: 1,
|
|
||||||
}
|
|
||||||
env := &env{
|
env := &env{
|
||||||
area: area,
|
scenario: scenario,
|
||||||
assets: assets,
|
|
||||||
state: state,
|
|
||||||
lastState: state,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
win, err := ui.NewWindow(env, "View Map "+*gameMap, *winX, *winY)
|
win, err := ui.NewWindow(env, "View Map "+*gameMap, *winX, *winY)
|
||||||
@@ -83,10 +57,9 @@ func main() {
|
|||||||
win.OnKeyUp(ebiten.KeyRight, env.changeOrigin(+64, +0))
|
win.OnKeyUp(ebiten.KeyRight, env.changeOrigin(+64, +0))
|
||||||
win.OnKeyUp(ebiten.KeyUp, env.changeOrigin(+0, -64))
|
win.OnKeyUp(ebiten.KeyUp, env.changeOrigin(+0, -64))
|
||||||
win.OnKeyUp(ebiten.KeyDown, env.changeOrigin(+0, +64))
|
win.OnKeyUp(ebiten.KeyDown, env.changeOrigin(+0, +64))
|
||||||
win.OnMouseWheel(env.changeZoom)
|
|
||||||
|
|
||||||
for i := 0; i < 6; i++ {
|
for i := 0; i <= 6; i++ {
|
||||||
win.OnKeyUp(ebiten.Key1+ebiten.Key(i), env.setZIdx(i+1))
|
win.OnKeyUp(ebiten.Key1+ebiten.Key(i), env.setZIdx(i))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := win.Run(); err != nil {
|
if err := win.Run(); err != nil {
|
||||||
@@ -95,150 +68,22 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *env) Update(screenX, screenY int) error {
|
func (e *env) Update(screenX, screenY int) error {
|
||||||
if e.step == 0 || e.lastState != e.state {
|
return e.scenario.Update(screenX, screenY)
|
||||||
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 {
|
func (e *env) Draw(screen *ebiten.Image) error {
|
||||||
// Bounds clipping
|
return e.scenario.Draw(screen)
|
||||||
// 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 -= 5 // Ensure we paint to every visible section of the screeen.
|
|
||||||
topLeft.X -= 5 // FIXME: haxxx
|
|
||||||
|
|
||||||
bottomRight := pixToCell(image.Pt(e.state.origin.X+sw, e.state.origin.Y+sh))
|
|
||||||
bottomRight.X += 5
|
|
||||||
bottomRight.Y += 5
|
|
||||||
|
|
||||||
// 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
|
|
||||||
})
|
|
||||||
|
|
||||||
counter := map[string]int{}
|
|
||||||
for _, pt := range toDraw {
|
|
||||||
for z := 0; z <= e.state.zIdx; z++ {
|
|
||||||
if err := e.renderCell(pt.X, pt.Y, z, screen, counter); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//log.Printf("%#+v", counter)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *env) renderCell(x, y, z int, screen *ebiten.Image, counter map[string]int) 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))
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
// if _, ok := counter[spr.ID]; !ok {
|
|
||||||
// counter[spr.ID] = 0
|
|
||||||
// }
|
|
||||||
// counter[spr.ID] = counter[spr.ID] + 1
|
|
||||||
|
|
||||||
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() {
|
func (e *env) changeOrigin(byX, byY int) func() {
|
||||||
return func() {
|
return func() {
|
||||||
e.state.origin.X += byX
|
e.scenario.Viewpoint.X += byX
|
||||||
e.state.origin.Y += byY
|
e.scenario.Viewpoint.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() {
|
func (e *env) setZIdx(to int) func() {
|
||||||
return func() {
|
return func() {
|
||||||
e.state.zIdx = to
|
e.scenario.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),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
@@ -120,7 +120,7 @@ func (a *AssetStore) lookup(name, ext string, dirs ...string) (string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", os.ErrNotExist
|
return "", fmt.Errorf("file %q does not exist", filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
func canonical(s string) string {
|
func canonical(s string) string {
|
||||||
|
@@ -1,6 +1,9 @@
|
|||||||
package assetstore
|
package assetstore
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"code.ur.gs/lupine/ordoor/internal/config"
|
"code.ur.gs/lupine/ordoor/internal/config"
|
||||||
"code.ur.gs/lupine/ordoor/internal/data"
|
"code.ur.gs/lupine/ordoor/internal/data"
|
||||||
)
|
)
|
||||||
@@ -22,6 +25,12 @@ func (a *AssetStore) Generic() (*data.Generic, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// These changes are made so data in generic plays nicer with assetstore
|
||||||
|
for i, filename := range generic.CampaignMaps {
|
||||||
|
generic.CampaignMaps[i] =
|
||||||
|
strings.TrimSuffix(filename, filepath.Ext(filename))
|
||||||
|
}
|
||||||
|
|
||||||
a.generic = generic
|
a.generic = generic
|
||||||
|
|
||||||
return generic, nil
|
return generic, nil
|
||||||
|
@@ -1,12 +1,13 @@
|
|||||||
package flow
|
package flow
|
||||||
|
|
||||||
func (f *Flow) linkBridge() {
|
func (f *Flow) linkBridge() {
|
||||||
// FIXME: sometimes these doors are frozen, depending on game state
|
// FIXME: sometimes these doors are frozen, depending on ship state, but we
|
||||||
|
// don't implement that yet.
|
||||||
|
|
||||||
f.onClick(bridge, "2.1", f.setDriver(briefing)) // TODO: Mission briefing clickable
|
f.onClick(bridge, "2.1", f.setDriver(briefing)) // Mission briefing clickable
|
||||||
f.onClick(bridge, "2.2", f.setDriver(choices)) // Options door hotspot
|
f.onClick(bridge, "2.2", f.setDriver(choices)) // Options door hotspot
|
||||||
f.setFreeze(bridge, "2.4", false) // FIXME: Enter combat door hotspot (!!!)
|
f.onClick(bridge, "2.4", f.playNextScenario()) // Enter combat door hotspot
|
||||||
f.setFreeze(bridge, "2.6", false) // FIXME: Vehicle configure door hotspot
|
f.setFreeze(bridge, "2.6", true) // TODO: Vehicle configure door hotspot
|
||||||
f.onClick(bridge, "2.8", f.setDriver(arrange)) // Squads configure door hotspot
|
f.onClick(bridge, "2.8", f.setDriver(arrange)) // Squads configure door hotspot
|
||||||
|
|
||||||
// link children
|
// link children
|
||||||
|
@@ -3,11 +3,15 @@ package flow
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
"github.com/hajimehoshi/ebiten"
|
"github.com/hajimehoshi/ebiten"
|
||||||
|
|
||||||
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
||||||
"code.ur.gs/lupine/ordoor/internal/config"
|
"code.ur.gs/lupine/ordoor/internal/config"
|
||||||
|
"code.ur.gs/lupine/ordoor/internal/data"
|
||||||
|
"code.ur.gs/lupine/ordoor/internal/scenario"
|
||||||
|
"code.ur.gs/lupine/ordoor/internal/ship"
|
||||||
"code.ur.gs/lupine/ordoor/internal/ui"
|
"code.ur.gs/lupine/ordoor/internal/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,6 +23,7 @@ type Flow struct {
|
|||||||
config *config.Config
|
config *config.Config
|
||||||
current *ui.Driver
|
current *ui.Driver
|
||||||
drivers map[driverName]*ui.Driver
|
drivers map[driverName]*ui.Driver
|
||||||
|
generic *data.Generic
|
||||||
|
|
||||||
// Some screens can be returned to from more than one place. Where this is
|
// Some screens can be returned to from more than one place. Where this is
|
||||||
// the case, instead of hardcoding it, we'll store an entry in here so we
|
// the case, instead of hardcoding it, we'll store an entry in here so we
|
||||||
@@ -27,6 +32,11 @@ type Flow struct {
|
|||||||
// FIXME: this really suggests wiring everything up at the start is wrong.
|
// FIXME: this really suggests wiring everything up at the start is wrong.
|
||||||
returns map[driverName]driverName
|
returns map[driverName]driverName
|
||||||
|
|
||||||
|
// If we're currently playing a scenario, it it placed here
|
||||||
|
scenario *scenario.Scenario
|
||||||
|
|
||||||
|
ship *ship.Ship
|
||||||
|
|
||||||
exit error
|
exit error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,12 +89,19 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func New(assets *assetstore.AssetStore, config *config.Config) (*Flow, error) {
|
func New(assets *assetstore.AssetStore, config *config.Config, ship *ship.Ship) (*Flow, error) {
|
||||||
|
generic, err := assets.Generic()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to read generic data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
out := &Flow{
|
out := &Flow{
|
||||||
assets: assets,
|
assets: assets,
|
||||||
config: config,
|
config: config,
|
||||||
|
generic: generic,
|
||||||
drivers: make(map[driverName]*ui.Driver, len(driverNames)),
|
drivers: make(map[driverName]*ui.Driver, len(driverNames)),
|
||||||
returns: make(map[driverName]driverName),
|
returns: make(map[driverName]driverName),
|
||||||
|
ship: ship,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load all the drivers upfront
|
// Load all the drivers upfront
|
||||||
@@ -127,7 +144,20 @@ func (f *Flow) Update(screenX, screenY int) error {
|
|||||||
return f.exit
|
return f.exit
|
||||||
}
|
}
|
||||||
|
|
||||||
return f.current.Update(screenX, screenY)
|
if f.scenario != nil {
|
||||||
|
if err := f.scenario.Update(screenX, screenY); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.current != nil {
|
||||||
|
if err := f.current.Update(screenX, screenY); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Flow) Draw(screen *ebiten.Image) error {
|
func (f *Flow) Draw(screen *ebiten.Image) error {
|
||||||
@@ -135,11 +165,29 @@ func (f *Flow) Draw(screen *ebiten.Image) error {
|
|||||||
return f.exit
|
return f.exit
|
||||||
}
|
}
|
||||||
|
|
||||||
return f.current.Draw(screen)
|
if f.scenario != nil {
|
||||||
|
if err := f.scenario.Draw(screen); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.current != nil {
|
||||||
|
if err := f.current.Draw(screen); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Flow) Cursor() (*ebiten.Image, *ebiten.DrawImageOptions, error) {
|
func (f *Flow) Cursor() (*ebiten.Image, *ebiten.DrawImageOptions, error) {
|
||||||
return f.current.Cursor()
|
if f.current != nil {
|
||||||
|
return f.current.Cursor()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: we should get a cursor from current all the time.
|
||||||
|
return nil, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Flow) linkDrivers() {
|
func (f *Flow) linkDrivers() {
|
||||||
@@ -215,6 +263,23 @@ func (f *Flow) setReturningDriver(from, to driverName) func() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *Flow) playNextScenario() func() {
|
||||||
|
return func() {
|
||||||
|
log.Printf("Loading scenario: %v", f.ship.NextScenario)
|
||||||
|
|
||||||
|
// TODO: we *could* load scenario assets in a separate assetstore to
|
||||||
|
// make it easier to chuck them away at the end?
|
||||||
|
scenario, err := scenario.NewScenario(f.assets, f.ship.NextScenario)
|
||||||
|
if err != nil {
|
||||||
|
f.exit = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
f.current = nil // TODO: show the UI for a scenario
|
||||||
|
f.scenario = scenario
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// from is the child menu, to is the parent
|
// from is the child menu, to is the parent
|
||||||
func (f *Flow) returnToLastDriverNow(from driverName) error {
|
func (f *Flow) returnToLastDriverNow(from driverName) error {
|
||||||
to, ok := f.returns[from]
|
to, ok := f.returns[from]
|
||||||
|
@@ -29,7 +29,10 @@ func (f *Flow) linkLevelPly() {
|
|||||||
// FIXME: we should select a savegame if Mighty Hero is selected here
|
// FIXME: we should select a savegame if Mighty Hero is selected here
|
||||||
// FIXME: we should show a movie here. Need an internal SMK player first
|
// FIXME: we should show a movie here. Need an internal SMK player first
|
||||||
// FIXME: we should set up new game state here!
|
// FIXME: we should set up new game state here!
|
||||||
f.onClick(levelPly, "2.7", f.setDriver(bridge)) // Select button
|
f.onClick(levelPly, "2.7", func() { // Select button
|
||||||
|
f.ship.NextScenario = f.generic.CampaignMaps[0]
|
||||||
|
f.setDriverNow(bridge)
|
||||||
|
})
|
||||||
|
|
||||||
// Link children
|
// Link children
|
||||||
f.linkBridge()
|
f.linkBridge()
|
||||||
|
@@ -5,7 +5,6 @@
|
|||||||
package ordoor
|
package ordoor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
@@ -15,34 +14,23 @@ import (
|
|||||||
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
||||||
"code.ur.gs/lupine/ordoor/internal/config"
|
"code.ur.gs/lupine/ordoor/internal/config"
|
||||||
"code.ur.gs/lupine/ordoor/internal/flow"
|
"code.ur.gs/lupine/ordoor/internal/flow"
|
||||||
|
"code.ur.gs/lupine/ordoor/internal/ship"
|
||||||
"code.ur.gs/lupine/ordoor/internal/ui"
|
"code.ur.gs/lupine/ordoor/internal/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
type gameState int
|
type gameState int
|
||||||
|
|
||||||
const (
|
|
||||||
StateInterface gameState = 1
|
|
||||||
StateExit gameState = 666
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
errExit = errors.New("User-requested exit action")
|
|
||||||
)
|
|
||||||
|
|
||||||
type Ordoor struct {
|
type Ordoor struct {
|
||||||
assets *assetstore.AssetStore
|
assets *assetstore.AssetStore
|
||||||
config *config.Config
|
config *config.Config
|
||||||
music *audio.Player
|
music *audio.Player
|
||||||
win *ui.Window
|
win *ui.Window
|
||||||
|
|
||||||
state gameState
|
|
||||||
nextState gameState
|
|
||||||
|
|
||||||
// Relevant to interface state
|
// Relevant to interface state
|
||||||
flow *flow.Flow
|
flow *flow.Flow
|
||||||
|
|
||||||
// Relevant to campaign state
|
// Relevant to campaign state
|
||||||
ship *Ship
|
ship *ship.Ship
|
||||||
}
|
}
|
||||||
|
|
||||||
func Run(configFile string, overrideX, overrideY int) error {
|
func Run(configFile string, overrideX, overrideY int) error {
|
||||||
@@ -73,11 +61,9 @@ func Run(configFile string, overrideX, overrideY int) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ordoor := &Ordoor{
|
ordoor := &Ordoor{
|
||||||
assets: assets,
|
assets: assets,
|
||||||
config: cfg,
|
config: cfg,
|
||||||
ship: &Ship{},
|
ship: &ship.Ship{},
|
||||||
state: StateInterface,
|
|
||||||
nextState: StateInterface,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
x, y := cfg.Options.XRes, cfg.Options.YRes
|
x, y := cfg.Options.XRes, cfg.Options.YRes
|
||||||
@@ -100,7 +86,7 @@ func Run(configFile string, overrideX, overrideY int) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := ordoor.Run(); err != nil {
|
if err := ordoor.Run(); err != nil {
|
||||||
return fmt.Errorf("Run returned %v", err)
|
return fmt.Errorf("Run finished with error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -124,6 +110,8 @@ func (o *Ordoor) Run() error {
|
|||||||
|
|
||||||
// Only one music track can play at a time. This is handled at the toplevel.
|
// Only one music track can play at a time. This is handled at the toplevel.
|
||||||
// FIXME: should take references from Sounds.dat
|
// FIXME: should take references from Sounds.dat
|
||||||
|
// FIXME: music probably properly belongs to flow. This package can just do
|
||||||
|
// initialization and wire the flow to the ship?
|
||||||
func (o *Ordoor) PlayMusic(name string) error {
|
func (o *Ordoor) PlayMusic(name string) error {
|
||||||
if o.music != nil {
|
if o.music != nil {
|
||||||
if err := o.music.Close(); err != nil {
|
if err := o.music.Close(); err != nil {
|
||||||
@@ -152,7 +140,7 @@ func (o *Ordoor) PlayMusic(name string) error {
|
|||||||
func (o *Ordoor) setupFlow() error {
|
func (o *Ordoor) setupFlow() error {
|
||||||
o.PlayMusic("music_interface")
|
o.PlayMusic("music_interface")
|
||||||
|
|
||||||
flow, err := flow.New(o.assets, o.config)
|
flow, err := flow.New(o.assets, o.config, o.ship)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -163,22 +151,6 @@ func (o *Ordoor) setupFlow() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (o *Ordoor) Update(screenX, screenY int) error {
|
func (o *Ordoor) Update(screenX, screenY int) error {
|
||||||
// Perform state transitions
|
|
||||||
if o.state != o.nextState {
|
|
||||||
log.Printf("State transition: %v -> %v", o.state, o.nextState)
|
|
||||||
switch o.nextState {
|
|
||||||
case StateExit:
|
|
||||||
{
|
|
||||||
return errExit
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("Unknown state transition: %v -> %v", o.state, o.nextState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// State transition is finished, hooray
|
|
||||||
o.state = o.nextState
|
|
||||||
|
|
||||||
// Ensure music is doing the right thing
|
// Ensure music is doing the right thing
|
||||||
if o.music != nil && o.music.IsPlaying() != o.config.Options.PlayMusic {
|
if o.music != nil && o.music.IsPlaying() != o.config.Options.PlayMusic {
|
||||||
if o.config.Options.PlayMusic {
|
if o.config.Options.PlayMusic {
|
||||||
@@ -189,27 +161,13 @@ func (o *Ordoor) Update(screenX, screenY int) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch o.state {
|
return o.flow.Update(screenX, screenY)
|
||||||
case StateInterface:
|
|
||||||
return o.flow.Update(screenX, screenY)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("Unknown state: %v", o.state)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Ordoor) Draw(screen *ebiten.Image) error {
|
func (o *Ordoor) Draw(screen *ebiten.Image) error {
|
||||||
switch o.state {
|
return o.flow.Draw(screen)
|
||||||
case StateInterface:
|
|
||||||
return o.flow.Draw(screen)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("Unknown state: %v", o.state)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Ordoor) Cursor() (*ebiten.Image, *ebiten.DrawImageOptions, error) {
|
func (o *Ordoor) Cursor() (*ebiten.Image, *ebiten.DrawImageOptions, error) {
|
||||||
if o.state == StateInterface {
|
return o.flow.Cursor()
|
||||||
return o.flow.Cursor()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil, nil
|
|
||||||
}
|
}
|
||||||
|
134
internal/scenario/draw.go
Normal file
134
internal/scenario/draw.go
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
package scenario
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Scenario) Update(screenX, screenY int) error {
|
||||||
|
s.tick += 1
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Scenario) 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(s.Viewpoint)
|
||||||
|
topLeft.X -= 5 // Ensure we paint to every visible section of the screeen.
|
||||||
|
topLeft.X -= 5 // FIXME: haxxx
|
||||||
|
|
||||||
|
bottomRight := pixToCell(image.Pt(s.Viewpoint.X+sw, s.Viewpoint.Y+sh))
|
||||||
|
bottomRight.X += 5
|
||||||
|
bottomRight.Y += 5
|
||||||
|
|
||||||
|
// 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(s.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
|
||||||
|
})
|
||||||
|
|
||||||
|
counter := map[string]int{}
|
||||||
|
for _, pt := range toDraw {
|
||||||
|
for z := 0; z <= s.ZIdx; z++ {
|
||||||
|
if err := s.renderCell(pt.X, pt.Y, z, screen, counter); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//log.Printf("%#+v", counter)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Scenario) renderCell(x, y, z int, screen *ebiten.Image, counter map[string]int) error {
|
||||||
|
sprites, err := s.area.SpritesForCell(x, y, z)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
iso := ebiten.GeoM{}
|
||||||
|
iso.Translate(-float64(s.Viewpoint.X), -float64(s.Viewpoint.Y))
|
||||||
|
|
||||||
|
pix := cellToPix(image.Pt(x, y))
|
||||||
|
iso.Translate(float64(pix.X), float64(pix.Y))
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
// if _, ok := counter[spr.ID]; !ok {
|
||||||
|
// counter[spr.ID] = 0
|
||||||
|
// }
|
||||||
|
// counter[spr.ID] = counter[spr.ID] + 1
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
}
|
41
internal/scenario/scenario.go
Normal file
41
internal/scenario/scenario.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// package play takes a map and turns it into a playable scenario
|
||||||
|
package scenario
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
|
||||||
|
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Scenario struct {
|
||||||
|
area *assetstore.Map
|
||||||
|
|
||||||
|
tick int
|
||||||
|
turn int
|
||||||
|
|
||||||
|
// All these must be modified by user actions somehow.
|
||||||
|
// TODO: extract into the idea of a viewport passed to Update / Draw somehow?
|
||||||
|
// Or have a separater Drawer for the Scenario?
|
||||||
|
Viewpoint image.Point // Top-left of the screen
|
||||||
|
ZIdx int // Currently-viewed Z index
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewScenario(assets *assetstore.AssetStore, name string) (*Scenario, error) {
|
||||||
|
area, err := assets.Map(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eager load sprites. TODO: do we really want to do this?
|
||||||
|
if err := area.LoadSprites(); err != nil {
|
||||||
|
return nil, fmt.Errorf("Eager-loading sprites failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out := &Scenario{
|
||||||
|
area: area,
|
||||||
|
Viewpoint: image.Pt(0, 3000), // FIXME: haxxx
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
@@ -1,9 +1,9 @@
|
|||||||
package ordoor
|
package ship
|
||||||
|
|
||||||
// Ship encapsulates campaign state, including current location in the campaign,
|
// Ship encapsulates campaign state, including current location in the campaign,
|
||||||
// marines and their stats, supplies, etc.
|
// marines and their stats, supplies, etc.
|
||||||
type Ship struct {
|
type Ship struct {
|
||||||
CurrentMission string
|
NextScenario string
|
||||||
|
|
||||||
Squads []*Squad
|
Squads []*Squad
|
||||||
Captain *Character
|
Captain *Character
|
Reference in New Issue
Block a user