Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c24a4791fa | ||
|
|
12e9f5d0d0 | ||
|
|
24284632d5 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 |
|
||||
|
||||
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 190 KiB After Width: | Height: | Size: 193 KiB |
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
package gameplay
|
||||
|
||||
import (
|
||||
"aego/contracts/physics2d"
|
||||
"errors"
|
||||
|
||||
"aego/core/clock"
|
||||
"aego/core/log"
|
||||
"aego/engine"
|
||||
"aego/engine/module"
|
||||
@@ -11,7 +13,11 @@ const (
|
||||
ID = "gameplay"
|
||||
ServiceID = ID + ".service"
|
||||
SystemMotion = ID + ".motion"
|
||||
SystemMotionPost = ID + ".motion_post"
|
||||
SystemPost = ID + ".post"
|
||||
SystemFrame = ID + ".frame"
|
||||
SystemFSMFixed = ID + ".fsm_fixed"
|
||||
SystemFSMUpdate = ID + ".fsm_update"
|
||||
scriptUpdate = "script.update"
|
||||
Category = "Gameplay"
|
||||
needsPhysics = "physics"
|
||||
needsScripts = "script"
|
||||
@@ -39,11 +45,16 @@ func (*Module) IconDir() string {
|
||||
}
|
||||
|
||||
func (m *Module) Register(r *module.Registry) error {
|
||||
if err := registerMotion(r, &m.sig); err != nil {
|
||||
if err := errors.Join(registerMotion(r, &m.sig), registerCombat(r, &m.sig), registerZones(r, &m.sig), registerFSM(r, &m.sig)); err != nil {
|
||||
return err
|
||||
}
|
||||
registerTemplates(r)
|
||||
r.SaveSection(ID+".respawn", respawnSection{})
|
||||
r.System(engine.SystemDesc{ID: SystemFSMFixed, Phase: engine.PhasePrePhysics, Scope: engine.ScopePlay, After: []string{scriptFixed}, Before: []string{SystemMotion}, System: fsmFixedSystem{}})
|
||||
r.System(engine.SystemDesc{ID: SystemMotion, Phase: engine.PhasePrePhysics, Scope: engine.ScopePlay, After: []string{scriptFixed}, System: fixedSystem{}})
|
||||
r.System(engine.SystemDesc{ID: SystemMotionPost, Phase: engine.PhasePostPhysics, Scope: engine.ScopePlay, Before: []string{scriptEvents}, System: postSystem{}})
|
||||
r.System(engine.SystemDesc{ID: SystemPost, Phase: engine.PhasePostPhysics, Scope: engine.ScopePlay, Before: []string{scriptEvents}, System: postSystem{}})
|
||||
r.System(engine.SystemDesc{ID: SystemFSMUpdate, Phase: engine.PhaseUpdate, Scope: engine.ScopePlay, After: []string{scriptUpdate}, System: fsmUpdateSystem{}})
|
||||
r.System(engine.SystemDesc{ID: SystemFrame, Phase: engine.PhasePostUpdate, Scope: engine.ScopePlay, Pause: engine.PauseAlways, System: frameSystem{}})
|
||||
registerDocs(r)
|
||||
return nil
|
||||
}
|
||||
@@ -75,14 +86,6 @@ func Of(w *engine.World) *Service {
|
||||
return s
|
||||
}
|
||||
|
||||
// queries finds the physics queries lazily: physics may attach to the world after this module.
|
||||
func (s *Service) queries() physics2d.Queries {
|
||||
if s.q == nil && s.world != nil {
|
||||
s.q, _ = engine.ServiceOf[physics2d.Queries](s.world, physics2d.ServiceID)
|
||||
}
|
||||
return s.q
|
||||
}
|
||||
|
||||
type fixedSystem struct{}
|
||||
|
||||
func (fixedSystem) Run(ctx *engine.Context) {
|
||||
@@ -98,3 +101,34 @@ func (postSystem) Run(ctx *engine.Context) {
|
||||
s.Post()
|
||||
}
|
||||
}
|
||||
|
||||
type frameSystem struct{}
|
||||
|
||||
func (frameSystem) Run(ctx *engine.Context) {
|
||||
s := Of(ctx.World)
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
game, real := float32(ctx.Dt), float32(ctx.Unscaled)
|
||||
if t := ctx.Time; t != nil {
|
||||
game = float32(t.Span(clock.Game).Delta)
|
||||
real = float32(t.Span(clock.Real).Delta)
|
||||
}
|
||||
s.Frame(game, real)
|
||||
}
|
||||
|
||||
type fsmFixedSystem struct{}
|
||||
|
||||
func (fsmFixedSystem) Run(ctx *engine.Context) {
|
||||
if s := Of(ctx.World); s != nil {
|
||||
s.FSMFixed(float32(ctx.Dt))
|
||||
}
|
||||
}
|
||||
|
||||
type fsmUpdateSystem struct{}
|
||||
|
||||
func (fsmUpdateSystem) Run(ctx *engine.Context) {
|
||||
if s := Of(ctx.World); s != nil {
|
||||
s.FSMUpdate(float32(ctx.Dt))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
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"))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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})
|
||||
}
|
||||
}
|
||||
@@ -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})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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})
|
||||
}
|
||||
}
|
||||
@@ -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})
|
||||
}
|
||||
}
|
||||
@@ -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})
|
||||
}
|
||||
}
|
||||
@@ -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})
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
}
|
||||
@@ -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 |