3 Commits
Author SHA1 Message Date
sanspie c24a4791fa Stage 10c2b: StateMachine and script calls, respawn persistence, gameplay editor tools (D332)
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci / checks (push) Failing after 4s
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 3s
ci / release (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
2026-10-22 10:53:54 +03:00
sanspie 12e9f5d0d0 Short component names prefer the project's own component over engine modules (D93) 2026-10-22 06:44:09 +03:00
sanspie 24284632d5 Stage 10c2a: health, hitboxes, zones, respawn, pickups, lifetime, templates (D313, D330) 2026-10-22 06:19:20 +03:00
79 changed files with 6785 additions and 120 deletions
+7 -4
View File
@@ -231,7 +231,7 @@ Decision IDs are stable references for reviews, migrations and ADR-style changes
|---|---|
| D91 | Aego Script is a statically typed, Go-like language compiled to register bytecode. Files are `*.aes`; a package is a directory under `scripts/` (overlaid into `res://`); package path = `project.Module + "/" + relative directory`. Host modules and script packages share one namespace (conflict → `AES4xxx`); `aego/` is reserved for stdlib; package name must equal its directory name. Module paths contain no dots. |
| D92 | No pointers, unsafe, goroutines, channels, defer, exceptions, closures, first-class functions, inheritance, interfaces, general generics, maps or operator overloading. Type arguments are parsed everywhere but resolved only for registered intrinsics (`node.Get<T>()`, `Has<T>()`). |
| D93 | `int` = int64, `float` = float32, `bool`, `string` (immutable, literals interned, `len` in bytes). Engine value types: `Vec2 Vec3 Color Quat Rect Transform2D Transform3D`, references `NodeRef AssetRef<T> ComponentRef<T>`. User types: `struct` (value), `enum`, `[]T` (heap). Vector values are constructed with composite literals only. A short component name in `Get<T>` resolves if unique, otherwise `AES2xxx` with candidates. |
| D93 | `int` = int64, `float` = float32, `bool`, `string` (immutable, literals interned, `len` in bytes). Engine value types: `Vec2 Vec3 Color Quat Rect Transform2D Transform3D`, references `NodeRef AssetRef<T> ComponentRef<T>`. User types: `struct` (value), `enum`, `[]T` (heap). Vector values are constructed with composite literals only. A short component name in `Get<T>`/`Has<T>` (and in `sdk.Connect`, `scene.WithComponent`, tween and mutation paths) resolves to the one project component with that name (owner module not `Builtin`), which hides engine components with the same name; with no project match it resolves to the one engine component; several project or several engine matches report AES2021 with candidates. Full names always resolve. `abi.Component.Builtin` carries the flag into the script ABI and its hash. |
| D94 | No mutable package globals; only `const`. State lives in script instances and components. |
| D95 | `script` is a stateful attachable type. One `ScriptComponent` (`engine.Script`) per node holds attachments, at most one per script type. Attachments are stored as `script "<FullName>" enabled=… { … }` blocks keyed by full type name and property name, never by runtime index; authoring values are a `schema.PropertyBag`; unknown properties and types are preserved. |
| D96 | Script types form a dynamic layer inside `schema.Registry`: the native layer is frozen by `Freeze()`, `SetDynamic` atomically replaces only the dynamic layer and rejects name/hash collisions with the native layer. `ByID/ByName/Field` see both layers; `Components()` returns native types, `Scripts()` dynamic ones. `scene.Types.Overlay` routes Get/SetProperty for script types into `ScriptComponent`; `scene` never imports `script`. |
@@ -557,8 +557,11 @@ The Stage 8 specification numbered these decisions D153, D155, D158, D159, D168
| ID | Decision |
|---|---|
| D328 | Shape casts. `physics/world` casts a circle, box, capsule or every non-trigger convex shape of a body along a translation (`CastCircle`, `CastBox`, `CastCapsule`, `CastBody`, which starts from the body's pose moved by an offset) with GJK distance between two polygon-with-radius cores and conservative advancement; a hit stops a quarter of linear slop before contact, reports the fraction of the move, the contact point on the obstacle and the obstacle's normal pointing back at the mover. A cast that starts in contact returns `Initial` with fraction 0 and the overlap depth; when the cores intersect, depth, normal and point come from the SAT manifold. `Penetration` lists the overlaps of a body's shapes with their depth and push-out normal. Casts skip triggers unless asked, skip the mover's own body, honour one-sided chains (a mover behind the chain is not blocked) and, with `SkipDynamic`, ignore dynamic bodies so a kinematic mover pushes them. `contracts/physics2d.Queries` exposes the casts by node together with `BodyPose`, `BodyVelocity`, `MoveKinematic`, `Teleport`, `ApplyImpulse` and `Gravity`; `QueryFilter.ExcludeBody` excludes the node's body and all its colliders. Casts allocate nothing. |
| D312 | Motion2D (`modules/gameplay`) is a cast-based kinematic controller over `contracts/physics2d`: each fixed step in `gameplay.motion` (PrePhysics, after `script.fixed`) it turns intents (`Move`, `MoveDir`, `Jump`, `JumpReleased`, `Dash`, `AddVelocity`, `Stop`, `Teleport`; the last Move stays, a Jump is one request) into a velocity, pushes the body out of overlaps (`Penetration`), slides along up to MaxSlides surfaces with `CastBody` from the moved pose, keeping SafeMargin, classifies each contact against UpDirection and MaxSlope as floor, wall or ceiling, climbs steps up to StepHeight, snaps to the floor by SnapToGround while grounded and moves the body with `MoveKinematic`, so physics carries it and pushes dynamic bodies (`PushBodies` casts through them). Grounded movement follows the floor tangent without gravity, so slopes below MaxSlope hold. Coyote time, jump buffer, jump cut, air control, dash and moving platforms (`BodyVelocity` of the ground body; PlatformOnLeave keep, drop or add) are part of the step. Signals `Landed(speed)`, `LeftGround`, `Jumped`, `HitWall(normal)`, `HitCeiling` are emitted in `gameplay.motion_post` (PostPhysics, before `script.events_fixed`). The controller state lives in an unexported field of the component, so rewind keys and pooled spawns carry it with the scene. GMP001: the body is not kinematic (logged once); GMP002: a platformer without gravity and Jump Speed. |
| D312 | Motion2D (`modules/gameplay`) is a cast-based kinematic controller over `contracts/physics2d`: each fixed step in `gameplay.motion` (PrePhysics, after `script.fixed`) it turns intents (`Move`, `MoveDir`, `Jump`, `JumpReleased`, `Dash`, `AddVelocity`, `Stop`, `Teleport`; the last Move stays, a Jump is one request) into a velocity, pushes the body out of overlaps (`Penetration`), slides along up to MaxSlides surfaces with `CastBody` from the moved pose, keeping SafeMargin, classifies each contact against UpDirection and MaxSlope as floor, wall or ceiling, climbs steps up to StepHeight, snaps to the floor by SnapToGround while grounded and moves the body with `MoveKinematic`, so physics carries it and pushes dynamic bodies (`PushBodies` casts through them). Grounded movement follows the floor tangent without gravity, so slopes below MaxSlope hold. Coyote time, jump buffer, jump cut, air control, dash and moving platforms (`BodyVelocity` of the ground body; PlatformOnLeave keep, drop or add) are part of the step. Signals `Landed(speed)`, `LeftGround`, `Jumped`, `HitWall(normal)`, `HitCeiling` are emitted in `gameplay.post` (PostPhysics, before `script.events_fixed`). The controller state lives in an unexported field of the component, so rewind keys and pooled spawns carry it with the scene. GMP001: the body is not kinematic (D330); GMP002: a platformer without gravity and Jump Speed. |
| D329 | Motion2D owns no Go map or per-step allocation: one service per play world walks the components with a stored callback, casts reuse the physics query buffers. |
| D313 | Gameplay components in `modules/gameplay`: Health (hit points, team, game-time invulnerability that starts after the step so hits of one step add up, `Died` once until `Revive`), Hurtbox2D and Hitbox2D (node-space areas tested exactly as boxes or circles, no physics colliders; policies onEnter, oncePerActivation, periodic; team and friendly fire; knockback is a velocity for a Motion 2D target and an impulse for a dynamic body and turns with the hitbox), Lifetime (game or unscaled seconds, then despawn into the pool or destroy), KillZone2D, Checkpoint2D (priority, once, spawn point node), Respawn (first point is the start position, delay, health and velocity reset) and Pickup2D (`Collected`, once removes the pickup). Zones react to a node's position entering the area. Signals of Health are queued and emitted by the module's systems: `gameplay.post` (PostPhysics: Motion2D events, hits, zones, respawns, health) and `gameplay.frame` (PostUpdate: health changed by scripts, lifetimes). Create → Gameplay has Platformer Character, Top-down Character, Enemy, Moving Platform, Checkpoint, Kill Zone and Pickup. |
| D330 | Health, hitbox, zone, respawn and lifetime state lives in unexported component fields like Motion2D's (D312), so rewind keys, pools and the module's state hash carry it. GMP001 is a validator that blocks Play when the body of a Motion 2D is not kinematic; adding Motion 2D makes an existing body kinematic. |
| D332 | StateMachine and script calls. `contracts/scriptcall.Caller` (view `script.call`, implemented by the scripting module over `script/runtime`) finds a plain method of a script on a node by name and parameter count — never a lifecycle method, an event handler or a coroutine — and calls it; a reference is tied to the instance and the program generation, so a hot reload makes it stale and the owner resolves it again by name. `gameplay.StateMachine` lists state names; hooks `Enter<State>()`, `Exit<State>()`, `Fixed<State>(dt)` and `Update<State>(dt)` are looked up on the node's scripts. `Go(state)` only queues: `gameplay.fsm_fixed` (PrePhysics, after `script.fixed`, before `gameplay.motion`) and `gameplay.fsm_update` (Update, after `script.update`) apply the queued state after each hook, run Exit of the old and Enter of the new state and emit `Changed(from, to)` with state indices; a Go inside Enter or Exit runs after that transition, at most eight transitions per call. The machine enters Initial in the first `fsm_update`, after the scripts' Ready. Time counts fixed steps. GMP005 warns about empty, duplicate or unknown states. Respawn points marked Persist are kept in the save section `gameplay.respawn`; Health.current is a persisted field. The editor plugin edits every gameplay area with eight handles, draws knockback, toggles Gameplay Areas and draws health bars in the Game View. |
## 4. Dependency rules
@@ -851,9 +854,9 @@ Services are typed registrations keyed by stable string IDs on the engine or on
FrameBegin world clock Begin (Real, Game, Fixed, FrameID), recorder stamp, drain OnMain, debug.expire, script.swap
PollEvents platform events, input.Apply, metrics
Input
FixedBegin × fixed steps (≤ maxFixedSteps; FixedTickID, recorder stamp; World.Break after each): PrePhysics (script.fixed) · Physics (app.fixed, physics.sync, physics.step, physics.writeback, physics.events) · PostPhysics (script.events_fixed, physics.debug)
FixedBegin × fixed steps (≤ maxFixedSteps; FixedTickID, recorder stamp; World.Break after each): PrePhysics (script.fixed, gameplay.fsm_fixed, gameplay.motion) · Physics (app.fixed, physics.sync, physics.step, physics.writeback, physics.events) · PostPhysics (gameplay.post, script.events_fixed, physics.debug)
FixedEnd
PreUpdate · Update (app.update, script.update, script.coroutines) · PostUpdate (tween.game, tween.unscaled)
PreUpdate · Update (app.update, script.update, gameplay.fsm_update, script.coroutines) · PostUpdate (tween.game, tween.unscaled, camera.update, gameplay.frame)
Animation spawn.park (always)
RenderPrepare structural flush, physics.present, display.scale, engine.transform, render2d.cameras, render3d.cameras
Render render2d.extract, render3d.extract, engine.views_submit, debug.draw, app.render, passes
+1
View File
@@ -90,6 +90,7 @@ func (s *Session) lookupType(name string) (*schema.TypeDescriptor, error) {
found = append(found, td)
}
}
found = schema.PreferProject(found)
switch len(found) {
case 1:
return found[0], nil
+3
View File
@@ -8,6 +8,8 @@ import (
cameraeditor "aego/modules/camera/editor"
"aego/modules/debug"
debugeditor "aego/modules/debug/editor"
"aego/modules/gameplay"
gameplayeditor "aego/modules/gameplay/editor"
inputmod "aego/modules/input"
inputeditor "aego/modules/input/editor"
"aego/modules/physics"
@@ -25,6 +27,7 @@ func With(c assembly.Catalog) assembly.Catalog {
With(render3d.ID, render3deditor.Hook).
With(physics.ID, physicseditor.Hook).
With(camera.ID, cameraeditor.Hook).
With(gameplay.ID, gameplayeditor.Hook).
With(audio.ID, audioeditor.Hook).
With(debug.ID, debugeditor.Hook)
}
+22
View File
@@ -0,0 +1,22 @@
package scriptcall
import (
"aego/scene"
"aego/script/vm"
)
const ServiceID = "script.call"
// Ref names a plain method of one script instance; a new program generation makes it stale.
type Ref struct {
Node scene.NodeHandle
Instance vm.InstanceID
Index int32
Gen uint64
}
type Caller interface {
Method(node scene.NodeHandle, name string, params int) (Ref, bool)
Call(ref *Ref, args []vm.Value) bool
Generation() uint64
}
+11 -2
View File
@@ -17,7 +17,7 @@ Sources are `<id>.svg` files in `engine/res/icons/src` and `modules/<id>/res/ico
| `cat.audio` | engine | 16 | Documentation: Audio group and module | ok |
| `cat.camera` | engine | 24 | New Project: template 2.5d | ok |
| `cat.game` | engine | 16 | Inspector and Add Component: components of a project module without an icon of their own | ok |
| `cat.gameplay` | engine | 16 | — | unused |
| `cat.gameplay` | engine | 16 | template menu Gameplay | ok |
| `cat.node` | engine | 24 | New Project: template empty; Search: node results | ok |
| `cat.physics` | engine | 16 | template menu Physics | ok |
| `cat.rendering` | engine | 24 | New Project: template 2d; Search: mesh assets | ok |
@@ -76,7 +76,16 @@ Sources are `<id>.svg` files in `engine/res/icons/src` and `modules/<id>/res/ico
| `file.table` | engine | 24 | Assets: table tiles | ok |
| `file.texture` | engine | 24 | Assets: texture tiles | ok |
| `file.unknown` | engine | 24 | Assets: tiles of other kinds | ok |
| `gameplay.motion` | engine | 16 | component gameplay.Motion2D | ok |
| `gameplay.checkpoint` | engine | 16 | component gameplay.Checkpoint2D; template gameplay.checkpoint | ok |
| `gameplay.fsm` | engine | 16 | component gameplay.StateMachine | ok |
| `gameplay.health` | engine | 16 | component gameplay.Health | ok |
| `gameplay.hitbox` | engine | 16 | component gameplay.Hitbox2D; template gameplay.enemy | ok |
| `gameplay.hurtbox` | engine | 16 | component gameplay.Hurtbox2D | ok |
| `gameplay.killzone` | engine | 16 | component gameplay.KillZone2D; template gameplay.killzone | ok |
| `gameplay.lifetime` | engine | 16 | component gameplay.Lifetime | ok |
| `gameplay.motion` | engine | 16 | component gameplay.Motion2D; template gameplay.platform; template gameplay.platformer; template gameplay.topdown | ok |
| `gameplay.pickup` | engine | 16 | component gameplay.Pickup2D; template gameplay.pickup | ok |
| `gameplay.respawn` | engine | 16 | component gameplay.Respawn | ok |
| `help.about` | engine | 16 | Help → About Aego | ok |
| `help.book` | engine | 16 | Documentation tab | ok |
| `log.error` | engine | 16 | Console: errors | ok |
File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

After

Width:  |  Height:  |  Size: 193 KiB

+74
View File
@@ -0,0 +1,74 @@
package gameplay
import "aego/core/mathx"
type area struct {
c mathx.Vec2
ax, ay mathx.Vec2
r float32
circle bool
}
func areaOf(m mathx.Mat3, r mathx.Rect, shape Shape) area {
c := m.Apply(mathx.Vec2{X: r.X + r.W/2, Y: r.Y + r.H/2})
ax := mathx.Vec2{X: m[0], Y: m[1]}.Scale(r.W / 2)
ay := mathx.Vec2{X: m[3], Y: m[4]}.Scale(r.H / 2)
if shape == ShapeCircle {
return area{c: c, r: min(ax.Len(), ay.Len()), circle: true}
}
return area{c: c, ax: ax, ay: ay}
}
func (a *area) project(n mathx.Vec2) float32 {
if a.circle {
return a.r
}
return mathx.Abs(a.ax.Dot(n)) + mathx.Abs(a.ay.Dot(n))
}
func (a *area) closest(p mathx.Vec2) mathx.Vec2 {
d := p.Sub(a.c)
out := a.c
for _, ax := range [2]mathx.Vec2{a.ax, a.ay} {
if l2 := ax.LenSq(); l2 > 0 {
out = out.Add(ax.Scale(mathx.Clamp(d.Dot(ax)/l2, -1, 1)))
}
}
return out
}
func (a *area) contains(p mathx.Vec2) bool {
if a.circle {
return p.Sub(a.c).LenSq() <= a.r*a.r
}
d := p.Sub(a.c)
for _, ax := range [2]mathx.Vec2{a.ax, a.ay} {
if l2 := ax.LenSq(); l2 == 0 || mathx.Abs(d.Dot(ax)) > l2 {
return false
}
}
return true
}
func overlap(a, b *area) bool {
switch {
case a.circle && b.circle:
r := a.r + b.r
return a.c.Sub(b.c).LenSq() <= r*r
case a.circle:
return b.closest(a.c).Sub(a.c).LenSq() <= a.r*a.r
case b.circle:
return a.closest(b.c).Sub(b.c).LenSq() <= b.r*b.r
}
d := b.c.Sub(a.c)
for _, ax := range [4]mathx.Vec2{a.ax, a.ay, b.ax, b.ay} {
n, l := normalize(ax)
if l == 0 {
continue
}
if mathx.Abs(d.Dot(n)) > a.project(n)+b.project(n) {
return false
}
}
return true
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+63
View File
@@ -0,0 +1,63 @@
package editor
import (
"aego/core/ids"
"aego/core/mathx"
"aego/editor/core/context"
"aego/modules/gameplay"
"aego/scene"
"aego/ui/widgets"
)
func one[T any](ctx *context.Editor, nodes []ids.NodeID) (*T, bool) {
d := ctx.Doc()
if d == nil || len(nodes) != 1 {
return nil, false
}
return scene.Get[T](d.Scene(), d.Scene().Handle(nodes[0]))
}
func flag(on bool) string {
if on {
return "●"
}
return "○"
}
func (p *plugin) motionInspector(ctx *context.Editor, nodes []ids.NodeID, def func()) {
def()
m, ok := one[gameplay.Motion2D](ctx, nodes)
if !ok || (m.Velocity == (mathx.Vec2{}) && !m.Grounded && !m.OnWall && !m.OnCeiling) {
return
}
widgets.Labelf(ctx.UI, "Grounded %s Wall %s Ceiling %s", flag(m.Grounded), flag(m.OnWall), flag(m.OnCeiling))
widgets.Labelf(ctx.UI, "Speed %.2f Floor %.0f°", m.Velocity.Len(), m.FloorAngle)
}
func (p *plugin) healthInspector(ctx *context.Editor, nodes []ids.NodeID, def func()) {
def()
hp, ok := one[gameplay.Health](ctx, nodes)
if !ok || hp.Max <= 0 {
return
}
ui := ctx.UI
th := &ui.Theme
cur := hp.Current
if cur == 0 && !hp.Dead {
cur = hp.Max
}
bar := ui.Cursor().Next(6)
ui.RoundRect(bar, 3, th.Widget)
k := mathx.Clamp(float32(cur)/float32(hp.Max), 0, 1)
ui.RoundRect(mathx.Rect{X: bar.X, Y: bar.Y, W: bar.W * k, H: bar.H}, 3, colHealth)
widgets.Labelf(ui, "%d / %d", cur, hp.Max)
}
func (p *plugin) fsmInspector(ctx *context.Editor, nodes []ids.NodeID, def func()) {
def()
m, ok := one[gameplay.StateMachine](ctx, nodes)
if !ok || m.Current == "" {
return
}
widgets.Labelf(ctx.UI, "State %s for %.1f s, before %s", m.Current, m.Time, m.Previous)
}
+65
View File
@@ -0,0 +1,65 @@
package editor
import (
"aego/core/mathx"
"aego/editor/core/shape"
"aego/modules/gameplay"
"aego/render/debug"
"aego/scene"
)
func outline(d *debug.Draw, m mathx.Mat3, r mathx.Rect, col mathx.Color) {
p := [4]mathx.Vec2{{X: r.X, Y: r.Y}, {X: r.X + r.W, Y: r.Y}, {X: r.X + r.W, Y: r.Y + r.H}, {X: r.X, Y: r.Y + r.H}}
for i := range p {
d.Line(m.Apply(p[i]), m.Apply(p[(i+1)%4]), col)
}
}
func eachArea[T any](d *debug.Draw, s *scene.Scene, col mathx.Color, get func(*T) (mathx.Rect, bool)) {
scene.Query1[T](s).Each(func(h scene.NodeHandle, c *T) {
if r, ok := get(c); ok && s.EffectiveEnabled(h) {
outline(d, s.World(h), r, col)
}
})
}
func zones(d *debug.Draw, s *scene.Scene, dim bool) {
pick := func(c mathx.Color) mathx.Color {
if dim {
return colDim
}
return c
}
eachArea(d, s, pick(colKill), func(c *gameplay.KillZone2D) (mathx.Rect, bool) { return c.Area, true })
eachArea(d, s, pick(colCheck), func(c *gameplay.Checkpoint2D) (mathx.Rect, bool) { return c.Area, true })
eachArea(d, s, pick(colPick), func(c *gameplay.Pickup2D) (mathx.Rect, bool) { return c.Area, true })
}
func (p *plugin) sceneOverlay(c *shape.OverlayCtx) {
if c.Mode3D || c.Game || c.Debug == nil || c.Scene == nil || !p.areas() {
return
}
zones(c.Debug, c.Scene, true)
eachArea(c.Debug, c.Scene, colDim, func(h *gameplay.Hitbox2D) (mathx.Rect, bool) { return h.Area, true })
eachArea(c.Debug, c.Scene, colDim, func(h *gameplay.Hurtbox2D) (mathx.Rect, bool) { return h.Area, true })
}
func (p *plugin) gameOverlay(c *shape.OverlayCtx) {
if c.Mode3D || c.Debug == nil || c.Scene == nil || !p.areas() {
return
}
s, d := c.Scene, c.Debug
zones(d, s, false)
eachArea(d, s, colHurt, func(h *gameplay.Hurtbox2D) (mathx.Rect, bool) { return h.Area, h.Active })
eachArea(d, s, colHit, func(h *gameplay.Hitbox2D) (mathx.Rect, bool) { return h.Area, h.Active })
scene.Query1[gameplay.Health](s).Each(func(h scene.NodeHandle, hp *gameplay.Health) {
if !s.EffectiveEnabled(h) || hp.Max <= 0 {
return
}
at := s.WorldPosition(h).Add(mathx.Vec2{Y: -0.9})
w := float32(0.8)
k := mathx.Clamp(float32(hp.Current)/float32(hp.Max), 0, 1)
d.Line(at.Sub(mathx.Vec2{X: w / 2}), at.Add(mathx.Vec2{X: w / 2}), colHurtBar)
d.Line(at.Sub(mathx.Vec2{X: w / 2}), at.Add(mathx.Vec2{X: w*k - w/2}), colHealth)
})
}
+98
View File
@@ -0,0 +1,98 @@
package editor
import (
"errors"
"aego/core/mathx"
"aego/editor/core/command"
"aego/editor/core/context"
"aego/editor/ext"
"aego/editor/ext/inspect"
"aego/modules/gameplay"
"aego/scene"
)
const (
CmdAreas = gameplay.ID + ".areas"
partBase = 60
minSize = float32(0.1)
)
var (
colHit = mathx.Color{R: 0.95, G: 0.3, B: 0.3, A: 0.9}
colHurt = mathx.Color{R: 0.3, G: 0.9, B: 0.45, A: 0.9}
colKill = mathx.Color{R: 0.85, G: 0.2, B: 0.6, A: 0.9}
colCheck = mathx.Color{R: 0.35, G: 0.7, B: 1, A: 0.9}
colPick = mathx.Color{R: 1, G: 0.8, B: 0.2, A: 0.9}
colDim = mathx.Color{R: 0.7, G: 0.7, B: 0.7, A: 0.3}
colHealth = mathx.Color{R: 0.3, G: 0.85, B: 0.4, A: 1}
colHurtBar = mathx.Color{R: 0.85, G: 0.25, B: 0.25, A: 1}
)
type plugin struct {
ed *context.Editor
}
func reader[T any](get func(*T) mathx.Rect) func(*scene.Scene, scene.NodeHandle) (mathx.Rect, bool) {
return func(s *scene.Scene, h scene.NodeHandle) (mathx.Rect, bool) {
c, ok := scene.Get[T](s, h)
if !ok {
return mathx.Rect{}, false
}
return get(c), true
}
}
func Hook(e *ext.Registry) error {
p := &plugin{ed: e.Editor()}
sr := e.Schema()
var errs []error
for _, a := range []struct {
name string
col mathx.Color
read func(*scene.Scene, scene.NodeHandle) (mathx.Rect, bool)
}{
{gameplay.NameHitbox, colHit, reader(func(c *gameplay.Hitbox2D) mathx.Rect { return c.Area })},
{gameplay.NameHurtbox, colHurt, reader(func(c *gameplay.Hurtbox2D) mathx.Rect { return c.Area })},
{gameplay.NameKillZone, colKill, reader(func(c *gameplay.KillZone2D) mathx.Rect { return c.Area })},
{gameplay.NameCheckpoint, colCheck, reader(func(c *gameplay.Checkpoint2D) mathx.Rect { return c.Area })},
{gameplay.NamePickup, colPick, reader(func(c *gameplay.Pickup2D) mathx.Rect { return c.Area })},
} {
td, ok := sr.ByName(a.name)
if !ok {
continue
}
g := &areaGizmo{td: td, col: a.col, read: a.read, active: -1}
if a.name == gameplay.NameHitbox {
g.extra = drawKnockback
}
errs = append(errs, e.Gizmo(a.name, g))
}
errs = append(errs,
e.Inspector(gameplay.NameMotion, inspect.CustomFunc(p.motionInspector)),
e.Inspector(gameplay.NameHealth, inspect.CustomFunc(p.healthInspector)),
e.Inspector(gameplay.NameStateMachine, inspect.CustomFunc(p.fsmInspector)),
)
if err := errors.Join(errs...); err != nil {
return err
}
e.Overlay(p.sceneOverlay)
e.GameOverlay(p.gameOverlay)
if p.ed == nil {
return nil
}
o := &p.ed.Overlay
o.Set(CmdAreas, true)
return e.Command(command.Desc{
ID: CmdAreas, Name: "Gameplay Areas", Category: gameplay.Category, Order: 100,
Checked: func() bool { return o.On(CmdAreas) },
Run: func() error {
o.Toggle(CmdAreas)
return nil
},
})
}
func (p *plugin) areas() bool {
return p.ed != nil && p.ed.Overlay.On(CmdAreas)
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+47 -11
View File
@@ -144,20 +144,56 @@ func (m *Motion2D) Teleport(p mathx.Vec2) {
m.st.teleport, m.st.teleportTo = true, p
}
const kinematic = 1
type signals struct {
landed module.Signal1[float32]
left module.Signal0
jumped module.Signal0
wall module.Signal1[mathx.Vec2]
ceiling module.Signal0
landed module.Signal1[float32]
left module.Signal0
jumped module.Signal0
wall module.Signal1[mathx.Vec2]
ceiling module.Signal0
damaged module.Signal2[int64, scene.NodeHandle]
healed module.Signal1[int64]
died module.Signal1[scene.NodeHandle]
revived module.Signal0
hit module.Signal2[scene.NodeHandle, int64]
reached module.Signal1[scene.NodeHandle]
respawned module.Signal0
collected module.Signal1[scene.NodeHandle]
changed module.Signal2[int64, int64]
}
func validateMotion(_ module.ValidateContext, m *Motion2D) []problems.Diagnostic {
if m.Mode == Platformer && m.GravityScale == 0 && m.JumpSpeed == 0 && m.JumpHeight > 0 {
return []problems.Diagnostic{{Code: CodeNoJump, Severity: problems.Error,
Message: "Motion 2D: with Gravity Scale 0 the jump speed cannot come from Jump Height, set Jump Speed"}}
func bodyType(s *scene.Scene) (*schema.TypeDescriptor, schema.FieldID, bool) {
td, ok := s.Registry().ByName(bodyName)
if !ok {
return nil, 0, false
}
fd, ok := td.Field("type")
if !ok {
return nil, 0, false
}
return td, fd.ID, true
}
func validateMotion(ctx module.ValidateContext, m *Motion2D) []problems.Diagnostic {
var out []problems.Diagnostic
if td, f, ok := bodyType(ctx.Scene); ok {
if v, err := ctx.Scene.GetProperty(ctx.Node, td.ID, f); err == nil && v.I != kinematic {
out = append(out, problems.Diagnostic{Code: CodeNotKinematic, Severity: problems.Error, BlocksPlay: true,
Message: "Motion 2D moves its body itself: set Body 2D Type to kinematic"})
}
}
if m.Mode == Platformer && m.GravityScale == 0 && m.JumpSpeed == 0 && m.JumpHeight > 0 {
out = append(out, problems.Diagnostic{Code: CodeNoJump, Severity: problems.Error,
Message: "Motion 2D: with Gravity Scale 0 the jump speed cannot come from Jump Height, set Jump Speed"})
}
return out
}
func kinematicOnAdd(ctx module.AddContext, _ *Motion2D) {
if td, f, ok := bodyType(ctx.Scene); ok {
_ = ctx.Scene.SetProperty(ctx.Node, td.ID, f, schema.Enum(kinematic))
}
return nil
}
func registerMotion(r *module.Registry, sig *signals) error {
@@ -208,7 +244,7 @@ func registerMotion(r *module.Registry, sig *signals) error {
schema.FieldDoc("groundBody", "Body the character stands on, during Play."),
schema.Runtime("velocity", "grounded", "onWall", "onCeiling", "wallNormal", "floorNormal", "floorAngle", "groundBody"),
schema.Bitset("collisionMask", needsPhysics, layersList),
).Requires(bodyName).Category(Category).Display("Motion 2D").Icon("gameplay.motion").NodeIcon(75).Default(DefaultMotion).Validate(validateMotion)
).Requires(bodyName).Category(Category).Display("Motion 2D").Icon("gameplay.motion").NodeIcon(75).Default(DefaultMotion).Validate(validateMotion).OnAdd(kinematicOnAdd)
b.Proc1("Move", func(_ *bind.Ctx, m *Motion2D, x float32) { m.Move(x) },
bind.Doc("Move sets the horizontal input, -1..1; it stays until the next Move."), bind.Params("x"))
File diff suppressed because it is too large Load Diff
+63
View File
@@ -0,0 +1,63 @@
package gameplay
import (
"aego/engine"
"aego/format/aetext"
"aego/save"
"aego/scene"
)
const respawnBlock = "respawn"
// respawnSection keeps the respawn points marked Persist in save games.
type respawnSection struct{}
func (respawnSection) Version() int {
return 1
}
func (respawnSection) Migrate(*aetext.Node, int, int) error {
return nil
}
func (respawnSection) Save(w *engine.World, out *save.Writer) error {
s := Of(w)
if s == nil {
return nil
}
scene.Query1[Respawn](s.scene).Each(func(h scene.NodeHandle, r *Respawn) {
if !r.Persist || !r.st.hasPoint {
return
}
ref, ok := out.RefValue(h)
if !ok {
return
}
b := out.Block(respawnBlock, "")
b.Set("node", ref)
b.Vec2("point", r.st.point)
b.Int("priority", int64(r.st.priority))
b.Bool("ranked", r.st.hasPri)
})
return nil
}
func (respawnSection) Load(w *engine.World, in *save.Reader) error {
s := Of(w)
if s == nil {
return nil
}
for _, b := range in.Blocks(respawnBlock) {
h, ok := b.Ref("node")
if !ok {
continue
}
r, ok := scene.Get[Respawn](s.scene, h)
if !ok {
continue
}
r.st.point, r.st.hasPoint = b.Vec2("point", r.st.point), true
r.st.priority, r.st.hasPri = int32(b.Int("priority", 0)), b.Bool("ranked", false)
}
return nil
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,50 @@
package demo
import (
"aego/debug"
"aego/input"
)
// Player takes checkpoints; after a fall it comes back at the last one.
script Player {
func Update(dt float) {
m := node.Get<Motion2D>()
x := 0.0
if input.Down(input.KeyA) || input.Down(input.KeyLeft) {
x -= 1
}
if input.Down(input.KeyD) || input.Down(input.KeyRight) {
x += 1
}
m.Move(x)
if input.KeyPressed(input.KeySpace) || input.KeyPressed(input.KeyW) || input.KeyPressed(input.KeyUp) {
m.Jump()
}
if input.KeyReleased(input.KeySpace) || input.KeyReleased(input.KeyW) || input.KeyReleased(input.KeyUp) {
m.JumpReleased()
}
}
}
// Flag says which checkpoint was taken.
script Flag {
@export var Number int
var shown float
var conn Connection
func Ready() {
conn = node.Get<Checkpoint2D>().Reached.Connect(OnReached)
}
func OnReached(by NodeRef) {
shown = 2
}
func Update(dt float) {
p := node.WorldPosition()
if shown > 0 {
shown -= dt
debug.Text(Vec2{p.X - 1, p.Y - 2}, "checkpoint " + string(Number), Color{0.4, 1, 0.5, 1})
}
}
}
File diff suppressed because it is too large Load Diff
+79
View File
@@ -0,0 +1,79 @@
package demo
import (
"aego/debug"
"aego/input"
)
// Player walks with A/D, jumps with Space and strikes with J.
script Player {
func Update(dt float) {
m := node.Get<Motion2D>()
x := 0.0
if input.Down(input.KeyA) || input.Down(input.KeyLeft) {
x -= 1
}
if input.Down(input.KeyD) || input.Down(input.KeyRight) {
x += 1
}
m.Move(x)
if input.KeyPressed(input.KeySpace) || input.KeyPressed(input.KeyW) || input.KeyPressed(input.KeyUp) {
m.Jump()
}
if input.KeyReleased(input.KeySpace) || input.KeyReleased(input.KeyW) || input.KeyReleased(input.KeyUp) {
m.JumpReleased()
}
}
}
// Sword strikes once per J press with its Hitbox 2D.
script Sword {
func Update(dt float) {
if input.KeyPressed(input.KeyJ) {
node.Get<Hitbox2D>().Pulse()
}
p := node.WorldPosition()
debug.Rect(Rect{p.X - 0.3, p.Y - 0.2, 0.6, 0.4}, Color{1, 1, 1, 0.5})
}
}
// Dummy shows its hit points, the last damage, and comes back a second after it dies.
script Dummy {
var wait float
var flash float
var shown int
var died Connection
var hurt Connection
func Ready() {
hp := node.Get<Health>()
died = hp.Died.Connect(OnDied)
hurt = hp.Damaged.Connect(OnDamaged)
}
func OnDied(source NodeRef) {
wait = 1
}
func OnDamaged(amount int, source NodeRef) {
shown = amount
flash = 1
}
func Update(dt float) {
hp := node.Get<Health>()
if wait > 0 {
wait -= dt
if wait <= 0 {
hp.Revive(0)
}
}
p := node.WorldPosition()
debug.Rect(Rect{p.X - 0.6, p.Y - 1.3, 1.2, 0.15}, Color{0.3, 0.3, 0.3, 1})
debug.Rect(Rect{p.X - 0.6, p.Y - 1.3, 1.2 * float(hp.Current) / float(hp.Max), 0.15}, Color{0.2, 0.85, 0.3, 1})
if flash > 0 {
flash -= dt
debug.Text(Vec2{p.X + 0.6, p.Y - 1.6 - (1 - flash) * 0.5}, "-" + string(shown), Color{1, 0.45, 0.3, 1})
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,40 @@
package demo
import (
"aego/debug"
"aego/input"
)
// Player walks over three spikes and shows its hit points.
script Player {
func Update(dt float) {
m := node.Get<Motion2D>()
x := 0.0
if input.Down(input.KeyA) || input.Down(input.KeyLeft) {
x -= 1
}
if input.Down(input.KeyD) || input.Down(input.KeyRight) {
x += 1
}
m.Move(x)
if input.KeyPressed(input.KeySpace) || input.KeyPressed(input.KeyW) || input.KeyPressed(input.KeyUp) {
m.Jump()
}
if input.KeyReleased(input.KeySpace) || input.KeyReleased(input.KeyW) || input.KeyReleased(input.KeyUp) {
m.JumpReleased()
}
hp := node.Get<Health>()
p := node.WorldPosition()
debug.Text(Vec2{p.X - 0.6, p.Y - 1}, "hp " + string(hp.Current), Color{1, 1, 1, 1})
}
}
// Label writes Text above its node.
script Label {
@export var Text string
func Update(dt float) {
p := node.WorldPosition()
debug.Text(Vec2{p.X - 0.8, p.Y - 1}, Text, Color{0.85, 0.85, 0.85, 1})
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,79 @@
package demo
import (
"aego/debug"
"aego/input"
)
// Player strikes with J; jump and strike to hit the head for double damage.
script Player {
func Update(dt float) {
m := node.Get<Motion2D>()
x := 0.0
if input.Down(input.KeyA) || input.Down(input.KeyLeft) {
x -= 1
}
if input.Down(input.KeyD) || input.Down(input.KeyRight) {
x += 1
}
m.Move(x)
if input.KeyPressed(input.KeySpace) || input.KeyPressed(input.KeyW) || input.KeyPressed(input.KeyUp) {
m.Jump()
}
if input.KeyReleased(input.KeySpace) || input.KeyReleased(input.KeyW) || input.KeyReleased(input.KeyUp) {
m.JumpReleased()
}
}
}
// Sword strikes once per J press with its Hitbox 2D.
script Sword {
func Update(dt float) {
if input.KeyPressed(input.KeyJ) {
node.Get<Hitbox2D>().Pulse()
}
p := node.WorldPosition()
debug.Rect(Rect{p.X - 0.3, p.Y - 0.2, 0.6, 0.4}, Color{1, 1, 1, 0.5})
}
}
// Dummy shows its hit points, the last damage, and comes back a second after it dies.
script Dummy {
var wait float
var flash float
var shown int
var died Connection
var hurt Connection
func Ready() {
hp := node.Get<Health>()
died = hp.Died.Connect(OnDied)
hurt = hp.Damaged.Connect(OnDamaged)
}
func OnDied(source NodeRef) {
wait = 1
}
func OnDamaged(amount int, source NodeRef) {
shown = amount
flash = 1
}
func Update(dt float) {
hp := node.Get<Health>()
if wait > 0 {
wait -= dt
if wait <= 0 {
hp.Revive(0)
}
}
p := node.WorldPosition()
debug.Rect(Rect{p.X - 0.6, p.Y - 1.3, 1.2, 0.15}, Color{0.3, 0.3, 0.3, 1})
debug.Rect(Rect{p.X - 0.6, p.Y - 1.3, 1.2 * float(hp.Current) / float(hp.Max), 0.15}, Color{0.2, 0.85, 0.3, 1})
if flash > 0 {
flash -= dt
debug.Text(Vec2{p.X + 0.6, p.Y - 1.6 - (1 - flash) * 0.5}, "-" + string(shown), Color{1, 0.45, 0.3, 1})
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,30 @@
package demo
import (
"aego/debug"
"aego/input"
)
// Player falls into the pit, dies and respawns at the start.
script Player {
func Update(dt float) {
m := node.Get<Motion2D>()
x := 0.0
if input.Down(input.KeyA) || input.Down(input.KeyLeft) {
x -= 1
}
if input.Down(input.KeyD) || input.Down(input.KeyRight) {
x += 1
}
m.Move(x)
if input.KeyPressed(input.KeySpace) || input.KeyPressed(input.KeyW) || input.KeyPressed(input.KeyUp) {
m.Jump()
}
if input.KeyReleased(input.KeySpace) || input.KeyReleased(input.KeyW) || input.KeyReleased(input.KeyUp) {
m.JumpReleased()
}
hp := node.Get<Health>()
p := node.WorldPosition()
debug.Text(Vec2{p.X - 0.6, p.Y - 1}, "hp " + string(hp.Current), Color{1, 1, 1, 1})
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,29 @@
prefab version=1 {
node "d6400000000000000000000000000021" name="Ball" enabled=true layers=1 {
transform {
position=[0, 0]
rotation=0
scale=[1, 1]
}
component "engine.SpriteRenderer" version=1 {
sprite=asset("ae900000000000000000000000000010")
layer=0
order=0
tint=#38bdf8ff
origin=[0.5, 0.5]
size=[0.4, 0.4]
flipX=false
flipY=false
blend=premultiplied
filter=nearest
}
component "gameplay.Lifetime" version=1 {
seconds=2
despawn=true
}
component "engine.Script" version=1 {
script "game.demo.Drift" enabled=true {
}
}
}
}
@@ -0,0 +1,6 @@
meta version=1 {
id="d6400000000000000000000000000f01"
importer="prefab"
importerVersion=2
settings {}
}
@@ -0,0 +1,42 @@
package demo
import (
"aego/debug"
"aego/scene"
)
// Spawner drops a ball every 0.3 s; each ball goes back to the pool two seconds later and is reused.
script Spawner {
@export var Ball AssetRef<Prefab>
var t float
var k int
func Update(dt float) {
t += dt
if t >= 0.3 {
t = 0
k++
x := float(k % 7) - 3
scene.Spawn(Ball, node.WorldPosition() + Vec2{x * 0.6, 0})
}
}
}
// Drift sinks a ball faster the longer it lives.
script Drift {
func Update(dt float) {
age := 2 - node.Get<Lifetime>().Remaining
tr := node.Transform2D()
tr.Position.Y += 6 * age * dt
node.SetTransform2D(tr)
}
}
// Fuse shows the seconds left before its node is destroyed.
script Fuse {
func Update(dt float) {
p := node.WorldPosition()
debug.Text(Vec2{p.X - 1, p.Y - 0.8}, "destroyed in " + string(int(node.Get<Lifetime>().Remaining)) + " s", Color{1, 1, 1, 1})
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,61 @@
package demo
import (
"aego/camera"
"aego/debug"
"aego/input"
)
// Player walks over the coins.
script Player {
func Update(dt float) {
m := node.Get<Motion2D>()
x := 0.0
if input.Down(input.KeyA) || input.Down(input.KeyLeft) {
x -= 1
}
if input.Down(input.KeyD) || input.Down(input.KeyRight) {
x += 1
}
m.Move(x)
if input.KeyPressed(input.KeySpace) || input.KeyPressed(input.KeyW) || input.KeyPressed(input.KeyUp) {
m.Jump()
}
if input.KeyReleased(input.KeySpace) || input.KeyReleased(input.KeyW) || input.KeyReleased(input.KeyUp) {
m.JumpReleased()
}
}
}
// Counter adds up the coins taken and writes the count in the corner of the view.
script Counter {
@export var C1 NodeRef
@export var C2 NodeRef
@export var C3 NodeRef
@export var C4 NodeRef
@export var C5 NodeRef
var coins int
var k1 Connection
var k2 Connection
var k3 Connection
var k4 Connection
var k5 Connection
func Ready() {
k1 = C1.Get<Pickup2D>().Collected.Connect(OnCollected)
k2 = C2.Get<Pickup2D>().Collected.Connect(OnCollected)
k3 = C3.Get<Pickup2D>().Collected.Connect(OnCollected)
k4 = C4.Get<Pickup2D>().Collected.Connect(OnCollected)
k5 = C5.Get<Pickup2D>().Collected.Connect(OnCollected)
}
func OnCollected(by NodeRef) {
coins++
}
func Update(dt float) {
p := camera.LogicalPose().Position
debug.Text(Vec2{p.X - 6, p.Y - 3.5}, "coins " + string(coins) + " / 5", Color{1, 0.85, 0.2, 1})
}
}
File diff suppressed because it is too large Load Diff
+32
View File
@@ -0,0 +1,32 @@
package demo
import (
"aego/debug"
"aego/input"
)
// Player respawns after a fall or when R is pressed.
script Player {
func Update(dt float) {
m := node.Get<Motion2D>()
x := 0.0
if input.Down(input.KeyA) || input.Down(input.KeyLeft) {
x -= 1
}
if input.Down(input.KeyD) || input.Down(input.KeyRight) {
x += 1
}
m.Move(x)
if input.KeyPressed(input.KeySpace) || input.KeyPressed(input.KeyW) || input.KeyPressed(input.KeyUp) {
m.Jump()
}
if input.KeyReleased(input.KeySpace) || input.KeyReleased(input.KeyW) || input.KeyReleased(input.KeyUp) {
m.JumpReleased()
}
if input.KeyPressed(input.KeyR) {
node.Get<Respawn>().Trigger()
}
p := node.WorldPosition()
debug.Text(Vec2{p.X - 1, p.Y - 1}, "R: respawn", Color{0.8, 0.8, 0.8, 1})
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,66 @@
package demo
import (
"aego/debug"
"aego/input"
)
// Player keeps its states in a State Machine: Idle, Run and Air, each with its own hooks and colour.
script Player {
func Update(dt float) {
m := node.Get<Motion2D>()
x := 0.0
if input.Down(input.KeyA) || input.Down(input.KeyLeft) {
x -= 1
}
if input.Down(input.KeyD) || input.Down(input.KeyRight) {
x += 1
}
m.Move(x)
if input.KeyPressed(input.KeySpace) || input.KeyPressed(input.KeyW) || input.KeyPressed(input.KeyUp) {
m.Jump()
}
if input.KeyReleased(input.KeySpace) || input.KeyReleased(input.KeyW) || input.KeyReleased(input.KeyUp) {
m.JumpReleased()
}
sm := node.Get<StateMachine>()
p := node.WorldPosition()
debug.Text(Vec2{p.X - 0.4, p.Y - 1}, sm.Current, Color{1, 1, 1, 1})
}
func UpdateIdle(dt float) {
m := node.Get<Motion2D>()
if !m.Grounded {
node.Get<StateMachine>().Go("Air")
} else if m.Velocity.X > 0.5 || m.Velocity.X < -0.5 {
node.Get<StateMachine>().Go("Run")
}
}
func UpdateRun(dt float) {
m := node.Get<Motion2D>()
if !m.Grounded {
node.Get<StateMachine>().Go("Air")
} else if m.Velocity.X < 0.5 && m.Velocity.X > -0.5 {
node.Get<StateMachine>().Go("Idle")
}
}
func UpdateAir(dt float) {
if node.Get<Motion2D>().Grounded {
node.Get<StateMachine>().Go("Idle")
}
}
func EnterIdle() {
node.Get<SpriteRenderer>().Tint = Color{0.96, 0.62, 0.04, 1}
}
func EnterRun() {
node.Get<SpriteRenderer>().Tint = Color{0.2, 0.8, 0.4, 1}
}
func EnterAir() {
node.Get<SpriteRenderer>().Tint = Color{0.3, 0.6, 1, 1}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#fff" d="M3 1L4.5 1L4.5 15L3 15Z M4.5 2L13 2L11 5L13 8L4.5 8Z"/></svg>

After

Width:  |  Height:  |  Size: 143 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#fff" d="M2 2L7 2L7 6L2 6Z M9 10L14 10L14 14L9 14Z M6.5 6L7.5 6L7.5 11.5L9 11.5L9 12.5L6.5 12.5Z"/></svg>

After

Width:  |  Height:  |  Size: 178 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#fff" d="M8 14L2.5 8.5C1 7 1 4.5 2.8 3.2C4.4 2 6.6 2.5 8 4.2C9.4 2.5 11.6 2 13.2 3.2C15 4.5 15 7 13.5 8.5Z"/></svg>

After

Width:  |  Height:  |  Size: 188 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#fff" d="M13.5 1.5L14.5 2.5L6 11L5 10Z M3 9L7 13L6 14L4.5 12.5L2.5 14.5L1.5 13.5L3.5 11.5L2 10Z"/></svg>

After

Width:  |  Height:  |  Size: 177 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#fff" d="M2 2L14 2L14 14L2 14Z M4 4L4 12L12 12L12 4Z"/></svg>

After

Width:  |  Height:  |  Size: 134 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#fff" d="M2.5 1L8 6.5L13.5 1L15 2.5L9.5 8L15 13.5L13.5 15L8 9.5L2.5 15L1 13.5L6.5 8L1 2.5Z"/></svg>

After

Width:  |  Height:  |  Size: 172 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#fff" d="M3 1L13 1L13 3L9.2 8L13 13L13 15L3 15L3 13L6.8 8L3 3Z"/></svg>

After

Width:  |  Height:  |  Size: 144 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#fff" d="M8 1L10 6L15 6L11 9.5L12.5 15L8 11.8L3.5 15L5 9.5L1 6L6 6Z"/></svg>

After

Width:  |  Height:  |  Size: 149 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#fff" d="M8 1L11 4L8 7L8 5C6.3 5 5 6.3 5 8C5 9.7 6.3 11 8 11C9.7 11 11 9.7 11 8L13 8C13 10.8 10.8 13 8 13C5.2 13 3 10.8 3 8C3 5.2 5.2 3 8 3Z"/></svg>

After

Width:  |  Height:  |  Size: 222 B

Some files were not shown because too many files have changed in this diff Show More