Compare commits

...

33 Commits

Author SHA1 Message Date
494fe4eb02 Trim map based on trailer data 2020-06-08 00:53:45 +01:00
30d1786e64 Get SaW maps displaying 2020-06-08 00:48:33 +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
6e70ddcb60 Fix some errors in iso->pix->iso conversions, add debugging
Maps now render a bit more properly, and our mouse position can be
correctly assigned to a cell. Kind of, mostly.
2020-04-18 00:12:15 +01:00
1e141a2fb9 More-efficient scenario draw call 2020-04-17 22:45:02 +01:00
59 changed files with 2180 additions and 1285 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

184
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
@@ -110,31 +184,3 @@ $ ./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
"Mission Setup" includes information about available squad types
From EquipDef.cpp Dumo: CEquipment we learn the following object types:
0. DELETED
1. WEAPON
2. GRENADE
3. MEDIPACK
4. SCANNER
5. GENESEED
6. CLIP
7. DOOR KEY
8. DOOR KEY
9. DOOR KEY
10. DOOR KEY
And we learn they can be "on"....
0. CHARACTER
1. VEHICLE
2. CANISTER
I'm starting to see some parallels with [this](https://github.com/shlainn/game-file-formats/wiki/)
in the data formats, and the timeline (1997) seems about right. Worth keeping an
eye on!

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)
@@ -127,20 +140,20 @@ func loadMapsFrom(part string) {
log.Printf("Maps in %s:", mapsPath) log.Printf("Maps in %s:", mapsPath)
for key, gameMap := range gameMaps { for key, gameMap := range gameMaps {
rect := gameMap.Rect()
hdr := gameMap.Header hdr := gameMap.Header
fmt.Printf( fmt.Printf(
" * `%s`: IsCampaignMap=%v W=%v:%v L=%v:%v SetName=%s\n", " * `%s`: IsCampaignMap=%v W=%v:%v L=%v:%v SetName=%s\n",
key, key,
hdr.IsCampaignMap, hdr.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(hdr.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 +169,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 +199,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 +212,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)

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

@@ -11,11 +11,14 @@ import (
"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{

View File

@@ -10,11 +10,14 @@ import (
"github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten"
"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)
} }

View File

@@ -3,18 +3,22 @@ package main
import ( import (
"flag" "flag"
"log" "log"
"math"
"os" "os"
"github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten"
"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/scenario" "code.ur.gs/lupine/ordoor/internal/scenario"
"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")
@@ -27,14 +31,19 @@ type env struct {
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)
@@ -51,17 +60,19 @@ func main() {
log.Fatal("Couldn't create window: %v", err) log.Fatal("Couldn't create window: %v", err)
} }
// TODO: click to view cell data step := 32
win.WhileKeyDown(ebiten.KeyLeft, env.changeOrigin(-step, +0))
win.OnKeyUp(ebiten.KeyLeft, env.changeOrigin(-64, +0)) win.WhileKeyDown(ebiten.KeyRight, env.changeOrigin(+step, +0))
win.OnKeyUp(ebiten.KeyRight, env.changeOrigin(+64, +0)) win.WhileKeyDown(ebiten.KeyUp, env.changeOrigin(+0, -step))
win.OnKeyUp(ebiten.KeyUp, env.changeOrigin(+0, -64)) win.WhileKeyDown(ebiten.KeyDown, env.changeOrigin(+0, +step))
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), env.setZIdx(i))
} }
win.OnMouseClick(env.showCellData)
win.OnMouseWheel(env.changeZoom)
if err := win.Run(); err != nil { if err := win.Run(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@@ -82,8 +93,23 @@ 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
} }
} }
func (e *env) showCellData() {
screenX, screenY := ebiten.CursorPosition()
viewX, viewY := e.scenario.Viewpoint.X+screenX, e.scenario.Viewpoint.Y+screenY
log.Printf("Click registered at (%d,%d) screen, (%d,%d) virtual", screenX, screenY, viewX, viewY)
cell, pos := e.scenario.CellAtCursor()
log.Printf("Viewpoint: %#+v z=%v", e.scenario.Viewpoint, e.scenario.ZIdx)
log.Printf("Cell under cursor: (%.2f,%.2f,%d): %#+v", pos.X, pos.Y, pos.Z, cell)
}

View File

@@ -8,11 +8,14 @@ import (
"github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten"
"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)
} }

View File

@@ -172,18 +172,15 @@ 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, err := ebiten.NewImage(rect.Dx(), rect.Dy(), ebiten.FilterDefault)
int(gameMap.MaxLength),
ebiten.FilterDefault,
)
if err != nil { if err != nil {
return err return err
} }
for y := int(gameMap.MinLength); y < int(gameMap.MaxLength); y++ { for y := int(rect.Min.Y); y < int(rect.Max.Y); y++ {
for x := int(gameMap.MinWidth); x < int(gameMap.MaxWidth); x++ { for x := int(rect.Min.X); x < int(rect.Max.X); x++ {
cell := gameMap.Cells.At(x, y, int(e.state.zIdx)) cell := gameMap.Cells.At(x, y, int(e.state.zIdx))
imd.Set(x, y, makeColour(&cell, e.state.cellIdx)) imd.Set(x, y, makeColour(&cell, e.state.cellIdx))
} }

View File

@@ -10,14 +10,17 @@ import (
"github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten"
"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

View File

@@ -10,12 +10,15 @@ import (
"github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten"
"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)
} }

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,210 @@ 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 | 4 | Number of thingies (26 vs 1) |
| 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.
It's hard to say where the alignment should be at this point. We need to compare
more characters with each other. Other notes...
Characters are organised into Squads somehow.
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?
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 |
## 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.

6
go.mod
View File

@@ -1,14 +1,16 @@
module code.ur.gs/lupine/ordoor module code.ur.gs/lupine/ordoor
go 1.12 go 1.14
require ( require (
github.com/BurntSushi/toml v0.3.1 github.com/BurntSushi/toml v0.3.1
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 v1.11.1
github.com/jfreymuth/oggvorbis v1.0.1 // indirect github.com/jfreymuth/oggvorbis v1.0.1 // indirect
github.com/kr/text v0.2.0 // indirect github.com/kr/text v0.2.0 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/samuel/go-pcx v0.0.0-20180426214139-d9c017170db4
github.com/stretchr/testify v1.5.1
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5 // indirect golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5 // indirect
golang.org/x/image v0.0.0-20200119044424-58c23975cae1 golang.org/x/image v0.0.0-20200119044424-58c23975cae1
golang.org/x/mobile v0.0.0-20200329125638-4c31acba0007 // indirect golang.org/x/mobile v0.0.0-20200329125638-4c31acba0007 // indirect

14
go.sum
View File

@@ -3,6 +3,8 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72 h1:b+9H1GAsx5RsjvDFLoS5zkNBzIQMuVKUYQDmxU3N5XE=
@@ -17,6 +19,8 @@ github.com/hajimehoshi/ebiten v1.11.0-alpha.2.0.20200101150127-38815ba801a5 h1:h
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-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 h1:+pIxfzfVgRbHGM7wBAJtgzPiWiZopA7lyIKNQqc9amk=
github.com/hajimehoshi/ebiten v1.11.0/go.mod h1:aDEhx0K9gSpXw3Cxf2hCXDxPSoF8vgjNqKxrZa/B4Dg= github.com/hajimehoshi/ebiten v1.11.0/go.mod h1:aDEhx0K9gSpXw3Cxf2hCXDxPSoF8vgjNqKxrZa/B4Dg=
github.com/hajimehoshi/ebiten v1.11.1 h1:7gy2bHBDNtfTh3GlcUAilk3lNWW9fTLaP7iZAodS9F8=
github.com/hajimehoshi/ebiten v1.11.1/go.mod h1:aDEhx0K9gSpXw3Cxf2hCXDxPSoF8vgjNqKxrZa/B4Dg=
github.com/hajimehoshi/go-mp3 v0.2.1 h1:DH4ns3cPv39n3cs8MPcAlWqPeAwLCK8iNgqvg0QBWI8= github.com/hajimehoshi/go-mp3 v0.2.1 h1:DH4ns3cPv39n3cs8MPcAlWqPeAwLCK8iNgqvg0QBWI8=
github.com/hajimehoshi/go-mp3 v0.2.1/go.mod h1:Rr+2P46iH6PwTPVgSsEwBkon0CK5DxCAeX/Rp65DCTE= 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.3.4/go.mod h1:PgjqsBJff0efqL2nlMJidJgVJywLn6M4y8PI4TfeWfA=
@@ -39,6 +43,13 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/samuel/go-pcx v0.0.0-20180426214139-d9c017170db4 h1:Y/KOCu+ZLB730PudefxfsKVjtI0m0RhvFk9a0l4O1+c=
github.com/samuel/go-pcx v0.0.0-20180426214139-d9c017170db4/go.mod h1:qxuIawynlRhuaHowuXvd1xjyFWx87Ro4gkZlKRXtHnQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -91,7 +102,10 @@ golang.org/x/tools v0.0.0-20200117220505-0cba7a3a9ee9/go.mod h1:TB2adYChydJhpapK
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/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-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/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 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

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"
"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" }, ...}
@@ -41,6 +46,7 @@ type AssetStore struct {
fonts map[string]*Font fonts map[string]*Font
generic *data.Generic generic *data.Generic
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 +56,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 +86,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)
@@ -96,6 +112,7 @@ func (a *AssetStore) Refresh() error {
a.entries = newEntryMap a.entries = newEntryMap
a.fonts = make(map[string]*Font) a.fonts = make(map[string]*Font)
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 +135,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

@@ -0,0 +1,42 @@
package assetstore
import (
"image"
"os"
"github.com/hajimehoshi/ebiten"
_ "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, err := ebiten.NewImageFromImage(rawImg, ebiten.FilterDefault)
if err != nil {
return nil, err
}
a.images[name] = img
return img, nil
}

View File

@@ -1,6 +1,7 @@
package assetstore package assetstore
import ( import (
"fmt"
"image" "image"
"log" "log"
@@ -46,12 +47,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 +60,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
@@ -77,6 +73,11 @@ func (m *Map) LoadSprites() error {
return nil return nil
} }
// FIXME: get rid of this
func (m *Map) Cell(x, y, z int) maps.Cell {
return m.raw.Cells.At(x, y, z)
}
// SpritesForCell returns the sprites needed to correctly render this cell. // SpritesForCell returns the sprites needed to correctly render this cell.
// They should be rendered from first to last to get the correct ordering // They should be rendered from first to last to get the correct ordering
func (m *Map) SpritesForCell(x, y, z int) ([]*Sprite, error) { func (m *Map) SpritesForCell(x, y, z int) ([]*Sprite, error) {
@@ -90,7 +91,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())

View File

@@ -1,6 +1,8 @@
package assetstore package assetstore
import ( import (
"log"
"github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten"
"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

@@ -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,7 +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, err := ebiten.NewImageFromImage(raw.ToImage(o.assets.Palette), ebiten.FilterDefault)
if err != nil { if err != nil {
return nil, err return nil, err
} }

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

@@ -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

@@ -92,25 +92,29 @@ 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) {
menu, err := assets.Menu(string(name))
if err != nil {
return nil, err
}
driver, err := ui.NewDriver(assets, menu)
if err != nil {
return nil, err
}
return driver, nil
}
func (f *Flow) Update(screenX, screenY int) error { func (f *Flow) Update(screenX, screenY int) error {
if f.exit != nil { if f.exit != nil {
return f.exit return f.exit
} }
// 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 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 +245,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 +283,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

@@ -4,52 +4,153 @@ package flow
// 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 buttont
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
}))
} }

View File

@@ -5,6 +5,7 @@ import (
"compress/gzip" "compress/gzip"
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"image"
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
@@ -14,8 +15,8 @@ import (
) )
var ( var (
expectedMagic = []byte("\x08\x00WHMAP\x00") expectedMagic = []byte("\x15\x00AMB_MAP\x00")
expectedSetNameOffset = uint32(0x34) expectedSetNameOffset = uint32(0x10)
notImplemented = fmt.Errorf("Not implemented") notImplemented = fmt.Errorf("Not implemented")
) )
@@ -24,41 +25,53 @@ const (
MaxLength = 100 // Y coordinate MaxLength = 100 // Y coordinate
MaxWidth = 130 // X coordinate MaxWidth = 130 // X coordinate
CellSize = 16 // seems to be CellSize = 13 // seems to be
cellDataOffset = 0x110 // tentatively cellDataOffset = 0xc0
cellCount = MaxHeight * MaxLength * MaxWidth cellCount = MaxHeight * MaxLength * MaxWidth
) )
type Header struct { type Header struct {
IsCampaignMap uint32 // Tentatively: 0 = no, 1 = yes Magic [10]byte // "\x15\x00AMB_MAP\x00"
MinWidth uint32 SetName [8]byte // Links to a filename in `/Sets/*.set`
MinLength uint32
MaxWidth uint32
MaxLength uint32
Unknown1 uint32
Unknown2 uint32
Unknown3 uint32
Unknown4 uint32
Magic [8]byte // "\x08\x00WHMAP\x00"
Unknown5 uint32
Unknown6 uint32
SetName [8]byte // Links to a filename in `/Sets/*.set`
// Need to investigate the rest of the header too // Need to investigate the rest of the header too
IsCampaignMap byte
} }
func (h Header) Width() int { type TrailerHeader struct {
return int(h.MaxWidth - h.MinWidth) Discard1 [3]byte // No idea what this lot is
MaxWidth uint32
MaxLength uint32
MinWidth uint32
MinLength uint32
NumCharacters uint32
Unknown1 uint32
Unknown2 uint16
Unknown3 uint16
Unknown4 uint32
NumThingies uint32
Padding1 [20]byte
} }
func (h Header) Length() int { type TrailerTrailer struct {
return int(h.MaxLength - h.MinLength) Title [255]byte
Briefing [2048]byte
Unknown1 [85]uint8 // Maybe? each contains either 0 or 1? Hard to say
} }
func (h Header) Height() int { type Character struct {
return MaxHeight Unknown1 uint32
} }
type Characters []Character
// TODO. These are triggers/reactors/etc.
type Thingy struct {}
type Thingies []Thingy
func (h Header) MapSetName() string { func (h Header) MapSetName() string {
idx := bytes.IndexByte(h.SetName[:], 0) idx := bytes.IndexByte(h.SetName[:], 0)
if idx < 0 { if idx < 0 {
@@ -79,7 +92,7 @@ type ObjRef struct {
// 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 & 0x7f)
} }
func (o ObjRef) Sprite() int { func (o ObjRef) Sprite() int {
@@ -90,12 +103,13 @@ func (o ObjRef) Sprite() int {
// The top bit seems to say whether we should draw or not. // The top bit seems to say whether we should draw or not.
func (o ObjRef) IsActive() bool { func (o ObjRef) IsActive() bool {
return (o.SpriteAndFlagByte & 0x80) == 0x80 return (o.SpriteAndFlagByte & 0x80) == 0x80
} } // PARIS is 78 x 60 x 7
// 4E 3C 7
/*
type Cell struct { type Cell struct {
DoorAndCanisterRelated byte DoorAndCanisterRelated byte
DoorLockAndReactorRelated byte // DoorLockAndReactorRelated byte
Unknown2 byte // Unknown2 byte
Surface ObjRef Surface ObjRef
Left ObjRef Left ObjRef
Right ObjRef Right ObjRef
@@ -104,43 +118,60 @@ type Cell struct {
Unknown12 byte Unknown12 byte
Unknown13 byte Unknown13 byte
Unknown14 byte Unknown14 byte
SquadRelated byte // SquadRelated byte
}*/
type Cell struct {
Unknown1 byte
Surface ObjRef
Left ObjRef
Right ObjRef
Center ObjRef
Unknown2 [4]byte
/*
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:
return c.DoorAndCanisterRelated return c.Unknown1
case 1: case 1:
return c.DoorLockAndReactorRelated
case 2:
return c.Unknown2
case 3:
return c.Surface.AreaByte return c.Surface.AreaByte
case 4: case 2:
return c.Surface.SpriteAndFlagByte return c.Surface.SpriteAndFlagByte
case 5: case 3:
return c.Left.AreaByte return c.Left.AreaByte
case 6: case 4:
return c.Left.SpriteAndFlagByte return c.Left.SpriteAndFlagByte
case 7: case 5:
return c.Right.AreaByte return c.Right.AreaByte
case 8: case 6:
return c.Right.SpriteAndFlagByte return c.Right.SpriteAndFlagByte
case 9: case 7:
return c.Center.AreaByte return c.Center.AreaByte
case 10: case 8:
return c.Center.SpriteAndFlagByte return c.Center.SpriteAndFlagByte
case 9:
return c.Unknown2[0]
case 10:
return c.Unknown2[1]
case 11: case 11:
return c.Unknown11 return c.Unknown2[2]
case 12: case 12:
return c.Unknown12 return c.Unknown2[3]
case 13:
return c.Unknown13
case 14:
return c.Unknown14
case 15:
return c.SquadRelated
} }
return 0 return 0
@@ -149,15 +180,23 @@ func (c *Cell) At(n int) byte {
// Cells is always a fixed size; use At to get a cell according to x,y,z // Cells is always a fixed size; use At to get a cell according to x,y,z
type Cells []Cell type Cells []Cell
// 6 Possibilities for being laid out in memory. Most likely:
// XXYYZZ
// OR
// XYZXYZ
func (c Cells) At(x, y, z int) Cell { func (c Cells) At(x, y, z int) Cell {
return c[(z*MaxLength*MaxWidth)+(y*MaxWidth)+x] // log.Printf("At (%v,%v,%v)=%v", x, y, z, x*y*z)
return c[(z*MaxLength*MaxWidth)+
(y*MaxWidth)+
x]
} }
func (h Header) Check() []error { func (h Header) Check() []error {
var out []error var out []error
if h.IsCampaignMap > 1 { // if h.IsCampaignMap > 1 {
out = append(out, fmt.Errorf("Expected 0 or 1 for IsCampaignMap, got %v", h.IsCampaignMap)) // out = append(out, fmt.Errorf("Expected 0 or 1 for IsCampaignMap, got %v", h.IsCampaignMap))
} // }
if bytes.Compare(expectedMagic, h.Magic[:]) != 0 { if bytes.Compare(expectedMagic, h.Magic[:]) != 0 {
out = append(out, fmt.Errorf("Unexpected magic value: %v", h.Magic)) out = append(out, fmt.Errorf("Unexpected magic value: %v", h.Magic))
@@ -169,10 +208,26 @@ func (h Header) Check() []error {
type GameMap struct { type GameMap struct {
Header Header
Cells Cells
// TODO: parse this into sections TrailerHeader
Characters
Thingies
TrailerTrailer
// TODO: parse this into sections. This is the text that comes from the
// .TXT file. Maybe we don't need it at all since it should be duplicated in
// the trailer.
Text string Text string
} }
func (m *GameMap) Rect() image.Rectangle {
return image.Rect(
int(m.TrailerHeader.MinWidth),
int(m.TrailerHeader.MinLength),
int(m.TrailerHeader.MaxWidth-1),
int(m.TrailerHeader.MaxLength-1),
)
}
// 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,
// just pass the directory + basename to load both // just pass the directory + basename to load both
func LoadGameMap(prefix string) (*GameMap, error) { func LoadGameMap(prefix string) (*GameMap, error) {
@@ -276,5 +331,42 @@ func loadMapFile(filename string) (*GameMap, error) {
return nil, fmt.Errorf("Error parsing cells for %s: %v", filename, err) return nil, fmt.Errorf("Error parsing cells for %s: %v", filename, err)
} }
// no gzip.SeekReader, so discard unread trailer bytes for now
if _, err := io.CopyN(ioutil.Discard, zr, int64(3320-2)); err != nil { // observed
return nil, err
}
if err := binary.Read(zr, binary.LittleEndian, &out.TrailerHeader); err != nil {
return nil, fmt.Errorf("Error parsing trailer header for %s: %v", filename, err)
}
log.Printf("Trailer Header: %#+v", out.TrailerHeader)
/*
// TODO: until we know how large each character record should be, we can't read this lot
out.Characters = make(Characters, int(out.TrailerHeader.NumCharacters))
if err := binary.Read(zr, binary.LittleEndian, &out.Characters); err != nil {
return nil, fmt.Errorf("Error parsing characters for %s: %v", filename, err)
}
out.Thingies = make(Thingies, int(out.TrailerHeader.NumThingies))
if err := binary.Read(zr, binary.LittleEndian, &out.Thingies); err != nil {
return nil, fmt.Errorf("Error parsing thingies for %s: %v", filename, err)
}
if err := binary.Read(zr, binary.LittleEndian, &out.TrailerTrailer); err != nil {
return nil, fmt.Errorf("Error parsing trailer trailer for %s: %v", filename, err)
}
log.Printf("Trailer Trailer: %s", out.TrailerTrailer.String())
*/
return &out, nil return &out, nil
} }
func (t *TrailerTrailer) String() string {
return fmt.Sprintf(
"title=%q briefing=%q rest=%#+v",
strings.TrimRight(string(t.Title[:]), "\x00"),
strings.TrimRight(string(t.Briefing[:]), "\x00"),
t.Unknown1,
)
}

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"
) )
@@ -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,6 +7,7 @@ package ordoor
import ( import (
"fmt" "fmt"
"log" "log"
"time"
"github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/ebiten/audio" "github.com/hajimehoshi/ebiten/audio"
@@ -29,17 +30,21 @@ type Ordoor struct {
// Relevant to interface state // Relevant to interface state
flow *flow.Flow flow *flow.Flow
// 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)
} }
@@ -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,24 @@ func (o *Ordoor) Update(screenX, screenY int) error {
} }
func (o *Ordoor) Draw(screen *ebiten.Image) error { func (o *Ordoor) Draw(screen *ebiten.Image) error {
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)
return screen.DrawImage(pic, do)
}
return o.flow.Draw(screen) return o.flow.Draw(screen)
} }
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

@@ -1,15 +1,43 @@
package scenario package scenario
import ( import (
"fmt"
"image" "image"
"sort" "sort"
"github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/ebiten/ebitenutil"
) )
type CartPt struct {
X float64
Y float64
}
type IsoPt struct {
X float64
Y float64
}
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.selectedCell = screenPos.ToISO()
return nil return nil
} }
@@ -18,30 +46,34 @@ 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 := pixToCell(s.Viewpoint) 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 := pixToCell(image.Pt(s.Viewpoint.X+sw, s.Viewpoint.Y+sh)) 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
// However, the drawing order is odd unless we reorder explicitly. // However, the drawing order is odd unless we reorder explicitly.
toDraw := []image.Point{} toDraw := []IsoPt{}
for a := topLeft.X + topLeft.Y; a <= bottomRight.X+bottomRight.Y; a++ { for a := int(topLeft.X + topLeft.Y); a <= int(bottomRight.X+bottomRight.Y); a++ {
for b := topLeft.X - topLeft.Y; b <= bottomRight.X-bottomRight.Y; b++ { for b := int(topLeft.X - topLeft.Y); b <= int(bottomRight.X-bottomRight.Y); b++ {
if b&1 != a&1 { if b&1 != a&1 {
continue continue
} }
pt := image.Pt((a+b)/2, (a-b)/2) pt := IsoPt{X: float64((a + b) / 2), Y: float64((a - b) / 2)}
ipt := image.Pt(int(pt.X), int(pt.Y))
if !pt.In(s.area.Rect) { if !ipt.In(s.area.Rect) {
continue continue
} }
toDraw = append(toDraw, pt) toDraw = append(toDraw, pt)
@@ -49,8 +81,8 @@ func (s *Scenario) Draw(screen *ebiten.Image) error {
} }
sort.Slice(toDraw, func(i, j int) bool { sort.Slice(toDraw, func(i, j int) bool {
iPix := cellToPix(toDraw[i]) iPix := toDraw[i].ToCart()
jPix := cellToPix(toDraw[j]) jPix := toDraw[j].ToCart()
if iPix.Y < jPix.Y { if iPix.Y < jPix.Y {
return true return true
@@ -66,7 +98,7 @@ func (s *Scenario) Draw(screen *ebiten.Image) error {
counter := map[string]int{} counter := map[string]int{}
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.X, pt.Y, z, screen, counter); err != nil { if err := s.renderCell(int(pt.X), int(pt.Y), z, screen, counter); err != nil {
return err return err
} }
} }
@@ -74,61 +106,116 @@ func (s *Scenario) Draw(screen *ebiten.Image) error {
//log.Printf("%#+v", counter) //log.Printf("%#+v", counter)
// Finally, draw cursor chrome
// FIXME: it looks like we might need to do this in normal painting order...
spr, err := s.specials.Sprite(0)
if err != nil {
return err
}
op := ebiten.DrawImageOptions{}
geo := s.geoForCoords(int(s.selectedCell.X), int(s.selectedCell.Y), 0)
op.GeoM = geo
op.GeoM.Translate(-209, -332)
op.GeoM.Translate(float64(spr.Rect.Min.X), float64(spr.Rect.Min.Y))
op.GeoM.Scale(s.Zoom, s.Zoom)
if err := screen.DrawImage(spr.Image, &op); err != nil {
return err
}
x1, y1 := geo.Apply(0, 0)
ebitenutil.DebugPrintAt(
screen,
fmt.Sprintf("(%d,%d)", int(s.selectedCell.X), int(s.selectedCell.Y)),
int(x1),
int(y1),
)
/*
// 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 return nil
} }
func (s *Scenario) geoForCam() ebiten.GeoM {
geo := ebiten.GeoM{}
geo.Translate(-float64(s.Viewpoint.X), -float64(s.Viewpoint.Y))
return geo
}
func (s *Scenario) geoForCoords(x, y, z int) ebiten.GeoM {
geo := s.geoForCam()
pix := IsoPt{X: float64(x), Y: float64(y)}.ToCart()
geo.Translate(pix.X, pix.Y)
// Taking the Z index away *seems* to draw the object in the correct place.
// FIXME: There are some artifacts, investigate more
geo.Translate(0.0, -float64(z*48.0)) // offset for Z index
return geo
}
func (s *Scenario) renderCell(x, y, z int, screen *ebiten.Image, counter map[string]int) error { func (s *Scenario) renderCell(x, y, z int, screen *ebiten.Image, counter map[string]int) error {
sprites, err := s.area.SpritesForCell(x, y, z) sprites, err := s.area.SpritesForCell(x, y, z)
if err != nil { if err != nil {
return err return err
} }
iso := ebiten.GeoM{}
iso.Translate(-float64(s.Viewpoint.X), -float64(s.Viewpoint.Y))
pix := cellToPix(image.Pt(x, y))
iso.Translate(float64(pix.X), float64(pix.Y))
// Taking the Z index away *seems* to draw the object in the correct place.
// FIXME: There are some artifacts, investigate more
iso.Translate(0.0, -float64(z*48.0)) // offset for Z index
// TODO: iso.Scale(e.state.zoom, e.state.zoom) // apply current zoom factor // 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 { // if _, ok := counter[spr.ID]; !ok {
// counter[spr.ID] = 0 // counter[spr.ID] = 0
// } // }
// counter[spr.ID] = counter[spr.ID] + 1 // counter[spr.ID] = counter[spr.ID] + 1
op := ebiten.DrawImageOptions{GeoM: iso}
iso.Translate(float64(spr.XOffset), float64(spr.YOffset)) op.GeoM.Translate(float64(spr.Rect.Min.X), float64(spr.Rect.Min.Y))
if err := screen.DrawImage(spr.Image, &ebiten.DrawImageOptions{GeoM: iso}); err != nil { // Zoom has to come last
op.GeoM.Scale(s.Zoom, s.Zoom)
if err := screen.DrawImage(spr.Image, &op); err != nil {
return err return err
} }
iso.Translate(float64(-spr.XOffset), float64(-spr.YOffset))
} }
return nil return nil
} }
const ( const (
cellWidth = 64 cellWidth = 128.0
cellHeight = 64 cellHeight = 63.0
cellWidthHalf = cellWidth / 2.0
cellHeightHalf = cellHeight / 2.0
) )
// Doesn't take the camera or Z level into account func (p CartPt) ToISO() IsoPt {
func cellToPix(pt image.Point) image.Point { return IsoPt{
return image.Pt( X: (p.Y / cellHeight) + (p.X / cellWidth),
(pt.X-pt.Y)*cellWidth, Y: (p.Y / cellHeight) - (p.X / cellWidth),
(pt.X+pt.Y)*cellHeight/2, }
)
} }
// Doesn't take the camera or Z level into account func (p IsoPt) ToCart() CartPt {
func pixToCell(pt image.Point) image.Point { return CartPt{
return image.Pt( X: (p.X - p.Y) * cellWidthHalf,
pt.Y/cellHeight+pt.X/(cellWidth*2), Y: (p.X + p.Y) * cellHeightHalf,
pt.Y/cellHeight-pt.X/(cellWidth*2), }
)
} }

View File

@@ -0,0 +1,21 @@
package scenario
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestCARTtoISOtoCART(t *testing.T) {
for i := 0; i < 120; i++ {
for j := 0; j < 120; j++ {
orig := IsoPt{X: float64(i), Y: float64(j)}
asPix := orig.ToCart()
andBack := asPix.ToISO()
t.Logf("%v,%v: asPix = %v", i, j, asPix)
require.Equal(t, orig, andBack)
}
}
}

View File

@@ -0,0 +1,28 @@
package scenario
import (
"code.ur.gs/lupine/ordoor/internal/maps"
)
type CellPoint struct {
IsoPt
Z int
}
func (s *Scenario) CellAtCursor() (maps.Cell, CellPoint) {
cell := s.area.Cell(int(s.selectedCell.X), int(s.selectedCell.Y), 0)
return cell, CellPoint{IsoPt: s.selectedCell, Z: 0}
}
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

@@ -9,16 +9,19 @@ import (
) )
type Scenario struct { type Scenario struct {
area *assetstore.Map area *assetstore.Map
specials *assetstore.Object
tick int tick int
turn int turn int
selectedCell IsoPt
// 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,6 +30,11 @@ func NewScenario(assets *assetstore.AssetStore, name string) (*Scenario, error)
return nil, err return nil, err
} }
specials, err := assets.Object("specials") // FIXME: should this be hardcoded?
if err != nil {
return nil, err
}
// Eager load sprites. TODO: do we really want to do this? // Eager load sprites. TODO: do we really want to do this?
if err := area.LoadSprites(); err != nil { if err := area.LoadSprites(); err != nil {
return nil, fmt.Errorf("Eager-loading sprites failed: %v", err) return nil, fmt.Errorf("Eager-loading sprites failed: %v", err)
@@ -34,7 +42,9 @@ func NewScenario(assets *assetstore.AssetStore, name string) (*Scenario, error)
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

@@ -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

@@ -169,11 +169,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 +182,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 +196,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 +208,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 +221,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 +234,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 +249,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 {
@@ -127,6 +130,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

@@ -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},
} }

View File

@@ -67,7 +67,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 +83,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 +101,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},
} }
@@ -116,6 +118,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},
} }
@@ -151,10 +154,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 +187,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},
} }

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

@@ -35,6 +35,9 @@ type Window struct {
Title string Title string
KeyUpHandlers map[ebiten.Key]func() KeyUpHandlers map[ebiten.Key]func()
MouseWheelHandler func(float64, float64) MouseWheelHandler func(float64, float64)
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
@@ -53,13 +56,16 @@ func NewWindow(game Game, title string, xRes int, yRes int) (*Window, error) {
ebiten.SetRunnableInBackground(true) 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
} }
@@ -68,10 +74,18 @@ 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
} }
func (w *Window) OnMouseClick(f func()) {
w.MouseClickHandler = f
}
func (w *Window) Layout(_, _ int) (int, int) { func (w *Window) Layout(_, _ int) (int, int) {
return w.xRes, w.yRes return w.xRes, w.yRes
} }
@@ -114,7 +128,8 @@ func (w *Window) Update(screen *ebiten.Image) (outErr error) {
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 {
@@ -123,6 +138,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 {
@@ -130,6 +151,12 @@ func (w *Window) Update(screen *ebiten.Image) (outErr error) {
} }
} }
if w.MouseClickHandler != nil {
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
w.MouseClickHandler()
}
}
if ebiten.IsDrawingSkipped() { if ebiten.IsDrawingSkipped() {
return nil return nil
} }

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