Compare commits

...

47 Commits

Author SHA1 Message Date
55c2232e08 Fix a SIGSEGV 2024-10-23 23:54:48 +01:00
ac4675fa2c Fix some log.Fatal calls 2024-10-23 23:47:55 +01:00
5f7654d267 Update dependencies 2024-10-23 23:45:26 +01:00
89888ce004 Stringify AnimAction 2024-10-23 23:44:44 +01:00
16767da6f1 Bump a few more dependencies 2023-07-16 23:11:01 +01:00
c5b80ed8bc Bump ebiten 2023-07-16 22:51:16 +01:00
891edecc60 Update dependencies 2023-01-17 22:10:46 +00:00
85979834c8 Update ebiten and golang 2021-10-23 22:44:39 +01:00
96dbb297cd Bump ebiten 2021-03-01 22:26:45 +00:00
92fa0fc5d6 UNTESTED: ebiten v2 2020-11-21 19:27:09 +00:00
c5e6abb798 First attempt at character orientation 2020-06-13 23:10:21 +01:00
5df050b4ef Substitute unknown glyphs 2020-06-13 18:23:50 +01:00
4d336b9189 Get character stats (kind of) displaying in-scenario 2020-06-13 18:11:45 +01:00
3b7cfb6ecc Drag flow into view-map
This is pretty awful, but will let me wire up items more easily without
needing to do the big refactor into independent menu handlers
2020-06-13 16:37:39 +01:00
7677c30572 Start displaying characters on maps 2020-06-13 15:07:32 +01:00
eac6017c2c Count sprite draw calls 2020-06-13 13:42:26 +01:00
f971ba320c Some more character investigations 2020-06-11 02:54:57 +01:00
cf624cc77b Switch from encoding/binary to struc
It's not perfect, but struc can deserialize the whole thing into one
struct while encoding/binary can't. It's nice to have that.
2020-06-09 00:36:56 +01:00
65bae80d40 Add a note about SaW trailer 2020-06-08 00:48:19 +01:00
e8e9811b5d More map trailer work 2020-06-08 00:24:57 +01:00
a6fdbaef2b Make some progress decoding map trailer 2020-06-07 01:44:28 +01:00
0bf8233cd1 Fix binary paths in README 2020-06-06 12:45:10 +01:00
c2cbf1d95d Get the initial copyright notice displaying
This is really awful, but it's nice to check it off.
2020-06-06 12:44:08 +01:00
54fe95239e More README niceness 2020-06-05 22:47:06 +01:00
63d3ee0ed6 Update README.md a bit 2020-06-01 01:55:55 +01:00
5c869fc33c Make some Wages of War sprites displayable 2020-06-01 01:43:03 +01:00
4358951e15 Add the Wages of War palette (first guess) 2020-06-01 01:41:45 +01:00
250a6033c8 Fix an error in the palette generator 2020-06-01 01:41:24 +01:00
f64af717b7 Fix format strings 2020-06-01 01:32:03 +01:00
3866ee07a8 Source the palette name from data 2020-06-01 01:24:44 +01:00
c1268e8d57 Start reorganising for multiple games 2020-06-01 01:08:53 +01:00
59baf20c35 Remove unneeded palette investigation 2020-06-01 00:46:02 +01:00
cf58be6a20 Use map rect 2020-05-31 14:58:46 +01:00
14fdab72a0 Update ebiten to v1.11.1
This seems like a significant performance boost. I get 60fps now \o/
2020-05-31 14:50:11 +01:00
c7a2fa80e7 Internalise the map rect 2020-05-20 01:43:44 +01:00
def40a1ee2 Document SaW map format some more 2020-05-20 01:40:46 +01:00
48d098134e One more palette fix 2020-05-20 01:03:40 +01:00
597e346869 Correct the palettes 2020-05-19 22:05:42 +01:00
eea5dea98a Determine the Soldiers At War palette
This commit also takes the first step towards making it configurable;
override `internal/palettes.DefaultPaletteName` at build time to choose
one palette over another. It would be nice to set this at runtime!
2020-05-19 21:33:49 +01:00
04bdf3e352 Make i18n optional, add SoW note 2020-05-19 11:07:10 +01:00
9d0750d134 Scenario viewpoint, Z index management, and arrow controls 2020-04-20 00:16:21 +01:00
1f4bfc771c HAXXX: make the main game UI appear at the bottom 2020-04-19 20:57:45 +01:00
c058f651dc Wire up mission objectives dialogue/menu 2020-04-19 18:49:19 +01:00
9be93b6091 More work for MainGame.mnu 2020-04-19 18:21:08 +01:00
f8828c95bd Add GIF to README 2020-04-18 13:44:00 +01:00
903ddba2ac Selected cursor chrome 2020-04-18 12:23:03 +01:00
b191ba2a94 Simplify bounds clipping a tiny bit 2020-04-18 11:44:05 +01:00
65 changed files with 2678 additions and 1560 deletions

19
.gitignore vendored
View File

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

View File

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

185
README.md
View File

@@ -1,32 +1,107 @@
# Ordoor # Ordoor
Ordoor is an **unofficial** [game engine recreation](https://en.wikipedia.org/wiki/Game_engine_recreation) 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 Four games are known to have been published for this engine:
the current publisher of this game; [you can purchase it here](https://www.gog.com/game/warhammer_40000_chaos_gate).
"Warhammer 40,000" is a trademark of Games Workshop, and the game data used by * [Wages of War: The Business of Battle](https://en.wikipedia.org/wiki/Wages_of_War) (1996)
Ordoor contains Games Workshop intellectual property. I am confident that this * [Soldiers At War](https://en.wikipedia.org/wiki/Soldiers_at_War) (1998)
project uses all those things in accordance with the * [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)
[Intellectual Property Policy](https://www.games-workshop.com/en-GB/Intellectual-Property-Policy) * [Avalon Hill's Squad Leader](https://en.wikipedia.org/wiki/Avalon_Hill%27s_Squad_Leader) (2000)
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. 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 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 ## Current status
### Chaos Gate
Some of the original file formats are either partially or fully decoded. Maps, 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 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 (with a preprocessing step). Some UI tookit work is done. No game mechanics are
implemented yet. implemented yet.
I keep a GIF showcasing interesting progress here:
![](https://ur.gs/img/ordoor-main-menu.gif)
I've just been informed that another game from 1998, [Soldiers At War](https://en.wikipedia.org/wiki/Soldiers_at_War),
seems to use the same engine. Maybe at some point Ordoor will be able to play
both. Will that need a rename? Hmm. Watch this space.
### Soldiers At War
(At least some) objects display. Map support is being worked on in the
`soldiers-at-war` branch, which can more-or-less display them, albeit with many
errors.
### Squad Leader
Squad Leader is the most recent of the games created with this engine. Nothing
has been done with it yet, but a preliminary look at the game data suggests many
changes are afoot. The object files are a different format, at the very least.
### Wages of War
This is the oldest of the four games. The object file format seems to be mostly
the same. the installer only copies some data to the game directory; we may want
to work directly from the CDROM instead, if we can.
Maps are uncompressed, around 243K, and no header is present. They look similar
in principle to the tile data of Soldiers At War or Chaos Gate maps, otherwise.
The menu system seen in Chaos Gate is not present; instead, there is a `BUTTONS`
directory and a lot of `pcx` files under `PIC` that, I suspect, do the job for
this game.
Even with a full installation, Wages of War leaves a lot of data on the CD. It
may be best to run solely from the `WOW` directory on the CD, assuming it's a
strict superset of what gets installed, data-wise.
## Long-term goals
Once full playthrough of the official single-player campaign for all four games
has been achieved, thoughts turn to other things we could do. Here are some
ideas, mostly at random.
Multi-player support.
Graphics enhancements - 3D models instead of sprites, high-resolution tile sets,
32-bit colour, etc. Hopefully we'd be able to drop these in one at a time.
Vastly improved AI.
Mash-ups? How do mercenaries fare against cultists fare against Nazis? Only one
way to find out!
New campaigns with existing assets. Tell new stories, or elaborate on / modify
existing ones.
Completely new fantasy game using the same engine.
## Building from source ## Building from source
I'm writing code in Go at the moment, so you'll need to have a Go runtime 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 $ go version
@@ -44,28 +119,25 @@ Debian:
You can then run `make all` in the source tree to get the binaries that are You can then run `make all` in the source tree to get the binaries that are
present at the moment. present at the moment.
Place your WH40K: Chaos Gate installation in `./orig` to benefit from automatic ## Configuring
path defaults. Otherwise, point to it with `-game-path`
The `view-map` binary attempts to render a map, and is the current focus of Since we support multiple games, a fair bit of configuration is required. Copy
effort. Once I can render a whole map, including pre-placed characters (cultist `config.toml.example` to `config.toml` and edit it to your requirements. The
scum), things can start to get more interesting. `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 The various games all use snapshots of the original engine at different points
coordinate: floor, centre, left, and right) are rendered fine, and each Z level in time, and specify a lot in code that we need to specify in data. That should
looks good. There are a few minor artifacts here and there. 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 ## Running
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.
To run: To run:
``` ```
$ make view-map $ make view-map
$ ./view-map -map Chapter01 $ ./bin/view-map -map Chapter01
``` ```
Looks like this: 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 Use the arrow keys to scroll around the map, the mouse wheel to zoom, and the
`1` - `7` keys to change Z level. `1` - `7` keys to change Z level.
Dependency management uses `go mod`, so ensure you have at least Go 1.11. Menus / UI widgets have fairly good support now; you can use the `view-menu`
binary to inspect them:
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:
``` ```
make view-menu 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 ## Sound
@@ -111,30 +185,9 @@ $ ./scripts/convert-wav ./orig/Wav
As with video playback, the ambition is to *eventually* remove this dependency As with video playback, the ambition is to *eventually* remove this dependency
and operate on the unmodified files instead. 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: * [Historical geocities modders](http://www.oocities.org/timessquare/galaxy/6777/)
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!

View File

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

View File

@@ -5,7 +5,7 @@ import (
"os" "os"
"strconv" "strconv"
"code.ur.gs/lupine/ordoor/internal/data" "code.ur.gs/lupine/ordoor/internal/palettes"
) )
func main() { func main() {
@@ -13,11 +13,11 @@ func main() {
case "index", "i": case "index", "i":
idx, err := strconv.ParseInt(os.Args[2], 16, 64) idx, err := strconv.ParseInt(os.Args[2], 16, 64)
if err != nil { if err != nil {
fmt.Println("Usage: palette-idx i <0-255>") fmt.Println("Usage: palette-idx index <0-255> <name>")
os.Exit(1) 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": case "color", "colour", "c":
fmt.Println("TODO!") fmt.Println("TODO!")
os.Exit(1) os.Exit(1)

View File

@@ -7,15 +7,18 @@ import (
"math" "math"
"os" "os"
"github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten/v2"
"golang.org/x/image/colornames" "golang.org/x/image/colornames"
"code.ur.gs/lupine/ordoor/internal/assetstore" "code.ur.gs/lupine/ordoor/internal/assetstore"
"code.ur.gs/lupine/ordoor/internal/config"
"code.ur.gs/lupine/ordoor/internal/ui" "code.ur.gs/lupine/ordoor/internal/ui"
) )
var ( var (
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation") configFile = flag.String("config", "config.toml", "Config file")
engine = flag.String("engine", "", "Override engine to use")
groupIdx = flag.Int("group", 1, "Group index to start at") groupIdx = flag.Int("group", 1, "Group index to start at")
recIdx = flag.Int("record", 0, "Record index to start at") recIdx = flag.Int("record", 0, "Record index to start at")
@@ -43,14 +46,19 @@ type state struct {
func main() { func main() {
flag.Parse() flag.Parse()
if *gamePath == "" { if *configFile == "" {
flag.Usage() flag.Usage()
os.Exit(1) os.Exit(1)
} }
assets, err := assetstore.New(*gamePath) cfg, err := config.Load(*configFile, *engine)
if err != nil { if err != nil {
log.Fatal("Failed to set up asset store: %v", err) log.Fatalf("Failed to load config: %v", err)
}
assets, err := assetstore.New(cfg.DefaultEngine())
if err != nil {
log.Fatalf("Failed to set up asset store: %v", err)
} }
state := state{ state := state{
@@ -92,7 +100,7 @@ func main() {
func (e *env) Update(screenX, screenY int) error { func (e *env) Update(screenX, screenY int) error {
if e.step == 0 || e.lastState != e.state { if e.step == 0 || e.lastState != e.state {
ani, err := e.assets.Animation(e.state.groupIdx, e.state.recIdx) ani, err := e.assets.Animation(e.state.groupIdx, e.state.recIdx, 0) // FIXME: why 0?
if err != nil { if err != nil {
return err return err
} }
@@ -123,7 +131,7 @@ func (e *env) Draw(screen *ebiten.Image) error {
if len(e.ani.Frames) > 0 { if len(e.ani.Frames) > 0 {
sprite := e.ani.Frames[e.step/4%len(e.ani.Frames)] sprite := e.ani.Frames[e.step/4%len(e.ani.Frames)]
return screen.DrawImage(sprite.Image, &ebiten.DrawImageOptions{GeoM: cam}) screen.DrawImage(sprite.Image, &ebiten.DrawImageOptions{GeoM: cam})
} }
return nil return nil

View File

@@ -7,14 +7,17 @@ import (
"math" "math"
"os" "os"
"github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten/v2"
"code.ur.gs/lupine/ordoor/internal/assetstore" "code.ur.gs/lupine/ordoor/internal/assetstore"
"code.ur.gs/lupine/ordoor/internal/config"
"code.ur.gs/lupine/ordoor/internal/ui" "code.ur.gs/lupine/ordoor/internal/ui"
) )
var ( 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") fontName = flag.String("font", "", "Name of a font, e.g., basfont12")
txt = flag.String("text", "Test string", "Text to render") txt = flag.String("text", "Test string", "Text to render")
@@ -37,12 +40,17 @@ type state struct {
func main() { func main() {
flag.Parse() flag.Parse()
if *gamePath == "" || *fontName == "" { if *configFile == "" || *fontName == "" {
flag.Usage() flag.Usage()
os.Exit(1) 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 { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@@ -61,7 +69,7 @@ func main() {
win, err := ui.NewWindow(env, "View Font: "+*fontName, *winX, *winY) win, err := ui.NewWindow(env, "View Font: "+*fontName, *winX, *winY)
if err != nil { if err != nil {
log.Fatal("Couldn't create window: %v", err) log.Fatalf("Couldn't create window: %v", err)
} }
win.OnMouseWheel(env.changeZoom) win.OnMouseWheel(env.changeZoom)
@@ -95,9 +103,7 @@ func (e *env) Draw(screen *ebiten.Image) error {
op.GeoM.Translate(float64(xOff), 0) op.GeoM.Translate(float64(xOff), 0)
op.GeoM.Scale(e.state.zoom, e.state.zoom) // apply current zoom factor op.GeoM.Scale(e.state.zoom, e.state.zoom) // apply current zoom factor
if err := screen.DrawImage(glyph.Image, op); err != nil { screen.DrawImage(glyph.Image, op)
return err
}
xOff += glyph.Rect.Dx() xOff += glyph.Rect.Dx()
} }

View File

@@ -3,38 +3,50 @@ package main
import ( import (
"flag" "flag"
"log" "log"
"math"
"os" "os"
"github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten/v2"
"code.ur.gs/lupine/ordoor/internal/assetstore" "code.ur.gs/lupine/ordoor/internal/assetstore"
"code.ur.gs/lupine/ordoor/internal/config"
"code.ur.gs/lupine/ordoor/internal/flow"
"code.ur.gs/lupine/ordoor/internal/scenario" "code.ur.gs/lupine/ordoor/internal/scenario"
"code.ur.gs/lupine/ordoor/internal/ship"
"code.ur.gs/lupine/ordoor/internal/ui" "code.ur.gs/lupine/ordoor/internal/ui"
) )
var ( var (
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation") configFile = flag.String("config", "config.toml", "Config file")
gameMap = flag.String("map", "", "Name of a map, e.g., Chapter01") 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") winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension")
winY = flag.Int("win-y", 1024, "Pre-scaled window Y dimension") winY = flag.Int("win-y", 1024, "Pre-scaled window Y dimension")
) )
type env struct { type env struct {
flow *flow.Flow
scenario *scenario.Scenario scenario *scenario.Scenario
} }
func main() { func main() {
flag.Parse() flag.Parse()
if *gamePath == "" || *gameMap == "" { if *configFile == "" || *gameMap == "" {
flag.Usage() flag.Usage()
os.Exit(1) os.Exit(1)
} }
assets, err := assetstore.New(*gamePath) cfg, err := config.Load(*configFile, *engine)
if err != nil { if err != nil {
log.Fatalf("Failed to scan root directory %v: %v", *gamePath, err) log.Fatalf("Failed to load config: %v", err)
}
assets, err := assetstore.New(cfg.DefaultEngine())
if err != nil {
log.Fatalf("Failed to scan root directory: %v", err)
} }
scenario, err := scenario.NewScenario(assets, *gameMap) scenario, err := scenario.NewScenario(assets, *gameMap)
@@ -42,25 +54,39 @@ func main() {
log.Fatalf("Failed to load scenario %v: %v", *gameMap, err) log.Fatalf("Failed to load scenario %v: %v", *gameMap, err)
} }
env := &env{ var realEnv *env
scenario: scenario, if cfg.DefaultEngineName == "ordoor" {
ship := &ship.Ship{}
flow, err := flow.New(assets, cfg, ship)
if err != nil {
log.Fatalf("Failed to setup flow: %v", err)
}
flow.SetScenario(scenario)
realEnv = &env{flow: flow, scenario: scenario}
} else {
realEnv = &env{scenario: scenario}
} }
win, err := ui.NewWindow(env, "View Map "+*gameMap, *winX, *winY) win, err := ui.NewWindow(realEnv, "View Map "+*gameMap, *winX, *winY)
if err != nil { if err != nil {
log.Fatal("Couldn't create window: %v", err) log.Fatalf("Couldn't create window: %v", err)
} }
win.OnKeyUp(ebiten.KeyLeft, env.changeOrigin(-64, +0))
win.OnKeyUp(ebiten.KeyRight, env.changeOrigin(+64, +0))
win.OnKeyUp(ebiten.KeyUp, env.changeOrigin(+0, -64))
win.OnKeyUp(ebiten.KeyDown, env.changeOrigin(+0, +64))
for i := 0; i <= 6; i++ { for i := 0; i <= 6; i++ {
win.OnKeyUp(ebiten.Key1+ebiten.Key(i), env.setZIdx(i)) win.OnKeyUp(ebiten.Key1+ebiten.Key(i), realEnv.setZIdx(i))
} }
win.OnMouseClick(env.showCellData) win.OnMouseClick(realEnv.showCellData)
win.OnMouseWheel(realEnv.changeZoom)
if realEnv.flow == nil {
step := 32
win.WhileKeyDown(ebiten.KeyLeft, realEnv.changeOrigin(-step, +0))
win.WhileKeyDown(ebiten.KeyRight, realEnv.changeOrigin(+step, +0))
win.WhileKeyDown(ebiten.KeyUp, realEnv.changeOrigin(+0, -step))
win.WhileKeyDown(ebiten.KeyDown, realEnv.changeOrigin(+0, +step))
}
if err := win.Run(); err != nil { if err := win.Run(); err != nil {
log.Fatal(err) log.Fatal(err)
@@ -68,11 +94,19 @@ func main() {
} }
func (e *env) Update(screenX, screenY int) error { func (e *env) Update(screenX, screenY int) error {
return e.scenario.Update(screenX, screenY) if e.flow != nil {
return e.flow.Update(screenX, screenY)
} else {
return e.scenario.Update(screenX, screenY)
}
} }
func (e *env) Draw(screen *ebiten.Image) error { func (e *env) Draw(screen *ebiten.Image) error {
return e.scenario.Draw(screen) if e.flow != nil {
return e.flow.Draw(screen)
} else {
return e.scenario.Draw(screen)
}
} }
func (e *env) changeOrigin(byX, byY int) func() { func (e *env) changeOrigin(byX, byY int) func() {
@@ -82,6 +116,10 @@ func (e *env) changeOrigin(byX, byY int) func() {
} }
} }
func (e *env) changeZoom(_, byY float64) {
e.scenario.Zoom *= math.Pow(1.2, byY)
}
func (e *env) setZIdx(to int) func() { func (e *env) setZIdx(to int) func() {
return func() { return func() {
e.scenario.ZIdx = to e.scenario.ZIdx = to

View File

@@ -5,14 +5,17 @@ import (
"log" "log"
"os" "os"
"github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten/v2"
"code.ur.gs/lupine/ordoor/internal/assetstore" "code.ur.gs/lupine/ordoor/internal/assetstore"
"code.ur.gs/lupine/ordoor/internal/config"
"code.ur.gs/lupine/ordoor/internal/ui" "code.ur.gs/lupine/ordoor/internal/ui"
) )
var ( 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") menuName = flag.String("menu", "", "Name of a menu, e.g. Main")
winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension") winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension")
@@ -28,12 +31,17 @@ type dlg struct {
func main() { func main() {
flag.Parse() flag.Parse()
if *gamePath == "" || *menuName == "" { if *configFile == "" || *menuName == "" {
flag.Usage() flag.Usage()
os.Exit(1) 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 { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@@ -50,7 +58,7 @@ func main() {
win, err := ui.NewWindow(driver, "View Menu: "+*menuName, *winX, *winY) win, err := ui.NewWindow(driver, "View Menu: "+*menuName, *winX, *winY)
if err != nil { if err != nil {
log.Fatal("Couldn't create window: %v", err) log.Fatalf("Couldn't create window: %v", err)
} }
// Change the active dialogue // Change the active dialogue

View File

@@ -10,7 +10,7 @@ import (
"path/filepath" "path/filepath"
"time" "time"
"github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten/v2"
"code.ur.gs/lupine/ordoor/internal/maps" "code.ur.gs/lupine/ordoor/internal/maps"
"code.ur.gs/lupine/ordoor/internal/sets" "code.ur.gs/lupine/ordoor/internal/sets"
@@ -76,7 +76,7 @@ func main() {
win, err := ui.NewWindow(env, "View Map "+*mapFile, *winX, *winY) win, err := ui.NewWindow(env, "View Map "+*mapFile, *winX, *winY)
if err != nil { 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) 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 { func (e *env) Draw(screen *ebiten.Image) error {
gameMap := e.gameMap gameMap := e.gameMap
imd, err := ebiten.NewImage( rect := gameMap.Rect()
int(gameMap.MaxWidth), imd := ebiten.NewImage(rect.Dx(), rect.Dy())
int(gameMap.MaxLength),
ebiten.FilterDefault,
)
if err != nil { for y := int(rect.Min.Y); y < int(rect.Max.Y); y++ {
return err 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))
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))
} }
} }
@@ -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.Scale(e.state.zoom, e.state.zoom) // apply current zoom factor
cam.Rotate(0.785) // Apply isometric angle 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 // Converts pixel coordinates to cell coordinates

View File

@@ -7,17 +7,20 @@ import (
"math" "math"
"os" "os"
"github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten/v2"
"code.ur.gs/lupine/ordoor/internal/assetstore" "code.ur.gs/lupine/ordoor/internal/assetstore"
"code.ur.gs/lupine/ordoor/internal/config"
"code.ur.gs/lupine/ordoor/internal/ui" "code.ur.gs/lupine/ordoor/internal/ui"
) )
var ( var (
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation") configFile = flag.String("config", "config.toml", "Config file")
objFile = flag.String("obj-file", "", "Path of an .obj file, e.g. ./orig/Obj/TZEENTCH.OBJ") engine = flag.String("engine", "", "Override engine to use")
objName = flag.String("obj-name", "", "Name of an .obj file, e.g. TZEENTCH")
sprIdx = flag.Int("spr-idx", 0, "Sprite index to start at") 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") winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension")
winY = flag.Int("win-y", 1024, "Pre-scaled window Y dimension") winY = flag.Int("win-y", 1024, "Pre-scaled window Y dimension")
@@ -42,14 +45,19 @@ type state struct {
func main() { func main() {
flag.Parse() flag.Parse()
if *gamePath == "" || (*objName == "" && *objFile == "") { if *configFile == "" || (*objName == "" && *objFile == "") {
flag.Usage() flag.Usage()
os.Exit(1) os.Exit(1)
} }
assets, err := assetstore.New(*gamePath) cfg, err := config.Load(*configFile, *engine)
if err != nil { 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 var obj *assetstore.Object
@@ -128,7 +136,9 @@ func (e *env) Draw(screen *ebiten.Image) error {
cam.Translate(float64(e.state.origin.X), float64(e.state.origin.Y)) // Move to origin cam.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 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() { func (e *env) changeSprite(by int) func() {

View File

@@ -7,15 +7,18 @@ import (
"math" "math"
"os" "os"
"github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten/v2"
"code.ur.gs/lupine/ordoor/internal/assetstore" "code.ur.gs/lupine/ordoor/internal/assetstore"
"code.ur.gs/lupine/ordoor/internal/config"
"code.ur.gs/lupine/ordoor/internal/ui" "code.ur.gs/lupine/ordoor/internal/ui"
) )
var ( var (
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation") configFile = flag.String("config", "config.toml", "Config file")
setName = flag.String("set", "", "Name of a set, e.g., map01") 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") winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension")
winY = flag.Int("win-y", 1024, "Pre-scaled window Y dimension") winY = flag.Int("win-y", 1024, "Pre-scaled window Y dimension")
@@ -39,12 +42,17 @@ type state struct {
func main() { func main() {
flag.Parse() flag.Parse()
if *gamePath == "" || *setName == "" { if *configFile == "" || *setName == "" {
flag.Usage() flag.Usage()
os.Exit(1) 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 { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@@ -63,7 +71,7 @@ func main() {
win, err := ui.NewWindow(env, "View Set: "+*setName, *winX, *winY) win, err := ui.NewWindow(env, "View Set: "+*setName, *winX, *winY)
if err != nil { 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)) win.OnKeyUp(ebiten.KeyLeft, env.changeObjIdx(-1))
@@ -115,7 +123,9 @@ func (e *env) Draw(screen *ebiten.Image) error {
// TODO: centre the image // 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() { func (e *env) changeObjIdx(by int) func() {

View File

@@ -1,6 +1,22 @@
[ordoor] video_player = ["mpv", "--no-config", "--keep-open=no", "--force-window=no", "--no-border", "--no-osc", "--fullscreen", "--no-input-default-bindings"]
data_dir = "./orig"
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] [options]
play_movies = true play_movies = true

View File

@@ -392,8 +392,11 @@ well-aligned amount.
Investigation has so far suggested the following: Investigation has so far suggested the following:
* `Cell[0]` seems related to doors and canisters. Observed: * `Cell[0]` seems related to doors and canisters. Observed:
* Nothing special: 0x38
* ???: 0x39
* Imperial crate: 0x28 * Imperial crate: 0x28
* Door: 0xB8 * Door: 0xB8
* `Cell[1]` seems related to special placeables (but not triggers). Bitfield. Observed: * `Cell[1]` seems related to special placeables (but not triggers). Bitfield. Observed:
* 0x01: Reactor * 0x01: Reactor
* 0x20: Door or door lock? * 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[7]` Object 2 (Right) Area (Sets/*.set lookup)
* `Cell[6]` Object 2 (Right) Sprite + active flag * `Cell[6]` Object 2 (Right) Sprite + active flag
* `Cell[9]` Object 3 (Center) Area (Sets/*.set lookup) * `Cell[9]` Object 3 (Center) Area (Sets/*.set lookup)
* `Cell[10]` Object 3 (Right) Sprite + active flag * `Cell[10]` Object 3 (Center) Sprite + active flag
* `Cell[11]` all 255? * `Cell[11]` all 255? Vehicle?
* `Cell[12]` all 0? * `Cell[12]` all 0?
* `Cell[13]` all 0? * `Cell[13]` all 0?
* `Cell[14]` 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 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` 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 ................ 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 Relative offsets from the start of the trailer, we have:
assume these are all a fixed number of fixed-size records when looking into it.
| Offset | Text |
| -------- | ---- |
| `0xEE` | Mania |
| `0x78A` | Dagon |
| `0xE26` | Nihasa |
| `0x14C2` | Samnu |
| `0x1b5e` | Bael |
| `0x2896` | Gigamen |
| `0x2f32` | Valefor |
| `0x35ce` | Baalberith |
| `0x3c6a` | Fenriz |
| `0x4306` | #Character |
| `0x49a2` | Apollyon |
So there are 1692 bytes between each name (the names probably don't come at the
start of each block, but it's still a useful stride). Presumably `#Character` is
a space for one of the player characters, while the others specify an NPC placed
on the map.
There's 56 of these records between the first and last name we see - `Ahpuch`.
Then there are a number of other strings that seem related to triggers / events,
including lots that say `NO FILE`. The first two are 96 bytes apart; from then
on they seem to be placed variably apart from each other; I've seen 96, 256, and
352 byte offsets.
At 0x20916 the mission objective is readable.
At 0x2092a the mission description is readable.
Generating another map with just 5 characters on it, things look different:
* Trailer size is 13543 bytes
* There are only 5 names
* There are none of the trigger/event strings
* Mission title is found at 0x2b93
* Mission briefing is found at 0x2c92
Since the trailer is a variable size, there must be a header that tells us how
many of each type of record to read. Peeking at the differences in `vbindiff`:
```
Chapter01.MAP.Trailer
0000 0000: 38 00 00 68 00 00 00 50 00 00 00 1A 00 00 00 14 8..h...P ........
0000 0010: 00 00 00 3A 00 00 00 00 38 25 00 04 00 00 00 00 ...:.... 8%......
0000 0020: 00 00 00 1A 00 00 00 00 00 00 00 00 00 00 00 00 ........ ........
0000 0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ........ ........
0000 0040: 00 00 00 00 00 00 00 00 00 00 00 02 00 00 00 00 ........ ........
0000 0050: 00 00 00 32 00 00 00 00 00 00 00 00 00 00 00 00 ...2.... ........
TINYSQUAD.MAP.Trailer
0000 0000: 38 00 00 4B 00 00 00 3C 00 00 00 37 00 00 00 28 8..K...< ...7...(
0000 0010: 00 00 00 05 00 00 00 00 2B 3A 00 04 00 00 00 05 ........ +:......
0000 0020: 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 ........ ........
0000 0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ........ ........
0000 0040: 00 00 00 00 00 00 00 00 00 00 00 02 00 00 00 00 ........ ........
0000 0050: 00 00 00 32 00 00 00 00 00 00 00 00 00 00 00 00 ...2.... ........
```
The size of the trailer for Chapter01 is 139,483 bytes, assuming it starts at
`0x163890`. However, things may be a lot more sensible if we drop 3 bytes off
the start of that to get the fields into little-endian alignment. Have I made a
maths error above somewhere? Is it some sort of alignment thing? Do those 3
bytes actually have meaning?
Ignoring them for now, here's a first guess at a header:
| Offset | Size | Meaning |
| ------ | ---- | ------- |
| 0 | 4 | Map maximum X + 1 |
| 4 | 4 | Map maximum Y + 1 |
| 8 | 4 | Map minimum X |
| 12 | 4 | Map minimum Y |
| 16 | 4 | Number of character records |
| 20 | 4 | Padding? - invariant `00 00 00 00` |
| 24 | 2 | ??? - varies. Seems related to character/squad position? |
| 26 | 2 | ??? - invariant `00 04` |
| 28 | 4 | ??? - varies (0 vs 5) |
| 32 | 1 | Number of thingies |
| 33 | 3 | ???. With a Lord of Change on the map, only one byte works as thingies count |
| 36 | 20 | Padding? |
56 bytes of data is interesting because the value of that first, ignored byte is
0x38 - perhaps it's a skip value + 2 bytes of padding? It's just weird. Keep
ignoring it for now.
0x4b contains the next non-null byte; is the gap between the the number of
thingies, and it, padding? Minus a bit? 0x50 is another non-null byte. Then
it's all zeroes until one byte before the first name at 0xee.
Individual cells seem to have a flag to say "We have a character in us", but not
the number for the character themselves, so the coordinates must be in the
per-character records also. There are several candidates for this.
Placing a single character at (64,49) causes those bytes to show up at four
offsets - 0x18 (!), 0x1F4, 0x1F8, and 0x6C8.
Generating a map with no characters at all, the trailer is 2,447 bytes, and the
mission title starts at 0x3B (59). So we can say we have 20 bytes of padding as
a first approximation?
Here's where we're at with the per-character data, going from the padding values
suggested above:
| Offset | Size | Meaning |
| ------ | ---- | ------- |
| 0 | 178 | ??? |
| 178 | 1 | Character type |
| 179 | 80 | Character name |
| 259 | 1 | Weapon Skill |
| 260 | 1 | Ballistic Skill |
| 261 | 1 | Unknown |
| 262 | 1 | Leadership |
| 263 | 1 | Toughness |
| 264 | 1 | Strength |
| 265 | 1 | Action Points |
| 266 | 1 | Unknown |
| 267 | 1 | Unknown |
| 268 | 1 | Health |
| 269 | 495 | ??? |
| 764 | 1(?) | Squad number |
| 765 | 895 | ??? |
| 1660 | 1? | Orientation? Could also be `0x680`... |
| 1661 | 31 | ??? |
There's still a lot of bytes to dig through, but this allows me to load the
character names from Chapter01 correctly, with the exception of record 57 which
just contains `\x02` and is then null-terminated all the way through - but maybe
that's just a data thing.
How about their types? `HasAction.dat` lists numbers for character types, and
those show up immediately before the name. Going from the character type to the
animation group is not yet fully deciphered - squad leaders mess up a direct
correlation - but a fixed offset table allows me to draw the characters \o/.
Putting 8 characters onto a map and orienting them in the compass points, we see
numbers ranging from 0 to 7 at 0x67c and 0x680. Assuming this is the scheme
used, north is value 1, northeast value 2, and northwest value 0.
Given two characters of the same type, just in different locations, differing
values are seen at:
* `0x103 - 0x10c` (hodgepodge)
* `0x178 - 0x1be` (hodgepodge)
* `0x2fc` (0, 1) - squad number?
I can easily correlate certain bytes in the first range to various character
attributes. A few remain unset.
In Chapter01, picking a random character (Gorgon) and looking at his squadmates,
they are all in the same squad, and no other characters are in that squad, so it
looks pretty diagnostic to me. There's nothing in the UI to indicate the squad,
though.
Now let's look for position. In my 2-character map, they're at 65,50 and 70,55.
Within a character, I see those numbers repeated twice - around `0x1b{9,a}` and
`0x1b{d,e}`. This may be some kind of multiple-squares-taken-up thing.
Adding a (tall) Lord of Change to the map gave me `02 36 45 00 02 37 45`, which
doesn't quite match what my eyes are telling me for Z,Y,X. In addition, the data
immediately after this offset changed into a large number of coordinate-like
sets of values - far too many for it to actually be a bounding box. However, the
first one remains good as a position specifier.
Down in `0x679` (Chaos Sorcerer) or `0x68D` (Lord of Change), the map coords for
the *other* character appears, which is downright odd. For now, just use the
first-indexed value.
Thingies next: these aren't decoded at all yet, and the sizes seem to be
variable.
| Offset | Size | Meaning |
| ------ | ---- | ------- |
| | | |
Finally, the "trailer trailer", for want of a better term, seems to be organised
as:
| Offset | Size | Meaning |
| ----- | ---- | ------- |
| 0 | 255 | Title |
| 255 | 2048 | Briefing |
| 2304 | 85 | ??? - each byte is 1 or 0. Spaced so it may be partly uint32 |
This duplicates the information found in the `.TXT` files. No idea what the end
data is yet.
## Soldiers At War
All the above applies to Chaos Gate maps. Maps for Soldiers At War seem to have
a lot of similarities, but also some differences. For a start, the maps are a
variable size!
Starting with the header, given a tiny 26x20 generated map, the first 256 bytes
look like this:
```
00000000: 1500414d 425f4d41 50005041 52495300 ..AMB_MAP.PARIS.
00000010: 00000000 00000000 00000000 00000000 ................
00000020: 00000000 00000000 00000000 00000000 ................
00000030: 00000000 00000000 00000000 00000000 ................
00000040: 00000000 00000000 00000000 00000000 ................
00000050: 00000000 00000000 00000000 00000000 ................
00000060: 00000000 00000000 00000000 00000000 ................
00000070: 00000000 00000000 00000000 00000000 ................
00000080: 00000000 00000000 00001e00 45000100 ............E...
00000090: 1f004600 10010000 52000000 00001b00 ..F.....R.......
000000a0: 38000100 00000500 0a000001 00f0f9ff 8...............
000000b0: ffb60500 00000100 ff370a00 64006400 .........7..d.d.
000000c0: 08008501 00000000 00ff0000 1f008082 ................
000000d0: 01000000 0000ff00 001f0080 84010000 ................
000000e0: 000000ff 00001f00 00810100 00000000 ................
000000f0: ff00001f 00808301 00000000 00ff0000 ................
```
Almost everything we knew is out of the window, but a few things look familiar.
First, the header seems simplified down to just two recognisable-at-first-glance
fields: Magic bytes (now `\x15\x00AMV_MAP\x00`) and the set name, coming
immediately after.
Like Chaos Gate, all map files are the same size once uncompressed, but they are
smaller - at 1,214,559 bytes, they are 76% the size. This is quite significant.
We now have 13.3 bytes per voxel, rather than the 17.5 bytes per voxel that was
available to Chaos Gate. This means that the number of bytes *per cell* must be
reduced, in addition to the header (and trailer?) values.
Looking at data from 0x110, it seems to group naturally into 13-byte records:
```
$ xxd -s 0x110 -c 13 -l 65 -g 1 TINYMAP.MAP
00000110: 80 01 00 00 00 00 00 ff 00 00 1f 00 00 .............
0000011d: 85 01 00 00 00 00 00 ff 00 00 1f 00 00 .............
0000012a: 82 01 00 00 00 00 00 ff 00 00 1f 00 80 .............
00000137: 82 01 00 00 00 00 00 ff 00 00 1f 00 00 .............
00000144: 82 01 00 00 00 00 00 ff 00 00 1f 00 80 .............
```
It's a strange number. Chaos Gate cells group nicely on 16 bytes:
```
$ xxd -s 0x110 -c 16 -l 64 -g 1 Chapter01.MAP
00000110: 3f 00 00 00 83 01 00 00 00 00 00 ff 00 00 00 00 ?...............
00000120: 38 00 00 00 85 01 00 00 00 00 00 ff 00 00 00 00 8...............
00000130: 38 00 00 00 84 01 00 00 00 00 00 ff 00 00 00 00 8...............
00000140: 38 00 00 00 8a 01 00 00 00 00 00 ff 00 00 00 00 8...............
00000150: 38 00 00 00 83 01 00 00 00 00 00 ff 00 00 00 00 8...............
```
That grouping is very enticing, though. I feel strongly that it's the right
number.
Now we need to ask about start offset. Where is byte 0 of the per-cell data, and
do the 13 bytes it has line up neatly to the functions of some of the 16 bytes
seen in Chaos Gate?
I generated a `BIGGESTMAP` (130x100) to investigate. It's just grass, nothing
but grass, and 0xC0 is the first offset where it starts to look nicely grouped:
```
xxd -s 0xc0 -c 13 -l 260 -g 13 BIGGESTMAP.MAP
000000c0: 08 80 81 01 00 00 00 00 00 ff 00 00 1f .............
000000cd: 00 80 81 01 00 00 00 00 00 ff 00 00 1f .............
000000da: 00 00 81 01 00 00 00 00 00 ff 00 00 1f .............
000000e7: 00 00 85 01 00 00 00 00 00 ff 00 00 1f .............
# ...
```
This can be interpreted more or less the same way as the Chaos Gate maps now,
and the `soldiers-at-war` branch contains a hacked-up implementation that kind
of works \o/.
Does the same trailer apply? Seemingly not. Looking at `PARIS.MAP`, there's no
similarity at first glance.
However, I did manage to track down 4 32-bit ints inside the trailer, starting
at `0x121ad1`, which specify dimensions of the map, at least. Perhaps the
position has moved, but some of the data is the same? It's 3320 bytes into the
trailer.

35
go.mod
View File

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

131
go.sum
View File

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

View File

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

View File

@@ -2,24 +2,25 @@ package assetstore
import ( import (
"fmt" "fmt"
"image/color"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/hajimehoshi/ebiten/v2"
"code.ur.gs/lupine/ordoor/internal/config"
"code.ur.gs/lupine/ordoor/internal/data" "code.ur.gs/lupine/ordoor/internal/data"
"code.ur.gs/lupine/ordoor/internal/idx" "code.ur.gs/lupine/ordoor/internal/idx"
) "code.ur.gs/lupine/ordoor/internal/palettes"
const (
RootDir = "" // Used in the entryMap for entries pertaining to the root dir
) )
type entryMap map[string]map[string]string type entryMap map[string]map[string]string
// type AssetStore is responsible for lazily loading game data when it is // 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 // 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. // 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 // Cross-platform differences such as filename case sensitivity are also dealt
@@ -27,8 +28,12 @@ type entryMap map[string]map[string]string
// //
// We assume the directory is read-only. You can run Refresh() if you make a // We assume the directory is read-only. You can run Refresh() if you make a
// change. // change.
//
// To mix assets from different games, either construct a synthetic directory
// or instantiate two separate asset stores.
type AssetStore struct { type AssetStore struct {
RootDir string RootDir string
Palette color.Palette
// Case-insensitive file lookup. // Case-insensitive file lookup.
// {"":{"anim":"Anim", "obj":"Obj", ...}, "anim":{ "warhammer.ani":"WarHammer.ani" }, ...} // {"":{"anim":"Anim", "obj":"Obj", ...}, "anim":{ "warhammer.ani":"WarHammer.ani" }, ...}
@@ -40,7 +45,9 @@ type AssetStore struct {
cursors map[CursorName]*Cursor cursors map[CursorName]*Cursor
fonts map[string]*Font fonts map[string]*Font
generic *data.Generic generic *data.Generic
hasAction *data.HasAction
idx *idx.Idx idx *idx.Idx
images map[string]*ebiten.Image
maps map[string]*Map maps map[string]*Map
menus map[string]*Menu menus map[string]*Menu
objs map[string]*Object objs map[string]*Object
@@ -50,9 +57,19 @@ type AssetStore struct {
} }
// New returns a new AssetStore // 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{ store := &AssetStore{
RootDir: dir, RootDir: engine.DataDir,
Palette: palette,
} }
// fill entryMap // fill entryMap
@@ -70,7 +87,7 @@ func (a *AssetStore) Refresh() error {
} }
newEntryMap := make(entryMap, len(rootEntries)) newEntryMap := make(entryMap, len(rootEntries))
newEntryMap[RootDir] = rootEntries newEntryMap[""] = rootEntries
for lower, natural := range rootEntries { for lower, natural := range rootEntries {
path := filepath.Join(a.RootDir, natural) path := filepath.Join(a.RootDir, natural)
@@ -95,7 +112,10 @@ func (a *AssetStore) Refresh() error {
a.cursors = make(map[CursorName]*Cursor) a.cursors = make(map[CursorName]*Cursor)
a.entries = newEntryMap a.entries = newEntryMap
a.fonts = make(map[string]*Font) a.fonts = make(map[string]*Font)
a.generic = nil
a.hasAction = nil
a.idx = nil a.idx = nil
a.images = make(map[string]*ebiten.Image)
a.maps = make(map[string]*Map) a.maps = make(map[string]*Map)
a.menus = make(map[string]*Menu) a.menus = make(map[string]*Menu)
a.objs = make(map[string]*Object) a.objs = make(map[string]*Object)
@@ -118,7 +138,7 @@ func (a *AssetStore) lookup(name, ext string, dirs ...string) (string, error) {
dir = canonical(dir) dir = canonical(dir)
if base, ok := a.entries[dir]; ok { if base, ok := a.entries[dir]; ok {
if file, ok := base[filename]; 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 filepath.Join(a.RootDir, actualDir, file), nil
} }
} }

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,11 @@
package assetstore package assetstore
import ( import (
"fmt"
"image" "image"
"log" "log"
"code.ur.gs/lupine/ordoor/internal/data"
"code.ur.gs/lupine/ordoor/internal/maps" "code.ur.gs/lupine/ordoor/internal/maps"
) )
@@ -46,12 +48,7 @@ func (a *AssetStore) Map(name string) (*Map, error) {
} }
m := &Map{ m := &Map{
Rect: image.Rect( Rect: raw.Rect(),
int(raw.MinWidth),
int(raw.MinLength),
int(raw.MaxWidth),
int(raw.MaxLength),
),
assets: a, assets: a,
raw: raw, raw: raw,
set: set, set: set,
@@ -64,8 +61,8 @@ func (a *AssetStore) Map(name string) (*Map, error) {
func (m *Map) LoadSprites() error { func (m *Map) LoadSprites() error {
// Eager load the sprites we use // Eager load the sprites we use
for x := m.Rect.Min.X; x <= m.Rect.Max.X; x++ { 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 y := m.Rect.Min.Y; y < m.Rect.Max.Y; y++ {
for z := 0; z < maps.MaxHeight; z++ { for z := 0; z < maps.MaxHeight; z++ {
if _, err := m.SpritesForCell(x, y, z); err != nil { if _, err := m.SpritesForCell(x, y, z); err != nil {
return err return err
@@ -78,8 +75,8 @@ func (m *Map) LoadSprites() error {
} }
// FIXME: get rid of this // FIXME: get rid of this
func (m *Map) Cell(x, y, z int) maps.Cell { func (m *Map) Cell(x, y, z int) *maps.Cell {
return m.raw.Cells.At(x, y, z) return m.raw.At(x, y, z)
} }
// SpritesForCell returns the sprites needed to correctly render this cell. // SpritesForCell returns the sprites needed to correctly render this cell.
@@ -95,7 +92,7 @@ func (m *Map) SpritesForCell(x, y, z int) ([]*Sprite, error) {
obj, err := m.set.Object(ref.Index()) obj, err := m.set.Object(ref.Index())
if err != nil { 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()) sprite, err := obj.Sprite(ref.Sprite())
@@ -105,6 +102,27 @@ func (m *Map) SpritesForCell(x, y, z int) ([]*Sprite, error) {
sprites = append(sprites, sprite) sprites = append(sprites, sprite)
} }
if chr := m.CharacterAt(x, y, z); chr != nil {
// Look up the correct animation, get the frame, boom 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 return sprites, nil
} }
func (m *Map) CharacterAt(x, y, z int) *maps.Character {
// FIXME: don't iterate
for i, _ := range m.raw.Characters {
chr := &m.raw.Characters[i]
if chr.XPos == x && chr.YPos == y && z == 0 { // FIXME: sort out ZPos
return chr
}
}
return nil
}

View File

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

View File

@@ -7,7 +7,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten/v2"
"code.ur.gs/lupine/ordoor/internal/data" "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) log.Printf("Loading object %v", name)
filename, err := a.lookup(name, "obj", "Obj") filename, err := a.lookup(name, "obj", "Obj", "spr")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -117,10 +117,7 @@ func (o *Object) Sprite(idx int) (*Sprite, error) {
} }
raw := o.raw.Sprites[idx] raw := o.raw.Sprites[idx]
img, err := ebiten.NewImageFromImage(raw.ToImage(), ebiten.FilterDefault) img := ebiten.NewImageFromImage(raw.ToImage(o.assets.Palette))
if err != nil {
return nil, err
}
rect := image.Rect( rect := image.Rect(
int(raw.XOffset), int(raw.XOffset),

View File

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

View File

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

View File

@@ -35,7 +35,7 @@ const (
AnimActionRun AnimAction = 14 AnimActionRun AnimAction = 14
AnimActionCrouch AnimAction = 15 AnimActionCrouch AnimAction = 15
AnimActionStand AnimAction = 16 AnimActionStand AnimAction = 16
AnimActionStandingRead AnimAction = 17 AnimActionStandingReady AnimAction = 17
AnimActionStandingUnready AnimAction = 18 AnimActionStandingUnready AnimAction = 18
AnimActionCrouchingReady AnimAction = 19 AnimActionCrouchingReady AnimAction = 19
AnimActionCrouchingUnready AnimAction = 20 AnimActionCrouchingUnready AnimAction = 20
@@ -94,6 +94,89 @@ type HasAction struct {
bits bitfield.BitField 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) { func LoadHasAction(filename string) (*HasAction, error) {
scanner, err := asciiscan.New(filename) scanner, err := asciiscan.New(filename)
if err != nil { if err != nil {
@@ -161,6 +244,17 @@ func (h *HasAction) Actions(c CharacterType) []AnimAction {
return out return out
} }
// FIXME: Too slow
func (h *HasAction) Index(c CharacterType, requestedAction AnimAction) int {
for i, action := range h.Actions(c) {
if action == requestedAction {
return i
}
}
return -1
}
func (h *HasAction) Print() { func (h *HasAction) Print() {
fmt.Println(" Tac Ass Dev Term Apo Tech Chp Lib Cpt CMar CLrd CChp CSrc CTrm Kbz BTh BL FHnd LoC Flm PHr BHr Cult") fmt.Println(" Tac Ass Dev Term Apo Tech Chp Lib Cpt CMar CLrd CChp CSrc CTrm Kbz BTh BL FHnd LoC Flm PHr BHr Cult")
for a := AnimActionStart; a <= AnimActionEnd; a++ { for a := AnimActionStart; a <= AnimActionEnd; a++ {

View File

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

View File

@@ -2,6 +2,9 @@ package flow
import ( import (
"fmt" "fmt"
"code.ur.gs/lupine/ordoor/internal/assetstore"
"code.ur.gs/lupine/ordoor/internal/ui"
) )
type driverName string type driverName string
@@ -36,8 +39,62 @@ var (
configureUltEquip, configureVehiclesUltra, configureUltEquip, configureVehiclesUltra,
mainGame, 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() { func (f *Flow) returnToLastDriver(from driverName) func() {
return func() { return func() {
to, ok := f.returns[from] to, ok := f.returns[from]

View File

@@ -6,7 +6,8 @@ import (
"log" "log"
"strings" "strings"
"github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil"
"code.ur.gs/lupine/ordoor/internal/assetstore" "code.ur.gs/lupine/ordoor/internal/assetstore"
"code.ur.gs/lupine/ordoor/internal/config" "code.ur.gs/lupine/ordoor/internal/config"
@@ -92,18 +93,9 @@ func New(assets *assetstore.AssetStore, config *config.Config, ship *ship.Ship)
return out, out.exit return out, out.exit
} }
func buildDriver(assets *assetstore.AssetStore, name driverName) (*ui.Driver, error) { func (f *Flow) SetScenario(scenario *scenario.Scenario) {
menu, err := assets.Menu(string(name)) f.current = f.drivers[mainGame]
if err != nil { f.scenario = scenario
return nil, err
}
driver, err := ui.NewDriver(assets, menu)
if err != nil {
return nil, err
}
return driver, nil
} }
func (f *Flow) Update(screenX, screenY int) error { func (f *Flow) Update(screenX, screenY int) error {
@@ -111,6 +103,32 @@ func (f *Flow) Update(screenX, screenY int) error {
return f.exit 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 f.scenario != nil {
if err := f.scenario.Update(screenX, screenY); err != nil { if err := f.scenario.Update(screenX, screenY); err != nil {
return err return err
@@ -241,9 +259,37 @@ func (f *Flow) playNextScenario(from driverName) func() {
} }
} }
func (f *Flow) setActive(driver driverName, id string, value bool) func() {
return func() {
if f.exit != nil {
return
}
f.exit = maybeErr(driver, f.setActiveNow(driver, id, value))
}
}
func (f *Flow) setActiveNow(driver driverName, id string, value bool) error {
return f.drivers[driver].SetActive(locator(driver, id), value)
}
func (f *Flow) toggleActive(driver driverName, id string) func() {
return func() {
if f.exit != nil {
return
}
f.exit = maybeErr(driver, f.drivers[driver].ToggleActive(locator(driver, id)))
}
}
func (f *Flow) showDialogue(driver driverName, id string) func() { func (f *Flow) showDialogue(driver driverName, id string) func() {
return func() { return func() {
f.drivers[driver].ShowDialogue(locator(driver, id)) if f.exit != nil {
return
}
f.exit = maybeErr(driver, f.drivers[driver].ShowDialogue(locator(driver, id)))
} }
} }
@@ -251,6 +297,14 @@ func (f *Flow) hideDialogue(driver driverName) func() {
return f.drivers[driver].HideDialogue return f.drivers[driver].HideDialogue
} }
func (f *Flow) withScenario(then func()) func() {
return func() {
if f.scenario != nil {
then()
}
}
}
func (f *Flow) reset() { func (f *Flow) reset() {
if f.exit != nil { if f.exit != nil {
return return

View File

@@ -1,55 +1,205 @@
package flow package flow
import (
"code.ur.gs/lupine/ordoor/internal/maps"
)
// TODO: There are Chaos and Ultramarine versions of MainGame. Do we really want // TODO: There are Chaos and Ultramarine versions of MainGame. Do we really want
// to duplicate everything for both? // to duplicate everything for both?
func (f *Flow) linkMainGame() { func (f *Flow) linkMainGame() {
// 3: Action menu f.linkMainGameActionMenu()
f.linkMainGameInterfaceOptionsMenu()
// 4: Interface options menu
f.onClick(mainGame, "4.1", f.setReturningDriver(mainGame, options)) // Options button
// 4.2: Map button
// 4.3: Mission objectives button
// 4.4: Inventory
// 4.5: Next man
// 4.6: Next enemy
// 4.7: Total enemy text
// 5: Holding menu // 5: Holding menu
// 6: View menu f.linkMainGameViewMenu()
// 7: General character menu // 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 // 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 // 9: Visible enemy menu
// 10: Friendly squad menu // 10: Friendly squad menu
// 11: Psyker spell dialogue // 11: Psyker spell dialogue
// FIXME: lots and lots and lots of wiring up to do.
// For now, just link all the exit buttons to go back to the bridge
f.onClick(mainGame, "11.6", func() {
f.scenario = nil
f.returnToLastDriverNow(mainGame)
})
// 12: Inventory dialogue // 12: Inventory dialogue
f.onClick(mainGame, "12.21", func() { f.onClick(mainGame, "12.21", f.hideDialogue(mainGame)) // Exit
f.scenario = nil
f.returnToLastDriverNow(mainGame)
})
// 13: Exchange menu // 13: exchange menu
f.onClick(mainGame, "13.1", func() {
f.scenario = nil
f.returnToLastDriverNow(mainGame)
})
// 14: Map // 14: Map
// 14.1: MAP_SPRITE
// 14.2: Multiplier button (2x)
f.onClick(mainGame, "14.3", f.setActive(mainGame, "14", false))
// 14.4: Area
// 15: Interface wing left // FIXME: the display of left and right interface buttons is hidden by these
// 16: Interface wing right // 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 // 17: Grenade dialogue
// 18: Info dialogue
f.onClick(mainGame, "18.12", f.setActive(mainGame, "18", false)) // Info "dialogue"
// 19: Turn start dialogue // 19: Turn start dialogue
// 20: Chat menu // 20: Chat menu
// 21: Chat list menu box
// Chat list menu box - active by default, hide it
f.exit = maybeErr(mainGame, f.setActiveNow(mainGame, "21", false))
}
func (f *Flow) linkMainGameActionMenu() {
// 3: Action menu. These are mostly predicated on selected character state
// 3.1: Aimed shot
// 3.2: Shooting
// 3.3: Walk
// 3.4: Run
// 3.5: Crouch/Stand
// 3.6: Hand to hand (commented out)
// 3.7: Retrieve
// 3.8: Door
// 3.9: Switch
// 3.10: Overwatch
// 3.11: Rally/Formation
// 3.12: Board/Disembark
// FIXME: for now, this is "end scenario", for convenience
f.onClick(mainGame, "3.13", func() { // End turn button.
f.scenario = nil
f.returnToLastDriverNow(mainGame)
})
// 3.14: Special action heal
// 3.15: Special action techmarine
// 3.16: Special action jump pack
// 3.17: Special action spell
}
func (f *Flow) linkMainGameInterfaceOptionsMenu() {
// 4: Interface options menu
f.onClick(mainGame, "4.1", f.setReturningDriver(mainGame, options)) // Options button
// FIXME: map should be shown top-right, not top-left. We need to support 2x
// mode as well.
f.onClick(mainGame, "4.2", f.toggleActive(mainGame, "14")) // Map button
// FIXME: mission objectives should be shown top-left, not centered
f.onClick(mainGame, "4.3", f.toggleActive(mainGame, "18")) // Mission objectives
f.onClick(mainGame, "4.4", f.showDialogue(mainGame, "12")) // Inventory
// 4.5: Next man
// 4.6: Next enemy
// 4.7: Total enemy text
}
func (f *Flow) linkMainGameViewMenu() {
// FIXME: all these buttons should show current state as well as have an
// effect
f.onClick(mainGame, "6.1", f.withScenario(func() { // View 100%
f.scenario.Zoom = 1.0
}))
f.onClick(mainGame, "6.2", f.withScenario(func() { // View 50%
f.scenario.Zoom = 0.5
}))
f.onClick(mainGame, "6.3", f.withScenario(func() { // View 25%
f.scenario.Zoom = 0.25
}))
f.onClick(mainGame, "6.4", f.withScenario(func() { // Z index up
f.scenario.ChangeZIdx(+1)
}))
f.onClick(mainGame, "6.5", f.withScenario(func() { // Z index down
f.scenario.ChangeZIdx(-1)
}))
f.onClick(mainGame, "6.6", f.withScenario(func() { // Z index 1
f.scenario.ZIdx = 0
}))
f.onClick(mainGame, "6.7", f.withScenario(func() { // Z index 2
f.scenario.ZIdx = 1
}))
f.onClick(mainGame, "6.8", f.withScenario(func() { // Z index 3
f.scenario.ZIdx = 2
}))
f.onClick(mainGame, "6.9", f.withScenario(func() { // Z index 4
f.scenario.ZIdx = 3
}))
f.onClick(mainGame, "6.10", f.withScenario(func() { // Z index 5
f.scenario.ZIdx = 4
}))
f.onClick(mainGame, "6.11", f.withScenario(func() { // Z index 6
f.scenario.ZIdx = 5
}))
f.onClick(mainGame, "6.12", f.withScenario(func() { // Z index 7
f.scenario.ZIdx = 6
}))
}
func (f *Flow) maybeSetErr(next func() error) {
if f.exit != nil {
return
}
f.exit = next()
}
func (f *Flow) selectedMainGameCharacter(chr *maps.Character) {
if chr == nil {
chr = &maps.Character{}
}
d := f.drivers[mainGame]
// 7.1 Portrait
f.maybeSetErr(func() error { return d.SetValue("7.2", chr.Name) }) // Name
// 7.3 doesn't exit
// 7.4 more button (ignore)
// 7.5 AP icon
// f.maybeSetErr(func() error { return d.SetValueInt("7.6", chr.ActionPoints)}) // AP meter
f.maybeSetErr(func() error { return d.SetValueInt("7.7", chr.ActionPoints) }) // AP value
// 7.8 armor icon
// 7.9 armor meter
f.maybeSetErr(func() error { return d.SetValueInt("7.10", chr.Armor) }) // armor value
// 7.11 health icon
// 7.12 health meter
f.maybeSetErr(func() error { return d.SetValueInt("7.13", chr.Health) }) // health value
// 7.14 action points status bar
// 7.15 armor status bar
// 7.16 health status bar
// 8.1 to 8.10 are hot spots
f.maybeSetErr(func() error { return d.SetValueInt("8.11", chr.ActionPoints) }) // AP
f.maybeSetErr(func() error { return d.SetValueInt("8.12", chr.Health) }) // Health
f.maybeSetErr(func() error { return d.SetValueInt("8.13", chr.Armor) }) // Armor
f.maybeSetErr(func() error { return d.SetValueInt("8.14", chr.BallisticSkill) }) // Ballistic Skill
f.maybeSetErr(func() error { return d.SetValueInt("8.15", chr.WeaponSkill) }) // Weapon Skill
f.maybeSetErr(func() error { return d.SetValueInt("8.16", chr.Strength) }) // Strength
f.maybeSetErr(func() error { return d.SetValueInt("8.17", chr.Toughness) }) // Toughness
// 8.18 Initiative
// 8.19 Attacks
f.maybeSetErr(func() error { return d.SetValueInt("8.20", chr.Leadership) }) // Leadership
} }

View File

@@ -5,12 +5,15 @@ import (
"compress/gzip" "compress/gzip"
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"io" "image"
"io/ioutil" "io/ioutil"
"log"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/lunixbochs/struc"
"code.ur.gs/lupine/ordoor/internal/data"
) )
var ( var (
@@ -26,63 +29,135 @@ const (
CellSize = 16 // seems to be CellSize = 16 // seems to be
cellDataOffset = 0x110 // tentatively cellDataOffset = 0x110 // definitely
cellCount = MaxHeight * MaxLength * MaxWidth cellCount = MaxHeight * MaxLength * MaxWidth
) )
type Header struct { type GameMap struct {
IsCampaignMap uint32 // Tentatively: 0 = no, 1 = yes // Main Header
MinWidth uint32 IsCampaignMap bool `struc:"uint32"` // Tentatively: 0 = no, 1 = yes
MinLength uint32 MinWidth int `struc:"uint32"`
MaxWidth uint32 MinLength int `struc:"uint32"`
MaxLength uint32 MaxWidth int `struc:"uint32"`
Unknown1 uint32 MaxLength int `struc:"uint32"`
Unknown2 uint32 Unknown1 int `struc:"uint32"`
Unknown3 uint32 Unknown2 int `struc:"uint32"`
Unknown4 uint32 Unknown3 int `struc:"uint32"`
Magic [8]byte // "\x08\x00WHMAP\x00" Unknown4 int `struc:"uint32"`
Unknown5 uint32 Magic []byte `struc:"[8]byte"` // "\x08\x00WHMAP\x00"
Unknown6 uint32 Unknown5 int `struc:"uint32"`
SetName [8]byte // Links to a filename in `/Sets/*.set` Unknown6 int `struc:"uint32"`
// Need to investigate the rest of the header too SetName string `struc:"[8]byte"` // Links to a filename in `/Sets/*.set`
Padding []byte `struc:"[212]byte"`
// Per-cell data
NumCells int `struc:"skip"` // FIXME: We can't use []Cell below without this field
Cells []Cell `struc:"[]Cell,sizefrom=NumCells"`
// Trailer header
Discard1 [3]byte `struc:"[3]byte"` // First byte is size of trailer header?
TrailerMaxWidth int `struc:"uint32"`
TrailerMaxLength int `struc:"uint32"`
TrailerMinWidth int `struc:"uint32"`
TrailerMinLength int `struc:"uint32"`
NumCharacters int `struc:"uint32"`
TrailerUnknown1 int `struc:"uint32"`
TrailerUnknown2 int `struc:"uint16"`
TrailerUnknown3 int `struc:"uint16"`
TrailerUnknown4 int `struc:"uint32"`
NumThingies int `struc:"byte"`
TrailerUnknown5 []byte `struc:"[3]byte"`
Padding1 []byte `struc:"[20]byte"`
// FIXME: The rest is trash until Character & Thingy are worked out
Characters []Character `struc:"[]Character,sizefrom=NumCharacters"`
Thingies []Thingy `struc:"[]Thingy,sizefrom=NumThingies"`
Title string `struc:"[255]byte"`
Briefing string `struc:"[2048]byte"`
// Maybe? each contains either 0 or 1? Hard to say
TrailerUnknown6 []byte `struc:"[85]byte"`
} }
func (h Header) Width() int { type Cell struct {
return int(h.MaxWidth - h.MinWidth) 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 { type Character struct {
return int(h.MaxLength - h.MinLength) 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 { type Characters []Character
return MaxHeight
// TODO. These are triggers/reactors/etc.
type Thingy struct {
Unknown1 int `struc:"uint32"`
} }
func (h Header) MapSetName() string { type Thingies []Thingy
idx := bytes.IndexByte(h.SetName[:], 0)
if idx < 0 {
idx = 8 // all 8 bytes are used
}
return string(h.SetName[0:idx:idx]) func (g *GameMap) MapSetName() string {
return g.SetName
} }
func (h Header) MapSetFilename() string { func (g *GameMap) MapSetFilename() string {
return h.MapSetName() + ".set" return g.MapSetName() + ".set"
} }
type ObjRef struct { type ObjRef struct {
AreaByte byte AreaByte byte `struc:"byte"`
SpriteAndFlagByte byte SpriteAndFlagByte byte `struc:"byte"`
} }
// The index into a set palette to retrieve the object // The index into a set palette to retrieve the object
func (o ObjRef) Index() int { func (o *ObjRef) Index() int {
return int(o.AreaByte) return int(o.AreaByte)
} }
func (o ObjRef) Sprite() int { func (o *ObjRef) Sprite() int {
// The top bit seems to be a flag of some kind // The top bit seems to be a flag of some kind
return int(o.SpriteAndFlagByte & 0x7f) return int(o.SpriteAndFlagByte & 0x7f)
} }
@@ -92,21 +167,6 @@ func (o ObjRef) IsActive() bool {
return (o.SpriteAndFlagByte & 0x80) == 0x80 return (o.SpriteAndFlagByte & 0x80) == 0x80
} }
type Cell struct {
DoorAndCanisterRelated byte
DoorLockAndReactorRelated byte
Unknown2 byte
Surface ObjRef
Left ObjRef
Right ObjRef
Center ObjRef
Unknown11 byte
Unknown12 byte
Unknown13 byte
Unknown14 byte
SquadRelated byte
}
func (c *Cell) At(n int) byte { func (c *Cell) At(n int) byte {
switch n { switch n {
case 0: case 0:
@@ -146,31 +206,27 @@ func (c *Cell) At(n int) byte {
return 0 return 0
} }
// Cells is always a fixed size; use At to get a cell according to x,y,z func (g *GameMap) At(x, y, z int) *Cell {
type Cells []Cell return &g.Cells[(z*MaxLength*MaxWidth)+(y*MaxWidth)+x]
func (c Cells) At(x, y, z int) Cell {
return c[(z*MaxLength*MaxWidth)+(y*MaxWidth)+x]
} }
func (h Header) Check() []error { func (g *GameMap) Check() error {
var out []error if bytes.Compare(expectedMagic, g.Magic) != 0 {
if h.IsCampaignMap > 1 { return fmt.Errorf("Unexpected magic value: %v", g.Magic)
out = append(out, fmt.Errorf("Expected 0 or 1 for IsCampaignMap, got %v", h.IsCampaignMap))
} }
if bytes.Compare(expectedMagic, h.Magic[:]) != 0 { // TODO: other consistency checks
out = append(out, fmt.Errorf("Unexpected magic value: %v", h.Magic))
}
return out return nil
} }
type GameMap struct { func (m *GameMap) Rect() image.Rectangle {
Header return image.Rect(
Cells int(m.MinWidth),
// TODO: parse this into sections int(m.MinLength),
Text string int(m.MaxWidth),
int(m.MaxLength),
)
} }
// A game map contains a .txt and a .map. If they're in the same directory, // 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) return nil, fmt.Errorf("Couldn't find %s.{map,txt}, even ignoring case", prefix)
} }
// A game map is composed of two files: .map and .txt // A game map is composed of two files: .map and .txt. We ignore the text file,
// since the content is replicated in the map file.
func LoadGameMapByFiles(mapFile, txtFile string) (*GameMap, error) { func LoadGameMapByFiles(mapFile, txtFile string) (*GameMap, error) {
out, err := loadMapFile(mapFile) out, err := loadMapFile(mapFile)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// TODO: load text and parse into sections if err := out.Check(); err != nil {
txt, err := ioutil.ReadFile(txtFile)
if err != nil {
return nil, err return nil, err
} }
out.Text = string(txt)
for _, err := range out.Check() {
log.Printf("%s: %v", mapFile, err)
}
return out, nil return out, nil
} }
@@ -246,6 +296,7 @@ func LoadGameMaps(dir string) (map[string]*GameMap, error) {
func loadMapFile(filename string) (*GameMap, error) { func loadMapFile(filename string) (*GameMap, error) {
var out GameMap var out GameMap
out.NumCells = cellCount
mf, err := os.Open(filename) mf, err := os.Open(filename)
if err != nil { if err != nil {
@@ -261,20 +312,46 @@ func loadMapFile(filename string) (*GameMap, error) {
defer zr.Close() defer zr.Close()
if err := binary.Read(zr, binary.LittleEndian, &out.Header); err != nil { if err := struc.UnpackWithOrder(zr, &out, binary.LittleEndian); err != nil {
return nil, fmt.Errorf("Error parsing %s: %v", filename, err)
}
// no gzip.SeekReader, so discard unread header bytes for now
discardSize := int64(cellDataOffset - binary.Size(&out.Header))
if _, err := io.CopyN(ioutil.Discard, zr, discardSize); err != nil {
return nil, err return nil, err
} }
out.Cells = make(Cells, cellCount) // Trim any trailing nulls off of the strings
if err := binary.Read(zr, binary.LittleEndian, &out.Cells); err != nil { nullTerminate(&out.SetName)
return nil, fmt.Errorf("Error parsing cells for %s: %v", filename, err) nullTerminate(&out.Title)
nullTerminate(&out.Briefing)
for i, _ := range out.Characters {
chr := &out.Characters[i]
nullTerminate(&chr.Name)
fmt.Printf("Character %v: %s\n", i, chr.String())
} }
fmt.Printf("Mission Title: %q\n", out.Title)
fmt.Printf("Mission Briefing: %q\n", out.Briefing)
return &out, nil return &out, nil
} }
func nullTerminate(s *string) {
sCpy := *s
idx := strings.Index(sCpy, "\x00")
if idx < 0 {
return
}
*s = sCpy[0:idx]
}
func (c *Character) String() string {
return fmt.Sprintf(
"squad=%v pos=(%v,%v) type=%q name=%q\n"+
"\t%3d %3d %3d %3d %3d\n\t%3d %3d ??? ??? %3d\n",
c.SquadNumber,
c.XPos, c.YPos,
c.Type.String(),
c.Name,
c.ActionPoints, c.Health, c.Armor, c.BallisticSkill, c.WeaponSkill,
c.Strength, c.Toughness /*c.Initiative, c.Attacks,*/, c.Leadership,
)
}

View File

@@ -2,13 +2,13 @@ package menus
import ( import (
"fmt" "fmt"
"image"
"image/color" "image/color"
"io/ioutil" "io/ioutil"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"code.ur.gs/lupine/ordoor/internal/data"
"code.ur.gs/lupine/ordoor/internal/util/asciiscan" "code.ur.gs/lupine/ordoor/internal/util/asciiscan"
) )
@@ -33,7 +33,7 @@ const (
SubTypeLineBriefing SubMenuType = 41 SubTypeLineBriefing SubMenuType = 41
SubTypeThumb SubMenuType = 45 // A "thumb" appears to be a vertical slider SubTypeThumb SubMenuType = 45 // A "thumb" appears to be a vertical slider
SubTypeInvokeButton SubMenuType = 50 SubTypeInvokeButton SubMenuType = 50
SubTypeDoorHotspot3 SubMenuType = 60 // Maybe? Appears in Arrange.mnu SubTypeClickText SubMenuType = 60
SubTypeOverlay SubMenuType = 61 SubTypeOverlay SubMenuType = 61
SubTypeHypertext SubMenuType = 70 SubTypeHypertext SubMenuType = 70
SubTypeCheckbox SubMenuType = 91 SubTypeCheckbox SubMenuType = 91
@@ -134,7 +134,15 @@ type Properties struct {
Help string Help string
} }
func LoadMenu(filename string) (*Menu, error) { func (p *Properties) Point() image.Point {
if p.X > 0 || p.Y > 0 {
return image.Pt(p.X, p.Y)
}
return image.Point{}
}
func LoadMenu(filename string, palette color.Palette) (*Menu, error) {
name := filepath.Base(filename) name := filepath.Base(filename)
name = strings.TrimSuffix(name, filepath.Ext(name)) name = strings.TrimSuffix(name, filepath.Ext(name))
name = strings.ToLower(name) name = strings.ToLower(name)
@@ -154,7 +162,7 @@ func LoadMenu(filename string) (*Menu, error) {
return nil, err return nil, err
} }
if err := loadProperties(out, scanner); err != nil { if err := loadProperties(out, scanner, palette); err != nil {
return nil, err return nil, err
} }
@@ -180,7 +188,7 @@ func loadObjects(menu *Menu, scanner *asciiscan.Scanner) error {
return nil return nil
} }
func loadProperties(menu *Menu, scanner *asciiscan.Scanner) error { func loadProperties(menu *Menu, scanner *asciiscan.Scanner, palette color.Palette) error {
for { for {
ok, err := scanner.PeekProperty() ok, err := scanner.PeekProperty()
@@ -209,9 +217,9 @@ func loadProperties(menu *Menu, scanner *asciiscan.Scanner) error {
switch strings.ToUpper(k) { switch strings.ToUpper(k) {
case "BACKGROUND COLOR": case "BACKGROUND COLOR":
menu.BackgroundColor = data.ColorPalette[vInt] menu.BackgroundColor = palette[vInt]
case "HYPERTEXT COLOR": case "HYPERTEXT COLOR":
menu.HypertextColor = data.ColorPalette[vInt] menu.HypertextColor = palette[vInt]
case "FONT TYPE": case "FONT TYPE":
menu.FontType = vInt menu.FontType = vInt
default: default:
@@ -310,7 +318,7 @@ func loadRecords(baseDir string, menu *Menu, scanner *asciiscan.Scanner) error {
return nil return nil
} }
func LoadMenus(dir string) (map[string]*Menu, error) { func LoadMenus(dir string, palette color.Palette) (map[string]*Menu, error) {
fis, err := ioutil.ReadDir(dir) fis, err := ioutil.ReadDir(dir)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -328,7 +336,7 @@ func LoadMenus(dir string) (map[string]*Menu, error) {
continue continue
} }
built, err := LoadMenu(filepath.Join(dir, relname)) built, err := LoadMenu(filepath.Join(dir, relname), palette)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s: %v", filepath.Join(dir, relname), err) return nil, fmt.Errorf("%s: %v", filepath.Join(dir, relname), err)
} }

View File

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

View File

@@ -7,9 +7,11 @@ package ordoor
import ( import (
"fmt" "fmt"
"log" "log"
"sync"
"time"
"github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/audio" "github.com/hajimehoshi/ebiten/v2/audio"
"code.ur.gs/lupine/ordoor/internal/assetstore" "code.ur.gs/lupine/ordoor/internal/assetstore"
"code.ur.gs/lupine/ordoor/internal/config" "code.ur.gs/lupine/ordoor/internal/config"
@@ -27,19 +29,24 @@ type Ordoor struct {
win *ui.Window win *ui.Window
// Relevant to interface state // 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 // Relevant to campaign state
ship *ship.Ship ship *ship.Ship
} }
func Run(configFile string, overrideX, overrideY int) error { func Run(configFile string, overrideX, overrideY int) error {
cfg, err := config.Load(configFile) cfg, err := config.Load(configFile, "ordoor")
if err != nil { if err != nil {
return fmt.Errorf("Couldn't load config file: %v", err) 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 { if err != nil {
return fmt.Errorf("Failed to initialize asset store: %v", err) return fmt.Errorf("Failed to initialize asset store: %v", err)
} }
@@ -56,9 +63,7 @@ func Run(configFile string, overrideX, overrideY int) error {
} }
} }
if _, err := audio.NewContext(48000); err != nil { _ = audio.NewContext(48000)
return fmt.Errorf("Failed to set up audio context: %v", err)
}
ordoor := &Ordoor{ ordoor := &Ordoor{
assets: assets, assets: assets,
@@ -81,10 +86,6 @@ func Run(configFile string, overrideX, overrideY int) error {
ordoor.win = win 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 { if err := ordoor.Run(); err != nil {
return fmt.Errorf("Run finished with error: %v", err) return fmt.Errorf("Run finished with error: %v", err)
} }
@@ -93,12 +94,16 @@ func Run(configFile string, overrideX, overrideY int) error {
} }
func (o *Ordoor) Run() error { 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 { if o.config.Options.PlayMovies {
o.PlaySkippableVideo("LOGOS") o.PlaySkippableVideo("LOGOS")
o.PlaySkippableVideo("movie1") 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() err := o.win.Run()
if err == flow.ErrExit { if err == flow.ErrExit {
log.Printf("Exit requested") log.Printf("Exit requested")
@@ -151,6 +156,16 @@ func (o *Ordoor) setupFlow() error {
} }
func (o *Ordoor) Update(screenX, screenY int) error { func (o *Ordoor) Update(screenX, screenY int) error {
if pic := o.pic; pic != nil {
return nil // Ignore flow until we don't have a pic any more
}
if o.flow == nil {
if err := o.setupFlow(); err != nil {
return fmt.Errorf("failed to setup UI flow: %v", err)
}
}
// Ensure music is doing the right thing // Ensure music is doing the right thing
if o.music != nil && o.music.IsPlaying() != o.config.Options.PlayMusic { if o.music != nil && o.music.IsPlaying() != o.config.Options.PlayMusic {
if o.config.Options.PlayMusic { if o.config.Options.PlayMusic {
@@ -165,9 +180,30 @@ func (o *Ordoor) Update(screenX, screenY int) error {
} }
func (o *Ordoor) Draw(screen *ebiten.Image) error { func (o *Ordoor) Draw(screen *ebiten.Image) error {
return o.flow.Draw(screen)
if pic := o.pic; pic != nil {
// Scale the picture to the screen and draw it
scaleX := float64(screen.Bounds().Dx()) / float64(pic.Bounds().Dx())
scaleY := float64(screen.Bounds().Dy()) / float64(pic.Bounds().Dy())
do := &ebiten.DrawImageOptions{}
do.GeoM.Scale(scaleX, scaleY)
screen.DrawImage(pic, do)
return nil
}
if o.flow != nil {
return o.flow.Draw(screen)
}
return nil // Draw() may be called before Update()
} }
func (o *Ordoor) Cursor() (*ebiten.Image, *ebiten.DrawImageOptions, error) { func (o *Ordoor) Cursor() (*ebiten.Image, *ebiten.DrawImageOptions, error) {
return o.flow.Cursor() if o.flow != nil {
return o.flow.Cursor()
}
return nil, nil, nil
} }

View File

@@ -6,7 +6,7 @@ import (
) )
func (o *Ordoor) PlayVideo(name string, skippable bool) { 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 { if len(o.config.VideoPlayer) == 0 {
log.Printf("Video player not configured, skipping video %v", filename) log.Printf("Video player not configured, skipping video %v", filename)

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,8 +5,8 @@ import (
"image" "image"
"sort" "sort"
"github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/ebitenutil" "github.com/hajimehoshi/ebiten/v2/ebitenutil"
) )
type CartPt struct { type CartPt struct {
@@ -22,6 +22,22 @@ type IsoPt struct {
func (s *Scenario) Update(screenX, screenY int) error { func (s *Scenario) Update(screenX, screenY int) error {
s.tick += 1 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 return nil
} }
@@ -30,16 +46,19 @@ func (s *Scenario) Draw(screen *ebiten.Image) error {
// http://www.java-gaming.org/index.php?topic=24922.0 // http://www.java-gaming.org/index.php?topic=24922.0
// https://stackoverflow.com/questions/892811/drawing-isometric-game-worlds // 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 // 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() sw, sh := screen.Size()
topLeft := CartPt{X: float64(s.Viewpoint.X), Y: float64(s.Viewpoint.Y)}.ToISO() topLeft := CartPt{
topLeft.X -= 5 // Ensure we paint to every visible section of the screeen. X: float64(s.Viewpoint.X) - (2 * cellWidth / s.Zoom), // Ensure all visible cells are rendered
topLeft.X -= 5 // FIXME: haxxx Y: float64(s.Viewpoint.Y) - (2 * cellHeight / s.Zoom),
}.ToISO()
bottomRight := CartPt{X: float64(s.Viewpoint.X + sw), Y: float64(s.Viewpoint.Y + sh)}.ToISO() bottomRight := CartPt{
bottomRight.X += 5 X: float64(s.Viewpoint.X) + (float64(sw) / s.Zoom) + (2 * cellHeight / s.Zoom),
bottomRight.Y += 5 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 column
// X-Y is constant for all tiles in a row // X-Y is constant for all tiles in a row
@@ -76,70 +95,108 @@ func (s *Scenario) Draw(screen *ebiten.Image) error {
return false return false
}) })
counter := map[string]int{} counter := 0
for _, pt := range toDraw { for _, pt := range toDraw {
for z := 0; z <= s.ZIdx; z++ { for z := 0; z <= s.ZIdx; z++ {
if err := s.renderCell(pt, z, screen, counter); err != nil { if err := s.renderCell(int(pt.X), int(pt.Y), z, screen, &counter); err != nil {
return err return err
} }
} }
} }
//log.Printf("%#+v", counter) // Finally, draw cursor chrome
// FIXME: it looks like we might need to do this in normal painting order...
return nil spr, err := s.specials.Sprite(0)
}
func (s *Scenario) renderCell(pos IsoPt, z int, screen *ebiten.Image, counter map[string]int) error {
sprites, err := s.area.SpritesForCell(int(pos.X), int(pos.Y), z)
if err != nil { if err != nil {
return err return err
} }
iso := ebiten.GeoM{} op := ebiten.DrawImageOptions{}
iso.Translate(-float64(s.Viewpoint.X), -float64(s.Viewpoint.Y)) 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)
pix := pos.ToCart() screen.DrawImage(spr.Image, &op)
iso.Translate(pix.X, pix.Y)
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. // Taking the Z index away *seems* to draw the object in the correct place.
// FIXME: There are some artifacts, investigate more // FIXME: There are some artifacts, investigate more
iso.Translate(0.0, -float64(z*48.0)) // offset for Z index 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 // 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 { for _, spr := range sprites {
// if _, ok := counter[spr.ID]; !ok { *counter = *counter + 1
// counter[spr.ID] = 0
// }
// counter[spr.ID] = counter[spr.ID] + 1
op := ebiten.DrawImageOptions{GeoM: iso} op := ebiten.DrawImageOptions{GeoM: iso}
// FIXME: this fixed offset is found in jungtil.obj. Drawing with it op.GeoM.Translate(float64(spr.Rect.Min.X), float64(spr.Rect.Min.Y))
// means we put everywhere where the iso->pix conversion expects, but
// it's a bit nasty. Is there a better way?
op.GeoM.Translate(float64(spr.Rect.Min.X-209), float64(spr.Rect.Min.Y-322))
if err := screen.DrawImage(spr.Image, &op); err != nil { // Zoom has to come last
return err op.GeoM.Scale(s.Zoom, s.Zoom)
}
if z == 0 {
x, y := op.GeoM.Apply(0, 0)
ebitenutil.DebugPrintAt(screen, fmt.Sprintf("(%d,%d)", int(pos.X), int(pos.Y)), int(x), int(y))
}
screen.DrawImage(spr.Image, &op)
} }
return nil return nil
} }
const ( const (
cellWidth = 128 cellWidth = 128.0
cellHeight = 64 cellHeight = 63.0
cellWidthHalf = cellWidth / 2 cellWidthHalf = cellWidth / 2.0
cellHeightHalf = cellHeight / 2 cellHeightHalf = cellHeight / 2.0
) )
func (p CartPt) ToISO() IsoPt { func (p CartPt) ToISO() IsoPt {
@@ -155,28 +212,3 @@ func (p IsoPt) ToCart() CartPt {
Y: (p.X + p.Y) * cellHeightHalf, Y: (p.X + p.Y) * cellHeightHalf,
} }
} }
/*
// Doesn't take the camera or Z level into account
func cellToPix(pt image.Point) image.Point {
return image.Pt(
(pt.X-pt.Y)*cellWidthHalf,
(pt.X+pt.Y)*cellHeightHalf,
)
}
// Doesn't take the camera or Z level into account
func pixToCell(pt image.Point) image.Point {
fX := pt.X
fY := pt.Y
return image.Pt(
// (pt.X / cellWidthHalf + pt.Y / cellHeightHalf) / 2,
// (pt.Y / cellHeightHalf - (pt.Y / cellWidthHalf)) / 2,
// int(fY/cellHeight+fX/(cellWidth*2)),
// int(fY/cellHeight-fX/(cellWidth*2)),
//int((fY / cellHeight) + (fX / cellWidth)),
//int((-fX / cellWidth) + (fY / cellHeight)),
)
}*/

View File

@@ -1,9 +1,7 @@
package scenario package scenario
import ( import (
// "image" "log"
"github.com/hajimehoshi/ebiten"
"code.ur.gs/lupine/ordoor/internal/maps" "code.ur.gs/lupine/ordoor/internal/maps"
) )
@@ -13,13 +11,35 @@ type CellPoint struct {
Z int Z int
} }
func (s *Scenario) CellAtCursor() (maps.Cell, CellPoint) { func (s *Scenario) CellAtCursor() (*maps.Cell, CellPoint) {
x, y := ebiten.CursorPosition() cell := s.area.Cell(int(s.highlightedCell.X), int(s.highlightedCell.Y), 0)
screenPos := CartPt{X: float64(s.Viewpoint.X + x), Y: float64(s.Viewpoint.Y + y)} return cell, CellPoint{IsoPt: s.highlightedCell, Z: 0}
isoPos := screenPos.ToISO() }
// Convert to cell coordinates.
// TODO: zoom support will need a camera func (s *Scenario) HighlightedCharacter() *maps.Character {
// FIXME: adjust for Z level // FIXME: characters are always at zIdx 0 right now
return s.area.CharacterAt(int(s.highlightedCell.X), int(s.highlightedCell.Y), 0)
return s.area.Cell(int(isoPos.X), int(isoPos.Y), 0), CellPoint{IsoPt: isoPos, Z: s.ZIdx} }
func (s *Scenario) SelectedCharacter() *maps.Character {
return s.selectedCharacter
}
func (s *Scenario) SelectHighlightedCharacter() {
chr := s.HighlightedCharacter()
log.Printf("Selected character %s", chr)
s.selectedCharacter = chr
}
func (s *Scenario) ChangeZIdx(by int) {
newZ := s.ZIdx + by
if newZ < 0 {
newZ = 0
}
if newZ > 6 {
newZ = 6
}
s.ZIdx = newZ
} }

View File

@@ -2,23 +2,28 @@
package scenario package scenario
import ( import (
"fmt"
"image" "image"
"code.ur.gs/lupine/ordoor/internal/assetstore" "code.ur.gs/lupine/ordoor/internal/assetstore"
"code.ur.gs/lupine/ordoor/internal/maps"
) )
type Scenario struct { type Scenario struct {
area *assetstore.Map area *assetstore.Map
specials *assetstore.Object
tick int tick int
turn int turn int
highlightedCell IsoPt
selectedCharacter *maps.Character
// All these must be modified by user actions somehow. // All these must be modified by user actions somehow.
// TODO: extract into the idea of a viewport passed to Update / Draw somehow? // TODO: extract into the idea of a viewport passed to Update / Draw somehow?
// Or have a separater Drawer for the Scenario? // Or have a separater Drawer for the Scenario?
Viewpoint image.Point // Top-left of the screen Viewpoint image.Point // Top-left of the screen
ZIdx int // Currently-viewed Z index ZIdx int // Currently-viewed Z index
Zoom float64 // Zoom level to set
} }
func NewScenario(assets *assetstore.AssetStore, name string) (*Scenario, error) { func NewScenario(assets *assetstore.AssetStore, name string) (*Scenario, error) {
@@ -27,14 +32,21 @@ func NewScenario(assets *assetstore.AssetStore, name string) (*Scenario, error)
return nil, err return nil, err
} }
// Eager load sprites. TODO: do we really want to do this? specials, err := assets.Object("specials") // FIXME: should this be hardcoded?
if err := area.LoadSprites(); err != nil { if err != nil {
return nil, fmt.Errorf("Eager-loading sprites failed: %v", err) 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{ out := &Scenario{
area: area, area: area,
specials: specials,
Viewpoint: image.Pt(0, 3000), // FIXME: haxxx Viewpoint: image.Pt(0, 3000), // FIXME: haxxx
Zoom: 1.0,
} }
return out, nil return out, nil

View File

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

View File

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

View File

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

View File

@@ -5,8 +5,8 @@ import (
"image" "image"
"runtime/debug" "runtime/debug"
"github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/ebitenutil" "github.com/hajimehoshi/ebiten/v2/ebitenutil"
"code.ur.gs/lupine/ordoor/internal/assetstore" "code.ur.gs/lupine/ordoor/internal/assetstore"
) )
@@ -137,9 +137,7 @@ func (d *Driver) Draw(screen *ebiten.Image) error {
do.GeoM = d.orig2native do.GeoM = d.orig2native
do.GeoM.Translate(x, y) do.GeoM.Translate(x, y)
if err := screen.DrawImage(region.image, &do); err != nil { screen.DrawImage(region.image, &do)
return err
}
} }
} }
@@ -169,11 +167,11 @@ func (d *Driver) allClickables() []clickable {
var out []clickable var out []clickable
for _, widget := range d.widgets { for _, widget := range d.widgets {
out = append(out, widget.clickables()...) out = append(out, widget.allClickables()...)
} }
for _, widget := range d.dialogues { for _, widget := range d.dialogues {
out = append(out, widget.clickables()...) out = append(out, widget.allClickables()...)
} }
return out return out
@@ -182,11 +180,11 @@ func (d *Driver) allClickables() []clickable {
func (d *Driver) allFreezables() []freezable { func (d *Driver) allFreezables() []freezable {
var out []freezable var out []freezable
for _, widget := range d.widgets { for _, widget := range d.widgets {
out = append(out, widget.freezables()...) out = append(out, widget.allFreezables()...)
} }
for _, widget := range d.dialogues { for _, widget := range d.dialogues {
out = append(out, widget.freezables()...) out = append(out, widget.allFreezables()...)
} }
return out return out
@@ -196,11 +194,11 @@ func (d *Driver) allValueables() []valueable {
var out []valueable var out []valueable
for _, widget := range d.widgets { for _, widget := range d.widgets {
out = append(out, widget.valueables()...) out = append(out, widget.allValueables()...)
} }
for _, widget := range d.dialogues { for _, widget := range d.dialogues {
out = append(out, widget.valueables()...) out = append(out, widget.allValueables()...)
} }
return out return out
@@ -208,12 +206,12 @@ func (d *Driver) allValueables() []valueable {
func (d *Driver) activeClickables() []clickable { func (d *Driver) activeClickables() []clickable {
if d.activeDialogue != nil { if d.activeDialogue != nil {
return d.activeDialogue.clickables() return d.activeDialogue.activeClickables()
} }
var out []clickable var out []clickable
for _, widget := range d.widgets { for _, widget := range d.widgets {
out = append(out, widget.clickables()...) out = append(out, widget.activeClickables()...)
} }
return out return out
@@ -221,12 +219,12 @@ func (d *Driver) activeClickables() []clickable {
func (d *Driver) activeHoverables() []hoverable { func (d *Driver) activeHoverables() []hoverable {
if d.activeDialogue != nil { if d.activeDialogue != nil {
return d.activeDialogue.hoverables() return d.activeDialogue.activeHoverables()
} }
var out []hoverable var out []hoverable
for _, widget := range d.widgets { for _, widget := range d.widgets {
out = append(out, widget.hoverables()...) out = append(out, widget.activeHoverables()...)
} }
return out return out
@@ -234,12 +232,12 @@ func (d *Driver) activeHoverables() []hoverable {
func (d *Driver) activeMouseables() []mouseable { func (d *Driver) activeMouseables() []mouseable {
if d.activeDialogue != nil { if d.activeDialogue != nil {
return d.activeDialogue.mouseables() return d.activeDialogue.activeMouseables()
} }
var out []mouseable var out []mouseable
for _, widget := range d.widgets { for _, widget := range d.widgets {
out = append(out, widget.mouseables()...) out = append(out, widget.activeMouseables()...)
} }
return out return out
@@ -249,12 +247,23 @@ func (d *Driver) activePaintables() []paintable {
var out []paintable var out []paintable
for _, widget := range d.widgets { for _, widget := range d.widgets {
out = append(out, widget.paintables()...) out = append(out, widget.activePaintables()...)
} }
if d.activeDialogue != nil { if d.activeDialogue != nil {
out = append(out, d.activeDialogue.paintables()...) out = append(out, d.activeDialogue.activePaintables()...)
} }
return out return out
} }
func (d *Driver) findWidget(locator string) *Widget {
toplevels := append(d.widgets, d.dialogues...)
for _, widget := range toplevels {
if w := widget.findWidget(locator); w != nil {
return w
}
}
return nil
}

View File

@@ -28,7 +28,10 @@ func (d *Driver) registerGroup(group *menus.Group) error {
return err return err
} }
} else { } else {
groupWidget = &Widget{Locator: group.Locator} groupWidget = &Widget{
Locator: group.Locator,
Active: group.Active,
}
} }
if dialogue { if dialogue {
@@ -75,8 +78,10 @@ func (d *Driver) buildRecord(r *menus.Record) (*Widget, error) {
switch r.Type { switch r.Type {
case menus.SubTypeSimpleButton, menus.SubTypeInvokeButton: case menus.SubTypeSimpleButton, menus.SubTypeInvokeButton:
_, widget, err = d.buildButton(r.Props()) _, widget, err = d.buildButton(r.Props())
case menus.SubTypeDoorHotspot1, menus.SubTypeDoorHotspot2, menus.SubTypeDoorHotspot3: case menus.SubTypeDoorHotspot1, menus.SubTypeDoorHotspot2:
_, widget, err = d.buildDoorHotspot(r.Props()) _, widget, err = d.buildDoorHotspot(r.Props())
case menus.SubTypeClickText:
_, widget, err = d.buildClickText(r.Props())
case menus.SubTypeOverlay: case menus.SubTypeOverlay:
_, widget, err = d.buildOverlay(r.Props()) _, widget, err = d.buildOverlay(r.Props())
case menus.SubTypeHypertext: case menus.SubTypeHypertext:
@@ -127,6 +132,7 @@ func (d *Driver) maybeBuildInventorySelect(group *menus.Group, records []*menus.
elements := make([]*inventorySelect, len(touched)) elements := make([]*inventorySelect, len(touched))
widget := &Widget{ widget := &Widget{
Locator: group.Locator, Locator: group.Locator,
Active: group.Active,
} }
for i, record := range touched { for i, record := range touched {

View File

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

View File

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

View File

@@ -77,6 +77,7 @@ func (d *Driver) buildListBox(group *menus.Group, up, down, thumb *menus.Record,
// mostly self-registered at the moment. // mostly self-registered at the moment.
widget := &Widget{ widget := &Widget{
Children: []*Widget{upWidget, downWidget}, Children: []*Widget{upWidget, downWidget},
Active: group.Active, // FIXME: children have their own active state
ownPaintables: []paintable{element}, ownPaintables: []paintable{element},
ownValueables: []valueable{element}, ownValueables: []valueable{element},
} }
@@ -156,9 +157,9 @@ func (l *listBox) refresh() {
// FIXME: noninteractive isn't set up for dynamic text yet. Need to // FIXME: noninteractive isn't set up for dynamic text yet. Need to
// generate textImg on demand instead of once at start. // generate textImg on demand instead of once at start.
if ni.label != nil { if ni.label != nil {
ni.label.text = "" ni.label.str = ""
if len(l.strings) > l.offset+i { if len(l.strings) > l.offset+i {
ni.label.text = l.strings[l.offset+i] ni.label.str = l.strings[l.offset+i]
} }
} }
} }

View File

@@ -32,12 +32,13 @@ type noninteractive struct {
hoverImpl hoverImpl
} }
// Paint some text to screen // Paint some text to screen, possibly settable
type label struct { type label struct {
align AlignMode locator string
rect image.Rectangle align AlignMode
text string rect image.Rectangle
font *assetstore.Font font *assetstore.Font
valueImpl
} }
// This particular animation has entry and exit sequences, which are invoked // This particular animation has entry and exit sequences, which are invoked
@@ -67,7 +68,7 @@ func (d *Driver) buildNoninteractive(p *menus.Properties) (*noninteractive, erro
ni := &noninteractive{ ni := &noninteractive{
locator: p.Locator, locator: p.Locator,
frames: animation{sprite.Image}, frames: animation{sprite.Image},
rect: sprite.Rect, rect: sprite.Rect.Add(p.Point()),
} }
return ni, nil return ni, nil
@@ -83,6 +84,7 @@ func (d *Driver) buildStatic(p *menus.Properties) (*noninteractive, *Widget, err
widget := &Widget{ widget := &Widget{
Locator: ni.locator, Locator: ni.locator,
Active: p.Active,
ownClickables: []clickable{ni}, // FIXME: credits background needs to be clickable ownClickables: []clickable{ni}, // FIXME: credits background needs to be clickable
ownHoverables: []hoverable{ni}, ownHoverables: []hoverable{ni},
ownPaintables: []paintable{ni}, ownPaintables: []paintable{ni},
@@ -100,6 +102,7 @@ func (d *Driver) buildHypertext(p *menus.Properties) (*noninteractive, *Widget,
// FIXME: check if this is still needed on the bridge -> briefing transition // FIXME: check if this is still needed on the bridge -> briefing transition
widget := &Widget{ widget := &Widget{
Locator: ni.locator, Locator: ni.locator,
Active: p.Active,
ownClickables: []clickable{ni}, ownClickables: []clickable{ni},
ownHoverables: []hoverable{ni}, ownHoverables: []hoverable{ni},
} }
@@ -107,6 +110,33 @@ func (d *Driver) buildHypertext(p *menus.Properties) (*noninteractive, *Widget,
return ni, widget, nil return ni, widget, nil
} }
func (d *Driver) buildClickText(p *menus.Properties) (*noninteractive, *Widget, error) {
ni, err := d.buildNoninteractive(p)
if err != nil {
return nil, nil, err
}
fnt := d.menu.Font(p.FontType/10 - 1)
// FIXME: is this always right? Seems to make sense for Main.mnu
ni.label = &label{
locator: ni.locator,
font: fnt,
rect: ni.rect, // We will be centered by default
// Starts with no text. The text specified in the menu is hovertext
}
widget := &Widget{
Locator: ni.locator,
Active: p.Active,
ownClickables: []clickable{ni},
ownPaintables: []paintable{ni},
ownValueables: []valueable{ni.label},
}
return ni, widget, nil
}
// An overlay is a static image + some text that needs to be rendered // An overlay is a static image + some text that needs to be rendered
func (d *Driver) buildOverlay(p *menus.Properties) (*noninteractive, *Widget, error) { func (d *Driver) buildOverlay(p *menus.Properties) (*noninteractive, *Widget, error) {
ni, err := d.buildNoninteractive(p) ni, err := d.buildNoninteractive(p)
@@ -116,6 +146,7 @@ func (d *Driver) buildOverlay(p *menus.Properties) (*noninteractive, *Widget, er
widget := &Widget{ widget := &Widget{
Locator: ni.locator, Locator: ni.locator,
Active: p.Active,
ownPaintables: []paintable{ni}, ownPaintables: []paintable{ni},
} }
@@ -124,9 +155,9 @@ func (d *Driver) buildOverlay(p *menus.Properties) (*noninteractive, *Widget, er
fnt := d.menu.Font(p.FontType/10 - 1) fnt := d.menu.Font(p.FontType/10 - 1)
ni.label = &label{ ni.label = &label{
font: fnt, font: fnt,
rect: ni.rect, // We will be centered by default rect: ni.rect, // We will be centered by default
text: p.Text, valueImpl: valueImpl{str: p.Text},
} }
} else { } else {
log.Printf("Overlay without text detected in %v", p.Locator) log.Printf("Overlay without text detected in %v", p.Locator)
@@ -151,10 +182,11 @@ func (d *Driver) buildAnimationSample(p *menus.Properties) (*noninteractive, *Wi
locator: p.Locator, locator: p.Locator,
frames: animation(frames), frames: animation(frames),
hoverImpl: hoverImpl{text: p.Text}, hoverImpl: hoverImpl{text: p.Text},
rect: sprite.Rect, rect: sprite.Rect.Add(p.Point()),
} }
widget := &Widget{ widget := &Widget{
Active: p.Active,
ownHoverables: []hoverable{ani}, ownHoverables: []hoverable{ani},
ownPaintables: []paintable{ani}, ownPaintables: []paintable{ani},
} }
@@ -183,13 +215,14 @@ func (d *Driver) buildAnimationHover(p *menus.Properties) (*animationHover, *Wid
locator: p.Locator, locator: p.Locator,
frames: animation(enterFrames), frames: animation(enterFrames),
hoverImpl: hoverImpl{text: p.Text}, hoverImpl: hoverImpl{text: p.Text},
rect: sprite.Rect, rect: sprite.Rect.Add(p.Point()),
}, },
exitFrames: animation(exitFrames), exitFrames: animation(exitFrames),
} }
widget := &Widget{ widget := &Widget{
Active: p.Active,
ownHoverables: []hoverable{ani}, ownHoverables: []hoverable{ani},
ownPaintables: []paintable{ani}, ownPaintables: []paintable{ani},
} }
@@ -248,6 +281,10 @@ func (a *animationHover) setHoverState(value bool) {
a.hoverImpl.setHoverState(value) a.hoverImpl.setHoverState(value)
} }
func (l *label) id() string {
return l.locator
}
// Top-left of where to start drawing the text. We want it to appear to be in // Top-left of where to start drawing the text. We want it to appear to be in
// the centre of the rect. // the centre of the rect.
// //
@@ -255,7 +292,7 @@ func (a *animationHover) setHoverState(value bool) {
func (l *label) pos() image.Point { func (l *label) pos() image.Point {
pos := l.rect.Min pos := l.rect.Min
textRect := l.font.CalculateBounds(l.text) textRect := l.font.CalculateBounds(l.str)
// Centre the text horizontally // Centre the text horizontally
if l.align == AlignModeCentre { if l.align == AlignModeCentre {
@@ -282,15 +319,21 @@ func (l *label) regions(tick int) []region {
pt := l.pos() pt := l.pos()
for _, r := range l.text { for _, r := range l.str {
glyph, err := l.font.Glyph(r) var sprite *assetstore.Sprite
if err != nil { if glyph, err := l.font.Glyph(r); err != nil {
log.Printf("FIXME: ignoring misssing glyph %v", r) if glyph, err := l.font.Glyph('?'); err != nil {
continue log.Printf("FIXME: ignoring glyph %v", r)
continue
} else {
sprite = glyph
}
} else {
sprite = glyph
} }
out = append(out, oneRegion(pt, glyph.Image)...) out = append(out, oneRegion(pt, sprite.Image)...)
pt.X += glyph.Rect.Dx() pt.X += sprite.Rect.Dx()
} }
return out return out

View File

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

View File

@@ -61,6 +61,26 @@ func (d *Driver) SetFreeze(id string, value bool) error {
return fmt.Errorf("Couldn't find clickable widget %v:%v", d.menu.Name, id) 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 { func (d *Driver) OnClick(id string, f func()) error {
for _, clickable := range d.allClickables() { for _, clickable := range d.allClickables() {
if clickable.id() == d.realId(id) { if clickable.id() == d.realId(id) {

View File

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

View File

@@ -8,9 +8,9 @@ import (
"runtime/debug" "runtime/debug"
"runtime/pprof" "runtime/pprof"
"github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/ebitenutil" "github.com/hajimehoshi/ebiten/v2/ebitenutil"
"github.com/hajimehoshi/ebiten/inpututil" "github.com/hajimehoshi/ebiten/v2/inpututil"
) )
type Game interface { type Game interface {
@@ -37,6 +37,8 @@ type Window struct {
MouseWheelHandler func(float64, float64) MouseWheelHandler func(float64, float64)
MouseClickHandler func() MouseClickHandler func()
WhileKeyDownHandlers map[ebiten.Key]func()
// Allow the "game" to be switched out at any time // Allow the "game" to be switched out at any time
game Game game Game
@@ -51,16 +53,17 @@ type Window struct {
// //
// ebiten assumes a single window, so only call this once... // ebiten assumes a single window, so only call this once...
func NewWindow(game Game, title string, xRes int, yRes int) (*Window, error) { func NewWindow(game Game, title string, xRes int, yRes int) (*Window, error) {
ebiten.SetRunnableInBackground(true)
return &Window{ 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()), KeyUpHandlers: make(map[ebiten.Key]func()),
debug: true,
firstRun: true,
game: game,
xRes: xRes,
yRes: yRes,
}, nil }, nil
} }
@@ -69,6 +72,10 @@ func (w *Window) OnKeyUp(key ebiten.Key, f func()) {
w.KeyUpHandlers[key] = f 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)) { func (w *Window) OnMouseWheel(f func(x, y float64)) {
w.MouseWheelHandler = f w.MouseWheelHandler = f
} }
@@ -100,10 +107,12 @@ func (w *Window) drawCursor(screen *ebiten.Image) error {
ebiten.SetCursorMode(ebiten.CursorModeHidden) ebiten.SetCursorMode(ebiten.CursorModeHidden)
return screen.DrawImage(cursor, op) screen.DrawImage(cursor, op)
return nil
} }
func (w *Window) Update(screen *ebiten.Image) (outErr error) { func (w *Window) Update() (outErr error) {
// Ebiten does not like it if we panic inside its main loop // Ebiten does not like it if we panic inside its main loop
defer func() { defer func() {
if panicErr := recover(); panicErr != nil { if panicErr := recover(); panicErr != nil {
@@ -115,11 +124,13 @@ func (w *Window) Update(screen *ebiten.Image) (outErr error) {
} }
}() }()
if err := w.game.Update(screen.Size()); err != nil { // FIXME: remove need for update generally
if err := w.game.Update(w.xRes, w.yRes); err != nil {
return err return err
} }
// Process keys // Process keys.
// FIXME: : should this happen before or after update?
// TODO: efficient set operations // TODO: efficient set operations
for key, cb := range w.KeyUpHandlers { for key, cb := range w.KeyUpHandlers {
@@ -128,6 +139,12 @@ func (w *Window) Update(screen *ebiten.Image) (outErr error) {
} }
} }
for key, cb := range w.WhileKeyDownHandlers {
if ebiten.IsKeyPressed(key) {
cb()
}
}
if w.MouseWheelHandler != nil { if w.MouseWheelHandler != nil {
x, y := ebiten.Wheel() x, y := ebiten.Wheel()
if x != 0 || y != 0 { if x != 0 || y != 0 {
@@ -141,13 +158,11 @@ func (w *Window) Update(screen *ebiten.Image) (outErr error) {
} }
} }
if ebiten.IsDrawingSkipped() { return nil
return nil }
}
if err := w.game.Draw(screen); err != nil { func (w *Window) Draw(screen *ebiten.Image) {
return err w.game.Draw(screen)
}
if w.debug { if w.debug {
// Draw FPS, etc, to the screen // Draw FPS, etc, to the screen
@@ -156,7 +171,7 @@ func (w *Window) Update(screen *ebiten.Image) (outErr error) {
} }
// Draw the cursor last // Draw the cursor last
return w.drawCursor(screen) w.drawCursor(screen)
} }
// TODO: a stop or other cancellation mechanism // TODO: a stop or other cancellation mechanism

View File

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

View File

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

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

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

View File

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

Binary file not shown.

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

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