From 7677c30572ceef8d5db5cca0ac3f11240006af63 Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Sat, 13 Jun 2020 15:07:32 +0100 Subject: [PATCH] Start displaying characters on maps --- doc/formats/maps.md | 51 ++++++++++++++++--------------- internal/assetstore/ani.go | 50 ++++++++++++++++++++++++++++++ internal/assetstore/assetstore.go | 3 ++ internal/assetstore/data.go | 20 ++++++++++++ internal/assetstore/map.go | 19 +++++------- internal/data/has_action.go | 47 ++++++++++++++++++++++++++++ internal/maps/maps.go | 11 +++++-- internal/scenario/draw.go | 22 ++++++------- 8 files changed, 173 insertions(+), 50 deletions(-) diff --git a/doc/formats/maps.md b/doc/formats/maps.md index 7bd2fa8..66b255e 100644 --- a/doc/formats/maps.md +++ b/doc/formats/maps.md @@ -631,26 +631,45 @@ suggested above: | Offset | Size | Meaning | | ------ | ---- | ------- | -| 0 | 179 | ??? | +| 0 | 178 | ??? | +| 178 | 1 | Character type | | 179 | 80 | Character name | -| 259 | 10 | Character attributes | +| 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 | 927 | ??? | 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. Looking -at the bytes for one character record, I can easily correlate certain bytes to -various attributes; that's just done in code for the moment. +just contains `\x02` and is then null-terminated all the way through - but maybe +that's just a data thing. -Given two characters of the same time, just in different locations, differing +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 - captains mess up a direct +correlation - but a fixed offset table allows me to draw the characters \o/. +Orientation is not yet deciphered, however. + +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, @@ -658,10 +677,7 @@ 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}`. - -Characters don't seem to take up multiple x,y squares, but they *can* take up -multiple Z levels. So maybe this is a bounding box of some kind? +`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 @@ -673,21 +689,6 @@ 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. -How about their types? We need to be able to work out which animations to show, -so they must be uniquely identified somehow. Ultramarine captain is animation -group 12, while chaos lord is group... 14? Not certain. I don't see either of -those numbers in a promising spot, anyway. - - -I do see differences at around `0x170`: - -``` -Ultramarine Captain: 00 00 00 00 00 00 00 00 50 03 00 00 00 00 00 00 -Chaos Lord: 00 00 00 00 11 01 00 00 47 03 00 04 00 00 02 00 -``` - -Maybe they're somewhat relevant, it's hard to say. - Thingies next: these aren't decoded at all yet, and the sizes seem to be variable. diff --git a/internal/assetstore/ani.go b/internal/assetstore/ani.go index c9c4500..89ddf6c 100644 --- a/internal/assetstore/ani.go +++ b/internal/assetstore/ani.go @@ -1,6 +1,9 @@ package assetstore import ( + "fmt" + + "code.ur.gs/lupine/ordoor/internal/data" "code.ur.gs/lupine/ordoor/internal/idx" ) @@ -76,3 +79,50 @@ func (a *AssetStore) Animation(groupIdx, recIdx int) (*Animation, error) { return &Animation{Frames: sprites}, nil } + +func (a *AssetStore) CharacterAnimation(ctype data.CharacterType, action data.AnimAction) (*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) + } + + rec := ha.Index(ctype, action) + + return a.Animation(group, rec) +} diff --git a/internal/assetstore/assetstore.go b/internal/assetstore/assetstore.go index 381911b..cdeaefb 100644 --- a/internal/assetstore/assetstore.go +++ b/internal/assetstore/assetstore.go @@ -45,6 +45,7 @@ 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 @@ -111,6 +112,8 @@ 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) diff --git a/internal/assetstore/data.go b/internal/assetstore/data.go index 4f64b4c..52f1715 100644 --- a/internal/assetstore/data.go +++ b/internal/assetstore/data.go @@ -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 } diff --git a/internal/assetstore/map.go b/internal/assetstore/map.go index b2ac8d4..785440a 100644 --- a/internal/assetstore/map.go +++ b/internal/assetstore/map.go @@ -5,6 +5,7 @@ import ( "image" "log" + "code.ur.gs/lupine/ordoor/internal/data" "code.ur.gs/lupine/ordoor/internal/maps" ) @@ -102,19 +103,15 @@ func (m *Map) SpritesForCell(x, y, z int) ([]*Sprite, error) { sprites = append(sprites, sprite) } - // FIXME: this just marks character positions with sprite 19 for now. - specialsObj, err := m.assets.Object("specials") - if err != nil { - return nil, err - } - chrSpr, err := specialsObj.Sprite(19) - if err != nil { - return nil, err - } - for _, chr := range m.raw.Characters { if chr.XPos == x && chr.YPos == y && z == 1 { // FIXME: sort out ZPos - sprites = append(sprites, chrSpr) + // Look up the correct animation, get the frame, boom + anim, err := m.assets.CharacterAnimation(chr.Type, data.AnimActionNone) + if err != nil { + return nil, err + } + + sprites = append(sprites, anim.Frames[0]) } } diff --git a/internal/data/has_action.go b/internal/data/has_action.go index ecd9553..ec912cf 100644 --- a/internal/data/has_action.go +++ b/internal/data/has_action.go @@ -94,6 +94,42 @@ type HasAction struct { bits bitfield.BitField } +var ( + 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 (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 +197,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++ { diff --git a/internal/maps/maps.go b/internal/maps/maps.go index 7bb16f8..94ed490 100644 --- a/internal/maps/maps.go +++ b/internal/maps/maps.go @@ -12,6 +12,8 @@ import ( "strings" "github.com/lunixbochs/struc" + + "code.ur.gs/lupine/ordoor/internal/data" ) var ( @@ -99,8 +101,9 @@ type Cell struct { } type Character struct { - Unknown1 []byte `struc:"[179]byte"` - Name string `struc:"[80]byte"` + 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"` @@ -339,9 +342,11 @@ func nullTerminate(s *string) { func (c *Character) String() string { return fmt.Sprintf( - "squad=%v pos=(%v,%v) name=%q\n\t%3d %3d %3d %3d %3d\n\t%3d %3d ??? ??? %3d\n", + "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, diff --git a/internal/scenario/draw.go b/internal/scenario/draw.go index 9916dcd..358556a 100644 --- a/internal/scenario/draw.go +++ b/internal/scenario/draw.go @@ -51,13 +51,13 @@ func (s *Scenario) Draw(screen *ebiten.Image) error { sw, sh := screen.Size() topLeft := CartPt{ - X: float64(s.Viewpoint.X) - (2*cellWidth/s.Zoom), // Ensure all visible cells are rendered - Y: float64(s.Viewpoint.Y) - (2*cellHeight/s.Zoom), + X: float64(s.Viewpoint.X) - (2 * cellWidth / s.Zoom), // Ensure all visible cells are rendered + Y: float64(s.Viewpoint.Y) - (2 * cellHeight / s.Zoom), }.ToISO() bottomRight := CartPt{ - X: float64(s.Viewpoint.X) + (float64(sw)/s.Zoom) + (2*cellHeight/s.Zoom), - Y: float64(s.Viewpoint.Y) + (float64(sh)/s.Zoom) + (5*cellHeight/s.Zoom), // Z dimension requires it + 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 @@ -121,7 +121,7 @@ func (s *Scenario) Draw(screen *ebiten.Image) error { if err := screen.DrawImage(spr.Image, &op); err != nil { return err } - + x1, y1 := geo.Apply(0, 0) ebitenutil.DebugPrintAt( screen, @@ -133,12 +133,12 @@ func (s *Scenario) Draw(screen *ebiten.Image) error { 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 + // 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