Compare commits

...

6 Commits

Author SHA1 Message Date
4d336b9189 Get character stats (kind of) displaying in-scenario 2020-06-13 18:11:45 +01:00
3b7cfb6ecc Drag flow into view-map
This is pretty awful, but will let me wire up items more easily without
needing to do the big refactor into independent menu handlers
2020-06-13 16:37:39 +01:00
7677c30572 Start displaying characters on maps 2020-06-13 15:07:32 +01:00
eac6017c2c Count sprite draw calls 2020-06-13 13:42:26 +01:00
f971ba320c Some more character investigations 2020-06-11 02:54:57 +01:00
cf624cc77b Switch from encoding/binary to struc
It's not perfect, but struc can deserialize the whole thing into one
struct while encoding/binary can't. It's nice to have that.
2020-06-09 00:36:56 +01:00
21 changed files with 584 additions and 231 deletions

View File

@@ -139,16 +139,15 @@ func loadMapsFrom(mapsPath string) {
} }
log.Printf("Maps in %s:", mapsPath) log.Printf("Maps in %s:", mapsPath)
for key, gameMap := range gameMaps { for key, gm := range gameMaps {
rect := gameMap.Rect() rect := gm.Rect()
hdr := gameMap.Header
fmt.Printf( fmt.Printf(
" * `%s`: IsCampaignMap=%v W=%v:%v L=%v:%v SetName=%s\n", " * `%s`: IsCampaignMap=%v W=%v:%v L=%v:%v SetName=%s\n",
key, key,
hdr.IsCampaignMap, gm.IsCampaignMap,
rect.Min.X, rect.Max.X, rect.Min.X, rect.Max.X,
rect.Min.Y, rect.Max.Y, rect.Min.Y, rect.Max.Y,
string(hdr.SetName[:]), string(gm.SetName),
) )
} }
} }

View File

@@ -10,7 +10,9 @@ 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/scenario" "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"
) )
@@ -25,6 +27,7 @@ var (
) )
type env struct { type env struct {
flow *flow.Flow
scenario *scenario.Scenario scenario *scenario.Scenario
} }
@@ -51,27 +54,39 @@ func main() {
log.Fatalf("Failed to load scenario %v: %v", *gameMap, err) log.Fatalf("Failed to load scenario %v: %v", *gameMap, err)
} }
env := &env{ var realEnv *env
scenario: scenario, if cfg.DefaultEngineName == "ordoor" {
ship := &ship.Ship{}
flow, err := flow.New(assets, cfg, ship)
if err != nil {
log.Fatalf("Failed to setup flow: %v", err)
}
flow.SetScenario(scenario)
realEnv = &env{flow: flow, scenario: scenario}
} else {
realEnv = &env{scenario: scenario}
} }
win, err := ui.NewWindow(env, "View Map "+*gameMap, *winX, *winY) win, err := ui.NewWindow(realEnv, "View Map "+*gameMap, *winX, *winY)
if err != nil { if err != nil {
log.Fatal("Couldn't create window: %v", err) log.Fatal("Couldn't create window: %v", err)
} }
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++ { for i := 0; i <= 6; i++ {
win.OnKeyUp(ebiten.Key1+ebiten.Key(i), env.setZIdx(i)) win.OnKeyUp(ebiten.Key1+ebiten.Key(i), realEnv.setZIdx(i))
} }
win.OnMouseClick(env.showCellData) win.OnMouseClick(realEnv.showCellData)
win.OnMouseWheel(env.changeZoom) win.OnMouseWheel(realEnv.changeZoom)
if realEnv.flow == nil {
step := 32
win.WhileKeyDown(ebiten.KeyLeft, realEnv.changeOrigin(-step, +0))
win.WhileKeyDown(ebiten.KeyRight, realEnv.changeOrigin(+step, +0))
win.WhileKeyDown(ebiten.KeyUp, realEnv.changeOrigin(+0, -step))
win.WhileKeyDown(ebiten.KeyDown, realEnv.changeOrigin(+0, +step))
}
if err := win.Run(); err != nil { if err := win.Run(); err != nil {
log.Fatal(err) log.Fatal(err)
@@ -79,12 +94,20 @@ func main() {
} }
func (e *env) Update(screenX, screenY int) error { func (e *env) Update(screenX, screenY int) error {
if e.flow != nil {
return e.flow.Update(screenX, screenY)
} else {
return e.scenario.Update(screenX, screenY) return e.scenario.Update(screenX, screenY)
} }
}
func (e *env) Draw(screen *ebiten.Image) error { func (e *env) Draw(screen *ebiten.Image) error {
if e.flow != nil {
return e.flow.Draw(screen)
} else {
return e.scenario.Draw(screen) return e.scenario.Draw(screen)
} }
}
func (e *env) changeOrigin(byX, byY int) func() { func (e *env) changeOrigin(byX, byY int) func() {
return func() { return func() {

View File

@@ -181,8 +181,8 @@ func (e *env) Draw(screen *ebiten.Image) error {
for y := int(rect.Min.Y); y < int(rect.Max.Y); y++ { for y := int(rect.Min.Y); y < int(rect.Max.Y); y++ {
for x := int(rect.Min.X); x < int(rect.Max.X); x++ { for x := int(rect.Min.X); x < int(rect.Max.X); x++ {
cell := gameMap.Cells.At(x, y, int(e.state.zIdx)) cell := gameMap.At(x, y, int(e.state.zIdx))
imd.Set(x, y, makeColour(&cell, e.state.cellIdx)) imd.Set(x, y, makeColour(cell, e.state.cellIdx))
} }
} }

View File

@@ -598,7 +598,8 @@ Ignoring them for now, here's a first guess at a header:
| 24 | 2 | ??? - varies. Seems related to character/squad position? | | 24 | 2 | ??? - varies. Seems related to character/squad position? |
| 26 | 2 | ??? - invariant `00 04` | | 26 | 2 | ??? - invariant `00 04` |
| 28 | 4 | ??? - varies (0 vs 5) | | 28 | 4 | ??? - varies (0 vs 5) |
| 32 | 4 | Number of thingies (26 vs 1) | | 32 | 1 | Number of thingies |
| 33 | 3 | ???. With a Lord of Change on the map, only one byte works as thingies count |
| 36 | 20 | Padding? | | 36 | 20 | Padding? |
56 bytes of data is interesting because the value of that first, ignored byte is 56 bytes of data is interesting because the value of that first, ignored byte is
@@ -625,7 +626,79 @@ Generating a map with no characters at all, the trailer is 2,447 bytes, and the
mission title starts at 0x3B (59). So we can say we have 20 bytes of padding as mission title starts at 0x3B (59). So we can say we have 20 bytes of padding as
a first approximation? a first approximation?
The "trailer trailer", for want of a better term, seems to be organised as: Here's where we're at with the per-character data, going from the padding values
suggested above:
| Offset | Size | Meaning |
| ------ | ---- | ------- |
| 0 | 178 | ??? |
| 178 | 1 | Character type |
| 179 | 80 | Character name |
| 259 | 1 | Weapon Skill |
| 260 | 1 | Ballistic Skill |
| 261 | 1 | Unknown |
| 262 | 1 | Leadership |
| 263 | 1 | Toughness |
| 264 | 1 | Strength |
| 265 | 1 | Action Points |
| 266 | 1 | Unknown |
| 267 | 1 | Unknown |
| 268 | 1 | Health |
| 269 | 495 | ??? |
| 764 | 1(?) | Squad number? |
| 765 | 927 | ??? |
There's still a lot of bytes to dig through, but this allows me to load the
character names from Chapter01 correctly, with the exception of record 57 which
just contains `\x02` and is then null-terminated all the way through - but maybe
that's just a data thing.
How about their types? `HasAction.dat` lists numbers for character types, and
those show up immediately before the name. going from the character type to the
animation group is not yet fully deciphered - captains mess up a direct
correlation - but a fixed offset table allows me to draw the characters \o/.
Orientation is not yet deciphered, however.
Given two characters of the same type, just in different locations, differing
values are seen at:
* `0x103 - 0x10c` (hodgepodge)
* `0x178 - 0x1be` (hodgepodge)
* `0x2fc` (0, 1) - squad number?
I can easily correlate certain bytes in the first range to various character
attributes. A few remain unset.
In Chapter01, picking a random character (Gorgon) and looking at his squadmates,
they are all in the same squad, and no other characters are in that squad, so it
looks pretty diagnostic to me. There's nothing in the UI to indicate the squad,
though.
Now let's look for position. In my 2-character map, they're at 65,50 and 70,55.
Within a character, I see those numbers repeated twice - around `0x1b{9,a}` and
`0x1b{d,e}`. This may be some kind of multiple-squares-taken-up thing.
Adding a (tall) Lord of Change to the map gave me `02 36 45 00 02 37 45`, which
doesn't quite match what my eyes are telling me for Z,Y,X. In addition, the data
immediately after this offset changed into a large number of coordinate-like
sets of values - far too many for it to actually be a bounding box. However, the
first one remains good as a position specifier.
Down in `0x679` (Chaos Sorcerer) or `0x68D` (Lord of Change), the map coords for
the *other* character appears, which is downright odd. For now, just use the
first-indexed value.
Thingies next: these aren't decoded at all yet, and the sizes seem to be
variable.
| Offset | Size | Meaning |
| ------ | ---- | ------- |
| | | |
Finally, the "trailer trailer", for want of a better term, seems to be organised
as:
| Offset | Size | Meaning | | Offset | Size | Meaning |
| ----- | ---- | ------- | | ----- | ---- | ------- |
@@ -633,6 +706,8 @@ The "trailer trailer", for want of a better term, seems to be organised as:
| 255 | 2048 | Briefing | | 255 | 2048 | Briefing |
| 2304 | 85 | ??? - each byte is 1 or 0. Spaced so it may be partly uint32 | | 2304 | 85 | ??? - each byte is 1 or 0. Spaced so it may be partly uint32 |
This duplicates the information found in the `.TXT` files. No idea what the end
data is yet.
## Soldiers At War ## Soldiers At War

3
go.mod
View File

@@ -8,7 +8,9 @@ require (
github.com/hajimehoshi/ebiten v1.11.1 github.com/hajimehoshi/ebiten v1.11.1
github.com/jfreymuth/oggvorbis v1.0.1 // indirect github.com/jfreymuth/oggvorbis v1.0.1 // indirect
github.com/kr/text v0.2.0 // indirect github.com/kr/text v0.2.0 // indirect
github.com/lunixbochs/struc v0.0.0-20200521075829-a4cb8d33dbbe
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/samuel/go-pcx v0.0.0-20180426214139-d9c017170db4 github.com/samuel/go-pcx v0.0.0-20180426214139-d9c017170db4
github.com/stretchr/testify v1.5.1 github.com/stretchr/testify v1.5.1
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5 // indirect golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5 // indirect
@@ -16,4 +18,5 @@ require (
golang.org/x/mobile v0.0.0-20200329125638-4c31acba0007 // indirect golang.org/x/mobile v0.0.0-20200329125638-4c31acba0007 // indirect
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 // indirect golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/restruct.v1 v1.0.0-20190323193435-3c2afb705f3c
) )

6
go.sum
View File

@@ -40,9 +40,13 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lunixbochs/struc v0.0.0-20200521075829-a4cb8d33dbbe h1:ewr1srjRCmcQogPQ/NCx6XCk6LGVmsVCc9Y3vvPZj+Y=
github.com/lunixbochs/struc v0.0.0-20200521075829-a4cb8d33dbbe/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/samuel/go-pcx v0.0.0-20180426214139-d9c017170db4 h1:Y/KOCu+ZLB730PudefxfsKVjtI0m0RhvFk9a0l4O1+c= github.com/samuel/go-pcx v0.0.0-20180426214139-d9c017170db4 h1:Y/KOCu+ZLB730PudefxfsKVjtI0m0RhvFk9a0l4O1+c=
@@ -107,5 +111,7 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogR
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/restruct.v1 v1.0.0-20190323193435-3c2afb705f3c h1:7j7Yy/3gedviEts3jKY0bEruQkTFKh+8pDmEFaM6UBc=
gopkg.in/restruct.v1 v1.0.0-20190323193435-3c2afb705f3c/go.mod h1:WJaLhyHHEQFOgwIxu/SJxvUHJA18glYsMETBTMIySTY=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@@ -1,6 +1,9 @@
package assetstore package assetstore
import ( import (
"fmt"
"code.ur.gs/lupine/ordoor/internal/data"
"code.ur.gs/lupine/ordoor/internal/idx" "code.ur.gs/lupine/ordoor/internal/idx"
) )
@@ -76,3 +79,50 @@ func (a *AssetStore) Animation(groupIdx, recIdx int) (*Animation, error) {
return &Animation{Frames: sprites}, nil return &Animation{Frames: sprites}, nil
} }
func (a *AssetStore) CharacterAnimation(ctype data.CharacterType, action data.AnimAction) (*Animation, error) {
ha, err := a.HasAction()
if err != nil {
return nil, err
}
if !ha.Check(ctype, action) {
return nil, fmt.Errorf("character %s: animation %s: not available", ctype, action)
}
// FIXME: we still need to be able to go from CTYPE to GROUP. In particular,
// squad leaders seem to be a modification on top of a previous group, which
// is a bit awkward. For now, hardcode it. How are captain modifiers stored?
group, ok := map[data.CharacterType]int{
data.CharacterTypeTactical: 1, // Has captain
data.CharacterTypeAssault: 3, // Has captain
data.CharacterTypeDevastator: 5,
data.CharacterTypeTerminator: 6, // Has captain
data.CharacterTypeApothecary: 8,
data.CharacterTypeTechmarine: 9,
data.CharacterTypeChaplain: 10,
data.CharacterTypeLibrarian: 11,
data.CharacterTypeCaptain: 12,
data.CharacterTypeChaosMarine: 13,
data.CharacterTypeChaosLord: 14,
data.CharacterTypeChaosChaplain: 15,
data.CharacterTypeChaosSorcerer: 16,
data.CharacterTypeChaosTerminator: 17,
data.CharacterTypeKhorneBerserker: 18,
data.CharacterTypeBloodThirster: 19, // This is a rotating thing?
data.CharacterTypeBloodLetter: 20,
data.CharacterTypeFleshHound: 21,
data.CharacterTypeLordOfChange: 22, // Another rotating thing?
data.CharacterTypeFlamer: 23,
data.CharacterTypePinkHorror: 24,
data.CharacterTypeBlueHorror: 25,
data.CharacterTypeChaosCultist: 26,
}[ctype]
if !ok {
return nil, fmt.Errorf("Unknown character type: %s", ctype)
}
rec := ha.Index(ctype, action)
return a.Animation(group, rec)
}

View File

@@ -45,6 +45,7 @@ type AssetStore struct {
cursors map[CursorName]*Cursor cursors map[CursorName]*Cursor
fonts map[string]*Font fonts map[string]*Font
generic *data.Generic generic *data.Generic
hasAction *data.HasAction
idx *idx.Idx idx *idx.Idx
images map[string]*ebiten.Image images map[string]*ebiten.Image
maps map[string]*Map maps map[string]*Map
@@ -111,6 +112,8 @@ func (a *AssetStore) Refresh() error {
a.cursors = make(map[CursorName]*Cursor) a.cursors = make(map[CursorName]*Cursor)
a.entries = newEntryMap a.entries = newEntryMap
a.fonts = make(map[string]*Font) a.fonts = make(map[string]*Font)
a.generic = nil
a.hasAction = nil
a.idx = nil a.idx = nil
a.images = make(map[string]*ebiten.Image) a.images = make(map[string]*ebiten.Image)
a.maps = make(map[string]*Map) a.maps = make(map[string]*Map)

View File

@@ -66,6 +66,26 @@ func (a *AssetStore) DefaultOptions() (*config.Options, error) {
return cfg, nil return cfg, nil
} }
func (a *AssetStore) HasAction() (*data.HasAction, error) {
if a.hasAction != nil {
return a.hasAction, nil
}
filename, err := a.lookup("HasAction", "dat", "Data")
if err != nil {
return nil, err
}
hasAction, err := data.LoadHasAction(filename)
if err != nil {
return nil, err
}
a.hasAction = hasAction
return hasAction, nil
}
func intToBool(i int) bool { func intToBool(i int) bool {
return i > 0 return i > 0
} }

View File

@@ -5,6 +5,7 @@ import (
"image" "image"
"log" "log"
"code.ur.gs/lupine/ordoor/internal/data"
"code.ur.gs/lupine/ordoor/internal/maps" "code.ur.gs/lupine/ordoor/internal/maps"
) )
@@ -74,8 +75,8 @@ func (m *Map) LoadSprites() error {
} }
// FIXME: get rid of this // FIXME: get rid of this
func (m *Map) Cell(x, y, z int) maps.Cell { func (m *Map) Cell(x, y, z int) *maps.Cell {
return m.raw.Cells.At(x, y, z) return m.raw.At(x, y, z)
} }
// SpritesForCell returns the sprites needed to correctly render this cell. // SpritesForCell returns the sprites needed to correctly render this cell.
@@ -101,6 +102,27 @@ func (m *Map) SpritesForCell(x, y, z int) ([]*Sprite, error) {
sprites = append(sprites, sprite) sprites = append(sprites, sprite)
} }
if chr := m.CharacterAt(x, y, z); chr != nil {
// Look up the correct animation, get the frame, boom
anim, err := m.assets.CharacterAnimation(chr.Type, data.AnimActionNone)
if err != nil {
return nil, err
}
sprites = append(sprites, anim.Frames[0])
}
return sprites, nil return sprites, nil
} }
func (m *Map) CharacterAt(x, y, z int) *maps.Character {
// FIXME: don't iterate
for i, _ := range m.raw.Characters {
chr := &m.raw.Characters[i]
if chr.XPos == x && chr.YPos == y && z == 0 { // FIXME: sort out ZPos
return chr
}
}
return nil
}

View File

@@ -94,6 +94,42 @@ type HasAction struct {
bits bitfield.BitField bits bitfield.BitField
} }
var (
cTypes = map[CharacterType]string{
CharacterTypeTactical: "Tactical",
CharacterTypeAssault: "Assault",
CharacterTypeDevastator: "Devastator",
CharacterTypeTerminator: "Terminator",
CharacterTypeApothecary: "Apothecary",
CharacterTypeTechmarine: "Techmarine",
CharacterTypeChaplain: "Chaplain",
CharacterTypeLibrarian: "Librarian",
CharacterTypeCaptain: "Captain",
CharacterTypeChaosMarine: "Chaos Marine",
CharacterTypeChaosLord: "Chaos Lord",
CharacterTypeChaosChaplain: "Chaos Chaplain",
CharacterTypeChaosSorcerer: "Chaos Sorcerer",
CharacterTypeChaosTerminator: "Chaos Terminator",
CharacterTypeKhorneBerserker: "Knorne Berserker",
CharacterTypeBloodThirster: "Bloodthirster",
CharacterTypeBloodLetter: "Bloodletter",
CharacterTypeFleshHound: "Flesh Hound",
CharacterTypeLordOfChange: "Lord of Change",
CharacterTypeFlamer: "Flamer",
CharacterTypePinkHorror: "Pink Horror",
CharacterTypeBlueHorror: "Blue Horror",
CharacterTypeChaosCultist: "Cultist",
}
)
func (c CharacterType) String() string {
if str, ok := cTypes[c]; ok {
return str
}
return "Unknown Character"
}
func LoadHasAction(filename string) (*HasAction, error) { func LoadHasAction(filename string) (*HasAction, error) {
scanner, err := asciiscan.New(filename) scanner, err := asciiscan.New(filename)
if err != nil { if err != nil {
@@ -161,6 +197,17 @@ func (h *HasAction) Actions(c CharacterType) []AnimAction {
return out return out
} }
// FIXME: Too slow
func (h *HasAction) Index(c CharacterType, requestedAction AnimAction) int {
for i, action := range h.Actions(c) {
if action == requestedAction {
return i
}
}
return -1
}
func (h *HasAction) Print() { func (h *HasAction) Print() {
fmt.Println(" Tac Ass Dev Term Apo Tech Chp Lib Cpt CMar CLrd CChp CSrc CTrm Kbz BTh BL FHnd LoC Flm PHr BHr Cult") fmt.Println(" Tac Ass Dev Term Apo Tech Chp Lib Cpt CMar CLrd CChp CSrc CTrm Kbz BTh BL FHnd LoC Flm PHr BHr Cult")
for a := AnimActionStart; a <= AnimActionEnd; a++ { for a := AnimActionStart; a <= AnimActionEnd; a++ {

View File

@@ -7,6 +7,7 @@ import (
"strings" "strings"
"github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/ebiten/inpututil"
"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"
@@ -92,6 +93,11 @@ func New(assets *assetstore.AssetStore, config *config.Config, ship *ship.Ship)
return out, out.exit return out, out.exit
} }
func (f *Flow) SetScenario(scenario *scenario.Scenario) {
f.current = f.drivers[mainGame]
f.scenario = scenario
}
func (f *Flow) Update(screenX, screenY int) error { func (f *Flow) Update(screenX, screenY int) error {
if f.exit != nil { if f.exit != nil {
return f.exit return f.exit
@@ -113,6 +119,14 @@ func (f *Flow) Update(screenX, screenY int) error {
if ebiten.IsKeyPressed(ebiten.KeyDown) { if ebiten.IsKeyPressed(ebiten.KeyDown) {
f.scenario.Viewpoint.Y += step f.scenario.Viewpoint.Y += step
} }
if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonLeft) {
f.scenario.SelectHighlightedCharacter()
// Now we need to update the info screens with data about the
// selected character. FIXME: oh, for data binding
f.selectedMainGameCharacter(f.scenario.SelectedCharacter())
}
} }
if f.scenario != nil { if f.scenario != nil {

View File

@@ -1,5 +1,9 @@
package flow package flow
import (
"code.ur.gs/lupine/ordoor/internal/maps"
)
// TODO: There are Chaos and Ultramarine versions of MainGame. Do we really want // TODO: There are Chaos and Ultramarine versions of MainGame. Do we really want
// to duplicate everything for both? // to duplicate everything for both?
@@ -16,7 +20,7 @@ func (f *Flow) linkMainGame() {
}) })
// 8: Character stats // 8: Character stats
f.onClick(mainGame, "8.21", func() { // Stat more buttont f.onClick(mainGame, "8.21", func() { // Stat more buttons
f.setActiveNow(mainGame, "7", true) f.setActiveNow(mainGame, "7", true)
f.setActiveNow(mainGame, "8", false) f.setActiveNow(mainGame, "8", false)
}) })
@@ -154,3 +158,48 @@ func (f *Flow) linkMainGameViewMenu() {
})) }))
} }
func (f *Flow) maybeSetErr(next func() error) {
if f.exit != nil {
return
}
f.exit = next()
}
func (f *Flow) selectedMainGameCharacter(chr *maps.Character) {
if chr == nil {
chr = &maps.Character{}
}
d := f.drivers[mainGame]
// 7.1 Portrait
f.maybeSetErr(func() error { return d.SetValue("7.2", chr.Name) }) // Name
// 7.3 doesn't exit
// 7.4 more button (ignore)
// 7.5 AP icon
// f.maybeSetErr(func() error { return d.SetValueInt("7.6", chr.ActionPoints)}) // AP meter
f.maybeSetErr(func() error { return d.SetValueInt("7.7", chr.ActionPoints) }) // AP value
// 7.8 armor icon
// 7.9 armor meter
f.maybeSetErr(func() error { return d.SetValueInt("7.10", chr.Armor) }) // armor value
// 7.11 health icon
// 7.12 health meter
f.maybeSetErr(func() error { return d.SetValueInt("7.13", chr.Health) }) // health value
// 7.14 action points status bar
// 7.15 armor status bar
// 7.16 health status bar
// 8.1 to 8.10 are hot spots
f.maybeSetErr(func() error { return d.SetValueInt("8.11", chr.ActionPoints) }) // AP
f.maybeSetErr(func() error { return d.SetValueInt("8.12", chr.Health) }) // Health
f.maybeSetErr(func() error { return d.SetValueInt("8.13", chr.Armor) }) // Armor
f.maybeSetErr(func() error { return d.SetValueInt("8.14", chr.BallisticSkill) }) // Ballistic Skill
f.maybeSetErr(func() error { return d.SetValueInt("8.15", chr.WeaponSkill) }) // Weapon Skill
f.maybeSetErr(func() error { return d.SetValueInt("8.16", chr.Strength) }) // Strength
f.maybeSetErr(func() error { return d.SetValueInt("8.17", chr.Toughness) }) // Toughness
// 8.18 Initiative
// 8.19 Attacks
f.maybeSetErr(func() error { return d.SetValueInt("8.20", chr.Leadership) }) // Leadership
}

View File

@@ -6,12 +6,14 @@ import (
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"image" "image"
"io"
"io/ioutil" "io/ioutil"
"log"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/lunixbochs/struc"
"code.ur.gs/lupine/ordoor/internal/data"
) )
var ( var (
@@ -31,93 +33,129 @@ const (
cellCount = MaxHeight * MaxLength * MaxWidth cellCount = MaxHeight * MaxLength * MaxWidth
) )
type Header struct { type GameMap struct {
IsCampaignMap uint32 // Tentatively: 0 = no, 1 = yes // Main Header
MinWidth uint32 IsCampaignMap bool `struc:"uint32"` // Tentatively: 0 = no, 1 = yes
MinLength uint32 MinWidth int `struc:"uint32"`
MaxWidth uint32 MinLength int `struc:"uint32"`
MaxLength uint32 MaxWidth int `struc:"uint32"`
Unknown1 uint32 MaxLength int `struc:"uint32"`
Unknown2 uint32 Unknown1 int `struc:"uint32"`
Unknown3 uint32 Unknown2 int `struc:"uint32"`
Unknown4 uint32 Unknown3 int `struc:"uint32"`
Magic [8]byte // "\x08\x00WHMAP\x00" Unknown4 int `struc:"uint32"`
Unknown5 uint32 Magic []byte `struc:"[8]byte"` // "\x08\x00WHMAP\x00"
Unknown6 uint32 Unknown5 int `struc:"uint32"`
SetName [8]byte // Links to a filename in `/Sets/*.set` Unknown6 int `struc:"uint32"`
// Need to investigate the rest of the header too SetName string `struc:"[8]byte"` // Links to a filename in `/Sets/*.set`
Padding []byte `struc:"[212]byte"`
// Per-cell data
NumCells int `struc:"skip"` // FIXME: We can't use []Cell below without this field
Cells []Cell `struc:"[]Cell,sizefrom=NumCells"`
// Trailer header
Discard1 [3]byte `struc:"[3]byte"` // First byte is size of trailer header?
TrailerMaxWidth int `struc:"uint32"`
TrailerMaxLength int `struc:"uint32"`
TrailerMinWidth int `struc:"uint32"`
TrailerMinLength int `struc:"uint32"`
NumCharacters int `struc:"uint32"`
TrailerUnknown1 int `struc:"uint32"`
TrailerUnknown2 int `struc:"uint16"`
TrailerUnknown3 int `struc:"uint16"`
TrailerUnknown4 int `struc:"uint32"`
NumThingies int `struc:"byte"`
TrailerUnknown5 []byte `struc:"[3]byte"`
Padding1 []byte `struc:"[20]byte"`
// FIXME: The rest is trash until Character & Thingy are worked out
Characters []Character `struc:"[]Character,sizefrom=NumCharacters"`
Thingies []Thingy `struc:"[]Thingy,sizefrom=NumThingies"`
Title string `struc:"[255]byte"`
Briefing string `struc:"[2048]byte"`
// Maybe? each contains either 0 or 1? Hard to say
TrailerUnknown6 []byte `struc:"[85]byte"`
} }
type TrailerHeader struct { type Cell struct {
Discard1 [3]byte // No idea what this lot is DoorAndCanisterRelated byte `struc:"byte"`
MaxWidth uint32 DoorLockAndReactorRelated byte `struc:"byte"`
MaxLength uint32 Unknown2 byte `struc:"byte"`
MinWidth uint32 Surface ObjRef
MinLength uint32 Left ObjRef
Right ObjRef
NumCharacters uint32 Center ObjRef
Unknown11 byte `struc:"byte"`
Unknown1 uint32 Unknown12 byte `struc:"byte"`
Unknown2 uint16 Unknown13 byte `struc:"byte"`
Unknown3 uint16 Unknown14 byte `struc:"byte"`
Unknown4 uint32 SquadRelated byte `struc:"byte"`
NumThingies uint32
Padding1 [20]byte
}
type TrailerTrailer struct {
Title [255]byte
Briefing [2048]byte
Unknown1 [85]uint8 // Maybe? each contains either 0 or 1? Hard to say
} }
type Character struct { type Character struct {
Unknown1 uint32 Unknown1 []byte `struc:"[178]byte"`
Type data.CharacterType `struc:"byte"`
Name string `struc:"[80]byte"`
// Attributes guessed by matching up numbers. Starts at 0x103
WeaponSkill int `struc:"byte"`
BallisticSkill int `struc:"byte"`
Unknown2 byte `struc:"byte"`
Leadership int `struc:"byte"`
Toughness int `struc:"byte"`
Strength int `struc:"byte"`
ActionPoints int `struc:"byte"`
Unknown3 byte `struc:"byte"`
Unknown4 byte `struc:"byte"`
Health int `struc:"byte"`
Unknown5 []byte `struc:"[91]byte"`
Armor int `struc:"byte"`
Unknown6 []byte `struc:"[84]byte"`
YPos int `struc:"byte"` // These are actually much more complicated
XPos int `struc:"byte"`
Unknown7 []byte `struc:"[317]byte"`
SquadNumber byte `struc:"byte"`
Unknown8 []byte `struc:"[927]byte"`
// TODO: each character may have a fixed number of subrecords for inventory
} }
type Characters []Character type Characters []Character
// TODO. These are triggers/reactors/etc. // TODO. These are triggers/reactors/etc.
type Thingy struct {} type Thingy struct {
Unknown1 int `struc:"uint32"`
}
type Thingies []Thingy type Thingies []Thingy
func (h Header) Width() int { func (g *GameMap) MapSetName() string {
return int(h.MaxWidth - h.MinWidth) return g.SetName
} }
func (h Header) Length() int { func (g *GameMap) MapSetFilename() string {
return int(h.MaxLength - h.MinLength) return g.MapSetName() + ".set"
}
func (h Header) Height() int {
return MaxHeight
}
func (h Header) MapSetName() string {
idx := bytes.IndexByte(h.SetName[:], 0)
if idx < 0 {
idx = 8 // all 8 bytes are used
}
return string(h.SetName[0:idx:idx])
}
func (h Header) MapSetFilename() string {
return h.MapSetName() + ".set"
} }
type ObjRef struct { type ObjRef struct {
AreaByte byte AreaByte byte `struc:"byte"`
SpriteAndFlagByte byte SpriteAndFlagByte byte `struc:"byte"`
} }
// The index into a set palette to retrieve the object // The index into a set palette to retrieve the object
func (o ObjRef) Index() int { func (o *ObjRef) Index() int {
return int(o.AreaByte) return int(o.AreaByte)
} }
func (o ObjRef) Sprite() int { func (o *ObjRef) Sprite() int {
// The top bit seems to be a flag of some kind // The top bit seems to be a flag of some kind
return int(o.SpriteAndFlagByte & 0x7f) return int(o.SpriteAndFlagByte & 0x7f)
} }
@@ -127,21 +165,6 @@ func (o ObjRef) IsActive() bool {
return (o.SpriteAndFlagByte & 0x80) == 0x80 return (o.SpriteAndFlagByte & 0x80) == 0x80
} }
type Cell struct {
DoorAndCanisterRelated byte
DoorLockAndReactorRelated byte
Unknown2 byte
Surface ObjRef
Left ObjRef
Right ObjRef
Center ObjRef
Unknown11 byte
Unknown12 byte
Unknown13 byte
Unknown14 byte
SquadRelated byte
}
func (c *Cell) At(n int) byte { func (c *Cell) At(n int) byte {
switch n { switch n {
case 0: case 0:
@@ -181,46 +204,26 @@ func (c *Cell) At(n int) byte {
return 0 return 0
} }
// Cells is always a fixed size; use At to get a cell according to x,y,z func (g *GameMap) At(x, y, z int) *Cell {
type Cells []Cell return &g.Cells[(z*MaxLength*MaxWidth)+(y*MaxWidth)+x]
func (c Cells) At(x, y, z int) Cell {
return c[(z*MaxLength*MaxWidth)+(y*MaxWidth)+x]
} }
func (h Header) Check() []error { func (g *GameMap) Check() error {
var out []error if bytes.Compare(expectedMagic, g.Magic) != 0 {
if h.IsCampaignMap > 1 { return fmt.Errorf("Unexpected magic value: %v", g.Magic)
out = append(out, fmt.Errorf("Expected 0 or 1 for IsCampaignMap, got %v", h.IsCampaignMap))
} }
if bytes.Compare(expectedMagic, h.Magic[:]) != 0 { // TODO: other consistency checks
out = append(out, fmt.Errorf("Unexpected magic value: %v", h.Magic))
}
return out return nil
}
type GameMap struct {
Header
Cells
TrailerHeader
Characters
Thingies
TrailerTrailer
// TODO: parse this into sections. This is the text that comes from the
// .TXT file. Maybe we don't need it at all since it should be duplicated in
// the trailer.
Text string
} }
func (m *GameMap) Rect() image.Rectangle { func (m *GameMap) Rect() image.Rectangle {
return image.Rect( return image.Rect(
int(m.Header.MinWidth), int(m.MinWidth),
int(m.Header.MinLength), int(m.MinLength),
int(m.Header.MaxWidth), int(m.MaxWidth),
int(m.Header.MaxLength), int(m.MaxLength),
) )
} }
@@ -243,23 +246,17 @@ func LoadGameMap(prefix string) (*GameMap, error) {
return nil, fmt.Errorf("Couldn't find %s.{map,txt}, even ignoring case", prefix) return nil, fmt.Errorf("Couldn't find %s.{map,txt}, even ignoring case", prefix)
} }
// A game map is composed of two files: .map and .txt // A game map is composed of two files: .map and .txt. We ignore the text file,
// since the content is replicated in the map file.
func LoadGameMapByFiles(mapFile, txtFile string) (*GameMap, error) { func LoadGameMapByFiles(mapFile, txtFile string) (*GameMap, error) {
out, err := loadMapFile(mapFile) out, err := loadMapFile(mapFile)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// TODO: load text and parse into sections if err := out.Check(); err != nil {
txt, err := ioutil.ReadFile(txtFile)
if err != nil {
return nil, err return nil, err
} }
out.Text = string(txt)
for _, err := range out.Check() {
log.Printf("%s: %v", mapFile, err)
}
return out, nil return out, nil
} }
@@ -297,6 +294,7 @@ func LoadGameMaps(dir string) (map[string]*GameMap, error) {
func loadMapFile(filename string) (*GameMap, error) { func loadMapFile(filename string) (*GameMap, error) {
var out GameMap var out GameMap
out.NumCells = cellCount
mf, err := os.Open(filename) mf, err := os.Open(filename)
if err != nil { if err != nil {
@@ -312,52 +310,46 @@ func loadMapFile(filename string) (*GameMap, error) {
defer zr.Close() defer zr.Close()
if err := binary.Read(zr, binary.LittleEndian, &out.Header); err != nil { if err := struc.UnpackWithOrder(zr, &out, binary.LittleEndian); err != nil {
return nil, fmt.Errorf("Error parsing %s: %v", filename, err)
}
// no gzip.SeekReader, so discard unread header bytes for now
discardSize := int64(cellDataOffset - binary.Size(&out.Header))
if _, err := io.CopyN(ioutil.Discard, zr, discardSize); err != nil {
return nil, err return nil, err
} }
out.Cells = make(Cells, cellCount) // Trim any trailing nulls off of the strings
if err := binary.Read(zr, binary.LittleEndian, &out.Cells); err != nil { nullTerminate(&out.SetName)
return nil, fmt.Errorf("Error parsing cells for %s: %v", filename, err) nullTerminate(&out.Title)
nullTerminate(&out.Briefing)
for i, _ := range out.Characters {
chr := &out.Characters[i]
nullTerminate(&chr.Name)
fmt.Printf("Character %v: %s\n", i, chr.String())
} }
if err := binary.Read(zr, binary.LittleEndian, &out.TrailerHeader); err != nil { fmt.Printf("Mission Title: %q\n", out.Title)
return nil, fmt.Errorf("Error parsing trailer header for %s: %v", filename, err) fmt.Printf("Mission Briefing: %q\n", out.Briefing)
}
log.Printf("Trailer Header: %#+v", out.TrailerHeader)
/*
// TODO: until we know how large each character record should be, we can't read this lot
out.Characters = make(Characters, int(out.TrailerHeader.NumCharacters))
if err := binary.Read(zr, binary.LittleEndian, &out.Characters); err != nil {
return nil, fmt.Errorf("Error parsing characters for %s: %v", filename, err)
}
out.Thingies = make(Thingies, int(out.TrailerHeader.NumThingies))
if err := binary.Read(zr, binary.LittleEndian, &out.Thingies); err != nil {
return nil, fmt.Errorf("Error parsing thingies for %s: %v", filename, err)
}
if err := binary.Read(zr, binary.LittleEndian, &out.TrailerTrailer); err != nil {
return nil, fmt.Errorf("Error parsing trailer trailer for %s: %v", filename, err)
}
log.Printf("Trailer Trailer: %s", out.TrailerTrailer.String())
*/
return &out, nil return &out, nil
} }
func (t *TrailerTrailer) String() string { func nullTerminate(s *string) {
sCpy := *s
idx := strings.Index(sCpy, "\x00")
if idx < 0 {
return
}
*s = sCpy[0:idx]
}
func (c *Character) String() string {
return fmt.Sprintf( return fmt.Sprintf(
"title=%q briefing=%q rest=%#+v", "squad=%v pos=(%v,%v) type=%q name=%q\n"+
strings.TrimRight(string(t.Title[:]), "\x00"), "\t%3d %3d %3d %3d %3d\n\t%3d %3d ??? ??? %3d\n",
strings.TrimRight(string(t.Briefing[:]), "\x00"), c.SquadNumber,
t.Unknown1, c.XPos, c.YPos,
c.Type.String(),
c.Name,
c.ActionPoints, c.Health, c.Armor, c.BallisticSkill, c.WeaponSkill,
c.Strength, c.Toughness /*c.Initiative, c.Attacks,*/, c.Leadership,
) )
} }

View File

@@ -33,7 +33,7 @@ const (
SubTypeLineBriefing SubMenuType = 41 SubTypeLineBriefing SubMenuType = 41
SubTypeThumb SubMenuType = 45 // A "thumb" appears to be a vertical slider SubTypeThumb SubMenuType = 45 // A "thumb" appears to be a vertical slider
SubTypeInvokeButton SubMenuType = 50 SubTypeInvokeButton SubMenuType = 50
SubTypeDoorHotspot3 SubMenuType = 60 // Maybe? Appears in Arrange.mnu SubTypeClickText SubMenuType = 60
SubTypeOverlay SubMenuType = 61 SubTypeOverlay SubMenuType = 61
SubTypeHypertext SubMenuType = 70 SubTypeHypertext SubMenuType = 70
SubTypeCheckbox SubMenuType = 91 SubTypeCheckbox SubMenuType = 91

View File

@@ -36,7 +36,7 @@ func (s *Scenario) Update(screenX, screenY int) error {
} }
// FIXME: adjust for Z level // FIXME: adjust for Z level
s.selectedCell = screenPos.ToISO() s.highlightedCell = screenPos.ToISO()
return nil return nil
} }
@@ -95,17 +95,15 @@ func (s *Scenario) Draw(screen *ebiten.Image) error {
return false return false
}) })
counter := map[string]int{} counter := 0
for _, pt := range toDraw { for _, pt := range toDraw {
for z := 0; z <= s.ZIdx; z++ { for z := 0; z <= s.ZIdx; z++ {
if err := s.renderCell(int(pt.X), int(pt.Y), z, screen, counter); err != nil { if err := s.renderCell(int(pt.X), int(pt.Y), z, screen, &counter); err != nil {
return err return err
} }
} }
} }
//log.Printf("%#+v", counter)
// Finally, draw cursor chrome // Finally, draw cursor chrome
// FIXME: it looks like we might need to do this in normal painting order... // FIXME: it looks like we might need to do this in normal painting order...
spr, err := s.specials.Sprite(0) spr, err := s.specials.Sprite(0)
@@ -114,7 +112,7 @@ func (s *Scenario) Draw(screen *ebiten.Image) error {
} }
op := ebiten.DrawImageOptions{} op := ebiten.DrawImageOptions{}
geo := s.geoForCoords(int(s.selectedCell.X), int(s.selectedCell.Y), 0) geo := s.geoForCoords(int(s.highlightedCell.X), int(s.highlightedCell.Y), 0)
op.GeoM = geo op.GeoM = geo
op.GeoM.Translate(-209, -332) op.GeoM.Translate(-209, -332)
op.GeoM.Translate(float64(spr.Rect.Min.X), float64(spr.Rect.Min.Y)) op.GeoM.Translate(float64(spr.Rect.Min.X), float64(spr.Rect.Min.Y))
@@ -127,11 +125,13 @@ func (s *Scenario) Draw(screen *ebiten.Image) error {
x1, y1 := geo.Apply(0, 0) x1, y1 := geo.Apply(0, 0)
ebitenutil.DebugPrintAt( ebitenutil.DebugPrintAt(
screen, screen,
fmt.Sprintf("(%d,%d)", int(s.selectedCell.X), int(s.selectedCell.Y)), fmt.Sprintf("(%d,%d)", int(s.highlightedCell.X), int(s.highlightedCell.Y)),
int(x1), int(x1),
int(y1), int(y1),
) )
ebitenutil.DebugPrintAt(screen, fmt.Sprintf("Sprites: %v", counter), 0, 16)
/* /*
// debug: draw a square around the selected cell // debug: draw a square around the selected cell
x2, y2 := geo.Apply(cellWidth, cellHeight) x2, y2 := geo.Apply(cellWidth, cellHeight)
@@ -164,7 +164,7 @@ func (s *Scenario) geoForCoords(x, y, z int) ebiten.GeoM {
return geo return geo
} }
func (s *Scenario) renderCell(x, y, z int, screen *ebiten.Image, counter map[string]int) error { func (s *Scenario) renderCell(x, y, z int, screen *ebiten.Image, counter *int) error {
sprites, err := s.area.SpritesForCell(x, y, z) sprites, err := s.area.SpritesForCell(x, y, z)
if err != nil { if err != nil {
return err return err
@@ -179,10 +179,7 @@ func (s *Scenario) renderCell(x, y, z int, screen *ebiten.Image, counter map[str
iso.Translate(-209, -332) iso.Translate(-209, -332)
for _, spr := range sprites { for _, spr := range sprites {
// if _, ok := counter[spr.ID]; !ok { *counter = *counter + 1
// counter[spr.ID] = 0
// }
// counter[spr.ID] = counter[spr.ID] + 1
op := ebiten.DrawImageOptions{GeoM: iso} op := ebiten.DrawImageOptions{GeoM: iso}
op.GeoM.Translate(float64(spr.Rect.Min.X), float64(spr.Rect.Min.Y)) op.GeoM.Translate(float64(spr.Rect.Min.X), float64(spr.Rect.Min.Y))

View File

@@ -1,6 +1,8 @@
package scenario package scenario
import ( import (
"log"
"code.ur.gs/lupine/ordoor/internal/maps" "code.ur.gs/lupine/ordoor/internal/maps"
) )
@@ -9,9 +11,24 @@ type CellPoint struct {
Z int Z int
} }
func (s *Scenario) CellAtCursor() (maps.Cell, CellPoint) { func (s *Scenario) CellAtCursor() (*maps.Cell, CellPoint) {
cell := s.area.Cell(int(s.selectedCell.X), int(s.selectedCell.Y), 0) cell := s.area.Cell(int(s.highlightedCell.X), int(s.highlightedCell.Y), 0)
return cell, CellPoint{IsoPt: s.selectedCell, Z: 0} return cell, CellPoint{IsoPt: s.highlightedCell, Z: 0}
}
func (s *Scenario) HighlightedCharacter() *maps.Character {
// FIXME: characters are always at zIdx 0 right now
return s.area.CharacterAt(int(s.highlightedCell.X), int(s.highlightedCell.Y), 0)
}
func (s *Scenario) SelectedCharacter() *maps.Character {
return s.selectedCharacter
}
func (s *Scenario) SelectHighlightedCharacter() {
chr := s.HighlightedCharacter()
log.Printf("Selected character %s", chr)
s.selectedCharacter = chr
} }
func (s *Scenario) ChangeZIdx(by int) { func (s *Scenario) ChangeZIdx(by int) {

View File

@@ -2,10 +2,10 @@
package scenario package scenario
import ( import (
"fmt"
"image" "image"
"code.ur.gs/lupine/ordoor/internal/assetstore" "code.ur.gs/lupine/ordoor/internal/assetstore"
"code.ur.gs/lupine/ordoor/internal/maps"
) )
type Scenario struct { type Scenario struct {
@@ -14,7 +14,9 @@ type Scenario struct {
tick int tick int
turn int turn int
selectedCell IsoPt
highlightedCell IsoPt
selectedCharacter *maps.Character
// All these must be modified by user actions somehow. // All these must be modified by user actions somehow.
// TODO: extract into the idea of a viewport passed to Update / Draw somehow? // TODO: extract into the idea of a viewport passed to Update / Draw somehow?
@@ -36,9 +38,9 @@ func NewScenario(assets *assetstore.AssetStore, name string) (*Scenario, error)
} }
// Eager load sprites. TODO: do we really want to do this? // Eager load sprites. TODO: do we really want to do this?
if err := area.LoadSprites(); err != nil { //if err := area.LoadSprites(); err != nil {
return nil, fmt.Errorf("Eager-loading sprites failed: %v", err) // return nil, fmt.Errorf("Eager-loading sprites failed: %v", err)
} //}
out := &Scenario{ out := &Scenario{
area: area, area: area,

View File

@@ -78,8 +78,10 @@ func (d *Driver) buildRecord(r *menus.Record) (*Widget, error) {
switch r.Type { switch r.Type {
case menus.SubTypeSimpleButton, menus.SubTypeInvokeButton: case menus.SubTypeSimpleButton, menus.SubTypeInvokeButton:
_, widget, err = d.buildButton(r.Props()) _, widget, err = d.buildButton(r.Props())
case menus.SubTypeDoorHotspot1, menus.SubTypeDoorHotspot2, menus.SubTypeDoorHotspot3: case menus.SubTypeDoorHotspot1, menus.SubTypeDoorHotspot2:
_, widget, err = d.buildDoorHotspot(r.Props()) _, widget, err = d.buildDoorHotspot(r.Props())
case menus.SubTypeClickText:
_, widget, err = d.buildClickText(r.Props())
case menus.SubTypeOverlay: case menus.SubTypeOverlay:
_, widget, err = d.buildOverlay(r.Props()) _, widget, err = d.buildOverlay(r.Props())
case menus.SubTypeHypertext: case menus.SubTypeHypertext:

View File

@@ -157,9 +157,9 @@ func (l *listBox) refresh() {
// FIXME: noninteractive isn't set up for dynamic text yet. Need to // FIXME: noninteractive isn't set up for dynamic text yet. Need to
// generate textImg on demand instead of once at start. // generate textImg on demand instead of once at start.
if ni.label != nil { if ni.label != nil {
ni.label.text = "" ni.label.str = ""
if len(l.strings) > l.offset+i { if len(l.strings) > l.offset+i {
ni.label.text = l.strings[l.offset+i] ni.label.str = l.strings[l.offset+i]
} }
} }
} }

View File

@@ -32,12 +32,13 @@ type noninteractive struct {
hoverImpl hoverImpl
} }
// Paint some text to screen // Paint some text to screen, possibly settable
type label struct { type label struct {
locator string
align AlignMode align AlignMode
rect image.Rectangle rect image.Rectangle
text string
font *assetstore.Font font *assetstore.Font
valueImpl
} }
// This particular animation has entry and exit sequences, which are invoked // This particular animation has entry and exit sequences, which are invoked
@@ -109,6 +110,33 @@ func (d *Driver) buildHypertext(p *menus.Properties) (*noninteractive, *Widget,
return ni, widget, nil return ni, widget, nil
} }
func (d *Driver) buildClickText(p *menus.Properties) (*noninteractive, *Widget, error) {
ni, err := d.buildNoninteractive(p)
if err != nil {
return nil, nil, err
}
fnt := d.menu.Font(p.FontType/10 - 1)
// FIXME: is this always right? Seems to make sense for Main.mnu
ni.label = &label{
locator: ni.locator,
font: fnt,
rect: ni.rect, // We will be centered by default
// Starts with no text. The text specified in the menu is hovertext
}
widget := &Widget{
Locator: ni.locator,
Active: p.Active,
ownClickables: []clickable{ni},
ownPaintables: []paintable{ni},
ownValueables: []valueable{ni.label},
}
return ni, widget, nil
}
// An overlay is a static image + some text that needs to be rendered // An overlay is a static image + some text that needs to be rendered
func (d *Driver) buildOverlay(p *menus.Properties) (*noninteractive, *Widget, error) { func (d *Driver) buildOverlay(p *menus.Properties) (*noninteractive, *Widget, error) {
ni, err := d.buildNoninteractive(p) ni, err := d.buildNoninteractive(p)
@@ -129,7 +157,7 @@ func (d *Driver) buildOverlay(p *menus.Properties) (*noninteractive, *Widget, er
ni.label = &label{ ni.label = &label{
font: fnt, font: fnt,
rect: ni.rect, // We will be centered by default rect: ni.rect, // We will be centered by default
text: p.Text, valueImpl: valueImpl{str: p.Text},
} }
} else { } else {
log.Printf("Overlay without text detected in %v", p.Locator) log.Printf("Overlay without text detected in %v", p.Locator)
@@ -253,6 +281,10 @@ func (a *animationHover) setHoverState(value bool) {
a.hoverImpl.setHoverState(value) a.hoverImpl.setHoverState(value)
} }
func (l *label) id() string {
return l.locator
}
// Top-left of where to start drawing the text. We want it to appear to be in // Top-left of where to start drawing the text. We want it to appear to be in
// the centre of the rect. // the centre of the rect.
// //
@@ -260,7 +292,7 @@ func (a *animationHover) setHoverState(value bool) {
func (l *label) pos() image.Point { func (l *label) pos() image.Point {
pos := l.rect.Min pos := l.rect.Min
textRect := l.font.CalculateBounds(l.text) textRect := l.font.CalculateBounds(l.str)
// Centre the text horizontally // Centre the text horizontally
if l.align == AlignModeCentre { if l.align == AlignModeCentre {
@@ -287,7 +319,7 @@ func (l *label) regions(tick int) []region {
pt := l.pos() pt := l.pos()
for _, r := range l.text { for _, r := range l.str {
glyph, err := l.font.Glyph(r) glyph, err := l.font.Glyph(r)
if err != nil { if err != nil {
log.Printf("FIXME: ignoring misssing glyph %v", r) log.Printf("FIXME: ignoring misssing glyph %v", r)