Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54693f97d2 | ||
|
|
080948f434 |
@@ -272,6 +272,7 @@ check-all: cli
|
||||
.cache/bin/aego check examples/native-rotator
|
||||
.cache/bin/aego check examples/platformer
|
||||
.cache/bin/aego test examples/platformer
|
||||
.cache/bin/aego check examples/topdown
|
||||
.cache/bin/aego check examples/native-settings
|
||||
.cache/bin/aego check examples/native-editor
|
||||
.cache/bin/aego check app/playground/examples
|
||||
|
||||
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 159 KiB After Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 203 KiB After Width: | Height: | Size: 207 KiB |
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 126 KiB |
@@ -80,7 +80,7 @@ aego edit mygame/scenes/main.aescene
|
||||
aego edit --launcher new
|
||||
```
|
||||
|
||||
`aego new` creates a 2D project with a scene holding a camera and a sprite, and a script to start from; `--template 3d` and the other templates are listed by `aego help`. In the editor, the Getting Started panel walks through the first six steps: a node, a script, Play, a live tweak, hot reload and fixing an error.
|
||||
`aego new` creates a 2D project with a scene holding a camera and a sprite, and a script to start from; `--template 3d` and the other templates are listed by `aego help`. `aego new mygame --example platformer` copies the platformer (a character on Motion 2D with stomps, a pit, a checkpoint and save games) and `--example topdown` the top-down game (a sword, enemies that patrol and chase with a State Machine, coins and lava); the launcher lists the same examples under **Examples**. In the editor, the Getting Started panel walks through the first six steps: a node, a script, Play, a live tweak, hot reload and fixing an error.
|
||||
|
||||
| Command | Does |
|
||||
|---|---|
|
||||
|
||||
@@ -5,12 +5,6 @@ import (
|
||||
"aego/sdk"
|
||||
)
|
||||
|
||||
type Health struct {
|
||||
Max float32 `aego:"max,min=1"`
|
||||
Current float32 `aego:"current,min=0"`
|
||||
Hits int64 `aego:"hits"`
|
||||
}
|
||||
|
||||
type EnemyStats struct {
|
||||
Speed float32 `aego:"speed,min=0,max=10"`
|
||||
Cooldown float32 `aego:"cooldown,min=0,max=5"`
|
||||
@@ -25,38 +19,10 @@ func (Module) Info() sdk.ModuleInfo {
|
||||
}
|
||||
|
||||
func (Module) Register(r *sdk.Registry) error {
|
||||
if err := sdk.Component[Health](r, sdk.Name("game.Health"), sdk.Doc("hit points of a character"), sdk.Persist("current", "hits")).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := sdk.DataType[EnemyStats](r, sdk.Name("mygame.EnemyStats"), sdk.Doc("movement and attack numbers of one kind of enemy")).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
sdk.Live[EnemyStats](r, "speed")
|
||||
m := r.ScriptModule("mygame/combat")
|
||||
sdk.Proc2(m, "Damage", func(c *sdk.ScriptCtx, target sdk.NodeHandle, amount int64) {
|
||||
if c.Scene == nil || !c.Scene.IsAlive(target) {
|
||||
c.Fail("damage target is nil or destroyed")
|
||||
return
|
||||
}
|
||||
h, ok := sdk.Get[Health](c.Scene, target)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
h.Current -= float32(amount)
|
||||
h.Hits++
|
||||
if h.Current <= 0 {
|
||||
h.Current = h.Max
|
||||
}
|
||||
})
|
||||
sdk.Func1(m, "HealthOf", func(c *sdk.ScriptCtx, target sdk.NodeHandle) float32 {
|
||||
if c.Scene == nil {
|
||||
return 0
|
||||
}
|
||||
if h, ok := sdk.Get[Health](c.Scene, target); ok {
|
||||
return h.Current
|
||||
}
|
||||
return 0
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -39,13 +39,19 @@ prefab version=1 {
|
||||
density=1
|
||||
size=[0.9, 0.9]
|
||||
}
|
||||
component "physics.PolygonCollider2D" version=1 {
|
||||
offset=[0, 0]
|
||||
isTrigger=true
|
||||
friction=0.6
|
||||
restitution=0
|
||||
density=0
|
||||
points=[[-0.35, -0.85], [0.35, -0.85], [0.35, -0.65], [-0.35, -0.65]]
|
||||
component "gameplay.Health" version=1 {
|
||||
max=1
|
||||
team=1
|
||||
invulnTime=0
|
||||
}
|
||||
component "gameplay.Hurtbox2D" version=1 {
|
||||
area=[-0.4, -0.45, 0.8, 0.35]
|
||||
}
|
||||
component "gameplay.Hitbox2D" version=1 {
|
||||
area=[-0.45, -0.1, 0.9, 0.55]
|
||||
policy=periodic
|
||||
knockback=[7, 0]
|
||||
knockbackFromCenter=true
|
||||
}
|
||||
component "save.Persist" version=1 {
|
||||
noTransform=false
|
||||
@@ -55,7 +61,6 @@ prefab version=1 {
|
||||
script "mygame.enemy.EnemyAI" enabled=true {
|
||||
PointA=node("00000000000000000000000000000000")
|
||||
PointB=node("00000000000000000000000000000000")
|
||||
Target=node("00000000000000000000000000000000")
|
||||
Stats=asset("da7a0000000000000000000000000001")
|
||||
DieSound=asset("a0d10000000000000000000000000013")
|
||||
}
|
||||
|
||||
@@ -3,10 +3,7 @@ package enemy
|
||||
import (
|
||||
"aego/audio"
|
||||
"aego/curve"
|
||||
"aego/physics"
|
||||
"aego/timer"
|
||||
"aego/tween"
|
||||
"mygame/combat"
|
||||
"mygame/shared"
|
||||
)
|
||||
|
||||
@@ -19,9 +16,6 @@ script EnemyAI {
|
||||
@export
|
||||
var PointB NodeRef
|
||||
|
||||
@export
|
||||
var Target NodeRef
|
||||
|
||||
@export(tip="Speed, cooldown, damage and tint of this kind of enemy")
|
||||
var Stats AssetRef<EnemyStats>
|
||||
|
||||
@@ -29,16 +23,15 @@ script EnemyAI {
|
||||
var DieSound AssetRef<AudioEvent>
|
||||
|
||||
var toB bool = true
|
||||
var hit timer.Timer
|
||||
var dead bool
|
||||
var fade Connection
|
||||
|
||||
func Ready() {
|
||||
hit = timer.Cooldown(Stats.Cooldown)
|
||||
sprite := node.Get<SpriteRenderer>()
|
||||
if sprite != nil {
|
||||
sprite.Tint = Stats.Tint
|
||||
}
|
||||
node.Get<SpriteRenderer>().Tint = Stats.Tint
|
||||
hit := node.Get<Hitbox2D>()
|
||||
hit.Damage = Stats.Damage
|
||||
hit.Period = Stats.Cooldown
|
||||
node.Get<Health>().Died.Connect(OnDied)
|
||||
}
|
||||
|
||||
func FixedUpdate(dt float) {
|
||||
@@ -46,9 +39,6 @@ script EnemyAI {
|
||||
return
|
||||
}
|
||||
body := node.Get<Body2D>()
|
||||
if body == nil {
|
||||
return
|
||||
}
|
||||
goal := PointA.Position()
|
||||
if toB {
|
||||
goal = PointB.Position()
|
||||
@@ -62,15 +52,10 @@ script EnemyAI {
|
||||
body.SetVelocity(v)
|
||||
}
|
||||
|
||||
func OnTriggerEnter(other NodeRef) {
|
||||
if dead || other != Target {
|
||||
return
|
||||
}
|
||||
tb := other.Get<Body2D>()
|
||||
if tb == nil || tb.Velocity.Y <= 0 {
|
||||
return
|
||||
}
|
||||
// OnDied stops the attack, tells the player and fades the enemy out.
|
||||
func OnDied(source NodeRef) {
|
||||
dead = true
|
||||
node.Get<Hitbox2D>().Deactivate()
|
||||
Died.Emit()
|
||||
audio.PlayEventAt(DieSound, node.Position())
|
||||
fade = tween.ToFloat(node, "SpriteRenderer.Tint.a", 0, 0.2, curve.OutQuad).Completed.Connect(OnFaded)
|
||||
@@ -79,11 +64,4 @@ script EnemyAI {
|
||||
func OnFaded(finished bool) {
|
||||
node.Destroy()
|
||||
}
|
||||
|
||||
func OnCollisionEnter(other NodeRef, c physics.Contact) {
|
||||
if dead || other != Target || c.Normal.Y > 0.5 || !hit.Use() {
|
||||
return
|
||||
}
|
||||
combat.Damage(Target, Stats.Damage)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
.aego/
|
||||
exports/
|
||||
|
After Width: | Height: | Size: 596 B |
@@ -0,0 +1,67 @@
|
||||
project version=1 {
|
||||
name="Top-down"
|
||||
module="topdown"
|
||||
mainScene=asset("70d0000000000000000000000000a001")
|
||||
native=false
|
||||
window {
|
||||
width=1280
|
||||
height=720
|
||||
title="Aego Top-down"
|
||||
}
|
||||
input version=2 {
|
||||
stack=[Gameplay]
|
||||
action {
|
||||
name=MoveX
|
||||
type=axis1
|
||||
bind=["Key.A/Key.D", "Key.Left/Key.Right", "Pad.LeftX"]
|
||||
}
|
||||
action {
|
||||
name=MoveY
|
||||
type=axis1
|
||||
bind=["Key.W/Key.S", "Key.Up/Key.Down", "Pad.LeftY"]
|
||||
}
|
||||
action {
|
||||
name=AimX
|
||||
type=axis1
|
||||
bind=["Pad.RightX"]
|
||||
}
|
||||
action {
|
||||
name=AimY
|
||||
type=axis1
|
||||
bind=["Pad.RightY"]
|
||||
}
|
||||
action {
|
||||
name=Attack
|
||||
type=button
|
||||
bind=["Key.J", "Mouse.Left", "Pad.X"]
|
||||
}
|
||||
action {
|
||||
name=Pause
|
||||
type=button
|
||||
bind=["Key.Escape", "Pad.Start"]
|
||||
}
|
||||
action {
|
||||
name=Back
|
||||
type=button
|
||||
bind=["Key.Escape", "Pad.B"]
|
||||
}
|
||||
context {
|
||||
name=Gameplay
|
||||
actions=[MoveX, MoveY, AimX, AimY, Attack, Pause]
|
||||
}
|
||||
context {
|
||||
name=Menu
|
||||
priority=10
|
||||
consume=true
|
||||
actions=[Back]
|
||||
}
|
||||
}
|
||||
physics {
|
||||
gravity=[0, 0]
|
||||
layers=[Default, Walls, Player, Enemy]
|
||||
collide {
|
||||
Player=[Walls, Enemy]
|
||||
Enemy=[Walls, Player]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
meta version=1 {
|
||||
id="70d0000000000000000000000000a001"
|
||||
importer="scene"
|
||||
importerVersion=1
|
||||
settings {}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package enemy
|
||||
|
||||
import "aego/math"
|
||||
|
||||
// Grunt patrols between two points and chases the player while it is close; its State Machine runs FixedPatrol or FixedChase.
|
||||
script Grunt {
|
||||
@export
|
||||
var Player NodeRef
|
||||
|
||||
@export
|
||||
var PointA NodeRef
|
||||
|
||||
@export
|
||||
var PointB NodeRef
|
||||
|
||||
@export(min=0)
|
||||
var SeeRange float = 4
|
||||
|
||||
@export(min=0)
|
||||
var LoseRange float = 6
|
||||
|
||||
var toB bool = true
|
||||
|
||||
func toPlayer() Vec2 {
|
||||
return Player.WorldPosition() - node.WorldPosition()
|
||||
}
|
||||
|
||||
func FixedPatrol(dt float) {
|
||||
goal := PointA.WorldPosition()
|
||||
if toB {
|
||||
goal = PointB.WorldPosition()
|
||||
}
|
||||
d := goal - node.WorldPosition()
|
||||
if math.Length(d) < 0.2 {
|
||||
toB = !toB
|
||||
}
|
||||
node.Get<Motion2D>().MoveDir(math.Normalize(d) * 0.5)
|
||||
if Player != nil && Player.IsAlive() && math.Length(toPlayer()) < SeeRange {
|
||||
node.Get<StateMachine>().Go("Chase")
|
||||
}
|
||||
}
|
||||
|
||||
func FixedChase(dt float) {
|
||||
if Player == nil || !Player.IsAlive() || math.Length(toPlayer()) > LoseRange {
|
||||
node.Get<StateMachine>().Go("Patrol")
|
||||
return
|
||||
}
|
||||
node.Get<Motion2D>().MoveDir(math.Normalize(toPlayer()))
|
||||
}
|
||||
|
||||
func EnterChase() {
|
||||
node.Get<SpriteRenderer>().Tint = Color{1, 0.35, 0.35, 1}
|
||||
}
|
||||
|
||||
func ExitChase() {
|
||||
node.Get<SpriteRenderer>().Tint = Color{0.75, 0.5, 0.5, 1}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
meta version=1 {
|
||||
id="70d0000000000000000000000000a012"
|
||||
importer="script"
|
||||
importerVersion=1
|
||||
settings {}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package hud
|
||||
|
||||
import (
|
||||
"aego/camera"
|
||||
"aego/debug"
|
||||
)
|
||||
|
||||
// Coins counts the collected coins and writes the count in the corner of the view.
|
||||
script Coins {
|
||||
@export
|
||||
var C1 NodeRef
|
||||
|
||||
@export
|
||||
var C2 NodeRef
|
||||
|
||||
@export
|
||||
var C3 NodeRef
|
||||
|
||||
@export
|
||||
var C4 NodeRef
|
||||
|
||||
var coins int
|
||||
var k1 Connection
|
||||
var k2 Connection
|
||||
var k3 Connection
|
||||
var k4 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)
|
||||
}
|
||||
|
||||
func OnCollected(by NodeRef) {
|
||||
coins++
|
||||
}
|
||||
|
||||
func Update(dt float) {
|
||||
p := camera.LogicalPose().Position
|
||||
debug.Text(Vec2{p.X - 12, p.Y - 6.5}, "coins " + string(coins) + " / 4", Color{1, 0.85, 0.2, 1})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
meta version=1 {
|
||||
id="70d0000000000000000000000000a013"
|
||||
importer="script"
|
||||
importerVersion=1
|
||||
settings {}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"aego/input"
|
||||
"aego/math"
|
||||
"aego/settings"
|
||||
"aego/timer"
|
||||
)
|
||||
|
||||
// Hero moves with the left stick or WASD and swings its Sword towards the right stick, the mouse or the way it walks.
|
||||
script Hero {
|
||||
@export
|
||||
var Sword NodeRef
|
||||
|
||||
@export(min=0)
|
||||
var Reach float = 0.8
|
||||
|
||||
var facing Vec2 = Vec2{1, 0}
|
||||
var swing timer.Timer
|
||||
var mouse Vec2
|
||||
var useMouse bool
|
||||
|
||||
func Ready() {
|
||||
swing = timer.Cooldown(0.3)
|
||||
mouse = input.MousePos()
|
||||
}
|
||||
|
||||
func Update(dt float) {
|
||||
move := Vec2{input.Axis("MoveX"), input.Axis("MoveY")}
|
||||
node.Get<Motion2D>().MoveDir(move)
|
||||
aim := Vec2{input.Axis("AimX"), input.Axis("AimY")}
|
||||
p := input.MousePos()
|
||||
if math.Dist(p, mouse) > 2 {
|
||||
mouse = p
|
||||
useMouse = true
|
||||
}
|
||||
if math.Length(aim) > 0.3 {
|
||||
facing = math.Normalize(aim)
|
||||
useMouse = false
|
||||
} else if useMouse {
|
||||
to := input.MouseWorld() - node.WorldPosition()
|
||||
if math.Length(to) > 0.2 {
|
||||
facing = math.Normalize(to)
|
||||
}
|
||||
} else if math.Length(move) > 0.1 {
|
||||
facing = math.Normalize(move)
|
||||
}
|
||||
Sword.SetPosition(facing * Reach)
|
||||
Sword.SetRotation(math.Angle(facing))
|
||||
if input.Pressed("Attack") && swing.Use() {
|
||||
Sword.Get<Hitbox2D>().Pulse()
|
||||
}
|
||||
if input.Pressed("Pause") {
|
||||
settings.OpenView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
meta version=1 {
|
||||
id="70d0000000000000000000000000a011"
|
||||
importer="script"
|
||||
importerVersion=1
|
||||
settings {}
|
||||
}
|
||||
@@ -13,6 +13,8 @@ The gameplay module adds the parts most 2D games rebuild by hand: a character co
|
||||
|
||||
Quick start: Create → Gameplay → Platformer Character gives a node with a kinematic Rigid Body 2D, a collider, Motion 2D, Health, Hurtbox 2D and Respawn. Give it a script that calls `Move` and `Jump`, add a floor and press Play.
|
||||
|
||||
Two complete games use the kit: `aego new mygame --example platformer` (stomping enemies with a foot hitbox, a pit with a kill zone, a checkpoint, a persisted respawn point and a state machine for the player's look) and `aego new mygame --example topdown` (a sword swung towards the stick, the mouse or the walking direction, enemies whose State Machine switches between Patrol and Chase, coins and lava). The launcher offers both under **Examples**.
|
||||
|
||||
| Component | Purpose |
|
||||
|---|---|
|
||||
| Motion 2D | walks, jumps and slides a kinematic body: slopes, steps, moving platforms, top-down movement |
|
||||
|
||||
@@ -208,18 +208,18 @@ func TestApplyAndInverse(t *testing.T) {
|
||||
|
||||
func TestPrefabFlow(t *testing.T) {
|
||||
e := open(t)
|
||||
out := e.must("prefab.create", agent.Args{"file": level, "node": "Ground", "out": "prefabs/ground"})
|
||||
out := e.must("prefab.create", agent.Args{"file": level, "node": "GroundLeft", "out": "prefabs/ground"})
|
||||
if len(out.Created) != 1 {
|
||||
t.Fatalf("create %+v", out)
|
||||
}
|
||||
ins := e.must("prefab.instantiate", agent.Args{"file": level, "prefab": "prefabs/ground.aeprefab", "name": "Ground2"})
|
||||
second := ins.Created[0]
|
||||
e.must("scene.set", agent.Args{"file": level, "node": "Ground", "path": "SpriteRenderer.tint", "value": "#123456ff"})
|
||||
e.must("prefab.apply", agent.Args{"file": level, "node": "Ground", "level": "property", "owner": "engine.SpriteRenderer", "field": "tint"})
|
||||
e.must("scene.set", agent.Args{"file": level, "node": "GroundLeft", "path": "SpriteRenderer.tint", "value": "#123456ff"})
|
||||
e.must("prefab.apply", agent.Args{"file": level, "node": "GroundLeft", "level": "property", "owner": "engine.SpriteRenderer", "field": "tint"})
|
||||
if got := e.must("scene.get", agent.Args{"file": level, "node": second, "path": "SpriteRenderer.tint"}).Text; got != "#123456ff" {
|
||||
t.Fatalf("second instance tint %q", got)
|
||||
}
|
||||
again := e.must("prefab.apply", agent.Args{"file": level, "node": "Ground", "level": "property", "owner": "engine.SpriteRenderer", "field": "tint"})
|
||||
again := e.must("prefab.apply", agent.Args{"file": level, "node": "GroundLeft", "level": "property", "owner": "engine.SpriteRenderer", "field": "tint"})
|
||||
if len(again.Notes) != 1 || again.Notes[0] != "nothing to apply" {
|
||||
t.Fatalf("second apply %+v", again)
|
||||
}
|
||||
|
||||
@@ -270,11 +270,6 @@
|
||||
"owner": "save",
|
||||
"doc": "Persist saves the node's transform and enabled flag in save games."
|
||||
},
|
||||
{
|
||||
"name": "game.Health",
|
||||
"kind": "component",
|
||||
"owner": "mygame"
|
||||
},
|
||||
{
|
||||
"name": "audio.AudioEvent",
|
||||
"kind": "data",
|
||||
|
||||
@@ -11,12 +11,6 @@ import (
|
||||
"aego/sdk"
|
||||
)
|
||||
|
||||
type health struct {
|
||||
Max float32 `aego:"max,min=1"`
|
||||
Current float32 `aego:"current,min=0"`
|
||||
Hits int64 `aego:"hits"`
|
||||
}
|
||||
|
||||
type enemyStats struct {
|
||||
Speed float32 `aego:"speed,min=0,max=10"`
|
||||
Cooldown float32 `aego:"cooldown,min=0,max=5"`
|
||||
@@ -31,24 +25,10 @@ func (Platformer) Info() sdk.ModuleInfo {
|
||||
}
|
||||
|
||||
func (Platformer) Register(r *sdk.Registry) error {
|
||||
if err := sdk.Component[health](r, sdk.Name("game.Health"), sdk.Persist("current", "hits")).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := sdk.DataType[enemyStats](r, sdk.Name("mygame.EnemyStats")).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
m := r.ScriptModule("mygame/combat")
|
||||
sdk.Proc2(m, "Damage", func(c *sdk.ScriptCtx, target sdk.NodeHandle, amount int64) {
|
||||
if h, ok := sdk.Get[health](c.Scene, target); ok {
|
||||
h.Current -= float32(amount)
|
||||
}
|
||||
})
|
||||
sdk.Func1(m, "HealthOf", func(c *sdk.ScriptCtx, target sdk.NodeHandle) float32 {
|
||||
if h, ok := sdk.Get[health](c.Scene, target); ok {
|
||||
return h.Current
|
||||
}
|
||||
return 0
|
||||
})
|
||||
sdk.Live[enemyStats](r, "speed")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -54,11 +54,11 @@ func TestScriptVectorDefaultPublished(t *testing.T) {
|
||||
if !ok {
|
||||
t.Fatal("PlayerController is not published")
|
||||
}
|
||||
fd, ok := td.Field("JumpImpulse")
|
||||
if !ok || !fd.HasDefault || fd.Default.V != (mathx.Vec2{Y: -5}) {
|
||||
t.Fatalf("JumpImpulse default %+v", fd)
|
||||
fd, ok := td.Field("Bounce")
|
||||
if !ok || !fd.HasDefault || fd.Default.V != (mathx.Vec2{Y: -9}) {
|
||||
t.Fatalf("Bounce default %+v", fd)
|
||||
}
|
||||
if v := pf.field("JumpImpulse"); v.Lane(1) != -5 {
|
||||
t.Fatalf("instance JumpImpulse lane y = %v", v.Lane(1))
|
||||
if v := pf.field("Bounce"); v.Lane(1) != -9 {
|
||||
t.Fatalf("instance Bounce lane y = %v", v.Lane(1))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ func TestExamplesCanonical(t *testing.T) {
|
||||
}{
|
||||
{"platformer", []sdk.Module{combatModule{}}},
|
||||
{"physics-sandbox", nil},
|
||||
{"topdown", nil},
|
||||
} {
|
||||
t.Run(tc.dir, func(t *testing.T) {
|
||||
canonicalExample(t, filepath.Join("..", "..", "examples", tc.dir), tc.mods)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"aego/contracts/debugdraw"
|
||||
"aego/contracts/persistence"
|
||||
"aego/modules/debug"
|
||||
"aego/modules/gameplay"
|
||||
"aego/modules/physics"
|
||||
"aego/save"
|
||||
"aego/script/runtime"
|
||||
@@ -249,7 +250,7 @@ func TestKeyframeSeekInsideCoroutineWait(t *testing.T) {
|
||||
for i := 0; i < 240; i++ {
|
||||
pc.Tick(1.0 / 60)
|
||||
hashes[i+1] = pausedHash(pc)
|
||||
if h, ok := scene.Get[Health](pc.Scene(), pc.Scene().Find(pc.Scene().Root(), "Player")); ok && h.Hits > 0 && hit == 0 {
|
||||
if h, ok := scene.Get[gameplay.Health](pc.Scene(), pc.Scene().Find(pc.Scene().Root(), "Player")); ok && h.Current < h.Max && hit == 0 {
|
||||
hit = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,8 +312,8 @@ func TestPlatformerEnemyPrefabs(t *testing.T) {
|
||||
if !ok || !pi.Owner || s.Local(h).Position.X != x {
|
||||
t.Fatalf("%s must be an instance at x=%v", name, x)
|
||||
}
|
||||
if !hasComponent(s, h, "physics.Body2D") || !hasComponent(s, h, "physics.PolygonCollider2D") {
|
||||
t.Fatalf("%s lost its body or stomp trigger", name)
|
||||
if !hasComponent(s, h, "physics.Body2D") || !hasComponent(s, h, "gameplay.Hurtbox2D") || !hasComponent(s, h, "gameplay.Hitbox2D") {
|
||||
t.Fatalf("%s lost its body, hurtbox or hitbox", name)
|
||||
}
|
||||
}
|
||||
out := string(sceneio.SaveWith(s, lib))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"aego/app/edit"
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
"aego/core/rewind"
|
||||
"aego/editor/core/play"
|
||||
"aego/modules/camera"
|
||||
inputmod "aego/modules/input"
|
||||
"aego/modules/physics"
|
||||
"aego/platform"
|
||||
"aego/platform/input"
|
||||
@@ -180,31 +182,33 @@ func cameraHash(t *testing.T, pc *play.Controller) [32]byte {
|
||||
}
|
||||
|
||||
func TestEditorRewindKeepsTheCamera(t *testing.T) {
|
||||
s, pc := rewindSession(t)
|
||||
in := s.Host.Engine.Input()
|
||||
hold := func(k input.Scancode, down bool) {
|
||||
kind := platform.EvKeyUp
|
||||
if down {
|
||||
kind = platform.EvKeyDown
|
||||
}
|
||||
platform.ApplyInput(in, platform.Event{Kind: kind, Key: input.KeyEvent{Scan: k}})
|
||||
_, pc := rewindSession(t)
|
||||
data, err := os.ReadFile(runFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
actions := inputmod.Of(pc.World())
|
||||
if actions == nil {
|
||||
t.Fatal("the play world has no input service")
|
||||
}
|
||||
if _, err := actions.Replay(data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
const before, inside = 20, 330
|
||||
marks := map[int][32]byte{}
|
||||
for i := 0; i < 240; i++ {
|
||||
hold(input.KeyD, i >= 10 && i < 200)
|
||||
hold(input.KeySpace, i >= 40 && i < 44)
|
||||
for i := 1; i <= 360; i++ {
|
||||
pc.Tick(1.0 / 60)
|
||||
if i+1 == 20 || i+1 == 120 {
|
||||
marks[i+1] = cameraHash(t, pc)
|
||||
if i == before || i == inside {
|
||||
marks[i] = cameraHash(t, pc)
|
||||
}
|
||||
}
|
||||
if camera.Of(pc.World()).Active("").IsZero() {
|
||||
t.Fatal("the platformer camera has no active virtual camera")
|
||||
}
|
||||
if marks[20] == marks[120] {
|
||||
if marks[before] == marks[inside] {
|
||||
t.Fatal("the camera state is the same before and inside the arena zone")
|
||||
}
|
||||
for _, f := range []int{120, 20, 120} {
|
||||
for _, f := range []int{inside, before, inside} {
|
||||
if err := pc.Seek(f); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -31,23 +31,25 @@ func TestComponentSignalReachesScriptAndGo(t *testing.T) {
|
||||
root := copyExample(t, "platformer")
|
||||
patch(t, filepath.Join(root, "scripts", "player", "player.aes"),
|
||||
"\tvar blink Coroutine\n", "\tvar blink Coroutine\n\tvar hitConn Connection\n\tvar signalHits int\n",
|
||||
"\t\twatch(EnemyB)\n", "\t\twatch(EnemyB)\n\t\thitConn = node.Get<Health>().Hit.Connect(OnHit)\n",
|
||||
"\t\twatch(EnemyB)\n", "\t\twatch(EnemyB)\n\t\thitConn = node.Get<Health>().Healed.Connect(OnHit)\n",
|
||||
"\tfunc OnEnemyDied() {", "\tfunc OnHit(hits int) {\n\t\tsignalHits = hits\n\t}\n\n\tfunc OnEnemyDied() {")
|
||||
pf := platformerAt(t, root)
|
||||
pf.step(1)
|
||||
var goHits int64
|
||||
ctx := &sdk.Context{World: pf.app.Engine.MainWorld(), Engine: pf.app.Engine}
|
||||
if _, err := sdk.Connect1[int64](ctx, pf.player, "Health.Hit", func(n int64) { goHits = n }); err != nil {
|
||||
if _, err := sdk.Connect1[int64](ctx, pf.player, "Health.Healed", func(n int64) { goHits = n }); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pf.step(29)
|
||||
pf.hitPlayer(t)
|
||||
hits := pf.health().Hits
|
||||
pf.health().Heal(1)
|
||||
pf.step(1)
|
||||
hits := int64(1)
|
||||
if got := pf.field("signalHits").Int(); got != hits {
|
||||
t.Fatalf("script handler saw %d hits, health has %d", got, hits)
|
||||
t.Fatalf("script handler saw %d healed, want %d", got, hits)
|
||||
}
|
||||
if goHits != hits {
|
||||
t.Fatalf("Go handler saw %d hits, health has %d", goHits, hits)
|
||||
t.Fatalf("Go handler saw %d healed, want %d", goHits, hits)
|
||||
}
|
||||
if !pf.app.Scripts.SignalConnected(pf.field("hitConn")) {
|
||||
t.Fatal("the script connection must stay connected")
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
aeinput 1
|
||||
project platformer
|
||||
seed 0
|
||||
fixedHz 60
|
||||
players 1
|
||||
actions Move Jump Attack Pause Back QuickSave QuickLoad ToggleSettings
|
||||
frames 900
|
||||
s 1 0 3 3f800000 0
|
||||
s 31 1 3 3f800000 0
|
||||
s 71 0 1 3f5ea880 0
|
||||
s 71 1 4 0 0
|
||||
s 72 0 1 3f384200 0
|
||||
s 73 0 1 3f161fc0 0
|
||||
s 74 0 4 3ec5d900 0
|
||||
s 75 0 0 3df54200 0
|
||||
s 76 0 0 be055f00 0
|
||||
s 77 0 0 beba2680 0
|
||||
s 78 0 3 bf13b040 0
|
||||
s 79 0 1 bf452e80 0
|
||||
s 80 0 1 bf718e00 0
|
||||
s 81 0 1 bf800000 0
|
||||
s 96 0 1 bf7f3440 0
|
||||
s 97 0 1 bf58cdc0 0
|
||||
s 98 0 1 bf3296c0 0
|
||||
s 99 0 1 bf10a400 0
|
||||
s 100 0 4 bee5eb00 0
|
||||
s 101 0 0 beb31680 0
|
||||
s 102 0 0 be88cb00 0
|
||||
s 103 0 0 be4e1000 0
|
||||
s 104 0 0 be1b9b00 0
|
||||
s 105 0 0 bdf46e00 0
|
||||
s 106 0 0 bdd3c800 0
|
||||
s 107 0 0 bdd54400 0
|
||||
s 108 0 3 3f800000 0
|
||||
s 149 0 4 0 0
|
||||
s 163 0 3 3f800000 0
|
||||
s 163 1 3 3f800000 0
|
||||
s 203 1 4 0 0
|
||||
s 225 0 1 bf800000 0
|
||||
s 245 0 1 3f800000 0
|
||||
s 269 0 1 bf800000 0
|
||||
s 280 0 1 3f800000 0
|
||||
s 296 0 1 bf800000 0
|
||||
s 307 0 1 3f800000 0
|
||||
s 321 0 4 3e862f80 0
|
||||
s 322 0 0 3df3e400 0
|
||||
s 323 0 0 bbed4000 0
|
||||
s 324 0 0 bdef6a00 0
|
||||
s 325 0 0 be538500 0
|
||||
s 326 0 0 be8d6d00 0
|
||||
s 327 0 0 bea6da80 0
|
||||
s 328 0 0 beb60a00 0
|
||||
s 329 0 0 bebafc80 0
|
||||
s 330 0 0 beb5b180 0
|
||||
s 331 0 0 bea62900 0
|
||||
s 332 0 0 be8c6300 0
|
||||
s 333 0 0 be50bf00 0
|
||||
s 334 0 0 bde87a00 0
|
||||
s 335 0 0 bb500000 0
|
||||
s 336 0 0 3dda6800 0
|
||||
s 337 0 0 3e4c9800 0
|
||||
s 338 0 0 3e8bc080 0
|
||||
s 339 0 3 3f800000 0
|
||||
s 391 0 4 0 0
|
||||
@@ -151,7 +151,7 @@ func writeMeta(path, id, importer string, version int, s pipeline.Settings) erro
|
||||
}
|
||||
|
||||
func encodeOgg(bin string, src []byte) ([]byte, error) {
|
||||
cmd := exec.Command(bin, "-hide_banner", "-loglevel", "error", "-f", "wav", "-i", "pipe:0", "-c:a", "libvorbis", "-q:a", "1", "-f", "ogg", "pipe:1")
|
||||
cmd := exec.Command(bin, "-hide_banner", "-loglevel", "error", "-f", "wav", "-i", "pipe:0", "-c:a", "libvorbis", "-q:a", "3", "-f", "ogg", "pipe:1")
|
||||
cmd.Stdin = bytes.NewReader(src)
|
||||
var out, stderr bytes.Buffer
|
||||
cmd.Stdout, cmd.Stderr = &out, &stderr
|
||||
|
||||
@@ -113,15 +113,18 @@ func (s *song) drumBar(bar int) {
|
||||
s.snare(s.at(bar, b), 0.45+0.15*(b-3)*4)
|
||||
}
|
||||
}
|
||||
if bar%8 == 0 {
|
||||
s.cymbal(s.at(bar, 0), 0.45, 0.9, 4500, 0.5)
|
||||
}
|
||||
for e := range 8 {
|
||||
v, decay := 0.55, 0.022
|
||||
v, decay := 0.55, 0.035
|
||||
if e%2 == 1 {
|
||||
v = 1
|
||||
}
|
||||
if e == 7 && bar%2 == 1 {
|
||||
decay = 0.11
|
||||
v, decay = 0.9, 0.16
|
||||
}
|
||||
s.hat(s.at(bar, float64(e)*0.5), v, decay)
|
||||
s.cymbal(s.at(bar, float64(e)*0.5), v, decay, 7000, 0.1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,15 +160,34 @@ func (s *song) snare(at int, v float64) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *song) hat(at int, v, decay float64) {
|
||||
n := int((decay*6 + 0.01) * rate)
|
||||
p1, p2 := 0.0, 0.0
|
||||
var metal = [6]float64{205.3, 304.4, 369.6, 522.7, 540, 800}
|
||||
|
||||
// cymbal is six detuned square waves and noise through a three-pole high-pass, like an 808's metal circuit.
|
||||
func (s *song) cymbal(at int, v, decay, cutoff, wet float64) {
|
||||
n := int((decay*5 + 0.01) * rate)
|
||||
var ph [6]float64
|
||||
a := onePole(cutoff)
|
||||
l1, l2, l3 := 0.0, 0.0, 0.0
|
||||
v *= 0.9 + 0.1*s.rnd.next()
|
||||
for j := range n {
|
||||
t := float64(j) / rate
|
||||
x := s.rnd.next()
|
||||
h := x - 2*p1 + p2
|
||||
p2, p1 = p1, x
|
||||
s.add(s.drums, at+j, 0.045*v*h*math.Exp(-t/decay)*edge(j, n, rate/2000))
|
||||
m := 0.0
|
||||
for k, f := range metal {
|
||||
dt := f / rate
|
||||
ph[k] += dt
|
||||
ph[k] -= math.Floor(ph[k])
|
||||
q := ph[k] + 0.5
|
||||
m += blepSaw(ph[k], dt) - blepSaw(q-math.Floor(q), dt)
|
||||
}
|
||||
x := m/6 + 0.5*s.rnd.next()
|
||||
l1 += a * (x - l1)
|
||||
h1 := x - l1
|
||||
l2 += a * (h1 - l2)
|
||||
h2 := h1 - l2
|
||||
l3 += a * (h2 - l3)
|
||||
y := 2 * v * (h2 - l3) * math.Exp(-t/decay) * math.Min(t/0.0015, 1) * edge(j, n, rate/2000)
|
||||
s.add(s.drums, at+j, y)
|
||||
s.add(s.reverb, at+j, wet*y)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.8 KiB |