Added bitmap font.
Added monsters. Added animations. Added rendering of geometries.
This commit is contained in:
parent
418eea0195
commit
13b6a50a45
117
animation.go
Normal file
117
animation.go
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
package tins2021
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"opslag.de/schobers/geom"
|
||||||
|
"opslag.de/schobers/zntg/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AnimatedTexture struct {
|
||||||
|
Texture ui.Texture
|
||||||
|
Frames []geom.RectangleF32
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAnimatedTexture(texture ui.Texture, n int) AnimatedTexture {
|
||||||
|
frames := make([]geom.RectangleF32, 0, n)
|
||||||
|
height := float32(texture.Height())
|
||||||
|
width := float32(texture.Width())
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
left := width * float32(i) / float32(n)
|
||||||
|
right := width * float32(i+1) / float32(n)
|
||||||
|
frames = append(frames, geom.RectF32(left, 0, right, height))
|
||||||
|
}
|
||||||
|
return AnimatedTexture{Texture: texture, Frames: frames}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSquareAnimatedTexture(texture ui.Texture) AnimatedTexture {
|
||||||
|
var frames []geom.RectangleF32
|
||||||
|
height := float32(texture.Height())
|
||||||
|
width := float32(texture.Width())
|
||||||
|
for left := float32(0); left < width; left += height {
|
||||||
|
frames = append(frames, geom.RectF32(left, 0, left+height, height))
|
||||||
|
}
|
||||||
|
return AnimatedTexture{Texture: texture, Frames: frames}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t AnimatedTexture) Scale(scale float32) AnimatedTexture {
|
||||||
|
frames := make([]geom.RectangleF32, 0, len(t.Frames))
|
||||||
|
for _, frame := range t.Frames {
|
||||||
|
frames = append(frames, geom.RectangleF32{Min: frame.Min.Mul(scale), Max: frame.Max.Mul(scale)})
|
||||||
|
}
|
||||||
|
return AnimatedTexture{
|
||||||
|
Texture: t.Texture,
|
||||||
|
Frames: frames,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t AnimatedTexture) Draw(renderer ui.Renderer, pos geom.PointF32, frame int) {
|
||||||
|
renderer.DrawTexturePointOptions(t.Texture, pos, ui.DrawOptions{Source: &t.Frames[frame]})
|
||||||
|
}
|
||||||
|
|
||||||
|
type Animation struct {
|
||||||
|
LastUpdate time.Time
|
||||||
|
Frame int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Animations struct {
|
||||||
|
Values map[geom.Point]*Animation
|
||||||
|
Interval time.Duration
|
||||||
|
Frames int
|
||||||
|
AutoReset bool
|
||||||
|
RandomInit bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAnimations(interval time.Duration, frames int, autoReset, randomInit bool) *Animations {
|
||||||
|
return &Animations{
|
||||||
|
Values: map[geom.Point]*Animation{},
|
||||||
|
Interval: interval,
|
||||||
|
Frames: frames,
|
||||||
|
AutoReset: autoReset,
|
||||||
|
RandomInit: randomInit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Animations) Update() {
|
||||||
|
now := time.Now()
|
||||||
|
update := now.Add(-a.Interval)
|
||||||
|
for _, value := range a.Values {
|
||||||
|
if value.Frame == a.Frames {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
for value.LastUpdate.Before(update) {
|
||||||
|
value.LastUpdate = value.LastUpdate.Add(a.Interval)
|
||||||
|
value.Frame = value.Frame + 1
|
||||||
|
if value.Frame == a.Frames {
|
||||||
|
if a.AutoReset {
|
||||||
|
value.Frame = 0
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Animations) newAnimation() *Animation {
|
||||||
|
if a.RandomInit {
|
||||||
|
return &Animation{
|
||||||
|
LastUpdate: time.Now().Add(time.Duration(-rand.Int63n(int64(a.Interval)))),
|
||||||
|
Frame: rand.Intn(a.Frames),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &Animation{
|
||||||
|
LastUpdate: time.Now(),
|
||||||
|
Frame: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Animations) Frame(pos geom.Point) int {
|
||||||
|
value, ok := a.Values[pos]
|
||||||
|
if !ok {
|
||||||
|
value = a.newAnimation()
|
||||||
|
a.Values[pos] = value
|
||||||
|
}
|
||||||
|
return value.Frame
|
||||||
|
}
|
106
bitmapfont.go
Normal file
106
bitmapfont.go
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
package tins2021
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"opslag.de/schobers/geom"
|
||||||
|
"opslag.de/schobers/zntg/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BitmapFont struct {
|
||||||
|
texture ui.Texture
|
||||||
|
height float32
|
||||||
|
runes map[rune]*geom.RectangleF32
|
||||||
|
}
|
||||||
|
|
||||||
|
var AllCharacters = []rune(`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 ,.?!;:+-=_()[]{}"'@#$%^&*<>\/`)
|
||||||
|
var AlphaCharacters = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")
|
||||||
|
var LowerCaseAlphaCharacters = []rune("abcdefghijklmnopqrstuvwxyz")
|
||||||
|
var NumericCharacters = []rune("0123456789")
|
||||||
|
var SpecialCharacters = []rune(` ,.?!;:+-=_()[]{}"'@#$%^&*<>\/`)
|
||||||
|
var UpperCaseAlphaCharacters = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||||
|
|
||||||
|
func NewBitmapFont(renderer ui.Renderer, font ui.Font, set ...rune) (*BitmapFont, error) {
|
||||||
|
texture, err := renderer.TextTexture(font, color.White, string(set))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
runes := map[rune]*geom.RectangleF32{}
|
||||||
|
height := float32(texture.Height())
|
||||||
|
var left float32
|
||||||
|
for _, r := range set {
|
||||||
|
width := font.WidthOf(string([]rune{r}))
|
||||||
|
right := left + width
|
||||||
|
rect := geom.RectF32(left, 0, right, height)
|
||||||
|
runes[r] = &rect
|
||||||
|
left = right
|
||||||
|
}
|
||||||
|
return &BitmapFont{texture, font.Height(), runes}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *BitmapFont) Destroy() error { return f.texture.Destroy() }
|
||||||
|
func (f *BitmapFont) Height() float32 { return f.height }
|
||||||
|
|
||||||
|
func (f *BitmapFont) Measure(t string) geom.RectangleF32 {
|
||||||
|
if len(t) == 0 {
|
||||||
|
return geom.RectF32(0, 0, 0, 0)
|
||||||
|
}
|
||||||
|
var minY, maxY float32
|
||||||
|
var width float32
|
||||||
|
var first int
|
||||||
|
for i, r := range t {
|
||||||
|
rect := f.runes[r]
|
||||||
|
if rect == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
width = rect.Dx()
|
||||||
|
minY, maxY = rect.Min.Y, rect.Max.Y
|
||||||
|
first = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
for _, r := range t[first+1:] {
|
||||||
|
rect := f.runes[r]
|
||||||
|
if rect == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
width += rect.Dx()
|
||||||
|
if minY > rect.Min.Y {
|
||||||
|
minY = rect.Min.Y
|
||||||
|
}
|
||||||
|
if maxY < rect.Max.Y {
|
||||||
|
maxY = rect.Max.Y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return geom.RectF32(0, minY, width, maxY)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *BitmapFont) Text(renderer ui.Renderer, pos geom.PointF32, color color.Color, text string) {
|
||||||
|
f.text(renderer, pos.X, pos.Y, color, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *BitmapFont) TextAlign(renderer ui.Renderer, pos geom.PointF32, color color.Color, text string, align ui.HorizontalAlignment) {
|
||||||
|
left := pos.X
|
||||||
|
width := f.WidthOf(text)
|
||||||
|
switch align {
|
||||||
|
case ui.AlignCenter:
|
||||||
|
left -= .5 * width
|
||||||
|
case ui.AlignRight:
|
||||||
|
left -= width
|
||||||
|
}
|
||||||
|
f.text(renderer, left, pos.Y, color, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *BitmapFont) text(renderer ui.Renderer, left, top float32, color color.Color, text string) {
|
||||||
|
for _, r := range text {
|
||||||
|
src := f.runes[r]
|
||||||
|
if src == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
renderer.DrawTexturePointOptions(f.texture, geom.PtF32(left, top), ui.DrawOptions{Tint: color, Source: src})
|
||||||
|
left += src.Dx()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *BitmapFont) WidthOf(t string) float32 {
|
||||||
|
return f.Measure(t).Dx()
|
||||||
|
}
|
@ -41,6 +41,7 @@ func (a *app) Init(ctx ui.Context) error {
|
|||||||
if err := a.loadFonts(ctx,
|
if err := a.loadFonts(ctx,
|
||||||
fontDescriptor{"debug", "fonts/FiraMono-Regular.ttf", 12},
|
fontDescriptor{"debug", "fonts/FiraMono-Regular.ttf", 12},
|
||||||
fontDescriptor{"default", "fonts/escheresk.ttf", 32},
|
fontDescriptor{"default", "fonts/escheresk.ttf", 32},
|
||||||
|
fontDescriptor{"small", "fonts/escheresk.ttf", 16},
|
||||||
fontDescriptor{"title", "fonts/escher.ttf", 80},
|
fontDescriptor{"title", "fonts/escher.ttf", 80},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -68,10 +69,10 @@ func (a *app) Handle(ctx ui.Context, e ui.Event) bool {
|
|||||||
case *ui.DisplayMoveEvent:
|
case *ui.DisplayMoveEvent:
|
||||||
location := e.Bounds.Min.ToInt()
|
location := e.Bounds.Min.ToInt()
|
||||||
a.settings.Window.Location = &location
|
a.settings.Window.Location = &location
|
||||||
case *ui.DisplayResizeEvent:
|
// case *ui.DisplayResizeEvent:
|
||||||
a.Arrange(ctx, e.Bounds, geom.ZeroPtF32, nil)
|
// a.Arrange(ctx, e.Bounds, geom.ZeroPtF32, nil)
|
||||||
size := e.Bounds.Size().ToInt()
|
// size := e.Bounds.Size().ToInt()
|
||||||
a.settings.Window.Size = &size
|
// a.settings.Window.Size = &size
|
||||||
case *ui.KeyDownEvent:
|
case *ui.KeyDownEvent:
|
||||||
switch e.Key {
|
switch e.Key {
|
||||||
case ui.KeyEscape:
|
case ui.KeyEscape:
|
||||||
@ -124,7 +125,8 @@ func (s Sprite) Destroy() { s.Texture.Destroy() }
|
|||||||
func newIntroView(ctx ui.Context) *introView {
|
func newIntroView(ctx ui.Context) *introView {
|
||||||
view := &introView{}
|
view := &introView{}
|
||||||
|
|
||||||
level := tins2021.NewRandomLevel()
|
level := tins2021.NewLevel()
|
||||||
|
level.Randomize(100, 10)
|
||||||
|
|
||||||
view.Children = []ui.Control{
|
view.Children = []ui.Control{
|
||||||
label("QBITTER", "title"),
|
label("QBITTER", "title"),
|
||||||
|
@ -1,86 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"opslag.de/schobers/geom"
|
|
||||||
"opslag.de/schobers/tins2021"
|
|
||||||
"opslag.de/schobers/zntg"
|
|
||||||
"opslag.de/schobers/zntg/ui"
|
|
||||||
)
|
|
||||||
|
|
||||||
type levelControl struct {
|
|
||||||
ui.ControlBase
|
|
||||||
|
|
||||||
Scale float32
|
|
||||||
Level *tins2021.Level
|
|
||||||
}
|
|
||||||
|
|
||||||
func newLevelControl(ctx ui.Context, level *tins2021.Level) *levelControl {
|
|
||||||
renderer := &levelControl{Level: level, Scale: .3}
|
|
||||||
ctx.Textures().CreateTextureGo("cube1", tins2021.GenerateCube(tins2021.Orange), true)
|
|
||||||
ctx.Textures().CreateTextureGo("cube1_inversed", tins2021.GenerateHole(tins2021.Orange), true)
|
|
||||||
return renderer
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsModifierPressed(modifiers ui.KeyModifier, pressed ui.KeyModifier) bool {
|
|
||||||
return modifiers&pressed == pressed
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r levelControl) Handle(ctx ui.Context, e ui.Event) bool {
|
|
||||||
switch e := e.(type) {
|
|
||||||
case *ui.KeyDownEvent:
|
|
||||||
switch e.Key {
|
|
||||||
case ui.KeyW:
|
|
||||||
r.Level.MovePlayer(tins2021.DirectionUpLeft)
|
|
||||||
case ui.KeyD:
|
|
||||||
r.Level.MovePlayer(tins2021.DirectionUpRight)
|
|
||||||
case ui.KeyS:
|
|
||||||
r.Level.MovePlayer(tins2021.DirectionDownRight)
|
|
||||||
case ui.KeyA:
|
|
||||||
r.Level.MovePlayer(tins2021.DirectionDownLeft)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r levelControl) Render(ctx ui.Context) {
|
|
||||||
const twelfth = (1. / 6) * geom.Pi
|
|
||||||
renderer := ctx.Renderer()
|
|
||||||
|
|
||||||
size := geom.Floor32(256. * r.Scale)
|
|
||||||
scale := size / 256
|
|
||||||
|
|
||||||
centerTopSquare := geom.PtF32(.5, .5*geom.Sin32(twelfth))
|
|
||||||
delta := geom.PtF32(geom.Cos32(twelfth), .5+centerTopSquare.Y).Mul(size)
|
|
||||||
centerTopSquare = centerTopSquare.Mul(size)
|
|
||||||
|
|
||||||
delta.X = geom.Round32(delta.X)
|
|
||||||
delta.Y = geom.Round32(delta.Y)
|
|
||||||
toScreen := func(p geom.Point) geom.PointF32 {
|
|
||||||
if p.Y%2 == 0 {
|
|
||||||
return p.ToF32().Mul2D(delta.XY()).Add2D(.5*delta.X, 0)
|
|
||||||
}
|
|
||||||
return p.ToF32().Mul2D(delta.XY())
|
|
||||||
}
|
|
||||||
cube := ctx.Textures().ScaledByName("cube1", scale)
|
|
||||||
inversed := ctx.Textures().ScaledByName("cube1_inversed", scale)
|
|
||||||
player := ctx.Textures().ScaledByName("dwarf", scale)
|
|
||||||
|
|
||||||
for pos, tile := range r.Level.Tiles {
|
|
||||||
if tile.Inversed {
|
|
||||||
renderer.DrawTexturePoint(inversed, toScreen(pos))
|
|
||||||
} else {
|
|
||||||
renderer.DrawTexturePoint(cube, toScreen(pos))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
playerPosition := toScreen(r.Level.Player).Sub(geom.Pt(player.Width()/2, player.Height()).ToF32())
|
|
||||||
if r.Level.Tiles[r.Level.Player].Inversed {
|
|
||||||
centerBottomSquare := geom.PtF32(centerTopSquare.X, size-centerTopSquare.Y)
|
|
||||||
renderer.DrawTexturePointOptions(player, playerPosition.Add(centerBottomSquare), ui.DrawOptions{
|
|
||||||
Tint: zntg.MustHexColor(tins2021.Blue),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
renderer.DrawTexturePointOptions(player, playerPosition.Add(centerTopSquare), ui.DrawOptions{
|
|
||||||
Tint: zntg.MustHexColor(tins2021.Lighten(tins2021.Blue, 0.1)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
226
cmd/tins2021/levelcontroller.go
Normal file
226
cmd/tins2021/levelcontroller.go
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
"math/rand"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"opslag.de/schobers/geom"
|
||||||
|
"opslag.de/schobers/tins2021"
|
||||||
|
"opslag.de/schobers/zntg/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type levelController struct {
|
||||||
|
ui.ControlBase
|
||||||
|
|
||||||
|
Scale float32
|
||||||
|
Level *tins2021.Level
|
||||||
|
|
||||||
|
Cubes map[string]cubeTexture
|
||||||
|
Animations map[string]*tins2021.Animations
|
||||||
|
|
||||||
|
MonsterTextureNames map[tins2021.MonsterType]string
|
||||||
|
IdleMonsters *tins2021.Animations
|
||||||
|
MovingMonsters *tins2021.Animations
|
||||||
|
|
||||||
|
SmallFont *tins2021.BitmapFont
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLevelControl(ctx ui.Context, level *tins2021.Level) *levelController {
|
||||||
|
control := &levelController{Level: level, Scale: .6}
|
||||||
|
textures := ctx.Textures()
|
||||||
|
control.Cubes = map[string]cubeTexture{
|
||||||
|
"regular": newCubeTexture(textures, tins2021.Orange),
|
||||||
|
"blocked": newCubeTexture(textures, tins2021.Purple),
|
||||||
|
"colored": newCubeTexture(textures, tins2021.Blue),
|
||||||
|
}
|
||||||
|
newAnimatedTexture(textures, "star", tins2021.CreateStar(5), tins2021.Yellow, tins2021.NewRotateAnimation(defaultAnimationFrames))
|
||||||
|
newAnimatedTexture(textures, "heart", tins2021.CreateHeart(), tins2021.Red, tins2021.NewRotateAnimation(defaultAnimationFrames))
|
||||||
|
|
||||||
|
control.MonsterTextureNames = map[tins2021.MonsterType]string{
|
||||||
|
tins2021.MonsterTypeStraight: "straight-walking-monster",
|
||||||
|
tins2021.MonsterTypeRandom: "random-walking-monster",
|
||||||
|
tins2021.MonsterTypeChaser: "chasing-monster",
|
||||||
|
}
|
||||||
|
newAnimatedTexture(textures, control.MonsterTextureNames[tins2021.MonsterTypeStraight], tins2021.CreateHexagon(), tins2021.Green, tins2021.NewWobbleAnimation(defaultAnimationFrames, 30))
|
||||||
|
newAnimatedTexture(textures, control.MonsterTextureNames[tins2021.MonsterTypeRandom], tins2021.CreateHexagon(), tins2021.Blue, tins2021.NewWobbleAnimation(defaultAnimationFrames, 30))
|
||||||
|
newAnimatedTexture(textures, control.MonsterTextureNames[tins2021.MonsterTypeChaser], tins2021.CreateHexagon(), tins2021.Purple, tins2021.NewWobbleAnimation(defaultAnimationFrames, 30))
|
||||||
|
|
||||||
|
small, err := tins2021.NewBitmapFont(ctx.Renderer(), ctx.Fonts().Font("small"), tins2021.AllCharacters...)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
control.SmallFont = small
|
||||||
|
control.Animations = map[string]*tins2021.Animations{
|
||||||
|
"star": tins2021.NewAnimations(50*time.Millisecond, defaultAnimationFrames, true, true),
|
||||||
|
"heart": tins2021.NewAnimations(80*time.Millisecond, defaultAnimationFrames, true, true),
|
||||||
|
}
|
||||||
|
control.IdleMonsters = tins2021.NewAnimations(500*time.Millisecond, 100, false, false)
|
||||||
|
control.MovingMonsters = tins2021.NewAnimations(50*time.Millisecond, 20, false, false)
|
||||||
|
for monster := range level.Monsters {
|
||||||
|
control.IdleMonsters.Frame(monster)
|
||||||
|
}
|
||||||
|
for _, monster := range control.MonsterTextureNames {
|
||||||
|
control.Animations[monster] = tins2021.NewAnimations(80*time.Millisecond, defaultAnimationFrames, true, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return control
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsModifierPressed(modifiers ui.KeyModifier, pressed ui.KeyModifier) bool {
|
||||||
|
return modifiers&pressed == pressed
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r levelController) Handle(ctx ui.Context, e ui.Event) bool {
|
||||||
|
switch e := e.(type) {
|
||||||
|
case *ui.KeyDownEvent:
|
||||||
|
switch e.Key {
|
||||||
|
case ui.KeyW:
|
||||||
|
r.Level.MovePlayer(tins2021.DirectionUpLeft)
|
||||||
|
case ui.KeyD:
|
||||||
|
r.Level.MovePlayer(tins2021.DirectionUpRight)
|
||||||
|
case ui.KeyS:
|
||||||
|
r.Level.MovePlayer(tins2021.DirectionDownRight)
|
||||||
|
case ui.KeyA:
|
||||||
|
r.Level.MovePlayer(tins2021.DirectionDownLeft)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, animations := range r.Animations {
|
||||||
|
animations.Update()
|
||||||
|
}
|
||||||
|
r.IdleMonsters.Update()
|
||||||
|
r.MovingMonsters.Update()
|
||||||
|
var jumped []geom.Point
|
||||||
|
for pos, animation := range r.MovingMonsters.Values {
|
||||||
|
if animation.Frame < 20 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
target := r.Level.MonsterTargets[pos]
|
||||||
|
r.Level.Monsters[target] = r.Level.Monsters[pos]
|
||||||
|
delete(r.Level.MonsterTargets, pos)
|
||||||
|
delete(r.Level.Monsters, pos)
|
||||||
|
jumped = append(jumped, pos)
|
||||||
|
r.IdleMonsters.Frame(target)
|
||||||
|
}
|
||||||
|
for _, pos := range jumped {
|
||||||
|
delete(r.MovingMonsters.Values, pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
var jumping []geom.Point
|
||||||
|
for pos, animation := range r.IdleMonsters.Values {
|
||||||
|
for animation.Frame > 0 {
|
||||||
|
if rand.Intn(10) != 0 {
|
||||||
|
monster, ok := r.Level.Monsters[pos]
|
||||||
|
if ok && monster != nil {
|
||||||
|
target, ok := monster.FindTarget(r.Level, pos)
|
||||||
|
if ok {
|
||||||
|
r.Level.MonsterTargets[pos] = target
|
||||||
|
r.MovingMonsters.Frame(pos)
|
||||||
|
jumping = append(jumping, pos)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
animation.Frame--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, pos := range jumping {
|
||||||
|
delete(r.IdleMonsters.Values, pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Animate()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultAnimationFrames = 20
|
||||||
|
|
||||||
|
func (r levelController) Render(ctx ui.Context) {
|
||||||
|
const twelfth = (1. / 6) * geom.Pi
|
||||||
|
renderer := ctx.Renderer()
|
||||||
|
|
||||||
|
size := geom.Floor32(tins2021.TextureSize * r.Scale)
|
||||||
|
scale := size / tins2021.TextureSize
|
||||||
|
|
||||||
|
centerTopSquare := geom.PtF32(.5, .5*geom.Sin32(twelfth))
|
||||||
|
delta := geom.PtF32(geom.Cos32(twelfth), .5+centerTopSquare.Y).Mul(size)
|
||||||
|
centerTopSquare = centerTopSquare.Mul(size)
|
||||||
|
|
||||||
|
delta.X = geom.Round32(delta.X)
|
||||||
|
delta.Y = geom.Round32(delta.Y)
|
||||||
|
toScreen := func(p geom.Point) geom.PointF32 {
|
||||||
|
if p.Y%2 == 0 {
|
||||||
|
return p.ToF32().Mul2D(delta.XY()).Add2D(.5*delta.X, 0)
|
||||||
|
}
|
||||||
|
return p.ToF32().Mul2D(delta.XY())
|
||||||
|
}
|
||||||
|
|
||||||
|
textures := ctx.Textures()
|
||||||
|
regular := r.Cubes["regular"].Scaled(textures, scale)
|
||||||
|
// blocked := r.Cubes["blocked"].Scaled(textures, scale)
|
||||||
|
// colors := r.Cubes["colored"].Scaled(textures, scale)
|
||||||
|
|
||||||
|
cubeWidth := float32(regular.Normal.Width())
|
||||||
|
cubeHeight := float32(regular.Normal.Height())
|
||||||
|
|
||||||
|
player := ctx.Textures().ScaledByName("dwarf", scale*.6)
|
||||||
|
star := tins2021.NewAnimatedTexture(ctx.Textures().ScaledByName("star", scale*.4), defaultAnimationFrames)
|
||||||
|
heart := tins2021.NewAnimatedTexture(ctx.Textures().ScaledByName("heart", scale*.4), defaultAnimationFrames)
|
||||||
|
monsterTextures := map[tins2021.MonsterType]tins2021.AnimatedTexture{}
|
||||||
|
for typ, name := range r.MonsterTextureNames {
|
||||||
|
monsterTextures[typ] = tins2021.NewAnimatedTexture(ctx.Textures().ScaledByName(name, scale*.4), defaultAnimationFrames)
|
||||||
|
}
|
||||||
|
propOffset := geom.PtF32(-.5*float32(star.Texture.Height()), -.8*float32(star.Texture.Height()))
|
||||||
|
|
||||||
|
distances := r.Level.Tiles.Distances(r.Level.Player)
|
||||||
|
|
||||||
|
positionOfTile := func(position geom.Point, tile *tins2021.Tile) (topLeft, centerOfPlatform geom.PointF32) {
|
||||||
|
topLeft = toScreen(position)
|
||||||
|
if tile.Inversed {
|
||||||
|
return topLeft, topLeft.Add2D(.5*float32(cubeWidth), .6*float32(cubeHeight))
|
||||||
|
}
|
||||||
|
return topLeft, topLeft.Add2D(.5*float32(cubeWidth), .2*float32(cubeHeight))
|
||||||
|
}
|
||||||
|
|
||||||
|
for y := r.Level.Bounds.Min.Y; y < r.Level.Bounds.Max.Y; y++ {
|
||||||
|
for x := r.Level.Bounds.Min.X; x < r.Level.Bounds.Max.X; x++ {
|
||||||
|
pos := geom.Pt(x, y)
|
||||||
|
tile := r.Level.Tiles[pos]
|
||||||
|
if tile == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
screenPos, platformPos := positionOfTile(pos, tile)
|
||||||
|
tileTexture := regular.Normal.Texture
|
||||||
|
if tile.Inversed {
|
||||||
|
tileTexture = regular.Inversed.Texture
|
||||||
|
}
|
||||||
|
renderer.DrawTexturePoint(tileTexture, screenPos)
|
||||||
|
r.SmallFont.TextAlign(renderer, platformPos, color.Black, strconv.Itoa(distances[pos]), ui.AlignCenter)
|
||||||
|
|
||||||
|
if tile.Star {
|
||||||
|
star.Draw(renderer, platformPos.Add(propOffset), r.Animations["star"].Frame(pos))
|
||||||
|
}
|
||||||
|
if tile.Heart {
|
||||||
|
heart.Draw(renderer, platformPos.Add(propOffset), r.Animations["heart"].Frame(pos))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
playerPosition := toScreen(r.Level.Player).Sub(geom.Pt(player.Width()/2, player.Height()).ToF32())
|
||||||
|
if r.Level.Tiles[r.Level.Player].Inversed {
|
||||||
|
centerBottomSquare := geom.PtF32(centerTopSquare.X, size-centerTopSquare.Y)
|
||||||
|
renderer.DrawTexturePoint(player, playerPosition.Add(centerBottomSquare))
|
||||||
|
} else {
|
||||||
|
renderer.DrawTexturePoint(player, playerPosition.Add(centerTopSquare))
|
||||||
|
}
|
||||||
|
|
||||||
|
for pos, monsterType := range r.Level.Monsters {
|
||||||
|
tile := r.Level.Tiles[pos]
|
||||||
|
if tile == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_, platformPos := positionOfTile(pos, tile)
|
||||||
|
name := r.MonsterTextureNames[monsterType.Type()]
|
||||||
|
monsterTextures[monsterType.Type()].Draw(renderer, platformPos.Add(propOffset), r.Animations[name].Frame(pos))
|
||||||
|
}
|
||||||
|
}
|
53
cmd/tins2021/textures.go
Normal file
53
cmd/tins2021/textures.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
|
||||||
|
"opslag.de/schobers/geom"
|
||||||
|
"opslag.de/schobers/tins2021"
|
||||||
|
"opslag.de/schobers/zntg/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type namedTexture struct {
|
||||||
|
ui.Texture
|
||||||
|
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNamedTexture(textures *ui.Textures, name string, im image.Image) namedTexture {
|
||||||
|
texture, err := textures.CreateTextureGo(name, im, true)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return namedTexture{texture, name}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t namedTexture) Scaled(textures *ui.Textures, scale float32) namedTexture {
|
||||||
|
return namedTexture{textures.ScaledByName(t.Name, scale), t.Name}
|
||||||
|
}
|
||||||
|
|
||||||
|
type cubeTexture struct {
|
||||||
|
Normal, Inversed namedTexture
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCubeTexture(textures *ui.Textures, color string) cubeTexture {
|
||||||
|
return cubeTexture{
|
||||||
|
Normal: newNamedTexture(textures, "cube_"+color, tins2021.GenerateCube(color)),
|
||||||
|
Inversed: newNamedTexture(textures, "cube_"+color+"_inversed", tins2021.GenerateHole(color)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t cubeTexture) Scaled(textures *ui.Textures, scale float32) cubeTexture {
|
||||||
|
return cubeTexture{
|
||||||
|
Normal: t.Normal.Scaled(textures, scale),
|
||||||
|
Inversed: t.Inversed.Scaled(textures, scale),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAnimatedTexture(textures *ui.Textures, name string, polygon geom.PolygonF, color string, animation tins2021.AnimationRenderer) tins2021.AnimatedTexture {
|
||||||
|
texture, err := textures.CreateTextureGo(name, tins2021.AnimatePolygon(polygon, color, animation), true)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return tins2021.NewSquareAnimatedTexture(texture)
|
||||||
|
}
|
@ -54,9 +54,9 @@ func run() error {
|
|||||||
if settings.Window.Location != nil {
|
if settings.Window.Location != nil {
|
||||||
location = &geom.PointF32{X: float32(settings.Window.Location.X), Y: float32(settings.Window.Location.Y)}
|
location = &geom.PointF32{X: float32(settings.Window.Location.X), Y: float32(settings.Window.Location.Y)}
|
||||||
}
|
}
|
||||||
if settings.Window.Size == nil {
|
// if settings.Window.Size == nil {
|
||||||
settings.Window.Size = ptPtr(800, 600)
|
settings.Window.Size = ptPtr(1024, 768)
|
||||||
}
|
// }
|
||||||
if settings.Window.VSync == nil {
|
if settings.Window.VSync == nil {
|
||||||
vsync := true
|
vsync := true
|
||||||
settings.Window.VSync = &vsync
|
settings.Window.VSync = &vsync
|
||||||
|
34
colors.go
Normal file
34
colors.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package tins2021
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"github.com/lucasb-eyer/go-colorful"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Blue = `#499BFF`
|
||||||
|
var Gray = `#E5E5E5`
|
||||||
|
var Green = `#9BFF49`
|
||||||
|
var Orange = `#FF9849`
|
||||||
|
var Purple = `#9E49FF`
|
||||||
|
var Red = `#FF4949`
|
||||||
|
var Yellow = `#FFEF49`
|
||||||
|
|
||||||
|
func Darken(hexColor string, lighten float64) string { return Lighten(hexColor, -lighten) }
|
||||||
|
|
||||||
|
func Lighten(hexColor string, lighten float64) string {
|
||||||
|
color := mustHexColor(hexColor)
|
||||||
|
h, c, l := color.Hcl()
|
||||||
|
lightened := colorful.Hcl(h, c, Clamp(l+lighten)).Clamped()
|
||||||
|
return lightened.Hex()
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustHexColor(s string) colorful.Color {
|
||||||
|
c, err := colorful.Hex(s)
|
||||||
|
if err != nil {
|
||||||
|
panic("invalid color")
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustHexColor(s string) color.Color { return mustHexColor(s) }
|
48
cube.go
48
cube.go
@ -9,8 +9,7 @@ import (
|
|||||||
"opslag.de/schobers/geom"
|
"opslag.de/schobers/geom"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Orange = `#FF9849`
|
const hexagonRadius = TextureSize / 2
|
||||||
var Blue = `#499BFF`
|
|
||||||
|
|
||||||
func Clamp(f float64) float64 {
|
func Clamp(f float64) float64 {
|
||||||
if f < 0 {
|
if f < 0 {
|
||||||
@ -22,24 +21,19 @@ func Clamp(f float64) float64 {
|
|||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
const hexagonRadius = 128
|
func drawToGC(gc *draw2dimg.GraphicContext, points ...geom.PointF) {
|
||||||
|
|
||||||
func drawToGC(gc *draw2dimg.GraphicContext, color color.Color, points ...geom.PointF) {
|
|
||||||
gc.SetStrokeColor(color)
|
|
||||||
gc.SetFillColor(color)
|
|
||||||
gc.MoveTo(points[0].XY())
|
gc.MoveTo(points[0].XY())
|
||||||
for _, p := range points[1:] {
|
for _, p := range points[1:] {
|
||||||
gc.LineTo(p.XY())
|
gc.LineTo(p.XY())
|
||||||
}
|
}
|
||||||
gc.Close()
|
gc.Close()
|
||||||
gc.FillStroke()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Lighten(hexColor string, lighten float64) string {
|
func fillStrokeToGC(gc *draw2dimg.GraphicContext, color color.Color, points ...geom.PointF) {
|
||||||
color := mustHexColor(hexColor)
|
gc.SetStrokeColor(color)
|
||||||
h, c, l := color.Hcl()
|
gc.SetFillColor(color)
|
||||||
lightened := colorful.Hcl(h, c, Clamp(l+lighten)).Clamped()
|
drawToGC(gc, points...)
|
||||||
return lightened.Hex()
|
gc.FillStroke()
|
||||||
}
|
}
|
||||||
|
|
||||||
func GenerateCube(hexColor string) image.Image {
|
func GenerateCube(hexColor string) image.Image {
|
||||||
@ -53,9 +47,9 @@ func GenerateCube(hexColor string) image.Image {
|
|||||||
center, points := Hexagon(hexagonRadius)
|
center, points := Hexagon(hexagonRadius)
|
||||||
|
|
||||||
gc := draw2dimg.NewGraphicContext(im)
|
gc := draw2dimg.NewGraphicContext(im)
|
||||||
drawToGC(gc, dark, points[2], points[3], points[4], center)
|
fillStrokeToGC(gc, dark, points[2], points[3], points[4], center)
|
||||||
drawToGC(gc, normal, points[4], points[5], points[0], center)
|
fillStrokeToGC(gc, normal, points[4], points[5], points[0], center)
|
||||||
drawToGC(gc, light, points[0], points[1], points[2], center)
|
fillStrokeToGC(gc, light, points[0], points[1], points[2], center)
|
||||||
|
|
||||||
return im
|
return im
|
||||||
}
|
}
|
||||||
@ -66,9 +60,9 @@ func GenerateHexagon(hexColor string) image.Image {
|
|||||||
center, points := Hexagon(hexagonRadius)
|
center, points := Hexagon(hexagonRadius)
|
||||||
|
|
||||||
gc := draw2dimg.NewGraphicContext(im)
|
gc := draw2dimg.NewGraphicContext(im)
|
||||||
drawToGC(gc, color, points[2], points[3], points[4], center)
|
fillStrokeToGC(gc, color, points[2], points[3], points[4], center)
|
||||||
drawToGC(gc, color, points[4], points[5], points[0], center)
|
fillStrokeToGC(gc, color, points[4], points[5], points[0], center)
|
||||||
drawToGC(gc, color, points[0], points[1], points[2], center)
|
fillStrokeToGC(gc, color, points[0], points[1], points[2], center)
|
||||||
|
|
||||||
return im
|
return im
|
||||||
}
|
}
|
||||||
@ -94,17 +88,15 @@ func GenerateHole(hexColor string) image.Image {
|
|||||||
center, points := Hexagon(hexagonRadius)
|
center, points := Hexagon(hexagonRadius)
|
||||||
|
|
||||||
gc := draw2dimg.NewGraphicContext(im)
|
gc := draw2dimg.NewGraphicContext(im)
|
||||||
drawToGC(gc, dark, points[5], points[0], points[1], center)
|
fillStrokeToGC(gc, dark, points[5], points[0], points[1], center)
|
||||||
drawToGC(gc, normal, points[1], points[2], points[3], center)
|
fillStrokeToGC(gc, normal, points[1], points[2], points[3], center)
|
||||||
drawToGC(gc, light, points[3], points[4], points[5], center)
|
fillStrokeToGC(gc, light, points[3], points[4], points[5], center)
|
||||||
|
|
||||||
return im
|
return im
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustHexColor(s string) colorful.Color {
|
func strokeToGC(gc *draw2dimg.GraphicContext, color color.Color, points ...geom.PointF) {
|
||||||
c, err := colorful.Hex(s)
|
gc.SetStrokeColor(color)
|
||||||
if err != nil {
|
drawToGC(gc, points...)
|
||||||
panic("invalid color")
|
gc.Stroke()
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
}
|
||||||
|
191
level.go
191
level.go
@ -6,66 +6,67 @@ import (
|
|||||||
"opslag.de/schobers/geom"
|
"opslag.de/schobers/geom"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Direction int
|
|
||||||
|
|
||||||
const (
|
|
||||||
DirectionDownRight Direction = iota
|
|
||||||
DirectionDownLeft
|
|
||||||
DirectionUpLeft
|
|
||||||
DirectionUpRight
|
|
||||||
)
|
|
||||||
|
|
||||||
type Level struct {
|
type Level struct {
|
||||||
Player geom.Point
|
Player geom.Point
|
||||||
Tiles map[geom.Point]*Tile
|
Lives int
|
||||||
|
StarsCollected int
|
||||||
|
Tiles Tiles
|
||||||
|
Monsters Monsters
|
||||||
|
MonsterTargets map[geom.Point]geom.Point
|
||||||
|
Bounds geom.Rectangle
|
||||||
}
|
}
|
||||||
|
|
||||||
func AdjacentPosition(pos geom.Point, dir Direction) geom.Point {
|
func NewLevel() *Level {
|
||||||
if pos.Y%2 == 0 {
|
const dims = 12
|
||||||
switch dir {
|
f := &Level{
|
||||||
case DirectionDownRight:
|
Player: geom.Pt(1, 1),
|
||||||
return geom.Pt(pos.X+1, pos.Y+1)
|
Lives: 3,
|
||||||
case DirectionDownLeft:
|
StarsCollected: 0,
|
||||||
return geom.Pt(pos.X, pos.Y+1)
|
Tiles: Tiles{},
|
||||||
case DirectionUpLeft:
|
Monsters: Monsters{},
|
||||||
return geom.Pt(pos.X, pos.Y-1)
|
MonsterTargets: map[geom.Point]geom.Point{},
|
||||||
case DirectionUpRight:
|
Bounds: geom.Rect(1, 1, dims+1, dims+1),
|
||||||
return geom.Pt(pos.X+1, pos.Y-1)
|
}
|
||||||
default:
|
for y := 1; y <= dims; y++ {
|
||||||
panic("invalid direction")
|
endX := dims
|
||||||
|
if y%2 == 0 {
|
||||||
|
endX--
|
||||||
|
}
|
||||||
|
for x := 1; x <= endX; x++ {
|
||||||
|
f.Tiles[geom.Pt(x, y)] = &Tile{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch dir {
|
return f
|
||||||
case DirectionDownRight:
|
|
||||||
return geom.Pt(pos.X, pos.Y+1)
|
|
||||||
case DirectionDownLeft:
|
|
||||||
return geom.Pt(pos.X-1, pos.Y+1)
|
|
||||||
case DirectionUpLeft:
|
|
||||||
return geom.Pt(pos.X-1, pos.Y-1)
|
|
||||||
case DirectionUpRight:
|
|
||||||
return geom.Pt(pos.X, pos.Y-1)
|
|
||||||
default:
|
|
||||||
panic("invalid direction")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l Level) CanPlayerMove(dir Direction) (geom.Point, bool) {
|
func (l Level) CanPlayerMove(dir Direction) (geom.Point, bool) {
|
||||||
towards := AdjacentPosition(l.Player, dir)
|
return l.Tiles.CanMove(l.Player, dir)
|
||||||
from := l.Tiles[l.Player]
|
|
||||||
to := l.Tiles[towards]
|
|
||||||
if to == nil {
|
|
||||||
return geom.ZeroPt, false
|
|
||||||
}
|
}
|
||||||
if dir == DirectionDownRight || dir == DirectionDownLeft {
|
|
||||||
if !from.Inversed && to.Inversed {
|
func (l Level) CanMonsterMove(p geom.Point, dir Direction) (geom.Point, bool) {
|
||||||
return geom.ZeroPt, false
|
q, ok := l.Tiles.CanMove(p, dir)
|
||||||
|
if !ok {
|
||||||
|
return geom.Point{}, false
|
||||||
}
|
}
|
||||||
} else {
|
if l.CanMonsterMoveTo(q) {
|
||||||
if from.Inversed && !to.Inversed {
|
return q, true
|
||||||
return geom.ZeroPt, false
|
}
|
||||||
|
return geom.Point{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l Level) CanMonsterMoveTo(p geom.Point) bool {
|
||||||
|
if l.Tiles[p].Occupied() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if _, ok := l.Monsters[p]; ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, target := range l.MonsterTargets {
|
||||||
|
if p == target {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return towards, true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Level) MovePlayer(dir Direction) bool {
|
func (l *Level) MovePlayer(dir Direction) bool {
|
||||||
@ -73,30 +74,92 @@ func (l *Level) MovePlayer(dir Direction) bool {
|
|||||||
if !allowed {
|
if !allowed {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
l.Tiles[l.Player].Invert()
|
|
||||||
l.Player = towards
|
l.Player = towards
|
||||||
|
tile := l.Tiles[towards]
|
||||||
|
if tile.Heart {
|
||||||
|
l.Lives++
|
||||||
|
tile.Heart = false
|
||||||
|
}
|
||||||
|
if tile.Star {
|
||||||
|
l.StarsCollected++
|
||||||
|
tile.Star = false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRandomLevel() *Level {
|
func (l *Level) Randomize(difficulty int, stars int) {
|
||||||
f := &Level{
|
if difficulty < 0 {
|
||||||
Tiles: map[geom.Point]*Tile{},
|
difficulty = 0
|
||||||
Player: geom.Pt(1, 1),
|
|
||||||
}
|
}
|
||||||
for y := 1; y <= 10; y++ {
|
positions := make([]geom.Point, 0, len(l.Tiles))
|
||||||
endX := 10
|
for pos := range l.Tiles {
|
||||||
if y%2 == 0 {
|
positions = append(positions, pos)
|
||||||
endX--
|
|
||||||
}
|
}
|
||||||
for x := 1; x <= endX; x++ {
|
flip := difficulty * len(l.Tiles) / 200
|
||||||
f.Tiles[geom.Pt(x, y)] = &Tile{Inversed: rand.Intn(6) == 0}
|
if flip > len(l.Tiles)/2 {
|
||||||
|
flip = len(l.Tiles) / 2
|
||||||
|
}
|
||||||
|
for ; flip > 0; flip-- {
|
||||||
|
for {
|
||||||
|
i := rand.Intn(len(positions))
|
||||||
|
pos := positions[i]
|
||||||
|
if l.Tiles[pos].Inversed {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
l.Tiles[pos].Invert()
|
||||||
|
if l.Tiles.AllReachable(l.Player) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
l.Tiles[pos].Invert()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return f
|
for stars > 0 {
|
||||||
|
i := rand.Intn(len(positions))
|
||||||
|
pos := positions[i]
|
||||||
|
if l.Tiles[pos].Occupied() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
l.Tiles[pos].Star = true
|
||||||
|
stars--
|
||||||
|
}
|
||||||
|
hearts := 1 + (80-difficulty)*4/80 // [5..0]
|
||||||
|
for hearts > 0 {
|
||||||
|
i := rand.Intn(len(positions))
|
||||||
|
pos := positions[i]
|
||||||
|
if l.Tiles[pos].Occupied() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
l.Tiles[pos].Heart = true
|
||||||
|
hearts--
|
||||||
|
}
|
||||||
|
monsters := 1 + (4 * difficulty / 100)
|
||||||
|
minRandomMonster := (100 - difficulty)
|
||||||
|
minChaserMonster := (200 - difficulty) / 2
|
||||||
|
for monsters > 0 {
|
||||||
|
i := rand.Intn(len(positions))
|
||||||
|
pos := positions[i]
|
||||||
|
curr := l.Monsters[pos]
|
||||||
|
if l.Tiles[pos].Occupied() || curr != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
monster := MonsterTypeStraight
|
||||||
|
m := rand.Intn(100)
|
||||||
|
if m >= minChaserMonster {
|
||||||
|
monster = MonsterTypeChaser
|
||||||
|
} else if m >= minRandomMonster {
|
||||||
|
monster = MonsterTypeRandom
|
||||||
|
}
|
||||||
|
switch monster {
|
||||||
|
case MonsterTypeStraight:
|
||||||
|
l.Monsters[pos] = &StraightWalkingMonster{Direction: RandomDirection()}
|
||||||
|
case MonsterTypeRandom:
|
||||||
|
l.Monsters[pos] = &RandomWalkingMonster{}
|
||||||
|
case MonsterTypeChaser:
|
||||||
|
l.Monsters[pos] = &ChasingMonster{}
|
||||||
|
default:
|
||||||
|
panic("not implemented")
|
||||||
|
// l.Monsters[pos] = monster
|
||||||
|
}
|
||||||
|
monsters--
|
||||||
}
|
}
|
||||||
|
|
||||||
type Tile struct {
|
|
||||||
Inversed bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tile) Invert() { t.Inversed = !t.Inversed }
|
|
||||||
|
31
level_test.go
Normal file
31
level_test.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package tins2021
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewLevelIsAllReachable(t *testing.T) {
|
||||||
|
level := NewLevel()
|
||||||
|
assert.True(t, level.Tiles.AllReachable(level.Player))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRandomizedLevelIsAllReachable(t *testing.T) {
|
||||||
|
level := NewLevel()
|
||||||
|
level.Randomize(len(level.Tiles)/2, 0)
|
||||||
|
assert.True(t, level.Tiles.AllReachable(level.Player))
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkRandomizedLevel(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
level := NewLevel()
|
||||||
|
level.Randomize(len(level.Tiles)*50/100, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkNewLevel(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
NewLevel()
|
||||||
|
}
|
||||||
|
}
|
67
monsters.go
Normal file
67
monsters.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package tins2021
|
||||||
|
|
||||||
|
import (
|
||||||
|
"opslag.de/schobers/geom"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ChasingMonster struct{}
|
||||||
|
|
||||||
|
func (m ChasingMonster) Type() MonsterType { return MonsterTypeChaser }
|
||||||
|
func (m *ChasingMonster) FindTarget(level *Level, src geom.Point) (geom.Point, bool) {
|
||||||
|
path := level.Tiles.ShortestPath(src, level.Player, func(_ geom.Point, t *Tile) bool { return !t.Occupied() })
|
||||||
|
if len(path) < 2 {
|
||||||
|
return geom.Point{}, false
|
||||||
|
}
|
||||||
|
if level.CanMonsterMoveTo(path[1]) {
|
||||||
|
return path[1], true
|
||||||
|
}
|
||||||
|
return geom.Point{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
type Monster interface {
|
||||||
|
Type() MonsterType
|
||||||
|
FindTarget(*Level, geom.Point) (geom.Point, bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Monsters map[geom.Point]Monster
|
||||||
|
|
||||||
|
type MonsterType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
MonsterTypeStraight MonsterType = iota
|
||||||
|
MonsterTypeRandom
|
||||||
|
MonsterTypeChaser
|
||||||
|
)
|
||||||
|
|
||||||
|
type RandomWalkingMonster struct{}
|
||||||
|
|
||||||
|
func (m RandomWalkingMonster) Type() MonsterType { return MonsterTypeRandom }
|
||||||
|
func (m *RandomWalkingMonster) FindTarget(level *Level, src geom.Point) (geom.Point, bool) {
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
dir := RandomDirection()
|
||||||
|
dst, ok := level.CanMonsterMove(src, dir)
|
||||||
|
if ok {
|
||||||
|
return dst, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return geom.Point{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
type StraightWalkingMonster struct {
|
||||||
|
Direction Direction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m StraightWalkingMonster) Type() MonsterType { return MonsterTypeStraight }
|
||||||
|
func (m *StraightWalkingMonster) FindTarget(level *Level, src geom.Point) (geom.Point, bool) {
|
||||||
|
dst, ok := level.CanMonsterMove(src, m.Direction)
|
||||||
|
if ok {
|
||||||
|
return dst, true
|
||||||
|
}
|
||||||
|
reverse := m.Direction.Reverse()
|
||||||
|
dst, ok = level.CanMonsterMove(src, reverse)
|
||||||
|
if ok {
|
||||||
|
m.Direction = reverse
|
||||||
|
return dst, true
|
||||||
|
}
|
||||||
|
return geom.Point{}, false
|
||||||
|
}
|
211
rendereranimation.go
Normal file
211
rendereranimation.go
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
package tins2021
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/fogleman/fauxgl"
|
||||||
|
"github.com/nfnt/resize"
|
||||||
|
"golang.org/x/image/draw"
|
||||||
|
"opslag.de/schobers/geom"
|
||||||
|
"opslag.de/schobers/zntg/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
fovy = 40 // vertical field of view in degrees
|
||||||
|
near = 1 // near clipping plane
|
||||||
|
far = 10 // far clipping plane
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
eye = fauxgl.V(0, 0, 4) // camera position
|
||||||
|
center = fauxgl.V(0, 0, 0) // view center position
|
||||||
|
up = fauxgl.V(0, 1, 0) // up vector
|
||||||
|
light = fauxgl.V(.5, 1, .75).Normalize() // light direction
|
||||||
|
)
|
||||||
|
|
||||||
|
func AnimatePolygon(polygon geom.PolygonF, hexColor string, renderer AnimationRenderer) image.Image {
|
||||||
|
mesh := generateMeshFromPolygon(polygon, .2)
|
||||||
|
renderer.setup(mesh)
|
||||||
|
return renderMeshAnimation(hexColor, renderer.frames(), renderer.render)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AnimateSTL(resources ui.PhysicalResources, name, hexColor string, renderer AnimationRenderer) image.Image {
|
||||||
|
path, err := resources.FetchResource(name)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
mesh, err := fauxgl.LoadSTL(path)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
renderer.setup(mesh)
|
||||||
|
return renderMeshAnimation(hexColor, renderer.frames(), renderer.render)
|
||||||
|
}
|
||||||
|
|
||||||
|
type animationRendererBase struct {
|
||||||
|
Frames int
|
||||||
|
Mesh *fauxgl.Mesh
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r animationRendererBase) frames() int { return r.Frames }
|
||||||
|
|
||||||
|
func (r *animationRendererBase) setup(mesh *fauxgl.Mesh) {
|
||||||
|
r.Mesh = mesh
|
||||||
|
mesh.BiUnitCube()
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ AnimationRenderer = &RotateAnimationRenderer{}
|
||||||
|
var _ AnimationRenderer = &WobbleAnimationRenderer{}
|
||||||
|
|
||||||
|
type AnimationRenderer interface {
|
||||||
|
frames() int
|
||||||
|
setup(*fauxgl.Mesh)
|
||||||
|
render(*fauxgl.Context, int, float64)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateMeshFromPolygon(polygon geom.PolygonF, thickness float64) *fauxgl.Mesh {
|
||||||
|
vec := func(p geom.PointF, z float64) fauxgl.Vector { return fauxgl.V(p.X, p.Y, z) }
|
||||||
|
tri := fauxgl.NewTriangleForPoints
|
||||||
|
face := func(q, r, s geom.PointF, n float64) *fauxgl.Triangle {
|
||||||
|
return tri(vec(q, n*thickness), vec(r, n*thickness), vec(s, n*thickness))
|
||||||
|
}
|
||||||
|
var triangles []*fauxgl.Triangle
|
||||||
|
// generate front & back
|
||||||
|
for _, t := range polygon.Triangulate() {
|
||||||
|
triangles = append(triangles,
|
||||||
|
face(t.Points[0], t.Points[1], t.Points[2], 1), // front
|
||||||
|
face(t.Points[2], t.Points[1], t.Points[0], -1), // back
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// generate side
|
||||||
|
back, front := -thickness, thickness
|
||||||
|
for i, p := range polygon.Points {
|
||||||
|
next := polygon.Points[(i+1)%len(polygon.Points)]
|
||||||
|
q, r, s, t := vec(p, back), vec(next, back), vec(next, front), vec(p, front)
|
||||||
|
triangles = append(triangles, tri(q, r, s), tri(q, s, t))
|
||||||
|
}
|
||||||
|
|
||||||
|
mesh := fauxgl.NewTriangleMesh(triangles)
|
||||||
|
return mesh
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderMeshAnimation(hexColor string, frames int, render func(*fauxgl.Context, int, float64)) image.Image {
|
||||||
|
const scale = 4
|
||||||
|
context := fauxgl.NewContext(TextureSize*scale, TextureSize*scale)
|
||||||
|
|
||||||
|
// matrix := fauxgl.LookAt(eye, center, up).Perspective(fovy, 1, near, far)
|
||||||
|
|
||||||
|
const s = 1.1
|
||||||
|
// rot3 := func(m fauxgl.Matrix) fauxgl.Matrix {
|
||||||
|
// return fauxgl.Matrix{
|
||||||
|
// X00: m.X20, X01: m.X10, X02: m.X00, X03: m.X03,
|
||||||
|
// X10: m.X21, X11: m.X11, X12: m.X01, X13: m.X13,
|
||||||
|
// X20: m.X22, X21: m.X12, X22: m.X02, X23: m.X23,
|
||||||
|
// X30: m.X30, X31: m.X31, X32: m.X32, X33: m.X33,
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// sqrt_6_1 := 1 / geom.Sqrt(6)
|
||||||
|
// iso := fauxgl.Matrix{
|
||||||
|
// X00: sqrt_6_1 * geom.Sqrt(3), X01: 0, X02: -sqrt_6_1 * geom.Sqrt(3), X03: 0,
|
||||||
|
// X10: sqrt_6_1, X11: 2 * sqrt_6_1, X12: sqrt_6_1, X13: 0,
|
||||||
|
// X20: sqrt_6_1 * geom.Sqrt(2), X21: -sqrt_6_1 * geom.Sqrt(2), X22: sqrt_6_1 * geom.Sqrt(2), X23: 0,
|
||||||
|
// X30: 0, X31: 0, X32: 0, X33: 1}
|
||||||
|
|
||||||
|
matrix := fauxgl.Orthographic(-s, s, -s, s, near, far).Mul(fauxgl.LookAt(eye, center, up))
|
||||||
|
|
||||||
|
color := fauxgl.HexColor(hexColor)
|
||||||
|
|
||||||
|
animation := image.NewNRGBA(image.Rect(0, 0, TextureSize*frames, TextureSize))
|
||||||
|
for i := 0; i < frames; i++ {
|
||||||
|
context.ClearDepthBuffer()
|
||||||
|
context.ClearColorBufferWith(fauxgl.Transparent)
|
||||||
|
|
||||||
|
shader := fauxgl.NewPhongShader(matrix, light, eye)
|
||||||
|
shader.ObjectColor = color
|
||||||
|
shader.AmbientColor = fauxgl.MakeColor(mustHexColor(`#7F7F7F`))
|
||||||
|
context.Shader = shader
|
||||||
|
|
||||||
|
render(context, i, float64(i)/float64(frames))
|
||||||
|
|
||||||
|
frame := resize.Resize(TextureSize, TextureSize, context.Image(), resize.Bilinear)
|
||||||
|
draw.Copy(animation, image.Pt(i*TextureSize, 0), frame, frame.Bounds(), draw.Src, nil)
|
||||||
|
}
|
||||||
|
return animation
|
||||||
|
}
|
||||||
|
|
||||||
|
type RotateAnimationRenderer struct {
|
||||||
|
animationRendererBase
|
||||||
|
|
||||||
|
Rotation float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRotateAnimation(frames int) AnimationRenderer {
|
||||||
|
return &RotateAnimationRenderer{
|
||||||
|
animationRendererBase: animationRendererBase{Frames: frames},
|
||||||
|
Rotation: 2 * geom.Pi / float64(frames),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a RotateAnimationRenderer) render(context *fauxgl.Context, _ int, _ float64) {
|
||||||
|
context.DrawMesh(a.Mesh)
|
||||||
|
a.Mesh.Transform(fauxgl.Rotate(up, a.Rotation))
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveMeshSTL(path, name string, mesh *fauxgl.Mesh) error {
|
||||||
|
stl, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer stl.Close()
|
||||||
|
|
||||||
|
fmt.Fprintf(stl, "solid %s\n", name)
|
||||||
|
for _, triangle := range mesh.Triangles {
|
||||||
|
normal := triangle.Normal()
|
||||||
|
fmt.Fprintf(stl, " facet normal %f, %f, %f\n", normal.X, normal.Y, normal.Z)
|
||||||
|
fmt.Fprintf(stl, " outer loop\n")
|
||||||
|
fmt.Fprintf(stl, " vertex %f, %f, %f\n", triangle.V1.Position.X, triangle.V1.Position.Y, triangle.V1.Position.Z)
|
||||||
|
fmt.Fprintf(stl, " vertex %f, %f, %f\n", triangle.V2.Position.X, triangle.V2.Position.Y, triangle.V2.Position.Z)
|
||||||
|
fmt.Fprintf(stl, " vertex %f, %f, %f\n", triangle.V3.Position.X, triangle.V3.Position.Y, triangle.V3.Position.Z)
|
||||||
|
fmt.Fprintf(stl, " endloop\n")
|
||||||
|
fmt.Fprintf(stl, " endfacet\n")
|
||||||
|
}
|
||||||
|
fmt.Fprintf(stl, "endsolid %s\n", name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveSTLFromPolygon(path, name string, polygon geom.PolygonF, thickness float64) {
|
||||||
|
mesh := generateMeshFromPolygon(polygon, thickness)
|
||||||
|
saveMeshSTL(path, name, mesh)
|
||||||
|
}
|
||||||
|
|
||||||
|
type WobbleAnimationRenderer struct {
|
||||||
|
animationRendererBase
|
||||||
|
|
||||||
|
Wobble float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWobbleAnimation(frames int, wobble float64) AnimationRenderer {
|
||||||
|
return &WobbleAnimationRenderer{
|
||||||
|
animationRendererBase: animationRendererBase{Frames: frames},
|
||||||
|
Wobble: wobble,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a WobbleAnimationRenderer) animate(frame float64) float64 {
|
||||||
|
frame += .25
|
||||||
|
if frame >= 1 {
|
||||||
|
frame -= 1
|
||||||
|
}
|
||||||
|
// return geom.Cos(float64(frame) * 2 * geom.Pi / float64(a.Frames))
|
||||||
|
return geom.Abs(frame*4-2) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a WobbleAnimationRenderer) render(context *fauxgl.Context, frame int, animation float64) {
|
||||||
|
context.DrawMesh(a.Mesh)
|
||||||
|
curr := a.animate(animation)
|
||||||
|
next := a.animate(float64(frame+1) / float64(a.Frames))
|
||||||
|
a.Mesh.Transform(fauxgl.Rotate(up, (next-curr)*a.Wobble*geom.Pi/180))
|
||||||
|
}
|
94
scenery.go
Normal file
94
scenery.go
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
package tins2021
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
|
||||||
|
"github.com/llgcode/draw2d/draw2dimg"
|
||||||
|
"opslag.de/schobers/geom"
|
||||||
|
)
|
||||||
|
|
||||||
|
const TextureSize = 128
|
||||||
|
|
||||||
|
func CreateHeart() geom.PolygonF {
|
||||||
|
var polygon geom.PolygonF
|
||||||
|
const segments = 100
|
||||||
|
for segment := 0; segment < 100; segment++ {
|
||||||
|
t := 2 * geom.Pi * float64(segment) / segments
|
||||||
|
st := geom.Sin(t)
|
||||||
|
polygon.Points = append(polygon.Points, geom.PtF(
|
||||||
|
16*st*st*st,
|
||||||
|
13*geom.Cos(t)-5*geom.Cos(2*t)-2*geom.Cos(3*t)-geom.Cos(4*t)))
|
||||||
|
}
|
||||||
|
return polygon.Reverse().Mul(1. / 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateHexagon() geom.PolygonF {
|
||||||
|
var polygon geom.PolygonF
|
||||||
|
pt := func(rotation float64) geom.PointF {
|
||||||
|
a := .5*geom.Pi + 2*geom.Pi*rotation
|
||||||
|
return geom.PtF(geom.Cos(a), geom.Sin(a))
|
||||||
|
}
|
||||||
|
const sides = 6
|
||||||
|
for side := 0; side < 6; side++ {
|
||||||
|
polygon.Points = append(polygon.Points,
|
||||||
|
pt(float64(side)/float64(sides)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return polygon
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateStar(sides int) geom.PolygonF {
|
||||||
|
var polygon geom.PolygonF
|
||||||
|
pt := func(rotation float64) geom.PointF {
|
||||||
|
a := .5*geom.Pi + 2*geom.Pi*rotation
|
||||||
|
return geom.PtF(geom.Cos(a), geom.Sin(a))
|
||||||
|
}
|
||||||
|
for side := 0; side < sides; side++ {
|
||||||
|
polygon.Points = append(polygon.Points,
|
||||||
|
pt(float64(side)/float64(sides)),
|
||||||
|
pt((float64(side)+0.5)/float64(sides)).Mul(.5),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return polygon
|
||||||
|
}
|
||||||
|
|
||||||
|
func RenderPolygon2D(polygon geom.PolygonF, hexColor string) image.Image {
|
||||||
|
im := image.NewRGBA(image.Rect(0, 0, TextureSize, TextureSize))
|
||||||
|
color := mustHexColor(hexColor)
|
||||||
|
gc := draw2dimg.NewGraphicContext(im)
|
||||||
|
fillStrokeToGC(gc, color, polygon.Points...)
|
||||||
|
return im
|
||||||
|
}
|
||||||
|
|
||||||
|
func RenderTriangles2D(triangles []geom.TriangleF, hexColor string) image.Image {
|
||||||
|
im := image.NewRGBA(image.Rect(0, 0, TextureSize, TextureSize))
|
||||||
|
color := mustHexColor(hexColor)
|
||||||
|
gc := draw2dimg.NewGraphicContext(im)
|
||||||
|
for _, triangle := range triangles {
|
||||||
|
strokeToGC(gc, color, triangle.Points[:]...)
|
||||||
|
}
|
||||||
|
return im
|
||||||
|
}
|
||||||
|
|
||||||
|
func RenderTriangleSides2D(triangles []geom.TriangleF) image.Image {
|
||||||
|
im := image.NewRGBA(image.Rect(0, 0, TextureSize, TextureSize))
|
||||||
|
r, g, b := mustHexColor(`#FF0000`), mustHexColor(`#00FF00`), mustHexColor(`#0000FF`)
|
||||||
|
gc := draw2dimg.NewGraphicContext(im)
|
||||||
|
gc.SetLineWidth(2)
|
||||||
|
for _, triangle := range triangles {
|
||||||
|
inset := triangle.Inset(5)
|
||||||
|
gc.SetStrokeColor(r)
|
||||||
|
gc.MoveTo(inset.Points[0].XY())
|
||||||
|
gc.LineTo(inset.Points[1].XY())
|
||||||
|
gc.Stroke()
|
||||||
|
gc.SetStrokeColor(g)
|
||||||
|
gc.MoveTo(inset.Points[1].XY())
|
||||||
|
gc.LineTo(inset.Points[2].XY())
|
||||||
|
gc.Stroke()
|
||||||
|
gc.SetStrokeColor(b)
|
||||||
|
gc.MoveTo(inset.Points[2].XY())
|
||||||
|
gc.LineTo(inset.Points[0].XY())
|
||||||
|
gc.Stroke()
|
||||||
|
}
|
||||||
|
return im
|
||||||
|
}
|
197
tiles.go
Normal file
197
tiles.go
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
package tins2021
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"math/rand"
|
||||||
|
|
||||||
|
"opslag.de/schobers/geom"
|
||||||
|
)
|
||||||
|
|
||||||
|
var AllDirections = []Direction{DirectionDownRight, DirectionDownLeft, DirectionUpLeft, DirectionUpRight}
|
||||||
|
|
||||||
|
func RandomDirection() Direction {
|
||||||
|
return AllDirections[rand.Intn(len(AllDirections))]
|
||||||
|
}
|
||||||
|
|
||||||
|
func AdjacentPosition(pos geom.Point, dir Direction) geom.Point {
|
||||||
|
if pos.Y%2 == 0 {
|
||||||
|
switch dir {
|
||||||
|
case DirectionDownRight:
|
||||||
|
return geom.Pt(pos.X+1, pos.Y+1)
|
||||||
|
case DirectionDownLeft:
|
||||||
|
return geom.Pt(pos.X, pos.Y+1)
|
||||||
|
case DirectionUpLeft:
|
||||||
|
return geom.Pt(pos.X, pos.Y-1)
|
||||||
|
case DirectionUpRight:
|
||||||
|
return geom.Pt(pos.X+1, pos.Y-1)
|
||||||
|
default:
|
||||||
|
panic("invalid direction")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch dir {
|
||||||
|
case DirectionDownRight:
|
||||||
|
return geom.Pt(pos.X, pos.Y+1)
|
||||||
|
case DirectionDownLeft:
|
||||||
|
return geom.Pt(pos.X-1, pos.Y+1)
|
||||||
|
case DirectionUpLeft:
|
||||||
|
return geom.Pt(pos.X-1, pos.Y-1)
|
||||||
|
case DirectionUpRight:
|
||||||
|
return geom.Pt(pos.X, pos.Y-1)
|
||||||
|
default:
|
||||||
|
panic("invalid direction")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Direction int
|
||||||
|
|
||||||
|
const (
|
||||||
|
DirectionDownRight Direction = iota
|
||||||
|
DirectionDownLeft
|
||||||
|
DirectionUpLeft
|
||||||
|
DirectionUpRight
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d Direction) Reverse() Direction {
|
||||||
|
switch d {
|
||||||
|
case DirectionDownRight:
|
||||||
|
return DirectionUpLeft
|
||||||
|
case DirectionDownLeft:
|
||||||
|
return DirectionUpRight
|
||||||
|
case DirectionUpLeft:
|
||||||
|
return DirectionDownRight
|
||||||
|
case DirectionUpRight:
|
||||||
|
return DirectionDownLeft
|
||||||
|
default:
|
||||||
|
panic("direction not supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tile struct {
|
||||||
|
Inversed bool
|
||||||
|
Star bool
|
||||||
|
Heart bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tile) Occupied() bool {
|
||||||
|
return t.Star || t.Heart
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tile) Invert() { t.Inversed = !t.Inversed }
|
||||||
|
|
||||||
|
type Tiles map[geom.Point]*Tile
|
||||||
|
|
||||||
|
func (t Tiles) AllReachable(from geom.Point) bool {
|
||||||
|
visited := map[geom.Point]bool{}
|
||||||
|
visit := []geom.Point{from}
|
||||||
|
for len(visit) > 0 {
|
||||||
|
next := visit[0]
|
||||||
|
visit = visit[1:]
|
||||||
|
for _, dir := range AllDirections {
|
||||||
|
neighbour, ok := t.CanMove(next, dir)
|
||||||
|
if !ok || visited[neighbour] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
visited[neighbour] = true
|
||||||
|
visit = append(visit, neighbour)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(visited) == len(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tiles) CanMove(p geom.Point, dir Direction) (geom.Point, bool) {
|
||||||
|
towards := AdjacentPosition(p, dir)
|
||||||
|
from := t[p]
|
||||||
|
to := t[towards]
|
||||||
|
if to == nil {
|
||||||
|
return geom.Point{}, false
|
||||||
|
}
|
||||||
|
if dir == DirectionDownRight || dir == DirectionDownLeft {
|
||||||
|
if !from.Inversed && to.Inversed {
|
||||||
|
return geom.Point{}, false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if from.Inversed && !to.Inversed {
|
||||||
|
return geom.Point{}, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return towards, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tiles) CanMoveTile(p geom.Point, dir Direction) (geom.Point, *Tile, bool) {
|
||||||
|
to, ok := t.CanMove(p, dir)
|
||||||
|
if !ok {
|
||||||
|
return geom.Point{}, nil, false
|
||||||
|
}
|
||||||
|
return to, t[to], true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tiles) Distances(from geom.Point) map[geom.Point]int {
|
||||||
|
distance := map[geom.Point]int{
|
||||||
|
from: 0,
|
||||||
|
}
|
||||||
|
visit := []geom.Point{from}
|
||||||
|
for len(visit) > 0 {
|
||||||
|
next := visit[0]
|
||||||
|
visit = visit[1:]
|
||||||
|
for _, dir := range AllDirections {
|
||||||
|
neighbour, ok := t.CanMove(next, dir)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
d := distance[next] + 1
|
||||||
|
if neighbourD, ok := distance[neighbour]; ok && neighbourD <= d {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
distance[neighbour] = d
|
||||||
|
visit = append(visit, neighbour)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return distance
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tiles) ShortestPath(from, to geom.Point, canMoveTo func(geom.Point, *Tile) bool) []geom.Point {
|
||||||
|
distances := map[geom.Point]int{
|
||||||
|
from: 0,
|
||||||
|
}
|
||||||
|
origins := map[geom.Point]geom.Point{
|
||||||
|
from: from,
|
||||||
|
}
|
||||||
|
estimated := map[geom.Point]int{
|
||||||
|
from: from.DistInt(to),
|
||||||
|
}
|
||||||
|
visit := map[geom.Point]bool{from: true}
|
||||||
|
for len(visit) > 0 {
|
||||||
|
var next geom.Point
|
||||||
|
best := math.MaxInt32
|
||||||
|
for candidate := range visit {
|
||||||
|
e := estimated[candidate]
|
||||||
|
if e < best {
|
||||||
|
next = candidate
|
||||||
|
best = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if next == to {
|
||||||
|
path := []geom.Point{to}
|
||||||
|
for path[0] != from {
|
||||||
|
path = append([]geom.Point{origins[path[0]]}, path...)
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
delete(visit, next)
|
||||||
|
for _, dir := range AllDirections {
|
||||||
|
neighbour, ok := t.CanMove(next, dir)
|
||||||
|
if !ok || (canMoveTo != nil && !canMoveTo(neighbour, t[neighbour])) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
d := distances[next] + 1
|
||||||
|
if neighbourD, ok := distances[neighbour]; ok && neighbourD <= d {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
distances[neighbour] = d
|
||||||
|
origins[neighbour] = next
|
||||||
|
estimated[neighbour] = d + neighbour.DistInt(to)
|
||||||
|
visit[neighbour] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []geom.Point{}
|
||||||
|
}
|
89
tiles_test.go
Normal file
89
tiles_test.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package tins2021
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"opslag.de/schobers/geom"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTilesNotTraversableWhenNoTile(t *testing.T) {
|
||||||
|
tiles := Tiles{}
|
||||||
|
pos := geom.ZeroPt
|
||||||
|
tiles[pos] = &Tile{}
|
||||||
|
for _, dir := range AllDirections {
|
||||||
|
_, ok := tiles.CanMove(pos, dir)
|
||||||
|
assert.False(t, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTilesIsTraversableInDirection(t *testing.T) {
|
||||||
|
tiles := Tiles{}
|
||||||
|
center := geom.Pt(1, 1)
|
||||||
|
tiles[center] = &Tile{}
|
||||||
|
for _, dir := range AllDirections {
|
||||||
|
tiles[AdjacentPosition(center, dir)] = &Tile{}
|
||||||
|
}
|
||||||
|
tiles[geom.Pt(1, 2)] = &Tile{}
|
||||||
|
for _, dir := range AllDirections {
|
||||||
|
pos, ok := tiles.CanMove(center, dir)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, AdjacentPosition(center, dir), pos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTilesNotTraversableWhenGoingUpFromInversedTile(t *testing.T) {
|
||||||
|
tiles := Tiles{}
|
||||||
|
center := geom.Pt(1, 1)
|
||||||
|
tiles[center] = &Tile{Inversed: true}
|
||||||
|
leftUp := AdjacentPosition(center, DirectionUpLeft)
|
||||||
|
rightUp := AdjacentPosition(center, DirectionUpRight)
|
||||||
|
tiles[leftUp] = &Tile{}
|
||||||
|
tiles[rightUp] = &Tile{}
|
||||||
|
_, ok := tiles.CanMove(center, DirectionUpLeft)
|
||||||
|
assert.False(t, ok)
|
||||||
|
_, ok = tiles.CanMove(center, DirectionUpLeft)
|
||||||
|
assert.False(t, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTilesNotTraversableWhenGoingDownToInversedTile(t *testing.T) {
|
||||||
|
tiles := Tiles{}
|
||||||
|
center := geom.Pt(1, 1)
|
||||||
|
tiles[center] = &Tile{}
|
||||||
|
leftDown := AdjacentPosition(center, DirectionDownLeft)
|
||||||
|
rightDown := AdjacentPosition(center, DirectionDownRight)
|
||||||
|
tiles[leftDown] = &Tile{Inversed: true}
|
||||||
|
tiles[rightDown] = &Tile{Inversed: true}
|
||||||
|
_, ok := tiles.CanMove(center, DirectionDownLeft)
|
||||||
|
assert.False(t, ok)
|
||||||
|
_, ok = tiles.CanMove(center, DirectionDownLeft)
|
||||||
|
assert.False(t, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTilesTraversableWhenGoingDownFromInversedTile(t *testing.T) {
|
||||||
|
tiles := Tiles{}
|
||||||
|
center := geom.Pt(1, 1)
|
||||||
|
tiles[center] = &Tile{Inversed: true}
|
||||||
|
leftDown := AdjacentPosition(center, DirectionDownLeft)
|
||||||
|
rightDown := AdjacentPosition(center, DirectionDownRight)
|
||||||
|
tiles[leftDown] = &Tile{}
|
||||||
|
tiles[rightDown] = &Tile{}
|
||||||
|
_, ok := tiles.CanMove(center, DirectionDownLeft)
|
||||||
|
assert.True(t, ok)
|
||||||
|
_, ok = tiles.CanMove(center, DirectionDownLeft)
|
||||||
|
assert.True(t, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTilesTraversableWhenGoingUpToInversedTile(t *testing.T) {
|
||||||
|
tiles := Tiles{}
|
||||||
|
center := geom.Pt(1, 1)
|
||||||
|
tiles[center] = &Tile{}
|
||||||
|
leftUp := AdjacentPosition(center, DirectionUpLeft)
|
||||||
|
rightUp := AdjacentPosition(center, DirectionUpRight)
|
||||||
|
tiles[leftUp] = &Tile{Inversed: true}
|
||||||
|
tiles[rightUp] = &Tile{Inversed: true}
|
||||||
|
_, ok := tiles.CanMove(center, DirectionUpLeft)
|
||||||
|
assert.True(t, ok)
|
||||||
|
_, ok = tiles.CanMove(center, DirectionUpLeft)
|
||||||
|
assert.True(t, ok)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user