Compare commits

...

45 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
63d3ee0ed6 Update README.md a bit 2020-06-01 01:55:55 +01:00
5c869fc33c Make some Wages of War sprites displayable 2020-06-01 01:43:03 +01:00
4358951e15 Add the Wages of War palette (first guess) 2020-06-01 01:41:45 +01:00
250a6033c8 Fix an error in the palette generator 2020-06-01 01:41:24 +01:00
f64af717b7 Fix format strings 2020-06-01 01:32:03 +01:00
3866ee07a8 Source the palette name from data 2020-06-01 01:24:44 +01:00
c1268e8d57 Start reorganising for multiple games 2020-06-01 01:08:53 +01:00
59baf20c35 Remove unneeded palette investigation 2020-06-01 00:46:02 +01:00
cf58be6a20 Use map rect 2020-05-31 14:58:46 +01:00
14fdab72a0 Update ebiten to v1.11.1
This seems like a significant performance boost. I get 60fps now \o/
2020-05-31 14:50:11 +01:00
c7a2fa80e7 Internalise the map rect 2020-05-20 01:43:44 +01:00
def40a1ee2 Document SaW map format some more 2020-05-20 01:40:46 +01:00
48d098134e One more palette fix 2020-05-20 01:03:40 +01:00
597e346869 Correct the palettes 2020-05-19 22:05:42 +01:00
eea5dea98a Determine the Soldiers At War palette
This commit also takes the first step towards making it configurable;
override `internal/palettes.DefaultPaletteName` at build time to choose
one palette over another. It would be nice to set this at runtime!
2020-05-19 21:33:49 +01:00
04bdf3e352 Make i18n optional, add SoW note 2020-05-19 11:07:10 +01:00
9d0750d134 Scenario viewpoint, Z index management, and arrow controls 2020-04-20 00:16:21 +01:00
1f4bfc771c HAXXX: make the main game UI appear at the bottom 2020-04-19 20:57:45 +01:00
c058f651dc Wire up mission objectives dialogue/menu 2020-04-19 18:49:19 +01:00
9be93b6091 More work for MainGame.mnu 2020-04-19 18:21:08 +01:00
f8828c95bd Add GIF to README 2020-04-18 13:44:00 +01:00
65 changed files with 2636 additions and 1547 deletions

19
.gitignore vendored
View File

@@ -1,14 +1,9 @@
/config.toml
/loader
/orig
/palette-idx
/view-ani
/view-font
/view-obj
/view-map
/view-minimap
/view-menu
/view-set
/ordoor
/investigation/Maps
/investigation/Obj
/isos
/CG
/SL
/SaW
/WoW
/WoW-CD
/bin

View File

@@ -4,37 +4,40 @@ GOBUILD ?= go build -tags ebitengl
all: loader ordoor palette-idx view-ani view-font view-obj view-map view-menu view-minimap view-set
loader: $(srcfiles)
$(GOBUILD) -o loader ./cmd/loader
bin:
mkdir bin
palette-idx: $(srcfiles)
$(GOBUILD) -o palette-idx ./cmd/palette-idx
loader: bin $(srcfiles)
$(GOBUILD) -o bin/loader ./cmd/loader
view-ani: $(srcfiles)
$(GOBUILD) -o view-ani ./cmd/view-ani
palette-idx: bin $(srcfiles)
$(GOBUILD) -o bin/palette-idx ./cmd/palette-idx
view-font: $(srcfiles)
$(GOBUILD) -o view-font ./cmd/view-font
view-ani: bin $(srcfiles)
$(GOBUILD) -o bin/view-ani ./cmd/view-ani
view-obj: $(srcfiles)
$(GOBUILD) -o view-obj ./cmd/view-obj
view-font: bin $(srcfiles)
$(GOBUILD) -o bin/view-font ./cmd/view-font
view-map: $(srcfiles)
$(GOBUILD) -o view-map ./cmd/view-map
view-obj: bin $(srcfiles)
$(GOBUILD) -o bin/view-obj ./cmd/view-obj
view-menu: $(srcfiles)
$(GOBUILD) -o view-menu ./cmd/view-menu
view-map: bin $(srcfiles)
$(GOBUILD) -o bin/view-map ./cmd/view-map
view-minimap: $(srcfiles)
$(GOBUILD) -o view-minimap ./cmd/view-minimap
view-menu: bin $(srcfiles)
$(GOBUILD) -o bin/view-menu ./cmd/view-menu
view-set: $(srcfiles)
$(GOBUILD) -o view-set ./cmd/view-set
view-minimap: bin $(srcfiles)
$(GOBUILD) -o bin/view-minimap ./cmd/view-minimap
ordoor: $(srcfiles)
$(GOBUILD) -o ordoor ./cmd/ordoor
view-set: bin $(srcfiles)
$(GOBUILD) -o bin/view-set ./cmd/view-set
ordoor: bin $(srcfiles)
$(GOBUILD) -o bin/ordoor ./cmd/ordoor
clean:
rm -f loader ordoor view-ani view-obj view-map view-minimap view-set palette-idx view-font
rm -rf bin
.PHONY: all clean

185
README.md
View File

@@ -1,32 +1,107 @@
# Ordoor
Ordoor is an **unofficial** [game engine recreation](https://en.wikipedia.org/wiki/Game_engine_recreation)
of the classic game from 1998, [Warhammer 40,000: Chaos Gate](https://en.wikipedia.org/wiki/Warhammer_40,000:_Chaos_Gate)
of the Random Games, Inc., [Strategy Engine](https://www.mobygames.com/game-group/game-engine-random-games-1996-2000-strategy-engine),
which was in use from 1996 - 2000.
**You must have a copy of the original game data to use this project**. GOG is
the current publisher of this game; [you can purchase it here](https://www.gog.com/game/warhammer_40000_chaos_gate).
Four games are known to have been published for this engine:
"Warhammer 40,000" is a trademark of Games Workshop, and the game data used by
Ordoor contains Games Workshop intellectual property. I am confident that this
project uses all those things in accordance with the
[Intellectual Property Policy](https://www.games-workshop.com/en-GB/Intellectual-Property-Policy)
and the license granted when purchasing a copy of the game in question. Do let
me know if you see or suspect any violation, and I'll address it immediately.
* [Wages of War: The Business of Battle](https://en.wikipedia.org/wiki/Wages_of_War) (1996)
* [Soldiers At War](https://en.wikipedia.org/wiki/Soldiers_at_War) (1998)
* [Warhammer 40,000: Chaos Gate](https://en.wikipedia.org/wiki/Warhammer_40,000:_Chaos_Gate) (1998) [GOG](https://www.gog.com/game/warhammer_40000_chaos_gate)
* [Avalon Hill's Squad Leader](https://en.wikipedia.org/wiki/Avalon_Hill%27s_Squad_Leader) (2000)
The aim of Ordoor is to be a complete reimplementation that allows all four
of these games to be played on modern hardware. It should also permit new games
of the same style to be built.
For each of the games above, **You must have a copy of the original game data to play**.
Links are provided above if we're aware of an active publisher; otherwise, check
your back catalogue, or perhaps a local charity shop.
Trademarks and intellectual property are the property of their respective
owners, and the games mentioned above (including the game data) are protected by
copyright. As a mere game engine recreation, we're confident that this project
operates legally, and that its goal is a noble one. Do get in touch if you
believe otherwise!
Ordoor is a portmanteau of Order Door, which is, of course, the opposite of a
Chaos Gate.
Chaos Gate. The project began with a Chaos Gate recreation, then more games were
discovered, so scope expanded. A rename and/or rewrite may be on the cards as a
result.
## Current status
### Chaos Gate
Some of the original file formats are either partially or fully decoded. Maps,
menus, and most visual data can be rendered pixel-perfect. Sound can be played
(with a preprocessing step). Some UI tookit work is done. No game mechanics are
implemented yet.
I keep a GIF showcasing interesting progress here:
![](https://ur.gs/img/ordoor-main-menu.gif)
I've just been informed that another game from 1998, [Soldiers At War](https://en.wikipedia.org/wiki/Soldiers_at_War),
seems to use the same engine. Maybe at some point Ordoor will be able to play
both. Will that need a rename? Hmm. Watch this space.
### Soldiers At War
(At least some) objects display. Map support is being worked on in the
`soldiers-at-war` branch, which can more-or-less display them, albeit with many
errors.
### Squad Leader
Squad Leader is the most recent of the games created with this engine. Nothing
has been done with it yet, but a preliminary look at the game data suggests many
changes are afoot. The object files are a different format, at the very least.
### Wages of War
This is the oldest of the four games. The object file format seems to be mostly
the same. the installer only copies some data to the game directory; we may want
to work directly from the CDROM instead, if we can.
Maps are uncompressed, around 243K, and no header is present. They look similar
in principle to the tile data of Soldiers At War or Chaos Gate maps, otherwise.
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
has been achieved, thoughts turn to other things we could do. Here are some
ideas, mostly at random.
Multi-player support.
Graphics enhancements - 3D models instead of sprites, high-resolution tile sets,
32-bit colour, etc. Hopefully we'd be able to drop these in one at a time.
Vastly improved AI.
Mash-ups? How do mercenaries fare against cultists fare against Nazis? Only one
way to find out!
New campaigns with existing assets. Tell new stories, or elaborate on / modify
existing ones.
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
@@ -44,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:
@@ -75,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
@@ -111,30 +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.
## Miscellany
## Resources
"Mission Setup" includes information about available squad types
Here's a collection of links that I'm finding useful or otherwise interesting,
and don't want to lose track of...
From EquipDef.cpp Dumo: CEquipment we learn the following object types:
0. DELETED
1. WEAPON
2. GRENADE
3. MEDIPACK
4. SCANNER
5. GENESEED
6. CLIP
7. DOOR KEY
8. DOOR KEY
9. DOOR KEY
10. DOOR KEY
And we learn they can be "on"....
0. CHARACTER
1. VEHICLE
2. CANISTER
I'm starting to see some parallels with [this](https://github.com/shlainn/game-file-formats/wiki/)
in the data formats, and the timeline (1997) seems about right. Worth keeping an
eye on!
* [Historical geocities modders](http://www.oocities.org/timessquare/galaxy/6777/)

View File

@@ -3,42 +3,58 @@ package main
import (
"flag"
"fmt"
"image/color"
"log"
"path/filepath"
"strings"
"code.ur.gs/lupine/ordoor/internal/config"
"code.ur.gs/lupine/ordoor/internal/data"
"code.ur.gs/lupine/ordoor/internal/fonts"
"code.ur.gs/lupine/ordoor/internal/idx"
"code.ur.gs/lupine/ordoor/internal/maps"
"code.ur.gs/lupine/ordoor/internal/menus"
"code.ur.gs/lupine/ordoor/internal/palettes"
"code.ur.gs/lupine/ordoor/internal/sets"
)
var (
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
skipObj = flag.Bool("skip-obj", true, "Skip loading .obj files")
configFile = flag.String("config", "config.toml", "Config file")
engine = flag.String("engine", "", "Override engine to use")
skipObj = flag.Bool("skip-obj", true, "Skip loading .obj files")
)
// FIXME: all these paths are hardcoded with Chaos Gate in mind
func main() {
flag.Parse()
loadData()
if !*skipObj {
loadObj()
cfg, err := config.Load(*configFile, *engine)
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
engine := cfg.DefaultEngine()
gamePath := engine.DataDir
palette, ok := palettes.Get(engine.Palette)
if !ok {
log.Fatalf("Unknown palette name: %v", engine.Palette)
}
loadMapsFrom("Maps")
loadMapsFrom("MultiMaps")
loadSets()
loadMenus()
loadFonts()
loadIdx()
loadData(filepath.Join(gamePath, "Data"))
if !*skipObj {
loadObj(filepath.Join(gamePath, "Obj"))
}
loadMapsFrom(filepath.Join(gamePath, "Maps"))
loadMapsFrom(filepath.Join(gamePath, "MultiMaps"))
loadSets(filepath.Join(gamePath, "Sets"))
loadMenus(filepath.Join(gamePath, "Menu"), palette)
loadFonts(filepath.Join(gamePath, "Fonts"))
loadIdx(filepath.Join(gamePath, "Idx", "WarHammer.idx"))
}
func loadData() {
dataPath := filepath.Join(*gamePath, "Data")
func loadData(dataPath string) {
accountingPath := filepath.Join(dataPath, "Accounting.dat")
aniObDefPath := filepath.Join(dataPath, "AniObDef.dat")
genericDataPath := filepath.Join(dataPath, "GenericData.dat")
@@ -84,9 +100,7 @@ func loadData() {
ha.Print()
}
func loadObj() {
objDataPath := filepath.Join(*gamePath, "Obj")
func loadObj(objDataPath string) {
// TODO: Obj/cpiece.rec isn't loaded by this. Do we need it? How do we know?
log.Printf("Loading %s...", objDataPath)
objects, err := data.LoadObjects(objDataPath)
@@ -116,8 +130,7 @@ func loadObj() {
}
}
func loadMapsFrom(part string) {
mapsPath := filepath.Join(*gamePath, part)
func loadMapsFrom(mapsPath string) {
log.Printf("Loading maps from %s", mapsPath)
gameMaps, err := maps.LoadGameMaps(mapsPath)
@@ -126,21 +139,20 @@ func loadMapsFrom(part string) {
}
log.Printf("Maps in %s:", mapsPath)
for key, gameMap := range gameMaps {
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,
hdr.MinWidth, hdr.MaxWidth,
hdr.MinLength, hdr.MaxLength,
string(hdr.SetName[:]),
gm.IsCampaignMap,
rect.Min.X, rect.Max.X,
rect.Min.Y, rect.Max.Y,
string(gm.SetName),
)
}
}
func loadSets() {
setsPath := filepath.Join(*gamePath, "Sets")
func loadSets(setsPath string) {
log.Printf("Loading sets from %s", setsPath)
mapSets, err := sets.LoadSets(setsPath)
@@ -156,10 +168,10 @@ func loadSets() {
}
}
func loadMenus() {
menusPath := filepath.Join(*gamePath, "Menu")
func loadMenus(menusPath string, palette color.Palette) {
log.Printf("Loading menus from %s", menusPath)
menus, err := menus.LoadMenus(menusPath)
menus, err := menus.LoadMenus(menusPath, palette)
if err != nil {
log.Fatalf("Failed to parse %s/*.mnu as menus: %v", menusPath, err)
}
@@ -186,8 +198,8 @@ func displayRecord(record *menus.Record, depth int) {
fmt.Printf("%s* %s\n", strings.Repeat(" ", depth), content)
}
func loadFonts() {
fontsPath := filepath.Join(*gamePath, "Fonts")
func loadFonts(fontsPath string) {
log.Printf("Loading fonts from %s", fontsPath)
fonts, err := fonts.LoadFonts(fontsPath)
if err != nil {
@@ -199,8 +211,9 @@ func loadFonts() {
}
}
func loadIdx() {
idxPath := filepath.Join(*gamePath, "Idx", "WarHammer.idx")
func loadIdx(idxPath string) {
log.Printf("Loading idx from %s", idxPath)
idx, err := idx.Load(idxPath)
if err != nil {
log.Fatalf("Failed to parse %s as idx: %v", idxPath, err)
@@ -208,5 +221,8 @@ func loadIdx() {
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

@@ -5,7 +5,7 @@ import (
"os"
"strconv"
"code.ur.gs/lupine/ordoor/internal/data"
"code.ur.gs/lupine/ordoor/internal/palettes"
)
func main() {
@@ -13,11 +13,11 @@ func main() {
case "index", "i":
idx, err := strconv.ParseInt(os.Args[2], 16, 64)
if err != nil {
fmt.Println("Usage: palette-idx i <0-255>")
fmt.Println("Usage: palette-idx index <0-255> <name>")
os.Exit(1)
}
fmt.Printf("Palette[%v]: %#v\n", idx, data.ColorPalette[idx])
fmt.Printf("%vPalette[%v]: %#v\n", os.Args[3], idx, palettes.Palettes[os.Args[3]][idx])
case "color", "colour", "c":
fmt.Println("TODO!")
os.Exit(1)

View File

@@ -7,15 +7,18 @@ import (
"math"
"os"
"github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/ebiten/v2"
"golang.org/x/image/colornames"
"code.ur.gs/lupine/ordoor/internal/assetstore"
"code.ur.gs/lupine/ordoor/internal/config"
"code.ur.gs/lupine/ordoor/internal/ui"
)
var (
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
configFile = flag.String("config", "config.toml", "Config file")
engine = flag.String("engine", "", "Override engine to use")
groupIdx = flag.Int("group", 1, "Group index to start at")
recIdx = flag.Int("record", 0, "Record index to start at")
@@ -43,14 +46,19 @@ type state struct {
func main() {
flag.Parse()
if *gamePath == "" {
if *configFile == "" {
flag.Usage()
os.Exit(1)
}
assets, err := assetstore.New(*gamePath)
cfg, err := config.Load(*configFile, *engine)
if err != nil {
log.Fatal("Failed to set up asset store: %v", err)
log.Fatalf("Failed to load config: %v", err)
}
assets, err := assetstore.New(cfg.DefaultEngine())
if err != nil {
log.Fatalf("Failed to set up asset store: %v", err)
}
state := state{
@@ -92,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
}
@@ -123,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,14 +7,17 @@ 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/ui"
)
var (
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
configFile = flag.String("config", "config.toml", "Config file")
engine = flag.String("engine", "", "Override engine to use")
fontName = flag.String("font", "", "Name of a font, e.g., basfont12")
txt = flag.String("text", "Test string", "Text to render")
@@ -37,12 +40,17 @@ type state struct {
func main() {
flag.Parse()
if *gamePath == "" || *fontName == "" {
if *configFile == "" || *fontName == "" {
flag.Usage()
os.Exit(1)
}
assets, err := assetstore.New(*gamePath)
cfg, err := config.Load(*configFile, *engine)
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
assets, err := assetstore.New(cfg.DefaultEngine())
if err != nil {
log.Fatal(err)
}
@@ -61,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)
@@ -95,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

@@ -3,38 +3,50 @@ package main
import (
"flag"
"log"
"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"
)
var (
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
gameMap = flag.String("map", "", "Name of a map, e.g., Chapter01")
configFile = flag.String("config", "config.toml", "Config file")
engine = flag.String("engine", "", "Override engine to use")
gameMap = flag.String("map", "", "Name of a map, e.g., Chapter01")
winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension")
winY = flag.Int("win-y", 1024, "Pre-scaled window Y dimension")
)
type env struct {
flow *flow.Flow
scenario *scenario.Scenario
}
func main() {
flag.Parse()
if *gamePath == "" || *gameMap == "" {
if *configFile == "" || *gameMap == "" {
flag.Usage()
os.Exit(1)
}
assets, err := assetstore.New(*gamePath)
cfg, err := config.Load(*configFile, *engine)
if err != nil {
log.Fatalf("Failed to scan root directory %v: %v", *gamePath, err)
log.Fatalf("Failed to load config: %v", err)
}
assets, err := assetstore.New(cfg.DefaultEngine())
if err != nil {
log.Fatalf("Failed to scan root directory: %v", err)
}
scenario, err := scenario.NewScenario(assets, *gameMap)
@@ -42,25 +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)
}
win.OnKeyUp(ebiten.KeyLeft, env.changeOrigin(-64, +0))
win.OnKeyUp(ebiten.KeyRight, env.changeOrigin(+64, +0))
win.OnKeyUp(ebiten.KeyUp, env.changeOrigin(+0, -64))
win.OnKeyUp(ebiten.KeyDown, env.changeOrigin(+0, +64))
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(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)
@@ -68,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() {
@@ -82,6 +116,10 @@ func (e *env) changeOrigin(byX, byY int) func() {
}
}
func (e *env) changeZoom(_, byY float64) {
e.scenario.Zoom *= math.Pow(1.2, byY)
}
func (e *env) setZIdx(to int) func() {
return func() {
e.scenario.ZIdx = to

View File

@@ -5,14 +5,17 @@ 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"
"code.ur.gs/lupine/ordoor/internal/ui"
)
var (
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
configFile = flag.String("config", "config.toml", "Config file")
engine = flag.String("engine", "", "Override engine to use")
menuName = flag.String("menu", "", "Name of a menu, e.g. Main")
winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension")
@@ -28,12 +31,17 @@ type dlg struct {
func main() {
flag.Parse()
if *gamePath == "" || *menuName == "" {
if *configFile == "" || *menuName == "" {
flag.Usage()
os.Exit(1)
}
assets, err := assetstore.New(*gamePath)
cfg, err := config.Load(*configFile, *engine)
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
assets, err := assetstore.New(cfg.DefaultEngine())
if err != nil {
log.Fatal(err)
}
@@ -50,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)
@@ -172,20 +172,13 @@ func (e *env) Update(screenX, screenY int) error {
func (e *env) Draw(screen *ebiten.Image) error {
gameMap := e.gameMap
imd, err := ebiten.NewImage(
int(gameMap.MaxWidth),
int(gameMap.MaxLength),
ebiten.FilterDefault,
)
rect := gameMap.Rect()
imd := ebiten.NewImage(rect.Dx(), rect.Dy())
if err != nil {
return err
}
for y := int(gameMap.MinLength); y < int(gameMap.MaxLength); y++ {
for x := int(gameMap.MinWidth); x < int(gameMap.MaxWidth); x++ {
cell := gameMap.Cells.At(x, y, int(e.state.zIdx))
imd.Set(x, y, makeColour(&cell, e.state.cellIdx))
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.At(x, y, int(e.state.zIdx))
imd.Set(x, y, makeColour(cell, e.state.cellIdx))
}
}
@@ -196,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,17 +7,20 @@ 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/ui"
)
var (
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
objFile = flag.String("obj-file", "", "Path of an .obj file, e.g. ./orig/Obj/TZEENTCH.OBJ")
objName = flag.String("obj-name", "", "Name of an .obj file, e.g. TZEENTCH")
sprIdx = flag.Int("spr-idx", 0, "Sprite index to start at")
configFile = flag.String("config", "config.toml", "Config file")
engine = flag.String("engine", "", "Override engine to use")
objFile = flag.String("obj-file", "", "Path of an .obj file, e.g. ./orig/Obj/TZEENTCH.OBJ")
objName = flag.String("obj-name", "", "Name of an .obj file, e.g. TZEENTCH")
sprIdx = flag.Int("spr-idx", 0, "Sprite index to start at")
winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension")
winY = flag.Int("win-y", 1024, "Pre-scaled window Y dimension")
@@ -42,14 +45,19 @@ type state struct {
func main() {
flag.Parse()
if *gamePath == "" || (*objName == "" && *objFile == "") {
if *configFile == "" || (*objName == "" && *objFile == "") {
flag.Usage()
os.Exit(1)
}
assets, err := assetstore.New(*gamePath)
cfg, err := config.Load(*configFile, *engine)
if err != nil {
log.Fatal("Failed to set up asset store: %v", err)
log.Fatalf("Failed to load config: %v", err)
}
assets, err := assetstore.New(cfg.DefaultEngine())
if err != nil {
log.Fatalf("Failed to set up asset store: %v", err)
}
var obj *assetstore.Object
@@ -128,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,15 +7,18 @@ 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/ui"
)
var (
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
setName = flag.String("set", "", "Name of a set, e.g., map01")
configFile = flag.String("config", "config.toml", "Config file")
engine = flag.String("engine", "", "Override engine to use")
setName = flag.String("set", "", "Name of a set, e.g., map01")
winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension")
winY = flag.Int("win-y", 1024, "Pre-scaled window Y dimension")
@@ -39,12 +42,17 @@ type state struct {
func main() {
flag.Parse()
if *gamePath == "" || *setName == "" {
if *configFile == "" || *setName == "" {
flag.Usage()
os.Exit(1)
}
assets, err := assetstore.New(*gamePath)
cfg, err := config.Load(*configFile, *engine)
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
assets, err := assetstore.New(cfg.DefaultEngine())
if err != nil {
log.Fatal(err)
}
@@ -63,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))
@@ -115,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

@@ -1,6 +1,22 @@
[ordoor]
data_dir = "./orig"
video_player = ["mpv", "--no-config", "--keep-open=no", "--force-window=no", "--no-border", "--no-osc", "--fullscreen", "--no-input-default-bindings"]
video_player = ["mpv", "--no-config", "--keep-open=no", "--force-window=no", "--no-border", "--no-osc", "--fullscreen", "--no-input-default-bindings"]
default_engine = "ordoor"
[engines.geas] # Wages of War -> Gifts of Peace -> Geas
data_dir = "./WoW-CD"
palette = "WagesOfWar"
[engines.ordoor] # Chaos Gate -> Order Door -> Ordoor
data_dir = "./CG"
palette = "ChaosGate"
[engines.baps] # Soldiers At War -> Boys at Play -> Baps
data_dir = "./SaW"
palette = "SoldiersAtWar"
[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,5 +518,285 @@ 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
All the above applies to Chaos Gate maps. Maps for Soldiers At War seem to have
a lot of similarities, but also some differences. For a start, the maps are a
variable size!
Starting with the header, given a tiny 26x20 generated map, the first 256 bytes
look like this:
```
00000000: 1500414d 425f4d41 50005041 52495300 ..AMB_MAP.PARIS.
00000010: 00000000 00000000 00000000 00000000 ................
00000020: 00000000 00000000 00000000 00000000 ................
00000030: 00000000 00000000 00000000 00000000 ................
00000040: 00000000 00000000 00000000 00000000 ................
00000050: 00000000 00000000 00000000 00000000 ................
00000060: 00000000 00000000 00000000 00000000 ................
00000070: 00000000 00000000 00000000 00000000 ................
00000080: 00000000 00000000 00001e00 45000100 ............E...
00000090: 1f004600 10010000 52000000 00001b00 ..F.....R.......
000000a0: 38000100 00000500 0a000001 00f0f9ff 8...............
000000b0: ffb60500 00000100 ff370a00 64006400 .........7..d.d.
000000c0: 08008501 00000000 00ff0000 1f008082 ................
000000d0: 01000000 0000ff00 001f0080 84010000 ................
000000e0: 000000ff 00001f00 00810100 00000000 ................
000000f0: ff00001f 00808301 00000000 00ff0000 ................
```
Almost everything we knew is out of the window, but a few things look familiar.
First, the header seems simplified down to just two recognisable-at-first-glance
fields: Magic bytes (now `\x15\x00AMV_MAP\x00`) and the set name, coming
immediately after.
Like Chaos Gate, all map files are the same size once uncompressed, but they are
smaller - at 1,214,559 bytes, they are 76% the size. This is quite significant.
We now have 13.3 bytes per voxel, rather than the 17.5 bytes per voxel that was
available to Chaos Gate. This means that the number of bytes *per cell* must be
reduced, in addition to the header (and trailer?) values.
Looking at data from 0x110, it seems to group naturally into 13-byte records:
```
$ xxd -s 0x110 -c 13 -l 65 -g 1 TINYMAP.MAP
00000110: 80 01 00 00 00 00 00 ff 00 00 1f 00 00 .............
0000011d: 85 01 00 00 00 00 00 ff 00 00 1f 00 00 .............
0000012a: 82 01 00 00 00 00 00 ff 00 00 1f 00 80 .............
00000137: 82 01 00 00 00 00 00 ff 00 00 1f 00 00 .............
00000144: 82 01 00 00 00 00 00 ff 00 00 1f 00 80 .............
```
It's a strange number. Chaos Gate cells group nicely on 16 bytes:
```
$ xxd -s 0x110 -c 16 -l 64 -g 1 Chapter01.MAP
00000110: 3f 00 00 00 83 01 00 00 00 00 00 ff 00 00 00 00 ?...............
00000120: 38 00 00 00 85 01 00 00 00 00 00 ff 00 00 00 00 8...............
00000130: 38 00 00 00 84 01 00 00 00 00 00 ff 00 00 00 00 8...............
00000140: 38 00 00 00 8a 01 00 00 00 00 00 ff 00 00 00 00 8...............
00000150: 38 00 00 00 83 01 00 00 00 00 00 ff 00 00 00 00 8...............
```
That grouping is very enticing, though. I feel strongly that it's the right
number.
Now we need to ask about start offset. Where is byte 0 of the per-cell data, and
do the 13 bytes it has line up neatly to the functions of some of the 16 bytes
seen in Chaos Gate?
I generated a `BIGGESTMAP` (130x100) to investigate. It's just grass, nothing
but grass, and 0xC0 is the first offset where it starts to look nicely grouped:
```
xxd -s 0xc0 -c 13 -l 260 -g 13 BIGGESTMAP.MAP
000000c0: 08 80 81 01 00 00 00 00 00 ff 00 00 1f .............
000000cd: 00 80 81 01 00 00 00 00 00 ff 00 00 1f .............
000000da: 00 00 81 01 00 00 00 00 00 ff 00 00 1f .............
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.12
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.0
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
)

131
go.sum
View File

@@ -1,107 +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/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

@@ -2,24 +2,25 @@ package assetstore
import (
"fmt"
"image/color"
"io/ioutil"
"os"
"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"
)
const (
RootDir = "" // Used in the entryMap for entries pertaining to the root dir
"code.ur.gs/lupine/ordoor/internal/palettes"
)
type entryMap map[string]map[string]string
// type AssetStore is responsible for lazily loading game data when it is
// required. Applications shouldn't need to do anything except set one of these
// up, pointing at the game dir root, to access all assets.
// up, pointing at the game dir root, to access all assets for that game.
//
// Assets should be loaded on-demand to keep memory costs as low as possible.
// Cross-platform differences such as filename case sensitivity are also dealt
@@ -27,8 +28,12 @@ type entryMap map[string]map[string]string
//
// We assume the directory is read-only. You can run Refresh() if you make a
// change.
//
// To mix assets from different games, either construct a synthetic directory
// or instantiate two separate asset stores.
type AssetStore struct {
RootDir string
Palette color.Palette
// Case-insensitive file lookup.
// {"":{"anim":"Anim", "obj":"Obj", ...}, "anim":{ "warhammer.ani":"WarHammer.ani" }, ...}
@@ -40,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
@@ -50,9 +57,19 @@ type AssetStore struct {
}
// New returns a new AssetStore
func New(dir string) (*AssetStore, error) {
func New(engine *config.Engine) (*AssetStore, error) {
if engine == nil {
return nil, fmt.Errorf("Unconfigured engine passed to assetstore")
}
palette, ok := palettes.Get(engine.Palette)
if !ok {
return nil, fmt.Errorf("Couldn't find palette %q for engine", engine.Palette)
}
store := &AssetStore{
RootDir: dir,
RootDir: engine.DataDir,
Palette: palette,
}
// fill entryMap
@@ -70,7 +87,7 @@ func (a *AssetStore) Refresh() error {
}
newEntryMap := make(entryMap, len(rootEntries))
newEntryMap[RootDir] = rootEntries
newEntryMap[""] = rootEntries
for lower, natural := range rootEntries {
path := filepath.Join(a.RootDir, natural)
@@ -95,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)
@@ -118,7 +138,7 @@ func (a *AssetStore) lookup(name, ext string, dirs ...string) (string, error) {
dir = canonical(dir)
if base, ok := a.entries[dir]; ok {
if file, ok := base[filename]; ok {
actualDir := a.entries[RootDir][dir]
actualDir := a.entries[""][dir]
return filepath.Join(a.RootDir, actualDir, file), nil
}
}

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

@@ -1,9 +1,11 @@
package assetstore
import (
"fmt"
"image"
"log"
"code.ur.gs/lupine/ordoor/internal/data"
"code.ur.gs/lupine/ordoor/internal/maps"
)
@@ -46,12 +48,7 @@ func (a *AssetStore) Map(name string) (*Map, error) {
}
m := &Map{
Rect: image.Rect(
int(raw.MinWidth),
int(raw.MinLength),
int(raw.MaxWidth),
int(raw.MaxLength),
),
Rect: raw.Rect(),
assets: a,
raw: raw,
set: set,
@@ -64,8 +61,8 @@ func (a *AssetStore) Map(name string) (*Map, error) {
func (m *Map) LoadSprites() error {
// Eager load the sprites we use
for x := m.Rect.Min.X; x <= m.Rect.Max.X; x++ {
for y := m.Rect.Min.Y; y <= m.Rect.Max.Y; y++ {
for x := m.Rect.Min.X; x < m.Rect.Max.X; x++ {
for y := m.Rect.Min.Y; y < m.Rect.Max.Y; y++ {
for z := 0; z < maps.MaxHeight; z++ {
if _, err := m.SpritesForCell(x, y, z); err != nil {
return err
@@ -78,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.
@@ -95,7 +92,7 @@ func (m *Map) SpritesForCell(x, y, z int) ([]*Sprite, error) {
obj, err := m.set.Object(ref.Index())
if err != nil {
return nil, err
return nil, fmt.Errorf("Failed to get object for %#+v: %v", ref, err)
}
sprite, err := obj.Sprite(ref.Sprite())
@@ -105,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

@@ -1,7 +1,9 @@
package assetstore
import (
"github.com/hajimehoshi/ebiten"
"log"
"github.com/hajimehoshi/ebiten/v2"
"code.ur.gs/lupine/ordoor/internal/menus"
)
@@ -60,7 +62,7 @@ func (a *AssetStore) Menu(name string) (*Menu, error) {
return nil, err
}
raw, err := menus.LoadMenu(filename)
raw, err := menus.LoadMenu(filename, a.Palette)
if err != nil {
return nil, err
}
@@ -77,11 +79,11 @@ func (a *AssetStore) Menu(name string) (*Menu, error) {
i18n, err := a.i18n()
if err != nil {
return nil, err
log.Printf("Failed to load i18n data, skipping internationalisatoin: %s", err)
} else {
raw.Internationalize(i18n)
}
raw.Internationalize(i18n)
// FIXME: we should parse the menu into a list of elements like "ListBox",
// "Dialogue", etc, and present those with objects already selected
objects, err := a.loadMenuObjects(raw)

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"
)
@@ -42,7 +42,7 @@ func (a *AssetStore) Object(name string) (*Object, error) {
}
log.Printf("Loading object %v", name)
filename, err := a.lookup(name, "obj", "Obj")
filename, err := a.lookup(name, "obj", "Obj", "spr")
if err != nil {
return nil, err
}
@@ -117,10 +117,7 @@ func (o *Object) Sprite(idx int) (*Sprite, error) {
}
raw := o.raw.Sprites[idx]
img, err := ebiten.NewImageFromImage(raw.ToImage(), 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

@@ -2,15 +2,16 @@ package config
import (
"errors"
"fmt"
"os"
"path/filepath"
"github.com/BurntSushi/toml"
)
type Ordoor struct {
DataDir string `toml:"data_dir"`
VideoPlayer []string `toml:"video_player"`
type Engine struct {
DataDir string `toml:"data_dir"`
Palette string `toml:"palette"`
}
// Things set in the options hash
@@ -38,12 +39,40 @@ type Options struct {
type Config struct {
filename string `toml:"-"`
VideoPlayer []string `toml:"video_player"`
Engines map[string]Engine `toml:"engines"`
DefaultEngineName string `toml:"default_engine"`
// FIXME: options may well end up being per-engine too
Defaults *Options `toml:"-"`
Ordoor `toml:"ordoor"`
Options `toml:"options"`
}
func Load(filename string) (*Config, error) {
func (c *Config) Engine(name string) *Engine {
engine, ok := c.Engines[name]
if !ok {
return nil
}
return &engine
}
func (c *Config) DefaultEngine() *Engine {
return c.Engine(c.DefaultEngineName)
}
// TODO: case-insensitive lookup
func (c *Config) DataFile(engine string, path string) string {
cfg, ok := c.Engines[engine]
if !ok {
return ""
}
return filepath.Join(cfg.DataDir, path)
}
func Load(filename string, overrideDefaultEngine string) (*Config, error) {
var out Config
_, err := toml.DecodeFile(filename, &out)
@@ -53,7 +82,15 @@ func Load(filename string) (*Config, error) {
out.filename = filename
return &out, err
if overrideDefaultEngine != "" {
out.DefaultEngineName = overrideDefaultEngine
}
if out.DefaultEngine() == nil {
return nil, fmt.Errorf("Default engine %q not configured", out.DefaultEngineName)
}
return &out, nil
}
func (c *Config) HasUnsetOptions() bool {
@@ -72,11 +109,6 @@ func (c *Config) Save() error {
return toml.NewEncoder(f).Encode(c)
}
// TODO: case-insensitive lookup
func (c *Config) DataFile(path string) string {
return filepath.Join(c.DataDir, path)
}
func (c *Config) ResetDefaults() error {
if c.Defaults == nil {
return errors.New("Defaults not available")

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

@@ -4,6 +4,7 @@ import (
"encoding/binary"
"fmt"
"image"
"image/color"
"io"
"io/ioutil"
"log"
@@ -27,12 +28,16 @@ type SpriteHeader struct {
func (s SpriteHeader) Check(expectedSize uint32) error {
if s.Padding1 != 0 || s.Padding2 != 0 {
return fmt.Errorf("Sprite header padding contains unknown values: %d %d", s.Padding1, s.Padding2)
if s.Padding1 == 271 && s.Padding2 == 0 {
log.Printf("Sprite header padding matches FIXME value")
} else {
return fmt.Errorf("Sprite header padding contains unknown values: %d %d", s.Padding1, s.Padding2)
}
}
// TODO: WarHammer.ani sets Unknown1 to this for all 188,286 sprites. I am
// very interested in seeing if there are any others
if s.Unknown1[0] | s.Unknown1[1] | s.Unknown1[2] | s.Unknown1[3] > 0 {
if s.Unknown1[0]|s.Unknown1[1]|s.Unknown1[2]|s.Unknown1[3] > 0 {
if s.Unknown1[0] != 212 || s.Unknown1[1] != 113 || s.Unknown1[2] != 59 || s.Unknown1[3] != 1 {
log.Printf("Value of Unknown1 field: %v", s.Unknown1)
}
@@ -52,12 +57,12 @@ type Sprite struct {
Data []byte
}
func (s *Sprite) ToImage() *image.Paletted {
func (s *Sprite) ToImage(palette color.Palette) *image.Paletted {
return &image.Paletted{
Pix: s.Data,
Stride: int(s.Width),
Rect: image.Rect(0, 0, int(s.Width), int(s.Height)),
Palette: ColorPalette,
Palette: palette,
}
}

View File

@@ -2,6 +2,9 @@ package flow
import (
"fmt"
"code.ur.gs/lupine/ordoor/internal/assetstore"
"code.ur.gs/lupine/ordoor/internal/ui"
)
type driverName string
@@ -36,8 +39,62 @@ var (
configureUltEquip, configureVehiclesUltra,
mainGame,
}
menuTransforms = map[driverName]func(*assetstore.Menu){
mainGame: offsetMainGame,
}
)
// FIXME: HURK: MainGame elements need changes to show up in the right place
func offsetMainGame(menu *assetstore.Menu) {
for _, group := range menu.Groups() {
id := group.ID
// Bottom-aligned, not top-aligned
if id == 1 || id == 2 || id == 3 || id == 4 || id == 5 || id == 6 ||
id == 7 || id == 8 || id == 9 || id == 10 || id == 15 || id == 16 {
group.Y = 320 // Down by 320px
// FIXME: in reality, this appears to be a property of the group only
for _, rec := range group.Records {
rec.Y = 320
}
}
// Right-aligned, not left-aligned
// FIXME: this presents problems as there are two sizes and both need to
// be right-aligned, so a static offset won't quite work
// if id == 14 {
// group.X = 400 (or so)
// }
// Left-aligned, not centered
// FIXME: we're re-using the X-CORD and Y-CORD elements here. How do we
// signal a negative number?
// if id == 18 {
// group.X = 0
// }
}
}
func buildDriver(assets *assetstore.AssetStore, name driverName) (*ui.Driver, error) {
menu, err := assets.Menu(string(name))
if err != nil {
return nil, err
}
if tf, ok := menuTransforms[name]; ok {
tf(menu)
}
driver, err := ui.NewDriver(assets, menu)
if err != nil {
return nil, err
}
return driver, nil
}
func (f *Flow) returnToLastDriver(from driverName) func() {
return func() {
to, ok := f.returns[from]

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,18 +93,9 @@ func New(assets *assetstore.AssetStore, config *config.Config, ship *ship.Ship)
return out, out.exit
}
func buildDriver(assets *assetstore.AssetStore, name driverName) (*ui.Driver, error) {
menu, err := assets.Menu(string(name))
if err != nil {
return nil, err
}
driver, err := ui.NewDriver(assets, menu)
if err != nil {
return nil, err
}
return driver, nil
func (f *Flow) SetScenario(scenario *scenario.Scenario) {
f.current = f.drivers[mainGame]
f.scenario = scenario
}
func (f *Flow) Update(screenX, screenY int) error {
@@ -111,6 +103,32 @@ func (f *Flow) Update(screenX, screenY int) error {
return f.exit
}
// Keybindings for map control
// FIXME: this needs a big rethink
if f.current != nil && f.scenario != nil && !f.current.IsInDialogue() {
step := 32
if ebiten.IsKeyPressed(ebiten.KeyLeft) {
f.scenario.Viewpoint.X -= step
}
if ebiten.IsKeyPressed(ebiten.KeyRight) {
f.scenario.Viewpoint.X += step
}
if ebiten.IsKeyPressed(ebiten.KeyUp) {
f.scenario.Viewpoint.Y -= step
}
if ebiten.IsKeyPressed(ebiten.KeyDown) {
f.scenario.Viewpoint.Y += step
}
if 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 err := f.scenario.Update(screenX, screenY); err != nil {
return err
@@ -241,9 +259,37 @@ func (f *Flow) playNextScenario(from driverName) func() {
}
}
func (f *Flow) setActive(driver driverName, id string, value bool) func() {
return func() {
if f.exit != nil {
return
}
f.exit = maybeErr(driver, f.setActiveNow(driver, id, value))
}
}
func (f *Flow) setActiveNow(driver driverName, id string, value bool) error {
return f.drivers[driver].SetActive(locator(driver, id), value)
}
func (f *Flow) toggleActive(driver driverName, id string) func() {
return func() {
if f.exit != nil {
return
}
f.exit = maybeErr(driver, f.drivers[driver].ToggleActive(locator(driver, id)))
}
}
func (f *Flow) showDialogue(driver driverName, id string) func() {
return func() {
f.drivers[driver].ShowDialogue(locator(driver, id))
if f.exit != nil {
return
}
f.exit = maybeErr(driver, f.drivers[driver].ShowDialogue(locator(driver, id)))
}
}
@@ -251,6 +297,14 @@ func (f *Flow) hideDialogue(driver driverName) func() {
return f.drivers[driver].HideDialogue
}
func (f *Flow) withScenario(then func()) func() {
return func() {
if f.scenario != nil {
then()
}
}
}
func (f *Flow) reset() {
if f.exit != nil {
return

View File

@@ -1,55 +1,205 @@
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?
func (f *Flow) linkMainGame() {
// 3: Action menu
// 4: Interface options menu
f.onClick(mainGame, "4.1", f.setReturningDriver(mainGame, options)) // Options button
// 4.2: Map button
// 4.3: Mission objectives button
// 4.4: Inventory
// 4.5: Next man
// 4.6: Next enemy
// 4.7: Total enemy text
f.linkMainGameActionMenu()
f.linkMainGameInterfaceOptionsMenu()
// 5: Holding menu
// 6: View menu
f.linkMainGameViewMenu()
// 7: General character menu
f.onClick(mainGame, "7.4", func() { // More button
f.setActiveNow(mainGame, "7", false)
f.setActiveNow(mainGame, "8", true)
})
// 8: Character stats
f.onClick(mainGame, "8.21", func() { // Stat more buttons
f.setActiveNow(mainGame, "7", true)
f.setActiveNow(mainGame, "8", false)
})
// 9: Visible enemy menu
// 10: Friendly squad menu
// 11: Psyker spell dialogue
// FIXME: lots and lots and lots of wiring up to do.
// For now, just link all the exit buttons to go back to the bridge
f.onClick(mainGame, "11.6", func() {
f.scenario = nil
f.returnToLastDriverNow(mainGame)
})
// 12: Inventory dialogue
f.onClick(mainGame, "12.21", func() {
f.scenario = nil
f.returnToLastDriverNow(mainGame)
})
f.onClick(mainGame, "12.21", f.hideDialogue(mainGame)) // Exit
// 13: Exchange menu
f.onClick(mainGame, "13.1", func() {
f.scenario = nil
f.returnToLastDriverNow(mainGame)
})
// 13: exchange menu
// 14: Map
// 14.1: MAP_SPRITE
// 14.2: Multiplier button (2x)
f.onClick(mainGame, "14.3", f.setActive(mainGame, "14", false))
// 14.4: Area
// 15: Interface wing left
// 16: Interface wing right
// FIXME: the display of left and right interface buttons is hidden by these
// sprites, because we draw in strict numeric order. Just hide them for now.
//
// FIXME: The child element is already set to hidden, while the menu itself
// is set to active, so maybe this is a hint that menus shouldn't be drawn?
//
// FIXME: the approach taken by the original binary in resolutions greater
// than 640x480 is to draw the menu elements *unscaled*. They are centered,
// and the dead space is filled by the "interface wing" sprites in the
// background. Should we replicate this, or keep with the current scaling
// behaviour? Which is better?
f.exit = maybeErr(mainGame, f.setActiveNow(mainGame, "15", false)) // Interface wing left
f.exit = maybeErr(mainGame, f.setActiveNow(mainGame, "16", false)) // Interface wing right
// 17: Grenade dialogue
// 18: Info dialogue
f.onClick(mainGame, "18.12", f.setActive(mainGame, "18", false)) // Info "dialogue"
// 19: Turn start dialogue
// 20: Chat menu
// 21: Chat list menu box
// Chat list menu box - active by default, hide it
f.exit = maybeErr(mainGame, f.setActiveNow(mainGame, "21", false))
}
func (f *Flow) linkMainGameActionMenu() {
// 3: Action menu. These are mostly predicated on selected character state
// 3.1: Aimed shot
// 3.2: Shooting
// 3.3: Walk
// 3.4: Run
// 3.5: Crouch/Stand
// 3.6: Hand to hand (commented out)
// 3.7: Retrieve
// 3.8: Door
// 3.9: Switch
// 3.10: Overwatch
// 3.11: Rally/Formation
// 3.12: Board/Disembark
// FIXME: for now, this is "end scenario", for convenience
f.onClick(mainGame, "3.13", func() { // End turn button.
f.scenario = nil
f.returnToLastDriverNow(mainGame)
})
// 3.14: Special action heal
// 3.15: Special action techmarine
// 3.16: Special action jump pack
// 3.17: Special action spell
}
func (f *Flow) linkMainGameInterfaceOptionsMenu() {
// 4: Interface options menu
f.onClick(mainGame, "4.1", f.setReturningDriver(mainGame, options)) // Options button
// FIXME: map should be shown top-right, not top-left. We need to support 2x
// mode as well.
f.onClick(mainGame, "4.2", f.toggleActive(mainGame, "14")) // Map button
// FIXME: mission objectives should be shown top-left, not centered
f.onClick(mainGame, "4.3", f.toggleActive(mainGame, "18")) // Mission objectives
f.onClick(mainGame, "4.4", f.showDialogue(mainGame, "12")) // Inventory
// 4.5: Next man
// 4.6: Next enemy
// 4.7: Total enemy text
}
func (f *Flow) linkMainGameViewMenu() {
// FIXME: all these buttons should show current state as well as have an
// effect
f.onClick(mainGame, "6.1", f.withScenario(func() { // View 100%
f.scenario.Zoom = 1.0
}))
f.onClick(mainGame, "6.2", f.withScenario(func() { // View 50%
f.scenario.Zoom = 0.5
}))
f.onClick(mainGame, "6.3", f.withScenario(func() { // View 25%
f.scenario.Zoom = 0.25
}))
f.onClick(mainGame, "6.4", f.withScenario(func() { // Z index up
f.scenario.ChangeZIdx(+1)
}))
f.onClick(mainGame, "6.5", f.withScenario(func() { // Z index down
f.scenario.ChangeZIdx(-1)
}))
f.onClick(mainGame, "6.6", f.withScenario(func() { // Z index 1
f.scenario.ZIdx = 0
}))
f.onClick(mainGame, "6.7", f.withScenario(func() { // Z index 2
f.scenario.ZIdx = 1
}))
f.onClick(mainGame, "6.8", f.withScenario(func() { // Z index 3
f.scenario.ZIdx = 2
}))
f.onClick(mainGame, "6.9", f.withScenario(func() { // Z index 4
f.scenario.ZIdx = 3
}))
f.onClick(mainGame, "6.10", f.withScenario(func() { // Z index 5
f.scenario.ZIdx = 4
}))
f.onClick(mainGame, "6.11", f.withScenario(func() { // Z index 6
f.scenario.ZIdx = 5
}))
f.onClick(mainGame, "6.12", f.withScenario(func() { // Z index 7
f.scenario.ZIdx = 6
}))
}
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

@@ -5,12 +5,15 @@ import (
"compress/gzip"
"encoding/binary"
"fmt"
"io"
"image"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"github.com/lunixbochs/struc"
"code.ur.gs/lupine/ordoor/internal/data"
)
var (
@@ -26,63 +29,135 @@ const (
CellSize = 16 // seems to be
cellDataOffset = 0x110 // tentatively
cellDataOffset = 0x110 // definitely
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)
}
@@ -92,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:
@@ -146,31 +206,27 @@ 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
return nil
}
type GameMap struct {
Header
Cells
// TODO: parse this into sections
Text string
func (m *GameMap) Rect() image.Rectangle {
return image.Rect(
int(m.MinWidth),
int(m.MinLength),
int(m.MaxWidth),
int(m.MaxLength),
)
}
// A game map contains a .txt and a .map. If they're in the same directory,
@@ -192,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
}
@@ -246,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 {
@@ -261,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

@@ -2,13 +2,13 @@ package menus
import (
"fmt"
"image"
"image/color"
"io/ioutil"
"path/filepath"
"strconv"
"strings"
"code.ur.gs/lupine/ordoor/internal/data"
"code.ur.gs/lupine/ordoor/internal/util/asciiscan"
)
@@ -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
@@ -134,7 +134,15 @@ type Properties struct {
Help string
}
func LoadMenu(filename string) (*Menu, error) {
func (p *Properties) Point() image.Point {
if p.X > 0 || p.Y > 0 {
return image.Pt(p.X, p.Y)
}
return image.Point{}
}
func LoadMenu(filename string, palette color.Palette) (*Menu, error) {
name := filepath.Base(filename)
name = strings.TrimSuffix(name, filepath.Ext(name))
name = strings.ToLower(name)
@@ -154,7 +162,7 @@ func LoadMenu(filename string) (*Menu, error) {
return nil, err
}
if err := loadProperties(out, scanner); err != nil {
if err := loadProperties(out, scanner, palette); err != nil {
return nil, err
}
@@ -180,7 +188,7 @@ func loadObjects(menu *Menu, scanner *asciiscan.Scanner) error {
return nil
}
func loadProperties(menu *Menu, scanner *asciiscan.Scanner) error {
func loadProperties(menu *Menu, scanner *asciiscan.Scanner, palette color.Palette) error {
for {
ok, err := scanner.PeekProperty()
@@ -209,9 +217,9 @@ func loadProperties(menu *Menu, scanner *asciiscan.Scanner) error {
switch strings.ToUpper(k) {
case "BACKGROUND COLOR":
menu.BackgroundColor = data.ColorPalette[vInt]
menu.BackgroundColor = palette[vInt]
case "HYPERTEXT COLOR":
menu.HypertextColor = data.ColorPalette[vInt]
menu.HypertextColor = palette[vInt]
case "FONT TYPE":
menu.FontType = vInt
default:
@@ -310,7 +318,7 @@ func loadRecords(baseDir string, menu *Menu, scanner *asciiscan.Scanner) error {
return nil
}
func LoadMenus(dir string) (map[string]*Menu, error) {
func LoadMenus(dir string, palette color.Palette) (map[string]*Menu, error) {
fis, err := ioutil.ReadDir(dir)
if err != nil {
return nil, err
@@ -328,7 +336,7 @@ func LoadMenus(dir string) (map[string]*Menu, error) {
continue
}
built, err := LoadMenu(filepath.Join(dir, relname))
built, err := LoadMenu(filepath.Join(dir, relname), palette)
if err != nil {
return nil, fmt.Errorf("%s: %v", filepath.Join(dir, relname), err)
}

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,19 +29,24 @@ 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
}
func Run(configFile string, overrideX, overrideY int) error {
cfg, err := config.Load(configFile)
cfg, err := config.Load(configFile, "ordoor")
if err != nil {
return fmt.Errorf("Couldn't load config file: %v", err)
}
assets, err := assetstore.New(cfg.Ordoor.DataDir)
assets, err := assetstore.New(cfg.DefaultEngine())
if err != nil {
return fmt.Errorf("Failed to initialize asset store: %v", err)
}
@@ -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

@@ -6,7 +6,7 @@ import (
)
func (o *Ordoor) PlayVideo(name string, skippable bool) {
filename := o.config.DataFile("SMK/" + name + ".smk")
filename := o.config.DataFile("ordoor", "SMK/"+name+".smk")
if len(o.config.VideoPlayer) == 0 {
log.Printf("Video player not configured, skipping video %v", filename)

View File

@@ -1,12 +1,11 @@
package data
package palettes
import "image/color"
var (
Transparent = color.RGBA{R: 0, G: 0, B: 0, A: 0}
ColorPalette = color.Palette{
ChaosGatePalette = color.Palette{
Transparent,
color.RGBA{R: 128, G: 0, B: 0, A: 255},
color.RGBA{R: 0, G: 128, B: 0, A: 255},
color.RGBA{R: 128, G: 128, B: 0, A: 255},
@@ -264,3 +263,7 @@ var (
color.RGBA{R: 255, G: 255, B: 255, A: 255},
}
)
func init() {
Palettes["ChaosGate"] = ChaosGatePalette
}

View File

@@ -0,0 +1,20 @@
package palettes
import (
"image/color"
"sync"
)
var (
Transparent = color.RGBA{R: 0, G: 0, B: 0, A: 0}
Palettes = map[string]color.Palette{}
initPalettes = sync.Once{}
)
func Get(name string) (color.Palette, bool) {
p, ok := Palettes[name]
return p, ok
}

View File

@@ -0,0 +1,269 @@
package palettes
import "image/color"
var (
SoldiersAtWarPalette = color.Palette{
Transparent,
color.RGBA{R: 128, G: 0, B: 0, A: 255},
color.RGBA{R: 0, G: 128, B: 0, A: 255},
color.RGBA{R: 128, G: 128, B: 0, A: 255},
color.RGBA{R: 0, G: 0, B: 128, A: 255},
color.RGBA{R: 128, G: 0, B: 128, A: 255},
color.RGBA{R: 0, G: 128, B: 128, A: 255},
color.RGBA{R: 192, G: 192, B: 192, A: 255},
color.RGBA{R: 192, G: 220, B: 192, A: 255},
color.RGBA{R: 166, G: 202, B: 240, A: 255},
color.RGBA{R: 0, G: 136, B: 65, A: 255},
color.RGBA{R: 24, G: 153, B: 68, A: 255},
color.RGBA{R: 48, G: 170, B: 71, A: 255},
color.RGBA{R: 72, G: 187, B: 73, A: 255},
color.RGBA{R: 96, G: 204, B: 76, A: 255},
color.RGBA{R: 120, G: 218, B: 122, A: 255},
color.RGBA{R: 24, G: 12, B: 0, A: 255},
color.RGBA{R: 31, G: 14, B: 7, A: 255},
color.RGBA{R: 43, G: 19, B: 5, A: 255},
color.RGBA{R: 48, G: 21, B: 3, A: 255},
color.RGBA{R: 60, G: 26, B: 2, A: 255},
color.RGBA{R: 79, G: 43, B: 18, A: 255},
color.RGBA{R: 99, G: 60, B: 35, A: 255},
color.RGBA{R: 118, G: 77, B: 52, A: 255},
color.RGBA{R: 137, G: 94, B: 69, A: 255},
color.RGBA{R: 157, G: 111, B: 86, A: 255},
color.RGBA{R: 176, G: 127, B: 103, A: 255},
color.RGBA{R: 195, G: 144, B: 120, A: 255},
color.RGBA{R: 214, G: 161, B: 137, A: 255},
color.RGBA{R: 234, G: 178, B: 154, A: 255},
color.RGBA{R: 253, G: 197, B: 173, A: 255},
color.RGBA{R: 11, G: 11, B: 230, A: 255},
color.RGBA{R: 41, G: 50, B: 255, A: 255},
color.RGBA{R: 62, G: 79, B: 255, A: 255},
color.RGBA{R: 83, G: 109, B: 255, A: 255},
color.RGBA{R: 103, G: 138, B: 255, A: 255},
color.RGBA{R: 124, G: 168, B: 255, A: 255},
color.RGBA{R: 145, G: 197, B: 255, A: 255},
color.RGBA{R: 166, G: 227, B: 255, A: 255},
color.RGBA{R: 0, G: 16, B: 0, A: 255},
color.RGBA{R: 1, G: 27, B: 0, A: 255},
color.RGBA{R: 2, G: 37, B: 0, A: 255},
color.RGBA{R: 3, G: 48, B: 0, A: 255},
color.RGBA{R: 4, G: 59, B: 0, A: 255},
color.RGBA{R: 5, G: 70, B: 0, A: 255},
color.RGBA{R: 6, G: 80, B: 0, A: 255},
color.RGBA{R: 7, G: 91, B: 0, A: 255},
color.RGBA{R: 48, G: 4, B: 0, A: 255},
color.RGBA{R: 64, G: 4, B: 0, A: 255},
color.RGBA{R: 93, G: 8, B: 0, A: 255},
color.RGBA{R: 121, G: 24, B: 0, A: 255},
color.RGBA{R: 145, G: 40, B: 0, A: 255},
color.RGBA{R: 174, G: 64, B: 0, A: 255},
color.RGBA{R: 198, G: 89, B: 0, A: 255},
color.RGBA{R: 226, G: 121, B: 0, A: 255},
color.RGBA{R: 238, G: 155, B: 0, A: 255},
color.RGBA{R: 255, G: 182, B: 28, A: 255},
color.RGBA{R: 255, G: 202, B: 60, A: 255},
color.RGBA{R: 255, G: 218, B: 97, A: 255},
color.RGBA{R: 255, G: 234, B: 129, A: 255},
color.RGBA{R: 255, G: 242, B: 165, A: 255},
color.RGBA{R: 255, G: 250, B: 198, A: 255},
color.RGBA{R: 255, G: 238, B: 238, A: 255},
color.RGBA{R: 0, G: 32, B: 16, A: 255},
color.RGBA{R: 0, G: 44, B: 24, A: 255},
color.RGBA{R: 4, G: 60, B: 36, A: 255},
color.RGBA{R: 8, G: 76, B: 52, A: 255},
color.RGBA{R: 16, G: 89, B: 64, A: 255},
color.RGBA{R: 24, G: 105, B: 80, A: 255},
color.RGBA{R: 36, G: 121, B: 97, A: 255},
color.RGBA{R: 4, G: 56, B: 0, A: 255},
color.RGBA{R: 11, G: 68, B: 9, A: 255},
color.RGBA{R: 19, G: 80, B: 18, A: 255},
color.RGBA{R: 26, G: 92, B: 27, A: 255},
color.RGBA{R: 34, G: 105, B: 37, A: 255},
color.RGBA{R: 41, G: 117, B: 46, A: 255},
color.RGBA{R: 49, G: 129, B: 55, A: 255},
color.RGBA{R: 56, G: 141, B: 64, A: 255},
color.RGBA{R: 40, G: 0, B: 0, A: 255},
color.RGBA{R: 56, G: 0, B: 0, A: 255},
color.RGBA{R: 76, G: 0, B: 0, A: 255},
color.RGBA{R: 97, G: 0, B: 0, A: 255},
color.RGBA{R: 113, G: 0, B: 0, A: 255},
color.RGBA{R: 133, G: 4, B: 4, A: 255},
color.RGBA{R: 153, G: 4, B: 4, A: 255},
color.RGBA{R: 170, G: 8, B: 8, A: 255},
color.RGBA{R: 190, G: 12, B: 12, A: 255},
color.RGBA{R: 210, G: 16, B: 16, A: 255},
color.RGBA{R: 214, G: 48, B: 48, A: 255},
color.RGBA{R: 222, G: 80, B: 80, A: 255},
color.RGBA{R: 230, G: 117, B: 117, A: 255},
color.RGBA{R: 238, G: 153, B: 153, A: 255},
color.RGBA{R: 246, G: 194, B: 194, A: 255},
color.RGBA{R: 255, G: 212, B: 227, A: 255},
color.RGBA{R: 10, G: 10, B: 10, A: 255},
color.RGBA{R: 25, G: 25, B: 25, A: 255},
color.RGBA{R: 41, G: 41, B: 41, A: 255},
color.RGBA{R: 56, G: 56, B: 56, A: 255},
color.RGBA{R: 71, G: 71, B: 71, A: 255},
color.RGBA{R: 87, G: 87, B: 87, A: 255},
color.RGBA{R: 102, G: 102, B: 102, A: 255},
color.RGBA{R: 117, G: 117, B: 117, A: 255},
color.RGBA{R: 133, G: 133, B: 133, A: 255},
color.RGBA{R: 148, G: 148, B: 148, A: 255},
color.RGBA{R: 163, G: 163, B: 163, A: 255},
color.RGBA{R: 179, G: 179, B: 179, A: 255},
color.RGBA{R: 194, G: 194, B: 194, A: 255},
color.RGBA{R: 209, G: 209, B: 209, A: 255},
color.RGBA{R: 225, G: 225, B: 225, A: 255},
color.RGBA{R: 240, G: 240, B: 240, A: 255},
color.RGBA{R: 7, G: 7, B: 19, A: 255},
color.RGBA{R: 12, G: 12, B: 24, A: 255},
color.RGBA{R: 20, G: 20, B: 32, A: 255},
color.RGBA{R: 32, G: 32, B: 48, A: 255},
color.RGBA{R: 44, G: 44, B: 64, A: 255},
color.RGBA{R: 60, G: 60, B: 80, A: 255},
color.RGBA{R: 72, G: 72, B: 97, A: 255},
color.RGBA{R: 85, G: 85, B: 109, A: 255},
color.RGBA{R: 101, G: 101, B: 125, A: 255},
color.RGBA{R: 117, G: 117, B: 128, A: 255},
color.RGBA{R: 133, G: 133, B: 157, A: 255},
color.RGBA{R: 149, G: 149, B: 170, A: 255},
color.RGBA{R: 165, G: 165, B: 186, A: 255},
color.RGBA{R: 182, G: 182, B: 202, A: 255},
color.RGBA{R: 202, G: 202, B: 218, A: 255},
color.RGBA{R: 222, G: 222, B: 234, A: 255},
color.RGBA{R: 158, G: 158, B: 158, A: 255},
color.RGBA{R: 146, G: 146, B: 146, A: 255},
color.RGBA{R: 75, G: 65, B: 55, A: 255},
color.RGBA{R: 71, G: 55, B: 46, A: 255},
color.RGBA{R: 64, G: 36, B: 22, A: 255},
color.RGBA{R: 60, G: 26, B: 1, A: 255},
color.RGBA{R: 66, G: 44, B: 38, A: 255},
color.RGBA{R: 64, G: 63, B: 54, A: 255},
color.RGBA{R: 88, G: 84, B: 82, A: 255},
color.RGBA{R: 81, G: 95, B: 95, A: 255},
color.RGBA{R: 101, G: 102, B: 102, A: 255},
color.RGBA{R: 116, G: 117, B: 117, A: 255},
color.RGBA{R: 132, G: 133, B: 133, A: 255},
color.RGBA{R: 63, G: 62, B: 53, A: 255},
color.RGBA{R: 87, G: 83, B: 81, A: 255},
color.RGBA{R: 80, G: 94, B: 94, A: 255},
color.RGBA{R: 195, G: 80, B: 0, A: 255},
color.RGBA{R: 235, G: 150, B: 0, A: 255},
color.RGBA{R: 250, G: 200, B: 60, A: 255},
color.RGBA{R: 95, G: 95, B: 95, A: 255},
color.RGBA{R: 110, G: 110, B: 110, A: 255},
color.RGBA{R: 170, G: 170, B: 170, A: 255},
color.RGBA{R: 85, G: 52, B: 12, A: 255},
color.RGBA{R: 68, G: 40, B: 4, A: 255},
color.RGBA{R: 52, G: 32, B: 0, A: 255},
color.RGBA{R: 36, G: 20, B: 0, A: 255},
color.RGBA{R: 68, G: 42, B: 22, A: 255},
color.RGBA{R: 77, G: 48, B: 25, A: 255},
color.RGBA{R: 86, G: 55, B: 29, A: 255},
color.RGBA{R: 93, G: 61, B: 32, A: 255},
color.RGBA{R: 106, G: 72, B: 38, A: 255},
color.RGBA{R: 114, G: 80, B: 42, A: 255},
color.RGBA{R: 122, G: 87, B: 46, A: 255},
color.RGBA{R: 130, G: 96, B: 56, A: 255},
color.RGBA{R: 136, G: 103, B: 64, A: 255},
color.RGBA{R: 141, G: 110, B: 73, A: 255},
color.RGBA{R: 145, G: 115, B: 80, A: 255},
color.RGBA{R: 173, G: 142, B: 102, A: 255},
color.RGBA{R: 200, G: 168, B: 123, A: 255},
color.RGBA{R: 228, G: 195, B: 145, A: 255},
color.RGBA{R: 0, G: 0, B: 79, A: 255},
color.RGBA{R: 25, G: 25, B: 100, A: 255},
color.RGBA{R: 51, G: 51, B: 121, A: 255},
color.RGBA{R: 76, G: 76, B: 141, A: 255},
color.RGBA{R: 101, G: 101, B: 162, A: 255},
color.RGBA{R: 126, G: 126, B: 183, A: 255},
color.RGBA{R: 152, G: 152, B: 204, A: 255},
color.RGBA{R: 80, G: 100, B: 143, A: 255},
color.RGBA{R: 20, G: 27, B: 49, A: 255},
color.RGBA{R: 30, G: 42, B: 64, A: 255},
color.RGBA{R: 13, G: 33, B: 69, A: 255},
color.RGBA{R: 24, G: 45, B: 82, A: 255},
color.RGBA{R: 35, G: 55, B: 94, A: 255},
color.RGBA{R: 46, G: 66, B: 106, A: 255},
color.RGBA{R: 57, G: 77, B: 118, A: 255},
color.RGBA{R: 69, G: 89, B: 130, A: 255},
color.RGBA{R: 40, G: 48, B: 0, A: 255},
color.RGBA{R: 58, G: 66, B: 12, A: 255},
color.RGBA{R: 77, G: 84, B: 24, A: 255},
color.RGBA{R: 95, G: 102, B: 36, A: 255},
color.RGBA{R: 114, G: 120, B: 48, A: 255},
color.RGBA{R: 132, G: 138, B: 61, A: 255},
color.RGBA{R: 151, G: 156, B: 73, A: 255},
color.RGBA{R: 169, G: 174, B: 85, A: 255},
color.RGBA{R: 188, G: 192, B: 97, A: 255},
color.RGBA{R: 206, G: 210, B: 109, A: 255},
color.RGBA{R: 56, G: 56, B: 40, A: 255},
color.RGBA{R: 76, G: 76, B: 56, A: 255},
color.RGBA{R: 101, G: 101, B: 76, A: 255},
color.RGBA{R: 125, G: 125, B: 101, A: 255},
color.RGBA{R: 145, G: 145, B: 121, A: 255},
color.RGBA{R: 170, G: 170, B: 145, A: 255},
color.RGBA{R: 41, G: 46, B: 30, A: 255},
color.RGBA{R: 48, G: 53, B: 33, A: 255},
color.RGBA{R: 55, G: 60, B: 37, A: 255},
color.RGBA{R: 63, G: 67, B: 40, A: 255},
color.RGBA{R: 70, G: 74, B: 43, A: 255},
color.RGBA{R: 77, G: 81, B: 47, A: 255},
color.RGBA{R: 84, G: 89, B: 50, A: 255},
color.RGBA{R: 91, G: 96, B: 54, A: 255},
color.RGBA{R: 98, G: 103, B: 57, A: 255},
color.RGBA{R: 106, G: 110, B: 60, A: 255},
color.RGBA{R: 113, G: 117, B: 64, A: 255},
color.RGBA{R: 120, G: 124, B: 67, A: 255},
color.RGBA{R: 230, G: 230, B: 230, A: 255},
color.RGBA{R: 218, G: 218, B: 218, A: 255},
color.RGBA{R: 206, G: 206, B: 206, A: 255},
color.RGBA{R: 182, G: 182, B: 182, A: 255},
color.RGBA{R: 100, G: 59, B: 34, A: 255},
color.RGBA{R: 125, G: 84, B: 59, A: 255},
color.RGBA{R: 150, G: 109, B: 84, A: 255},
color.RGBA{R: 174, G: 134, B: 108, A: 255},
color.RGBA{R: 199, G: 159, B: 133, A: 255},
color.RGBA{R: 97, G: 64, B: 16, A: 255},
color.RGBA{R: 113, G: 76, B: 28, A: 255},
color.RGBA{R: 129, G: 93, B: 36, A: 255},
color.RGBA{R: 141, G: 105, B: 48, A: 255},
color.RGBA{R: 157, G: 117, B: 60, A: 255},
color.RGBA{R: 174, G: 133, B: 76, A: 255},
color.RGBA{R: 190, G: 149, B: 93, A: 255},
color.RGBA{R: 202, G: 165, B: 109, A: 255},
color.RGBA{R: 218, G: 182, B: 129, A: 255},
color.RGBA{R: 234, G: 198, B: 145, A: 255},
color.RGBA{R: 250, G: 218, B: 170, A: 255},
color.RGBA{R: 157, G: 76, B: 36, A: 255},
color.RGBA{R: 161, G: 93, B: 60, A: 255},
color.RGBA{R: 174, G: 113, B: 72, A: 255},
color.RGBA{R: 202, G: 139, B: 104, A: 255},
color.RGBA{R: 178, G: 127, B: 100, A: 255},
color.RGBA{R: 28, G: 36, B: 0, A: 255},
color.RGBA{R: 41, G: 49, B: 10, A: 255},
color.RGBA{R: 55, G: 62, B: 20, A: 255},
color.RGBA{R: 68, G: 75, B: 30, A: 255},
color.RGBA{R: 81, G: 88, B: 40, A: 255},
color.RGBA{R: 95, G: 100, B: 50, A: 255},
color.RGBA{R: 121, G: 126, B: 70, A: 255},
color.RGBA{R: 135, G: 139, B: 80, A: 255},
color.RGBA{R: 148, G: 152, B: 90, A: 255},
color.RGBA{R: 162, G: 165, B: 100, A: 255},
color.RGBA{R: 175, G: 178, B: 110, A: 255},
color.RGBA{R: 255, G: 251, B: 240, A: 255},
color.RGBA{R: 160, G: 160, B: 164, A: 255},
color.RGBA{R: 128, G: 128, B: 128, A: 255},
color.RGBA{R: 255, G: 0, B: 0, A: 255},
color.RGBA{R: 0, G: 255, B: 0, A: 255},
color.RGBA{R: 255, G: 255, B: 0, A: 255},
color.RGBA{R: 0, G: 0, B: 255, A: 255},
color.RGBA{R: 255, G: 0, B: 255, A: 255},
color.RGBA{R: 0, G: 255, B: 255, A: 255},
color.RGBA{R: 255, G: 255, B: 255, A: 255},
}
)
func init() {
Palettes["SoldiersAtWar"] = SoldiersAtWarPalette
}

View File

@@ -0,0 +1,269 @@
package palettes
import "image/color"
var (
WagesOfWarPalette = color.Palette{
Transparent,
color.RGBA{R: 1, G: 1, B: 1, A: 255},
color.RGBA{R: 254, G: 254, B: 254, A: 255},
color.RGBA{R: 187, G: 4, B: 4, A: 255},
color.RGBA{R: 181, G: 195, B: 2, A: 255},
color.RGBA{R: 94, G: 79, B: 148, A: 255},
color.RGBA{R: 233, G: 91, B: 4, A: 255},
color.RGBA{R: 60, G: 60, B: 60, A: 255},
color.RGBA{R: 102, G: 102, B: 102, A: 255},
color.RGBA{R: 170, G: 170, B: 170, A: 255},
color.RGBA{R: 197, G: 195, B: 193, A: 255},
color.RGBA{R: 234, G: 234, B: 235, A: 255},
color.RGBA{R: 218, G: 218, B: 240, A: 255},
color.RGBA{R: 202, G: 202, B: 245, A: 255},
color.RGBA{R: 186, G: 186, B: 250, A: 255},
color.RGBA{R: 170, G: 170, B: 255, A: 255},
color.RGBA{R: 238, G: 238, B: 238, A: 255},
color.RGBA{R: 222, G: 222, B: 222, A: 255},
color.RGBA{R: 206, G: 206, B: 206, A: 255},
color.RGBA{R: 190, G: 190, B: 190, A: 255},
color.RGBA{R: 174, G: 174, B: 174, A: 255},
color.RGBA{R: 157, G: 157, B: 157, A: 255},
color.RGBA{R: 141, G: 141, B: 141, A: 255},
color.RGBA{R: 129, G: 129, B: 129, A: 255},
color.RGBA{R: 113, G: 113, B: 113, A: 255},
color.RGBA{R: 97, G: 97, B: 97, A: 255},
color.RGBA{R: 80, G: 80, B: 80, A: 255},
color.RGBA{R: 64, G: 64, B: 64, A: 255},
color.RGBA{R: 48, G: 48, B: 48, A: 255},
color.RGBA{R: 32, G: 32, B: 32, A: 255},
color.RGBA{R: 16, G: 16, B: 16, A: 255},
color.RGBA{R: 4, G: 4, B: 4, A: 255},
color.RGBA{R: 255, G: 238, B: 238, A: 255},
color.RGBA{R: 246, G: 194, B: 194, A: 255},
color.RGBA{R: 238, G: 153, B: 153, A: 255},
color.RGBA{R: 230, G: 117, B: 117, A: 255},
color.RGBA{R: 222, G: 80, B: 80, A: 255},
color.RGBA{R: 214, G: 48, B: 48, A: 255},
color.RGBA{R: 210, G: 16, B: 16, A: 255},
color.RGBA{R: 190, G: 12, B: 12, A: 255},
color.RGBA{R: 170, G: 8, B: 8, A: 255},
color.RGBA{R: 153, G: 4, B: 4, A: 255},
color.RGBA{R: 133, G: 4, B: 4, A: 255},
color.RGBA{R: 113, G: 0, B: 0, A: 255},
color.RGBA{R: 97, G: 0, B: 0, A: 255},
color.RGBA{R: 76, G: 0, B: 0, A: 255},
color.RGBA{R: 56, G: 0, B: 0, A: 255},
color.RGBA{R: 40, G: 0, B: 0, A: 255},
color.RGBA{R: 255, G: 255, B: 250, A: 255},
color.RGBA{R: 255, G: 255, B: 165, A: 255},
color.RGBA{R: 255, G: 255, B: 80, A: 255},
color.RGBA{R: 255, G: 255, B: 0, A: 255},
color.RGBA{R: 234, G: 222, B: 12, A: 255},
color.RGBA{R: 214, G: 194, B: 28, A: 255},
color.RGBA{R: 194, G: 170, B: 40, A: 255},
color.RGBA{R: 174, G: 149, B: 52, A: 255},
color.RGBA{R: 255, G: 202, B: 153, A: 255},
color.RGBA{R: 255, G: 170, B: 101, A: 255},
color.RGBA{R: 255, G: 133, B: 48, A: 255},
color.RGBA{R: 255, G: 93, B: 0, A: 255},
color.RGBA{R: 222, G: 89, B: 12, A: 255},
color.RGBA{R: 190, G: 85, B: 28, A: 255},
color.RGBA{R: 157, G: 76, B: 36, A: 255},
color.RGBA{R: 129, G: 68, B: 40, A: 255},
color.RGBA{R: 198, G: 255, B: 206, A: 255},
color.RGBA{R: 170, G: 238, B: 182, A: 255},
color.RGBA{R: 149, G: 222, B: 157, A: 255},
color.RGBA{R: 125, G: 206, B: 137, A: 255},
color.RGBA{R: 105, G: 190, B: 113, A: 255},
color.RGBA{R: 89, G: 174, B: 97, A: 255},
color.RGBA{R: 72, G: 157, B: 76, A: 255},
color.RGBA{R: 56, G: 141, B: 64, A: 255},
color.RGBA{R: 40, G: 125, B: 48, A: 255},
color.RGBA{R: 32, G: 109, B: 36, A: 255},
color.RGBA{R: 20, G: 93, B: 24, A: 255},
color.RGBA{R: 12, G: 76, B: 16, A: 255},
color.RGBA{R: 4, G: 60, B: 8, A: 255},
color.RGBA{R: 0, G: 44, B: 4, A: 255},
color.RGBA{R: 0, G: 32, B: 0, A: 255},
color.RGBA{R: 0, G: 16, B: 0, A: 255},
color.RGBA{R: 210, G: 255, B: 255, A: 255},
color.RGBA{R: 182, G: 238, B: 234, A: 255},
color.RGBA{R: 157, G: 222, B: 218, A: 255},
color.RGBA{R: 137, G: 210, B: 202, A: 255},
color.RGBA{R: 113, G: 194, B: 186, A: 255},
color.RGBA{R: 97, G: 178, B: 165, A: 255},
color.RGBA{R: 76, G: 165, B: 149, A: 255},
color.RGBA{R: 64, G: 149, B: 129, A: 255},
color.RGBA{R: 48, G: 133, B: 113, A: 255},
color.RGBA{R: 36, G: 121, B: 97, A: 255},
color.RGBA{R: 24, G: 105, B: 80, A: 255},
color.RGBA{R: 16, G: 89, B: 64, A: 255},
color.RGBA{R: 8, G: 76, B: 52, A: 255},
color.RGBA{R: 4, G: 60, B: 36, A: 255},
color.RGBA{R: 0, G: 44, B: 24, A: 255},
color.RGBA{R: 0, G: 32, B: 16, A: 255},
color.RGBA{R: 255, G: 255, B: 170, A: 255},
color.RGBA{R: 238, G: 238, B: 149, A: 255},
color.RGBA{R: 222, G: 222, B: 129, A: 255},
color.RGBA{R: 206, G: 210, B: 109, A: 255},
color.RGBA{R: 190, G: 194, B: 93, A: 255},
color.RGBA{R: 174, G: 182, B: 76, A: 255},
color.RGBA{R: 157, G: 165, B: 64, A: 255},
color.RGBA{R: 141, G: 149, B: 52, A: 255},
color.RGBA{R: 125, G: 137, B: 40, A: 255},
color.RGBA{R: 109, G: 121, B: 28, A: 255},
color.RGBA{R: 97, G: 109, B: 20, A: 255},
color.RGBA{R: 80, G: 93, B: 12, A: 255},
color.RGBA{R: 64, G: 76, B: 4, A: 255},
color.RGBA{R: 52, G: 64, B: 4, A: 255},
color.RGBA{R: 40, G: 48, B: 0, A: 255},
color.RGBA{R: 28, G: 36, B: 0, A: 255},
color.RGBA{R: 214, G: 255, B: 214, A: 255},
color.RGBA{R: 157, G: 255, B: 157, A: 255},
color.RGBA{R: 105, G: 255, B: 105, A: 255},
color.RGBA{R: 48, G: 255, B: 48, A: 255},
color.RGBA{R: 0, G: 255, B: 0, A: 255},
color.RGBA{R: 24, G: 210, B: 28, A: 255},
color.RGBA{R: 44, G: 165, B: 48, A: 255},
color.RGBA{R: 52, G: 125, B: 56, A: 255},
color.RGBA{R: 226, G: 121, B: 0, A: 255},
color.RGBA{R: 198, G: 89, B: 0, A: 255},
color.RGBA{R: 174, G: 64, B: 0, A: 255},
color.RGBA{R: 145, G: 40, B: 0, A: 255},
color.RGBA{R: 121, G: 24, B: 0, A: 255},
color.RGBA{R: 93, G: 8, B: 0, A: 255},
color.RGBA{R: 64, G: 4, B: 0, A: 255},
color.RGBA{R: 40, G: 0, B: 0, A: 255},
color.RGBA{R: 194, G: 255, B: 255, A: 255},
color.RGBA{R: 165, G: 255, B: 255, A: 255},
color.RGBA{R: 109, G: 255, B: 255, A: 255},
color.RGBA{R: 0, G: 255, B: 255, A: 255},
color.RGBA{R: 4, G: 222, B: 222, A: 255},
color.RGBA{R: 16, G: 194, B: 194, A: 255},
color.RGBA{R: 20, G: 161, B: 161, A: 255},
color.RGBA{R: 24, G: 133, B: 133, A: 255},
color.RGBA{R: 137, G: 157, B: 255, A: 255},
color.RGBA{R: 101, G: 121, B: 255, A: 255},
color.RGBA{R: 64, G: 80, B: 255, A: 255},
color.RGBA{R: 28, G: 40, B: 255, A: 255},
color.RGBA{R: 0, G: 0, B: 255, A: 255},
color.RGBA{R: 12, G: 12, B: 206, A: 255},
color.RGBA{R: 20, G: 20, B: 157, A: 255},
color.RGBA{R: 24, G: 24, B: 109, A: 255},
color.RGBA{R: 234, G: 234, B: 255, A: 255},
color.RGBA{R: 202, G: 202, B: 238, A: 255},
color.RGBA{R: 178, G: 178, B: 222, A: 255},
color.RGBA{R: 153, G: 153, B: 206, A: 255},
color.RGBA{R: 129, G: 129, B: 194, A: 255},
color.RGBA{R: 105, G: 105, B: 178, A: 255},
color.RGBA{R: 89, G: 89, B: 161, A: 255},
color.RGBA{R: 68, G: 68, B: 145, A: 255},
color.RGBA{R: 52, G: 52, B: 133, A: 255},
color.RGBA{R: 40, G: 40, B: 117, A: 255},
color.RGBA{R: 28, G: 28, B: 101, A: 255},
color.RGBA{R: 16, G: 16, B: 89, A: 255},
color.RGBA{R: 8, G: 8, B: 72, A: 255},
color.RGBA{R: 4, G: 4, B: 56, A: 255},
color.RGBA{R: 0, G: 0, B: 40, A: 255},
color.RGBA{R: 0, G: 0, B: 28, A: 255},
color.RGBA{R: 250, G: 218, B: 170, A: 255},
color.RGBA{R: 234, G: 198, B: 145, A: 255},
color.RGBA{R: 218, G: 182, B: 129, A: 255},
color.RGBA{R: 202, G: 165, B: 109, A: 255},
color.RGBA{R: 190, G: 149, B: 93, A: 255},
color.RGBA{R: 174, G: 133, B: 76, A: 255},
color.RGBA{R: 157, G: 117, B: 60, A: 255},
color.RGBA{R: 141, G: 105, B: 48, A: 255},
color.RGBA{R: 129, G: 93, B: 36, A: 255},
color.RGBA{R: 113, G: 76, B: 28, A: 255},
color.RGBA{R: 97, G: 64, B: 16, A: 255},
color.RGBA{R: 85, G: 52, B: 12, A: 255},
color.RGBA{R: 68, G: 40, B: 4, A: 255},
color.RGBA{R: 52, G: 32, B: 0, A: 255},
color.RGBA{R: 36, G: 20, B: 0, A: 255},
color.RGBA{R: 24, G: 12, B: 0, A: 255},
color.RGBA{R: 255, G: 230, B: 186, A: 255},
color.RGBA{R: 238, G: 210, B: 161, A: 255},
color.RGBA{R: 226, G: 190, B: 141, A: 255},
color.RGBA{R: 214, G: 170, B: 121, A: 255},
color.RGBA{R: 202, G: 149, B: 105, A: 255},
color.RGBA{R: 186, G: 133, B: 89, A: 255},
color.RGBA{R: 174, G: 113, B: 72, A: 255},
color.RGBA{R: 161, G: 93, B: 60, A: 255},
color.RGBA{R: 145, G: 76, B: 48, A: 255},
color.RGBA{R: 133, G: 60, B: 36, A: 255},
color.RGBA{R: 121, G: 44, B: 24, A: 255},
color.RGBA{R: 109, G: 32, B: 16, A: 255},
color.RGBA{R: 93, G: 20, B: 8, A: 255},
color.RGBA{R: 80, G: 8, B: 4, A: 255},
color.RGBA{R: 68, G: 4, B: 0, A: 255},
color.RGBA{R: 56, G: 0, B: 0, A: 255},
color.RGBA{R: 218, G: 218, B: 198, A: 255},
color.RGBA{R: 194, G: 194, B: 170, A: 255},
color.RGBA{R: 170, G: 170, B: 145, A: 255},
color.RGBA{R: 145, G: 145, B: 121, A: 255},
color.RGBA{R: 125, G: 125, B: 101, A: 255},
color.RGBA{R: 101, G: 101, B: 76, A: 255},
color.RGBA{R: 76, G: 76, B: 56, A: 255},
color.RGBA{R: 56, G: 56, B: 40, A: 255},
color.RGBA{R: 246, G: 222, B: 206, A: 255},
color.RGBA{R: 234, G: 206, B: 190, A: 255},
color.RGBA{R: 226, G: 194, B: 170, A: 255},
color.RGBA{R: 214, G: 178, B: 153, A: 255},
color.RGBA{R: 206, G: 165, B: 141, A: 255},
color.RGBA{R: 194, G: 153, B: 125, A: 255},
color.RGBA{R: 186, G: 141, B: 113, A: 255},
color.RGBA{R: 174, G: 129, B: 101, A: 255},
color.RGBA{R: 165, G: 117, B: 89, A: 255},
color.RGBA{R: 153, G: 109, B: 76, A: 255},
color.RGBA{R: 145, G: 97, B: 64, A: 255},
color.RGBA{R: 133, G: 89, B: 56, A: 255},
color.RGBA{R: 125, G: 76, B: 48, A: 255},
color.RGBA{R: 113, G: 68, B: 36, A: 255},
color.RGBA{R: 105, G: 60, B: 32, A: 255},
color.RGBA{R: 93, G: 52, B: 24, A: 255},
color.RGBA{R: 85, G: 44, B: 16, A: 255},
color.RGBA{R: 72, G: 36, B: 12, A: 255},
color.RGBA{R: 64, G: 32, B: 8, A: 255},
color.RGBA{R: 56, G: 24, B: 4, A: 255},
color.RGBA{R: 44, G: 16, B: 0, A: 255},
color.RGBA{R: 36, G: 12, B: 0, A: 255},
color.RGBA{R: 24, G: 8, B: 0, A: 255},
color.RGBA{R: 16, G: 4, B: 0, A: 255},
color.RGBA{R: 255, G: 255, B: 234, A: 255},
color.RGBA{R: 255, G: 250, B: 198, A: 255},
color.RGBA{R: 255, G: 242, B: 165, A: 255},
color.RGBA{R: 255, G: 234, B: 129, A: 255},
color.RGBA{R: 255, G: 218, B: 97, A: 255},
color.RGBA{R: 255, G: 202, B: 60, A: 255},
color.RGBA{R: 255, G: 182, B: 28, A: 255},
color.RGBA{R: 255, G: 157, B: 0, A: 255},
color.RGBA{R: 222, G: 222, B: 234, A: 255},
color.RGBA{R: 202, G: 202, B: 218, A: 255},
color.RGBA{R: 182, G: 182, B: 202, A: 255},
color.RGBA{R: 165, G: 165, B: 186, A: 255},
color.RGBA{R: 149, G: 149, B: 170, A: 255},
color.RGBA{R: 133, G: 133, B: 157, A: 255},
color.RGBA{R: 117, G: 117, B: 141, A: 255},
color.RGBA{R: 101, G: 101, B: 125, A: 255},
color.RGBA{R: 85, G: 85, B: 109, A: 255},
color.RGBA{R: 72, G: 72, B: 97, A: 255},
color.RGBA{R: 60, G: 60, B: 80, A: 255},
color.RGBA{R: 44, G: 44, B: 64, A: 255},
color.RGBA{R: 32, G: 32, B: 48, A: 255},
color.RGBA{R: 20, G: 20, B: 32, A: 255},
color.RGBA{R: 12, G: 12, B: 24, A: 255},
color.RGBA{R: 0, G: 0, B: 0, A: 255},
color.RGBA{R: 0, G: 0, B: 0, A: 255},
color.RGBA{R: 0, G: 0, B: 0, A: 255},
color.RGBA{R: 0, G: 0, B: 0, A: 255},
color.RGBA{R: 0, G: 0, B: 0, A: 255},
color.RGBA{R: 0, G: 0, B: 0, A: 255},
color.RGBA{R: 0, G: 0, B: 0, A: 255},
color.RGBA{R: 0, G: 0, B: 0, A: 255},
color.RGBA{R: 0, G: 0, B: 0, A: 255},
}
)
func init() {
Palettes["WagesOfWar"] = WagesOfWarPalette
}

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 {
@@ -22,16 +22,21 @@ type IsoPt struct {
func (s *Scenario) Update(screenX, screenY int) error {
s.tick += 1
x, y := ebiten.CursorPosition()
geo := s.geoForCam()
geo.Translate(cellWidthHalf, 0)
geo.Scale(s.Zoom, s.Zoom)
geo.Invert()
cX, cY := ebiten.CursorPosition()
x, y := geo.Apply(float64(cX), float64(cY))
screenPos := CartPt{
X: float64(s.Viewpoint.X + x),
Y: float64(s.Viewpoint.Y + y),
X: x,
Y: y,
}
s.selectedCell = screenPos.ToISO()
// TODO: zoom support will need a camera
// FIXME: adjust for Z level
s.highlightedCell = screenPos.ToISO()
return nil
}
@@ -41,17 +46,18 @@ func (s *Scenario) Draw(screen *ebiten.Image) error {
// http://www.java-gaming.org/index.php?topic=24922.0
// https://stackoverflow.com/questions/892811/drawing-isometric-game-worlds
// https://gamedev.stackexchange.com/questions/25896/how-do-i-find-which-isometric-tiles-are-inside-the-cameras-current-view
// FIXME: we don't cope with zoom very neatly here
sw, sh := screen.Size()
topLeft := CartPt{
X: float64(s.Viewpoint.X - 2*cellWidth), // Ensure all visible cells are rendered
Y: float64(s.Viewpoint.Y - 2*cellHeight),
X: float64(s.Viewpoint.X) - (2 * cellWidth / s.Zoom), // Ensure all visible cells are rendered
Y: float64(s.Viewpoint.Y) - (2 * cellHeight / s.Zoom),
}.ToISO()
bottomRight := CartPt{
X: float64(s.Viewpoint.X + sw + 2*cellHeight),
Y: float64(s.Viewpoint.Y + sh + 5*cellHeight), // Z dimension requires it
X: float64(s.Viewpoint.X) + (float64(sw) / s.Zoom) + (2 * cellHeight / s.Zoom),
Y: float64(s.Viewpoint.Y) + (float64(sh) / s.Zoom) + (5 * cellHeight / s.Zoom), // Z dimension requires it
}.ToISO()
// X+Y is constant for all tiles in a column
@@ -89,41 +95,63 @@ 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)
if err != nil {
return err
}
op := ebiten.DrawImageOptions{}
op.GeoM = s.geoForCoords(int(s.selectedCell.X), int(s.selectedCell.Y), 0)
op.GeoM.Translate(-cellWidthHalf, -cellHeightHalf)
geo := s.geoForCoords(int(s.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)
sx, sy := op.GeoM.Apply(0, 0)
ebitenutil.DebugPrintAt(screen, fmt.Sprintf("(%.0f,%.0f)", s.selectedCell.X, s.selectedCell.Y), int(sx), int(sy))
x1, y1 := geo.Apply(0, 0)
ebitenutil.DebugPrintAt(
screen,
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
*/
return nil
}
func (s *Scenario) geoForCoords(x, y, z int) ebiten.GeoM {
func (s *Scenario) geoForCam() ebiten.GeoM {
geo := ebiten.GeoM{}
geo.Translate(-float64(s.Viewpoint.X), -float64(s.Viewpoint.Y))
return geo
}
func (s *Scenario) geoForCoords(x, y, z int) ebiten.GeoM {
geo := s.geoForCam()
pix := IsoPt{X: float64(x), Y: float64(y)}.ToCart()
geo.Translate(pix.X, pix.Y)
@@ -134,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
@@ -149,28 +177,26 @@ 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))
if err := screen.DrawImage(spr.Image, &op); err != nil {
return err
}
// Zoom has to come last
op.GeoM.Scale(s.Zoom, s.Zoom)
screen.DrawImage(spr.Image, &op)
}
return nil
}
const (
cellWidth = 128
cellHeight = 64
cellWidth = 128.0
cellHeight = 63.0
cellWidthHalf = cellWidth / 2
cellHeightHalf = cellHeight / 2
cellWidthHalf = cellWidth / 2.0
cellHeightHalf = cellHeight / 2.0
)
func (p CartPt) ToISO() IsoPt {
@@ -186,28 +212,3 @@ func (p IsoPt) ToCart() CartPt {
Y: (p.X + p.Y) * cellHeightHalf,
}
}
/*
// Doesn't take the camera or Z level into account
func cellToPix(pt image.Point) image.Point {
return image.Pt(
(pt.X-pt.Y)*cellWidthHalf,
(pt.X+pt.Y)*cellHeightHalf,
)
}
// Doesn't take the camera or Z level into account
func pixToCell(pt image.Point) image.Point {
fX := pt.X
fY := pt.Y
return image.Pt(
// (pt.X / cellWidthHalf + pt.Y / cellHeightHalf) / 2,
// (pt.Y / cellHeightHalf - (pt.Y / cellWidthHalf)) / 2,
// int(fY/cellHeight+fX/(cellWidth*2)),
// int(fY/cellHeight-fX/(cellWidth*2)),
//int((fY / cellHeight) + (fX / cellWidth)),
//int((-fX / cellWidth) + (fY / cellHeight)),
)
}*/

View File

@@ -1,6 +1,8 @@
package scenario
import (
"log"
"code.ur.gs/lupine/ordoor/internal/maps"
)
@@ -9,7 +11,35 @@ 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) {
newZ := s.ZIdx + by
if newZ < 0 {
newZ = 0
}
if newZ > 6 {
newZ = 6
}
s.ZIdx = newZ
}

View File

@@ -2,25 +2,28 @@
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?
// Or have a separater Drawer for the Scenario?
Viewpoint image.Point // Top-left of the screen
ZIdx int // Currently-viewed Z index
Zoom float64 // Zoom level to set
}
func NewScenario(assets *assetstore.AssetStore, name string) (*Scenario, error) {
@@ -35,14 +38,15 @@ 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,
specials: specials,
Viewpoint: image.Pt(0, 3000), // FIXME: haxxx
Zoom: 1.0,
}
return out, nil

View File

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

View File

@@ -13,6 +13,8 @@ import (
type button struct {
locator string
rect image.Rectangle
baseSpr *assetstore.Sprite
clickSpr *assetstore.Sprite
frozenSpr *assetstore.Sprite
@@ -37,6 +39,7 @@ func (d *Driver) buildButton(p *menus.Properties) (*button, *Widget, error) {
btn := &button{
locator: p.Locator,
rect: sprites[0].Rect.Add(p.Point()),
baseSpr: sprites[0],
clickSpr: sprites[1],
frozenSpr: sprites[2],
@@ -44,6 +47,8 @@ func (d *Driver) buildButton(p *menus.Properties) (*button, *Widget, error) {
}
widget := &Widget{
Locator: p.Locator,
Active: p.Active,
ownClickables: []clickable{btn},
ownFreezables: []freezable{btn},
ownHoverables: []hoverable{btn},
@@ -68,6 +73,7 @@ func (d *Driver) buildMainButton(p *menus.Properties) (*mainButton, *Widget, err
hoverAnim: animation(hovers),
button: button{
locator: p.Locator,
rect: sprites[0].Rect.Add(p.Point()),
baseSpr: sprites[0],
clickSpr: sprites[1],
frozenSpr: sprites[2],
@@ -76,6 +82,8 @@ func (d *Driver) buildMainButton(p *menus.Properties) (*mainButton, *Widget, err
}
widget := &Widget{
Locator: p.Locator,
Active: p.Active,
ownClickables: []clickable{btn},
ownFreezables: []freezable{btn},
ownHoverables: []hoverable{btn},
@@ -93,6 +101,7 @@ func (d *Driver) buildDoorHotspot(p *menus.Properties) (*button, *Widget, error)
btn := &button{
locator: p.Locator,
rect: sprites[0].Rect.Add(p.Point()),
baseSpr: sprites[0],
clickSpr: sprites[1],
frozenSpr: sprites[0], // No disabled sprite
@@ -100,6 +109,8 @@ func (d *Driver) buildDoorHotspot(p *menus.Properties) (*button, *Widget, error)
}
widget := &Widget{
Locator: p.Locator,
Active: p.Active,
ownClickables: []clickable{btn},
ownFreezables: []freezable{btn},
ownHoverables: []hoverable{btn},
@@ -115,7 +126,7 @@ func (b *button) id() string {
}
func (b *button) bounds() image.Rectangle {
return b.baseSpr.Rect
return b.rect
}
func (b *button) mouseDownState() bool {

View File

@@ -14,11 +14,16 @@ func (d *Driver) Dialogues() []string {
return out
}
func (d *Driver) IsInDialogue() bool {
return d.activeDialogue != nil
}
func (d *Driver) ShowDialogue(locator string) error {
for _, dialogue := range d.dialogues {
if dialogue.Locator == locator {
// FIXME: we should unhover and mouseup the non-dialogue elements
dialogue.Active = true
d.activeDialogue = dialogue
return nil
@@ -28,5 +33,9 @@ func (d *Driver) ShowDialogue(locator string) error {
}
func (d *Driver) HideDialogue() {
if d.activeDialogue != nil {
d.activeDialogue.Active = false
}
d.activeDialogue = nil
}

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)
}
}
@@ -169,11 +167,11 @@ func (d *Driver) allClickables() []clickable {
var out []clickable
for _, widget := range d.widgets {
out = append(out, widget.clickables()...)
out = append(out, widget.allClickables()...)
}
for _, widget := range d.dialogues {
out = append(out, widget.clickables()...)
out = append(out, widget.allClickables()...)
}
return out
@@ -182,11 +180,11 @@ func (d *Driver) allClickables() []clickable {
func (d *Driver) allFreezables() []freezable {
var out []freezable
for _, widget := range d.widgets {
out = append(out, widget.freezables()...)
out = append(out, widget.allFreezables()...)
}
for _, widget := range d.dialogues {
out = append(out, widget.freezables()...)
out = append(out, widget.allFreezables()...)
}
return out
@@ -196,11 +194,11 @@ func (d *Driver) allValueables() []valueable {
var out []valueable
for _, widget := range d.widgets {
out = append(out, widget.valueables()...)
out = append(out, widget.allValueables()...)
}
for _, widget := range d.dialogues {
out = append(out, widget.valueables()...)
out = append(out, widget.allValueables()...)
}
return out
@@ -208,12 +206,12 @@ func (d *Driver) allValueables() []valueable {
func (d *Driver) activeClickables() []clickable {
if d.activeDialogue != nil {
return d.activeDialogue.clickables()
return d.activeDialogue.activeClickables()
}
var out []clickable
for _, widget := range d.widgets {
out = append(out, widget.clickables()...)
out = append(out, widget.activeClickables()...)
}
return out
@@ -221,12 +219,12 @@ func (d *Driver) activeClickables() []clickable {
func (d *Driver) activeHoverables() []hoverable {
if d.activeDialogue != nil {
return d.activeDialogue.hoverables()
return d.activeDialogue.activeHoverables()
}
var out []hoverable
for _, widget := range d.widgets {
out = append(out, widget.hoverables()...)
out = append(out, widget.activeHoverables()...)
}
return out
@@ -234,12 +232,12 @@ func (d *Driver) activeHoverables() []hoverable {
func (d *Driver) activeMouseables() []mouseable {
if d.activeDialogue != nil {
return d.activeDialogue.mouseables()
return d.activeDialogue.activeMouseables()
}
var out []mouseable
for _, widget := range d.widgets {
out = append(out, widget.mouseables()...)
out = append(out, widget.activeMouseables()...)
}
return out
@@ -249,12 +247,23 @@ func (d *Driver) activePaintables() []paintable {
var out []paintable
for _, widget := range d.widgets {
out = append(out, widget.paintables()...)
out = append(out, widget.activePaintables()...)
}
if d.activeDialogue != nil {
out = append(out, d.activeDialogue.paintables()...)
out = append(out, d.activeDialogue.activePaintables()...)
}
return out
}
func (d *Driver) findWidget(locator string) *Widget {
toplevels := append(d.widgets, d.dialogues...)
for _, widget := range toplevels {
if w := widget.findWidget(locator); w != nil {
return w
}
}
return nil
}

View File

@@ -28,7 +28,10 @@ func (d *Driver) registerGroup(group *menus.Group) error {
return err
}
} else {
groupWidget = &Widget{Locator: group.Locator}
groupWidget = &Widget{
Locator: group.Locator,
Active: group.Active,
}
}
if dialogue {
@@ -75,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:
@@ -127,6 +132,7 @@ func (d *Driver) maybeBuildInventorySelect(group *menus.Group, records []*menus.
elements := make([]*inventorySelect, len(touched))
widget := &Widget{
Locator: group.Locator,
Active: group.Active,
}
for i, record := range touched {

View File

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

View File

@@ -27,6 +27,7 @@ func (d *Driver) buildInventorySelect(p *menus.Properties) (*inventorySelect, *W
element := &inventorySelect{checkbox: *c}
widget := &Widget{
Active: p.Active,
ownClickables: []clickable{element},
ownFreezables: []freezable{element},
ownHoverables: []hoverable{element},

View File

@@ -77,6 +77,7 @@ func (d *Driver) buildListBox(group *menus.Group, up, down, thumb *menus.Record,
// mostly self-registered at the moment.
widget := &Widget{
Children: []*Widget{upWidget, downWidget},
Active: group.Active, // FIXME: children have their own active state
ownPaintables: []paintable{element},
ownValueables: []valueable{element},
}
@@ -156,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
@@ -67,7 +68,7 @@ func (d *Driver) buildNoninteractive(p *menus.Properties) (*noninteractive, erro
ni := &noninteractive{
locator: p.Locator,
frames: animation{sprite.Image},
rect: sprite.Rect,
rect: sprite.Rect.Add(p.Point()),
}
return ni, nil
@@ -83,6 +84,7 @@ func (d *Driver) buildStatic(p *menus.Properties) (*noninteractive, *Widget, err
widget := &Widget{
Locator: ni.locator,
Active: p.Active,
ownClickables: []clickable{ni}, // FIXME: credits background needs to be clickable
ownHoverables: []hoverable{ni},
ownPaintables: []paintable{ni},
@@ -100,6 +102,7 @@ func (d *Driver) buildHypertext(p *menus.Properties) (*noninteractive, *Widget,
// FIXME: check if this is still needed on the bridge -> briefing transition
widget := &Widget{
Locator: ni.locator,
Active: p.Active,
ownClickables: []clickable{ni},
ownHoverables: []hoverable{ni},
}
@@ -107,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)
@@ -116,6 +146,7 @@ func (d *Driver) buildOverlay(p *menus.Properties) (*noninteractive, *Widget, er
widget := &Widget{
Locator: ni.locator,
Active: p.Active,
ownPaintables: []paintable{ni},
}
@@ -124,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)
@@ -151,10 +182,11 @@ func (d *Driver) buildAnimationSample(p *menus.Properties) (*noninteractive, *Wi
locator: p.Locator,
frames: animation(frames),
hoverImpl: hoverImpl{text: p.Text},
rect: sprite.Rect,
rect: sprite.Rect.Add(p.Point()),
}
widget := &Widget{
Active: p.Active,
ownHoverables: []hoverable{ani},
ownPaintables: []paintable{ani},
}
@@ -183,13 +215,14 @@ func (d *Driver) buildAnimationHover(p *menus.Properties) (*animationHover, *Wid
locator: p.Locator,
frames: animation(enterFrames),
hoverImpl: hoverImpl{text: p.Text},
rect: sprite.Rect,
rect: sprite.Rect.Add(p.Point()),
},
exitFrames: animation(exitFrames),
}
widget := &Widget{
Active: p.Active,
ownHoverables: []hoverable{ani},
ownPaintables: []paintable{ani},
}
@@ -248,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.
//
@@ -255,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 {
@@ -282,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

@@ -20,6 +20,8 @@ type checkbox struct {
type slider struct {
locator string
rect image.Rectangle
baseSpr *assetstore.Sprite
clickSpr *assetstore.Sprite
sliderSpr *assetstore.Sprite
@@ -42,6 +44,7 @@ func (d *Driver) buildCheckbox(p *menus.Properties) (*checkbox, *Widget, error)
checkbox := &checkbox{
button: button{
locator: p.Locator,
rect: sprites[0].Rect.Add(p.Point()),
baseSpr: sprites[0], // unchecked
clickSpr: sprites[2], // checked
frozenSpr: sprites[1], // disabled
@@ -51,6 +54,8 @@ func (d *Driver) buildCheckbox(p *menus.Properties) (*checkbox, *Widget, error)
}
widget := &Widget{
Locator: p.Locator,
Active: p.Active,
ownClickables: []clickable{checkbox},
ownFreezables: []freezable{checkbox},
ownHoverables: []hoverable{checkbox},
@@ -69,6 +74,7 @@ func (d *Driver) buildSlider(p *menus.Properties) (*slider, *Widget, error) {
slider := &slider{
locator: p.Locator,
rect: sprites[0].Rect.Add(p.Point()),
baseSpr: sprites[0],
clickSpr: sprites[1],
sliderSpr: sprites[2],
@@ -76,6 +82,8 @@ func (d *Driver) buildSlider(p *menus.Properties) (*slider, *Widget, error) {
}
widget := &Widget{
Locator: p.Locator,
Active: p.Active,
ownClickables: []clickable{slider},
ownMouseables: []mouseable{slider},
ownPaintables: []paintable{slider},
@@ -111,7 +119,7 @@ func (s *slider) id() string {
// The bounds of the slider are the whole thing
func (s *slider) bounds() image.Rectangle {
return s.baseSpr.Rect
return s.rect
}
func (s *slider) registerMouseClick() {

View File

@@ -61,6 +61,26 @@ func (d *Driver) SetFreeze(id string, value bool) error {
return fmt.Errorf("Couldn't find clickable widget %v:%v", d.menu.Name, id)
}
func (d *Driver) ToggleActive(locator string) error {
if widget := d.findWidget(locator); widget != nil {
widget.Active = !widget.Active
return nil
}
return fmt.Errorf("Couldn't find activatable widget %v to toggle", locator)
}
func (d *Driver) SetActive(locator string, value bool) error {
if widget := d.findWidget(locator); widget != nil {
widget.Active = value
return nil
}
return fmt.Errorf("Couldn't find activeatable widget %v to set to %v", locator, value)
}
func (d *Driver) OnClick(id string, f func()) error {
for _, clickable := range d.allClickables() {
if clickable.id() == d.realId(id) {

View File

@@ -3,6 +3,7 @@ package ui
type Widget struct {
Locator string
Children []*Widget
Active bool
ownClickables []clickable
ownFreezables []freezable
@@ -12,62 +13,130 @@ type Widget struct {
ownValueables []valueable
}
func (w *Widget) clickables() []clickable {
func (w *Widget) allClickables() []clickable {
out := w.ownClickables
for _, widget := range w.Children {
out = append(out, widget.clickables()...)
out = append(out, widget.allClickables()...)
}
return out
}
func (w *Widget) freezables() []freezable {
func (w *Widget) allFreezables() []freezable {
out := w.ownFreezables
for _, widget := range w.Children {
out = append(out, widget.freezables()...)
out = append(out, widget.allFreezables()...)
}
return out
}
func (w *Widget) hoverables() []hoverable {
out := w.ownHoverables
for _, widget := range w.Children {
out = append(out, widget.hoverables()...)
}
return out
}
func (w *Widget) mouseables() []mouseable {
out := w.ownMouseables
for _, widget := range w.Children {
out = append(out, widget.mouseables()...)
}
return out
}
func (w *Widget) paintables() []paintable {
out := w.ownPaintables
for _, widget := range w.Children {
out = append(out, widget.paintables()...)
}
return out
}
func (w *Widget) valueables() []valueable {
func (w *Widget) allValueables() []valueable {
out := w.ownValueables
for _, widget := range w.Children {
out = append(out, widget.valueables()...)
out = append(out, widget.allValueables()...)
}
return out
}
func (w *Widget) activeClickables() []clickable {
if !w.Active {
return nil
}
out := w.ownClickables
for _, widget := range w.Children {
out = append(out, widget.activeClickables()...)
}
return out
}
func (w *Widget) activeFreezables() []freezable {
if !w.Active {
return nil
}
out := w.ownFreezables
for _, widget := range w.Children {
out = append(out, widget.activeFreezables()...)
}
return out
}
func (w *Widget) activeHoverables() []hoverable {
if !w.Active {
return nil
}
out := w.ownHoverables
for _, widget := range w.Children {
out = append(out, widget.activeHoverables()...)
}
return out
}
func (w *Widget) activeMouseables() []mouseable {
if !w.Active {
return nil
}
out := w.ownMouseables
for _, widget := range w.Children {
out = append(out, widget.activeMouseables()...)
}
return out
}
func (w *Widget) activePaintables() []paintable {
if !w.Active {
return nil
}
out := w.ownPaintables
for _, widget := range w.Children {
out = append(out, widget.activePaintables()...)
}
return out
}
func (w *Widget) activeValueables() []valueable {
if !w.Active {
return nil
}
out := w.ownValueables
for _, widget := range w.Children {
out = append(out, widget.activeValueables()...)
}
return out
}
func (w *Widget) findWidget(locator string) *Widget {
if w.Locator == locator {
return w
}
for _, child := range w.Children {
if found := child.findWidget(locator); found != nil {
return found
}
}
return nil
}

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 {
@@ -37,6 +37,8 @@ type Window struct {
MouseWheelHandler func(float64, float64)
MouseClickHandler func()
WhileKeyDownHandlers map[ebiten.Key]func()
// Allow the "game" to be switched out at any time
game Game
@@ -51,16 +53,17 @@ 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,
Title: title,
debug: true,
firstRun: true,
game: game,
xRes: xRes,
yRes: yRes,
WhileKeyDownHandlers: make(map[ebiten.Key]func()),
KeyUpHandlers: make(map[ebiten.Key]func()),
debug: true,
firstRun: true,
game: game,
xRes: xRes,
yRes: yRes,
}, nil
}
@@ -69,6 +72,10 @@ func (w *Window) OnKeyUp(key ebiten.Key, f func()) {
w.KeyUpHandlers[key] = f
}
func (w *Window) WhileKeyDown(key ebiten.Key, f func()) {
w.WhileKeyDownHandlers[key] = f
}
func (w *Window) OnMouseWheel(f func(x, y float64)) {
w.MouseWheelHandler = f
}
@@ -100,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 {
@@ -115,11 +124,13 @@ 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
}
// Process keys
// Process keys.
// FIXME: : should this happen before or after update?
// TODO: efficient set operations
for key, cb := range w.KeyUpHandlers {
@@ -128,6 +139,12 @@ func (w *Window) Update(screen *ebiten.Image) (outErr error) {
}
}
for key, cb := range w.WhileKeyDownHandlers {
if ebiten.IsKeyPressed(key) {
cb()
}
}
if w.MouseWheelHandler != nil {
x, y := ebiten.Wheel()
if x != 0 || y != 0 {
@@ -141,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
@@ -156,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

View File

@@ -1,34 +0,0 @@
#!/usr/bin/env ruby
lines = File.read("palette.ppm").split("\n")
lines = lines.select { |l| l[0] != "#" }
raise "Not an ASCII ppm" if lines.shift != "P3"
raise "Incorrect dimensions" unless lines.shift == "1 256"
raise "Incorrect maxval" unless lines.shift == "255"
raise "Too many lines left" unless lines.size == 768
puts <<EOF
package data
import "image/color"
var (
Transparent = color.RGBA{R: 0, G: 0, B: 0, A: 0}
ColorPalette = color.Palette{
Transparent,
EOF
lines.shift(3) # Ignore idx 0
255.times do
r, g, b = lines.shift(3).map(&:to_i)
puts "\t\tcolor.RGBA{R: #{r}, G: #{g}, B: #{b}, A: 255},"
end
puts <<EOF
}
)
EOF

View File

@@ -1,12 +0,0 @@
# jungle floor
# jungtil.obj/.asn
# /--> d:\warflics\missions\jungtil.flc
#
0:DEF 2;
0:TYPE 2;
END OF FILE

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -1,772 +0,0 @@
P3
# CREATOR: GIMP PNM Filter Version 1.1
1 256
255
255
255
255
128
0
0
0
128
0
128
128
0
0
0
128
128
0
128
0
128
128
192
192
192
192
220
192
166
202
240
255
255
255
240
240
240
221
221
221
203
203
203
187
187
187
178
178
178
168
168
168
157
157
157
147
147
147
137
137
137
127
127
127
117
117
117
106
106
106
96
96
96
86
86
86
76
76
76
61
61
61
49
49
49
36
36
36
24
24
24
12
12
12
0
0
0
134
134
255
113
113
241
93
93
228
72
72
214
63
63
200
55
55
186
46
46
172
38
38
158
29
29
144
20
20
131
12
12
117
3
3
103
3
3
91
3
3
79
3
3
68
3
3
56
255
145
145
242
123
123
230
101
101
217
79
79
205
70
70
193
61
61
181
53
53
169
44
44
157
35
35
144
26
26
132
18
18
120
9
9
108
8
8
94
7
7
79
7
7
65
6
6
147
142
185
132
126
172
117
109
159
102
93
146
95
86
133
88
78
123
82
73
115
77
67
107
72
61
100
67
55
92
61
50
84
56
44
76
51
38
68
46
32
60
40
27
52
35
21
44
200
150
137
187
130
115
175
110
94
164
95
77
154
79
61
143
64
44
137
60
42
132
55
38
125
50
33
120
48
29
111
44
26
103
39
24
94
35
21
83
30
18
72
25
14
61
20
10
121
107
34
109
94
29
96
82
25
84
69
20
77
62
17
70
55
14
63
47
12
56
40
9
93
120
53
80
103
42
66
86
31
53
69
20
49
60
16
45
52
12
43
44
10
43
38
8
136
145
44
118
128
37
101
111
30
83
94
23
70
79
17
56
65
11
42
50
6
28
36
0
57
134
64
48
118
54
38
101
43
29
85
33
22
71
25
15
58
17
7
45
8
0
32
0
143
87
56
126
75
45
110
64
35
93
52
24
85
44
16
72
36
12
64
32
8
56
24
4
127
96
54
115
85
46
102
75
39
90
64
31
82
57
25
75
51
20
68
44
15
61
38
10
141
86
56
126
75
46
110
65
36
95
54
26
88
51
25
73
40
18
57
29
10
42
18
3
172
199
199
138
173
173
104
148
148
71
122
122
37
97
97
3
71
71
4
56
56
4
41
41
217
209
200
202
194
184
188
178
167
173
163
151
158
147
134
148
136
123
137
125
112
126
114
101
116
104
91
105
93
80
94
82
69
84
71
58
73
60
47
62
50
36
52
39
25
41
28
14
231
232
207
219
217
180
208
201
152
196
186
125
184
171
98
173
155
70
161
140
43
150
129
39
139
119
37
127
109
33
117
99
29
105
89
25
90
76
21
75
62
18
60
49
14
45
35
10
128
99
127
113
73
112
97
47
97
82
21
82
75
2
74
68
2
67
52
3
50
35
4
33
247
178
102
229
152
75
212
127
48
194
101
21
179
87
16
161
73
12
142
59
9
124
45
5
255
0
0
194
3
3
161
2
2
255
227
11
209
185
8
169
150
6
103
190
255
2
130
232
4
4
209
0
255
0
7
180
7
3
132
3
255
114
230
255
17
205
203
6
156
246
164
73
221
123
16
177
95
4
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
132
18
18
113
13
13
94
7
7
107
8
8
120
9
9
193
62
62
181
54
54
168
45
45
157
36
36
144
27
27
62
50
36
57
45
31
52
39
25
46
34
20
41
28
14
255
251
240
160
160
164
128
128
128
255
0
0
0
255
0
255
255
0
0
0
255
255
0
255
0
255
255
255
255
255

View File

@@ -1,13 +0,0 @@
To build this palette, I did the following:
* Created the .asn + .obj files by hand
* Referenced them in a .set file and asked WH40K_TD.exe to render the sprites
* Screenshotted the output and used GIMP to create a 1x255 image with them all
* Exported that image as an ASCII .ppm file
* Used the `generate_palette` script to output Go code \o/
Note that palette index 0 is ignored, and hardcoded to be transparent black.
This is because 0x00 seems to be used as a record separator in .obj files, so
can't be used as a palette index anyway.

Binary file not shown.

45
scripts/palette-from-pcx Executable file
View File

@@ -0,0 +1,45 @@
#!/usr/bin/env ruby
if ARGV.size != 2
puts "Usage: $0 <filename.pcx> <Palette name>"
exit 1
end
FILENAME = ARGV[0]
PALETTENAME = ARGV[1]
f = File.open(FILENAME)
# In a 256-colour PCX file, the palette is stored in the final 768 bytes as RGB
# triplets, and the byte before that should be 0x0C: https://en.wikipedia.org/wiki/PCX#Color_palette
f.seek(-769, IO::SEEK_END)
raise "Check byte is wrong" unless f.read(1) == "\x0C"
puts <<EOF
package palettes
import "image/color"
var (
#{PALETTENAME}Palette = color.Palette{
Transparent,
EOF
f.read(3) # Ignore idx 0 so we can make it transparent
255.times do
r, g, b = f.read(3).bytes
puts "\t\tcolor.RGBA{R: #{r}, G: #{g}, B: #{b}, A: 255},"
end
puts <<EOF
}
)
func init() {
Palettes["#{PALETTENAME}"] = #{PALETTENAME}Palette
}
EOF