Compare commits

...

24 Commits

Author SHA1 Message Date
55c2232e08 Fix a SIGSEGV 2024-10-23 23:54:48 +01:00
ac4675fa2c Fix some log.Fatal calls 2024-10-23 23:47:55 +01:00
5f7654d267 Update dependencies 2024-10-23 23:45:26 +01:00
89888ce004 Stringify AnimAction 2024-10-23 23:44:44 +01:00
16767da6f1 Bump a few more dependencies 2023-07-16 23:11:01 +01:00
c5b80ed8bc Bump ebiten 2023-07-16 22:51:16 +01:00
891edecc60 Update dependencies 2023-01-17 22:10:46 +00:00
85979834c8 Update ebiten and golang 2021-10-23 22:44:39 +01:00
96dbb297cd Bump ebiten 2021-03-01 22:26:45 +00:00
92fa0fc5d6 UNTESTED: ebiten v2 2020-11-21 19:27:09 +00:00
c5e6abb798 First attempt at character orientation 2020-06-13 23:10:21 +01:00
5df050b4ef Substitute unknown glyphs 2020-06-13 18:23:50 +01:00
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
65bae80d40 Add a note about SaW trailer 2020-06-08 00:48:19 +01:00
e8e9811b5d More map trailer work 2020-06-08 00:24:57 +01:00
a6fdbaef2b Make some progress decoding map trailer 2020-06-07 01:44:28 +01:00
0bf8233cd1 Fix binary paths in README 2020-06-06 12:45:10 +01:00
c2cbf1d95d Get the initial copyright notice displaying
This is really awful, but it's nice to check it off.
2020-06-06 12:44:08 +01:00
54fe95239e More README niceness 2020-06-05 22:47:06 +01:00
40 changed files with 1068 additions and 403 deletions

1
.gitignore vendored
View File

@@ -5,4 +5,5 @@
/SL
/SaW
/WoW
/WoW-CD
/bin

View File

@@ -72,6 +72,10 @@ The menu system seen in Chaos Gate is not present; instead, there is a `BUTTONS`
directory and a lot of `pcx` files under `PIC` that, I suspect, do the job for
this game.
Even with a full installation, Wages of War leaves a lot of data on the CD. It
may be best to run solely from the `WOW` directory on the CD, assuming it's a
strict superset of what gets installed, data-wise.
## Long-term goals
Once full playthrough of the official single-player campaign for all four games
@@ -96,7 +100,8 @@ Completely new fantasy game using the same engine.
## Building from source
I'm writing code in Go at the moment, so you'll need to have a Go runtime
installed on your system:
installed on your system. Dependency management uses `go mod`, so ensure you
have at least Go 1.11.
```
$ go version
@@ -114,28 +119,25 @@ Debian:
You can then run `make all` in the source tree to get the binaries that are
present at the moment.
Place your WH40K: Chaos Gate installation in `./orig` to benefit from automatic
path defaults. Otherwise, point to it with `-game-path`
## Configuring
The `view-map` binary attempts to render a map, and is the current focus of
effort. Once I can render a whole map, including pre-placed characters (cultist
scum), things can start to get more interesting.
Since we support multiple games, a fair bit of configuration is required. Copy
`config.toml.example` to `config.toml` and edit it to your requirements. The
`data_dir` for the engine(s) you want to use is probably the most important bit,
along with the `default_engine`.
Current status: almost pixel-perfect map rendering. Static objects (four per map
coordinate: floor, centre, left, and right) are rendered fine, and each Z level
looks good. There are a few minor artifacts here and there.
The various games all use snapshots of the original engine at different points
in time, and specify a lot in code that we need to specify in data. That should
all go into the config file, so new games will be able to adapt the engine to
their needs.
Characters and animations aren't touched at all yet. Rendering performance is
poor. No gameplay, no campaign logic. Interaction with the play area is minimal
and limited to pan, zoom, and click for basic console output.
Still, I'm proud of myself.
## Running
To run:
```
$ make view-map
$ ./view-map -map Chapter01
$ ./bin/view-map -map Chapter01
```
Looks like this:
@@ -145,27 +147,29 @@ Looks like this:
Use the arrow keys to scroll around the map, the mouse wheel to zoom, and the
`1` - `7` keys to change Z level.
Dependency management uses `go mod`, so ensure you have at least Go 1.11.
There is the **start** of the menu / campaign flow in a `ordoor` binary:
```
$ cp config.toml.example config.toml
$ make ordoor
$ ./ordoor
```
This plays the introductory videos so far, and nothing else.
Menus are in the process of being rendered; you can use the `view-menu` binary
to inspect them:
Menus / UI widgets have fairly good support now; you can use the `view-menu`
binary to inspect them:
```
make view-menu
./view-menu -menu ./orig/Menu/Main.mnu
./bin/view-menu -menu Main
```
This menu *displays* OK, including
This renders the menus found in Chaos Gate and Soldiers At War. The Squad Leader
format seems basically the same, but has some extra files and aren't 8-bit
colour. They don't display at the moment. Wages of War uses a different format
altogether.
For Chaos Gate, there is the **start** of the game in an `ordoor` binary:
```
$ make ordoor
$ ./bin/ordoor
```
The idea is to hook all the different parts together, and to an abstract game
state (which is called `ship` for ordoor), to make the whole thing playable. It
isn't playable *yet*, but it's heading in that direction.
## Sound
@@ -181,3 +185,9 @@ $ ./scripts/convert-wav ./orig/Wav
As with video playback, the ambition is to *eventually* remove this dependency
and operate on the unmodified files instead.
## Resources
Here's a collection of links that I'm finding useful or otherwise interesting,
and don't want to lose track of...
* [Historical geocities modders](http://www.oocities.org/timessquare/galaxy/6777/)

View File

@@ -139,16 +139,15 @@ func loadMapsFrom(mapsPath string) {
}
log.Printf("Maps in %s:", mapsPath)
for key, gameMap := range gameMaps {
rect := gameMap.Rect()
hdr := gameMap.Header
for key, gm := range gameMaps {
rect := gm.Rect()
fmt.Printf(
" * `%s`: IsCampaignMap=%v W=%v:%v L=%v:%v SetName=%s\n",
key,
hdr.IsCampaignMap,
gm.IsCampaignMap,
rect.Min.X, rect.Max.X,
rect.Min.Y, rect.Max.Y,
string(hdr.SetName[:]),
string(gm.SetName),
)
}
}
@@ -222,5 +221,8 @@ func loadIdx(idxPath string) {
for i, group := range idx.Groups {
log.Printf("Group %2d: %4d records, start sprite is %6d", i, len(group.Records), group.Spec.SpriteIdx)
for i, rec := range group.Records {
log.Printf("\t%3d: %#+v", i, rec)
}
}
}

View File

@@ -7,7 +7,7 @@ import (
"math"
"os"
"github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/ebiten/v2"
"golang.org/x/image/colornames"
"code.ur.gs/lupine/ordoor/internal/assetstore"
@@ -100,7 +100,7 @@ func main() {
func (e *env) Update(screenX, screenY int) error {
if e.step == 0 || e.lastState != e.state {
ani, err := e.assets.Animation(e.state.groupIdx, e.state.recIdx)
ani, err := e.assets.Animation(e.state.groupIdx, e.state.recIdx, 0) // FIXME: why 0?
if err != nil {
return err
}
@@ -131,7 +131,7 @@ func (e *env) Draw(screen *ebiten.Image) error {
if len(e.ani.Frames) > 0 {
sprite := e.ani.Frames[e.step/4%len(e.ani.Frames)]
return screen.DrawImage(sprite.Image, &ebiten.DrawImageOptions{GeoM: cam})
screen.DrawImage(sprite.Image, &ebiten.DrawImageOptions{GeoM: cam})
}
return nil

View File

@@ -7,7 +7,7 @@ import (
"math"
"os"
"github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/ebiten/v2"
"code.ur.gs/lupine/ordoor/internal/assetstore"
"code.ur.gs/lupine/ordoor/internal/config"
@@ -69,7 +69,7 @@ func main() {
win, err := ui.NewWindow(env, "View Font: "+*fontName, *winX, *winY)
if err != nil {
log.Fatal("Couldn't create window: %v", err)
log.Fatalf("Couldn't create window: %v", err)
}
win.OnMouseWheel(env.changeZoom)
@@ -103,9 +103,7 @@ func (e *env) Draw(screen *ebiten.Image) error {
op.GeoM.Translate(float64(xOff), 0)
op.GeoM.Scale(e.state.zoom, e.state.zoom) // apply current zoom factor
if err := screen.DrawImage(glyph.Image, op); err != nil {
return err
}
screen.DrawImage(glyph.Image, op)
xOff += glyph.Rect.Dx()
}

View File

@@ -6,11 +6,13 @@ import (
"math"
"os"
"github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/ebiten/v2"
"code.ur.gs/lupine/ordoor/internal/assetstore"
"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/ship"
"code.ur.gs/lupine/ordoor/internal/ui"
)
@@ -25,6 +27,7 @@ var (
)
type env struct {
flow *flow.Flow
scenario *scenario.Scenario
}
@@ -51,27 +54,39 @@ func main() {
log.Fatalf("Failed to load scenario %v: %v", *gameMap, err)
}
env := &env{
scenario: scenario,
var realEnv *env
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 {
log.Fatal("Couldn't create window: %v", err)
log.Fatalf("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++ {
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.OnMouseWheel(env.changeZoom)
win.OnMouseClick(realEnv.showCellData)
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 {
log.Fatal(err)
@@ -79,11 +94,19 @@ func main() {
}
func (e *env) Update(screenX, screenY int) error {
return e.scenario.Update(screenX, screenY)
if e.flow != nil {
return e.flow.Update(screenX, screenY)
} else {
return e.scenario.Update(screenX, screenY)
}
}
func (e *env) Draw(screen *ebiten.Image) error {
return e.scenario.Draw(screen)
if e.flow != nil {
return e.flow.Draw(screen)
} else {
return e.scenario.Draw(screen)
}
}
func (e *env) changeOrigin(byX, byY int) func() {

View File

@@ -5,7 +5,7 @@ import (
"log"
"os"
"github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/ebiten/v2"
"code.ur.gs/lupine/ordoor/internal/assetstore"
"code.ur.gs/lupine/ordoor/internal/config"
@@ -58,7 +58,7 @@ func main() {
win, err := ui.NewWindow(driver, "View Menu: "+*menuName, *winX, *winY)
if err != nil {
log.Fatal("Couldn't create window: %v", err)
log.Fatalf("Couldn't create window: %v", err)
}
// Change the active dialogue

View File

@@ -10,7 +10,7 @@ import (
"path/filepath"
"time"
"github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/ebiten/v2"
"code.ur.gs/lupine/ordoor/internal/maps"
"code.ur.gs/lupine/ordoor/internal/sets"
@@ -76,7 +76,7 @@ func main() {
win, err := ui.NewWindow(env, "View Map "+*mapFile, *winX, *winY)
if err != nil {
log.Fatal("Couldn't create window: %v", err)
log.Fatalf("Couldn't create window: %v", err)
}
win.OnKeyUp(ebiten.KeyEnter, env.toggleAutoUpdate)
@@ -173,16 +173,12 @@ func (e *env) Update(screenX, screenY int) error {
func (e *env) Draw(screen *ebiten.Image) error {
gameMap := e.gameMap
rect := gameMap.Rect()
imd, err := ebiten.NewImage(rect.Dx(), rect.Dy(), ebiten.FilterDefault)
if err != nil {
return err
}
imd := ebiten.NewImage(rect.Dx(), rect.Dy())
for y := int(rect.Min.Y); y < int(rect.Max.Y); y++ {
for x := int(rect.Min.X); x < int(rect.Max.X); x++ {
cell := gameMap.Cells.At(x, y, int(e.state.zIdx))
imd.Set(x, y, makeColour(&cell, e.state.cellIdx))
cell := gameMap.At(x, y, int(e.state.zIdx))
imd.Set(x, y, makeColour(cell, e.state.cellIdx))
}
}
@@ -193,7 +189,9 @@ func (e *env) Draw(screen *ebiten.Image) error {
cam.Scale(e.state.zoom, e.state.zoom) // apply current zoom factor
cam.Rotate(0.785) // Apply isometric angle
return screen.DrawImage(imd, &ebiten.DrawImageOptions{GeoM: cam})
screen.DrawImage(imd, &ebiten.DrawImageOptions{GeoM: cam})
return nil
}
// Converts pixel coordinates to cell coordinates

View File

@@ -7,7 +7,7 @@ import (
"math"
"os"
"github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/ebiten/v2"
"code.ur.gs/lupine/ordoor/internal/assetstore"
"code.ur.gs/lupine/ordoor/internal/config"
@@ -136,7 +136,9 @@ func (e *env) Draw(screen *ebiten.Image) error {
cam.Translate(float64(e.state.origin.X), float64(e.state.origin.Y)) // Move to origin
cam.Scale(e.state.zoom, e.state.zoom) // apply current zoom factor
return screen.DrawImage(sprite.Image, &ebiten.DrawImageOptions{GeoM: cam})
screen.DrawImage(sprite.Image, &ebiten.DrawImageOptions{GeoM: cam})
return nil
}
func (e *env) changeSprite(by int) func() {

View File

@@ -7,7 +7,7 @@ import (
"math"
"os"
"github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/ebiten/v2"
"code.ur.gs/lupine/ordoor/internal/assetstore"
"code.ur.gs/lupine/ordoor/internal/config"
@@ -71,7 +71,7 @@ func main() {
win, err := ui.NewWindow(env, "View Set: "+*setName, *winX, *winY)
if err != nil {
log.Fatal("Couldn't create window: %v", err)
log.Fatalf("Couldn't create window: %v", err)
}
win.OnKeyUp(ebiten.KeyLeft, env.changeObjIdx(-1))
@@ -123,7 +123,9 @@ func (e *env) Draw(screen *ebiten.Image) error {
// TODO: centre the image
return screen.DrawImage(sprite.Image, &ebiten.DrawImageOptions{GeoM: cam})
screen.DrawImage(sprite.Image, &ebiten.DrawImageOptions{GeoM: cam})
return nil
}
func (e *env) changeObjIdx(by int) func() {

View File

@@ -3,7 +3,7 @@ video_player = ["mpv", "--no-config", "--keep-open=no", "--force-window=no", "--
default_engine = "ordoor"
[engines.geas] # Wages of War -> Gifts of Peace -> Geas
data_dir = "./WoW"
data_dir = "./WoW-CD"
palette = "WagesOfWar"
[engines.ordoor] # Chaos Gate -> Order Door -> Ordoor
@@ -14,9 +14,9 @@ default_engine = "ordoor"
data_dir = "./SaW"
palette = "SoldiersAtWar"
# [engines.] Squad Leader -> ??? -> ???
# data_dir = "./SL"
# palette = "SquadLeader" # may not be relevant?
[engines.sl] # Squad Leader -> ??? -> ???
data_dir = "./SL"
palette = "ChaosGate" # may not be relevant?
[options]
play_movies = true

View File

@@ -392,8 +392,11 @@ well-aligned amount.
Investigation has so far suggested the following:
* `Cell[0]` seems related to doors and canisters. Observed:
* Nothing special: 0x38
* ???: 0x39
* Imperial crate: 0x28
* Door: 0xB8
* `Cell[1]` seems related to special placeables (but not triggers). Bitfield. Observed:
* 0x01: Reactor
* 0x20: Door or door lock?
@@ -408,12 +411,12 @@ Investigation has so far suggested the following:
* `Cell[7]` Object 2 (Right) Area (Sets/*.set lookup)
* `Cell[6]` Object 2 (Right) Sprite + active flag
* `Cell[9]` Object 3 (Center) Area (Sets/*.set lookup)
* `Cell[10]` Object 3 (Right) Sprite + active flag
* `Cell[11]` all 255?
* `Cell[10]` Object 3 (Center) Sprite + active flag
* `Cell[11]` all 255? Vehicle?
* `Cell[12]` all 0?
* `Cell[13]` all 0?
* `Cell[14]` all 0?
* `Cell[15]` shows squad positions, MP start positions, etc, as 0x04
* `Cell[15]` shows squad positions, MP start positions, etc, as 0x04. Bitfield?
Mapping the altar in Chapter01 to the map01 set suggests it's a palette entry
lookup, 0-indexed. `U` debug in WH40K_TD.exe says the cell's `Object 3-Center`
@@ -515,11 +518,196 @@ Around 001841A0: mission objectives!
00184240 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
```
Since all the files are exactly the same length uncompressed, I'm going to
assume these are all a fixed number of fixed-size records when looking into it.
Relative offsets from the start of the trailer, we have:
| Offset | Text |
| -------- | ---- |
| `0xEE` | Mania |
| `0x78A` | Dagon |
| `0xE26` | Nihasa |
| `0x14C2` | Samnu |
| `0x1b5e` | Bael |
| `0x2896` | Gigamen |
| `0x2f32` | Valefor |
| `0x35ce` | Baalberith |
| `0x3c6a` | Fenriz |
| `0x4306` | #Character |
| `0x49a2` | Apollyon |
So there are 1692 bytes between each name (the names probably don't come at the
start of each block, but it's still a useful stride). Presumably `#Character` is
a space for one of the player characters, while the others specify an NPC placed
on the map.
There's 56 of these records between the first and last name we see - `Ahpuch`.
Then there are a number of other strings that seem related to triggers / events,
including lots that say `NO FILE`. The first two are 96 bytes apart; from then
on they seem to be placed variably apart from each other; I've seen 96, 256, and
352 byte offsets.
At 0x20916 the mission objective is readable.
At 0x2092a the mission description is readable.
Generating another map with just 5 characters on it, things look different:
* Trailer size is 13543 bytes
* There are only 5 names
* There are none of the trigger/event strings
* Mission title is found at 0x2b93
* Mission briefing is found at 0x2c92
Since the trailer is a variable size, there must be a header that tells us how
many of each type of record to read. Peeking at the differences in `vbindiff`:
```
Chapter01.MAP.Trailer
0000 0000: 38 00 00 68 00 00 00 50 00 00 00 1A 00 00 00 14 8..h...P ........
0000 0010: 00 00 00 3A 00 00 00 00 38 25 00 04 00 00 00 00 ...:.... 8%......
0000 0020: 00 00 00 1A 00 00 00 00 00 00 00 00 00 00 00 00 ........ ........
0000 0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ........ ........
0000 0040: 00 00 00 00 00 00 00 00 00 00 00 02 00 00 00 00 ........ ........
0000 0050: 00 00 00 32 00 00 00 00 00 00 00 00 00 00 00 00 ...2.... ........
TINYSQUAD.MAP.Trailer
0000 0000: 38 00 00 4B 00 00 00 3C 00 00 00 37 00 00 00 28 8..K...< ...7...(
0000 0010: 00 00 00 05 00 00 00 00 2B 3A 00 04 00 00 00 05 ........ +:......
0000 0020: 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 ........ ........
0000 0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ........ ........
0000 0040: 00 00 00 00 00 00 00 00 00 00 00 02 00 00 00 00 ........ ........
0000 0050: 00 00 00 32 00 00 00 00 00 00 00 00 00 00 00 00 ...2.... ........
```
The size of the trailer for Chapter01 is 139,483 bytes, assuming it starts at
`0x163890`. However, things may be a lot more sensible if we drop 3 bytes off
the start of that to get the fields into little-endian alignment. Have I made a
maths error above somewhere? Is it some sort of alignment thing? Do those 3
bytes actually have meaning?
Ignoring them for now, here's a first guess at a header:
| Offset | Size | Meaning |
| ------ | ---- | ------- |
| 0 | 4 | Map maximum X + 1 |
| 4 | 4 | Map maximum Y + 1 |
| 8 | 4 | Map minimum X |
| 12 | 4 | Map minimum Y |
| 16 | 4 | Number of character records |
| 20 | 4 | Padding? - invariant `00 00 00 00` |
| 24 | 2 | ??? - varies. Seems related to character/squad position? |
| 26 | 2 | ??? - invariant `00 04` |
| 28 | 4 | ??? - varies (0 vs 5) |
| 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? |
56 bytes of data is interesting because the value of that first, ignored byte is
0x38 - perhaps it's a skip value + 2 bytes of padding? It's just weird. Keep
ignoring it for now.
0x4b contains the next non-null byte; is the gap between the the number of
thingies, and it, padding? Minus a bit? 0x50 is another non-null byte. Then
it's all zeroes until one byte before the first name at 0xee.
Individual cells seem to have a flag to say "We have a character in us", but not
the number for the character themselves, so the coordinates must be in the
per-character records also. There are several candidates for this.
Placing a single character at (64,49) causes those bytes to show up at four
offsets - 0x18 (!), 0x1F4, 0x1F8, and 0x6C8.
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
a first approximation?
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 | 895 | ??? |
| 1660 | 1? | Orientation? Could also be `0x680`... |
| 1661 | 31 | ??? |
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 - squad leaders mess up a direct
correlation - but a fixed offset table allows me to draw the characters \o/.
Putting 8 characters onto a map and orienting them in the compass points, we see
numbers ranging from 0 to 7 at 0x67c and 0x680. Assuming this is the scheme
used, north is value 1, northeast value 2, and northwest value 0.
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 |
| ----- | ---- | ------- |
| 0 | 255 | Title |
| 255 | 2048 | Briefing |
| 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
@@ -600,3 +788,15 @@ xxd -s 0xc0 -c 13 -l 260 -g 13 BIGGESTMAP.MAP
000000e7: 00 00 85 01 00 00 00 00 00 ff 00 00 1f .............
# ...
```
This can be interpreted more or less the same way as the Chaos Gate maps now,
and the `soldiers-at-war` branch contains a hacked-up implementation that kind
of works \o/.
Does the same trailer apply? Seemingly not. Looking at `PARIS.MAP`, there's no
similarity at first glance.
However, I did manage to track down 4 32-bit ints inside the trailer, starting
at `0x121ad1`, which specify dimensions of the map, at least. Perhaps the
position has moved, but some of the data is the same? It's 3320 bytes into the
trailer.

35
go.mod
View File

@@ -1,18 +1,33 @@
module code.ur.gs/lupine/ordoor
go 1.14
go 1.22.0
toolchain go1.23.2
require (
github.com/BurntSushi/toml v0.3.1
github.com/BurntSushi/toml v1.4.0
github.com/emef/bitfield v0.0.0-20170503144143-7d3f8f823065
github.com/hajimehoshi/ebiten v1.11.1
github.com/jfreymuth/oggvorbis v1.0.1 // indirect
github.com/hajimehoshi/ebiten/v2 v2.8.2
github.com/kr/text v0.2.0 // indirect
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/stretchr/testify v1.5.1
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5 // indirect
golang.org/x/image v0.0.0-20200119044424-58c23975cae1
golang.org/x/mobile v0.0.0-20200329125638-4c31acba0007 // indirect
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
github.com/samuel/go-pcx v0.0.0-20210515040514-6a5ce4d132f7
github.com/stretchr/testify v1.9.0
golang.org/x/image v0.21.0
golang.org/x/sys v0.26.0 // indirect
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/ebitengine/gomobile v0.0.0-20241016134836-cc2e38a7c0ee // indirect
github.com/ebitengine/hideconsole v1.0.0 // indirect
github.com/ebitengine/oto/v3 v3.3.1 // indirect
github.com/ebitengine/purego v0.8.1 // indirect
github.com/jezek/xgb v1.1.1 // indirect
github.com/jfreymuth/oggvorbis v1.0.5 // indirect
github.com/jfreymuth/vorbis v1.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sync v0.8.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

133
go.sum
View File

@@ -1,109 +1,48 @@
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ebitengine/gomobile v0.0.0-20241016134836-cc2e38a7c0ee h1:YoNt0DHeZ92kjR78SfyUn1yEf7KnBypOFlFZO14cJ6w=
github.com/ebitengine/gomobile v0.0.0-20241016134836-cc2e38a7c0ee/go.mod h1:ZDIonJlTRW7gahIn5dEXZtN4cM8Qwtlduob8cOCflmg=
github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj1yReDqE=
github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A=
github.com/ebitengine/oto/v3 v3.3.1 h1:d4McwGQuXOT0GL7bA5g9ZnaUEIEjQvG3hafzMy+T3qE=
github.com/ebitengine/oto/v3 v3.3.1/go.mod h1:MZeb/lwoC4DCOdiTIxYezrURTw7EvK/yF863+tmBI+U=
github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/emef/bitfield v0.0.0-20170503144143-7d3f8f823065 h1:7QVNyw2v9R1qOvbe9vfeVJWWKCSnd2Ap+8l8/CtG9LM=
github.com/emef/bitfield v0.0.0-20170503144143-7d3f8f823065/go.mod h1:uN4GbWHfit2ByfOKQ4K6fuLy1/Os2eLynsIrDvjiDgM=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72 h1:b+9H1GAsx5RsjvDFLoS5zkNBzIQMuVKUYQDmxU3N5XE=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/gofrs/flock v0.7.1 h1:DP+LD/t0njgoPBvT5MJLeliUIVQR03hiKR6vezdwHlc=
github.com/gofrs/flock v0.7.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/hajimehoshi/bitmapfont v1.2.0/go.mod h1:h9QrPk6Ktb2neObTlAbma6Ini1xgMjbJ3w7ysmD7IOU=
github.com/hajimehoshi/ebiten v1.11.0-alpha.2.0.20200101150127-38815ba801a5 h1:hke9UdXY1YPfqjXG1bCSZnoVnfVBw9SzvmlrRn3dL3w=
github.com/hajimehoshi/ebiten v1.11.0-alpha.2.0.20200101150127-38815ba801a5/go.mod h1:0SLvfr8iI2NxzpNB/olBM+dLN9Ur5a9szG13wOgQ0nQ=
github.com/hajimehoshi/ebiten v1.11.0 h1:+pIxfzfVgRbHGM7wBAJtgzPiWiZopA7lyIKNQqc9amk=
github.com/hajimehoshi/ebiten v1.11.0/go.mod h1:aDEhx0K9gSpXw3Cxf2hCXDxPSoF8vgjNqKxrZa/B4Dg=
github.com/hajimehoshi/ebiten v1.11.1 h1:7gy2bHBDNtfTh3GlcUAilk3lNWW9fTLaP7iZAodS9F8=
github.com/hajimehoshi/ebiten v1.11.1/go.mod h1:aDEhx0K9gSpXw3Cxf2hCXDxPSoF8vgjNqKxrZa/B4Dg=
github.com/hajimehoshi/go-mp3 v0.2.1 h1:DH4ns3cPv39n3cs8MPcAlWqPeAwLCK8iNgqvg0QBWI8=
github.com/hajimehoshi/go-mp3 v0.2.1/go.mod h1:Rr+2P46iH6PwTPVgSsEwBkon0CK5DxCAeX/Rp65DCTE=
github.com/hajimehoshi/oto v0.3.4/go.mod h1:PgjqsBJff0efqL2nlMJidJgVJywLn6M4y8PI4TfeWfA=
github.com/hajimehoshi/oto v0.5.4 h1:Dn+WcYeF310xqStKm0tnvoruYUV5Sce8+sfUaIvWGkE=
github.com/hajimehoshi/oto v0.5.4/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
github.com/jakecoffman/cp v0.1.0/go.mod h1:a3xPx9N8RyFAACD644t2dj/nK4SuLg1v+jL61m2yVo4=
github.com/jfreymuth/oggvorbis v1.0.0 h1:aOpiihGrFLXpsh2osOlEvTcg5/aluzGQeC7m3uYWOZ0=
github.com/jfreymuth/oggvorbis v1.0.0/go.mod h1:abe6F9QRjuU9l+2jek3gj46lu40N4qlYxh2grqkLEDM=
github.com/jfreymuth/oggvorbis v1.0.1 h1:NT0eXBgE2WHzu6RT/6zcb2H10Kxj6Fm3PccT0LE6bqw=
github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps1aEZH1sumIk=
github.com/jfreymuth/vorbis v1.0.0 h1:SmDf783s82lIjGZi8EGUUaS7YxPHgRj4ZXW/h7rUi7U=
github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/hajimehoshi/ebiten/v2 v2.8.2 h1:cvZ5d3LSVFzvcSZVGjTPyV43DzWzJWbwy1b+2V5zJPI=
github.com/hajimehoshi/ebiten/v2 v2.8.2/go.mod h1:SXx/whkvpfsavGo6lvZykprerakl+8Uo1X8d2U5aAnA=
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
github.com/jfreymuth/oggvorbis v1.0.5 h1:u+Ck+R0eLSRhgq8WTmffYnrVtSztJcYrl588DM4e3kQ=
github.com/jfreymuth/oggvorbis v1.0.5/go.mod h1:1U4pqWmghcoVsCJJ4fRBKv9peUJMBHixthRlBeD6uII=
github.com/jfreymuth/vorbis v1.0.2 h1:m1xH6+ZI4thH927pgKD8JOH4eaGRm18rEE9/0WKjvNE=
github.com/jfreymuth/vorbis v1.0.2/go.mod h1:DoftRo4AznKnShRl1GxiTFCseHr4zR9BN3TWXyuzrqQ=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
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.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg=
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/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5 h1:FR+oGxGfbQu1d+jglI3rCkjAjUnhRSZcUxr+DqlDLNo=
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200119044424-58c23975cae1 h1:5h3ngYt7+vXCDZCup/HkCQgW5XwmSvR/nA2JmJ0RErg=
golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mobile v0.0.0-20191025110607-73ccc5ba0426 h1:8RjY2wWN6kjy6JvJjDPT51tx4ht4+ldy/a5Yw0AyEr4=
golang.org/x/mobile v0.0.0-20191025110607-73ccc5ba0426/go.mod h1:p895TfNkDgPEmEQrNiOtIl3j98d/tGU95djDj7NfyjQ=
golang.org/x/mobile v0.0.0-20200222142934-3c8601c510d0/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4=
golang.org/x/mobile v0.0.0-20200329125638-4c31acba0007 h1:JxsyO7zPDWn1rBZW8FV5RFwCKqYeXnyaS/VQPLpXu6I=
golang.org/x/mobile v0.0.0-20200329125638-4c31acba0007/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d h1:nc5K6ox/4lTFbMVSL9WRR81ixkcwXThoiF6yf+R9scA=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 h1:opSr2sbRXk5X5/givKrrKj9HXxFpW2sdCiP8MJSKLQY=
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190909214602-067311248421/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191026034945-b2104f82a97d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117220505-0cba7a3a9ee9/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
github.com/samuel/go-pcx v0.0.0-20210515040514-6a5ce4d132f7 h1:WhAiClm3vGzSl2EWdFsCFBEu2jEhHGa8qGsz4iIEpRc=
github.com/samuel/go-pcx v0.0.0-20210515040514-6a5ce4d132f7/go.mod h1:8ofl4LzpDayZKQZYbUyCDW41Y6lgVoO02ABp57OASxY=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s=
golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,6 +1,9 @@
package assetstore
import (
"fmt"
"code.ur.gs/lupine/ordoor/internal/data"
"code.ur.gs/lupine/ordoor/internal/idx"
)
@@ -46,24 +49,37 @@ func (a *AssetStore) AnimationsObject() (*Object, error) {
return obj, nil
}
func (a *AssetStore) Animation(groupIdx, recIdx int) (*Animation, error) {
idx, err := a.AnimationsIndex()
func (a *AssetStore) Animation(groupIdx int, recId int, compass int) (*Animation, error) {
realIdx, err := a.AnimationsIndex()
if err != nil {
return nil, err
}
// FIXME: are we using the right value if we need to make this change?
if compass == 0 {
compass = 8
}
obj, err := a.AnimationsObject()
if err != nil {
return nil, err
}
group := idx.Groups[groupIdx]
group := realIdx.Groups[groupIdx]
if group.Spec.Count == 0 {
return &Animation{}, nil
}
// rec := group.Records[recIdx]
det := group.Details[recIdx]
var det *idx.Detail
for i, rec := range group.Records {
if /*int(rec.ActionID) == int(action) && */ int(rec.Compass) == compass {
det = &group.Details[i]
}
}
if det == nil {
return nil, fmt.Errorf("Couldn't find anim (%v %v %v)", groupIdx, recId, compass)
}
first := int(group.Spec.SpriteIdx) + int(det.FirstSprite)
last := int(group.Spec.SpriteIdx) + int(det.LastSprite)
@@ -76,3 +92,48 @@ func (a *AssetStore) Animation(groupIdx, recIdx int) (*Animation, error) {
return &Animation{Frames: sprites}, nil
}
func (a *AssetStore) CharacterAnimation(ctype data.CharacterType, action data.AnimAction, compass int) (*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)
}
return a.Animation(group, int(action), compass)
}

View File

@@ -8,6 +8,8 @@ import (
"path/filepath"
"strings"
"github.com/hajimehoshi/ebiten/v2"
"code.ur.gs/lupine/ordoor/internal/config"
"code.ur.gs/lupine/ordoor/internal/data"
"code.ur.gs/lupine/ordoor/internal/idx"
@@ -43,7 +45,9 @@ type AssetStore struct {
cursors map[CursorName]*Cursor
fonts map[string]*Font
generic *data.Generic
hasAction *data.HasAction
idx *idx.Idx
images map[string]*ebiten.Image
maps map[string]*Map
menus map[string]*Menu
objs map[string]*Object
@@ -108,7 +112,10 @@ func (a *AssetStore) Refresh() error {
a.cursors = make(map[CursorName]*Cursor)
a.entries = newEntryMap
a.fonts = make(map[string]*Font)
a.generic = nil
a.hasAction = nil
a.idx = nil
a.images = make(map[string]*ebiten.Image)
a.maps = make(map[string]*Map)
a.menus = make(map[string]*Menu)
a.objs = make(map[string]*Object)

View File

@@ -3,7 +3,7 @@ package assetstore
import (
"image"
"github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/ebiten/v2"
)
// These are just offsets into the Cursors.cur file

View File

@@ -66,6 +66,26 @@ func (a *AssetStore) DefaultOptions() (*config.Options, error) {
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 {
return i > 0
}

View File

@@ -0,0 +1,39 @@
package assetstore
import (
"image"
"os"
"github.com/hajimehoshi/ebiten/v2"
_ "github.com/samuel/go-pcx/pcx" // PCX support
)
func (a *AssetStore) Image(name string) (*ebiten.Image, error) {
name = canonical(name)
if img, ok := a.images[name]; ok {
return img, nil
}
// baps, ordoor, geas store .pcx files in Pic
// TODO: SL stores .bmp files in Res
filename, err := a.lookup(name, "pcx", "Pic")
if err != nil {
return nil, err
}
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
rawImg, _, err := image.Decode(f)
if err != nil {
return nil, err
}
img := ebiten.NewImageFromImage(rawImg)
a.images[name] = img
return img, nil
}

View File

@@ -5,6 +5,7 @@ import (
"image"
"log"
"code.ur.gs/lupine/ordoor/internal/data"
"code.ur.gs/lupine/ordoor/internal/maps"
)
@@ -47,7 +48,7 @@ func (a *AssetStore) Map(name string) (*Map, error) {
}
m := &Map{
Rect: raw.Rect(),
Rect: raw.Rect(),
assets: a,
raw: raw,
set: set,
@@ -74,8 +75,8 @@ func (m *Map) LoadSprites() error {
}
// FIXME: get rid of this
func (m *Map) Cell(x, y, z int) maps.Cell {
return m.raw.Cells.At(x, y, z)
func (m *Map) Cell(x, y, z int) *maps.Cell {
return m.raw.At(x, y, z)
}
// 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)
}
if chr := m.CharacterAt(x, y, z); chr != nil {
// Look up the correct animation, get the frame, boom shakalaka
anim, err := m.assets.CharacterAnimation(chr.Type, data.AnimActionNone, int(chr.Orientation))
if err != nil {
return nil, err
}
sprites = append(sprites, anim.Frames[0])
}
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

@@ -3,7 +3,7 @@ package assetstore
import (
"log"
"github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/ebiten/v2"
"code.ur.gs/lupine/ordoor/internal/menus"
)

View File

@@ -7,7 +7,7 @@ import (
"path/filepath"
"strings"
"github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/ebiten/v2"
"code.ur.gs/lupine/ordoor/internal/data"
)
@@ -117,10 +117,7 @@ func (o *Object) Sprite(idx int) (*Sprite, error) {
}
raw := o.raw.Sprites[idx]
img, err := ebiten.NewImageFromImage(raw.ToImage(o.assets.Palette), ebiten.FilterDefault)
if err != nil {
return nil, err
}
img := ebiten.NewImageFromImage(raw.ToImage(o.assets.Palette))
rect := image.Rect(
int(raw.XOffset),

View File

@@ -4,8 +4,8 @@ import (
"log"
"os"
"github.com/hajimehoshi/ebiten/audio"
"github.com/hajimehoshi/ebiten/audio/vorbis"
"github.com/hajimehoshi/ebiten/v2/audio"
"github.com/hajimehoshi/ebiten/v2/audio/vorbis"
)
type Sound struct {
@@ -57,7 +57,7 @@ func (s *Sound) InfinitePlayer() (*audio.Player, error) {
return nil, err
}
infinite := audio.NewInfiniteLoop(decoder, decoder.Size())
infinite := audio.NewInfiniteLoop(decoder, decoder.Length())
return audio.NewPlayer(audio.CurrentContext(), infinite)
}

View File

@@ -35,7 +35,7 @@ const (
AnimActionRun AnimAction = 14
AnimActionCrouch AnimAction = 15
AnimActionStand AnimAction = 16
AnimActionStandingRead AnimAction = 17
AnimActionStandingReady AnimAction = 17
AnimActionStandingUnready AnimAction = 18
AnimActionCrouchingReady AnimAction = 19
AnimActionCrouchingUnready AnimAction = 20
@@ -94,6 +94,89 @@ type HasAction struct {
bits bitfield.BitField
}
var (
aActions = map[AnimAction]string{
AnimActionNone: "None",
AnimActionAnim: "Anim",
AnimActionWalk: "Walk",
AnimActionExplosion: "Explosion",
AnimActionProjectile: "Projectile",
AnimActionSmoke: "Smoke",
AnimActionStandingShoot: "Standing Shoot",
AnimActionStandingDeath: "Standing Death",
AnimActionPain: "Pain",
AnimActionSpellFx1: "Spell FX 1",
AnimActionSpellFx2: "Spell FX 2",
AnimActionSpellFx3: "Spell FX 3",
AnimActionSpellFx4: "Spell FX 4",
AnimActionSpellFx5: "Spell FX 5",
AnimActionRun: "Run",
AnimActionCrouch: "Crouch",
AnimActionStand: "Stand",
AnimActionStandingReady: "Standing Ready",
AnimActionStandingUnready: "Standing Unready",
AnimActionCrouchingReady: "Crouching Ready",
AnimActionCrouchingUnready: "Crouching Unready",
AnimActionCrouchingShoot: "Crouching Shoot",
AnimActionStandingGrenade: "Standing Grenade",
AnimActionCrouchingGrenade: "Crouching Grenade",
AnimActionDrawMelee: "Draw Melee",
AnimActionSlash: "Slash",
AnimActionStab: "Stab",
AnimActionBlown: "Blown",
AnimActionCrouchingDeath: "Crouching Death",
AnimActionJump: "Jump",
AnimActionHeal: "Heal",
AnimActionTechWork: "Tech Work",
AnimActionCast: "Cast",
AnimActionShoot: "Shoot",
AnimActionDeath: "Death",
AnimActionFromWarp: "From Warp",
}
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 (a AnimAction) String() string {
if str, ok := aActions[a]; ok {
return str
}
return "Unknown Action"
}
func (c CharacterType) String() string {
if str, ok := cTypes[c]; ok {
return str
}
return "Unknown Character"
}
func LoadHasAction(filename string) (*HasAction, error) {
scanner, err := asciiscan.New(filename)
if err != nil {
@@ -161,6 +244,17 @@ func (h *HasAction) Actions(c CharacterType) []AnimAction {
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() {
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++ {

View File

@@ -6,7 +6,8 @@ import (
"log"
"strings"
"github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil"
"code.ur.gs/lupine/ordoor/internal/assetstore"
"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
}
func (f *Flow) SetScenario(scenario *scenario.Scenario) {
f.current = f.drivers[mainGame]
f.scenario = scenario
}
func (f *Flow) Update(screenX, screenY int) error {
if f.exit != nil {
return f.exit
@@ -113,6 +119,14 @@ func (f *Flow) Update(screenX, screenY int) error {
if ebiten.IsKeyPressed(ebiten.KeyDown) {
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 {

View File

@@ -1,5 +1,9 @@
package flow
import (
"code.ur.gs/lupine/ordoor/internal/maps"
)
// TODO: There are Chaos and Ultramarine versions of MainGame. Do we really want
// to duplicate everything for both?
@@ -16,7 +20,7 @@ func (f *Flow) linkMainGame() {
})
// 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, "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"
"fmt"
"image"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"github.com/lunixbochs/struc"
"code.ur.gs/lupine/ordoor/internal/data"
)
var (
@@ -31,59 +33,131 @@ const (
cellCount = MaxHeight * MaxLength * MaxWidth
)
type Header struct {
IsCampaignMap uint32 // Tentatively: 0 = no, 1 = yes
MinWidth uint32
MinLength uint32
MaxWidth uint32
MaxLength uint32
Unknown1 uint32
Unknown2 uint32
Unknown3 uint32
Unknown4 uint32
Magic [8]byte // "\x08\x00WHMAP\x00"
Unknown5 uint32
Unknown6 uint32
SetName [8]byte // Links to a filename in `/Sets/*.set`
// Need to investigate the rest of the header too
type GameMap struct {
// Main Header
IsCampaignMap bool `struc:"uint32"` // Tentatively: 0 = no, 1 = yes
MinWidth int `struc:"uint32"`
MinLength int `struc:"uint32"`
MaxWidth int `struc:"uint32"`
MaxLength int `struc:"uint32"`
Unknown1 int `struc:"uint32"`
Unknown2 int `struc:"uint32"`
Unknown3 int `struc:"uint32"`
Unknown4 int `struc:"uint32"`
Magic []byte `struc:"[8]byte"` // "\x08\x00WHMAP\x00"
Unknown5 int `struc:"uint32"`
Unknown6 int `struc:"uint32"`
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"`
}
func (h Header) Width() int {
return int(h.MaxWidth - h.MinWidth)
type Cell struct {
DoorAndCanisterRelated byte `struc:"byte"`
DoorLockAndReactorRelated byte `struc:"byte"`
Unknown2 byte `struc:"byte"`
Surface ObjRef
Left ObjRef
Right ObjRef
Center ObjRef
Unknown11 byte `struc:"byte"`
Unknown12 byte `struc:"byte"`
Unknown13 byte `struc:"byte"`
Unknown14 byte `struc:"byte"`
SquadRelated byte `struc:"byte"`
}
func (h Header) Length() int {
return int(h.MaxLength - h.MinLength)
type Character struct {
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:"[895]byte"`
Orientation byte `struc:"byte"`
Unknown9 []byte `struc:"[31]byte"`
// TODO: each character may have a fixed number of subrecords for inventory
}
func (h Header) Height() int {
return MaxHeight
type Characters []Character
// TODO. These are triggers/reactors/etc.
type Thingy struct {
Unknown1 int `struc:"uint32"`
}
func (h Header) MapSetName() string {
idx := bytes.IndexByte(h.SetName[:], 0)
if idx < 0 {
idx = 8 // all 8 bytes are used
}
type Thingies []Thingy
return string(h.SetName[0:idx:idx])
func (g *GameMap) MapSetName() string {
return g.SetName
}
func (h Header) MapSetFilename() string {
return h.MapSetName() + ".set"
func (g *GameMap) MapSetFilename() string {
return g.MapSetName() + ".set"
}
type ObjRef struct {
AreaByte byte
SpriteAndFlagByte byte
AreaByte byte `struc:"byte"`
SpriteAndFlagByte byte `struc:"byte"`
}
// The index into a set palette to retrieve the object
func (o ObjRef) Index() int {
func (o *ObjRef) Index() int {
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
return int(o.SpriteAndFlagByte & 0x7f)
}
@@ -93,21 +167,6 @@ func (o ObjRef) IsActive() bool {
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 {
switch n {
case 0:
@@ -147,39 +206,26 @@ func (c *Cell) At(n int) byte {
return 0
}
// Cells is always a fixed size; use At to get a cell according to x,y,z
type Cells []Cell
func (c Cells) At(x, y, z int) Cell {
return c[(z*MaxLength*MaxWidth)+(y*MaxWidth)+x]
func (g *GameMap) At(x, y, z int) *Cell {
return &g.Cells[(z*MaxLength*MaxWidth)+(y*MaxWidth)+x]
}
func (h Header) Check() []error {
var out []error
if h.IsCampaignMap > 1 {
out = append(out, fmt.Errorf("Expected 0 or 1 for IsCampaignMap, got %v", h.IsCampaignMap))
func (g *GameMap) Check() error {
if bytes.Compare(expectedMagic, g.Magic) != 0 {
return fmt.Errorf("Unexpected magic value: %v", g.Magic)
}
if bytes.Compare(expectedMagic, h.Magic[:]) != 0 {
out = append(out, fmt.Errorf("Unexpected magic value: %v", h.Magic))
}
// TODO: other consistency checks
return out
}
type GameMap struct {
Header
Cells
// TODO: parse this into sections
Text string
return nil
}
func (m *GameMap) Rect() image.Rectangle {
return image.Rect(
int(m.Header.MinWidth),
int(m.Header.MinLength),
int(m.Header.MaxWidth),
int(m.Header.MaxLength),
int(m.MinWidth),
int(m.MinLength),
int(m.MaxWidth),
int(m.MaxLength),
)
}
@@ -202,23 +248,17 @@ func LoadGameMap(prefix string) (*GameMap, error) {
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) {
out, err := loadMapFile(mapFile)
if err != nil {
return nil, err
}
// TODO: load text and parse into sections
txt, err := ioutil.ReadFile(txtFile)
if err != nil {
if err := out.Check(); err != nil {
return nil, err
}
out.Text = string(txt)
for _, err := range out.Check() {
log.Printf("%s: %v", mapFile, err)
}
return out, nil
}
@@ -256,6 +296,7 @@ func LoadGameMaps(dir string) (map[string]*GameMap, error) {
func loadMapFile(filename string) (*GameMap, error) {
var out GameMap
out.NumCells = cellCount
mf, err := os.Open(filename)
if err != nil {
@@ -271,20 +312,46 @@ func loadMapFile(filename string) (*GameMap, error) {
defer zr.Close()
if err := binary.Read(zr, binary.LittleEndian, &out.Header); 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 {
if err := struc.UnpackWithOrder(zr, &out, binary.LittleEndian); err != nil {
return nil, err
}
out.Cells = make(Cells, cellCount)
if err := binary.Read(zr, binary.LittleEndian, &out.Cells); err != nil {
return nil, fmt.Errorf("Error parsing cells for %s: %v", filename, err)
// Trim any trailing nulls off of the strings
nullTerminate(&out.SetName)
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())
}
fmt.Printf("Mission Title: %q\n", out.Title)
fmt.Printf("Mission Briefing: %q\n", out.Briefing)
return &out, nil
}
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(
"squad=%v pos=(%v,%v) type=%q name=%q\n"+
"\t%3d %3d %3d %3d %3d\n\t%3d %3d ??? ??? %3d\n",
c.SquadNumber,
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
SubTypeThumb SubMenuType = 45 // A "thumb" appears to be a vertical slider
SubTypeInvokeButton SubMenuType = 50
SubTypeDoorHotspot3 SubMenuType = 60 // Maybe? Appears in Arrange.mnu
SubTypeClickText SubMenuType = 60
SubTypeOverlay SubMenuType = 61
SubTypeHypertext SubMenuType = 70
SubTypeCheckbox SubMenuType = 91

View File

@@ -0,0 +1,20 @@
package ordoor
import (
"time"
)
func (o *Ordoor) DisplayImageFor(d time.Duration, name string) error {
img, err := o.assets.Image(name)
if err != nil {
return err
}
o.pic = img
go func() {
<-time.After(d)
o.pic = nil // FIXME: this is a race condition and a half
}()
return nil
}

View File

@@ -7,9 +7,11 @@ package ordoor
import (
"fmt"
"log"
"sync"
"time"
"github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/ebiten/audio"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/audio"
"code.ur.gs/lupine/ordoor/internal/assetstore"
"code.ur.gs/lupine/ordoor/internal/config"
@@ -27,7 +29,12 @@ type Ordoor struct {
win *ui.Window
// Relevant to interface state
flow *flow.Flow
flow *flow.Flow
flowOnce sync.Once
// FIXME: should be put inside flow
// If this is set, we display it instead of flow
pic *ebiten.Image
// Relevant to campaign state
ship *ship.Ship
@@ -56,9 +63,7 @@ func Run(configFile string, overrideX, overrideY int) error {
}
}
if _, err := audio.NewContext(48000); err != nil {
return fmt.Errorf("Failed to set up audio context: %v", err)
}
_ = audio.NewContext(48000)
ordoor := &Ordoor{
assets: assets,
@@ -81,10 +86,6 @@ func Run(configFile string, overrideX, overrideY int) error {
ordoor.win = win
if err := ordoor.setupFlow(); err != nil {
return fmt.Errorf("failed to setup UI flow: %v", err)
}
if err := ordoor.Run(); err != nil {
return fmt.Errorf("Run finished with error: %v", err)
}
@@ -93,12 +94,16 @@ func Run(configFile string, overrideX, overrideY int) error {
}
func (o *Ordoor) Run() error {
// FIXME: we're missing a screen about SSI here
// FIXME: these should be displayed *after*, not *before*, the copyright
if o.config.Options.PlayMovies {
o.PlaySkippableVideo("LOGOS")
o.PlaySkippableVideo("movie1")
}
if err := o.DisplayImageFor(time.Second, "copyright"); err != nil {
log.Printf("Failed to display copyright image: %v", err)
}
err := o.win.Run()
if err == flow.ErrExit {
log.Printf("Exit requested")
@@ -151,6 +156,16 @@ func (o *Ordoor) setupFlow() error {
}
func (o *Ordoor) Update(screenX, screenY int) error {
if pic := o.pic; pic != nil {
return nil // Ignore flow until we don't have a pic any more
}
if o.flow == nil {
if err := o.setupFlow(); err != nil {
return fmt.Errorf("failed to setup UI flow: %v", err)
}
}
// Ensure music is doing the right thing
if o.music != nil && o.music.IsPlaying() != o.config.Options.PlayMusic {
if o.config.Options.PlayMusic {
@@ -165,9 +180,30 @@ func (o *Ordoor) Update(screenX, screenY int) error {
}
func (o *Ordoor) Draw(screen *ebiten.Image) error {
return o.flow.Draw(screen)
if pic := o.pic; pic != nil {
// Scale the picture to the screen and draw it
scaleX := float64(screen.Bounds().Dx()) / float64(pic.Bounds().Dx())
scaleY := float64(screen.Bounds().Dy()) / float64(pic.Bounds().Dy())
do := &ebiten.DrawImageOptions{}
do.GeoM.Scale(scaleX, scaleY)
screen.DrawImage(pic, do)
return nil
}
if o.flow != nil {
return o.flow.Draw(screen)
}
return nil // Draw() may be called before Update()
}
func (o *Ordoor) Cursor() (*ebiten.Image, *ebiten.DrawImageOptions, error) {
return o.flow.Cursor()
if o.flow != nil {
return o.flow.Cursor()
}
return nil, nil, nil
}

View File

@@ -5,8 +5,8 @@ import (
"image"
"sort"
"github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/ebiten/ebitenutil"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
)
type CartPt struct {
@@ -36,7 +36,7 @@ func (s *Scenario) Update(screenX, screenY int) error {
}
// FIXME: adjust for Z level
s.selectedCell = screenPos.ToISO()
s.highlightedCell = screenPos.ToISO()
return nil
}
@@ -51,13 +51,13 @@ func (s *Scenario) Draw(screen *ebiten.Image) error {
sw, sh := screen.Size()
topLeft := CartPt{
X: float64(s.Viewpoint.X) - (2*cellWidth/s.Zoom), // Ensure all visible cells are rendered
Y: float64(s.Viewpoint.Y) - (2*cellHeight/s.Zoom),
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) + (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
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
@@ -95,17 +95,15 @@ func (s *Scenario) Draw(screen *ebiten.Image) error {
return false
})
counter := map[string]int{}
counter := 0
for _, pt := range toDraw {
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
}
}
}
//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)
@@ -114,31 +112,31 @@ func (s *Scenario) Draw(screen *ebiten.Image) error {
}
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.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
}
screen.DrawImage(spr.Image, &op)
x1, y1 := geo.Apply(0, 0)
ebitenutil.DebugPrintAt(
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(y1),
)
ebitenutil.DebugPrintAt(screen, fmt.Sprintf("Sprites: %v", counter), 0, 16)
/*
// 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
// 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
@@ -164,7 +162,7 @@ func (s *Scenario) geoForCoords(x, y, z int) ebiten.GeoM {
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)
if err != nil {
return err
@@ -179,10 +177,7 @@ func (s *Scenario) renderCell(x, y, z int, screen *ebiten.Image, counter map[str
iso.Translate(-209, -332)
for _, spr := range sprites {
// if _, ok := counter[spr.ID]; !ok {
// counter[spr.ID] = 0
// }
// counter[spr.ID] = counter[spr.ID] + 1
*counter = *counter + 1
op := ebiten.DrawImageOptions{GeoM: iso}
op.GeoM.Translate(float64(spr.Rect.Min.X), float64(spr.Rect.Min.Y))
@@ -190,9 +185,7 @@ func (s *Scenario) renderCell(x, y, z int, screen *ebiten.Image, counter map[str
// Zoom has to come last
op.GeoM.Scale(s.Zoom, s.Zoom)
if err := screen.DrawImage(spr.Image, &op); err != nil {
return err
}
screen.DrawImage(spr.Image, &op)
}
return nil

View File

@@ -1,6 +1,8 @@
package scenario
import (
"log"
"code.ur.gs/lupine/ordoor/internal/maps"
)
@@ -9,9 +11,24 @@ type CellPoint struct {
Z int
}
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) CellAtCursor() (*maps.Cell, CellPoint) {
cell := s.area.Cell(int(s.highlightedCell.X), int(s.highlightedCell.Y), 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) {

View File

@@ -2,19 +2,21 @@
package scenario
import (
"fmt"
"image"
"code.ur.gs/lupine/ordoor/internal/assetstore"
"code.ur.gs/lupine/ordoor/internal/maps"
)
type Scenario struct {
area *assetstore.Map
specials *assetstore.Object
tick int
turn int
selectedCell IsoPt
tick int
turn int
highlightedCell IsoPt
selectedCharacter *maps.Character
// All these must be modified by user actions 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?
if err := area.LoadSprites(); err != nil {
return nil, fmt.Errorf("Eager-loading sprites failed: %v", err)
}
//if err := area.LoadSprites(); err != nil {
// return nil, fmt.Errorf("Eager-loading sprites failed: %v", err)
//}
out := &Scenario{
area: area,

View File

@@ -1,7 +1,7 @@
package ui
import (
"github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/ebiten/v2"
)
var (

View File

@@ -5,8 +5,8 @@ import (
"image"
"runtime/debug"
"github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/ebiten/ebitenutil"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
"code.ur.gs/lupine/ordoor/internal/assetstore"
)
@@ -137,9 +137,7 @@ func (d *Driver) Draw(screen *ebiten.Image) error {
do.GeoM = d.orig2native
do.GeoM.Translate(x, y)
if err := screen.DrawImage(region.image, &do); err != nil {
return err
}
screen.DrawImage(region.image, &do)
}
}

View File

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

View File

@@ -3,7 +3,7 @@ package ui
import (
"image"
"github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/ebiten/v2"
)
type region struct {

View File

@@ -157,9 +157,9 @@ func (l *listBox) refresh() {
// FIXME: noninteractive isn't set up for dynamic text yet. Need to
// generate textImg on demand instead of once at start.
if ni.label != nil {
ni.label.text = ""
ni.label.str = ""
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
}
// Paint some text to screen
// Paint some text to screen, possibly settable
type label struct {
align AlignMode
rect image.Rectangle
text string
font *assetstore.Font
locator string
align AlignMode
rect image.Rectangle
font *assetstore.Font
valueImpl
}
// 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
}
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
func (d *Driver) buildOverlay(p *menus.Properties) (*noninteractive, *Widget, error) {
ni, err := d.buildNoninteractive(p)
@@ -127,9 +155,9 @@ func (d *Driver) buildOverlay(p *menus.Properties) (*noninteractive, *Widget, er
fnt := d.menu.Font(p.FontType/10 - 1)
ni.label = &label{
font: fnt,
rect: ni.rect, // We will be centered by default
text: p.Text,
font: fnt,
rect: ni.rect, // We will be centered by default
valueImpl: valueImpl{str: p.Text},
}
} else {
log.Printf("Overlay without text detected in %v", p.Locator)
@@ -253,6 +281,10 @@ func (a *animationHover) setHoverState(value bool) {
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
// the centre of the rect.
//
@@ -260,7 +292,7 @@ func (a *animationHover) setHoverState(value bool) {
func (l *label) pos() image.Point {
pos := l.rect.Min
textRect := l.font.CalculateBounds(l.text)
textRect := l.font.CalculateBounds(l.str)
// Centre the text horizontally
if l.align == AlignModeCentre {
@@ -287,15 +319,21 @@ func (l *label) regions(tick int) []region {
pt := l.pos()
for _, r := range l.text {
glyph, err := l.font.Glyph(r)
if err != nil {
log.Printf("FIXME: ignoring misssing glyph %v", r)
continue
for _, r := range l.str {
var sprite *assetstore.Sprite
if glyph, err := l.font.Glyph(r); err != nil {
if glyph, err := l.font.Glyph('?'); err != nil {
log.Printf("FIXME: ignoring glyph %v", r)
continue
} else {
sprite = glyph
}
} else {
sprite = glyph
}
out = append(out, oneRegion(pt, glyph.Image)...)
pt.X += glyph.Rect.Dx()
out = append(out, oneRegion(pt, sprite.Image)...)
pt.X += sprite.Rect.Dx()
}
return out

View File

@@ -8,9 +8,9 @@ import (
"runtime/debug"
"runtime/pprof"
"github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/ebiten/ebitenutil"
"github.com/hajimehoshi/ebiten/inpututil"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
"github.com/hajimehoshi/ebiten/v2/inpututil"
)
type Game interface {
@@ -53,8 +53,6 @@ type Window struct {
//
// ebiten assumes a single window, so only call this once...
func NewWindow(game Game, title string, xRes int, yRes int) (*Window, error) {
ebiten.SetRunnableInBackground(true)
return &Window{
Title: title,
debug: true,
@@ -109,10 +107,12 @@ func (w *Window) drawCursor(screen *ebiten.Image) error {
ebiten.SetCursorMode(ebiten.CursorModeHidden)
return screen.DrawImage(cursor, op)
screen.DrawImage(cursor, op)
return nil
}
func (w *Window) Update(screen *ebiten.Image) (outErr error) {
func (w *Window) Update() (outErr error) {
// Ebiten does not like it if we panic inside its main loop
defer func() {
if panicErr := recover(); panicErr != nil {
@@ -124,7 +124,8 @@ func (w *Window) Update(screen *ebiten.Image) (outErr error) {
}
}()
if err := w.game.Update(screen.Size()); err != nil {
// FIXME: remove need for update generally
if err := w.game.Update(w.xRes, w.yRes); err != nil {
return err
}
@@ -157,13 +158,11 @@ func (w *Window) Update(screen *ebiten.Image) (outErr error) {
}
}
if ebiten.IsDrawingSkipped() {
return nil
}
return nil
}
if err := w.game.Draw(screen); err != nil {
return err
}
func (w *Window) Draw(screen *ebiten.Image) {
w.game.Draw(screen)
if w.debug {
// Draw FPS, etc, to the screen
@@ -172,7 +171,7 @@ func (w *Window) Update(screen *ebiten.Image) (outErr error) {
}
// Draw the cursor last
return w.drawCursor(screen)
w.drawCursor(screen)
}
// TODO: a stop or other cancellation mechanism