- Learn
- Build a Game
- Crystal Cascade
- 8 Four Ways to Play
8 Four Ways to Play
Give each menu choice its own way to play. Finish a race, beat the clock, or let a full well sparkle clean and carry on.
Click the game, then press Enter to start. Try each mode from the Main Menu.
This chapter’s finished project gives you a working comparison whenever you need it.
To open it, right-click the downloaded file, choose Extract All... and then Extract. Start Myth Engine, choose File › Open Project... and pick the .mythproj file in the new folder. Nunito-OFL.txt beside it comes with the game.
8.1 The Idea
The crystals have more ways to fall into trouble than they did when the well only filled. This time, the reason for placing them changes with your choice. Marathon hurries you. Sprint asks how quickly you can finish. Ultra watches the clock, while Zen lets you stay.
Zen forgives a full well. The stack sparkles away, a fresh piece falls, and your score stays. The sky has room again.
These differences live in Modes, a Data Table with a row for each way to play. Think of its row as the rules sheet beside a game. Each part reads what it needs.
The table is called Modes, but its rows aren’t Event Modes. Event Modes still choose which response an Event runs. This table describes whole games.
The main problem is the ending. A lost game turns the stack to stone; a completed goal celebrates. Both stop play.
8.2 The Plan
- Start The Chosen Mode sets mode -> other pieces of Logic read its row of Modes.
- Count Lines updates lines -> Finish The Mode checks the goal and announces modeFinished, then gameOver.
- The timeUp timer reaches the same ending. Time The Game keeps the final clock reading.
- Zen’s Check Top Out sends freshStart -> Start Afresh empties the well and announces wellReset.
- Keep Bests saves the matching record, and the menu later reads it beside the choice that earned it. The stats and menu show that mode’s own values.
8.3 Set Up
Continue with your Chapter 7 project. Chapter 7 finished supplies the same starting point if you need a working copy. Keep its Title, menu, results, tables and choosing Logic. The Starter Kit already contains Stats Panel, Victory Jingle and Zen Tide.
Add the listed Globals once. clockStart remembers when this stretch of play began; clockHeld keeps time already counted. clockOn tells the clock whether that stretch is running. The saved records have their own Globals too.
Use the setup card as a placement list while each group is built. Most new words use Screen UI offsets in pixels. Stats Panel uses its world position in meters. Several texts share a position because only the matching mode shows them.
The Pause cue is a picture for now. The working panel comes next.
8.4 Rules Beside the Well
Each choice gets a row of its own, bringing its pace and its goal into the same table.
Make Modes Entry before Modes. Its Boolean fields become Yes/No cells in the table. Preserve the row order: the mode Global counts from one, while table rows count from zero. Each reader therefore uses mode minus one.
Marathon ends at 150 lines. Sprint ends at 40, even if its last clear crosses that goal. Ultra gets 120 seconds, a two-minute game. Zen’s fixed fallSeconds gives every row a gentler wait, with no finishing goal.
Keep a zero where the card gives one. A zero timeLimit means no timed ending; a zero fallSeconds asks gravity to use its level curve. The reader of each field gives that zero its meaning.
One row of the "Modes" Data Table. Its fields are the columns of the table. The four ways to play; row = $mode - 1 (Marathon, Sprint 40, Ultra, Zen). name: shown on the stats panel. calmTheme: plays Zen Tide instead of Crystal Current. levelRow, linesRow and clockRow: which of the stats panel's rows show (LEVEL, GOAL or RESETS; LINES or LINES LEFT; TIME or TIME LEFT). goalLines: the lines that finish the mode (0: no goal); stopsAtGoal: the lines count stops at the goal (Sprint 40). timeLimit: the seconds a game lasts (0: no limit). finishTitle: the results title when the mode is finished. levelClimbs: the level rises with the lines (Marathon, Ultra). fallSeconds: a fixed fall time for every level (Zen; 0: by level). freshStart: topping out empties the well and the game goes on (Zen). startsAtChosenLevel: the game starts at the level chosen on the Main Menu (Marathon; the others at level 1). bestKey: the Persistent Value key its best is kept under (the keys of the original), which also marks the text of the mode on the PERSONAL BEST panel.
MARATHONNoLEVELLINESTIME150No0MARATHON COMPLETE!Yes0NoYesbestMarathonSPRINT 40NoGOALLINES LEFTTIME40Yes0SPRINT COMPLETE!No0NoNobestSprintULTRANoLEVELLINESTIME LEFT0No120TIME UP!Yes0NoNobestUltraZENYesRESETSLINESTIME0No0No1.1YesNobestZen8.4.1 Count Lines
Sprint’s last clear may reach beyond its finish line. Count Lines keeps that result at the goal, so LINES LEFT can reach zero without showing a negative debt. Other modes keep counting their earned lines.
Adds the lines the well cleared to the lines count (the Project Global lines).
Any ObjectGlass Well- Variable
$lines- Value
0
- Variable
$lines- Operator
+=- Value
@self.well.wellState.linesCleared
data('Modes', global('mode') - 1, 'stopsAtGoal') == true && $lines > data('Modes', global('mode') - 1, 'goalLines')- Variable
$lines- Value
data('Modes', global('mode') - 1, 'goalLines')
- Logic Component
Count Lines- State
Disable
8.4.2 Count Level
Marathon still takes the starting level you chose. Now Count Level asks the table whether that choice belongs to this mode. The same row also decides whether clears can raise the level.
Make Count Level Data before updating the Logic. Its DATA STRUCTURES entry uses countLevelData. This is the component marked From Logic on Game Rules; attaching the updated Logic supplies it. Keep the existing scoring component alongside it.
Object Data of Logic "Count Level" (its state that lasts between Events). reachedLevel: the level the lines cleared so far reach.
Raises the level (the Project Global currentLevel) by one for every linesPerLevel lines from the level the game started at ($firstLevel), up to the last row of Levels, and says so.
Any Objectself.countLevelData$mode == 1data('Modes', global('mode') - 1, 'startsAtChosenLevel') == true- Variable
$firstLevel- Value
$startLevel
- Variable
$firstLevel- Value
1
- Variable
$currentLevel- Value
$firstLevel
- Name
reachedLevel- Value
0
- Variable
reachedLevel- Value
global('firstLevel') + floor(global('lines') / @self.linesPerLevel)
reachedLevel > @self.lastLevel- Variable
reachedLevel- Value
@self.lastLevel
reachedLevel > global('currentLevel')- Variable
$currentLevel- Value
reachedLevel
- Custom Event
levelUp
data('Modes', global('mode') - 1, 'levelClimbs') == true- Component Value
self.countLevelData.reachedLevel- Value
global('firstLevel') + floor(global('lines') / @self.linesPerLevel)
self.countLevelData.reachedLevel > @self.lastLevel- Component Value
self.countLevelData.reachedLevel- Value
@self.lastLevel
self.countLevelData.reachedLevel > global('currentLevel')- Variable
$currentLevel- Value
self.countLevelData.reachedLevel
- Custom Event
levelUp
- Logic Component
Count Level- State
Disable
8.4.3 Piece Gravity
Zen keeps the same falling pace as its score grows. The fixed fallSeconds takes priority. Its other Modes still handle falling, resting and the lock delay.
The new gameOver listener also stops this Logic at the ending. A finish can happen while a piece is airborne; it must leave that piece still.
Moves the falling piece down one row whenever the gravity timer comes due; when it cannot fall it rests, and locks after the lock delay.
Any Objectself.piece- Name
levelsUp- Value
global('currentLevel') - 1
- Component Value
self.piece.fallSeconds- Value
pow(@self.curveStartSeconds - levelsUp * @self.curveStepSeconds, levelsUp)
data('Modes', global('mode') - 1, 'fallSeconds') > 0- Component Value
self.piece.fallSeconds- Value
data('Modes', global('mode') - 1, 'fallSeconds')
- Name
levelsUp- Value
global('currentLevel') - 1
- Component Value
self.piece.fallSeconds- Value
pow(@self.curveStartSeconds - levelsUp * @self.curveStepSeconds, levelsUp)
- Timer Name
"gravity"- Repeat Every (s)
self.piece.fallSeconds
- Event
On the Gravity Timer- Mode
Falling
- Component Value
self.piece.lowestRow- Value
self.piece.row
- Component Value
self.piece.lockResets- Value
0
- Component Value
self.piece.hasTouchedDown- Value
false
- Component Value
self.piece.isLockDelayOver- Value
false
- Action Name
roomBelowOnGravity- piece
self- columnShift
0- rowShift
-1
roomBelowOnGravity.value == true- Component Value
self.piece.row- Value
self.piece.row - 1
- Component Value
self.piece.lastMoveWasRotation- Value
false
- piece
self
- Custom Event
pieceMoved
- Custom Event
pieceFell
- Action Name
roomBelowAfterFall- piece
self- columnShift
0- rowShift
-1
roomBelowAfterFall.value == false- Custom Event
pieceLanded
- Custom Event
pieceLanded
- Event
On the Gravity Timer- Mode
Resting
self.piece.hasTouchedDown == false- Component Value
self.piece.hasTouchedDown- Value
true
- Timer Name
"lockDelay"- Repeat Every (s)
self.piece.lockDelaySeconds- Ring Once
Yes
- Custom Event
lockDelayStarted
self.piece.isLockDelayOver == true- Custom Event
lockPiece
- Component Value
self.piece.isLockDelayOver- Value
true
- Action Name
roomBelowAfterDelay- piece
self- columnShift
0- rowShift
-1
roomBelowAfterDelay.value == true- Event
On the Gravity Timer- Mode
Falling
- Custom Event
lockPiece
- piece
self
self.piece.hasTouchedDown == trueself.piece.lockResets < @self.lockResetLimit- Component Value
self.piece.lockResets- Value
self.piece.lockResets + 1
- Component Value
self.piece.isLockDelayOver- Value
false
- Timer Name
"lockDelay"- Repeat Every (s)
self.piece.lockDelaySeconds- Ring Once
Yes
- Custom Event
lockDelayStarted
- Action Name
roomBelowAfterShift- piece
self- columnShift
0- rowShift
-1
roomBelowAfterShift.value == true- Event
On the Gravity Timer- Mode
Falling
- Custom Event
pieceLanded
- Logic Component
Piece Gravity- State
Disable
The well now reads its rules. The next parts will use the same row to decide how long play lasts and what ends it.
8.5 A Clock That Can Keep Its Place
Time needs somewhere to wait after play stops.
Remove startSeconds from Scoring State as marked. The new clock Globals replace that starting reading, while playedSeconds still belongs to the finished record.
What Game Rules scored: isBackToBackReady, the last line clear was difficult (4 lines or a twist); isBackToBack, the last lock was a difficult clear right after another; lastClearRow, the Clear Names row of the last lock; combo, the clears in a row less one (-1: none yet; 1 and up score); bestCombo, the longest of the game. isNewBest: the game that just ended beat the best kept on this device. cascades: four-line clears this game; twists: locks that were twists; playedSeconds: how long the game was played.
8.5.1 Time Played
A stopwatch can resume from a reading it kept earlier. Time Played adds both stretches. With the clock off, the held reading becomes its entire answer.
Its Return Type is Number. Its local seconds exists only during that call. The same answer can feed a record or a comparison without starting another clock.
Answers the time played so far in this game, in seconds: while the clock runs ($clockOn 1), the play clock since $clockStart plus the time kept before ($clockHeld); otherwise the time kept.
- Name
seconds- Value
0
$clockOn == 1- Variable
seconds- Value
global('playTime') - global('clockStart') + global('clockHeld')
- Variable
seconds- Value
$clockHeld
- Value
seconds
8.5.2 Time The Game
The live clock begins with Gameplay in this chapter. Time The Game resets its beginning, turns it on, and clears the previous held time. At gameOver it keeps the answer from Time Played, then switches the live clock off.
Ultra also caps the recorded duration at its limit. A timer heard on the next frame mustn’t make the results report extra playing time. This change gives the results and live display the same stopping point.
Times the game with the engine's play clock ($playTime, which stops while the game is paused): the clock the stats panel shows ($clockStart, $clockOn, $clockHeld) and the time played kept for the results.
Any Objectself.scoring- Component Value
self.scoring.startSeconds- Value
global('playTime')
- Variable
$clockStart- Value
$playTime
- Variable
$clockOn- Value
1
- Variable
$clockHeld- Value
0
- Action Name
playedAtTheEnd
- Component Value
self.scoring.playedSeconds- Value
playedAtTheEnd.value- Value
global('playTime') - self.scoring.startSeconds
data('Modes', global('mode') - 1, 'timeLimit') > 0 && self.scoring.playedSeconds > data('Modes', global('mode') - 1, 'timeLimit')- Component Value
self.scoring.playedSeconds- Value
data('Modes', global('mode') - 1, 'timeLimit')
- Variable
$clockHeld- Value
self.scoring.playedSeconds
- Variable
$clockOn- Value
0
The clock has one answer. The next pieces decide when that answer becomes the finished record, rather than a clock still running.
8.6 Reaching the Finish
A completed game leaves its crystals bright.
8.6.1 Finish The Mode
A clear reaches the line goal, or the timeUp timer rings. Both routes arrive at goalReached. Finish The Mode settles an earned clear before celebrating, so its last reward reaches the score before bests are saved.
That settling message goes to Glass Well. Set the well declaration’s Object Kind and later Object value from the card. The celebration places its shower over the well and its fountain beneath the floor.
The two ending messages have different jobs. modeFinished tells the title and Sprint record that a goal was completed. gameOver then stops play and opens the familiar results panel. Make both messages before choosing them on listeners.
Finishes the mode at its goal lines or when its time limit is up (Modes): Game Over, Victory Jingle, Crystal Fountain and Sparkle Shower, modeFinished and gameOver.
Any ObjectGlass Welldata('Modes', global('mode') - 1, 'timeLimit') > 0- Timer Name
"timeUp"- Repeat Every (s)
data('Modes', global('mode') - 1, 'timeLimit')- Ring Once
Yes
data('Modes', global('mode') - 1, 'goalLines') > 0 && $lines >= data('Modes', global('mode') - 1, 'goalLines')- Custom Event
goalReached
- Custom Event
goalReached
- Applies to
@self.well- Custom Event
settleClear
- Game State
Game Over
- Sound
Victory Jingle
- Action Name
finishMiddle- column
(data('Well Layout', 0, 'columns') - 1) / 2- row
(data('Well Layout', 0, 'visibleRows') - 1) / 2
- Particles
Sparkle Shower- Position Relative To
World- X
finishMiddle.x- Y
finishMiddle.y- Count
@self.sparkleCount- Z Index Offset
7
- Action Name
finishFloor- column
(data('Well Layout', 0, 'columns') - 1) / 2- row
@self.fountainRow
- Particles
Crystal Fountain- Position Relative To
World- X
finishFloor.x- Y
finishFloor.y- Count
@self.fountainCount- Z Index Offset
7
- Custom Event
modeFinished
- Custom Event
gameOver
- Logic Component
Finish The Mode- State
Disable
8.6.2 Check Top Out
The rim still means a loss. Check Top Out now adds gameLost before gameOver on that route. The stone wave now has its own message, separate from the one that stops every finished game.
Zen chooses a different response to toppedOut: Start Afresh. Make both Modes before the Scene Start Action chooses between them. End The Game is the Starting Mode; the table selects the other response when needed.
Ends the game when the player tops out: a new piece with no room where it appears, or a piece that locked wholly in the hidden rows without clearing a line (a lock out).
Any ObjectFalling PieceGlass Well- Game State
Playing
data('Modes', global('mode') - 1, 'freshStart') == true- Event
On Topped Out- Mode
Start Afresh
- Custom Event
toppedOut
@self.well.wellState.linesCleared == 0 && @self.piece.piece.crystalsInView == 0- Custom Event
toppedOut
- Game State
Game Over
- Sound
Game Over Jingle
- Intensity
@self.shakeStrength- Seconds
@self.shakeSeconds
- Custom Event
gameLost
- Custom Event
gameOver
- Logic Component
Check Top Out- State
Disable
- Custom Event
freshStart
8.6.3 Show Well Cell
The stack should turn gray only when the well is lost. Change its message to gameLost. A goal finish will leave the colored crystals where they were.
One cell of the well: it finds its cell from where it is placed, then shows the crystal stored there whenever the well changes; a crystal a lock just put there pops, and a crystal in a full line bursts into shards.
Any Objectself.showWellCellData- Action Name
myCell- x
self.transform.position.x- y
self.transform.position.y
- Action Name
thisCellIndex- column
myCell.column- row
myCell.row
- Component Value
self.showWellCellData.cellIndex- Value
thisCellIndex.value
- Custom Event
wellChanged
self.showWellCellData.shownKind == 0 && data('Well', self.showWellCellData.cellIndex, 'kind') != 0- Horizontal Scale
@self.popScale- Vertical Scale
@self.popScale
- Value
self.transform.scale- Target
1- Duration
@self.popSeconds- Ease
Quad Out
- Particles
Lock Glint- Z Index Offset
6
- Custom Event
wellChanged
data('Well', self.showWellCellData.cellIndex, 'isClearing') == true && self.showWellCellData.isShowingShatter == false- Particles
Crystal Shards- Z Index Offset
6- Tint
data('Piece Kinds', data('Well', self.showWellCellData.cellIndex, 'kind') - 1, 'color')
- Component Value
self.showWellCellData.isShowingShatter- Value
data('Well', self.showWellCellData.cellIndex, 'isClearing')
- Component Value
self.showWellCellData.shownKind- Value
data('Well', self.showWellCellData.cellIndex, 'kind')
self.showWellCellData.isShowingShatter == true- Frame
data('Crystal Frames', 0, 'shatter')
- Frame
self.showWellCellData.shownKind
- Name
waitSeconds- Value
0
- Name
rowFromBottom- Value
0
- Variable
rowFromBottom- Value
floor(self.showWellCellData.cellIndex / data('Well Layout', 0, 'columns'))
- Variable
waitSeconds- Value
@self.stoneDelaySeconds + rowFromBottom * @self.stoneRowSeconds
- Timer Name
"turnToStone"- Repeat Every (s)
waitSeconds- Ring Once
Yes
self.showWellCellData.shownKind != 0- Frame
data('Crystal Frames', 0, 'stone')
8.6.4 Show Result Title
The panel can now name the ending it received. Make Result Title Text now. Show Result Title replaces that text only when modeFinished arrives.
On the existing Results Title Object, add one plain component using Result Title Text. Name it shown before attaching the new Logic, then change the Text template from its card. Show With Results still controls when the title appears.
What the results title shows. resultTitle: GAME OVER, or the finishing title of the mode (its template {resultTitle} reads it).
The results title: GAME OVER, or the finishing title of the mode (Modes finishTitle) when the mode was finished, in the Result Title Text field resultTitle that the template of the text shows.
Any Objectself.shown- Component Value
self.shown.resultTitle- Value
data('Modes', global('mode') - 1, 'finishTitle')
Window / UITop CenterUIX 128, Y 48{resultTitle}GAME OVERNunito69Bold1.5CenterWrap to Width900Result Title Text8708.6.5 Open Results
The ending still needs its quiet delay before the player chooses what should happen to the well next. Add the immediate Play deactivation on gameOver. Movement must stop while the celebration waits, before Menus takes over for the results.
Opens the results panel resultsDelaySeconds after the game ends (resultsShown).
Any ObjectObjects with Panel State- Input Context
Play
- Input Context
Menus- Active
No
- Timer Name
"resultsDelay"- Repeat Every (s)
@self.resultsDelaySeconds- Ring Once
Yes
- Input Context
Play- Active
No
- Input Context
Menus
- Input Context
Play- Active
No
- Applies to
@self.resultsPanel- Custom Event
panelOpened
- Custom Event
resultsShown
- Sound
Menu Back
A finish and a loss share the panel. The two different messages let the pictures recognize which ending brought the player to that same panel.
8.7 Room to Begin Again
Zen lets the full well breathe.
8.7.1 Start Afresh
The reset clears the stack and tells its pictures to redraw. It leaves the score, queue and game clock running. Start Afresh also counts a reset and showers the newly empty space with light.
Empty The Well is the Function you already built. Keep that resource; its work is useful again here. wellChanged refreshes the cells, while wellReset tells a waiting piece that room has returned.
Zen's fresh start: empties the well, counts a reset ($resets), plays All Clear Fanfare and bursts sparkleCount Sparkle Shower over the well.
Any Object- Variable
$resets- Value
0
- Custom Event
wellChanged
- Custom Event
wellReset
- Variable
$resets- Operator
+=- Value
1
- Sound
All Clear Fanfare
- Action Name
freshMiddle- column
(data('Well Layout', 0, 'columns') - 1) / 2- row
(data('Well Layout', 0, 'visibleRows') - 1) / 2
- Particles
Sparkle Shower- Position Relative To
World- X
freshMiddle.x- Y
freshMiddle.y- Count
@self.sparkleCount2- Z Index Offset
7
8.7.2 Spawn Piece
A new piece may be the one that discovers the well is full. Piece remembers that in isBlocked. The reset can then invite that same piece to try again.
Add the field before building its expressions. On wellReset, Spawn Piece retries only a blocked arrival and clears the flag. A piece already locking doesn’t need another invitation.
The falling piece: its kind (1-7, Piece Kinds), rotation (0 up, 1 right, 2 down, 3 left), the well cell of its 4 x 4 box's bottom-left corner (column from the left, row from the bottom), how long it rests before it locks (lockDelaySeconds, read by the piece and its crystals), how long it takes to fall one row (fallSeconds), and which way the player is moving it (moveDirection: -1 left, 1 right, 0 still). landingRow: the lowest row its box can fall to (Find Landing Row). hardDropRows: the rows its last hard drop crossed. lowestRow: the lowest row it has reached; lockResets: the moves or rotations that restarted its lock delay since touching down there; hasTouchedDown: whether it has touched down since reaching that lowest row; isLockDelayOver: whether its lock delay ran out before it landed again. heldKind: the piece put away with Hold (0 none); hasHeld: Hold was used for this piece. lastMoveWasRotation: its last successful action was a rotation; lastKickTest: the Kicks test (0-4) that rotation used; twist: what it locked as (0 none, 1 mini, 2 full twist). crystalsInView: how many of its crystals locked inside the visible well (0: a lock out). isBlocked: the new piece found no room where it appears (it waits for a fresh start in Zen).
Puts a new piece at the top of the well: the next kind from the Queue (or the held piece, from Hold Piece), rotation up, its box in the hidden rows, then one row down into view if it fits.
Any Objectself.piece- Component Value
self.piece.rotation- Value
0
- Component Value
self.piece.lastMoveWasRotation- Value
false
- Component Value
self.piece.lastKickTest- Value
0
- Component Value
self.piece.column- Value
@self.startColumn
- Component Value
self.piece.row- Value
data('Well Layout', 0, 'rows') - 4
- Action Name
roomForNewPiece- piece
self- columnShift
0- rowShift
0
roomForNewPiece.value == true- Logic Component
Hold Piece
- Logic Component
Piece Gravity
- Logic Component
Move Piece
- Logic Component
Rotate Piece
- Logic Component
Hard Drop Piece
- Action Name
roomBelowNewPiece- piece
self- columnShift
0- rowShift
-1
roomBelowNewPiece.value == true- Component Value
self.piece.row- Value
self.piece.row - 1
- piece
self
- Custom Event
pieceMoved
- Component Value
self.piece.isBlocked- Value
true
- Custom Event
pieceBlocked
- Action Name
isPlaying- Game State
Playing
isPlaying.value == true- Component Value
self.piece.kind- Value
data('Queue', 0, 'piece')
- Custom Event
pieceDealt
- Custom Event
spawnPiece
self.piece.isBlocked == true- Component Value
self.piece.isBlocked- Value
false
- Custom Event
spawnPiece
8.7.3 Show Fresh Start
The empty space gets a brief name. Show Fresh Start hands FRESH START to the same Fade Out Message used by earlier announcements. Make its text Object and use the placement’s longer display settings from Set up.
Shows "FRESH START" when Zen starts afresh (Fade Out Message on the same text hides it again).
Any Object- Custom Event
messageShown
Window / UITop CenterUIX 128, Y 48FRESH STARTNunito39Bold1.5CenterWrap to Width4358.7.4 Game Music
Zen Tide accompanies this quieter well. Crystal Current keeps the other games moving. The choice now belongs to the same rules row that gives Zen its gentler fall.
Plays the mode's music while a game is played: Zen Tide in Zen (Modes calmTheme), the Game Theme otherwise.
Any Object- Operation
Crossfade Music- Sound
Crystal Current (Game Theme)- Crossfade Seconds
@self.musicFadeSeconds
data('Modes', global('mode') - 1, 'calmTheme') == true- Operation
Crossfade Music- Sound
Zen Tide (Zen Theme)- Crossfade Seconds
@self.musicFadeSeconds
- Operation
Crossfade Music- Sound
Crystal Current (Game Theme)- Crossfade Seconds
@self.musicFadeSeconds
data('Modes', global('mode') - 1, 'calmTheme') == true- Operation
Stop- Sound
Zen Tide (Zen Theme)
- Operation
Stop- Sound
Crystal Current (Game Theme)
The well keeps playing.
8.8 A Record for Each Choice
Sprint remembers a time; the other modes remember a score.
8.8.1 Read The Bests
An empty record starts at zero. Read The Bests loads each mode’s own saved key into its matching Global. The menu needs those values before a game begins, so this reading becomes a shared Function.
Reads the four bests kept on this device (0 when none yet) into $bestMarathonScore, $bestSprintTime (seconds), $bestUltraScore and $bestZenScore.
- Action Name
bestMarathonRead- Key
"bestMarathon"- Default
0
- Variable
$bestMarathonScore- Value
bestMarathonRead.value
- Action Name
bestSprintRead- Key
"bestSprint"- Default
0
- Variable
$bestSprintTime- Value
bestSprintRead.value
- Action Name
bestUltraRead- Key
"bestUltra"- Default
0
- Variable
$bestUltraScore- Value
bestUltraRead.value
- Action Name
bestZenRead- Key
"bestZen"- Default
0
- Variable
$bestZenScore- Value
bestZenRead.value
8.8.2 Keep Bests
A higher score beats the saved score. A faster completed Sprint beats its saved time. A lost Sprint doesn’t earn a time, even if it ends quickly.
bestKey chooses its listeners. Its empty No Score Best and No Time Best Modes are the starting responses. Make those before the Create Event’s Switch Mode Actions.
Sprint compares on modeFinished, while Time Played can still answer the running clock. The other records compare on gameOver. isNewBest keeps the result of that comparison for the notice you’ve already built.
Zen still has no way to leave during play here. Its save-and-leave route arrives with the next chapter: Pause, then MAIN MENU. That chapter moves its saving listener to gameLeft. For now, test saved records in Marathon, Sprint or Ultra; a Zen reset keeps playing without saving.
Each mode's best: reads the four kept bests when a game begins (Read The Bests) and keeps a new best of the mode played: the highest score (Marathon, Ultra, Zen, when the game ends) or the fastest finished time (Sprint 40).
Any Objectself.scoring- Action Name
savedBest- Key
"bestMarathon"- Default
0
- Variable
$bestMarathonScore- Value
savedBest.value
data('Modes', global('mode') - 1, 'bestKey') == "bestMarathon"- Event
On Game Over- Mode
Keep A Marathon Best
data('Modes', global('mode') - 1, 'bestKey') == "bestSprint"- Event
On Mode Finished- Mode
Keep A Sprint Best
data('Modes', global('mode') - 1, 'bestKey') == "bestUltra"- Event
On Game Over- Mode
Keep An Ultra Best
data('Modes', global('mode') - 1, 'bestKey') == "bestZen"- Event
On Game Over- Mode
Keep A Zen Best
global('score') > global('bestMarathonScore')$score > $bestMarathonScore- Variable
$bestMarathonScore- Value
$score- Value
global('score')
- Action Name
savedBestWrite- Action Name
bestMarathonWrite- Key
"bestMarathon"- Value
global('bestMarathonScore')
- Component Value
self.scoring.isNewBest- Value
true
$score > $bestUltraScore- Variable
$bestUltraScore- Value
$score
- Action Name
bestUltraWrite- Key
"bestUltra"- Value
global('bestUltraScore')
- Component Value
self.scoring.isNewBest- Value
true
$score > $bestZenScore- Variable
$bestZenScore- Value
$score
- Action Name
bestZenWrite- Key
"bestZen"- Value
global('bestZenScore')
- Component Value
self.scoring.isNewBest- Value
true
- Action Name
sprintTime
$bestSprintTime == 0 || sprintTime.value < $bestSprintTime- Variable
$bestSprintTime- Value
sprintTime.value
- Action Name
bestSprintWrite- Key
"bestSprint"- Value
global('bestSprintTime')
- Component Value
self.scoring.isNewBest- Value
true
8.8.4 Count Pieces
The stats will also remember how many pieces you placed. Count Pieces listens for piecePlaced and stops at gameOver. Lock Piece sends that message to Game Rules before announcing the lock to the rest of the game.
Add rules to Lock Piece with Object Kind Game Rules. Attach the updated Logic, then choose Game Rules for rules in Falling Piece’s Shared Variables section. Keep its existing piece data and locking work.
Counts the pieces locked in this game ($piecesPlaced), shown on the stats panel.
Any Object- Variable
$piecesPlaced- Value
0
- Variable
$piecesPlaced- Operator
+=- Value
1
- Logic Component
Count Pieces- State
Disable
Writes the falling piece's four crystals into the Well, takes the piece out of play, tells the well it locked and the Well Cells that crystals joined.
Any Objectself.pieceGame Rules- Name
lockedKind- Value
0
- Timer Name
"gravity"
- Timer Name
"lockDelay"
- Variable
lockedKind- Value
self.piece.kind
- Component Value
self.piece.crystalsInView- Value
0
4times- Action Name
lockedCrystal- kind
self.piece.kind- rotation
self.piece.rotation- crystal
local.index + 1
- Action Name
lockedCell- column
self.piece.column + lockedCrystal.column- row
self.piece.row + lockedCrystal.row
- Name
cellIndex- Value
lockedCell.value
- Table
Well- Row
cellIndex- Column
"kind"- Value
lockedKind
(self.piece.row + lockedCrystal.row) < data('Well Layout', 0, 'visibleRows')- Component Value
self.piece.crystalsInView- Value
self.piece.crystalsInView + 1
- Sound
Lock Click
- Logic Component
Piece Gravity- State
Disable
- Logic Component
Move Piece- State
Disable
- Logic Component
Rotate Piece- State
Disable
- Logic Component
Hard Drop Piece- State
Disable
- Logic Component
Hold Piece- State
Disable
- Component Value
self.piece.hasHeld- Value
false
- Custom Event
holdChanged
- Applies to
@self.rules- Custom Event
piecePlaced
- Custom Event
pieceLocked
- Custom Event
crystalsLocked
Attach the new Logics to Game Rules in the shown order. Count Lines must finish counting before Finish The Mode checks its goal. Its Scoring State component stays named scoring. Count Level Data comes From Logic, so keep that single supplied component.
Scoring StateCount LevelCount Level DataGlass WellFalling PieceResults PanelThe record has owners again. Now give each value a place to be seen.
8.9 The Stats Follow the Mode
The left panel changes.
8.9.1 Show In Mode Row
Sprint needs GOAL where Marathon shows LEVEL. Show In Mode Row compares a column of the current Modes row with the text Object’s row value. Only the matching label and value appear.
These Objects can share positions because their visibility differs. Compare column and row on each card before copying the familiar text styling. Those two values decide which game sees that text.
A text of the stats panel shown only when the mode's row of Modes, in column, names row (for example levelRow = GOAL).
Any Objectdata('Modes', global('mode') - 1, @self.column) == @self.row- Visible
Yes
- Visible
No
-20Stats PanelX 11.7188, Y 10.7813Attach that Logic to the existing LEVEL and LINES pairs as their Changes cards show. Their templates still read the same Globals. The new visibility decides when those familiar values belong on screen.
Window / UICenterUIX 128, Y 48LEVELNunito28.5Bold#BFEAFF1.5Wrap to Width210Window / UICenterUIX 128, Y 48{$currentLevel}Nunito39Bold1.5RightWrap to Width240Window / UICenterUIX 128, Y 48LINESNunito28.5Bold#BFEAFF1.5Wrap to Width210linesRowLINESWindow / UICenterUIX 128, Y 48{$lines}Nunito39Bold1.5RightWrap to Width240linesRowLINES8.9.2 Show Mode Name
The heading names the game you chose. Make Mode Name Text, then add it as one plain component to Mode Name. Name the component shown before attaching Show Mode Name and entering its expression.
What the Mode Name text shows. modeName: the name of the mode being played (its template {modeName} reads it).
Shows the name of the mode being played (Modes, row $mode - 1): writes it into the Mode Name Text field modeName, which the template of the text ({modeName}) reads.
Any Objectself.shown- Component Value
self.shown.modeName- Value
data('Modes', global('mode') - 1, 'name')
Window / UICenterUIX 128, Y 48{modeName}Nunito39Bold1.5CenterWrap to Width345Mode Name Text8.9.3 Show Mode Goal
Sprint puts its finish line beside the well. GOAL Value needs Goal Text as a plain component, named shown. Show Mode Goal fills it once; Show In Mode Row decides whether it belongs in this game.
What the GOAL value shows. goalLines: the lines that finish the mode (its template {goalLines} LINES reads it).
Shows the lines that finish the mode (Modes goalLines) in the Goal Text field goalLines, which its template reads ({goalLines} LINES).
Any Objectself.shown- Component Value
self.shown.goalLines- Value
data('Modes', global('mode') - 1, 'goalLines')
Window / UICenterUIX 128, Y 48GOALNunito28.5Bold#BFEAFF1.5Wrap to Width210GOALWindow / UICenterUIX 128, Y 48{goalLines} LINESNunito39Bold1.5RightWrap to Width240Goal TextGOAL8.9.4 Show Lines Left
The distance to Sprint’s finish shrinks with every clear. Show Lines Left starts at the goal, then subtracts the updated lines count. Its lower bound keeps the display at zero.
Give LINES LEFT Value one plain Lines Left Text component named shown before attaching this Logic. The label needs no component for a changing value. Their visibility Logic decides whether this label and its value should occupy those positions in the current game.
What the LINES LEFT value shows. linesLeft: the lines still to clear to the goal of the mode, never below 0 (its template {linesLeft} reads it).
Shows how many lines are left to the goal of the mode (Modes goalLines - $lines, not below 0) in the Lines Left Text field linesLeft, which its template reads ({linesLeft}).
Any Objectself.shown- Component Value
self.shown.linesLeft- Value
data('Modes', global('mode') - 1, 'goalLines')
- Component Value
self.shown.linesLeft- Value
data('Modes', global('mode') - 1, 'goalLines') - global('lines')
self.shown.linesLeft < 0- Component Value
self.shown.linesLeft- Value
0
Window / UICenterUIX 128, Y 48LINES LEFTNunito28.5Bold#BFEAFF1.5Wrap to Width210linesRowLINES LEFTWindow / UICenterUIX 128, Y 48{linesLeft}Nunito39Bold1.5RightWrap to Width240Lines Left TextlinesRowLINES LEFT8.9.5 Show Time Limit
Ultra counts toward the ending from the other direction. Time Limit Text holds the beginning. The TIME LEFT Value template subtracts the running clock from that beginning.
Add that plain component and name it shown before attaching the Logic. TIME Value instead reads the clock Globals directly. The templates split their clocks at the colon. Minutes go before it. After it, the remainder gives the seconds, and the formatting suffix keeps the clock’s digits lined up. Keep every brace and punctuation mark together when copying the expression into its Text field.
What the TIME LEFT value counts down from. timeLimit: the seconds the mode lasts (its template takes the time played from it).
Holds the time limit of the mode (Modes timeLimit) in the Time Limit Text field timeLimit, for the TIME LEFT template, which takes the time played from it.
Any Objectself.shown- Component Value
self.shown.timeLimit- Value
data('Modes', global('mode') - 1, 'timeLimit')
Window / UICenterUIX 128, Y 48TIMENunito28.5Bold#BFEAFF1.5Wrap to Width210clockRowTIMEWindow / UICenterUIX 128, Y 48{((($playTime - $clockStart) * $clockOn + $clockHeld) - (($playTime - $clockStart) * $clockOn + $clockHeld) % 60) / 60}:{(($playTime - $clockStart) * $clockOn + $clockHeld) % 60:02.2}Nunito39Bold1.5RightWrap to Width240clockRowTIMEWindow / UICenterUIX 128, Y 48TIME LEFTNunito28.5Bold#BFEAFF1.5Wrap to Width210clockRowTIME LEFTWindow / UICenterUIX 128, Y 48{((timeLimit - (($playTime - $clockStart) * $clockOn + $clockHeld)) - (timeLimit - (($playTime - $clockStart) * $clockOn + $clockHeld)) % 60) / 60}:{(timeLimit - (($playTime - $clockStart) * $clockOn + $clockHeld)) % 60:02.2}Nunito36Bold1.5RightWrap to Width240Time Limit TextclockRowTIME LEFTMake RESETS and PIECES from their cards. These values already have Globals to read. RESETS takes the place of LEVEL in Zen; PIECES remains at the bottom.
Window / UICenterUIX 128, Y 48RESETSNunito28.5Bold#BFEAFF1.5Wrap to Width210RESETSWindow / UICenterUIX 128, Y 48{$resets}Nunito39Bold1.5RightWrap to Width240RESETSWindow / UIBottom CenterUIX 128, Y 48PIECESNunito28.5Bold#BFEAFF1.5Wrap to Width210Window / UIBottom CenterUIX 128, Y 48{$piecesPlaced}Nunito39Bold1.5RightWrap to Width240Place the stats group using Gameplay’s setup list. Check each pair’s anchor as well as its offset. A clock and a remaining-time clock can now occupy the same space without both appearing.
8.11 Play It!
- Choose each mode. Watch the name and left-hand labels follow the choice; Sprint shows GOAL and LINES LEFT, Ultra shows TIME LEFT, and Zen shows RESETS.
- Return to Marathon with a later starting level. Its opening level follows your choice; Sprint and Zen start at their own steady beginning.
- Play Ultra until its clock reaches zero. TIME UP! opens the results, TIME LEFT stops, and the last piece stays still. Its result records the full time played.
- Fill Zen’s well with hard drops. Watch the stack sparkle clean, RESETS rise, and another piece fall. The score and clock keep going.
- Drop and lock several pieces. PIECES counts the ones placed, while swapping Hold alone doesn’t count a lock.
- Finish Sprint’s goal or Marathon’s goal. The results use their completion titles; a top out instead keeps GAME OVER and its stone wave.
- After an ended game, return to the menu and compare PERSONAL BEST with the record it should have kept. Sprint starts with NO TIME YET until a completed race. Zen’s save-and-leave check waits for the next chapter’s pause menu.
- Start another game. Its counters begin fresh, while the saved records remain on this device.
8.12 How It Works
The menu chooses a row. Gravity reads its pace. The clock reads its limit, and each text reads whether it belongs. A shared table keeps those decisions together.
The ending shares its work through messages that let the same results panel show either outcome. A completed goal announces modeFinished; a loss announces gameLost. Both reach gameOver, where the familiar record and results work can stop together.
Zen takes another route. A fresh well can still belong to the same game.
8.13 If It Didn’t Work
Every choice plays like Marathon? Check mode in Start The Chosen Mode, then compare the Modes row order. Each lookup subtracts one from mode. Restore that match, choose Sprint again, and watch its labels.
Both clocks appear together? Compare Show In Mode Row’s column and row on each text Object. The same screen position is intentional. Restore the different visibility values from their cards, then enter Ultra and watch the remaining time take that place.
Zen sparkles clean but no piece returns? Look at Spawn Piece. A blocked arrival needs that invitation after the table empties. Restore the flag and the wellReset listener, then fill the well again and watch the waiting piece arrive.
A completed game turns to stone? The cells must hear gameLost. Keep gameOver on the Logics that stop the controls and counters when either kind of ending arrives. Restore the cell’s message.
Sprint saves a time after a loss? Check Keep Bests chooses No Score Best for gameOver and saves its time on modeFinished. Restore those separate Events and start another race. A completed run can now earn a record that a shorter, lost game will leave alone.
Red self.shown expressions on a new text? Add the named component first. Compare its resource with that text’s card and keep one copy. The value now has somewhere to arrive.
The last clear misses its reward? Finish The Mode sends settleClear to Glass Well before modeFinished and gameOver. Check that well names Glass Well on Game Rules, then compare the order of the finishing Actions. Restore the connection before testing another finish.
Still stuck? Search the manual, or ask in the Myth Engine Discord (opens a new tab).
8.14 Challenge
What kind of game would you want to stay in? Try another Zen pace, your own goal, or words that describe a mode in your voice. Change its Modes row and the matching menu description together. Keep a working copy so you can compare how each choice feels.
Each choice has its own reason to play. Next, the well gives you a breath before it begins.
