aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md2
-rw-r--r--ecs/component.go10
-rwxr-xr-xentitybin0 -> 10923288 bytes
-rw-r--r--entity.go38
-rw-r--r--gather_order.go19
-rw-r--r--image.go5
-rw-r--r--main.go84
-rw-r--r--patrol_order.go16
-rw-r--r--physics/geometry.go (renamed from geometry.go)4
-rw-r--r--physics/move.go (renamed from move.go)81
-rw-r--r--selection.go8
11 files changed, 141 insertions, 126 deletions
diff --git a/README.md b/README.md
index ad0db3f..84d4fc9 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,8 @@
## TODO
+- atm, stops moving if someone settles at destination
+
```go
func (g \*Game) updatePlacingWall(cursor Point) {
if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonLeft) {
diff --git a/ecs/component.go b/ecs/component.go
new file mode 100644
index 0000000..29ef967
--- /dev/null
+++ b/ecs/component.go
@@ -0,0 +1,10 @@
+package ecs
+
+type Component[T any] struct {
+ IsEnabled bool
+ Value T
+}
+
+func C[T any](t T) Component[T] {
+ return Component[T]{IsEnabled: true, Value: t}
+}
diff --git a/entity b/entity
new file mode 100755
index 0000000..eae6e8d
--- /dev/null
+++ b/entity
Binary files differ
diff --git a/entity.go b/entity.go
index 838bd71..8d6dfb9 100644
--- a/entity.go
+++ b/entity.go
@@ -1,31 +1,27 @@
package main
-import "github.com/hajimehoshi/ebiten/v2"
+import (
+ "age_of_empires/ecs"
+ "age_of_empires/physics"
-type Component[T any] struct {
- IsEnabled bool
- Value T
-}
-
-func C[T any](t T) Component[T] {
- return Component[T]{IsEnabled: true, Value: t}
-}
+ "github.com/hajimehoshi/ebiten/v2"
+)
type Entity struct {
- Position Component[Point]
- Image Component[*ebiten.Image]
- Selection Component[Selection]
- Move Component[Move]
- Order Component[Order]
- ResourceGatherer Component[ResourceGatherer]
- ResourceSource Component[ResourceSource]
- ResourceStorage Component[ResourceStorage]
+ Position ecs.Component[physics.Point]
+ Image ecs.Component[*ebiten.Image]
+ Selection ecs.Component[Selection]
+ Move ecs.Component[physics.Move]
+ Order ecs.Component[Order]
+ ResourceGatherer ecs.Component[ResourceGatherer]
+ ResourceSource ecs.Component[ResourceSource]
+ ResourceStorage ecs.Component[ResourceStorage]
}
-func (e Entity) Bounds() Rectangle {
- return Rectangle{
- e.Position.Value,
- e.Position.Value.Add(e.Image.Value.Bounds().Size()),
+func (e Entity) Bounds() physics.Rectangle {
+ return physics.Rectangle{
+ Min: e.Position.Value,
+ Max: e.Position.Value.Add(e.Image.Value.Bounds().Size()),
}
}
diff --git a/gather_order.go b/gather_order.go
index b4f3c9e..2f5a060 100644
--- a/gather_order.go
+++ b/gather_order.go
@@ -1,6 +1,7 @@
package main
import (
+ "age_of_empires/physics"
"math"
"time"
@@ -22,7 +23,7 @@ func (o *GatherOrder) Update(e *Entity, g *Game) {
o.updateGathererFull(e, g, gatherer)
return
}
- if Distance(e.Position.Value, gatherer.CurrentTarget.Position.Value) <= 100 {
+ if physics.Distance(e.Position.Value, gatherer.CurrentTarget.Position.Value) <= 100 {
o.updateGathering(gatherer, source)
return
}
@@ -56,7 +57,7 @@ func (o *GatherOrder) updateGathererFull(e *Entity, g *Game, gatherer *ResourceG
slog.Info("storage target", slog.String("storage", destination.String()), slog.Int("distance", distance))
if distance > 1 {
slog.Info("moving to storage", slog.String("storage", destination.String()))
- e.StartMove(destination, g.getMoveMap())
+ physics.StartMove(&e.Move,e.Position, destination, g.getMoveMap())
return
}
gatherer.CurrentVolume = 0
@@ -65,21 +66,21 @@ func (o *GatherOrder) updateGathererFull(e *Entity, g *Game, gatherer *ResourceG
}
func (o *GatherOrder) startMoveToSource(e *Entity, gatherer *ResourceGatherer, g *Game) {
- sourceDockings := AdjacentPoints(gatherer.CurrentTarget.Position.Value)
+ sourceDockings := physics.AdjacentPoints(gatherer.CurrentTarget.Position.Value)
destination, distance := g.Closest(e.Position.Value, sourceDockings)
if distance == math.MaxInt {
slog.Info("no accessible storage, canceling gather order")
e.Order.Value = nil
return
}
- e.StartMove(destination, g.getMoveMap())
+ physics.StartMove(&e.Move,e.Position, destination, g.getMoveMap())
}
-func getAllStorageDockings(g *Game) []Point {
- storageDockings := []Point{}
+func getAllStorageDockings(g *Game) []physics.Point {
+ storageDockings := []physics.Point{}
for _, entity := range g.Entities {
if entity.Position.IsEnabled && entity.ResourceStorage.IsEnabled {
- storageDockings = append(storageDockings, AdjacentPoints(entity.Position.Value)...)
+ storageDockings = append(storageDockings, physics.AdjacentPoints(entity.Position.Value)...)
}
}
return storageDockings
@@ -91,14 +92,14 @@ func Gather(e *Entity, source *Entity, g *Game) Order {
slog.Info("gathering from", slog.String("source", source.Position.Value.String()))
e.Order.Value = &GatherOrder{source: source}
e.ResourceGatherer.Value.CurrentTarget = source
- sourceDockings := AdjacentPoints(source.Position.Value)
+ sourceDockings := physics.AdjacentPoints(source.Position.Value)
destination, distance := g.Closest(e.Position.Value, sourceDockings)
if distance == math.MaxInt {
slog.Info("no accessible storage, canceling gather order")
e.Order.Value = nil
return nil
}
- e.StartMove(destination, g.getMoveMap())
+ physics.StartMove(&e.Move,e.Position, destination, g.getMoveMap())
}
}
return &GatherOrder{source: source}
diff --git a/image.go b/image.go
index 5045d90..50f1a19 100644
--- a/image.go
+++ b/image.go
@@ -1,19 +1,20 @@
package main
import (
+ "age_of_empires/physics"
"image/color"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/vector"
)
-func NewFilledRectangleImage(size Point, color color.Color) *ebiten.Image {
+func NewFilledRectangleImage(size physics.Point, color color.Color) *ebiten.Image {
image := ebiten.NewImage(size.X, size.Y)
image.Fill(color)
return image
}
-func NewStrokeRectangleImage(size Point, strokeWidth int, color color.Color) *ebiten.Image {
+func NewStrokeRectangleImage(size physics.Point, strokeWidth float32, color color.Color) *ebiten.Image {
image := ebiten.NewImage(size.X, size.Y)
vector.StrokeRect(image, 0, 0, float32(size.X), float32(size.Y), float32(strokeWidth), color, true)
return image
diff --git a/main.go b/main.go
index 8ed3133..5eb330e 100644
--- a/main.go
+++ b/main.go
@@ -1,6 +1,8 @@
package main
import (
+ "age_of_empires/ecs"
+ "age_of_empires/physics"
"image"
"image/color"
_ "image/jpeg"
@@ -24,7 +26,7 @@ var soilColor = color.RGBA{0x60, 0x40, 0x20, 0xff}
type GlobalSelection struct {
IsActive bool
- Start Point
+ Start physics.Point
}
type Game struct {
@@ -33,22 +35,36 @@ type Game struct {
Selection GlobalSelection
}
-func (g *Game) getMoveMap() MoveMap {
- blocked := map[Point]bool{}
+func DrawMove(screen *ebiten.Image, e *Entity) {
+ if e.Move.IsEnabled && e.Position.IsEnabled && e.Selection.IsEnabled {
+ if e.Selection.Value.IsSelected && e.Move.Value.IsActive {
+ last := e.Position.Value
+ dx := +e.Image.Value.Bounds().Dx() / 2
+ dy := +e.Image.Value.Bounds().Dy() / 2
+ for _, point := range e.Move.Value.Path {
+ vector.StrokeLine(screen, float32(last.X+dx), float32(last.Y+dy), float32(point.X+dx), float32(point.Y+dy), 10.0, color.RGBA{256 * 3 / 16, 256 * 3 / 16, 256 * 3 / 16, 256 / 4}, true)
+ last = point
+ }
+ }
+ }
+}
+
+func (g *Game) getMoveMap() physics.MoveMap {
+ blocked := map[physics.Point]bool{}
for _, e := range g.Entities {
if e.Position.IsEnabled {
blocked[e.Position.Value] = true
}
}
- return MoveMap{Width: 3200, Height: 2400, Blocked: blocked}
+ return physics.MoveMap{Width: 3200, Height: 2400, Blocked: blocked}
}
-func (g *Game) Closest(from Point, candidates []Point) (Point, int) {
- var closest Point
+func (g *Game) Closest(from physics.Point, candidates []physics.Point) (physics.Point, int) {
+ var closest physics.Point
closestDistance := math.MaxInt
moveMap := g.getMoveMap()
for _, dest := range candidates {
- path, ok := SearchPath(from, dest, moveMap)
+ path, ok := physics.SearchPath(from, dest, moveMap)
if !ok {
continue
}
@@ -60,7 +76,7 @@ func (g *Game) Closest(from Point, candidates []Point) (Point, int) {
return closest, closestDistance
}
-func (g *Game) updateSelecting(cursor Point, moveMap MoveMap) {
+func (g *Game) updateSelecting(cursor physics.Point, moveMap physics.MoveMap) {
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
g.Selection.Start = cursor
g.Selection.IsActive = true
@@ -80,13 +96,13 @@ func (g *Game) updateSelecting(cursor Point, moveMap MoveMap) {
if entityAtDestination != nil && entityAtDestination.ResourceSource.IsEnabled {
Gather(e, entityAtDestination, g)
} else {
- e.StartMove(destination, moveMap)
+ physics.StartMove(&e.Move,e.Position, destination, moveMap)
}
}
}
}
if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonLeft) {
- if g.Selection.IsActive && Distance(g.Selection.Start, cursor) > 10 {
+ if g.Selection.IsActive && physics.Distance(g.Selection.Start, cursor) > 10 {
for _, e := range g.Entities {
e.SelectMultiple(cursor, g.Selection)
}
@@ -102,7 +118,7 @@ func (g *Game) updateSelecting(cursor Point, moveMap MoveMap) {
}
}
-func (g *Game) updatePatrolling(cursor Point) {
+func (g *Game) updatePatrolling(cursor physics.Point) {
if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonRight) {
destination := cursor.Div(100).Mul(100)
for _, e := range g.Entities {
@@ -114,7 +130,7 @@ func (g *Game) updatePatrolling(cursor Point) {
func (g *Game) Update() error {
x, y := ebiten.CursorPosition()
- cursor := Point{x, y}
+ cursor := physics.Point{X: x, Y: y}
if inpututil.IsKeyJustReleased(ebiten.KeyEscape) {
g.CurrentAction = Selecting
slog.Info("selecting action")
@@ -131,7 +147,7 @@ func (g *Game) Update() error {
g.updatePatrolling(cursor)
}
for _, e := range g.Entities {
- e.UpdateMove(moveMap)
+ physics.UpdateMove(&e.Move,&e.Position, moveMap)
e.UpdateOrder(g)
}
return nil
@@ -139,7 +155,7 @@ func (g *Game) Update() error {
func (g *Game) Draw(screen *ebiten.Image) {
x, y := ebiten.CursorPosition()
- cursor := Point{x, y}
+ cursor := physics.Point{X: x, Y: y}
screen.Fill(soilColor)
for _, e := range g.Entities {
Draw(screen, e)
@@ -175,39 +191,39 @@ func main() {
}
ebiten.SetWindowIcon([]image.Image{iconImg})
game := &Game{}
- ironImage := NewFilledRectangleImage(Point{100, 100}, color.RGBA{0x80, 0x80, 0x80, 0xff})
+ ironImage := NewFilledRectangleImage(physics.Point{X: 100, Y: 100}, color.RGBA{0x80, 0x80, 0x80, 0xff})
ironMine := Entity{
- Position: C(Point{1000, 1000}),
- Image: C(ironImage),
- ResourceSource: C(ResourceSource{Remaining: 1000}),
+ Position: ecs.C(physics.Point{X: 1000, Y: 1000}),
+ Image: ecs.C(ironImage),
+ ResourceSource: ecs.C(ResourceSource{Remaining: 1000}),
}
game.Entities = append(game.Entities, &ironMine)
- storageImage := NewFilledRectangleImage(Point{100, 100}, color.RGBA{0x00, 0x00, 0xff, 0xff})
+ storageImage := NewFilledRectangleImage(physics.Point{X: 100, Y: 100}, color.RGBA{0x00, 0x00, 0xff, 0xff})
storage := Entity{
- Position: C(Point{1000, 2000}),
- Image: C(storageImage),
- ResourceStorage: C(ResourceStorage{}),
+ Position: ecs.C(physics.Point{X: 1000, Y: 2000}),
+ Image: ecs.C(storageImage),
+ ResourceStorage: ecs.C(ResourceStorage{}),
}
game.Entities = append(game.Entities, &storage)
var order Order
personImage := NewFilledCircleImage(100, color.RGBA{0xff, 0xff, 0xff, 0xff})
personSelectionHalo := NewStrokeCircleImage(110, SELECTION_HALO_WIDTH, color.RGBA{0xff, 0x00, 0x00, 0xff})
person1 := Entity{
- Position: C(Point{2000, 2000}),
- Image: C(personImage),
- Selection: C(Selection{IsSelected: false, Halo: personSelectionHalo}),
- Move: C(Move{IsActive: false}),
- Order: C(order),
- ResourceGatherer: C(ResourceGatherer{MaxCapacity: 15}),
+ Position: ecs.C(physics.Point{X: 2000, Y: 2000}),
+ Image: ecs.C(personImage),
+ Selection: ecs.C(Selection{IsSelected: false, Halo: personSelectionHalo}),
+ Move: ecs.C(physics.Move{IsActive: false}),
+ Order: ecs.C(order),
+ ResourceGatherer: ecs.C(ResourceGatherer{MaxCapacity: 15}),
}
game.Entities = append(game.Entities, &person1)
person2 := Entity{
- Position: C(Point{2200, 2200}),
- Image: C(personImage),
- Selection: C(Selection{IsSelected: false, Halo: personSelectionHalo}),
- Move: C(Move{IsActive: false}),
- Order: C(order),
- ResourceGatherer: C(ResourceGatherer{MaxCapacity: 15}),
+ Position: ecs.C(physics.Point{X: 2200, Y: 2200}),
+ Image: ecs.C(personImage),
+ Selection: ecs.C(Selection{IsSelected: false, Halo: personSelectionHalo}),
+ Move: ecs.C(physics.Move{IsActive: false}),
+ Order: ecs.C(order),
+ ResourceGatherer: ecs.C(ResourceGatherer{MaxCapacity: 15}),
}
game.Entities = append(game.Entities, &person2)
game.CurrentAction = Selecting
diff --git a/patrol_order.go b/patrol_order.go
index b7201ed..eb9dfb0 100644
--- a/patrol_order.go
+++ b/patrol_order.go
@@ -1,10 +1,14 @@
package main
-import "golang.org/x/exp/slog"
+import (
+ "age_of_empires/physics"
+
+ "golang.org/x/exp/slog"
+)
type PatrolOrder struct {
- origin Point
- destination Point
+ origin physics.Point
+ destination physics.Point
}
func (o *PatrolOrder) Update(e *Entity, g *Game) {
@@ -12,15 +16,15 @@ func (o *PatrolOrder) Update(e *Entity, g *Game) {
if !e.Move.Value.IsActive {
moveMap := g.getMoveMap()
if e.Position.Value == o.destination {
- e.StartMove(o.origin, moveMap)
+ physics.StartMove(&e.Move, e.Position, o.origin, moveMap)
} else {
- e.StartMove(o.destination, moveMap)
+ physics.StartMove(&e.Move, e.Position, o.destination, moveMap)
}
}
}
}
-func Patrol(e *Entity, destination Point) Order {
+func Patrol(e *Entity, destination physics.Point) Order {
if e.Selection.IsEnabled && e.Position.IsEnabled && e.Move.IsEnabled && e.Order.IsEnabled {
if e.Selection.Value.IsSelected {
origin := e.Position.Value
diff --git a/geometry.go b/physics/geometry.go
index ba3f643..1902fdf 100644
--- a/geometry.go
+++ b/physics/geometry.go
@@ -1,4 +1,4 @@
-package main
+package physics
import (
"image"
@@ -13,7 +13,7 @@ func Distance(p1, p2 Point) float64 {
}
func Length(p Point) float64 {
- return Distance(p, Point{0, 0})
+ return Distance(p, Point{X:0,Y:0})
}
func Normalize(p Point) Point {
diff --git a/move.go b/physics/move.go
index 1322a51..b6f13d7 100644
--- a/move.go
+++ b/physics/move.go
@@ -1,12 +1,10 @@
-package main
+package physics
import (
+ "age_of_empires/ecs"
"fmt"
- "image/color"
"math"
- "github.com/hajimehoshi/ebiten/v2"
- "github.com/hajimehoshi/ebiten/v2/vector"
"golang.org/x/exp/slices"
"golang.org/x/exp/slog"
)
@@ -63,16 +61,16 @@ func SearchPath(origin Point, destination Point, moveMap MoveMap) (Path, bool) {
func (s *PathSearch) addNeighbors(point Point, node Node) {
if point.X >= 100 {
- s.consider(Point{point.X - 100, point.Y}, point)
+ s.consider(Point{X: point.X - 100, Y: point.Y}, point)
}
if point.X <= s.moveMap.Width-100 {
- s.consider(Point{point.X + 100, point.Y}, point)
+ s.consider(Point{X: point.X + 100, Y: point.Y}, point)
}
if point.Y >= 100 {
- s.consider(Point{point.X, point.Y - 100}, point)
+ s.consider(Point{X: point.X, Y: point.Y - 100}, point)
}
if point.Y <= s.moveMap.Height-100 {
- s.consider(Point{point.X, point.Y + 100}, point)
+ s.consider(Point{X: point.X, Y: point.Y + 100}, point)
}
}
@@ -95,7 +93,7 @@ func (s *PathSearch) consider(neighbor Point, point Point) {
// openList must not be empty
func (s *PathSearch) bestOpenNode() (Point, Node) {
- bestPoint := Point{}
+ bestPoint := Point{X: 0, Y: 0}
bestNode := Node{}
bestCost := math.MaxInt32
for point, node := range s.openList {
@@ -139,8 +137,8 @@ func (s *PathSearch) search() (Path, bool) {
func NewMove(origin Point, destination Point, moveMap MoveMap) Move {
current := Point{
- origin.X / 100 * 100,
- origin.Y / 100 * 100,
+ X: origin.X / 100 * 100,
+ Y: origin.Y / 100 * 100,
}
path, ok := SearchPath(current, destination, moveMap)
if !ok {
@@ -159,62 +157,47 @@ func (p *Path) isValid(moveMap MoveMap) bool {
return true
}
-func (e *Entity) StartMove(destination Point, moveMap MoveMap) {
- if e.Move.IsEnabled && e.Position.IsEnabled && e.Selection.IsEnabled {
- e.Move.Value = NewMove(e.Position.Value, destination, moveMap)
+func StartMove(move *ecs.Component[Move], position ecs.Component[Point], destination Point, moveMap MoveMap) {
+ if move.IsEnabled && position.IsEnabled {
+ move.Value = NewMove(position.Value, destination, moveMap)
slog.Info("entity starting move", slog.String("destination", destination.String()))
}
}
-func (e *Entity) moveToward(destination Point) {
- remainingMove := destination.Sub(e.Position.Value)
+func moveToward(position *ecs.Component[Point], destination Point) {
+ remainingMove := destination.Sub(position.Value)
delta := remainingMove.Mul(MOVE_SPEED).Div(int(Length(remainingMove)))
- e.Position.Value = e.Position.Value.Add(delta)
+ position.Value = position.Value.Add(delta)
}
-func (e *Entity) UpdateMove(moveMap MoveMap) {
- if !e.Move.IsEnabled || !e.Position.IsEnabled || !e.Move.Value.IsActive {
+func UpdateMove(move *ecs.Component[Move], position *ecs.Component[Point], moveMap MoveMap) {
+ if !move.IsEnabled || !position.IsEnabled || !move.Value.IsActive {
return
}
- position := e.Position.Value
- if len(e.Move.Value.Path) == 0 {
- e.Move.Value.IsActive = false
+ if len(move.Value.Path) == 0 {
+ move.Value.IsActive = false
return
}
- next := e.Move.Value.Path[0]
- if Distance(position, next) < MOVE_SPEED {
- e.Position.Value = next
- remainingPath := e.Move.Value.Path[1:]
- if next == e.Move.Value.Destination {
- e.Move.Value.IsActive = false
- slog.Info("move finished", slog.String("destination", e.Move.Value.Destination.String()))
+ next := move.Value.Path[0]
+ if Distance(position.Value, next) < MOVE_SPEED {
+ position.Value = next
+ remainingPath := move.Value.Path[1:]
+ if next == move.Value.Destination {
+ move.Value.IsActive = false
+ slog.Info("move finished", slog.String("destination", move.Value.Destination.String()))
} else if remainingPath.isValid(moveMap) {
- e.Move.Value.Path = remainingPath
+ move.Value.Path = remainingPath
} else {
// Here maybe the destination has to change
- path, ok := SearchPath(next, e.Move.Value.Destination, moveMap)
+ path, ok := SearchPath(next, move.Value.Destination, moveMap)
if !ok {
- slog.Info("no path found", slog.String("destination", e.Move.Value.Destination.String()))
- e.Move.Value.IsActive = false
+ slog.Info("no path found", slog.String("destination", move.Value.Destination.String()))
+ move.Value.IsActive = false
} else {
- e.Move.Value.Path = path[1:]
+ move.Value.Path = path[1:]
}
}
} else {
- e.moveToward(next)
- }
-}
-
-func DrawMove(screen *ebiten.Image, e *Entity) {
- if e.Move.IsEnabled && e.Position.IsEnabled && e.Selection.IsEnabled {
- if e.Selection.Value.IsSelected && e.Move.Value.IsActive {
- last := e.Position.Value
- dx := +e.Image.Value.Bounds().Dx() / 2
- dy := +e.Image.Value.Bounds().Dy() / 2
- for _, point := range e.Move.Value.Path {
- vector.StrokeLine(screen, float32(last.X+dx), float32(last.Y+dy), float32(point.X+dx), float32(point.Y+dy), 10.0, color.RGBA{256 * 3 / 16, 256 * 3 / 16, 256 * 3 / 16, 256 / 4}, true)
- last = point
- }
- }
+ moveToward(position, next)
}
}
diff --git a/selection.go b/selection.go
index e41fce3..91f35fe 100644
--- a/selection.go
+++ b/selection.go
@@ -1,6 +1,8 @@
package main
import (
+ "age_of_empires/physics"
+
"github.com/hajimehoshi/ebiten/v2"
"golang.org/x/exp/slog"
)
@@ -12,10 +14,10 @@ type Selection struct {
Halo *ebiten.Image
}
-func (e *Entity) SelectMultiple(cursor Point, selection GlobalSelection) {
+func (e *Entity) SelectMultiple(cursor physics.Point, selection GlobalSelection) {
if e.Selection.IsEnabled {
e.Selection.Value.IsSelected = false
- selectionBounds := Rectangle{selection.Start, cursor}.Canon()
+ selectionBounds := physics.Rectangle{Min:selection.Start, Max:cursor}.Canon()
if selectionBounds.Overlaps(e.Bounds()) {
e.Selection.Value.IsSelected = true
slog.Info("entity selected", slog.String("position", e.Position.Value.String()))
@@ -23,7 +25,7 @@ func (e *Entity) SelectMultiple(cursor Point, selection GlobalSelection) {
}
}
-func (e *Entity) SelectSingle(cursor Point, canBeSelected bool) bool {
+func (e *Entity) SelectSingle(cursor physics.Point, canBeSelected bool) bool {
if e.Selection.IsEnabled {
e.Selection.Value.IsSelected = false
if canBeSelected && cursor.In(e.Bounds()) {