Add Technical Doodads + UI Fixes
New category for the Doodad Dropper: "Technical" Technical doodads have a dashed outline and label for now, and they turn invisible on level start, and are for hidden technical effects on your level. The doodads include: * Goal Region: acts like an invisible Exit Flag (128x128), the level is won when the player character touches this region. * Fire Region: acts like a death barrier (128x128), kills the player when a generic "You have died!" message. * Power Source: on level start, acts like a switch and emits a power(true) signal to all linked doodads. Link it to your Electric Door for it to be open by default in your level! * Stall Player (250ms): The player is paused for a moment the first time it touches this region. Useful to work around timing issues, e.g. help prevent the player from winning a race against another character. There are some UI improvements to the Doodad Dropper window: * If the first page of doodads is short, extra spacers are added so the alignment and size shows correctly. * Added a 'background pattern' to the window: any unoccupied icon space has an inset rectangle slot. * "Last pages" which are short still render weirdly without reserving the correct height in the TabFrame. Doodad scripting engine updates: * Self.Hide() and Self.Show() available. * Subscribe to "broadcast:ready" to know when the level is ready, so you can safely Publish messages without deadlocks!
This commit is contained in:
parent
df3a1679b6
commit
97e179716c
|
@ -66,6 +66,10 @@ objects() {
|
||||||
cd crumbly-floor/
|
cd crumbly-floor/
|
||||||
make
|
make
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
|
cd regions/
|
||||||
|
make
|
||||||
|
cd ..
|
||||||
}
|
}
|
||||||
|
|
||||||
onoff() {
|
onoff() {
|
||||||
|
|
25
dev-assets/doodads/regions/Makefile
Normal file
25
dev-assets/doodads/regions/Makefile
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
ALL: build
|
||||||
|
|
||||||
|
.PHONY: build
|
||||||
|
build:
|
||||||
|
# Goal Region
|
||||||
|
doodad convert -t "Goal Region" goal-128.png reg-goal.doodad
|
||||||
|
doodad install-script goal.js reg-goal.doodad
|
||||||
|
|
||||||
|
# Fire Region
|
||||||
|
doodad convert -t "Fire Region" fire-128.png reg-fire.doodad
|
||||||
|
doodad install-script fire.js reg-fire.doodad
|
||||||
|
|
||||||
|
# Stall Region
|
||||||
|
doodad convert -t "Stall Player (250ms)" stall-128.png reg-stall-250.doodad
|
||||||
|
doodad edit-doodad --tag "ms=250" reg-stall-250.doodad
|
||||||
|
doodad install-script stall.js reg-stall-250.doodad
|
||||||
|
|
||||||
|
# Power Source
|
||||||
|
doodad convert -t "Power Source" power-64.png power-source.doodad
|
||||||
|
doodad install-script power.js power-source.doodad
|
||||||
|
|
||||||
|
for i in *.doodad; do\
|
||||||
|
doodad edit-doodad --tag "category=technical" $${i};\
|
||||||
|
done
|
||||||
|
cp *.doodad ../../../assets/doodads/
|
BIN
dev-assets/doodads/regions/fire-128.png
Normal file
BIN
dev-assets/doodads/regions/fire-128.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 965 B |
19
dev-assets/doodads/regions/fire.js
Normal file
19
dev-assets/doodads/regions/fire.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
// Goal Region.
|
||||||
|
function main() {
|
||||||
|
Self.Hide();
|
||||||
|
|
||||||
|
Events.OnCollide(function (e) {
|
||||||
|
if (!e.Settled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only care if it's the player.
|
||||||
|
if (!e.Actor.IsPlayer()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.InHitbox) {
|
||||||
|
FailLevel("You have died!");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
BIN
dev-assets/doodads/regions/goal-128.png
Normal file
BIN
dev-assets/doodads/regions/goal-128.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 986 B |
19
dev-assets/doodads/regions/goal.js
Normal file
19
dev-assets/doodads/regions/goal.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
// Goal Region.
|
||||||
|
function main() {
|
||||||
|
Self.Hide();
|
||||||
|
|
||||||
|
Events.OnCollide(function (e) {
|
||||||
|
if (!e.Settled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only care if it's the player.
|
||||||
|
if (!e.Actor.IsPlayer()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.InHitbox) {
|
||||||
|
EndLevel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
BIN
dev-assets/doodads/regions/power-128.png
Normal file
BIN
dev-assets/doodads/regions/power-128.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
BIN
dev-assets/doodads/regions/power-64.png
Normal file
BIN
dev-assets/doodads/regions/power-64.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 873 B |
22
dev-assets/doodads/regions/power.js
Normal file
22
dev-assets/doodads/regions/power.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// Power source.
|
||||||
|
// Emits a power(true) signal once on level start.
|
||||||
|
// If it receives a power signal, it will repeat it after 5 seconds.
|
||||||
|
// Link two of these bad boys together and you got yourself a clock.
|
||||||
|
function main() {
|
||||||
|
Self.Hide();
|
||||||
|
|
||||||
|
// See if we are not linked to anything.
|
||||||
|
var links = Self.GetLinks();
|
||||||
|
if (links.length === 0) {
|
||||||
|
console.error(
|
||||||
|
"%s at %s is not linked to anything! This doodad emits a power(true) on level start to all linked doodads.",
|
||||||
|
Self.Title,
|
||||||
|
Self.Position()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Message.Subscribe("broadcast:ready", function () {
|
||||||
|
Message.Publish("switch:toggle", true);
|
||||||
|
Message.Publish("power", true);
|
||||||
|
});
|
||||||
|
}
|
BIN
dev-assets/doodads/regions/stall-128.png
Normal file
BIN
dev-assets/doodads/regions/stall-128.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
40
dev-assets/doodads/regions/stall.js
Normal file
40
dev-assets/doodads/regions/stall.js
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
// Stall Player.
|
||||||
|
// Tags: ms (int)
|
||||||
|
// Grabs the player one time. Resets if it receives power.
|
||||||
|
function main() {
|
||||||
|
Self.Hide();
|
||||||
|
|
||||||
|
var active = true,
|
||||||
|
timeout = 250,
|
||||||
|
ms = Self.GetTag("ms");
|
||||||
|
|
||||||
|
if (ms.length > 0) {
|
||||||
|
timeout = parseInt(ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
Events.OnCollide(function (e) {
|
||||||
|
if (!active || !e.Settled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only care if it's the player.
|
||||||
|
if (!e.Actor.IsPlayer()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.InHitbox) {
|
||||||
|
// Grab hold of the player.
|
||||||
|
e.Actor.Freeze();
|
||||||
|
setTimeout(function () {
|
||||||
|
e.Actor.Unfreeze();
|
||||||
|
}, timeout);
|
||||||
|
|
||||||
|
active = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset the trap if powered by a button.
|
||||||
|
Message.Subscribe("power", function (powered) {
|
||||||
|
active = true;
|
||||||
|
});
|
||||||
|
}
|
|
@ -83,6 +83,15 @@ func (w *Canvas) InstallScripts() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Broadcast the "ready" signal to any actors that want to publish
|
||||||
|
// messages ASAP on level start.
|
||||||
|
for _, actor := range w.actors {
|
||||||
|
w.scripting.To(actor.ID()).Inbound <- scripting.Message{
|
||||||
|
Name: "broadcast:ready",
|
||||||
|
Args: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,8 @@ func (w *Canvas) MakeSelfAPI(actor *Actor) map[string]interface{} {
|
||||||
"HasItem": actor.HasItem,
|
"HasItem": actor.HasItem,
|
||||||
"ClearInventory": actor.ClearInventory,
|
"ClearInventory": actor.ClearInventory,
|
||||||
"Destroy": actor.Destroy,
|
"Destroy": actor.Destroy,
|
||||||
|
"Hide": actor.Hide,
|
||||||
|
"Show": actor.Show,
|
||||||
"GetLinks": func() []map[string]interface{} {
|
"GetLinks": func() []map[string]interface{} {
|
||||||
var result = []map[string]interface{}{}
|
var result = []map[string]interface{}{}
|
||||||
for _, linked := range w.GetLinkedActors(actor) {
|
for _, linked := range w.GetLinkedActors(actor) {
|
||||||
|
|
|
@ -91,6 +91,7 @@ func NewDoodadDropper(config DoodadDropper) *ui.Window {
|
||||||
{"doors", "Doors"},
|
{"doors", "Doors"},
|
||||||
{"gizmos", "Gizmos"},
|
{"gizmos", "Gizmos"},
|
||||||
{"creatures", "Creatures"},
|
{"creatures", "Creatures"},
|
||||||
|
{"technical", "Technical"},
|
||||||
{"", "All"},
|
{"", "All"},
|
||||||
}
|
}
|
||||||
for _, category := range categories {
|
for _, category := range categories {
|
||||||
|
@ -114,6 +115,10 @@ func makeDoodadTab(config DoodadDropper, frame *ui.Frame, size render.Rect, cate
|
||||||
columns = balance.DoodadDropperCols
|
columns = balance.DoodadDropperCols
|
||||||
rows = balance.DoodadDropperRows
|
rows = balance.DoodadDropperRows
|
||||||
|
|
||||||
|
// Count how many doodad buttons we need vs. how many can fit.
|
||||||
|
iconsDrawn int
|
||||||
|
iconsPossible = columns * rows
|
||||||
|
|
||||||
// pagination values
|
// pagination values
|
||||||
page = 1
|
page = 1
|
||||||
pages int
|
pages int
|
||||||
|
@ -144,6 +149,50 @@ func makeDoodadTab(config DoodadDropper, frame *ui.Frame, size render.Rect, cate
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// First, draw the empty grid of inset frames to serve as the 'background'
|
||||||
|
// of the drawer. This both serves an aesthetic purpose and reserves space
|
||||||
|
// in the widget for short page views.
|
||||||
|
{
|
||||||
|
var (
|
||||||
|
decorFrame = ui.NewFrame("Background Slots")
|
||||||
|
row *ui.Frame
|
||||||
|
)
|
||||||
|
|
||||||
|
for i := 0; i < iconsPossible; i++ {
|
||||||
|
if row == nil || i%columns == 0 {
|
||||||
|
row = ui.NewFrame("BG Row")
|
||||||
|
decorFrame.Pack(row, ui.Pack{
|
||||||
|
Side: ui.N,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
spacer := ui.NewFrame("Spacer")
|
||||||
|
spacer.Configure(ui.Config{
|
||||||
|
BorderSize: 2,
|
||||||
|
BorderStyle: ui.BorderSunken,
|
||||||
|
Background: render.Grey.Darken(20),
|
||||||
|
})
|
||||||
|
spacer.Resize(render.NewRect(
|
||||||
|
buttonSize-2, // TODO: without the -2 the button border
|
||||||
|
buttonSize-2, // rests on top of the window border
|
||||||
|
))
|
||||||
|
spacer.Compute(config.Engine)
|
||||||
|
row.Pack(spacer, ui.Pack{
|
||||||
|
Side: ui.W,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
decorFrame.Compute(config.Engine)
|
||||||
|
|
||||||
|
// frame.Pack(decorFrame, ui.Pack{
|
||||||
|
// Side: ui.NW,
|
||||||
|
// })
|
||||||
|
frame.Place(decorFrame, ui.Place{
|
||||||
|
Top: 0,
|
||||||
|
Left: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Draw the doodad buttons in rows.
|
// Draw the doodad buttons in rows.
|
||||||
var btnRows = []*ui.Frame{}
|
var btnRows = []*ui.Frame{}
|
||||||
{
|
{
|
||||||
|
@ -151,7 +200,8 @@ func makeDoodadTab(config DoodadDropper, frame *ui.Frame, size render.Rect, cate
|
||||||
row *ui.Frame
|
row *ui.Frame
|
||||||
rowCount int // for labeling the ui.Frame for each row
|
rowCount int // for labeling the ui.Frame for each row
|
||||||
|
|
||||||
// TODO: pre-size btnRows by calculating how many needed
|
// the state we end up at when we exhaust all doodads
|
||||||
|
lastColumn int // last position in current row
|
||||||
)
|
)
|
||||||
|
|
||||||
for i, doodad := range items {
|
for i, doodad := range items {
|
||||||
|
@ -162,17 +212,21 @@ func makeDoodadTab(config DoodadDropper, frame *ui.Frame, size render.Rect, cate
|
||||||
rowCount++
|
rowCount++
|
||||||
|
|
||||||
row = ui.NewFrame(fmt.Sprintf("Doodad Row %d", rowCount))
|
row = ui.NewFrame(fmt.Sprintf("Doodad Row %d", rowCount))
|
||||||
row.SetBackground(balance.DoodadButtonBackground)
|
|
||||||
|
row.Resize(render.NewRect(size.W, buttonSize))
|
||||||
|
row.Compute(config.Engine)
|
||||||
btnRows = append(btnRows, row)
|
btnRows = append(btnRows, row)
|
||||||
frame.Pack(row, ui.Pack{
|
frame.Pack(row, ui.Pack{
|
||||||
Side: ui.N,
|
Side: ui.N,
|
||||||
// Fill: true,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Hide overflowing rows until we scroll to them.
|
// Hide overflowing rows until we page to them.
|
||||||
if hidden {
|
if hidden {
|
||||||
row.Hide()
|
row.Hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New row, new columns.
|
||||||
|
lastColumn = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
can := uix.NewCanvas(int(buttonSize), true)
|
can := uix.NewCanvas(int(buttonSize), true)
|
||||||
|
@ -213,6 +267,62 @@ func makeDoodadTab(config DoodadDropper, frame *ui.Frame, size render.Rect, cate
|
||||||
))
|
))
|
||||||
|
|
||||||
btn.Compute(config.Engine)
|
btn.Compute(config.Engine)
|
||||||
|
|
||||||
|
iconsDrawn++
|
||||||
|
lastColumn++
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have fewer doodad icons than this page can hold,
|
||||||
|
// fill out dummy placeholder cells to maintain the UI shape.
|
||||||
|
// TODO: this is very redundant compared to the ATTEMPT above
|
||||||
|
// to only do this once. It seems our background widget doesn't
|
||||||
|
// size up the full tab height properly, so doodad tabs that
|
||||||
|
// have fewer than one page worth (short first page) the sizing
|
||||||
|
// was wrong. The below hack pads out the screen for short first
|
||||||
|
// pages only. There is still a bug with short LAST pages where
|
||||||
|
// it doesn't hold height and the pager buttons come up.
|
||||||
|
if iconsDrawn < iconsPossible {
|
||||||
|
for i := lastColumn; i < iconsPossible; i++ {
|
||||||
|
if row == nil || i%columns == 0 {
|
||||||
|
var hidden = rowCount >= rows
|
||||||
|
rowCount++
|
||||||
|
|
||||||
|
row = ui.NewFrame(fmt.Sprintf("Doodad Row %d", rowCount))
|
||||||
|
row.SetBackground(balance.DoodadButtonBackground)
|
||||||
|
btnRows = append(btnRows, row)
|
||||||
|
frame.Pack(row, ui.Pack{
|
||||||
|
Side: ui.N,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Hide overflowing rows until we page to them.
|
||||||
|
if hidden {
|
||||||
|
row.Hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spacer := ui.NewFrame("Spacer")
|
||||||
|
spacer.Configure(ui.Config{
|
||||||
|
BorderSize: 2,
|
||||||
|
BorderStyle: ui.BorderSunken,
|
||||||
|
Background: render.Grey,
|
||||||
|
})
|
||||||
|
spacer.Resize(render.NewRect(
|
||||||
|
buttonSize-2, // TODO: without the -2 the button border
|
||||||
|
buttonSize-2, // rests on top of the window border
|
||||||
|
))
|
||||||
|
spacer.Compute(config.Engine)
|
||||||
|
row.Pack(spacer, ui.Pack{
|
||||||
|
Side: ui.W,
|
||||||
|
})
|
||||||
|
|
||||||
|
// debug
|
||||||
|
// lbl := ui.NewLabel(ui.Label{
|
||||||
|
// Text: fmt.Sprintf("i=%d\nrow=%d", i, rowCount),
|
||||||
|
// })
|
||||||
|
// spacer.Pack(lbl, ui.Pack{
|
||||||
|
// Side: ui.NW,
|
||||||
|
// })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
88
scripts/fpm-bundle-32bit.sh
Executable file
88
scripts/fpm-bundle-32bit.sh
Executable file
|
@ -0,0 +1,88 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# fpm-bundle: create bundles for the app.
|
||||||
|
|
||||||
|
# Add the user-level "gem install fpm" to the $PATH.
|
||||||
|
# Might need fixing over time.
|
||||||
|
export PATH="$PATH:$HOME/.local/share/gem/ruby/3.0.0/bin"
|
||||||
|
|
||||||
|
INSTALL_ROOT="/opt/sketchy-maze"
|
||||||
|
LAUNCHER_FILENAME="etc/linux/net.kirsle.ProjectDoodle.desktop"
|
||||||
|
LAUNCHER_ROOT="/usr/share/applications" # Where the .desktop file goes.
|
||||||
|
ICON_ROOT="/usr/share/icons/hicolor/"
|
||||||
|
|
||||||
|
# Find out how many levels up we need to go, so this
|
||||||
|
# script can run from either of these locations:
|
||||||
|
# ./dist/sketchymaze-$version/
|
||||||
|
# ./dist/stage/$version/linux/
|
||||||
|
UPLEVELS="."
|
||||||
|
if [[ -f "../../${LAUNCHER_FILENAME}" ]]; then
|
||||||
|
# run from a ./dist/x folder.
|
||||||
|
UPLEVELS="../.."
|
||||||
|
elif [[ -f "../../../../${LAUNCHER_FILENAME}" ]]; then
|
||||||
|
# run from a release stage folder
|
||||||
|
UPLEVELS="../../../.."
|
||||||
|
else
|
||||||
|
echo Did not find ${LAUNCHER_FILENAME} relative to your working directory.
|
||||||
|
echo Good places to run this script include:
|
||||||
|
echo " * ./dist/sketchymaze-\$version/ (as in 'make dist')"
|
||||||
|
echo " * ./dist/stage/\$version/linux/ (as in 'make release')"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION=`egrep -e 'Version\s+=' ${UPLEVELS}/pkg/branding/branding.go | head -n 1 | cut -d '"' -f 2`
|
||||||
|
LAUNCHER_FILE="${UPLEVELS}/${LAUNCHER_FILENAME}"
|
||||||
|
|
||||||
|
if [[ ! -f "./sketchymaze" ]]; then
|
||||||
|
echo Run this script from the directory containing the Doodle binary.
|
||||||
|
echo This is usually at /dist/doodle-VERSION/ relative to the git root.
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$LAUNCHER_FILE" ]]; then
|
||||||
|
echo "Didn't find Linux desktop launcher relative to current folder."
|
||||||
|
echo "I looked at $LAUNCHER_FILE."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clean previous artifacts.
|
||||||
|
rm *.rpm *.deb
|
||||||
|
|
||||||
|
# Create the root structure.
|
||||||
|
mkdir -p root
|
||||||
|
mkdir -p root$INSTALL_ROOT root$LAUNCHER_ROOT
|
||||||
|
cp * root$INSTALL_ROOT/
|
||||||
|
cp $LAUNCHER_FILE root$LAUNCHER_ROOT/
|
||||||
|
|
||||||
|
# Copy icons in.
|
||||||
|
mkdir -p root$ICON_ROOT/{256x256,128x128,64x64,32x32,16x16}/apps
|
||||||
|
cp ${UPLEVELS}/etc/icons/256.png "root${ICON_ROOT}256x256/apps/project-doodle.png"
|
||||||
|
cp ${UPLEVELS}/etc/icons/128.png "root${ICON_ROOT}128x128/apps/project-doodle.png"
|
||||||
|
cp ${UPLEVELS}/etc/icons/64.png "root$ICON_ROOT/64x64/apps/project-doodle.png"
|
||||||
|
cp ${UPLEVELS}/etc/icons/32.png "root$ICON_ROOT/32x32/apps/project-doodle.png"
|
||||||
|
cp ${UPLEVELS}/etc/icons/16.png "root$ICON_ROOT/16x16/apps/project-doodle.png"
|
||||||
|
|
||||||
|
# Copy runtime package and guidebook
|
||||||
|
cp -r guidebook rtp "root$INSTALL_ROOT/"
|
||||||
|
|
||||||
|
echo =====================
|
||||||
|
echo Starting fpm package build.
|
||||||
|
echo =====================
|
||||||
|
|
||||||
|
# RPM Package
|
||||||
|
fpm -C ./root -s dir -t rpm \
|
||||||
|
-d SDL2 -d SDL2_ttf -a i386 \
|
||||||
|
-n sketchy-maze -v ${VERSION} \
|
||||||
|
--license="Copyright" \
|
||||||
|
--maintainer=noah@kirsle.net \
|
||||||
|
--description="Sketchy Maze - A drawing-based maze game." \
|
||||||
|
--url="https://www.sketchymaze.com"
|
||||||
|
|
||||||
|
# Debian Package
|
||||||
|
fpm -C ./root -s dir -t deb \
|
||||||
|
-d libsdl2 -d libsdl2-ttf -a i386 \
|
||||||
|
-n sketchy-maze -v ${VERSION} \
|
||||||
|
--license="Copyright" \
|
||||||
|
--maintainer=noah@kirsle.net \
|
||||||
|
--description="Sketchy Maze - A drawing-based maze game." \
|
||||||
|
--url="https://www.sketchymaze.com"
|
Loading…
Reference in New Issue
Block a user