Compare commits
73 Commits
8ce24ce5f8
...
main
Author | SHA1 | Date | |
---|---|---|---|
55c2232e08 | |||
ac4675fa2c | |||
5f7654d267 | |||
89888ce004 | |||
16767da6f1 | |||
c5b80ed8bc | |||
891edecc60 | |||
85979834c8 | |||
96dbb297cd | |||
92fa0fc5d6 | |||
c5e6abb798 | |||
5df050b4ef | |||
4d336b9189 | |||
3b7cfb6ecc | |||
7677c30572 | |||
eac6017c2c | |||
f971ba320c | |||
cf624cc77b | |||
65bae80d40 | |||
e8e9811b5d | |||
a6fdbaef2b | |||
0bf8233cd1 | |||
c2cbf1d95d | |||
54fe95239e | |||
63d3ee0ed6 | |||
5c869fc33c | |||
4358951e15 | |||
250a6033c8 | |||
f64af717b7 | |||
3866ee07a8 | |||
c1268e8d57 | |||
59baf20c35 | |||
cf58be6a20 | |||
14fdab72a0 | |||
c7a2fa80e7 | |||
def40a1ee2 | |||
48d098134e | |||
597e346869 | |||
eea5dea98a | |||
04bdf3e352 | |||
9d0750d134 | |||
1f4bfc771c | |||
c058f651dc | |||
9be93b6091 | |||
f8828c95bd | |||
903ddba2ac | |||
b191ba2a94 | |||
6e70ddcb60 | |||
1e141a2fb9 | |||
4df6be4fb1 | |||
4fe9a75d69 | |||
2b83ce4f7f | |||
b690c763bb | |||
beebfda3ba | |||
87c0aae54b | |||
32fd9f9aa9 | |||
80c65f68ca | |||
acb7882549 | |||
e2ad8f61c1 | |||
2f65cd312a | |||
26c976353f | |||
82d3849402 | |||
786d261f98 | |||
dc131939f4 | |||
76bf8438b0 | |||
5f8606377a | |||
0025daf8dd | |||
f3fea83173 | |||
bb3ddc4896 | |||
d99a5b9ec3 | |||
fd73f03aa5 | |||
df1f116b3d | |||
aa43011e8d |
18
.gitignore
vendored
18
.gitignore
vendored
@@ -1,13 +1,9 @@
|
||||
/config.toml
|
||||
/loader
|
||||
/orig
|
||||
/palette-idx
|
||||
/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
|
||||
|
46
Makefile
46
Makefile
@@ -2,36 +2,42 @@ srcfiles = Makefile go.mod $(shell find . -iname *.go)
|
||||
|
||||
GOBUILD ?= go build -tags ebitengl
|
||||
|
||||
all: loader ordoor palette-idx view-font view-obj view-map view-menu view-minimap view-set
|
||||
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-font: $(srcfiles)
|
||||
$(GOBUILD) -o view-font ./cmd/view-font
|
||||
palette-idx: bin $(srcfiles)
|
||||
$(GOBUILD) -o bin/palette-idx ./cmd/palette-idx
|
||||
|
||||
view-obj: $(srcfiles)
|
||||
$(GOBUILD) -o view-obj ./cmd/view-obj
|
||||
view-ani: bin $(srcfiles)
|
||||
$(GOBUILD) -o bin/view-ani ./cmd/view-ani
|
||||
|
||||
view-map: $(srcfiles)
|
||||
$(GOBUILD) -o view-map ./cmd/view-map
|
||||
view-font: bin $(srcfiles)
|
||||
$(GOBUILD) -o bin/view-font ./cmd/view-font
|
||||
|
||||
view-menu: $(srcfiles)
|
||||
$(GOBUILD) -o view-menu ./cmd/view-menu
|
||||
view-obj: bin $(srcfiles)
|
||||
$(GOBUILD) -o bin/view-obj ./cmd/view-obj
|
||||
|
||||
view-minimap: $(srcfiles)
|
||||
$(GOBUILD) -o view-minimap ./cmd/view-minimap
|
||||
view-map: bin $(srcfiles)
|
||||
$(GOBUILD) -o bin/view-map ./cmd/view-map
|
||||
|
||||
view-set: $(srcfiles)
|
||||
$(GOBUILD) -o view-set ./cmd/view-set
|
||||
view-menu: bin $(srcfiles)
|
||||
$(GOBUILD) -o bin/view-menu ./cmd/view-menu
|
||||
|
||||
ordoor: $(srcfiles)
|
||||
$(GOBUILD) -o ordoor ./cmd/ordoor
|
||||
view-minimap: bin $(srcfiles)
|
||||
$(GOBUILD) -o bin/view-minimap ./cmd/view-minimap
|
||||
|
||||
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-obj view-map view-minimap view-set palette-idx view-font
|
||||
rm -rf bin
|
||||
|
||||
.PHONY: all clean
|
||||
|
185
README.md
185
README.md
@@ -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:
|
||||
|
||||

|
||||
|
||||
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/)
|
||||
|
@@ -3,43 +3,62 @@ 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()
|
||||
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")
|
||||
genericDataPath := filepath.Join(dataPath, "GenericData.dat")
|
||||
aniObDefPath := filepath.Join(dataPath, "AniObDef.dat")
|
||||
genericDataPath := filepath.Join(dataPath, "GenericData.dat")
|
||||
hasActionPath := filepath.Join(dataPath, "HasAction.dat")
|
||||
i18nPath := filepath.Join(dataPath, data.I18nFile)
|
||||
|
||||
log.Printf("Loading %s...", accountingPath)
|
||||
@@ -73,11 +92,15 @@ func loadData() {
|
||||
}
|
||||
|
||||
log.Printf("%s: len=%v", i18nPath, i18n.Len())
|
||||
|
||||
ha, err := data.LoadHasAction(hasActionPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse %s: %v", hasActionPath, err)
|
||||
}
|
||||
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)
|
||||
@@ -107,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)
|
||||
@@ -117,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)
|
||||
@@ -147,38 +168,38 @@ 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)
|
||||
}
|
||||
|
||||
for _, menu := range menus {
|
||||
fmt.Printf(" * `%s`: objects=%v fonts=%v\n", menu.Name, menu.ObjectFiles, menu.FontNames)
|
||||
for _, record := range menu.Records {
|
||||
displayRecord(record, 2)
|
||||
|
||||
for _, group := range menu.Groups {
|
||||
// TODO: display group
|
||||
for _, record := range group.Records {
|
||||
displayRecord(record, 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func displayRecord(record *menus.Record, depth int) {
|
||||
content := fmt.Sprintf("id=%v type=%v sprite=%v", record.Id, record.Type, record.SpriteId)
|
||||
content := fmt.Sprintf("id=%v type=%v sprite=%v", record.ID, record.Type, record.SpriteId)
|
||||
|
||||
if !record.Active {
|
||||
content = "(" + content + ")"
|
||||
}
|
||||
|
||||
fmt.Printf("%s* %s\n", strings.Repeat(" ", depth), content)
|
||||
|
||||
for _, child := range record.Children {
|
||||
displayRecord(child, depth+1)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -189,3 +210,19 @@ func loadFonts() {
|
||||
fmt.Printf(" * `%s`: obj=%v entries=%v\n", font.Name, font.ObjectFile, font.Entries())
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
|
177
cmd/view-ani/main.go
Normal file
177
cmd/view-ani/main.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"image"
|
||||
"log"
|
||||
"math"
|
||||
"os"
|
||||
|
||||
"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 (
|
||||
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")
|
||||
|
||||
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 {
|
||||
assets *assetstore.AssetStore
|
||||
ani *assetstore.Animation
|
||||
step int
|
||||
|
||||
state state
|
||||
lastState state
|
||||
}
|
||||
|
||||
type state struct {
|
||||
groupIdx int
|
||||
recIdx int
|
||||
|
||||
zoom float64
|
||||
origin image.Point
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
if *configFile == "" {
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
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.Fatalf("Failed to set up asset store: %v", err)
|
||||
}
|
||||
|
||||
state := state{
|
||||
zoom: 6.0,
|
||||
origin: image.Point{-300, -200}, // Show them somewhat centered
|
||||
groupIdx: *groupIdx,
|
||||
recIdx: *recIdx,
|
||||
}
|
||||
|
||||
env := &env{
|
||||
assets: assets,
|
||||
state: state,
|
||||
lastState: state,
|
||||
}
|
||||
|
||||
win, err := ui.NewWindow(env, "View Animations", *winX, *winY)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
win.OnKeyUp(ebiten.KeyMinus, env.changeGroup(-1))
|
||||
win.OnKeyUp(ebiten.KeyEqual, env.changeGroup(+1))
|
||||
|
||||
win.OnKeyUp(ebiten.KeyComma, env.changeRec(-1))
|
||||
win.OnKeyUp(ebiten.KeyPeriod, env.changeRec(+1))
|
||||
|
||||
win.OnKeyUp(ebiten.KeyLeft, env.changeOrigin(-16, +0))
|
||||
win.OnKeyUp(ebiten.KeyRight, env.changeOrigin(+16, +0))
|
||||
win.OnKeyUp(ebiten.KeyUp, env.changeOrigin(+0, -16))
|
||||
win.OnKeyUp(ebiten.KeyDown, env.changeOrigin(+0, +16))
|
||||
win.OnMouseWheel(env.changeZoom)
|
||||
|
||||
// The main thread now belongs to ebiten
|
||||
if err := win.Run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
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, 0) // FIXME: why 0?
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e.ani = ani
|
||||
|
||||
log.Printf(
|
||||
"new state: group=%d rec=%d zoom=%.2f, origin=%+v",
|
||||
e.state.groupIdx,
|
||||
e.state.recIdx,
|
||||
e.state.zoom,
|
||||
e.state.origin,
|
||||
)
|
||||
}
|
||||
|
||||
// This should be the final action
|
||||
e.step += 1
|
||||
e.lastState = e.state
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *env) Draw(screen *ebiten.Image) error {
|
||||
cam := ebiten.GeoM{}
|
||||
cam.Scale(e.state.zoom, e.state.zoom) // apply current zoom factor
|
||||
cam.Translate(float64(-e.state.origin.X), float64(-e.state.origin.Y)) // Move to origin
|
||||
screen.Fill(colornames.White)
|
||||
|
||||
if len(e.ani.Frames) > 0 {
|
||||
sprite := e.ani.Frames[e.step/4%len(e.ani.Frames)]
|
||||
|
||||
screen.DrawImage(sprite.Image, &ebiten.DrawImageOptions{GeoM: cam})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *env) changeGroup(by int) func() {
|
||||
return func() {
|
||||
e.state.groupIdx += by
|
||||
|
||||
if e.state.groupIdx < 0 {
|
||||
e.state.groupIdx = 0
|
||||
}
|
||||
|
||||
// Reset the record if the group changes
|
||||
e.state.recIdx = 0
|
||||
|
||||
// TODO: upper bounds checking
|
||||
}
|
||||
}
|
||||
|
||||
func (e *env) changeRec(by int) func() {
|
||||
return func() {
|
||||
e.state.recIdx += by
|
||||
|
||||
if e.state.recIdx < 0 {
|
||||
e.state.recIdx = 0
|
||||
}
|
||||
|
||||
// TODO: upper bounds checking
|
||||
}
|
||||
}
|
||||
|
||||
func (e *env) changeOrigin(byX, byY int) func() {
|
||||
return func() {
|
||||
e.state.origin.X += byX
|
||||
e.state.origin.Y += byY
|
||||
}
|
||||
}
|
||||
|
||||
func (e *env) changeZoom(_, y float64) {
|
||||
// Zoom in and out with the mouse wheel
|
||||
e.state.zoom *= math.Pow(1.2, y)
|
||||
}
|
@@ -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()
|
||||
}
|
||||
|
@@ -2,91 +2,90 @@ package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"image"
|
||||
"log"
|
||||
"math"
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
"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 {
|
||||
assets *assetstore.AssetStore
|
||||
area *assetstore.Map
|
||||
|
||||
step int
|
||||
state state
|
||||
lastState state
|
||||
}
|
||||
|
||||
type state struct {
|
||||
zoom float64
|
||||
origin image.Point
|
||||
zIdx int
|
||||
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)
|
||||
}
|
||||
|
||||
area, err := assets.Map(*gameMap)
|
||||
assets, err := assetstore.New(cfg.DefaultEngine())
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load map %v: %v", *gameMap, err)
|
||||
log.Fatalf("Failed to scan root directory: %v", err)
|
||||
}
|
||||
|
||||
// Eager load sprites
|
||||
if err := area.LoadSprites(); err != nil {
|
||||
log.Fatal("Eager-loading sprites failed: %v", err)
|
||||
}
|
||||
|
||||
state := state{
|
||||
zoom: 1.0,
|
||||
origin: image.Point{0, 3000}, // FIXME: haxxx
|
||||
zIdx: 1,
|
||||
}
|
||||
env := &env{
|
||||
area: area,
|
||||
assets: assets,
|
||||
state: state,
|
||||
lastState: state,
|
||||
}
|
||||
|
||||
win, err := ui.NewWindow(env, "View Map "+*gameMap, *winX, *winY)
|
||||
scenario, err := scenario.NewScenario(assets, *gameMap)
|
||||
if err != nil {
|
||||
log.Fatal("Couldn't create window: %v", err)
|
||||
log.Fatalf("Failed to load scenario %v: %v", *gameMap, err)
|
||||
}
|
||||
|
||||
// TODO: click to view cell data
|
||||
var realEnv *env
|
||||
if cfg.DefaultEngineName == "ordoor" {
|
||||
ship := &ship.Ship{}
|
||||
|
||||
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))
|
||||
win.OnMouseWheel(env.changeZoom)
|
||||
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}
|
||||
}
|
||||
|
||||
for i := 0; i < 6; i++ {
|
||||
win.OnKeyUp(ebiten.Key1+ebiten.Key(i), env.setZIdx(i+1))
|
||||
win, err := ui.NewWindow(realEnv, "View Map "+*gameMap, *winX, *winY)
|
||||
if err != nil {
|
||||
log.Fatalf("Couldn't create window: %v", err)
|
||||
}
|
||||
|
||||
for i := 0; i <= 6; i++ {
|
||||
win.OnKeyUp(ebiten.Key1+ebiten.Key(i), realEnv.setZIdx(i))
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -95,150 +94,45 @@ func main() {
|
||||
}
|
||||
|
||||
func (e *env) Update(screenX, screenY int) error {
|
||||
if e.step == 0 || e.lastState != e.state {
|
||||
log.Printf("zoom=%.2f zIdx=%v camPos=%#v", e.state.zoom, e.state.zIdx, e.state.origin)
|
||||
if e.flow != nil {
|
||||
return e.flow.Update(screenX, screenY)
|
||||
} else {
|
||||
return e.scenario.Update(screenX, screenY)
|
||||
}
|
||||
|
||||
e.lastState = e.state
|
||||
e.step += 1
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *env) Draw(screen *ebiten.Image) error {
|
||||
// Bounds clipping
|
||||
// http://www.java-gaming.org/index.php?topic=24922.0
|
||||
// https://stackoverflow.com/questions/892811/drawing-isometric-game-worlds
|
||||
// https://gamedev.stackexchange.com/questions/25896/how-do-i-find-which-isometric-tiles-are-inside-the-cameras-current-view
|
||||
|
||||
sw, sh := screen.Size()
|
||||
|
||||
topLeft := pixToCell(e.state.origin)
|
||||
topLeft.X -= 5 // Ensure we paint to every visible section of the screeen.
|
||||
topLeft.X -= 5 // FIXME: haxxx
|
||||
|
||||
bottomRight := pixToCell(image.Pt(e.state.origin.X+sw, e.state.origin.Y+sh))
|
||||
bottomRight.X += 5
|
||||
bottomRight.Y += 5
|
||||
|
||||
// X+Y is constant for all tiles in a column
|
||||
// X-Y is constant for all tiles in a row
|
||||
// However, the drawing order is odd unless we reorder explicitly.
|
||||
toDraw := []image.Point{}
|
||||
for a := topLeft.X + topLeft.Y; a <= bottomRight.X+bottomRight.Y; a++ {
|
||||
for b := topLeft.X - topLeft.Y; b <= bottomRight.X-bottomRight.Y; b++ {
|
||||
if b&1 != a&1 {
|
||||
continue
|
||||
}
|
||||
|
||||
pt := image.Pt((a+b)/2, (a-b)/2)
|
||||
|
||||
if !pt.In(e.area.Rect) {
|
||||
continue
|
||||
}
|
||||
toDraw = append(toDraw, pt)
|
||||
}
|
||||
if e.flow != nil {
|
||||
return e.flow.Draw(screen)
|
||||
} else {
|
||||
return e.scenario.Draw(screen)
|
||||
}
|
||||
|
||||
sort.Slice(toDraw, func(i, j int) bool {
|
||||
iPix := cellToPix(toDraw[i])
|
||||
jPix := cellToPix(toDraw[j])
|
||||
|
||||
if iPix.Y < jPix.Y {
|
||||
return true
|
||||
}
|
||||
|
||||
if iPix.Y == jPix.Y {
|
||||
return iPix.X < jPix.X
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
counter := map[string]int{}
|
||||
for _, pt := range toDraw {
|
||||
for z := 0; z <= e.state.zIdx; z++ {
|
||||
if err := e.renderCell(pt.X, pt.Y, z, screen, counter); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//log.Printf("%#+v", counter)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *env) renderCell(x, y, z int, screen *ebiten.Image, counter map[string]int) error {
|
||||
sprites, err := e.area.SpritesForCell(x, y, z)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
iso := ebiten.GeoM{}
|
||||
iso.Translate(-float64(e.state.origin.X), -float64(e.state.origin.Y))
|
||||
|
||||
pix := cellToPix(image.Pt(x, y))
|
||||
iso.Translate(float64(pix.X), float64(pix.Y))
|
||||
|
||||
// Taking the Z index away *seems* to draw the object in the correct place.
|
||||
// FIXME: There are some artifacts, investigate more
|
||||
iso.Translate(0.0, -float64(z*48.0)) // offset for Z index
|
||||
|
||||
// TODO: iso.Scale(e.state.zoom, e.state.zoom) // apply current zoom factor
|
||||
|
||||
for _, spr := range sprites {
|
||||
// if _, ok := counter[spr.ID]; !ok {
|
||||
// counter[spr.ID] = 0
|
||||
// }
|
||||
// counter[spr.ID] = counter[spr.ID] + 1
|
||||
|
||||
iso.Translate(float64(spr.XOffset), float64(spr.YOffset))
|
||||
|
||||
if err := screen.DrawImage(spr.Image, &ebiten.DrawImageOptions{GeoM: iso}); err != nil {
|
||||
return err
|
||||
}
|
||||
iso.Translate(float64(-spr.XOffset), float64(-spr.YOffset))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *env) changeOrigin(byX, byY int) func() {
|
||||
return func() {
|
||||
e.state.origin.X += byX
|
||||
e.state.origin.Y += byY
|
||||
e.scenario.Viewpoint.X += byX
|
||||
e.scenario.Viewpoint.Y += byY
|
||||
}
|
||||
}
|
||||
|
||||
func (e *env) changeZoom(_, y float64) {
|
||||
// Zoom in and out with the mouse wheel
|
||||
e.state.zoom *= math.Pow(1.2, y)
|
||||
func (e *env) changeZoom(_, byY float64) {
|
||||
e.scenario.Zoom *= math.Pow(1.2, byY)
|
||||
}
|
||||
|
||||
func (e *env) setZIdx(to int) func() {
|
||||
return func() {
|
||||
e.state.zIdx = to
|
||||
e.scenario.ZIdx = to
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
cellWidth = 64
|
||||
cellHeight = 64
|
||||
)
|
||||
func (e *env) showCellData() {
|
||||
screenX, screenY := ebiten.CursorPosition()
|
||||
viewX, viewY := e.scenario.Viewpoint.X+screenX, e.scenario.Viewpoint.Y+screenY
|
||||
|
||||
// Doesn't take the camera or Z level into account
|
||||
func cellToPix(pt image.Point) image.Point {
|
||||
return image.Pt(
|
||||
(pt.X-pt.Y)*cellWidth,
|
||||
(pt.X+pt.Y)*cellHeight/2,
|
||||
)
|
||||
}
|
||||
log.Printf("Click registered at (%d,%d) screen, (%d,%d) virtual", screenX, screenY, viewX, viewY)
|
||||
|
||||
// Doesn't take the camera or Z level into account
|
||||
func pixToCell(pt image.Point) image.Point {
|
||||
return image.Pt(
|
||||
pt.Y/cellHeight+pt.X/(cellWidth*2),
|
||||
pt.Y/cellHeight-pt.X/(cellWidth*2),
|
||||
)
|
||||
cell, pos := e.scenario.CellAtCursor()
|
||||
log.Printf("Viewpoint: %#+v z=%v", e.scenario.Viewpoint, e.scenario.ZIdx)
|
||||
log.Printf("Cell under cursor: (%.2f,%.2f,%d): %#+v", pos.X, pos.Y, pos.Z, cell)
|
||||
}
|
||||
|
@@ -5,27 +5,43 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"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")
|
||||
winY = flag.Int("win-y", 1024, "Pre-scaled window Y dimension")
|
||||
)
|
||||
|
||||
type dlg struct {
|
||||
driver *ui.Driver
|
||||
list []string
|
||||
pos int
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -35,17 +51,48 @@ func main() {
|
||||
log.Fatalf("Couldn't load menu %s: %v", *menuName, err)
|
||||
}
|
||||
|
||||
driver, err := ui.NewDriver(menu)
|
||||
driver, err := ui.NewDriver(assets, menu)
|
||||
if err != nil {
|
||||
log.Fatalf("Couldn't initialize interface: %v", err)
|
||||
}
|
||||
|
||||
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
|
||||
dialogues := driver.Dialogues()
|
||||
if len(dialogues) > 0 {
|
||||
dlg := &dlg{
|
||||
driver: driver,
|
||||
list: dialogues,
|
||||
}
|
||||
win.OnKeyUp(ebiten.KeyLeft, dlg.changeDialogue(-1))
|
||||
win.OnKeyUp(ebiten.KeyRight, dlg.changeDialogue(+1))
|
||||
for i, dialogue := range dlg.list {
|
||||
log.Printf("Dialogue %v: %v", i, dialogue)
|
||||
}
|
||||
}
|
||||
|
||||
if err := win.Run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dlg) changeDialogue(by int) func() {
|
||||
return func() {
|
||||
newPos := d.pos + by
|
||||
if newPos < 0 || newPos > len(d.list)-1 {
|
||||
log.Printf("Hiding dialogue %v: %q", d.pos, d.list[d.pos])
|
||||
d.driver.HideDialogue()
|
||||
return
|
||||
}
|
||||
|
||||
locator := d.list[newPos]
|
||||
log.Printf("Showing dialogue %v: %q", newPos, locator)
|
||||
|
||||
d.driver.ShowDialogue(locator)
|
||||
d.pos = newPos
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -7,16 +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")
|
||||
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")
|
||||
@@ -41,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
|
||||
@@ -62,8 +71,9 @@ func main() {
|
||||
}
|
||||
|
||||
state := state{
|
||||
zoom: 6.0,
|
||||
origin: image.Point{0, 0},
|
||||
zoom: 6.0,
|
||||
origin: image.Point{0, 0},
|
||||
spriteIdx: *sprIdx,
|
||||
}
|
||||
|
||||
env := &env{
|
||||
@@ -126,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() {
|
||||
|
@@ -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() {
|
||||
|
@@ -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
|
||||
|
@@ -1,167 +0,0 @@
|
||||
Hypothesis: Idx/WarHammer.idx points objects into bitmap data in Anim/WarHammer.ani
|
||||
|
||||
We can use WH40K_TD.exe and investigate reads of .idx followed by reads of .ani
|
||||
to test this.
|
||||
|
||||
WH40K_TD.exe opens files in this order:
|
||||
|
||||
1. Data/USEng.dta
|
||||
1. WH40K_TD.exe (?)
|
||||
1. Cursor/Cursors.cur
|
||||
1. pread64(fd, 23, 0) = 23
|
||||
1. _llseek(fd, 0, [0], SEEK_CUR) = 0
|
||||
1. _llseek(fd, 0, [0], SEEK_CUR) = 0
|
||||
1. _llseek(fd, 40666, [40666], SEEK_SET) = 0
|
||||
1. _llseek(fd, 0, [0], SEEK_SET) = 0
|
||||
1. close(fd) = 0
|
||||
1. read(fd, "\x26\x00\x00\x00\x20\x00\x00\x00\x30\x01\x00\x00\x50\x01\x00\x00\x8a\x9d\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 32) = 32
|
||||
1. (...)
|
||||
1. _llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Cursor/Cursors.cur>, 39868, [39868], SEEK_SET) = 0
|
||||
1. read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Cursor/Cursors.cur>, "...", 798) = 798
|
||||
1. (some statting of Idx/WarHammer.idx, no reading that I saw)
|
||||
1. Anim/WarHammer.ani
|
||||
1. read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "...", 32) = 32
|
||||
1. (some clones of child procs, I didn't follow them)
|
||||
1. Sounds/wh40k.ds
|
||||
1. pread64(31</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Sounds/wh40k.ds>, "...", 23, 0) = 23
|
||||
1. read(31</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Sounds/wh40k.ds>, "...", 417792) = 417792
|
||||
1. read(31</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Sounds/wh40k.ds>, "...", 4096) = 4096
|
||||
1. Data/Sounds.dat
|
||||
1. pread64(34</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Data/Sounds.dat>, "#**********************", 23, 0) = 23
|
||||
1. ...
|
||||
1. Pic/wh40k.pcx
|
||||
1. read(34</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Pic/wh40k.pcx>, "...", 168509) = 168509
|
||||
1. Sets/*
|
||||
1. (lots of statting these)
|
||||
1. Data/Randchar.dat
|
||||
1. pread64(34</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Data/RandChar.dat>, "#**********************", 23, 0) = 23
|
||||
1. read(34</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Data/RandChar.dat>, "#***************************************************************"..., 4096) = 4096
|
||||
1. ...
|
||||
1. Data/WeapDef.dat
|
||||
1. pread64(35</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Data/WeapDef.dat>, "#**********************", 23, 0) = 23
|
||||
1. read(35</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Data/WeapDef.dat>, "#***************************************************************"..., 4096) = 4096
|
||||
1. ...
|
||||
1. Data/SpellDef.dat
|
||||
1. pread64(35</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Data/SpellDef.dat>, "#**********************", 23, 0) = 23
|
||||
1. read(35</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Data/SpellDef.dat>, "#***************************************************************"..., 4096) = 4096
|
||||
1. ...
|
||||
1. Data/AniObDef.dat
|
||||
1. pread64(35</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Data/AniObDef.dat>, "# ******** ANIMATED OBJ", 23, 0) = 23
|
||||
1. read(35</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Data/AniObDef.dat>, "# ******** ANIMATED OBJECT DEFINITIONS **************\r\n#\t\t0 : **"..., 4096) = 4096
|
||||
1. ...
|
||||
1. Data/VehicDef.dat
|
||||
1. pread64(35</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Data/VehicDef.dat>, "# ******** VEHICLE DEFI", 23, 0) = 23
|
||||
1. read(35</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Data/VehicDef.dat>, "# ******** VEHICLE DEFINITIONS **************\r\n#\t\t0 : *** VEHICL"..., 4096) = 4096
|
||||
1. ...
|
||||
1. Data/StdWeap.dat
|
||||
1. pread64(35</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Data/StdWeap.dat>, "# ******** SQUAD STANDA", 23, 0) = 23
|
||||
1. read(35</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Data/StdWeap.dat>, "# ******** SQUAD STANDARD WEAPONS **************\r\n#\t\t0 : *** SQU"..., 4096) = 4096
|
||||
1. ...
|
||||
1. Data/Ultnames.dat
|
||||
1. Data/Chanames.dat
|
||||
1. Data/keymap.dta
|
||||
1. Filters/wh40k.flt
|
||||
1. _llseek(35</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Filters/wh40k.flt>, 0, [0], SEEK_SET) = 0
|
||||
1. read(35</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Filters/wh40k.flt>, "\x01\x00\x00\x00", 4) = 4
|
||||
1. _llseek(35</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Filters/wh40k.flt>, 4, [4], SEEK_SET) = 0
|
||||
1. read(35</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Filters/wh40k.flt>, "...", 72) = 72
|
||||
1. _llseek(35</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Filters/wh40k.flt>, 1444, [1444], SEEK_SET) = 0
|
||||
1. read(35</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Filters/wh40k.flt>, "...", 327680) = 327680
|
||||
1. Misc/occlusio.lis
|
||||
1. pread64(35</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Misc/occlusio.lis>, "62 # Number of Absol", 23, 0) = 23
|
||||
1. read(35</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Misc/occlusio.lis>, "62 # Number of Absolute Deltas.\r\n # These Deltas are off"..., 4096) = 982
|
||||
1. read(35</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Misc/occlusio.lis>, "", 3114) = 0
|
||||
1. Data/GDestroy.dat
|
||||
1. (stat Obj/destroy.obj)
|
||||
1. Data/minimap.dat
|
||||
1. Misc/occlusio.list
|
||||
1. Obj/specials.obj
|
||||
1. Obj/Man_Shadow.obj
|
||||
1. Sets/map01.set
|
||||
1. Data/Defs.dat
|
||||
1. [`Assign/jungtil.asn`](docs/formats/obj.md#assign)
|
||||
1. [`Obj/jungtil.obj`](docs/formats/obj.md)
|
||||
1. (more assign + obj pairs)
|
||||
1. Data/Cycle.cyc
|
||||
|
||||
Adding a Librarian to the mission builder performs these seeks and reads:
|
||||
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 509440, [509440], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "\fM\266\th\16\0\0", 8) = 8
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 509448, [509448], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "t[\266\t\376\16\0\0", 8) = 8
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 509456, [509456], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "rj\266\tg\17\0\0", 8) = 8
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 509464, [509464], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "\331y\266\t\251\17\0\0", 8) = 8
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 509472, [509472], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "\202\211\266\t\273\17\0\0", 8) = 8
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 509480, [509480], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "=\231\266\t\10\20\0\0", 8) = 8
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 509488, [509488], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "E\251\266\t\321\17\0\0", 8) = 8
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 509496, [509496], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "\26\271\266\t\1\17\0\0", 8) = 8
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 509504, [509504], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "\27\310\266\t\304\16\0\0", 8) = 8
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 509512, [509512], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "\333\326\266\t\343\16\0\0", 8) = 8
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 509520, [509520], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "\276\345\266\t\f\17\0\0", 8) = 8
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 509528, [509528], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "\312\364\266\tA\17\0\0", 8) = 8
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 509536, [509536], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "\v\4\267\t\246\17\0\0", 8) = 8
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 509440, [509440], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "\fM\266\th\16\0\0", 8) = 8
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 164448540, [164448540], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "\367\0\n\0015\0T\0\0\0\0\0P\16\0\0\324q;\1\0\0\0\0\200\23\207**+*+"..., 3688) = 3688
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 509448, [509448], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "t[\266\t\376\16\0\0", 8) = 8
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 164452228, [164452228], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "\365\0\10\0017\0W\0\0\0\0\0\346\16\0\0\324q;\1\0\0\0\0\200\25\3*\212+*,"..., 3838) = 3838
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 509456, [509456], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "rj\266\tg\17\0\0", 8) = 8
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 164456066, [164456066], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "\364\0\10\0019\0Z\0\0\0\0\0O\17\0\0\324q;\1\0\0\0\0\200\30\201*\5+\200\33"..., 3943) = 3943
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 509464, [509464], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "\331y\266\t\251\17\0\0", 8) = 8
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 164460009, [164460009], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "\356\0\7\1B\0[\0\0\0\0\0\221\17\0\0\324q;\1\0\0\0\0\200\"\201*\200\37\0\200"..., 4009) = 4009
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 509472, [509472], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "\202\211\266\t\273\17\0\0", 8) = 8
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 164464018, [164464018], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "\356\0\n\1C\0\\\0\0\0\0\0\243\17\0\0\324q;\1\0\0\0\0\200#\3)\3+\200\32"..., 4027) = 4027
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 509480, [509480], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "=\231\266\t\10\20\0\0", 8) = 8
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 164468045, [164468045], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "\354\0\t\1C\0Z\0\0\0\0\0\360\17\0\0\324q;\1\0\0\0\0\200$\201*\200\36\0\200"..., 4104) = 4104
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 509488, [509488], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "E\251\266\t\321\17\0\0", 8) = 8
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 164472149, [164472149], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "\356\0\t\1?\0V\0\0\0\0\0\271\17\0\0\324q;\1\0\0\0\0\200\35\212&&H)*"..., 4049) = 4049
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 509496, [509496], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "\26\271\266\t\1\17\0\0", 8) = 8
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 164476198, [164476198], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "\366\0\10\0015\0[\0\0\0\0\0\351\16\0\0\324q;\1\0\0\0\0\200\20\201*\4+\206*"..., 3841) = 3841
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 509504, [509504], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "\27\310\266\t\304\16\0\0", 8) = 8
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 164480039, [164480039], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "\367\0\7\0013\0[\0\0\0\0\0\254\16\0\0\324q;\1\0\0\0\0\200\33\210+,+,,"..., 3780) = 3780
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 509512, [509512], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "\333\326\266\t\343\16\0\0", 8) = 8
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 164483819, [164483819], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "\370\0\7\1A\0_\0\0\0\0\0\313\16\0\0\324q;\1\0\0\0\0\200\34\203,,*\200\""..., 3811) = 3811
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 509520, [509520], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "\276\345\266\t\f\17\0\0", 8) = 8
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 164487630, [164487630], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "\370\0\7\1H\0`\0\0\0\0\0\364\16\0\0\324q;\1\0\0\0\0\200\35\201,\200*\0\200"..., 3852) = 3852
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 509528, [509528], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "\312\364\266\tA\17\0\0", 8) = 8
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 164491482, [164491482], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "\373\0\10\1;\0Z\0\0\0\0\0)\17\0\0\324q;\1\0\0\0\0\200\36\202+,\200\33\0"..., 3905) = 3905
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 509536, [509536], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "\v\4\267\t\246\17\0\0", 8) = 8
|
||||
_llseek(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, 164495387, [164495387], SEEK_SET) = 0
|
||||
read(15</home/lupine/.wine/drive_c/GOG Games/ChaosGate/Anim/WarHammer.ani>, "\366\0\n\0018\0Y\0\0\0\0\0\216\17\0\0\324q;\1\0\0\0\0\200\32\t+\205*\4,"..., 4006) = 4006
|
||||
|
402
doc/formats/ani.md
Normal file
402
doc/formats/ani.md
Normal file
@@ -0,0 +1,402 @@
|
||||
# `Anim/WarHammer.ani`
|
||||
|
||||
This turns out to simply be an [`obj`](obj.md) file.
|
||||
|
||||
The first 1,064 sprites are all of the same Ultramarine, carryng a bolter. There
|
||||
are eight "facing" orientations:
|
||||
|
||||
* North
|
||||
* Northeast
|
||||
* East
|
||||
* Southeast
|
||||
* South
|
||||
* Southwest
|
||||
* West
|
||||
* Northwest
|
||||
|
||||
For each orientation, an action is pictured in a variable number of frames. The
|
||||
final frame for each action appears to be "stationary".
|
||||
|
||||
* Walk (13 frames)
|
||||
* Run (9 frames)
|
||||
* Crouch down (8 frames)
|
||||
* Stand up (8 frames)
|
||||
* Take aim (standing) (6 frames)
|
||||
* Fire (standing) (6 frames)
|
||||
* Relax aim (standing) (6 frames)
|
||||
* Throw grenade (standing) (18 frames)
|
||||
* Take aim (crouched) (5 frames)
|
||||
* Fire (crouched) (5 frames)
|
||||
* Relax aim (crouched) (5 frames)
|
||||
* Throw grenade (crouched) (17 frames)
|
||||
* Draw melee weapon (standing) (10 frames)
|
||||
* Strike down with melee weapon (standing) (8 frames)
|
||||
* Stab with melee weapon (standing) (9 frames)
|
||||
|
||||
Added together and multiplied by 87, that's 1064.
|
||||
|
||||
The next sprite is a walking-north action for an ultramarine with a flamer. The
|
||||
total number of frames for this character is 1120 - 56 additional frames, or 7
|
||||
per orientation. Could be an extra action, or an extra frame per action.
|
||||
|
||||
Also notable is that while the bolter showed muzzle flash in the animation, the
|
||||
flamer only showed a tiny hint of fire. I think the animation for spewing flame
|
||||
is held elsewhere.
|
||||
|
||||
I strongly suspect the actions and the number of frames in each action are
|
||||
configurable. So, what other files are implicated in its interpretation? Here's
|
||||
a few possibilities:
|
||||
|
||||
* `Data/AniObDef.dat`
|
||||
* `Data/Coordinates.dat`
|
||||
* `Data/HasAction.dat`
|
||||
* `Data/VehicDef.dat`
|
||||
* `Data/WeapDef.dat`
|
||||
- `Idx/WarHammer.idx`
|
||||
|
||||
## `Data/AniObDef.dat`
|
||||
|
||||
Including comments, this is 4098 lines, giving approx. 45 lines for each
|
||||
of the ~188 characters in the `ani`. That doesn't seem many, and there's no
|
||||
obvious correspondence between the commented-on names (`SMOKE01`?) and the
|
||||
viewed frames... but then, I've not viewed all the frames.
|
||||
|
||||
## `Data/HasAction.dat`
|
||||
|
||||
This file seems relevant as it says whether or not particular animations exist
|
||||
for the different types of character, which maps directly to what is stored in
|
||||
the .ani file - and so must affect lookups thereof.
|
||||
|
||||
Fortunately, it's commented extensively. For each "Character Type", there are
|
||||
36 different possible animations.
|
||||
|
||||
Here's a table representation of the data:
|
||||
|
||||
```
|
||||
Tac Ass Dev Term Apo Tech Chp Lib Cpt CMar CLrd CChp CSrc CTrm Kbz BTh BL FHnd LoC Flm PHr BHr Cult
|
||||
00 x x x x x x x x x x x x x x x x x x x x x x x
|
||||
01 x x x x x x x x x x x x x x x x x x x x x x x
|
||||
02 x x x x x x x x x x x x x x x x x x x x x x x
|
||||
03
|
||||
04
|
||||
05
|
||||
06 x x x x x x x x x x x x x x x x x x x x
|
||||
07 x x x x x x x x x x x x x x x x x x x x x x x
|
||||
08 x x x x x x x x x x x x x x x x
|
||||
09
|
||||
10
|
||||
11
|
||||
12
|
||||
13
|
||||
14 x x x x x x x x x x x x x
|
||||
15 x x x x x x x x x x x x x
|
||||
16 x x x x x x x x x x x x x
|
||||
17 x x x x x x x x x x x x x
|
||||
18 x x x x x x x x x x x x x
|
||||
19 x x x x x x x x x x x x x
|
||||
20 x x x x x x x x x x x x x
|
||||
21 x x x x x x x x x x x x x
|
||||
22 x x x x x x x x x x x x x x
|
||||
23 x x x x x x x x x x x x x
|
||||
24 x x x x x x x x x x x x x x x x
|
||||
25 x x x x x x x x x x x x x x x x x x x x x x
|
||||
26 x x x x x x x x x x x x x x x x x
|
||||
27 x x x x x x x x x x x x x x x x x x x x x
|
||||
28 x x x x x x x x x x x x x x
|
||||
29 x x x
|
||||
30 x
|
||||
31 x
|
||||
32 x x x
|
||||
33 x x x x x x x x x x x x x x x x x x x x x
|
||||
34 x x x x x x x x x x x x x x x x x x x x x x x
|
||||
35 x x x x x x x
|
||||
```
|
||||
|
||||
`WarHammer.ani` doesn't have blank sprites for the unchecked cells, so this must
|
||||
surely be used to map between set-of-sprites and `AnimAction`. The names map
|
||||
very well to the descriptions I came up with when observing the sprites.
|
||||
|
||||
I think we still need the data in `.idx` for a full picture, though. Things we
|
||||
still need:
|
||||
|
||||
* Mapping of character type to sprite directory index in `WarHammer.ani`
|
||||
* Number of frames in each AnimAction
|
||||
|
||||
Either of these could be hardcoded, or dynamic.
|
||||
|
||||
## `Idx/WarHammer.idx`
|
||||
|
||||
`WarHammer.idx` (1,880,078 bytes, binary, so around 10KiB per character, in
|
||||
theory) is more reasonable.
|
||||
|
||||
Here's a list of operations on the file when `WH40K_TD.EXE` is instructed to
|
||||
place a single Librarian:
|
||||
|
||||
<details>
|
||||
|
||||
```
|
||||
_llseek(<WarHammer.idx>, 132, [132], SEEK_SET) = 0
|
||||
read(<WarHammer.idx>, "\x30\x7c\x09\x00\x98\x00\x00\x00\x88\xf8\x00\x00", 12) = 12
|
||||
|
||||
_llseek(<WarHammer.idx>, 132, [132], SEEK_SET) = 0
|
||||
read(<WarHammer.idx>, "\x30\x7c\x09\x00\x98\x00\x00\x00\x88\xf8\x00\x00", 12) = 12
|
||||
|
||||
_llseek(<WarHammer.idx>, 621616, [621616], SEEK_SET) = 0
|
||||
read(<WarHammer.idx>, "\x02\x01\x01\x33\x50\x83\x09\x00\x0d\x00\x00\x00", 12) = 12
|
||||
|
||||
_llseek(<WarHammer.idx>, 621628, [621628], SEEK_SET) = 0
|
||||
read(<WarHammer.idx>, "\x02\x01\x02\x33\xb2\x83\x09\x00\x0d\x00\x00\x00", 12) = 12
|
||||
|
||||
_llseek(<WarHammer.idx>, 621640, [621640], SEEK_SET) = 0
|
||||
read(<WarHammer.idx>, "\x02\x01\x03\x33\x14\x84\x09\x00\x0d\x00\x00\x00", 12) = 12
|
||||
|
||||
_llseek(<WarHammer.idx>, 621652, [621652], SEEK_SET) = 0
|
||||
read(<WarHammer.idx>, "\x02\x01\x04\x33\x76\x84\x09\x00\x0d\x00\x00\x00", 12) = 12
|
||||
|
||||
_llseek(<WarHammer.idx>, 621664, [621664], SEEK_SET) = 0
|
||||
read(<WarHammer.idx>, "\x02\x01\x05\x33\xd8\x84\x09\x00\x0d\x00\x00\x00", 12) = 12
|
||||
|
||||
_llseek(<WarHammer.idx>, 623832, [623832], SEEK_SET) = 0
|
||||
read(<WarHammer.idx>, "\x34\x00\x40\x00\x40\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 20) = 20
|
||||
|
||||
_llseek(<WarHammer.idx>, 0, [623852], SEEK_CUR) = 0
|
||||
_llseek(<WarHammer.idx>, 623852, [623852], SEEK_SET) = 0
|
||||
_llseek(<WarHammer.idx>, 623930, [623930], SEEK_SET) = 0
|
||||
|
||||
_llseek(<WarHammer.ani>, 509440, [509440], SEEK_SET) = 0
|
||||
read(<WarHammer.ani>, "\x0c\x4d\xb6\x09\x68\x0e\x00\x00", 8) = 8
|
||||
|
||||
_llseek(<WarHammer.ani>, 509448, [509448], SEEK_SET) = 0
|
||||
read(<WarHammer.ani>, "\x74\x5b\xb6\x09\xfe\x0e\x00\x00", 8) = 8
|
||||
|
||||
_llseek(<WarHammer.ani>, 509456, [509456], SEEK_SET) = 0
|
||||
read(<WarHammer.ani>, "\x72\x6a\xb6\x09\x67\x0f\x00\x00", 8) = 8
|
||||
|
||||
_llseek(<WarHammer.ani>, 509464, [509464], SEEK_SET) = 0
|
||||
read(<WarHammer.ani>, "\xd9\x79\xb6\x09\xa9\x0f\x00\x00", 8) = 8
|
||||
|
||||
_llseek(<WarHammer.ani>, 509472, [509472], SEEK_SET) = 0
|
||||
read(<WarHammer.ani>, "\x82\x89\xb6\x09\xbb\x0f\x00\x00", 8) = 8
|
||||
|
||||
_llseek(<WarHammer.ani>, 509480, [509480], SEEK_SET) = 0
|
||||
read(<WarHammer.ani>, "\x3d\x99\xb6\x09\x08\x10\x00\x00", 8) = 8
|
||||
|
||||
_llseek(<WarHammer.ani>, 509488, [509488], SEEK_SET) = 0
|
||||
read(<WarHammer.ani>, "\x45\xa9\xb6\x09\xd1\x0f\x00\x00", 8) = 8
|
||||
|
||||
_llseek(<WarHammer.ani>, 509496, [509496], SEEK_SET) = 0
|
||||
read(<WarHammer.ani>, "\x16\xb9\xb6\x09\x01\x0f\x00\x00", 8) = 8
|
||||
|
||||
_llseek(<WarHammer.ani>, 509504, [509504], SEEK_SET) = 0
|
||||
read(<WarHammer.ani>, "\x17\xc8\xb6\x09\xc4\x0e\x00\x00", 8) = 8
|
||||
|
||||
_llseek(<WarHammer.ani>, 509512, [509512], SEEK_SET) = 0
|
||||
read(<WarHammer.ani>, "\xdb\xd6\xb6\x09\xe3\x0e\x00\x00", 8) = 8
|
||||
|
||||
_llseek(<WarHammer.ani>, 509520, [509520], SEEK_SET) = 0
|
||||
read(<WarHammer.ani>, "\xbe\xe5\xb6\x09\x0c\x0f\x00\x00", 8) = 8
|
||||
|
||||
_llseek(<WarHammer.ani>, 509528, [509528], SEEK_SET) = 0
|
||||
read(<WarHammer.ani>, "\xca\xf4\xb6\x09\x41\x0f\x00\x00", 8) = 8
|
||||
|
||||
_llseek(<WarHammer.ani>, 509536, [509536], SEEK_SET) = 0
|
||||
read(<WarHammer.ani>, "\x0b\x04\xb7\x09\xa6\x0f\x00\x00", 8) = 8
|
||||
|
||||
_llseek(<WarHammer.ani>, 509440, [509440], SEEK_SET) = 0
|
||||
read(<WarHammer.ani>, "\x0c\x4d\xb6\x09\x68\x0e\x00\x00", 8) = 8
|
||||
|
||||
_llseek(<WarHammer.ani>, 164448540, [164448540], SEEK_SET) = 0
|
||||
read(<WarHammer.ani>, "\xf7\x00\x0a\x01\x35\x00\x54\x00\x00\x00\x00\x00\x50\x0e\x00\x00\xd4\x71\x3b\x01\x00\x00\x00\x00\x80\x13\x87\x2a\x2a\x2b\x2a\x2b"..., 3688) = 3688
|
||||
|
||||
_llseek(<WarHammer.ani>, 509448, [509448], SEEK_SET) = 0
|
||||
read(<WarHammer.ani>, "\x74\x5b\xb6\x09\xfe\x0e\x00\x00", 8) = 8
|
||||
|
||||
_llseek(<WarHammer.ani>, 164452228, [164452228], SEEK_SET) = 0
|
||||
read(<WarHammer.ani>, "\xf5\x00\x08\x01\x37\x00\x57\x00\x00\x00\x00\x00\xe6\x0e\x00\x00\xd4\x71\x3b\x01\x00\x00\x00\x00\x80\x15\x03\x2a\x8a\x2b\x2a\x2c"..., 3838) = 3838
|
||||
|
||||
_llseek(<WarHammer.ani>, 509456, [509456], SEEK_SET) = 0
|
||||
read(<WarHammer.ani>, "\x72\x6a\xb6\x09\x67\x0f\x00\x00", 8) = 8
|
||||
|
||||
_llseek(<WarHammer.ani>, 164456066, [164456066], SEEK_SET) = 0
|
||||
read(<WarHammer.ani>, "\xf4\x00\x08\x01\x39\x00\x5a\x00\x00\x00\x00\x00\x4f\x0f\x00\x00\xd4\x71\x3b\x01\x00\x00\x00\x00\x80\x18\x81\x2a\x05\x2b\x80\x1b"..., 3943) = 3943
|
||||
|
||||
_llseek(<WarHammer.ani>, 509464, [509464], SEEK_SET) = 0
|
||||
read(<WarHammer.ani>, "\xd9\x79\xb6\x09\xa9\x0f\x00\x00", 8) = 8
|
||||
|
||||
_llseek(<WarHammer.ani>, 164460009, [164460009], SEEK_SET) = 0
|
||||
read(<WarHammer.ani>, "\xee\x00\x07\x01\x42\x00\x5b\x00\x00\x00\x00\x00\x91\x0f\x00\x00\xd4\x71\x3b\x01\x00\x00\x00\x00\x80\x22\x81\x2a\x80\x1f\x00\x80"..., 4009) = 4009
|
||||
|
||||
_llseek(<WarHammer.ani>, 509472, [509472], SEEK_SET) = 0
|
||||
read(<WarHammer.ani>, "\x82\x89\xb6\x09\xbb\x0f\x00\x00", 8) = 8
|
||||
|
||||
_llseek(<WarHammer.ani>, 164464018, [164464018], SEEK_SET) = 0
|
||||
read(<WarHammer.ani>, "\xee\x00\x0a\x01\x43\x00\x5c\x00\x00\x00\x00\x00\xa3\x0f\x00\x00\xd4\x71\x3b\x01\x00\x00\x00\x00\x80\x23\x03\x29\x03\x2b\x80\x1a"..., 4027) = 4027
|
||||
|
||||
_llseek(<WarHammer.ani>, 509480, [509480], SEEK_SET) = 0
|
||||
read(<WarHammer.ani>, "\x3d\x99\xb6\x09\x08\x10\x00\x00", 8) = 8
|
||||
|
||||
_llseek(<WarHammer.ani>, 164468045, [164468045], SEEK_SET) = 0
|
||||
read(<WarHammer.ani>, "\xec\x00\x09\x01\x43\x00\x5a\x00\x00\x00\x00\x00\xf0\x0f\x00\x00\xd4\x71\x3b\x01\x00\x00\x00\x00\x80\x24\x81\x2a\x80\x1e\x00\x80"..., 4104) = 4104
|
||||
|
||||
_llseek(<WarHammer.ani>, 509488, [509488], SEEK_SET) = 0
|
||||
read(<WarHammer.ani>, "\x45\xa9\xb6\x09\xd1\x0f\x00\x00", 8) = 8
|
||||
|
||||
_llseek(<WarHammer.ani>, 164472149, [164472149], SEEK_SET) = 0
|
||||
read(<WarHammer.ani>, "\xee\x00\x09\x01\x3f\x00\x56\x00\x00\x00\x00\x00\xb9\x0f\x00\x00\xd4\x71\x3b\x01\x00\x00\x00\x00\x80\x1d\x8a\x26\x26\x48\x29\x2a"..., 4049) = 4049
|
||||
|
||||
_llseek(<WarHammer.ani>, 509496, [509496], SEEK_SET) = 0
|
||||
read(<WarHammer.ani>, "\x16\xb9\xb6\x09\x01\x0f\x00\x00", 8) = 8
|
||||
|
||||
_llseek(<WarHammer.ani>, 164476198, [164476198], SEEK_SET) = 0
|
||||
read(<WarHammer.ani>, "\xf6\x00\x08\x01\x35\x00\x5b\x00\x00\x00\x00\x00\xe9\x0e\x00\x00\xd4\x71\x3b\x01\x00\x00\x00\x00\x80\x10\x81\x2a\x04\x2b\x86\x2a"..., 3841) = 3841
|
||||
|
||||
_llseek(<WarHammer.ani>, 509504, [509504], SEEK_SET) = 0
|
||||
read(<WarHammer.ani>, "\x17\xc8\xb6\x09\xc4\x0e\x00\x00", 8) = 8
|
||||
|
||||
_llseek(<WarHammer.ani>, 164480039, [164480039], SEEK_SET) = 0
|
||||
read(<WarHammer.ani>, "\xf7\x00\x07\x01\x33\x00\x5b\x00\x00\x00\x00\x00\xac\x0e\x00\x00\xd4\x71\x3b\x01\x00\x00\x00\x00\x80\x1b\x88\x2b\x2c\x2b\x2c\x2c"..., 3780) = 3780
|
||||
|
||||
_llseek(<WarHammer.ani>, 509512, [509512], SEEK_SET) = 0
|
||||
read(<WarHammer.ani>, "\xdb\xd6\xb6\x09\xe3\x0e\x00\x00", 8) = 8
|
||||
|
||||
_llseek(<WarHammer.ani>, 164483819, [164483819], SEEK_SET) = 0
|
||||
read(<WarHammer.ani>, "\xf8\x00\x07\x01\x41\x00\x5f\x00\x00\x00\x00\x00\xcb\x0e\x00\x00\xd4\x71\x3b\x01\x00\x00\x00\x00\x80\x1c\x83\x2c\x2c\x2a\x80\x22"..., 3811) = 3811
|
||||
|
||||
_llseek(<WarHammer.ani>, 509520, [509520], SEEK_SET) = 0
|
||||
read(<WarHammer.ani>, "\xbe\xe5\xb6\x09\x0c\x0f\x00\x00", 8) = 8
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Notable is that we read from `idx` **before** we read from `ani` - so it does
|
||||
seem like the former should tell us where to pull from the latter.
|
||||
|
||||
So what are we doing here? What did we read? Here's what I get:
|
||||
|
||||
### Type 1 record
|
||||
|
||||
From `0x84`:
|
||||
|
||||
```
|
||||
# xxd -s 0x84 -c 12 -e -l 12 -u orig/Idx/WarHammer.idx
|
||||
00000084: 00097C30 00000098 0000F888 0|..........
|
||||
```
|
||||
|
||||
The first read contains `0x097C30`. The second (+5) read, at `0x097C30`,
|
||||
contains `0x0984D8`. We then read 20, followed by 78, bytes, and go on to read
|
||||
from the `.ani` file.
|
||||
|
||||
The whole start of the file looks like a directory of the same kind of records
|
||||
(call them type 1). The record at offset 0 is empty, as are the last few, but
|
||||
the rest have always-increasing offsets in the first and third position. The
|
||||
first appears to be for a "tactical marine", or at least, it is read (similarly
|
||||
to the librarian) when placing a "tactical squad". That has an offset of
|
||||
0x1800` in the first position, which gives us space for 512 of these 12-byte
|
||||
records. We can say they look like:
|
||||
|
||||
Is there anything in here that can link us to what we're reading from the `.ani`
|
||||
file? From it, we read 14 entries from the sprite directory, starting at
|
||||
byte offset `0x07C600` and direntry offset 63676 (`0xF8BC`). We then load 10
|
||||
sprites. The first is at byte offset `0x9CD491C`, and is 3688 bytes.
|
||||
|
||||
Looking at that sprite in the object viewer, it is the librarian \o/ - facing
|
||||
south \o/. However, it's not the sprite we see in `WH40K_TD.exe`. That one is,
|
||||
I think, number 63688 (`0xF8C8`) - 12 sprites on. Nothing matches these numbers.
|
||||
|
||||
However, the **first** librarian sprite is at index 63624 (`0xF888`), which
|
||||
matches the value at offset 8. This, then, must be the link.
|
||||
|
||||
If the first sprite is 0, the displayed sprite is 64 (`0x40`)...
|
||||
|
||||
We still need to know how to go from "librarian" to "index 11", though. The
|
||||
`CTYPE_LIBRARIAN` value in `HasAction.ani` gives librarians an 8...
|
||||
|
||||
|
||||
| Offset | Size | Meaning |
|
||||
| ------ | ---- | ------- |
|
||||
| 0 | 4 | Offset of type 2 records |
|
||||
| 4 | 4 | Number of type 2 records |
|
||||
| 8 | 4 | First sprite in `WarHammer.ani` for this record |
|
||||
|
||||
### Type 2 record(s)
|
||||
|
||||
From `0x097C30`:
|
||||
|
||||
```
|
||||
# xxd -s 0x00097C30 -g 1 -c 12 -l 60 -u orig/Idx/WarHammer.idx
|
||||
00097c30: 02 01 01 33 50 83 09 00 0D 00 00 00 ...3P.......
|
||||
00097c3c: 02 01 02 33 B2 83 09 00 0D 00 00 00 ...3........
|
||||
00097c48: 02 01 03 33 14 84 09 00 0D 00 00 00 ...3........
|
||||
00097c54: 02 01 04 33 76 84 09 00 0D 00 00 00 ...3v.......
|
||||
00097c60: 02 01 05 33 D8 84 09 00 0D 00 00 00 ...3........
|
||||
```
|
||||
|
||||
Next, we read 5x 12-byte records - 60 bytes total - from that offset in the type
|
||||
1 record. The address of the next read is embedded in the fifth, which is where
|
||||
the reads of type 2 records stop - so we were searching for it.
|
||||
|
||||
In the first 12-byte record, we have a close offset: `0x098350`. So we have
|
||||
1,824 bytes available in this block of type 2 records - enough for 152 of them,
|
||||
which is the number specified in the second position of the type 1 header.
|
||||
|
||||
What is the significance of the fifth 12-byte read? Why do we move onto type 3
|
||||
records when we reach it? When we place the librarian, he is **facing** south,
|
||||
and that facing is the fifth one in the listing (N, NE, E, SE, S). It's all I
|
||||
can come up with.
|
||||
|
||||
Perhaps this is the fifth facing of the first action? Looking ahead in the file,
|
||||
we can see that the third byte counts from 1 to 8 and falls again, so this is a
|
||||
tempting idea.
|
||||
|
||||
If so, since we know the librarian has 23 actions, we'd expect room for 23 * 8
|
||||
type 2 records in this block. That would need 2208 bytes, and we only have 1824
|
||||
- enough for 19 animations, which is quite close.
|
||||
|
||||
Looking at the librarian in the `ani` file, we see they have 1055 sprites in
|
||||
total, but I haven't counted the actions yet.
|
||||
|
||||
| Offset | Size | Meaning |
|
||||
| ------ | ---- | ------- |
|
||||
| 0 | 2? | ActionID? Static per each group of 8 type-2 records? |
|
||||
| 2 | 1? | Counts up from `01` to `08` in each group of 8 type-2 records? |
|
||||
| 3 | 1? | Is `0x33` for all but the last 4 groups of 8 type-2 records? |
|
||||
| 4 | 4 | Position of type 3 record |
|
||||
| 8 | 4? | ??? - small values though. Count of frames? |
|
||||
|
||||
### Type 3 record
|
||||
|
||||
From `0x0984D8`:
|
||||
|
||||
```
|
||||
# xxd -s 0x984D8 -g 1 -c 12 -l 20 -u orig/Idx/WarHammer.idx
|
||||
000984d8: 34 00 40 00 40 00 01 00 00 00 00 00 4.@.@.......
|
||||
000984e4: 00 00 00 00 00 00 00 00
|
||||
|
||||
# xxd -s 0x984EC -g 1 -c 12 -l 78 -u orig/Idx/WarHammer.idx
|
||||
000984ec: 00 00 06 00 04 00 00 00 06 00 04 00 ............
|
||||
000984f8: 00 00 06 00 04 00 00 00 05 00 04 00 ............
|
||||
00098504: 00 00 05 00 04 00 00 00 03 00 04 00 ............
|
||||
00098510: 00 00 04 00 FC FF 00 00 05 00 FC FF ............
|
||||
0009851c: 00 00 06 00 FC FF 00 00 05 00 FC FF ............
|
||||
00098528: 00 00 06 00 FC FF 00 00 06 00 FC FF ............
|
||||
00098534: 00 00 00 00 00 00
|
||||
```
|
||||
|
||||
Here, in the first read, we see `34 00` and `40 00`. These are the **relative**
|
||||
offsets of the frames we load.
|
||||
|
||||
| Offset | Size | Meaning |
|
||||
| ------ | ---- | ------- |
|
||||
| 0 | 2 | First sprite in animation (relative offset) |
|
||||
| 2 | 2 | Last sprite in animation (relative offset)? |
|
||||
| 4 | 2? | Could also be last sprite in animation? |
|
||||
| 6 | 2? | ??? |
|
||||
| 8 | 12? | ??? - unset in this case |
|
||||
|
||||
The remaining 78-byte chunk is impenetrable so far, but we should now have the
|
||||
information we need to display all the animated sequences in `WarHammer.ani`!
|
||||
|
||||
How do we know it needs to be 78 bytes? One option is multiplying the final
|
||||
field of the type 2 record by 6. Maybe we have 6 bytes of description per frame,
|
||||
or maybe it's unrelated to frames?
|
@@ -6,29 +6,27 @@ remake.
|
||||
|
||||
## Filesystem layout
|
||||
|
||||
* `Anim/`
|
||||
* `WarHammer.ani` # Doesn't seem to be a RIFF file. 398M so very important.
|
||||
* There's a pcx image header at `dd ... bs=1 skip=213` but it seems to be a false alert
|
||||
* Hits for "AmigaOS bitmap font"... probably a false positive
|
||||
* Lots of 8-byte reads when loading stuff in the mission editor
|
||||
* Some ~4K reads, havent found one corresponding to a known format yet
|
||||
* [✓] [`Anim/`](obj.md#WarHammer.ani)
|
||||
* [`WarHammer.ani`](obj.md#WarHammer.ani)
|
||||
* [`Assign/`](obj.md#assign)
|
||||
* `*.asn` # Unknown, seems to be related to .obj files
|
||||
* `*.asn` # Specify properties for frames in .obj files
|
||||
* `Cursor/`
|
||||
* `*.ani` # RIFF data
|
||||
* `*.cur` # Presumably standard windows-format non-animated cursors
|
||||
* `*.ani` # RIFF data, standard ANI format \o/
|
||||
* [`Cursors.cur`](obj.md) # `obj` file containing pointers and drag elements
|
||||
* `Data/`
|
||||
* `*.dat` # plaintext files defining properties of objects. No single format
|
||||
* **PARSED**
|
||||
* `Accounting.dat` # key = value => internal/data/accounting.go
|
||||
* `AniObjDef.dat` # animated object definitions
|
||||
* `GenericData.dat` # Generic Game Settings
|
||||
* [`HasAction.dat`](ani.md) # "Are there animation for each of the character" - list of booleans
|
||||
* **TODO**
|
||||
* `ChaNames.dat` # list of character names
|
||||
* `Coordinates.dat` # Weapon Firing Coordinates
|
||||
* `Credits.dat` # list of credits
|
||||
* `Defs.dat` # defines properties for objects and tiles, each of which seems to have an id
|
||||
* `GDestroy.dat` # table of what destroys what?
|
||||
* `HasAction.dat` # "Are there animation for each of the character" - list of booleans
|
||||
|
||||
* `MiniMap.dat` # lots of seemingly random numbers. IDs?
|
||||
* `MissionBriefing.dat` # Contains all Campaign Mission Briefing Text. Sections: "CAMPAIGN MISSION X ... END CAMPAIGN MISSION X"
|
||||
* `PWeight.dat` # Personality weights for the individual character types
|
||||
@@ -42,7 +40,6 @@ remake.
|
||||
* `WeapDef.dat` # Weapon definitions
|
||||
* **PROBABLY NOT NEEDED**
|
||||
* `BugHunt.dat` # Contains SMF text for Bug hunt random missions
|
||||
* `Credits.dat` # list of credits
|
||||
* `GenArm.dat` # "Random campaign armory levels - space marine"
|
||||
* `HeroArm.dat` # "Random campaign armory levels - Veteran"
|
||||
* `MHeroArm.dat` # "Random campaign armory levels - Veteran"
|
||||
@@ -52,35 +49,34 @@ remake.
|
||||
* `SpArm.dat` # "RANDOM CAMPAIGN ARMORY LEVELS - Veteran"
|
||||
* `VetArm.dat` # "RANDOM CAMPAIGN ARMORY LEVELS - Veteran"
|
||||
* `*.chk` # checksums? Mentions all the .dat files
|
||||
* `*.cyc` # ColorCycle DataFile.
|
||||
* `*.dta` # localized strings and things
|
||||
* `Cycle.cyc` # ColorCycle DataFile.
|
||||
* `Encyclopedia.dta` # encyclopedia entries
|
||||
* `KeyMap.dta` # unknown
|
||||
* `Keymap.dta` # unknown
|
||||
* `keymap.dta` # unknown
|
||||
* `USEng.dta` # Localized strings
|
||||
* `EquipmentMenuData` # gzip-compressed, presumably to do with (initial?) squad configuration
|
||||
* `Filters/`
|
||||
* `wh40k.flt` # Audio filter(s?)
|
||||
* [✓] [`Fonts/`](fonts.md)
|
||||
* `cboxfont` # ???
|
||||
* `*.fnt`
|
||||
* `*.spr`
|
||||
* `Idx/`
|
||||
* `WarHammer.idx` # unknown, 1.8M
|
||||
* [`*.fnt`](fonts.md)
|
||||
* [`*.spr`](obj.md) # `obj` file
|
||||
* [ ] [`Idx/`](ani.md)
|
||||
* [`WarHammer.idx`](ani.md) # unknown, 1.8M
|
||||
* [`Maps/`](maps.md)
|
||||
* `*.MAP`
|
||||
* `*.TXT`
|
||||
* [`*.MAP`](maps.md)
|
||||
* [`*.TXT`](maps.md)
|
||||
* [`Menu/`](mnu.md) - UI element definitions
|
||||
* `*.mni`
|
||||
* `*.mnu`
|
||||
* [`*.mni`](mnu.md) # Menu include file
|
||||
* [`*.mnu`](mnu.md)
|
||||
* [`*.obj`](obj.md)
|
||||
* `Misc/`
|
||||
* `occlusio.lis` # plain text, presumably occlusion mappings?
|
||||
* [`MultiMaps/`](maps.md#multimaps)
|
||||
* `*.MAP`
|
||||
* `*.TXT`
|
||||
* [✓] [`Obj/`](obj.md)
|
||||
* `*.obj`
|
||||
* [`*.MAP`](maps.md)
|
||||
* [`*.TXT`](maps.md)
|
||||
* [`Obj/`](obj.md)
|
||||
* [ ] `cpiece.rec` # "Rects for various cursor piece types..."
|
||||
* [✓] [`*.obj`](obj.md)
|
||||
* [✓] `Pic/`
|
||||
* `*.pcx` # Standard .pcx format
|
||||
* `RandomMaps/`
|
||||
@@ -91,12 +87,12 @@ remake.
|
||||
* `*.txt` # Seems to be a copy of one of Maps/*.txt
|
||||
* [✓] [`Sets/`](sets.md)
|
||||
* `Data.chk`
|
||||
* `*.set`
|
||||
* [`*.set`](sets.md)
|
||||
* [✓] `SMK/`
|
||||
* `*.smk` # Videos: RAD Game Tools Smacker Multimedia version 2
|
||||
* [ ] `Sounds/`
|
||||
* [ ] [`Sounds/`](sound.md)
|
||||
* [`wh40k.ds`](sound.md)
|
||||
* [ ] `Wav/`
|
||||
* [ ] [`Wav/`](sound.md)
|
||||
* [`*.wav`](sound.md)
|
||||
|
||||
Phew.
|
||||
@@ -107,7 +103,7 @@ Phew.
|
||||
* *Almost everything* seems to be in a data file somewhere. Helpful!
|
||||
* `make loader` creates a `load` binary that will try to load various bits
|
||||
of data from `investigation/` and `orig/`. I use it to investigate file
|
||||
formats and the parsers I'm writing. Ensure you're in a `GOPATH`!
|
||||
formats and the parsers I'm writing.
|
||||
|
||||
## Cross-links / associations
|
||||
|
||||
|
@@ -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.
|
||||
|
@@ -134,7 +134,27 @@ $GenLoad.mni
|
||||
It looks like we just interpolate the named file into the text when we come
|
||||
across one of these lines.
|
||||
|
||||
## (Sub)menu types
|
||||
The `MENUID` in `GenDialog` and `GenLoad` is a 2-element list, like `1000,1`
|
||||
or `2000,2`. The second number corresponds to the offset in the list of object
|
||||
files.
|
||||
|
||||
## `MENUTYPE`
|
||||
|
||||
Here's the full list of values for `MENUTYPE`:
|
||||
|
||||
| Value | Meaning |
|
||||
| ----- | ------------ |
|
||||
| 0 | `Background` |
|
||||
| 1 | `Menu` |
|
||||
| 2 | `DragMenu` |
|
||||
| 3 | `RadioMenu` ??? - only seen in `LevelPly` and `LoadGame` around select-one items |
|
||||
| 45 | `MainBackground` ??? - only seen in `MainGame` and `MainGameChaos` |
|
||||
| 300 | `Dialogue` |
|
||||
|
||||
The `MENUTYPE` acts as a logical grouping of a set of objects onscreen, and
|
||||
gives strong hints about how to handle their children.
|
||||
|
||||
## `SUBMENUTYPE`
|
||||
|
||||
The types seem to refer to different types of UI widget. Here's a list of unique
|
||||
values:
|
||||
@@ -142,47 +162,49 @@ values:
|
||||
|
||||
| Value | Meaning |
|
||||
|-------|---------|
|
||||
| 0 | Background |
|
||||
| 1 | Logical menu grouping? |
|
||||
| 2 | ? |
|
||||
| 3 | Standard button? |
|
||||
| 30 | Equipment? |
|
||||
| 31 | "Character helmet" / "Slot" |
|
||||
| 40 | "X Line Y" |
|
||||
| 41 | "X Line Y" |
|
||||
| 45 | ? |
|
||||
| 45,10,11,9 | ? |
|
||||
| 45,11,12,10 | ? |
|
||||
| 45,14,15,13 | ? |
|
||||
| 45,17,18,16 | ? |
|
||||
| 45,3,4,2 | ? |
|
||||
| 45,5,6,4 | ? |
|
||||
| 45,6,7,5 | ? |
|
||||
| 45,7,8,6 | ? |
|
||||
| 45,8,9,7 | ? |
|
||||
| 45,9,10,8 | ? |
|
||||
| 50 | ? |
|
||||
| 60 | Other text to display? (`UltEquip.mnu`) |
|
||||
| 61 | Text to display |
|
||||
| 70 | Hypertext to display |
|
||||
| 91 | ? |
|
||||
| 100 | ? |
|
||||
| 110 | ? |
|
||||
| 120 | ? |
|
||||
| 200 | Drop-down button? |
|
||||
| 205 | Single list box item? |
|
||||
| 220 | Psyker power? |
|
||||
| 221 | Page? |
|
||||
| 228 | Big buttons in `Main.mnu` |
|
||||
| 232 | ? |
|
||||
| 233 | ? |
|
||||
| 300 | Pop-up dialog box |
|
||||
| 400,0,0,{8, 16} | ? |
|
||||
| 400,22,22,{2, 4, 5, 6, 7, 8, 9, 9, 10, 13, 16} | ? |
|
||||
| 400,30,-1,5 | ? |
|
||||
| 405,0,0,{8, 16} | ? |
|
||||
| 405,22,22,{2, 4, 5, 6, 7, 8, 9, 10, 13, 16} | ? |
|
||||
| 405,30,-1,5 | ? |
|
||||
| 3 | `Button` |
|
||||
| 30 | `DoorHotspot1` |
|
||||
| 31 | `DoorHotspot2` |
|
||||
| 40 | `LineKbd` |
|
||||
| 41 | `LineBriefing` |
|
||||
| 45 | `Thumb` |
|
||||
| 50 | `InvokeButton` |
|
||||
| 60 | `DoorHotspot3` |
|
||||
| 61 | `Overlay` |
|
||||
| 70 | `Hypertext` |
|
||||
| 91 | `Checkbox` |
|
||||
| 100 | `EditBox` |
|
||||
| 110 | `InventorySelect` |
|
||||
| 120 | `RadioButton` |
|
||||
| 200 | `DropdownButton` |
|
||||
| 205 | `ComboBoxItem` |
|
||||
| 220 | `AnimationSample` |
|
||||
| 221 | `AnimationHover` |
|
||||
| 228 | `MainButton` |
|
||||
| 232 | `Slider` |
|
||||
| 233 | `StatusBar` |
|
||||
| 400 | `ListBoxUp` |
|
||||
| 405 | `ListBoxDown` |
|
||||
|
||||
`400`, `405`, and `45`, can all accept 4 values for `SUBMENUTYPE` in a
|
||||
comma-separated list. These records combine to form a `TListBox` control, with a
|
||||
number of visible slots that act as a viewport. There is a draggable vertical
|
||||
slider (the "thumb") to show where in the full list the viewport is, and up +
|
||||
down buttons to move the position of the thumb by one, so it's feasible that
|
||||
these values tell us about the available steps.
|
||||
|
||||
Here are the values in `Briefing.mnu`:
|
||||
|
||||
```
|
||||
#rem..........List Box Menu
|
||||
MENUTYPE : 1 # List Box Menu
|
||||
SUBMENUTYPE: 400,22,22,13 # Scroll Up
|
||||
SUBMENUTYPE: 405,22,22,13 # Scroll Down
|
||||
SUBMENUTYPE: 45, 14,15,13 # Thumb
|
||||
```
|
||||
|
||||
There are 13 elements in this listbox, which sorts out the fourth number (but
|
||||
what is it used for?). The other two need more investigation.
|
||||
|
||||
## Positioning
|
||||
|
||||
@@ -195,6 +217,9 @@ successfully, for instance:
|
||||
|
||||

|
||||
|
||||
However, it's *not* sufficient to put all the items for `MainGame.mnu` in the
|
||||
right place.
|
||||
|
||||
## Animation
|
||||
|
||||
This seems to be done by choosing a different sprite to draw every N ticks. They
|
||||
@@ -238,28 +263,6 @@ attributes plucked from `Main.mnu`:
|
||||
|
||||
The buttons, menu title and version hotspot are submenus of the start menu.
|
||||
|
||||
### `MENUTYPE`
|
||||
|
||||
This is the only menu where we see a type of 228. ~750 other unique values are
|
||||
observed, suggesting structure. For instance, we have `24`, `240`, `241` and
|
||||
`2410`, but not `2411` or `2409`. Sometimes we have a comma-separated list,
|
||||
e.g.: `400,30,-1,5`.
|
||||
|
||||
A listing of currently-known values:
|
||||
|
||||
| Value | Type |
|
||||
| ----- | ---------------- |
|
||||
| 0 | Static image |
|
||||
| 1 | Menu |
|
||||
| 3 | Button |
|
||||
| 50 | Invoke? Button? |
|
||||
| 61 | "Overlay" |
|
||||
| 70 | "Hypertext" |
|
||||
| 91 | Checkbox |
|
||||
| 220 | Animation sample |
|
||||
| 228 | Main menu button |
|
||||
| 232 | Slider |
|
||||
|
||||
### `ACTIVE`
|
||||
|
||||
There are only 4 values seen across all menus: `0`, `1`, `1,0`, `102` and `1,1`.
|
||||
|
@@ -6,80 +6,6 @@ be placed on it - a `SURFACE`, `LEFT`, `RIGHT`, and `CENTER`. Maps reference
|
||||
objects in a space-efficient way via sets, which seem to be a kind of object
|
||||
palette.
|
||||
|
||||
## Assign
|
||||
|
||||
The `Assign/` directory contains a matching `.asn` file for each `Obj/*.obj`.
|
||||
It's a plain-text format which seems to assign properties to frames, and has
|
||||
references to a `<name>.flc` file which does not exist in the tree.
|
||||
|
||||
Theory: .obj files were originally generated from `.flc` files. This is an
|
||||
AutoDesk format for visual data, so this suggests the .obj files contain pixels
|
||||
\o/
|
||||
|
||||
`blank.asn` references 6 frames (0-5):
|
||||
|
||||
```
|
||||
# single pixel tile
|
||||
# transpix.obj/.asn
|
||||
#/--> transpix.flc
|
||||
#
|
||||
|
||||
0-5:DEF 1;
|
||||
|
||||
|
||||
0-5:TYPE 13;
|
||||
|
||||
|
||||
END OF FILE
|
||||
```
|
||||
|
||||
`jungtil.asn` references 18 frames (0-17):
|
||||
|
||||
```
|
||||
# jungle floor
|
||||
# jungtil.obj/.asn
|
||||
# /--> d:\warflics\missions\jungtil.flc
|
||||
#
|
||||
|
||||
0:DEF 2;
|
||||
1-11:DEF 454;
|
||||
|
||||
|
||||
#damaged frames!!!!
|
||||
12:DEF 2;
|
||||
13-16:DEF 454;
|
||||
17:DEF 454;
|
||||
|
||||
|
||||
0:TYPE 2;
|
||||
1-11:TYPE 0;
|
||||
12:TYPE 2;
|
||||
13-16:TYPE 0;
|
||||
17:TYPE 0;
|
||||
|
||||
|
||||
0:DESTROY 12;
|
||||
1-3:DESTROY 13;
|
||||
4-6:DESTROY 14;
|
||||
7-9:DESTROY 15;
|
||||
10-11:DESTROY 16;
|
||||
17:DESTROY 15;
|
||||
|
||||
1-11:Dmg1Lnk 17;
|
||||
|
||||
END OF FILE
|
||||
```
|
||||
|
||||
So it seems this visual data can have quite complicated attributes. At a minimum
|
||||
we see:
|
||||
|
||||
* `TYPE`
|
||||
* `DEF`
|
||||
* `DESTROY`
|
||||
* `Dmg1Lnk`
|
||||
|
||||
The `type` field **may** tell us what format each sprite is in.
|
||||
|
||||
## OBJ container structure
|
||||
|
||||
`.obj` files represent visual data. They contain a number of sprites, which are
|
||||
@@ -206,8 +132,8 @@ in the CENTER position. Interesting.
|
||||
| 0x0004 | x,y size (16 bits each) |
|
||||
| 0x0008 | ? (blank in all cases so far)
|
||||
| 0x000c | Size of remaining pixeldata |
|
||||
| 0x0010 | Padding? |
|
||||
| 0x0014 | Padding? |
|
||||
| 0x0010 | Set in `WarHammer.ani` |
|
||||
| 0x0014 | ? (blank in all cases so far) |
|
||||
|
||||
The volume represented by a cell is a little odd. We see three faces of a fake
|
||||
3D volume of size 64x64x32(ish). This is presented in an isomorphic fashion, so
|
||||
@@ -512,3 +438,144 @@ break *0x41DD10
|
||||
This lets me focus very narrowly on what happens when loading sprites, and
|
||||
might give clues.
|
||||
|
||||
## Assign
|
||||
|
||||
The `Assign/` directory contains a matching `.asn` file for each `Obj/*.obj`.
|
||||
It's a plain-text format which seems to assign properties to frames, and has
|
||||
references to a `<name>.flc` file which does not exist in the tree.
|
||||
|
||||
Theory: .obj files were originally generated from `.flc` files. This is an
|
||||
AutoDesk format for visual data, so this suggests the .obj files contain pixels
|
||||
\o/
|
||||
|
||||
`blank.asn` references 6 frames (0-5):
|
||||
|
||||
```
|
||||
# single pixel tile
|
||||
# transpix.obj/.asn
|
||||
#/--> transpix.flc
|
||||
#
|
||||
|
||||
0-5:DEF 1;
|
||||
|
||||
|
||||
0-5:TYPE 13;
|
||||
|
||||
|
||||
END OF FILE
|
||||
```
|
||||
|
||||
`jungtil.asn` references 18 frames (0-17):
|
||||
|
||||
```
|
||||
# jungle floor
|
||||
# jungtil.obj/.asn
|
||||
# /--> d:\warflics\missions\jungtil.flc
|
||||
#
|
||||
|
||||
0:DEF 2;
|
||||
1-11:DEF 454;
|
||||
|
||||
|
||||
#damaged frames!!!!
|
||||
12:DEF 2;
|
||||
13-16:DEF 454;
|
||||
17:DEF 454;
|
||||
|
||||
|
||||
0:TYPE 2;
|
||||
1-11:TYPE 0;
|
||||
12:TYPE 2;
|
||||
13-16:TYPE 0;
|
||||
17:TYPE 0;
|
||||
|
||||
|
||||
0:DESTROY 12;
|
||||
1-3:DESTROY 13;
|
||||
4-6:DESTROY 14;
|
||||
7-9:DESTROY 15;
|
||||
10-11:DESTROY 16;
|
||||
17:DESTROY 15;
|
||||
|
||||
1-11:Dmg1Lnk 17;
|
||||
|
||||
END OF FILE
|
||||
```
|
||||
|
||||
So it seems this visual data can have quite complicated attributes. We see:
|
||||
|
||||
* `DEF`
|
||||
* `DESTROY`
|
||||
* `Dmg1Lnk`
|
||||
* `MacroLnk`
|
||||
* `TYPE`
|
||||
|
||||
Each command takes the form:
|
||||
|
||||
```
|
||||
<sprite-spec>:<command> <args>;
|
||||
```
|
||||
|
||||
The sprite-spec can either be a single number, or a range of n-m.
|
||||
|
||||
### `DESTROY`
|
||||
|
||||
### `DEF`
|
||||
|
||||
I wonder if this `DEF`ines sprites an object, represented by an integer. In
|
||||
`Data/Defs.dat`, we see sections that `EDIT <integer>`. Perhaps the two point to
|
||||
the same thing?
|
||||
|
||||
`Assign/cabbage.asn` has `DEF 17` and `DEF 20`, covering two ranges of sprites.
|
||||
`Data/Defs.dat` has `EDIT 17 # TREE LEAVES` and `EDIT 20 # NON-CENTERED CENTER
|
||||
BUSHES`.
|
||||
|
||||
`Assign/TZEENTCH.asn` has `DEF 24` and `DEF 25`. `Data/Defs.dat` has `EDIT 24 #
|
||||
SWITCHES & HOSES AS CENTER OBJS...RIGHT` and `EDIT 25 # SWITCHES & HOSES AS
|
||||
CENTER OBJS...LEFT`.
|
||||
|
||||
`Assign/altar.asn` has `DEF 232`, and `Data/Defs.data` has `EDIT 232 # MRS
|
||||
BUILDING PIECES - POOL TABLE`.
|
||||
|
||||
These all seem really close.
|
||||
|
||||
Then we have `EDIT 19 #big rocks` and these instances of `DEF 19;`:
|
||||
|
||||
```
|
||||
Assign/grayroks.asn:5:0-7:DEF 19;
|
||||
Assign/grayroks.asn:9:8-15:DEF 19;
|
||||
Assign/sn_roks.asn:6:0-7:DEF 19;
|
||||
Assign/sn_roks.asn:10:8-15:DEF 19;
|
||||
```
|
||||
|
||||
The sprites for `grayroks` depicts some grey rocks, while `sn_roks` depicts
|
||||
the same, but with snow on them. So this seems correct to me.
|
||||
|
||||
Does each object ID represent a unique thing? Or is it an index for a set of
|
||||
characteristics that may be shared across multiple things? A set could quite
|
||||
easily specify both `grayroks` and `sn_roks` for a map...
|
||||
|
||||
### `Dmg1Lnk`
|
||||
|
||||
This command is fairly easy. It takes a sprite-spec and says "if damage has been
|
||||
sustained and you would normally display these sprites, display this sprite
|
||||
instead". This is particularly obvious for the `cabbage` pair.
|
||||
|
||||
### `MacroLnk`
|
||||
|
||||
### `TYPE`
|
||||
|
||||
|
||||
## WarHammer.ani
|
||||
|
||||
This 400MiB file appears to be a standard object file, it's just very large.
|
||||
The directory contains 188,286 sprites!
|
||||
|
||||
The field at 0x10 in each sprite header is set in `WarHammer.ani`, but not in
|
||||
the other object files encountered so far. However, it seems to be
|
||||
set statically to the bytes `[212 113 59 1]` for all of them.
|
||||
|
||||
Assuming ~1000 sprites per character, `WarHammer.ani` contains 188 characters.
|
||||
|
||||
Two other files have been implicated in animation - `Data/AniObDefs.dat` and
|
||||
`Idx/WarHammer.idx`. More on those in [ani.md](ani.md)
|
||||
|
35
go.mod
35
go.mod
@@ -1,16 +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/hajimehoshi/ebiten v1.11.0-alpha.2.0.20200101150127-38815ba801a5
|
||||
github.com/jfreymuth/oggvorbis v1.0.1 // indirect
|
||||
github.com/BurntSushi/toml v1.4.0
|
||||
github.com/emef/bitfield v0.0.0-20170503144143-7d3f8f823065
|
||||
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
|
||||
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5 // indirect
|
||||
golang.org/x/image v0.0.0-20200119044424-58c23975cae1 // indirect
|
||||
golang.org/x/mobile v0.0.0-20200329125638-4c31acba0007 // indirect
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d // 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
|
||||
)
|
||||
|
123
go.sum
123
go.sum
@@ -1,89 +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/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/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/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/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=
|
||||
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-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/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-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=
|
||||
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=
|
||||
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/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-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=
|
||||
|
139
internal/assetstore/ani.go
Normal file
139
internal/assetstore/ani.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package assetstore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.ur.gs/lupine/ordoor/internal/data"
|
||||
"code.ur.gs/lupine/ordoor/internal/idx"
|
||||
)
|
||||
|
||||
type Animation struct {
|
||||
Frames []*Sprite
|
||||
}
|
||||
|
||||
func (a *AssetStore) AnimationsIndex() (*idx.Idx, error) {
|
||||
if a.idx != nil {
|
||||
return a.idx, nil
|
||||
}
|
||||
|
||||
filename, err := a.lookup("WarHammer", "idx", "Idx")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
idx, err := idx.Load(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a.idx = idx
|
||||
return idx, nil
|
||||
}
|
||||
|
||||
func (a *AssetStore) AnimationsObject() (*Object, error) {
|
||||
if a.aniObj != nil {
|
||||
return a.aniObj, nil
|
||||
}
|
||||
|
||||
filename, err := a.lookup("WarHammer", "ani", "Anim")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
obj, err := a.ObjectByPath(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a.aniObj = obj
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
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 := realIdx.Groups[groupIdx]
|
||||
if group.Spec.Count == 0 {
|
||||
return &Animation{}, nil
|
||||
}
|
||||
|
||||
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)
|
||||
count := last - first + 1
|
||||
|
||||
sprites, err := obj.Sprites(first, count)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
@@ -2,23 +2,25 @@ package assetstore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"code.ur.gs/lupine/ordoor/internal/data"
|
||||
)
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
|
||||
const (
|
||||
RootDir = "" // Used in the entryMap for entries pertaining to the root dir
|
||||
"code.ur.gs/lupine/ordoor/internal/config"
|
||||
"code.ur.gs/lupine/ordoor/internal/data"
|
||||
"code.ur.gs/lupine/ordoor/internal/idx"
|
||||
"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
|
||||
@@ -26,27 +28,48 @@ 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" }, ...}
|
||||
entries entryMap
|
||||
|
||||
// These members are used to store things we've already loaded
|
||||
fonts map[string]*Font
|
||||
maps map[string]*Map
|
||||
menus map[string]*Menu
|
||||
objs map[string]*Object
|
||||
sets map[string]*Set
|
||||
sounds map[string]*Sound
|
||||
strings *data.I18n
|
||||
aniObj *Object
|
||||
cursorObj *Object
|
||||
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
|
||||
sets map[string]*Set
|
||||
sounds map[string]*Sound
|
||||
strings *data.I18n
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -64,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)
|
||||
@@ -84,8 +107,15 @@ func (a *AssetStore) Refresh() error {
|
||||
}
|
||||
|
||||
// Refresh
|
||||
a.aniObj = nil
|
||||
a.cursorObj = nil
|
||||
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)
|
||||
@@ -108,13 +138,13 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", os.ErrNotExist
|
||||
return "", fmt.Errorf("file %q does not exist", filename)
|
||||
}
|
||||
|
||||
func canonical(s string) string {
|
||||
|
100
internal/assetstore/cursor.go
Normal file
100
internal/assetstore/cursor.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package assetstore
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
||||
// These are just offsets into the Cursors.cur file
|
||||
type CursorName int
|
||||
|
||||
type Cursor struct {
|
||||
Hotspot image.Point
|
||||
Image *ebiten.Image
|
||||
}
|
||||
|
||||
const (
|
||||
UltPointer CursorName = 0
|
||||
ChaosPointer CursorName = 1
|
||||
UltWaiter CursorName = 2
|
||||
ChaosWaiter CursorName = 3
|
||||
|
||||
// I think these cursors are used in drag + drop
|
||||
ChaosMarine1 CursorName = 4
|
||||
ChaosMarine2 CursorName = 5
|
||||
ChaosMarine3 CursorName = 6
|
||||
|
||||
UltMarine1 CursorName = 7
|
||||
UltMarine2 CursorName = 8
|
||||
UltMarine3 CursorName = 9
|
||||
UltMarine4 CursorName = 10
|
||||
UltMarine5 CursorName = 11
|
||||
|
||||
ChaosHeavy1 CursorName = 12
|
||||
ChaosHeavy2 CursorName = 13
|
||||
|
||||
UltHeavy1 CursorName = 14
|
||||
UltHeavy2 CursorName = 15
|
||||
UltHeavy3 CursorName = 16
|
||||
UltHeavy4 CursorName = 17
|
||||
UltHeavy5 CursorName = 18
|
||||
UltHeavy6 CursorName = 19
|
||||
|
||||
ChaosTerminator1 CursorName = 20
|
||||
ChaosTerminator2 CursorName = 21
|
||||
|
||||
UltTerminator1 CursorName = 22
|
||||
UltTerminator2 CursorName = 23
|
||||
UltTerminator3 CursorName = 24
|
||||
UltTerminator4 CursorName = 25
|
||||
UltTerminator5 CursorName = 26
|
||||
|
||||
Deny CursorName = 27 // Red X
|
||||
|
||||
UltLogo CursorName = 28
|
||||
UltSquadMarine CursorName = 29
|
||||
UltSquadHeavy CursorName = 30
|
||||
UltSquadAssault CursorName = 31
|
||||
|
||||
UltCaptain CursorName = 32
|
||||
UltChaplain CursorName = 33 // (maybe?)
|
||||
UltApothecary CursorName = 34
|
||||
UltTechmarine CursorName = 35
|
||||
UltLibrarian CursorName = 36
|
||||
|
||||
DenyAgain CursorName = 37 // Identical to Deny as far as I can see *shrug*
|
||||
)
|
||||
|
||||
func (a *AssetStore) Cursor(name CursorName) (*Cursor, error) {
|
||||
if cur, ok := a.cursors[name]; ok {
|
||||
return cur, nil
|
||||
}
|
||||
|
||||
if a.cursorObj == nil {
|
||||
filename, err := a.lookup("Cursors.cur", "", "Cursor")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
obj, err := a.ObjectByPath(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a.cursorObj = obj
|
||||
}
|
||||
|
||||
spr, err := a.cursorObj.Sprite(int(name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: hotspot info. We're using Cursor.cur because it's object format,
|
||||
// but we do also have .ani files that might contain hotspots.
|
||||
cur := &Cursor{Image: spr.Image}
|
||||
|
||||
a.cursors[name] = cur
|
||||
|
||||
return cur, nil
|
||||
}
|
91
internal/assetstore/data.go
Normal file
91
internal/assetstore/data.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package assetstore
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"code.ur.gs/lupine/ordoor/internal/config"
|
||||
"code.ur.gs/lupine/ordoor/internal/data"
|
||||
)
|
||||
|
||||
// Generic returns a struct containing a grab-bag of otherwise-unrelated data
|
||||
// TODO: it would be nice if this could be cleaner
|
||||
func (a *AssetStore) Generic() (*data.Generic, error) {
|
||||
if a.generic != nil {
|
||||
return a.generic, nil
|
||||
}
|
||||
|
||||
filename, err := a.lookup("GenericData", "dat", "Data")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
generic, err := data.LoadGeneric(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// These changes are made so data in generic plays nicer with assetstore
|
||||
for i, filename := range generic.CampaignMaps {
|
||||
generic.CampaignMaps[i] =
|
||||
strings.TrimSuffix(filename, filepath.Ext(filename))
|
||||
}
|
||||
|
||||
a.generic = generic
|
||||
|
||||
return generic, nil
|
||||
}
|
||||
|
||||
func (a *AssetStore) DefaultOptions() (*config.Options, error) {
|
||||
cfg := &config.Options{}
|
||||
g, err := a.Generic()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg.PlayMovies = intToBool(g.Options[data.OptionMovies])
|
||||
cfg.PlayMusic = intToBool(g.Options[data.OptionMusic])
|
||||
cfg.CombatVoices = intToBool(g.Options[data.OptionCombatVoices])
|
||||
cfg.ShowGrid = intToBool(g.Options[data.OptionGrid])
|
||||
cfg.ShowPaths = intToBool(g.Options[data.OptionShowPaths])
|
||||
cfg.PointSaving = intToBool(g.Options[data.OptionPointSave])
|
||||
cfg.AutoCutLevel = intToBool(g.Options[data.OptionAutoCutLevel])
|
||||
cfg.Animations = intToBool(g.Options[data.OptionShowUnitAnimations])
|
||||
|
||||
// These are overrides of data.OptionCombatResolution. *This* default from
|
||||
// 1998 is no good at all!
|
||||
cfg.XRes = 1280
|
||||
cfg.YRes = 1024
|
||||
|
||||
cfg.MusicVolume = g.Options[data.OptionMusicVolume]
|
||||
cfg.SFXVolume = g.Options[data.OptionSoundEffectsVolume]
|
||||
|
||||
cfg.UnitSpeed = g.Options[data.OptionUnitAnimationSpeed]
|
||||
cfg.AnimSpeed = g.Options[data.OptionEffectAnimationSpeed]
|
||||
|
||||
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
|
||||
}
|
39
internal/assetstore/image.go
Normal file
39
internal/assetstore/image.go
Normal 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
|
||||
}
|
@@ -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
|
||||
@@ -77,6 +74,11 @@ func (m *Map) LoadSprites() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// FIXME: get rid of this
|
||||
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.
|
||||
// They should be rendered from first to last to get the correct ordering
|
||||
func (m *Map) SpritesForCell(x, y, z int) ([]*Sprite, error) {
|
||||
@@ -90,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())
|
||||
@@ -100,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
|
||||
}
|
||||
|
@@ -1,23 +1,25 @@
|
||||
package assetstore
|
||||
|
||||
import (
|
||||
"github.com/hajimehoshi/ebiten"
|
||||
"log"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
|
||||
"code.ur.gs/lupine/ordoor/internal/menus"
|
||||
)
|
||||
|
||||
type Menu struct {
|
||||
assets *AssetStore
|
||||
fonts []*Font // TODO: place the fonts directly into the relevant records
|
||||
obj *Object // TODO: handle multiple objects in the menu
|
||||
raw *menus.Menu // TODO: remove raw
|
||||
assets *AssetStore
|
||||
fonts []*Font // TODO: place the fonts directly into the relevant records
|
||||
objects []*Object // TODO: place the objects directly into the relevant records
|
||||
raw *menus.Menu // TODO: remove raw
|
||||
|
||||
Name string
|
||||
}
|
||||
|
||||
// FIXME: don't expose this
|
||||
func (m *Menu) Records() []*menus.Record {
|
||||
return m.raw.Records
|
||||
func (m *Menu) Groups() []*menus.Group {
|
||||
return m.raw.Groups
|
||||
}
|
||||
|
||||
// FIXME: don't expose this
|
||||
@@ -25,10 +27,10 @@ func (m *Menu) Font(idx int) *Font {
|
||||
return m.fonts[idx]
|
||||
}
|
||||
|
||||
func (m *Menu) Images(start, count int) ([]*ebiten.Image, error) {
|
||||
func (m *Menu) Images(objIdx, start, count int) ([]*ebiten.Image, error) {
|
||||
out := make([]*ebiten.Image, count)
|
||||
|
||||
sprites, err := m.Sprites(start, count)
|
||||
sprites, err := m.Sprites(objIdx, start, count)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -40,23 +42,12 @@ func (m *Menu) Images(start, count int) ([]*ebiten.Image, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (m *Menu) Sprites(start, count int) ([]*Sprite, error) {
|
||||
out := make([]*Sprite, count)
|
||||
|
||||
for i := start; i < start+count; i++ {
|
||||
sprite, err := m.Sprite(i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out[i-start] = sprite
|
||||
}
|
||||
|
||||
return out, nil
|
||||
func (m *Menu) Sprites(objIdx, start, count int) ([]*Sprite, error) {
|
||||
return m.objects[objIdx].Sprites(start, count)
|
||||
}
|
||||
|
||||
func (m *Menu) Sprite(idx int) (*Sprite, error) {
|
||||
return m.obj.Sprite(idx)
|
||||
func (m *Menu) Sprite(objIdx, idx int) (*Sprite, error) {
|
||||
return m.objects[objIdx].Sprite(idx)
|
||||
}
|
||||
|
||||
func (a *AssetStore) Menu(name string) (*Menu, error) {
|
||||
@@ -71,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
|
||||
}
|
||||
@@ -88,34 +79,45 @@ 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)
|
||||
|
||||
obj, err := a.loadMenuObject(raw) // TODO: multiple objects
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
menu := &Menu{
|
||||
assets: a,
|
||||
fonts: fonts,
|
||||
obj: obj,
|
||||
raw: raw,
|
||||
Name: name,
|
||||
assets: a,
|
||||
fonts: fonts,
|
||||
objects: objects,
|
||||
raw: raw,
|
||||
Name: name,
|
||||
}
|
||||
|
||||
a.menus[name] = menu
|
||||
return menu, nil
|
||||
}
|
||||
|
||||
func (a *AssetStore) loadMenuObject(menu *menus.Menu) (*Object, error) {
|
||||
filename := menu.ObjectFiles[0]
|
||||
filename, err := a.lookup(filename, "", "Menu") // Extension already present
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func (a *AssetStore) loadMenuObjects(menu *menus.Menu) ([]*Object, error) {
|
||||
out := make([]*Object, len(menu.ObjectFiles))
|
||||
for i, name := range menu.ObjectFiles {
|
||||
filename, err := a.lookup(name, "", "Menu") // Extension already present
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
obj, err := a.ObjectByPath(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out[i] = obj
|
||||
}
|
||||
|
||||
return a.ObjectByPath(filename)
|
||||
return out, nil
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
@@ -90,6 +90,21 @@ func (o *Object) LoadSprites() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Object) Sprites(start, count int) ([]*Sprite, error) {
|
||||
out := make([]*Sprite, count)
|
||||
|
||||
for i := start; i < start+count; i++ {
|
||||
sprite, err := o.Sprite(i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out[i-start] = sprite
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (o *Object) Sprite(idx int) (*Sprite, error) {
|
||||
if sprite := o.sprites[idx]; sprite != nil {
|
||||
return sprite, nil
|
||||
@@ -102,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),
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -1,15 +1,17 @@
|
||||
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
|
||||
@@ -37,11 +39,40 @@ type Options struct {
|
||||
type Config struct {
|
||||
filename string `toml:"-"`
|
||||
|
||||
Ordoor `toml:"ordoor"`
|
||||
Options `toml:"options"`
|
||||
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:"-"`
|
||||
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)
|
||||
@@ -51,7 +82,21 @@ 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 {
|
||||
var empty Options
|
||||
|
||||
return c.Options == empty
|
||||
}
|
||||
|
||||
func (c *Config) Save() error {
|
||||
@@ -64,9 +109,14 @@ 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")
|
||||
}
|
||||
|
||||
c.Options = *c.Defaults
|
||||
|
||||
return c.Save()
|
||||
}
|
||||
|
||||
func (o *Options) ResolutionIndex() int {
|
||||
|
272
internal/data/has_action.go
Normal file
272
internal/data/has_action.go
Normal file
@@ -0,0 +1,272 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/emef/bitfield"
|
||||
|
||||
"code.ur.gs/lupine/ordoor/internal/util/asciiscan"
|
||||
)
|
||||
|
||||
// AnimAction represents an animation that is stored in WarHammer.ani
|
||||
type AnimAction int
|
||||
|
||||
// CharacterType represents one of the different types of character in the game.
|
||||
//
|
||||
// TODO: can we load the list of character types anywhere or is it hardcoded in
|
||||
// the original too?
|
||||
type CharacterType int
|
||||
|
||||
const (
|
||||
AnimActionNone AnimAction = 0
|
||||
AnimActionAnim AnimAction = 1
|
||||
AnimActionWalk AnimAction = 2
|
||||
AnimActionExplosion AnimAction = 3
|
||||
AnimActionProjectile AnimAction = 4
|
||||
AnimActionSmoke AnimAction = 5
|
||||
AnimActionStandingShoot AnimAction = 6
|
||||
AnimActionStandingDeath AnimAction = 7
|
||||
AnimActionPain AnimAction = 8
|
||||
AnimActionSpellFx1 AnimAction = 9
|
||||
AnimActionSpellFx2 AnimAction = 10
|
||||
AnimActionSpellFx3 AnimAction = 11
|
||||
AnimActionSpellFx4 AnimAction = 12
|
||||
AnimActionSpellFx5 AnimAction = 13
|
||||
AnimActionRun AnimAction = 14
|
||||
AnimActionCrouch AnimAction = 15
|
||||
AnimActionStand AnimAction = 16
|
||||
AnimActionStandingReady AnimAction = 17
|
||||
AnimActionStandingUnready AnimAction = 18
|
||||
AnimActionCrouchingReady AnimAction = 19
|
||||
AnimActionCrouchingUnready AnimAction = 20
|
||||
AnimActionCrouchingShoot AnimAction = 21
|
||||
AnimActionStandingGrenade AnimAction = 22
|
||||
AnimActionCrouchingGrenade AnimAction = 23
|
||||
AnimActionDrawMelee AnimAction = 24
|
||||
AnimActionSlash AnimAction = 25
|
||||
AnimActionStab AnimAction = 26
|
||||
AnimActionBlown AnimAction = 27
|
||||
AnimActionCrouchingDeath AnimAction = 28
|
||||
AnimActionJump AnimAction = 29
|
||||
AnimActionHeal AnimAction = 30
|
||||
AnimActionTechWork AnimAction = 31
|
||||
AnimActionCast AnimAction = 32
|
||||
AnimActionShoot AnimAction = 33
|
||||
AnimActionDeath AnimAction = 34
|
||||
AnimActionFromWarp AnimAction = 35
|
||||
|
||||
AnimActionStart = AnimActionNone
|
||||
AnimActionEnd = AnimActionFromWarp
|
||||
AnimActionCount = AnimActionEnd - AnimActionStart + 1
|
||||
|
||||
// FIXME: indexed from 1, very annoying
|
||||
CharacterTypeTactical CharacterType = 1
|
||||
CharacterTypeAssault CharacterType = 2
|
||||
CharacterTypeDevastator CharacterType = 3
|
||||
CharacterTypeTerminator CharacterType = 4
|
||||
CharacterTypeApothecary CharacterType = 5
|
||||
CharacterTypeTechmarine CharacterType = 6
|
||||
CharacterTypeChaplain CharacterType = 7
|
||||
CharacterTypeLibrarian CharacterType = 8
|
||||
CharacterTypeCaptain CharacterType = 9
|
||||
CharacterTypeChaosMarine CharacterType = 10
|
||||
CharacterTypeChaosLord CharacterType = 11
|
||||
CharacterTypeChaosChaplain CharacterType = 12
|
||||
CharacterTypeChaosSorcerer CharacterType = 13
|
||||
CharacterTypeChaosTerminator CharacterType = 14
|
||||
CharacterTypeKhorneBerserker CharacterType = 15
|
||||
CharacterTypeBloodThirster CharacterType = 16
|
||||
CharacterTypeBloodLetter CharacterType = 17
|
||||
CharacterTypeFleshHound CharacterType = 18
|
||||
CharacterTypeLordOfChange CharacterType = 19
|
||||
CharacterTypeFlamer CharacterType = 20
|
||||
CharacterTypePinkHorror CharacterType = 21
|
||||
CharacterTypeBlueHorror CharacterType = 22
|
||||
CharacterTypeChaosCultist CharacterType = 23
|
||||
|
||||
CharacterTypeStart = CharacterTypeTactical
|
||||
CharacterTypeEnd = CharacterTypeChaosCultist
|
||||
CharacterTypeCount = CharacterTypeEnd - CharacterTypeStart + 1
|
||||
)
|
||||
|
||||
// HasAction tells us whether a character has an animation or not.
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer scanner.Close()
|
||||
|
||||
out := &HasAction{
|
||||
bits: bitfield.New(int(CharacterTypeCount) * int(AnimActionCount)),
|
||||
}
|
||||
|
||||
// Reuse this for every loop
|
||||
var actions [AnimActionCount]bool
|
||||
ptrs := make([]*bool, len(actions))
|
||||
for i, _ := range actions {
|
||||
ptrs[i] = &actions[i]
|
||||
}
|
||||
|
||||
for c := CharacterTypeStart; c <= CharacterTypeEnd; c++ {
|
||||
if err := scanner.ConsumeBoolPtrs(ptrs...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for j, value := range actions {
|
||||
a := AnimActionStart + AnimAction(j)
|
||||
|
||||
out.set(c, a, value)
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (h *HasAction) Check(c CharacterType, a AnimAction) bool {
|
||||
return h.bits.Test(h.offset(c, a))
|
||||
}
|
||||
|
||||
func (h *HasAction) offset(c CharacterType, a AnimAction) uint32 {
|
||||
// Best to view this as a 2D array with CharacterTypeCount * AnimActionCount elements
|
||||
i := uint32(c - CharacterTypeStart)
|
||||
j := uint32(a - AnimActionStart)
|
||||
|
||||
return (i * uint32(AnimActionCount)) + j
|
||||
}
|
||||
|
||||
func (h *HasAction) set(c CharacterType, a AnimAction, value bool) {
|
||||
if value {
|
||||
h.bits.Set(h.offset(c, a))
|
||||
} else {
|
||||
h.bits.Clear(h.offset(c, a))
|
||||
}
|
||||
}
|
||||
|
||||
// Actions returns the list of animations that a character type has
|
||||
func (h *HasAction) Actions(c CharacterType) []AnimAction {
|
||||
var out []AnimAction
|
||||
|
||||
for j := AnimActionStart; j < AnimActionCount; j++ {
|
||||
if h.Check(c, j) {
|
||||
out = append(out, j)
|
||||
}
|
||||
}
|
||||
|
||||
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++ {
|
||||
fmt.Printf("%.2d", int(a))
|
||||
for c := CharacterTypeStart; c <= CharacterTypeEnd; c++ {
|
||||
if h.Check(c, a) {
|
||||
fmt.Print(" x ")
|
||||
} else {
|
||||
fmt.Print(" ")
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("")
|
||||
}
|
||||
}
|
@@ -4,8 +4,10 @@ import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -19,13 +21,26 @@ type SpriteHeader struct {
|
||||
Width uint16
|
||||
Height uint16
|
||||
Padding1 uint32 // I don't think this is used. Could be wrong.
|
||||
PixelSize uint32 // Size of PixelData, excluding this sprite header
|
||||
Padding2 uint64 // I don't think this is used either. Could be wrong.
|
||||
PixelSize uint32
|
||||
Unknown1 [4]byte // ??? Only observed in `WarHammer.ani` so far
|
||||
Padding2 uint32 // I don't think this is used either. Could be wrong.
|
||||
}
|
||||
|
||||
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] != 212 || s.Unknown1[1] != 113 || s.Unknown1[2] != 59 || s.Unknown1[3] != 1 {
|
||||
log.Printf("Value of Unknown1 field: %v", s.Unknown1)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove 24 bytes from passed-in size to account for the header
|
||||
@@ -42,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,
|
||||
}
|
||||
}
|
||||
|
||||
|
38
internal/flow/bridge.go
Normal file
38
internal/flow/bridge.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package flow
|
||||
|
||||
func (f *Flow) linkBridge() {
|
||||
// FIXME: sometimes these doors are frozen, depending on ship state, but we
|
||||
// don't implement that yet.
|
||||
|
||||
f.onClick(bridge, "2.1", f.setReturningDriver(bridge, briefing)) // Mission briefing clickable
|
||||
f.onClick(bridge, "2.2", f.setReturningDriver(bridge, choices)) // Options door hotspot
|
||||
f.onClick(bridge, "2.4", f.playNextScenario(bridge)) // Enter combat door hotspot
|
||||
f.setFreeze(bridge, "2.6", true) // TODO: Vehicle configure door hotspot
|
||||
|
||||
// FIXME: setReturningDriver would leave behind junk
|
||||
f.onClick(bridge, "2.8", f.setDriver(arrange)) // Squads configure door hotspot.
|
||||
|
||||
// link children
|
||||
f.linkBriefing()
|
||||
f.linkChoices()
|
||||
f.linkMainGame()
|
||||
f.linkArrange()
|
||||
}
|
||||
|
||||
func (f *Flow) linkBriefing() {
|
||||
f.onClick(briefing, "3.1", f.setDriver(bridge))
|
||||
}
|
||||
|
||||
func (f *Flow) linkArrange() {
|
||||
// FIXME: we should be operating on game data in here
|
||||
f.onClick(arrange, "8.1", f.setDriver(bridge)) // Return to bridge ("cathedral")
|
||||
f.onClick(arrange, "8.3", f.setDriver(configureUltEquip)) // Configure squads
|
||||
|
||||
f.linkConfigureUltEquip()
|
||||
}
|
||||
|
||||
func (f *Flow) linkConfigureUltEquip() {
|
||||
// FIXME: we should be modifying loadouts of selected squad members here
|
||||
|
||||
f.onClick(configureUltEquip, "8.1", f.setDriver(bridge)) // Return to bridge
|
||||
}
|
20
internal/flow/choices.go
Normal file
20
internal/flow/choices.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package flow
|
||||
|
||||
func (f *Flow) linkChoices() {
|
||||
f.onClick(choices, "2.1", f.setReturningDriver(choices, loadGame)) // Load another game button
|
||||
f.onClick(choices, "2.2", f.setReturningDriver(choices, saveGame)) // Save this game button
|
||||
f.onClick(choices, "2.3", f.setReturningDriver(choices, options)) // More options button
|
||||
f.onClick(choices, "2.4", func() { // New Game button. FIXME: should ask about the emperor
|
||||
f.ship.Reset() // Throws away in-progress game
|
||||
f.reset()
|
||||
})
|
||||
|
||||
f.onClick(choices, "2.5", f.setReturningDriver(choices, credits)) // Credits button
|
||||
f.onClick(choices, "2.6", f.setExit) // Quit button. FIXME: should ask about the emperor
|
||||
f.onClick(choices, "2.7", f.returnToLastDriver(choices)) // Back button
|
||||
|
||||
// loadGame is linked by main
|
||||
f.linkSaveGame()
|
||||
// options is linked by main
|
||||
f.linkCredits()
|
||||
}
|
8
internal/flow/credits.go
Normal file
8
internal/flow/credits.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package flow
|
||||
|
||||
func (f *Flow) linkCredits() {
|
||||
// Clicking anywhere in credits should return us
|
||||
f.onClick(credits, "1", f.returnToLastDriver(credits))
|
||||
|
||||
// TODO: lots of text
|
||||
}
|
145
internal/flow/drivers.go
Normal file
145
internal/flow/drivers.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package flow
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
||||
"code.ur.gs/lupine/ordoor/internal/ui"
|
||||
)
|
||||
|
||||
type driverName string
|
||||
|
||||
const (
|
||||
// Names of all the drivers
|
||||
main driverName = "Main"
|
||||
levelPly driverName = "LevelPly"
|
||||
singles driverName = "Singles"
|
||||
randomMap driverName = "RandomMap"
|
||||
newGame driverName = "NewGame"
|
||||
loadGame driverName = "LoadGame"
|
||||
options driverName = "Options"
|
||||
kbd driverName = "Keyboard"
|
||||
bridge driverName = "Bridge"
|
||||
briefing driverName = "Briefing"
|
||||
choices driverName = "Choices"
|
||||
saveGame driverName = "SaveGame"
|
||||
credits driverName = "Credits"
|
||||
arrange driverName = "Arrange"
|
||||
|
||||
configureUltEquip driverName = "Configure_UltEquip"
|
||||
configureVehiclesUltra driverName = "Configure_Vehicles_Ultra"
|
||||
|
||||
mainGame driverName = "MainGame"
|
||||
)
|
||||
|
||||
var (
|
||||
driverNames = []driverName{
|
||||
main, levelPly, singles, randomMap, newGame, loadGame, options, kbd,
|
||||
bridge, briefing, choices, saveGame, credits, arrange,
|
||||
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]
|
||||
if !ok {
|
||||
f.exit = fmt.Errorf("Couldn't work out where to return to from %v", from)
|
||||
return
|
||||
}
|
||||
|
||||
delete(f.returns, from)
|
||||
|
||||
f.setDriverNow(to)
|
||||
}
|
||||
}
|
||||
|
||||
// from is the child menu, to is the parent
|
||||
func (f *Flow) returnToLastDriverNow(from driverName) error {
|
||||
to, ok := f.returns[from]
|
||||
if !ok {
|
||||
return fmt.Errorf("Couldn't work out where to return to from %v", from)
|
||||
}
|
||||
|
||||
delete(f.returns, from)
|
||||
|
||||
f.setDriverNow(to)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Flow) setDriver(name driverName) func() {
|
||||
return func() {
|
||||
f.setDriverNow(name)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Flow) setDriverNow(name driverName) {
|
||||
f.current = f.drivers[name]
|
||||
}
|
||||
|
||||
// from is the parent menu, to is the child
|
||||
func (f *Flow) setReturningDriver(from, to driverName) func() {
|
||||
return func() {
|
||||
f.setReturningDriverNow(from, to)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Flow) setReturningDriverNow(from, to driverName) {
|
||||
f.returns[to] = from
|
||||
f.setDriverNow(to)
|
||||
}
|
330
internal/flow/flow.go
Normal file
330
internal/flow/flow.go
Normal file
@@ -0,0 +1,330 @@
|
||||
package flow
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
"code.ur.gs/lupine/ordoor/internal/data"
|
||||
"code.ur.gs/lupine/ordoor/internal/scenario"
|
||||
"code.ur.gs/lupine/ordoor/internal/ship"
|
||||
"code.ur.gs/lupine/ordoor/internal/ui"
|
||||
)
|
||||
|
||||
// type Flow is responsible for wiring up UI elements to each other and ensuring
|
||||
// they behave as expected. This includes forward / back buttons to switch
|
||||
// between screens, loading and saving options, launching a scenario, etc
|
||||
type Flow struct {
|
||||
assets *assetstore.AssetStore
|
||||
config *config.Config
|
||||
current *ui.Driver
|
||||
drivers map[driverName]*ui.Driver
|
||||
generic *data.Generic
|
||||
|
||||
// Some screens can be returned to from more than one place. Where this is
|
||||
// the case, instead of hardcoding it, we'll store an entry in here so we
|
||||
// know where we're going back to
|
||||
//
|
||||
// FIXME: this really suggests wiring everything up at the start is wrong.
|
||||
returns map[driverName]driverName
|
||||
|
||||
// If we're currently playing a scenario, it it placed here
|
||||
scenario *scenario.Scenario
|
||||
|
||||
ship *ship.Ship
|
||||
|
||||
exit error
|
||||
}
|
||||
|
||||
var (
|
||||
ErrExit = errors.New("exiting gracefully")
|
||||
|
||||
// Constants used for sliders
|
||||
|
||||
h3Slider = map[int]int{1: 8, 2: 56, 3: 110, 4: 120}
|
||||
|
||||
v10Slider = map[int]int{
|
||||
0: 0,
|
||||
10: 9, 20: 18, 30: 27, 40: 36, 50: 45,
|
||||
60: 54, 70: 63, 80: 72, 90: 81, 100: 90,
|
||||
}
|
||||
|
||||
h9Slider = map[int]int{
|
||||
0: 0,
|
||||
10: 10, 20: 20, 30: 30, 40: 40,
|
||||
50: 50, 60: 60, 70: 70, 80: 80,
|
||||
}
|
||||
)
|
||||
|
||||
func New(assets *assetstore.AssetStore, config *config.Config, ship *ship.Ship) (*Flow, error) {
|
||||
generic, err := assets.Generic()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to read generic data: %v", err)
|
||||
}
|
||||
|
||||
out := &Flow{
|
||||
assets: assets,
|
||||
config: config,
|
||||
generic: generic,
|
||||
drivers: make(map[driverName]*ui.Driver, len(driverNames)),
|
||||
returns: make(map[driverName]driverName),
|
||||
ship: ship,
|
||||
}
|
||||
|
||||
// Load all the drivers upfront
|
||||
for _, name := range driverNames {
|
||||
driver, err := buildDriver(assets, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out.drivers[name] = driver
|
||||
}
|
||||
|
||||
out.linkDrivers()
|
||||
out.reset()
|
||||
|
||||
return out, out.exit
|
||||
}
|
||||
|
||||
func (f *Flow) SetScenario(scenario *scenario.Scenario) {
|
||||
f.current = f.drivers[mainGame]
|
||||
f.scenario = scenario
|
||||
}
|
||||
|
||||
func (f *Flow) Update(screenX, screenY int) error {
|
||||
if f.exit != nil {
|
||||
return f.exit
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if f.current != nil {
|
||||
if err := f.current.Update(screenX, screenY); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Flow) Draw(screen *ebiten.Image) error {
|
||||
if f.exit != nil {
|
||||
return f.exit
|
||||
}
|
||||
|
||||
if f.scenario != nil {
|
||||
if err := f.scenario.Draw(screen); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if f.current != nil {
|
||||
if err := f.current.Draw(screen); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Flow) Cursor() (*ebiten.Image, *ebiten.DrawImageOptions, error) {
|
||||
if f.current != nil {
|
||||
return f.current.Cursor()
|
||||
}
|
||||
|
||||
// FIXME: we should get a cursor from current all the time.
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func (f *Flow) linkDrivers() {
|
||||
// linkMain
|
||||
f.onClick(main, "2.1", f.setReturningDriver(main, newGame)) // New game
|
||||
f.onClick(main, "2.2", f.setReturningDriver(main, loadGame)) // Load game
|
||||
f.setFreeze(main, "2.3", true) // Multiplayer - disable for now
|
||||
f.onClick(main, "2.4", f.setReturningDriver(main, options)) // Options
|
||||
f.onClick(main, "2.5", f.setExit) // Quit
|
||||
|
||||
// Now link immediate children. They will link their children, and so on
|
||||
f.linkNewGame()
|
||||
f.linkLoadGame()
|
||||
// TODO: link multiplayer
|
||||
f.linkOptions()
|
||||
}
|
||||
|
||||
func maybeErr(driver driverName, err error) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("%v: %v", driver, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Flow) configureSlider(driver driverName, id string, steps map[int]int) {
|
||||
if f.exit != nil {
|
||||
return
|
||||
}
|
||||
|
||||
f.exit = f.drivers[driver].ConfigureSlider(id, steps)
|
||||
}
|
||||
|
||||
func (f *Flow) onClick(driver driverName, id string, fn func()) {
|
||||
if f.exit != nil {
|
||||
return
|
||||
}
|
||||
|
||||
f.exit = maybeErr(driver, f.drivers[driver].OnClick(id, fn))
|
||||
}
|
||||
|
||||
func (f *Flow) setFreeze(driver driverName, id string, value bool) {
|
||||
if f.exit != nil {
|
||||
return
|
||||
}
|
||||
|
||||
f.exit = maybeErr(driver, f.drivers[driver].SetFreeze(id, value))
|
||||
}
|
||||
|
||||
func (f *Flow) setValueBool(driver driverName, id string, value bool) {
|
||||
if f.exit != nil {
|
||||
return
|
||||
}
|
||||
|
||||
f.exit = f.drivers[driver].SetValueBool(id, value)
|
||||
}
|
||||
|
||||
func (f *Flow) valueBool(driver driverName, id string) bool {
|
||||
if f.exit != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var value bool
|
||||
|
||||
f.exit = f.drivers[driver].ValueBool(id, &value)
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func (f *Flow) playNextScenario(from driverName) func() {
|
||||
return func() {
|
||||
log.Printf("Loading scenario: %v", f.ship.NextScenario)
|
||||
|
||||
// TODO: we *could* load scenario assets in a separate assetstore to
|
||||
// make it easier to chuck them away at the end?
|
||||
scenario, err := scenario.NewScenario(f.assets, f.ship.NextScenario)
|
||||
if err != nil {
|
||||
f.exit = err
|
||||
return
|
||||
}
|
||||
|
||||
f.setReturningDriverNow(from, mainGame)
|
||||
f.scenario = scenario
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
if f.exit != nil {
|
||||
return
|
||||
}
|
||||
|
||||
f.exit = maybeErr(driver, f.drivers[driver].ShowDialogue(locator(driver, id)))
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
f.setDriverNow(main) // Back to the main interface
|
||||
|
||||
// Wipe out any returns that may exist
|
||||
f.returns = make(map[driverName]driverName)
|
||||
|
||||
// FIXME: these should really happen via data binding.
|
||||
f.resetLevelPlyInventorySelect()
|
||||
f.exit = f.configIntoOptions()
|
||||
}
|
||||
|
||||
func (f *Flow) setExit() {
|
||||
f.exit = ErrExit
|
||||
}
|
||||
|
||||
// TODO: convert all to locators
|
||||
func locator(driver driverName, id string) string {
|
||||
return fmt.Sprintf("%v:%v", strings.ToLower(string(driver)), id)
|
||||
}
|
25
internal/flow/keyboard.go
Normal file
25
internal/flow/keyboard.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package flow
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
func (f *Flow) linkKeyboard() {
|
||||
// Keyboard settings
|
||||
// TODO: implement keybindings save/load behaviour
|
||||
f.onClick(kbd, "3.1", f.returnToLastDriver(kbd)) // Done button
|
||||
f.onClick(kbd, "3.2", f.returnToLastDriver(kbd)) // Cancel button
|
||||
f.onClick(kbd, "3.4", func() {}) // TODO: Reset to defaults button
|
||||
|
||||
for i := 1; i <= 13; i++ {
|
||||
f.onClick(kbd, fmt.Sprintf("2.%v", i), f.captureKeybinding(i))
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Flow) captureKeybinding(forLine int) func() {
|
||||
return func() {
|
||||
log.Printf("HELLO %v", forLine)
|
||||
f.showDialogue(kbd, "4")()
|
||||
}
|
||||
}
|
6
internal/flow/load_game.go
Normal file
6
internal/flow/load_game.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package flow
|
||||
|
||||
func (f *Flow) linkLoadGame() {
|
||||
// Load game
|
||||
f.onClick(loadGame, "3.3", f.returnToLastDriver(loadGame)) // Cancel button
|
||||
}
|
205
internal/flow/main_game.go
Normal file
205
internal/flow/main_game.go
Normal file
@@ -0,0 +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() {
|
||||
f.linkMainGameActionMenu()
|
||||
f.linkMainGameInterfaceOptionsMenu()
|
||||
// 5: Holding 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
|
||||
// 12: Inventory dialogue
|
||||
f.onClick(mainGame, "12.21", f.hideDialogue(mainGame)) // Exit
|
||||
|
||||
// 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
|
||||
|
||||
// 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
|
||||
|
||||
f.onClick(mainGame, "18.12", f.setActive(mainGame, "18", false)) // Info "dialogue"
|
||||
|
||||
// 19: Turn start dialogue
|
||||
// 20: Chat menu
|
||||
|
||||
// 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
|
||||
}
|
79
internal/flow/new_game.go
Normal file
79
internal/flow/new_game.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package flow
|
||||
|
||||
import (
|
||||
"code.ur.gs/lupine/ordoor/internal/ship"
|
||||
)
|
||||
|
||||
func (f *Flow) linkNewGame() {
|
||||
// New game
|
||||
f.onClick(newGame, "2.1", f.setReturningDriver(newGame, levelPly)) // New campaign button
|
||||
f.onClick(newGame, "2.2", f.setReturningDriver(newGame, singles)) // Single scenario button
|
||||
f.onClick(newGame, "2.3", f.setReturningDriver(newGame, randomMap)) // Random scenario button
|
||||
f.onClick(newGame, "2.4", f.returnToLastDriver(newGame)) // Back button
|
||||
|
||||
f.linkLevelPly()
|
||||
f.linkSingles()
|
||||
f.linkRandomMap()
|
||||
}
|
||||
|
||||
func (f *Flow) resetLevelPlyInventorySelect() {
|
||||
// FIXME: Make the radio button respect changes via setValue
|
||||
for _, v := range []string{"2.1", "2.2", "2.3", "2.4"} {
|
||||
f.setValueBool(levelPly, v, false)
|
||||
}
|
||||
|
||||
switch f.ship.Difficulty {
|
||||
case ship.DifficultyLevelMarine:
|
||||
f.setValueBool(levelPly, "2.1", true)
|
||||
case ship.DifficultyLevelVeteran:
|
||||
f.setValueBool(levelPly, "2.2", true)
|
||||
case ship.DifficultyLevelHero:
|
||||
f.setValueBool(levelPly, "2.3", true)
|
||||
case ship.DifficultyLevelMighty:
|
||||
f.setValueBool(levelPly, "2.4", true)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Flow) linkLevelPly() {
|
||||
f.onClick(levelPly, "2.5", func() { // Back button
|
||||
f.resetLevelPlyInventorySelect() // FIXME: should use data binding
|
||||
f.returnToLastDriverNow(levelPly)
|
||||
})
|
||||
|
||||
// FIXME: we should be able to read the difficulty level from the group
|
||||
f.onClick(levelPly, "2.7", func() { // Select button
|
||||
if f.valueBool(levelPly, "2.1") {
|
||||
f.ship.Difficulty = ship.DifficultyLevelMarine
|
||||
}
|
||||
|
||||
if f.valueBool(levelPly, "2.2") {
|
||||
f.ship.Difficulty = ship.DifficultyLevelVeteran
|
||||
}
|
||||
|
||||
if f.valueBool(levelPly, "2.3") {
|
||||
f.ship.Difficulty = ship.DifficultyLevelHero
|
||||
}
|
||||
|
||||
if f.valueBool(levelPly, "2.4") {
|
||||
// FIXME: we should select a savegame. Mighty Hero disables manual saves.
|
||||
f.ship.Difficulty = ship.DifficultyLevelMighty
|
||||
}
|
||||
|
||||
f.ship.NextScenario = f.generic.CampaignMaps[0]
|
||||
|
||||
// FIXME: we should show a movie here. Need an internal SMK player first
|
||||
|
||||
f.setDriverNow(bridge)
|
||||
})
|
||||
|
||||
// Link children
|
||||
f.linkBridge()
|
||||
}
|
||||
|
||||
func (f *Flow) linkSingles() {
|
||||
f.onClick(singles, "4.11", f.returnToLastDriver(singles)) // Back button
|
||||
}
|
||||
|
||||
func (f *Flow) linkRandomMap() {
|
||||
f.onClick(randomMap, "2.19", f.returnToLastDriver(randomMap)) // Back button
|
||||
}
|
@@ -5,47 +5,39 @@ import (
|
||||
)
|
||||
|
||||
func (f *Flow) linkOptions() {
|
||||
f.onClick(options, "2.8", f.setDriver(kbd)) // Keyboard settings button
|
||||
f.onClick(options, "2.8", f.setReturningDriver(options, kbd)) // Keyboard settings button
|
||||
|
||||
f.configureSlider(options, "2.9", h3Slider) // Resolution slider
|
||||
f.configureSlider(options, "2.10", v10Slider) // Music volume slider
|
||||
f.configureSlider(options, "2.11", v10Slider) // SFX volume slider
|
||||
|
||||
f.onClick(options, "2.12", f.acceptOptions()) // OK button
|
||||
f.onClick(options, "2.24", f.cancelOptions()) // Cancel button
|
||||
f.onClick(options, "2.12", f.acceptOptions) // OK button
|
||||
f.onClick(options, "2.24", f.cancelOptions) // Cancel button
|
||||
|
||||
f.configureSlider(options, "2.26", h9Slider) // Unit speed slider
|
||||
f.configureSlider(options, "2.27", h9Slider) // Animation speed slider
|
||||
|
||||
// Keyboard settings
|
||||
// TODO: implement keybindings save/load behaviour
|
||||
f.onClick(kbd, "3.1", f.setDriver(options)) // Done button
|
||||
f.onClick(kbd, "3.2", f.setDriver(options)) // Cancel button
|
||||
f.onClick(kbd, "3.4", func() {}) // Reset to defaults button
|
||||
f.linkKeyboard()
|
||||
}
|
||||
|
||||
// FIXME: exiting is a bit OTT. Perhaps display "save failed"?
|
||||
func (f *Flow) acceptOptions() func() {
|
||||
return func() {
|
||||
if err := f.optionsIntoConfig(); err != nil {
|
||||
log.Printf("Saving options to config failed: %v", err)
|
||||
f.exit = err
|
||||
} else {
|
||||
f.setDriverNow(main)
|
||||
}
|
||||
func (f *Flow) acceptOptions() {
|
||||
if err := f.optionsIntoConfig(); err != nil {
|
||||
log.Printf("Saving options to config failed: %v", err)
|
||||
f.exit = err
|
||||
} else {
|
||||
f.returnToLastDriverNow(options)
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: again, exiting is OTT. We're just resetting the state of
|
||||
// the interface to the values in config.
|
||||
func (f *Flow) cancelOptions() func() {
|
||||
return func() {
|
||||
if err := f.configIntoOptions(); err != nil {
|
||||
log.Printf("Saving options to config failed: %v", err)
|
||||
f.exit = err
|
||||
} else {
|
||||
f.exit = f.returnToLastDriverNow(options)
|
||||
}
|
||||
func (f *Flow) cancelOptions() {
|
||||
if err := f.configIntoOptions(); err != nil {
|
||||
log.Printf("Saving options to config failed: %v", err)
|
||||
f.exit = err
|
||||
} else {
|
||||
f.exit = f.returnToLastDriverNow(options)
|
||||
}
|
||||
}
|
||||
|
10
internal/flow/save_game.go
Normal file
10
internal/flow/save_game.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package flow
|
||||
|
||||
func (f *Flow) linkSaveGame() {
|
||||
// Save game button is disabled unless a listbox item is selected
|
||||
// 3.2 is a hypertext that should be displayed when 3.1 is disabled... but
|
||||
// it has no DESC.
|
||||
f.setFreeze(saveGame, "3.1", true)
|
||||
f.onClick(saveGame, "3.1", func() {}) // TODO: Save Game button
|
||||
f.onClick(saveGame, "3.3", f.returnToLastDriver(saveGame)) // Back button
|
||||
}
|
109
internal/idx/idx.go
Normal file
109
internal/idx/idx.go
Normal file
@@ -0,0 +1,109 @@
|
||||
// package idx parses the Idx/WarHammer.idx file. It groups the sprites in
|
||||
// Anim/WarHammer.ani into playable animations.
|
||||
package idx
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
//"log"
|
||||
"os"
|
||||
//"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
NumGroups = 512 // Experimentally determined
|
||||
)
|
||||
|
||||
type Idx struct {
|
||||
Filename string
|
||||
Groups []Group
|
||||
}
|
||||
|
||||
type Group struct {
|
||||
Spec Spec
|
||||
Records []Record
|
||||
Details []Detail // Records and details are correlated by index
|
||||
}
|
||||
|
||||
// type Spec links a set of animations to a starting sprite in WarHammer.ani
|
||||
type Spec struct {
|
||||
Offset uint32 // Where the Records for this Spec are to be found
|
||||
Count uint32 // Number of Records for this Spec
|
||||
SpriteIdx uint32 // Index of the first sprite in
|
||||
}
|
||||
|
||||
type Record struct {
|
||||
// A guess, but each group of 8 records with increasing compass points share
|
||||
// this value.
|
||||
ActionID uint16
|
||||
|
||||
Compass byte // It's odd to only have one byte. Maybe Unknown1 belongs to this too?
|
||||
|
||||
Unknown1 byte // ??? Only see values 0x33 and 0x00 for librarian.
|
||||
Offset uint32 // Where the Detail for this Record is to be found.
|
||||
NumFrames uint32 // A guess, but seems to fit. Number of frames for this action + compass.
|
||||
}
|
||||
|
||||
type Detail struct {
|
||||
FirstSprite uint16 // Relative offset from the group's SpriteIdx
|
||||
LastSprite uint16 // Relative offset from the group's SpriteIdx
|
||||
Unknown1 uint16 // Could also be LastSprite? Something else? AtRestSprite?
|
||||
Unknown2 uint16 // Number of resting sprites, if we're AtRestSprite?
|
||||
|
||||
Padding [12]byte // Set to zero in the cases I've looked at so far.
|
||||
|
||||
// Remainder []byte // FIXME: no idea what this is yet, but we seem to have NumFrames*6 of them
|
||||
}
|
||||
|
||||
func Load(filename string) (*Idx, error) {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
out := &Idx{
|
||||
Filename: filename,
|
||||
Groups: make([]Group, NumGroups),
|
||||
}
|
||||
|
||||
var specs [NumGroups]Spec
|
||||
|
||||
if err := binary.Read(f, binary.LittleEndian, &specs); err != nil {
|
||||
return nil, fmt.Errorf("reading specs: %v", err)
|
||||
}
|
||||
|
||||
for i, spec := range specs {
|
||||
group := &out.Groups[i]
|
||||
group.Spec = spec
|
||||
group.Records = make([]Record, spec.Count)
|
||||
group.Details = make([]Detail, spec.Count)
|
||||
|
||||
if _, err := f.Seek(int64(spec.Offset), 0); err != nil {
|
||||
return nil, fmt.Errorf("spec %v: seeking: %v", i, err)
|
||||
}
|
||||
|
||||
// We can read all records at once
|
||||
if err := binary.Read(f, binary.LittleEndian, &group.Records); err != nil {
|
||||
return nil, fmt.Errorf("spec %v: reading records: %v", i, err)
|
||||
}
|
||||
|
||||
// But we need to step through the records to learn where to read details
|
||||
for j, rec := range group.Records {
|
||||
// group.Details[j].Remainder = make([]byte, rec.NumFrames*6)
|
||||
|
||||
if _, err := f.Seek(int64(rec.Offset), 0); err != nil {
|
||||
return nil, fmt.Errorf("spec %v, record %v: seeking to detail: %v", i, j, err)
|
||||
}
|
||||
|
||||
if err := binary.Read(f, binary.LittleEndian, &group.Details[j]); err != nil {
|
||||
return nil, fmt.Errorf("spec %v, record %v: reading detail: %v", i, j, err)
|
||||
}
|
||||
}
|
||||
|
||||
out.Groups[i] = *group
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
@@ -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,
|
||||
)
|
||||
}
|
||||
|
@@ -2,48 +2,54 @@ package menus
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.ur.gs/lupine/ordoor/internal/data"
|
||||
"code.ur.gs/lupine/ordoor/internal/util/asciiscan"
|
||||
)
|
||||
|
||||
// MenuType tells us what sort of Group we have
|
||||
type MenuType int
|
||||
|
||||
const (
|
||||
TypeStatic MenuType = 0
|
||||
TypeMenu MenuType = 1
|
||||
TypeDragMenu MenuType = 2 // Only seen in Configure_Vehicle_{Chaos,Ultra}
|
||||
TypeSimpleButton MenuType = 3
|
||||
TypeDoorHotspot MenuType = 30 // Like a button I guess? "FONTTYPE is animation speed"
|
||||
TypeDoorHotspot2 MenuType = 31 // Seems like a duplicate of the above? What's different?
|
||||
TypeLineKbd MenuType = 40
|
||||
TypeThumb MenuType = 45 // A "thumb" appears to be a vertical slider
|
||||
TypeLineBriefing MenuType = 41
|
||||
TypeInvokeButton MenuType = 50
|
||||
TypeDoorHotspot3 MenuType = 60 // Maybe? Appears in Arrange.mnu
|
||||
TypeOverlay MenuType = 61
|
||||
TypeHypertext MenuType = 70
|
||||
TypeCheckbox MenuType = 91
|
||||
TypeEditBox MenuType = 100
|
||||
TypeInventorySelect MenuType = 110
|
||||
TypeRadioButton MenuType = 120
|
||||
TypeDropdownButton MenuType = 200
|
||||
TypeComboBoxItem MenuType = 205
|
||||
TypeAnimationSample MenuType = 220
|
||||
TypeAnimationHover MenuType = 221 // FONTTYPE is animation speed. Only animate when hovered
|
||||
TypeMainButton MenuType = 228
|
||||
TypeSlider MenuType = 232
|
||||
TypeStatusBar MenuType = 233
|
||||
TypeDialogue MenuType = 300
|
||||
// SubMenuType tells us what sort of Record we have
|
||||
type SubMenuType int
|
||||
|
||||
TypeListBoxUp MenuType = 400 // FIXME: these have multiple items in MENUTYPE
|
||||
TypeListBoxDown MenuType = 405
|
||||
const (
|
||||
TypeStatic MenuType = 0
|
||||
TypeMenu MenuType = 1
|
||||
TypeDragMenu MenuType = 2 // Only seen in Configure_Vehicle_{Chaos,Ultra}
|
||||
TypeRadioMenu MenuType = 3 // ???
|
||||
TypeMainBackground MenuType = 45 // ???
|
||||
TypeDialogue MenuType = 300
|
||||
|
||||
SubTypeSimpleButton SubMenuType = 3
|
||||
SubTypeDoorHotspot1 SubMenuType = 30 // Like a button I guess? "FONTTYPE is animation speed"
|
||||
SubTypeDoorHotspot2 SubMenuType = 31 // Seems like a duplicate of the above? What's different?
|
||||
SubTypeLineKbd SubMenuType = 40
|
||||
SubTypeLineBriefing SubMenuType = 41
|
||||
SubTypeThumb SubMenuType = 45 // A "thumb" appears to be a vertical slider
|
||||
SubTypeInvokeButton SubMenuType = 50
|
||||
SubTypeClickText SubMenuType = 60
|
||||
SubTypeOverlay SubMenuType = 61
|
||||
SubTypeHypertext SubMenuType = 70
|
||||
SubTypeCheckbox SubMenuType = 91
|
||||
SubTypeEditBox SubMenuType = 100
|
||||
SubTypeInventorySelect SubMenuType = 110
|
||||
SubTypeRadioButton SubMenuType = 120
|
||||
SubTypeDropdownButton SubMenuType = 200
|
||||
SubTypeComboBoxItem SubMenuType = 205
|
||||
SubTypeAnimationSample SubMenuType = 220
|
||||
SubTypeAnimationHover SubMenuType = 221 // FONTTYPE is animation speed. Only animate when hovered
|
||||
SubTypeMainButton SubMenuType = 228
|
||||
SubTypeSlider SubMenuType = 232
|
||||
SubTypeStatusBar SubMenuType = 233
|
||||
|
||||
SubTypeListBoxUp SubMenuType = 400 // FIXME: these have multiple items in SUBMENUTYPE
|
||||
SubTypeListBoxDown SubMenuType = 405
|
||||
)
|
||||
|
||||
// FIXME: certain elements - especially overlays - don't have a DESC specified
|
||||
@@ -63,39 +69,11 @@ var TextOverrides = map[string]string{
|
||||
"main:2.7": "0.1-ordoor",
|
||||
}
|
||||
|
||||
// FIXME: The menu is specified as type 2 (button) in these cases, which is
|
||||
// weird. Make it a menu for now.
|
||||
var TypeOverrides = map[string]MenuType{
|
||||
"levelply:2": TypeMenu,
|
||||
"savegame:2": TypeMenu,
|
||||
"loadgame:2": TypeMenu,
|
||||
|
||||
// ???
|
||||
"configure_ultequip:7.5": TypeListBoxUp,
|
||||
"configure_ultequip:7.6": TypeListBoxDown,
|
||||
}
|
||||
|
||||
type Record struct {
|
||||
Menu *Menu
|
||||
Parent *Record
|
||||
Children []*Record
|
||||
|
||||
Id int
|
||||
Type MenuType
|
||||
DrawType int
|
||||
FontType int
|
||||
Active bool
|
||||
SpriteId []int
|
||||
Share int
|
||||
X int
|
||||
Y int
|
||||
|
||||
// From i18n
|
||||
Text string
|
||||
Help string
|
||||
|
||||
// FIXME: turn these into first-class data
|
||||
properties map[string]string
|
||||
var TypeOverrides = map[string]SubMenuType{
|
||||
// FIXME: These are put down as simple buttons, but it's a *lot* easier to
|
||||
// understand them as list box buttons.
|
||||
"configure_ultequip:7.5": SubTypeListBoxUp,
|
||||
"configure_ultequip:7.6": SubTypeListBoxDown,
|
||||
}
|
||||
|
||||
type Menu struct {
|
||||
@@ -104,23 +82,71 @@ type Menu struct {
|
||||
ObjectFiles []string
|
||||
FontNames []string
|
||||
|
||||
// These are properties set in the menu header. We don't know what they're
|
||||
// all for.
|
||||
BackgroundColor color.Color
|
||||
HypertextColor color.Color
|
||||
|
||||
// FIXME: turn these into first-class data
|
||||
Properties map[string]string
|
||||
FontType int
|
||||
|
||||
// The actual menu records. There are multiple top-level items. Submenus are
|
||||
// only ever nested one deep.
|
||||
Records []*Record
|
||||
Groups []*Group
|
||||
}
|
||||
|
||||
func LoadMenu(filename string) (*Menu, error) {
|
||||
// Group represents an element with a MENUTYPE. It is part of a Menu and may
|
||||
// have children.
|
||||
type Group struct {
|
||||
Menu *Menu
|
||||
Records []*Record
|
||||
|
||||
Properties
|
||||
Type MenuType
|
||||
}
|
||||
|
||||
type Record struct {
|
||||
Menu *Menu
|
||||
Group *Group
|
||||
|
||||
Properties
|
||||
Type SubMenuType
|
||||
}
|
||||
|
||||
type Properties struct {
|
||||
Locator string // Not strictly a property. Set for tracking.
|
||||
|
||||
ID int
|
||||
ObjectIdx int // Can be specified in MENUID, defaults to 0
|
||||
|
||||
Accelerator int
|
||||
Active bool
|
||||
Desc string
|
||||
DrawType int
|
||||
FontType int
|
||||
Moveable bool
|
||||
Share int
|
||||
SoundType int
|
||||
SpriteId []int
|
||||
X int
|
||||
Y int
|
||||
|
||||
// From i18n
|
||||
Text string
|
||||
Help string
|
||||
}
|
||||
|
||||
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.Replace(name, filepath.Ext(name), "", -1)
|
||||
name = strings.TrimSuffix(name, filepath.Ext(name))
|
||||
name = strings.ToLower(name)
|
||||
|
||||
// FIXME: this needs turning into a real parser sometime
|
||||
scanner, err := asciiscan.New(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -128,84 +154,171 @@ func LoadMenu(filename string) (*Menu, error) {
|
||||
|
||||
defer scanner.Close()
|
||||
|
||||
var str string
|
||||
var record *Record
|
||||
|
||||
section := 0
|
||||
isProp := false
|
||||
out := &Menu{
|
||||
Name: name,
|
||||
Properties: map[string]string{},
|
||||
Name: name,
|
||||
}
|
||||
|
||||
for {
|
||||
str, err = scanner.ConsumeString()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Whether the lines are properties or not alternate with each section,
|
||||
// except the records use `*` as a separator
|
||||
if section < 3 && isProp != asciiscan.IsProperty(str) {
|
||||
section += 1
|
||||
isProp = !isProp
|
||||
}
|
||||
|
||||
if str == "~" {
|
||||
break
|
||||
}
|
||||
|
||||
switch section {
|
||||
case 0: // List of object files
|
||||
out.ObjectFiles = append(out.ObjectFiles, str)
|
||||
case 1: // List of properties
|
||||
k, v := asciiscan.ConsumeProperty(str)
|
||||
vInt, err := strconv.Atoi(v) // FIXME:
|
||||
switch k {
|
||||
case "BACKGROUND COLOR 0..255..-1 trans":
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out.BackgroundColor = data.ColorPalette[vInt]
|
||||
case "HYPERTEXT COLOR 0..255":
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out.HypertextColor = data.ColorPalette[vInt]
|
||||
default:
|
||||
out.Properties[k] = v
|
||||
}
|
||||
case 2: // list of fonts
|
||||
// FIXME: do we need to do something cleverer here?
|
||||
if str == "NULL" {
|
||||
continue
|
||||
}
|
||||
out.FontNames = append(out.FontNames, str)
|
||||
case 3: // Menu records
|
||||
if str == "*" { // NEXT RECORD
|
||||
out.Records = append(out.Records, record.Toplevel())
|
||||
continue
|
||||
}
|
||||
|
||||
k, v := asciiscan.ConsumeProperty(str)
|
||||
switch k {
|
||||
case "MENUID":
|
||||
record = newRecord(out, nil)
|
||||
case "SUBMENUID":
|
||||
record = newRecord(out, record.Toplevel())
|
||||
}
|
||||
setProperty(record, k, v)
|
||||
}
|
||||
if err := loadObjects(out, scanner); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Printf("Menu properties: %#+v", out.Properties)
|
||||
if err := loadProperties(out, scanner, palette); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := loadFonts(out, scanner); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := loadRecords(filepath.Dir(filename), out, scanner); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func LoadMenus(dir string) (map[string]*Menu, error) {
|
||||
func loadObjects(menu *Menu, scanner *asciiscan.Scanner) error {
|
||||
strs, err := scanner.ConsumeStringList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
menu.ObjectFiles = strs
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadProperties(menu *Menu, scanner *asciiscan.Scanner, palette color.Palette) error {
|
||||
for {
|
||||
ok, err := scanner.PeekProperty()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
|
||||
k, v, err := scanner.ConsumeProperty()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vInt, err := strconv.Atoi(v) // All properties have been int
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// DeBrief.mnu misspells these
|
||||
parts := strings.SplitN(strings.ToUpper(k), " ", 3)
|
||||
if len(parts) > 2 {
|
||||
k = strings.Join(parts[0:2], " ")
|
||||
}
|
||||
|
||||
switch strings.ToUpper(k) {
|
||||
case "BACKGROUND COLOR":
|
||||
menu.BackgroundColor = palette[vInt]
|
||||
case "HYPERTEXT COLOR":
|
||||
menu.HypertextColor = palette[vInt]
|
||||
case "FONT TYPE":
|
||||
menu.FontType = vInt
|
||||
default:
|
||||
return fmt.Errorf("Unhandled menu property in %v: %q=%q", menu.Name, k, v)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadFonts(menu *Menu, scanner *asciiscan.Scanner) error {
|
||||
// FIXME: Can we just ignore NULL, or does the index matter?
|
||||
strs, err := scanner.ConsumeStringList("NULL")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
menu.FontNames = strs
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadRecords(baseDir string, menu *Menu, scanner *asciiscan.Scanner) error {
|
||||
// We build things up line by line in these variables
|
||||
var group *Group
|
||||
var record *Record
|
||||
var properties *Properties
|
||||
|
||||
for {
|
||||
str, err := scanner.ConsumeString()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if strings.HasPrefix(str, "$") {
|
||||
subScanner, err := asciiscan.New(filepath.Join(baseDir, str[1:]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = loadRecords(baseDir, menu, subScanner)
|
||||
subScanner.Close() // Don't keep this around for all of loadRecords
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Processing child %q: %v", str, err)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if str == "*" {
|
||||
if record != nil {
|
||||
group.Records = append(group.Records, record)
|
||||
record = nil
|
||||
}
|
||||
|
||||
if group != nil {
|
||||
menu.Groups = append(menu.Groups, group)
|
||||
group = nil
|
||||
}
|
||||
|
||||
continue // New group
|
||||
}
|
||||
|
||||
if str == "~" {
|
||||
break // THE END
|
||||
}
|
||||
|
||||
k, v := asciiscan.ConsumeProperty(str)
|
||||
switch strings.ToUpper(k) {
|
||||
case "MENUID":
|
||||
if group != nil {
|
||||
menu.Groups = append(menu.Groups, group)
|
||||
}
|
||||
|
||||
group = newGroup(menu, v)
|
||||
properties = &group.Properties
|
||||
case "SUBMENUID":
|
||||
if record != nil {
|
||||
group.Records = append(group.Records, record)
|
||||
}
|
||||
|
||||
record = newRecord(group, v)
|
||||
properties = &record.Properties
|
||||
case "MENUTYPE":
|
||||
group.setMenuType(v)
|
||||
case "SUBMENUTYPE":
|
||||
record.setSubMenuType(v)
|
||||
default:
|
||||
if err := properties.setProperty(k, v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func LoadMenus(dir string, palette color.Palette) (map[string]*Menu, error) {
|
||||
fis, err := ioutil.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -223,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)
|
||||
}
|
||||
@@ -234,68 +347,94 @@ func LoadMenus(dir string) (map[string]*Menu, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func newRecord(menu *Menu, parent *Record) *Record {
|
||||
out := &Record{
|
||||
Menu: menu,
|
||||
Parent: parent,
|
||||
properties: map[string]string{},
|
||||
}
|
||||
|
||||
if parent != nil {
|
||||
parent.Children = append(parent.Children, out)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *Record) Toplevel() *Record {
|
||||
if r.Parent != nil {
|
||||
return r.Parent.Toplevel()
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func setProperty(r *Record, k, v string) {
|
||||
vSplit := strings.Split(v, ",")
|
||||
vInt, _ := strconv.Atoi(v)
|
||||
func listOfInts(s string) []int {
|
||||
vSplit := strings.Split(s, ",")
|
||||
vSplitInt := make([]int, len(vSplit))
|
||||
|
||||
for i, subV := range vSplit {
|
||||
vSplitInt[i], _ = strconv.Atoi(subV)
|
||||
}
|
||||
|
||||
switch k {
|
||||
case "MENUID", "SUBMENUID":
|
||||
r.Id = vInt
|
||||
case "MENUTYPE", "SUBMENUTYPE":
|
||||
if strings.Contains(v, ",") {
|
||||
r.Type = MenuType(vSplitInt[0]) // FIXME: what are the other values in this case?
|
||||
} else {
|
||||
r.Type = MenuType(vInt)
|
||||
}
|
||||
return vSplitInt
|
||||
}
|
||||
|
||||
// FIXME: Type override. Note that MENUID is specified first, so this works
|
||||
if override, ok := TypeOverrides[r.Locator()]; ok {
|
||||
r.Type = override
|
||||
}
|
||||
case "ACTIVE":
|
||||
r.Active = (vInt != 0)
|
||||
case "SPRITEID":
|
||||
r.SpriteId = vSplitInt
|
||||
case "X-CORD":
|
||||
r.X = vInt
|
||||
case "Y-CORD":
|
||||
r.Y = vInt
|
||||
case "FONTTYPE":
|
||||
r.FontType = vInt
|
||||
case "DRAW TYPE":
|
||||
r.DrawType = vInt
|
||||
case "SHARE":
|
||||
r.Share = vInt
|
||||
default:
|
||||
r.properties[k] = v
|
||||
func newGroup(menu *Menu, idStr string) *Group {
|
||||
out := &Group{Menu: menu}
|
||||
|
||||
// ObjectIdx can be specified in the MENUID. Only seen for .mni files
|
||||
ints := listOfInts(idStr)
|
||||
out.ID = ints[0]
|
||||
if len(ints) > 1 {
|
||||
out.ObjectIdx = ints[1]
|
||||
}
|
||||
|
||||
out.Locator = fmt.Sprintf("%v:%v", menu.Name, out.ID)
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func newRecord(group *Group, idStr string) *Record {
|
||||
out := &Record{Group: group}
|
||||
|
||||
out.ID, _ = strconv.Atoi(idStr) // FIXME: we're ignoring conversion errors here
|
||||
out.ObjectIdx = group.ObjectIdx // FIXME: we shouldn't *copy* this
|
||||
|
||||
out.Locator = fmt.Sprintf("%v.%v", group.Locator, out.ID)
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (g *Group) setMenuType(s string) {
|
||||
v, _ := strconv.Atoi(s) // FIXME: conversion errors
|
||||
g.Type = MenuType(v)
|
||||
}
|
||||
|
||||
func (r *Record) setSubMenuType(s string) {
|
||||
// FIXME: Type overrides shouldn't be necessary!
|
||||
if override, ok := TypeOverrides[r.Locator]; ok {
|
||||
r.Type = override
|
||||
return
|
||||
}
|
||||
|
||||
// FIXME: what are the other types here? Related to list boxes?
|
||||
ints := listOfInts(s)
|
||||
r.Type = SubMenuType(ints[0])
|
||||
}
|
||||
|
||||
func (p *Properties) setProperty(k, v string) error {
|
||||
ints := listOfInts(v)
|
||||
vInt := ints[0]
|
||||
asBool := (vInt != 0)
|
||||
|
||||
switch strings.ToUpper(k) {
|
||||
case "ACCELERATOR":
|
||||
p.Accelerator = vInt
|
||||
case "ACTIVE":
|
||||
p.Active = asBool
|
||||
case "DESC":
|
||||
p.Desc = v // Usually int, occasionally string
|
||||
case "DRAW TYPE":
|
||||
p.DrawType = vInt
|
||||
case "FONTTYPE":
|
||||
p.FontType = vInt
|
||||
case "MOVEABLE":
|
||||
p.Moveable = asBool
|
||||
case "SOUNDTYPE":
|
||||
p.SoundType = vInt
|
||||
case "SPRITEID":
|
||||
p.SpriteId = ints
|
||||
case "X-CORD":
|
||||
p.X = vInt
|
||||
case "Y-CORD":
|
||||
p.Y = vInt
|
||||
case "SHARE":
|
||||
p.Share = vInt
|
||||
|
||||
default:
|
||||
return fmt.Errorf("Unknown property for %v: %v=%v", p.Locator, k, v)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Replacer interface {
|
||||
@@ -304,43 +443,47 @@ type Replacer interface {
|
||||
}
|
||||
|
||||
func (r *Record) Internationalize(replacer Replacer) {
|
||||
if override, ok := TextOverrides[r.Locator()]; ok {
|
||||
delete(r.properties, "DESC")
|
||||
if override, ok := TextOverrides[r.Locator]; ok {
|
||||
r.Text = override
|
||||
return
|
||||
}
|
||||
|
||||
if override, ok := DescOverrides[r.Locator()]; ok {
|
||||
r.properties["DESC"] = strconv.Itoa(override)
|
||||
if override, ok := DescOverrides[r.Locator]; ok {
|
||||
r.Desc = strconv.Itoa(override)
|
||||
}
|
||||
|
||||
id, err := strconv.Atoi(r.properties["DESC"])
|
||||
id, err := strconv.Atoi(r.Desc)
|
||||
if err == nil {
|
||||
delete(r.properties, "DESC")
|
||||
replacer.ReplaceText(id, &r.Text)
|
||||
replacer.ReplaceHelp(id, &r.Help)
|
||||
}
|
||||
|
||||
for _, child := range r.Children {
|
||||
child.Internationalize(replacer)
|
||||
} else {
|
||||
r.Text = r.Desc // Sometimes it's a string like "EQUIPMENT"
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Menu) Internationalize(replacer Replacer) {
|
||||
for _, record := range m.Records {
|
||||
record.Internationalize(replacer)
|
||||
for _, group := range m.Groups {
|
||||
for _, record := range group.Records {
|
||||
record.Internationalize(replacer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Record) Path() string {
|
||||
var path []string
|
||||
func (g *Group) Props() *Properties {
|
||||
return &g.Properties
|
||||
}
|
||||
|
||||
for rec := r; rec != nil; rec = rec.Parent {
|
||||
path = append([]string{strconv.Itoa(rec.Id)}, path...)
|
||||
func (r *Record) Props() *Properties {
|
||||
return &r.Properties
|
||||
}
|
||||
|
||||
func (p *Properties) BaseSpriteID() int {
|
||||
base := p.Share
|
||||
|
||||
// SpriteId takes precedence if present
|
||||
if len(p.SpriteId) > 0 && p.SpriteId[0] >= 0 {
|
||||
base = p.SpriteId[0]
|
||||
}
|
||||
|
||||
return strings.Join(path, ".")
|
||||
}
|
||||
|
||||
func (r *Record) Locator() string {
|
||||
return fmt.Sprintf("%v:%v", r.Menu.Name, r.Path())
|
||||
return base
|
||||
}
|
||||
|
20
internal/ordoor/display.go
Normal file
20
internal/ordoor/display.go
Normal 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
|
||||
}
|
@@ -1,47 +0,0 @@
|
||||
package flow
|
||||
|
||||
func (f *Flow) linkBridge() {
|
||||
// FIXME: sometimes these doors are frozen, depending on game state
|
||||
|
||||
f.onClick(bridge, "2.1", f.setDriver(briefing)) // TODO: Mission briefing clickable
|
||||
f.onClick(bridge, "2.2", f.setDriver(choices)) // Options door hotspot
|
||||
f.setFreeze(bridge, "2.4", false) // FIXME: Enter combat door hotspot (!!!)
|
||||
f.setFreeze(bridge, "2.6", false) // FIXME: Vehicle configure door hotspot
|
||||
f.onClick(bridge, "2.8", f.setDriver(arrange)) // Squads configure door hotspot
|
||||
|
||||
// link children
|
||||
f.linkBriefing()
|
||||
f.linkChoices()
|
||||
f.linkArrange()
|
||||
}
|
||||
|
||||
func (f *Flow) linkBriefing() {
|
||||
f.onClick(briefing, "3.1", f.setDriver(bridge))
|
||||
}
|
||||
|
||||
func (f *Flow) linkChoices() {
|
||||
f.onClick(choices, "2.1", f.setDriver(loadGame)) // Load another game button
|
||||
f.onClick(choices, "2.2", f.setDriver(saveGame)) // Save this game button
|
||||
f.onClick(choices, "2.3", f.setReturningDriver(choices, options)) // More options button
|
||||
|
||||
// FIXME: wipe out game state when this goes through
|
||||
f.onClick(choices, "2.4", f.setDriver(main)) // Restart button
|
||||
|
||||
f.onClick(choices, "2.5", f.setDriver(credits)) // Credits button
|
||||
f.onClick(choices, "2.6", f.setExit) // Quit button
|
||||
f.onClick(choices, "2.7", f.setDriver(bridge)) // Back button
|
||||
}
|
||||
|
||||
func (f *Flow) linkArrange() {
|
||||
// FIXME: we should be operating on game data in here
|
||||
f.onClick(arrange, "8.1", f.setDriver(bridge)) // Return to bridge ("cathedral")
|
||||
f.onClick(arrange, "8.3", f.setDriver(configureUltEquip)) // Configure squads
|
||||
|
||||
f.linkConfigureUltEquip()
|
||||
}
|
||||
|
||||
func (f *Flow) linkConfigureUltEquip() {
|
||||
// FIXME: we should be modifying loadouts of selected squad members here
|
||||
|
||||
f.onClick(configureUltEquip, "8.1", f.setDriver(bridge)) // Return to bridge
|
||||
}
|
@@ -1,229 +0,0 @@
|
||||
package flow
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/hajimehoshi/ebiten"
|
||||
|
||||
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
||||
"code.ur.gs/lupine/ordoor/internal/config"
|
||||
"code.ur.gs/lupine/ordoor/internal/ui"
|
||||
)
|
||||
|
||||
// type Flow is responsible for wiring up UI elements to each other and ensuring
|
||||
// they behave as expected. This includes forward / back buttons to switch
|
||||
// between screens, loading and saving options, launching a scenario, etc
|
||||
type Flow struct {
|
||||
assets *assetstore.AssetStore
|
||||
config *config.Config
|
||||
current *ui.Driver
|
||||
drivers map[driverName]*ui.Driver
|
||||
|
||||
// Some screens can be returned to from more than one place. Where this is
|
||||
// the case, instead of hardcoding it, we'll store an entry in here so we
|
||||
// know where we're going back to
|
||||
//
|
||||
// FIXME: this really suggests wiring everything up at the start is wrong.
|
||||
returns map[driverName]driverName
|
||||
|
||||
exit error
|
||||
}
|
||||
|
||||
type driverName string
|
||||
|
||||
const (
|
||||
// Names of all the drivers
|
||||
main driverName = "Main"
|
||||
levelPly driverName = "LevelPly"
|
||||
singles driverName = "Singles"
|
||||
randomMap driverName = "RandomMap"
|
||||
newGame driverName = "NewGame"
|
||||
loadGame driverName = "LoadGame"
|
||||
options driverName = "Options"
|
||||
kbd driverName = "Keyboard"
|
||||
bridge driverName = "Bridge"
|
||||
briefing driverName = "Briefing"
|
||||
choices driverName = "Choices"
|
||||
saveGame driverName = "SaveGame"
|
||||
credits driverName = "Credits"
|
||||
arrange driverName = "Arrange"
|
||||
|
||||
configureUltEquip driverName = "Configure_UltEquip"
|
||||
configureVehiclesUltra driverName = "Configure_Vehicles_Ultra"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrExit = errors.New("exiting gracefully")
|
||||
|
||||
driverNames = []driverName{
|
||||
main, levelPly, singles, randomMap, newGame, loadGame, options, kbd,
|
||||
bridge, briefing, choices, saveGame, credits, arrange,
|
||||
configureUltEquip, configureVehiclesUltra,
|
||||
}
|
||||
|
||||
// Constants used for sliders
|
||||
|
||||
h3Slider = map[int]int{1: 8, 2: 56, 3: 110, 4: 120}
|
||||
|
||||
v10Slider = map[int]int{
|
||||
0: 0,
|
||||
10: 9, 20: 18, 30: 27, 40: 36, 50: 45,
|
||||
60: 54, 70: 63, 80: 72, 90: 81, 100: 90,
|
||||
}
|
||||
|
||||
h9Slider = map[int]int{
|
||||
0: 0,
|
||||
10: 10, 20: 20, 30: 30, 40: 40,
|
||||
50: 50, 60: 60, 70: 70, 80: 80,
|
||||
}
|
||||
)
|
||||
|
||||
func New(assets *assetstore.AssetStore, config *config.Config) (*Flow, error) {
|
||||
out := &Flow{
|
||||
assets: assets,
|
||||
config: config,
|
||||
drivers: make(map[driverName]*ui.Driver, len(driverNames)),
|
||||
returns: make(map[driverName]driverName),
|
||||
}
|
||||
|
||||
// Load all the drivers upfront
|
||||
for _, name := range driverNames {
|
||||
driver, err := buildDriver(assets, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out.drivers[name] = driver
|
||||
}
|
||||
|
||||
// Initial load of the config into the options UI
|
||||
if err := out.configIntoOptions(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out.linkDrivers()
|
||||
out.setDriverNow(main)
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
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(menu)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return driver, nil
|
||||
}
|
||||
|
||||
func (f *Flow) Update(screenX, screenY int) error {
|
||||
if f.exit != nil {
|
||||
return f.exit
|
||||
}
|
||||
|
||||
return f.current.Update(screenX, screenY)
|
||||
}
|
||||
|
||||
func (f *Flow) Draw(screen *ebiten.Image) error {
|
||||
if f.exit != nil {
|
||||
return f.exit
|
||||
}
|
||||
|
||||
return f.current.Draw(screen)
|
||||
}
|
||||
|
||||
func (f *Flow) linkDrivers() {
|
||||
// linkMain
|
||||
f.onClick(main, "2.1", f.setDriver(newGame)) // New game
|
||||
f.onClick(main, "2.2", f.setDriver(loadGame)) // Load game
|
||||
f.setFreeze(main, "2.3", true) // Multiplayer - disable for now
|
||||
f.onClick(main, "2.4", f.setReturningDriver(main, options)) // Options
|
||||
f.onClick(main, "2.5", f.setExit) // Quit
|
||||
|
||||
// Now link immediate children. They will link their children, and so on
|
||||
f.linkNewGame()
|
||||
f.linkLoadGame()
|
||||
// TODO: link multiplayer
|
||||
f.linkOptions()
|
||||
}
|
||||
|
||||
func maybeErr(driver driverName, err error) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("%v: %v", driver, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Flow) configureSlider(driver driverName, id string, steps map[int]int) {
|
||||
if f.exit != nil {
|
||||
return
|
||||
}
|
||||
|
||||
f.exit = f.drivers[driver].ConfigureSlider(id, steps)
|
||||
}
|
||||
|
||||
func (f *Flow) onClick(driver driverName, id string, fn func()) {
|
||||
if f.exit != nil {
|
||||
return
|
||||
}
|
||||
|
||||
f.exit = maybeErr(driver, f.drivers[driver].OnClick(id, fn))
|
||||
}
|
||||
|
||||
func (f *Flow) setFreeze(driver driverName, id string, value bool) {
|
||||
if f.exit != nil {
|
||||
return
|
||||
}
|
||||
|
||||
f.exit = maybeErr(driver, f.drivers[driver].SetFreeze(id, value))
|
||||
}
|
||||
|
||||
func (f *Flow) setValueBool(driver driverName, id string, value bool) {
|
||||
if f.exit != nil {
|
||||
return
|
||||
}
|
||||
|
||||
f.exit = f.drivers[driver].SetValueBool(id, value)
|
||||
}
|
||||
|
||||
func (f *Flow) setDriver(name driverName) func() {
|
||||
return func() {
|
||||
f.setDriverNow(name)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Flow) setDriverNow(name driverName) {
|
||||
f.current = f.drivers[name]
|
||||
}
|
||||
|
||||
// from is the parent menu, to is the child
|
||||
func (f *Flow) setReturningDriver(from, to driverName) func() {
|
||||
return func() {
|
||||
f.returns[to] = from
|
||||
f.setDriverNow(to)
|
||||
}
|
||||
}
|
||||
|
||||
// from is the child menu, to is the parent
|
||||
func (f *Flow) returnToLastDriverNow(from driverName) error {
|
||||
to, ok := f.returns[from]
|
||||
if !ok {
|
||||
return fmt.Errorf("Couldn't work out where to return to from %v", from)
|
||||
}
|
||||
|
||||
delete(f.returns, from)
|
||||
|
||||
f.setDriverNow(to)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Flow) setExit() {
|
||||
f.exit = ErrExit
|
||||
}
|
@@ -1,6 +0,0 @@
|
||||
package flow
|
||||
|
||||
func (f *Flow) linkLoadGame() {
|
||||
// Load game
|
||||
f.onClick(loadGame, "3.3", f.setDriver(main)) // Cancel button
|
||||
}
|
@@ -1,44 +0,0 @@
|
||||
package flow
|
||||
|
||||
func (f *Flow) linkNewGame() {
|
||||
// New game
|
||||
f.onClick(newGame, "2.1", f.setDriver(levelPly)) // New campaign button
|
||||
f.onClick(newGame, "2.2", f.setDriver(singles)) // Single scenario button
|
||||
f.onClick(newGame, "2.3", f.setDriver(randomMap)) // Random scenario button
|
||||
f.onClick(newGame, "2.4", f.setDriver(main)) // Back button
|
||||
|
||||
f.linkLevelPly()
|
||||
f.linkSingles()
|
||||
f.linkRandomMap()
|
||||
}
|
||||
|
||||
func (f *Flow) linkLevelPly() {
|
||||
// We want the default difficulty level to be Veteran, not Hero.
|
||||
// FIXME: Make the radio button respect changes via setValue
|
||||
resetLevel := func() {
|
||||
f.setValueBool(levelPly, "2.1", false)
|
||||
f.setValueBool(levelPly, "2.2", true)
|
||||
}
|
||||
resetLevel()
|
||||
|
||||
f.onClick(levelPly, "2.5", func() { // Back button
|
||||
resetLevel()
|
||||
f.setDriverNow(newGame)
|
||||
})
|
||||
|
||||
// FIXME: we should select a savegame if Mighty Hero is selected here
|
||||
// FIXME: we should show a movie here. Need an internal SMK player first
|
||||
// FIXME: we should set up new game state here!
|
||||
f.onClick(levelPly, "2.7", f.setDriver(bridge)) // Select button
|
||||
|
||||
// Link children
|
||||
f.linkBridge()
|
||||
}
|
||||
|
||||
func (f *Flow) linkSingles() {
|
||||
f.onClick(singles, "4.11", f.setDriver(newGame)) // Back button
|
||||
}
|
||||
|
||||
func (f *Flow) linkRandomMap() {
|
||||
f.onClick(randomMap, "2.19", f.setDriver(newGame)) // Back button
|
||||
}
|
@@ -5,63 +5,70 @@
|
||||
package ordoor
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"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"
|
||||
"code.ur.gs/lupine/ordoor/internal/ordoor/flow"
|
||||
"code.ur.gs/lupine/ordoor/internal/flow"
|
||||
"code.ur.gs/lupine/ordoor/internal/ship"
|
||||
"code.ur.gs/lupine/ordoor/internal/ui"
|
||||
)
|
||||
|
||||
type gameState int
|
||||
|
||||
const (
|
||||
StateInterface gameState = 1
|
||||
StateExit gameState = 666
|
||||
)
|
||||
|
||||
var (
|
||||
errExit = errors.New("User-requested exit action")
|
||||
)
|
||||
|
||||
type Ordoor struct {
|
||||
assets *assetstore.AssetStore
|
||||
config *config.Config
|
||||
music *audio.Player
|
||||
win *ui.Window
|
||||
|
||||
state gameState
|
||||
nextState gameState
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
if _, err := audio.NewContext(48000); err != nil {
|
||||
return fmt.Errorf("Failed to set up audio context: %v", err)
|
||||
defaults, err := assets.DefaultOptions()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to read option defaults: %v", err)
|
||||
}
|
||||
|
||||
cfg.Defaults = defaults
|
||||
if cfg.HasUnsetOptions() {
|
||||
if err := cfg.ResetDefaults(); err != nil {
|
||||
return fmt.Errorf("Failed to set options on first-start: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
_ = audio.NewContext(48000)
|
||||
|
||||
ordoor := &Ordoor{
|
||||
assets: assets,
|
||||
config: cfg,
|
||||
state: StateInterface,
|
||||
nextState: StateInterface,
|
||||
assets: assets,
|
||||
config: cfg,
|
||||
ship: ship.New(),
|
||||
}
|
||||
|
||||
x, y := cfg.Options.XRes, cfg.Options.YRes
|
||||
@@ -79,24 +86,24 @@ 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 returned %v", err)
|
||||
return fmt.Errorf("Run finished with error: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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.PlayUnskippableVideo("LOGOS")
|
||||
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")
|
||||
@@ -108,6 +115,8 @@ func (o *Ordoor) Run() error {
|
||||
|
||||
// Only one music track can play at a time. This is handled at the toplevel.
|
||||
// FIXME: should take references from Sounds.dat
|
||||
// FIXME: music probably properly belongs to flow. This package can just do
|
||||
// initialization and wire the flow to the ship?
|
||||
func (o *Ordoor) PlayMusic(name string) error {
|
||||
if o.music != nil {
|
||||
if err := o.music.Close(); err != nil {
|
||||
@@ -136,7 +145,7 @@ func (o *Ordoor) PlayMusic(name string) error {
|
||||
func (o *Ordoor) setupFlow() error {
|
||||
o.PlayMusic("music_interface")
|
||||
|
||||
flow, err := flow.New(o.assets, o.config)
|
||||
flow, err := flow.New(o.assets, o.config, o.ship)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -147,21 +156,15 @@ func (o *Ordoor) setupFlow() error {
|
||||
}
|
||||
|
||||
func (o *Ordoor) Update(screenX, screenY int) error {
|
||||
// Perform state transitions
|
||||
if o.state != o.nextState {
|
||||
log.Printf("State transition: %v -> %v", o.state, o.nextState)
|
||||
switch o.nextState {
|
||||
case StateExit:
|
||||
{
|
||||
return errExit
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("Unknown state transition: %v -> %v", o.state, o.nextState)
|
||||
}
|
||||
if pic := o.pic; pic != nil {
|
||||
return nil // Ignore flow until we don't have a pic any more
|
||||
}
|
||||
|
||||
// State transition is finished, hooray
|
||||
o.state = o.nextState
|
||||
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 {
|
||||
@@ -173,19 +176,34 @@ func (o *Ordoor) Update(screenX, screenY int) error {
|
||||
}
|
||||
}
|
||||
|
||||
switch o.state {
|
||||
case StateInterface:
|
||||
return o.flow.Update(screenX, screenY)
|
||||
default:
|
||||
return fmt.Errorf("Unknown state: %v", o.state)
|
||||
}
|
||||
return o.flow.Update(screenX, screenY)
|
||||
}
|
||||
|
||||
func (o *Ordoor) Draw(screen *ebiten.Image) error {
|
||||
switch o.state {
|
||||
case StateInterface:
|
||||
return o.flow.Draw(screen)
|
||||
default:
|
||||
return fmt.Errorf("Unknown state: %v", o.state)
|
||||
|
||||
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) {
|
||||
if o.flow != nil {
|
||||
return o.flow.Cursor()
|
||||
}
|
||||
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
}
|
20
internal/palettes/palettes.go
Normal file
20
internal/palettes/palettes.go
Normal 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
|
||||
}
|
269
internal/palettes/soldiers_at_war.go
Normal file
269
internal/palettes/soldiers_at_war.go
Normal 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
|
||||
}
|
269
internal/palettes/wages_of_war.go
Normal file
269
internal/palettes/wages_of_war.go
Normal 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
|
||||
}
|
214
internal/scenario/draw.go
Normal file
214
internal/scenario/draw.go
Normal file
@@ -0,0 +1,214 @@
|
||||
package scenario
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"sort"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
||||
)
|
||||
|
||||
type CartPt struct {
|
||||
X float64
|
||||
Y float64
|
||||
}
|
||||
|
||||
type IsoPt struct {
|
||||
X float64
|
||||
Y float64
|
||||
}
|
||||
|
||||
func (s *Scenario) Update(screenX, screenY int) error {
|
||||
s.tick += 1
|
||||
|
||||
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: x,
|
||||
Y: y,
|
||||
}
|
||||
|
||||
// FIXME: adjust for Z level
|
||||
s.highlightedCell = screenPos.ToISO()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Scenario) Draw(screen *ebiten.Image) error {
|
||||
// Bounds clipping
|
||||
// http://www.java-gaming.org/index.php?topic=24922.0
|
||||
// https://stackoverflow.com/questions/892811/drawing-isometric-game-worlds
|
||||
// https://gamedev.stackexchange.com/questions/25896/how-do-i-find-which-isometric-tiles-are-inside-the-cameras-current-view
|
||||
// FIXME: we don't cope with zoom very neatly here
|
||||
|
||||
sw, sh := screen.Size()
|
||||
|
||||
topLeft := CartPt{
|
||||
X: float64(s.Viewpoint.X) - (2 * cellWidth / s.Zoom), // Ensure all visible cells are rendered
|
||||
Y: float64(s.Viewpoint.Y) - (2 * cellHeight / s.Zoom),
|
||||
}.ToISO()
|
||||
|
||||
bottomRight := CartPt{
|
||||
X: float64(s.Viewpoint.X) + (float64(sw) / s.Zoom) + (2 * cellHeight / s.Zoom),
|
||||
Y: float64(s.Viewpoint.Y) + (float64(sh) / s.Zoom) + (5 * cellHeight / s.Zoom), // Z dimension requires it
|
||||
}.ToISO()
|
||||
|
||||
// X+Y is constant for all tiles in a column
|
||||
// X-Y is constant for all tiles in a row
|
||||
// However, the drawing order is odd unless we reorder explicitly.
|
||||
toDraw := []IsoPt{}
|
||||
for a := int(topLeft.X + topLeft.Y); a <= int(bottomRight.X+bottomRight.Y); a++ {
|
||||
for b := int(topLeft.X - topLeft.Y); b <= int(bottomRight.X-bottomRight.Y); b++ {
|
||||
if b&1 != a&1 {
|
||||
continue
|
||||
}
|
||||
|
||||
pt := IsoPt{X: float64((a + b) / 2), Y: float64((a - b) / 2)}
|
||||
ipt := image.Pt(int(pt.X), int(pt.Y))
|
||||
|
||||
if !ipt.In(s.area.Rect) {
|
||||
continue
|
||||
}
|
||||
toDraw = append(toDraw, pt)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(toDraw, func(i, j int) bool {
|
||||
iPix := toDraw[i].ToCart()
|
||||
jPix := toDraw[j].ToCart()
|
||||
|
||||
if iPix.Y < jPix.Y {
|
||||
return true
|
||||
}
|
||||
|
||||
if iPix.Y == jPix.Y {
|
||||
return iPix.X < jPix.X
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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{}
|
||||
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)
|
||||
|
||||
screen.DrawImage(spr.Image, &op)
|
||||
|
||||
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) 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)
|
||||
|
||||
// Taking the Z index away *seems* to draw the object in the correct place.
|
||||
// FIXME: There are some artifacts, investigate more
|
||||
geo.Translate(0.0, -float64(z*48.0)) // offset for Z index
|
||||
|
||||
return geo
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// TODO: iso.Scale(e.state.zoom, e.state.zoom) // apply current zoom factor
|
||||
iso := s.geoForCoords(x, y, z)
|
||||
|
||||
// FIXME: this fixed offset is found in jungtil.obj. Drawing with it
|
||||
// means we put everywhere where the iso->pix conversion expects, but
|
||||
// it's a bit nasty. Is there a better way?
|
||||
iso.Translate(-209, -332)
|
||||
|
||||
for _, spr := range sprites {
|
||||
*counter = *counter + 1
|
||||
op := ebiten.DrawImageOptions{GeoM: iso}
|
||||
|
||||
op.GeoM.Translate(float64(spr.Rect.Min.X), float64(spr.Rect.Min.Y))
|
||||
|
||||
// Zoom has to come last
|
||||
op.GeoM.Scale(s.Zoom, s.Zoom)
|
||||
|
||||
screen.DrawImage(spr.Image, &op)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
cellWidth = 128.0
|
||||
cellHeight = 63.0
|
||||
|
||||
cellWidthHalf = cellWidth / 2.0
|
||||
cellHeightHalf = cellHeight / 2.0
|
||||
)
|
||||
|
||||
func (p CartPt) ToISO() IsoPt {
|
||||
return IsoPt{
|
||||
X: (p.Y / cellHeight) + (p.X / cellWidth),
|
||||
Y: (p.Y / cellHeight) - (p.X / cellWidth),
|
||||
}
|
||||
}
|
||||
|
||||
func (p IsoPt) ToCart() CartPt {
|
||||
return CartPt{
|
||||
X: (p.X - p.Y) * cellWidthHalf,
|
||||
Y: (p.X + p.Y) * cellHeightHalf,
|
||||
}
|
||||
}
|
21
internal/scenario/draw_test.go
Normal file
21
internal/scenario/draw_test.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package scenario
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCARTtoISOtoCART(t *testing.T) {
|
||||
for i := 0; i < 120; i++ {
|
||||
for j := 0; j < 120; j++ {
|
||||
orig := IsoPt{X: float64(i), Y: float64(j)}
|
||||
asPix := orig.ToCart()
|
||||
andBack := asPix.ToISO()
|
||||
|
||||
t.Logf("%v,%v: asPix = %v", i, j, asPix)
|
||||
|
||||
require.Equal(t, orig, andBack)
|
||||
}
|
||||
}
|
||||
}
|
45
internal/scenario/manage.go
Normal file
45
internal/scenario/manage.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package scenario
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"code.ur.gs/lupine/ordoor/internal/maps"
|
||||
)
|
||||
|
||||
type CellPoint struct {
|
||||
IsoPt
|
||||
Z int
|
||||
}
|
||||
|
||||
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
|
||||
}
|
53
internal/scenario/scenario.go
Normal file
53
internal/scenario/scenario.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// package play takes a map and turns it into a playable scenario
|
||||
package scenario
|
||||
|
||||
import (
|
||||
"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
|
||||
|
||||
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) {
|
||||
area, err := assets.Map(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
specials, err := assets.Object("specials") // FIXME: should this be hardcoded?
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Eager load sprites. TODO: do we really want to do this?
|
||||
//if err := area.LoadSprites(); err != nil {
|
||||
// return nil, fmt.Errorf("Eager-loading sprites failed: %v", err)
|
||||
//}
|
||||
|
||||
out := &Scenario{
|
||||
area: area,
|
||||
specials: specials,
|
||||
Viewpoint: image.Pt(0, 3000), // FIXME: haxxx
|
||||
Zoom: 1.0,
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
93
internal/ship/ship.go
Normal file
93
internal/ship/ship.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package ship
|
||||
|
||||
type DifficultyLevel int
|
||||
|
||||
const (
|
||||
DifficultyLevelMarine DifficultyLevel = 0
|
||||
DifficultyLevelVeteran DifficultyLevel = 1
|
||||
DifficultyLevelHero DifficultyLevel = 2
|
||||
DifficultyLevelMighty DifficultyLevel = 3
|
||||
)
|
||||
|
||||
// Ship encapsulates campaign state, including current location in the campaign,
|
||||
// marines and their stats, supplies, etc.
|
||||
type Ship struct {
|
||||
Difficulty DifficultyLevel
|
||||
NextScenario string
|
||||
|
||||
Squads []*Squad
|
||||
Captain *Character
|
||||
Chaplain *Character
|
||||
Apothecary *Character
|
||||
Techmarines [2]*Character
|
||||
Librarians [4]*Character
|
||||
}
|
||||
|
||||
type SquadType int
|
||||
type CharacterType int
|
||||
|
||||
const (
|
||||
SquadTypeTactical SquadType = 0
|
||||
SquadTypeTerminator SquadType = 1
|
||||
SquadTypeAssault SquadType = 2
|
||||
SquadTypeDevastator SquadType = 3
|
||||
|
||||
CharTypeMarine CharacterType = 0
|
||||
CharTypeCaptain CharacterType = 1
|
||||
CharTypeChaplain CharacterType = 2
|
||||
CharTypeApothecary CharacterType = 3
|
||||
CharTypeTechmarine CharacterType = 4
|
||||
CharTypeLibrarian CharacterType = 5
|
||||
)
|
||||
|
||||
type Squad struct {
|
||||
Type SquadType
|
||||
|
||||
Characters []*Character
|
||||
}
|
||||
|
||||
type Character struct {
|
||||
Name string
|
||||
Type CharacterType
|
||||
|
||||
Stats
|
||||
Honours
|
||||
}
|
||||
|
||||
type Stats struct {
|
||||
ActionPoints int
|
||||
Health int
|
||||
Armour int
|
||||
BallisticSkill int
|
||||
WeaponSkill int
|
||||
Strength int
|
||||
Toughness int
|
||||
Initiative int
|
||||
Attacks int
|
||||
Leadership int
|
||||
|
||||
MissionCount int
|
||||
KillCount int
|
||||
|
||||
Experience int
|
||||
}
|
||||
|
||||
type Honours struct {
|
||||
Marksman bool
|
||||
CruxTerminatus bool
|
||||
PuritySeal bool
|
||||
ImperialLaurel bool
|
||||
}
|
||||
|
||||
func New() *Ship {
|
||||
s := &Ship{}
|
||||
s.Reset()
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Ship) Reset() {
|
||||
*s = Ship{
|
||||
Difficulty: DifficultyLevelVeteran, // Default difficulty level
|
||||
}
|
||||
}
|
@@ -1,7 +1,7 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"github.com/hajimehoshi/ebiten"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@@ -7,20 +7,13 @@ import (
|
||||
"code.ur.gs/lupine/ordoor/internal/menus"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerBuilder(menus.TypeSimpleButton, noChildren(registerSimpleButton))
|
||||
registerBuilder(menus.TypeInvokeButton, noChildren(registerInvokeButton))
|
||||
registerBuilder(menus.TypeMainButton, noChildren(registerMainButton))
|
||||
registerBuilder(menus.TypeDoorHotspot, noChildren(registerDoorHotspot))
|
||||
registerBuilder(menus.TypeDoorHotspot2, noChildren(registerDoorHotspot))
|
||||
registerBuilder(menus.TypeDoorHotspot3, noChildren(registerDoorHotspot))
|
||||
}
|
||||
|
||||
// A button without hover animation
|
||||
// FIXME: Keyboard.mnu has TypeSimpleButton instances that seem to include a
|
||||
// hover in the SpriteId field
|
||||
type button struct {
|
||||
path string
|
||||
locator string
|
||||
|
||||
rect image.Rectangle
|
||||
|
||||
baseSpr *assetstore.Sprite
|
||||
clickSpr *assetstore.Sprite
|
||||
@@ -38,99 +31,102 @@ type mainButton struct {
|
||||
button
|
||||
}
|
||||
|
||||
func registerSimpleButton(d *Driver, r *menus.Record) error {
|
||||
_, err := registerButton(d, r, r.SpriteId[0])
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func registerInvokeButton(d *Driver, r *menus.Record) error {
|
||||
_, err := registerButton(d, r, r.Share)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func registerMainButton(d *Driver, r *menus.Record) error {
|
||||
sprites, err := d.menu.Sprites(r.Share, 3) // base, pressed, disabled
|
||||
func (d *Driver) buildButton(p *menus.Properties) (*button, *Widget, error) {
|
||||
sprites, err := d.menu.Sprites(p.ObjectIdx, p.BaseSpriteID(), 3) // base, pressed, disabled
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
hovers, err := d.menu.Images(r.SpriteId[0], r.DrawType)
|
||||
btn := &button{
|
||||
locator: p.Locator,
|
||||
rect: sprites[0].Rect.Add(p.Point()),
|
||||
baseSpr: sprites[0],
|
||||
clickSpr: sprites[1],
|
||||
frozenSpr: sprites[2],
|
||||
hoverImpl: hoverImpl{text: p.Text},
|
||||
}
|
||||
|
||||
widget := &Widget{
|
||||
Locator: p.Locator,
|
||||
Active: p.Active,
|
||||
ownClickables: []clickable{btn},
|
||||
ownFreezables: []freezable{btn},
|
||||
ownHoverables: []hoverable{btn},
|
||||
ownPaintables: []paintable{btn},
|
||||
}
|
||||
|
||||
return btn, widget, nil
|
||||
}
|
||||
|
||||
func (d *Driver) buildMainButton(p *menus.Properties) (*mainButton, *Widget, error) {
|
||||
sprites, err := d.menu.Sprites(p.ObjectIdx, p.Share, 3) // base, pressed, disabled
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
hovers, err := d.menu.Images(p.ObjectIdx, p.SpriteId[0], p.DrawType)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
btn := &mainButton{
|
||||
hoverAnim: animation(hovers),
|
||||
button: button{
|
||||
path: r.Path(),
|
||||
locator: p.Locator,
|
||||
rect: sprites[0].Rect.Add(p.Point()),
|
||||
baseSpr: sprites[0],
|
||||
clickSpr: sprites[1],
|
||||
frozenSpr: sprites[2],
|
||||
hoverImpl: hoverImpl{text: r.Text},
|
||||
hoverImpl: hoverImpl{text: p.Text},
|
||||
},
|
||||
}
|
||||
|
||||
d.clickables = append(d.clickables, btn)
|
||||
d.freezables = append(d.freezables, btn)
|
||||
d.hoverables = append(d.hoverables, btn)
|
||||
d.paintables = append(d.paintables, btn)
|
||||
widget := &Widget{
|
||||
Locator: p.Locator,
|
||||
Active: p.Active,
|
||||
ownClickables: []clickable{btn},
|
||||
ownFreezables: []freezable{btn},
|
||||
ownHoverables: []hoverable{btn},
|
||||
ownPaintables: []paintable{btn},
|
||||
}
|
||||
|
||||
return nil
|
||||
return btn, widget, nil
|
||||
}
|
||||
|
||||
func registerDoorHotspot(d *Driver, r *menus.Record) error {
|
||||
sprites, err := d.menu.Sprites(r.Share, 2) // base, pressed
|
||||
func (d *Driver) buildDoorHotspot(p *menus.Properties) (*button, *Widget, error) {
|
||||
sprites, err := d.menu.Sprites(p.ObjectIdx, p.Share, 2) // base, pressed
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
btn := &button{
|
||||
path: r.Path(),
|
||||
locator: p.Locator,
|
||||
rect: sprites[0].Rect.Add(p.Point()),
|
||||
baseSpr: sprites[0],
|
||||
clickSpr: sprites[1],
|
||||
frozenSpr: sprites[0], // No disabled sprite
|
||||
hoverImpl: hoverImpl{text: r.Text},
|
||||
hoverImpl: hoverImpl{text: p.Text},
|
||||
}
|
||||
|
||||
d.clickables = append(d.clickables, btn)
|
||||
d.freezables = append(d.freezables, btn)
|
||||
d.hoverables = append(d.hoverables, btn)
|
||||
d.paintables = append(d.paintables, btn)
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func registerButton(d *Driver, r *menus.Record, spriteId int) (*button, error) {
|
||||
sprites, err := d.menu.Sprites(spriteId, 3) // base, pressed, disabled
|
||||
if err != nil {
|
||||
return nil, err
|
||||
widget := &Widget{
|
||||
Locator: p.Locator,
|
||||
Active: p.Active,
|
||||
ownClickables: []clickable{btn},
|
||||
ownFreezables: []freezable{btn},
|
||||
ownHoverables: []hoverable{btn},
|
||||
ownPaintables: []paintable{btn},
|
||||
}
|
||||
|
||||
btn := &button{
|
||||
path: r.Path(),
|
||||
baseSpr: sprites[0],
|
||||
clickSpr: sprites[1],
|
||||
frozenSpr: sprites[2],
|
||||
hoverImpl: hoverImpl{text: r.Text},
|
||||
}
|
||||
return btn, widget, nil
|
||||
|
||||
d.clickables = append(d.clickables, btn)
|
||||
d.freezables = append(d.freezables, btn)
|
||||
d.hoverables = append(d.hoverables, btn)
|
||||
d.paintables = append(d.paintables, btn)
|
||||
|
||||
return btn, nil
|
||||
}
|
||||
|
||||
func (b *button) id() string {
|
||||
return b.path
|
||||
return b.locator
|
||||
}
|
||||
|
||||
func (b *button) bounds() image.Rectangle {
|
||||
return b.baseSpr.Rect
|
||||
return b.rect
|
||||
}
|
||||
|
||||
func (b *button) mouseDownState() bool {
|
||||
@@ -160,6 +156,7 @@ func (b *button) regions(tick int) []region {
|
||||
}
|
||||
|
||||
func (m *mainButton) regions(tick int) []region {
|
||||
// FIXME: main button should complete its animation when we mouse away
|
||||
if !m.isFrozen() && !m.mouseDownState() && m.hoverState() {
|
||||
return oneRegion(m.bounds().Min, m.hoverAnim.image(tick))
|
||||
}
|
||||
|
41
internal/ui/dialogues.go
Normal file
41
internal/ui/dialogues.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func (d *Driver) Dialogues() []string {
|
||||
out := make([]string, len(d.dialogues))
|
||||
|
||||
for i, dialogue := range d.dialogues {
|
||||
out[i] = dialogue.Locator
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("Couldn't find dialogue %v", locator)
|
||||
}
|
||||
|
||||
func (d *Driver) HideDialogue() {
|
||||
if d.activeDialogue != nil {
|
||||
d.activeDialogue.Active = false
|
||||
}
|
||||
|
||||
d.activeDialogue = nil
|
||||
}
|
@@ -3,102 +3,42 @@ package ui
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"log"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
|
||||
"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"
|
||||
"code.ur.gs/lupine/ordoor/internal/menus"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// FIXME: these need implementing
|
||||
|
||||
// Needed for Keyboard.mnu (main -> options -> keyboard)
|
||||
registerBuilder(menus.TypeDialogue, registerDebug("Unimplemented Dialogue", nil))
|
||||
|
||||
// Needed for ChaEquip.mnu
|
||||
|
||||
// Needed for MainGameChaos.mnu
|
||||
registerBuilder(menus.TypeStatusBar, registerDebug("Unimplemented StatusBar", nil))
|
||||
|
||||
// Needed for Multiplayer_Choose.mnu
|
||||
registerBuilder(menus.TypeComboBoxItem, registerDebug("Unimplemented ComboBoxItem", nil))
|
||||
registerBuilder(menus.TypeDropdownButton, registerDebug("Unimplemented DropdownButton", nil))
|
||||
|
||||
// Needed for Multiplayer_Configure.mnu
|
||||
registerBuilder(menus.TypeEditBox, registerDebug("Unimplemented EditBox", nil))
|
||||
|
||||
// Needed for Multiplayer_Connect.mnu
|
||||
registerBuilder(menus.TypeRadioButton, registerDebug("Unimplemented RadioButton", nil))
|
||||
}
|
||||
|
||||
const (
|
||||
OriginalX = 640.0
|
||||
OriginalY = 480.0
|
||||
)
|
||||
|
||||
var (
|
||||
// Widgets register their builder here
|
||||
widgetBuilders = map[menus.MenuType]builderFunc{}
|
||||
)
|
||||
|
||||
// Used to add widgets to a driver
|
||||
type builderFunc func(d *Driver, r *menus.Record) (children []*menus.Record, err error)
|
||||
|
||||
func registerDebug(reason string, onward builderFunc) builderFunc {
|
||||
return func(d *Driver, r *menus.Record) ([]*menus.Record, error) {
|
||||
log.Printf("%v: %#+v", reason, r)
|
||||
if onward == nil {
|
||||
return r.Children, nil
|
||||
}
|
||||
|
||||
return onward(d, r)
|
||||
}
|
||||
}
|
||||
|
||||
func noChildren(f func(d *Driver, r *menus.Record) error) builderFunc {
|
||||
return func(d *Driver, r *menus.Record) ([]*menus.Record, error) {
|
||||
if len(r.Children) > 0 {
|
||||
return nil, fmt.Errorf("Children in record %v:%v (%#+v)", r.Menu.Name, r.Path(), r)
|
||||
}
|
||||
|
||||
return nil, f(d, r)
|
||||
}
|
||||
}
|
||||
|
||||
func ownedByMenu(d *Driver, r *menus.Record) ([]*menus.Record, error) {
|
||||
return nil, fmt.Errorf("This record should be handled by a menu: %v:%v (%#+v)", r.Menu.Name, r.Path(), r)
|
||||
}
|
||||
|
||||
func registerBuilder(t menus.MenuType, f builderFunc) {
|
||||
if _, ok := widgetBuilders[t]; ok {
|
||||
panic(fmt.Sprintf("A builder for menu type %v already exists", t))
|
||||
}
|
||||
|
||||
widgetBuilders[t] = f
|
||||
}
|
||||
|
||||
// Driver acts as an interface between the main loop and the widgets specified
|
||||
// in a menu.
|
||||
//
|
||||
// Menu assets assume a 640x480 screen; Driver is responsible for scaling to the
|
||||
// actual screen size when drawing.
|
||||
//
|
||||
// TODO: move scaling responsibilities to Window?
|
||||
type Driver struct {
|
||||
Name string
|
||||
|
||||
menu *assetstore.Menu
|
||||
assets *assetstore.AssetStore
|
||||
menu *assetstore.Menu
|
||||
|
||||
// UI elements we need to drive
|
||||
clickables []clickable
|
||||
freezables []freezable
|
||||
hoverables []hoverable
|
||||
mouseables []mouseable
|
||||
paintables []paintable
|
||||
valueables []valueable
|
||||
// UI elements we need to drive. Note that widgets are hierarchical - these
|
||||
// are just the toplevel. Dialogues are separated out. We only want to show
|
||||
// one dialogue at a time, and if a dialogue is active, the main widgets are
|
||||
// unusable (i.e., dialogues are modal)
|
||||
dialogues []*Widget
|
||||
widgets []*Widget
|
||||
|
||||
activeDialogue *Widget
|
||||
|
||||
cursor assetstore.CursorName
|
||||
|
||||
// The cursor in two different coordinate spaces: original, and screen-scaled
|
||||
cursorOrig image.Point
|
||||
@@ -112,14 +52,16 @@ type Driver struct {
|
||||
tooltip string
|
||||
}
|
||||
|
||||
func NewDriver(menu *assetstore.Menu) (*Driver, error) {
|
||||
func NewDriver(assets *assetstore.AssetStore, menu *assetstore.Menu) (*Driver, error) {
|
||||
driver := &Driver{
|
||||
Name: menu.Name,
|
||||
menu: menu,
|
||||
|
||||
assets: assets,
|
||||
menu: menu,
|
||||
}
|
||||
|
||||
for _, record := range menu.Records() {
|
||||
if err := driver.addRecord(record); err != nil {
|
||||
for _, group := range menu.Groups() {
|
||||
if err := driver.registerGroup(group); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -127,104 +69,6 @@ func NewDriver(menu *assetstore.Menu) (*Driver, error) {
|
||||
return driver, nil
|
||||
}
|
||||
|
||||
func (d *Driver) Value(id string, into *string) error {
|
||||
for _, valueable := range d.valueables {
|
||||
if valueable.id() == id {
|
||||
*into = valueable.value()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("Couldn't find valueable widget %v:%v", d.menu.Name, id)
|
||||
}
|
||||
|
||||
func (d *Driver) SetValue(id, value string) error {
|
||||
for _, valueable := range d.valueables {
|
||||
if valueable.id() == id {
|
||||
valueable.setValue(value)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("Couldn't find valueable widget %v:%v", d.menu.Name, id)
|
||||
}
|
||||
|
||||
func (d *Driver) ValueBool(id string, into *bool) error {
|
||||
var vStr string
|
||||
if err := d.Value(id, &vStr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*into = vStr == "1"
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Driver) SetValueBool(id string, value bool) error {
|
||||
vStr := "0"
|
||||
if value {
|
||||
vStr = "1"
|
||||
}
|
||||
|
||||
return d.SetValue(id, vStr)
|
||||
}
|
||||
|
||||
func (d *Driver) SetFreeze(id string, value bool) error {
|
||||
for _, freezable := range d.freezables {
|
||||
if freezable.id() == id {
|
||||
freezable.setFreezeState(value)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("Couldn't find clickable widget %v:%v", d.menu.Name, id)
|
||||
}
|
||||
|
||||
func (d *Driver) OnClick(id string, f func()) error {
|
||||
for _, clickable := range d.clickables {
|
||||
if clickable.id() == id {
|
||||
clickable.onClick(f)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("Couldn't find clickable widget %v:%v", d.menu.Name, id)
|
||||
}
|
||||
|
||||
// FIXME: HURK. Surely I'm missing something? steps is value:offset
|
||||
func (d *Driver) ConfigureSlider(id string, steps map[int]int) error {
|
||||
for _, clickable := range d.clickables {
|
||||
if slider, ok := clickable.(*slider); ok && slider.id() == id {
|
||||
slider.steps = steps
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("Couldn't find slider %v:%v", d.menu.Name, id)
|
||||
}
|
||||
|
||||
func (d *Driver) ValueInt(id string, into *int) error {
|
||||
var vStr string
|
||||
if err := d.Value(id, &vStr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
value, err := strconv.Atoi(vStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*into = value
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Driver) SetValueInt(id string, value int) error {
|
||||
vStr := strconv.Itoa(value)
|
||||
|
||||
return d.SetValue(id, vStr)
|
||||
}
|
||||
|
||||
func (d *Driver) Update(screenX, screenY int) error {
|
||||
if d == nil {
|
||||
debug.PrintStack()
|
||||
@@ -250,7 +94,7 @@ func (d *Driver) Update(screenX, screenY int) error {
|
||||
d.cursorOrig = image.Pt(int(mnX), int(mnY))
|
||||
|
||||
// Dispatch notifications to our widgets
|
||||
for _, hoverable := range d.hoverables {
|
||||
for _, hoverable := range d.activeHoverables() {
|
||||
inBounds := d.cursorOrig.In(hoverable.bounds())
|
||||
|
||||
d.hoverStartEvent(hoverable, inBounds)
|
||||
@@ -262,7 +106,7 @@ func (d *Driver) Update(screenX, screenY int) error {
|
||||
}
|
||||
|
||||
mouseIsDown := ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft)
|
||||
for _, clickable := range d.clickables {
|
||||
for _, clickable := range d.activeClickables() {
|
||||
inBounds := d.cursorOrig.In(clickable.bounds())
|
||||
mouseWasDown := clickable.mouseDownState()
|
||||
|
||||
@@ -271,7 +115,7 @@ func (d *Driver) Update(screenX, screenY int) error {
|
||||
d.mouseUpEvent(clickable, inBounds, mouseWasDown, mouseIsDown)
|
||||
}
|
||||
|
||||
for _, mouseable := range d.mouseables {
|
||||
for _, mouseable := range d.activeMouseables() {
|
||||
mouseable.registerMousePosition(d.cursorOrig)
|
||||
}
|
||||
|
||||
@@ -286,16 +130,14 @@ func (d *Driver) Draw(screen *ebiten.Image) error {
|
||||
|
||||
var do ebiten.DrawImageOptions
|
||||
|
||||
for _, paint := range d.paintables {
|
||||
for _, paint := range d.activePaintables() {
|
||||
for _, region := range paint.regions(d.ticks) {
|
||||
x, y := d.orig2native.Apply(float64(region.offset.X), float64(region.offset.Y))
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,71 +149,121 @@ func (d *Driver) Draw(screen *ebiten.Image) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Driver) addRecord(record *menus.Record) error {
|
||||
//log.Printf("Adding record: %#+v", record)
|
||||
children := record.Children
|
||||
|
||||
handler, ok := widgetBuilders[record.Type]
|
||||
if !ok {
|
||||
return fmt.Errorf("UI driver encountered unknown menu record: %#+v", record)
|
||||
func (d *Driver) Cursor() (*ebiten.Image, *ebiten.DrawImageOptions, error) {
|
||||
cursor, err := d.assets.Cursor(d.cursor)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if handler != nil {
|
||||
var err error
|
||||
children, err = handler(d, record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
op := &ebiten.DrawImageOptions{}
|
||||
op.GeoM.Translate(float64(d.cursorOrig.X), float64(d.cursorOrig.Y))
|
||||
op.GeoM.Concat(d.orig2native)
|
||||
op.GeoM.Translate(float64(-cursor.Hotspot.X), float64(-cursor.Hotspot.Y))
|
||||
|
||||
return cursor.Image, op, nil
|
||||
}
|
||||
|
||||
func (d *Driver) allClickables() []clickable {
|
||||
var out []clickable
|
||||
|
||||
for _, widget := range d.widgets {
|
||||
out = append(out, widget.allClickables()...)
|
||||
}
|
||||
|
||||
// Recursively add all remaining children of this record
|
||||
for _, record := range children {
|
||||
if err := d.addRecord(record); err != nil {
|
||||
return err
|
||||
for _, widget := range d.dialogues {
|
||||
out = append(out, widget.allClickables()...)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (d *Driver) allFreezables() []freezable {
|
||||
var out []freezable
|
||||
for _, widget := range d.widgets {
|
||||
out = append(out, widget.allFreezables()...)
|
||||
}
|
||||
|
||||
for _, widget := range d.dialogues {
|
||||
out = append(out, widget.allFreezables()...)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (d *Driver) allValueables() []valueable {
|
||||
var out []valueable
|
||||
|
||||
for _, widget := range d.widgets {
|
||||
out = append(out, widget.allValueables()...)
|
||||
}
|
||||
|
||||
for _, widget := range d.dialogues {
|
||||
out = append(out, widget.allValueables()...)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (d *Driver) activeClickables() []clickable {
|
||||
if d.activeDialogue != nil {
|
||||
return d.activeDialogue.activeClickables()
|
||||
}
|
||||
|
||||
var out []clickable
|
||||
for _, widget := range d.widgets {
|
||||
out = append(out, widget.activeClickables()...)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (d *Driver) activeHoverables() []hoverable {
|
||||
if d.activeDialogue != nil {
|
||||
return d.activeDialogue.activeHoverables()
|
||||
}
|
||||
|
||||
var out []hoverable
|
||||
for _, widget := range d.widgets {
|
||||
out = append(out, widget.activeHoverables()...)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (d *Driver) activeMouseables() []mouseable {
|
||||
if d.activeDialogue != nil {
|
||||
return d.activeDialogue.activeMouseables()
|
||||
}
|
||||
|
||||
var out []mouseable
|
||||
for _, widget := range d.widgets {
|
||||
out = append(out, widget.activeMouseables()...)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (d *Driver) activePaintables() []paintable {
|
||||
var out []paintable
|
||||
|
||||
for _, widget := range d.widgets {
|
||||
out = append(out, widget.activePaintables()...)
|
||||
}
|
||||
|
||||
if d.activeDialogue != nil {
|
||||
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
|
||||
}
|
||||
|
||||
func (d *Driver) hoverStartEvent(h hoverable, inBounds bool) {
|
||||
if inBounds && !h.hoverState() {
|
||||
//log.Printf("hoverable false -> true")
|
||||
h.setHoverState(true)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Driver) hoverEndEvent(h hoverable, inBounds bool) {
|
||||
if !inBounds && h.hoverState() {
|
||||
//log.Printf("hoverable true -> false")
|
||||
h.setHoverState(false)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Driver) mouseDownEvent(c clickable, inBounds, wasDown, isDown bool) {
|
||||
if inBounds && !wasDown && isDown {
|
||||
//log.Printf("mouse down false -> true")
|
||||
c.setMouseDownState(true)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Driver) mouseClickEvent(c clickable, inBounds, wasDown, isDown bool) {
|
||||
if inBounds && wasDown && !isDown {
|
||||
//log.Printf("mouse click")
|
||||
c.registerMouseClick()
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Driver) mouseUpEvent(c clickable, inBounds, wasDown, isDown bool) {
|
||||
if inBounds {
|
||||
if wasDown && !isDown {
|
||||
//log.Printf("mouse down true -> false")
|
||||
c.setMouseDownState(false)
|
||||
}
|
||||
} else {
|
||||
if wasDown {
|
||||
//log.Printf("mouse down true -> false")
|
||||
c.setMouseDownState(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
43
internal/ui/events.go
Normal file
43
internal/ui/events.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package ui
|
||||
|
||||
func (d *Driver) hoverStartEvent(h hoverable, inBounds bool) {
|
||||
if inBounds && !h.hoverState() {
|
||||
//log.Printf("hoverable false -> true")
|
||||
h.setHoverState(true)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Driver) hoverEndEvent(h hoverable, inBounds bool) {
|
||||
if !inBounds && h.hoverState() {
|
||||
//log.Printf("hoverable true -> false")
|
||||
h.setHoverState(false)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Driver) mouseDownEvent(c clickable, inBounds, wasDown, isDown bool) {
|
||||
if inBounds && !wasDown && isDown {
|
||||
//log.Printf("mouse down false -> true")
|
||||
c.setMouseDownState(true)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Driver) mouseClickEvent(c clickable, inBounds, wasDown, isDown bool) {
|
||||
if inBounds && wasDown && !isDown {
|
||||
//log.Printf("mouse click")
|
||||
c.registerMouseClick()
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Driver) mouseUpEvent(c clickable, inBounds, wasDown, isDown bool) {
|
||||
if inBounds {
|
||||
if wasDown && !isDown {
|
||||
//log.Printf("mouse down true -> false")
|
||||
c.setMouseDownState(false)
|
||||
}
|
||||
} else {
|
||||
if wasDown {
|
||||
//log.Printf("mouse down true -> false")
|
||||
c.setMouseDownState(false)
|
||||
}
|
||||
}
|
||||
}
|
202
internal/ui/group.go
Normal file
202
internal/ui/group.go
Normal file
@@ -0,0 +1,202 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"code.ur.gs/lupine/ordoor/internal/menus"
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
func (d *Driver) registerGroup(group *menus.Group) error {
|
||||
// log.Printf("Adding group %v: %#+v", group.Locator, group)
|
||||
|
||||
var dialogue bool
|
||||
|
||||
switch group.Type {
|
||||
case menus.TypeStatic, menus.TypeMainBackground, menus.TypeMenu, menus.TypeDragMenu, menus.TypeRadioMenu:
|
||||
case menus.TypeDialogue:
|
||||
dialogue = true
|
||||
default:
|
||||
return fmt.Errorf("Unknown group type: %v", group.Type)
|
||||
}
|
||||
|
||||
var groupWidget *Widget
|
||||
// Groups have a background sprite (FIXME: always?)
|
||||
if group.BaseSpriteID() >= 0 {
|
||||
var err error
|
||||
_, groupWidget, err = d.buildStatic(group.Props())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
groupWidget = &Widget{
|
||||
Locator: group.Locator,
|
||||
Active: group.Active,
|
||||
}
|
||||
}
|
||||
|
||||
if dialogue {
|
||||
d.dialogues = append(d.dialogues, groupWidget)
|
||||
} else {
|
||||
d.widgets = append(d.widgets, groupWidget)
|
||||
}
|
||||
|
||||
// TRadioGroup is best handled like this
|
||||
records, widget, err := d.maybeBuildInventorySelect(group, group.Records)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if widget != nil {
|
||||
groupWidget.Children = append(groupWidget.Children, widget)
|
||||
}
|
||||
|
||||
records, widget, err = d.maybeBuildListBox(group, records)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if widget != nil {
|
||||
groupWidget.Children = append(groupWidget.Children, widget)
|
||||
}
|
||||
|
||||
for _, record := range records {
|
||||
child, err := d.buildRecord(record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if child != nil {
|
||||
groupWidget.Children = append(groupWidget.Children, child)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Driver) buildRecord(r *menus.Record) (*Widget, error) {
|
||||
var widget *Widget
|
||||
var err error
|
||||
|
||||
switch r.Type {
|
||||
case menus.SubTypeSimpleButton, menus.SubTypeInvokeButton:
|
||||
_, widget, err = d.buildButton(r.Props())
|
||||
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:
|
||||
_, widget, err = d.buildHypertext(r.Props())
|
||||
case menus.SubTypeCheckbox:
|
||||
_, widget, err = d.buildCheckbox(r.Props())
|
||||
case menus.SubTypeEditBox:
|
||||
log.Printf("Unimplemented: SubTypeEditBox: %v", r.Locator) // TODO
|
||||
case menus.SubTypeRadioButton:
|
||||
log.Printf("Unimplemented: SubTypeRadioButton: %v", r.Locator) // TODO
|
||||
case menus.SubTypeDropdownButton:
|
||||
log.Printf("Unimplemented: SubTypeDropdownButton: %v", r.Locator) // TODO
|
||||
case menus.SubTypeComboBoxItem:
|
||||
log.Printf("Unimplemented: SubTypeComboBoxItem: %v", r.Locator) // TODO
|
||||
case menus.SubTypeAnimationSample:
|
||||
_, widget, err = d.buildAnimationSample(r.Props())
|
||||
case menus.SubTypeAnimationHover:
|
||||
_, widget, err = d.buildAnimationHover(r.Props())
|
||||
case menus.SubTypeMainButton:
|
||||
_, widget, err = d.buildMainButton(r.Props())
|
||||
case menus.SubTypeSlider:
|
||||
_, widget, err = d.buildSlider(r.Props()) // TODO: take sliders at an earlier point?
|
||||
case menus.SubTypeStatusBar:
|
||||
log.Printf("Unimplemented: SubTypeStatusBar: %v", r.Locator) // TODO
|
||||
default:
|
||||
return nil, fmt.Errorf("Unknown record type for %v: %v", r.Locator, r.Type)
|
||||
}
|
||||
|
||||
return widget, err
|
||||
}
|
||||
|
||||
func (d *Driver) maybeBuildInventorySelect(group *menus.Group, records []*menus.Record) ([]*menus.Record, *Widget, error) {
|
||||
var untouched []*menus.Record
|
||||
var touched []*menus.Record
|
||||
|
||||
for _, record := range records {
|
||||
if record.Type == menus.SubTypeInventorySelect {
|
||||
touched = append(touched, record)
|
||||
} else {
|
||||
untouched = append(untouched, record)
|
||||
}
|
||||
}
|
||||
|
||||
if len(touched) == 0 {
|
||||
return untouched, nil, nil
|
||||
}
|
||||
|
||||
elements := make([]*inventorySelect, len(touched))
|
||||
widget := &Widget{
|
||||
Locator: group.Locator,
|
||||
Active: group.Active,
|
||||
}
|
||||
|
||||
for i, record := range touched {
|
||||
element, childWidget, err := d.buildInventorySelect(record.Props())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
elements[i] = element
|
||||
widget.Children = append(widget.Children, childWidget)
|
||||
}
|
||||
|
||||
elements[0].setValue("1")
|
||||
|
||||
for _, element := range elements {
|
||||
element.others = elements
|
||||
}
|
||||
|
||||
return untouched, widget, nil
|
||||
}
|
||||
|
||||
func (d *Driver) maybeBuildListBox(group *menus.Group, records []*menus.Record) ([]*menus.Record, *Widget, error) {
|
||||
// Unless up, down, thumb, and items, are all present, it's not a listbox
|
||||
var up *menus.Record
|
||||
var down *menus.Record
|
||||
var thumb *menus.Record
|
||||
var items []*menus.Record
|
||||
|
||||
var untouched []*menus.Record
|
||||
|
||||
for _, rec := range records {
|
||||
switch rec.Type {
|
||||
case menus.SubTypeListBoxUp:
|
||||
if up != nil {
|
||||
return nil, nil, fmt.Errorf("Duplicate up buttons in menu %v", group.Locator)
|
||||
}
|
||||
up = rec
|
||||
case menus.SubTypeListBoxDown:
|
||||
if down != nil {
|
||||
return nil, nil, fmt.Errorf("Duplicate down buttons in menu %v", group.Locator)
|
||||
}
|
||||
down = rec
|
||||
case menus.SubTypeLineKbd, menus.SubTypeLineBriefing:
|
||||
items = append(items, rec)
|
||||
case menus.SubTypeThumb:
|
||||
if thumb != nil {
|
||||
return nil, nil, fmt.Errorf("Duplicate thumbs in menu %v", group.Locator)
|
||||
}
|
||||
thumb = rec
|
||||
default:
|
||||
// e.g. maingame:18.12 includes a button that is not part of the box
|
||||
untouched = append(untouched, rec)
|
||||
}
|
||||
}
|
||||
|
||||
// Since not all the elements are present, this isn't a listbox
|
||||
if len(items) == 0 || thumb == nil || up == nil || down == nil {
|
||||
return untouched, nil, nil
|
||||
}
|
||||
|
||||
_, widget, err := d.buildListBox(group, up, down, thumb, items...)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return untouched, widget, nil
|
||||
}
|
@@ -3,7 +3,7 @@ package ui
|
||||
import (
|
||||
"image"
|
||||
|
||||
"github.com/hajimehoshi/ebiten"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
||||
type region struct {
|
||||
|
@@ -4,10 +4,6 @@ import (
|
||||
"code.ur.gs/lupine/ordoor/internal/menus"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerBuilder(menus.TypeInventorySelect, ownedByMenu)
|
||||
}
|
||||
|
||||
// An inventory select is a sort of radio button. If 2 share the same menu,
|
||||
// selecting one deselects the other. Otherwise, they act like checkboxes.
|
||||
//
|
||||
@@ -20,33 +16,26 @@ type inventorySelect struct {
|
||||
}
|
||||
|
||||
// Called from the menu, which fills "others" for us
|
||||
func registerInventorySelect(d *Driver, r *menus.Record) (*inventorySelect, error) {
|
||||
sprites, err := d.menu.Sprites(r.Share, 3) // unchecked, checked, disabled
|
||||
func (d *Driver) buildInventorySelect(p *menus.Properties) (*inventorySelect, *Widget, error) {
|
||||
c, _, err := d.buildCheckbox(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
element := &inventorySelect{
|
||||
checkbox: checkbox{
|
||||
button: button{
|
||||
path: r.Path(),
|
||||
baseSpr: sprites[0], // unchecked
|
||||
clickSpr: sprites[1], // checked
|
||||
frozenSpr: sprites[2], // disabled
|
||||
hoverImpl: hoverImpl{text: r.Text},
|
||||
},
|
||||
// In an inventorySelect, the frozen and click sprites are reversed
|
||||
c.clickSpr, c.frozenSpr = c.frozenSpr, c.clickSpr
|
||||
|
||||
valueImpl: valueImpl{str: "0"},
|
||||
},
|
||||
element := &inventorySelect{checkbox: *c}
|
||||
widget := &Widget{
|
||||
Active: p.Active,
|
||||
ownClickables: []clickable{element},
|
||||
ownFreezables: []freezable{element},
|
||||
ownHoverables: []hoverable{element},
|
||||
ownPaintables: []paintable{element},
|
||||
ownValueables: []valueable{element},
|
||||
}
|
||||
|
||||
d.clickables = append(d.clickables, element)
|
||||
d.freezables = append(d.freezables, element)
|
||||
d.hoverables = append(d.hoverables, element)
|
||||
d.paintables = append(d.paintables, element)
|
||||
d.valueables = append(d.valueables, element)
|
||||
|
||||
return element, nil
|
||||
return element, widget, nil
|
||||
}
|
||||
|
||||
func (i *inventorySelect) registerMouseClick() {
|
||||
|
@@ -8,21 +8,14 @@ import (
|
||||
"code.ur.gs/lupine/ordoor/internal/menus"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerBuilder(menus.TypeLineKbd, ownedByMenu)
|
||||
registerBuilder(menus.TypeLineBriefing, ownedByMenu)
|
||||
|
||||
registerBuilder(menus.TypeThumb, ownedByMenu)
|
||||
registerBuilder(menus.TypeListBoxUp, ownedByMenu)
|
||||
registerBuilder(menus.TypeListBoxDown, ownedByMenu)
|
||||
}
|
||||
|
||||
// listBox is a TListBox in VCL terms. It has a number of lines of text, one of
|
||||
// which may be selected, and a slider with up and down buttons to scroll if the
|
||||
// options in the box exceed its viewing capacity.
|
||||
//
|
||||
// TODO: multi-select functionality? Is it needed?
|
||||
type listBox struct {
|
||||
locator string
|
||||
|
||||
upBtn *button
|
||||
downBtn *button
|
||||
|
||||
@@ -30,7 +23,6 @@ type listBox struct {
|
||||
thumbBase *assetstore.Sprite // Bounds are given by this
|
||||
thumbImg *assetstore.Sprite // This is displayed at offset * (height / steps)
|
||||
|
||||
base *noninteractive // The menu itself has a sprite to display
|
||||
lines []*noninteractive // We display to these
|
||||
|
||||
// The list box acts as a window onto these
|
||||
@@ -40,71 +32,32 @@ type listBox struct {
|
||||
offset int
|
||||
}
|
||||
|
||||
func registerListBox(d *Driver, menu *menus.Record) ([]*menus.Record, error) {
|
||||
var upBtn *menus.Record
|
||||
var downBtn *menus.Record
|
||||
var thumb *menus.Record
|
||||
var items []*menus.Record
|
||||
|
||||
for _, rec := range menu.Children {
|
||||
switch rec.Type {
|
||||
case menus.TypeListBoxUp:
|
||||
if upBtn != nil {
|
||||
return nil, fmt.Errorf("Duplicate up buttons in menu %v", menu.Locator())
|
||||
}
|
||||
upBtn = rec
|
||||
case menus.TypeListBoxDown:
|
||||
if downBtn != nil {
|
||||
return nil, fmt.Errorf("Duplicate down buttons in menu %v", menu.Locator())
|
||||
}
|
||||
downBtn = rec
|
||||
case menus.TypeLineKbd, menus.TypeLineBriefing:
|
||||
items = append(items, rec)
|
||||
case menus.TypeThumb:
|
||||
if thumb != nil {
|
||||
return nil, fmt.Errorf("Duplicate thumbs in menu %v", menu.Locator())
|
||||
}
|
||||
thumb = rec
|
||||
default:
|
||||
return nil, fmt.Errorf("Unrecognised child in listbox menu: %v", rec.Locator())
|
||||
}
|
||||
}
|
||||
|
||||
if len(items) == 0 || thumb == nil || upBtn == nil || downBtn == nil {
|
||||
return nil, fmt.Errorf("Missing items in menu %v", menu.Locator())
|
||||
}
|
||||
|
||||
// Now build the wonderful thing
|
||||
baseElem, err := registerNoninteractive(d, menu)
|
||||
func (d *Driver) buildListBox(group *menus.Group, up, down, thumb *menus.Record, items ...*menus.Record) (*listBox, *Widget, error) {
|
||||
upElem, upWidget, err := d.buildButton(up.Props())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
elemUp, err := registerButton(d, upBtn, upBtn.SpriteId[0])
|
||||
downElem, downWidget, err := d.buildButton(down.Props())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
elemDown, err := registerButton(d, downBtn, downBtn.SpriteId[0])
|
||||
thumbBaseSpr, err := d.menu.Sprite(thumb.ObjectIdx, thumb.Share)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
thumbBaseSpr, err := d.menu.Sprite(thumb.Share)
|
||||
thumbImgSpr, err := d.menu.Sprite(thumb.ObjectIdx, thumb.BaseSpriteID())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
thumbImgSpr, err := d.menu.Sprite(thumb.SpriteId[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
element := &listBox{
|
||||
base: baseElem,
|
||||
locator: group.Locator,
|
||||
// TODO: upBtn needs to be frozen when offset == 0; downBtn when offset == max
|
||||
upBtn: elemUp,
|
||||
downBtn: elemDown,
|
||||
upBtn: upElem,
|
||||
downBtn: downElem,
|
||||
|
||||
// TODO: need to be able to drag the thumb
|
||||
thumbBase: thumbBaseSpr,
|
||||
@@ -112,8 +65,8 @@ func registerListBox(d *Driver, menu *menus.Record) ([]*menus.Record, error) {
|
||||
}
|
||||
|
||||
// Internal wiring-up
|
||||
elemUp.onClick(element.up)
|
||||
elemDown.onClick(element.down)
|
||||
upElem.onClick(element.up)
|
||||
downElem.onClick(element.down)
|
||||
|
||||
// FIXME: Test data for now
|
||||
for i := 0; i < 50; i++ {
|
||||
@@ -122,30 +75,48 @@ func registerListBox(d *Driver, menu *menus.Record) ([]*menus.Record, error) {
|
||||
|
||||
// Register everything. Since we're a composite of other controls, they are
|
||||
// mostly self-registered at the moment.
|
||||
d.paintables = append(d.paintables, element)
|
||||
widget := &Widget{
|
||||
Children: []*Widget{upWidget, downWidget},
|
||||
Active: group.Active, // FIXME: children have their own active state
|
||||
ownPaintables: []paintable{element},
|
||||
ownValueables: []valueable{element},
|
||||
}
|
||||
|
||||
// FIXME: we should be able to freeze/unfreeze as a group.
|
||||
|
||||
// HURK: These need to be registered after the other elements so they are
|
||||
// drawn in the correct order to be visible
|
||||
for _, rec := range items {
|
||||
ni, err := registerNoninteractive(d, rec)
|
||||
ni, niWidget, err := d.buildStatic(rec.Props())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
niWidget.ownClickables = append(niWidget.ownClickables, ni)
|
||||
|
||||
// TODO: pick the correct font
|
||||
ni.label = &label{
|
||||
align: AlignModeLeft,
|
||||
font: d.menu.Font(0),
|
||||
rect: ni.rect,
|
||||
}
|
||||
|
||||
element.lines = append(element.lines, ni)
|
||||
widget.Children = append(widget.Children, niWidget)
|
||||
}
|
||||
|
||||
element.refresh()
|
||||
|
||||
return nil, nil
|
||||
return element, widget, nil
|
||||
}
|
||||
|
||||
func (l *listBox) id() string {
|
||||
return l.locator
|
||||
}
|
||||
|
||||
func (l *listBox) value() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (l *listBox) setValue(s string) {
|
||||
}
|
||||
|
||||
func (l *listBox) SetStrings(to []string) {
|
||||
@@ -186,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]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,72 +0,0 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"code.ur.gs/lupine/ordoor/internal/menus"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// These menu types don't need driving, so we can ignore them
|
||||
registerBuilder(menus.TypeMenu, registerMenu)
|
||||
registerBuilder(menus.TypeDragMenu, nil) // Menus are just containers
|
||||
}
|
||||
|
||||
func registerMenu(d *Driver, r *menus.Record) ([]*menus.Record, error) {
|
||||
childrenLeft, err := listBoxFromMenu(d, r, r.Children)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
childrenLeft, err = inventorySelectFromMenu(d, r, childrenLeft)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return all the unhandled children to be processed further
|
||||
return childrenLeft, nil
|
||||
}
|
||||
|
||||
func listBoxFromMenu(d *Driver, menu *menus.Record, children []*menus.Record) ([]*menus.Record, error) {
|
||||
ok := false
|
||||
for _, rec := range children {
|
||||
if rec.Type == menus.TypeThumb { // FIXME: we're using this to indicate a listbox
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return children, nil
|
||||
}
|
||||
|
||||
return registerListBox(d, menu)
|
||||
}
|
||||
|
||||
// Group all inventory selects that share a menu together
|
||||
func inventorySelectFromMenu(d *Driver, menu *menus.Record, children []*menus.Record) ([]*menus.Record, error) {
|
||||
var childrenLeft []*menus.Record
|
||||
var inventorySelects []*inventorySelect
|
||||
|
||||
for _, child := range children {
|
||||
switch child.Type {
|
||||
case menus.TypeInventorySelect:
|
||||
is, err := registerInventorySelect(d, child)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
inventorySelects = append(inventorySelects, is)
|
||||
default:
|
||||
childrenLeft = append(childrenLeft, child)
|
||||
}
|
||||
}
|
||||
|
||||
if len(inventorySelects) > 0 {
|
||||
inventorySelects[0].setValue("1") // Always start with one selected
|
||||
|
||||
for _, is := range inventorySelects {
|
||||
is.others = inventorySelects
|
||||
}
|
||||
}
|
||||
|
||||
return childrenLeft, nil
|
||||
}
|
@@ -1,6 +1,7 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"log"
|
||||
|
||||
@@ -15,22 +16,14 @@ const (
|
||||
AlignModeLeft AlignMode = 1
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerBuilder(menus.TypeStatic, noChildren(registerStatic))
|
||||
registerBuilder(menus.TypeHypertext, noChildren(registerHypertext))
|
||||
registerBuilder(menus.TypeOverlay, noChildren(registerOverlay))
|
||||
registerBuilder(menus.TypeAnimationSample, noChildren(registerAnimation))
|
||||
registerBuilder(menus.TypeAnimationHover, noChildren(registerAnimationHover))
|
||||
}
|
||||
|
||||
// A non-interactive element is not a widget; it merely displays some pixels and
|
||||
// may optionally have a tooltip for display within bounds.
|
||||
//
|
||||
// For non-animated non-interactive elements, just give them a single frame.
|
||||
type noninteractive struct {
|
||||
path string
|
||||
frames animation
|
||||
rect image.Rectangle
|
||||
locator string
|
||||
frames animation
|
||||
rect image.Rectangle
|
||||
|
||||
// Some non-interactives, e.g., overlays, are an image + text to be shown
|
||||
label *label
|
||||
@@ -39,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
|
||||
@@ -58,145 +52,186 @@ type animationHover struct {
|
||||
closing bool
|
||||
}
|
||||
|
||||
func registerStatic(d *Driver, r *menus.Record) error {
|
||||
_, err := registerNoninteractive(d, r)
|
||||
return err
|
||||
}
|
||||
|
||||
func registerNoninteractive(d *Driver, r *menus.Record) (*noninteractive, error) {
|
||||
// FIXME: SpriteID takes precedence over SHARE if present, but is that right?
|
||||
spriteId := r.Share
|
||||
if len(r.SpriteId) > 0 && r.SpriteId[0] != -1 {
|
||||
spriteId = r.SpriteId[0]
|
||||
func (d *Driver) buildNoninteractive(p *menus.Properties) (*noninteractive, error) {
|
||||
// FIXME: SpriteID takes precedence over SHARE if present, but is that
|
||||
// always right?
|
||||
spriteId := p.BaseSpriteID()
|
||||
if spriteId < 0 {
|
||||
return nil, fmt.Errorf("No base sprite for %v", p.Locator)
|
||||
}
|
||||
|
||||
sprite, err := d.menu.Sprite(spriteId)
|
||||
sprite, err := d.menu.Sprite(p.ObjectIdx, spriteId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ni := &noninteractive{
|
||||
path: r.Path(),
|
||||
frames: animation{sprite.Image},
|
||||
hoverImpl: hoverImpl{text: r.Text},
|
||||
rect: sprite.Rect,
|
||||
locator: p.Locator,
|
||||
frames: animation{sprite.Image},
|
||||
rect: sprite.Rect.Add(p.Point()),
|
||||
}
|
||||
|
||||
d.hoverables = append(d.hoverables, ni)
|
||||
d.paintables = append(d.paintables, ni)
|
||||
|
||||
return ni, nil
|
||||
}
|
||||
|
||||
func registerHypertext(d *Driver, r *menus.Record) error {
|
||||
sprite, err := d.menu.Sprite(r.Share)
|
||||
func (d *Driver) buildStatic(p *menus.Properties) (*noninteractive, *Widget, error) {
|
||||
ni, err := d.buildNoninteractive(p)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
ni := &noninteractive{
|
||||
path: r.Path(),
|
||||
hoverImpl: hoverImpl{text: r.Text},
|
||||
rect: sprite.Rect,
|
||||
ni.hoverImpl.text = p.Text
|
||||
|
||||
widget := &Widget{
|
||||
Locator: ni.locator,
|
||||
Active: p.Active,
|
||||
ownClickables: []clickable{ni}, // FIXME: credits background needs to be clickable
|
||||
ownHoverables: []hoverable{ni},
|
||||
ownPaintables: []paintable{ni},
|
||||
}
|
||||
|
||||
d.clickables = append(d.clickables, ni)
|
||||
d.hoverables = append(d.hoverables, ni)
|
||||
return ni, widget, nil
|
||||
}
|
||||
|
||||
return nil
|
||||
func (d *Driver) buildHypertext(p *menus.Properties) (*noninteractive, *Widget, error) {
|
||||
ni, err := d.buildNoninteractive(p)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// 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},
|
||||
}
|
||||
|
||||
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 registerOverlay(d *Driver, r *menus.Record) error {
|
||||
sprite, err := d.menu.Sprite(r.Share)
|
||||
func (d *Driver) buildOverlay(p *menus.Properties) (*noninteractive, *Widget, error) {
|
||||
ni, err := d.buildNoninteractive(p)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
ni := &noninteractive{
|
||||
path: r.Path(),
|
||||
frames: animation{sprite.Image},
|
||||
rect: sprite.Rect,
|
||||
widget := &Widget{
|
||||
Locator: ni.locator,
|
||||
Active: p.Active,
|
||||
ownPaintables: []paintable{ni},
|
||||
}
|
||||
|
||||
d.paintables = append(d.paintables, ni)
|
||||
|
||||
if r.Text != "" {
|
||||
if p.Text != "" {
|
||||
// FIXME: is this always right? Seems to make sense for Main.mnu
|
||||
fnt := d.menu.Font(r.FontType/10 - 1)
|
||||
fnt := d.menu.Font(p.FontType/10 - 1)
|
||||
|
||||
ni.label = &label{
|
||||
font: fnt,
|
||||
rect: ni.rect, // We will be centered by default
|
||||
text: r.Text,
|
||||
font: fnt,
|
||||
rect: ni.rect, // We will be centered by default
|
||||
valueImpl: valueImpl{str: p.Text},
|
||||
}
|
||||
} else {
|
||||
log.Printf("Overlay without text detected: %#+v", r)
|
||||
log.Printf("Overlay without text detected in %v", p.Locator)
|
||||
}
|
||||
|
||||
return nil
|
||||
return ni, widget, nil
|
||||
}
|
||||
|
||||
// An animation is a non-interactive element that displays something in a loop
|
||||
func registerAnimation(d *Driver, r *menus.Record) error {
|
||||
sprite, err := d.menu.Sprite(r.SpriteId[0])
|
||||
func (d *Driver) buildAnimationSample(p *menus.Properties) (*noninteractive, *Widget, error) {
|
||||
sprite, err := d.menu.Sprite(p.ObjectIdx, p.SpriteId[0])
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
frames, err := d.menu.Images(r.SpriteId[0], r.DrawType)
|
||||
frames, err := d.menu.Images(p.ObjectIdx, p.SpriteId[0], p.DrawType)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
ani := &noninteractive{
|
||||
path: r.Path(),
|
||||
locator: p.Locator,
|
||||
frames: animation(frames),
|
||||
hoverImpl: hoverImpl{text: r.Text},
|
||||
rect: sprite.Rect,
|
||||
hoverImpl: hoverImpl{text: p.Text},
|
||||
rect: sprite.Rect.Add(p.Point()),
|
||||
}
|
||||
|
||||
d.hoverables = append(d.hoverables, ani)
|
||||
d.paintables = append(d.paintables, ani)
|
||||
widget := &Widget{
|
||||
Active: p.Active,
|
||||
ownHoverables: []hoverable{ani},
|
||||
ownPaintables: []paintable{ani},
|
||||
}
|
||||
|
||||
return nil
|
||||
return ani, widget, nil
|
||||
}
|
||||
|
||||
func registerAnimationHover(d *Driver, r *menus.Record) error {
|
||||
sprite, err := d.menu.Sprite(r.SpriteId[0])
|
||||
func (d *Driver) buildAnimationHover(p *menus.Properties) (*animationHover, *Widget, error) {
|
||||
sprite, err := d.menu.Sprite(p.ObjectIdx, p.SpriteId[0])
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
enterFrames, err := d.menu.Images(r.SpriteId[0], r.DrawType)
|
||||
enterFrames, err := d.menu.Images(p.ObjectIdx, p.SpriteId[0], p.DrawType)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
exitFrames, err := d.menu.Images(r.SpriteId[0]+r.DrawType, r.DrawType)
|
||||
exitFrames, err := d.menu.Images(p.ObjectIdx, p.SpriteId[0]+p.DrawType, p.DrawType)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
ani := &animationHover{
|
||||
noninteractive: noninteractive{
|
||||
path: r.Path(),
|
||||
locator: p.Locator,
|
||||
frames: animation(enterFrames),
|
||||
hoverImpl: hoverImpl{text: r.Text},
|
||||
rect: sprite.Rect,
|
||||
hoverImpl: hoverImpl{text: p.Text},
|
||||
rect: sprite.Rect.Add(p.Point()),
|
||||
},
|
||||
|
||||
exitFrames: animation(exitFrames),
|
||||
}
|
||||
|
||||
d.hoverables = append(d.hoverables, ani)
|
||||
d.paintables = append(d.paintables, ani)
|
||||
widget := &Widget{
|
||||
Active: p.Active,
|
||||
ownHoverables: []hoverable{ani},
|
||||
ownPaintables: []paintable{ani},
|
||||
}
|
||||
|
||||
return nil
|
||||
return ani, widget, nil
|
||||
}
|
||||
|
||||
func (n *noninteractive) id() string {
|
||||
return n.path
|
||||
return n.locator
|
||||
}
|
||||
|
||||
func (n *noninteractive) bounds() image.Rectangle {
|
||||
@@ -246,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.
|
||||
//
|
||||
@@ -253,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 {
|
||||
@@ -280,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
|
||||
|
@@ -9,11 +9,6 @@ import (
|
||||
"code.ur.gs/lupine/ordoor/internal/menus"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerBuilder(menus.TypeCheckbox, noChildren(registerCheckbox))
|
||||
registerBuilder(menus.TypeSlider, noChildren(registerSlider))
|
||||
}
|
||||
|
||||
// A checkbox can be a fancy button
|
||||
type checkbox struct {
|
||||
button
|
||||
@@ -23,7 +18,9 @@ type checkbox struct {
|
||||
|
||||
// A slider is harder. Two separate elements to render
|
||||
type slider struct {
|
||||
path string
|
||||
locator string
|
||||
|
||||
rect image.Rectangle
|
||||
|
||||
baseSpr *assetstore.Sprite
|
||||
clickSpr *assetstore.Sprite
|
||||
@@ -38,52 +35,62 @@ type slider struct {
|
||||
}
|
||||
|
||||
// A checkbox has 3 sprites, and 3 states: unchecked, checked, disabled.
|
||||
func registerCheckbox(d *Driver, r *menus.Record) error {
|
||||
sprites, err := d.menu.Sprites(r.Share, 3) // unchecked, disabled, checked
|
||||
func (d *Driver) buildCheckbox(p *menus.Properties) (*checkbox, *Widget, error) {
|
||||
sprites, err := d.menu.Sprites(p.ObjectIdx, p.Share, 3) // unchecked, disabled, checked
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
checkbox := &checkbox{
|
||||
button: button{
|
||||
path: r.Path(),
|
||||
locator: p.Locator,
|
||||
rect: sprites[0].Rect.Add(p.Point()),
|
||||
baseSpr: sprites[0], // unchecked
|
||||
clickSpr: sprites[2], // checked
|
||||
frozenSpr: sprites[1],
|
||||
hoverImpl: hoverImpl{text: r.Text},
|
||||
frozenSpr: sprites[1], // disabled
|
||||
hoverImpl: hoverImpl{text: p.Text},
|
||||
},
|
||||
valueImpl: valueImpl{str: "0"},
|
||||
}
|
||||
|
||||
d.clickables = append(d.clickables, checkbox)
|
||||
d.freezables = append(d.freezables, checkbox)
|
||||
d.hoverables = append(d.hoverables, checkbox)
|
||||
d.paintables = append(d.paintables, checkbox)
|
||||
d.valueables = append(d.valueables, checkbox)
|
||||
widget := &Widget{
|
||||
Locator: p.Locator,
|
||||
Active: p.Active,
|
||||
ownClickables: []clickable{checkbox},
|
||||
ownFreezables: []freezable{checkbox},
|
||||
ownHoverables: []hoverable{checkbox},
|
||||
ownPaintables: []paintable{checkbox},
|
||||
ownValueables: []valueable{checkbox},
|
||||
}
|
||||
|
||||
return nil
|
||||
return checkbox, widget, nil
|
||||
}
|
||||
|
||||
func registerSlider(d *Driver, r *menus.Record) error {
|
||||
sprites, err := d.menu.Sprites(r.Share, 3) // base, clicked, slider element
|
||||
func (d *Driver) buildSlider(p *menus.Properties) (*slider, *Widget, error) {
|
||||
sprites, err := d.menu.Sprites(p.ObjectIdx, p.Share, 3) // base, clicked, slider element
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
slider := &slider{
|
||||
path: r.Path(),
|
||||
locator: p.Locator,
|
||||
rect: sprites[0].Rect.Add(p.Point()),
|
||||
baseSpr: sprites[0],
|
||||
clickSpr: sprites[1],
|
||||
sliderSpr: sprites[2],
|
||||
hv: sprites[0].Rect.Dy() > sprites[0].Rect.Dx(), // A best guess
|
||||
}
|
||||
|
||||
d.clickables = append(d.clickables, slider)
|
||||
d.mouseables = append(d.mouseables, slider)
|
||||
d.paintables = append(d.paintables, slider)
|
||||
d.valueables = append(d.valueables, slider)
|
||||
widget := &Widget{
|
||||
Locator: p.Locator,
|
||||
Active: p.Active,
|
||||
ownClickables: []clickable{slider},
|
||||
ownMouseables: []mouseable{slider},
|
||||
ownPaintables: []paintable{slider},
|
||||
ownValueables: []valueable{slider},
|
||||
}
|
||||
|
||||
return nil
|
||||
return slider, widget, nil
|
||||
}
|
||||
|
||||
func (c *checkbox) registerMouseClick() {
|
||||
@@ -107,12 +114,12 @@ func (c *checkbox) regions(tick int) []region {
|
||||
}
|
||||
|
||||
func (s *slider) id() string {
|
||||
return s.path
|
||||
return s.locator
|
||||
}
|
||||
|
||||
// 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() {
|
||||
|
128
internal/ui/value.go
Normal file
128
internal/ui/value.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func (d *Driver) realId(id string) string {
|
||||
return fmt.Sprintf("%v:%v", d.menu.Name, id)
|
||||
}
|
||||
|
||||
func (d *Driver) Value(id string, into *string) error {
|
||||
for _, valueable := range d.allValueables() {
|
||||
if valueable.id() == d.realId(id) {
|
||||
*into = valueable.value()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("Couldn't find valueable widget %v:%v", d.menu.Name, id)
|
||||
}
|
||||
|
||||
func (d *Driver) SetValue(id, value string) error {
|
||||
for _, valueable := range d.allValueables() {
|
||||
if valueable.id() == d.realId(id) {
|
||||
valueable.setValue(value)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("Couldn't find valueable widget %v:%v", d.menu.Name, id)
|
||||
}
|
||||
|
||||
func (d *Driver) ValueBool(id string, into *bool) error {
|
||||
var vStr string
|
||||
if err := d.Value(id, &vStr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*into = vStr == "1"
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Driver) SetValueBool(id string, value bool) error {
|
||||
vStr := "0"
|
||||
if value {
|
||||
vStr = "1"
|
||||
}
|
||||
|
||||
return d.SetValue(id, vStr)
|
||||
}
|
||||
|
||||
func (d *Driver) SetFreeze(id string, value bool) error {
|
||||
for _, freezable := range d.allFreezables() {
|
||||
if freezable.id() == d.realId(id) {
|
||||
freezable.setFreezeState(value)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
clickable.onClick(f)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("Couldn't find clickable widget %v:%v", d.menu.Name, id)
|
||||
}
|
||||
|
||||
// FIXME: HURK. Surely I'm missing something? steps is value:offset
|
||||
func (d *Driver) ConfigureSlider(id string, steps map[int]int) error {
|
||||
for _, clickable := range d.activeClickables() {
|
||||
if slider, ok := clickable.(*slider); ok && slider.id() == d.realId(id) {
|
||||
slider.steps = steps
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("Couldn't find slider %v:%v", d.menu.Name, id)
|
||||
}
|
||||
|
||||
func (d *Driver) ValueInt(id string, into *int) error {
|
||||
var vStr string
|
||||
if err := d.Value(id, &vStr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
value, err := strconv.Atoi(vStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*into = value
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Driver) SetValueInt(id string, value int) error {
|
||||
vStr := strconv.Itoa(value)
|
||||
|
||||
return d.SetValue(id, vStr)
|
||||
}
|
142
internal/ui/widget.go
Normal file
142
internal/ui/widget.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package ui
|
||||
|
||||
type Widget struct {
|
||||
Locator string
|
||||
Children []*Widget
|
||||
Active bool
|
||||
|
||||
ownClickables []clickable
|
||||
ownFreezables []freezable
|
||||
ownHoverables []hoverable
|
||||
ownMouseables []mouseable
|
||||
ownPaintables []paintable
|
||||
ownValueables []valueable
|
||||
}
|
||||
|
||||
func (w *Widget) allClickables() []clickable {
|
||||
out := w.ownClickables
|
||||
|
||||
for _, widget := range w.Children {
|
||||
out = append(out, widget.allClickables()...)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (w *Widget) allFreezables() []freezable {
|
||||
out := w.ownFreezables
|
||||
|
||||
for _, widget := range w.Children {
|
||||
out = append(out, widget.allFreezables()...)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (w *Widget) allValueables() []valueable {
|
||||
out := w.ownValueables
|
||||
|
||||
for _, widget := range w.Children {
|
||||
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
|
||||
}
|
@@ -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 {
|
||||
@@ -18,15 +18,26 @@ type Game interface {
|
||||
Draw(*ebiten.Image) error
|
||||
}
|
||||
|
||||
type CustomCursor interface {
|
||||
// The cursor draw operation
|
||||
Cursor() (*ebiten.Image, *ebiten.DrawImageOptions, error)
|
||||
}
|
||||
|
||||
var (
|
||||
screenScale = flag.Float64("screen-scale", 1.0, "Scale the window by this factor")
|
||||
cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`")
|
||||
)
|
||||
|
||||
// TODO: move all scaling into Window, so drivers only need to cope with one
|
||||
// coordinate space. This will allow us to draw custom mouse cursors in the
|
||||
// window, rather than in the driver.
|
||||
type Window struct {
|
||||
Title string
|
||||
KeyUpHandlers map[ebiten.Key]func()
|
||||
MouseWheelHandler func(float64, float64)
|
||||
MouseClickHandler func()
|
||||
|
||||
WhileKeyDownHandlers map[ebiten.Key]func()
|
||||
|
||||
// Allow the "game" to be switched out at any time
|
||||
game Game
|
||||
@@ -42,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
|
||||
}
|
||||
|
||||
@@ -60,15 +72,47 @@ 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
|
||||
}
|
||||
|
||||
func (w *Window) OnMouseClick(f func()) {
|
||||
w.MouseClickHandler = f
|
||||
}
|
||||
|
||||
func (w *Window) Layout(_, _ int) (int, int) {
|
||||
return w.xRes, w.yRes
|
||||
}
|
||||
|
||||
func (w *Window) Update(screen *ebiten.Image) (outErr error) {
|
||||
func (w *Window) drawCursor(screen *ebiten.Image) error {
|
||||
cIface, ok := w.game.(CustomCursor)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
cursor, op, err := cIface.Cursor()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Hide the system cursor if we have a custom one
|
||||
if cursor == nil {
|
||||
ebiten.SetCursorMode(ebiten.CursorModeVisible)
|
||||
return nil
|
||||
}
|
||||
|
||||
ebiten.SetCursorMode(ebiten.CursorModeHidden)
|
||||
|
||||
screen.DrawImage(cursor, op)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -80,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 {
|
||||
@@ -93,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 {
|
||||
@@ -100,13 +152,17 @@ func (w *Window) Update(screen *ebiten.Image) (outErr error) {
|
||||
}
|
||||
}
|
||||
|
||||
if ebiten.IsDrawingSkipped() {
|
||||
return nil
|
||||
if w.MouseClickHandler != nil {
|
||||
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
|
||||
w.MouseClickHandler()
|
||||
}
|
||||
}
|
||||
|
||||
if err := w.game.Draw(screen); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Window) Draw(screen *ebiten.Image) {
|
||||
w.game.Draw(screen)
|
||||
|
||||
if w.debug {
|
||||
// Draw FPS, etc, to the screen
|
||||
@@ -114,7 +170,8 @@ func (w *Window) Update(screen *ebiten.Image) (outErr error) {
|
||||
ebitenutil.DebugPrint(screen, msg)
|
||||
}
|
||||
|
||||
return nil
|
||||
// Draw the cursor last
|
||||
w.drawCursor(screen)
|
||||
}
|
||||
|
||||
// TODO: a stop or other cancellation mechanism
|
||||
|
@@ -4,6 +4,7 @@ package asciiscan
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
@@ -15,6 +16,9 @@ var hashComment = []byte("#")
|
||||
type Scanner struct {
|
||||
bufio *bufio.Scanner
|
||||
closer io.Closer
|
||||
|
||||
// If we've peeked, there will be items here
|
||||
buffered []string
|
||||
}
|
||||
|
||||
func New(filename string) (*Scanner, error) {
|
||||
@@ -38,6 +42,13 @@ func (s *Scanner) Close() error {
|
||||
}
|
||||
|
||||
func (s *Scanner) ConsumeString() (string, error) {
|
||||
if len(s.buffered) > 0 {
|
||||
out, buffered := s.buffered[0], s.buffered[1:]
|
||||
s.buffered = buffered
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
for s.bufio.Scan() {
|
||||
line := s.bufio.Bytes()
|
||||
|
||||
@@ -68,15 +79,41 @@ func ConsumeProperty(s string) (string, string) {
|
||||
}
|
||||
|
||||
parts := strings.SplitN(s, ":", 2)
|
||||
|
||||
return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
|
||||
}
|
||||
|
||||
// Peek ahead in the input stream to see if the next line might be a property
|
||||
// (contain a colon character).
|
||||
// Check to see if the line looks like a property (contains a colon character).
|
||||
func IsProperty(s string) bool {
|
||||
return strings.Contains(s, ":")
|
||||
}
|
||||
|
||||
// Checks if the next line might be a property, without reading it
|
||||
func (s *Scanner) PeekProperty() (bool, error) {
|
||||
str, err := s.ConsumeString()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
s.buffered = append(s.buffered, str)
|
||||
|
||||
return IsProperty(str), nil
|
||||
}
|
||||
|
||||
func (s *Scanner) ConsumeProperty() (string, string, error) {
|
||||
str, err := s.ConsumeString()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
if !IsProperty(str) {
|
||||
return "", "", fmt.Errorf("Not a property: %q", str)
|
||||
}
|
||||
|
||||
k, v := ConsumeProperty(str)
|
||||
return k, v, nil
|
||||
}
|
||||
|
||||
func (s *Scanner) ConsumeInt() (int, error) {
|
||||
str, err := s.ConsumeString()
|
||||
if err != nil {
|
||||
@@ -86,6 +123,48 @@ func (s *Scanner) ConsumeInt() (int, error) {
|
||||
return strconv.Atoi(str)
|
||||
}
|
||||
|
||||
func (s *Scanner) ConsumeBool() (bool, error) {
|
||||
integer, err := s.ConsumeInt()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return (integer > 0), nil
|
||||
}
|
||||
|
||||
// Reads a list of non-property lines, skipping any that match the given strings
|
||||
func (s *Scanner) ConsumeStringList(skip ...string) ([]string, error) {
|
||||
skipper := make(map[string]bool, len(skip))
|
||||
for _, str := range skip {
|
||||
skipper[str] = true
|
||||
}
|
||||
|
||||
var out []string
|
||||
|
||||
for {
|
||||
isProp, err := s.PeekProperty()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The object list is terminated by the first property
|
||||
if isProp {
|
||||
break
|
||||
}
|
||||
|
||||
str, err := s.ConsumeString()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !skipper[str] {
|
||||
out = append(out, str)
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *Scanner) ConsumeIntPtr(to *int) error {
|
||||
val, err := s.ConsumeInt()
|
||||
if err != nil {
|
||||
@@ -96,6 +175,16 @@ func (s *Scanner) ConsumeIntPtr(to *int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Scanner) ConsumeBoolPtr(to *bool) error {
|
||||
val, err := s.ConsumeBool()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*to = val
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Scanner) ConsumeIntPtrs(ptrs ...*int) error {
|
||||
for _, ptr := range ptrs {
|
||||
if err := s.ConsumeIntPtr(ptr); err != nil {
|
||||
@@ -105,3 +194,13 @@ func (s *Scanner) ConsumeIntPtrs(ptrs ...*int) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Scanner) ConsumeBoolPtrs(ptrs ...*bool) error {
|
||||
for _, ptr := range ptrs {
|
||||
if err := s.ConsumeBoolPtr(ptr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@@ -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
|
@@ -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 |
@@ -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
|
Binary file not shown.
@@ -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
45
scripts/palette-from-pcx
Executable 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
|
@@ -19,7 +19,7 @@ module Obj
|
||||
|
||||
def self.parse(data)
|
||||
hdr = new(*data[0..SIZE - 1].unpack("VVVVV"))
|
||||
pp hdr
|
||||
# pp hdr
|
||||
hdr.validate!(data.bytes.size)
|
||||
hdr
|
||||
end
|
||||
@@ -96,7 +96,7 @@ module Obj
|
||||
DirEntry.parse(rel_data.byteslice(rel_offset, DirEntry::SIZE))
|
||||
end
|
||||
|
||||
pp entries
|
||||
# pp entries
|
||||
|
||||
new(entries)
|
||||
end
|
||||
@@ -380,6 +380,18 @@ def correlate(filenames)
|
||||
pp results
|
||||
end
|
||||
|
||||
def directory(filename, num)
|
||||
data = File.read(filename).force_encoding("BINARY")
|
||||
|
||||
hdr = Obj::Header.parse(data)
|
||||
dir = Obj::SpriteDir.parse(data[hdr.dir_range])
|
||||
entry = dir.entries[num]
|
||||
|
||||
puts "Sprite directory starts at 0x#{hdr.dir_offset.to_s(16)}"
|
||||
puts "Directory entry for sprite #{num} is at 0x#{(hdr.dir_offset + (Obj::DirEntry::SIZE * num)).to_s(16)}"
|
||||
puts "Sprite #{num} is at 0x#{(hdr.data_offset + entry.rel_offset).to_s(16)} and is #{entry.sprite_size} bytes"
|
||||
end
|
||||
|
||||
def sprites(filename)
|
||||
obj = load_obj(filename)
|
||||
|
||||
@@ -499,7 +511,25 @@ def build(filename)
|
||||
File.open(filename, "w") { |f| f.write(built.to_data) }
|
||||
end
|
||||
|
||||
def unknown16(filenames)
|
||||
objs = filenames.map { |f| load_obj(f) }
|
||||
results = Set.new
|
||||
|
||||
objs.each do |obj|
|
||||
obj.sprites.each do |spr|
|
||||
results << spr.header.unknown16
|
||||
end
|
||||
end
|
||||
|
||||
puts "Unique widths for u16,4"
|
||||
pp results
|
||||
end
|
||||
|
||||
case command = ARGV.shift
|
||||
when "directory" then
|
||||
directory(ARGV[0], ARGV[1].to_i)
|
||||
when "unknown16" then
|
||||
unknown16(ARGV)
|
||||
when "sprites" then
|
||||
ARGV.each { |filename| sprites(filename) }
|
||||
when "sprite" then
|
||||
|
Reference in New Issue
Block a user