Thief and Inventory APIs

This commit adds the Thief character with starter graphics
(no animations).

The Thief walks back and forth and will steal items from other
doodads, including the player. For singleton items that have no
quantity, like the Colored Keys, the Thief will only steal one
if he does not already have it. Quantitied items like the
Small Key are always stolen.

Flexibility in the playable character is introduced: Boy,
Azulian, Bird, and Thief all respond to playable controls.
There is not currently a method to enable these apart from
modifying balance.PlayerCharacterDoodad at compile time.

New and Changed Doodads

* Thief: new doodad that walks back and forth and will steal
  items from other characters inventory.
* Bird: has no inventory and cannot pick up items, unless player
  controlled. Its hitbox has also been fixed so it collides with
  floors correctly - not something normally seen in the Bird.
* Boy: opts in to have inventory.
* Keys (all): only gives themselves to actors having inventories.

JavaScript API - New functions available

* Self.IsPlayer() - returns if the current actor IS the player.
* Self.SetInventory(bool) - doodads must opt-in to having an
  inventory. Keys should only give themselves to doodads having
  an inventory.
* Self.HasInventory() bool
* Self.AddItem(filename, qty)
* Self.RemoveItem(filename, qty)
* Self.HasItem(filename)
* Self.Inventory() - returns map[string]int
* Self.ClearInventory()
* Self.OnLeave(func(e)) now receives a CollideEvent as parameter
  instead of the useless actor ID. Notably, e.Actor is the
  leaving actor and e.Settled is always true.

Other Changes

* Play Mode: if playing as a character which doesn't obey gravity,
  such as the bird, antigravity controls are enabled by default.
  If you `import antigravity` you can turn gravity back on.
* Doodad collision scripts are no longer run in parallel
  goroutines. It made the Thief's job difficult trying to steal
  items in many threads simultaneously!
This commit is contained in:
Noah 2021-08-09 22:42:22 -07:00
parent 0518df226c
commit d7a96d1770
16 changed files with 225 additions and 36 deletions

View File

@ -7,6 +7,7 @@ function main() {
Self.SetHitbox(0, 0, 32, 32) Self.SetHitbox(0, 0, 32, 32)
Self.SetMobile(true); Self.SetMobile(true);
Self.SetInventory(true);
Self.SetGravity(true); Self.SetGravity(true);
Self.AddAnimation("walk-left", 100, ["red-wl1", "red-wl2", "red-wl3", "red-wl4"]); Self.AddAnimation("walk-left", 100, ["red-wl1", "red-wl2", "red-wl3", "red-wl4"]);
Self.AddAnimation("walk-right", 100, ["red-wr1", "red-wr2", "red-wr3", "red-wr4"]); Self.AddAnimation("walk-right", 100, ["red-wr1", "red-wr2", "red-wr3", "red-wr4"]);
@ -16,7 +17,7 @@ function main() {
var sampleRate = 5; var sampleRate = 5;
var lastSampledX = 0; var lastSampledX = 0;
setInterval(function() { setInterval(function () {
if (sampleTick % sampleRate === 0) { if (sampleTick % sampleRate === 0) {
var curX = Self.Position().X; var curX = Self.Position().X;
var delta = Math.abs(curX - lastSampledX); var delta = Math.abs(curX - lastSampledX);
@ -33,7 +34,7 @@ function main() {
Self.SetVelocity(Vector(Vx, 0.0)); Self.SetVelocity(Vector(Vx, 0.0));
if (!Self.IsAnimating()) { if (!Self.IsAnimating()) {
Self.PlayAnimation("walk-"+direction, null); Self.PlayAnimation("walk-" + direction, null);
} }
}, 100); }, 100);
} }

View File

@ -9,11 +9,12 @@ function main() {
Self.SetMobile(true); Self.SetMobile(true);
Self.SetGravity(true); Self.SetGravity(true);
Self.SetInventory(true);
Self.SetHitbox(7, 4, 17, 28); Self.SetHitbox(7, 4, 17, 28);
Self.AddAnimation("walk-left", 100, ["blu-wl1", "blu-wl2", "blu-wl3", "blu-wl4"]); Self.AddAnimation("walk-left", 100, ["blu-wl1", "blu-wl2", "blu-wl3", "blu-wl4"]);
Self.AddAnimation("walk-right", 100, ["blu-wr1", "blu-wr2", "blu-wr3", "blu-wr4"]); Self.AddAnimation("walk-right", 100, ["blu-wr1", "blu-wr2", "blu-wr3", "blu-wr4"]);
Events.OnKeypress(function(ev) { Events.OnKeypress(function (ev) {
Vx = 0; Vx = 0;
Vy = 0; Vy = 0;

View File

@ -1,4 +1,5 @@
// Red bird mob. // Bird
function main() { function main() {
var speed = 4; var speed = 4;
var Vx = Vy = 0; var Vx = Vy = 0;
@ -13,11 +14,16 @@ function main() {
Self.SetMobile(true); Self.SetMobile(true);
Self.SetGravity(false); Self.SetGravity(false);
Self.SetHitbox(0, 10, 46, 32); Self.SetHitbox(0, 0, 46, 32);
Self.AddAnimation("fly-left", 100, ["left-1", "left-2"]); Self.AddAnimation("fly-left", 100, ["left-1", "left-2"]);
Self.AddAnimation("fly-right", 100, ["right-1", "right-2"]); Self.AddAnimation("fly-right", 100, ["right-1", "right-2"]);
Events.OnCollide(function(e) { // Player Character controls?
if (Self.IsPlayer()) {
return player();
}
Events.OnCollide(function (e) {
if (e.Actor.IsMobile() && e.InHitbox) { if (e.Actor.IsMobile() && e.InHitbox) {
return false; return false;
} }
@ -29,7 +35,7 @@ function main() {
var lastSampledX = 0; var lastSampledX = 0;
var lastSampledY = 0; var lastSampledY = 0;
setInterval(function() { setInterval(function () {
if (sampleTick % sampleRate === 0) { if (sampleTick % sampleRate === 0) {
var curX = Self.Position().X; var curX = Self.Position().X;
var delta = Math.abs(curX - lastSampledX); var delta = Math.abs(curX - lastSampledX);
@ -46,7 +52,39 @@ function main() {
Self.SetVelocity(Vector(Vx, 0.0)); Self.SetVelocity(Vector(Vx, 0.0));
if (!Self.IsAnimating()) { if (!Self.IsAnimating()) {
Self.PlayAnimation("fly-"+direction, null); Self.PlayAnimation("fly-" + direction, null);
} }
}, 100); }, 100);
} }
// If under control of the player character.
function player() {
Self.SetInventory(true);
Events.OnKeypress(function (ev) {
Vx = 0;
Vy = 0;
if (ev.Up) {
Vy = -playerSpeed;
} else if (ev.Down) {
Vy = playerSpeed;
}
if (ev.Right) {
if (!Self.IsAnimating()) {
Self.PlayAnimation("fly-right", null);
}
Vx = playerSpeed;
} else if (ev.Left) {
if (!Self.IsAnimating()) {
Self.PlayAnimation("fly-left", null);
}
Vx = -playerSpeed;
} else {
Self.StopAnimation();
animating = false;
}
Self.SetVelocity(Vector(Vx, Vy));
})
}

View File

@ -8,12 +8,13 @@ function main() {
var animFrame = animStart; var animFrame = animStart;
Self.SetMobile(true); Self.SetMobile(true);
Self.SetInventory(true);
Self.SetGravity(true); Self.SetGravity(true);
Self.SetHitbox(0, 0, 32, 52); Self.SetHitbox(0, 0, 32, 52);
Self.AddAnimation("walk-left", 200, ["stand-left", "walk-left-1", "walk-left-2", "walk-left-3", "walk-left-2", "walk-left-1"]); Self.AddAnimation("walk-left", 200, ["stand-left", "walk-left-1", "walk-left-2", "walk-left-3", "walk-left-2", "walk-left-1"]);
Self.AddAnimation("walk-right", 200, ["stand-right", "walk-right-1", "walk-right-2", "walk-right-3", "walk-right-2", "walk-right-1"]); Self.AddAnimation("walk-right", 200, ["stand-right", "walk-right-1", "walk-right-2", "walk-right-3", "walk-right-2", "walk-right-1"]);
Events.OnKeypress(function(ev) { Events.OnKeypress(function (ev) {
Vx = 0; Vx = 0;
Vy = 0; Vy = 0;

View File

@ -12,6 +12,10 @@ boy() {
cd boy/ cd boy/
make make
cd .. cd ..
cd thief/
make
cd ..
} }
buttons() { buttons() {

View File

@ -2,11 +2,13 @@ function main() {
var color = Self.GetTag("color"); var color = Self.GetTag("color");
var quantity = color === "small" ? 1 : 0; var quantity = color === "small" ? 1 : 0;
Events.OnCollide(function(e) { Events.OnCollide(function (e) {
if (e.Settled) { if (e.Settled) {
if (e.Actor.HasInventory()) {
Sound.Play("item-get.wav") Sound.Play("item-get.wav")
e.Actor.AddItem(Self.Filename, quantity); e.Actor.AddItem(Self.Filename, quantity);
Self.Destroy(); Self.Destroy();
} }
}
}) })
} }

View File

@ -0,0 +1,11 @@
ALL: build
.PHONY: build
build:
doodad convert -t "Thief" stand-right.png stand-left.png \
thief.doodad
doodad install-script thief.js thief.doodad
doodad edit-doodad --tag "category=creatures" thief.doodad
cp *.doodad ../../../assets/doodads/

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,104 @@
// Thief
function main() {
Self.SetMobile(true);
Self.SetGravity(true);
Self.SetInventory(true);
Self.SetHitbox(0, 0, 32, 58);
Self.AddAnimation("walk-left", 200, ["stand-left"]); //, "walk-left-1", "walk-left-2", "walk-left-3", "walk-left-2", "walk-left-1"]);
Self.AddAnimation("walk-right", 200, ["stand-right"]); //, "walk-right-1", "walk-right-2", "walk-right-3", "walk-right-2", "walk-right-1"]);
// Controlled by the player character?
if (Self.IsPlayer()) {
return playable();
}
return ai();
}
// Enemy Doodad AI.
function ai() {
// Walks back and forth.
var Vx = Vy = 0.0,
playerSpeed = 4,
direction = "right",
lastSampledX = 0,
sampleTick = 0,
sampleRate = 2,
stolenItems = {}; // map item->qty
setInterval(function () {
if (sampleTick % sampleRate === 0) {
var curX = Self.Position().X,
delta = Math.abs(curX - lastSampledX);
if (delta < 5) {
direction = direction === "right" ? "left" : "right";
}
lastSampledX = curX;
}
sampleTick++;
Vx = parseFloat(playerSpeed * (direction === "left" ? -1 : 1));
Self.SetVelocity(Vector(Vx, Vy));
Self.StopAnimation();
Self.PlayAnimation("walk-" + direction, null);
}, 100);
// Steals your items.
Events.OnCollide(function (e) {
if (!e.Settled) {
return;
}
// Steal inventory
var stolen = 0;
if (e.Actor.HasInventory()) {
var myInventory = Self.Inventory(),
theirInventory = e.Actor.Inventory();
for (var key in theirInventory) {
if (!theirInventory.hasOwnProperty(key)) {
continue;
}
var value = theirInventory[key];
if (value > 0 || myInventory[key] === undefined) {
e.Actor.RemoveItem(key, value);
Self.AddItem(key, value);
stolenItems[key] = value;
stolen += (value === 0 ? 1 : value);
}
}
// Notify the player if it was them.
if (e.Actor.IsPlayer() && stolen > 0) {
Flash("Watch out for thieves! %d item%s stolen!", parseInt(stolen), stolen === 1 ? ' was' : 's were');
}
}
});
}
// If under control of the player character.
function playable() {
Events.OnKeypress(function (ev) {
Vx = 0;
Vy = 0;
if (ev.Right) {
if (!Self.IsAnimating()) {
Self.PlayAnimation("walk-right", null);
}
Vx = playerSpeed;
} else if (ev.Left) {
if (!Self.IsAnimating()) {
Self.PlayAnimation("walk-left", null);
}
Vx = -playerSpeed;
} else {
Self.StopAnimation();
animating = false;
}
// Self.SetVelocity(Point(Vx, Vy));
})
}

View File

@ -538,7 +538,7 @@ func (s *PlayScene) movePlayer(ev *event.State) {
) )
// Antigravity: player can move anywhere with arrow keys. // Antigravity: player can move anywhere with arrow keys.
if s.antigravity { if s.antigravity || !s.Player.HasGravity() {
velocity.X = 0 velocity.X = 0
velocity.Y = 0 velocity.Y = 0

View File

@ -19,8 +19,6 @@ This adds the global methods `Message.Subscribe(name, func)` and
`Message.Publish(name, args)` to the JavaScript VM's scope. `Message.Publish(name, args)` to the JavaScript VM's scope.
*/ */
func RegisterPublishHooks(s *Supervisor, vm *VM) { func RegisterPublishHooks(s *Supervisor, vm *VM) {
log.Error("RegisterPublishHooks called with %+v %+v", s, vm)
// Goroutine to watch the VM's inbound channel and invoke Subscribe handlers // Goroutine to watch the VM's inbound channel and invoke Subscribe handlers
// for any matching messages received. // for any matching messages received.
go func() { go func() {

View File

@ -35,6 +35,7 @@ type Actor struct {
// Actor runtime variables. // Actor runtime variables.
hasGravity bool hasGravity bool
hasInventory bool
isMobile bool // Mobile character, such as the player or an enemy isMobile bool // Mobile character, such as the player or an enemy
noclip bool // Disable collision detection noclip bool // Disable collision detection
hidden bool // invisible, via Hide() and Show() hidden bool // invisible, via Hide() and Show()
@ -114,6 +115,11 @@ func (a *Actor) SetMobile(v bool) {
a.isMobile = v a.isMobile = v
} }
// SetInventory configures whether the actor is capable of carrying items.
func (a *Actor) SetInventory(v bool) {
a.hasInventory = true
}
// IsMobile returns whether the actor is a mobile character. // IsMobile returns whether the actor is a mobile character.
func (a *Actor) IsMobile() bool { func (a *Actor) IsMobile() bool {
return a.isMobile return a.isMobile
@ -125,6 +131,16 @@ func (a *Actor) IsPlayer() bool {
return a.Canvas.Name == "PLAYER" return a.Canvas.Name == "PLAYER"
} }
// HasInventory returns if the actor is capable of carrying items.
func (a *Actor) HasInventory() bool {
return a.hasInventory
}
// HasGravity returns if gravity applies to the actor.
func (a *Actor) HasGravity() bool {
return a.hasGravity
}
// Size returns the size of the actor, from the underlying doodads.Drawing. // Size returns the size of the actor, from the underlying doodads.Drawing.
func (a *Actor) Size() render.Rect { func (a *Actor) Size() render.Rect {
return a.Drawing.Size() return a.Drawing.Size()

View File

@ -2,7 +2,6 @@ package uix
import ( import (
"errors" "errors"
"sync"
"time" "time"
"git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/balance"
@ -36,11 +35,14 @@ func (w *Canvas) loopActorCollision() error {
// Loop over all the actors in parallel, processing their movement and // Loop over all the actors in parallel, processing their movement and
// checking collision data against the level geometry. // checking collision data against the level geometry.
var wg sync.WaitGroup // NOTE: parallelism wasn't good for race conditions like the Thief
// trying to take your inventory.
// var wg sync.WaitGroup
for i, a := range w.actors { for i, a := range w.actors {
wg.Add(1) // wg.Add(1)
go func(i int, a *Actor) { //go
defer wg.Done() func(i int, a *Actor) {
// defer wg.Done()
originalPositions[a.ID()] = a.Position() originalPositions[a.ID()] = a.Position()
// Advance any animations for this actor. // Advance any animations for this actor.
@ -114,10 +116,10 @@ func (w *Canvas) loopActorCollision() error {
// Store this actor's bounding box after they've moved. // Store this actor's bounding box after they've moved.
boxes[i] = collision.SizePlusHitbox(collision.GetBoundingRect(a), a.Hitbox()) boxes[i] = collision.SizePlusHitbox(collision.GetBoundingRect(a), a.Hitbox())
}(i, a) }(i, a)
wg.Wait() // wg.Wait()
} }
var collidingActors = map[string]string{} var collidingActors = map[*Actor]*Actor{}
for tuple := range collision.BetweenBoxes(boxes) { for tuple := range collision.BetweenBoxes(boxes) {
a, b := w.actors[tuple.A], w.actors[tuple.B] a, b := w.actors[tuple.A], w.actors[tuple.B]
@ -126,7 +128,7 @@ func (w *Canvas) loopActorCollision() error {
continue continue
} }
collidingActors[a.ID()] = b.ID() collidingActors[a] = b
// log.Error("between boxes: %+v <%s> <%s>", tuple, a.ID(), b.ID()) // log.Error("between boxes: %+v <%s> <%s>", tuple, a.ID(), b.ID())
@ -293,9 +295,12 @@ func (w *Canvas) loopActorCollision() error {
} }
// Check for lacks of collisions since last frame. // Check for lacks of collisions since last frame.
for sourceID, targetID := range w.collidingActors { for sourceActor, targetActor := range w.collidingActors {
if _, ok := collidingActors[sourceID]; !ok { if _, ok := collidingActors[sourceActor]; !ok {
w.scripting.To(sourceID).Events.RunLeave(targetID) w.scripting.To(sourceActor.ID()).Events.RunLeave(&CollideEvent{
Actor: targetActor,
Settled: true,
})
} }
} }

View File

@ -61,7 +61,7 @@ type Canvas struct {
actors []*Actor // if this canvas CONTAINS actors (i.e., is a level) actors []*Actor // if this canvas CONTAINS actors (i.e., is a level)
// Collision memory for the actors. // Collision memory for the actors.
collidingActors map[string]string // mapping their IDs to each other collidingActors map[*Actor]*Actor // mapping their IDs to each other
// Doodad scripting engine supervisor. // Doodad scripting engine supervisor.
// NOTE: initialized and managed by the play_scene. // NOTE: initialized and managed by the play_scene.

View File

@ -22,13 +22,21 @@ func (w *Canvas) MakeSelfAPI(actor *Actor) map[string]interface{} {
"SetHitbox": actor.SetHitbox, "SetHitbox": actor.SetHitbox,
"SetVelocity": actor.SetVelocity, "SetVelocity": actor.SetVelocity,
"SetMobile": actor.SetMobile, "SetMobile": actor.SetMobile,
"SetInventory": actor.SetInventory,
"HasInventory": actor.HasInventory,
"SetGravity": actor.SetGravity, "SetGravity": actor.SetGravity,
"AddAnimation": actor.AddAnimation, "AddAnimation": actor.AddAnimation,
"IsAnimating": actor.IsAnimating, "IsAnimating": actor.IsAnimating,
"IsPlayer": actor.IsPlayer,
"PlayAnimation": actor.PlayAnimation, "PlayAnimation": actor.PlayAnimation,
"StopAnimation": actor.StopAnimation, "StopAnimation": actor.StopAnimation,
"ShowLayer": actor.ShowLayer, "ShowLayer": actor.ShowLayer,
"ShowLayerNamed": actor.ShowLayerNamed, "ShowLayerNamed": actor.ShowLayerNamed,
"Inventory": actor.Inventory,
"AddItem": actor.AddItem,
"RemoveItem": actor.RemoveItem,
"HasItem": actor.HasItem,
"ClearInventory": actor.ClearInventory,
"Destroy": actor.Destroy, "Destroy": actor.Destroy,
"GetLinks": func() []map[string]interface{} { "GetLinks": func() []map[string]interface{} {
var result = []map[string]interface{}{} var result = []map[string]interface{}{}