More demo work. Forge split into it's own project.

This commit is contained in:
Scott Duensing 2026-10-06 16:54:44 -05:00
parent 0e443f3adb
commit c4e9df5348
263 changed files with 1435 additions and 40244 deletions

259
CHANGELOG
View file

@ -119,20 +119,11 @@ API Changes
offered v3.00 the day it appears, and the minor is compared as a number, so
3.09 precedes 3.10. The launcher ranks the binaries beside it the same way.
- A Forge game says who made it. The game's form has credits fields --
developer, publisher, year, platform, genre, creator, source, and
description -- and a release writes them into its games.dat, where the menu
shows them. Notes go into a CREDITS.txt beside the game, and licence
files named in the credits travel with it. A port of an existing game gets
its original author's credits filled in from the original's own games.dat
and script header.
- A game can be sent to the master service for review from Forge. Shift+P
in the editor exports, packs, and uploads it; an upload is not in the catalogue
until it has been reviewed, and Forge shows where each version of yours
stands. Behind it: singePack(directory, database) packs a game from
a script exactly as --pack does, and the network layer can send a request
body straight from a file, so a game is never held in memory whole.
- A game can be sent to the master service for review. An upload is not in
the catalogue until it has been reviewed. Behind it: singePack(directory,
database) packs a game from a script exactly as --pack does, and the
network layer can send a request body straight from a file, so a game is
never held in memory whole.
- The menu knows which of its games are out of date, and updates them from the
game's own page. A packed game now carries VERSION in its games.dat, so the
@ -143,9 +134,7 @@ API Changes
the same checked download the Get Games tool uses. Nothing about this
machine is kept anywhere but the question: the menu says which games and
versions it holds and is told which are newer, because a record of what a
cabinet has installed could never have stayed true. Forge gives every
description a permanent GAME_ID the first time it is saved and writes it
into the games.dat it exports, so a Forge game can be published and updated.
cabinet has installed could never have stayed true.
- overlayGetFontWidth and overlayGetFontHeight report the size in pixels of one
character cell of the built-in console font. overlayPrint counts in cells
@ -1187,6 +1176,19 @@ New Features
mixer's buffers. Animating to the music no longer means counting ticks from
musicPlay and hoping they keep pace.
- Stretched, tinted, and faded sprites draw several times faster. A sprite
drawn larger or smaller than itself, or with spriteSetTint or
spriteSetAlpha, used to go pixel by pixel through a slow general path: a
sprite stretched over a whole 1920 by 1080 overlay took 8 ms, half a
frame. It now takes about 1 ms, and a coloured box the size of the screen
half that, so games with a large overlay keep their frame rate. What is
drawn is the same, give or take one step of rounding in a tinted colour.
- Run headless with SDL_VIDEODRIVER=offscreen, Singe keeps the window the
size it was given. That driver reports a 1024 by 768 screen, so a larger
window used to be shrunk to fit it, and a recording made that way came out
smaller than the game.
- 3D in front of the overlay. nodeSetLayer(node, LAYER_FRONT) moves a node,
and everything parented to it, out of the scene beneath the overlay and
into a front layer drawn after it, by the same camera with the same lights,
@ -1307,18 +1309,6 @@ New Features
- The menu no longer needs its background video as the disc; Menu.sh passes
its own arguments on to Singe.
- Forge can judge a shot by the picture rather than by a hitbox. lightSensor
is a behaviour of the framework now, the way a home laserdisc gun works: a
spot on the picture flashes on the frames a target is shown, and a shot is
good when the picture under the gun changes the way the spot does. It began
as five games' own vocabulary and was general all along, so it moved in, and
those five name no vocabulary of their own any more. pictureAt came with it,
a condition any rule can ask -- is the picture at this point bright, dark, or
neither -- for a flash hidden in a game's own video, a colour cue, or a lamp
in a scene; a game with no disc reads dark everywhere. Both are in the
book's vocabulary tables, which are generated from the manifest, and scene
100 covers them.
- The launcher runs the best Singe it finds, and the support folder can be
rewritten on demand. Menu.sh and Menu.bat read each Singe* name beside them
and rank it by this machine's architecture first and the version second.
@ -1338,9 +1328,9 @@ New Features
writes a fresh one, so a file an older release shipped and this one does not
cannot linger. A second --refresh keeps its own as old-Singe-<version>-1.
Nothing is deleted anywhere in this: what is set aside is the owner's to
remove. For the same reason the menu's game update and Forge's publish
write a new file beside the old one and only release the old copy once the
new one is in place, so an interrupted write cannot lose either.
remove. For the same reason the menu's game update writes a new file
beside the old one and only releases the old copy once the new one is in
place, so an interrupted write cannot lose either.
- --patch gives back the space a replaced file was using. SQLite keeps the
pages a deleted blob held for the next thing written, so a developer who
@ -1352,124 +1342,6 @@ New Features
pack. Under a megabyte it is left alone, since compacting rewrites the
whole file.
- Forge has tutorials: ten sittings at the front of Forge.pdf, each
ending with a game that plays, with the kit of pictures, sounds, a clip,
and a model they use packed in Forge.game, and the finished description
of each in the chooser. Each tutorial's scene presses the keys the text
names and checks the result, so the book cannot drift from the editor.
On the way the editor gained the game's own form (title, players, vars,
verbs, layers, and each layer's parameters), the digits as panel keys,
DELETE to empty a field, X to release a game with every file it names,
a file picker that keeps an unset field unset, and a panel that steps
aside while a polygon is drawn. What an entity is made from is now a
type, not a kind: types = { hero = ... } in a description, type = "hero"
on an entity and in an event's filter, and the Types panel. Forge's
launcher takes every key (MODE_FULL), so ENTER and TAB reach it, and
its panel's rows and fields take clicks. A value is edited in place:
it starts selected, so typing replaces it, and the arrows put a caret
into it. A field whose values are a fixed set opens a dropdown of them
-- true and false, the scancodes, the switches, the looks, the events,
the words a parameter takes, the game's own types, rooms, entities,
states, tracks, and nodes -- opened on the value it has, under its own
row, upward when there is no room below. The panel's list and fields
box scroll, with bars, and keep the chosen row in view. The game's
name at the top of the panel, or a press on empty canvas, brings the
game's own form back. A waves track (W in the tracks) spawns a type
by count and interval at a moment, a disc frame, or a rail camera's
stop, from an entity or a list of them in turn, or at a point; and
soundDone says when a clip playSound or a sound behaviour started has
ended. Ports of the test library began (FORGE.md section 14): the
five light gun games of the VHS era are Forge descriptions written by
a porting script, and play the same as the emulator under
the same inputs (testScripts/ports/). On the way: a lightSensor
behaviour, a pointer behaviour, fonts and anchors on the text look,
sprites drawn in copies, discPlay and discPause, pointerX and pointerY
in expressions, an event belonging to the room it happened in, and a
built game finding files beside its description. A game can bring a
vocabulary of its own: a description names a file of Lua
(vocabulary = "Vocabulary.singe") that adds looks, behaviours,
conditions, actions, layers, or events to the manifest for that game
alone; the editor loads it when the game opens and forgets it when
the game closes, the compiler checks against it, and the built game
carries it into a release. Their light sensor lives in one now,
beside those five games, and not in Forge. The Hypseus library's
eleven Karis Framework 3.32b titles are ported by the same converter, which
finds the framework where a game keeps it and notes its version; the
rest of that library is surveyed in FORGE.md 14.9. A branch on the
branching track can want its move mashed (mash = 6) or held (hold =
20). The seven Kimmy Script Engine titles are ported by the same
converter and a kimmy behaviour that
plays that engine's loop, and play as their originals do (FORGE.md
14.11).
util/forgePorts.py
assembles the ported games into a project folder of their own
(~/claude/singePorts), one game to a folder with its description, the
compiled port, the runtime, its files, and a games.dat. A description is a
.forge file now (it was .game, which is the packed release's, and the
chooser had to sniff which was which). The seventeen KarisFramework
games are ported too: util/forgePortKaris.lua reads a game's own
script, and the qte behaviour plays the framework's loop over the
framework's tables, running the game's script and add-ons in a shim;
every one plays as its original under the same driver. Flat
bodies are the size their looks are drawn at (they were half), and a
platformer stands on its point.
- A game can be described rather than written. Forge/Author.singe and
Forge/AuthorCompile.singe take a table of layers, entities, behaviours
and rules and compile it into an ordinary Singe game: the rules become
real Lua, so nothing walks a table every frame and the result can be
read and edited by hand. There is no notion of genre in it -- a game
declares which of the engine's layers it uses, and that is the whole
difference between a platformer and a quick-time event over video. The
vocabulary of conditions and actions is declared in a table rather than
built into the compiler, so new kinds of game are entries rather than
releases, and the "lua" action is the deliberate way out when a rule
needs something the vocabulary cannot say. See the manual.
- Forge runs as a game rather than only as a library: started from the
menu it opens on a chooser (its own descriptions, a new game from a
starter, a copy of anything dropped into its directory), the keys, the
mouse and the pad reach the editor, P saves, builds and plays what is on
screen and comes back to it, and ESC closes or leaves. Entities can be
added, duplicated, deleted and typed field by field -- position, look,
behaviours and their parameters, all from the manifest -- and renaming
one renames it in every rule. Conditions and actions are picked from
the vocabulary with their help beside them, rules reorder, a rule's
note is editable, and U undoes forty steps. The compiler loads the
runtime from Forge/, where it lives, so it no longer fails looking for
a Singe/Author.singe that never shipped.
- An editor for those descriptions, Forge, which is itself
a Singe game: the canvas is the same overlay at the same coordinates the
game will be played in, so what is placed is what is seen. Entity list
and details are an RmlUi document, the canvas beside them is drawn into
the overlay and picked the way a light gun game picks a target, and the
two compose because a button is offered to the GUI first while pointer
motion is never consumed. A description survives a round trip through
it: load, save, load again, and it compiles to the same game. Forge is
distributed on its own and no part of it ships inside Singe -- not the
editor, not the compiler, not the runtime. A game Forge builds carries
its own copy of that runtime, so it runs on a machine that has never had
Forge on it and cannot change behaviour because the engine moved on.
- A game released from Forge is standalone. forgeExport writes the
compiled script, a games.dat, the description it came from and a copy of
the runtime into a directory of its own, taken out of Forge. --pack turns
that directory into a .game. The game locates its own directory with
debug.getinfo rather than trusting DIR, which names the directory of the
script the engine was launched with -- not this one when a game is reached
by dofile.
- The editor edits rules, not only entity positions: Tab swaps the panel
between the entities and the event sheet, the selected rule opens in
place with its conditions and actions under it, and conditions and
actions are added from the same manifest the compiler reads, so the rule
editor needs no change when the vocabulary grows. It is driven by keys
as well as the pointer, which is how the bundled menu has always worked
and what a cabinet needs. ENTER types a value into the selected
condition or action and moves to its next one, ESC puts it back; a
number typed in comes back a number.
- The menu has an audio delay calibration screen for the machine's own
delay (DAC, receiver, Bluetooth, display). Press the service key on
the game list; a click plays once a second and the screen flashes.
@ -1582,22 +1454,22 @@ New Features
making games -- the programming model, packing, games.dat, the GUI, 3D,
physics, particles, the formats, and every function in the Lua API -- is
the new Singe Reference, docs/Reference.adoc. The two link into each
other where they always did. ./build-docs.sh builds both, the Forge book,
and the beginner's course, and packs the three books that do not ship in
the binary, with the lesson files and the licence, into
other where they always did. ./build-docs.sh builds both and the
beginner's course, and packs the two books that do not ship in the binary,
with the lesson files and the licence, into
Singe-Docs-v<version>.zip, an optional download beside the engine. The
HTML pages carry their pictures inside them, so each is one file wherever
it is copied.
- ./build-docs.sh preview builds the four books with PREVIEW laid diagonally
- ./build-docs.sh preview builds the three books with PREVIEW laid diagonally
across every page, PDF and HTML alike, for proofreaders, and names the
documentation download -preview so it cannot pass for a release. The
plain build, and the manual the binary carries, never wear it.
- The Singe logo is on the cover of all four books.
- The Singe logo is on the cover of all three books.
- A third book: "Learn to Program with Singe", docs/Learn.adoc, built by
./build-docs.sh beside the manual and the Forge book. Thirty lessons that
./build-docs.sh beside the manual and the Reference. Thirty lessons that
take somebody who has never written a line of code from one word on a black
screen to a finished, packaged, cabinet-ready game. Part one teaches
programming with no artwork and no video at all; part two adds sprites,
@ -1678,81 +1550,6 @@ New Features
are rendered into .builddir/ by the build and by build-docs.sh alike,
and the binary embeds the PDF from there.
- Forge's description format is new (FORGE.md section 4): kinds with
instances, rooms that remember themselves, rules triggered by events
(collisions between kinds, hits, deaths, timers, disc frames) as well as
every frame, vars and states on every instance, expressions where a
number goes, any-of groups, timers, sound, a HUD document bound to vars,
actions that take time run as coroutines, and hitbox tracks keyed to
disc frames or time. A shoot-em-up and a light gun game over video
prove it (testScripts/author, scenes 58 and 59). The editor gained
kinds, rooms, event pickers, and a timeline that scrubs the disc's video
under the cursor (scene 60). Nothing of the first slice's format loads;
it was never released.
- Forge builds 3D games (FORGE.md section 5): a scene3d layer, model,
mesh, light, billboard, and text looks, the character controller moving
relative to a camera that is fixed, follows, orbits, sits in first
person, or rides a rail of points with stops, enemies that seek, an
animator playing a clip per state, targets with zones on named bones so
a gun's ray says which part it hit, ragdolls on death, bodies and
triggers. A rail shooter in the manner of House of the Dead and a 3D
platformer prove it (scenes 61 and 62), and the editor shows a 3D room
as the scene itself under an orbiting camera (scene 63).
- Forge builds adventure games (FORGE.md section 6): rooms that remember
themselves, walk areas baked to a navigation mesh in a painted room or
a modelled one, hotspots, a verb line and a text parser with the most
specific rule winning, an inventory, dialogue trees, cut-scenes across
rooms with fades, depth sorting and scaling, and whole-game saves. The
same rules run a two-room adventure in 2D and in 3D (scenes 64 and 65).
- Forge builds racing games and tower defences (FORGE.md section 7):
vehicles on the engine's vehicle physics with an opponent that drives a
track on its own, walkers that follow a target or patrol a lane, turrets
that fire at what is in range, instances placed by a click on the floor,
thrust, and hinge, ball, and slider joints (scenes 66 and 67).
- Forge builds branching video games and carries the arcade plumbing
(FORGE.md section 8): windows on the disc that want a move, with the
disc sent to a success or a failure clip, the score bezel mirroring the
game's vars, coins, continues, a results card with the best score kept,
and scores sent to the master service's board (scene 68).
- Forge's editor gained a search in every picker, a file picker for any
field that names a file, polygon drawing for walk areas and hotspot
outlines, a dialogue outline panel, and play-from-this-room (FORGE.md
section 9, scene 69).
- Forge's long tail (FORGE.md section 10): sprite sheets stepped by
state, tile maps from a sheet, MIDI notes as events, water that bodies
float in, boats under their propellers, and soft bodies (scenes 70 and
71). Every kind of game in FORGE.md's catalogue now has its vocabulary.
- Forge's sprites turn (a var, a spin behaviour, or a projectile aiming
along its flight), a particles look streams from an instance, the
checker names misspelt fields, and 2D particle bursts are drawn at all
(they never had been).
- Forge builds painted rooms in 3D with occluders, and its 3D viewport
gained axis gizmos with a turn handle; entities carry a rotation, and
a sprite faces left through a frame range of its own (FORGE.md section
12, scenes 70, 79, and 80).
- Forge's catalogue is written out (FORGE.md section 11): a first-person
shooter, breakout, a maze, third-person action, a space shooter, a
rhythm game, and bowling join the samples (scenes 72 to 78), and what
they found is fixed: a gun's ray passes through what is not a target,
events raised while the first room is built reach the rules, and a
rule on a key or a room can run once per instance of a kind.
- Forge has its own manual, docs/Forge.adoc, built to Forge.html and
Forge.pdf beside the Singe manual; the engine's manual keeps a pointer.
The build packs Forge.game itself, with the manual and four samples
that need no art inside, and Forge's first run copies Forge.pdf out to
its data directory and says where.
R redoes what U undid, forty steps deep.
Fixes
-----

View file

@ -174,7 +174,6 @@ endfunction()
# assets/ holds only what is embedded in Singe. Sources those files are made from (the traced
# artwork, OBJ and STL models, the sound samples) and anything else not embedded live in media/.
# Forge, which ships on its own, lives in forge/.
# Images
singeEmbedImage(font)
@ -230,9 +229,6 @@ singeEmbed(${CMAKE_SOURCE_DIR}/assets/settings.cfg ${GENERATED_DIR}/settings_cfg
singeEmbed(${CMAKE_SOURCE_DIR}/assets/Menu.singe ${GENERATED_DIR}/Menu_singe.h "")
singeEmbed(${CMAKE_SOURCE_DIR}/assets/Tools.singe ${GENERATED_DIR}/Tools_singe.h "")
# Forge, the authoring tool, is built from forge but is NOT embedded: it is distributed on
# its own, not with the engine. Nothing of it -- not the editor, not the compiler, not the runtime
# a game it builds loads -- belongs inside Singe.
singeEmbed(${CMAKE_SOURCE_DIR}/assets/Net.singe ${GENERATED_DIR}/Net_singe.h "")
singeEmbed(${CMAKE_SOURCE_DIR}/assets/cacerts.pem ${GENERATED_DIR}/cacerts_pem.h "")
singeEmbed(${CMAKE_SOURCE_DIR}/assets/Master.singe ${GENERATED_DIR}/Master_singe.h "")
@ -321,34 +317,8 @@ singeEmbedLua(thirdparty/lume/lume.lua "")
singeEmbedLua(thirdparty/inspect.lua/inspect.lua "")
singeEmbedLua(thirdparty/bump.lua/bump.lua "")
# Forge's manual, rendered the same way. It is not embedded: the superbuild packs it into
# Forge.game (cmake/Superbuild.cmake), which is the one thing that ships Forge.
# Its vocabulary tables are generated from the manifest in Author.singe, so the manual cannot
# drift from it; the generated file is kept in the tree for a host without a Lua interpreter.
find_program(LUA_INTERPRETER NAMES lua5.4 lua)
if(LUA_INTERPRETER)
add_custom_command(
OUTPUT ${CMAKE_SOURCE_DIR}/docs/ForgeVocabulary.adoc
COMMAND ${CMAKE_COMMAND} -E chdir ${CMAKE_SOURCE_DIR} ${LUA_INTERPRETER} util/forgeVocabulary.lua > ${CMAKE_SOURCE_DIR}/docs/ForgeVocabulary.adoc
DEPENDS forge/Author.singe util/forgeVocabulary.lua
COMMENT "Writing the Forge vocabulary"
)
endif()
# The book embeds the tutorials' pictures and includes their shipped descriptions, so any of
# those changing renders it again; the globs are re-run on every build.
file(GLOB_RECURSE FORGE_BOOK_PARTS CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/docs/images/tutorials/* ${CMAKE_SOURCE_DIR}/forge/tutorials/*.game)
add_custom_command(
OUTPUT ${MANUAL_DIR}/Forge.pdf
COMMAND ${CMAKE_COMMAND} -E make_directory ${MANUAL_DIR}
COMMAND ${ASCIIDOCTOR_PDF} -a revnumber=${SINGE_VERSION_FULL} ${CMAKE_SOURCE_DIR}/docs/Forge.adoc -o ${MANUAL_DIR}/Forge.pdf
DEPENDS docs/Forge.adoc docs/ForgeVocabulary.adoc ${FORGE_BOOK_PARTS}
COMMENT "Rendering Forge.pdf"
VERBATIM
)
add_custom_target(forgeManual ALL DEPENDS ${MANUAL_DIR}/Forge.pdf)
# Optional HTML for browsing: cmake --build . --target docs. The user manual is the one book the
# binary carries; the reference, the Forge book, and the beginner's course are the optional
# binary carries; the reference and the beginner's course are the optional
# documentation download that build-docs.sh packs.
if(ASCIIDOCTOR)
add_custom_target(docs

View file

@ -1,8 +1,8 @@
#!/usr/bin/env bash
# Build the four books: the user manual (docs/Manual.adoc, which ships inside the binary), the
# programming reference (docs/Reference.adoc), the Forge manual (docs/Forge.adoc), and the
# beginner's course (docs/Learn.adoc). Produces .builddir/<Book>.html and .pdf for each, and then
# packs the three books that do not ship in the binary, with the lesson files and the licence, into
# Build the three books: the user manual (docs/Manual.adoc, which ships inside the binary), the
# programming reference (docs/Reference.adoc), and the beginner's course (docs/Learn.adoc).
# Produces .builddir/<Book>.html and .pdf for each, and then packs the two books that do not ship
# in the binary, with the lesson files and the licence, into
# .builddir/Singe-Docs-v<version>.zip, the optional documentation download. The version comes from
# CMakeLists.txt.
#
@ -24,17 +24,16 @@ elif [[ -n "${1:-}" ]]; then
exit 2
fi
src=$here/docs/Manual.adoc
forge=$here/docs/Forge.adoc
learn=$here/docs/Learn.adoc
ref=$here/docs/Reference.adoc
for doc in "$src" "$forge" "$learn" "$ref"; do
for doc in "$src" "$learn" "$ref"; do
if [[ ! -f $doc ]]; then
echo "error: $doc not found" >&2
exit 1
fi
done
for tool in asciidoctor asciidoctor-pdf lua5.4 zip; do
for tool in asciidoctor asciidoctor-pdf zip; do
if ! command -v "$tool" > /dev/null; then
echo "error: $tool not found (gem install asciidoctor asciidoctor-pdf rouge)" >&2
exit 1
@ -62,12 +61,8 @@ fi
out=$here/.builddir
mkdir -p "$out"
# The Forge vocabulary tables come from the manifest itself, so the manual cannot drift from it.
lua5.4 "$here/util/forgeVocabulary.lua" > "$here/docs/ForgeVocabulary.adoc"
asciidoctor ${htmlOpts[@]+"${htmlOpts[@]}"} -a revnumber="$version" "$src" -o "$out/Manual.html"
asciidoctor-pdf ${pdfOpts[@]+"${pdfOpts[@]}"} -a revnumber="$version" "$src" -o "$out/Manual.pdf"
asciidoctor ${htmlOpts[@]+"${htmlOpts[@]}"} -a revnumber="$version" "$forge" -o "$out/Forge.html"
asciidoctor-pdf ${pdfOpts[@]+"${pdfOpts[@]}"} -a revnumber="$version" "$forge" -o "$out/Forge.pdf"
asciidoctor ${htmlOpts[@]+"${htmlOpts[@]}"} -a revnumber="$version" "$learn" -o "$out/Learn.html"
asciidoctor-pdf ${pdfOpts[@]+"${pdfOpts[@]}"} -a revnumber="$version" "$learn" -o "$out/Learn.pdf"
asciidoctor ${htmlOpts[@]+"${htmlOpts[@]}"} -a revnumber="$version" "$ref" -o "$out/Reference.html"
@ -78,7 +73,7 @@ asciidoctor-pdf ${pdfOpts[@]+"${pdfOpts[@]}"} -a revnumber="$version" "$ref" -o
stage=$out/Singe-Docs-v$version$suffix
rm -rf "$stage" "$stage.zip"
mkdir -p "$stage/learn"
cp "$out/Reference.pdf" "$out/Reference.html" "$out/Forge.pdf" "$out/Forge.html" "$out/Learn.pdf" "$out/Learn.html" "$stage/"
cp "$out/Reference.pdf" "$out/Reference.html" "$out/Learn.pdf" "$out/Learn.html" "$stage/"
cp -r "$here/docs/learn/." "$stage/learn/"
cp "$here/COPYING" "$stage/"
cat > "$stage/README.txt" <<EOF
@ -86,15 +81,14 @@ Singe $version documentation
Reference.pdf / .html Everything a game can do: the programming model and every function.
Learn.pdf / .html Learn to Program with Singe: thirty-one lessons from nothing to a finished game.
Forge.pdf / .html Forge, the authoring tool: ten tutorials and the vocabulary.
learn/ The finished script of every lesson, and the art kit the lessons use.
COPYING The licence, GPL version 3.
The user manual, Manual.pdf, is inside Singe itself and is unpacked into the Singe folder the first
time it runs. Forge is a separate download.
time it runs.
EOF
(cd "$out" && zip -q -r "Singe-Docs-v$version$suffix.zip" "Singe-Docs-v$version$suffix")
rm -rf "$stage"
echo "built:"
ls -la "$out/Manual.html" "$out/Manual.pdf" "$out/Reference.html" "$out/Reference.pdf" "$out/Forge.html" "$out/Forge.pdf" "$out/Learn.html" "$out/Learn.pdf" "$out/Singe-Docs-v$version$suffix.zip"
ls -la "$out/Manual.html" "$out/Manual.pdf" "$out/Reference.html" "$out/Reference.pdf" "$out/Learn.html" "$out/Learn.pdf" "$out/Singe-Docs-v$version$suffix.zip"

View file

@ -463,38 +463,4 @@ ExternalProject_Add(singe
BUILD_ALWAYS ON
)
# Forge, packed into .builddir beside the binary so it can be copied out and tested. It is NOT
# part of the engine and nothing of it is embedded; this only puts the distributable where the
# binaries already are. Only a build that produces a binary this machine can run can pack it, since
# packing is the engine's own --pack: the architecture has to match as well as the system, or the
# shell is handed an ARM executable and answers "Syntax error". Every other build skips it and the
# host build does the work. The manual,
# rendered by the engine's own build into .builddir, goes in beside the scripts: Forge copies it
# out to its data directory on its first run.
if((KANGAROO_OS STREQUAL "linux") AND (CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux") AND (KANGAROO_ARCH STREQUAL CMAKE_HOST_SYSTEM_PROCESSOR))
# Everything under forge/ -- the scripts, the kit, the tutorials -- and re-globbed on
# every build, so a file added or changed anywhere in it repacks Forge.game without anyone
# having to remember.
file(GLOB_RECURSE forgeSources CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/forge/*)
set(forgePack ${CMAKE_SOURCE_DIR}/.builddir/Forge.game)
set(forgeManual ${CMAKE_SOURCE_DIR}/.builddir/Forge.pdf)
set(forgeStage ${SB_PREFIX}/forgepack)
add_custom_command(
OUTPUT ${forgePack}
# Packing refuses to overwrite, and a stale pack is worse than none.
COMMAND ${CMAKE_COMMAND} -E rm -rf ${forgePack} ${forgeStage}
COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/forge ${forgeStage}/Forge
COMMAND ${CMAKE_COMMAND} -E copy ${forgeManual} ${forgeStage}/Forge/Forge.pdf
# The samples that need no art beyond what the engine ships, so the chooser has something to
# open on a fresh install. They come from the test set, which is the one copy of them.
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_SOURCE_DIR}/testScripts/author/platformer.forge ${CMAKE_SOURCE_DIR}/testScripts/author/breakout.forge ${CMAKE_SOURCE_DIR}/testScripts/author/qte.forge ${CMAKE_SOURCE_DIR}/testScripts/author/fmv.forge ${forgeStage}/Forge/
# Run it somewhere harmless: the engine writes Singe/ and data/ into the working directory.
COMMAND ${CMAKE_COMMAND} -E chdir ${forgeStage} ${CMAKE_SOURCE_DIR}/.builddir/${singeBinaryName} --pack ${forgeStage}/Forge ${forgePack}
DEPENDS singe ${forgeSources} ${forgeManual} ${CMAKE_SOURCE_DIR}/testScripts/author/platformer.forge ${CMAKE_SOURCE_DIR}/testScripts/author/breakout.forge ${CMAKE_SOURCE_DIR}/testScripts/author/qte.forge ${CMAKE_SOURCE_DIR}/testScripts/author/fmv.forge
COMMENT "Packing Forge into .builddir/Forge.game"
VERBATIM
)
add_custom_target(forge ALL DEPENDS ${forgePack})
endif()
message(STATUS "Superbuild for ${KANGAROO_OS}/${KANGAROO_ARCH}: libraries into ${SB_PREFIX}, binary ${singeBinaryName}")

View file

@ -20,15 +20,31 @@ quad, a torus knot, and three effects worked out a pixel at a time in Lua every
a sprite with `spriteSetPixels`: a plasma with a cycling palette, a lookup-table tunnel, and a
tape's snow. Nothing in any of the three demos is a video.
Every demo opens on the Singe logo, faded up from black to white, held, and faded out, before its
music starts, and closes the same way on the Kangaroo Punch logo after the music ends and the
picture fades to black. The logos are rendered from assets/ into demos/art/ by util/demoLogos.sh.
The demos run at 1920 by 1080, the canvas and window in each `games.dat`. They are laid out in
design units, 480 tall and as wide as the screen's shape makes them (853 at 16:9), and the kit turns
those into the window's pixels as it draws, `K.S` (2.25) to a unit: `K.draw` and `K.drawGrid` for
sprites, `K.text` and `K.font` for lettering rendered at full resolution, `K.width` and `K.height`
for sizes. Art the util scripts draw is made 2.25 pixels to a unit and loaded with `K.sprite(path,
density)`, so it draws pixel for pixel; Insert Coin's pixel art is drawn a whole number of screen
pixels to each of its own instead, so every art pixel is the same size. A whole-screen 2D layer
would cost the CPU milliseconds a frame on a 1920 by 1080 overlay, so those are quads on the GPU:
`K.fill` (a background), `K.under` (a sprite or a colour), and `K.turn` (a sprite turned and
scaled, which on the overlay would be rebuilt every frame) between the 3D world and the overlay,
`K.backdrop` behind the world, and `K.flash` and `K.cover` (scanlines) over everything. Each demo
calls `K.screen(camera, fov, far)` once its camera is made.
Every demo opens on the Singe dragon and lettering, faded up on black, held, and faded out, before
its music starts, and closes the same way on the Kangaroo Punch logo after the music ends and the
picture fades to black. util/demoLogos.sh copies the logos' SVGs from media/ into demos/art/ (the
lettering turned near-white for black), and the kit draws them from the SVG at the screen's
resolution (`K.vector`).
They are on the demo's own clock (the opening card runs from -3.5 s to 0), so util/demoCapture.sh
takes them with a negative start or a stretch past the end.
Scroller text can be changed freely. Each scroller is given the bars it runs between, not a speed:
`K.scroller(t, font, text, y, amp, startTime, endTime, style)` works its speed out from the text so
the first letter enters as the scene starts and the last leaves as it ends. Text too short to fill
the first letter enters as the scene starts and the last has left a second and a half before it
ends, so the words are finished before the cut. Text too short to fill
the time at a readable speed is repeated until it does; text too long to read at its speed still
ends on time and puts a line in the trace (`DEMO scroller too long to read`, with about how many
characters would fit). `K.scroller3D(t, blocks, text, y, startTime, endTime)` is timed the same

79
demos/art/dragon.svg Normal file
View file

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="193 22 889 749" width="889" height="749"><g transform="translate(1275 0) scale(-1 1)">
<!-- Singe dragon, reconstructed as polygons from Dragon.jpeg; transparent background. -->
<defs>
<linearGradient id="g0" gradientUnits="userSpaceOnUse" x1="698.7" y1="96.1" x2="629.6" y2="137.3"><stop offset="0" stop-color="#6f7277"/><stop offset="1" stop-color="#97969a"/></linearGradient>
<linearGradient id="g1" gradientUnits="userSpaceOnUse" x1="503.6" y1="307.1" x2="382.6" y2="159.3"><stop offset="0" stop-color="#66686f"/><stop offset="1" stop-color="#8a8b90"/></linearGradient>
<linearGradient id="g2" gradientUnits="userSpaceOnUse" x1="845.5" y1="159.5" x2="866.9" y2="156.5"><stop offset="0" stop-color="#73777b"/><stop offset="1" stop-color="#73767c"/></linearGradient>
<linearGradient id="g3" gradientUnits="userSpaceOnUse" x1="835.5" y1="229.1" x2="842.3" y2="253.1"><stop offset="0" stop-color="#54575d"/><stop offset="1" stop-color="#5a5d63"/></linearGradient>
<linearGradient id="g4" gradientUnits="userSpaceOnUse" x1="738.8" y1="402.4" x2="778.4" y2="373.8"><stop offset="0" stop-color="#4e5357"/><stop offset="1" stop-color="#55595e"/></linearGradient>
<linearGradient id="g5" gradientUnits="userSpaceOnUse" x1="502.9" y1="620.5" x2="456.0" y2="520.4"><stop offset="0" stop-color="#52575e"/><stop offset="1" stop-color="#5a6166"/></linearGradient>
<linearGradient id="g6" gradientUnits="userSpaceOnUse" x1="854.9" y1="616.3" x2="901.1" y2="622.7"><stop offset="0" stop-color="#33393e"/><stop offset="1" stop-color="#373c42"/></linearGradient>
<linearGradient id="g7" gradientUnits="userSpaceOnUse" x1="364.2" y1="664.4" x2="360.7" y2="746.1"><stop offset="0" stop-color="#353b40"/><stop offset="1" stop-color="#40474d"/></linearGradient>
<linearGradient id="g8" gradientUnits="userSpaceOnUse" x1="965.4" y1="697.5" x2="906.6" y2="706.0"><stop offset="0" stop-color="#63686c"/><stop offset="1" stop-color="#676c70"/></linearGradient>
<linearGradient id="g9" gradientUnits="userSpaceOnUse" x1="894.1" y1="714.4" x2="875.2" y2="729.2"><stop offset="0" stop-color="#43474a"/><stop offset="1" stop-color="#45474b"/></linearGradient>
<linearGradient id="g10" gradientUnits="userSpaceOnUse" x1="682.0" y1="67.3" x2="737.5" y2="95.0"><stop offset="0" stop-color="#d5c9bc"/><stop offset="1" stop-color="#dbcdc1"/></linearGradient>
<linearGradient id="g11" gradientUnits="userSpaceOnUse" x1="979.3" y1="183.9" x2="958.7" y2="176.5"><stop offset="0" stop-color="#9b9187"/><stop offset="1" stop-color="#b9aea5"/></linearGradient>
<linearGradient id="g12" gradientUnits="userSpaceOnUse" x1="924.1" y1="150.5" x2="852.7" y2="205.4"><stop offset="0" stop-color="#c2b6a8"/><stop offset="1" stop-color="#f1e7d9"/></linearGradient>
<linearGradient id="g13" gradientUnits="userSpaceOnUse" x1="663.8" y1="225.3" x2="726.2" y2="233.1"><stop offset="0" stop-color="#d4c4b2"/><stop offset="1" stop-color="#dccebd"/></linearGradient>
<linearGradient id="g14" gradientUnits="userSpaceOnUse" x1="945.6" y1="245.5" x2="885.2" y2="257.5"><stop offset="0" stop-color="#837b70"/><stop offset="1" stop-color="#b0a292"/></linearGradient>
<linearGradient id="g15" gradientUnits="userSpaceOnUse" x1="889.0" y1="403.3" x2="817.3" y2="411.5"><stop offset="0" stop-color="#a1998c"/><stop offset="1" stop-color="#e4d7c8"/></linearGradient>
<linearGradient id="g16" gradientUnits="userSpaceOnUse" x1="886.4" y1="506.4" x2="918.1" y2="507.5"><stop offset="0" stop-color="#857f74"/><stop offset="1" stop-color="#8b8279"/></linearGradient>
<linearGradient id="g17" gradientUnits="userSpaceOnUse" x1="881.7" y1="569.7" x2="827.2" y2="506.1"><stop offset="0" stop-color="#888175"/><stop offset="1" stop-color="#a49b8d"/></linearGradient>
<linearGradient id="g18" gradientUnits="userSpaceOnUse" x1="208.2" y1="559.7" x2="242.4" y2="555.4"><stop offset="0" stop-color="#aa9c8d"/><stop offset="1" stop-color="#b3a693"/></linearGradient>
<linearGradient id="g19" gradientUnits="userSpaceOnUse" x1="852.5" y1="644.9" x2="736.7" y2="633.3"><stop offset="0" stop-color="#928a7d"/><stop offset="1" stop-color="#daccbc"/></linearGradient>
<linearGradient id="g20" gradientUnits="userSpaceOnUse" x1="224.4" y1="631.6" x2="303.9" y2="662.7"><stop offset="0" stop-color="#8d8376"/><stop offset="1" stop-color="#b0a396"/></linearGradient>
<linearGradient id="g21" gradientUnits="userSpaceOnUse" x1="1017.2" y1="708.7" x2="974.7" y2="752.6"><stop offset="0" stop-color="#726b60"/><stop offset="1" stop-color="#867e73"/></linearGradient>
<linearGradient id="g22" gradientUnits="userSpaceOnUse" x1="560.7" y1="705.9" x2="600.9" y2="763.2"><stop offset="0" stop-color="#898072"/><stop offset="1" stop-color="#a89b8b"/></linearGradient>
<linearGradient id="g23" gradientUnits="userSpaceOnUse" x1="801.0" y1="120.8" x2="796.8" y2="140.0"><stop offset="0" stop-color="#e5e4e4"/><stop offset="1" stop-color="#eff0ef"/></linearGradient>
<linearGradient id="g24" gradientUnits="userSpaceOnUse" x1="795.2" y1="187.5" x2="744.9" y2="264.6"><stop offset="0" stop-color="#d6dad6"/><stop offset="1" stop-color="#e7e8e7"/></linearGradient>
<linearGradient id="g25" gradientUnits="userSpaceOnUse" x1="444.3" y1="360.6" x2="343.6" y2="248.4"><stop offset="0" stop-color="#eaeaea"/><stop offset="1" stop-color="#f1f1f0"/></linearGradient>
<linearGradient id="g26" gradientUnits="userSpaceOnUse" x1="774.0" y1="291.5" x2="756.7" y2="328.6"><stop offset="0" stop-color="#e0e1de"/><stop offset="1" stop-color="#e7e8e5"/></linearGradient>
<linearGradient id="g27" gradientUnits="userSpaceOnUse" x1="504.5" y1="428.2" x2="600.5" y2="343.0"><stop offset="0" stop-color="#e6e8e7"/><stop offset="1" stop-color="#e9eae8"/></linearGradient>
<linearGradient id="g28" gradientUnits="userSpaceOnUse" x1="270.4" y1="455.9" x2="305.1" y2="499.0"><stop offset="0" stop-color="#e7e8e7"/><stop offset="1" stop-color="#ececeb"/></linearGradient>
<linearGradient id="g29" gradientUnits="userSpaceOnUse" x1="780.3" y1="469.6" x2="741.5" y2="534.9"><stop offset="0" stop-color="#dbdfdb"/><stop offset="1" stop-color="#e6e7e4"/></linearGradient>
<linearGradient id="g30" gradientUnits="userSpaceOnUse" x1="572.4" y1="460.6" x2="554.8" y2="532.2"><stop offset="0" stop-color="#e1e3e0"/><stop offset="1" stop-color="#eaeae9"/></linearGradient>
<linearGradient id="g31" gradientUnits="userSpaceOnUse" x1="373.0" y1="591.4" x2="450.6" y2="588.1"><stop offset="0" stop-color="#e1e4e0"/><stop offset="1" stop-color="#e5e7e4"/></linearGradient>
<linearGradient id="g32" gradientUnits="userSpaceOnUse" x1="634.8" y1="522.2" x2="694.0" y2="646.0"><stop offset="0" stop-color="#cfd5d1"/><stop offset="1" stop-color="#d8ddd9"/></linearGradient>
<linearGradient id="g33" gradientUnits="userSpaceOnUse" x1="488.1" y1="692.3" x2="494.0" y2="725.6"><stop offset="0" stop-color="#d5dad6"/><stop offset="1" stop-color="#e2e5e2"/></linearGradient>
<filter id="shadow" x="-10%" y="-10%" width="130%" height="130%"><feDropShadow dx="3" dy="5" stdDeviation="4" flood-color="#000" flood-opacity="0.18"/></filter>
<filter id="brush" x="0" y="0" width="100%" height="100%"><feTurbulence type="fractalNoise" baseFrequency="0.003 0.7" numOctaves="2" seed="7" result="noise"/><feColorMatrix in="noise" type="saturate" values="0" result="grey"/><feComponentTransfer in="grey" result="grain"><feFuncA type="linear" slope="0.22"/></feComponentTransfer><feComposite in="grain" in2="SourceGraphic" operator="in" result="clipped"/><feBlend in="clipped" in2="SourceGraphic" mode="overlay"/></filter>
</defs>
<title>Singe dragon</title>
<g filter="url(#shadow)">
<polygon points="620.3,450.0 420.1,406.2 575.1,277.9 680.5,456.9" fill="url(#g27)" fill-opacity="0.88" stroke="#c2ccc6" stroke-width="2" stroke-linejoin="round"/>
<polygon points="718.9,692.9 547.2,565.9 642.9,498.6 688.8,474.3 721.0,511.5" fill="url(#g32)" fill-opacity="0.88" stroke="#c2ccc6" stroke-width="2" stroke-linejoin="round"/>
<polygon points="832.0,437.8 829.7,543.9 705.5,374.0 673.4,303.6 704.9,256.9" fill="url(#g4)" stroke="#aaacae" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
<polygon points="573.2,269.1 519.7,314.7 404.5,263.3 302.7,135.5" fill="url(#g1)" stroke="#c4c5c7" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
<polygon points="281.8,224.7 375.5,258.3 510.4,318.9 414.4,400.8" fill="url(#g25)" fill-opacity="0.88" stroke="#c2ccc6" stroke-width="2" stroke-linejoin="round"/>
<polygon points="732.1,520.7 894.5,701.6 730.0,691.8" fill="url(#g19)" stroke="#ece5dd" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
<polygon points="412.5,697.6 353.4,656.7 383.4,543.9 473.3,459.7" fill="url(#g31)" fill-opacity="0.88" stroke="#c2ccc6" stroke-width="2" stroke-linejoin="round"/>
<polygon points="320.8,754.9 288.6,624.8 486.2,759.6" fill="url(#g7)" stroke="#9fa3a6" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
<polygon points="481.8,461.2 539.9,571.7 415.1,701.2" fill="url(#g5)" stroke="#acb0b2" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
<polygon points="785.2,280.5 915.9,436.6 874.0,506.8 815.2,407.5" fill="url(#g15)" stroke="#f1ebe3" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
<polygon points="674.6,467.0 539.5,557.8 484.1,450.4" fill="url(#g30)" fill-opacity="0.88" stroke="#c2ccc6" stroke-width="2" stroke-linejoin="round"/>
<polygon points="829.1,553.8 825.4,615.9 692.3,464.8 719.1,401.9" fill="url(#g29)" fill-opacity="0.88" stroke="#c2ccc6" stroke-width="2" stroke-linejoin="round"/>
<polygon points="346.0,386.3 325.7,487.9 266.2,553.8 224.8,479.9" fill="url(#g28)" fill-opacity="0.88" stroke="#c2ccc6" stroke-width="2" stroke-linejoin="round"/>
<polygon points="266.0,555.8 310.0,745.6 202.7,639.4" fill="url(#g20)" stroke="#d7d1ca" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
<polygon points="772.9,160.4 839.1,206.4 782.1,265.6 730.4,279.6 711.1,253.1" fill="url(#g24)" fill-opacity="0.88" stroke="#c2ccc6" stroke-width="2" stroke-linejoin="round"/>
<polygon points="481.7,755.7 587.6,673.5 660.8,760.2" fill="url(#g22)" stroke="#d3cdc5" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
<polygon points="656.2,206.4 767.6,153.0 644.6,335.1" fill="url(#g13)" stroke="#ede6de" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
<polygon points="837.1,618.3 834.8,444.4 906.2,561.9" fill="url(#g17)" stroke="#d1cdc6" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
<polygon points="956.0,158.3 956.0,190.3 862.2,209.3 774.5,150.0 819.0,146.0" fill="url(#g12)" stroke="#f8f3ec" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
<polygon points="933.1,751.6 995.8,671.8 1073.3,758.9" fill="url(#g21)" stroke="#c2beb9" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
<polygon points="778.2,149.8 640.3,144.9 552.5,57.2" fill="url(#g0)" stroke="#cbcacc" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
<polygon points="928.4,545.6 882.6,680.7 833.3,626.6" fill="url(#g6)" stroke="#9b9da0" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
<polygon points="421.4,698.7 579.0,670.2 483.0,746.1" fill="url(#g33)" fill-opacity="0.88" stroke="#c2ccc6" stroke-width="2" stroke-linejoin="round"/>
<polygon points="883.2,680.4 990.9,673.4 926.5,753.0" fill="url(#g8)" stroke="#b3b5b7" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
<polygon points="930.3,539.5 906.2,561.1 871.3,508.2 912.0,435.2" fill="url(#g16)" stroke="#c5c0bc" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
<polygon points="257.8,560.4 201.9,632.0 214.1,482.6" fill="url(#g18)" stroke="#d9d2c9" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
<polygon points="783.1,149.1 712.8,111.3 830.2,116.0 874.7,144.4" fill="url(#g23)" fill-opacity="0.88" stroke="#c2ccc6" stroke-width="2" stroke-linejoin="round"/>
<polygon points="940.3,291.2 842.3,211.4 966.9,239.2" fill="url(#g14)" stroke="#d7d0c8" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
<polygon points="887.2,248.1 785.9,269.7 841.6,212.7" fill="url(#g3)" stroke="#acaeb1" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
<polygon points="771.8,272.5 797.9,379.7 733.9,288.2" fill="url(#g26)" fill-opacity="0.88" stroke="#c2ccc6" stroke-width="2" stroke-linejoin="round"/>
<polygon points="700.8,100.8 653.0,30.8 778.0,107.7" fill="url(#g10)" stroke="#ede6e0" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
<polygon points="826.6,702.0 893.1,702.0 934.9,762.2" fill="url(#g9)" stroke="#a2a3a5" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
<polygon points="954.9,193.0 958.8,135.4 989.6,179.7 970.4,218.2" fill="url(#g11)" stroke="#dcd6d2" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
<polygon points="825.0,149.0 887.9,149.0 854.9,174.1" fill="url(#g2)" stroke="#b9bbbd" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
</g>
</g></svg>

After

Width:  |  Height:  |  Size: 13 KiB

221
demos/art/kangarooPunch.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 427 KiB

BIN
demos/art/kangarooPunchLogo.png (Stored with Git LFS)

Binary file not shown.

BIN
demos/art/singeLogo.png (Stored with Git LFS)

Binary file not shown.

8
demos/art/singeText.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

BIN
demos/insertCoin/art/bossArm.png (Stored with Git LFS)

Binary file not shown.

BIN
demos/insertCoin/art/bossCore.png (Stored with Git LFS)

Binary file not shown.

BIN
demos/insertCoin/art/bullet.png (Stored with Git LFS)

Binary file not shown.

BIN
demos/insertCoin/art/scanlines.png (Stored with Git LFS)

Binary file not shown.

BIN
demos/insertCoin/art/target.png (Stored with Git LFS)

Binary file not shown.

BIN
demos/insertCoin/art/turret.png (Stored with Git LFS)

Binary file not shown.

View file

@ -3,10 +3,10 @@ GAMES = {
{
TITLE = "Insert Coin",
SCRIPT = "insertCoin/insertCoin.singe",
CANVAS_X = 640,
CANVAS_Y = 480,
RESOLUTION_X = 1024,
RESOLUTION_Y = 768,
CANVAS_X = 1920,
CANVAS_Y = 1080,
RESOLUTION_X = 1920,
RESOLUTION_Y = 1080,
REQUIRES = { "SCENE", "POST", "SHADOWS" }
}
}

View file

@ -11,8 +11,6 @@
dofile("Singe/Framework.singe")
local K = dofile(DIR .. "../kit.lua")
local W = 640
local H = 480
local FOV = 60
local PHOSPHOR = { 70, 255, 140 }
local NEON = { { 255, 60, 90 }, { 255, 170, 40 }, { 255, 240, 70 }, { 70, 230, 120 }, { 60, 180, 255 }, { 190, 90, 255 } }
@ -37,6 +35,7 @@ local FEATURE_TEXT = "YOU ARE WATCHING INSERT COIN, A DEMO OF THE SINGE GAME ENG
local FEATURE_ALPHA = 190 -- How see-through its letters are: 0 to 255
local FEATURE_CELL = 12 -- A block of its letters, in pixels, as a letter rests
local ART_DENSITY = 2.25 -- The boss's, its bullets', and the targets' pixels to a design unit (util/insertCoinArt.py DENSITY)
local FX_W = 320 -- The live plasma's and tunnel's size, stretched to wherever they are shown
local FX_H = 240
local TUNNEL_LAP = 4 -- The tunnel's travel counts a lap of its texture as this many passes
@ -81,7 +80,9 @@ local COIN_THICK = 0.2
local BRICK_COLS = 9
local BRICK_ROWS = 6
local sync = K.load(DIR, W, H)
local sync = K.load(DIR)
local W = K.W -- The screen in design units: 480 tall (kit.lua)
local H = K.H
local beat = sync.beats[3] - sync.beats[2]
local art = {}
local world = {}
@ -102,27 +103,46 @@ end
-- Pixel drawing ------------------------------------------------------------------------------------
-- Frame frame of a sheet of frames frameW wide, at x, y, scale pixels a pixel.
local function px(sprite, frameW, frame, x, y, scale)
local h = spriteGetHeight(sprite)
spriteDrawGrid(sprite, math.floor(x), math.floor(y), frame * frameW, 0, frameW, h, math.floor(x + frameW * scale) - 1, math.floor(y + h * scale) - 1)
-- Pixel art is drawn a whole number of the window's pixels to each of its own, the nearest to scale
-- design units, so every art pixel is the same size and crisp on any screen.
local function pixels(scale)
return math.max(1, math.floor(scale * K.S + 0.5))
end
-- Text in the 8 by 8 font, scale pixels a pixel. align "center" centres it on x.
-- Frame frame of a sheet of frames frameW wide, at x, y, scale design units a pixel.
local function px(sprite, frameW, frame, x, y, scale)
local h = spriteGetHeight(sprite)
local size = pixels(scale)
local rx = math.floor(x * K.S)
local ry = math.floor(y * K.S)
spriteDrawGrid(sprite, rx, ry, frame * frameW, 0, frameW, h, rx + frameW * size - 1, ry + h * size - 1)
end
-- How wide text in the 8 by 8 font is at scale, in design units.
local function ptextWidth(text, scale)
return #text * 8 * pixels(scale) / K.S
end
-- Text in the 8 by 8 font, scale design units a pixel. align "center" centres it on x.
local function ptext(x, y, text, scale, r, g, b, align)
local w = #text * 8 * scale
local size = pixels(scale)
local rx = 0
local ry = math.floor(y * K.S)
if align == "center" then
x = x - w / 2
x = x - ptextWidth(text, scale) / 2
end
rx = math.floor(x * K.S)
spriteSetTint(art.font, r, g, b)
for i = 1, #text do
local c = text:byte(i) - 32
if c > 0 and c < 96 then
spriteDrawGrid(art.font, math.floor(x + (i - 1) * 8 * scale), math.floor(y), (c % 16) * 8, (c // 16) * 8, 8, 8, math.floor(x + i * 8 * scale) - 1, math.floor(y + 8 * scale) - 1)
spriteDrawGrid(art.font, rx + (i - 1) * 8 * size, ry, (c % 16) * 8, (c // 16) * 8, 8, 8, rx + i * 8 * size - 1, ry + 8 * size - 1)
end
end
spriteSetTint(art.font, 255, 255, 255)
@ -131,32 +151,34 @@ end
-- The same, each letter its own colour round the wheel, cycling: the high score table's rainbow.
local function rainbow(x, y, text, scale, t, align)
local w = #text * 8 * scale
local step = ptextWidth("X", scale)
if align == "center" then
x = x - w / 2
x = x - ptextWidth(text, scale) / 2
end
for i = 1, #text do
local r, g, b = K.hue(i * 0.06 - t * 0.8)
ptext(x + (i - 1) * 8 * scale, y, text:sub(i, i), scale, r, g, b)
ptext(x + (i - 1) * step, y, text:sub(i, i), scale, r, g, b)
end
end
-- A CRT's scanlines over everything: one screen row in three darkened.
local function scanlines(alpha)
spriteSetAlpha(art.scanlines, alpha or 255)
spriteDraw(art.scanlines, 0, 0, W - 1, H - 1)
spriteSetAlpha(art.scanlines, 255)
K.cover(art.scanlines, discGetHeight() / spriteGetHeight(art.scanlines), alpha)
end
-- Loading --------------------------------------------------------------------------------------------
local function loadArt()
for _, name in ipairs({ "invaderA", "invaderB", "cannon", "runner", "coin", "ship", "kart", "bossCore", "bossArm", "turret", "bullet", "font", "mountains", "hills", "ground", "scanlines", "fighterA", "fighterB", "target" }) do
for _, name in ipairs({ "invaderA", "invaderB", "cannon", "runner", "coin", "ship", "kart", "font", "mountains", "hills", "ground", "scanlines", "fighterA", "fighterB" }) do
art[name] = spriteLoad(DIR .. "art/" .. name .. ".png")
end
for _, name in ipairs({ "bossCore", "bossArm", "turret", "bullet", "target" }) do
art[name] = K.sprite(DIR .. "art/" .. name .. ".png", ART_DENSITY)
end
spriteQuality(art.bossCore, RENDER_SMOOTH)
spriteQuality(art.bossArm, RENDER_SMOOTH)
-- The plasma and the tunnel, worked out live in Lua a frame at a time.
@ -168,17 +190,17 @@ local function loadArt()
emitterSetBlend(art.boom, PARTICLE_ADD)
emitterSetRate(art.boom, 0)
emitterSetLife(art.boom, 0.3, 0.9)
emitterSetSpeed(art.boom, 60, 320)
emitterSetSpeed(art.boom, 60 * K.S, 320 * K.S)
emitterSetSpread(art.boom, 180)
emitterSetDrag(art.boom, 2)
emitterSetSize(art.boom, 9, 2)
emitterSetSize(art.boom, 9 * K.S, 2 * K.S)
emitterSetMax(art.boom, 4000)
emitterStart(art.boom)
end
local function boom(x, y, count, r, g, b)
emitterSetPosition(art.boom, x, y)
emitterSetPosition(art.boom, x * K.S, y * K.S)
emitterSetColor(art.boom, 255, 255, 255, 255, r, g, b, 0)
emitterBurst(art.boom, count)
end
@ -402,7 +424,7 @@ local function buildBonus()
materialSetTexture(tunnel, art.tunnel.sprite)
materialSetUnlit(tunnel, true)
materialSetDoubleSided(tunnel, true)
world.tunnel = K.screenQuad(world.camera, FOV, tunnel, 80, 1.8)
world.tunnel = K.screenQuad(world.camera, FOV, tunnel, 80, true)
-- A coin is a gold rim and two struck faces: the name on one side and the dragon on the other.
world.coinMesh = meshCylinder(COIN_RADIUS, COIN_THICK, 24)
world.coinMaterial = K.chrome(255, 200, 60, 0.25)
@ -508,6 +530,7 @@ local function buildWorld()
world.camera = nodeNew()
cameraSet(world.camera)
cameraSetPerspective(FOV, 0.1, 800)
K.screen(world.camera, FOV, 800)
art.blocks = K.blockFont(dofile(DIR .. "art/font.lua"), world.camera, FOV, FEATURE_CELL, FEATURE_ALPHA)
buildRacer()
buildPolygons()
@ -555,7 +578,7 @@ ERA.boot = {
hideAll()
end,
draw = function(t, bar, k)
K.flash(0, 0, 0, 255)
K.fill(0, 0, 0)
local warm = K.smooth(1, 1.6, bar)
if warm < 1 then
@ -589,7 +612,7 @@ ERA.pong = {
world.pongScore = { 0, 0 }
end,
draw = function(t, bar, k)
K.flash(0, 6, 2, 255)
K.fill(0, 6, 2)
local b = K.beat(t)
local left = 60
local right = W - 60
@ -649,7 +672,7 @@ ERA.invaders = {
world.alienBase = K.beat(t)
end,
draw = function(t, bar, k)
K.flash(0, 0, 8, 255)
K.fill(0, 0, 8)
local step = math.floor(K.beat(t) - world.alienBase)
local ox, oy = alienPositions(step)
local frame = step % 2
@ -736,11 +759,11 @@ ERA.runner = {
K.box(0, i * 30, W - 1, i * 30 + 29, r, g, math.min(255, b), 255)
end
local function layer(sprite, speed, y, scale)
local w = spriteGetWidth(sprite) * scale
local w = K.width(sprite) * scale
local offset = (t * speed) % w
for x = -offset, W, w do
spriteDraw(sprite, math.floor(x), y, math.floor(x + w) - 1, y + spriteGetHeight(sprite) * scale - 1)
K.draw(sprite, x, y, x + w, y + K.height(sprite) * scale)
end
end
layer(art.mountains, RUN_SPEED * 0.15, 170, 2)
@ -1039,7 +1062,7 @@ ERA.continue = {
hideAll()
end,
draw = function(t, bar, k)
K.flash(0, 0, 0, 255)
K.fill(0, 0, 0)
local n = 8 - math.floor(bar - 73)
if bar < 74 then
@ -1078,8 +1101,8 @@ ERA.boss = {
end,
draw = function(t, bar, k)
K.plasmaUpdate(art.plasma, t)
spriteDraw(art.plasma.sprite, 0, 0, W - 1, H - 1)
K.flash(0, 0, 10, 200)
K.under(art.plasma.sprite, 0, 0, W, H, nil, nil, nil, nil, true)
K.under(nil, 0, 0, W, H, 0, 0, 10, 200)
local dt = t - lastT
local cx = W / 2 + math.sin(t * 0.5) * 60
local cy = 150 + math.sin(t * 0.8) * 20
@ -1126,14 +1149,10 @@ ERA.boss = {
local ax = cx + math.cos(math.rad(a)) * 60
local ay = cy + math.sin(math.rad(a)) * 60
spriteRotate(art.bossArm, a)
spriteDraw(art.bossArm, math.floor(ax), math.floor(ay), true)
spriteDraw(art.turret, math.floor(cx + math.cos(math.rad(a)) * 110) - 14, math.floor(cy + math.sin(math.rad(a)) * 110) - 14)
K.turn(art.bossArm, ax, ay, a)
K.turn(art.turret, cx + math.cos(math.rad(a)) * 110, cy + math.sin(math.rad(a)) * 110, 0)
end
spriteSetTint(art.bossCore, 255, hitFlash and 200 or 255, hitFlash and 200 or 255)
spriteRotateAndScale(art.bossCore, -spin * 0.5, 1.4 + 0.1 * K.decay("kicks", t, 8))
spriteDraw(art.bossCore, math.floor(cx), math.floor(cy), true)
spriteSetTint(art.bossCore, 255, 255, 255)
K.turn(art.bossCore, cx, cy, -spin * 0.5, 1.4 + 0.1 * K.decay("kicks", t, 8), 255, hitFlash and 200 or 255, hitFlash and 200 or 255)
-- Bullets.
local tints = { { 255, 80, 220 }, { 80, 230, 255 }, { 255, 230, 80 } }
@ -1148,7 +1167,7 @@ ERA.boss = {
local c = tints[bl.c]
spriteSetTint(art.bullet, c[1], c[2], c[3])
spriteDraw(art.bullet, math.floor(bl.x) - 6, math.floor(bl.y) - 6)
K.draw(art.bullet, bl.x, bl.y, true)
end
end
spriteSetTint(art.bullet, 255, 255, 255)
@ -1173,10 +1192,6 @@ ERA.boss = {
K.flash(255, 255, 255, math.floor(255 * K.smooth(96.85, 97, bar)))
end
end,
leave = function(t)
spriteRotate(art.bossArm, 0)
spriteRotateAndScale(art.bossCore, 0, 1)
end,
}
-- The bonus stage: coins pour down further along the tunnel, COIN_RATE a beat, land on its floor, and the
@ -1262,26 +1277,39 @@ ERA.scores = {
end,
draw = function(t, bar, k)
K.plasmaUpdate(art.plasma, t)
spriteDraw(art.plasma.sprite, 0, 0, W - 1, H - 1)
K.flash(0, 0, 20, 190)
K.under(art.plasma.sprite, 0, 0, W, H, nil, nil, nil, nil, true)
K.under(nil, 0, 0, W, H, 0, 0, 20, 190)
rainbow(W / 2, 40, "HIGH SCORES", 4, t, "center")
-- Rows share the space under the title and appear one after another over the first half of the
-- scene, however many there are.
-- Rows share the space under the title, centred in it, and appear one after another over the
-- first half of the scene, however many there are. The table is centred across the screen by
-- its widest row; a name too long for the screen is drawn a size smaller.
local count = #HIGH_SCORES
local top = 110
local spacing = math.min(52, (H - 40 - top) / math.max(count, 1))
local room = H - 40 - top
local spacing = math.min(52, room / math.max(count, 1))
local scale = (spacing >= 34) and 3 or 2
local scoreAt = 100
local nameAt = 290
local fit = math.floor((W - 80 - nameAt) / ptextWidth("X", scale))
local sizes = {}
local widest = 0
for i, row in ipairs(HIGH_SCORES) do
local name = tostring(row[1])
sizes[i] = (#name > fit) and math.max(1, scale - 1) or scale
widest = math.max(widest, ptextWidth(name, sizes[i]))
end
local left = (W - nameAt - widest) / 2
top = top + (room - spacing * count) / 2
for i, row in ipairs(HIGH_SCORES) do
if bar >= 113 + (i - 1) * 4 / count then
local y = top + (i - 1) * spacing
local name = tostring(row[1])
local fit = math.floor((W - 330 - 16) / (8 * scale))
local size = (#name > fit) and math.max(1, scale - 1) or scale
ptext(40, y, ordinal(i), scale, 255, 255, 255)
ptext(140, y, string.format("%06d", math.floor(row[2] or 0) % 1000000), scale, 255, 230, 80)
rainbow(330, y, name:upper(), size, t + i * 0.3)
ptext(left, y, ordinal(i), scale, 255, 255, 255)
ptext(left + scoreAt, y, string.format("%06d", math.floor(row[2] or 0) % 1000000), scale, 255, 230, 80)
rainbow(left + nameAt, y, tostring(row[1]):upper(), sizes[i], t + i * 0.3)
end
end
scanlines(120)
@ -1296,7 +1324,7 @@ ERA.outro = {
hideAll()
end,
draw = function(t, bar, k)
K.flash(0, 0, 0, 255)
K.fill(0, 0, 0)
local p = t - K.barTime(137)
local last = K.duration - 0.9
@ -1529,7 +1557,7 @@ ERA.gallery = {
x = W - x
end
if not g.shot[id] and x > -40 and x < W + 40 then
spriteDraw(art.target, math.floor(x - 32), y - 20)
K.draw(art.target, x - 32, y - 20)
live[#live + 1] = { id = id, x = x, y = y + 12 }
end
end
@ -1576,7 +1604,7 @@ ERA.gallery = {
table.remove(g.falling, i)
else
spriteRotate(art.target, math.floor(f.spin) % 360)
spriteDraw(art.target, math.floor(f.x), math.floor(f.y), true)
K.draw(art.target, f.x, f.y, true)
end
end
spriteRotate(art.target, 0)
@ -1585,9 +1613,11 @@ ERA.gallery = {
local cx = math.floor(g.cx)
local cy = math.floor(g.cy)
-- The ring, drawn a screen pixel at a time as thick as one design unit.
colorForeground(255, 40, 40, 255)
overlayCircle(cx, cy, 26)
overlayCircle(cx, cy, 27)
for r = math.floor(26 * K.S), math.floor(28 * K.S) - 1 do
overlayCircle(math.floor(cx * K.S), math.floor(cy * K.S), r)
end
K.box(cx - 40, cy - 1, cx - 12, cy + 1, 255, 40, 40, 255)
K.box(cx + 12, cy - 1, cx + 40, cy + 1, 255, 40, 40, 255)
K.box(cx - 1, cy - 40, cx + 1, cy - 12, 255, 40, 40, 255)
@ -1625,7 +1655,7 @@ ERA.fighter = {
for i = 0, 9 do
K.box(0, i * 33, W - 1, i * 33 + 32, 60 + i * 14, 30 + i * 8, 90 + i * 6, 255)
end
spriteDraw(art.hills, 0, floor - 150, W - 1, floor - 30)
K.draw(art.hills, 0, floor - 150, W, floor - 30)
K.box(0, floor - 30, W - 1, H - 1, 90, 60, 50, 255)
for x = 0, W, 64 do
K.box(x, floor - 30, x + 2, H - 1, 70, 45, 38, 255)
@ -1702,8 +1732,8 @@ ERA.rhythm = {
local r = world.rhythm
K.plasmaUpdate(art.plasma, t)
spriteDraw(art.plasma.sprite, 0, 0, W - 1, H - 1)
K.flash(0, 0, 20, 200)
K.under(art.plasma.sprite, 0, 0, W, H, nil, nil, nil, nil, true)
K.under(nil, 0, 0, W, H, 0, 0, 20, 200)
-- The highway, a row at a time, narrowing to the horizon, and the beat lines rushing down it.
for y = RHYTHM_TOP, H - 1, 2 do
local s = (y - RHYTHM_TOP) / (RHYTHM_HIT - RHYTHM_TOP)
@ -1809,7 +1839,7 @@ ERA.montage = {
end
local r, g, b = K.hue(t * 0.7)
K.flash(r // 3, g // 3, b // 3, 255)
K.fill(r // 3, g // 3, b // 3)
local s = 10 + 4 * math.exp(-(bar - 135) * 6)
rainbow(W / 2, 170, "GAME ON", math.floor(s), t, "center")

View file

@ -23,10 +23,16 @@ local HIT_LISTS = { "kicks", "snares", "hats" }
local LEVELS = 63
local PIXEL_PNG = "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\x0b\x49\x44\x41\x54\x78\x9c\x63\xf8\x0f\x04\x00\x09\xfb\x03\xfd\xfb\x5e\x6b\x2b\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82"
local FONT_FILE = "Singe/FreeSansBold.ttf"
local DESIGN_H = 480 -- The demos are laid out this many units tall, whatever the window: K.S pixels to a unit
local QUAD_FRONT = 0.2 -- The screen quads (K.flash, K.under, K.backdrop): how far from the camera the flashes hang ...
local QUAD_UNDER = 0.25 -- ... the layer under the overlay, in front of the world ...
local QUAD_BACK = 0.9 -- ... and the backdrop, this share of the way to the far plane
local QUAD_STEP = 0.001 -- Each quad drawn after another in its layer comes this share nearer
local TAIL = 2 -- Seconds the demo runs on after the music ends, fading to black
local LOGO_FADE = 1.0 -- The logo cards before and after a demo: seconds fading up from black ...
local LOGO_HOLD = 1.5 -- ... held ...
local LOGO_TIME = LOGO_FADE * 2 + LOGO_HOLD -- ... and the whole card, back to black
local LOGO_GAP = 24 -- Design units between the logos stacked on a card
local sync = nil
local music = nil
@ -41,7 +47,11 @@ local lastTicks = 0
local frames = 0
local stats = { second = 0, frames = 0, worst = 0, last = 0 }
-- The scrollers' letters: each a sprite made once per font, white, so it can be tinted.
-- How many of a sprite's pixels make one design unit (K.sprite); 1 when not said.
local density = {}
-- The scrollers' letters: each a sprite made once per font, white, so it can be tinted, and its
-- size in design units. Fonts from K.font are rendered at the window's resolution.
local glyphs = {}
local function glyph(font, c)
@ -54,12 +64,105 @@ local function glyph(font, c)
fontQuality(FONT_QUALITY_BLENDED)
local s = fontToSprite(c)
g = { sprite = s, w = spriteGetWidth(s) }
density[s] = K.S
g = { sprite = s, w = spriteGetWidth(s) / K.S, h = spriteGetHeight(s) / K.S }
glyphs[key] = g
end
return g
end
-- The screen quads: pools of camera-facing quads, three layers deep, placed fresh every frame by
-- K.flash, K.fill, K.under, and K.backdrop and hidden again when a frame does not use them. They
-- draw on the GPU, where a whole-screen layer costs nothing; on the overlay it costs the CPU about
-- 8 ms at 1920 by 1080.
local screen = { camera = nil, fov = 60, far = 100, layers = {} }
local function quadLayer(name, distance, front)
local layer = { nodes = {}, used = 0, distance = distance, front = front }
screen.layers[name] = layer
return layer
end
-- The next quad of a layer, made the first time it is wanted: unlit, see-through, its own
-- material, hung off the camera.
local function quadNext(layer)
local q
layer.used = layer.used + 1
q = layer.nodes[layer.used]
if not q then
local m = materialNew()
local pivot = nodeNew(screen.camera)
local quad = nodeNew(pivot)
materialSetUnlit(m, true)
materialSetBlend(m, true)
materialSetDoubleSided(m, true)
nodeSetMesh(quad, screen.mesh, m)
nodeSetRotation(quad, 90, 0, 0)
nodeSetShadow(quad, false)
if layer.front then
nodeSetLayer(pivot, LAYER_FRONT)
end
q = { pivot = pivot, quad = quad, material = m, tiles = 1, nearest = false }
layer.nodes[layer.used] = q
end
nodeSetVisible(q.pivot, true)
return q
end
-- Places a layer's next quad over x1, y1 to x2, y2 in design units, in a colour, showing a sprite
-- or nothing, repeated tiles times down it (once when nil), sampled nearest when asked, and turned
-- by angle degrees about its middle. A sprite whose pixels change from frame to frame is sent again
-- only when dynamic.
local function quadPlace(layer, x1, y1, x2, y2, r, g, b, a, sprite, dynamic, tiles, nearest, angle)
local q = quadNext(layer)
local slope = 2 * math.tan(math.rad(screen.fov / 2)) / K.H
local dx = ((x1 + x2) / 2 - K.W / 2) * slope
local dy = (K.H / 2 - (y1 + y2) / 2) * slope
-- The scene draws see-through things farthest first by the distance of their middles from the
-- eye, so each quad slides along its own line of sight (where it shows does not change) until its
-- middle is as far as its turn in the layer says: then they draw in the order they were placed.
local d = layer.distance * (1 - QUAD_STEP * layer.used) / math.sqrt(1 + dx * dx + dy * dy)
local unit = d * slope
if sprite ~= q.sprite or dynamic then
materialSetTexture(q.material, sprite)
q.sprite = sprite
end
if (tiles or 1) ~= q.tiles then
q.tiles = tiles or 1
materialSetTiling(q.material, 1, q.tiles)
end
if (nearest or false) ~= q.nearest then
q.nearest = nearest or false
materialSetFilter(q.material, q.nearest and FILTER_NEAREST or FILTER_LINEAR)
end
materialSetColor(q.material, r, g, b, a or 255)
nodeSetPosition(q.pivot, dx * d, dy * d, -d)
nodeSetRotation(q.pivot, 0, 0, angle or 0)
nodeSetScale(q.quad, (x2 - x1) * unit, 1, (y2 - y1) * unit)
end
local function screenBegin()
for _, layer in pairs(screen.layers) do
layer.used = 0
end
end
local function screenEnd()
for _, layer in pairs(screen.layers) do
for i = layer.used + 1, #layer.nodes do
nodeSetVisible(layer.nodes[i].pivot, false)
end
end
end
-- The block letters of K.blockFont: the shade of each way a face can point (front, top, left, right
-- and back, bottom), as a strip of greys the hue materials multiply; how many hues round the wheel;
@ -171,6 +274,13 @@ K.W = 640
K.H = 480
-- A sprite behind the 3D world, over x1, y1 to x2, y2 in design units (the whole screen when
-- they are left out), tinted r, g, b, a; dynamic when its pixels change every frame. On the GPU.
function K.backdrop(sprite, x1, y1, x2, y2, r, g, b, a, dynamic)
quadPlace(screen.layers.back, x1 or 0, y1 or 0, x2 or K.W, y2 or K.H, r or 255, g or 255, b or 255, a, sprite, dynamic)
end
-- The fractional bar at time t, counting from 1: bar 1 starts on the beat the sync data names.
function K.bar(t)
return (K.beat(t) - (sync.barStart - 1)) / 4 + 1
@ -326,7 +436,7 @@ function K.bobs(t, bar, sprite, count, spread, size, cx, cy)
local cr, cg, cb = K.hue(p.i / count + t * 0.1)
spriteSetTint(sprite, cr, cg, cb)
spriteDraw(sprite, math.floor(px - r), math.floor(py - r), math.floor(px + r), math.floor(py + r))
K.draw(sprite, px - r, py - r, px + r, py + r)
end
spriteSetTint(sprite, 255, 255, 255)
end
@ -341,7 +451,7 @@ function K.box(x1, y1, x2, y2, r, g, b, a, blend)
spriteSetTint(pixel, r, g, b)
spriteSetAlpha(pixel, a or 255)
spriteSetBlend(pixel, blend or SPRITE_ALPHA)
spriteDraw(pixel, math.floor(x1), math.floor(y1), math.floor(x2), math.floor(y2))
K.draw(pixel, x1, y1, x2 + 1, y2 + 1)
end
@ -382,7 +492,7 @@ function K.circleScroller(t, font, text, cx, cy, r, speed, t0, t1)
-- so a narrow letter is not overlapped by its neighbours.
spriteSetTint(g.sprite, cr, cg, cb)
spriteRotate(g.sprite, math.floor(middle + 90) % 360)
spriteDraw(g.sprite, math.floor(cx + math.cos(rad) * r), math.floor(cy + math.sin(rad) * r), true)
K.draw(g.sprite, cx + math.cos(rad) * r, cy + math.sin(rad) * r, true)
spriteRotate(g.sprite, 0)
a = a + math.deg(g.w / r)
end
@ -400,6 +510,13 @@ function K.clamp(v, lo, hi)
end
-- A sprite over the whole screen and everything on it, repeated tiles times from top to bottom and
-- sampled nearest, at alpha a: scanlines, a grille, a screen door. On the GPU.
function K.cover(sprite, tiles, a)
quadPlace(screen.layers.front, 0, 0, K.W, K.H, 255, 255, 255, a, sprite, false, tiles, true)
end
-- A crawl: lines of text on a plane laid down toward the horizon, sliding away from the viewer and
-- shrinking as they go, faded into the distance. The first line starts at the bottom of the screen
-- at t0 and the last reaches the horizon at t1.
@ -422,11 +539,11 @@ function K.crawl(t, font, lines, t0, t1, horizonY)
for c = 1, #line do
local g = glyph(font, line:sub(c, c))
local gw = g.w * scale
local gh = spriteGetHeight(g.sprite) * scale
local gh = g.h * scale
spriteSetTint(g.sprite, 255, math.floor(200 + 55 * fade), 120 + math.floor(100 * (1 - fade)))
spriteSetAlpha(g.sprite, math.floor(255 * fade))
spriteDraw(g.sprite, math.floor(x), math.floor(y - gh), math.floor(x + gw), math.floor(y))
K.draw(g.sprite, x, y - gh, x + gw, y)
spriteSetAlpha(g.sprite, 255)
x = x + gw
end
@ -471,6 +588,45 @@ function K.disc(r, segments)
end
-- Draws a sprite in design units: at its own size from x, y, or centred on them when x2 is true,
-- or stretched from x, y up to (not including) x2, y2, so pieces that meet tile without gaps at any
-- scale. Its own size is its pixels over its density (K.sprite).
function K.draw(sprite, x, y, x2, y2)
local k = K.S / (density[sprite] or 1)
local w = 0
local h = 0
if y2 then
spriteDraw(sprite, math.floor(x * K.S), math.floor(y * K.S), math.floor(x2 * K.S) - 1, math.floor(y2 * K.S) - 1)
return
end
w = spriteGetWidth(sprite) * k
h = spriteGetHeight(sprite) * k
x = x * K.S
y = y * K.S
if x2 == true then
x = x - w / 2
y = y - h / 2
end
if k == 1 then
spriteDraw(sprite, math.floor(x), math.floor(y))
else
spriteDraw(sprite, math.floor(x), math.floor(y), math.floor(x + w), math.floor(y + h))
end
end
-- spriteDrawGrid in design units: the part sx, sy, w, h of the sprite (its own pixels) drawn at
-- x, y at its own size, or stretched up to (not including) x2, y2.
function K.drawGrid(sprite, x, y, sx, sy, w, h, x2, y2)
local k = 1 / (density[sprite] or 1)
x2 = x2 or (x + w * k)
y2 = y2 or (y + h * k)
spriteDrawGrid(sprite, math.floor(x * K.S), math.floor(y * K.S), sx, sy, w, h, math.floor(x2 * K.S) - 1, math.floor(y2 * K.S) - 1)
end
function K.ease(t)
t = K.clamp(t, 0, 1)
return t * t * (3 - 2 * t)
@ -511,9 +667,30 @@ function K.env(name, t)
end
-- The whole screen in one colour: a flash, a fade, or a tint.
function K.flash(r, g, b, a, blend)
K.box(0, 0, K.W - 1, K.H - 1, r, g, b, a, blend)
-- The whole screen in one solid colour under the overlay and over the 3D world: a 2D scene's
-- background. On the GPU.
function K.fill(r, g, b)
quadPlace(screen.layers.under, 0, 0, K.W, K.H, r, g, b, 255)
end
-- The whole screen in one colour over everything, the 3D front layer included: a flash, a fade, or
-- a tint. On the GPU.
function K.flash(r, g, b, a)
if (a or 255) > 0 then
quadPlace(screen.layers.front, 0, 0, K.W, K.H, r, g, b, a)
end
end
-- A font of the bundled face at a size in design units, rendered at the window's resolution.
function K.font(size)
local real = math.floor(size * K.S + 0.5)
if not fonts[real] then
fonts[real] = fontLoad(FONT_FILE, real)
end
return fonts[real]
end
@ -528,6 +705,12 @@ function K.glass(r, g, b, alpha)
end
-- A sprite's height in design units.
function K.height(sprite)
return spriteGetHeight(sprite) / (density[sprite] or 1)
end
-- Seconds since the most recent hit in a list at time t.
function K.hitAge(list, t)
local hits = sync[list]
@ -584,12 +767,14 @@ end
-- Reads the demo's sync data and its music, makes the overlay, and loads the kit's own pixel.
-- Envelopes become arrays of numbers from 0 to 1 here, once.
function K.load(dir, width, height)
function K.load(dir)
local value = {}
K.W = width or K.W
K.H = height or K.H
overlaySetResolution(K.W, K.H)
-- The window's size is the canvas in games.dat; the demo works in design units, DESIGN_H tall.
K.S = discGetHeight() / DESIGN_H
K.W = discGetWidth() / K.S
K.H = DESIGN_H
overlaySetResolution(discGetWidth(), discGetHeight())
for i = 1, #ALPHABET do
value[ALPHABET:byte(i)] = (i - 1) / LEVELS
end
@ -617,10 +802,10 @@ function K.load(dir, width, height)
music = musicLoad(dir .. "music.mp3")
end
pixel = spriteLoadData(PIXEL_PNG)
-- The logos' lettering is opaque black, which the colour key would drop: they go by alpha alone.
-- The logos go by their alpha alone: the colour key would drop any black in them.
drawTransparent(true)
logos.singe = spriteLoad(dir .. "../art/singeLogo.png")
logos.kangaroo = spriteLoad(dir .. "../art/kangarooPunchLogo.png")
logos.opening = { K.vector(dir .. "../art/dragon.svg", K.W * 0.6, 270), K.vector(dir .. "../art/singeText.svg", K.W * 0.5, 84) }
logos.closing = { K.vector(dir .. "../art/kangarooPunch.svg", K.W * 0.8, 400) }
drawTransparent(false)
return sync
end
@ -728,16 +913,22 @@ end
-- DEMO_CAPTURE = { from = seconds, to = seconds, every = frames } nothing plays: the clock starts
-- at from and moves by the frame (run Singe with --deterministic), every frame (or every "every"th)
-- is saved as frameNNNNNN.png, and the demo quits at to.
-- A logo card p seconds in: the screen fades from black to white with the logo coming up on it,
-- holds, and fades back to black, the way Singe's own start does. The logos' lettering is black.
local function logoCard(sprite, p)
-- A logo card p seconds in: its logos, stacked and centred on black, fade up, hold, and fade back.
local function logoCard(card, p)
local v = math.floor(255 * K.clamp(math.min(p, LOGO_TIME - p) / LOGO_FADE, 0, 1))
local x = (K.W - spriteGetWidth(sprite)) // 2
local y = (K.H - spriteGetHeight(sprite)) // 2
local height = LOGO_GAP * (#card - 1)
local y = 0
K.flash(v, v, v, 255)
for _, sprite in ipairs(card) do
height = height + K.height(sprite)
end
y = (K.H - height) / 2
K.fill(0, 0, 0)
for _, sprite in ipairs(card) do
spriteSetAlpha(sprite, v)
spriteDraw(sprite, x, y)
K.draw(sprite, K.W / 2, y + K.height(sprite) / 2, true)
y = y + K.height(sprite) + LOGO_GAP
end
end
@ -754,15 +945,16 @@ function K.run(scenes)
frames = frames + 1
colorBackground(0, 0, 0, 0)
overlayClear()
screenBegin()
if not DEMO_CAPTURE and not playing and t >= 0 then
musicPlay(music)
playing = true
end
-- The Singe card before the music, the demo, and the Kangaroo Punch card after it.
if t < 0 then
logoCard(logos.singe, t + LOGO_TIME)
logoCard(logos.opening, t + LOGO_TIME)
elseif t >= ending then
logoCard(logos.kangaroo, t - ending)
logoCard(logos.closing, t - ending)
else
local bar = K.bar(t)
@ -791,6 +983,7 @@ function K.run(scenes)
K.flash(0, 0, 0, math.floor(255 * K.clamp((t - sync.duration) / TAIL, 0, 1)))
end
end
screenEnd()
if DEMO_CAPTURE then
if frames % (DEMO_CAPTURE.every or 1) == 0 then
singeScreenshot(string.format("frame%06d", frames))
@ -807,12 +1000,27 @@ function K.run(scenes)
end
-- A flat quad that exactly fills the camera's view at distance d (oversize makes it larger, to stay
-- covering while it turns): for a video over, or as, the whole picture in 3D, where it can turn and
-- glow. What comes back is a pivot on the camera's axis with the quad stood up under it, so the
-- pivot's roll, nodeSetRotation(q, 0, 0, angle), turns the quad in its own plane. It starts hidden.
-- Hangs the screen quads off the demo's camera, whose field of view and far plane they are placed
-- by. Call it once the camera is made, before any K.flash, K.fill, K.under, or K.backdrop.
function K.screen(camera, fov, far)
screen.camera = camera
screen.fov = fov
screen.far = far
screen.mesh = meshPlane(1, 1)
quadLayer("front", QUAD_FRONT, true)
quadLayer("under", QUAD_UNDER, false)
quadLayer("back", far * QUAD_BACK, false)
end
-- A flat quad that exactly fills the camera's view at distance d (oversize makes it larger; true
-- makes it as large as it must be to keep the screen covered however it turns, the screen's diagonal
-- over its height): for a video over, or as, the whole picture in 3D, where it can turn and glow.
-- What comes back is a pivot on the camera's axis with the quad stood up under it, so the pivot's
-- roll, nodeSetRotation(q, 0, 0, angle), turns the quad in its own plane. It starts hidden.
function K.screenQuad(camera, fov, material, d, oversize)
local h = 2 * d * math.tan(math.rad(fov / 2)) * (oversize or 1)
local turning = math.sqrt(1 + (K.W / K.H) ^ 2)
local h = 2 * d * math.tan(math.rad(fov / 2)) * ((oversize == true) and turning or (oversize or 1))
local pivot = nodeNew(camera)
local quad = nodeNew(pivot)
@ -846,13 +1054,13 @@ function K.scroller(t, font, text, y, amp, t0, t1, style)
end
if x + g.w > 0 then
local r, gg, b = K.hue(x / K.W * 0.5 + t * 0.2)
local h = spriteGetHeight(g.sprite)
local h = g.h
spriteSetTint(g.sprite, r, gg, b)
if style == "bounce" then
local hop = math.abs(math.sin(t * 5 + i * 0.45)) * amp * (0.5 + kick)
spriteDraw(g.sprite, math.floor(x), math.floor(y - hop))
K.draw(g.sprite, x, y - hop)
elseif style == "zoom" then
-- Laid out at the zoomed width, so the swollen letters push their neighbours apart
-- rather than piling onto them.
@ -861,7 +1069,7 @@ function K.scroller(t, font, text, y, amp, t0, t1, style)
local w = g.w * scale
local hh = h * scale
spriteDraw(g.sprite, math.floor(x), math.floor(y + h - hh), math.floor(x + w), math.floor(y + h))
K.draw(g.sprite, x, y + h - hh, x + w, y + h)
x = x + w - g.w
elseif style == "spin" then
-- Each letter spins in from the right, reads upright across the middle, and spins
@ -869,10 +1077,10 @@ function K.scroller(t, font, text, y, amp, t0, t1, style)
local spin = K.smooth(K.W * 0.62, K.W, x) - K.smooth(K.W * 0.38, 0, x)
spriteRotate(g.sprite, math.floor(360 * spin) % 360)
spriteDraw(g.sprite, math.floor(x + g.w / 2), math.floor(y + h / 2 + math.sin(x * 0.012 + t * 3) * amp), true)
K.draw(g.sprite, x + g.w / 2, y + h / 2 + math.sin(x * 0.012 + t * 3) * amp, true)
spriteRotate(g.sprite, 0)
else
spriteDraw(g.sprite, math.floor(x), math.floor(y + math.sin(x * 0.018 + t * 4) * amp))
K.draw(g.sprite, x, y + math.sin(x * 0.018 + t * 4) * amp)
end
end
x = x + g.w
@ -1001,6 +1209,16 @@ end
-- Under DEMO_STATS, once a second: frames drawn in the last second, and the longest gap between two.
-- Loads a sprite whose pixels are drawn density to a design unit: art made for the window's
-- resolution says K.S and draws pixel for pixel; art made for the old 640 by 480 screen says 1.
function K.sprite(path, d)
local sprite = spriteLoad(path)
density[sprite] = d or 1
return sprite
end
function K.stats(t, bar)
local now = singeGetTicks()
@ -1031,12 +1249,8 @@ end
-- Text in the bundled font at a point size, in a colour, left, centred ("center"), or right
-- aligned on x.
function K.text(x, y, text, size, r, g, b, align)
local font = fonts[size]
local font = K.font(size)
if not font then
font = fontLoad(FONT_FILE, size)
fonts[size] = font
end
fontSelect(font)
fontQuality(FONT_QUALITY_BLENDED)
colorForeground(r, g, b, 255)
@ -1050,26 +1264,20 @@ function K.text(x, y, text, size, r, g, b, align)
x = x - w
end
end
fontPrint(math.floor(x), math.floor(y), text)
fontPrint(math.floor(x * K.S), math.floor(y * K.S), text)
end
-- How wide text is in the bundled font at a point size, in pixels; measured once and remembered.
-- How wide text is in the bundled font at a size, in design units; measured once and remembered.
function K.textWidth(text, size)
local key = size .. ":" .. text
local w = widths[key]
if not w then
local font = fonts[size]
if not font then
font = fontLoad(FONT_FILE, size)
fonts[size] = font
end
fontSelect(font)
fontSelect(K.font(size))
local sprite = fontToSprite(text)
w = spriteGetWidth(sprite)
w = spriteGetWidth(sprite) / K.S
widths[key] = w
spriteUnload(sprite)
end
@ -1247,4 +1455,40 @@ function K.tunnelUpdate(fx, travel, turn)
end
-- A sprite turned angle degrees and scaled about its middle at x, y, under the overlay and over the
-- 3D world, tinted r, g, b, a: spriteRotateAndScale and a centred draw, on the GPU, where turning a
-- large picture every frame costs nothing.
function K.turn(sprite, x, y, angle, scale, r, g, b, a)
local w = K.width(sprite) * (scale or 1) / 2
local h = K.height(sprite) * (scale or 1) / 2
quadPlace(screen.layers.under, x - w, y - h, x + w, y + h, r or 255, g or 255, b or 255, a, sprite, false, nil, false, angle)
end
-- A sprite, or a colour when it is nil, under the overlay and over the 3D world, over x1, y1 to
-- x2, y2 in design units, tinted r, g, b, a; dynamic when its pixels change every frame. For a
-- 2D effect as big as the screen, which the overlay would spend milliseconds a frame drawing. On
-- the GPU.
function K.under(sprite, x1, y1, x2, y2, r, g, b, a, dynamic)
quadPlace(screen.layers.under, x1, y1, x2, y2, r or 255, g or 255, b or 255, a, sprite, dynamic)
end
-- An SVG drawn to fit w by h design units, keeping its shape, rasterised at the window's resolution
-- so it is sharp at any size.
function K.vector(path, w, h)
local sprite = spriteLoad(path, math.floor(w * K.S), math.floor(h * K.S))
density[sprite] = K.S
return sprite
end
-- A sprite's width in design units.
function K.width(sprite)
return spriteGetWidth(sprite) / (density[sprite] or 1)
end
return K

BIN
demos/mirrorball/art/bob.png (Stored with Git LFS)

Binary file not shown.

BIN
demos/mirrorball/art/logo.png (Stored with Git LFS)

Binary file not shown.

BIN
demos/mirrorball/art/singe.png (Stored with Git LFS)

Binary file not shown.

View file

@ -3,10 +3,10 @@ GAMES = {
{
TITLE = "Mirrorball",
SCRIPT = "mirrorball/mirrorball.singe",
CANVAS_X = 640,
CANVAS_Y = 480,
RESOLUTION_X = 1024,
RESOLUTION_Y = 768,
CANVAS_X = 1920,
CANVAS_Y = 1080,
RESOLUTION_X = 1920,
RESOLUTION_Y = 1080,
REQUIRES = { "SCENE", "POST", "SHADOWS" }
}
}

View file

@ -12,8 +12,6 @@
dofile("Singe/Framework.singe")
local K = dofile(DIR .. "../kit.lua")
local W = 640
local H = 480
local FOV = 60
local TILES = 12 -- The dance floor is TILES by TILES
local TILE = 2 -- World units across a tile
@ -35,6 +33,7 @@ local GROOVES = 256 -- The record's texture, a side
local RAIN_MAX = 70 -- Balls that fall on the snares in the boing scene
local TWIST_R = 92 -- The twister's half width, in pixels
local TWIST_ROW = 2 -- Screen rows each slice of the twister covers
local ART_DENSITY = 2.25 -- The logo's, the title's, and the bob's pixels to a design unit (util/mirrorballArt.py DENSITY)
local FX_W = 320 -- The live plasma's and tunnel's size, stretched to wherever they are shown
local FX_H = 240
local TUNNEL_LAP = 4 -- The tunnel's travel counts a lap of its texture as this many passes
@ -42,7 +41,9 @@ local PLASMA_STOPS = { { 0.00, { 20, 0, 10 } }, { 0.25, { 150, 20, 40 } }, { 0.5
local TUNNEL_STOPS = { { 0.00, { 20, 0, 10 } }, { 0.30, { 150, 20, 40 } }, { 0.55, { 255, 150, 30 } }, { 0.80, { 255, 230, 140 } }, { 1.00, { 255, 255, 255 } } }
local DISCO = { { 255, 40, 80 }, { 255, 160, 20 }, { 255, 240, 60 }, { 60, 255, 120 }, { 40, 200, 255 }, { 160, 60, 255 } }
local sync = K.load(DIR, W, H)
local sync = K.load(DIR)
local W = K.W -- The screen in design units: 480 tall (kit.lua)
local H = K.H
local art = {}
local world = {}
local lastT = 0
@ -55,12 +56,15 @@ local PARTY_TEXT = "GREETINGS TO EVERYONE ON THE DANCE FLOOR ... KEEP IT FUNK
-- Loading --------------------------------------------------------------------------------------------
local function loadArt()
for _, name in ipairs({ "logo", "singe", "twister", "copper", "bob", "glow", "sparkle" }) do
for _, name in ipairs({ "twister", "copper", "glow", "sparkle" }) do
art[name] = spriteLoad(DIR .. "art/" .. name .. ".png")
end
for _, name in ipairs({ "logo", "singe", "bob" }) do
art[name] = K.sprite(DIR .. "art/" .. name .. ".png", ART_DENSITY)
end
spriteQuality(art.logo, RENDER_SMOOTH)
spriteQuality(art.singe, RENDER_SMOOTH)
art.font = fontLoad("Singe/FreeSansBold.ttf", 44)
art.font = K.font(44)
-- The plasma and the tunnel, worked out live in Lua a frame at a time.
art.plasma = K.plasmaNew(FX_W, FX_H, PLASMA_STOPS)
art.tunnel = K.tunnelNew(FX_W, FX_H, TUNNEL_STOPS)
@ -297,14 +301,14 @@ local function buildScreens()
materialSetDoubleSided(roto, true)
materialSetFilter(roto, FILTER_NEAREST)
world.rotoMaterial = roto
world.roto = K.screenQuad(world.camera, FOV, roto, 2, 2.2)
world.roto = K.screenQuad(world.camera, FOV, roto, 2, true)
local tunnel = materialNew()
world.tunnelMaterial = tunnel
materialSetTexture(tunnel, art.tunnel.sprite)
materialSetUnlit(tunnel, true)
materialSetDoubleSided(tunnel, true)
world.tunnel = K.screenQuad(world.camera, FOV, tunnel, 60, 1.8)
world.tunnel = K.screenQuad(world.camera, FOV, tunnel, 60, true)
world.kaleido = nodeNew(world.camera)
world.kaleidoMaterials = {}
nodeSetPosition(world.kaleido, 0, 0, -2)
@ -512,6 +516,7 @@ local function buildWorld()
world.camera = nodeNew()
cameraSet(world.camera)
cameraSetPerspective(FOV, 0.1, 400)
K.screen(world.camera, FOV, 400)
buildFloor()
buildBall()
buildLights()
@ -739,18 +744,15 @@ end
-- 2D effects --------------------------------------------------------------------------------------------
-- Copper bars: count bars swinging up and down, coloured by hue, each a gradient strip stretched.
-- Copper bars: count bars swinging up and down, coloured by hue, each a gradient strip stretched
-- across the screen under the overlay.
local function copperBars(t, count, y0, amp, hueShift, alpha)
for i = 1, count do
local y = y0 + math.sin(t * 2.1 + i * 0.55) * amp
local r, g, b = K.hue(i / count * 0.6 + hueShift)
spriteSetTint(art.copper, r, g, b)
spriteSetAlpha(art.copper, alpha or 255)
spriteDraw(art.copper, 0, math.floor(y), W - 1, math.floor(y) + 23)
K.under(art.copper, 0, y, W, y + 24, r, g, b, alpha)
end
spriteSetTint(art.copper, 255, 255, 255)
spriteSetAlpha(art.copper, 255)
end
@ -774,7 +776,7 @@ local function twister(t, cx)
local shade = math.floor(80 + 175 * math.sin(math.min(math.pi, (x[e + 1] - x[e]) / (2 * TWIST_R) * math.pi * 0.72)))
spriteSetTint(art.twister, shade, shade, shade)
spriteDrawGrid(art.twister, math.floor(x[e]), y, e * faceW, row, faceW, 1, math.floor(x[e + 1]), y + TWIST_ROW - 1)
K.drawGrid(art.twister, x[e], y, e * faceW, row, faceW, 1, x[e + 1], y + TWIST_ROW)
end
end
end
@ -1017,16 +1019,16 @@ local SCENES = {
hideAll()
end,
draw = function(t, bar, k)
K.flash(0, 0, 0, 255)
K.fill(0, 0, 0)
local gate = K.clamp((K.env("level", t) - 0.15) * 2.5, 0, 1)
copperBars(t, 10, 210, 170, t * 0.05, math.floor(255 * gate))
titleDrop(t, K.beat(K.barTime(2)), 2, 230)
if bar >= 7 then
local w = spriteGetWidth(art.singe) * 0.5
local w = K.width(art.singe) * 0.5
spriteSetAlpha(art.singe, math.floor(255 * K.smooth(7, 7.5, bar)))
spriteDraw(art.singe, math.floor(W / 2 - w / 2), 330, math.floor(W / 2 + w / 2), 330 + spriteGetHeight(art.singe) // 2)
K.draw(art.singe, W / 2 - w / 2, 330, W / 2 + w / 2, 330 + K.height(art.singe) / 2)
spriteSetAlpha(art.singe, 255)
K.text(W / 2, 312, "PRESENTED WITH", 18, 200, 180, 140, "center")
end
@ -1053,11 +1055,11 @@ local SCENES = {
floor(t, 0, 0.3 + 0.7 * drop)
if bar >= 13 then
local a = math.floor(255 * K.smooth(13, 13.5, bar))
local w = spriteGetWidth(art.logo) * 0.85
local h = spriteGetHeight(art.logo) * 0.85
local w = K.width(art.logo) * 0.85
local h = K.height(art.logo) * 0.85
spriteSetAlpha(art.logo, a)
spriteDraw(art.logo, math.floor(W / 2 - w / 2), math.floor(H - 20 - h), math.floor(W / 2 + w / 2), H - 20)
K.draw(art.logo, W / 2 - w / 2, H - 20 - h, W / 2 + w / 2, H - 20)
spriteSetAlpha(art.logo, 255)
end
K.flash(255, 255, 255, math.floor(255 * (1 - K.smooth(9, 9.25, bar))))
@ -1144,7 +1146,7 @@ local SCENES = {
hideAll()
end,
draw = function(t, bar, k)
K.flash(14, 4, 22, 255)
K.fill(14, 4, 22)
copperBars(t, 14, 220, 200, t * 0.03, 200)
twister(t, W / 2 + math.sin(t * 0.7) * 120)
K.scroller(t, art.font, SECOND_TEXT, 30, 14, K.barTime(41), K.barTime(49))
@ -1249,7 +1251,7 @@ local SCENES = {
end,
draw = function(t, bar, k)
K.plasmaUpdate(art.plasma, t)
spriteDraw(art.plasma.sprite, 0, 0, W - 1, H - 1)
K.under(art.plasma.sprite, 0, 0, W, H, nil, nil, nil, nil, true)
local punch = 1 + 0.3 * K.decay("kicks", t, 8)
for i = 1, 12 do
@ -1258,16 +1260,12 @@ local SCENES = {
local cr, cg, cb = K.hue(i / 12 + t * 0.1)
spriteSetTint(art.bob, cr, cg, cb)
spriteDraw(art.bob, math.floor(W / 2 + math.cos(a) * r - 18), math.floor(H / 2 + math.sin(a) * r * 0.8 - 18), math.floor(W / 2 + math.cos(a) * r + 18), math.floor(H / 2 + math.sin(a) * r * 0.8 + 18))
K.draw(art.bob, W / 2 + math.cos(a) * r - 18, H / 2 + math.sin(a) * r * 0.8 - 18, W / 2 + math.cos(a) * r + 18, H / 2 + math.sin(a) * r * 0.8 + 18)
end
spriteSetTint(art.bob, 255, 255, 255)
spriteRotateAndScale(art.singe, math.sin(t * 0.9) * 35, (1 + 0.4 * math.sin(t * 1.1)) * punch)
spriteDraw(art.singe, W // 2, H // 2, true)
K.turn(art.singe, W / 2, H / 2, math.sin(t * 0.9) * 35, (1 + 0.4 * math.sin(t * 1.1)) * punch)
K.flash(255, 255, 255, math.floor(255 * (1 - K.smooth(73, 73.2, bar))))
end,
leave = function(t)
spriteRotateAndScale(art.singe, 0, 1)
end,
},
-- Bars 81 to 88: the breakdown. The kick is gone; the club is dark but for the ball and its
-- sparkles, turning slowly. When the bass returns at bar 85 the floor comes back a tile a beat.

BIN
demos/neonCoast/art/bob.png (Stored with Git LFS)

Binary file not shown.

BIN
demos/neonCoast/art/car.png (Stored with Git LFS)

Binary file not shown.

BIN
demos/neonCoast/art/lamp.png (Stored with Git LFS)

Binary file not shown.

BIN
demos/neonCoast/art/logo.png (Stored with Git LFS)

Binary file not shown.

BIN
demos/neonCoast/art/palm.png (Stored with Git LFS)

Binary file not shown.

BIN
demos/neonCoast/art/singe.png (Stored with Git LFS)

Binary file not shown.

BIN
demos/neonCoast/art/traffic.png (Stored with Git LFS)

Binary file not shown.

View file

@ -3,10 +3,10 @@ GAMES = {
{
TITLE = "Neon Coast",
SCRIPT = "neonCoast/neonCoast.singe",
CANVAS_X = 640,
CANVAS_Y = 480,
RESOLUTION_X = 1024,
RESOLUTION_Y = 768,
CANVAS_X = 1920,
CANVAS_Y = 1080,
RESOLUTION_X = 1920,
RESOLUTION_Y = 1080,
REQUIRES = { "SCENE", "POST" }
}
}

View file

@ -13,9 +13,10 @@
-- five kinds, a rotozoomed logo, and fireworks. Nothing on the screen is a recording.
dofile("Singe/Framework.singe")
local K = dofile(DIR .. "../kit.lua")
local sync = K.load(DIR)
local W = K.W -- The screen in design units: 480 tall (kit.lua)
local H = K.H
local W = 640
local H = 480
local FOV = 62 -- The 3D camera's vertical field of view, in degrees
local GRID_CELL = 4 -- The floor grid's cell in world units: the tiling the floor uses
local FLOOR_SIZE = 600
@ -34,7 +35,8 @@ local LAMP_EVERY = 8 -- Segments between streetlights: two p
local SEGS_PER_BAR = LAMP_EVERY * 8
local PLAYER_AHEAD = 72 -- The car's distance ahead of the camera
local CAR_FRAMES = 7 -- The car's sheet: steering from hard left to hard right
local CAR_SCALE = 0.85
local CAR_SCALE = 0.85 -- The car's size: design units to a unit of its sheet as it was drawn for 640 by 480
local ART_DENSITY = 2.25 -- The drawn and rendered art's pixels to a design unit (util/neonCoastArt.py, util/neonCoastCar.sh DENSITY)
local TRAFFIC_FRAMES = 4 -- The traffic sheet: the same car in four other paints
local TRAFFIC_WIDTH = 34 -- A car's width in world units: what our own car is on the road
local TRAFFIC_LANE = 73 -- How far either side of the middle the outside lanes run
@ -61,7 +63,6 @@ local PEAK_FOG_FAR = 2700 -- Wholly fogged before the farthest tile
local PEAK_HAZE = { 225, 40, 125 } -- The sky's colour just above the horizon: the far ridges melt into it
local PEAK_SPEED = 240
local sync = K.load(DIR, W, H)
local beat = sync.beats[3] - sync.beats[2]
local speed = LAMP_EVERY * SEG / (beat / 2) -- So two streetlights pass on every beat
@ -116,9 +117,12 @@ local NECK_SHARE = 0.35 -- How much of the head's nod the neck tak
-- Loading --------------------------------------------------------------------------------------------
local function loadArt()
for _, name in ipairs({ "palm", "lamp", "road", "bob", "glow", "logo", "singe" }) do
for _, name in ipairs({ "road", "glow" }) do
art[name] = spriteLoad(DIR .. "art/" .. name .. ".png")
end
for _, name in ipairs({ "palm", "lamp", "bob", "logo", "singe" }) do
art[name] = K.sprite(DIR .. "art/" .. name .. ".png", ART_DENSITY)
end
art.car = spriteLoadFrames(CAR_FRAMES, DIR .. "art/car.png")
art.traffic = spriteLoad(DIR .. "art/traffic.png")
spriteQuality(art.traffic, RENDER_SMOOTH)
@ -143,6 +147,7 @@ local function buildWorld()
world.camera = nodeNew()
cameraSet(world.camera)
cameraSetPerspective(FOV, 0.1, 4000)
K.screen(world.camera, FOV, 4000)
-- A dark glossy floor that reflects the sun, with the grid in it as light alone: a black base
-- colour would tint the reflection black too.
@ -236,7 +241,7 @@ local function buildWorld()
materialSetTexture(tunnel, art.tunnel.sprite)
materialSetUnlit(tunnel, true)
materialSetDoubleSided(tunnel, true)
world.tunnel = K.screenQuad(world.camera, FOV, tunnel, 2, 1.7)
world.tunnel = K.screenQuad(world.camera, FOV, tunnel, 2, true)
-- The flag on its pole, cloth that the kicks blow about.
local pole = materialNew()
@ -582,13 +587,16 @@ local function drawRoad(t, position, bright, airY)
local height = segment.sy1 - segment.sy2
if top <= bottom then
spriteDrawGrid(art.road, 0, top, 0, (2 + light) * HAZE + haze, STRIP_W, 1, W - 1, bottom)
for row = bottom, top, -1 do
K.drawGrid(art.road, 0, top, 0, (2 + light) * HAZE + haze, STRIP_W, 1, W, bottom + 1)
-- The road itself a screen row at a time, the window's rows rather than design
-- units, so its edges are as fine as the screen.
for real = math.floor((bottom + 1) * K.S) - 1, math.floor(top * K.S), -1 do
local row = real / K.S
local k = (segment.sy1 - row) / height
local cx = segment.sx1 + (segment.sx2 - segment.sx1) * k
local half = ROAD_HALF * (s1 + (s2 - s1) * k)
spriteDrawGrid(art.road, math.floor(cx - half), row, 0, light * HAZE + haze, STRIP_W, 1, math.floor(cx + half), row)
spriteDrawGrid(art.road, math.floor((cx - half) * K.S), real, 0, light * HAZE + haze, STRIP_W, 1, math.floor((cx + half) * K.S), real)
end
clip = top
end
@ -621,18 +629,18 @@ local function drawRoad(t, position, bright, airY)
local top = foot - h
if w >= 1 and cx + w > 0 and cx - w < W and top < segment.clip then
local left = math.floor(cx - w / 2)
local right = math.floor(cx + w / 2)
local left = cx - w / 2
local right = cx + w / 2
if thing.width < 0 then
left, right = right, left
end
if foot <= segment.clip then
spriteDraw(thing.sprite, math.min(left, right), math.floor(top), math.max(left, right), math.floor(foot) - 1)
K.draw(thing.sprite, math.min(left, right), top, math.max(left, right), foot)
else
local shown = math.min(sh, math.max(1, math.floor(sh * (segment.clip - top) / h)))
spriteDrawGrid(thing.sprite, math.min(left, right), math.floor(top), 0, 0, sw, shown, math.max(left, right), segment.clip - 1)
K.drawGrid(thing.sprite, math.min(left, right), top, 0, 0, sw, shown, math.max(left, right), segment.clip)
end
if thing.lamp and foot <= segment.clip + h * 0.9 then
-- The lamp's light: a glow added over its head, the head being at the arm's end.
@ -642,7 +650,7 @@ local function drawRoad(t, position, bright, airY)
spriteSetBlend(art.glow, SPRITE_ADD)
spriteSetTint(art.glow, 255, 220, 170)
spriteSetAlpha(art.glow, 255)
spriteDraw(art.glow, math.floor(headX - g / 2), math.floor(top + h * 0.07 - g / 2), math.floor(headX + g / 2), math.floor(top + h * 0.07 + g / 2))
K.draw(art.glow, headX - g / 2, top + h * 0.07 - g / 2, headX + g / 2, top + h * 0.07 + g / 2)
end
end
end
@ -659,15 +667,15 @@ local function drawRoad(t, position, bright, airY)
if w >= 2 and cx + w > 0 and cx - w < W and top < segment.clip then
local shown = math.min(sh, math.max(1, math.floor(sh * (math.min(foot, segment.clip) - top) / h)))
local bottom = math.min(foot, segment.clip) - 1
local bottom = math.min(foot, segment.clip)
spriteDrawGrid(art.traffic, math.floor(cx - w / 2), math.floor(top), entry.car.paint * frame, 0, frame, shown, math.floor(cx + w / 2), math.floor(bottom))
K.drawGrid(art.traffic, cx - w / 2, top, entry.car.paint * frame, 0, frame, shown, cx + w / 2, bottom)
-- Its taillights, added over the road.
if foot <= segment.clip then
spriteSetBlend(art.glow, SPRITE_ADD)
spriteSetTint(art.glow, 255, 30, 60)
spriteSetAlpha(art.glow, 150)
spriteDraw(art.glow, math.floor(cx - w * 0.75), math.floor(top + h * 0.45), math.floor(cx + w * 0.75), math.floor(top + h * 0.95))
K.draw(art.glow, cx - w * 0.75, top + h * 0.45, cx + w * 0.75, top + h * 0.95)
spriteSetAlpha(art.glow, 255)
end
end
@ -689,15 +697,15 @@ local function drawCar(t, car, lift, spin)
local cy = H - 64 - bounce - (lift or 0)
spriteRotateFrame(art.car, lean, 4 - steer)
spriteDrawRotatedFrame(art.car, math.floor(cx), math.floor(cy), CAR_SCALE)
spriteDrawRotatedFrame(art.car, math.floor(cx * K.S), math.floor(cy * K.S), CAR_SCALE * K.S / ART_DENSITY)
local glowW = 230 + 70 * K.decay("kicks", t, 6)
spriteSetBlend(art.glow, SPRITE_ADD)
spriteSetTint(art.glow, 255, 30, 60)
spriteSetAlpha(art.glow, 170)
spriteDraw(art.glow, math.floor(cx - glowW / 2), math.floor(cy - 22), math.floor(cx + glowW / 2), math.floor(cy + 30))
K.draw(art.glow, cx - glowW / 2, cy - 22, cx + glowW / 2, cy + 30)
spriteSetTint(art.glow, 255, 40, 200)
spriteDraw(art.glow, math.floor(cx - 140), math.floor(cy + 30 + (lift or 0)), math.floor(cx + 140), math.floor(cy + 66 + (lift or 0)))
K.draw(art.glow, cx - 140, cy + 30 + (lift or 0), cx + 140, cy + 66 + (lift or 0))
spriteSetBlend(art.glow, SPRITE_ALPHA)
spriteSetTint(art.glow, 255, 255, 255)
spriteSetAlpha(art.glow, 255)
@ -805,13 +813,13 @@ end
-- new burst never recolours the sparks of the last, with a white crackle at its heart.
local function fireworks(t, lastT)
if K.hitCount("snares", lastT, t) > 0 then
local x = math.random(80, W - 80)
local x = math.random(80, math.floor(W - 80))
local y = math.random(60, H / 2 - 40)
art.nextFirework = art.nextFirework % #art.fireworks + 1
emitterSetPosition(art.fireworks[art.nextFirework], x, y)
emitterSetPosition(art.fireworks[art.nextFirework], x * K.S, y * K.S)
emitterBurst(art.fireworks[art.nextFirework], 140)
emitterSetPosition(art.crackle, x, y)
emitterSetPosition(art.crackle, x * K.S, y * K.S)
emitterBurst(art.crackle, 30)
end
for _, e in ipairs(art.fireworks) do
@ -833,10 +841,11 @@ local SCENES = {
end,
draw = function(t, bar, k)
K.snowUpdate(art.snow, t)
spriteDraw(art.snow.sprite, 0, 0, W - 1, H - 1)
K.under(art.snow.sprite, 0, 0, W, H, nil, nil, nil, nil, true)
local dark = 1 - 0.75 * K.smooth(1.0, 2.0, bar) + 0.75 * K.smooth(3.5, 3.95, bar)
K.flash(0, 0, 0, math.floor(255 * K.clamp(dark, 0, 1)))
-- The snow dims, under the deck's lettering.
K.under(nil, 0, 0, W, H, 0, 0, 0, math.floor(255 * K.clamp(dark, 0, 1)))
local blink = (math.floor(t * 2) % 2 == 0)
K.text(40, 34, "PLAY", 30, 255, 255, 255)
@ -882,16 +891,18 @@ local SCENES = {
-- The title slams in on the downbeat and settles; a shine runs across it each bar.
local p = (bar - 13)
local scale = 1 + 2.5 * math.exp(-p * 9)
local lw = spriteGetWidth(art.logo) * 0.8 * scale
local lh = spriteGetHeight(art.logo) * 0.8 * scale
local lw = K.width(art.logo) * 0.8 * scale
local lh = K.height(art.logo) * 0.8 * scale
local y = 395
spriteSetAlpha(art.logo, math.floor(255 * K.clamp(p * 6, 0, 1)))
spriteDraw(art.logo, math.floor(W / 2 - lw / 2), math.floor(y - lh / 2), math.floor(W / 2 + lw / 2), math.floor(y + lh / 2))
K.draw(art.logo, W / 2 - lw / 2, y - lh / 2, W / 2 + lw / 2, y + lh / 2)
spriteSetAlpha(art.logo, 255)
-- GAME ENGINE under the logotype, wherever the logotype is on the screen, spelled out
-- a letter at a time across the first beat.
local _, under = sceneProject(0, 2.6 - LOGO_DROP, -16)
under = under / K.S
local letters = math.floor(K.clamp(p * 4 * #ENGINE_TEXT, 0, #ENGINE_TEXT))
if letters > 0 then
@ -1062,9 +1073,7 @@ local SCENES = {
cityRun(K.barTime(39) + road.tape, K.barTime(39))
if rate < 0 then
K.snowUpdate(art.snow, t)
spriteSetAlpha(art.snow.sprite, REWIND_SNOW)
spriteDraw(art.snow.sprite, 0, 0, W - 1, H - 1)
spriteSetAlpha(art.snow.sprite, 255)
K.under(art.snow.sprite, 0, 0, W, H, 255, 255, 255, REWIND_SNOW, true)
label = "<< REW"
elseif resumed then
label = "PLAY >"
@ -1145,7 +1154,7 @@ local SCENES = {
end,
draw = function(t, bar, k)
K.plasmaUpdate(art.plasma, t)
spriteDraw(art.plasma.sprite, 0, 0, W - 1, H - 1)
K.under(art.plasma.sprite, 0, 0, W, H, nil, nil, nil, nil, true)
local punch = 1 + 0.25 * K.decay("kicks", t, 8)
for i = 1, 8 do
@ -1154,14 +1163,12 @@ local SCENES = {
local px = W / 2 + math.cos(math.rad(a)) * r
local py = H / 2 + math.sin(math.rad(a)) * r
spriteRotateAndScale(art.palm, a + 90, 0.55 * punch)
spriteDraw(art.palm, math.floor(px), math.floor(py), true)
K.turn(art.palm, px, py, a + 90, 0.55 * punch)
end
local scale = (0.9 + 0.35 * math.sin(t * 1.3)) * punch
local angle = math.sin(t * 0.7) * 25
spriteRotateAndScale(art.singe, angle, scale)
spriteDraw(art.singe, W // 2, H // 2, true)
K.turn(art.singe, W / 2, H / 2, angle, scale)
K.circleScroller(t, art.smallFont, CIRCLE_TEXT, W / 2, H / 2, 190, 50, K.barTime(61), K.barTime(65))
K.strobe(t, "kicks", 12, 255, 255, 255, 70)
local intro = 1 - K.smooth(61, 61.15, bar)
@ -1170,10 +1177,6 @@ local SCENES = {
K.flash(255, 255, 255, math.floor(255 * intro))
end
end,
leave = function(t)
spriteRotateAndScale(art.palm, 0, 1)
spriteRotateAndScale(art.singe, 0, 1)
end,
},
-- Bars 65 to 68: low and fast over neon mountains toward the sun.
{
@ -1340,20 +1343,20 @@ local SCENES = {
draw = function(t, bar, k)
local p = t - K.barTime(95)
K.flash(0, 0, 0, 255)
K.fill(0, 0, 0)
if p < 0.6 then
local hgt = (p < 0.25) and K.lerp(H, 3, K.easeIn(p / 0.25)) or 3
local wid = (p < 0.25) and W or K.lerp(W, 2, K.easeIn((p - 0.25) / 0.3))
K.box(W / 2 - wid / 2, H / 2 - hgt / 2, W / 2 + wid / 2, H / 2 + hgt / 2, 255, 240, 255, 255)
K.under(nil, W / 2 - wid / 2, H / 2 - hgt / 2, W / 2 + wid / 2, H / 2 + hgt / 2, 255, 240, 255, 255)
end
end,
},
}
loadArt()
art.scrollFont = fontLoad("Singe/FreeSansBold.ttf", 44)
art.smallFont = fontLoad("Singe/FreeSansBold.ttf", 30)
art.scrollFont = K.font(44)
art.smallFont = K.font(30)
art.fireworks = {}
art.nextFirework = 0
for _, c in ipairs(FIREWORK_COLOURS) do
@ -1362,12 +1365,12 @@ for _, c in ipairs(FIREWORK_COLOURS) do
emitterSetBlend(e, PARTICLE_ADD)
emitterSetRate(e, 0)
emitterSetLife(e, 0.8, 1.6)
emitterSetSpeed(e, 80, 260)
emitterSetSpeed(e, 80 * K.S, 260 * K.S)
emitterSetSpread(e, 180)
emitterSetGravity(e, 0, 160)
emitterSetGravity(e, 0, 160 * K.S)
emitterSetDrag(e, 1.2)
emitterSetSize(e, 8, 2)
emitterSetTrail(e, 6, 3)
emitterSetSize(e, 8 * K.S, 2 * K.S)
emitterSetTrail(e, 6, 3 * K.S)
emitterSetColor(e, c[1], c[2], c[3], 255, c[4], c[5], c[6], 0)
emitterSetMax(e, 800)
emitterStart(e)
@ -1377,9 +1380,9 @@ art.crackle = emitterNew()
emitterSetBlend(art.crackle, PARTICLE_ADD)
emitterSetRate(art.crackle, 0)
emitterSetLife(art.crackle, 0.2, 0.5)
emitterSetSpeed(art.crackle, 20, 90)
emitterSetSpeed(art.crackle, 20 * K.S, 90 * K.S)
emitterSetSpread(art.crackle, 180)
emitterSetSize(art.crackle, 5, 1)
emitterSetSize(art.crackle, 5 * K.S, 1 * K.S)
emitterSetColor(art.crackle, 255, 255, 255, 255, 255, 240, 200, 0)
emitterSetMax(art.crackle, 300)
emitterStart(art.crackle)

File diff suppressed because it is too large Load diff

View file

@ -1,721 +0,0 @@
// Generated by util/forgeVocabulary.lua from forge/Author.singe. Do not edit.
=== Layers
[cols="1,3,2"]
|===
| Name | What it does | Parameters
| `bezel`
| The arcade score panel around the picture, showing the vars score (and score2), lives (and lives2), and credits as they change.
| `twin` (boolean)
| `disc`
| The video the game is played over. The file is what the editor scrubs; games.dat names it when the game runs.
| `file` (file), `start` (number)
| `hud`
| An RmlUi document over everything. bind maps element ids to game vars, refreshed as they change.
| `bind` (table), `document` (file)
| `music`
| A track that plays from the start, looping.
| `file` (file), `volume` (number)
| `overlay`
| Flat drawing over everything: sprites, scores, prompts.
| --
| `parser`
| A text parser: a prompt the player types at, and words the game knows. What is typed raises the said event.
| `ignore` (table), `prompt` (string), `words` (table)
| `scene3d`
| The 3D scene: physics in three axes, a sun, a sky, fog. Positions are world units.
| `ambient` (table), `exposure` (number), `fog` (table), `fov` (number), `gravity` (number), `sky` (file), `sun` (table)
| `world2d`
| A flat world with gravity: physics in the XY plane, drawn into the overlay.
| `gravity` (number)
|===
=== Looks
[cols="1,3,2"]
|===
| Name | What it does | Parameters
| `billboard`
| A sprite in the scene, always facing the camera: a tree, a puff, a health bar.
| `d` (number), `file` (file), `h` (number), `height` (number), `w` (number)
| `box`
| A filled rectangle, centred on the instance -- or standing on it, with anchor feet.
| `anchor` (feet), `b` (number), `g` (number), `h` (number), `r` (number), `w` (number)
| `grid`
| A map of tiles from a sheet, its top left at the instance: columns is the sheet's width in tiles, tile the tile's size, map the rows of tile numbers (1 is the first; 0 is empty), rows separated by semicolons.
| `columns` (number), `file` (file), `map` (string), `tile` (number)
| `light`
| A point or spot light, or a sun, on the instance.
| `b` (number), `g` (number), `intensity` (number), `r` (number), `range` (number), `shadow` (boolean), `type` (point, spot, sun)
| `mesh`
| A box, sphere, cylinder, or plane with a colour or a texture. Floors, walls, crates, targets. An occluder is not seen but hides what is behind it: a pillar that is only in the painting.
| `b` (number), `d` (number), `g` (number), `h` (number), `metallic` (number), `occluder` (boolean), `r` (number), `roughness` (number), `shape` (box, sphere, cylinder, plane), `texture` (file), `unlit` (boolean), `w` (number)
| `model`
| A glTF model under the instance, scaled and turned, playing a clip. w, h, d say how big it counts as.
| `clip` (string), `d` (number), `file` (file), `h` (number), `rx` (number), `ry` (number), `rz` (number), `scale` (number), `w` (number)
| `none`
| Nothing drawn: a trigger, a spawner, a hitbox over video, a camera.
| `d` (number), `h` (number), `w` (number)
| `particles`
| A steady stream of particles from the instance: a fire, smoke, a thruster, rain. Colour, size, speed, and life as an emitter takes them.
| `b` (number), `g` (number), `gravity` (number), `life` (number), `r` (number), `rate` (number), `size` (number), `speed` (number), `spread` (number)
| `qteHud`
| The HUD of a quick-time-event game: what its qte behaviour's state says a player sees (the score and the record, the lives or the life bar, and the rest), drawn from the game's own pictures and font as the game lays them out.
| --
| `sprite`
| An image, centred on the instance -- or standing on it, with anchor feet. frames splits a sheet into that many columns; the var frame picks one. The var angle turns it, starting from the entity's rz. faces says which way the art looks (right unless said, or none): while the var facing is the other way the image is mirrored, so art drawn one way walks both. The var copies draws it that many times in a row, step pixels apart (a row of bullets, of hearts): negative steps go left.
| `anchor` (feet), `faces` (right, left, none), `file` (file), `frames` (number), `step` (number)
| `text`
| A line of text, its top left at the instance -- or centred on it, standing on it (feet), or hanging from it (top). The var text is what it says. A font file and size of its own, or the game's.
| `anchor` (centre, feet, top), `b` (number), `font` (file), `g` (number), `r` (number), `size` (number), `text` (string)
| `text3d`
| A line of text standing in the scene. The var text is what it says.
| `height` (number), `text` (string)
|===
=== Behaviours
[cols="1,3,2"]
|===
| Name | What it does | Parameters
| `animator`
| A clip per state: idle, walk, attack, hit, dead, or any state a rule sets. A clip that does not loop raises animationDone when it ends.
| `attack` (string), `dead` (string), `fade` (number), `hit` (string), `idle` (string), `loop` (string), `walk` (string)
| `body`
| A thing physics moves: a crate, a ball, debris. Mass, bounce, friction, and buoyancy as Jolt takes them.
| `bounce` (number), `buoyancy` (number), `friction` (number), `mass` (number), `shape` (box, sphere)
| `branching`
| Branch windows over the disc: a track of branches, each a disc frame window, the move it wants, and where the disc goes on success and on failure. A branch with mash wants the move that many times inside the window; one with hold wants it held that many frames. Raises branchOpen, branchTaken, and branchMissed.
| `track` (track)
| `camera`
| The scene is drawn from this instance. fixed looks at a point; follow keeps an offset from a target; first sits on a target's eyes and the mouse looks; orbit circles a target; rail rides a track of points with stops.
| `eye` (number), `lag` (number), `lookX` (number), `lookY` (number), `lookZ` (number), `mode` (fixed, follow, first, orbit, rail), `target` (string), `track` (track), `x` (number), `y` (number), `z` (number)
| `character`
| Walks, falls, and jumps in the scene: the engine's character controller. Moves by the vars dx and dy, relative to the camera.
| `height` (number), `jump` (number), `radius` (number), `speed` (number)
| `drift`
| Moves steadily, for a cloud, a platform, or a target.
| `vx` (number), `vy` (number)
| `frames`
| Steps a sprite look's frame: a range of frames per state, as "idle=1-1, walk=2-5", at a rate a second. A sheet drawn facing both ways names the other way's frames stateLeft or stateRight (walkLeft), used while the var facing is that way instead of mirroring the look.
| `fps` (number), `states` (string)
| `gun`
| The pointer as a gun for one player: a press of the trigger raises hit on what is under it, or miss. Ammo counts down; a shot off the screen reloads. With aim centre it fires from the middle of the picture -- a first-person gun.
| `aim` (centre), `ammo` (number), `player` (number), `reload` (offscreen), `trigger` (switch)
| `health`
| The var health; the damage action lowers it, and at zero the instance raises death and is destroyed unless keep is set. With ragdoll, a model falls limp instead and goes after linger seconds.
| `keep` (boolean), `linger` (number), `max` (number), `ragdoll` (boolean)
| `hitbox`
| The instance's hit shape comes from a track of boxes keyed by disc frame or time; between keys it is interpolated, outside them there is none. A track with step = true keeps a box only on the frames it keys.
| `track` (track)
| `hotspot`
| Something a verb can be used on: a name for the sentence line, a place to walk to first, and a polygon (x,y pairs) when its look's box will not do.
| `name` (string), `polygon` (table), `walkX` (number), `walkY` (number), `walkZ` (number)
| `joint`
| Joins this body to another's -- or to the world, with no other -- by a hinge, a ball, or a slider through a world point along an axis.
| `ax` (number), `ay` (number), `az` (number), `dx` (number), `dy` (number), `dz` (number), `other` (string), `type` (hinge, ball, slider)
| `keys`
| Reads a player's keys into the vars dx and dy (-1 to 1) and fire, so rules and movers read intent rather than hardware.
| `down` (scancode), `fire` (scancode), `left` (scancode), `player` (number), `right` (scancode), `up` (scancode)
| `lightSensor`
| Judges a shot by the picture rather than by a hitbox, the way a home laserdisc gun does: a spot on the picture (x, y) flashes bright or dark on the frames a target is shown, and a shot is good when the picture under the gun changes the way the spot does over the two frames after the trigger. A shot into the spot's own corner (left, top and beyond) is not judged, so the spot cannot be shot. Raises fire on the trigger, then hit or miss. For a game whose disc carries the targets in its own picture; a game with hitboxes wants gun and target instead.
| `high` (number), `left` (number), `low` (number), `player` (number), `top` (number), `trigger` (switch), `x` (number), `y` (number)
| `mover`
| Moves by the vars dx and dy at a speed, kept on the screen when clamp is set. Pair it with keys.
| `clamp` (boolean), `speed` (number)
| `patrol`
| Walks a list of points in turn -- a creep's lane, a guard's round -- looping or stopping, and raises patrolEnd at the last.
| `loop` (boolean), `points` (table)
| `platformer`
| Runs, falls, and jumps: the engine's character controller in 2D. Rules say which way with run and jump. Its point is its feet, so its look stands on it. Sets the var facing, and the var state to idle, walk, jump, or fall unless a rule set another.
| `jump` (number), `speed` (number)
| `pointer`
| Follows a player's pointer: a crosshair, a hand. Hidden when the engine says a real gun wants no crosshair drawn.
| `player` (number)
| `projectile`
| Flies at a velocity and is dropped past the edge (or far away, in 3D) or after life seconds. With aim, a sprite turns to point the way it flies.
| `aim` (boolean), `life` (number), `vx` (number), `vy` (number), `vz` (number)
| `qte`
| Plays a laserdisc quick-time-event game from the tables under game.qte: an attract loop, credits, levels of scenes with moves to answer in a window of frames, death clips, continues, the settings, and the game's own add-ons. qte.options.style says how its moves are judged and its levels run: windows (the default: a move answered in a window, compound moves too), held (moves judged by what is held together: combos, mashes, holds, sequences, paths, timed and yes/no branches, tilt, a life bar, die and retry, a new-game menu, a level select), segments (levels of segments, a map between them, the game's own level functions run as they are), checkpoints (checkpoints, a time-stop choice of three, a tally by deaths, a life per quota), or trophies (segmented levels judged the windows way, a difficulty select, a level-clear screen, trophies). The port converter writes the tables from an existing game's script.
| --
| `racer`
| Drives a vehicle round a track of points on its own: steering toward the next, throttling by how straight the road is.
| `reach` (number), `throttle` (number), `track` (track)
| `seek`
| Moves straight toward a target -- an entity's id, or "camera" -- and raises arrived within stopAt of it.
| `fly` (boolean), `speed` (number), `stopAt` (number), `target` (string)
| `shooter`
| Spawns a type from an offset while the var fire is set (or always, with auto), no faster than the rate.
| `auto` (boolean), `offsetX` (number), `offsetY` (number), `rate` (number), `spawns` (type)
| `soft`
| A mesh look made soft: a cloth, or a body inflated to a pressure.
| `mass` (number), `pressure` (number), `stiffness` (number), `type` (cloth, body)
| `solid`
| Immovable ground or a wall, in a world2d or a scene3d room. With moving, a body that follows wherever the instance is moved -- a paddle, a lift.
| `moving` (boolean)
| `sound`
| A clip per event: spawn, hit, miss, death, fire (a gun's trigger), or any event the instance sees. Values are file names. volume is the clips' own, 0 to 100, under the game's.
| `death` (file), `fire` (file), `hit` (file), `miss` (file), `pressed` (file), `spawn` (file), `volume` (number)
| `spawner`
| Makes instances of a type every so many seconds at its own position or at one of its points, up to max alive at once and total in all.
| `every` (number), `max` (number), `pick` (turn, random), `points` (table), `spawns` (type), `total` (number)
| `spin`
| Turns steadily: the var angle in 2D (a sprite turns with it), the node about Y in 3D, in degrees a second.
| `rate` (number)
| `target`
| Something a gun can hit. In 2D its look's box; in 3D a body the size of its look, plus zones -- boxes on named bones, so a shot says which part it hit.
| `zones` (table)
| `thrust`
| A body pushed along its nose by the var dy and turned by dx: a hovercraft, a ship, a plane without lift.
| `force` (number), `turn` (number)
| `timer`
| Raises the timer event with its name after some seconds, or every so many. start false waits for timerStart.
| `after` (number), `every` (number), `name` (string), `start` (boolean)
| `trigger`
| A volume that reports what enters and leaves it, as the enter and leave events. A checkpoint, a doorway, a prize.
| --
| `turret`
| Fires at the nearest instance of a type within range, no faster than the rate: spawns a projectile aimed at it, or, with no projectile, does the damage itself.
| `damage` (number), `range` (number), `rate` (number), `spawns` (type), `speed` (number), `targets` (type)
| `vehicle`
| A car, motorcycle, tank, or boat on the engine's vehicle physics, driven by the vars dy (throttle, up is forward) and dx (steering) from keys. Wheels hang at the four corners the sizes say; a boat needs none.
| `mass` (number), `radius` (number), `suspension` (number), `thrust` (number), `torque` (number), `type` (car, motorcycle, tank, boat), `wheelX` (number), `wheelZ` (number), `width` (number)
| `walker`
| Walks the room's walk areas (2D) or its floors (3D) on the navigation mesh: walkTo sends it, a click on the floor can, follow keeps it after an entity (or the camera), and it raises arrived. Its state is walk or idle as it goes.
| `every` (number), `follow` (string), `radius` (number), `speed` (number), `stopAt` (number)
| `water`
| Fills a mesh look with water: its top is the surface, and bodies inside float or sink by their buoyancy.
| `density` (number), `drag` (number)
|===
=== Events
[cols="1,3,2"]
|===
| Name | What it does | Parameters
| `animationDone`
| A clip that does not loop ended on self; event.state says which state it was for.
| `state` (state), `type` (type)
| `arrived`
| A seeking instance reached its target.
| `type` (type)
| `branchMissed`
| The window closed without the move and the disc went to the fail frame.
| --
| `branchOpen`
| A branch's window opened; event.move says what it wants, event.mash how many times, event.hold for how many frames.
| --
| `branchTaken`
| The move was made in time and the disc went to the success frame.
| --
| `collision`
| An instance of type a began touching one of type b (self is a, other is b).
| `a` (type), `b` (type)
| `death`
| self's health reached zero.
| `type` (type)
| `enter`
| An instance of type b entered a trigger of type a (self is the trigger, other what entered).
| `a` (type), `b` (type)
| `frame`
| Every frame.
| --
| `frameReached`
| The disc passed a frame.
| `frame` (number)
| `gameOver`
| The game ended.
| --
| `hit`
| A gun shot landed on self; other is the gun.
| `player` (number), `type` (type)
| `leave`
| An instance of type b left a trigger of type a.
| `a` (type), `b` (type)
| `midi`
| A MIDI note came in on the port the game opened; event.pitch and event.velocity say which and how hard. (pitch, since note is what a rule's comment is called.)
| `pitch` (number)
| `miss`
| A gun shot hit nothing; self is the gun. event.offscreen says whether it left the screen.
| `player` (number)
| `patrolEnd`
| A patrolling instance reached the last of its points.
| `type` (type)
| `pressed`
| A key or a switch went down.
| `key` (scancode), `player` (number), `switch` (switch)
| `railEnd`
| A rail camera reached the end of its track.
| --
| `released`
| A key or a switch came up.
| `key` (scancode), `player` (number), `switch` (switch)
| `roomEnd`
| A room is about to be left.
| `room` (room)
| `roomStart`
| A room has been entered.
| `room` (room)
| `said`
| A line was typed at the parser: its verb, noun, and second noun as the words table knows them, or unknown for a word it does not. The most specific rule wins.
| `noun` (string), `second` (string), `verb` (string)
| `soundDone`
| A clip that playSound or a sound behaviour started has ended; event.file says which, and self is the instance that played it.
| `file` (file), `type` (type)
| `spawn`
| self has just been made.
| `type` (type)
| `stopped`
| A rail camera reached a stop; event.name says which. pathNext moves it on.
| `name` (string)
| `timer`
| A timer on self went off; event.name says which.
| `name` (string), `type` (type)
| `verb`
| A verb was used on a hotspot (self), with an item or not: "use key on door". The most specific rule wins.
| `item` (string), `target` (string), `verb` (string)
|===
=== Conditions
[cols="1,3,2"]
|===
| Name | What it does | Parameters
| `below`
| An instance has fallen past a line -- a pit, or the bottom of the screen.
| `entity` (entity), `y` (number)
| `chance`
| True with a probability, 0 to 1.
| `p` (number)
| `discBetween`
| The disc is between two frames -- the window a QTE is answered in.
| `from` (number), `to` (number)
| `every`
| True once every so many seconds, for a rule that fires steadily.
| `seconds` (number), `tag` (string)
| `has`
| The inventory holds an item.
| `item` (string)
| `hitPart`
| The shot landed on a named zone of the target: head, weakspot.
| `part` (string)
| `hover`
| A player's pointer is over the named hotspot.
| `player` (number), `target` (string)
| `inState`
| An instance's state is this.
| `entity` (entity), `state` (state)
| `keyHeld`
| A key is down.
| `key` (scancode)
| `keyPressed`
| A key went down this frame.
| `key` (scancode)
| `onGround`
| A platformer has ground under it.
| `entity` (entity)
| `onScreen`
| An instance's box is at least partly on the overlay.
| `entity` (entity)
| `once`
| Only the first time this rule would run -- per instance and per room unless scope is "game".
| `scope` (string), `tag` (string)
| `pictureAt`
| The disc's picture at a point is bright, dark, or neither: a flash a game hides in its own video, a colour cue, or a lamp in the scene. A game with no disc reads black, since the engine answers zeroes when nothing is playing.
| `high` (number), `is` (string), `low` (number), `x` (number), `y` (number)
| `pointerIn`
| A player's pointer is over an instance.
| `entity` (entity), `player` (number)
| `pointerOffscreen`
| A player's pointer is off the picture, which is how a light gun reloads.
| `player` (number)
| `switchHeld`
| A pad, gun, or mouse switch is down.
| `switch` (switch)
| `test`
| An expression is true: self.health <= 0, count("zombie") == 0, score > 1000.
| `expr` (expression)
| `timeBetween`
| The game is between two moments, in seconds.
| `from` (number), `to` (number)
| `touching`
| Two instances overlap.
| `entity` (entity), `other` (entity)
| `waiting`
| A rail camera is held at a stop.
| `entity` (entity)
|===
=== Actions
[cols="1,3,2,1"]
|===
| Name | What it does | Parameters | Waits
| `addScore`
| Add to the score.
| `amount` (number)
|
| `addVar`
| Add to a var on an instance, or on the game with no entity.
| `amount` (number), `entity` (entity), `name` (string)
|
| `cameraCut`
| Draw the scene from another camera instance.
| `entity` (entity)
|
| `credit`
| A coin: one more credit.
| --
|
| `damage`
| Take from an instance's health; at zero it dies.
| `amount` (number), `entity` (entity)
|
| `destroy`
| Remove an instance.
| `entity` (entity)
|
| `die`
| A death: a message, then the game starts over.
| `text` (expression)
| yes
| `discPause`
| Hold the disc on its frame: a still behind a title.
| --
|
| `discPlay`
| Play the disc from where it stands.
| --
|
| `discTo`
| Send the disc to a frame -- the branch a QTE takes.
| `frame` (number)
|
| `emit`
| A burst of particles at an instance: an explosion, sparks, a puff.
| `b` (number), `count` (number), `entity` (entity), `g` (number), `r` (number), `speed` (number)
|
| `face`
| Turn a walker to face left or right, or toward an instance.
| `direction` (string), `entity` (entity), `target` (entity)
|
| `fade`
| Fade the picture to black, or back (with out false), and wait.
| `out` (boolean), `seconds` (number)
| yes
| `flash`
| Flash the screen a colour.
| `b` (number), `g` (number), `r` (number), `seconds` (number)
|
| `gameOver`
| End the game: rules stop, the gameOver event fires, restart begins again.
| --
|
| `give`
| Put an item in the inventory.
| `item` (string)
|
| `goTo`
| Go to a room, taking the named entity along and placing it at a point there -- or where the entity called at stands in that room.
| `at` (string), `entity` (entity), `room` (room), `x` (number), `y` (number), `z` (number)
|
| `jump`
| Ask a platformer to jump.
| `entity` (entity)
|
| `loadGame`
| Bring a slot back.
| `slot` (number)
|
| `lookAt`
| Turn an instance toward another.
| `entity` (entity), `target` (entity)
|
| `lua`
| Anything the vocabulary cannot say. The way out, and it is meant to be used.
| `code` (lua)
|
| `moveTo`
| Put an instance somewhere.
| `entity` (entity), `x` (number), `y` (number), `z` (number)
|
| `nextVerb`
| Cycle to the next verb in the game's list.
| --
|
| `pathNext`
| Send a rail camera on from its stop.
| `entity` (entity)
|
| `playAnimation`
| Play a model's clip, once or looping.
| `clip` (string), `entity` (entity), `loop` (boolean)
|
| `playMusic`
| Play a track, looping.
| `file` (file)
|
| `playSound`
| Play a clip, at a volume of its own (0 to 100) under the game's.
| `file` (file), `volume` (number)
|
| `push`
| Shove a body, or a ragdoll's bone, along a direction.
| `bone` (string), `entity` (entity), `x` (number), `y` (number), `z` (number)
|
| `reload`
| Fill a gun's ammo.
| `entity` (entity)
|
| `restart`
| Begin the game again from its first room.
| --
|
| `run`
| Drive a platformer left (-1) or right (1) this frame.
| `direction` (number), `entity` (entity)
|
| `saveGame`
| Keep the whole game -- room, positions, vars, inventory -- in a numbered slot.
| `slot` (number)
|
| `say`
| Show a line for a while, and wait for it. With a hud it goes in the element called say; otherwise it is drawn.
| `seconds` (number), `text` (expression)
| yes
| `setState`
| Change an instance's state.
| `entity` (entity), `state` (state)
|
| `setText`
| Change what a text instance says.
| `entity` (entity), `text` (expression)
|
| `setVar`
| Set a var on an instance, or on the game with no entity.
| `entity` (entity), `name` (string), `value` (expression)
|
| `setVerb`
| Choose the verb the next click uses.
| `verb` (string)
|
| `shake`
| Shake the camera for a moment.
| `amount` (number), `seconds` (number)
|
| `show`
| Show or hide an instance.
| `entity` (entity), `visible` (boolean)
|
| `spawn`
| Make an instance of a type at a point.
| `type` (type), `x` (number), `y` (number), `z` (number)
|
| `spawnAtPointer`
| Make an instance of a type where the player's pointer is: on the overlay in 2D, on the floor the ray meets in 3D.
| `player` (number), `type` (type)
|
| `stopMusic`
| Stop the track that is playing.
| --
|
| `submitScore`
| Send the score to the master service's board for this game, queued until it can go.
| `board` (string)
|
| `take`
| Take an item out of the inventory.
| `item` (string)
|
| `talk`
| Run a dialogue from the game's dialogues, and wait for it to end.
| `dialogue` (string)
| yes
| `timerStart`
| Start a named timer on an instance.
| `after` (number), `entity` (entity), `name` (string)
|
| `useItem`
| Choose an inventory item for the next click: "use key on ...".
| `item` (string)
|
| `wait`
| Wait some seconds before the next action.
| `seconds` (number)
| yes
| `walkTo`
| Send a walker to a point, and wait until it gets there.
| `entity` (entity), `x` (number), `y` (number), `z` (number)
| yes
| `walkToHotspot`
| Send a walker to a hotspot's walk point (the one the verb was used on, with self), and wait.
| `entity` (entity), `target` (entity)
| yes
| `walkToPointer`
| Send a walker where the player's pointer is, and wait.
| `entity` (entity), `player` (number)
| yes
|===

View file

@ -102,11 +102,11 @@ one changed file name.
=== This Is Not the Forge Book
Singe has a second book, _Forge_, which teaches you to make a game by
describing it instead of programming it. That is a different road to a
different place, for someone who does not want to write code at all. If you
came here to learn to program, you are in the right place. You do not need to
read the Forge book, and this one does not build on it.
Forge, a separate tool with a book of its own, makes a game by describing it
instead of programming it. That is a different road to a different place, for
someone who does not want to write code at all. If you came here to learn to
program, you are in the right place. You do not need Forge or its book, and
this one does not build on it.
The _Singe Reference_ is the reference: every function, every argument, every
return value, and the programming model behind them. It is not a tutorial and

View file

@ -765,7 +765,7 @@ The window and how the picture fills it.
| `-r`, `--rotate=DEGREES` | Turn the whole presentation clockwise by `0`, `90`, `180` or `270` degrees, for a cabinet whose panel is mounted on its side. The video, the overlay, the GUIs, the 3D scene and the particles all turn together, a quarter turn swaps the shape of the picture so a portrait window is filled, and mouse and light gun positions are turned back so a click still lands where it is drawn. Only the four right angles are accepted. A script may change it with `vldpSetRotate`.
| `--screen=N` | Open the window on display `N`, counting from `1` as Hypseus counts them. A number larger than the number of displays lists the displays that are there and exits. Default: the primary display.
| `-u`, `--stretch` | Stretch the video to fill the window instead of keeping its aspect ratio.
| `-x`, `--xresolution=VALUE` | Window width. The height is taken from the matching entry of the built in resolution table when omitted. A size larger than the display -- asked for here or by a game's `games.dat` -- is shrunk to fit it, keeping its shape, and the trace says so.
| `-x`, `--xresolution=VALUE` | Window width. The height is taken from the matching entry of the built in resolution table when omitted. A size larger than the display -- asked for here or by a game's `games.dat` -- is shrunk to fit it, keeping its shape, and the trace says so. Run headless, with `SDL_VIDEODRIVER=offscreen`, there is no display to fit, and the window keeps the size asked for.
| `-y`, `--yresolution=VALUE` | Window height.
|===
@ -1369,9 +1369,7 @@ since together they are larger than the engine.
* *Learn to Program with Singe* teaches programming from nothing, in thirty
lessons, to somebody who has never written code. It comes with the scripts
and the small art kit the lessons use.
* *Forge* is the authoring tool that makes a game from a description rather
than from code. Its book has ten tutorials and the vocabulary reference.
The download holds the three books as PDF and HTML, the lesson files, and the
licence. Forge itself is a separate download, since it is a Singe game in its
own right.
The download holds the two books as PDF and HTML, the lesson files, and the
licence. Forge, the authoring tool that makes a game from a description rather
than from code, is a separate project with its own download and its own book.

View file

@ -2430,9 +2430,7 @@ die.
A game can be written as a *description* -- a table of layers, entities and
rules -- and compiled into an ordinary Singe game by Forge, the authoring
tool. Forge is distributed on its own, beside the engine, and has its own
manual: `docs/Forge.adoc` in the source tree, built to `Forge.html` and
`Forge.pdf` beside this one.
tool. Forge is a separate project with its own download and its own book.
[[migrating]]
@ -14312,7 +14310,7 @@ end
result = singePack(directory, database)
----
Packs the game in `directory` into the single-file database `database`, exactly as `singe --pack` does from the command line, and returns `true` when it succeeded. The directory has to be a game's own folder: one that contains no `Singe/` support folder, no launcher, and nothing else the packer refuses, and the reasons for a refusal are printed to the console as they are for the command line. This is what lets an authoring tool release a game without leaving the engine; Forge's Publish presses it after an export.
Packs the game in `directory` into the single-file database `database`, exactly as `singe --pack` does from the command line, and returns `true` when it succeeded. The directory has to be a game's own folder: one that contains no `Singe/` support folder, no launcher, and nothing else the packer refuses, and the reasons for a refusal are printed to the console as they are for the command line. This is what lets an authoring tool release a game without leaving the engine.
*Parameters:*
@ -14545,7 +14543,7 @@ singeScreenshot()
singeScreenshot(name)
----
Requests a screenshot of the whole window, letterbox included, taken after the next frame is drawn and saved as a PNG in the game's data directory (see `singeGetDataPath`). Without a name, files are named `singe000.png` upward; each script run scans from zero for the first free name and later shots continue past the last one saved. With a name the shot goes to that file, `.png` added when the name has no extension, replacing any file of that name; a name with a directory in it aborts the script. Naming the shot is for scripts that produce pictures on purpose, such as the ones that illustrate the Forge tutorials, so a rerun replaces the picture rather than adding one. The call forces a redraw so the shot is taken even while the display is idle. The switch mapped to `INPUT_SCREENSHOT` in `controls.cfg` does the same thing.
Requests a screenshot of the whole window, letterbox included, taken after the next frame is drawn and saved as a PNG in the game's data directory (see `singeGetDataPath`). Without a name, files are named `singe000.png` upward; each script run scans from zero for the first free name and later shots continue past the last one saved. With a name the shot goes to that file, `.png` added when the name has no extension, replacing any file of that name; a name with a directory in it aborts the script. Naming the shot is for scripts that produce pictures on purpose, such as the ones that illustrate a book's tutorials, so a rerun replaces the picture rather than adding one. The call forces a redraw so the shot is taken even while the display is idle. The switch mapped to `INPUT_SCREENSHOT` in `controls.cfg` does the same thing.
*Since:* 1.x
*See also:* <<singegetdatapath,singeGetDataPath>>

BIN
docs/images/tutorials/01-chooser.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/01-entities.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/01-moved.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/01-playing.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/01-recoloured.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/01-rules.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/01-saved.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/01-types.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/02-cave.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/02-coinType.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/02-coins.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/02-placed.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/02-playing.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/02-rule.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/02-rules.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/03-coinSpin.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/03-heroType.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/03-jumpRule.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/03-music.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/03-playing.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/04-cleared.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/04-hitRule.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/04-placed.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/04-playing.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/04-rules.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/04-ship.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/04-types.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/05-disc.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/05-playing.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/05-prompts.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/05-rules.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/05-window.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/06-firstKey.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/06-playing.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/06-rules.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/06-tracks.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/06-types.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/07-dialogue.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/07-hall.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/07-hotspots.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/07-playing.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/07-verbRules.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/07-walkArea.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/08-hero.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/08-meshes.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/08-playing.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/08-rules.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/08-scene.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/08-viewport.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/09-hitRule.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/09-placed.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/09-playing.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/09-rail.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/09-room.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/09-rules.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/09-zombie.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/10-arcadeRules.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/10-bezel.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/10-playing.png (Stored with Git LFS)

Binary file not shown.

BIN
docs/images/tutorials/10-released.png (Stored with Git LFS)

Binary file not shown.

View file

@ -706,8 +706,9 @@ along, doing real work with the same functions you have. Working out how
somebody else's program does something is a skill of its own, and those are a
kind place to practise it.
And there is a second book, _Forge_, for describing a game rather than writing
one. It is a different road. You do not need it, but it is there.
And there is Forge, a separate tool with a book of its own, for describing a
game rather than writing one. It is a different road. You do not need it, but
it is there.
The next game is yours. Make the small one first, finish it, and put it in
front of somebody. Finishing is the part nobody tells you is the hard part, and

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more