Compare commits
45 Commits
903ddba2ac
...
main
Author | SHA1 | Date | |
---|---|---|---|
55c2232e08 | |||
ac4675fa2c | |||
5f7654d267 | |||
89888ce004 | |||
16767da6f1 | |||
c5b80ed8bc | |||
891edecc60 | |||
85979834c8 | |||
96dbb297cd | |||
92fa0fc5d6 | |||
c5e6abb798 | |||
5df050b4ef | |||
4d336b9189 | |||
3b7cfb6ecc | |||
7677c30572 | |||
eac6017c2c | |||
f971ba320c | |||
cf624cc77b | |||
65bae80d40 | |||
e8e9811b5d | |||
a6fdbaef2b | |||
0bf8233cd1 | |||
c2cbf1d95d | |||
54fe95239e | |||
63d3ee0ed6 | |||
5c869fc33c | |||
4358951e15 | |||
250a6033c8 | |||
f64af717b7 | |||
3866ee07a8 | |||
c1268e8d57 | |||
59baf20c35 | |||
cf58be6a20 | |||
14fdab72a0 | |||
c7a2fa80e7 | |||
def40a1ee2 | |||
48d098134e | |||
597e346869 | |||
eea5dea98a | |||
04bdf3e352 | |||
9d0750d134 | |||
1f4bfc771c | |||
c058f651dc | |||
9be93b6091 | |||
f8828c95bd |
19
.gitignore
vendored
19
.gitignore
vendored
@@ -1,14 +1,9 @@
|
||||
/config.toml
|
||||
/loader
|
||||
/orig
|
||||
/palette-idx
|
||||
/view-ani
|
||||
/view-font
|
||||
/view-obj
|
||||
/view-map
|
||||
/view-minimap
|
||||
/view-menu
|
||||
/view-set
|
||||
/ordoor
|
||||
/investigation/Maps
|
||||
/investigation/Obj
|
||||
/isos
|
||||
/CG
|
||||
/SL
|
||||
/SaW
|
||||
/WoW
|
||||
/WoW-CD
|
||||
/bin
|
||||
|
45
Makefile
45
Makefile
@@ -4,37 +4,40 @@ GOBUILD ?= go build -tags ebitengl
|
||||
|
||||
all: loader ordoor palette-idx view-ani view-font view-obj view-map view-menu view-minimap view-set
|
||||
|
||||
loader: $(srcfiles)
|
||||
$(GOBUILD) -o loader ./cmd/loader
|
||||
bin:
|
||||
mkdir bin
|
||||
|
||||
palette-idx: $(srcfiles)
|
||||
$(GOBUILD) -o palette-idx ./cmd/palette-idx
|
||||
loader: bin $(srcfiles)
|
||||
$(GOBUILD) -o bin/loader ./cmd/loader
|
||||
|
||||
view-ani: $(srcfiles)
|
||||
$(GOBUILD) -o view-ani ./cmd/view-ani
|
||||
palette-idx: bin $(srcfiles)
|
||||
$(GOBUILD) -o bin/palette-idx ./cmd/palette-idx
|
||||
|
||||
view-font: $(srcfiles)
|
||||
$(GOBUILD) -o view-font ./cmd/view-font
|
||||
view-ani: bin $(srcfiles)
|
||||
$(GOBUILD) -o bin/view-ani ./cmd/view-ani
|
||||
|
||||
view-obj: $(srcfiles)
|
||||
$(GOBUILD) -o view-obj ./cmd/view-obj
|
||||
view-font: bin $(srcfiles)
|
||||
$(GOBUILD) -o bin/view-font ./cmd/view-font
|
||||
|
||||
view-map: $(srcfiles)
|
||||
$(GOBUILD) -o view-map ./cmd/view-map
|
||||
view-obj: bin $(srcfiles)
|
||||
$(GOBUILD) -o bin/view-obj ./cmd/view-obj
|
||||
|
||||
view-menu: $(srcfiles)
|
||||
$(GOBUILD) -o view-menu ./cmd/view-menu
|
||||
view-map: bin $(srcfiles)
|
||||
$(GOBUILD) -o bin/view-map ./cmd/view-map
|
||||
|
||||
view-minimap: $(srcfiles)
|
||||
$(GOBUILD) -o view-minimap ./cmd/view-minimap
|
||||
view-menu: bin $(srcfiles)
|
||||
$(GOBUILD) -o bin/view-menu ./cmd/view-menu
|
||||
|
||||
view-set: $(srcfiles)
|
||||
$(GOBUILD) -o view-set ./cmd/view-set
|
||||
view-minimap: bin $(srcfiles)
|
||||
$(GOBUILD) -o bin/view-minimap ./cmd/view-minimap
|
||||
|
||||
ordoor: $(srcfiles)
|
||||
$(GOBUILD) -o ordoor ./cmd/ordoor
|
||||
view-set: bin $(srcfiles)
|
||||
$(GOBUILD) -o bin/view-set ./cmd/view-set
|
||||
|
||||
ordoor: bin $(srcfiles)
|
||||
$(GOBUILD) -o bin/ordoor ./cmd/ordoor
|
||||
|
||||
clean:
|
||||
rm -f loader ordoor view-ani view-obj view-map view-minimap view-set palette-idx view-font
|
||||
rm -rf bin
|
||||
|
||||
.PHONY: all clean
|
||||
|
185
README.md
185
README.md
@@ -1,32 +1,107 @@
|
||||
# Ordoor
|
||||
|
||||
Ordoor is an **unofficial** [game engine recreation](https://en.wikipedia.org/wiki/Game_engine_recreation)
|
||||
of the classic game from 1998, [Warhammer 40,000: Chaos Gate](https://en.wikipedia.org/wiki/Warhammer_40,000:_Chaos_Gate)
|
||||
of the Random Games, Inc., [Strategy Engine](https://www.mobygames.com/game-group/game-engine-random-games-1996-2000-strategy-engine),
|
||||
which was in use from 1996 - 2000.
|
||||
|
||||
**You must have a copy of the original game data to use this project**. GOG is
|
||||
the current publisher of this game; [you can purchase it here](https://www.gog.com/game/warhammer_40000_chaos_gate).
|
||||
Four games are known to have been published for this engine:
|
||||
|
||||
"Warhammer 40,000" is a trademark of Games Workshop, and the game data used by
|
||||
Ordoor contains Games Workshop intellectual property. I am confident that this
|
||||
project uses all those things in accordance with the
|
||||
[Intellectual Property Policy](https://www.games-workshop.com/en-GB/Intellectual-Property-Policy)
|
||||
and the license granted when purchasing a copy of the game in question. Do let
|
||||
me know if you see or suspect any violation, and I'll address it immediately.
|
||||
* [Wages of War: The Business of Battle](https://en.wikipedia.org/wiki/Wages_of_War) (1996)
|
||||
* [Soldiers At War](https://en.wikipedia.org/wiki/Soldiers_at_War) (1998)
|
||||
* [Warhammer 40,000: Chaos Gate](https://en.wikipedia.org/wiki/Warhammer_40,000:_Chaos_Gate) (1998) [GOG](https://www.gog.com/game/warhammer_40000_chaos_gate)
|
||||
* [Avalon Hill's Squad Leader](https://en.wikipedia.org/wiki/Avalon_Hill%27s_Squad_Leader) (2000)
|
||||
|
||||
The aim of Ordoor is to be a complete reimplementation that allows all four
|
||||
of these games to be played on modern hardware. It should also permit new games
|
||||
of the same style to be built.
|
||||
|
||||
For each of the games above, **You must have a copy of the original game data to play**.
|
||||
Links are provided above if we're aware of an active publisher; otherwise, check
|
||||
your back catalogue, or perhaps a local charity shop.
|
||||
|
||||
Trademarks and intellectual property are the property of their respective
|
||||
owners, and the games mentioned above (including the game data) are protected by
|
||||
copyright. As a mere game engine recreation, we're confident that this project
|
||||
operates legally, and that its goal is a noble one. Do get in touch if you
|
||||
believe otherwise!
|
||||
|
||||
Ordoor is a portmanteau of Order Door, which is, of course, the opposite of a
|
||||
Chaos Gate.
|
||||
Chaos Gate. The project began with a Chaos Gate recreation, then more games were
|
||||
discovered, so scope expanded. A rename and/or rewrite may be on the cards as a
|
||||
result.
|
||||
|
||||
## Current status
|
||||
|
||||
### Chaos Gate
|
||||
|
||||
Some of the original file formats are either partially or fully decoded. Maps,
|
||||
menus, and most visual data can be rendered pixel-perfect. Sound can be played
|
||||
(with a preprocessing step). Some UI tookit work is done. No game mechanics are
|
||||
implemented yet.
|
||||
|
||||
I keep a GIF showcasing interesting progress here:
|
||||
|
||||

|
||||
|
||||
I've just been informed that another game from 1998, [Soldiers At War](https://en.wikipedia.org/wiki/Soldiers_at_War),
|
||||
seems to use the same engine. Maybe at some point Ordoor will be able to play
|
||||
both. Will that need a rename? Hmm. Watch this space.
|
||||
|
||||
### Soldiers At War
|
||||
|
||||
(At least some) objects display. Map support is being worked on in the
|
||||
`soldiers-at-war` branch, which can more-or-less display them, albeit with many
|
||||
errors.
|
||||
|
||||
### Squad Leader
|
||||
|
||||
Squad Leader is the most recent of the games created with this engine. Nothing
|
||||
has been done with it yet, but a preliminary look at the game data suggests many
|
||||
changes are afoot. The object files are a different format, at the very least.
|
||||
|
||||
### Wages of War
|
||||
|
||||
This is the oldest of the four games. The object file format seems to be mostly
|
||||
the same. the installer only copies some data to the game directory; we may want
|
||||
to work directly from the CDROM instead, if we can.
|
||||
|
||||
Maps are uncompressed, around 243K, and no header is present. They look similar
|
||||
in principle to the tile data of Soldiers At War or Chaos Gate maps, otherwise.
|
||||
|
||||
The menu system seen in Chaos Gate is not present; instead, there is a `BUTTONS`
|
||||
directory and a lot of `pcx` files under `PIC` that, I suspect, do the job for
|
||||
this game.
|
||||
|
||||
Even with a full installation, Wages of War leaves a lot of data on the CD. It
|
||||
may be best to run solely from the `WOW` directory on the CD, assuming it's a
|
||||
strict superset of what gets installed, data-wise.
|
||||
|
||||
## Long-term goals
|
||||
|
||||
Once full playthrough of the official single-player campaign for all four games
|
||||
has been achieved, thoughts turn to other things we could do. Here are some
|
||||
ideas, mostly at random.
|
||||
|
||||
Multi-player support.
|
||||
|
||||
Graphics enhancements - 3D models instead of sprites, high-resolution tile sets,
|
||||
32-bit colour, etc. Hopefully we'd be able to drop these in one at a time.
|
||||
|
||||
Vastly improved AI.
|
||||
|
||||
Mash-ups? How do mercenaries fare against cultists fare against Nazis? Only one
|
||||
way to find out!
|
||||
|
||||
New campaigns with existing assets. Tell new stories, or elaborate on / modify
|
||||
existing ones.
|
||||
|
||||
Completely new fantasy game using the same engine.
|
||||
|
||||
## Building from source
|
||||
|
||||
I'm writing code in Go at the moment, so you'll need to have a Go runtime
|
||||
installed on your system:
|
||||
installed on your system. Dependency management uses `go mod`, so ensure you
|
||||
have at least Go 1.11.
|
||||
|
||||
```
|
||||
$ go version
|
||||
@@ -44,28 +119,25 @@ Debian:
|
||||
You can then run `make all` in the source tree to get the binaries that are
|
||||
present at the moment.
|
||||
|
||||
Place your WH40K: Chaos Gate installation in `./orig` to benefit from automatic
|
||||
path defaults. Otherwise, point to it with `-game-path`
|
||||
## Configuring
|
||||
|
||||
The `view-map` binary attempts to render a map, and is the current focus of
|
||||
effort. Once I can render a whole map, including pre-placed characters (cultist
|
||||
scum), things can start to get more interesting.
|
||||
Since we support multiple games, a fair bit of configuration is required. Copy
|
||||
`config.toml.example` to `config.toml` and edit it to your requirements. The
|
||||
`data_dir` for the engine(s) you want to use is probably the most important bit,
|
||||
along with the `default_engine`.
|
||||
|
||||
Current status: almost pixel-perfect map rendering. Static objects (four per map
|
||||
coordinate: floor, centre, left, and right) are rendered fine, and each Z level
|
||||
looks good. There are a few minor artifacts here and there.
|
||||
The various games all use snapshots of the original engine at different points
|
||||
in time, and specify a lot in code that we need to specify in data. That should
|
||||
all go into the config file, so new games will be able to adapt the engine to
|
||||
their needs.
|
||||
|
||||
Characters and animations aren't touched at all yet. Rendering performance is
|
||||
poor. No gameplay, no campaign logic. Interaction with the play area is minimal
|
||||
and limited to pan, zoom, and click for basic console output.
|
||||
|
||||
Still, I'm proud of myself.
|
||||
## Running
|
||||
|
||||
To run:
|
||||
|
||||
```
|
||||
$ make view-map
|
||||
$ ./view-map -map Chapter01
|
||||
$ ./bin/view-map -map Chapter01
|
||||
```
|
||||
|
||||
Looks like this:
|
||||
@@ -75,27 +147,29 @@ Looks like this:
|
||||
Use the arrow keys to scroll around the map, the mouse wheel to zoom, and the
|
||||
`1` - `7` keys to change Z level.
|
||||
|
||||
Dependency management uses `go mod`, so ensure you have at least Go 1.11.
|
||||
|
||||
There is the **start** of the menu / campaign flow in a `ordoor` binary:
|
||||
|
||||
```
|
||||
$ cp config.toml.example config.toml
|
||||
$ make ordoor
|
||||
$ ./ordoor
|
||||
```
|
||||
|
||||
This plays the introductory videos so far, and nothing else.
|
||||
|
||||
Menus are in the process of being rendered; you can use the `view-menu` binary
|
||||
to inspect them:
|
||||
Menus / UI widgets have fairly good support now; you can use the `view-menu`
|
||||
binary to inspect them:
|
||||
|
||||
```
|
||||
make view-menu
|
||||
./view-menu -menu ./orig/Menu/Main.mnu
|
||||
./bin/view-menu -menu Main
|
||||
```
|
||||
|
||||
This menu *displays* OK, including
|
||||
This renders the menus found in Chaos Gate and Soldiers At War. The Squad Leader
|
||||
format seems basically the same, but has some extra files and aren't 8-bit
|
||||
colour. They don't display at the moment. Wages of War uses a different format
|
||||
altogether.
|
||||
|
||||
For Chaos Gate, there is the **start** of the game in an `ordoor` binary:
|
||||
|
||||
```
|
||||
$ make ordoor
|
||||
$ ./bin/ordoor
|
||||
```
|
||||
|
||||
The idea is to hook all the different parts together, and to an abstract game
|
||||
state (which is called `ship` for ordoor), to make the whole thing playable. It
|
||||
isn't playable *yet*, but it's heading in that direction.
|
||||
|
||||
## Sound
|
||||
|
||||
@@ -111,30 +185,9 @@ $ ./scripts/convert-wav ./orig/Wav
|
||||
As with video playback, the ambition is to *eventually* remove this dependency
|
||||
and operate on the unmodified files instead.
|
||||
|
||||
## Miscellany
|
||||
## Resources
|
||||
|
||||
"Mission Setup" includes information about available squad types
|
||||
Here's a collection of links that I'm finding useful or otherwise interesting,
|
||||
and don't want to lose track of...
|
||||
|
||||
From EquipDef.cpp Dumo: CEquipment we learn the following object types:
|
||||
|
||||
0. DELETED
|
||||
1. WEAPON
|
||||
2. GRENADE
|
||||
3. MEDIPACK
|
||||
4. SCANNER
|
||||
5. GENESEED
|
||||
6. CLIP
|
||||
7. DOOR KEY
|
||||
8. DOOR KEY
|
||||
9. DOOR KEY
|
||||
10. DOOR KEY
|
||||
|
||||
And we learn they can be "on"....
|
||||
|
||||
0. CHARACTER
|
||||
1. VEHICLE
|
||||
2. CANISTER
|
||||
|
||||
I'm starting to see some parallels with [this](https://github.com/shlainn/game-file-formats/wiki/)
|
||||
in the data formats, and the timeline (1997) seems about right. Worth keeping an
|
||||
eye on!
|
||||
* [Historical geocities modders](http://www.oocities.org/timessquare/galaxy/6777/)
|
||||
|
@@ -3,42 +3,58 @@ package main
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"image/color"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"code.ur.gs/lupine/ordoor/internal/config"
|
||||
"code.ur.gs/lupine/ordoor/internal/data"
|
||||
"code.ur.gs/lupine/ordoor/internal/fonts"
|
||||
"code.ur.gs/lupine/ordoor/internal/idx"
|
||||
"code.ur.gs/lupine/ordoor/internal/maps"
|
||||
"code.ur.gs/lupine/ordoor/internal/menus"
|
||||
"code.ur.gs/lupine/ordoor/internal/palettes"
|
||||
"code.ur.gs/lupine/ordoor/internal/sets"
|
||||
)
|
||||
|
||||
var (
|
||||
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
|
||||
skipObj = flag.Bool("skip-obj", true, "Skip loading .obj files")
|
||||
configFile = flag.String("config", "config.toml", "Config file")
|
||||
engine = flag.String("engine", "", "Override engine to use")
|
||||
|
||||
skipObj = flag.Bool("skip-obj", true, "Skip loading .obj files")
|
||||
)
|
||||
|
||||
// FIXME: all these paths are hardcoded with Chaos Gate in mind
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
loadData()
|
||||
|
||||
if !*skipObj {
|
||||
loadObj()
|
||||
cfg, err := config.Load(*configFile, *engine)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
engine := cfg.DefaultEngine()
|
||||
gamePath := engine.DataDir
|
||||
palette, ok := palettes.Get(engine.Palette)
|
||||
if !ok {
|
||||
log.Fatalf("Unknown palette name: %v", engine.Palette)
|
||||
}
|
||||
|
||||
loadMapsFrom("Maps")
|
||||
loadMapsFrom("MultiMaps")
|
||||
loadSets()
|
||||
loadMenus()
|
||||
loadFonts()
|
||||
loadIdx()
|
||||
loadData(filepath.Join(gamePath, "Data"))
|
||||
|
||||
if !*skipObj {
|
||||
loadObj(filepath.Join(gamePath, "Obj"))
|
||||
}
|
||||
|
||||
loadMapsFrom(filepath.Join(gamePath, "Maps"))
|
||||
loadMapsFrom(filepath.Join(gamePath, "MultiMaps"))
|
||||
loadSets(filepath.Join(gamePath, "Sets"))
|
||||
loadMenus(filepath.Join(gamePath, "Menu"), palette)
|
||||
loadFonts(filepath.Join(gamePath, "Fonts"))
|
||||
loadIdx(filepath.Join(gamePath, "Idx", "WarHammer.idx"))
|
||||
}
|
||||
|
||||
func loadData() {
|
||||
dataPath := filepath.Join(*gamePath, "Data")
|
||||
func loadData(dataPath string) {
|
||||
accountingPath := filepath.Join(dataPath, "Accounting.dat")
|
||||
aniObDefPath := filepath.Join(dataPath, "AniObDef.dat")
|
||||
genericDataPath := filepath.Join(dataPath, "GenericData.dat")
|
||||
@@ -84,9 +100,7 @@ func loadData() {
|
||||
ha.Print()
|
||||
}
|
||||
|
||||
func loadObj() {
|
||||
objDataPath := filepath.Join(*gamePath, "Obj")
|
||||
|
||||
func loadObj(objDataPath string) {
|
||||
// TODO: Obj/cpiece.rec isn't loaded by this. Do we need it? How do we know?
|
||||
log.Printf("Loading %s...", objDataPath)
|
||||
objects, err := data.LoadObjects(objDataPath)
|
||||
@@ -116,8 +130,7 @@ func loadObj() {
|
||||
}
|
||||
}
|
||||
|
||||
func loadMapsFrom(part string) {
|
||||
mapsPath := filepath.Join(*gamePath, part)
|
||||
func loadMapsFrom(mapsPath string) {
|
||||
log.Printf("Loading maps from %s", mapsPath)
|
||||
|
||||
gameMaps, err := maps.LoadGameMaps(mapsPath)
|
||||
@@ -126,21 +139,20 @@ func loadMapsFrom(part string) {
|
||||
}
|
||||
|
||||
log.Printf("Maps in %s:", mapsPath)
|
||||
for key, gameMap := range gameMaps {
|
||||
hdr := gameMap.Header
|
||||
for key, gm := range gameMaps {
|
||||
rect := gm.Rect()
|
||||
fmt.Printf(
|
||||
" * `%s`: IsCampaignMap=%v W=%v:%v L=%v:%v SetName=%s\n",
|
||||
key,
|
||||
hdr.IsCampaignMap,
|
||||
hdr.MinWidth, hdr.MaxWidth,
|
||||
hdr.MinLength, hdr.MaxLength,
|
||||
string(hdr.SetName[:]),
|
||||
gm.IsCampaignMap,
|
||||
rect.Min.X, rect.Max.X,
|
||||
rect.Min.Y, rect.Max.Y,
|
||||
string(gm.SetName),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func loadSets() {
|
||||
setsPath := filepath.Join(*gamePath, "Sets")
|
||||
func loadSets(setsPath string) {
|
||||
log.Printf("Loading sets from %s", setsPath)
|
||||
|
||||
mapSets, err := sets.LoadSets(setsPath)
|
||||
@@ -156,10 +168,10 @@ func loadSets() {
|
||||
}
|
||||
}
|
||||
|
||||
func loadMenus() {
|
||||
menusPath := filepath.Join(*gamePath, "Menu")
|
||||
func loadMenus(menusPath string, palette color.Palette) {
|
||||
log.Printf("Loading menus from %s", menusPath)
|
||||
|
||||
menus, err := menus.LoadMenus(menusPath)
|
||||
menus, err := menus.LoadMenus(menusPath, palette)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse %s/*.mnu as menus: %v", menusPath, err)
|
||||
}
|
||||
@@ -186,8 +198,8 @@ func displayRecord(record *menus.Record, depth int) {
|
||||
fmt.Printf("%s* %s\n", strings.Repeat(" ", depth), content)
|
||||
}
|
||||
|
||||
func loadFonts() {
|
||||
fontsPath := filepath.Join(*gamePath, "Fonts")
|
||||
func loadFonts(fontsPath string) {
|
||||
log.Printf("Loading fonts from %s", fontsPath)
|
||||
|
||||
fonts, err := fonts.LoadFonts(fontsPath)
|
||||
if err != nil {
|
||||
@@ -199,8 +211,9 @@ func loadFonts() {
|
||||
}
|
||||
}
|
||||
|
||||
func loadIdx() {
|
||||
idxPath := filepath.Join(*gamePath, "Idx", "WarHammer.idx")
|
||||
func loadIdx(idxPath string) {
|
||||
log.Printf("Loading idx from %s", idxPath)
|
||||
|
||||
idx, err := idx.Load(idxPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse %s as idx: %v", idxPath, err)
|
||||
@@ -208,5 +221,8 @@ func loadIdx() {
|
||||
|
||||
for i, group := range idx.Groups {
|
||||
log.Printf("Group %2d: %4d records, start sprite is %6d", i, len(group.Records), group.Spec.SpriteIdx)
|
||||
for i, rec := range group.Records {
|
||||
log.Printf("\t%3d: %#+v", i, rec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,7 +5,7 @@ import (
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"code.ur.gs/lupine/ordoor/internal/data"
|
||||
"code.ur.gs/lupine/ordoor/internal/palettes"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -13,11 +13,11 @@ func main() {
|
||||
case "index", "i":
|
||||
idx, err := strconv.ParseInt(os.Args[2], 16, 64)
|
||||
if err != nil {
|
||||
fmt.Println("Usage: palette-idx i <0-255>")
|
||||
fmt.Println("Usage: palette-idx index <0-255> <name>")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Palette[%v]: %#v\n", idx, data.ColorPalette[idx])
|
||||
fmt.Printf("%vPalette[%v]: %#v\n", os.Args[3], idx, palettes.Palettes[os.Args[3]][idx])
|
||||
case "color", "colour", "c":
|
||||
fmt.Println("TODO!")
|
||||
os.Exit(1)
|
||||
|
@@ -7,15 +7,18 @@ import (
|
||||
"math"
|
||||
"os"
|
||||
|
||||
"github.com/hajimehoshi/ebiten"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"golang.org/x/image/colornames"
|
||||
|
||||
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
||||
"code.ur.gs/lupine/ordoor/internal/config"
|
||||
"code.ur.gs/lupine/ordoor/internal/ui"
|
||||
)
|
||||
|
||||
var (
|
||||
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
|
||||
configFile = flag.String("config", "config.toml", "Config file")
|
||||
engine = flag.String("engine", "", "Override engine to use")
|
||||
|
||||
groupIdx = flag.Int("group", 1, "Group index to start at")
|
||||
recIdx = flag.Int("record", 0, "Record index to start at")
|
||||
|
||||
@@ -43,14 +46,19 @@ type state struct {
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
if *gamePath == "" {
|
||||
if *configFile == "" {
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
assets, err := assetstore.New(*gamePath)
|
||||
cfg, err := config.Load(*configFile, *engine)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to set up asset store: %v", err)
|
||||
log.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
assets, err := assetstore.New(cfg.DefaultEngine())
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to set up asset store: %v", err)
|
||||
}
|
||||
|
||||
state := state{
|
||||
@@ -92,7 +100,7 @@ func main() {
|
||||
func (e *env) Update(screenX, screenY int) error {
|
||||
if e.step == 0 || e.lastState != e.state {
|
||||
|
||||
ani, err := e.assets.Animation(e.state.groupIdx, e.state.recIdx)
|
||||
ani, err := e.assets.Animation(e.state.groupIdx, e.state.recIdx, 0) // FIXME: why 0?
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -123,7 +131,7 @@ func (e *env) Draw(screen *ebiten.Image) error {
|
||||
if len(e.ani.Frames) > 0 {
|
||||
sprite := e.ani.Frames[e.step/4%len(e.ani.Frames)]
|
||||
|
||||
return screen.DrawImage(sprite.Image, &ebiten.DrawImageOptions{GeoM: cam})
|
||||
screen.DrawImage(sprite.Image, &ebiten.DrawImageOptions{GeoM: cam})
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@@ -7,14 +7,17 @@ import (
|
||||
"math"
|
||||
"os"
|
||||
|
||||
"github.com/hajimehoshi/ebiten"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
|
||||
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
||||
"code.ur.gs/lupine/ordoor/internal/config"
|
||||
"code.ur.gs/lupine/ordoor/internal/ui"
|
||||
)
|
||||
|
||||
var (
|
||||
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
|
||||
configFile = flag.String("config", "config.toml", "Config file")
|
||||
engine = flag.String("engine", "", "Override engine to use")
|
||||
|
||||
fontName = flag.String("font", "", "Name of a font, e.g., basfont12")
|
||||
txt = flag.String("text", "Test string", "Text to render")
|
||||
|
||||
@@ -37,12 +40,17 @@ type state struct {
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
if *gamePath == "" || *fontName == "" {
|
||||
if *configFile == "" || *fontName == "" {
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
assets, err := assetstore.New(*gamePath)
|
||||
cfg, err := config.Load(*configFile, *engine)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
assets, err := assetstore.New(cfg.DefaultEngine())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -61,7 +69,7 @@ func main() {
|
||||
|
||||
win, err := ui.NewWindow(env, "View Font: "+*fontName, *winX, *winY)
|
||||
if err != nil {
|
||||
log.Fatal("Couldn't create window: %v", err)
|
||||
log.Fatalf("Couldn't create window: %v", err)
|
||||
}
|
||||
|
||||
win.OnMouseWheel(env.changeZoom)
|
||||
@@ -95,9 +103,7 @@ func (e *env) Draw(screen *ebiten.Image) error {
|
||||
op.GeoM.Translate(float64(xOff), 0)
|
||||
op.GeoM.Scale(e.state.zoom, e.state.zoom) // apply current zoom factor
|
||||
|
||||
if err := screen.DrawImage(glyph.Image, op); err != nil {
|
||||
return err
|
||||
}
|
||||
screen.DrawImage(glyph.Image, op)
|
||||
|
||||
xOff += glyph.Rect.Dx()
|
||||
}
|
||||
|
@@ -3,38 +3,50 @@ package main
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"math"
|
||||
"os"
|
||||
|
||||
"github.com/hajimehoshi/ebiten"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
|
||||
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
||||
"code.ur.gs/lupine/ordoor/internal/config"
|
||||
"code.ur.gs/lupine/ordoor/internal/flow"
|
||||
"code.ur.gs/lupine/ordoor/internal/scenario"
|
||||
"code.ur.gs/lupine/ordoor/internal/ship"
|
||||
"code.ur.gs/lupine/ordoor/internal/ui"
|
||||
)
|
||||
|
||||
var (
|
||||
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
|
||||
gameMap = flag.String("map", "", "Name of a map, e.g., Chapter01")
|
||||
configFile = flag.String("config", "config.toml", "Config file")
|
||||
engine = flag.String("engine", "", "Override engine to use")
|
||||
|
||||
gameMap = flag.String("map", "", "Name of a map, e.g., Chapter01")
|
||||
|
||||
winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension")
|
||||
winY = flag.Int("win-y", 1024, "Pre-scaled window Y dimension")
|
||||
)
|
||||
|
||||
type env struct {
|
||||
flow *flow.Flow
|
||||
scenario *scenario.Scenario
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
if *gamePath == "" || *gameMap == "" {
|
||||
if *configFile == "" || *gameMap == "" {
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
assets, err := assetstore.New(*gamePath)
|
||||
cfg, err := config.Load(*configFile, *engine)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to scan root directory %v: %v", *gamePath, err)
|
||||
log.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
assets, err := assetstore.New(cfg.DefaultEngine())
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to scan root directory: %v", err)
|
||||
}
|
||||
|
||||
scenario, err := scenario.NewScenario(assets, *gameMap)
|
||||
@@ -42,25 +54,39 @@ func main() {
|
||||
log.Fatalf("Failed to load scenario %v: %v", *gameMap, err)
|
||||
}
|
||||
|
||||
env := &env{
|
||||
scenario: scenario,
|
||||
var realEnv *env
|
||||
if cfg.DefaultEngineName == "ordoor" {
|
||||
ship := &ship.Ship{}
|
||||
|
||||
flow, err := flow.New(assets, cfg, ship)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to setup flow: %v", err)
|
||||
}
|
||||
flow.SetScenario(scenario)
|
||||
realEnv = &env{flow: flow, scenario: scenario}
|
||||
} else {
|
||||
realEnv = &env{scenario: scenario}
|
||||
}
|
||||
|
||||
win, err := ui.NewWindow(env, "View Map "+*gameMap, *winX, *winY)
|
||||
win, err := ui.NewWindow(realEnv, "View Map "+*gameMap, *winX, *winY)
|
||||
if err != nil {
|
||||
log.Fatal("Couldn't create window: %v", err)
|
||||
log.Fatalf("Couldn't create window: %v", err)
|
||||
}
|
||||
|
||||
win.OnKeyUp(ebiten.KeyLeft, env.changeOrigin(-64, +0))
|
||||
win.OnKeyUp(ebiten.KeyRight, env.changeOrigin(+64, +0))
|
||||
win.OnKeyUp(ebiten.KeyUp, env.changeOrigin(+0, -64))
|
||||
win.OnKeyUp(ebiten.KeyDown, env.changeOrigin(+0, +64))
|
||||
|
||||
for i := 0; i <= 6; i++ {
|
||||
win.OnKeyUp(ebiten.Key1+ebiten.Key(i), env.setZIdx(i))
|
||||
win.OnKeyUp(ebiten.Key1+ebiten.Key(i), realEnv.setZIdx(i))
|
||||
}
|
||||
|
||||
win.OnMouseClick(env.showCellData)
|
||||
win.OnMouseClick(realEnv.showCellData)
|
||||
win.OnMouseWheel(realEnv.changeZoom)
|
||||
|
||||
if realEnv.flow == nil {
|
||||
step := 32
|
||||
win.WhileKeyDown(ebiten.KeyLeft, realEnv.changeOrigin(-step, +0))
|
||||
win.WhileKeyDown(ebiten.KeyRight, realEnv.changeOrigin(+step, +0))
|
||||
win.WhileKeyDown(ebiten.KeyUp, realEnv.changeOrigin(+0, -step))
|
||||
win.WhileKeyDown(ebiten.KeyDown, realEnv.changeOrigin(+0, +step))
|
||||
}
|
||||
|
||||
if err := win.Run(); err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -68,11 +94,19 @@ func main() {
|
||||
}
|
||||
|
||||
func (e *env) Update(screenX, screenY int) error {
|
||||
return e.scenario.Update(screenX, screenY)
|
||||
if e.flow != nil {
|
||||
return e.flow.Update(screenX, screenY)
|
||||
} else {
|
||||
return e.scenario.Update(screenX, screenY)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *env) Draw(screen *ebiten.Image) error {
|
||||
return e.scenario.Draw(screen)
|
||||
if e.flow != nil {
|
||||
return e.flow.Draw(screen)
|
||||
} else {
|
||||
return e.scenario.Draw(screen)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *env) changeOrigin(byX, byY int) func() {
|
||||
@@ -82,6 +116,10 @@ func (e *env) changeOrigin(byX, byY int) func() {
|
||||
}
|
||||
}
|
||||
|
||||
func (e *env) changeZoom(_, byY float64) {
|
||||
e.scenario.Zoom *= math.Pow(1.2, byY)
|
||||
}
|
||||
|
||||
func (e *env) setZIdx(to int) func() {
|
||||
return func() {
|
||||
e.scenario.ZIdx = to
|
||||
|
@@ -5,14 +5,17 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/hajimehoshi/ebiten"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
|
||||
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
||||
"code.ur.gs/lupine/ordoor/internal/config"
|
||||
"code.ur.gs/lupine/ordoor/internal/ui"
|
||||
)
|
||||
|
||||
var (
|
||||
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
|
||||
configFile = flag.String("config", "config.toml", "Config file")
|
||||
engine = flag.String("engine", "", "Override engine to use")
|
||||
|
||||
menuName = flag.String("menu", "", "Name of a menu, e.g. Main")
|
||||
|
||||
winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension")
|
||||
@@ -28,12 +31,17 @@ type dlg struct {
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
if *gamePath == "" || *menuName == "" {
|
||||
if *configFile == "" || *menuName == "" {
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
assets, err := assetstore.New(*gamePath)
|
||||
cfg, err := config.Load(*configFile, *engine)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
assets, err := assetstore.New(cfg.DefaultEngine())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -50,7 +58,7 @@ func main() {
|
||||
|
||||
win, err := ui.NewWindow(driver, "View Menu: "+*menuName, *winX, *winY)
|
||||
if err != nil {
|
||||
log.Fatal("Couldn't create window: %v", err)
|
||||
log.Fatalf("Couldn't create window: %v", err)
|
||||
}
|
||||
|
||||
// Change the active dialogue
|
||||
|
@@ -10,7 +10,7 @@ import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/hajimehoshi/ebiten"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
|
||||
"code.ur.gs/lupine/ordoor/internal/maps"
|
||||
"code.ur.gs/lupine/ordoor/internal/sets"
|
||||
@@ -76,7 +76,7 @@ func main() {
|
||||
|
||||
win, err := ui.NewWindow(env, "View Map "+*mapFile, *winX, *winY)
|
||||
if err != nil {
|
||||
log.Fatal("Couldn't create window: %v", err)
|
||||
log.Fatalf("Couldn't create window: %v", err)
|
||||
}
|
||||
|
||||
win.OnKeyUp(ebiten.KeyEnter, env.toggleAutoUpdate)
|
||||
@@ -172,20 +172,13 @@ func (e *env) Update(screenX, screenY int) error {
|
||||
|
||||
func (e *env) Draw(screen *ebiten.Image) error {
|
||||
gameMap := e.gameMap
|
||||
imd, err := ebiten.NewImage(
|
||||
int(gameMap.MaxWidth),
|
||||
int(gameMap.MaxLength),
|
||||
ebiten.FilterDefault,
|
||||
)
|
||||
rect := gameMap.Rect()
|
||||
imd := ebiten.NewImage(rect.Dx(), rect.Dy())
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for y := int(gameMap.MinLength); y < int(gameMap.MaxLength); y++ {
|
||||
for x := int(gameMap.MinWidth); x < int(gameMap.MaxWidth); x++ {
|
||||
cell := gameMap.Cells.At(x, y, int(e.state.zIdx))
|
||||
imd.Set(x, y, makeColour(&cell, e.state.cellIdx))
|
||||
for y := int(rect.Min.Y); y < int(rect.Max.Y); y++ {
|
||||
for x := int(rect.Min.X); x < int(rect.Max.X); x++ {
|
||||
cell := gameMap.At(x, y, int(e.state.zIdx))
|
||||
imd.Set(x, y, makeColour(cell, e.state.cellIdx))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,7 +189,9 @@ func (e *env) Draw(screen *ebiten.Image) error {
|
||||
cam.Scale(e.state.zoom, e.state.zoom) // apply current zoom factor
|
||||
cam.Rotate(0.785) // Apply isometric angle
|
||||
|
||||
return screen.DrawImage(imd, &ebiten.DrawImageOptions{GeoM: cam})
|
||||
screen.DrawImage(imd, &ebiten.DrawImageOptions{GeoM: cam})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Converts pixel coordinates to cell coordinates
|
||||
|
@@ -7,17 +7,20 @@ import (
|
||||
"math"
|
||||
"os"
|
||||
|
||||
"github.com/hajimehoshi/ebiten"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
|
||||
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
||||
"code.ur.gs/lupine/ordoor/internal/config"
|
||||
"code.ur.gs/lupine/ordoor/internal/ui"
|
||||
)
|
||||
|
||||
var (
|
||||
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
|
||||
objFile = flag.String("obj-file", "", "Path of an .obj file, e.g. ./orig/Obj/TZEENTCH.OBJ")
|
||||
objName = flag.String("obj-name", "", "Name of an .obj file, e.g. TZEENTCH")
|
||||
sprIdx = flag.Int("spr-idx", 0, "Sprite index to start at")
|
||||
configFile = flag.String("config", "config.toml", "Config file")
|
||||
engine = flag.String("engine", "", "Override engine to use")
|
||||
|
||||
objFile = flag.String("obj-file", "", "Path of an .obj file, e.g. ./orig/Obj/TZEENTCH.OBJ")
|
||||
objName = flag.String("obj-name", "", "Name of an .obj file, e.g. TZEENTCH")
|
||||
sprIdx = flag.Int("spr-idx", 0, "Sprite index to start at")
|
||||
|
||||
winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension")
|
||||
winY = flag.Int("win-y", 1024, "Pre-scaled window Y dimension")
|
||||
@@ -42,14 +45,19 @@ type state struct {
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
if *gamePath == "" || (*objName == "" && *objFile == "") {
|
||||
if *configFile == "" || (*objName == "" && *objFile == "") {
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
assets, err := assetstore.New(*gamePath)
|
||||
cfg, err := config.Load(*configFile, *engine)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to set up asset store: %v", err)
|
||||
log.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
assets, err := assetstore.New(cfg.DefaultEngine())
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to set up asset store: %v", err)
|
||||
}
|
||||
|
||||
var obj *assetstore.Object
|
||||
@@ -128,7 +136,9 @@ func (e *env) Draw(screen *ebiten.Image) error {
|
||||
cam.Translate(float64(e.state.origin.X), float64(e.state.origin.Y)) // Move to origin
|
||||
cam.Scale(e.state.zoom, e.state.zoom) // apply current zoom factor
|
||||
|
||||
return screen.DrawImage(sprite.Image, &ebiten.DrawImageOptions{GeoM: cam})
|
||||
screen.DrawImage(sprite.Image, &ebiten.DrawImageOptions{GeoM: cam})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *env) changeSprite(by int) func() {
|
||||
|
@@ -7,15 +7,18 @@ import (
|
||||
"math"
|
||||
"os"
|
||||
|
||||
"github.com/hajimehoshi/ebiten"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
|
||||
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
||||
"code.ur.gs/lupine/ordoor/internal/config"
|
||||
"code.ur.gs/lupine/ordoor/internal/ui"
|
||||
)
|
||||
|
||||
var (
|
||||
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
|
||||
setName = flag.String("set", "", "Name of a set, e.g., map01")
|
||||
configFile = flag.String("config", "config.toml", "Config file")
|
||||
engine = flag.String("engine", "", "Override engine to use")
|
||||
|
||||
setName = flag.String("set", "", "Name of a set, e.g., map01")
|
||||
|
||||
winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension")
|
||||
winY = flag.Int("win-y", 1024, "Pre-scaled window Y dimension")
|
||||
@@ -39,12 +42,17 @@ type state struct {
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
if *gamePath == "" || *setName == "" {
|
||||
if *configFile == "" || *setName == "" {
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
assets, err := assetstore.New(*gamePath)
|
||||
cfg, err := config.Load(*configFile, *engine)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
assets, err := assetstore.New(cfg.DefaultEngine())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -63,7 +71,7 @@ func main() {
|
||||
|
||||
win, err := ui.NewWindow(env, "View Set: "+*setName, *winX, *winY)
|
||||
if err != nil {
|
||||
log.Fatal("Couldn't create window: %v", err)
|
||||
log.Fatalf("Couldn't create window: %v", err)
|
||||
}
|
||||
|
||||
win.OnKeyUp(ebiten.KeyLeft, env.changeObjIdx(-1))
|
||||
@@ -115,7 +123,9 @@ func (e *env) Draw(screen *ebiten.Image) error {
|
||||
|
||||
// TODO: centre the image
|
||||
|
||||
return screen.DrawImage(sprite.Image, &ebiten.DrawImageOptions{GeoM: cam})
|
||||
screen.DrawImage(sprite.Image, &ebiten.DrawImageOptions{GeoM: cam})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *env) changeObjIdx(by int) func() {
|
||||
|
@@ -1,6 +1,22 @@
|
||||
[ordoor]
|
||||
data_dir = "./orig"
|
||||
video_player = ["mpv", "--no-config", "--keep-open=no", "--force-window=no", "--no-border", "--no-osc", "--fullscreen", "--no-input-default-bindings"]
|
||||
video_player = ["mpv", "--no-config", "--keep-open=no", "--force-window=no", "--no-border", "--no-osc", "--fullscreen", "--no-input-default-bindings"]
|
||||
|
||||
default_engine = "ordoor"
|
||||
|
||||
[engines.geas] # Wages of War -> Gifts of Peace -> Geas
|
||||
data_dir = "./WoW-CD"
|
||||
palette = "WagesOfWar"
|
||||
|
||||
[engines.ordoor] # Chaos Gate -> Order Door -> Ordoor
|
||||
data_dir = "./CG"
|
||||
palette = "ChaosGate"
|
||||
|
||||
[engines.baps] # Soldiers At War -> Boys at Play -> Baps
|
||||
data_dir = "./SaW"
|
||||
palette = "SoldiersAtWar"
|
||||
|
||||
[engines.sl] # Squad Leader -> ??? -> ???
|
||||
data_dir = "./SL"
|
||||
palette = "ChaosGate" # may not be relevant?
|
||||
|
||||
[options]
|
||||
play_movies = true
|
||||
|
@@ -392,8 +392,11 @@ well-aligned amount.
|
||||
Investigation has so far suggested the following:
|
||||
|
||||
* `Cell[0]` seems related to doors and canisters. Observed:
|
||||
* Nothing special: 0x38
|
||||
* ???: 0x39
|
||||
* Imperial crate: 0x28
|
||||
* Door: 0xB8
|
||||
|
||||
* `Cell[1]` seems related to special placeables (but not triggers). Bitfield. Observed:
|
||||
* 0x01: Reactor
|
||||
* 0x20: Door or door lock?
|
||||
@@ -408,12 +411,12 @@ Investigation has so far suggested the following:
|
||||
* `Cell[7]` Object 2 (Right) Area (Sets/*.set lookup)
|
||||
* `Cell[6]` Object 2 (Right) Sprite + active flag
|
||||
* `Cell[9]` Object 3 (Center) Area (Sets/*.set lookup)
|
||||
* `Cell[10]` Object 3 (Right) Sprite + active flag
|
||||
* `Cell[11]` all 255?
|
||||
* `Cell[10]` Object 3 (Center) Sprite + active flag
|
||||
* `Cell[11]` all 255? Vehicle?
|
||||
* `Cell[12]` all 0?
|
||||
* `Cell[13]` all 0?
|
||||
* `Cell[14]` all 0?
|
||||
* `Cell[15]` shows squad positions, MP start positions, etc, as 0x04
|
||||
* `Cell[15]` shows squad positions, MP start positions, etc, as 0x04. Bitfield?
|
||||
|
||||
Mapping the altar in Chapter01 to the map01 set suggests it's a palette entry
|
||||
lookup, 0-indexed. `U` debug in WH40K_TD.exe says the cell's `Object 3-Center`
|
||||
@@ -515,5 +518,285 @@ Around 001841A0: mission objectives!
|
||||
00184240 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
|
||||
```
|
||||
|
||||
Since all the files are exactly the same length uncompressed, I'm going to
|
||||
assume these are all a fixed number of fixed-size records when looking into it.
|
||||
Relative offsets from the start of the trailer, we have:
|
||||
|
||||
| Offset | Text |
|
||||
| -------- | ---- |
|
||||
| `0xEE` | Mania |
|
||||
| `0x78A` | Dagon |
|
||||
| `0xE26` | Nihasa |
|
||||
| `0x14C2` | Samnu |
|
||||
| `0x1b5e` | Bael |
|
||||
| `0x2896` | Gigamen |
|
||||
| `0x2f32` | Valefor |
|
||||
| `0x35ce` | Baalberith |
|
||||
| `0x3c6a` | Fenriz |
|
||||
| `0x4306` | #Character |
|
||||
| `0x49a2` | Apollyon |
|
||||
|
||||
So there are 1692 bytes between each name (the names probably don't come at the
|
||||
start of each block, but it's still a useful stride). Presumably `#Character` is
|
||||
a space for one of the player characters, while the others specify an NPC placed
|
||||
on the map.
|
||||
|
||||
There's 56 of these records between the first and last name we see - `Ahpuch`.
|
||||
|
||||
Then there are a number of other strings that seem related to triggers / events,
|
||||
including lots that say `NO FILE`. The first two are 96 bytes apart; from then
|
||||
on they seem to be placed variably apart from each other; I've seen 96, 256, and
|
||||
352 byte offsets.
|
||||
|
||||
At 0x20916 the mission objective is readable.
|
||||
|
||||
At 0x2092a the mission description is readable.
|
||||
|
||||
Generating another map with just 5 characters on it, things look different:
|
||||
|
||||
* Trailer size is 13543 bytes
|
||||
* There are only 5 names
|
||||
* There are none of the trigger/event strings
|
||||
* Mission title is found at 0x2b93
|
||||
* Mission briefing is found at 0x2c92
|
||||
|
||||
Since the trailer is a variable size, there must be a header that tells us how
|
||||
many of each type of record to read. Peeking at the differences in `vbindiff`:
|
||||
|
||||
```
|
||||
Chapter01.MAP.Trailer
|
||||
0000 0000: 38 00 00 68 00 00 00 50 00 00 00 1A 00 00 00 14 8..h...P ........
|
||||
0000 0010: 00 00 00 3A 00 00 00 00 38 25 00 04 00 00 00 00 ...:.... 8%......
|
||||
0000 0020: 00 00 00 1A 00 00 00 00 00 00 00 00 00 00 00 00 ........ ........
|
||||
0000 0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ........ ........
|
||||
0000 0040: 00 00 00 00 00 00 00 00 00 00 00 02 00 00 00 00 ........ ........
|
||||
0000 0050: 00 00 00 32 00 00 00 00 00 00 00 00 00 00 00 00 ...2.... ........
|
||||
|
||||
TINYSQUAD.MAP.Trailer
|
||||
0000 0000: 38 00 00 4B 00 00 00 3C 00 00 00 37 00 00 00 28 8..K...< ...7...(
|
||||
0000 0010: 00 00 00 05 00 00 00 00 2B 3A 00 04 00 00 00 05 ........ +:......
|
||||
0000 0020: 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 ........ ........
|
||||
0000 0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ........ ........
|
||||
0000 0040: 00 00 00 00 00 00 00 00 00 00 00 02 00 00 00 00 ........ ........
|
||||
0000 0050: 00 00 00 32 00 00 00 00 00 00 00 00 00 00 00 00 ...2.... ........
|
||||
```
|
||||
|
||||
The size of the trailer for Chapter01 is 139,483 bytes, assuming it starts at
|
||||
`0x163890`. However, things may be a lot more sensible if we drop 3 bytes off
|
||||
the start of that to get the fields into little-endian alignment. Have I made a
|
||||
maths error above somewhere? Is it some sort of alignment thing? Do those 3
|
||||
bytes actually have meaning?
|
||||
|
||||
Ignoring them for now, here's a first guess at a header:
|
||||
|
||||
| Offset | Size | Meaning |
|
||||
| ------ | ---- | ------- |
|
||||
| 0 | 4 | Map maximum X + 1 |
|
||||
| 4 | 4 | Map maximum Y + 1 |
|
||||
| 8 | 4 | Map minimum X |
|
||||
| 12 | 4 | Map minimum Y |
|
||||
| 16 | 4 | Number of character records |
|
||||
| 20 | 4 | Padding? - invariant `00 00 00 00` |
|
||||
| 24 | 2 | ??? - varies. Seems related to character/squad position? |
|
||||
| 26 | 2 | ??? - invariant `00 04` |
|
||||
| 28 | 4 | ??? - varies (0 vs 5) |
|
||||
| 32 | 1 | Number of thingies |
|
||||
| 33 | 3 | ???. With a Lord of Change on the map, only one byte works as thingies count |
|
||||
| 36 | 20 | Padding? |
|
||||
|
||||
56 bytes of data is interesting because the value of that first, ignored byte is
|
||||
0x38 - perhaps it's a skip value + 2 bytes of padding? It's just weird. Keep
|
||||
ignoring it for now.
|
||||
|
||||
0x4b contains the next non-null byte; is the gap between the the number of
|
||||
thingies, and it, padding? Minus a bit? 0x50 is another non-null byte. Then
|
||||
it's all zeroes until one byte before the first name at 0xee.
|
||||
|
||||
Individual cells seem to have a flag to say "We have a character in us", but not
|
||||
the number for the character themselves, so the coordinates must be in the
|
||||
per-character records also. There are several candidates for this.
|
||||
|
||||
Placing a single character at (64,49) causes those bytes to show up at four
|
||||
offsets - 0x18 (!), 0x1F4, 0x1F8, and 0x6C8.
|
||||
|
||||
Generating a map with no characters at all, the trailer is 2,447 bytes, and the
|
||||
mission title starts at 0x3B (59). So we can say we have 20 bytes of padding as
|
||||
a first approximation?
|
||||
|
||||
Here's where we're at with the per-character data, going from the padding values
|
||||
suggested above:
|
||||
|
||||
| Offset | Size | Meaning |
|
||||
| ------ | ---- | ------- |
|
||||
| 0 | 178 | ??? |
|
||||
| 178 | 1 | Character type |
|
||||
| 179 | 80 | Character name |
|
||||
| 259 | 1 | Weapon Skill |
|
||||
| 260 | 1 | Ballistic Skill |
|
||||
| 261 | 1 | Unknown |
|
||||
| 262 | 1 | Leadership |
|
||||
| 263 | 1 | Toughness |
|
||||
| 264 | 1 | Strength |
|
||||
| 265 | 1 | Action Points |
|
||||
| 266 | 1 | Unknown |
|
||||
| 267 | 1 | Unknown |
|
||||
| 268 | 1 | Health |
|
||||
| 269 | 495 | ??? |
|
||||
| 764 | 1(?) | Squad number |
|
||||
| 765 | 895 | ??? |
|
||||
| 1660 | 1? | Orientation? Could also be `0x680`... |
|
||||
| 1661 | 31 | ??? |
|
||||
|
||||
There's still a lot of bytes to dig through, but this allows me to load the
|
||||
character names from Chapter01 correctly, with the exception of record 57 which
|
||||
just contains `\x02` and is then null-terminated all the way through - but maybe
|
||||
that's just a data thing.
|
||||
|
||||
How about their types? `HasAction.dat` lists numbers for character types, and
|
||||
those show up immediately before the name. Going from the character type to the
|
||||
animation group is not yet fully deciphered - squad leaders mess up a direct
|
||||
correlation - but a fixed offset table allows me to draw the characters \o/.
|
||||
|
||||
Putting 8 characters onto a map and orienting them in the compass points, we see
|
||||
numbers ranging from 0 to 7 at 0x67c and 0x680. Assuming this is the scheme
|
||||
used, north is value 1, northeast value 2, and northwest value 0.
|
||||
|
||||
Given two characters of the same type, just in different locations, differing
|
||||
values are seen at:
|
||||
|
||||
* `0x103 - 0x10c` (hodgepodge)
|
||||
* `0x178 - 0x1be` (hodgepodge)
|
||||
* `0x2fc` (0, 1) - squad number?
|
||||
|
||||
|
||||
I can easily correlate certain bytes in the first range to various character
|
||||
attributes. A few remain unset.
|
||||
|
||||
In Chapter01, picking a random character (Gorgon) and looking at his squadmates,
|
||||
they are all in the same squad, and no other characters are in that squad, so it
|
||||
looks pretty diagnostic to me. There's nothing in the UI to indicate the squad,
|
||||
though.
|
||||
|
||||
Now let's look for position. In my 2-character map, they're at 65,50 and 70,55.
|
||||
Within a character, I see those numbers repeated twice - around `0x1b{9,a}` and
|
||||
`0x1b{d,e}`. This may be some kind of multiple-squares-taken-up thing.
|
||||
|
||||
Adding a (tall) Lord of Change to the map gave me `02 36 45 00 02 37 45`, which
|
||||
doesn't quite match what my eyes are telling me for Z,Y,X. In addition, the data
|
||||
immediately after this offset changed into a large number of coordinate-like
|
||||
sets of values - far too many for it to actually be a bounding box. However, the
|
||||
first one remains good as a position specifier.
|
||||
|
||||
Down in `0x679` (Chaos Sorcerer) or `0x68D` (Lord of Change), the map coords for
|
||||
the *other* character appears, which is downright odd. For now, just use the
|
||||
first-indexed value.
|
||||
|
||||
Thingies next: these aren't decoded at all yet, and the sizes seem to be
|
||||
variable.
|
||||
|
||||
| Offset | Size | Meaning |
|
||||
| ------ | ---- | ------- |
|
||||
| | | |
|
||||
|
||||
|
||||
Finally, the "trailer trailer", for want of a better term, seems to be organised
|
||||
as:
|
||||
|
||||
| Offset | Size | Meaning |
|
||||
| ----- | ---- | ------- |
|
||||
| 0 | 255 | Title |
|
||||
| 255 | 2048 | Briefing |
|
||||
| 2304 | 85 | ??? - each byte is 1 or 0. Spaced so it may be partly uint32 |
|
||||
|
||||
This duplicates the information found in the `.TXT` files. No idea what the end
|
||||
data is yet.
|
||||
|
||||
## Soldiers At War
|
||||
|
||||
All the above applies to Chaos Gate maps. Maps for Soldiers At War seem to have
|
||||
a lot of similarities, but also some differences. For a start, the maps are a
|
||||
variable size!
|
||||
|
||||
Starting with the header, given a tiny 26x20 generated map, the first 256 bytes
|
||||
look like this:
|
||||
|
||||
```
|
||||
00000000: 1500414d 425f4d41 50005041 52495300 ..AMB_MAP.PARIS.
|
||||
00000010: 00000000 00000000 00000000 00000000 ................
|
||||
00000020: 00000000 00000000 00000000 00000000 ................
|
||||
00000030: 00000000 00000000 00000000 00000000 ................
|
||||
00000040: 00000000 00000000 00000000 00000000 ................
|
||||
00000050: 00000000 00000000 00000000 00000000 ................
|
||||
00000060: 00000000 00000000 00000000 00000000 ................
|
||||
00000070: 00000000 00000000 00000000 00000000 ................
|
||||
00000080: 00000000 00000000 00001e00 45000100 ............E...
|
||||
00000090: 1f004600 10010000 52000000 00001b00 ..F.....R.......
|
||||
000000a0: 38000100 00000500 0a000001 00f0f9ff 8...............
|
||||
000000b0: ffb60500 00000100 ff370a00 64006400 .........7..d.d.
|
||||
000000c0: 08008501 00000000 00ff0000 1f008082 ................
|
||||
000000d0: 01000000 0000ff00 001f0080 84010000 ................
|
||||
000000e0: 000000ff 00001f00 00810100 00000000 ................
|
||||
000000f0: ff00001f 00808301 00000000 00ff0000 ................
|
||||
```
|
||||
|
||||
Almost everything we knew is out of the window, but a few things look familiar.
|
||||
First, the header seems simplified down to just two recognisable-at-first-glance
|
||||
fields: Magic bytes (now `\x15\x00AMV_MAP\x00`) and the set name, coming
|
||||
immediately after.
|
||||
|
||||
Like Chaos Gate, all map files are the same size once uncompressed, but they are
|
||||
smaller - at 1,214,559 bytes, they are 76% the size. This is quite significant.
|
||||
We now have 13.3 bytes per voxel, rather than the 17.5 bytes per voxel that was
|
||||
available to Chaos Gate. This means that the number of bytes *per cell* must be
|
||||
reduced, in addition to the header (and trailer?) values.
|
||||
|
||||
Looking at data from 0x110, it seems to group naturally into 13-byte records:
|
||||
|
||||
```
|
||||
$ xxd -s 0x110 -c 13 -l 65 -g 1 TINYMAP.MAP
|
||||
00000110: 80 01 00 00 00 00 00 ff 00 00 1f 00 00 .............
|
||||
0000011d: 85 01 00 00 00 00 00 ff 00 00 1f 00 00 .............
|
||||
0000012a: 82 01 00 00 00 00 00 ff 00 00 1f 00 80 .............
|
||||
00000137: 82 01 00 00 00 00 00 ff 00 00 1f 00 00 .............
|
||||
00000144: 82 01 00 00 00 00 00 ff 00 00 1f 00 80 .............
|
||||
```
|
||||
|
||||
It's a strange number. Chaos Gate cells group nicely on 16 bytes:
|
||||
|
||||
```
|
||||
$ xxd -s 0x110 -c 16 -l 64 -g 1 Chapter01.MAP
|
||||
00000110: 3f 00 00 00 83 01 00 00 00 00 00 ff 00 00 00 00 ?...............
|
||||
00000120: 38 00 00 00 85 01 00 00 00 00 00 ff 00 00 00 00 8...............
|
||||
00000130: 38 00 00 00 84 01 00 00 00 00 00 ff 00 00 00 00 8...............
|
||||
00000140: 38 00 00 00 8a 01 00 00 00 00 00 ff 00 00 00 00 8...............
|
||||
00000150: 38 00 00 00 83 01 00 00 00 00 00 ff 00 00 00 00 8...............
|
||||
```
|
||||
|
||||
That grouping is very enticing, though. I feel strongly that it's the right
|
||||
number.
|
||||
|
||||
Now we need to ask about start offset. Where is byte 0 of the per-cell data, and
|
||||
do the 13 bytes it has line up neatly to the functions of some of the 16 bytes
|
||||
seen in Chaos Gate?
|
||||
|
||||
I generated a `BIGGESTMAP` (130x100) to investigate. It's just grass, nothing
|
||||
but grass, and 0xC0 is the first offset where it starts to look nicely grouped:
|
||||
|
||||
```
|
||||
xxd -s 0xc0 -c 13 -l 260 -g 13 BIGGESTMAP.MAP
|
||||
000000c0: 08 80 81 01 00 00 00 00 00 ff 00 00 1f .............
|
||||
000000cd: 00 80 81 01 00 00 00 00 00 ff 00 00 1f .............
|
||||
000000da: 00 00 81 01 00 00 00 00 00 ff 00 00 1f .............
|
||||
000000e7: 00 00 85 01 00 00 00 00 00 ff 00 00 1f .............
|
||||
# ...
|
||||
```
|
||||
|
||||
This can be interpreted more or less the same way as the Chaos Gate maps now,
|
||||
and the `soldiers-at-war` branch contains a hacked-up implementation that kind
|
||||
of works \o/.
|
||||
|
||||
Does the same trailer apply? Seemingly not. Looking at `PARIS.MAP`, there's no
|
||||
similarity at first glance.
|
||||
|
||||
However, I did manage to track down 4 32-bit ints inside the trailer, starting
|
||||
at `0x121ad1`, which specify dimensions of the map, at least. Perhaps the
|
||||
position has moved, but some of the data is the same? It's 3320 bytes into the
|
||||
trailer.
|
||||
|
35
go.mod
35
go.mod
@@ -1,18 +1,33 @@
|
||||
module code.ur.gs/lupine/ordoor
|
||||
|
||||
go 1.12
|
||||
go 1.22.0
|
||||
|
||||
toolchain go1.23.2
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v0.3.1
|
||||
github.com/BurntSushi/toml v1.4.0
|
||||
github.com/emef/bitfield v0.0.0-20170503144143-7d3f8f823065
|
||||
github.com/hajimehoshi/ebiten v1.11.0
|
||||
github.com/jfreymuth/oggvorbis v1.0.1 // indirect
|
||||
github.com/hajimehoshi/ebiten/v2 v2.8.2
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||
github.com/stretchr/testify v1.5.1
|
||||
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5 // indirect
|
||||
golang.org/x/image v0.0.0-20200119044424-58c23975cae1
|
||||
golang.org/x/mobile v0.0.0-20200329125638-4c31acba0007 // indirect
|
||||
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
||||
github.com/samuel/go-pcx v0.0.0-20210515040514-6a5ce4d132f7
|
||||
github.com/stretchr/testify v1.9.0
|
||||
golang.org/x/image v0.21.0
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/ebitengine/gomobile v0.0.0-20241016134836-cc2e38a7c0ee // indirect
|
||||
github.com/ebitengine/hideconsole v1.0.0 // indirect
|
||||
github.com/ebitengine/oto/v3 v3.3.1 // indirect
|
||||
github.com/ebitengine/purego v0.8.1 // indirect
|
||||
github.com/jezek/xgb v1.1.1 // indirect
|
||||
github.com/jfreymuth/oggvorbis v1.0.5 // indirect
|
||||
github.com/jfreymuth/vorbis v1.0.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
131
go.sum
131
go.sum
@@ -1,107 +1,48 @@
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
|
||||
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/ebitengine/gomobile v0.0.0-20241016134836-cc2e38a7c0ee h1:YoNt0DHeZ92kjR78SfyUn1yEf7KnBypOFlFZO14cJ6w=
|
||||
github.com/ebitengine/gomobile v0.0.0-20241016134836-cc2e38a7c0ee/go.mod h1:ZDIonJlTRW7gahIn5dEXZtN4cM8Qwtlduob8cOCflmg=
|
||||
github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj1yReDqE=
|
||||
github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A=
|
||||
github.com/ebitengine/oto/v3 v3.3.1 h1:d4McwGQuXOT0GL7bA5g9ZnaUEIEjQvG3hafzMy+T3qE=
|
||||
github.com/ebitengine/oto/v3 v3.3.1/go.mod h1:MZeb/lwoC4DCOdiTIxYezrURTw7EvK/yF863+tmBI+U=
|
||||
github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
|
||||
github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/emef/bitfield v0.0.0-20170503144143-7d3f8f823065 h1:7QVNyw2v9R1qOvbe9vfeVJWWKCSnd2Ap+8l8/CtG9LM=
|
||||
github.com/emef/bitfield v0.0.0-20170503144143-7d3f8f823065/go.mod h1:uN4GbWHfit2ByfOKQ4K6fuLy1/Os2eLynsIrDvjiDgM=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72 h1:b+9H1GAsx5RsjvDFLoS5zkNBzIQMuVKUYQDmxU3N5XE=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/gofrs/flock v0.7.1 h1:DP+LD/t0njgoPBvT5MJLeliUIVQR03hiKR6vezdwHlc=
|
||||
github.com/gofrs/flock v0.7.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/hajimehoshi/bitmapfont v1.2.0/go.mod h1:h9QrPk6Ktb2neObTlAbma6Ini1xgMjbJ3w7ysmD7IOU=
|
||||
github.com/hajimehoshi/ebiten v1.11.0-alpha.2.0.20200101150127-38815ba801a5 h1:hke9UdXY1YPfqjXG1bCSZnoVnfVBw9SzvmlrRn3dL3w=
|
||||
github.com/hajimehoshi/ebiten v1.11.0-alpha.2.0.20200101150127-38815ba801a5/go.mod h1:0SLvfr8iI2NxzpNB/olBM+dLN9Ur5a9szG13wOgQ0nQ=
|
||||
github.com/hajimehoshi/ebiten v1.11.0 h1:+pIxfzfVgRbHGM7wBAJtgzPiWiZopA7lyIKNQqc9amk=
|
||||
github.com/hajimehoshi/ebiten v1.11.0/go.mod h1:aDEhx0K9gSpXw3Cxf2hCXDxPSoF8vgjNqKxrZa/B4Dg=
|
||||
github.com/hajimehoshi/go-mp3 v0.2.1 h1:DH4ns3cPv39n3cs8MPcAlWqPeAwLCK8iNgqvg0QBWI8=
|
||||
github.com/hajimehoshi/go-mp3 v0.2.1/go.mod h1:Rr+2P46iH6PwTPVgSsEwBkon0CK5DxCAeX/Rp65DCTE=
|
||||
github.com/hajimehoshi/oto v0.3.4/go.mod h1:PgjqsBJff0efqL2nlMJidJgVJywLn6M4y8PI4TfeWfA=
|
||||
github.com/hajimehoshi/oto v0.5.4 h1:Dn+WcYeF310xqStKm0tnvoruYUV5Sce8+sfUaIvWGkE=
|
||||
github.com/hajimehoshi/oto v0.5.4/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
|
||||
github.com/jakecoffman/cp v0.1.0/go.mod h1:a3xPx9N8RyFAACD644t2dj/nK4SuLg1v+jL61m2yVo4=
|
||||
github.com/jfreymuth/oggvorbis v1.0.0 h1:aOpiihGrFLXpsh2osOlEvTcg5/aluzGQeC7m3uYWOZ0=
|
||||
github.com/jfreymuth/oggvorbis v1.0.0/go.mod h1:abe6F9QRjuU9l+2jek3gj46lu40N4qlYxh2grqkLEDM=
|
||||
github.com/jfreymuth/oggvorbis v1.0.1 h1:NT0eXBgE2WHzu6RT/6zcb2H10Kxj6Fm3PccT0LE6bqw=
|
||||
github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps1aEZH1sumIk=
|
||||
github.com/jfreymuth/vorbis v1.0.0 h1:SmDf783s82lIjGZi8EGUUaS7YxPHgRj4ZXW/h7rUi7U=
|
||||
github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/hajimehoshi/ebiten/v2 v2.8.2 h1:cvZ5d3LSVFzvcSZVGjTPyV43DzWzJWbwy1b+2V5zJPI=
|
||||
github.com/hajimehoshi/ebiten/v2 v2.8.2/go.mod h1:SXx/whkvpfsavGo6lvZykprerakl+8Uo1X8d2U5aAnA=
|
||||
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
|
||||
github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
|
||||
github.com/jfreymuth/oggvorbis v1.0.5 h1:u+Ck+R0eLSRhgq8WTmffYnrVtSztJcYrl588DM4e3kQ=
|
||||
github.com/jfreymuth/oggvorbis v1.0.5/go.mod h1:1U4pqWmghcoVsCJJ4fRBKv9peUJMBHixthRlBeD6uII=
|
||||
github.com/jfreymuth/vorbis v1.0.2 h1:m1xH6+ZI4thH927pgKD8JOH4eaGRm18rEE9/0WKjvNE=
|
||||
github.com/jfreymuth/vorbis v1.0.2/go.mod h1:DoftRo4AznKnShRl1GxiTFCseHr4zR9BN3TWXyuzrqQ=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
|
||||
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU=
|
||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
|
||||
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5 h1:FR+oGxGfbQu1d+jglI3rCkjAjUnhRSZcUxr+DqlDLNo=
|
||||
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20200119044424-58c23975cae1 h1:5h3ngYt7+vXCDZCup/HkCQgW5XwmSvR/nA2JmJ0RErg=
|
||||
golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mobile v0.0.0-20191025110607-73ccc5ba0426 h1:8RjY2wWN6kjy6JvJjDPT51tx4ht4+ldy/a5Yw0AyEr4=
|
||||
golang.org/x/mobile v0.0.0-20191025110607-73ccc5ba0426/go.mod h1:p895TfNkDgPEmEQrNiOtIl3j98d/tGU95djDj7NfyjQ=
|
||||
golang.org/x/mobile v0.0.0-20200222142934-3c8601c510d0/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4=
|
||||
golang.org/x/mobile v0.0.0-20200329125638-4c31acba0007 h1:JxsyO7zPDWn1rBZW8FV5RFwCKqYeXnyaS/VQPLpXu6I=
|
||||
golang.org/x/mobile v0.0.0-20200329125638-4c31acba0007/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d h1:nc5K6ox/4lTFbMVSL9WRR81ixkcwXThoiF6yf+R9scA=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 h1:opSr2sbRXk5X5/givKrrKj9HXxFpW2sdCiP8MJSKLQY=
|
||||
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190909214602-067311248421/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191026034945-b2104f82a97d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117220505-0cba7a3a9ee9/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
github.com/samuel/go-pcx v0.0.0-20210515040514-6a5ce4d132f7 h1:WhAiClm3vGzSl2EWdFsCFBEu2jEhHGa8qGsz4iIEpRc=
|
||||
github.com/samuel/go-pcx v0.0.0-20210515040514-6a5ce4d132f7/go.mod h1:8ofl4LzpDayZKQZYbUyCDW41Y6lgVoO02ABp57OASxY=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s=
|
||||
golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
|
||||
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
@@ -1,6 +1,9 @@
|
||||
package assetstore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.ur.gs/lupine/ordoor/internal/data"
|
||||
"code.ur.gs/lupine/ordoor/internal/idx"
|
||||
)
|
||||
|
||||
@@ -46,24 +49,37 @@ func (a *AssetStore) AnimationsObject() (*Object, error) {
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
func (a *AssetStore) Animation(groupIdx, recIdx int) (*Animation, error) {
|
||||
idx, err := a.AnimationsIndex()
|
||||
func (a *AssetStore) Animation(groupIdx int, recId int, compass int) (*Animation, error) {
|
||||
realIdx, err := a.AnimationsIndex()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// FIXME: are we using the right value if we need to make this change?
|
||||
if compass == 0 {
|
||||
compass = 8
|
||||
}
|
||||
|
||||
obj, err := a.AnimationsObject()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
group := idx.Groups[groupIdx]
|
||||
group := realIdx.Groups[groupIdx]
|
||||
if group.Spec.Count == 0 {
|
||||
return &Animation{}, nil
|
||||
}
|
||||
|
||||
// rec := group.Records[recIdx]
|
||||
det := group.Details[recIdx]
|
||||
var det *idx.Detail
|
||||
for i, rec := range group.Records {
|
||||
if /*int(rec.ActionID) == int(action) && */ int(rec.Compass) == compass {
|
||||
det = &group.Details[i]
|
||||
}
|
||||
}
|
||||
|
||||
if det == nil {
|
||||
return nil, fmt.Errorf("Couldn't find anim (%v %v %v)", groupIdx, recId, compass)
|
||||
}
|
||||
|
||||
first := int(group.Spec.SpriteIdx) + int(det.FirstSprite)
|
||||
last := int(group.Spec.SpriteIdx) + int(det.LastSprite)
|
||||
@@ -76,3 +92,48 @@ func (a *AssetStore) Animation(groupIdx, recIdx int) (*Animation, error) {
|
||||
|
||||
return &Animation{Frames: sprites}, nil
|
||||
}
|
||||
|
||||
func (a *AssetStore) CharacterAnimation(ctype data.CharacterType, action data.AnimAction, compass int) (*Animation, error) {
|
||||
ha, err := a.HasAction()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !ha.Check(ctype, action) {
|
||||
return nil, fmt.Errorf("character %s: animation %s: not available", ctype, action)
|
||||
}
|
||||
|
||||
// FIXME: we still need to be able to go from CTYPE to GROUP. In particular,
|
||||
// squad leaders seem to be a modification on top of a previous group, which
|
||||
// is a bit awkward. For now, hardcode it. How are captain modifiers stored?
|
||||
group, ok := map[data.CharacterType]int{
|
||||
data.CharacterTypeTactical: 1, // Has captain
|
||||
data.CharacterTypeAssault: 3, // Has captain
|
||||
data.CharacterTypeDevastator: 5,
|
||||
data.CharacterTypeTerminator: 6, // Has captain
|
||||
data.CharacterTypeApothecary: 8,
|
||||
data.CharacterTypeTechmarine: 9,
|
||||
data.CharacterTypeChaplain: 10,
|
||||
data.CharacterTypeLibrarian: 11,
|
||||
data.CharacterTypeCaptain: 12,
|
||||
data.CharacterTypeChaosMarine: 13,
|
||||
data.CharacterTypeChaosLord: 14,
|
||||
data.CharacterTypeChaosChaplain: 15,
|
||||
data.CharacterTypeChaosSorcerer: 16,
|
||||
data.CharacterTypeChaosTerminator: 17,
|
||||
data.CharacterTypeKhorneBerserker: 18,
|
||||
data.CharacterTypeBloodThirster: 19, // This is a rotating thing?
|
||||
data.CharacterTypeBloodLetter: 20,
|
||||
data.CharacterTypeFleshHound: 21,
|
||||
data.CharacterTypeLordOfChange: 22, // Another rotating thing?
|
||||
data.CharacterTypeFlamer: 23,
|
||||
data.CharacterTypePinkHorror: 24,
|
||||
data.CharacterTypeBlueHorror: 25,
|
||||
data.CharacterTypeChaosCultist: 26,
|
||||
}[ctype]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Unknown character type: %s", ctype)
|
||||
}
|
||||
|
||||
return a.Animation(group, int(action), compass)
|
||||
}
|
||||
|
@@ -2,24 +2,25 @@ package assetstore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
|
||||
"code.ur.gs/lupine/ordoor/internal/config"
|
||||
"code.ur.gs/lupine/ordoor/internal/data"
|
||||
"code.ur.gs/lupine/ordoor/internal/idx"
|
||||
)
|
||||
|
||||
const (
|
||||
RootDir = "" // Used in the entryMap for entries pertaining to the root dir
|
||||
"code.ur.gs/lupine/ordoor/internal/palettes"
|
||||
)
|
||||
|
||||
type entryMap map[string]map[string]string
|
||||
|
||||
// type AssetStore is responsible for lazily loading game data when it is
|
||||
// required. Applications shouldn't need to do anything except set one of these
|
||||
// up, pointing at the game dir root, to access all assets.
|
||||
// up, pointing at the game dir root, to access all assets for that game.
|
||||
//
|
||||
// Assets should be loaded on-demand to keep memory costs as low as possible.
|
||||
// Cross-platform differences such as filename case sensitivity are also dealt
|
||||
@@ -27,8 +28,12 @@ type entryMap map[string]map[string]string
|
||||
//
|
||||
// We assume the directory is read-only. You can run Refresh() if you make a
|
||||
// change.
|
||||
//
|
||||
// To mix assets from different games, either construct a synthetic directory
|
||||
// or instantiate two separate asset stores.
|
||||
type AssetStore struct {
|
||||
RootDir string
|
||||
Palette color.Palette
|
||||
|
||||
// Case-insensitive file lookup.
|
||||
// {"":{"anim":"Anim", "obj":"Obj", ...}, "anim":{ "warhammer.ani":"WarHammer.ani" }, ...}
|
||||
@@ -40,7 +45,9 @@ type AssetStore struct {
|
||||
cursors map[CursorName]*Cursor
|
||||
fonts map[string]*Font
|
||||
generic *data.Generic
|
||||
hasAction *data.HasAction
|
||||
idx *idx.Idx
|
||||
images map[string]*ebiten.Image
|
||||
maps map[string]*Map
|
||||
menus map[string]*Menu
|
||||
objs map[string]*Object
|
||||
@@ -50,9 +57,19 @@ type AssetStore struct {
|
||||
}
|
||||
|
||||
// New returns a new AssetStore
|
||||
func New(dir string) (*AssetStore, error) {
|
||||
func New(engine *config.Engine) (*AssetStore, error) {
|
||||
if engine == nil {
|
||||
return nil, fmt.Errorf("Unconfigured engine passed to assetstore")
|
||||
}
|
||||
|
||||
palette, ok := palettes.Get(engine.Palette)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Couldn't find palette %q for engine", engine.Palette)
|
||||
}
|
||||
|
||||
store := &AssetStore{
|
||||
RootDir: dir,
|
||||
RootDir: engine.DataDir,
|
||||
Palette: palette,
|
||||
}
|
||||
|
||||
// fill entryMap
|
||||
@@ -70,7 +87,7 @@ func (a *AssetStore) Refresh() error {
|
||||
}
|
||||
|
||||
newEntryMap := make(entryMap, len(rootEntries))
|
||||
newEntryMap[RootDir] = rootEntries
|
||||
newEntryMap[""] = rootEntries
|
||||
|
||||
for lower, natural := range rootEntries {
|
||||
path := filepath.Join(a.RootDir, natural)
|
||||
@@ -95,7 +112,10 @@ func (a *AssetStore) Refresh() error {
|
||||
a.cursors = make(map[CursorName]*Cursor)
|
||||
a.entries = newEntryMap
|
||||
a.fonts = make(map[string]*Font)
|
||||
a.generic = nil
|
||||
a.hasAction = nil
|
||||
a.idx = nil
|
||||
a.images = make(map[string]*ebiten.Image)
|
||||
a.maps = make(map[string]*Map)
|
||||
a.menus = make(map[string]*Menu)
|
||||
a.objs = make(map[string]*Object)
|
||||
@@ -118,7 +138,7 @@ func (a *AssetStore) lookup(name, ext string, dirs ...string) (string, error) {
|
||||
dir = canonical(dir)
|
||||
if base, ok := a.entries[dir]; ok {
|
||||
if file, ok := base[filename]; ok {
|
||||
actualDir := a.entries[RootDir][dir]
|
||||
actualDir := a.entries[""][dir]
|
||||
return filepath.Join(a.RootDir, actualDir, file), nil
|
||||
}
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@ package assetstore
|
||||
import (
|
||||
"image"
|
||||
|
||||
"github.com/hajimehoshi/ebiten"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
||||
// These are just offsets into the Cursors.cur file
|
||||
|
@@ -66,6 +66,26 @@ func (a *AssetStore) DefaultOptions() (*config.Options, error) {
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (a *AssetStore) HasAction() (*data.HasAction, error) {
|
||||
if a.hasAction != nil {
|
||||
return a.hasAction, nil
|
||||
}
|
||||
|
||||
filename, err := a.lookup("HasAction", "dat", "Data")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hasAction, err := data.LoadHasAction(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a.hasAction = hasAction
|
||||
|
||||
return hasAction, nil
|
||||
}
|
||||
|
||||
func intToBool(i int) bool {
|
||||
return i > 0
|
||||
}
|
||||
|
39
internal/assetstore/image.go
Normal file
39
internal/assetstore/image.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package assetstore
|
||||
|
||||
import (
|
||||
"image"
|
||||
"os"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
_ "github.com/samuel/go-pcx/pcx" // PCX support
|
||||
)
|
||||
|
||||
func (a *AssetStore) Image(name string) (*ebiten.Image, error) {
|
||||
name = canonical(name)
|
||||
if img, ok := a.images[name]; ok {
|
||||
return img, nil
|
||||
}
|
||||
|
||||
// baps, ordoor, geas store .pcx files in Pic
|
||||
// TODO: SL stores .bmp files in Res
|
||||
filename, err := a.lookup(name, "pcx", "Pic")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
rawImg, _, err := image.Decode(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
img := ebiten.NewImageFromImage(rawImg)
|
||||
|
||||
a.images[name] = img
|
||||
return img, nil
|
||||
}
|
@@ -1,9 +1,11 @@
|
||||
package assetstore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"log"
|
||||
|
||||
"code.ur.gs/lupine/ordoor/internal/data"
|
||||
"code.ur.gs/lupine/ordoor/internal/maps"
|
||||
)
|
||||
|
||||
@@ -46,12 +48,7 @@ func (a *AssetStore) Map(name string) (*Map, error) {
|
||||
}
|
||||
|
||||
m := &Map{
|
||||
Rect: image.Rect(
|
||||
int(raw.MinWidth),
|
||||
int(raw.MinLength),
|
||||
int(raw.MaxWidth),
|
||||
int(raw.MaxLength),
|
||||
),
|
||||
Rect: raw.Rect(),
|
||||
assets: a,
|
||||
raw: raw,
|
||||
set: set,
|
||||
@@ -64,8 +61,8 @@ func (a *AssetStore) Map(name string) (*Map, error) {
|
||||
|
||||
func (m *Map) LoadSprites() error {
|
||||
// Eager load the sprites we use
|
||||
for x := m.Rect.Min.X; x <= m.Rect.Max.X; x++ {
|
||||
for y := m.Rect.Min.Y; y <= m.Rect.Max.Y; y++ {
|
||||
for x := m.Rect.Min.X; x < m.Rect.Max.X; x++ {
|
||||
for y := m.Rect.Min.Y; y < m.Rect.Max.Y; y++ {
|
||||
for z := 0; z < maps.MaxHeight; z++ {
|
||||
if _, err := m.SpritesForCell(x, y, z); err != nil {
|
||||
return err
|
||||
@@ -78,8 +75,8 @@ func (m *Map) LoadSprites() error {
|
||||
}
|
||||
|
||||
// FIXME: get rid of this
|
||||
func (m *Map) Cell(x, y, z int) maps.Cell {
|
||||
return m.raw.Cells.At(x, y, z)
|
||||
func (m *Map) Cell(x, y, z int) *maps.Cell {
|
||||
return m.raw.At(x, y, z)
|
||||
}
|
||||
|
||||
// SpritesForCell returns the sprites needed to correctly render this cell.
|
||||
@@ -95,7 +92,7 @@ func (m *Map) SpritesForCell(x, y, z int) ([]*Sprite, error) {
|
||||
|
||||
obj, err := m.set.Object(ref.Index())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("Failed to get object for %#+v: %v", ref, err)
|
||||
}
|
||||
|
||||
sprite, err := obj.Sprite(ref.Sprite())
|
||||
@@ -105,6 +102,27 @@ func (m *Map) SpritesForCell(x, y, z int) ([]*Sprite, error) {
|
||||
|
||||
sprites = append(sprites, sprite)
|
||||
}
|
||||
if chr := m.CharacterAt(x, y, z); chr != nil {
|
||||
// Look up the correct animation, get the frame, boom shakalaka
|
||||
anim, err := m.assets.CharacterAnimation(chr.Type, data.AnimActionNone, int(chr.Orientation))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sprites = append(sprites, anim.Frames[0])
|
||||
}
|
||||
|
||||
return sprites, nil
|
||||
}
|
||||
|
||||
func (m *Map) CharacterAt(x, y, z int) *maps.Character {
|
||||
// FIXME: don't iterate
|
||||
for i, _ := range m.raw.Characters {
|
||||
chr := &m.raw.Characters[i]
|
||||
if chr.XPos == x && chr.YPos == y && z == 0 { // FIXME: sort out ZPos
|
||||
return chr
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@@ -1,7 +1,9 @@
|
||||
package assetstore
|
||||
|
||||
import (
|
||||
"github.com/hajimehoshi/ebiten"
|
||||
"log"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
|
||||
"code.ur.gs/lupine/ordoor/internal/menus"
|
||||
)
|
||||
@@ -60,7 +62,7 @@ func (a *AssetStore) Menu(name string) (*Menu, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
raw, err := menus.LoadMenu(filename)
|
||||
raw, err := menus.LoadMenu(filename, a.Palette)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -77,11 +79,11 @@ func (a *AssetStore) Menu(name string) (*Menu, error) {
|
||||
|
||||
i18n, err := a.i18n()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
log.Printf("Failed to load i18n data, skipping internationalisatoin: %s", err)
|
||||
} else {
|
||||
raw.Internationalize(i18n)
|
||||
}
|
||||
|
||||
raw.Internationalize(i18n)
|
||||
|
||||
// FIXME: we should parse the menu into a list of elements like "ListBox",
|
||||
// "Dialogue", etc, and present those with objects already selected
|
||||
objects, err := a.loadMenuObjects(raw)
|
||||
|
@@ -7,7 +7,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/hajimehoshi/ebiten"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
|
||||
"code.ur.gs/lupine/ordoor/internal/data"
|
||||
)
|
||||
@@ -42,7 +42,7 @@ func (a *AssetStore) Object(name string) (*Object, error) {
|
||||
}
|
||||
log.Printf("Loading object %v", name)
|
||||
|
||||
filename, err := a.lookup(name, "obj", "Obj")
|
||||
filename, err := a.lookup(name, "obj", "Obj", "spr")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -117,10 +117,7 @@ func (o *Object) Sprite(idx int) (*Sprite, error) {
|
||||
}
|
||||
|
||||
raw := o.raw.Sprites[idx]
|
||||
img, err := ebiten.NewImageFromImage(raw.ToImage(), ebiten.FilterDefault)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
img := ebiten.NewImageFromImage(raw.ToImage(o.assets.Palette))
|
||||
|
||||
rect := image.Rect(
|
||||
int(raw.XOffset),
|
||||
|
@@ -4,8 +4,8 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/audio"
|
||||
"github.com/hajimehoshi/ebiten/audio/vorbis"
|
||||
"github.com/hajimehoshi/ebiten/v2/audio"
|
||||
"github.com/hajimehoshi/ebiten/v2/audio/vorbis"
|
||||
)
|
||||
|
||||
type Sound struct {
|
||||
@@ -57,7 +57,7 @@ func (s *Sound) InfinitePlayer() (*audio.Player, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
infinite := audio.NewInfiniteLoop(decoder, decoder.Size())
|
||||
infinite := audio.NewInfiniteLoop(decoder, decoder.Length())
|
||||
|
||||
return audio.NewPlayer(audio.CurrentContext(), infinite)
|
||||
}
|
||||
|
@@ -2,15 +2,16 @@ package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
type Ordoor struct {
|
||||
DataDir string `toml:"data_dir"`
|
||||
VideoPlayer []string `toml:"video_player"`
|
||||
type Engine struct {
|
||||
DataDir string `toml:"data_dir"`
|
||||
Palette string `toml:"palette"`
|
||||
}
|
||||
|
||||
// Things set in the options hash
|
||||
@@ -38,12 +39,40 @@ type Options struct {
|
||||
type Config struct {
|
||||
filename string `toml:"-"`
|
||||
|
||||
VideoPlayer []string `toml:"video_player"`
|
||||
Engines map[string]Engine `toml:"engines"`
|
||||
|
||||
DefaultEngineName string `toml:"default_engine"`
|
||||
|
||||
// FIXME: options may well end up being per-engine too
|
||||
Defaults *Options `toml:"-"`
|
||||
Ordoor `toml:"ordoor"`
|
||||
Options `toml:"options"`
|
||||
}
|
||||
|
||||
func Load(filename string) (*Config, error) {
|
||||
func (c *Config) Engine(name string) *Engine {
|
||||
engine, ok := c.Engines[name]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &engine
|
||||
}
|
||||
|
||||
func (c *Config) DefaultEngine() *Engine {
|
||||
return c.Engine(c.DefaultEngineName)
|
||||
}
|
||||
|
||||
// TODO: case-insensitive lookup
|
||||
func (c *Config) DataFile(engine string, path string) string {
|
||||
cfg, ok := c.Engines[engine]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
return filepath.Join(cfg.DataDir, path)
|
||||
}
|
||||
|
||||
func Load(filename string, overrideDefaultEngine string) (*Config, error) {
|
||||
var out Config
|
||||
|
||||
_, err := toml.DecodeFile(filename, &out)
|
||||
@@ -53,7 +82,15 @@ func Load(filename string) (*Config, error) {
|
||||
|
||||
out.filename = filename
|
||||
|
||||
return &out, err
|
||||
if overrideDefaultEngine != "" {
|
||||
out.DefaultEngineName = overrideDefaultEngine
|
||||
}
|
||||
|
||||
if out.DefaultEngine() == nil {
|
||||
return nil, fmt.Errorf("Default engine %q not configured", out.DefaultEngineName)
|
||||
}
|
||||
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (c *Config) HasUnsetOptions() bool {
|
||||
@@ -72,11 +109,6 @@ func (c *Config) Save() error {
|
||||
return toml.NewEncoder(f).Encode(c)
|
||||
}
|
||||
|
||||
// TODO: case-insensitive lookup
|
||||
func (c *Config) DataFile(path string) string {
|
||||
return filepath.Join(c.DataDir, path)
|
||||
}
|
||||
|
||||
func (c *Config) ResetDefaults() error {
|
||||
if c.Defaults == nil {
|
||||
return errors.New("Defaults not available")
|
||||
|
@@ -35,7 +35,7 @@ const (
|
||||
AnimActionRun AnimAction = 14
|
||||
AnimActionCrouch AnimAction = 15
|
||||
AnimActionStand AnimAction = 16
|
||||
AnimActionStandingRead AnimAction = 17
|
||||
AnimActionStandingReady AnimAction = 17
|
||||
AnimActionStandingUnready AnimAction = 18
|
||||
AnimActionCrouchingReady AnimAction = 19
|
||||
AnimActionCrouchingUnready AnimAction = 20
|
||||
@@ -94,6 +94,89 @@ type HasAction struct {
|
||||
bits bitfield.BitField
|
||||
}
|
||||
|
||||
var (
|
||||
aActions = map[AnimAction]string{
|
||||
AnimActionNone: "None",
|
||||
AnimActionAnim: "Anim",
|
||||
AnimActionWalk: "Walk",
|
||||
AnimActionExplosion: "Explosion",
|
||||
AnimActionProjectile: "Projectile",
|
||||
AnimActionSmoke: "Smoke",
|
||||
AnimActionStandingShoot: "Standing Shoot",
|
||||
AnimActionStandingDeath: "Standing Death",
|
||||
AnimActionPain: "Pain",
|
||||
AnimActionSpellFx1: "Spell FX 1",
|
||||
AnimActionSpellFx2: "Spell FX 2",
|
||||
AnimActionSpellFx3: "Spell FX 3",
|
||||
AnimActionSpellFx4: "Spell FX 4",
|
||||
AnimActionSpellFx5: "Spell FX 5",
|
||||
AnimActionRun: "Run",
|
||||
AnimActionCrouch: "Crouch",
|
||||
AnimActionStand: "Stand",
|
||||
AnimActionStandingReady: "Standing Ready",
|
||||
AnimActionStandingUnready: "Standing Unready",
|
||||
AnimActionCrouchingReady: "Crouching Ready",
|
||||
AnimActionCrouchingUnready: "Crouching Unready",
|
||||
AnimActionCrouchingShoot: "Crouching Shoot",
|
||||
AnimActionStandingGrenade: "Standing Grenade",
|
||||
AnimActionCrouchingGrenade: "Crouching Grenade",
|
||||
AnimActionDrawMelee: "Draw Melee",
|
||||
AnimActionSlash: "Slash",
|
||||
AnimActionStab: "Stab",
|
||||
AnimActionBlown: "Blown",
|
||||
AnimActionCrouchingDeath: "Crouching Death",
|
||||
AnimActionJump: "Jump",
|
||||
AnimActionHeal: "Heal",
|
||||
AnimActionTechWork: "Tech Work",
|
||||
AnimActionCast: "Cast",
|
||||
AnimActionShoot: "Shoot",
|
||||
AnimActionDeath: "Death",
|
||||
AnimActionFromWarp: "From Warp",
|
||||
}
|
||||
|
||||
cTypes = map[CharacterType]string{
|
||||
CharacterTypeTactical: "Tactical",
|
||||
CharacterTypeAssault: "Assault",
|
||||
CharacterTypeDevastator: "Devastator",
|
||||
CharacterTypeTerminator: "Terminator",
|
||||
CharacterTypeApothecary: "Apothecary",
|
||||
CharacterTypeTechmarine: "Techmarine",
|
||||
CharacterTypeChaplain: "Chaplain",
|
||||
CharacterTypeLibrarian: "Librarian",
|
||||
CharacterTypeCaptain: "Captain",
|
||||
CharacterTypeChaosMarine: "Chaos Marine",
|
||||
CharacterTypeChaosLord: "Chaos Lord",
|
||||
CharacterTypeChaosChaplain: "Chaos Chaplain",
|
||||
CharacterTypeChaosSorcerer: "Chaos Sorcerer",
|
||||
CharacterTypeChaosTerminator: "Chaos Terminator",
|
||||
CharacterTypeKhorneBerserker: "Knorne Berserker",
|
||||
CharacterTypeBloodThirster: "Bloodthirster",
|
||||
CharacterTypeBloodLetter: "Bloodletter",
|
||||
CharacterTypeFleshHound: "Flesh Hound",
|
||||
CharacterTypeLordOfChange: "Lord of Change",
|
||||
CharacterTypeFlamer: "Flamer",
|
||||
CharacterTypePinkHorror: "Pink Horror",
|
||||
CharacterTypeBlueHorror: "Blue Horror",
|
||||
CharacterTypeChaosCultist: "Cultist",
|
||||
}
|
||||
)
|
||||
|
||||
func (a AnimAction) String() string {
|
||||
if str, ok := aActions[a]; ok {
|
||||
return str
|
||||
}
|
||||
|
||||
return "Unknown Action"
|
||||
}
|
||||
|
||||
func (c CharacterType) String() string {
|
||||
if str, ok := cTypes[c]; ok {
|
||||
return str
|
||||
}
|
||||
|
||||
return "Unknown Character"
|
||||
}
|
||||
|
||||
func LoadHasAction(filename string) (*HasAction, error) {
|
||||
scanner, err := asciiscan.New(filename)
|
||||
if err != nil {
|
||||
@@ -161,6 +244,17 @@ func (h *HasAction) Actions(c CharacterType) []AnimAction {
|
||||
return out
|
||||
}
|
||||
|
||||
// FIXME: Too slow
|
||||
func (h *HasAction) Index(c CharacterType, requestedAction AnimAction) int {
|
||||
for i, action := range h.Actions(c) {
|
||||
if action == requestedAction {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func (h *HasAction) Print() {
|
||||
fmt.Println(" Tac Ass Dev Term Apo Tech Chp Lib Cpt CMar CLrd CChp CSrc CTrm Kbz BTh BL FHnd LoC Flm PHr BHr Cult")
|
||||
for a := AnimActionStart; a <= AnimActionEnd; a++ {
|
||||
|
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
@@ -27,12 +28,16 @@ type SpriteHeader struct {
|
||||
|
||||
func (s SpriteHeader) Check(expectedSize uint32) error {
|
||||
if s.Padding1 != 0 || s.Padding2 != 0 {
|
||||
return fmt.Errorf("Sprite header padding contains unknown values: %d %d", s.Padding1, s.Padding2)
|
||||
if s.Padding1 == 271 && s.Padding2 == 0 {
|
||||
log.Printf("Sprite header padding matches FIXME value")
|
||||
} else {
|
||||
return fmt.Errorf("Sprite header padding contains unknown values: %d %d", s.Padding1, s.Padding2)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: WarHammer.ani sets Unknown1 to this for all 188,286 sprites. I am
|
||||
// very interested in seeing if there are any others
|
||||
if s.Unknown1[0] | s.Unknown1[1] | s.Unknown1[2] | s.Unknown1[3] > 0 {
|
||||
if s.Unknown1[0]|s.Unknown1[1]|s.Unknown1[2]|s.Unknown1[3] > 0 {
|
||||
if s.Unknown1[0] != 212 || s.Unknown1[1] != 113 || s.Unknown1[2] != 59 || s.Unknown1[3] != 1 {
|
||||
log.Printf("Value of Unknown1 field: %v", s.Unknown1)
|
||||
}
|
||||
@@ -52,12 +57,12 @@ type Sprite struct {
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func (s *Sprite) ToImage() *image.Paletted {
|
||||
func (s *Sprite) ToImage(palette color.Palette) *image.Paletted {
|
||||
return &image.Paletted{
|
||||
Pix: s.Data,
|
||||
Stride: int(s.Width),
|
||||
Rect: image.Rect(0, 0, int(s.Width), int(s.Height)),
|
||||
Palette: ColorPalette,
|
||||
Palette: palette,
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -2,6 +2,9 @@ package flow
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
||||
"code.ur.gs/lupine/ordoor/internal/ui"
|
||||
)
|
||||
|
||||
type driverName string
|
||||
@@ -36,8 +39,62 @@ var (
|
||||
configureUltEquip, configureVehiclesUltra,
|
||||
mainGame,
|
||||
}
|
||||
|
||||
menuTransforms = map[driverName]func(*assetstore.Menu){
|
||||
mainGame: offsetMainGame,
|
||||
}
|
||||
)
|
||||
|
||||
// FIXME: HURK: MainGame elements need changes to show up in the right place
|
||||
func offsetMainGame(menu *assetstore.Menu) {
|
||||
for _, group := range menu.Groups() {
|
||||
id := group.ID
|
||||
|
||||
// Bottom-aligned, not top-aligned
|
||||
if id == 1 || id == 2 || id == 3 || id == 4 || id == 5 || id == 6 ||
|
||||
id == 7 || id == 8 || id == 9 || id == 10 || id == 15 || id == 16 {
|
||||
group.Y = 320 // Down by 320px
|
||||
|
||||
// FIXME: in reality, this appears to be a property of the group only
|
||||
for _, rec := range group.Records {
|
||||
rec.Y = 320
|
||||
}
|
||||
}
|
||||
|
||||
// Right-aligned, not left-aligned
|
||||
// FIXME: this presents problems as there are two sizes and both need to
|
||||
// be right-aligned, so a static offset won't quite work
|
||||
// if id == 14 {
|
||||
// group.X = 400 (or so)
|
||||
// }
|
||||
|
||||
// Left-aligned, not centered
|
||||
// FIXME: we're re-using the X-CORD and Y-CORD elements here. How do we
|
||||
// signal a negative number?
|
||||
// if id == 18 {
|
||||
// group.X = 0
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
func buildDriver(assets *assetstore.AssetStore, name driverName) (*ui.Driver, error) {
|
||||
menu, err := assets.Menu(string(name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if tf, ok := menuTransforms[name]; ok {
|
||||
tf(menu)
|
||||
}
|
||||
|
||||
driver, err := ui.NewDriver(assets, menu)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return driver, nil
|
||||
}
|
||||
|
||||
func (f *Flow) returnToLastDriver(from driverName) func() {
|
||||
return func() {
|
||||
to, ok := f.returns[from]
|
||||
|
@@ -6,7 +6,8 @@ import (
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/hajimehoshi/ebiten"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||
|
||||
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
||||
"code.ur.gs/lupine/ordoor/internal/config"
|
||||
@@ -92,18 +93,9 @@ func New(assets *assetstore.AssetStore, config *config.Config, ship *ship.Ship)
|
||||
return out, out.exit
|
||||
}
|
||||
|
||||
func buildDriver(assets *assetstore.AssetStore, name driverName) (*ui.Driver, error) {
|
||||
menu, err := assets.Menu(string(name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
driver, err := ui.NewDriver(assets, menu)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return driver, nil
|
||||
func (f *Flow) SetScenario(scenario *scenario.Scenario) {
|
||||
f.current = f.drivers[mainGame]
|
||||
f.scenario = scenario
|
||||
}
|
||||
|
||||
func (f *Flow) Update(screenX, screenY int) error {
|
||||
@@ -111,6 +103,32 @@ func (f *Flow) Update(screenX, screenY int) error {
|
||||
return f.exit
|
||||
}
|
||||
|
||||
// Keybindings for map control
|
||||
// FIXME: this needs a big rethink
|
||||
if f.current != nil && f.scenario != nil && !f.current.IsInDialogue() {
|
||||
step := 32
|
||||
if ebiten.IsKeyPressed(ebiten.KeyLeft) {
|
||||
f.scenario.Viewpoint.X -= step
|
||||
}
|
||||
if ebiten.IsKeyPressed(ebiten.KeyRight) {
|
||||
f.scenario.Viewpoint.X += step
|
||||
}
|
||||
if ebiten.IsKeyPressed(ebiten.KeyUp) {
|
||||
f.scenario.Viewpoint.Y -= step
|
||||
}
|
||||
if ebiten.IsKeyPressed(ebiten.KeyDown) {
|
||||
f.scenario.Viewpoint.Y += step
|
||||
}
|
||||
|
||||
if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonLeft) {
|
||||
f.scenario.SelectHighlightedCharacter()
|
||||
|
||||
// Now we need to update the info screens with data about the
|
||||
// selected character. FIXME: oh, for data binding
|
||||
f.selectedMainGameCharacter(f.scenario.SelectedCharacter())
|
||||
}
|
||||
}
|
||||
|
||||
if f.scenario != nil {
|
||||
if err := f.scenario.Update(screenX, screenY); err != nil {
|
||||
return err
|
||||
@@ -241,9 +259,37 @@ func (f *Flow) playNextScenario(from driverName) func() {
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Flow) setActive(driver driverName, id string, value bool) func() {
|
||||
return func() {
|
||||
if f.exit != nil {
|
||||
return
|
||||
}
|
||||
|
||||
f.exit = maybeErr(driver, f.setActiveNow(driver, id, value))
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Flow) setActiveNow(driver driverName, id string, value bool) error {
|
||||
return f.drivers[driver].SetActive(locator(driver, id), value)
|
||||
}
|
||||
|
||||
func (f *Flow) toggleActive(driver driverName, id string) func() {
|
||||
return func() {
|
||||
if f.exit != nil {
|
||||
return
|
||||
}
|
||||
|
||||
f.exit = maybeErr(driver, f.drivers[driver].ToggleActive(locator(driver, id)))
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Flow) showDialogue(driver driverName, id string) func() {
|
||||
return func() {
|
||||
f.drivers[driver].ShowDialogue(locator(driver, id))
|
||||
if f.exit != nil {
|
||||
return
|
||||
}
|
||||
|
||||
f.exit = maybeErr(driver, f.drivers[driver].ShowDialogue(locator(driver, id)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,6 +297,14 @@ func (f *Flow) hideDialogue(driver driverName) func() {
|
||||
return f.drivers[driver].HideDialogue
|
||||
}
|
||||
|
||||
func (f *Flow) withScenario(then func()) func() {
|
||||
return func() {
|
||||
if f.scenario != nil {
|
||||
then()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Flow) reset() {
|
||||
if f.exit != nil {
|
||||
return
|
||||
|
@@ -1,55 +1,205 @@
|
||||
package flow
|
||||
|
||||
import (
|
||||
"code.ur.gs/lupine/ordoor/internal/maps"
|
||||
)
|
||||
|
||||
// TODO: There are Chaos and Ultramarine versions of MainGame. Do we really want
|
||||
// to duplicate everything for both?
|
||||
|
||||
func (f *Flow) linkMainGame() {
|
||||
// 3: Action menu
|
||||
|
||||
// 4: Interface options menu
|
||||
f.onClick(mainGame, "4.1", f.setReturningDriver(mainGame, options)) // Options button
|
||||
// 4.2: Map button
|
||||
// 4.3: Mission objectives button
|
||||
// 4.4: Inventory
|
||||
// 4.5: Next man
|
||||
// 4.6: Next enemy
|
||||
// 4.7: Total enemy text
|
||||
|
||||
f.linkMainGameActionMenu()
|
||||
f.linkMainGameInterfaceOptionsMenu()
|
||||
// 5: Holding menu
|
||||
// 6: View menu
|
||||
f.linkMainGameViewMenu()
|
||||
|
||||
// 7: General character menu
|
||||
f.onClick(mainGame, "7.4", func() { // More button
|
||||
f.setActiveNow(mainGame, "7", false)
|
||||
f.setActiveNow(mainGame, "8", true)
|
||||
})
|
||||
|
||||
// 8: Character stats
|
||||
f.onClick(mainGame, "8.21", func() { // Stat more buttons
|
||||
f.setActiveNow(mainGame, "7", true)
|
||||
f.setActiveNow(mainGame, "8", false)
|
||||
})
|
||||
|
||||
// 9: Visible enemy menu
|
||||
// 10: Friendly squad menu
|
||||
// 11: Psyker spell dialogue
|
||||
|
||||
// FIXME: lots and lots and lots of wiring up to do.
|
||||
// For now, just link all the exit buttons to go back to the bridge
|
||||
f.onClick(mainGame, "11.6", func() {
|
||||
f.scenario = nil
|
||||
f.returnToLastDriverNow(mainGame)
|
||||
})
|
||||
|
||||
// 12: Inventory dialogue
|
||||
f.onClick(mainGame, "12.21", func() {
|
||||
f.scenario = nil
|
||||
f.returnToLastDriverNow(mainGame)
|
||||
})
|
||||
f.onClick(mainGame, "12.21", f.hideDialogue(mainGame)) // Exit
|
||||
|
||||
// 13: Exchange menu
|
||||
|
||||
f.onClick(mainGame, "13.1", func() {
|
||||
f.scenario = nil
|
||||
f.returnToLastDriverNow(mainGame)
|
||||
})
|
||||
// 13: exchange menu
|
||||
|
||||
// 14: Map
|
||||
// 14.1: MAP_SPRITE
|
||||
// 14.2: Multiplier button (2x)
|
||||
f.onClick(mainGame, "14.3", f.setActive(mainGame, "14", false))
|
||||
// 14.4: Area
|
||||
|
||||
// 15: Interface wing left
|
||||
// 16: Interface wing right
|
||||
// FIXME: the display of left and right interface buttons is hidden by these
|
||||
// sprites, because we draw in strict numeric order. Just hide them for now.
|
||||
//
|
||||
// FIXME: The child element is already set to hidden, while the menu itself
|
||||
// is set to active, so maybe this is a hint that menus shouldn't be drawn?
|
||||
//
|
||||
// FIXME: the approach taken by the original binary in resolutions greater
|
||||
// than 640x480 is to draw the menu elements *unscaled*. They are centered,
|
||||
// and the dead space is filled by the "interface wing" sprites in the
|
||||
// background. Should we replicate this, or keep with the current scaling
|
||||
// behaviour? Which is better?
|
||||
f.exit = maybeErr(mainGame, f.setActiveNow(mainGame, "15", false)) // Interface wing left
|
||||
f.exit = maybeErr(mainGame, f.setActiveNow(mainGame, "16", false)) // Interface wing right
|
||||
// 17: Grenade dialogue
|
||||
// 18: Info dialogue
|
||||
|
||||
f.onClick(mainGame, "18.12", f.setActive(mainGame, "18", false)) // Info "dialogue"
|
||||
|
||||
// 19: Turn start dialogue
|
||||
// 20: Chat menu
|
||||
// 21: Chat list menu box
|
||||
|
||||
// Chat list menu box - active by default, hide it
|
||||
f.exit = maybeErr(mainGame, f.setActiveNow(mainGame, "21", false))
|
||||
|
||||
}
|
||||
|
||||
func (f *Flow) linkMainGameActionMenu() {
|
||||
// 3: Action menu. These are mostly predicated on selected character state
|
||||
// 3.1: Aimed shot
|
||||
// 3.2: Shooting
|
||||
// 3.3: Walk
|
||||
// 3.4: Run
|
||||
// 3.5: Crouch/Stand
|
||||
// 3.6: Hand to hand (commented out)
|
||||
// 3.7: Retrieve
|
||||
// 3.8: Door
|
||||
// 3.9: Switch
|
||||
// 3.10: Overwatch
|
||||
// 3.11: Rally/Formation
|
||||
// 3.12: Board/Disembark
|
||||
// FIXME: for now, this is "end scenario", for convenience
|
||||
f.onClick(mainGame, "3.13", func() { // End turn button.
|
||||
f.scenario = nil
|
||||
f.returnToLastDriverNow(mainGame)
|
||||
})
|
||||
// 3.14: Special action heal
|
||||
// 3.15: Special action techmarine
|
||||
// 3.16: Special action jump pack
|
||||
// 3.17: Special action spell
|
||||
}
|
||||
|
||||
func (f *Flow) linkMainGameInterfaceOptionsMenu() {
|
||||
// 4: Interface options menu
|
||||
f.onClick(mainGame, "4.1", f.setReturningDriver(mainGame, options)) // Options button
|
||||
|
||||
// FIXME: map should be shown top-right, not top-left. We need to support 2x
|
||||
// mode as well.
|
||||
f.onClick(mainGame, "4.2", f.toggleActive(mainGame, "14")) // Map button
|
||||
|
||||
// FIXME: mission objectives should be shown top-left, not centered
|
||||
f.onClick(mainGame, "4.3", f.toggleActive(mainGame, "18")) // Mission objectives
|
||||
|
||||
f.onClick(mainGame, "4.4", f.showDialogue(mainGame, "12")) // Inventory
|
||||
// 4.5: Next man
|
||||
// 4.6: Next enemy
|
||||
// 4.7: Total enemy text
|
||||
}
|
||||
|
||||
func (f *Flow) linkMainGameViewMenu() {
|
||||
// FIXME: all these buttons should show current state as well as have an
|
||||
// effect
|
||||
f.onClick(mainGame, "6.1", f.withScenario(func() { // View 100%
|
||||
f.scenario.Zoom = 1.0
|
||||
}))
|
||||
|
||||
f.onClick(mainGame, "6.2", f.withScenario(func() { // View 50%
|
||||
f.scenario.Zoom = 0.5
|
||||
}))
|
||||
|
||||
f.onClick(mainGame, "6.3", f.withScenario(func() { // View 25%
|
||||
f.scenario.Zoom = 0.25
|
||||
}))
|
||||
|
||||
f.onClick(mainGame, "6.4", f.withScenario(func() { // Z index up
|
||||
f.scenario.ChangeZIdx(+1)
|
||||
}))
|
||||
|
||||
f.onClick(mainGame, "6.5", f.withScenario(func() { // Z index down
|
||||
f.scenario.ChangeZIdx(-1)
|
||||
}))
|
||||
|
||||
f.onClick(mainGame, "6.6", f.withScenario(func() { // Z index 1
|
||||
f.scenario.ZIdx = 0
|
||||
}))
|
||||
|
||||
f.onClick(mainGame, "6.7", f.withScenario(func() { // Z index 2
|
||||
f.scenario.ZIdx = 1
|
||||
}))
|
||||
|
||||
f.onClick(mainGame, "6.8", f.withScenario(func() { // Z index 3
|
||||
f.scenario.ZIdx = 2
|
||||
}))
|
||||
|
||||
f.onClick(mainGame, "6.9", f.withScenario(func() { // Z index 4
|
||||
f.scenario.ZIdx = 3
|
||||
}))
|
||||
|
||||
f.onClick(mainGame, "6.10", f.withScenario(func() { // Z index 5
|
||||
f.scenario.ZIdx = 4
|
||||
}))
|
||||
|
||||
f.onClick(mainGame, "6.11", f.withScenario(func() { // Z index 6
|
||||
f.scenario.ZIdx = 5
|
||||
}))
|
||||
|
||||
f.onClick(mainGame, "6.12", f.withScenario(func() { // Z index 7
|
||||
f.scenario.ZIdx = 6
|
||||
}))
|
||||
|
||||
}
|
||||
|
||||
func (f *Flow) maybeSetErr(next func() error) {
|
||||
if f.exit != nil {
|
||||
return
|
||||
}
|
||||
|
||||
f.exit = next()
|
||||
}
|
||||
|
||||
func (f *Flow) selectedMainGameCharacter(chr *maps.Character) {
|
||||
if chr == nil {
|
||||
chr = &maps.Character{}
|
||||
}
|
||||
|
||||
d := f.drivers[mainGame]
|
||||
|
||||
// 7.1 Portrait
|
||||
f.maybeSetErr(func() error { return d.SetValue("7.2", chr.Name) }) // Name
|
||||
// 7.3 doesn't exit
|
||||
// 7.4 more button (ignore)
|
||||
// 7.5 AP icon
|
||||
// f.maybeSetErr(func() error { return d.SetValueInt("7.6", chr.ActionPoints)}) // AP meter
|
||||
f.maybeSetErr(func() error { return d.SetValueInt("7.7", chr.ActionPoints) }) // AP value
|
||||
// 7.8 armor icon
|
||||
// 7.9 armor meter
|
||||
f.maybeSetErr(func() error { return d.SetValueInt("7.10", chr.Armor) }) // armor value
|
||||
// 7.11 health icon
|
||||
// 7.12 health meter
|
||||
f.maybeSetErr(func() error { return d.SetValueInt("7.13", chr.Health) }) // health value
|
||||
// 7.14 action points status bar
|
||||
// 7.15 armor status bar
|
||||
// 7.16 health status bar
|
||||
|
||||
// 8.1 to 8.10 are hot spots
|
||||
f.maybeSetErr(func() error { return d.SetValueInt("8.11", chr.ActionPoints) }) // AP
|
||||
f.maybeSetErr(func() error { return d.SetValueInt("8.12", chr.Health) }) // Health
|
||||
f.maybeSetErr(func() error { return d.SetValueInt("8.13", chr.Armor) }) // Armor
|
||||
f.maybeSetErr(func() error { return d.SetValueInt("8.14", chr.BallisticSkill) }) // Ballistic Skill
|
||||
f.maybeSetErr(func() error { return d.SetValueInt("8.15", chr.WeaponSkill) }) // Weapon Skill
|
||||
f.maybeSetErr(func() error { return d.SetValueInt("8.16", chr.Strength) }) // Strength
|
||||
f.maybeSetErr(func() error { return d.SetValueInt("8.17", chr.Toughness) }) // Toughness
|
||||
// 8.18 Initiative
|
||||
// 8.19 Attacks
|
||||
f.maybeSetErr(func() error { return d.SetValueInt("8.20", chr.Leadership) }) // Leadership
|
||||
}
|
||||
|
@@ -5,12 +5,15 @@ import (
|
||||
"compress/gzip"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"image"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/lunixbochs/struc"
|
||||
|
||||
"code.ur.gs/lupine/ordoor/internal/data"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -26,63 +29,135 @@ const (
|
||||
|
||||
CellSize = 16 // seems to be
|
||||
|
||||
cellDataOffset = 0x110 // tentatively
|
||||
cellDataOffset = 0x110 // definitely
|
||||
cellCount = MaxHeight * MaxLength * MaxWidth
|
||||
)
|
||||
|
||||
type Header struct {
|
||||
IsCampaignMap uint32 // Tentatively: 0 = no, 1 = yes
|
||||
MinWidth uint32
|
||||
MinLength uint32
|
||||
MaxWidth uint32
|
||||
MaxLength uint32
|
||||
Unknown1 uint32
|
||||
Unknown2 uint32
|
||||
Unknown3 uint32
|
||||
Unknown4 uint32
|
||||
Magic [8]byte // "\x08\x00WHMAP\x00"
|
||||
Unknown5 uint32
|
||||
Unknown6 uint32
|
||||
SetName [8]byte // Links to a filename in `/Sets/*.set`
|
||||
// Need to investigate the rest of the header too
|
||||
type GameMap struct {
|
||||
// Main Header
|
||||
IsCampaignMap bool `struc:"uint32"` // Tentatively: 0 = no, 1 = yes
|
||||
MinWidth int `struc:"uint32"`
|
||||
MinLength int `struc:"uint32"`
|
||||
MaxWidth int `struc:"uint32"`
|
||||
MaxLength int `struc:"uint32"`
|
||||
Unknown1 int `struc:"uint32"`
|
||||
Unknown2 int `struc:"uint32"`
|
||||
Unknown3 int `struc:"uint32"`
|
||||
Unknown4 int `struc:"uint32"`
|
||||
Magic []byte `struc:"[8]byte"` // "\x08\x00WHMAP\x00"
|
||||
Unknown5 int `struc:"uint32"`
|
||||
Unknown6 int `struc:"uint32"`
|
||||
SetName string `struc:"[8]byte"` // Links to a filename in `/Sets/*.set`
|
||||
Padding []byte `struc:"[212]byte"`
|
||||
|
||||
// Per-cell data
|
||||
NumCells int `struc:"skip"` // FIXME: We can't use []Cell below without this field
|
||||
Cells []Cell `struc:"[]Cell,sizefrom=NumCells"`
|
||||
|
||||
// Trailer header
|
||||
Discard1 [3]byte `struc:"[3]byte"` // First byte is size of trailer header?
|
||||
|
||||
TrailerMaxWidth int `struc:"uint32"`
|
||||
TrailerMaxLength int `struc:"uint32"`
|
||||
TrailerMinWidth int `struc:"uint32"`
|
||||
TrailerMinLength int `struc:"uint32"`
|
||||
|
||||
NumCharacters int `struc:"uint32"`
|
||||
|
||||
TrailerUnknown1 int `struc:"uint32"`
|
||||
TrailerUnknown2 int `struc:"uint16"`
|
||||
TrailerUnknown3 int `struc:"uint16"`
|
||||
TrailerUnknown4 int `struc:"uint32"`
|
||||
|
||||
NumThingies int `struc:"byte"`
|
||||
TrailerUnknown5 []byte `struc:"[3]byte"`
|
||||
Padding1 []byte `struc:"[20]byte"`
|
||||
|
||||
// FIXME: The rest is trash until Character & Thingy are worked out
|
||||
|
||||
Characters []Character `struc:"[]Character,sizefrom=NumCharacters"`
|
||||
Thingies []Thingy `struc:"[]Thingy,sizefrom=NumThingies"`
|
||||
|
||||
Title string `struc:"[255]byte"`
|
||||
Briefing string `struc:"[2048]byte"`
|
||||
|
||||
// Maybe? each contains either 0 or 1? Hard to say
|
||||
TrailerUnknown6 []byte `struc:"[85]byte"`
|
||||
}
|
||||
|
||||
func (h Header) Width() int {
|
||||
return int(h.MaxWidth - h.MinWidth)
|
||||
type Cell struct {
|
||||
DoorAndCanisterRelated byte `struc:"byte"`
|
||||
DoorLockAndReactorRelated byte `struc:"byte"`
|
||||
Unknown2 byte `struc:"byte"`
|
||||
Surface ObjRef
|
||||
Left ObjRef
|
||||
Right ObjRef
|
||||
Center ObjRef
|
||||
Unknown11 byte `struc:"byte"`
|
||||
Unknown12 byte `struc:"byte"`
|
||||
Unknown13 byte `struc:"byte"`
|
||||
Unknown14 byte `struc:"byte"`
|
||||
SquadRelated byte `struc:"byte"`
|
||||
}
|
||||
|
||||
func (h Header) Length() int {
|
||||
return int(h.MaxLength - h.MinLength)
|
||||
type Character struct {
|
||||
Unknown1 []byte `struc:"[178]byte"`
|
||||
Type data.CharacterType `struc:"byte"`
|
||||
Name string `struc:"[80]byte"`
|
||||
|
||||
// Attributes guessed by matching up numbers. Starts at 0x103
|
||||
WeaponSkill int `struc:"byte"`
|
||||
BallisticSkill int `struc:"byte"`
|
||||
Unknown2 byte `struc:"byte"`
|
||||
Leadership int `struc:"byte"`
|
||||
Toughness int `struc:"byte"`
|
||||
Strength int `struc:"byte"`
|
||||
ActionPoints int `struc:"byte"`
|
||||
Unknown3 byte `struc:"byte"`
|
||||
Unknown4 byte `struc:"byte"`
|
||||
Health int `struc:"byte"`
|
||||
|
||||
Unknown5 []byte `struc:"[91]byte"`
|
||||
Armor int `struc:"byte"`
|
||||
Unknown6 []byte `struc:"[84]byte"`
|
||||
YPos int `struc:"byte"` // These are actually much more complicated
|
||||
XPos int `struc:"byte"`
|
||||
Unknown7 []byte `struc:"[317]byte"`
|
||||
SquadNumber byte `struc:"byte"`
|
||||
Unknown8 []byte `struc:"[895]byte"`
|
||||
Orientation byte `struc:"byte"`
|
||||
Unknown9 []byte `struc:"[31]byte"`
|
||||
// TODO: each character may have a fixed number of subrecords for inventory
|
||||
}
|
||||
|
||||
func (h Header) Height() int {
|
||||
return MaxHeight
|
||||
type Characters []Character
|
||||
|
||||
// TODO. These are triggers/reactors/etc.
|
||||
type Thingy struct {
|
||||
Unknown1 int `struc:"uint32"`
|
||||
}
|
||||
|
||||
func (h Header) MapSetName() string {
|
||||
idx := bytes.IndexByte(h.SetName[:], 0)
|
||||
if idx < 0 {
|
||||
idx = 8 // all 8 bytes are used
|
||||
}
|
||||
type Thingies []Thingy
|
||||
|
||||
return string(h.SetName[0:idx:idx])
|
||||
func (g *GameMap) MapSetName() string {
|
||||
return g.SetName
|
||||
}
|
||||
|
||||
func (h Header) MapSetFilename() string {
|
||||
return h.MapSetName() + ".set"
|
||||
func (g *GameMap) MapSetFilename() string {
|
||||
return g.MapSetName() + ".set"
|
||||
}
|
||||
|
||||
type ObjRef struct {
|
||||
AreaByte byte
|
||||
SpriteAndFlagByte byte
|
||||
AreaByte byte `struc:"byte"`
|
||||
SpriteAndFlagByte byte `struc:"byte"`
|
||||
}
|
||||
|
||||
// The index into a set palette to retrieve the object
|
||||
func (o ObjRef) Index() int {
|
||||
func (o *ObjRef) Index() int {
|
||||
return int(o.AreaByte)
|
||||
}
|
||||
|
||||
func (o ObjRef) Sprite() int {
|
||||
func (o *ObjRef) Sprite() int {
|
||||
// The top bit seems to be a flag of some kind
|
||||
return int(o.SpriteAndFlagByte & 0x7f)
|
||||
}
|
||||
@@ -92,21 +167,6 @@ func (o ObjRef) IsActive() bool {
|
||||
return (o.SpriteAndFlagByte & 0x80) == 0x80
|
||||
}
|
||||
|
||||
type Cell struct {
|
||||
DoorAndCanisterRelated byte
|
||||
DoorLockAndReactorRelated byte
|
||||
Unknown2 byte
|
||||
Surface ObjRef
|
||||
Left ObjRef
|
||||
Right ObjRef
|
||||
Center ObjRef
|
||||
Unknown11 byte
|
||||
Unknown12 byte
|
||||
Unknown13 byte
|
||||
Unknown14 byte
|
||||
SquadRelated byte
|
||||
}
|
||||
|
||||
func (c *Cell) At(n int) byte {
|
||||
switch n {
|
||||
case 0:
|
||||
@@ -146,31 +206,27 @@ func (c *Cell) At(n int) byte {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Cells is always a fixed size; use At to get a cell according to x,y,z
|
||||
type Cells []Cell
|
||||
|
||||
func (c Cells) At(x, y, z int) Cell {
|
||||
return c[(z*MaxLength*MaxWidth)+(y*MaxWidth)+x]
|
||||
func (g *GameMap) At(x, y, z int) *Cell {
|
||||
return &g.Cells[(z*MaxLength*MaxWidth)+(y*MaxWidth)+x]
|
||||
}
|
||||
|
||||
func (h Header) Check() []error {
|
||||
var out []error
|
||||
if h.IsCampaignMap > 1 {
|
||||
out = append(out, fmt.Errorf("Expected 0 or 1 for IsCampaignMap, got %v", h.IsCampaignMap))
|
||||
func (g *GameMap) Check() error {
|
||||
if bytes.Compare(expectedMagic, g.Magic) != 0 {
|
||||
return fmt.Errorf("Unexpected magic value: %v", g.Magic)
|
||||
}
|
||||
|
||||
if bytes.Compare(expectedMagic, h.Magic[:]) != 0 {
|
||||
out = append(out, fmt.Errorf("Unexpected magic value: %v", h.Magic))
|
||||
}
|
||||
// TODO: other consistency checks
|
||||
|
||||
return out
|
||||
return nil
|
||||
}
|
||||
|
||||
type GameMap struct {
|
||||
Header
|
||||
Cells
|
||||
// TODO: parse this into sections
|
||||
Text string
|
||||
func (m *GameMap) Rect() image.Rectangle {
|
||||
return image.Rect(
|
||||
int(m.MinWidth),
|
||||
int(m.MinLength),
|
||||
int(m.MaxWidth),
|
||||
int(m.MaxLength),
|
||||
)
|
||||
}
|
||||
|
||||
// A game map contains a .txt and a .map. If they're in the same directory,
|
||||
@@ -192,23 +248,17 @@ func LoadGameMap(prefix string) (*GameMap, error) {
|
||||
return nil, fmt.Errorf("Couldn't find %s.{map,txt}, even ignoring case", prefix)
|
||||
}
|
||||
|
||||
// A game map is composed of two files: .map and .txt
|
||||
// A game map is composed of two files: .map and .txt. We ignore the text file,
|
||||
// since the content is replicated in the map file.
|
||||
func LoadGameMapByFiles(mapFile, txtFile string) (*GameMap, error) {
|
||||
out, err := loadMapFile(mapFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: load text and parse into sections
|
||||
txt, err := ioutil.ReadFile(txtFile)
|
||||
if err != nil {
|
||||
if err := out.Check(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out.Text = string(txt)
|
||||
|
||||
for _, err := range out.Check() {
|
||||
log.Printf("%s: %v", mapFile, err)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
@@ -246,6 +296,7 @@ func LoadGameMaps(dir string) (map[string]*GameMap, error) {
|
||||
|
||||
func loadMapFile(filename string) (*GameMap, error) {
|
||||
var out GameMap
|
||||
out.NumCells = cellCount
|
||||
|
||||
mf, err := os.Open(filename)
|
||||
if err != nil {
|
||||
@@ -261,20 +312,46 @@ func loadMapFile(filename string) (*GameMap, error) {
|
||||
|
||||
defer zr.Close()
|
||||
|
||||
if err := binary.Read(zr, binary.LittleEndian, &out.Header); err != nil {
|
||||
return nil, fmt.Errorf("Error parsing %s: %v", filename, err)
|
||||
}
|
||||
|
||||
// no gzip.SeekReader, so discard unread header bytes for now
|
||||
discardSize := int64(cellDataOffset - binary.Size(&out.Header))
|
||||
if _, err := io.CopyN(ioutil.Discard, zr, discardSize); err != nil {
|
||||
if err := struc.UnpackWithOrder(zr, &out, binary.LittleEndian); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out.Cells = make(Cells, cellCount)
|
||||
if err := binary.Read(zr, binary.LittleEndian, &out.Cells); err != nil {
|
||||
return nil, fmt.Errorf("Error parsing cells for %s: %v", filename, err)
|
||||
// Trim any trailing nulls off of the strings
|
||||
nullTerminate(&out.SetName)
|
||||
nullTerminate(&out.Title)
|
||||
nullTerminate(&out.Briefing)
|
||||
|
||||
for i, _ := range out.Characters {
|
||||
chr := &out.Characters[i]
|
||||
nullTerminate(&chr.Name)
|
||||
fmt.Printf("Character %v: %s\n", i, chr.String())
|
||||
}
|
||||
|
||||
fmt.Printf("Mission Title: %q\n", out.Title)
|
||||
fmt.Printf("Mission Briefing: %q\n", out.Briefing)
|
||||
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func nullTerminate(s *string) {
|
||||
sCpy := *s
|
||||
idx := strings.Index(sCpy, "\x00")
|
||||
if idx < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
*s = sCpy[0:idx]
|
||||
}
|
||||
|
||||
func (c *Character) String() string {
|
||||
return fmt.Sprintf(
|
||||
"squad=%v pos=(%v,%v) type=%q name=%q\n"+
|
||||
"\t%3d %3d %3d %3d %3d\n\t%3d %3d ??? ??? %3d\n",
|
||||
c.SquadNumber,
|
||||
c.XPos, c.YPos,
|
||||
c.Type.String(),
|
||||
c.Name,
|
||||
c.ActionPoints, c.Health, c.Armor, c.BallisticSkill, c.WeaponSkill,
|
||||
c.Strength, c.Toughness /*c.Initiative, c.Attacks,*/, c.Leadership,
|
||||
)
|
||||
}
|
||||
|
@@ -2,13 +2,13 @@ package menus
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.ur.gs/lupine/ordoor/internal/data"
|
||||
"code.ur.gs/lupine/ordoor/internal/util/asciiscan"
|
||||
)
|
||||
|
||||
@@ -33,7 +33,7 @@ const (
|
||||
SubTypeLineBriefing SubMenuType = 41
|
||||
SubTypeThumb SubMenuType = 45 // A "thumb" appears to be a vertical slider
|
||||
SubTypeInvokeButton SubMenuType = 50
|
||||
SubTypeDoorHotspot3 SubMenuType = 60 // Maybe? Appears in Arrange.mnu
|
||||
SubTypeClickText SubMenuType = 60
|
||||
SubTypeOverlay SubMenuType = 61
|
||||
SubTypeHypertext SubMenuType = 70
|
||||
SubTypeCheckbox SubMenuType = 91
|
||||
@@ -134,7 +134,15 @@ type Properties struct {
|
||||
Help string
|
||||
}
|
||||
|
||||
func LoadMenu(filename string) (*Menu, error) {
|
||||
func (p *Properties) Point() image.Point {
|
||||
if p.X > 0 || p.Y > 0 {
|
||||
return image.Pt(p.X, p.Y)
|
||||
}
|
||||
|
||||
return image.Point{}
|
||||
}
|
||||
|
||||
func LoadMenu(filename string, palette color.Palette) (*Menu, error) {
|
||||
name := filepath.Base(filename)
|
||||
name = strings.TrimSuffix(name, filepath.Ext(name))
|
||||
name = strings.ToLower(name)
|
||||
@@ -154,7 +162,7 @@ func LoadMenu(filename string) (*Menu, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := loadProperties(out, scanner); err != nil {
|
||||
if err := loadProperties(out, scanner, palette); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -180,7 +188,7 @@ func loadObjects(menu *Menu, scanner *asciiscan.Scanner) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadProperties(menu *Menu, scanner *asciiscan.Scanner) error {
|
||||
func loadProperties(menu *Menu, scanner *asciiscan.Scanner, palette color.Palette) error {
|
||||
for {
|
||||
ok, err := scanner.PeekProperty()
|
||||
|
||||
@@ -209,9 +217,9 @@ func loadProperties(menu *Menu, scanner *asciiscan.Scanner) error {
|
||||
|
||||
switch strings.ToUpper(k) {
|
||||
case "BACKGROUND COLOR":
|
||||
menu.BackgroundColor = data.ColorPalette[vInt]
|
||||
menu.BackgroundColor = palette[vInt]
|
||||
case "HYPERTEXT COLOR":
|
||||
menu.HypertextColor = data.ColorPalette[vInt]
|
||||
menu.HypertextColor = palette[vInt]
|
||||
case "FONT TYPE":
|
||||
menu.FontType = vInt
|
||||
default:
|
||||
@@ -310,7 +318,7 @@ func loadRecords(baseDir string, menu *Menu, scanner *asciiscan.Scanner) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func LoadMenus(dir string) (map[string]*Menu, error) {
|
||||
func LoadMenus(dir string, palette color.Palette) (map[string]*Menu, error) {
|
||||
fis, err := ioutil.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -328,7 +336,7 @@ func LoadMenus(dir string) (map[string]*Menu, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
built, err := LoadMenu(filepath.Join(dir, relname))
|
||||
built, err := LoadMenu(filepath.Join(dir, relname), palette)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %v", filepath.Join(dir, relname), err)
|
||||
}
|
||||
|
20
internal/ordoor/display.go
Normal file
20
internal/ordoor/display.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package ordoor
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func (o *Ordoor) DisplayImageFor(d time.Duration, name string) error {
|
||||
img, err := o.assets.Image(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.pic = img
|
||||
go func() {
|
||||
<-time.After(d)
|
||||
o.pic = nil // FIXME: this is a race condition and a half
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
@@ -7,9 +7,11 @@ package ordoor
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/hajimehoshi/ebiten"
|
||||
"github.com/hajimehoshi/ebiten/audio"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/audio"
|
||||
|
||||
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
||||
"code.ur.gs/lupine/ordoor/internal/config"
|
||||
@@ -27,19 +29,24 @@ type Ordoor struct {
|
||||
win *ui.Window
|
||||
|
||||
// Relevant to interface state
|
||||
flow *flow.Flow
|
||||
flow *flow.Flow
|
||||
flowOnce sync.Once
|
||||
|
||||
// FIXME: should be put inside flow
|
||||
// If this is set, we display it instead of flow
|
||||
pic *ebiten.Image
|
||||
|
||||
// Relevant to campaign state
|
||||
ship *ship.Ship
|
||||
}
|
||||
|
||||
func Run(configFile string, overrideX, overrideY int) error {
|
||||
cfg, err := config.Load(configFile)
|
||||
cfg, err := config.Load(configFile, "ordoor")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Couldn't load config file: %v", err)
|
||||
}
|
||||
|
||||
assets, err := assetstore.New(cfg.Ordoor.DataDir)
|
||||
assets, err := assetstore.New(cfg.DefaultEngine())
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to initialize asset store: %v", err)
|
||||
}
|
||||
@@ -56,9 +63,7 @@ func Run(configFile string, overrideX, overrideY int) error {
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := audio.NewContext(48000); err != nil {
|
||||
return fmt.Errorf("Failed to set up audio context: %v", err)
|
||||
}
|
||||
_ = audio.NewContext(48000)
|
||||
|
||||
ordoor := &Ordoor{
|
||||
assets: assets,
|
||||
@@ -81,10 +86,6 @@ func Run(configFile string, overrideX, overrideY int) error {
|
||||
|
||||
ordoor.win = win
|
||||
|
||||
if err := ordoor.setupFlow(); err != nil {
|
||||
return fmt.Errorf("failed to setup UI flow: %v", err)
|
||||
}
|
||||
|
||||
if err := ordoor.Run(); err != nil {
|
||||
return fmt.Errorf("Run finished with error: %v", err)
|
||||
}
|
||||
@@ -93,12 +94,16 @@ func Run(configFile string, overrideX, overrideY int) error {
|
||||
}
|
||||
|
||||
func (o *Ordoor) Run() error {
|
||||
// FIXME: we're missing a screen about SSI here
|
||||
// FIXME: these should be displayed *after*, not *before*, the copyright
|
||||
if o.config.Options.PlayMovies {
|
||||
o.PlaySkippableVideo("LOGOS")
|
||||
o.PlaySkippableVideo("movie1")
|
||||
}
|
||||
|
||||
if err := o.DisplayImageFor(time.Second, "copyright"); err != nil {
|
||||
log.Printf("Failed to display copyright image: %v", err)
|
||||
}
|
||||
|
||||
err := o.win.Run()
|
||||
if err == flow.ErrExit {
|
||||
log.Printf("Exit requested")
|
||||
@@ -151,6 +156,16 @@ func (o *Ordoor) setupFlow() error {
|
||||
}
|
||||
|
||||
func (o *Ordoor) Update(screenX, screenY int) error {
|
||||
if pic := o.pic; pic != nil {
|
||||
return nil // Ignore flow until we don't have a pic any more
|
||||
}
|
||||
|
||||
if o.flow == nil {
|
||||
if err := o.setupFlow(); err != nil {
|
||||
return fmt.Errorf("failed to setup UI flow: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure music is doing the right thing
|
||||
if o.music != nil && o.music.IsPlaying() != o.config.Options.PlayMusic {
|
||||
if o.config.Options.PlayMusic {
|
||||
@@ -165,9 +180,30 @@ func (o *Ordoor) Update(screenX, screenY int) error {
|
||||
}
|
||||
|
||||
func (o *Ordoor) Draw(screen *ebiten.Image) error {
|
||||
return o.flow.Draw(screen)
|
||||
|
||||
if pic := o.pic; pic != nil {
|
||||
// Scale the picture to the screen and draw it
|
||||
scaleX := float64(screen.Bounds().Dx()) / float64(pic.Bounds().Dx())
|
||||
scaleY := float64(screen.Bounds().Dy()) / float64(pic.Bounds().Dy())
|
||||
|
||||
do := &ebiten.DrawImageOptions{}
|
||||
do.GeoM.Scale(scaleX, scaleY)
|
||||
|
||||
screen.DrawImage(pic, do)
|
||||
return nil
|
||||
}
|
||||
|
||||
if o.flow != nil {
|
||||
return o.flow.Draw(screen)
|
||||
}
|
||||
|
||||
return nil // Draw() may be called before Update()
|
||||
}
|
||||
|
||||
func (o *Ordoor) Cursor() (*ebiten.Image, *ebiten.DrawImageOptions, error) {
|
||||
return o.flow.Cursor()
|
||||
if o.flow != nil {
|
||||
return o.flow.Cursor()
|
||||
}
|
||||
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@ import (
|
||||
)
|
||||
|
||||
func (o *Ordoor) PlayVideo(name string, skippable bool) {
|
||||
filename := o.config.DataFile("SMK/" + name + ".smk")
|
||||
filename := o.config.DataFile("ordoor", "SMK/"+name+".smk")
|
||||
|
||||
if len(o.config.VideoPlayer) == 0 {
|
||||
log.Printf("Video player not configured, skipping video %v", filename)
|
||||
|
@@ -1,12 +1,11 @@
|
||||
package data
|
||||
package palettes
|
||||
|
||||
import "image/color"
|
||||
|
||||
var (
|
||||
Transparent = color.RGBA{R: 0, G: 0, B: 0, A: 0}
|
||||
|
||||
ColorPalette = color.Palette{
|
||||
ChaosGatePalette = color.Palette{
|
||||
Transparent,
|
||||
|
||||
color.RGBA{R: 128, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 0, G: 128, B: 0, A: 255},
|
||||
color.RGBA{R: 128, G: 128, B: 0, A: 255},
|
||||
@@ -264,3 +263,7 @@ var (
|
||||
color.RGBA{R: 255, G: 255, B: 255, A: 255},
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
Palettes["ChaosGate"] = ChaosGatePalette
|
||||
}
|
20
internal/palettes/palettes.go
Normal file
20
internal/palettes/palettes.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package palettes
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
Transparent = color.RGBA{R: 0, G: 0, B: 0, A: 0}
|
||||
|
||||
Palettes = map[string]color.Palette{}
|
||||
|
||||
initPalettes = sync.Once{}
|
||||
)
|
||||
|
||||
func Get(name string) (color.Palette, bool) {
|
||||
p, ok := Palettes[name]
|
||||
|
||||
return p, ok
|
||||
}
|
269
internal/palettes/soldiers_at_war.go
Normal file
269
internal/palettes/soldiers_at_war.go
Normal file
@@ -0,0 +1,269 @@
|
||||
package palettes
|
||||
|
||||
import "image/color"
|
||||
|
||||
var (
|
||||
SoldiersAtWarPalette = color.Palette{
|
||||
Transparent,
|
||||
|
||||
color.RGBA{R: 128, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 0, G: 128, B: 0, A: 255},
|
||||
color.RGBA{R: 128, G: 128, B: 0, A: 255},
|
||||
color.RGBA{R: 0, G: 0, B: 128, A: 255},
|
||||
color.RGBA{R: 128, G: 0, B: 128, A: 255},
|
||||
color.RGBA{R: 0, G: 128, B: 128, A: 255},
|
||||
color.RGBA{R: 192, G: 192, B: 192, A: 255},
|
||||
color.RGBA{R: 192, G: 220, B: 192, A: 255},
|
||||
color.RGBA{R: 166, G: 202, B: 240, A: 255},
|
||||
color.RGBA{R: 0, G: 136, B: 65, A: 255},
|
||||
color.RGBA{R: 24, G: 153, B: 68, A: 255},
|
||||
color.RGBA{R: 48, G: 170, B: 71, A: 255},
|
||||
color.RGBA{R: 72, G: 187, B: 73, A: 255},
|
||||
color.RGBA{R: 96, G: 204, B: 76, A: 255},
|
||||
color.RGBA{R: 120, G: 218, B: 122, A: 255},
|
||||
color.RGBA{R: 24, G: 12, B: 0, A: 255},
|
||||
color.RGBA{R: 31, G: 14, B: 7, A: 255},
|
||||
color.RGBA{R: 43, G: 19, B: 5, A: 255},
|
||||
color.RGBA{R: 48, G: 21, B: 3, A: 255},
|
||||
color.RGBA{R: 60, G: 26, B: 2, A: 255},
|
||||
color.RGBA{R: 79, G: 43, B: 18, A: 255},
|
||||
color.RGBA{R: 99, G: 60, B: 35, A: 255},
|
||||
color.RGBA{R: 118, G: 77, B: 52, A: 255},
|
||||
color.RGBA{R: 137, G: 94, B: 69, A: 255},
|
||||
color.RGBA{R: 157, G: 111, B: 86, A: 255},
|
||||
color.RGBA{R: 176, G: 127, B: 103, A: 255},
|
||||
color.RGBA{R: 195, G: 144, B: 120, A: 255},
|
||||
color.RGBA{R: 214, G: 161, B: 137, A: 255},
|
||||
color.RGBA{R: 234, G: 178, B: 154, A: 255},
|
||||
color.RGBA{R: 253, G: 197, B: 173, A: 255},
|
||||
color.RGBA{R: 11, G: 11, B: 230, A: 255},
|
||||
color.RGBA{R: 41, G: 50, B: 255, A: 255},
|
||||
color.RGBA{R: 62, G: 79, B: 255, A: 255},
|
||||
color.RGBA{R: 83, G: 109, B: 255, A: 255},
|
||||
color.RGBA{R: 103, G: 138, B: 255, A: 255},
|
||||
color.RGBA{R: 124, G: 168, B: 255, A: 255},
|
||||
color.RGBA{R: 145, G: 197, B: 255, A: 255},
|
||||
color.RGBA{R: 166, G: 227, B: 255, A: 255},
|
||||
color.RGBA{R: 0, G: 16, B: 0, A: 255},
|
||||
color.RGBA{R: 1, G: 27, B: 0, A: 255},
|
||||
color.RGBA{R: 2, G: 37, B: 0, A: 255},
|
||||
color.RGBA{R: 3, G: 48, B: 0, A: 255},
|
||||
color.RGBA{R: 4, G: 59, B: 0, A: 255},
|
||||
color.RGBA{R: 5, G: 70, B: 0, A: 255},
|
||||
color.RGBA{R: 6, G: 80, B: 0, A: 255},
|
||||
color.RGBA{R: 7, G: 91, B: 0, A: 255},
|
||||
color.RGBA{R: 48, G: 4, B: 0, A: 255},
|
||||
color.RGBA{R: 64, G: 4, B: 0, A: 255},
|
||||
color.RGBA{R: 93, G: 8, B: 0, A: 255},
|
||||
color.RGBA{R: 121, G: 24, B: 0, A: 255},
|
||||
color.RGBA{R: 145, G: 40, B: 0, A: 255},
|
||||
color.RGBA{R: 174, G: 64, B: 0, A: 255},
|
||||
color.RGBA{R: 198, G: 89, B: 0, A: 255},
|
||||
color.RGBA{R: 226, G: 121, B: 0, A: 255},
|
||||
color.RGBA{R: 238, G: 155, B: 0, A: 255},
|
||||
color.RGBA{R: 255, G: 182, B: 28, A: 255},
|
||||
color.RGBA{R: 255, G: 202, B: 60, A: 255},
|
||||
color.RGBA{R: 255, G: 218, B: 97, A: 255},
|
||||
color.RGBA{R: 255, G: 234, B: 129, A: 255},
|
||||
color.RGBA{R: 255, G: 242, B: 165, A: 255},
|
||||
color.RGBA{R: 255, G: 250, B: 198, A: 255},
|
||||
color.RGBA{R: 255, G: 238, B: 238, A: 255},
|
||||
color.RGBA{R: 0, G: 32, B: 16, A: 255},
|
||||
color.RGBA{R: 0, G: 44, B: 24, A: 255},
|
||||
color.RGBA{R: 4, G: 60, B: 36, A: 255},
|
||||
color.RGBA{R: 8, G: 76, B: 52, A: 255},
|
||||
color.RGBA{R: 16, G: 89, B: 64, A: 255},
|
||||
color.RGBA{R: 24, G: 105, B: 80, A: 255},
|
||||
color.RGBA{R: 36, G: 121, B: 97, A: 255},
|
||||
color.RGBA{R: 4, G: 56, B: 0, A: 255},
|
||||
color.RGBA{R: 11, G: 68, B: 9, A: 255},
|
||||
color.RGBA{R: 19, G: 80, B: 18, A: 255},
|
||||
color.RGBA{R: 26, G: 92, B: 27, A: 255},
|
||||
color.RGBA{R: 34, G: 105, B: 37, A: 255},
|
||||
color.RGBA{R: 41, G: 117, B: 46, A: 255},
|
||||
color.RGBA{R: 49, G: 129, B: 55, A: 255},
|
||||
color.RGBA{R: 56, G: 141, B: 64, A: 255},
|
||||
color.RGBA{R: 40, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 56, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 76, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 97, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 113, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 133, G: 4, B: 4, A: 255},
|
||||
color.RGBA{R: 153, G: 4, B: 4, A: 255},
|
||||
color.RGBA{R: 170, G: 8, B: 8, A: 255},
|
||||
color.RGBA{R: 190, G: 12, B: 12, A: 255},
|
||||
color.RGBA{R: 210, G: 16, B: 16, A: 255},
|
||||
color.RGBA{R: 214, G: 48, B: 48, A: 255},
|
||||
color.RGBA{R: 222, G: 80, B: 80, A: 255},
|
||||
color.RGBA{R: 230, G: 117, B: 117, A: 255},
|
||||
color.RGBA{R: 238, G: 153, B: 153, A: 255},
|
||||
color.RGBA{R: 246, G: 194, B: 194, A: 255},
|
||||
color.RGBA{R: 255, G: 212, B: 227, A: 255},
|
||||
color.RGBA{R: 10, G: 10, B: 10, A: 255},
|
||||
color.RGBA{R: 25, G: 25, B: 25, A: 255},
|
||||
color.RGBA{R: 41, G: 41, B: 41, A: 255},
|
||||
color.RGBA{R: 56, G: 56, B: 56, A: 255},
|
||||
color.RGBA{R: 71, G: 71, B: 71, A: 255},
|
||||
color.RGBA{R: 87, G: 87, B: 87, A: 255},
|
||||
color.RGBA{R: 102, G: 102, B: 102, A: 255},
|
||||
color.RGBA{R: 117, G: 117, B: 117, A: 255},
|
||||
color.RGBA{R: 133, G: 133, B: 133, A: 255},
|
||||
color.RGBA{R: 148, G: 148, B: 148, A: 255},
|
||||
color.RGBA{R: 163, G: 163, B: 163, A: 255},
|
||||
color.RGBA{R: 179, G: 179, B: 179, A: 255},
|
||||
color.RGBA{R: 194, G: 194, B: 194, A: 255},
|
||||
color.RGBA{R: 209, G: 209, B: 209, A: 255},
|
||||
color.RGBA{R: 225, G: 225, B: 225, A: 255},
|
||||
color.RGBA{R: 240, G: 240, B: 240, A: 255},
|
||||
color.RGBA{R: 7, G: 7, B: 19, A: 255},
|
||||
color.RGBA{R: 12, G: 12, B: 24, A: 255},
|
||||
color.RGBA{R: 20, G: 20, B: 32, A: 255},
|
||||
color.RGBA{R: 32, G: 32, B: 48, A: 255},
|
||||
color.RGBA{R: 44, G: 44, B: 64, A: 255},
|
||||
color.RGBA{R: 60, G: 60, B: 80, A: 255},
|
||||
color.RGBA{R: 72, G: 72, B: 97, A: 255},
|
||||
color.RGBA{R: 85, G: 85, B: 109, A: 255},
|
||||
color.RGBA{R: 101, G: 101, B: 125, A: 255},
|
||||
color.RGBA{R: 117, G: 117, B: 128, A: 255},
|
||||
color.RGBA{R: 133, G: 133, B: 157, A: 255},
|
||||
color.RGBA{R: 149, G: 149, B: 170, A: 255},
|
||||
color.RGBA{R: 165, G: 165, B: 186, A: 255},
|
||||
color.RGBA{R: 182, G: 182, B: 202, A: 255},
|
||||
color.RGBA{R: 202, G: 202, B: 218, A: 255},
|
||||
color.RGBA{R: 222, G: 222, B: 234, A: 255},
|
||||
color.RGBA{R: 158, G: 158, B: 158, A: 255},
|
||||
color.RGBA{R: 146, G: 146, B: 146, A: 255},
|
||||
color.RGBA{R: 75, G: 65, B: 55, A: 255},
|
||||
color.RGBA{R: 71, G: 55, B: 46, A: 255},
|
||||
color.RGBA{R: 64, G: 36, B: 22, A: 255},
|
||||
color.RGBA{R: 60, G: 26, B: 1, A: 255},
|
||||
color.RGBA{R: 66, G: 44, B: 38, A: 255},
|
||||
color.RGBA{R: 64, G: 63, B: 54, A: 255},
|
||||
color.RGBA{R: 88, G: 84, B: 82, A: 255},
|
||||
color.RGBA{R: 81, G: 95, B: 95, A: 255},
|
||||
color.RGBA{R: 101, G: 102, B: 102, A: 255},
|
||||
color.RGBA{R: 116, G: 117, B: 117, A: 255},
|
||||
color.RGBA{R: 132, G: 133, B: 133, A: 255},
|
||||
color.RGBA{R: 63, G: 62, B: 53, A: 255},
|
||||
color.RGBA{R: 87, G: 83, B: 81, A: 255},
|
||||
color.RGBA{R: 80, G: 94, B: 94, A: 255},
|
||||
color.RGBA{R: 195, G: 80, B: 0, A: 255},
|
||||
color.RGBA{R: 235, G: 150, B: 0, A: 255},
|
||||
color.RGBA{R: 250, G: 200, B: 60, A: 255},
|
||||
color.RGBA{R: 95, G: 95, B: 95, A: 255},
|
||||
color.RGBA{R: 110, G: 110, B: 110, A: 255},
|
||||
color.RGBA{R: 170, G: 170, B: 170, A: 255},
|
||||
color.RGBA{R: 85, G: 52, B: 12, A: 255},
|
||||
color.RGBA{R: 68, G: 40, B: 4, A: 255},
|
||||
color.RGBA{R: 52, G: 32, B: 0, A: 255},
|
||||
color.RGBA{R: 36, G: 20, B: 0, A: 255},
|
||||
color.RGBA{R: 68, G: 42, B: 22, A: 255},
|
||||
color.RGBA{R: 77, G: 48, B: 25, A: 255},
|
||||
color.RGBA{R: 86, G: 55, B: 29, A: 255},
|
||||
color.RGBA{R: 93, G: 61, B: 32, A: 255},
|
||||
color.RGBA{R: 106, G: 72, B: 38, A: 255},
|
||||
color.RGBA{R: 114, G: 80, B: 42, A: 255},
|
||||
color.RGBA{R: 122, G: 87, B: 46, A: 255},
|
||||
color.RGBA{R: 130, G: 96, B: 56, A: 255},
|
||||
color.RGBA{R: 136, G: 103, B: 64, A: 255},
|
||||
color.RGBA{R: 141, G: 110, B: 73, A: 255},
|
||||
color.RGBA{R: 145, G: 115, B: 80, A: 255},
|
||||
color.RGBA{R: 173, G: 142, B: 102, A: 255},
|
||||
color.RGBA{R: 200, G: 168, B: 123, A: 255},
|
||||
color.RGBA{R: 228, G: 195, B: 145, A: 255},
|
||||
color.RGBA{R: 0, G: 0, B: 79, A: 255},
|
||||
color.RGBA{R: 25, G: 25, B: 100, A: 255},
|
||||
color.RGBA{R: 51, G: 51, B: 121, A: 255},
|
||||
color.RGBA{R: 76, G: 76, B: 141, A: 255},
|
||||
color.RGBA{R: 101, G: 101, B: 162, A: 255},
|
||||
color.RGBA{R: 126, G: 126, B: 183, A: 255},
|
||||
color.RGBA{R: 152, G: 152, B: 204, A: 255},
|
||||
color.RGBA{R: 80, G: 100, B: 143, A: 255},
|
||||
color.RGBA{R: 20, G: 27, B: 49, A: 255},
|
||||
color.RGBA{R: 30, G: 42, B: 64, A: 255},
|
||||
color.RGBA{R: 13, G: 33, B: 69, A: 255},
|
||||
color.RGBA{R: 24, G: 45, B: 82, A: 255},
|
||||
color.RGBA{R: 35, G: 55, B: 94, A: 255},
|
||||
color.RGBA{R: 46, G: 66, B: 106, A: 255},
|
||||
color.RGBA{R: 57, G: 77, B: 118, A: 255},
|
||||
color.RGBA{R: 69, G: 89, B: 130, A: 255},
|
||||
color.RGBA{R: 40, G: 48, B: 0, A: 255},
|
||||
color.RGBA{R: 58, G: 66, B: 12, A: 255},
|
||||
color.RGBA{R: 77, G: 84, B: 24, A: 255},
|
||||
color.RGBA{R: 95, G: 102, B: 36, A: 255},
|
||||
color.RGBA{R: 114, G: 120, B: 48, A: 255},
|
||||
color.RGBA{R: 132, G: 138, B: 61, A: 255},
|
||||
color.RGBA{R: 151, G: 156, B: 73, A: 255},
|
||||
color.RGBA{R: 169, G: 174, B: 85, A: 255},
|
||||
color.RGBA{R: 188, G: 192, B: 97, A: 255},
|
||||
color.RGBA{R: 206, G: 210, B: 109, A: 255},
|
||||
color.RGBA{R: 56, G: 56, B: 40, A: 255},
|
||||
color.RGBA{R: 76, G: 76, B: 56, A: 255},
|
||||
color.RGBA{R: 101, G: 101, B: 76, A: 255},
|
||||
color.RGBA{R: 125, G: 125, B: 101, A: 255},
|
||||
color.RGBA{R: 145, G: 145, B: 121, A: 255},
|
||||
color.RGBA{R: 170, G: 170, B: 145, A: 255},
|
||||
color.RGBA{R: 41, G: 46, B: 30, A: 255},
|
||||
color.RGBA{R: 48, G: 53, B: 33, A: 255},
|
||||
color.RGBA{R: 55, G: 60, B: 37, A: 255},
|
||||
color.RGBA{R: 63, G: 67, B: 40, A: 255},
|
||||
color.RGBA{R: 70, G: 74, B: 43, A: 255},
|
||||
color.RGBA{R: 77, G: 81, B: 47, A: 255},
|
||||
color.RGBA{R: 84, G: 89, B: 50, A: 255},
|
||||
color.RGBA{R: 91, G: 96, B: 54, A: 255},
|
||||
color.RGBA{R: 98, G: 103, B: 57, A: 255},
|
||||
color.RGBA{R: 106, G: 110, B: 60, A: 255},
|
||||
color.RGBA{R: 113, G: 117, B: 64, A: 255},
|
||||
color.RGBA{R: 120, G: 124, B: 67, A: 255},
|
||||
color.RGBA{R: 230, G: 230, B: 230, A: 255},
|
||||
color.RGBA{R: 218, G: 218, B: 218, A: 255},
|
||||
color.RGBA{R: 206, G: 206, B: 206, A: 255},
|
||||
color.RGBA{R: 182, G: 182, B: 182, A: 255},
|
||||
color.RGBA{R: 100, G: 59, B: 34, A: 255},
|
||||
color.RGBA{R: 125, G: 84, B: 59, A: 255},
|
||||
color.RGBA{R: 150, G: 109, B: 84, A: 255},
|
||||
color.RGBA{R: 174, G: 134, B: 108, A: 255},
|
||||
color.RGBA{R: 199, G: 159, B: 133, A: 255},
|
||||
color.RGBA{R: 97, G: 64, B: 16, A: 255},
|
||||
color.RGBA{R: 113, G: 76, B: 28, A: 255},
|
||||
color.RGBA{R: 129, G: 93, B: 36, A: 255},
|
||||
color.RGBA{R: 141, G: 105, B: 48, A: 255},
|
||||
color.RGBA{R: 157, G: 117, B: 60, A: 255},
|
||||
color.RGBA{R: 174, G: 133, B: 76, A: 255},
|
||||
color.RGBA{R: 190, G: 149, B: 93, A: 255},
|
||||
color.RGBA{R: 202, G: 165, B: 109, A: 255},
|
||||
color.RGBA{R: 218, G: 182, B: 129, A: 255},
|
||||
color.RGBA{R: 234, G: 198, B: 145, A: 255},
|
||||
color.RGBA{R: 250, G: 218, B: 170, A: 255},
|
||||
color.RGBA{R: 157, G: 76, B: 36, A: 255},
|
||||
color.RGBA{R: 161, G: 93, B: 60, A: 255},
|
||||
color.RGBA{R: 174, G: 113, B: 72, A: 255},
|
||||
color.RGBA{R: 202, G: 139, B: 104, A: 255},
|
||||
color.RGBA{R: 178, G: 127, B: 100, A: 255},
|
||||
color.RGBA{R: 28, G: 36, B: 0, A: 255},
|
||||
color.RGBA{R: 41, G: 49, B: 10, A: 255},
|
||||
color.RGBA{R: 55, G: 62, B: 20, A: 255},
|
||||
color.RGBA{R: 68, G: 75, B: 30, A: 255},
|
||||
color.RGBA{R: 81, G: 88, B: 40, A: 255},
|
||||
color.RGBA{R: 95, G: 100, B: 50, A: 255},
|
||||
color.RGBA{R: 121, G: 126, B: 70, A: 255},
|
||||
color.RGBA{R: 135, G: 139, B: 80, A: 255},
|
||||
color.RGBA{R: 148, G: 152, B: 90, A: 255},
|
||||
color.RGBA{R: 162, G: 165, B: 100, A: 255},
|
||||
color.RGBA{R: 175, G: 178, B: 110, A: 255},
|
||||
color.RGBA{R: 255, G: 251, B: 240, A: 255},
|
||||
color.RGBA{R: 160, G: 160, B: 164, A: 255},
|
||||
color.RGBA{R: 128, G: 128, B: 128, A: 255},
|
||||
color.RGBA{R: 255, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 0, G: 255, B: 0, A: 255},
|
||||
color.RGBA{R: 255, G: 255, B: 0, A: 255},
|
||||
color.RGBA{R: 0, G: 0, B: 255, A: 255},
|
||||
color.RGBA{R: 255, G: 0, B: 255, A: 255},
|
||||
color.RGBA{R: 0, G: 255, B: 255, A: 255},
|
||||
color.RGBA{R: 255, G: 255, B: 255, A: 255},
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
Palettes["SoldiersAtWar"] = SoldiersAtWarPalette
|
||||
}
|
269
internal/palettes/wages_of_war.go
Normal file
269
internal/palettes/wages_of_war.go
Normal file
@@ -0,0 +1,269 @@
|
||||
package palettes
|
||||
|
||||
import "image/color"
|
||||
|
||||
var (
|
||||
WagesOfWarPalette = color.Palette{
|
||||
Transparent,
|
||||
|
||||
color.RGBA{R: 1, G: 1, B: 1, A: 255},
|
||||
color.RGBA{R: 254, G: 254, B: 254, A: 255},
|
||||
color.RGBA{R: 187, G: 4, B: 4, A: 255},
|
||||
color.RGBA{R: 181, G: 195, B: 2, A: 255},
|
||||
color.RGBA{R: 94, G: 79, B: 148, A: 255},
|
||||
color.RGBA{R: 233, G: 91, B: 4, A: 255},
|
||||
color.RGBA{R: 60, G: 60, B: 60, A: 255},
|
||||
color.RGBA{R: 102, G: 102, B: 102, A: 255},
|
||||
color.RGBA{R: 170, G: 170, B: 170, A: 255},
|
||||
color.RGBA{R: 197, G: 195, B: 193, A: 255},
|
||||
color.RGBA{R: 234, G: 234, B: 235, A: 255},
|
||||
color.RGBA{R: 218, G: 218, B: 240, A: 255},
|
||||
color.RGBA{R: 202, G: 202, B: 245, A: 255},
|
||||
color.RGBA{R: 186, G: 186, B: 250, A: 255},
|
||||
color.RGBA{R: 170, G: 170, B: 255, A: 255},
|
||||
color.RGBA{R: 238, G: 238, B: 238, A: 255},
|
||||
color.RGBA{R: 222, G: 222, B: 222, A: 255},
|
||||
color.RGBA{R: 206, G: 206, B: 206, A: 255},
|
||||
color.RGBA{R: 190, G: 190, B: 190, A: 255},
|
||||
color.RGBA{R: 174, G: 174, B: 174, A: 255},
|
||||
color.RGBA{R: 157, G: 157, B: 157, A: 255},
|
||||
color.RGBA{R: 141, G: 141, B: 141, A: 255},
|
||||
color.RGBA{R: 129, G: 129, B: 129, A: 255},
|
||||
color.RGBA{R: 113, G: 113, B: 113, A: 255},
|
||||
color.RGBA{R: 97, G: 97, B: 97, A: 255},
|
||||
color.RGBA{R: 80, G: 80, B: 80, A: 255},
|
||||
color.RGBA{R: 64, G: 64, B: 64, A: 255},
|
||||
color.RGBA{R: 48, G: 48, B: 48, A: 255},
|
||||
color.RGBA{R: 32, G: 32, B: 32, A: 255},
|
||||
color.RGBA{R: 16, G: 16, B: 16, A: 255},
|
||||
color.RGBA{R: 4, G: 4, B: 4, A: 255},
|
||||
color.RGBA{R: 255, G: 238, B: 238, A: 255},
|
||||
color.RGBA{R: 246, G: 194, B: 194, A: 255},
|
||||
color.RGBA{R: 238, G: 153, B: 153, A: 255},
|
||||
color.RGBA{R: 230, G: 117, B: 117, A: 255},
|
||||
color.RGBA{R: 222, G: 80, B: 80, A: 255},
|
||||
color.RGBA{R: 214, G: 48, B: 48, A: 255},
|
||||
color.RGBA{R: 210, G: 16, B: 16, A: 255},
|
||||
color.RGBA{R: 190, G: 12, B: 12, A: 255},
|
||||
color.RGBA{R: 170, G: 8, B: 8, A: 255},
|
||||
color.RGBA{R: 153, G: 4, B: 4, A: 255},
|
||||
color.RGBA{R: 133, G: 4, B: 4, A: 255},
|
||||
color.RGBA{R: 113, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 97, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 76, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 56, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 40, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 255, G: 255, B: 250, A: 255},
|
||||
color.RGBA{R: 255, G: 255, B: 165, A: 255},
|
||||
color.RGBA{R: 255, G: 255, B: 80, A: 255},
|
||||
color.RGBA{R: 255, G: 255, B: 0, A: 255},
|
||||
color.RGBA{R: 234, G: 222, B: 12, A: 255},
|
||||
color.RGBA{R: 214, G: 194, B: 28, A: 255},
|
||||
color.RGBA{R: 194, G: 170, B: 40, A: 255},
|
||||
color.RGBA{R: 174, G: 149, B: 52, A: 255},
|
||||
color.RGBA{R: 255, G: 202, B: 153, A: 255},
|
||||
color.RGBA{R: 255, G: 170, B: 101, A: 255},
|
||||
color.RGBA{R: 255, G: 133, B: 48, A: 255},
|
||||
color.RGBA{R: 255, G: 93, B: 0, A: 255},
|
||||
color.RGBA{R: 222, G: 89, B: 12, A: 255},
|
||||
color.RGBA{R: 190, G: 85, B: 28, A: 255},
|
||||
color.RGBA{R: 157, G: 76, B: 36, A: 255},
|
||||
color.RGBA{R: 129, G: 68, B: 40, A: 255},
|
||||
color.RGBA{R: 198, G: 255, B: 206, A: 255},
|
||||
color.RGBA{R: 170, G: 238, B: 182, A: 255},
|
||||
color.RGBA{R: 149, G: 222, B: 157, A: 255},
|
||||
color.RGBA{R: 125, G: 206, B: 137, A: 255},
|
||||
color.RGBA{R: 105, G: 190, B: 113, A: 255},
|
||||
color.RGBA{R: 89, G: 174, B: 97, A: 255},
|
||||
color.RGBA{R: 72, G: 157, B: 76, A: 255},
|
||||
color.RGBA{R: 56, G: 141, B: 64, A: 255},
|
||||
color.RGBA{R: 40, G: 125, B: 48, A: 255},
|
||||
color.RGBA{R: 32, G: 109, B: 36, A: 255},
|
||||
color.RGBA{R: 20, G: 93, B: 24, A: 255},
|
||||
color.RGBA{R: 12, G: 76, B: 16, A: 255},
|
||||
color.RGBA{R: 4, G: 60, B: 8, A: 255},
|
||||
color.RGBA{R: 0, G: 44, B: 4, A: 255},
|
||||
color.RGBA{R: 0, G: 32, B: 0, A: 255},
|
||||
color.RGBA{R: 0, G: 16, B: 0, A: 255},
|
||||
color.RGBA{R: 210, G: 255, B: 255, A: 255},
|
||||
color.RGBA{R: 182, G: 238, B: 234, A: 255},
|
||||
color.RGBA{R: 157, G: 222, B: 218, A: 255},
|
||||
color.RGBA{R: 137, G: 210, B: 202, A: 255},
|
||||
color.RGBA{R: 113, G: 194, B: 186, A: 255},
|
||||
color.RGBA{R: 97, G: 178, B: 165, A: 255},
|
||||
color.RGBA{R: 76, G: 165, B: 149, A: 255},
|
||||
color.RGBA{R: 64, G: 149, B: 129, A: 255},
|
||||
color.RGBA{R: 48, G: 133, B: 113, A: 255},
|
||||
color.RGBA{R: 36, G: 121, B: 97, A: 255},
|
||||
color.RGBA{R: 24, G: 105, B: 80, A: 255},
|
||||
color.RGBA{R: 16, G: 89, B: 64, A: 255},
|
||||
color.RGBA{R: 8, G: 76, B: 52, A: 255},
|
||||
color.RGBA{R: 4, G: 60, B: 36, A: 255},
|
||||
color.RGBA{R: 0, G: 44, B: 24, A: 255},
|
||||
color.RGBA{R: 0, G: 32, B: 16, A: 255},
|
||||
color.RGBA{R: 255, G: 255, B: 170, A: 255},
|
||||
color.RGBA{R: 238, G: 238, B: 149, A: 255},
|
||||
color.RGBA{R: 222, G: 222, B: 129, A: 255},
|
||||
color.RGBA{R: 206, G: 210, B: 109, A: 255},
|
||||
color.RGBA{R: 190, G: 194, B: 93, A: 255},
|
||||
color.RGBA{R: 174, G: 182, B: 76, A: 255},
|
||||
color.RGBA{R: 157, G: 165, B: 64, A: 255},
|
||||
color.RGBA{R: 141, G: 149, B: 52, A: 255},
|
||||
color.RGBA{R: 125, G: 137, B: 40, A: 255},
|
||||
color.RGBA{R: 109, G: 121, B: 28, A: 255},
|
||||
color.RGBA{R: 97, G: 109, B: 20, A: 255},
|
||||
color.RGBA{R: 80, G: 93, B: 12, A: 255},
|
||||
color.RGBA{R: 64, G: 76, B: 4, A: 255},
|
||||
color.RGBA{R: 52, G: 64, B: 4, A: 255},
|
||||
color.RGBA{R: 40, G: 48, B: 0, A: 255},
|
||||
color.RGBA{R: 28, G: 36, B: 0, A: 255},
|
||||
color.RGBA{R: 214, G: 255, B: 214, A: 255},
|
||||
color.RGBA{R: 157, G: 255, B: 157, A: 255},
|
||||
color.RGBA{R: 105, G: 255, B: 105, A: 255},
|
||||
color.RGBA{R: 48, G: 255, B: 48, A: 255},
|
||||
color.RGBA{R: 0, G: 255, B: 0, A: 255},
|
||||
color.RGBA{R: 24, G: 210, B: 28, A: 255},
|
||||
color.RGBA{R: 44, G: 165, B: 48, A: 255},
|
||||
color.RGBA{R: 52, G: 125, B: 56, A: 255},
|
||||
color.RGBA{R: 226, G: 121, B: 0, A: 255},
|
||||
color.RGBA{R: 198, G: 89, B: 0, A: 255},
|
||||
color.RGBA{R: 174, G: 64, B: 0, A: 255},
|
||||
color.RGBA{R: 145, G: 40, B: 0, A: 255},
|
||||
color.RGBA{R: 121, G: 24, B: 0, A: 255},
|
||||
color.RGBA{R: 93, G: 8, B: 0, A: 255},
|
||||
color.RGBA{R: 64, G: 4, B: 0, A: 255},
|
||||
color.RGBA{R: 40, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 194, G: 255, B: 255, A: 255},
|
||||
color.RGBA{R: 165, G: 255, B: 255, A: 255},
|
||||
color.RGBA{R: 109, G: 255, B: 255, A: 255},
|
||||
color.RGBA{R: 0, G: 255, B: 255, A: 255},
|
||||
color.RGBA{R: 4, G: 222, B: 222, A: 255},
|
||||
color.RGBA{R: 16, G: 194, B: 194, A: 255},
|
||||
color.RGBA{R: 20, G: 161, B: 161, A: 255},
|
||||
color.RGBA{R: 24, G: 133, B: 133, A: 255},
|
||||
color.RGBA{R: 137, G: 157, B: 255, A: 255},
|
||||
color.RGBA{R: 101, G: 121, B: 255, A: 255},
|
||||
color.RGBA{R: 64, G: 80, B: 255, A: 255},
|
||||
color.RGBA{R: 28, G: 40, B: 255, A: 255},
|
||||
color.RGBA{R: 0, G: 0, B: 255, A: 255},
|
||||
color.RGBA{R: 12, G: 12, B: 206, A: 255},
|
||||
color.RGBA{R: 20, G: 20, B: 157, A: 255},
|
||||
color.RGBA{R: 24, G: 24, B: 109, A: 255},
|
||||
color.RGBA{R: 234, G: 234, B: 255, A: 255},
|
||||
color.RGBA{R: 202, G: 202, B: 238, A: 255},
|
||||
color.RGBA{R: 178, G: 178, B: 222, A: 255},
|
||||
color.RGBA{R: 153, G: 153, B: 206, A: 255},
|
||||
color.RGBA{R: 129, G: 129, B: 194, A: 255},
|
||||
color.RGBA{R: 105, G: 105, B: 178, A: 255},
|
||||
color.RGBA{R: 89, G: 89, B: 161, A: 255},
|
||||
color.RGBA{R: 68, G: 68, B: 145, A: 255},
|
||||
color.RGBA{R: 52, G: 52, B: 133, A: 255},
|
||||
color.RGBA{R: 40, G: 40, B: 117, A: 255},
|
||||
color.RGBA{R: 28, G: 28, B: 101, A: 255},
|
||||
color.RGBA{R: 16, G: 16, B: 89, A: 255},
|
||||
color.RGBA{R: 8, G: 8, B: 72, A: 255},
|
||||
color.RGBA{R: 4, G: 4, B: 56, A: 255},
|
||||
color.RGBA{R: 0, G: 0, B: 40, A: 255},
|
||||
color.RGBA{R: 0, G: 0, B: 28, A: 255},
|
||||
color.RGBA{R: 250, G: 218, B: 170, A: 255},
|
||||
color.RGBA{R: 234, G: 198, B: 145, A: 255},
|
||||
color.RGBA{R: 218, G: 182, B: 129, A: 255},
|
||||
color.RGBA{R: 202, G: 165, B: 109, A: 255},
|
||||
color.RGBA{R: 190, G: 149, B: 93, A: 255},
|
||||
color.RGBA{R: 174, G: 133, B: 76, A: 255},
|
||||
color.RGBA{R: 157, G: 117, B: 60, A: 255},
|
||||
color.RGBA{R: 141, G: 105, B: 48, A: 255},
|
||||
color.RGBA{R: 129, G: 93, B: 36, A: 255},
|
||||
color.RGBA{R: 113, G: 76, B: 28, A: 255},
|
||||
color.RGBA{R: 97, G: 64, B: 16, A: 255},
|
||||
color.RGBA{R: 85, G: 52, B: 12, A: 255},
|
||||
color.RGBA{R: 68, G: 40, B: 4, A: 255},
|
||||
color.RGBA{R: 52, G: 32, B: 0, A: 255},
|
||||
color.RGBA{R: 36, G: 20, B: 0, A: 255},
|
||||
color.RGBA{R: 24, G: 12, B: 0, A: 255},
|
||||
color.RGBA{R: 255, G: 230, B: 186, A: 255},
|
||||
color.RGBA{R: 238, G: 210, B: 161, A: 255},
|
||||
color.RGBA{R: 226, G: 190, B: 141, A: 255},
|
||||
color.RGBA{R: 214, G: 170, B: 121, A: 255},
|
||||
color.RGBA{R: 202, G: 149, B: 105, A: 255},
|
||||
color.RGBA{R: 186, G: 133, B: 89, A: 255},
|
||||
color.RGBA{R: 174, G: 113, B: 72, A: 255},
|
||||
color.RGBA{R: 161, G: 93, B: 60, A: 255},
|
||||
color.RGBA{R: 145, G: 76, B: 48, A: 255},
|
||||
color.RGBA{R: 133, G: 60, B: 36, A: 255},
|
||||
color.RGBA{R: 121, G: 44, B: 24, A: 255},
|
||||
color.RGBA{R: 109, G: 32, B: 16, A: 255},
|
||||
color.RGBA{R: 93, G: 20, B: 8, A: 255},
|
||||
color.RGBA{R: 80, G: 8, B: 4, A: 255},
|
||||
color.RGBA{R: 68, G: 4, B: 0, A: 255},
|
||||
color.RGBA{R: 56, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 218, G: 218, B: 198, A: 255},
|
||||
color.RGBA{R: 194, G: 194, B: 170, A: 255},
|
||||
color.RGBA{R: 170, G: 170, B: 145, A: 255},
|
||||
color.RGBA{R: 145, G: 145, B: 121, A: 255},
|
||||
color.RGBA{R: 125, G: 125, B: 101, A: 255},
|
||||
color.RGBA{R: 101, G: 101, B: 76, A: 255},
|
||||
color.RGBA{R: 76, G: 76, B: 56, A: 255},
|
||||
color.RGBA{R: 56, G: 56, B: 40, A: 255},
|
||||
color.RGBA{R: 246, G: 222, B: 206, A: 255},
|
||||
color.RGBA{R: 234, G: 206, B: 190, A: 255},
|
||||
color.RGBA{R: 226, G: 194, B: 170, A: 255},
|
||||
color.RGBA{R: 214, G: 178, B: 153, A: 255},
|
||||
color.RGBA{R: 206, G: 165, B: 141, A: 255},
|
||||
color.RGBA{R: 194, G: 153, B: 125, A: 255},
|
||||
color.RGBA{R: 186, G: 141, B: 113, A: 255},
|
||||
color.RGBA{R: 174, G: 129, B: 101, A: 255},
|
||||
color.RGBA{R: 165, G: 117, B: 89, A: 255},
|
||||
color.RGBA{R: 153, G: 109, B: 76, A: 255},
|
||||
color.RGBA{R: 145, G: 97, B: 64, A: 255},
|
||||
color.RGBA{R: 133, G: 89, B: 56, A: 255},
|
||||
color.RGBA{R: 125, G: 76, B: 48, A: 255},
|
||||
color.RGBA{R: 113, G: 68, B: 36, A: 255},
|
||||
color.RGBA{R: 105, G: 60, B: 32, A: 255},
|
||||
color.RGBA{R: 93, G: 52, B: 24, A: 255},
|
||||
color.RGBA{R: 85, G: 44, B: 16, A: 255},
|
||||
color.RGBA{R: 72, G: 36, B: 12, A: 255},
|
||||
color.RGBA{R: 64, G: 32, B: 8, A: 255},
|
||||
color.RGBA{R: 56, G: 24, B: 4, A: 255},
|
||||
color.RGBA{R: 44, G: 16, B: 0, A: 255},
|
||||
color.RGBA{R: 36, G: 12, B: 0, A: 255},
|
||||
color.RGBA{R: 24, G: 8, B: 0, A: 255},
|
||||
color.RGBA{R: 16, G: 4, B: 0, A: 255},
|
||||
color.RGBA{R: 255, G: 255, B: 234, A: 255},
|
||||
color.RGBA{R: 255, G: 250, B: 198, A: 255},
|
||||
color.RGBA{R: 255, G: 242, B: 165, A: 255},
|
||||
color.RGBA{R: 255, G: 234, B: 129, A: 255},
|
||||
color.RGBA{R: 255, G: 218, B: 97, A: 255},
|
||||
color.RGBA{R: 255, G: 202, B: 60, A: 255},
|
||||
color.RGBA{R: 255, G: 182, B: 28, A: 255},
|
||||
color.RGBA{R: 255, G: 157, B: 0, A: 255},
|
||||
color.RGBA{R: 222, G: 222, B: 234, A: 255},
|
||||
color.RGBA{R: 202, G: 202, B: 218, A: 255},
|
||||
color.RGBA{R: 182, G: 182, B: 202, A: 255},
|
||||
color.RGBA{R: 165, G: 165, B: 186, A: 255},
|
||||
color.RGBA{R: 149, G: 149, B: 170, A: 255},
|
||||
color.RGBA{R: 133, G: 133, B: 157, A: 255},
|
||||
color.RGBA{R: 117, G: 117, B: 141, A: 255},
|
||||
color.RGBA{R: 101, G: 101, B: 125, A: 255},
|
||||
color.RGBA{R: 85, G: 85, B: 109, A: 255},
|
||||
color.RGBA{R: 72, G: 72, B: 97, A: 255},
|
||||
color.RGBA{R: 60, G: 60, B: 80, A: 255},
|
||||
color.RGBA{R: 44, G: 44, B: 64, A: 255},
|
||||
color.RGBA{R: 32, G: 32, B: 48, A: 255},
|
||||
color.RGBA{R: 20, G: 20, B: 32, A: 255},
|
||||
color.RGBA{R: 12, G: 12, B: 24, A: 255},
|
||||
color.RGBA{R: 0, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 0, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 0, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 0, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 0, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 0, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 0, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 0, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 0, G: 0, B: 0, A: 255},
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
Palettes["WagesOfWar"] = WagesOfWarPalette
|
||||
}
|
@@ -5,8 +5,8 @@ import (
|
||||
"image"
|
||||
"sort"
|
||||
|
||||
"github.com/hajimehoshi/ebiten"
|
||||
"github.com/hajimehoshi/ebiten/ebitenutil"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
||||
)
|
||||
|
||||
type CartPt struct {
|
||||
@@ -22,16 +22,21 @@ type IsoPt struct {
|
||||
func (s *Scenario) Update(screenX, screenY int) error {
|
||||
s.tick += 1
|
||||
|
||||
x, y := ebiten.CursorPosition()
|
||||
geo := s.geoForCam()
|
||||
geo.Translate(cellWidthHalf, 0)
|
||||
geo.Scale(s.Zoom, s.Zoom)
|
||||
geo.Invert()
|
||||
|
||||
cX, cY := ebiten.CursorPosition()
|
||||
x, y := geo.Apply(float64(cX), float64(cY))
|
||||
|
||||
screenPos := CartPt{
|
||||
X: float64(s.Viewpoint.X + x),
|
||||
Y: float64(s.Viewpoint.Y + y),
|
||||
X: x,
|
||||
Y: y,
|
||||
}
|
||||
|
||||
s.selectedCell = screenPos.ToISO()
|
||||
|
||||
// TODO: zoom support will need a camera
|
||||
// FIXME: adjust for Z level
|
||||
s.highlightedCell = screenPos.ToISO()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -41,17 +46,18 @@ func (s *Scenario) Draw(screen *ebiten.Image) error {
|
||||
// http://www.java-gaming.org/index.php?topic=24922.0
|
||||
// https://stackoverflow.com/questions/892811/drawing-isometric-game-worlds
|
||||
// https://gamedev.stackexchange.com/questions/25896/how-do-i-find-which-isometric-tiles-are-inside-the-cameras-current-view
|
||||
// FIXME: we don't cope with zoom very neatly here
|
||||
|
||||
sw, sh := screen.Size()
|
||||
|
||||
topLeft := CartPt{
|
||||
X: float64(s.Viewpoint.X - 2*cellWidth), // Ensure all visible cells are rendered
|
||||
Y: float64(s.Viewpoint.Y - 2*cellHeight),
|
||||
X: float64(s.Viewpoint.X) - (2 * cellWidth / s.Zoom), // Ensure all visible cells are rendered
|
||||
Y: float64(s.Viewpoint.Y) - (2 * cellHeight / s.Zoom),
|
||||
}.ToISO()
|
||||
|
||||
bottomRight := CartPt{
|
||||
X: float64(s.Viewpoint.X + sw + 2*cellHeight),
|
||||
Y: float64(s.Viewpoint.Y + sh + 5*cellHeight), // Z dimension requires it
|
||||
X: float64(s.Viewpoint.X) + (float64(sw) / s.Zoom) + (2 * cellHeight / s.Zoom),
|
||||
Y: float64(s.Viewpoint.Y) + (float64(sh) / s.Zoom) + (5 * cellHeight / s.Zoom), // Z dimension requires it
|
||||
}.ToISO()
|
||||
|
||||
// X+Y is constant for all tiles in a column
|
||||
@@ -89,41 +95,63 @@ func (s *Scenario) Draw(screen *ebiten.Image) error {
|
||||
return false
|
||||
})
|
||||
|
||||
counter := map[string]int{}
|
||||
counter := 0
|
||||
for _, pt := range toDraw {
|
||||
for z := 0; z <= s.ZIdx; z++ {
|
||||
if err := s.renderCell(int(pt.X), int(pt.Y), z, screen, counter); err != nil {
|
||||
if err := s.renderCell(int(pt.X), int(pt.Y), z, screen, &counter); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//log.Printf("%#+v", counter)
|
||||
|
||||
// Finally, draw cursor chrome
|
||||
// FIXME: it looks like we might need to do this in normal painting order...
|
||||
spr, err := s.specials.Sprite(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
op := ebiten.DrawImageOptions{}
|
||||
op.GeoM = s.geoForCoords(int(s.selectedCell.X), int(s.selectedCell.Y), 0)
|
||||
op.GeoM.Translate(-cellWidthHalf, -cellHeightHalf)
|
||||
geo := s.geoForCoords(int(s.highlightedCell.X), int(s.highlightedCell.Y), 0)
|
||||
op.GeoM = geo
|
||||
op.GeoM.Translate(-209, -332)
|
||||
op.GeoM.Translate(float64(spr.Rect.Min.X), float64(spr.Rect.Min.Y))
|
||||
op.GeoM.Scale(s.Zoom, s.Zoom)
|
||||
|
||||
if err := screen.DrawImage(spr.Image, &op); err != nil {
|
||||
return err
|
||||
}
|
||||
screen.DrawImage(spr.Image, &op)
|
||||
|
||||
sx, sy := op.GeoM.Apply(0, 0)
|
||||
ebitenutil.DebugPrintAt(screen, fmt.Sprintf("(%.0f,%.0f)", s.selectedCell.X, s.selectedCell.Y), int(sx), int(sy))
|
||||
x1, y1 := geo.Apply(0, 0)
|
||||
ebitenutil.DebugPrintAt(
|
||||
screen,
|
||||
fmt.Sprintf("(%d,%d)", int(s.highlightedCell.X), int(s.highlightedCell.Y)),
|
||||
int(x1),
|
||||
int(y1),
|
||||
)
|
||||
|
||||
ebitenutil.DebugPrintAt(screen, fmt.Sprintf("Sprites: %v", counter), 0, 16)
|
||||
|
||||
/*
|
||||
// debug: draw a square around the selected cell
|
||||
x2, y2 := geo.Apply(cellWidth, cellHeight)
|
||||
ebitenutil.DrawLine(screen, x1, y1, x2, y1, colornames.Green) // top line
|
||||
ebitenutil.DrawLine(screen, x1, y1, x1, y2, colornames.Green) // left line
|
||||
ebitenutil.DrawLine(screen, x2, y1, x2, y2, colornames.Green) // right line
|
||||
ebitenutil.DrawLine(screen, x1, y2, x2, y2, colornames.Green) // bottom line
|
||||
*/
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Scenario) geoForCoords(x, y, z int) ebiten.GeoM {
|
||||
func (s *Scenario) geoForCam() ebiten.GeoM {
|
||||
geo := ebiten.GeoM{}
|
||||
geo.Translate(-float64(s.Viewpoint.X), -float64(s.Viewpoint.Y))
|
||||
|
||||
return geo
|
||||
}
|
||||
|
||||
func (s *Scenario) geoForCoords(x, y, z int) ebiten.GeoM {
|
||||
geo := s.geoForCam()
|
||||
|
||||
pix := IsoPt{X: float64(x), Y: float64(y)}.ToCart()
|
||||
geo.Translate(pix.X, pix.Y)
|
||||
|
||||
@@ -134,7 +162,7 @@ func (s *Scenario) geoForCoords(x, y, z int) ebiten.GeoM {
|
||||
return geo
|
||||
}
|
||||
|
||||
func (s *Scenario) renderCell(x, y, z int, screen *ebiten.Image, counter map[string]int) error {
|
||||
func (s *Scenario) renderCell(x, y, z int, screen *ebiten.Image, counter *int) error {
|
||||
sprites, err := s.area.SpritesForCell(x, y, z)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -149,28 +177,26 @@ func (s *Scenario) renderCell(x, y, z int, screen *ebiten.Image, counter map[str
|
||||
iso.Translate(-209, -332)
|
||||
|
||||
for _, spr := range sprites {
|
||||
// if _, ok := counter[spr.ID]; !ok {
|
||||
// counter[spr.ID] = 0
|
||||
// }
|
||||
// counter[spr.ID] = counter[spr.ID] + 1
|
||||
*counter = *counter + 1
|
||||
op := ebiten.DrawImageOptions{GeoM: iso}
|
||||
|
||||
op.GeoM.Translate(float64(spr.Rect.Min.X), float64(spr.Rect.Min.Y))
|
||||
|
||||
if err := screen.DrawImage(spr.Image, &op); err != nil {
|
||||
return err
|
||||
}
|
||||
// Zoom has to come last
|
||||
op.GeoM.Scale(s.Zoom, s.Zoom)
|
||||
|
||||
screen.DrawImage(spr.Image, &op)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
cellWidth = 128
|
||||
cellHeight = 64
|
||||
cellWidth = 128.0
|
||||
cellHeight = 63.0
|
||||
|
||||
cellWidthHalf = cellWidth / 2
|
||||
cellHeightHalf = cellHeight / 2
|
||||
cellWidthHalf = cellWidth / 2.0
|
||||
cellHeightHalf = cellHeight / 2.0
|
||||
)
|
||||
|
||||
func (p CartPt) ToISO() IsoPt {
|
||||
@@ -186,28 +212,3 @@ func (p IsoPt) ToCart() CartPt {
|
||||
Y: (p.X + p.Y) * cellHeightHalf,
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
// Doesn't take the camera or Z level into account
|
||||
func cellToPix(pt image.Point) image.Point {
|
||||
return image.Pt(
|
||||
(pt.X-pt.Y)*cellWidthHalf,
|
||||
(pt.X+pt.Y)*cellHeightHalf,
|
||||
)
|
||||
}
|
||||
|
||||
// Doesn't take the camera or Z level into account
|
||||
func pixToCell(pt image.Point) image.Point {
|
||||
fX := pt.X
|
||||
fY := pt.Y
|
||||
return image.Pt(
|
||||
// (pt.X / cellWidthHalf + pt.Y / cellHeightHalf) / 2,
|
||||
// (pt.Y / cellHeightHalf - (pt.Y / cellWidthHalf)) / 2,
|
||||
// int(fY/cellHeight+fX/(cellWidth*2)),
|
||||
// int(fY/cellHeight-fX/(cellWidth*2)),
|
||||
//int((fY / cellHeight) + (fX / cellWidth)),
|
||||
//int((-fX / cellWidth) + (fY / cellHeight)),
|
||||
|
||||
|
||||
)
|
||||
}*/
|
||||
|
@@ -1,6 +1,8 @@
|
||||
package scenario
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"code.ur.gs/lupine/ordoor/internal/maps"
|
||||
)
|
||||
|
||||
@@ -9,7 +11,35 @@ type CellPoint struct {
|
||||
Z int
|
||||
}
|
||||
|
||||
func (s *Scenario) CellAtCursor() (maps.Cell, CellPoint) {
|
||||
cell := s.area.Cell(int(s.selectedCell.X), int(s.selectedCell.Y), 0)
|
||||
return cell, CellPoint{IsoPt: s.selectedCell, Z: 0}
|
||||
func (s *Scenario) CellAtCursor() (*maps.Cell, CellPoint) {
|
||||
cell := s.area.Cell(int(s.highlightedCell.X), int(s.highlightedCell.Y), 0)
|
||||
return cell, CellPoint{IsoPt: s.highlightedCell, Z: 0}
|
||||
}
|
||||
|
||||
func (s *Scenario) HighlightedCharacter() *maps.Character {
|
||||
// FIXME: characters are always at zIdx 0 right now
|
||||
return s.area.CharacterAt(int(s.highlightedCell.X), int(s.highlightedCell.Y), 0)
|
||||
}
|
||||
|
||||
func (s *Scenario) SelectedCharacter() *maps.Character {
|
||||
return s.selectedCharacter
|
||||
}
|
||||
|
||||
func (s *Scenario) SelectHighlightedCharacter() {
|
||||
chr := s.HighlightedCharacter()
|
||||
log.Printf("Selected character %s", chr)
|
||||
s.selectedCharacter = chr
|
||||
}
|
||||
|
||||
func (s *Scenario) ChangeZIdx(by int) {
|
||||
newZ := s.ZIdx + by
|
||||
if newZ < 0 {
|
||||
newZ = 0
|
||||
}
|
||||
|
||||
if newZ > 6 {
|
||||
newZ = 6
|
||||
}
|
||||
|
||||
s.ZIdx = newZ
|
||||
}
|
||||
|
@@ -2,25 +2,28 @@
|
||||
package scenario
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
|
||||
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
||||
"code.ur.gs/lupine/ordoor/internal/maps"
|
||||
)
|
||||
|
||||
type Scenario struct {
|
||||
area *assetstore.Map
|
||||
specials *assetstore.Object
|
||||
|
||||
tick int
|
||||
turn int
|
||||
selectedCell IsoPt
|
||||
tick int
|
||||
turn int
|
||||
|
||||
highlightedCell IsoPt
|
||||
selectedCharacter *maps.Character
|
||||
|
||||
// All these must be modified by user actions somehow.
|
||||
// TODO: extract into the idea of a viewport passed to Update / Draw somehow?
|
||||
// Or have a separater Drawer for the Scenario?
|
||||
Viewpoint image.Point // Top-left of the screen
|
||||
ZIdx int // Currently-viewed Z index
|
||||
Zoom float64 // Zoom level to set
|
||||
}
|
||||
|
||||
func NewScenario(assets *assetstore.AssetStore, name string) (*Scenario, error) {
|
||||
@@ -35,14 +38,15 @@ func NewScenario(assets *assetstore.AssetStore, name string) (*Scenario, error)
|
||||
}
|
||||
|
||||
// Eager load sprites. TODO: do we really want to do this?
|
||||
if err := area.LoadSprites(); err != nil {
|
||||
return nil, fmt.Errorf("Eager-loading sprites failed: %v", err)
|
||||
}
|
||||
//if err := area.LoadSprites(); err != nil {
|
||||
// return nil, fmt.Errorf("Eager-loading sprites failed: %v", err)
|
||||
//}
|
||||
|
||||
out := &Scenario{
|
||||
area: area,
|
||||
specials: specials,
|
||||
Viewpoint: image.Pt(0, 3000), // FIXME: haxxx
|
||||
Zoom: 1.0,
|
||||
}
|
||||
|
||||
return out, nil
|
||||
|
@@ -1,7 +1,7 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"github.com/hajimehoshi/ebiten"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@@ -13,6 +13,8 @@ import (
|
||||
type button struct {
|
||||
locator string
|
||||
|
||||
rect image.Rectangle
|
||||
|
||||
baseSpr *assetstore.Sprite
|
||||
clickSpr *assetstore.Sprite
|
||||
frozenSpr *assetstore.Sprite
|
||||
@@ -37,6 +39,7 @@ func (d *Driver) buildButton(p *menus.Properties) (*button, *Widget, error) {
|
||||
|
||||
btn := &button{
|
||||
locator: p.Locator,
|
||||
rect: sprites[0].Rect.Add(p.Point()),
|
||||
baseSpr: sprites[0],
|
||||
clickSpr: sprites[1],
|
||||
frozenSpr: sprites[2],
|
||||
@@ -44,6 +47,8 @@ func (d *Driver) buildButton(p *menus.Properties) (*button, *Widget, error) {
|
||||
}
|
||||
|
||||
widget := &Widget{
|
||||
Locator: p.Locator,
|
||||
Active: p.Active,
|
||||
ownClickables: []clickable{btn},
|
||||
ownFreezables: []freezable{btn},
|
||||
ownHoverables: []hoverable{btn},
|
||||
@@ -68,6 +73,7 @@ func (d *Driver) buildMainButton(p *menus.Properties) (*mainButton, *Widget, err
|
||||
hoverAnim: animation(hovers),
|
||||
button: button{
|
||||
locator: p.Locator,
|
||||
rect: sprites[0].Rect.Add(p.Point()),
|
||||
baseSpr: sprites[0],
|
||||
clickSpr: sprites[1],
|
||||
frozenSpr: sprites[2],
|
||||
@@ -76,6 +82,8 @@ func (d *Driver) buildMainButton(p *menus.Properties) (*mainButton, *Widget, err
|
||||
}
|
||||
|
||||
widget := &Widget{
|
||||
Locator: p.Locator,
|
||||
Active: p.Active,
|
||||
ownClickables: []clickable{btn},
|
||||
ownFreezables: []freezable{btn},
|
||||
ownHoverables: []hoverable{btn},
|
||||
@@ -93,6 +101,7 @@ func (d *Driver) buildDoorHotspot(p *menus.Properties) (*button, *Widget, error)
|
||||
|
||||
btn := &button{
|
||||
locator: p.Locator,
|
||||
rect: sprites[0].Rect.Add(p.Point()),
|
||||
baseSpr: sprites[0],
|
||||
clickSpr: sprites[1],
|
||||
frozenSpr: sprites[0], // No disabled sprite
|
||||
@@ -100,6 +109,8 @@ func (d *Driver) buildDoorHotspot(p *menus.Properties) (*button, *Widget, error)
|
||||
}
|
||||
|
||||
widget := &Widget{
|
||||
Locator: p.Locator,
|
||||
Active: p.Active,
|
||||
ownClickables: []clickable{btn},
|
||||
ownFreezables: []freezable{btn},
|
||||
ownHoverables: []hoverable{btn},
|
||||
@@ -115,7 +126,7 @@ func (b *button) id() string {
|
||||
}
|
||||
|
||||
func (b *button) bounds() image.Rectangle {
|
||||
return b.baseSpr.Rect
|
||||
return b.rect
|
||||
}
|
||||
|
||||
func (b *button) mouseDownState() bool {
|
||||
|
@@ -14,11 +14,16 @@ func (d *Driver) Dialogues() []string {
|
||||
return out
|
||||
}
|
||||
|
||||
func (d *Driver) IsInDialogue() bool {
|
||||
return d.activeDialogue != nil
|
||||
}
|
||||
|
||||
func (d *Driver) ShowDialogue(locator string) error {
|
||||
for _, dialogue := range d.dialogues {
|
||||
if dialogue.Locator == locator {
|
||||
|
||||
// FIXME: we should unhover and mouseup the non-dialogue elements
|
||||
dialogue.Active = true
|
||||
d.activeDialogue = dialogue
|
||||
|
||||
return nil
|
||||
@@ -28,5 +33,9 @@ func (d *Driver) ShowDialogue(locator string) error {
|
||||
}
|
||||
|
||||
func (d *Driver) HideDialogue() {
|
||||
if d.activeDialogue != nil {
|
||||
d.activeDialogue.Active = false
|
||||
}
|
||||
|
||||
d.activeDialogue = nil
|
||||
}
|
||||
|
@@ -5,8 +5,8 @@ import (
|
||||
"image"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/hajimehoshi/ebiten"
|
||||
"github.com/hajimehoshi/ebiten/ebitenutil"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
||||
|
||||
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
||||
)
|
||||
@@ -137,9 +137,7 @@ func (d *Driver) Draw(screen *ebiten.Image) error {
|
||||
do.GeoM = d.orig2native
|
||||
do.GeoM.Translate(x, y)
|
||||
|
||||
if err := screen.DrawImage(region.image, &do); err != nil {
|
||||
return err
|
||||
}
|
||||
screen.DrawImage(region.image, &do)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,11 +167,11 @@ func (d *Driver) allClickables() []clickable {
|
||||
var out []clickable
|
||||
|
||||
for _, widget := range d.widgets {
|
||||
out = append(out, widget.clickables()...)
|
||||
out = append(out, widget.allClickables()...)
|
||||
}
|
||||
|
||||
for _, widget := range d.dialogues {
|
||||
out = append(out, widget.clickables()...)
|
||||
out = append(out, widget.allClickables()...)
|
||||
}
|
||||
|
||||
return out
|
||||
@@ -182,11 +180,11 @@ func (d *Driver) allClickables() []clickable {
|
||||
func (d *Driver) allFreezables() []freezable {
|
||||
var out []freezable
|
||||
for _, widget := range d.widgets {
|
||||
out = append(out, widget.freezables()...)
|
||||
out = append(out, widget.allFreezables()...)
|
||||
}
|
||||
|
||||
for _, widget := range d.dialogues {
|
||||
out = append(out, widget.freezables()...)
|
||||
out = append(out, widget.allFreezables()...)
|
||||
}
|
||||
|
||||
return out
|
||||
@@ -196,11 +194,11 @@ func (d *Driver) allValueables() []valueable {
|
||||
var out []valueable
|
||||
|
||||
for _, widget := range d.widgets {
|
||||
out = append(out, widget.valueables()...)
|
||||
out = append(out, widget.allValueables()...)
|
||||
}
|
||||
|
||||
for _, widget := range d.dialogues {
|
||||
out = append(out, widget.valueables()...)
|
||||
out = append(out, widget.allValueables()...)
|
||||
}
|
||||
|
||||
return out
|
||||
@@ -208,12 +206,12 @@ func (d *Driver) allValueables() []valueable {
|
||||
|
||||
func (d *Driver) activeClickables() []clickable {
|
||||
if d.activeDialogue != nil {
|
||||
return d.activeDialogue.clickables()
|
||||
return d.activeDialogue.activeClickables()
|
||||
}
|
||||
|
||||
var out []clickable
|
||||
for _, widget := range d.widgets {
|
||||
out = append(out, widget.clickables()...)
|
||||
out = append(out, widget.activeClickables()...)
|
||||
}
|
||||
|
||||
return out
|
||||
@@ -221,12 +219,12 @@ func (d *Driver) activeClickables() []clickable {
|
||||
|
||||
func (d *Driver) activeHoverables() []hoverable {
|
||||
if d.activeDialogue != nil {
|
||||
return d.activeDialogue.hoverables()
|
||||
return d.activeDialogue.activeHoverables()
|
||||
}
|
||||
|
||||
var out []hoverable
|
||||
for _, widget := range d.widgets {
|
||||
out = append(out, widget.hoverables()...)
|
||||
out = append(out, widget.activeHoverables()...)
|
||||
}
|
||||
|
||||
return out
|
||||
@@ -234,12 +232,12 @@ func (d *Driver) activeHoverables() []hoverable {
|
||||
|
||||
func (d *Driver) activeMouseables() []mouseable {
|
||||
if d.activeDialogue != nil {
|
||||
return d.activeDialogue.mouseables()
|
||||
return d.activeDialogue.activeMouseables()
|
||||
}
|
||||
|
||||
var out []mouseable
|
||||
for _, widget := range d.widgets {
|
||||
out = append(out, widget.mouseables()...)
|
||||
out = append(out, widget.activeMouseables()...)
|
||||
}
|
||||
|
||||
return out
|
||||
@@ -249,12 +247,23 @@ func (d *Driver) activePaintables() []paintable {
|
||||
var out []paintable
|
||||
|
||||
for _, widget := range d.widgets {
|
||||
out = append(out, widget.paintables()...)
|
||||
out = append(out, widget.activePaintables()...)
|
||||
}
|
||||
|
||||
if d.activeDialogue != nil {
|
||||
out = append(out, d.activeDialogue.paintables()...)
|
||||
out = append(out, d.activeDialogue.activePaintables()...)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (d *Driver) findWidget(locator string) *Widget {
|
||||
toplevels := append(d.widgets, d.dialogues...)
|
||||
for _, widget := range toplevels {
|
||||
if w := widget.findWidget(locator); w != nil {
|
||||
return w
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@@ -28,7 +28,10 @@ func (d *Driver) registerGroup(group *menus.Group) error {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
groupWidget = &Widget{Locator: group.Locator}
|
||||
groupWidget = &Widget{
|
||||
Locator: group.Locator,
|
||||
Active: group.Active,
|
||||
}
|
||||
}
|
||||
|
||||
if dialogue {
|
||||
@@ -75,8 +78,10 @@ func (d *Driver) buildRecord(r *menus.Record) (*Widget, error) {
|
||||
switch r.Type {
|
||||
case menus.SubTypeSimpleButton, menus.SubTypeInvokeButton:
|
||||
_, widget, err = d.buildButton(r.Props())
|
||||
case menus.SubTypeDoorHotspot1, menus.SubTypeDoorHotspot2, menus.SubTypeDoorHotspot3:
|
||||
case menus.SubTypeDoorHotspot1, menus.SubTypeDoorHotspot2:
|
||||
_, widget, err = d.buildDoorHotspot(r.Props())
|
||||
case menus.SubTypeClickText:
|
||||
_, widget, err = d.buildClickText(r.Props())
|
||||
case menus.SubTypeOverlay:
|
||||
_, widget, err = d.buildOverlay(r.Props())
|
||||
case menus.SubTypeHypertext:
|
||||
@@ -127,6 +132,7 @@ func (d *Driver) maybeBuildInventorySelect(group *menus.Group, records []*menus.
|
||||
elements := make([]*inventorySelect, len(touched))
|
||||
widget := &Widget{
|
||||
Locator: group.Locator,
|
||||
Active: group.Active,
|
||||
}
|
||||
|
||||
for i, record := range touched {
|
||||
|
@@ -3,7 +3,7 @@ package ui
|
||||
import (
|
||||
"image"
|
||||
|
||||
"github.com/hajimehoshi/ebiten"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
||||
type region struct {
|
||||
|
@@ -27,6 +27,7 @@ func (d *Driver) buildInventorySelect(p *menus.Properties) (*inventorySelect, *W
|
||||
|
||||
element := &inventorySelect{checkbox: *c}
|
||||
widget := &Widget{
|
||||
Active: p.Active,
|
||||
ownClickables: []clickable{element},
|
||||
ownFreezables: []freezable{element},
|
||||
ownHoverables: []hoverable{element},
|
||||
|
@@ -77,6 +77,7 @@ func (d *Driver) buildListBox(group *menus.Group, up, down, thumb *menus.Record,
|
||||
// mostly self-registered at the moment.
|
||||
widget := &Widget{
|
||||
Children: []*Widget{upWidget, downWidget},
|
||||
Active: group.Active, // FIXME: children have their own active state
|
||||
ownPaintables: []paintable{element},
|
||||
ownValueables: []valueable{element},
|
||||
}
|
||||
@@ -156,9 +157,9 @@ func (l *listBox) refresh() {
|
||||
// FIXME: noninteractive isn't set up for dynamic text yet. Need to
|
||||
// generate textImg on demand instead of once at start.
|
||||
if ni.label != nil {
|
||||
ni.label.text = ""
|
||||
ni.label.str = ""
|
||||
if len(l.strings) > l.offset+i {
|
||||
ni.label.text = l.strings[l.offset+i]
|
||||
ni.label.str = l.strings[l.offset+i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -32,12 +32,13 @@ type noninteractive struct {
|
||||
hoverImpl
|
||||
}
|
||||
|
||||
// Paint some text to screen
|
||||
// Paint some text to screen, possibly settable
|
||||
type label struct {
|
||||
align AlignMode
|
||||
rect image.Rectangle
|
||||
text string
|
||||
font *assetstore.Font
|
||||
locator string
|
||||
align AlignMode
|
||||
rect image.Rectangle
|
||||
font *assetstore.Font
|
||||
valueImpl
|
||||
}
|
||||
|
||||
// This particular animation has entry and exit sequences, which are invoked
|
||||
@@ -67,7 +68,7 @@ func (d *Driver) buildNoninteractive(p *menus.Properties) (*noninteractive, erro
|
||||
ni := &noninteractive{
|
||||
locator: p.Locator,
|
||||
frames: animation{sprite.Image},
|
||||
rect: sprite.Rect,
|
||||
rect: sprite.Rect.Add(p.Point()),
|
||||
}
|
||||
|
||||
return ni, nil
|
||||
@@ -83,6 +84,7 @@ func (d *Driver) buildStatic(p *menus.Properties) (*noninteractive, *Widget, err
|
||||
|
||||
widget := &Widget{
|
||||
Locator: ni.locator,
|
||||
Active: p.Active,
|
||||
ownClickables: []clickable{ni}, // FIXME: credits background needs to be clickable
|
||||
ownHoverables: []hoverable{ni},
|
||||
ownPaintables: []paintable{ni},
|
||||
@@ -100,6 +102,7 @@ func (d *Driver) buildHypertext(p *menus.Properties) (*noninteractive, *Widget,
|
||||
// FIXME: check if this is still needed on the bridge -> briefing transition
|
||||
widget := &Widget{
|
||||
Locator: ni.locator,
|
||||
Active: p.Active,
|
||||
ownClickables: []clickable{ni},
|
||||
ownHoverables: []hoverable{ni},
|
||||
}
|
||||
@@ -107,6 +110,33 @@ func (d *Driver) buildHypertext(p *menus.Properties) (*noninteractive, *Widget,
|
||||
return ni, widget, nil
|
||||
}
|
||||
|
||||
func (d *Driver) buildClickText(p *menus.Properties) (*noninteractive, *Widget, error) {
|
||||
ni, err := d.buildNoninteractive(p)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
fnt := d.menu.Font(p.FontType/10 - 1)
|
||||
|
||||
// FIXME: is this always right? Seems to make sense for Main.mnu
|
||||
ni.label = &label{
|
||||
locator: ni.locator,
|
||||
font: fnt,
|
||||
rect: ni.rect, // We will be centered by default
|
||||
// Starts with no text. The text specified in the menu is hovertext
|
||||
}
|
||||
|
||||
widget := &Widget{
|
||||
Locator: ni.locator,
|
||||
Active: p.Active,
|
||||
ownClickables: []clickable{ni},
|
||||
ownPaintables: []paintable{ni},
|
||||
ownValueables: []valueable{ni.label},
|
||||
}
|
||||
|
||||
return ni, widget, nil
|
||||
}
|
||||
|
||||
// An overlay is a static image + some text that needs to be rendered
|
||||
func (d *Driver) buildOverlay(p *menus.Properties) (*noninteractive, *Widget, error) {
|
||||
ni, err := d.buildNoninteractive(p)
|
||||
@@ -116,6 +146,7 @@ func (d *Driver) buildOverlay(p *menus.Properties) (*noninteractive, *Widget, er
|
||||
|
||||
widget := &Widget{
|
||||
Locator: ni.locator,
|
||||
Active: p.Active,
|
||||
ownPaintables: []paintable{ni},
|
||||
}
|
||||
|
||||
@@ -124,9 +155,9 @@ func (d *Driver) buildOverlay(p *menus.Properties) (*noninteractive, *Widget, er
|
||||
fnt := d.menu.Font(p.FontType/10 - 1)
|
||||
|
||||
ni.label = &label{
|
||||
font: fnt,
|
||||
rect: ni.rect, // We will be centered by default
|
||||
text: p.Text,
|
||||
font: fnt,
|
||||
rect: ni.rect, // We will be centered by default
|
||||
valueImpl: valueImpl{str: p.Text},
|
||||
}
|
||||
} else {
|
||||
log.Printf("Overlay without text detected in %v", p.Locator)
|
||||
@@ -151,10 +182,11 @@ func (d *Driver) buildAnimationSample(p *menus.Properties) (*noninteractive, *Wi
|
||||
locator: p.Locator,
|
||||
frames: animation(frames),
|
||||
hoverImpl: hoverImpl{text: p.Text},
|
||||
rect: sprite.Rect,
|
||||
rect: sprite.Rect.Add(p.Point()),
|
||||
}
|
||||
|
||||
widget := &Widget{
|
||||
Active: p.Active,
|
||||
ownHoverables: []hoverable{ani},
|
||||
ownPaintables: []paintable{ani},
|
||||
}
|
||||
@@ -183,13 +215,14 @@ func (d *Driver) buildAnimationHover(p *menus.Properties) (*animationHover, *Wid
|
||||
locator: p.Locator,
|
||||
frames: animation(enterFrames),
|
||||
hoverImpl: hoverImpl{text: p.Text},
|
||||
rect: sprite.Rect,
|
||||
rect: sprite.Rect.Add(p.Point()),
|
||||
},
|
||||
|
||||
exitFrames: animation(exitFrames),
|
||||
}
|
||||
|
||||
widget := &Widget{
|
||||
Active: p.Active,
|
||||
ownHoverables: []hoverable{ani},
|
||||
ownPaintables: []paintable{ani},
|
||||
}
|
||||
@@ -248,6 +281,10 @@ func (a *animationHover) setHoverState(value bool) {
|
||||
a.hoverImpl.setHoverState(value)
|
||||
}
|
||||
|
||||
func (l *label) id() string {
|
||||
return l.locator
|
||||
}
|
||||
|
||||
// Top-left of where to start drawing the text. We want it to appear to be in
|
||||
// the centre of the rect.
|
||||
//
|
||||
@@ -255,7 +292,7 @@ func (a *animationHover) setHoverState(value bool) {
|
||||
func (l *label) pos() image.Point {
|
||||
pos := l.rect.Min
|
||||
|
||||
textRect := l.font.CalculateBounds(l.text)
|
||||
textRect := l.font.CalculateBounds(l.str)
|
||||
|
||||
// Centre the text horizontally
|
||||
if l.align == AlignModeCentre {
|
||||
@@ -282,15 +319,21 @@ func (l *label) regions(tick int) []region {
|
||||
|
||||
pt := l.pos()
|
||||
|
||||
for _, r := range l.text {
|
||||
glyph, err := l.font.Glyph(r)
|
||||
if err != nil {
|
||||
log.Printf("FIXME: ignoring misssing glyph %v", r)
|
||||
continue
|
||||
for _, r := range l.str {
|
||||
var sprite *assetstore.Sprite
|
||||
if glyph, err := l.font.Glyph(r); err != nil {
|
||||
if glyph, err := l.font.Glyph('?'); err != nil {
|
||||
log.Printf("FIXME: ignoring glyph %v", r)
|
||||
continue
|
||||
} else {
|
||||
sprite = glyph
|
||||
}
|
||||
} else {
|
||||
sprite = glyph
|
||||
}
|
||||
|
||||
out = append(out, oneRegion(pt, glyph.Image)...)
|
||||
pt.X += glyph.Rect.Dx()
|
||||
out = append(out, oneRegion(pt, sprite.Image)...)
|
||||
pt.X += sprite.Rect.Dx()
|
||||
}
|
||||
|
||||
return out
|
||||
|
@@ -20,6 +20,8 @@ type checkbox struct {
|
||||
type slider struct {
|
||||
locator string
|
||||
|
||||
rect image.Rectangle
|
||||
|
||||
baseSpr *assetstore.Sprite
|
||||
clickSpr *assetstore.Sprite
|
||||
sliderSpr *assetstore.Sprite
|
||||
@@ -42,6 +44,7 @@ func (d *Driver) buildCheckbox(p *menus.Properties) (*checkbox, *Widget, error)
|
||||
checkbox := &checkbox{
|
||||
button: button{
|
||||
locator: p.Locator,
|
||||
rect: sprites[0].Rect.Add(p.Point()),
|
||||
baseSpr: sprites[0], // unchecked
|
||||
clickSpr: sprites[2], // checked
|
||||
frozenSpr: sprites[1], // disabled
|
||||
@@ -51,6 +54,8 @@ func (d *Driver) buildCheckbox(p *menus.Properties) (*checkbox, *Widget, error)
|
||||
}
|
||||
|
||||
widget := &Widget{
|
||||
Locator: p.Locator,
|
||||
Active: p.Active,
|
||||
ownClickables: []clickable{checkbox},
|
||||
ownFreezables: []freezable{checkbox},
|
||||
ownHoverables: []hoverable{checkbox},
|
||||
@@ -69,6 +74,7 @@ func (d *Driver) buildSlider(p *menus.Properties) (*slider, *Widget, error) {
|
||||
|
||||
slider := &slider{
|
||||
locator: p.Locator,
|
||||
rect: sprites[0].Rect.Add(p.Point()),
|
||||
baseSpr: sprites[0],
|
||||
clickSpr: sprites[1],
|
||||
sliderSpr: sprites[2],
|
||||
@@ -76,6 +82,8 @@ func (d *Driver) buildSlider(p *menus.Properties) (*slider, *Widget, error) {
|
||||
}
|
||||
|
||||
widget := &Widget{
|
||||
Locator: p.Locator,
|
||||
Active: p.Active,
|
||||
ownClickables: []clickable{slider},
|
||||
ownMouseables: []mouseable{slider},
|
||||
ownPaintables: []paintable{slider},
|
||||
@@ -111,7 +119,7 @@ func (s *slider) id() string {
|
||||
|
||||
// The bounds of the slider are the whole thing
|
||||
func (s *slider) bounds() image.Rectangle {
|
||||
return s.baseSpr.Rect
|
||||
return s.rect
|
||||
}
|
||||
|
||||
func (s *slider) registerMouseClick() {
|
||||
|
@@ -61,6 +61,26 @@ func (d *Driver) SetFreeze(id string, value bool) error {
|
||||
return fmt.Errorf("Couldn't find clickable widget %v:%v", d.menu.Name, id)
|
||||
}
|
||||
|
||||
func (d *Driver) ToggleActive(locator string) error {
|
||||
if widget := d.findWidget(locator); widget != nil {
|
||||
widget.Active = !widget.Active
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("Couldn't find activatable widget %v to toggle", locator)
|
||||
}
|
||||
|
||||
func (d *Driver) SetActive(locator string, value bool) error {
|
||||
if widget := d.findWidget(locator); widget != nil {
|
||||
widget.Active = value
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("Couldn't find activeatable widget %v to set to %v", locator, value)
|
||||
}
|
||||
|
||||
func (d *Driver) OnClick(id string, f func()) error {
|
||||
for _, clickable := range d.allClickables() {
|
||||
if clickable.id() == d.realId(id) {
|
||||
|
@@ -3,6 +3,7 @@ package ui
|
||||
type Widget struct {
|
||||
Locator string
|
||||
Children []*Widget
|
||||
Active bool
|
||||
|
||||
ownClickables []clickable
|
||||
ownFreezables []freezable
|
||||
@@ -12,62 +13,130 @@ type Widget struct {
|
||||
ownValueables []valueable
|
||||
}
|
||||
|
||||
func (w *Widget) clickables() []clickable {
|
||||
func (w *Widget) allClickables() []clickable {
|
||||
out := w.ownClickables
|
||||
|
||||
for _, widget := range w.Children {
|
||||
out = append(out, widget.clickables()...)
|
||||
out = append(out, widget.allClickables()...)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (w *Widget) freezables() []freezable {
|
||||
func (w *Widget) allFreezables() []freezable {
|
||||
out := w.ownFreezables
|
||||
|
||||
for _, widget := range w.Children {
|
||||
out = append(out, widget.freezables()...)
|
||||
out = append(out, widget.allFreezables()...)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (w *Widget) hoverables() []hoverable {
|
||||
out := w.ownHoverables
|
||||
|
||||
for _, widget := range w.Children {
|
||||
out = append(out, widget.hoverables()...)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (w *Widget) mouseables() []mouseable {
|
||||
out := w.ownMouseables
|
||||
|
||||
for _, widget := range w.Children {
|
||||
out = append(out, widget.mouseables()...)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (w *Widget) paintables() []paintable {
|
||||
out := w.ownPaintables
|
||||
|
||||
for _, widget := range w.Children {
|
||||
out = append(out, widget.paintables()...)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (w *Widget) valueables() []valueable {
|
||||
func (w *Widget) allValueables() []valueable {
|
||||
out := w.ownValueables
|
||||
|
||||
for _, widget := range w.Children {
|
||||
out = append(out, widget.valueables()...)
|
||||
out = append(out, widget.allValueables()...)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (w *Widget) activeClickables() []clickable {
|
||||
if !w.Active {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := w.ownClickables
|
||||
|
||||
for _, widget := range w.Children {
|
||||
out = append(out, widget.activeClickables()...)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (w *Widget) activeFreezables() []freezable {
|
||||
if !w.Active {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := w.ownFreezables
|
||||
|
||||
for _, widget := range w.Children {
|
||||
out = append(out, widget.activeFreezables()...)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (w *Widget) activeHoverables() []hoverable {
|
||||
if !w.Active {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := w.ownHoverables
|
||||
|
||||
for _, widget := range w.Children {
|
||||
out = append(out, widget.activeHoverables()...)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (w *Widget) activeMouseables() []mouseable {
|
||||
if !w.Active {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := w.ownMouseables
|
||||
|
||||
for _, widget := range w.Children {
|
||||
out = append(out, widget.activeMouseables()...)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (w *Widget) activePaintables() []paintable {
|
||||
if !w.Active {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := w.ownPaintables
|
||||
|
||||
for _, widget := range w.Children {
|
||||
out = append(out, widget.activePaintables()...)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (w *Widget) activeValueables() []valueable {
|
||||
if !w.Active {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := w.ownValueables
|
||||
|
||||
for _, widget := range w.Children {
|
||||
out = append(out, widget.activeValueables()...)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (w *Widget) findWidget(locator string) *Widget {
|
||||
if w.Locator == locator {
|
||||
return w
|
||||
}
|
||||
|
||||
for _, child := range w.Children {
|
||||
if found := child.findWidget(locator); found != nil {
|
||||
return found
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@@ -8,9 +8,9 @@ import (
|
||||
"runtime/debug"
|
||||
"runtime/pprof"
|
||||
|
||||
"github.com/hajimehoshi/ebiten"
|
||||
"github.com/hajimehoshi/ebiten/ebitenutil"
|
||||
"github.com/hajimehoshi/ebiten/inpututil"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
||||
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||
)
|
||||
|
||||
type Game interface {
|
||||
@@ -37,6 +37,8 @@ type Window struct {
|
||||
MouseWheelHandler func(float64, float64)
|
||||
MouseClickHandler func()
|
||||
|
||||
WhileKeyDownHandlers map[ebiten.Key]func()
|
||||
|
||||
// Allow the "game" to be switched out at any time
|
||||
game Game
|
||||
|
||||
@@ -51,16 +53,17 @@ type Window struct {
|
||||
//
|
||||
// ebiten assumes a single window, so only call this once...
|
||||
func NewWindow(game Game, title string, xRes int, yRes int) (*Window, error) {
|
||||
ebiten.SetRunnableInBackground(true)
|
||||
|
||||
return &Window{
|
||||
Title: title,
|
||||
Title: title,
|
||||
debug: true,
|
||||
firstRun: true,
|
||||
game: game,
|
||||
xRes: xRes,
|
||||
yRes: yRes,
|
||||
|
||||
WhileKeyDownHandlers: make(map[ebiten.Key]func()),
|
||||
|
||||
KeyUpHandlers: make(map[ebiten.Key]func()),
|
||||
debug: true,
|
||||
firstRun: true,
|
||||
game: game,
|
||||
xRes: xRes,
|
||||
yRes: yRes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -69,6 +72,10 @@ func (w *Window) OnKeyUp(key ebiten.Key, f func()) {
|
||||
w.KeyUpHandlers[key] = f
|
||||
}
|
||||
|
||||
func (w *Window) WhileKeyDown(key ebiten.Key, f func()) {
|
||||
w.WhileKeyDownHandlers[key] = f
|
||||
}
|
||||
|
||||
func (w *Window) OnMouseWheel(f func(x, y float64)) {
|
||||
w.MouseWheelHandler = f
|
||||
}
|
||||
@@ -100,10 +107,12 @@ func (w *Window) drawCursor(screen *ebiten.Image) error {
|
||||
|
||||
ebiten.SetCursorMode(ebiten.CursorModeHidden)
|
||||
|
||||
return screen.DrawImage(cursor, op)
|
||||
screen.DrawImage(cursor, op)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Window) Update(screen *ebiten.Image) (outErr error) {
|
||||
func (w *Window) Update() (outErr error) {
|
||||
// Ebiten does not like it if we panic inside its main loop
|
||||
defer func() {
|
||||
if panicErr := recover(); panicErr != nil {
|
||||
@@ -115,11 +124,13 @@ func (w *Window) Update(screen *ebiten.Image) (outErr error) {
|
||||
}
|
||||
}()
|
||||
|
||||
if err := w.game.Update(screen.Size()); err != nil {
|
||||
// FIXME: remove need for update generally
|
||||
if err := w.game.Update(w.xRes, w.yRes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Process keys
|
||||
// Process keys.
|
||||
// FIXME: : should this happen before or after update?
|
||||
// TODO: efficient set operations
|
||||
|
||||
for key, cb := range w.KeyUpHandlers {
|
||||
@@ -128,6 +139,12 @@ func (w *Window) Update(screen *ebiten.Image) (outErr error) {
|
||||
}
|
||||
}
|
||||
|
||||
for key, cb := range w.WhileKeyDownHandlers {
|
||||
if ebiten.IsKeyPressed(key) {
|
||||
cb()
|
||||
}
|
||||
}
|
||||
|
||||
if w.MouseWheelHandler != nil {
|
||||
x, y := ebiten.Wheel()
|
||||
if x != 0 || y != 0 {
|
||||
@@ -141,13 +158,11 @@ func (w *Window) Update(screen *ebiten.Image) (outErr error) {
|
||||
}
|
||||
}
|
||||
|
||||
if ebiten.IsDrawingSkipped() {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := w.game.Draw(screen); err != nil {
|
||||
return err
|
||||
}
|
||||
func (w *Window) Draw(screen *ebiten.Image) {
|
||||
w.game.Draw(screen)
|
||||
|
||||
if w.debug {
|
||||
// Draw FPS, etc, to the screen
|
||||
@@ -156,7 +171,7 @@ func (w *Window) Update(screen *ebiten.Image) (outErr error) {
|
||||
}
|
||||
|
||||
// Draw the cursor last
|
||||
return w.drawCursor(screen)
|
||||
w.drawCursor(screen)
|
||||
}
|
||||
|
||||
// TODO: a stop or other cancellation mechanism
|
||||
|
@@ -1,34 +0,0 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
lines = File.read("palette.ppm").split("\n")
|
||||
lines = lines.select { |l| l[0] != "#" }
|
||||
|
||||
raise "Not an ASCII ppm" if lines.shift != "P3"
|
||||
raise "Incorrect dimensions" unless lines.shift == "1 256"
|
||||
raise "Incorrect maxval" unless lines.shift == "255"
|
||||
raise "Too many lines left" unless lines.size == 768
|
||||
|
||||
puts <<EOF
|
||||
package data
|
||||
|
||||
import "image/color"
|
||||
|
||||
var (
|
||||
Transparent = color.RGBA{R: 0, G: 0, B: 0, A: 0}
|
||||
|
||||
ColorPalette = color.Palette{
|
||||
Transparent,
|
||||
EOF
|
||||
|
||||
lines.shift(3) # Ignore idx 0
|
||||
|
||||
255.times do
|
||||
r, g, b = lines.shift(3).map(&:to_i)
|
||||
|
||||
puts "\t\tcolor.RGBA{R: #{r}, G: #{g}, B: #{b}, A: 255},"
|
||||
end
|
||||
|
||||
puts <<EOF
|
||||
}
|
||||
)
|
||||
EOF
|
@@ -1,12 +0,0 @@
|
||||
# jungle floor
|
||||
# jungtil.obj/.asn
|
||||
# /--> d:\warflics\missions\jungtil.flc
|
||||
#
|
||||
|
||||
0:DEF 2;
|
||||
|
||||
0:TYPE 2;
|
||||
|
||||
END OF FILE
|
||||
|
||||
|
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,772 +0,0 @@
|
||||
P3
|
||||
# CREATOR: GIMP PNM Filter Version 1.1
|
||||
1 256
|
||||
255
|
||||
255
|
||||
255
|
||||
255
|
||||
128
|
||||
0
|
||||
0
|
||||
0
|
||||
128
|
||||
0
|
||||
128
|
||||
128
|
||||
0
|
||||
0
|
||||
0
|
||||
128
|
||||
128
|
||||
0
|
||||
128
|
||||
0
|
||||
128
|
||||
128
|
||||
192
|
||||
192
|
||||
192
|
||||
192
|
||||
220
|
||||
192
|
||||
166
|
||||
202
|
||||
240
|
||||
255
|
||||
255
|
||||
255
|
||||
240
|
||||
240
|
||||
240
|
||||
221
|
||||
221
|
||||
221
|
||||
203
|
||||
203
|
||||
203
|
||||
187
|
||||
187
|
||||
187
|
||||
178
|
||||
178
|
||||
178
|
||||
168
|
||||
168
|
||||
168
|
||||
157
|
||||
157
|
||||
157
|
||||
147
|
||||
147
|
||||
147
|
||||
137
|
||||
137
|
||||
137
|
||||
127
|
||||
127
|
||||
127
|
||||
117
|
||||
117
|
||||
117
|
||||
106
|
||||
106
|
||||
106
|
||||
96
|
||||
96
|
||||
96
|
||||
86
|
||||
86
|
||||
86
|
||||
76
|
||||
76
|
||||
76
|
||||
61
|
||||
61
|
||||
61
|
||||
49
|
||||
49
|
||||
49
|
||||
36
|
||||
36
|
||||
36
|
||||
24
|
||||
24
|
||||
24
|
||||
12
|
||||
12
|
||||
12
|
||||
0
|
||||
0
|
||||
0
|
||||
134
|
||||
134
|
||||
255
|
||||
113
|
||||
113
|
||||
241
|
||||
93
|
||||
93
|
||||
228
|
||||
72
|
||||
72
|
||||
214
|
||||
63
|
||||
63
|
||||
200
|
||||
55
|
||||
55
|
||||
186
|
||||
46
|
||||
46
|
||||
172
|
||||
38
|
||||
38
|
||||
158
|
||||
29
|
||||
29
|
||||
144
|
||||
20
|
||||
20
|
||||
131
|
||||
12
|
||||
12
|
||||
117
|
||||
3
|
||||
3
|
||||
103
|
||||
3
|
||||
3
|
||||
91
|
||||
3
|
||||
3
|
||||
79
|
||||
3
|
||||
3
|
||||
68
|
||||
3
|
||||
3
|
||||
56
|
||||
255
|
||||
145
|
||||
145
|
||||
242
|
||||
123
|
||||
123
|
||||
230
|
||||
101
|
||||
101
|
||||
217
|
||||
79
|
||||
79
|
||||
205
|
||||
70
|
||||
70
|
||||
193
|
||||
61
|
||||
61
|
||||
181
|
||||
53
|
||||
53
|
||||
169
|
||||
44
|
||||
44
|
||||
157
|
||||
35
|
||||
35
|
||||
144
|
||||
26
|
||||
26
|
||||
132
|
||||
18
|
||||
18
|
||||
120
|
||||
9
|
||||
9
|
||||
108
|
||||
8
|
||||
8
|
||||
94
|
||||
7
|
||||
7
|
||||
79
|
||||
7
|
||||
7
|
||||
65
|
||||
6
|
||||
6
|
||||
147
|
||||
142
|
||||
185
|
||||
132
|
||||
126
|
||||
172
|
||||
117
|
||||
109
|
||||
159
|
||||
102
|
||||
93
|
||||
146
|
||||
95
|
||||
86
|
||||
133
|
||||
88
|
||||
78
|
||||
123
|
||||
82
|
||||
73
|
||||
115
|
||||
77
|
||||
67
|
||||
107
|
||||
72
|
||||
61
|
||||
100
|
||||
67
|
||||
55
|
||||
92
|
||||
61
|
||||
50
|
||||
84
|
||||
56
|
||||
44
|
||||
76
|
||||
51
|
||||
38
|
||||
68
|
||||
46
|
||||
32
|
||||
60
|
||||
40
|
||||
27
|
||||
52
|
||||
35
|
||||
21
|
||||
44
|
||||
200
|
||||
150
|
||||
137
|
||||
187
|
||||
130
|
||||
115
|
||||
175
|
||||
110
|
||||
94
|
||||
164
|
||||
95
|
||||
77
|
||||
154
|
||||
79
|
||||
61
|
||||
143
|
||||
64
|
||||
44
|
||||
137
|
||||
60
|
||||
42
|
||||
132
|
||||
55
|
||||
38
|
||||
125
|
||||
50
|
||||
33
|
||||
120
|
||||
48
|
||||
29
|
||||
111
|
||||
44
|
||||
26
|
||||
103
|
||||
39
|
||||
24
|
||||
94
|
||||
35
|
||||
21
|
||||
83
|
||||
30
|
||||
18
|
||||
72
|
||||
25
|
||||
14
|
||||
61
|
||||
20
|
||||
10
|
||||
121
|
||||
107
|
||||
34
|
||||
109
|
||||
94
|
||||
29
|
||||
96
|
||||
82
|
||||
25
|
||||
84
|
||||
69
|
||||
20
|
||||
77
|
||||
62
|
||||
17
|
||||
70
|
||||
55
|
||||
14
|
||||
63
|
||||
47
|
||||
12
|
||||
56
|
||||
40
|
||||
9
|
||||
93
|
||||
120
|
||||
53
|
||||
80
|
||||
103
|
||||
42
|
||||
66
|
||||
86
|
||||
31
|
||||
53
|
||||
69
|
||||
20
|
||||
49
|
||||
60
|
||||
16
|
||||
45
|
||||
52
|
||||
12
|
||||
43
|
||||
44
|
||||
10
|
||||
43
|
||||
38
|
||||
8
|
||||
136
|
||||
145
|
||||
44
|
||||
118
|
||||
128
|
||||
37
|
||||
101
|
||||
111
|
||||
30
|
||||
83
|
||||
94
|
||||
23
|
||||
70
|
||||
79
|
||||
17
|
||||
56
|
||||
65
|
||||
11
|
||||
42
|
||||
50
|
||||
6
|
||||
28
|
||||
36
|
||||
0
|
||||
57
|
||||
134
|
||||
64
|
||||
48
|
||||
118
|
||||
54
|
||||
38
|
||||
101
|
||||
43
|
||||
29
|
||||
85
|
||||
33
|
||||
22
|
||||
71
|
||||
25
|
||||
15
|
||||
58
|
||||
17
|
||||
7
|
||||
45
|
||||
8
|
||||
0
|
||||
32
|
||||
0
|
||||
143
|
||||
87
|
||||
56
|
||||
126
|
||||
75
|
||||
45
|
||||
110
|
||||
64
|
||||
35
|
||||
93
|
||||
52
|
||||
24
|
||||
85
|
||||
44
|
||||
16
|
||||
72
|
||||
36
|
||||
12
|
||||
64
|
||||
32
|
||||
8
|
||||
56
|
||||
24
|
||||
4
|
||||
127
|
||||
96
|
||||
54
|
||||
115
|
||||
85
|
||||
46
|
||||
102
|
||||
75
|
||||
39
|
||||
90
|
||||
64
|
||||
31
|
||||
82
|
||||
57
|
||||
25
|
||||
75
|
||||
51
|
||||
20
|
||||
68
|
||||
44
|
||||
15
|
||||
61
|
||||
38
|
||||
10
|
||||
141
|
||||
86
|
||||
56
|
||||
126
|
||||
75
|
||||
46
|
||||
110
|
||||
65
|
||||
36
|
||||
95
|
||||
54
|
||||
26
|
||||
88
|
||||
51
|
||||
25
|
||||
73
|
||||
40
|
||||
18
|
||||
57
|
||||
29
|
||||
10
|
||||
42
|
||||
18
|
||||
3
|
||||
172
|
||||
199
|
||||
199
|
||||
138
|
||||
173
|
||||
173
|
||||
104
|
||||
148
|
||||
148
|
||||
71
|
||||
122
|
||||
122
|
||||
37
|
||||
97
|
||||
97
|
||||
3
|
||||
71
|
||||
71
|
||||
4
|
||||
56
|
||||
56
|
||||
4
|
||||
41
|
||||
41
|
||||
217
|
||||
209
|
||||
200
|
||||
202
|
||||
194
|
||||
184
|
||||
188
|
||||
178
|
||||
167
|
||||
173
|
||||
163
|
||||
151
|
||||
158
|
||||
147
|
||||
134
|
||||
148
|
||||
136
|
||||
123
|
||||
137
|
||||
125
|
||||
112
|
||||
126
|
||||
114
|
||||
101
|
||||
116
|
||||
104
|
||||
91
|
||||
105
|
||||
93
|
||||
80
|
||||
94
|
||||
82
|
||||
69
|
||||
84
|
||||
71
|
||||
58
|
||||
73
|
||||
60
|
||||
47
|
||||
62
|
||||
50
|
||||
36
|
||||
52
|
||||
39
|
||||
25
|
||||
41
|
||||
28
|
||||
14
|
||||
231
|
||||
232
|
||||
207
|
||||
219
|
||||
217
|
||||
180
|
||||
208
|
||||
201
|
||||
152
|
||||
196
|
||||
186
|
||||
125
|
||||
184
|
||||
171
|
||||
98
|
||||
173
|
||||
155
|
||||
70
|
||||
161
|
||||
140
|
||||
43
|
||||
150
|
||||
129
|
||||
39
|
||||
139
|
||||
119
|
||||
37
|
||||
127
|
||||
109
|
||||
33
|
||||
117
|
||||
99
|
||||
29
|
||||
105
|
||||
89
|
||||
25
|
||||
90
|
||||
76
|
||||
21
|
||||
75
|
||||
62
|
||||
18
|
||||
60
|
||||
49
|
||||
14
|
||||
45
|
||||
35
|
||||
10
|
||||
128
|
||||
99
|
||||
127
|
||||
113
|
||||
73
|
||||
112
|
||||
97
|
||||
47
|
||||
97
|
||||
82
|
||||
21
|
||||
82
|
||||
75
|
||||
2
|
||||
74
|
||||
68
|
||||
2
|
||||
67
|
||||
52
|
||||
3
|
||||
50
|
||||
35
|
||||
4
|
||||
33
|
||||
247
|
||||
178
|
||||
102
|
||||
229
|
||||
152
|
||||
75
|
||||
212
|
||||
127
|
||||
48
|
||||
194
|
||||
101
|
||||
21
|
||||
179
|
||||
87
|
||||
16
|
||||
161
|
||||
73
|
||||
12
|
||||
142
|
||||
59
|
||||
9
|
||||
124
|
||||
45
|
||||
5
|
||||
255
|
||||
0
|
||||
0
|
||||
194
|
||||
3
|
||||
3
|
||||
161
|
||||
2
|
||||
2
|
||||
255
|
||||
227
|
||||
11
|
||||
209
|
||||
185
|
||||
8
|
||||
169
|
||||
150
|
||||
6
|
||||
103
|
||||
190
|
||||
255
|
||||
2
|
||||
130
|
||||
232
|
||||
4
|
||||
4
|
||||
209
|
||||
0
|
||||
255
|
||||
0
|
||||
7
|
||||
180
|
||||
7
|
||||
3
|
||||
132
|
||||
3
|
||||
255
|
||||
114
|
||||
230
|
||||
255
|
||||
17
|
||||
205
|
||||
203
|
||||
6
|
||||
156
|
||||
246
|
||||
164
|
||||
73
|
||||
221
|
||||
123
|
||||
16
|
||||
177
|
||||
95
|
||||
4
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
132
|
||||
18
|
||||
18
|
||||
113
|
||||
13
|
||||
13
|
||||
94
|
||||
7
|
||||
7
|
||||
107
|
||||
8
|
||||
8
|
||||
120
|
||||
9
|
||||
9
|
||||
193
|
||||
62
|
||||
62
|
||||
181
|
||||
54
|
||||
54
|
||||
168
|
||||
45
|
||||
45
|
||||
157
|
||||
36
|
||||
36
|
||||
144
|
||||
27
|
||||
27
|
||||
62
|
||||
50
|
||||
36
|
||||
57
|
||||
45
|
||||
31
|
||||
52
|
||||
39
|
||||
25
|
||||
46
|
||||
34
|
||||
20
|
||||
41
|
||||
28
|
||||
14
|
||||
255
|
||||
251
|
||||
240
|
||||
160
|
||||
160
|
||||
164
|
||||
128
|
||||
128
|
||||
128
|
||||
255
|
||||
0
|
||||
0
|
||||
0
|
||||
255
|
||||
0
|
||||
255
|
||||
255
|
||||
0
|
||||
0
|
||||
0
|
||||
255
|
||||
255
|
||||
0
|
||||
255
|
||||
0
|
||||
255
|
||||
255
|
||||
255
|
||||
255
|
||||
255
|
Binary file not shown.
@@ -1,13 +0,0 @@
|
||||
To build this palette, I did the following:
|
||||
|
||||
* Created the .asn + .obj files by hand
|
||||
* Referenced them in a .set file and asked WH40K_TD.exe to render the sprites
|
||||
* Screenshotted the output and used GIMP to create a 1x255 image with them all
|
||||
* Exported that image as an ASCII .ppm file
|
||||
* Used the `generate_palette` script to output Go code \o/
|
||||
|
||||
Note that palette index 0 is ignored, and hardcoded to be transparent black.
|
||||
This is because 0x00 seems to be used as a record separator in .obj files, so
|
||||
can't be used as a palette index anyway.
|
||||
|
||||
|
Binary file not shown.
45
scripts/palette-from-pcx
Executable file
45
scripts/palette-from-pcx
Executable file
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
if ARGV.size != 2
|
||||
puts "Usage: $0 <filename.pcx> <Palette name>"
|
||||
exit 1
|
||||
end
|
||||
|
||||
FILENAME = ARGV[0]
|
||||
PALETTENAME = ARGV[1]
|
||||
|
||||
f = File.open(FILENAME)
|
||||
|
||||
# In a 256-colour PCX file, the palette is stored in the final 768 bytes as RGB
|
||||
# triplets, and the byte before that should be 0x0C: https://en.wikipedia.org/wiki/PCX#Color_palette
|
||||
f.seek(-769, IO::SEEK_END)
|
||||
|
||||
raise "Check byte is wrong" unless f.read(1) == "\x0C"
|
||||
|
||||
puts <<EOF
|
||||
package palettes
|
||||
|
||||
import "image/color"
|
||||
|
||||
var (
|
||||
#{PALETTENAME}Palette = color.Palette{
|
||||
Transparent,
|
||||
|
||||
EOF
|
||||
|
||||
f.read(3) # Ignore idx 0 so we can make it transparent
|
||||
|
||||
255.times do
|
||||
r, g, b = f.read(3).bytes
|
||||
|
||||
puts "\t\tcolor.RGBA{R: #{r}, G: #{g}, B: #{b}, A: 255},"
|
||||
end
|
||||
|
||||
puts <<EOF
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
Palettes["#{PALETTENAME}"] = #{PALETTENAME}Palette
|
||||
}
|
||||
EOF
|
Reference in New Issue
Block a user