2021-09-05 20:54:43 +10:00
|
|
|
package engine
|
|
|
|
|
|
|
|
import (
|
2021-09-05 20:58:55 +10:00
|
|
|
"encoding/gob"
|
2021-09-07 17:10:26 +10:00
|
|
|
"fmt"
|
2021-09-05 20:54:43 +10:00
|
|
|
"image"
|
|
|
|
|
2021-09-08 20:08:57 +10:00
|
|
|
"drjosh.dev/gurgle/geom"
|
2021-09-05 20:54:43 +10:00
|
|
|
"github.com/hajimehoshi/ebiten/v2"
|
|
|
|
)
|
|
|
|
|
2021-09-05 20:58:55 +10:00
|
|
|
var (
|
|
|
|
_ interface {
|
2021-09-06 10:45:51 +10:00
|
|
|
Identifier
|
|
|
|
Collider
|
|
|
|
Disabler
|
|
|
|
Hider
|
2021-09-05 20:58:55 +10:00
|
|
|
Prepper
|
2021-09-21 14:42:18 +10:00
|
|
|
Scanner
|
2021-09-05 20:58:55 +10:00
|
|
|
Transformer
|
|
|
|
} = &PrismMap{}
|
|
|
|
|
|
|
|
_ interface {
|
2021-09-10 17:18:20 +10:00
|
|
|
BoundingBoxer
|
2021-09-05 20:58:55 +10:00
|
|
|
Drawer
|
|
|
|
Transformer
|
|
|
|
} = &Prism{}
|
|
|
|
)
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
gob.Register(&PrismMap{})
|
|
|
|
gob.Register(&Prism{})
|
|
|
|
}
|
|
|
|
|
2021-09-20 15:52:44 +10:00
|
|
|
// PrismMap is a generalised 3D tilemap/wallmap/voxelmap etc.
|
2021-09-05 20:54:43 +10:00
|
|
|
type PrismMap struct {
|
2021-09-06 10:45:51 +10:00
|
|
|
ID
|
2021-09-13 16:50:35 +10:00
|
|
|
Disables
|
|
|
|
Hides
|
2021-09-09 17:16:00 +10:00
|
|
|
Ersatz bool
|
|
|
|
Map map[geom.Int3]*Prism // pos -> prism
|
|
|
|
DrawOffset image.Point // offset applies to whole map
|
|
|
|
PosToWorld geom.IntMatrix3x4 // p.pos -> world voxelspace
|
|
|
|
PrismSize geom.Int3 // in world voxelspace units
|
|
|
|
PrismTop []image.Point // polygon vertices anticlockwise, Y means Z
|
|
|
|
Sheet Sheet
|
2021-09-07 14:46:05 +10:00
|
|
|
|
2021-09-07 17:10:26 +10:00
|
|
|
game *Game
|
2021-09-08 20:08:57 +10:00
|
|
|
pwinverse geom.RatMatrix3
|
2021-09-11 17:39:52 +10:00
|
|
|
topext [4]image.Point
|
2021-09-05 20:54:43 +10:00
|
|
|
}
|
|
|
|
|
2021-09-10 11:30:58 +10:00
|
|
|
// CollidesWith checks if the box collides with any prism.
|
2021-09-08 20:08:57 +10:00
|
|
|
func (m *PrismMap) CollidesWith(b geom.Box) bool {
|
2021-09-07 17:10:26 +10:00
|
|
|
if m.Ersatz {
|
|
|
|
return false
|
|
|
|
}
|
2021-09-08 17:08:21 +10:00
|
|
|
|
2021-09-07 17:10:26 +10:00
|
|
|
// To find the prisms need to test, we need to invert PosToWorld.
|
|
|
|
|
|
|
|
// Step 1: subtract whatever the translation component of PosToWorld is,
|
|
|
|
// reducing the rest of the problem to the 3x3 submatrix.
|
2021-09-07 21:55:05 +10:00
|
|
|
rb := b.Sub(m.PosToWorld.Translation())
|
2021-09-07 17:10:26 +10:00
|
|
|
// Step 2: invert the rest of the fucking matrix.
|
|
|
|
// (Spoilers: I did this already in Prepare)
|
2021-09-07 21:55:05 +10:00
|
|
|
rb.Min = m.pwinverse.IntApply(rb.Min)
|
|
|
|
rb.Max = m.pwinverse.IntApply(rb.Max) //.Sub(Int3{1, 1, 1}))
|
2021-09-07 20:45:04 +10:00
|
|
|
|
2021-09-07 21:55:05 +10:00
|
|
|
rb = rb.Canon() // inverse might flip the corners around...
|
2021-09-07 20:45:04 +10:00
|
|
|
|
2021-09-07 21:55:05 +10:00
|
|
|
// Check neighboring prisms too because there's a fencepost somewhere here
|
2021-09-08 20:09:52 +10:00
|
|
|
rb.Min = rb.Min.Sub(geom.Int3{X: 1, Y: 1, Z: 1})
|
|
|
|
rb.Max = rb.Max.Add(geom.Int3{X: 1, Y: 1, Z: 1})
|
2021-09-07 21:55:05 +10:00
|
|
|
|
2021-09-08 20:08:57 +10:00
|
|
|
var pp geom.Int3
|
2021-09-07 21:55:05 +10:00
|
|
|
for pp.Z = rb.Min.Z; pp.Z <= rb.Max.Z; pp.Z++ {
|
|
|
|
for pp.Y = rb.Min.Y; pp.Y <= rb.Max.Y; pp.Y++ {
|
|
|
|
for pp.X = rb.Min.X; pp.X <= rb.Max.X; pp.X++ {
|
2021-09-08 10:00:37 +10:00
|
|
|
// Is there a prism here?
|
2021-09-10 11:30:58 +10:00
|
|
|
prism, found := m.Map[pp]
|
|
|
|
if !found {
|
2021-09-07 21:55:05 +10:00
|
|
|
continue
|
|
|
|
}
|
2021-09-10 11:30:58 +10:00
|
|
|
// Do a cheaper test first against the bounding box.
|
|
|
|
if !b.Overlaps(prism.BoundingBox()) {
|
2021-09-08 10:00:37 +10:00
|
|
|
continue
|
2021-09-07 17:10:26 +10:00
|
|
|
}
|
2021-09-10 11:30:58 +10:00
|
|
|
// Exact test that takes into account the prism shape.
|
|
|
|
r := b.XZ().Sub(prism.pos.XZ())
|
2021-09-08 20:08:57 +10:00
|
|
|
if geom.PolygonRectOverlap(m.PrismTop, r) {
|
2021-09-08 17:08:21 +10:00
|
|
|
return true
|
|
|
|
}
|
2021-09-07 17:10:26 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-09-06 10:45:51 +10:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2021-09-10 11:30:58 +10:00
|
|
|
// Prepare computes an inverse of PosToWorld and prepares all the prisms.
|
2021-09-07 14:46:05 +10:00
|
|
|
func (m *PrismMap) Prepare(g *Game) error {
|
|
|
|
m.game = g
|
2021-09-07 17:10:26 +10:00
|
|
|
pwi, err := m.PosToWorld.ToRatMatrix3().Inverse()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("inverting PosToWorld: %w", err)
|
|
|
|
}
|
|
|
|
m.pwinverse = pwi
|
2021-09-05 20:54:43 +10:00
|
|
|
for v, p := range m.Map {
|
2021-09-10 11:30:58 +10:00
|
|
|
p.pos = m.PosToWorld.Apply(v)
|
2021-09-07 14:46:05 +10:00
|
|
|
p.m = m
|
2021-09-05 20:54:43 +10:00
|
|
|
}
|
2021-09-11 17:39:52 +10:00
|
|
|
m.topext = geom.PolygonExtrema(m.PrismTop)
|
2021-09-05 20:54:43 +10:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-09-22 15:55:38 +10:00
|
|
|
// Scan visits &m.Sheet and all Prisms.
|
2021-09-22 15:48:02 +10:00
|
|
|
func (m *PrismMap) Scan(visit func(interface{}) error) error {
|
|
|
|
if err := visit(&m.Sheet); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
for _, prism := range m.Map {
|
|
|
|
if err := visit(prism); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
2021-09-07 14:46:05 +10:00
|
|
|
}
|
|
|
|
|
2021-09-10 11:30:58 +10:00
|
|
|
// Transform retrurns a translation by the draw offset.
|
2021-09-07 14:00:50 +10:00
|
|
|
func (m *PrismMap) Transform() (opts ebiten.DrawImageOptions) {
|
2021-09-08 20:08:57 +10:00
|
|
|
opts.GeoM.Translate(geom.CFloat(m.DrawOffset))
|
2021-09-07 14:00:50 +10:00
|
|
|
return opts
|
2021-09-05 20:54:43 +10:00
|
|
|
}
|
|
|
|
|
2021-09-10 11:30:58 +10:00
|
|
|
// Prism represents a single prism in a PrismMap.
|
2021-09-05 20:54:43 +10:00
|
|
|
type Prism struct {
|
|
|
|
Cell int
|
|
|
|
|
2021-09-10 11:30:58 +10:00
|
|
|
pos geom.Int3 // world coordinates
|
2021-09-07 14:46:05 +10:00
|
|
|
m *PrismMap
|
2021-09-05 20:54:43 +10:00
|
|
|
}
|
|
|
|
|
2021-09-10 11:30:58 +10:00
|
|
|
// BoundingBox returns a bounding box for the prism.
|
|
|
|
func (p *Prism) BoundingBox() geom.Box {
|
|
|
|
return geom.Box{Min: p.pos, Max: p.pos.Add(p.m.PrismSize)}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Draw draws the prism.
|
2021-09-05 20:58:55 +10:00
|
|
|
func (p *Prism) Draw(screen *ebiten.Image, opts *ebiten.DrawImageOptions) {
|
2021-09-07 14:46:05 +10:00
|
|
|
screen.DrawImage(p.m.Sheet.SubImage(p.Cell), opts)
|
2021-09-05 20:54:43 +10:00
|
|
|
}
|
|
|
|
|
2021-09-10 17:18:20 +10:00
|
|
|
// DrawAfter reports if the prism should be drawn after x.
|
|
|
|
func (p *Prism) DrawAfter(x Drawer) bool {
|
|
|
|
pb := p.BoundingBox()
|
2021-09-11 17:53:31 +10:00
|
|
|
switch x := x.(type) {
|
2021-09-11 13:00:23 +10:00
|
|
|
case *Prism:
|
2021-09-11 17:39:52 +10:00
|
|
|
// Fast path for other prisms
|
2021-09-11 17:53:31 +10:00
|
|
|
if p.pos.Z == x.pos.Z {
|
2021-09-17 16:52:45 +10:00
|
|
|
// TODO: account for projection
|
2021-09-11 17:53:31 +10:00
|
|
|
return p.pos.Y < x.pos.Y
|
2021-09-11 13:00:23 +10:00
|
|
|
}
|
2021-09-11 17:53:31 +10:00
|
|
|
return p.pos.Z > x.pos.Z
|
2021-09-15 15:10:05 +10:00
|
|
|
|
2021-09-10 17:18:20 +10:00
|
|
|
case BoundingBoxer:
|
2021-09-11 17:53:31 +10:00
|
|
|
xb := x.BoundingBox()
|
2021-09-11 17:47:31 +10:00
|
|
|
// The prism special
|
|
|
|
split := p.m.topext[geom.North].X
|
|
|
|
threshold := p.m.topext[geom.East].Y
|
|
|
|
if xb.Min.X > split {
|
|
|
|
threshold = p.m.topext[geom.West].Y
|
|
|
|
}
|
|
|
|
if pb.Min.Z+threshold <= xb.Min.Z { // x is in front of the front half of p
|
2021-09-10 17:18:20 +10:00
|
|
|
return false
|
|
|
|
}
|
2021-09-11 17:47:31 +10:00
|
|
|
if pb.Min.Z+threshold >= xb.Max.Z { // x is behind the front half of p
|
2021-09-10 17:18:20 +10:00
|
|
|
return true
|
|
|
|
}
|
2021-09-11 13:00:23 +10:00
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// DrawBefore reports if the prism should be drawn before x.
|
|
|
|
func (p *Prism) DrawBefore(x Drawer) bool {
|
|
|
|
pb := p.BoundingBox()
|
2021-09-11 17:53:31 +10:00
|
|
|
switch x := x.(type) {
|
2021-09-11 13:00:23 +10:00
|
|
|
case *Prism:
|
2021-09-11 17:39:52 +10:00
|
|
|
// Fast path for other prisms
|
2021-09-11 17:53:31 +10:00
|
|
|
if p.pos.Z == x.pos.Z {
|
2021-09-17 16:52:45 +10:00
|
|
|
// TODO: account for projection
|
2021-09-11 17:53:31 +10:00
|
|
|
return p.pos.Y > x.pos.Y
|
2021-09-11 13:00:23 +10:00
|
|
|
}
|
2021-09-11 17:53:31 +10:00
|
|
|
return p.pos.Z < x.pos.Z
|
2021-09-15 15:10:05 +10:00
|
|
|
|
2021-09-11 13:00:23 +10:00
|
|
|
case BoundingBoxer:
|
2021-09-11 17:53:31 +10:00
|
|
|
xb := x.BoundingBox()
|
2021-09-11 17:47:31 +10:00
|
|
|
// The prism special
|
|
|
|
split := p.m.topext[geom.North].X
|
|
|
|
threshold := p.m.topext[geom.East].Y
|
|
|
|
if xb.Min.X > split {
|
|
|
|
threshold = p.m.topext[geom.West].Y
|
|
|
|
}
|
|
|
|
if pb.Min.Z+threshold >= xb.Max.Z { // x is behind the front half of p
|
2021-09-11 13:00:23 +10:00
|
|
|
return false
|
|
|
|
}
|
2021-09-11 17:47:31 +10:00
|
|
|
if pb.Min.Z+threshold <= xb.Min.Z { // x is in front of the front half of p
|
2021-09-11 13:00:23 +10:00
|
|
|
return true
|
|
|
|
}
|
2021-09-10 17:18:20 +10:00
|
|
|
}
|
|
|
|
return false
|
2021-09-05 20:54:43 +10:00
|
|
|
}
|
|
|
|
|
2021-09-17 11:13:39 +10:00
|
|
|
func (p *Prism) String() string {
|
|
|
|
return fmt.Sprintf("Prism(%d)@%v", p.Cell, p.pos)
|
|
|
|
}
|
|
|
|
|
2021-09-10 11:30:58 +10:00
|
|
|
// Transform returns a translation by the projected position.
|
2021-09-07 14:00:50 +10:00
|
|
|
func (p *Prism) Transform() (opts ebiten.DrawImageOptions) {
|
2021-09-08 20:08:57 +10:00
|
|
|
opts.GeoM.Translate(geom.CFloat(
|
2021-09-19 15:51:35 +10:00
|
|
|
geom.Project(p.m.game.Projection, p.pos),
|
2021-09-05 20:54:43 +10:00
|
|
|
))
|
2021-09-07 14:00:50 +10:00
|
|
|
return opts
|
2021-09-05 20:54:43 +10:00
|
|
|
}
|