Make a start on font rendering
I was hopeful I could use ebiten/text, but font.Face doesn't seem set up for fixed-colour fonts.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,7 @@
|
|||||||
/loader
|
/loader
|
||||||
/orig
|
/orig
|
||||||
/palette-idx
|
/palette-idx
|
||||||
|
/view-font
|
||||||
/view-obj
|
/view-obj
|
||||||
/view-map
|
/view-map
|
||||||
/view-minimap
|
/view-minimap
|
||||||
|
7
Makefile
7
Makefile
@@ -2,7 +2,7 @@ srcfiles = Makefile go.mod $(shell find . -iname *.go)
|
|||||||
|
|
||||||
GOBUILD ?= go build -tags ebitengl
|
GOBUILD ?= go build -tags ebitengl
|
||||||
|
|
||||||
all: loader ordoor palette-idx view-obj view-map view-menu view-minimap view-set
|
all: loader ordoor palette-idx view-font view-obj view-map view-menu view-minimap view-set
|
||||||
|
|
||||||
loader: $(srcfiles)
|
loader: $(srcfiles)
|
||||||
$(GOBUILD) -o loader ./cmd/loader
|
$(GOBUILD) -o loader ./cmd/loader
|
||||||
@@ -10,6 +10,9 @@ loader: $(srcfiles)
|
|||||||
palette-idx: $(srcfiles)
|
palette-idx: $(srcfiles)
|
||||||
$(GOBUILD) -o palette-idx ./cmd/palette-idx
|
$(GOBUILD) -o palette-idx ./cmd/palette-idx
|
||||||
|
|
||||||
|
view-font: $(srcfiles)
|
||||||
|
$(GOBUILD) -o view-font ./cmd/view-font
|
||||||
|
|
||||||
view-obj: $(srcfiles)
|
view-obj: $(srcfiles)
|
||||||
$(GOBUILD) -o view-obj ./cmd/view-obj
|
$(GOBUILD) -o view-obj ./cmd/view-obj
|
||||||
|
|
||||||
@@ -29,6 +32,6 @@ ordoor: $(srcfiles)
|
|||||||
$(GOBUILD) -o ordoor ./cmd/ordoor
|
$(GOBUILD) -o ordoor ./cmd/ordoor
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f loader ordoor view-obj view-map view-minimap view-set palette-idx
|
rm -f loader ordoor view-obj view-map view-minimap view-set palette-idx view-font
|
||||||
|
|
||||||
.PHONY: all clean
|
.PHONY: all clean
|
||||||
|
101
cmd/view-font/main.go
Normal file
101
cmd/view-font/main.go
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"image"
|
||||||
|
"log"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten"
|
||||||
|
|
||||||
|
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
||||||
|
"code.ur.gs/lupine/ordoor/internal/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
|
||||||
|
fontName = flag.String("font", "", "Name of a font, e.g., basfont12")
|
||||||
|
txt = flag.String("text", "Test string", "Text to render")
|
||||||
|
|
||||||
|
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 {
|
||||||
|
font *assetstore.Font
|
||||||
|
step int
|
||||||
|
state state
|
||||||
|
lastState state
|
||||||
|
}
|
||||||
|
|
||||||
|
type state struct {
|
||||||
|
zoom float64
|
||||||
|
origin image.Point
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *gamePath == "" || *fontName == "" {
|
||||||
|
flag.Usage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
assets, err := assetstore.New(*gamePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
font, err := assets.Font(*fontName)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Couldn't load font %s: %v", *fontName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
state := state{zoom: 8.0}
|
||||||
|
env := &env{
|
||||||
|
font: font,
|
||||||
|
state: state,
|
||||||
|
lastState: state,
|
||||||
|
}
|
||||||
|
|
||||||
|
win, err := ui.NewWindow(env, "View Font: "+*fontName, *winX, *winY)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Couldn't create window: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
win.OnMouseWheel(env.changeZoom)
|
||||||
|
|
||||||
|
// Main thread now belongs to ebiten
|
||||||
|
if err := win.Run(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *env) Update(screenX, screenY int) error {
|
||||||
|
if e.step == 0 || e.lastState != e.state {
|
||||||
|
log.Printf("new state: zoom=%.2f", e.state.zoom)
|
||||||
|
}
|
||||||
|
|
||||||
|
e.step += 1
|
||||||
|
e.lastState = e.state
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *env) Draw(screen *ebiten.Image) error {
|
||||||
|
op := &ebiten.DrawImageOptions{}
|
||||||
|
op.GeoM.Scale(e.state.zoom, e.state.zoom) // apply current zoom factor
|
||||||
|
|
||||||
|
img, err := e.font.DrawLine(*txt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return screen.DrawImage(img, op)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *env) changeZoom(_, y float64) {
|
||||||
|
// Zoom in and out with the mouse wheel
|
||||||
|
e.state.zoom *= math.Pow(1.2, y)
|
||||||
|
}
|
11
go.sum
11
go.sum
@@ -2,8 +2,6 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7
|
|||||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0=
|
|
||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72 h1:b+9H1GAsx5RsjvDFLoS5zkNBzIQMuVKUYQDmxU3N5XE=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72 h1:b+9H1GAsx5RsjvDFLoS5zkNBzIQMuVKUYQDmxU3N5XE=
|
||||||
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-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 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I=
|
||||||
@@ -12,9 +10,6 @@ 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/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/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/bitmapfont v1.2.0/go.mod h1:h9QrPk6Ktb2neObTlAbma6Ini1xgMjbJ3w7ysmD7IOU=
|
||||||
github.com/hajimehoshi/ebiten v1.10.2 h1:PiJBY4Q4udip675T+Zqvb3NKMp1eyLWBelp660ZMrkQ=
|
|
||||||
github.com/hajimehoshi/ebiten v1.10.2/go.mod h1:i9dIEUf5/MuPtbK1/wHR0PB7ZtqhjOxxg+U1xfxapcY=
|
|
||||||
github.com/hajimehoshi/ebiten v1.10.5 h1:hVb3GJP4IDqOETifRmPg4xmURRgbIJoB9gQk+Jqe8Uk=
|
|
||||||
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 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-alpha.2.0.20200101150127-38815ba801a5/go.mod h1:0SLvfr8iI2NxzpNB/olBM+dLN9Ur5a9szG13wOgQ0nQ=
|
||||||
github.com/hajimehoshi/go-mp3 v0.2.1 h1:DH4ns3cPv39n3cs8MPcAlWqPeAwLCK8iNgqvg0QBWI8=
|
github.com/hajimehoshi/go-mp3 v0.2.1 h1:DH4ns3cPv39n3cs8MPcAlWqPeAwLCK8iNgqvg0QBWI8=
|
||||||
@@ -39,10 +34,6 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
|||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
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 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU=
|
||||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
|
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
|
||||||
golang.org/x/exp v0.0.0-20200228211341-fcea875c7e85 h1:jqhIzSw5SQNkbu5hOGpgMHhkfXxrbsLJdkIRcX19gCY=
|
|
||||||
golang.org/x/exp v0.0.0-20200228211341-fcea875c7e85/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
|
|
||||||
golang.org/x/exp v0.0.0-20200319221330-857350248e3d h1:1kJNg12kVM6Xid7xoFkhq/YJVU4NMTv5b3hJCfQnwjc=
|
|
||||||
golang.org/x/exp v0.0.0-20200319221330-857350248e3d/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
|
|
||||||
golang.org/x/exp v0.0.0-20200320212757-167ffe94c325 h1:iPGJw87eUJvke9YLYKX0jIwLHiIrY/kXcFSgOpjav28=
|
golang.org/x/exp v0.0.0-20200320212757-167ffe94c325 h1:iPGJw87eUJvke9YLYKX0jIwLHiIrY/kXcFSgOpjav28=
|
||||||
golang.org/x/exp v0.0.0-20200320212757-167ffe94c325/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
|
golang.org/x/exp v0.0.0-20200320212757-167ffe94c325/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-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
@@ -74,8 +65,6 @@ golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/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 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200317113312-5766fd39f98d h1:62ap6LNOjDU6uGmKXHJbSfciMoV+FeI1sRXx/pLDL44=
|
|
||||||
golang.org/x/sys v0.0.0-20200317113312-5766fd39f98d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200321134203-328b4cd54aae h1:3tcmuaB7wwSZtelmiv479UjUB+vviwABz7a133ZwOKQ=
|
golang.org/x/sys v0.0.0-20200321134203-328b4cd54aae h1:3tcmuaB7wwSZtelmiv479UjUB+vviwABz7a133ZwOKQ=
|
||||||
golang.org/x/sys v0.0.0-20200321134203-328b4cd54aae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200321134203-328b4cd54aae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
@@ -34,6 +34,7 @@ type AssetStore struct {
|
|||||||
entries entryMap
|
entries entryMap
|
||||||
|
|
||||||
// These members are used to store things we've already loaded
|
// These members are used to store things we've already loaded
|
||||||
|
fonts map[string]*Font
|
||||||
maps map[string]*Map
|
maps map[string]*Map
|
||||||
menus map[string]*Menu
|
menus map[string]*Menu
|
||||||
objs map[string]*Object
|
objs map[string]*Object
|
||||||
@@ -84,6 +85,7 @@ func (a *AssetStore) Refresh() error {
|
|||||||
|
|
||||||
// Refresh
|
// Refresh
|
||||||
a.entries = newEntryMap
|
a.entries = newEntryMap
|
||||||
|
a.fonts = make(map[string]*Font)
|
||||||
a.maps = make(map[string]*Map)
|
a.maps = make(map[string]*Map)
|
||||||
a.menus = make(map[string]*Menu)
|
a.menus = make(map[string]*Menu)
|
||||||
a.objs = make(map[string]*Object)
|
a.objs = make(map[string]*Object)
|
||||||
|
116
internal/assetstore/font.go
Normal file
116
internal/assetstore/font.go
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
package assetstore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten"
|
||||||
|
|
||||||
|
"code.ur.gs/lupine/ordoor/internal/fonts"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Font struct {
|
||||||
|
Name string
|
||||||
|
mapping map[rune]*Sprite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AssetStore) Font(name string) (*Font, error) {
|
||||||
|
name = canonical(name)
|
||||||
|
|
||||||
|
// FIXME: these fonts don't exist. For now, point at one that does.
|
||||||
|
switch name {
|
||||||
|
case "imfnt13", "imfnt14":
|
||||||
|
name = "wh40k_12"
|
||||||
|
}
|
||||||
|
|
||||||
|
if font, ok := a.fonts[name]; ok {
|
||||||
|
return font, nil
|
||||||
|
}
|
||||||
|
log.Printf("Loading font %v", name)
|
||||||
|
|
||||||
|
filename, err := a.lookup(name, "fnt", "Fonts")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, err := fonts.LoadFont(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
objFile, err := a.lookup(raw.ObjectFile, "", "Fonts")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
obj, err := a.ObjectByPath(objFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
out := &Font{
|
||||||
|
Name: name,
|
||||||
|
mapping: make(map[rune]*Sprite, len(raw.Mapping)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for r, offset := range raw.Mapping {
|
||||||
|
spr, err := obj.Sprite(offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
out.mapping[r] = spr
|
||||||
|
}
|
||||||
|
|
||||||
|
a.fonts[name] = out
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: this violates the ebiten rules for fast drawing. We may need to do the
|
||||||
|
// draw ourselves, with image.Paletted for each glyph to a single ebiten.Image
|
||||||
|
//
|
||||||
|
// FIXME: it'd be great if we didn't have to implement this all by ourselves;
|
||||||
|
// golang.org/x/image/font and github.com/hajimehoshi/ebiten/text are *almost*
|
||||||
|
// sufficient, but don't seem to like it when the glyphs are literal colours
|
||||||
|
// instead of a mask.
|
||||||
|
//
|
||||||
|
// TODO: draw text in a bounding box, multiple lines, etc
|
||||||
|
func (f *Font) DrawLine(text string) (*ebiten.Image, error) {
|
||||||
|
sprites := make([]*Sprite, 0, len(text))
|
||||||
|
width := 0
|
||||||
|
height := 0
|
||||||
|
|
||||||
|
for _, r := range text {
|
||||||
|
spr, ok := f.mapping[r]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("Font %v does not specify rune %v", f.Name, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
width += spr.Rect.Dx()
|
||||||
|
if y := spr.Rect.Dy(); y > height {
|
||||||
|
height = y
|
||||||
|
}
|
||||||
|
|
||||||
|
sprites = append(sprites, spr)
|
||||||
|
}
|
||||||
|
|
||||||
|
img, err := ebiten.NewImage(width, height, ebiten.FilterDefault)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
xOff := 0
|
||||||
|
for _, spr := range sprites {
|
||||||
|
op := &ebiten.DrawImageOptions{}
|
||||||
|
op.GeoM.Translate(float64(xOff), 0)
|
||||||
|
|
||||||
|
xOff += spr.Rect.Dx()
|
||||||
|
|
||||||
|
if err := img.DrawImage(spr.Image, op); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return img, nil
|
||||||
|
}
|
@@ -8,8 +8,9 @@ import (
|
|||||||
|
|
||||||
type Menu struct {
|
type Menu struct {
|
||||||
assets *AssetStore
|
assets *AssetStore
|
||||||
obj *Object // TODO: handle multiple objects in the menu
|
fonts []*Font // TODO: place the fonts directly into the relevant records
|
||||||
raw *menus.Menu
|
obj *Object // TODO: handle multiple objects in the menu
|
||||||
|
raw *menus.Menu // TODO: remove raw
|
||||||
|
|
||||||
Name string
|
Name string
|
||||||
}
|
}
|
||||||
@@ -19,6 +20,11 @@ func (m *Menu) Records() []*menus.Record {
|
|||||||
return m.raw.Records
|
return m.raw.Records
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: don't expose this
|
||||||
|
func (m *Menu) Font(idx int) *Font {
|
||||||
|
return m.fonts[idx]
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Menu) Images(start, count int) ([]*ebiten.Image, error) {
|
func (m *Menu) Images(start, count int) ([]*ebiten.Image, error) {
|
||||||
out := make([]*ebiten.Image, count)
|
out := make([]*ebiten.Image, count)
|
||||||
|
|
||||||
@@ -70,6 +76,16 @@ func (a *AssetStore) Menu(name string) (*Menu, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var fonts []*Font
|
||||||
|
for _, fontName := range raw.FontNames {
|
||||||
|
fnt, err := a.Font(fontName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fonts = append(fonts, fnt)
|
||||||
|
}
|
||||||
|
|
||||||
i18n, err := a.i18n()
|
i18n, err := a.i18n()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -84,6 +100,7 @@ func (a *AssetStore) Menu(name string) (*Menu, error) {
|
|||||||
|
|
||||||
menu := &Menu{
|
menu := &Menu{
|
||||||
assets: a,
|
assets: a,
|
||||||
|
fonts: fonts,
|
||||||
obj: obj,
|
obj: obj,
|
||||||
raw: raw,
|
raw: raw,
|
||||||
Name: name,
|
Name: name,
|
||||||
|
@@ -10,17 +10,28 @@ import (
|
|||||||
"code.ur.gs/lupine/ordoor/internal/util/asciiscan"
|
"code.ur.gs/lupine/ordoor/internal/util/asciiscan"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Range struct {
|
||||||
|
Min Point
|
||||||
|
Max Point
|
||||||
|
}
|
||||||
|
|
||||||
|
type Point struct {
|
||||||
|
Rune rune
|
||||||
|
Sprite int
|
||||||
|
}
|
||||||
|
|
||||||
type Font struct {
|
type Font struct {
|
||||||
Name string
|
Name string
|
||||||
// Contains the sprite data for the font. FIXME: load this?
|
// Contains the sprite data for the font
|
||||||
ObjectFile string
|
ObjectFile string
|
||||||
|
|
||||||
// Maps ASCII bytes to a sprite offset in the ObjectFile
|
// Maps ASCII bytes to a sprite offset in the ObjectFile
|
||||||
mapping map[int]int
|
Ranges []Range
|
||||||
|
Mapping map[rune]int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Font) Entries() int {
|
func (f *Font) Entries() int {
|
||||||
return len(f.mapping)
|
return len(f.Mapping)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the offsets required to display a given string, returning an error if
|
// Returns the offsets required to display a given string, returning an error if
|
||||||
@@ -30,7 +41,7 @@ func (f *Font) Indices(s string) ([]int, error) {
|
|||||||
|
|
||||||
for i, b := range []byte(s) {
|
for i, b := range []byte(s) {
|
||||||
|
|
||||||
offset, ok := f.mapping[int(b)]
|
offset, ok := f.Mapping[rune(b)]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("Unknown codepoint %v at offset %v in string %s", b, i, s)
|
return nil, fmt.Errorf("Unknown codepoint %v at offset %v in string %s", b, i, s)
|
||||||
}
|
}
|
||||||
@@ -58,7 +69,7 @@ func LoadFont(filename string) (*Font, error) {
|
|||||||
out := &Font{
|
out := &Font{
|
||||||
Name: filepath.Base(filename),
|
Name: filepath.Base(filename),
|
||||||
ObjectFile: objFile,
|
ObjectFile: objFile,
|
||||||
mapping: make(map[int]int),
|
Mapping: make(map[rune]int),
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -82,15 +93,32 @@ func LoadFont(filename string) (*Font, error) {
|
|||||||
cpEnd, _ := strconv.Atoi(fields[2])
|
cpEnd, _ := strconv.Atoi(fields[2])
|
||||||
idxStart, _ := strconv.Atoi(fields[3])
|
idxStart, _ := strconv.Atoi(fields[3])
|
||||||
idxEnd, _ := strconv.Atoi(fields[4])
|
idxEnd, _ := strconv.Atoi(fields[4])
|
||||||
size := idxEnd - idxStart
|
cpSize := cpEnd - cpStart
|
||||||
|
idxSize := idxEnd - idxStart
|
||||||
|
|
||||||
// FIXME: I'd love this to be an error but several .fnt files do it
|
// FIXME: I'd love this to be an error but several .fnt files do it
|
||||||
if cpEnd-cpStart != size {
|
// Take the smallest range
|
||||||
fmt.Printf("WARNING: %v has mismatched codepoints and indices: %q\n", filename, str)
|
if cpSize != idxSize {
|
||||||
|
fmt.Printf("WARNING: %v has mismatched codepoints (sz=%v) and indices (sz=%v): %q\n", filename, cpSize, idxSize, str)
|
||||||
|
if cpSize < idxSize {
|
||||||
|
idxEnd = idxStart + cpSize
|
||||||
|
idxSize = cpSize
|
||||||
|
} else {
|
||||||
|
cpEnd = cpStart + idxSize
|
||||||
|
cpSize = idxSize
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for offset := 0; offset < size; offset++ {
|
r := Range{
|
||||||
out.mapping[cpStart+offset] = idxStart + offset
|
Min: Point{Rune: rune(cpStart), Sprite: idxStart},
|
||||||
|
Max: Point{Rune: rune(cpEnd), Sprite: idxEnd},
|
||||||
|
}
|
||||||
|
|
||||||
|
out.Ranges = append(out.Ranges, r)
|
||||||
|
|
||||||
|
for offset := 0; offset <= cpSize; offset++ {
|
||||||
|
out.Mapping[rune(cpStart+offset)] = idxStart + offset
|
||||||
}
|
}
|
||||||
case "v": // A single codepoint, 4 fields
|
case "v": // A single codepoint, 4 fields
|
||||||
if len(fields) < 3 {
|
if len(fields) < 3 {
|
||||||
@@ -99,8 +127,11 @@ func LoadFont(filename string) (*Font, error) {
|
|||||||
|
|
||||||
cp, _ := strconv.Atoi(fields[1])
|
cp, _ := strconv.Atoi(fields[1])
|
||||||
idx, _ := strconv.Atoi(fields[2])
|
idx, _ := strconv.Atoi(fields[2])
|
||||||
|
pt := Point{Rune: rune(cp), Sprite: idx}
|
||||||
|
|
||||||
out.mapping[cp] = idx
|
out.Ranges = append(out.Ranges, Range{Min: pt, Max: pt})
|
||||||
|
|
||||||
|
out.Mapping[rune(cp)] = idx
|
||||||
default:
|
default:
|
||||||
return nil, parseErr
|
return nil, parseErr
|
||||||
}
|
}
|
||||||
|
@@ -2,11 +2,14 @@ package menus
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image/color"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"code.ur.gs/lupine/ordoor/internal/data"
|
||||||
"code.ur.gs/lupine/ordoor/internal/util/asciiscan"
|
"code.ur.gs/lupine/ordoor/internal/util/asciiscan"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -93,6 +96,9 @@ type Menu struct {
|
|||||||
ObjectFiles []string
|
ObjectFiles []string
|
||||||
FontNames []string
|
FontNames []string
|
||||||
|
|
||||||
|
BackgroundColor color.Color
|
||||||
|
HypertextColor color.Color
|
||||||
|
|
||||||
// FIXME: turn these into first-class data
|
// FIXME: turn these into first-class data
|
||||||
Properties map[string]string
|
Properties map[string]string
|
||||||
|
|
||||||
@@ -146,7 +152,23 @@ func LoadMenu(filename string) (*Menu, error) {
|
|||||||
out.ObjectFiles = append(out.ObjectFiles, str)
|
out.ObjectFiles = append(out.ObjectFiles, str)
|
||||||
case 1: // List of properties
|
case 1: // List of properties
|
||||||
k, v := asciiscan.ConsumeProperty(str)
|
k, v := asciiscan.ConsumeProperty(str)
|
||||||
out.Properties[k] = v
|
vInt, err := strconv.Atoi(v) // FIXME:
|
||||||
|
switch k {
|
||||||
|
case "BACKGROUND COLOR 0..255..-1 trans":
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
out.BackgroundColor = data.ColorPalette[vInt]
|
||||||
|
case "HYPERTEXT COLOR 0..255":
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
out.HypertextColor = data.ColorPalette[vInt]
|
||||||
|
default:
|
||||||
|
out.Properties[k] = v
|
||||||
|
}
|
||||||
case 2: // list of fonts
|
case 2: // list of fonts
|
||||||
// FIXME: do we need to do something cleverer here?
|
// FIXME: do we need to do something cleverer here?
|
||||||
if str == "NULL" {
|
if str == "NULL" {
|
||||||
@@ -170,6 +192,8 @@ func LoadMenu(filename string) (*Menu, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("Menu properties: %#+v", out.Properties)
|
||||||
|
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -5,7 +5,6 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/hajimehoshi/ebiten"
|
"github.com/hajimehoshi/ebiten"
|
||||||
"github.com/hajimehoshi/ebiten/ebitenutil"
|
|
||||||
|
|
||||||
"code.ur.gs/lupine/ordoor/internal/menus"
|
"code.ur.gs/lupine/ordoor/internal/menus"
|
||||||
)
|
)
|
||||||
@@ -28,7 +27,8 @@ type noninteractive struct {
|
|||||||
rect image.Rectangle
|
rect image.Rectangle
|
||||||
|
|
||||||
// Some non-interactives, e.g., overlays, are an image + text to be shown
|
// Some non-interactives, e.g., overlays, are an image + text to be shown
|
||||||
textImg *ebiten.Image
|
textImg *ebiten.Image
|
||||||
|
textOffset image.Point
|
||||||
|
|
||||||
clickImpl // Alright, alright, it turns out the bridge mission briefing is clickable
|
clickImpl // Alright, alright, it turns out the bridge mission briefing is clickable
|
||||||
hoverImpl
|
hoverImpl
|
||||||
@@ -102,14 +102,27 @@ func registerOverlay(d *Driver, r *menus.Record) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if r.Text != "" {
|
if r.Text != "" {
|
||||||
// FIXME: we should be rendering the text much more nicely than this
|
// FIXME: is this always right? Seems to make sense for Main.mnu
|
||||||
textImg, err := ebiten.NewImage(sprite.Rect.Dx(), sprite.Rect.Dy(), ebiten.FilterDefault)
|
fnt := d.menu.Font(r.FontType/10 - 1)
|
||||||
|
|
||||||
|
textImg, err := fnt.DrawLine(r.Text)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ebitenutil.DebugPrint(textImg, r.Text)
|
|
||||||
ni.textImg = textImg
|
ni.textImg = textImg
|
||||||
|
|
||||||
|
// Centre the image
|
||||||
|
xSlack := ni.rect.Dx() - textImg.Bounds().Dx()
|
||||||
|
if xSlack > 0 {
|
||||||
|
ni.textOffset.X = xSlack / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
ySlack := ni.rect.Dy() - textImg.Bounds().Dy()
|
||||||
|
if ySlack > 0 {
|
||||||
|
ni.textOffset.Y = ySlack / 2
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
log.Printf("Overlay without text detected: %#+v", r)
|
log.Printf("Overlay without text detected: %#+v", r)
|
||||||
}
|
}
|
||||||
@@ -189,12 +202,16 @@ func (n *noninteractive) regions(tick int) []region {
|
|||||||
out := oneRegion(n.bounds().Min, n.frames.image(tick))
|
out := oneRegion(n.bounds().Min, n.frames.image(tick))
|
||||||
|
|
||||||
if n.textImg != nil {
|
if n.textImg != nil {
|
||||||
out = append(out, oneRegion(n.bounds().Min, n.textImg)...)
|
out = append(out, oneRegion(n.textPos(), n.textImg)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *noninteractive) textPos() image.Point {
|
||||||
|
return image.Pt(n.rect.Min.X+n.textOffset.X, n.rect.Min.Y+n.textOffset.Y)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *animationHover) regions(tick int) []region {
|
func (a *animationHover) regions(tick int) []region {
|
||||||
if a.opening || a.closing {
|
if a.opening || a.closing {
|
||||||
var anim animation
|
var anim animation
|
||||||
|
Reference in New Issue
Block a user