2 Commits
Author SHA1 Message Date
sanspie 54693f97d2 Music: metallic hi-hats and a crash per phrase instead of noise ticks; encode the track at vorbis q3
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 1s
ci / release (push) Skipped
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Failing after 0s
2026-10-22 11:58:43 +03:00
sanspie 080948f434 Stage 10c3: platformer and top-down examples on the gameplay kit 2026-10-22 11:58:38 +03:00
50 changed files with 2126 additions and 396 deletions
+1
View File
@@ -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
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 126 KiB

+1 -1
View File
@@ -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 |
|---|---|
Binary file not shown.
-34
View File
@@ -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
}
+13 -8
View File
@@ -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")
}
File diff suppressed because it is too large Load Diff
+8 -30
View File
@@ -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)
}
}
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
.aego/
exports/
Binary file not shown.

After

Width:  |  Height:  |  Size: 596 B

+67
View File
@@ -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]
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,6 @@
meta version=1 {
id="70d0000000000000000000000000a001"
importer="scene"
importerVersion=1
settings {}
}
+58
View File
@@ -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 {}
}
+43
View File
@@ -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 {}
}
+2
View File
@@ -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 |
+4 -4
View File
@@ -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)
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-5
View File
@@ -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",
+1 -21
View File
@@ -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
}
+5 -5
View File
@@ -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))
}
}
+1
View File
@@ -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)
+2 -1
View File
@@ -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
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -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))
+18 -14
View File
@@ -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
_, pc := rewindSession(t)
data, err := os.ReadFile(runFile)
if err != nil {
t.Fatal(err)
}
platform.ApplyInput(in, platform.Event{Kind: kind, Key: input.KeyEvent{Scan: k}})
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)
}
+7 -5
View File
@@ -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")
+64
View File
@@ -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
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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
+32 -10
View File
@@ -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)
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB