diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 31217b60..6214682b 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -53,6 +53,7 @@ jobs:
python3 tests/test_small_ram.py build/NumPlay.nwa
make -C games/numblocks host test
make -C games/championisland host test
+ make -C games/undertale host test
- uses: actions/upload-artifact@v4
with:
diff --git a/Makefile b/Makefile
index d590bb42..c59933fd 100644
--- a/Makefile
+++ b/Makefile
@@ -24,7 +24,7 @@ RAM_LIMIT = 148928
# custom builds of Epsilon 25.2's source give apps less: 133652 bytes; every game fits there too, in NumPlay and on
# its own (their big buffers are on the stack, 32 KB apart from that RAM, or in the RAM the software has left)
CUSTOM_RAM = 133652
-BIG_APPS = Celeste HollowKnight ChampionIsland FinalEarth2 NumPowder NumBlocks NumDrive
+BIG_APPS = Celeste HollowKnight Undertale ChampionIsland FinalEarth2 NumPowder NumBlocks NumDrive
# the calculator's space for installed apps (0x90200000 to 0x903F0000 in its flash)
APP_SPACE = 2031616
# Upsilon, the N0110 and N0115's custom software, gives .nwa apps less RAM: NumPlay-Upsilon.nwa has
@@ -205,6 +205,7 @@ APP_numvisuals = games/numvisuals/output/numvisuals.nwa:NumVisuals.nwa
APP_celeste = games/celeste/output/celeste.nwa:Celeste.nwa
APP_championisland = games/championisland/output/championisland.nwa:ChampionIsland.nwa
APP_hollowknight = games/hollowknight/output/hollowknight.nwa:HollowKnight.nwa
+APP_undertale = games/undertale/output/undertale.nwa:Undertale.nwa
APP_finalearth2 = games/finalearth2/output/finalearth2.nwa:FinalEarth2.nwa
APP_numpowder = games/numpowder/output/numpowder.nwa:NumPowder.nwa
@@ -220,6 +221,7 @@ apps: | $(B)
$(MAKE) -C games/celeste build NWLINK="$(NWLINK)"
$(MAKE) -C games/championisland build NWLINK="$(NWLINK)"
$(MAKE) -C games/hollowknight build NWLINK="$(NWLINK)"
+ $(MAKE) -C games/undertale build NWLINK="$(NWLINK)"
$(MAKE) -C games/finalearth2 build NWLINK="$(NWLINK)"
$(MAKE) -C games/numpowder build NWLINK="$(NWLINK)"
cd games/tetris/tetris && NWLINK="$(NWLINK)" $(CARGO) build --release --quiet
@@ -235,6 +237,7 @@ apps: | $(B)
cp $(word 1,$(subst :, ,$(APP_celeste))) $(B)/apps/$(word 2,$(subst :, ,$(APP_celeste)))
cp $(word 1,$(subst :, ,$(APP_championisland))) $(B)/apps/$(word 2,$(subst :, ,$(APP_championisland)))
cp $(word 1,$(subst :, ,$(APP_hollowknight))) $(B)/apps/$(word 2,$(subst :, ,$(APP_hollowknight)))
+ cp $(word 1,$(subst :, ,$(APP_undertale))) $(B)/apps/$(word 2,$(subst :, ,$(APP_undertale)))
cp $(word 1,$(subst :, ,$(APP_finalearth2))) $(B)/apps/$(word 2,$(subst :, ,$(APP_finalearth2)))
cp $(word 1,$(subst :, ,$(APP_numpowder))) $(B)/apps/$(word 2,$(subst :, ,$(APP_numpowder)))
arm-none-eabi-strip --strip-unneeded $(B)/apps/Tetris.nwa
@@ -291,6 +294,7 @@ clean:
-$(MAKE) -C games/numvisuals clean
-$(MAKE) -C games/championisland clean
-$(MAKE) -C games/hollowknight clean
+ -$(MAKE) -C games/undertale clean
-$(MAKE) -C games/finalearth2 clean
-$(MAKE) -C games/numpowder clean
-cd games/tetris/tetris && $(CARGO) clean
diff --git a/README.md b/README.md
index 214fc213..7c23fe8f 100644
--- a/README.md
+++ b/README.md
@@ -79,7 +79,10 @@ These are too big to fit next to NumPlay, so each one is its own app. Install it
 Celeste The whole climb, with every B-side and C-side Download |
 Champion Island The whole island and its seven sports Download |
- More on the way Undertale, The Binding of Isaac, Plants vs. Zombies, Doom and more: see the roadmap |
+  Undertale The whole Underground, from the golden flowers to the barrier and the neutral ending Download |
+
+
+ More on the way The Binding of Isaac, Plants vs. Zombies, Doom and more: see the roadmap |
@@ -171,7 +174,7 @@ Filmed on my own calculator, no emulator.
- [x] The Final Earth 2
- [x] The Powder Toy (NumPowder)
- [x] Block Blast
-- [ ] Undertale
+- [x] Undertale: the whole Underground, from the Ruins to New Home and the neutral ending
- [ ] The Binding of Isaac
- [ ] Plants vs. Zombies
- [ ] Doom
@@ -199,6 +202,7 @@ NumPlay is a fan project, made out of love for these games and for the NumWorks
- **Minecraft** by Mojang inspired NumBlocks, which follows Minecraft 1.8.8 exactly: it makes its worlds the way Minecraft 1.8.8 does and uses its blocks, items, recipes, textures, font and screens.
- **Celeste** inspired NumPlay's Celeste, which uses the game's own pictures, maps, texts and fonts. Not affiliated with Maddy Makes Games.
- **Hollow Knight** inspired NumPlay's Hollow Knight, which uses the game's own pictures, animations, maps, texts and fonts. Not affiliated with Team Cherry.
+- **Undertale** by Toby Fox inspired NumPlay's Undertale, which uses the game's maps, pictures and texts from the [Undertale Wiki](https://undertale.fandom.com) and the game's free demo, and the **Pixel Operator** fonts by Jayvee Enaguas (public domain). Not affiliated with Toby Fox.
- **[The Powder Toy](https://powdertoy.co.uk)** inspired NumPowder, which ports its materials' behaviour, its font and its icons from [its source code](https://github.com/The-Powder-Toy/The-Powder-Toy) (GNU GPL v3.0), by Stanislaw K Skowronek, Simon Robertshaw and its contributors. Not affiliated with them.
- **[The Final Earth 2](https://store.steampowered.com/app/1180130/The_Final_Earth_2/)** by [Florian van Strien](https://florianvanstrien.nl) inspired NumPlay's The Final Earth 2, which uses the game's own pictures, fonts and texts, with additional sprites by LukXD99. Not affiliated with him.
- **Buckshot Roulette** by Mike Klubnika inspired NumPlay's Buckshot Roulette, whose scenes are rendered from the game through the Open Buckshot Roulette project (1503Dev). Fonts: Fake Receipt by Ray Larabie and Dot Matrix by Dionaea.
@@ -215,7 +219,7 @@ NumPlay is a fan project, made out of love for these games and for the NumWorks
NumPlay is licensed under the [GNU General Public License v3.0](LICENSE). Copyright (c) 2026 Mason Chen. Code and fonts from others keep their own licenses: the `UNLICENSE` files in the games' folders and the [LICENSES](LICENSES) folder. The names, artwork and music of the games NumPlay is inspired by belong to their makers and are not covered by it.
-Unofficial fan games, not affiliated with NumWorks, RobTop Games (Geometry Dash), Hipster Whale (Crossy Road), the makers of Drive Mad, LocalThunk or Playstack (Balatro), Mike Klubnika or Critical Reflex (Buckshot Roulette), Hungry Studio (Block Blast!), Mojang or Microsoft (Minecraft), Maddy Makes Games (Celeste), Team Cherry (Hollow Knight), Florian van Strien (The Final Earth 2), The Powder Toy's developers (The Powder Toy), MateoConLechuga (Portal Returns), Valve (Portal), .GEARS (Flappy Bird), Bandai Namco (Pac-Man), Hasbro (Connect Four), Google (Snake, Solitaire, Block Breaker, Doodle Champion Island Games), STUDIO4°C or Microsoft (Minesweeper). Tetris is a trademark of The Tetris Company, Minecraft of Mojang Synergies AB, Pac-Man of Bandai Namco and Connect Four of Hasbro.
+Unofficial fan games, not affiliated with NumWorks, RobTop Games (Geometry Dash), Hipster Whale (Crossy Road), the makers of Drive Mad, LocalThunk or Playstack (Balatro), Mike Klubnika or Critical Reflex (Buckshot Roulette), Hungry Studio (Block Blast!), Mojang or Microsoft (Minecraft), Maddy Makes Games (Celeste), Team Cherry (Hollow Knight), Toby Fox (Undertale), Florian van Strien (The Final Earth 2), The Powder Toy's developers (The Powder Toy), MateoConLechuga (Portal Returns), Valve (Portal), .GEARS (Flappy Bird), Bandai Namco (Pac-Man), Hasbro (Connect Four), Google (Snake, Solitaire, Block Breaker, Doodle Champion Island Games), STUDIO4°C or Microsoft (Minesweeper). Tetris is a trademark of The Tetris Company, Minecraft of Mojang Synergies AB, Pac-Man of Bandai Namco and Connect Four of Hasbro.
## Gallery
@@ -262,5 +266,6 @@ NumPlay is licensed under the [GNU General Public License v3.0](LICENSE). Copyri
 Champion Island |
+  Undertale |
diff --git a/docs/media/gallery/undertale.gif b/docs/media/gallery/undertale.gif
new file mode 100644
index 00000000..6a300842
Binary files /dev/null and b/docs/media/gallery/undertale.gif differ
diff --git a/docs/media/thumb_undertale.png b/docs/media/thumb_undertale.png
new file mode 100644
index 00000000..b59ae10d
Binary files /dev/null and b/docs/media/thumb_undertale.png differ
diff --git a/docs/media/undertale.gif b/docs/media/undertale.gif
new file mode 100644
index 00000000..113d0b8f
Binary files /dev/null and b/docs/media/undertale.gif differ
diff --git a/docs/play.md b/docs/play.md
index d99446b0..202aa867 100644
--- a/docs/play.md
+++ b/docs/play.md
@@ -254,6 +254,24 @@ Every sport shows its rules and keys before it starts. Your progress is also cop
+## Undertale
+
+
+
+Undertale comes as its own app, `Undertale.nwa`: it is too big to share the calculator with NumPlay.
+
+| Key | What it does |
+| --- | --- |
+| **Arrows** (or 4, 6, 8, 2) | Walk, move your SOUL, choose |
+| **OK**, **EXE** or 5 | Talk, read, confirm (Z in the game) |
+| **Back** or **⌫** | Cancel, show the whole text at once (X in the game) |
+| **toolbox**, **shift** or **var** | The menu: ITEM, STAT, CELL (C in the game) |
+| **Home** | Keep the moment and quit |
+
+Name the fallen human, then walk and talk your way through the Ruins, Snowdin, Waterfall, Hotland, the MTT Resort, the CORE and New Home. In a battle, FIGHT attacks (press **OK** when the bar crosses the middle), ACT checks a monster or does something to it, ITEM eats, and MERCY spares a monster that no longer wants to fight, or flees. Then move your SOUL to dodge; when it turns blue, it falls, and **Up** jumps; when it turns green, it stays put, and the arrows turn its shield to block. On ice you slide until something stops you. In Snowdin Town, walk into the shop to buy, and into the inn to rest. In Waterfall, walk into a bridge seed to push it into the water, where it blooms into a flower you can walk on; the bell calls the seeds back. A green bullet heals you. Walk up to a star and press **OK** to SAVE. Home keeps the moment you were at, and the next start is back there. Your save is also copied into `undertale_saves.py`, so installing the app again doesn't erase it. The Tem Shop in Temmie Village also buys what you carry. In Hotland, steam vents throw you over the lava, orange lasers hit you if you stand still and blue ones if you move, and in a shooting puzzle the arrows push the dark blocks and Z shoots. On the second floor, press the switches as the top conveyor belt carries you past them, and in Mettaton's news report, touch each bomb and defuse it while it's in the DEFUSE ZONE. When Muffet turns your SOUL purple, it keeps to three strings: **Up** and **Down** hop between them. In the tile puzzle, red tiles are walls, yellow ones shock you back, orange makes you smell of oranges (and blue water's piranhas bite then), and purple soap slides you on and smells of lemons. When your SOUL turns yellow, **OK** shoots. At the MTT Resort, the desk rents you a room and the hotel's doors want room service. On the CORE's laser bridge, walk while the power is off and freeze when it comes back on. Mettaton EX's show is won on its ratings: Boast, Pose, a Heel Turn and his brand's food raise them. In New Home, find both keys to unchain the stairs. Asgore breaks MERCY, so you'll have to fight him. Against Omega Flowey, dodge, and reach the ACT button that comes down among a SOUL's attack; once all six SOULs are on your side, reach FIGHT. The game ends at the neutral ending.
+
+
+
## Portal Returns
diff --git a/games/undertale/.gitignore b/games/undertale/.gitignore
new file mode 100644
index 00000000..342d9dda
--- /dev/null
+++ b/games/undertale/.gitignore
@@ -0,0 +1,2 @@
+output/
+build/
diff --git a/games/undertale/Makefile b/games/undertale/Makefile
new file mode 100644
index 00000000..9d85865a
--- /dev/null
+++ b/games/undertale/Makefile
@@ -0,0 +1,79 @@
+# Undertale on the NumWorks calculator.
+# make output/undertale.nwa
+# make check link it like the calculator does (output/undertale.bin)
+# make host build/play: the game on a computer, for tests and screenshots
+# make test the game played on a computer (tests/check.py, with the sanitizers)
+# make data src/data.bin and src/data.h from the public material tools/fetch.py gathers
+# (REF=its folder, Python + Pillow + NumPy)
+Q ?= @
+# the ARM toolchain on the PATH, or in TOOLCHAIN=
+TOOLCHAIN ?=
+BIN = $(if $(TOOLCHAIN),$(TOOLCHAIN)/)
+CC = $(BIN)arm-none-eabi-gcc
+NWLINK ?= npx --yes -- nwlink@1.0.0
+STRIP ?= $(BIN)arm-none-eabi-strip --strip-unneeded
+SIZE = $(BIN)arm-none-eabi-size
+BUILD_DIR = output
+REF ?=
+
+# the drawing runs every frame at -O2; the rest is small at -Os
+HOT = src/gfx.c src/bullets.c
+COLD = $(filter-out $(HOT) src/plat_host.c src/plat_eadk.c,$(wildcard src/*.c)) src/lzma/LzmaDec.c
+COLD_OBJ = $(patsubst src/%.c,$(BUILD_DIR)/cold/%.o,$(COLD))
+HEADERS = $(wildcard src/*.h) ../common/epsilon_app.h ../common/epsilon_files.h
+
+CFLAGS = -std=gnu11 $(shell $(NWLINK) eadk-cflags-device) -DDATA_BIN='"src/data.bin"'
+CFLAGS += -O2 -Wall -Wextra -Wno-unused-parameter -fno-math-errno -fsingle-precision-constant
+CFLAGS += -fno-tree-loop-distribute-patterns -flto -fno-fat-lto-objects -fwhole-program -fvisibility=internal
+CFLAGS += -ffunction-sections -fdata-sections $(EXTRA)
+LDFLAGS = -Wl,--relocatable -nostartfiles --specs=nano.specs
+LDFLAGS += -Wl,-e,main -Wl,-u,eadk_app_name -Wl,-u,eadk_app_icon -Wl,-u,eadk_api_level
+LDFLAGS += -Wl,--gc-sections -flinker-output=nolto-rel
+
+.PHONY: build check clean host test data
+build: $(BUILD_DIR)/undertale.nwa
+
+# apps get 133652 bytes of RAM on custom builds of Epsilon 25.2 (148928 or more on official ones)
+check: $(BUILD_DIR)/undertale.nwa
+ $(Q) $(NWLINK) nwa-bin --ram-length 133652 $< $(BUILD_DIR)/undertale.bin
+ @echo "BIN $(BUILD_DIR)/undertale.bin: $$(wc -c < $(BUILD_DIR)/undertale.bin) bytes"
+
+$(BUILD_DIR)/data.o: src/data.S src/data.bin | $(BUILD_DIR)
+ $(Q) $(CC) -c -DDATA_BIN='"src/data.bin"' $< -o $@
+
+$(BUILD_DIR)/cold/%.o: src/%.c $(HEADERS)
+ @mkdir -p $(@D)
+ $(Q) $(CC) $(CFLAGS) -Os -c $< -o $@
+
+$(BUILD_DIR)/undertale.nwa: $(HOT) src/plat_eadk.c $(COLD_OBJ) $(HEADERS) $(BUILD_DIR)/icon.o $(BUILD_DIR)/data.o
+ @echo "LD $@"
+ $(Q) $(CC) $(CFLAGS) $(LDFLAGS) $(HOT) src/plat_eadk.c $(COLD_OBJ) $(BUILD_DIR)/data.o $(BUILD_DIR)/icon.o -lm -o $@
+ $(Q) $(STRIP) $@
+ $(Q) $(SIZE) $@
+
+$(BUILD_DIR)/icon.o: src/icon.png | $(BUILD_DIR)
+ @echo "ICON $<"
+ $(Q) $(NWLINK) png-icon-o $< $@
+
+$(BUILD_DIR):
+ $(Q) mkdir -p $@
+
+# on a computer: tests/play.c plays it with scripted keys and saves screenshots
+HOST_FLAGS = -std=gnu11 -O1 -g -Wall -Wextra -Wno-unused-parameter -Isrc -DHOST -DDATA_BIN='"src/data.bin"'
+HOST_SAN = -fsanitize=address,undefined -fno-sanitize-recover=undefined
+HOST_SRC = $(filter-out src/plat_eadk.c src/main.c,$(wildcard src/*.c)) src/lzma/LzmaDec.c
+host: build/play
+build/play: $(HOST_SRC) tests/play.c src/data.S src/data.bin $(HEADERS)
+ $(Q) mkdir -p $(dir $@)
+ $(Q) cc $(HOST_FLAGS) $(HOST_SAN) $(HOST_SRC) tests/play.c src/data.S -lm -o $@
+ @echo "HOST $@"
+
+test: build/play
+ python3 tests/check.py build/play
+
+data:
+ @test -n "$(REF)" || { echo "make data REF="; exit 1; }
+ python3 -I tools/pack.py $(REF)
+
+clean:
+ $(Q) rm -rf $(BUILD_DIR) build
diff --git a/games/undertale/README.md b/games/undertale/README.md
new file mode 100644
index 00000000..c3540967
--- /dev/null
+++ b/games/undertale/README.md
@@ -0,0 +1,213 @@
+> **Undertale is a [NumPlay](../../README.md) game that comes on its own.** It is too big to sit next to NumPlay. Get `Undertale.nwa` from the [latest release](https://github.com/Mason363/NumPlay/releases/latest).
+
+
+
+
+
+Undertale
+
+
+ Fall into the Underground as a human child, on the NumWorks calculator.
+ The whole Underground, from the bed of golden flowers to the barrier: the Ruins, Snowdin, Waterfall, Hotland, the MTT Resort, the CORE and New Home, to the neutral ending.
+
+
+
+ Download Undertale.nwa
+ · Controls
+
+
+
+
+
+
+## Highlights
+
+* **The whole Ruins:** every room, from the golden flowers through Toriel's puzzles, the falling leaves, the spikes, the rocks, the cracked floors and the switches, to Toriel's home and the long corridor under it.
+* **Its people, with all they have to say:** Flowey, Toriel and her phone calls, Napstablook, the talking rock, the Froggits who give advice, the spider bake sale, the snail facts, and every sign, bookshelf and mirror.
+* **Snowdin:** Sans and Papyrus, and every one of Papyrus's puzzles: the invisible electricity maze, the X and O tiles, the one on ice, the multicolor tiles and the Gauntlet. Then Snowdin Town, with its shop, inn, library and Papyrus's house.
+* **Waterfall:** Monster Kid, Grillby's with Sans, the echo flowers, the bridge seeds (and the secret bridge to the quiche), the wishing room, Undyne chasing you with her spears, Sans's telescope, the marsh where the land is black and the water glows, Onionsan, the piano's secret song, the statue in the rain and Monster Kid under your umbrella. Then the fall into the garbage dump, Mad Dummy, Napstablook's house and snail farm, the small bird back over the gap, Gerson's shop, the mushroom maze and Temmie Village with its Tem Shop. And at the end of Waterfall: the lantern maze, Undyne at the dead end (and Monster Kid in her way), the echo flowers' path, Monster Kid on the chasm's bridge, Undyne on her crag, her fight and her chase, and Hotland's edge, where the heat gets her and the water cooler helps.
+* **Hotland's first floor:** Alphys's Lab, where Mettaton bursts in with his quiz show, the conveyor belts, the Magma Chamber's steam vents and the burnt pan, the laser walkway, the two shooting puzzles that open the door, the vents over the lava and Mettaton's cooking show, with your phone's jetpack, up to the elevator. Alphys calls you as you go.
+* **Hotland's second floor:** Sans's hot dog stand and Heats Flamesman, the invisible path to the Art Club's room, the Stained Apron, the conveyor belt puzzle and its three switches, the vent puzzle, the Bad Opinion Zone, the Royal Guards, and Mettaton's news report, where everything is a bomb and the clock is running.
+* **Hotland's third floor:** the spiders' bake sale, the vent islands and their two shooting puzzles (the Diamond Boys, and Alphys asking you to watch anime), the conveyor belts past the laser drones, the spider entrance and Muffet's sticky lair, Mettaton's musical and the trapdoor to his tile puzzle against the clock, and the crossroads with the Nice Cream guy and Heats Flamesman, by the stairs up to the MTT Resort.
+* **The MTT Resort:** Sans out front, and dinner with him in the restaurant, where he tells you about the door in the forest; the lobby and its fountain, the MTT-Brand Burger Emporium with Burgerpants behind the counter, Bratty and Catty's shop up the alley, a night in the hotel, room service down the hall (and a dog behind door 4), and the door to the CORE.
+* **The CORE:** two black shapes backing away inside, its broken elevator, Madjick's ambush, the switch whose lasers Alphys gets backwards, Knight Knight north of the crossroads, the laser bridge where Alphys cuts the power (and it keeps coming back), and the Core Branch: the sage's path and its shooting puzzle, the warrior's path and its three fights, or a force field that a patient one just waits out. Then The End, Mettaton's last show, Alphys's confession in the last hallway, and the elevator up to the castle.
+* **New Home:** the grey city, Asgore's home (two keys for the chained stairs, two presents in the children's room), the story of Asriel and the first human told down its basement's corridors, the Last Corridor where Sans judges you, Asgore among the flowers of his throne room, the coffins below, and the barrier: Asgore, your choice, Flowey and the six SOULs, and the way out.
+* **Battles like in the game:** FIGHT with the timing bar, ACT, ITEM and MERCY, and your SOUL dodging each monster's own attacks in the box: Froggit, Whimsun, Moldsmal, Loox, Vegetoid, Migosp, Napstablook and Toriel in the Ruins; Snowdrake, Ice Cap, Gyftrot, Jerry, Chilldrake, Glyde, Doggo, Lesser Dog, Dogamy and Dogaressa, Greater Dog and Papyrus, whose blue attack makes your SOUL fall, in Snowdin; Aaron, Woshua, Moldbygg, Shyren, Temmie, Mad Dummy, who can't be hurt, and Undyne, who turns your SOUL green, in Waterfall; Vulkin, Tsunderplane, Pyrope and the Royal Guards in Hotland, the news report's bombs, defused in the DEFUSE ZONE, Muffet, whose purple SOUL keeps to her strings, and Mettaton, worn down by the yellow SOUL's shots; Final Froggit, Whimsalot, Astigmatism, Madjick and Knight Knight in the CORE, and Mettaton EX, whose show is won on its ratings; Asgore, who breaks MERCY, and Omega Flowey with the six human SOULs in New Home.
+* **Spare or fight:** every monster can be spared, and Flowey at the door remembers what you did. Lose to Papyrus and he only locks you in his shed.
+* **Saved like in the game:** SAVE at the stars, with the room and the time played. The C menu has your ITEM, STAT and CELL. Home keeps the moment you were at, and your save is also copied into `undertale_saves.py`, so installing the app again doesn't erase it.
+
+
+
+  The bed of golden flowers |
+  Flowey's friendliness pellets |
+
+
+  Toriel shows how the puzzles work |
+  A Froggit |
+
+
+  Napstablook |
+  Toriel's home |
+
+
+  Toriel won't let you leave |
+  Flowey waits at the door |
+
+
+  Sans, on the road through Snowdin Forest |
+  Papyrus's invisible electricity maze |
+
+
+  The X and O puzzle on ice |
+  Greater Dog |
+
+
+  Snowdin Town |
+  The shop |
+
+
+  Papyrus's blue attack |
+  Hanging out with Papyrus |
+
+
+  Monster Kid, by the first echo flower |
+  Bridge seeds bloom in the water |
+
+
+  Undyne's spears on the boardwalk |
+  Onionsan says hi |
+
+
+  The statue in the rain |
+  Monster Kid and the castle, far away |
+
+
+  Woshua's green drop heals |
+  Shyren sings when you hum along |
+
+
+  Mad Dummy sends its dummies |
+  At Napstablook's house |
+
+
+  Gerson's shop |
+  Temmie Village |
+
+
+  Monster Kid gets in Undyne's way |
+  Monster Kid hangs off the bridge |
+
+
+  Undyne on her crag |
+  Block Undyne's spears with the green SOUL |
+
+
+  Welcome to Hotland |
+  Water for Undyne, in Hotland's heat |
+
+
+  Alphys, in her Lab |
+  Mettaton's quiz show |
+
+
+  Orange lasers: keep moving |
+  A shooting puzzle |
+
+
+  Cooking with a killer robot |
+  Up the counter with the jetpack |
+
+
+  A hot dog from Sans |
+  The vent puzzle |
+
+
+  The Royal Guards got a tip |
+  Their team attack |
+
+
+  MTT News: two minutes to go |
+  Defuse it in the DEFUSE ZONE |
+
+
+  One shot to clear the way |
+  Muffet's strings, the purple SOUL |
+
+
+  Mettaton's musical |
+  The tile puzzle, against the clock |
+
+
+  The yellow button |
+  Nice Cream, by the MTT Resort |
+
+
+  Dinner with Sans at the MTT Resort |
+  Bratty and Catty's shop, up the alley |
+
+
+  The laser bridge: freeze when the power's on |
+  Knight Knight, before her lullaby |
+
+
+  The sage's path, two shots |
+  Mettaton EX: keep the ratings up |
+
+
+  Asgore's home, the stairs chained shut |
+  The Last Corridor: Sans's judgment |
+
+
+  Asgore among his flowers |
+  Asgore at the barrier, MERCY broken |
+
+
+  Omega Flowey and the six SOULs |
+ |
+
+
+
+## How to play
+
+A new game starts with the story of the war between humans and monsters, then the title. Name the fallen human, and you wake up on the golden flowers. Walk on and talk to whoever you meet: Flowey has a few things to teach you, then Toriel guides you through the Ruins.
+
+Monsters jump out as you walk. In a battle, choose FIGHT to attack (press OK when the bar crosses the middle), ACT to check them, talk or do something else, ITEM to eat, and MERCY to spare them once they no longer want to fight, or to flee. Then move your SOUL, the red heart, to dodge what they throw at you. Each monster has its own way to be spared: try what ACT offers.
+
+Snowdin brings new things: when Papyrus turns your SOUL blue, it falls to the bottom of the box, and Up jumps over his bones. On ice you slide until something stops you. Snowdin Town's shop sells food and gear, and its inn gives you a night's rest.
+
+In Waterfall, walk into a bridge seed to push it: in the water it blooms into a flower you can walk on, and the bell calls the seeds back. A green bullet heals you instead of hurting. Talk to the piano to play it, and keep an umbrella for the rain. The Tem Shop is the one shop that buys what you carry. When Undyne turns your SOUL green, it can't move: turn its shield with the arrows to block her spears. She can't be spared: run, and she'll chase you.
+
+In Hotland, conveyor belts carry you and steam vents throw you over the lava (the one in the middle of the puzzle hub turns each time). Keep moving through orange lasers and stand still for blue ones. In a shooting puzzle, the arrows push every dark block one cell and Z shoots: break your way to the target in two shots. In Mettaton's quiz, move your SOUL onto an answer and press OK; Alphys signals the right one. Alphys's upgrade puts the Dimensional Box in your phone's CELL. On the second floor, the top conveyor belt carries you past three switches: press them one after another as you go by. In the vent puzzle, every panel you land on turns all the vents around, and the one at the top turns them back. To spare the Royal Guards, clean 02's armor, catch its green stars, then whisper to 01. In the news report, touch a bomb to face it, and Defuse it (or FIGHT it) while it's in the DEFUSE ZONE. On the third floor, ride the belt past the blue drones without a step. When Muffet turns your SOUL purple, it keeps to three strings: Up and Down hop between them, Left and Right move along. In the tile puzzle, red tiles are walls, yellow ones shock you back, orange ones make you smell of oranges (and the blue water's piranhas bite then), purple soap slides you on and makes you smell of lemons, and blue next to yellow is electrified. After it, ACT and press the yellow button: your SOUL turns yellow, and OK shoots. At the MTT Resort, the front desk rents you a room for the night, and the hotel's doors down the hall want room service: bring what they ask for. On the CORE's laser bridge, walk while the power is off and freeze the moment it flickers back on. Against Mettaton, turn him round first; then it's a show: Boast, Pose, a Heel Turn, his brand's food and every hit you take make the ratings climb, and once they're high enough the show is over. In New Home, find both keys to unchain the stairs down. Asgore breaks MERCY: you'll have to fight him, and then choose. Against Omega Flowey there are no turns: dodge, and when a SOUL fights for him, reach the ACT button that comes down among its attack to call for help. Once all six are on your side, reach FIGHT.
+
+Walk up to a star and press OK to SAVE and fill your HP. The C menu (toolbox) shows your items, your STAT and the phone numbers you have.
+
+## Controls
+
+| Key | What it does |
+| --- | --- |
+| Arrows (or 4, 6, 8, 2) | Walk, move your SOUL, choose |
+| OK, EXE or 5 | Talk, read, confirm (Z in the game) |
+| Back or ⌫ | Cancel, show the whole text at once (X in the game) |
+| Toolbox, Shift or Var | The menu: ITEM, STAT, CELL (C in the game) |
+| Home | Keep the moment and quit: the next start is back there |
+
+## Good to know
+
+* **One app at a time:** Undertale is too big to sit next to NumPlay or the other big games. Installing it from the NumWorks website swaps them out, and installing them again swaps it out. Your saves in each one are kept.
+* **The neutral ending:** the game plays from the start to the barrier and the neutral ending, with Sans's call after. The true pacifist ending (the True Lab, Asriel) and the genocide route aren't in it.
+* **No music or sound:** the calculator has no speaker.
+* **The same Underground:** the rooms and pictures are the game's, everyone says what they say in the game, in shorter words of our own, and the puzzles and battles follow how the game plays them, at its 30 frames a second.
+
+## Credits
+
+Inspired by Undertale by Toby Fox, not affiliated with him.
+
+* The maps and pictures come from the [Undertale Wiki](https://undertale.fandom.com) and the game's free demo, and what's said follows them.
+* The **Pixel Operator** fonts are by Jayvee Enaguas, released into the public domain (CC0).
+* The LZMA decoder in `src/lzma` is by Igor Pavlov, in the public domain.
+
+Made by Mason Chen as part of NumPlay.
+
+## License
+
+The game's code is licensed under the GNU General Public License v3.0: see the `LICENSE` file at the root of the repository. The game's pictures, maps and texts keep their own terms (see Credits).
diff --git a/games/undertale/docs/alley.png b/games/undertale/docs/alley.png
new file mode 100644
index 00000000..24382104
Binary files /dev/null and b/games/undertale/docs/alley.png differ
diff --git a/games/undertale/docs/asgore.png b/games/undertale/docs/asgore.png
new file mode 100644
index 00000000..91a53273
Binary files /dev/null and b/games/undertale/docs/asgore.png differ
diff --git a/games/undertale/docs/asgorehome.png b/games/undertale/docs/asgorehome.png
new file mode 100644
index 00000000..8d330ca9
Binary files /dev/null and b/games/undertale/docs/asgorehome.png differ
diff --git a/games/undertale/docs/bridge.png b/games/undertale/docs/bridge.png
new file mode 100644
index 00000000..f14bc041
Binary files /dev/null and b/games/undertale/docs/bridge.png differ
diff --git a/games/undertale/docs/castle.png b/games/undertale/docs/castle.png
new file mode 100644
index 00000000..7585aea4
Binary files /dev/null and b/games/undertale/docs/castle.png differ
diff --git a/games/undertale/docs/chasm.png b/games/undertale/docs/chasm.png
new file mode 100644
index 00000000..f7b31502
Binary files /dev/null and b/games/undertale/docs/chasm.png differ
diff --git a/games/undertale/docs/cooking.png b/games/undertale/docs/cooking.png
new file mode 100644
index 00000000..38d7c59f
Binary files /dev/null and b/games/undertale/docs/cooking.png differ
diff --git a/games/undertale/docs/crag.png b/games/undertale/docs/crag.png
new file mode 100644
index 00000000..85b4489a
Binary files /dev/null and b/games/undertale/docs/crag.png differ
diff --git a/games/undertale/docs/crossroads.png b/games/undertale/docs/crossroads.png
new file mode 100644
index 00000000..3542031c
Binary files /dev/null and b/games/undertale/docs/crossroads.png differ
diff --git a/games/undertale/docs/date.png b/games/undertale/docs/date.png
new file mode 100644
index 00000000..b95de4df
Binary files /dev/null and b/games/undertale/docs/date.png differ
diff --git a/games/undertale/docs/deadend.png b/games/undertale/docs/deadend.png
new file mode 100644
index 00000000..109993c2
Binary files /dev/null and b/games/undertale/docs/deadend.png differ
diff --git a/games/undertale/docs/defuse.png b/games/undertale/docs/defuse.png
new file mode 100644
index 00000000..e6d6c305
Binary files /dev/null and b/games/undertale/docs/defuse.png differ
diff --git a/games/undertale/docs/dinner.png b/games/undertale/docs/dinner.png
new file mode 100644
index 00000000..51026c78
Binary files /dev/null and b/games/undertale/docs/dinner.png differ
diff --git a/games/undertale/docs/echo.png b/games/undertale/docs/echo.png
new file mode 100644
index 00000000..75e91d70
Binary files /dev/null and b/games/undertale/docs/echo.png differ
diff --git a/games/undertale/docs/flowers.png b/games/undertale/docs/flowers.png
new file mode 100644
index 00000000..64df8c88
Binary files /dev/null and b/games/undertale/docs/flowers.png differ
diff --git a/games/undertale/docs/flowey.png b/games/undertale/docs/flowey.png
new file mode 100644
index 00000000..b7ea1937
Binary files /dev/null and b/games/undertale/docs/flowey.png differ
diff --git a/games/undertale/docs/froggit.png b/games/undertale/docs/froggit.png
new file mode 100644
index 00000000..06862e3d
Binary files /dev/null and b/games/undertale/docs/froggit.png differ
diff --git a/games/undertale/docs/gerson.png b/games/undertale/docs/gerson.png
new file mode 100644
index 00000000..ea8d1680
Binary files /dev/null and b/games/undertale/docs/gerson.png differ
diff --git a/games/undertale/docs/greaterdog.png b/games/undertale/docs/greaterdog.png
new file mode 100644
index 00000000..918c653c
Binary files /dev/null and b/games/undertale/docs/greaterdog.png differ
diff --git a/games/undertale/docs/guards.png b/games/undertale/docs/guards.png
new file mode 100644
index 00000000..e6f017a7
Binary files /dev/null and b/games/undertale/docs/guards.png differ
diff --git a/games/undertale/docs/home.png b/games/undertale/docs/home.png
new file mode 100644
index 00000000..42617297
Binary files /dev/null and b/games/undertale/docs/home.png differ
diff --git a/games/undertale/docs/hotdog.png b/games/undertale/docs/hotdog.png
new file mode 100644
index 00000000..33cbfd58
Binary files /dev/null and b/games/undertale/docs/hotdog.png differ
diff --git a/games/undertale/docs/hotland.png b/games/undertale/docs/hotland.png
new file mode 100644
index 00000000..6fa8db7c
Binary files /dev/null and b/games/undertale/docs/hotland.png differ
diff --git a/games/undertale/docs/ice.png b/games/undertale/docs/ice.png
new file mode 100644
index 00000000..63836937
Binary files /dev/null and b/games/undertale/docs/ice.png differ
diff --git a/games/undertale/docs/jetpack.png b/games/undertale/docs/jetpack.png
new file mode 100644
index 00000000..3883b9e0
Binary files /dev/null and b/games/undertale/docs/jetpack.png differ
diff --git a/games/undertale/docs/judgement.png b/games/undertale/docs/judgement.png
new file mode 100644
index 00000000..09944804
Binary files /dev/null and b/games/undertale/docs/judgement.png differ
diff --git a/games/undertale/docs/knight.png b/games/undertale/docs/knight.png
new file mode 100644
index 00000000..fb0ef7df
Binary files /dev/null and b/games/undertale/docs/knight.png differ
diff --git a/games/undertale/docs/lab.png b/games/undertale/docs/lab.png
new file mode 100644
index 00000000..1a02a3e5
Binary files /dev/null and b/games/undertale/docs/lab.png differ
diff --git a/games/undertale/docs/lasers.png b/games/undertale/docs/lasers.png
new file mode 100644
index 00000000..57283a90
Binary files /dev/null and b/games/undertale/docs/lasers.png differ
diff --git a/games/undertale/docs/lastcorridor.png b/games/undertale/docs/lastcorridor.png
new file mode 100644
index 00000000..7f1c9092
Binary files /dev/null and b/games/undertale/docs/lastcorridor.png differ
diff --git a/games/undertale/docs/maddummy.png b/games/undertale/docs/maddummy.png
new file mode 100644
index 00000000..5129514e
Binary files /dev/null and b/games/undertale/docs/maddummy.png differ
diff --git a/games/undertale/docs/maze.png b/games/undertale/docs/maze.png
new file mode 100644
index 00000000..5b75aad8
Binary files /dev/null and b/games/undertale/docs/maze.png differ
diff --git a/games/undertale/docs/mttex.png b/games/undertale/docs/mttex.png
new file mode 100644
index 00000000..17561aa7
Binary files /dev/null and b/games/undertale/docs/mttex.png differ
diff --git a/games/undertale/docs/muffet.png b/games/undertale/docs/muffet.png
new file mode 100644
index 00000000..a185f25f
Binary files /dev/null and b/games/undertale/docs/muffet.png differ
diff --git a/games/undertale/docs/musical.png b/games/undertale/docs/musical.png
new file mode 100644
index 00000000..5ddf1c90
Binary files /dev/null and b/games/undertale/docs/musical.png differ
diff --git a/games/undertale/docs/napstablook.png b/games/undertale/docs/napstablook.png
new file mode 100644
index 00000000..6df66577
Binary files /dev/null and b/games/undertale/docs/napstablook.png differ
diff --git a/games/undertale/docs/napstahouse.png b/games/undertale/docs/napstahouse.png
new file mode 100644
index 00000000..83c69372
Binary files /dev/null and b/games/undertale/docs/napstahouse.png differ
diff --git a/games/undertale/docs/neon.png b/games/undertale/docs/neon.png
new file mode 100644
index 00000000..e5aa2d05
Binary files /dev/null and b/games/undertale/docs/neon.png differ
diff --git a/games/undertale/docs/news.png b/games/undertale/docs/news.png
new file mode 100644
index 00000000..9ebd5161
Binary files /dev/null and b/games/undertale/docs/news.png differ
diff --git a/games/undertale/docs/omega.png b/games/undertale/docs/omega.png
new file mode 100644
index 00000000..69c7b5f8
Binary files /dev/null and b/games/undertale/docs/omega.png differ
diff --git a/games/undertale/docs/onionsan.png b/games/undertale/docs/onionsan.png
new file mode 100644
index 00000000..147c2e01
Binary files /dev/null and b/games/undertale/docs/onionsan.png differ
diff --git a/games/undertale/docs/papyrus.png b/games/undertale/docs/papyrus.png
new file mode 100644
index 00000000..f04f39e0
Binary files /dev/null and b/games/undertale/docs/papyrus.png differ
diff --git a/games/undertale/docs/puzzles.png b/games/undertale/docs/puzzles.png
new file mode 100644
index 00000000..96a5c9a5
Binary files /dev/null and b/games/undertale/docs/puzzles.png differ
diff --git a/games/undertale/docs/quiz.png b/games/undertale/docs/quiz.png
new file mode 100644
index 00000000..0b7820c1
Binary files /dev/null and b/games/undertale/docs/quiz.png differ
diff --git a/games/undertale/docs/rguards.png b/games/undertale/docs/rguards.png
new file mode 100644
index 00000000..09d61535
Binary files /dev/null and b/games/undertale/docs/rguards.png differ
diff --git a/games/undertale/docs/sage.png b/games/undertale/docs/sage.png
new file mode 100644
index 00000000..f211b345
Binary files /dev/null and b/games/undertale/docs/sage.png differ
diff --git a/games/undertale/docs/sans.png b/games/undertale/docs/sans.png
new file mode 100644
index 00000000..23df68c1
Binary files /dev/null and b/games/undertale/docs/sans.png differ
diff --git a/games/undertale/docs/seeds.png b/games/undertale/docs/seeds.png
new file mode 100644
index 00000000..4bf52b5d
Binary files /dev/null and b/games/undertale/docs/seeds.png differ
diff --git a/games/undertale/docs/shoot3.png b/games/undertale/docs/shoot3.png
new file mode 100644
index 00000000..565e46b0
Binary files /dev/null and b/games/undertale/docs/shoot3.png differ
diff --git a/games/undertale/docs/shooting.png b/games/undertale/docs/shooting.png
new file mode 100644
index 00000000..e094e6f0
Binary files /dev/null and b/games/undertale/docs/shooting.png differ
diff --git a/games/undertale/docs/shop.png b/games/undertale/docs/shop.png
new file mode 100644
index 00000000..96162c75
Binary files /dev/null and b/games/undertale/docs/shop.png differ
diff --git a/games/undertale/docs/shyren.png b/games/undertale/docs/shyren.png
new file mode 100644
index 00000000..e8ba6cee
Binary files /dev/null and b/games/undertale/docs/shyren.png differ
diff --git a/games/undertale/docs/spears.png b/games/undertale/docs/spears.png
new file mode 100644
index 00000000..3c45dddc
Binary files /dev/null and b/games/undertale/docs/spears.png differ
diff --git a/games/undertale/docs/statue.png b/games/undertale/docs/statue.png
new file mode 100644
index 00000000..06e9c249
Binary files /dev/null and b/games/undertale/docs/statue.png differ
diff --git a/games/undertale/docs/temmie.png b/games/undertale/docs/temmie.png
new file mode 100644
index 00000000..585ab069
Binary files /dev/null and b/games/undertale/docs/temmie.png differ
diff --git a/games/undertale/docs/throne.png b/games/undertale/docs/throne.png
new file mode 100644
index 00000000..6e1bd43a
Binary files /dev/null and b/games/undertale/docs/throne.png differ
diff --git a/games/undertale/docs/tiles.png b/games/undertale/docs/tiles.png
new file mode 100644
index 00000000..9521c175
Binary files /dev/null and b/games/undertale/docs/tiles.png differ
diff --git a/games/undertale/docs/toriel.png b/games/undertale/docs/toriel.png
new file mode 100644
index 00000000..f00f736d
Binary files /dev/null and b/games/undertale/docs/toriel.png differ
diff --git a/games/undertale/docs/town.png b/games/undertale/docs/town.png
new file mode 100644
index 00000000..d5e918cd
Binary files /dev/null and b/games/undertale/docs/town.png differ
diff --git a/games/undertale/docs/undyne.png b/games/undertale/docs/undyne.png
new file mode 100644
index 00000000..6eba7344
Binary files /dev/null and b/games/undertale/docs/undyne.png differ
diff --git a/games/undertale/docs/ventpuzzle.png b/games/undertale/docs/ventpuzzle.png
new file mode 100644
index 00000000..7b4e8d2e
Binary files /dev/null and b/games/undertale/docs/ventpuzzle.png differ
diff --git a/games/undertale/docs/woshua.png b/games/undertale/docs/woshua.png
new file mode 100644
index 00000000..965bdcca
Binary files /dev/null and b/games/undertale/docs/woshua.png differ
diff --git a/games/undertale/docs/yellow.png b/games/undertale/docs/yellow.png
new file mode 100644
index 00000000..152b397a
Binary files /dev/null and b/games/undertale/docs/yellow.png differ
diff --git a/games/undertale/src/battle.c b/games/undertale/src/battle.c
new file mode 100644
index 00000000..72d0e310
--- /dev/null
+++ b/games/undertale/src/battle.c
@@ -0,0 +1,809 @@
+/* Battles: the turns, the menus, FIGHT's bar, the box, the end (battle.h). */
+#include
+#include
+#include "battle.h"
+#include "game.h"
+#include "gfx.h"
+#include "items.h"
+#include "res.h"
+#include "save.h"
+#include "text.h"
+#include "menu.h"
+#include "monsters.h"
+#include "world.h"
+
+/* the battles that play their own way: Flowey's first (flowey.c), Mettaton's shows and a laser's alarm (htshows.c),
+ * Omega Flowey's (nhmon.c) */
+typedef struct {
+ int form;
+ void (*start)(void);
+ void (*update)(uint32_t keys, uint32_t pressed);
+ void (*draw)(void);
+} Script;
+static const Script scripts[] = {
+ {FORM_FLOWEY, flowey_battle_start, flowey_battle_update, flowey_battle_draw},
+ {FORM_MTT_QUIZ, quiz_start, quiz_update, quiz_draw},
+ {FORM_MTT_COOK, cook_start, cook_update, cook_draw},
+ {FORM_LASERS, lasers_start, lasers_update, lasers_draw},
+ {FORM_OMEGA, omega_start, omega_update, omega_draw},
+};
+static const Script *script;
+
+Battle bt;
+static bool over = true;
+static int heart_x, heart_y; /* the SOUL's start, from Frisk */
+static int prev_mode;
+
+enum {
+ P_INTRO, P_MENU, P_TARGET, P_ACTS, P_ITEMS, P_MERCY, P_BAR, P_STRIKE, P_RESULT, P_TALK, P_BOX_IN, P_ATTACK,
+ P_BOX_OUT, P_WON, P_FLEE, P_DEATH, P_END, P_GLARE
+};
+enum { B_FIGHT, B_ACT, B_ITEM, B_MERCY };
+
+static const int16_t button_x[4] = {16, 92, 172, 250};
+#define BUTTON_Y 216
+
+/* ---------------------------------------------------------------- the box */
+static void box_to(int x0, int y0, int x1, int y1) { bt.tx0 = (int16_t)x0, bt.ty0 = (int16_t)y0, bt.tx1 = (int16_t)x1, bt.ty1 = (int16_t)y1; }
+static void box_menu(void) { box_to(BOX_X0, BOX_Y0, BOX_X1, BOX_Y1); }
+void board_set(int w, int h) {
+ int cx = 160, y1 = BOX_Y1;
+ box_to(cx - w / 2, y1 - h, cx + (w + 1) / 2, y1);
+}
+void board_set_rect(int x0, int y0, int x1, int y1) { box_to(x0, y0, x1, y1); }
+int board_cx(void) { return (bt.x0 + bt.x1) / 2; }
+int board_cy(void) { return (bt.y0 + bt.y1) / 2; }
+static bool box_settled(void) { return bt.x0 == bt.tx0 && bt.y0 == bt.ty0 && bt.x1 == bt.tx1 && bt.y1 == bt.ty1; }
+static int16_t approach(int16_t v, int16_t t, int step) {
+ if (v < t) return (int16_t)(v + step > t ? t : v + step);
+ if (v > t) return (int16_t)(v - step < t ? t : v - step);
+ return v;
+}
+static void box_step(void) {
+ bt.x0 = approach(bt.x0, bt.tx0, 12), bt.x1 = approach(bt.x1, bt.tx1, 12);
+ bt.y0 = approach(bt.y0, bt.ty0, 8), bt.y1 = approach(bt.y1, bt.ty1, 8);
+}
+
+/* ---------------------------------------------------------------- texts */
+static void say(const char *s) {
+ if (s != bt.text) snprintf(bt.text, sizeof bt.text, "%s", s);
+ text_wrap_width(264);
+ writer_start(&bt.w, bt.text, 26, 133, FONT_MAIN);
+ bt.w.line_h = 16;
+}
+
+int count_alive(void) {
+ int n = 0;
+ for (int i = 0; i < bt.nmon; i++) n += bt.mon[i].alive && !bt.mon[i].gone;
+ return n;
+}
+
+static const char *flavor_text(void) {
+ /* a monster's own, else one of its neutral texts, low HP's or spareable's */
+ int alive[MAX_MON], n = 0;
+ for (int i = 0; i < bt.nmon; i++)
+ if (bt.mon[i].alive && !bt.mon[i].gone) alive[n++] = i;
+ if (!n) return "";
+ Monster *m = &bt.mon[alive[rnd_n(n)]];
+ const MonDef *d = m->def;
+ if (d->flavor) {
+ const char *s = d->flavor(&bt, m);
+ if (s) return s;
+ }
+ if (m->spare && d->spareable) return d->spareable;
+ if (m->hp < m->maxhp / 3 && d->low_hp) return d->low_hp;
+ int k = 0;
+ while (k < 6 && d->neutral[k]) k++;
+ return k ? d->neutral[rnd_n(k)] : "";
+}
+
+/* ---------------------------------------------------------------- the start and the end */
+void battle_start(int form) {
+ memset(&bt, 0, sizeof bt);
+ bt.form = form;
+ bt.atk = -1;
+ form_setup(form, &bt);
+ for (int i = 0; i < bt.nmon; i++) {
+ Monster *m = &bt.mon[i];
+ const MonDef *d = m->def;
+ if (!m->maxhp) m->hp = m->maxhp = d->hp;
+ if (!m->at) m->at = d->at;
+ if (!m->df) m->df = d->df;
+ if (!m->exp) m->exp = d->exp;
+ if (!m->gold) m->gold = d->gold;
+ m->alive = true;
+ m->spare = d->flags & MF_SPARE_START;
+ m->spare_gold = d->spare_gold;
+ }
+ bt.x0 = bt.tx0 = BOX_X0, bt.y0 = bt.ty0 = BOX_Y0, bt.x1 = bt.tx1 = BOX_X1, bt.y1 = bt.ty1 = BOX_Y1;
+ bt.speed = 2 * FX;
+ soul_set_mode(SOUL_RED);
+ bt.hp_shown = sv.hp;
+ heart_x = pl.x + 10 - cam_x, heart_y = pl.y + 17 - cam_y;
+ prev_mode = g_mode;
+ g_mode = MODE_BATTLE;
+ bt.phase = P_INTRO;
+ bt.t = 0;
+ say(form_text(form));
+ bt.w.finished = true; /* (typed once the intro is over) */
+ script = NULL;
+ for (unsigned i = 0; i < sizeof scripts / sizeof scripts[0]; i++)
+ if (scripts[i].form == form) script = &scripts[i], script->start();
+ over = false;
+ bullets_clear();
+}
+
+bool battle_over(void) { return over; }
+int battle_result(void) { return bt.result; }
+int battle_ui(void) {
+ switch (bt.phase) {
+ case P_MENU: return BUI_MENU;
+ case P_TARGET: return BUI_TARGET;
+ case P_ACTS: return BUI_ACTS;
+ case P_ITEMS: return BUI_ITEMS;
+ case P_MERCY: return BUI_MERCY;
+ case P_BAR: return BUI_BAR;
+ default: return BUI_NONE;
+ }
+}
+
+static void battle_end(int result) {
+ bt.result = result;
+ bt.phase = P_END;
+ over = true;
+ bullets_clear();
+ if (result == BR_DIED) {
+ menu_gameover_open();
+ return;
+ }
+ g_mode = MODE_WORLD;
+ world_camera();
+}
+
+/* ---------------------------------------------------------------- FIGHT */
+static int fight_damage(Monster *m, int dist) {
+ /* the game's: (AT + weapon - DEF + a little luck), times how near the middle the bar was stopped */
+ int at = sv.at + weapon_at();
+ float base = (float)(at - m->df + rnd_n(3));
+ float stretch = dist <= 12 ? 2.2f : (1.0f - (float)dist / 273.0f) * 2.0f;
+ int dmg = (int)(base * stretch + 0.5f);
+ if (m->def->flags & MF_ONE_HIT) dmg = m->hp;
+ return dmg < 0 ? 0 : dmg;
+}
+
+static void strike(void) {
+ Monster *m = &bt.mon[bt.target];
+ /* the bar's distance from the middle, in the original's pixels */
+ int x = bt.bar_x / FX; /* 0..281 across the target */
+ int dist = abs_i(x - 140) * 2;
+ int dmg = bt.bar_x < 0 ? -1 : fight_damage(m, dist);
+ bt.strike_dmg = dmg;
+ bt.phase = P_STRIKE;
+ bt.t = 0;
+}
+
+static void after_player_turn(void);
+
+static void strike_land(void) {
+ Monster *m = &bt.mon[bt.target];
+ int dmg = bt.strike_dmg;
+ m->dmg_shown = (int16_t)dmg, m->dmg_t = 45;
+ m->bar_shown = m->hp;
+ if (dmg <= 0 && (m->def->flags & MF_MISS_IS_SPARE) && m->def->on_spare) m->def->on_spare(&bt, m);
+ if (dmg > 0) {
+ if (m->def->on_hit && m->def->on_hit(&bt, m, dmg)) return;
+ m->hp = (int16_t)(m->hp - dmg);
+ m->hurt = 30, m->shake = 8;
+ if (m->hp <= 0) {
+ m->hp = 0;
+ m->killed = true;
+ }
+ }
+}
+
+/* ---------------------------------------------------------------- the turns */
+static void begin_menu(void) {
+ /* the first Froggit: Toriel comes and glares at it, and it leaves */
+ if (bt.form == FORM_FROGGIT_FIRST && bt.turn >= 1 && count_alive()) {
+ bt.phase = P_GLARE, bt.t = 0;
+ box_menu();
+ say("");
+ return;
+ }
+ bt.phase = P_MENU;
+ box_menu();
+ say(flavor_text());
+ bt.turn++;
+ /* the Stained Apron heals a little, every other turn */
+ if (sv.armor == ITEM_STAINED_APRON && bt.turn % 2 == 0 && sv.hp < sv.maxhp) sv.hp++;
+}
+
+static void begin_talk(void) {
+ bt.phase = P_TALK;
+ bt.t = 0;
+ for (int i = 0; i < bt.nmon; i++) {
+ Monster *m = &bt.mon[i];
+ m->say = NULL;
+ if (!m->alive || m->gone) continue;
+ if (m->def->talk) m->say = m->def->talk(&bt, m);
+ else {
+ int k = 0;
+ while (k < 6 && m->def->quotes[k]) k++;
+ if (k) m->say = m->def->quotes[rnd_n(k)];
+ }
+ }
+ bt.talk_i = -1;
+ for (int i = 0; i < bt.nmon; i++)
+ if (bt.mon[i].say) {
+ bt.talk_i = i;
+ break;
+ }
+ if (bt.talk_i >= 0) {
+ writer_start(&bt.bw, bt.mon[bt.talk_i].say, 0, 0, FONT_SMALL);
+ bt.bw.line_h = 9;
+ }
+}
+static bool all_gone(void);
+static void win(void);
+
+static void begin_attack(void) {
+ /* the attack: one of the monsters' (at random among those alive) */
+ int who[MAX_MON], n = 0;
+ for (int i = 0; i < bt.nmon; i++)
+ if (bt.mon[i].alive && !bt.mon[i].gone && bt.mon[i].def->attack) who[n++] = i;
+ if (!n) {
+ begin_menu();
+ return;
+ }
+ int i = who[rnd_n(n)];
+ bt.atk_owner = i;
+ bt.atk = bt.mon[i].def->attack(&bt, &bt.mon[i]);
+ if (bt.atk < 0) {
+ begin_menu();
+ return;
+ }
+ bt.atk_len = attack_length(bt.atk);
+ bt.atk_t = 0;
+ board_set(BOARD_W, BOARD_H);
+ attack_run(&bt, bt.atk, -1); /* (t -1: the attack sets its board) */
+ bt.phase = P_BOX_IN;
+ bullets_clear();
+}
+
+static void win(void) {
+ int exp = 0, gold = 0;
+ for (int i = 0; i < bt.nmon; i++) {
+ if (bt.mon[i].killed) exp += bt.mon[i].exp, gold += bt.mon[i].gold;
+ else if (bt.mon[i].gone) gold += bt.mon[i].spare_gold;
+ }
+ gold += bt.gold_won;
+ sv.gold = (uint16_t)min_i(sv.gold + gold, 9999);
+ bool up = gain_exp(exp);
+ bt.exp_won = exp, bt.gold_won = gold;
+ char s[200];
+ snprintf(s, sizeof s, "* YOU WON!\n* You earned %d EXP and %d gold.%s", exp, gold, up ? "\n* Your LOVE increased." : "");
+ say(s);
+ bt.phase = P_WON;
+ box_menu();
+ bullets_clear();
+}
+
+static bool all_gone(void) { return count_alive() == 0; }
+
+static void after_player_turn(void) {
+ if (all_gone()) {
+ bool any_killed = false;
+ for (int i = 0; i < bt.nmon; i++) any_killed |= bt.mon[i].killed;
+ if (any_killed || bt.gold_won) win();
+ else {
+ bt.result = BR_SPARED;
+ win();
+ }
+ return;
+ }
+ begin_talk();
+}
+
+static void do_spare(void) {
+ bool any = false;
+ bt.last_action = 3;
+ for (int i = 0; i < bt.nmon; i++) {
+ Monster *m = &bt.mon[i];
+ if (m->alive && !m->gone && m->spare) m->gone = true, m->fade = 1, any = true;
+ else if (m->alive && !m->gone && m->def->on_spare) m->def->on_spare(&bt, m);
+ }
+ (void)any;
+ bt.text[0] = 0;
+ after_player_turn(); /* (all spared: the battle's won, as BR_SPARED) */
+}
+
+static void do_flee(void) {
+ bool can = !bt.no_flee;
+ bt.last_action = 4;
+ if (!can) {
+ after_player_turn();
+ return;
+ }
+ bool ok = sv.armor == ITEM_BANDAGE || rnd_n(100) < 50 + 10 * bt.turn;
+ if (!ok) {
+ after_player_turn();
+ return;
+ }
+ static const char *const lines[] = {"* Escaped...", "* I'm outta here.", "* I've got better to do.", "* Don't slow me down."};
+ say(lines[rnd_n(4)]);
+ bt.phase = P_FLEE;
+ bt.t = 0;
+}
+
+/* ---------------------------------------------------------------- input */
+static int alive_index(int k) { /* the k-th monster still there */
+ for (int i = 0; i < bt.nmon; i++)
+ if (bt.mon[i].alive && !bt.mon[i].gone && k-- == 0) return i;
+ return -1;
+}
+
+static void menu_input(uint32_t pressed) {
+ switch (bt.phase) {
+ case P_MENU:
+ if (pressed & K_LEFT) bt.button = (bt.button + 3) & 3;
+ if (pressed & K_RIGHT) bt.button = (bt.button + 1) & 3;
+ if (bt.no_mercy && bt.button == 3) bt.button = pressed & K_LEFT ? 2 : 0;
+ if (pressed & K_Z) {
+ bt.target = 0, bt.act = 0, bt.item = 0, bt.mercy = 0, bt.page = 0;
+ if (bt.button == B_FIGHT || bt.button == B_ACT) bt.phase = P_TARGET;
+ else if (bt.button == B_ITEM) {
+ if (item_count()) bt.phase = P_ITEMS;
+ } else bt.phase = P_MERCY;
+ }
+ break;
+ case P_TARGET: {
+ int n = count_alive();
+ if (pressed & K_UP) bt.target = (bt.target + n - 1) % n;
+ if (pressed & K_DOWN) bt.target = (bt.target + 1) % n;
+ if (pressed & K_X) bt.phase = P_MENU;
+ if (pressed & K_Z) {
+ if (bt.button == B_FIGHT) {
+ bt.last_action = 0;
+ bt.target = alive_index(bt.target);
+ bt.phase = P_BAR;
+ bt.bar_dir = (sv.weapon == ITEM_TOY_KNIFE && rnd_n(2)) ? -1 : 1;
+ bt.bar_x = bt.bar_dir > 0 ? 0 : 281 * FX;
+ bt.t = 0;
+ } else {
+ bt.phase = P_ACTS;
+ bt.act = 0;
+ }
+ }
+ break;
+ }
+ case P_ACTS: {
+ int m = alive_index(bt.target);
+ int n = 1 + bt.mon[m].def->nacts;
+ if (pressed & K_LEFT && bt.act % 2) bt.act--;
+ if (pressed & K_RIGHT && !(bt.act % 2) && bt.act + 1 < n) bt.act++;
+ if (pressed & K_UP && bt.act >= 2) bt.act -= 2;
+ if (pressed & K_DOWN && bt.act + 2 < n) bt.act += 2;
+ if (pressed & K_X) bt.phase = P_TARGET;
+ if (pressed & K_Z) {
+ Monster *mo = &bt.mon[m];
+ bt.target = m;
+ bt.last_action = 1;
+ if (bt.act == 0) {
+ snprintf(bt.text, sizeof bt.text, "* %s - ATK %d DEF %d\n* %s", mo->def->name, mo->at, mo->df, mo->def->check);
+ for (char *p = bt.text + 2; *p && *p != ' '; p++)
+ if (*p >= 'a' && *p <= 'z') *p = (char)(*p - 32);
+ } else {
+ bt.text[0] = 0;
+ mo->acts[bt.act - 1]++;
+ if (mo->def->act) mo->def->act(&bt, mo, bt.act - 1, bt.text, sizeof bt.text);
+ }
+ say(bt.text);
+ bt.phase = P_RESULT;
+ }
+ break;
+ }
+ case P_ITEMS: {
+ int n = item_count();
+ if (pressed & K_LEFT) bt.item = bt.item % 2 ? bt.item - 1 : (bt.item >= 4 ? bt.item - 3 : bt.item);
+ if (pressed & K_RIGHT) bt.item = !(bt.item % 2) && bt.item + 1 < n ? bt.item + 1 : (bt.item % 2 && bt.item + 3 < n ? bt.item + 3 : bt.item);
+ if (pressed & K_UP && bt.item % 4 >= 2) bt.item -= 2;
+ if (pressed & K_DOWN && bt.item % 4 < 2 && bt.item + 2 < n) bt.item += 2;
+ if (bt.item >= n) bt.item = n - 1;
+ if (pressed & K_X) bt.phase = P_MENU;
+ if (pressed & K_Z) {
+ bt.last_action = 2;
+ /* a monster may answer the item (a dog and the Stick) */
+ bool answered = false;
+ for (int i = 0; i < bt.nmon && !answered; i++) {
+ Monster *m = &bt.mon[i];
+ if (m->alive && !m->gone && m->def->on_item)
+ answered = m->def->on_item(&bt, m, sv.items[bt.item], bt.text, sizeof bt.text);
+ }
+ if (!answered) item_use(bt.item, bt.text, sizeof bt.text, 1);
+ say(bt.text);
+ bt.phase = P_RESULT;
+ }
+ break;
+ }
+ case P_MERCY:
+ if (pressed & K_UP) bt.mercy = 0;
+ if (pressed & K_DOWN && !bt.no_flee) bt.mercy = 1;
+ if (pressed & K_X) bt.phase = P_MENU;
+ if (pressed & K_Z) {
+ if (bt.mercy == 0) do_spare();
+ else do_flee();
+ }
+ break;
+ }
+}
+
+/* ---------------------------------------------------------------- the frame */
+void battle_update(uint32_t keys, uint32_t pressed) {
+ if (over) return;
+ bt.t++;
+ box_step();
+ if (bt.scripted && bt.phase != P_INTRO) {
+ script->update(keys, pressed);
+ if (bt.result == BR_SCRIPTED) battle_end(BR_SCRIPTED);
+ return;
+ }
+ /* (a monster's ways may end it at once: Mettaton, his switch flipped or his show a hit) */
+ if (bt.result == BR_SCRIPTED) {
+ battle_end(BR_SCRIPTED);
+ return;
+ }
+ for (int i = 0; i < bt.nmon; i++) {
+ Monster *m = &bt.mon[i];
+ if (m->hurt) m->hurt--;
+ if (m->dmg_t) m->dmg_t--;
+ if (m->fade && m->fade < 30) m->fade++;
+ if (m->dust && m->dust < 60) m->dust++;
+ }
+ if (bt.inv) bt.inv--;
+ switch (bt.phase) {
+ case P_INTRO:
+ if (bt.t >= 24) {
+ bt.phase = P_MENU;
+ say(bt.text);
+ bt.turn = 1;
+ }
+ break;
+ case P_MENU:
+ writer_update(&bt.w, pressed & K_X ? K_X : 0);
+ menu_input(pressed);
+ break;
+ case P_TARGET:
+ case P_ACTS:
+ case P_ITEMS:
+ case P_MERCY:
+ menu_input(pressed);
+ break;
+ case P_BAR:
+ bt.bar_x += bt.bar_dir * 7 * FX / 2;
+ if (pressed & K_Z) strike();
+ else if (bt.bar_x < 0 || bt.bar_x > 281 * FX) {
+ bt.bar_x = -1;
+ bt.strike_dmg = -1;
+ bt.target = bt.target;
+ bt.mon[bt.target].dmg_shown = -1, bt.mon[bt.target].dmg_t = 40;
+ if ((bt.mon[bt.target].def->flags & MF_MISS_IS_SPARE) && bt.mon[bt.target].def->on_spare)
+ bt.mon[bt.target].def->on_spare(&bt, &bt.mon[bt.target]);
+ bt.phase = P_STRIKE, bt.t = 20;
+ }
+ break;
+ case P_STRIKE:
+ if (bt.t == 18) strike_land();
+ if (bt.t >= 60) {
+ Monster *m = &bt.mon[bt.target];
+ if (m->killed && m->alive) {
+ m->alive = false, m->gone = true, m->dust = 1;
+ sv.kills++;
+ {
+ const RoomInfo *ri = world_room_info();
+ if (ri && (ri->area == AREA_SNOWDIN || ri->area == AREA_SNOWTOWN)) set_flag(F_KILLS_SNOWDIN, flag(F_KILLS_SNOWDIN) + 1);
+ }
+ }
+ after_player_turn();
+ }
+ break;
+ case P_RESULT:
+ if (!writer_update(&bt.w, pressed)) {
+ if (bt.phase == P_RESULT) after_player_turn();
+ }
+ if (bt.w.finished && bt.phase == P_RESULT) after_player_turn();
+ break;
+ case P_TALK:
+ if (bt.talk_i < 0) {
+ begin_attack();
+ break;
+ }
+ if (!writer_update(&bt.bw, pressed)) {
+ /* the next monster with something to say */
+ int i = bt.talk_i + 1;
+ while (i < bt.nmon && !bt.mon[i].say) i++;
+ if (i < bt.nmon) {
+ bt.talk_i = i;
+ writer_start(&bt.bw, bt.mon[i].say, 0, 0, FONT_SMALL);
+ bt.bw.line_h = 9;
+ } else {
+ /* some monsters leave after they have spoken (Napstablook) */
+ for (int k = 0; k < bt.nmon; k++) {
+ Monster *m = &bt.mon[k];
+ if (m->alive && !m->gone && m->def->ends && m->def->ends(&bt, m)) m->gone = true, m->fade = 1;
+ }
+ if (all_gone()) win();
+ else begin_attack();
+ }
+ }
+ break;
+ case P_BOX_IN:
+ if (box_settled()) {
+ bt.sx = board_cx() * FX, bt.sy = board_cy() * FX;
+ bt.phase = P_ATTACK;
+ }
+ break;
+ case P_ATTACK:
+ soul_update(keys);
+ attack_run(&bt, bt.atk, bt.atk_t);
+ bullets_update();
+ soul_hit_test();
+ if (sv.hp <= 0 && bt.captures) {
+ sv.hp = 1;
+ battle_end(BR_SCRIPTED);
+ break;
+ }
+ if (sv.hp <= 0) {
+ bt.phase = P_DEATH, bt.t = 0;
+ break;
+ }
+ if (++bt.atk_t >= bt.atk_len) {
+ bullets_clear();
+ bt.phase = P_BOX_OUT;
+ box_menu();
+ }
+ break;
+ case P_BOX_OUT:
+ if (box_settled()) begin_menu();
+ break;
+ case P_WON:
+ if (!writer_update(&bt.w, pressed)) battle_end(bt.result == BR_SPARED ? BR_SPARED : BR_WON);
+ break;
+ case P_FLEE:
+ writer_update(&bt.w, 0);
+ if (bt.t >= 40) battle_end(BR_FLED);
+ break;
+ case P_DEATH:
+ if (bt.t >= 70) battle_end(BR_DIED);
+ break;
+ case P_GLARE:
+ if (bt.t == 40) bt.mon[0].gone = true, bt.mon[0].fade = 1;
+ if (bt.t >= 80) win();
+ break;
+ }
+}
+
+/* ---------------------------------------------------------------- drawing */
+static void draw_monster(Monster *m, int i) {
+ const MonDef *d = m->def;
+ if (m->gone && !m->dust && m->fade >= 30) return;
+ if (!m->alive && !m->dust) return;
+ int x = m->x + (m->hurt ? (m->hurt / 2 % 2 ? 2 : -2) * (m->hurt > 10) : 0), y = m->y;
+ if (d->draw) {
+ d->draw(&bt, m, x, y);
+ return;
+ }
+ uint16_t s = m->spr_override ? (uint16_t)m->spr_override : m->hurt && d->spr_hurt ? d->spr_hurt : d->spr_idle;
+ int fr = m->hurt ? 0 : (int)(g_frame / (d->idle_speed ? d->idle_speed : 6));
+ if (m->dust) {
+ /* turning to dust: drawn whiter and thinner, from the top down */
+ int a = 255 - m->dust * 4;
+ if (a > 0) gfx_sprite_color(s, 0, x, y, 0, C_WHITE, (uint8_t)a);
+ return;
+ }
+ uint8_t alpha = m->fade ? (uint8_t)max_i(0, 128 - m->fade * 4) : 255;
+ gfx_sprite(s, fr, x, y, 0, alpha);
+ (void)i;
+}
+
+void battle_draw_hud(void) {
+ char s[40];
+ gfx_text(FONT_SMALL, 15, 201, sv.name, C_WHITE, 255);
+ snprintf(s, sizeof s, "LV %d", sv.lv);
+ gfx_text(FONT_SMALL, 66, 201, s, C_WHITE, 255);
+ gfx_sprite(SPR_HPNAME, 0, 122, 202, 0, 255);
+ int w = sv.maxhp * 6 / 10 + 1, f = max_i(0, sv.hp) * 6 / 10;
+ if (sv.hp > 0 && f == 0) f = 1;
+ gfx_rect(137, 200, w, 10, rgb565(0xC00000), 255);
+ gfx_rect(137, 200, f, 10, C_YELLOW, 255);
+ snprintf(s, sizeof s, "%d / %d", max_i(0, sv.hp), sv.maxhp);
+ gfx_text(FONT_SMALL, 137 + w + 8, 201, s, C_WHITE, 255);
+}
+
+void battle_draw_buttons(void) {
+ static const uint16_t spr[4] = {SPR_FIGHTBT, SPR_TALKBT, SPR_ITEMBT, SPR_SPAREBT};
+ bool menu = bt.phase >= P_MENU && bt.phase <= P_MERCY;
+ for (int i = 0; i < 4; i++) {
+ if (i == 3 && bt.no_mercy) { /* (its pieces, where it was) */
+ for (int k = 0; k < 4; k++) gfx_rect(button_x[3] + 6 + k * 14, BUTTON_Y + 14 + (k % 2) * 6, 6, 3, rgb565(0xFF8020), 160);
+ continue;
+ }
+ bool on = menu && bt.button == i;
+ gfx_sprite(spr[i], on ? 1 : 0, button_x[i], BUTTON_Y, 0, 255);
+ if (on && bt.phase == P_MENU) gfx_sprite(SPR_HEART, 0, button_x[i] + 8, BUTTON_Y + 11, 0, 255);
+ }
+}
+
+/* "* Name" lines in the box, two columns or one */
+static void draw_list(const char *const *names, int n, int sel, bool two_cols, const uint16_t *colors) {
+ for (int i = 0; i < n; i++) {
+ int col = two_cols ? i % 2 : 0, row = two_cols ? i / 2 : i;
+ int x = 50 + col * 128, y = 133 + row * 16;
+ char s[48];
+ snprintf(s, sizeof s, "* %s", names[i]);
+ gfx_text(FONT_MAIN, x, y, s, colors ? colors[i] : C_WHITE, 255);
+ if (i == sel) gfx_sprite(SPR_HEART, 0, x - 14, y + 7, 0, 255);
+ }
+}
+
+static void draw_box_contents(void) {
+ switch (bt.phase) {
+ case P_MENU:
+ case P_RESULT:
+ case P_WON:
+ case P_FLEE:
+ writer_draw(&bt.w, C_WHITE);
+ break;
+ case P_TARGET: {
+ const char *names[MAX_MON];
+ uint16_t cols[MAX_MON];
+ int n = 0;
+ for (int i = 0; i < bt.nmon; i++)
+ if (bt.mon[i].alive && !bt.mon[i].gone) {
+ names[n] = bt.mon[i].def->name;
+ cols[n] = bt.mon[i].spare && flag(F_FROGGIT_YELLOW) != 1 ? (flag(F_FROGGIT_YELLOW) == 2 ? rgb565(0xFF80C0) : C_YELLOW) : C_WHITE;
+ if (bt.button == B_FIGHT) {
+ /* the HP bar, in the game's green over red */
+ int y = 133 + n * 16 + 3, hp = bt.mon[i].hp * 50 / max_i(1, bt.mon[i].maxhp);
+ gfx_rect(200, y, 50, 8, rgb565(0xC00000), 255);
+ gfx_rect(200, y, hp, 8, rgb565(0x00FF00), 255);
+ }
+ n++;
+ }
+ draw_list(names, n, bt.target, false, cols);
+ break;
+ }
+ case P_ACTS: {
+ Monster *m = &bt.mon[alive_index(bt.target)];
+ const char *names[6] = {"Check"};
+ int n = 1;
+ for (int i = 0; i < m->def->nacts; i++) names[n++] = m->def->acts[i];
+ draw_list(names, n, bt.act, true, NULL);
+ break;
+ }
+ case P_ITEMS: {
+ const char *names[4];
+ int page = bt.item / 4, n = 0;
+ for (int i = page * 4; i < page * 4 + 4 && i < item_count(); i++) names[n++] = item_info(sv.items[i])->short_name;
+ draw_list(names, n, bt.item % 4, true, NULL);
+ char s[16];
+ snprintf(s, sizeof s, "PAGE %d", page + 1);
+ gfx_text(FONT_MAIN, 236, 165, s, C_WHITE, 255);
+ break;
+ }
+ case P_MERCY: {
+ const char *names[2] = {"Spare", "Flee"};
+ bool any = false;
+ for (int i = 0; i < bt.nmon; i++) any |= bt.mon[i].alive && !bt.mon[i].gone && bt.mon[i].spare;
+ uint16_t cols[2] = {any && flag(F_FROGGIT_YELLOW) != 1 ? (flag(F_FROGGIT_YELLOW) == 2 ? rgb565(0xFF80C0) : C_YELLOW) : C_WHITE, C_WHITE};
+ draw_list(names, bt.no_flee ? 1 : 2, bt.mercy, false, cols);
+ break;
+ }
+ case P_BAR:
+ case P_STRIKE:
+ gfx_sprite(SPR_TARGET, 0, 19, 128, 0, 255);
+ if (bt.phase == P_BAR || (bt.t / 2) % 2) {
+ int bx = 19 + bt.bar_x / FX - 3;
+ if (bt.bar_x >= 0) gfx_sprite(SPR_TARGETCHOICE, bt.phase == P_STRIKE ? (bt.t / 3) % 2 : 0, bx, 128, 0, 255);
+ }
+ break;
+ default:
+ break;
+ }
+}
+
+static void draw_bubble(void) {
+ if (bt.phase != P_TALK || bt.talk_i < 0) return;
+ Monster *m = &bt.mon[bt.talk_i];
+ int bx = m->x + 30, by = m->y - 70;
+ if (bx + 104 > 316) bx = 316 - 104;
+ int w = 100, h = 44;
+ gfx_rect(bx, by, w, h, C_WHITE, 255);
+ gfx_rect(bx - 6, by + 18, 6, 4, C_WHITE, 255);
+ bt.bw.x = bx + 6, bt.bw.y = by + 5;
+ writer_draw(&bt.bw, C_BLACK);
+}
+
+static void draw_damage(void) {
+ for (int i = 0; i < bt.nmon; i++) {
+ Monster *m = &bt.mon[i];
+ if (!m->dmg_t) continue;
+ int y = m->y - 70 - (m->dmg_t > 35 ? (45 - m->dmg_t) : 10);
+ char s[16];
+ if (m->dmg_shown <= 0) {
+ gfx_text(FONT_MAIN, m->x - 16, y, "MISS", rgb565(0xC0C0C0), 255);
+ } else {
+ snprintf(s, sizeof s, "%d", m->dmg_shown);
+ gfx_text(FONT_MAIN, m->x - 4 * (int)strlen(s), y, s, C_RED, 255);
+ /* the HP bar, going down */
+ int w = 60, shown = m->bar_shown - (int)((m->bar_shown - max_i(0, m->hp)) * min_i(45 - m->dmg_t, 15) / 15);
+ gfx_rect(m->x - w / 2, y + 14, w, 7, rgb565(0x404040), 255);
+ gfx_rect(m->x - w / 2, y + 14, shown * w / max_i(1, m->maxhp), 7, rgb565(0x00FF00), 255);
+ }
+ }
+}
+
+void battle_draw(void) {
+ if (bt.phase == P_INTRO) {
+ /* the SOUL flickers where Frisk was, then flies to FIGHT */
+ if (bt.t < 12) {
+ if ((bt.t / 2) % 2 == 0) gfx_sprite(SPR_HEART, 0, heart_x, heart_y, 0, 255);
+ return;
+ }
+ int k = bt.t - 12;
+ int tx = bt.scripted ? soul_x() : button_x[0] + 8, ty = bt.scripted ? soul_y() : BUTTON_Y + 11;
+ int x = heart_x + (tx - heart_x) * k / 12, y = heart_y + (ty - heart_y) * k / 12;
+ gfx_sprite(SPR_HEART, 0, x, y, 0, 255);
+ return;
+ }
+ if (bt.phase == P_DEATH) {
+ int x = soul_x(), y = soul_y();
+ if (bt.t < 25) gfx_sprite(SPR_HEART, 0, x, y, 0, 255);
+ else if (bt.t < 45) gfx_sprite(SPR_HEARTBREAK, 0, x, y, 0, 255);
+ else
+ for (int k = 0; k < 6; k++) {
+ int dt = bt.t - 45;
+ gfx_sprite(SPR_HEARTSHARDS, (dt / 4) % 4, x + (k - 3) * dt / 2, y - 10 + dt * dt / 30 - (k % 3) * 6, 0, 255);
+ }
+ return;
+ }
+ if (bt.scripted) {
+ script->draw();
+ return;
+ }
+ gfx_sprite(SPR_BATTLEBG, 0, 0, 0, 0, 255);
+ for (int i = 0; i < bt.nmon; i++) draw_monster(&bt.mon[i], i);
+ if (bt.phase == P_GLARE || (bt.form == FORM_FROGGIT_FIRST && bt.phase == P_WON))
+ gfx_sprite(SPR_TORIEL_B, 0, 330 - min_i(bt.phase == P_GLARE ? bt.t : 30, 30) * 2, 120, 0, 255);
+ /* the box */
+ gfx_rect(bt.x0, bt.y0, bt.x1 - bt.x0, bt.y1 - bt.y0, C_WHITE, 255);
+ gfx_rect(bt.x0 + 2, bt.y0 + 2, bt.x1 - bt.x0 - 4, bt.y1 - bt.y0 - 4, C_BLACK, 255);
+ gfx_clip(bt.x0 + 2, bt.y0 + 2, bt.x1 - bt.x0 - 4, bt.y1 - bt.y0 - 4);
+ draw_box_contents();
+ if (bt.phase == P_ATTACK) {
+ if (bt.soul_mode == SOUL_PURPLE)
+ for (int i = 0; i < NSTRINGS; i++) gfx_rect(bt.x0 + 2, soul_string_y(i), bt.x1 - bt.x0 - 4, 1, rgb565(0xD535D9), 255);
+ attack_draw(&bt, bt.atk, bt.atk_t);
+ bullets_draw();
+ if (!bt.inv || (bt.inv / 2) % 2) gfx_sprite(SPR_HEART, bt.soul_mode, soul_x(), soul_y(), 0, 255);
+ if (bt.soul_mode == SOUL_GREEN) {
+ /* its ring, and the shield on the side it faces */
+ int cx = soul_x(), cy = soul_y();
+ for (int a = 0; a < 256; a += 8)
+ gfx_rect(cx + 13 * gfx_cos(a) / 1024, cy + 13 * gfx_sin(a) / 1024, 1, 1, rgb565(0x00C000), 255);
+ static const int8_t ox[4] = {0, 0, -11, 11}, oy[4] = {11, -11, 0, 0};
+ int x = cx + ox[bt.shield], y = cy + oy[bt.shield];
+ if (bt.shield < 2) gfx_rect(x - 7, y - 1, 15, 3, rgb565(0x3070FF), 255);
+ else gfx_rect(x - 1, y - 7, 3, 15, rgb565(0x3070FF), 255);
+ }
+ }
+ gfx_clip_all();
+ if (bt.phase == P_FLEE) gfx_sprite(SPR_HEARTGTFO, (bt.t / 3) % 2, button_x[3] + 8 - bt.t * 3, BUTTON_Y + 11, 0, 255);
+ if (bt.phase == P_STRIKE && bt.t < 18 && bt.strike_dmg >= 0) {
+ Monster *m = &bt.mon[bt.target];
+ gfx_sprite(SPR_STRIKE, bt.t / 3, m->x, m->y - 30, 0, 255);
+ }
+ draw_damage();
+ draw_bubble();
+ battle_draw_hud();
+ battle_draw_buttons();
+}
diff --git a/games/undertale/src/battle.h b/games/undertale/src/battle.h
new file mode 100644
index 00000000..d6c4047d
--- /dev/null
+++ b/games/undertale/src/battle.h
@@ -0,0 +1,208 @@
+/* Undertale: battles.
+ *
+ * The original's battle is 640 x 480; here it is halved, as everything else:
+ * the box, the buttons and the bullets at half their size, the monsters at the
+ * size the game draws them (it doubles its sprites). Positions on the bullet
+ * board are kept in 1/16 pixels, so bullets move as finely as there.
+ *
+ * A battle is a formation (FORM_*: who is fought, where, its music none) of
+ * up to three monsters (MonDef: stats, texts, ACTs, attacks). A turn: the
+ * player picks FIGHT, ACT, ITEM or MERCY; the monsters talk in their bubbles;
+ * the box shrinks to the bullet board and the monsters attack the SOUL. */
+#ifndef UT_BATTLE_H
+#define UT_BATTLE_H
+#include
+#include
+#include "text.h"
+
+enum { BR_WON, BR_SPARED, BR_FLED, BR_DIED, BR_SCRIPTED };
+
+#define FX 16 /* fixed point: 1/16 pixel */
+#define MAX_MON 3
+#define MAX_BULLETS 96
+
+/* the box: the menu's, wide; the board's for attacks, by default */
+#define BOX_X0 16
+#define BOX_Y0 125
+#define BOX_X1 303
+#define BOX_Y1 195
+#define BOARD_W 77
+#define BOARD_H 65
+
+struct Battle;
+struct Monster;
+
+typedef struct MonDef {
+ const char *name;
+ int16_t hp, at, df, exp, gold;
+ int16_t spare_gold; /* the gold it gives when spared */
+ uint16_t spr_idle, spr_hurt; /* SPR_*: idle animation (its frames), hurt */
+ uint8_t idle_speed; /* frames between idle frames */
+ int8_t dx, dy; /* where it stands, from its slot */
+ const char *check; /* after "* NAME - ATK n DEF n\n" */
+ const char *acts[5]; /* besides Check */
+ uint8_t nacts;
+ const char *encounter; /* the first turn's text, alone */
+ const char *neutral[6]; /* flavour texts, at random */
+ const char *quotes[6]; /* what it says, at random */
+ const char *low_hp, *spareable; /* flavour texts then */
+ uint8_t flags; /* MF_* */
+ /* its own ways: an ACT (fills text, may set m->spare), its talk this turn, its attack this turn */
+ void (*act)(struct Battle *b, struct Monster *m, int act, char *text, int cap);
+ const char *(*talk)(struct Battle *b, struct Monster *m);
+ int (*attack)(struct Battle *b, struct Monster *m);
+ const char *(*flavor)(struct Battle *b, struct Monster *m);
+ bool (*on_hit)(struct Battle *b, struct Monster *m, int dmg); /* true: its own way (no death) */
+ void (*draw)(struct Battle *b, struct Monster *m, int x, int y);
+ bool (*ends)(struct Battle *b, struct Monster *m); /* after its talk: true, it leaves the battle */
+ void (*on_spare)(struct Battle *b, struct Monster *m); /* spared while it isn't spareable (Toriel counts) */
+ /* an item used: true if it answers (the dogs and the Stick), its text in text */
+ bool (*on_item)(struct Battle *b, struct Monster *m, int item, char *text, int cap);
+} MonDef;
+enum {
+ MF_ONE_HIT = 1, /* dies in one hit (Whimsun) */
+ MF_BOSS = 2, /* no fleeing */
+ MF_NO_CHECK = 4,
+ MF_SPARE_START = 8, /* spareable from the start */
+ MF_NOT_KILLABLE = 16,
+ MF_MISS_IS_SPARE = 32 /* a missed FIGHT counts as a spare (Toriel) */
+};
+
+typedef struct Monster {
+ const MonDef *def;
+ int16_t hp, maxhp, at, df, exp, gold;
+ int16_t x, y; /* its feet's middle */
+ bool alive, gone, spare; /* gone: killed, spared or ran */
+ bool killed;
+ uint8_t fade, dust; /* fading out (spared or ran) / turning to dust: frames */
+ uint8_t hurt; /* frames showing hurt */
+ int16_t shake; /* hurt: its shake */
+ int16_t acts[6]; /* each ACT, how often */
+ int16_t state, state2, turns;
+ const char *say; /* its bubble this turn */
+ int16_t dmg_shown, dmg_t; /* the damage number over it */
+ int16_t bar_shown; /* its HP before the hit (the bar's fall) */
+ int16_t spare_gold; /* the gold it gives when spared */
+ int16_t spr_override; /* a sprite its ways show instead, 0 none */
+} Monster;
+
+/* the bullets: a pool, moved by kind each frame; the attack's code makes them and steers them */
+enum { BK_NONE, BK_LINEAR, BK_GRAVITY, BK_HOMING, BK_ORBIT, BK_SCRIPT };
+enum { BC_WHITE, BC_BLUE, BC_ORANGE, BC_GREEN, BC_YELLOW };
+typedef struct {
+ uint8_t kind, color, frame, flags;
+ uint16_t spr;
+ int32_t x, y, vx, vy, ax, ay; /* 1/16 pixels (and per frame, per frame squared) */
+ int16_t r; /* its hit radius (pixels), a box if w/h */
+ int16_t w, h;
+ int16_t t, life; /* frames alive; dies at life (0: never) */
+ int16_t a, b, c, d; /* the attack's own */
+ uint8_t anim, alpha;
+ int8_t dmg; /* 0: the monster's AT */
+ int16_t angle; /* drawn turned (1/256 turns) */
+ uint8_t owner; /* the monster that made it */
+} Bullet;
+#define SPR_RECT 0xFFFF /* a bullet drawn as its box: a plain rectangle (white unless coloured) */
+enum { BF_NOCLIP = 1, BF_ONCE = 2, BF_HIDDEN = 4, BF_BOX = 8, BF_FRONT = 16, BF_HARMLESS = 32, BF_SHIELD = 64,
+ BF_PLAYER = 128 }; /* (shield: the green SOUL's blocks it; player: the yellow SOUL's own shot) */
+
+typedef struct Battle {
+ int form;
+ int phase, t;
+ Monster mon[MAX_MON];
+ int nmon;
+ int greens; /* green (healing) bullets the SOUL caught this battle */
+ int button, target, act, item, mercy, page;
+ Writer w; /* the box's text */
+ char text[600];
+ /* the box now, and where it is going */
+ int16_t x0, y0, x1, y1, tx0, ty0, tx1, ty1;
+ /* the SOUL */
+ int32_t sx, sy; /* its middle, 1/16 pixels */
+ int soul_mode;
+ int shield; /* green: the way its shield faces (DIR_*: down, up, left, right) */
+ int inv; /* invincible for frames */
+ int speed; /* 1/16 pixels a frame */
+ /* the attack */
+ int atk, atk_t, atk_len;
+ int atk_owner;
+ int turn;
+ int bar_x, bar_dir; /* FIGHT: the bar, 1/16 px */
+ int strike_t, strike_dmg;
+ bool flee_ok, scripted, no_flee, no_buttons;
+ bool no_mercy; /* MERCY broken (Asgore's): only FIGHT, ACT and ITEM */
+ bool captures; /* no death: at 0 HP the battle ends, HP 1 (Papyrus captures you) */
+ int result;
+ int exp_won, gold_won;
+ int kr; /* (for later) */
+ int df_loss; /* DEFENSE lost this battle (Vulkin's hugs) */
+ int16_t state[8]; /* the formation's own */
+ int32_t as[16]; /* the attack's own */
+ int talk_i; /* whose bubble shows */
+ Writer bw; /* the bubble's text */
+ int hp_shown;
+ int last_action; /* 0 FIGHT, 1 ACT, 2 ITEM, 3 Spare, 4 Flee */
+} Battle;
+extern Battle bt;
+extern Bullet bullets[MAX_BULLETS];
+
+void battle_start(int form);
+bool battle_over(void);
+int battle_result(void);
+/* what the battle waits for (for the tests): a button, a target, an ACT, an item, MERCY's choice or nothing */
+enum { BUI_NONE, BUI_MENU, BUI_TARGET, BUI_ACTS, BUI_ITEMS, BUI_MERCY, BUI_BAR };
+int battle_ui(void);
+void battle_update(uint32_t keys, uint32_t pressed);
+void battle_draw(void);
+
+/* for the attacks (bullets.c) */
+Bullet *bullet_new(uint16_t spr, int x, int y, int vx, int vy); /* x, y in 1/16 px */
+void bullets_clear(void);
+void bullets_update(void);
+void bullets_draw(void);
+bool soul_hit_test(void); /* bullets touching the SOUL hurt it */
+void soul_update(uint32_t keys);
+/* (the heart's frame too): green stays put, its shield turns; purple keeps to three strings across the board
+ * (up and down hop between them); yellow shoots up (Z) */
+enum { SOUL_RED, SOUL_BLUE, SOUL_GREEN, SOUL_PURPLE, SOUL_YELLOW };
+#define NSTRINGS 3
+int soul_string_y(int i); /* the purple SOUL's strings: where string i is now (pixels) */
+extern int32_t soul_strings_shift; /* how far they've been pulled down (1/16 px; they come round from the top) */
+void soul_set_mode(int mode); /* blue: it falls, up jumps */
+void gfx_bone(int cx, int cy, int w, int h, uint16_t col, uint8_t a);
+void soul_hurt(int dmg, int color);
+void board_set(int w, int h); /* the board, centred, as the game resizes it */
+void board_set_rect(int x0, int y0, int x1, int y1);
+int board_cx(void), board_cy(void); /* its middle (pixels) */
+int soul_x(void), soul_y(void); /* (pixels) */
+int damage_to_player(int at);
+int count_alive(void);
+/* for the battles that play their own way (their draw) */
+void battle_draw_hud(void);
+void battle_draw_buttons(void);
+/* those battles (flowey.c, htshows.c): set up, each frame (bt.result = BR_SCRIPTED ends it), drawn */
+void flowey_battle_start(void);
+void flowey_battle_update(uint32_t keys, uint32_t pressed);
+void flowey_battle_draw(void);
+void quiz_start(void);
+void quiz_update(uint32_t keys, uint32_t pressed);
+void quiz_draw(void);
+void cook_start(void);
+void cook_update(uint32_t keys, uint32_t pressed);
+void cook_draw(void);
+int cook_won(void); /* the substitute reached in time */
+void lasers_start(void);
+void lasers_update(uint32_t keys, uint32_t pressed);
+void lasers_draw(void);
+void omega_start(void); /* nhmon.c: Omega Flowey */
+void omega_update(uint32_t keys, uint32_t pressed);
+void omega_draw(void);
+
+/* the monsters and formations (monsters.c) */
+const MonDef *mon_def(int id);
+int form_setup(int form, Battle *b); /* places the monsters; returns the encounter text's id */
+const char *form_text(int form);
+void attack_run(Battle *b, int atk, int t); /* one frame of attack atk (ATK_*) */
+void attack_draw(Battle *b, int atk, int t); /* what it draws besides its bullets */
+int attack_length(int atk);
+#endif
diff --git a/games/undertale/src/blk.c b/games/undertale/src/blk.c
new file mode 100644
index 00000000..23b74619
--- /dev/null
+++ b/games/undertale/src/blk.c
@@ -0,0 +1,107 @@
+/* data.bin's LZMA blocks and the RAM they're unpacked into (blk.h). */
+#include
+#include "blk.h"
+#include "lzma/LzmaDec.h"
+#include "res.h"
+
+#define ARENA (60 * 1024) /* the cache: a screen of tiles is 20 KB, rarely 55; a battle's monsters 40 */
+#define MAX_IN 128 /* blocks in it at once */
+#define MAX_BLOCKS 4096 /* (tools/pack.py's) */
+#define NONE 0xFF
+
+static uint8_t arena[ARENA] __attribute__((aligned(4)));
+typedef struct {
+ uint16_t id, used; /* the block, the frame it was last used in */
+ uint32_t off, size; /* where it is in the arena */
+} In;
+static In in[MAX_IN]; /* by where they are */
+static int nin;
+static uint8_t where[MAX_BLOCKS]; /* each block's place in `in`, or NONE */
+static uint16_t frame_no = 1;
+static bool ready;
+uint32_t blk_unpacked, blk_most_in_frame;
+static uint32_t unpacked_now;
+
+/* the decoder's tables, in one piece (LzmaDecode asks once a block) */
+static uint16_t probs[2800];
+static void *probs_alloc(ISzAllocPtr p, size_t size) {
+ (void)p;
+ return size <= sizeof probs ? probs : NULL;
+}
+static void probs_free(ISzAllocPtr p, void *a) { (void)p, (void)a; }
+static const ISzAlloc alloc = {probs_alloc, probs_free};
+
+static void init(void) {
+ memset(where, NONE, sizeof where);
+ nin = 0, ready = true;
+}
+
+static void drop(int k) {
+ where[in[k].id] = NONE;
+ memmove(&in[k], &in[k + 1], (size_t)(nin - k - 1) * sizeof in[0]);
+ nin--;
+ for (int i = k; i < nin; i++) where[in[i].id] = (uint8_t)i;
+}
+
+/* room for n bytes: its place in `in` (the new entry, made), or -1 */
+static int make_room(uint32_t n) {
+ for (;;) {
+ if (nin < MAX_IN) {
+ uint32_t at = 0;
+ for (int i = 0; i <= nin; i++) {
+ uint32_t end = i < nin ? in[i].off : ARENA;
+ if (end - at >= n) {
+ memmove(&in[i + 1], &in[i], (size_t)(nin - i) * sizeof in[0]);
+ nin++;
+ for (int j = i + 1; j < nin; j++) where[in[j].id] = (uint8_t)j;
+ in[i].off = at, in[i].size = n;
+ return i;
+ }
+ if (i < nin) at = in[i].off + in[i].size;
+ }
+ }
+ if (!nin) return -1;
+ /* throw out the least recently used (those of this frame last) */
+ int old = 0;
+ for (int i = 1; i < nin; i++)
+ if ((uint16_t)(frame_no - in[i].used) > (uint16_t)(frame_no - in[old].used)) old = i;
+ drop(old);
+ }
+}
+
+const uint8_t *blk_get(int id) {
+ if (!ready) init();
+ const uint8_t *s = section(SEC_BLOCKS);
+ if (id < 0 || id >= (int)rd32(s)) return NULL;
+ if (where[id] != NONE) {
+ In *b = &in[where[id]];
+ b->used = frame_no;
+ return arena + b->off;
+ }
+ const uint8_t *e = s + 12 + 8 * id;
+ uint32_t off = rd32(e), size = rd32(e + 4);
+ uint32_t next = id + 1 < (int)rd32(s) ? rd32(e + 8) : 0;
+ int k = make_room(size);
+ if (k < 0) return NULL;
+ In *b = &in[k];
+ b->id = (uint16_t)id, b->used = frame_no;
+ where[id] = (uint8_t)k;
+ SizeT out = size, src = next ? next - off : 1u << 30;
+ ELzmaStatus st;
+ LzmaDecode(arena + b->off, &out, s + off, &src, s + 4, LZMA_PROPS_SIZE, LZMA_FINISH_ANY, &st, &alloc);
+ blk_unpacked++, unpacked_now++;
+ return arena + b->off;
+}
+
+void blk_frame(void) {
+ if (!ready) init();
+ /* the blocks packed together, so the free room is in one piece */
+ uint32_t at = 0;
+ for (int i = 0; i < nin; i++) {
+ if (in[i].off != at) memmove(arena + at, arena + in[i].off, in[i].size), in[i].off = at;
+ at += in[i].size;
+ }
+ if (unpacked_now > blk_most_in_frame) blk_most_in_frame = unpacked_now;
+ unpacked_now = 0;
+ frame_no++;
+}
diff --git a/games/undertale/src/blk.h b/games/undertale/src/blk.h
new file mode 100644
index 00000000..65b495d5
--- /dev/null
+++ b/games/undertale/src/blk.h
@@ -0,0 +1,16 @@
+/* Undertale: data.bin's BLOCKS, unpacked when needed (tools/pack.py packs them with LZMA).
+ *
+ * The frames and tiles live in blocks; blk_get unpacks a block into a RAM cache
+ * and gives its bytes. A pointer it gives holds until the next blk_get (which can
+ * throw a block out to make room), so callers use it right away, and nothing keeps
+ * one from a frame to the next: blk_frame, at the frame's start, packs the cache's
+ * blocks together. The blocks the frame used are thrown out last. */
+#ifndef UT_BLK_H
+#define UT_BLK_H
+#include
+
+const uint8_t *blk_get(int id);
+void blk_frame(void);
+/* (for the tests: blocks unpacked so far, the most in a frame) */
+extern uint32_t blk_unpacked, blk_most_in_frame;
+#endif
diff --git a/games/undertale/src/bullets.c b/games/undertale/src/bullets.c
new file mode 100644
index 00000000..dbc272fd
--- /dev/null
+++ b/games/undertale/src/bullets.c
@@ -0,0 +1,250 @@
+/* The bullet board: the SOUL, the bullets, what hurts (battle.h). */
+#include
+#include "battle.h"
+#include "game.h"
+#include "gfx.h"
+#include "items.h"
+#include "res.h"
+
+Bullet bullets[MAX_BULLETS];
+
+Bullet *bullet_new(uint16_t spr, int x, int y, int vx, int vy) {
+ for (int i = 0; i < MAX_BULLETS; i++)
+ if (bullets[i].kind == BK_NONE) {
+ Bullet *b = &bullets[i];
+ memset(b, 0, sizeof *b);
+ b->kind = BK_LINEAR, b->spr = spr, b->x = x, b->y = y, b->vx = vx, b->vy = vy;
+ b->alpha = 255;
+ Frame f;
+ if (frame_get(spr, 0, &f)) b->r = (int16_t)(((f.w < f.h ? f.w : f.h) * 2 / 5));
+ if (b->r < 2) b->r = 2;
+ return b;
+ }
+ return NULL;
+}
+
+void bullets_clear(void) { memset(bullets, 0, sizeof bullets); }
+
+int soul_x(void) { return bt.sx / FX; }
+int soul_y(void) { return bt.sy / FX; }
+
+/* the SOUL: 8 x 8, moved inside the board by the arrows (X held: slower, as in the game) */
+/* the blue SOUL: it falls, and up jumps (higher while held), as the game's (halved) */
+static int32_t soul_vy;
+#define BLUE_JUMP (3 * FX)
+#define BLUE_GRAVITY 3 /* 1/16 px a frame, each frame */
+/* the purple SOUL: the string it's on; the strings, evenly down the board (pulled down, they come round again) */
+static int string_on;
+int32_t soul_strings_shift;
+int soul_string_y(int i) {
+ int h = bt.y1 - bt.y0 - 8, gap = h / NSTRINGS;
+ return bt.y0 + 4 + (gap / 2 + gap * i + (int)(soul_strings_shift / FX)) % h;
+}
+static uint32_t keys_before;
+void soul_set_mode(int mode) {
+ bt.soul_mode = mode;
+ soul_vy = 0;
+ string_on = NSTRINGS / 2, soul_strings_shift = 0;
+ if (mode == SOUL_PURPLE) bt.sy = soul_string_y(string_on) * FX;
+}
+void soul_update(uint32_t keys) {
+ int sp = bt.speed;
+ if (keys & K_X) sp /= 2;
+ int dx = ((keys & K_RIGHT) != 0) - ((keys & K_LEFT) != 0), dy = ((keys & K_DOWN) != 0) - ((keys & K_UP) != 0);
+ int x0 = (bt.x0 + 2 + 4) * FX, x1 = (bt.x1 - 2 - 4) * FX, y0 = (bt.y0 + 2 + 4) * FX, y1 = (bt.y1 - 2 - 4) * FX;
+ uint32_t pressed = keys & ~keys_before;
+ keys_before = keys;
+ if (bt.soul_mode == SOUL_PURPLE) {
+ /* up or down: the next string that way (the nearest above, or below); along it, the arrows */
+ int y = soul_string_y(string_on);
+ if (pressed & (K_UP | K_DOWN)) {
+ int best = -1;
+ for (int i = 0; i < NSTRINGS; i++) {
+ int yi = soul_string_y(i);
+ if ((pressed & K_UP) ? yi < y - 2 && (best < 0 || yi > soul_string_y(best))
+ : yi > y + 2 && (best < 0 || yi < soul_string_y(best)))
+ best = i;
+ }
+ if (best >= 0) string_on = best, y = soul_string_y(best);
+ }
+ int ty = y * FX;
+ bt.sy += clamp_i(ty - bt.sy, -5 * FX, 5 * FX);
+ bt.sx = clamp_i(bt.sx + dx * sp, x0, x1);
+ return;
+ }
+ static int shot_cool;
+ if (shot_cool) shot_cool--;
+ if (bt.soul_mode == SOUL_YELLOW && (pressed & K_Z) && !shot_cool) {
+ /* a shot, straight up (a few frames apart) */
+ Bullet *b = bullet_new(SPR_ORB, bt.sx, bt.sy - 6 * FX, 0, -6 * FX);
+ if (b) b->flags |= BF_PLAYER | BF_HARMLESS, b->color = BC_YELLOW, b->r = 2, b->life = 50;
+ shot_cool = 6;
+ }
+ if (bt.soul_mode == SOUL_GREEN) {
+ /* it stays in the middle; the arrows turn its shield */
+ static const uint32_t bit[4] = {K_DOWN, K_UP, K_LEFT, K_RIGHT};
+ for (int d = 0; d < 4; d++)
+ if (keys & bit[d]) bt.shield = d;
+ bt.sx = board_cx() * FX, bt.sy = board_cy() * FX;
+ return;
+ }
+ if (bt.soul_mode == SOUL_BLUE) {
+ bt.sx += dx * sp;
+ if (bt.sy >= y1) {
+ soul_vy = (keys & K_UP) ? -BLUE_JUMP : 0;
+ } else {
+ /* letting go of up cuts the jump short */
+ if (soul_vy < -FX / 2 && !(keys & K_UP)) soul_vy = -FX / 2;
+ soul_vy += BLUE_GRAVITY;
+ }
+ bt.sy += soul_vy;
+ if (bt.sy < y0) bt.sy = y0, soul_vy = 0;
+ } else {
+ bt.sx += dx * sp, bt.sy += dy * sp;
+ }
+ if (bt.sx < x0) bt.sx = x0;
+ if (bt.sx > x1) bt.sx = x1;
+ if (bt.sy < y0) bt.sy = y0;
+ if (bt.sy > y1) bt.sy = y1;
+}
+
+void bullets_update(void) {
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind == BK_NONE) continue;
+ if (b->kind != BK_SCRIPT) b->t++;
+ switch (b->kind) {
+ case BK_LINEAR:
+ case BK_GRAVITY:
+ b->vx += b->ax, b->vy += b->ay;
+ b->x += b->vx, b->y += b->vy;
+ break;
+ case BK_HOMING: {
+ /* turns towards the SOUL, at its speed */
+ int dx = bt.sx - b->x, dy = bt.sy - b->y;
+ int d = abs_i(dx) + abs_i(dy);
+ if (d) {
+ b->vx += dx * b->a / d, b->vy += dy * b->a / d;
+ int cap = b->b;
+ if (b->vx > cap) b->vx = cap;
+ if (b->vx < -cap) b->vx = -cap;
+ if (b->vy > cap) b->vy = cap;
+ if (b->vy < -cap) b->vy = -cap;
+ }
+ b->x += b->vx, b->y += b->vy;
+ break;
+ }
+ case BK_ORBIT: {
+ /* around (a, b) in pixels, radius c (1/16 px), angle d (1/256 turns, x16) */
+ b->d = (int16_t)(b->d + b->vx);
+ int r = b->c;
+ b->x = b->a * FX + r * gfx_cos(b->d / 16) / 1024;
+ b->y = b->b * FX + r * gfx_sin(b->d / 16) / 1024;
+ b->c = (int16_t)(b->c + b->vy);
+ break;
+ }
+ default:
+ break;
+ }
+ if (b->kind != BK_SCRIPT && b->anim && b->t % b->anim == 0) b->frame++;
+ if (b->life && b->t >= b->life) b->kind = BK_NONE;
+ /* far out of the board: gone */
+ int px = b->x / FX, py = b->y / FX;
+ if (px < -60 || px > SCREEN_W + 60 || py < -60 || py > SCREEN_H + 60) b->kind = BK_NONE;
+ }
+}
+
+/* a bone: its box w x h around (cx, cy), lying along its longer side; knobs at its ends */
+void gfx_bone(int cx, int cy, int w, int h, uint16_t col, uint8_t a) {
+ if (h >= w) {
+ int x = cx - w / 2, y = cy - h / 2, k = w / 2 > 1 ? w / 2 : 1;
+ gfx_rect(x + w / 4, y + k, w - 2 * (w / 4), h - 2 * k, col, a);
+ gfx_circle(x + k / 2 + 1, y + k, k, col, a), gfx_circle(x + w - k / 2 - 1, y + k, k, col, a);
+ gfx_circle(x + k / 2 + 1, y + h - k - 1, k, col, a), gfx_circle(x + w - k / 2 - 1, y + h - k - 1, k, col, a);
+ } else {
+ int x = cx - w / 2, y = cy - h / 2, k = h / 2 > 1 ? h / 2 : 1;
+ gfx_rect(x + k, y + h / 4, w - 2 * k, h - 2 * (h / 4), col, a);
+ gfx_circle(x + k, y + k / 2 + 1, k, col, a), gfx_circle(x + k, y + h - k / 2 - 1, k, col, a);
+ gfx_circle(x + w - k - 1, y + k / 2 + 1, k, col, a), gfx_circle(x + w - k - 1, y + h - k / 2 - 1, k, col, a);
+ }
+}
+
+void bullets_draw(void) {
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ const Bullet *b = &bullets[i];
+ if (b->kind == BK_NONE || (b->flags & BF_HIDDEN)) continue;
+ uint16_t color = b->color == BC_BLUE ? rgb565(0x14A9FF) : b->color == BC_ORANGE ? rgb565(0xFCA600) : b->color == BC_GREEN ? rgb565(0x00C000)
+ : b->color == BC_YELLOW ? rgb565(0xFFFF00) : 0;
+ if (b->spr == SPR_RECT) {
+ gfx_rect(b->x / FX - b->w / 2, b->y / FX - b->h / 2, b->w, b->h, color ? color : C_WHITE, b->alpha);
+ continue;
+ }
+ if (b->spr == SPR_BONE) {
+ gfx_bone(b->x / FX, b->y / FX, b->w, b->h, color ? color : C_WHITE, b->alpha);
+ continue;
+ }
+ if (b->angle) gfx_sprite_ex(b->spr, b->frame, b->x / FX, b->y / FX, 256, 256, b->angle, color, b->alpha);
+ else if (color) gfx_sprite_color(b->spr, b->frame, b->x / FX, b->y / FX, 0, color, b->alpha);
+ else gfx_sprite(b->spr, b->frame, b->x / FX, b->y / FX, 0, b->alpha);
+ }
+}
+
+/* the damage of a monster's bullet, as the game reckons it: its AT, one more at each of the SOUL's HP
+ * thresholds, less a fifth of DF (the shown one), at least 1 */
+int damage_to_player(int at) {
+ static const int16_t steps[] = {21, 30, 40, 50, 60, 70, 80, 90};
+ int dmg = at;
+ for (int i = 0; i < 8; i++)
+ if (sv.maxhp >= steps[i]) dmg++;
+ int df = sv.df - 10 + armor_df() - bt.df_loss;
+ dmg -= (df + 2) / 5;
+ return dmg < 1 ? 1 : dmg;
+}
+
+void soul_hurt(int dmg, int color) {
+ (void)color;
+ if (bt.inv) return;
+ sv.hp = (int16_t)(sv.hp - dmg);
+ if (sv.hp < 0) sv.hp = 0;
+ bt.inv = 30;
+}
+
+bool soul_hit_test(void) {
+ int sx = bt.sx / FX, sy = bt.sy / FX;
+ bool hit = false;
+ static int last_x, last_y;
+ bool moved = sx != last_x || sy != last_y;
+ last_x = sx, last_y = sy;
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind == BK_NONE || (b->flags & (BF_HIDDEN | BF_HARMLESS))) continue;
+ int bx = b->x / FX, by = b->y / FX;
+ if (bt.soul_mode == SOUL_GREEN && (b->flags & BF_SHIELD)) {
+ /* the shield stops what comes at it from the side it faces */
+ int dx = bx - sx, dy = by - sy;
+ int side = abs_i(dx) > abs_i(dy) ? (dx < 0 ? 2 : 3) : (dy < 0 ? 1 : 0);
+ if (side == bt.shield && abs_i(dx) + abs_i(dy) <= 15) {
+ b->kind = BK_NONE;
+ continue;
+ }
+ }
+ bool touch;
+ if (b->flags & BF_BOX) touch = sx + 3 > bx - b->w / 2 && sx - 3 < bx + b->w / 2 && sy + 3 > by - b->h / 2 && sy - 3 < by + b->h / 2;
+ else touch = (sx - bx) * (sx - bx) + (sy - by) * (sy - by) <= (b->r + 3) * (b->r + 3);
+ if (!touch) continue;
+ if (b->color == BC_BLUE && !moved) continue; /* blue: still, it passes */
+ if (b->color == BC_ORANGE && moved) continue; /* orange: moving, it passes */
+ if (b->color == BC_GREEN) {
+ sv.hp = (int16_t)min_i(sv.maxhp, sv.hp + (b->dmg ? b->dmg : 1));
+ b->kind = BK_NONE;
+ bt.greens++;
+ continue;
+ }
+ if (bt.inv) continue;
+ int at = b->dmg ? b->dmg : bt.mon[b->owner].at;
+ soul_hurt(damage_to_player(at), b->color);
+ hit = true;
+ if (b->flags & BF_ONCE) b->kind = BK_NONE;
+ }
+ return hit;
+}
diff --git a/games/undertale/src/core.c b/games/undertale/src/core.c
new file mode 100644
index 00000000..fda6263c
--- /dev/null
+++ b/games/undertale/src/core.c
@@ -0,0 +1,557 @@
+/* The CORE: its approach (two black shapes back away inside), the lobby and its broken elevator, the fire pit, the
+ * doorway's ambush (Madjick), the switch and its lasers (Alphys gets their order backwards), the crossroads and the
+ * room north (Knight Knight), the laser bridge (Alphys cuts the power; it keeps coming back), the Core Branch: the
+ * sage's path (its puzzle), the warrior's path (three fights, a switch) and the force field east that a patient one
+ * waits out; The End's bridge, the Core End, and Mettaton's last show; the hallway, Alphys, and the elevator up to
+ * the king's castle (newhome.c). (As the Undertale Wiki describes them; what they say is ours, in
+ * short.) */
+#include
+#include "battle.h"
+#include "event.h"
+#include "events.h"
+#include "game.h"
+#include "gfx.h"
+#include "hotland.h"
+#include "items.h"
+#include "menu.h"
+#include "monsters.h"
+#include "res.h"
+#include "save.h"
+#include "shoot.h"
+#include "snowdin.h"
+#include "world.h"
+
+static void gone(Obj *o) { o->kind = OBJ_NONE; }
+static Obj *thing(int x, int y, int w, int h, int event, int name, int arg) {
+ Obj *o = obj_trigger(x, y, w, h, event, name);
+ if (o) o->flags = OF_TALK | OF_SOLID, o->arg = (int16_t)arg;
+ return o;
+}
+static Obj *touch(int x, int y, int w, int h, int event, int arg) {
+ Obj *o = obj_trigger(x, y, w, h, event, ACTOR_TRIGGER);
+ if (o) o->arg = (int16_t)arg;
+ return o;
+}
+static Obj *npc(int name, uint16_t spr, int x, int y) {
+ const uint16_t s[4] = {spr, spr, spr, spr};
+ Obj *o = obj_npc(name, s, x, y, DIR_DOWN, 0);
+ if (o) o->flags &= (uint16_t)~OF_SOLID;
+ return o;
+}
+static void field(int x, int y, int w, int h) {
+ Obj *o = obj_trigger(x, y, w, h, EV_NONE, ACTOR_FIELD);
+ if (o) o->flags = OF_SOLID;
+}
+static void fields_down(void) {
+ for (Obj *o = obj_find(ACTOR_FIELD); o; o = obj_find(ACTOR_FIELD)) gone(o);
+}
+
+/* Alphys's calls, one bit each (F_CORE_CALLS, then F_CORE_CALLS2) */
+enum { CALL_IN, CALL_LOBBY, CALL_LIFT, CALL_PIT, CALL_AMBUSH, CALL_SWITCH, CALL_ORDER, CALL_EAST, CALL_NORTH,
+ CALL_KNIGHT, CALL_POWER, CALL_STOP, CALL_BRANCH };
+static bool called(int n) { return flag(n < 8 ? F_CORE_CALLS : F_CORE_CALLS2) >> (n & 7) & 1; }
+static void set_called(int n) {
+ int f = n < 8 ? F_CORE_CALLS : F_CORE_CALLS2;
+ set_flag(f, flag(f) | 1 << (n & 7));
+}
+static void call_at(int x, int y, int w, int h, int n) {
+ if (!called(n)) touch(x, y, w, h, EV_CORE_CALL, n);
+}
+
+/* ---------------------------------------------------------------- the lasers */
+/* the ambush's: three beams that sweep across from the bridge once the switch is pressed (blue, blue, orange);
+ * the bridge's: its emitters, and the wall of blue ones, while the power is on */
+#define AMB_Y0 1339
+#define AMB_Y1 1486
+#define BR_Y0 1300
+#define BR_Y1 1402
+static int sweep_t = -1; /* frames since the switch, -1 none */
+static int power_t; /* the bridge's power: on while power_t % CYCLE < ON */
+#define CYCLE 130
+#define ON 55
+static bool power_on;
+static int laser_cool;
+static const int16_t bridge_lasers[][2] = { /* x, blue */
+ {1267, 1}, {1309, 1}, {1348, 0}, {1388, 1}, {1428, 0}, {1469, 1},
+};
+#define WALL_X0 1504
+#define WALL_X1 2000
+#define WALL_STEP 19
+
+static int sweep_x(int i) { return 560 - (sweep_t - i * 60) * 3; }
+static bool sweep_blue(int i) { return i < 2; }
+static bool laser_hits(int x, bool blue, int y0, int y1) {
+ if (pl.x + 3 > x + 1 || pl.x + 17 < x - 1 || pl.y + 29 < y0 || pl.y + 20 > y1) return false;
+ return blue ? pl.moving : !pl.moving;
+}
+static void laser_hit(void) {
+ if (laser_cool || event_running()) return;
+ event_start(EV_CORE_LASER, NULL);
+}
+static void lasers_watch(void) {
+ if (laser_cool) laser_cool--;
+ if (g_room == ROOM_CORE_WEST && sweep_t >= 0) {
+ sweep_t++;
+ for (int i = 0; i < 3; i++) {
+ int x = sweep_x(i);
+ if (x < 560 && x > 100 && laser_hits(x, sweep_blue(i), AMB_Y0, AMB_Y1)) laser_hit();
+ }
+ if (sweep_x(2) < 90) {
+ sweep_t = -1;
+ if (!called(CALL_ORDER)) event_start(EV_CORE_ORDER, NULL);
+ }
+ }
+ if (g_room == ROOM_CORE_BRIDGE && flag(F_CORE_POWER) == 1 && called(CALL_STOP)) {
+ power_t++;
+ power_on = power_t % CYCLE < ON;
+ if (power_on && power_t % CYCLE > 6) { /* (a moment to stop when it comes back on) */
+ for (unsigned i = 0; i < sizeof bridge_lasers / sizeof bridge_lasers[0]; i++)
+ if (laser_hits(bridge_lasers[i][0], bridge_lasers[i][1], BR_Y0, BR_Y1)) laser_hit();
+ for (int x = WALL_X0; x <= WALL_X1; x += WALL_STEP)
+ if (laser_hits(x, true, BR_Y0, BR_Y1)) laser_hit();
+ }
+ } else power_on = false;
+}
+static void draw_beam(int x, int y0, int y1, bool blue) {
+ uint16_t c = blue ? rgb565(0x14A9FF) : rgb565(0xFCA600);
+ gfx_rect(x - 1 - cam_x, y0 - cam_y, 3, y1 - y0, c, (g_frame / 2) % 2 ? 255 : 200);
+}
+
+void core_draw(void) {
+ if (world_room_info()->area != AREA_CORE) return;
+ for (int i = 0; i < MAX_OBJS; i++) {
+ const Obj *o = &objs[i];
+ if (o->kind == OBJ_NONE || o->name != ACTOR_FIELD) continue;
+ uint16_t c = (g_frame / 3) % 2 ? rgb565(0x60FFFF) : rgb565(0x20A0C0);
+ gfx_rect(o->x - cam_x, o->y - cam_y, o->bw, o->bh, c, 200);
+ }
+ if (g_room == ROOM_CORE_WEST && sweep_t >= 0)
+ for (int i = 0; i < 3; i++) {
+ int x = sweep_x(i);
+ if (x < 560 && x > 100) draw_beam(x, AMB_Y0, AMB_Y1, sweep_blue(i));
+ }
+ /* the bridge's: on until Alphys cuts the power, then on and off */
+ bool on = g_room == ROOM_CORE_BRIDGE && (!flag(F_CORE_POWER) || power_on);
+ /* (the power flickers a moment before it comes back) */
+ bool flicker = g_room == ROOM_CORE_BRIDGE && flag(F_CORE_POWER) == 1 && called(CALL_STOP) && power_t % CYCLE >= CYCLE - 20 &&
+ g_frame % 4 < 2;
+ if (on || flicker) {
+ for (unsigned i = 0; i < sizeof bridge_lasers / sizeof bridge_lasers[0]; i++)
+ draw_beam(bridge_lasers[i][0], BR_Y0, BR_Y1, bridge_lasers[i][1]);
+ for (int x = WALL_X0; x <= WALL_X1; x += WALL_STEP) draw_beam(x, BR_Y0, BR_Y1, true);
+ }
+}
+
+/* the force field by the east bridge: the sign says patience (a minute in front of it, and it's gone) */
+static int field_wait;
+void core_update(void) {
+ if (world_room_info()->area != AREA_CORE) return;
+ lasers_watch();
+ if (g_room == ROOM_CORE_BRANCH && !flag(F_CORE_FIELD)) {
+ if (pl.x > 2440 && pl.x < 2530 && pl.y > 830 && pl.y < 880) {
+ if (++field_wait >= 60 * 30) {
+ set_flag(F_CORE_FIELD, 1);
+ fields_down();
+ event_start(EV_CORE_FIELD, NULL);
+ }
+ } else field_wait = 0;
+ }
+}
+
+/* ---------------------------------------------------------------- the rooms */
+void core_setup(int room, int from) {
+ (void)from;
+ if (room < ROOM_CORE_APPROACH || room > ROOM_CORE_HALL) return;
+ sweep_t = -1, power_t = 0, field_wait = 0;
+ switch (room) {
+ case ROOM_CORE_APPROACH:
+ if (!flag(F_CORE_IN)) {
+ npc(ACTOR_ITEM, SPR_SAUSAGE, 316, 2560);
+ npc(ACTOR_ANY, SPR_SAUSAGE, 326, 2528);
+ touch(290, 2650, 60, 20, EV_CORE_APPROACH, 0);
+ }
+ break;
+ case ROOM_CORE_LOBBY:
+ call_at(298, 2080, 42, 30, CALL_LOBBY);
+ thing(296, 1986, 48, 54, EV_CORE_LIFT, ACTOR_DOOR, 0);
+ obj_sign(186, 1980, 28, 26, "* (A screen: CORE, the source of\n the Underground's power.)");
+ break;
+ case ROOM_CORE_PIT:
+ call_at(440, 1800, 79, 20, CALL_PIT);
+ obj_sign(440, 1740, 79, 22, "* (A pit of fire, far below.\n You can feel its heat.)");
+ break;
+ case ROOM_CORE_DOORWAY:
+ if (!flag(F_CORE_MADJICK)) touch(100, 1770, 120, 16, EV_CORE_MADJICK, 0);
+ break;
+ case ROOM_CORE_WEST:
+ call_at(119, 1420, 159, 16, CALL_AMBUSH);
+ thing(250, 1310, 20, 50, EV_CORE_SWITCH, ACTOR_SWITCH, 0);
+ if (!flag(F_CORE_SWITCH)) field(282, 1350, 6, 40);
+ call_at(740, 1339, 12, 66, CALL_EAST);
+ call_at(880, 1339, 12, 66, CALL_NORTH);
+ if (!flag(F_CORE_KNIGHT)) touch(766, 1150, 147, 14, EV_CORE_KNIGHT, 0);
+ break;
+ case ROOM_CORE_BRIDGE:
+ if (flag(F_CORE_POWER) < 2) {
+ call_at(1232, 1358, 12, 44, CALL_POWER);
+ call_at(1490, 1358, 12, 44, CALL_STOP);
+ touch(2062, 1300, 58, 12, EV_CORE_POWER, 0);
+ }
+ break;
+ case ROOM_CORE_BRANCH:
+ if (!called(CALL_BRANCH)) touch(2060, 1118, 58, 12, EV_CORE_BRANCH, 0); /* (on the way in) */
+ obj_save(2080, 1100, "* (The air smells of ozone. It\n fills you with determination.)");
+ obj_sign(2010, 1056, 20, 44, "* (North: the warrior's path.)\n* (West: the sage's path.)\n* (Any path leads to The End.)");
+ obj_sign(2150, 1056, 20, 44, "* (East... The End.)");
+ obj_sign(1670, 816, 22, 44, "* (Solve this puzzle, and The\n End will open.)");
+ obj_sign(1770, 312, 22, 44, "* (Why not stop for a snack?)");
+ obj_sign(2170, 816, 22, 44, "* (Get lost... and stay lost.)");
+ obj_sign(2010, 576, 22, 44, "* (Cross the room to the north,\n and The End will open.)");
+ obj_sign(2330, 816, 22, 44, "* (East! This way is The End.)");
+ obj_sign(2470, 816, 22, 44, "* (I can't fight. I can't think.)\n* (But with patience, I'll make\n my way through.)");
+ thing(1452, 834, 34, 28, EV_CORE_SAGE, ACTOR_ITEM, 0);
+ thing(1758, 372, 22, 20, EV_CORE_TRASH, ACTOR_ITEM, 0);
+ thing(2398, 372, 22, 20, EV_CORE_TRASH, ACTOR_ITEM, 1);
+ if (!flag(F_CORE_FIELD)) field(2524, 856, 10, 48);
+ break;
+ case ROOM_CORE_WARRIOR:
+ for (int i = flag(F_CORE_WARRIOR); i < 3; i++) touch(2220 + i * 130, 160, 10, 40, EV_CORE_WARRIOR, i);
+ thing(2660, 128, 20, 30, EV_CORE_WSWITCH, ACTOR_SWITCH, 0);
+ break;
+ case ROOM_CORE_END:
+ set_flag(F_CORE_END, 1);
+ obj_save(3478, 866, "* (The elevator to the king's\n castle must be past this door.)\n* (You're filled with determination.)");
+ thing(3418, 806, 40, 50, EV_CORE_LIFT, ACTOR_DOOR, 1);
+ break;
+ case ROOM_CORE_MTT:
+ if (!flag(F_CORE_MTT)) {
+ Obj *m = npc(ACTOR_METTATON, SPR_MTT_OW, 3360, 500);
+ if (m) m->flags |= OF_ANIM, m->anim_speed = 5;
+ touch(3342, 620, 36, 12, EV_CORE_MTT, 0);
+ field(3342, 336, 36, 8); /* (the way out north: shut until the show's over) */
+ } else {
+ Obj *m = npc(ACTOR_METTATON, flag(F_CORE_MTT) == 2 ? SPR_MTT_OW : SPR_MTTEX_OW, 3360, 500);
+ (void)m;
+ obj_sign(3346, 480, 30, 26, flag(F_CORE_MTT) == 2 ? "* (It's completely trashed.)" : "* (Mettaton. Out of power.)");
+ }
+ break;
+ case ROOM_CORE_HALL:
+ if (!flag(F_CORE_HALL)) {
+ static const uint16_t s[4] = {SPR_ALPHYS_OW, SPR_ALPHYS_OW, SPR_ALPHYS_OW, SPR_ALPHYS_OW};
+ Obj *a = obj_npc(ACTOR_ALPHYS, s, 3370, 230, DIR_UP, 0);
+ if (a) a->flags &= (uint16_t)~OF_SOLID;
+ touch(4220, 118, 10, 42, EV_CORE_HALL, 0);
+ }
+ thing(4278, 92, 46, 28, EV_CORE_LIFT, ACTOR_DOOR, 2);
+ break;
+ }
+}
+
+/* ---------------------------------------------------------------- the events */
+static int ev_approach(Ev *c) {
+ Obj *a = obj_get(ACTOR_ITEM), *b = obj_get(ACTOR_ANY);
+ EV_BEGIN;
+ if (c->self) gone(c->self);
+ /* two shapes, black, back away up the path, and in */
+ WALK_TO(b, b->x, 2290, 2);
+ WALK(a, a->x, 2300, 2);
+ gone(obj_get(ACTOR_ITEM)), gone(obj_get(ACTOR_ANY));
+ set_flag(F_CORE_IN, 1);
+ SAY(FACE_ALPHYS, "* Ring...|* Th-that's the CORE! We made it!|* ...wait. Were those\n monsters? Nobody's supposed\n to be in there...|* It's fine. Probably.\n* I'll guide you through!");
+ EV_END;
+}
+
+static int ev_call(Ev *c) {
+ EV_BEGIN;
+ if (c->self) c->v[0] = c->self->arg;
+ if (c->self) gone(c->self);
+ if (called(c->v[0])) return 1;
+ set_called(c->v[0]);
+ switch (c->v[0]) {
+ case CALL_LOBBY:
+ SAY(FACE_ALPHYS, "* Ring...|* The elevator in the middle\n goes right to the castle!\n* Head for it!");
+ break;
+ case CALL_PIT:
+ SAY(FACE_ALPHYS, "* Ring...|* A pit of fire? That's not on\n my map...|* Um. Go back, and try the\n other way.");
+ break;
+ case CALL_AMBUSH:
+ SAY(FACE_ALPHYS, "* Ring...|* See that switch? It turns off\n the force field...|* ...but it sets off lasers!\n* Orange, orange, then blue.\n* You'll be fine!");
+ break;
+ case CALL_ORDER:
+ SAY(FACE_ALPHYS, "* Ring...|* Wait. That was blue, blue,\n then orange.|* I had it backwards.\n* S-sorry...");
+ break;
+ case CALL_EAST:
+ SAY(FACE_ALPHYS, "* Ring...|* East! Keep going east!");
+ break;
+ case CALL_NORTH:
+ SAY(FACE_ALPHYS, "* Ring...|* N-no, wait! North!\n* Go north first!");
+ break;
+ case CALL_POWER:
+ SAY(FACE_ALPHYS, "* Ring...|* Those lasers... I can't turn\n them off.|* So I'll cut the power to the\n whole room!");
+ if (!flag(F_CORE_POWER)) set_flag(F_CORE_POWER, 1);
+ NARRATE("* (The lights go out.)");
+ field(1228, 1356, 6, 48); /* (back west: a field on its own battery) */
+ break;
+ case CALL_STOP:
+ SAY(FACE_ALPHYS, "* Ring...|* Stop! STOP MOVING!\n* The power's back on!|* I can't keep it off!\n* Move while it's off, and\n freeze when it's on!");
+ break;
+ }
+ EV_END;
+}
+
+/* the bridge crossed: its power stays off */
+static int ev_power(Ev *c) {
+ EV_BEGIN;
+ if (c->self) gone(c->self);
+ set_flag(F_CORE_POWER, 2);
+ fields_down();
+ SAY(FACE_ALPHYS, "* Ring...|* Phew... you made it.\n* Everything's under control!\n* Totally!");
+ EV_END;
+}
+
+static int ev_laser(Ev *c) {
+ EV_BEGIN;
+ g_exclaim = 20;
+ WAIT(15);
+ BATTLE(FORM_LASERS);
+ laser_cool = 50;
+ EV_END;
+}
+
+static int ev_switch(Ev *c) {
+ EV_BEGIN;
+ if (flag(F_CORE_SWITCH)) {
+ NARRATE("* (The switch is already pressed.)");
+ return 1;
+ }
+ NARRATE("* (You press the switch.)\n* (The force field shuts off.)");
+ set_flag(F_CORE_SWITCH, 1);
+ fields_down();
+ sweep_t = 0;
+ EV_END;
+}
+
+static int ev_madjick(Ev *c) {
+ EV_BEGIN;
+ if (c->self) gone(c->self);
+ SAY(FACE_ALPHYS, "* Ring...|* Watch out! Something's\n coming at you!");
+ g_exclaim = 20;
+ WAIT(15);
+ BATTLE(FORM_MADJICK);
+ set_flag(F_CORE_MADJICK, bt.mon[0].killed ? 2 : 1);
+ SAY(FACE_ALPHYS, "* Ring...|* Are you okay?|* Why are there so many\n monsters in here...?");
+ EV_END;
+}
+
+static int ev_knight(Ev *c) {
+ EV_BEGIN;
+ if (c->self) gone(c->self);
+ g_exclaim = 20;
+ WAIT(15);
+ BATTLE(FORM_KNIGHT);
+ set_flag(F_CORE_KNIGHT, bt.mon[0].killed ? 2 : 1);
+ set_called(CALL_KNIGHT);
+ SAY(FACE_ALPHYS, "* Ring...|* Are you all right?!\n* I'm so sorry...|* Okay. East now. Really.");
+ EV_END;
+}
+
+static int ev_branch(Ev *c) {
+ EV_BEGIN;
+ if (c->self) gone(c->self);
+ set_called(CALL_BRANCH);
+ SAY(FACE_ALPHYS, "* Ring...|* Okay, so, from here you go...\n* ...um...|* ...|* I don't know. My map's all\n wrong.|* I'm sorry. I'll... stop\n calling.");
+ EV_END;
+}
+
+static int ev_sage(Ev *c) {
+ EV_BEGIN;
+ if (flag(F_CORE_SAGE)) {
+ NARRATE("* (The puzzle is solved.)");
+ return 1;
+ }
+ shoot_open(SHOOT_SAGE);
+ WAIT_UNTIL(g_mode == MODE_WORLD);
+ if (shoot_result()) {
+ set_flag(F_CORE_SAGE, 1);
+ set_flag(F_CORE_FIELD, 1);
+ fields_down(); /* (the force field east, in this same room) */
+ NARRATE("* (Far to the east, something\n hums, then goes quiet.)");
+ }
+ EV_END;
+}
+
+static int ev_warrior(Ev *c) {
+ static const uint8_t forms[3] = {FORM_WHIM_FROG, FORM_KNIGHT_MADJICK, FORM_FROG_ASTIG_WHIM};
+ EV_BEGIN;
+ c->v[0] = c->self ? c->self->arg : 0;
+ if (c->self) gone(c->self);
+ if (flag(F_CORE_WARRIOR) > c->v[0]) return 1;
+ g_exclaim = 20;
+ WAIT(15);
+ BATTLE(forms[c->v[0]]);
+ if (battle_result() != BR_FLED && battle_result() != BR_DIED) set_flag(F_CORE_WARRIOR, c->v[0] + 1);
+ EV_END;
+}
+
+static int ev_wswitch(Ev *c) {
+ EV_BEGIN;
+ if (flag(F_CORE_WARRIOR) >= 4) {
+ NARRATE("* (The switch is already flipped.)");
+ return 1;
+ }
+ NARRATE("* (You flip the switch.)\n* (Far away, something hums,\n then goes quiet.)");
+ set_flag(F_CORE_WARRIOR, 4);
+ set_flag(F_CORE_FIELD, 1);
+ EV_END;
+}
+
+static int ev_field(Ev *c) {
+ EV_BEGIN;
+ NARRATE("* (The force field flickers...\n and fades away.)");
+ EV_END;
+}
+
+static int ev_trash(Ev *c) {
+ EV_BEGIN;
+ c->v[0] = c->self ? c->self->arg : 0;
+ if (flag(c->v[0] ? F_CORE_TRASH2 : F_CORE_TRASH1)) {
+ NARRATE("* (It's an empty trash can.)");
+ return 1;
+ }
+ if (c->v[0]) {
+ NARRATE("* (There's 100G inside the\n trash can.)");
+ sv.gold = (uint16_t)min_i(sv.gold + 100, 9999);
+ set_flag(F_CORE_TRASH2, 1);
+ } else if (item_add(ITEM_GLAMBURGER)) {
+ NARRATE("* (There's a Glamburger in the\n trash can. You take it.)");
+ set_flag(F_CORE_TRASH1, 1);
+ } else NARRATE("* (There's a Glamburger inside.)\n* (You're carrying too much.)");
+ EV_END;
+}
+
+/* the elevators: the lobby's (it works once the far side's reached), the Core End's back to it; the last one, up
+ * to the castle */
+static int ev_lift(Ev *c) {
+ EV_BEGIN;
+ c->v[0] = c->self ? c->self->arg : 0;
+ if (c->v[0] == 2) {
+ if (!flag(F_CORE_HALL)) {
+ NARRATE("* (The elevator to the king's\n castle.)");
+ return 1;
+ }
+ NARRATE("* (Take the elevator up to the\n castle?)@Yes@No");
+ if (CHOICE() == 1) return 1;
+ set_flag(F_CORE_DONE, 1);
+ FADE_TO(ROOM_NH_TOWN, 90, 4545, DIR_DOWN);
+ return 1;
+ }
+ if (c->v[0] == 0 && !flag(F_CORE_END)) {
+ NARRATE("* (The elevator doesn't work.)");
+ if (!called(CALL_LIFT)) {
+ set_called(CALL_LIFT);
+ SAY(FACE_ALPHYS, "* Ring...|* It's not working?\n* B-but my map says...|* Okay. Um. Go left!\n* No, right! ...Either!");
+ }
+ return 1;
+ }
+ NARRATE(c->v[0] ? "* (Take the elevator down to\n the lobby?)@Yes@No" : "* (Take the elevator to the\n Core End?)@Yes@No");
+ if (CHOICE() == 1) return 1;
+ if (c->v[0]) FADE_TO(ROOM_CORE_LOBBY, 310, 2045, DIR_DOWN);
+ else FADE_TO(ROOM_CORE_END, 3428, 862, DIR_DOWN);
+ EV_END;
+}
+
+/* Mettaton's room: the door locks; he tells Frisk the truth; his box, his switch, his new body */
+static int ev_mtt(Ev *c) {
+ Obj *m = obj_get(ACTOR_METTATON);
+ EV_BEGIN;
+ if (c->self) gone(c->self);
+ NARRATE("* (The door locks behind you.)");
+ WAIT_UNTIL(cam_pan(3200, 420, 4)); /* (up to him) */
+ text_set_top(0);
+ NARRATE("* OH YES. IT WAS ME.\n* THE MONSTERS. THE ELEVATOR.\n THE MAZE.|* AND ALL THOSE PUZZLES?\n* ALPHYS PLANNED THEM, SO SHE\n COULD SAVE YOU FROM ME.|* SHE WANTED TO BE YOUR HERO.\n* BUT I GOT BORED, DARLING.");
+ NARRATE("* WITH YOUR SOUL, I'LL CROSS THE\n BARRIER...|* ...AND BE A STAR UP THERE!\n* NOW... FOR THE GRAND FINALE!");
+ text_set_top(-1);
+ BATTLE(FORM_MTT_BOX);
+ if (battle_result() == BR_DIED) {
+ cam_free();
+ return 1;
+ }
+ text_set_top(0);
+ g_shake = 10;
+ NARRATE("* (Smoke pours out of Mettaton.)\n* WELL, WELL...|* MY SWITCH, DARLING? COULDN'T\n WAIT FOR MY NEW LOOK?|* NAUGHTY. BUT FINE...\n* IT'S TIME FOR MY PREMIERE!");
+ text_set_top(-1);
+ mttex_reset();
+ BATTLE(FORM_MTT_EX);
+ if (battle_result() == BR_DIED) {
+ cam_free();
+ return 1;
+ }
+ text_set_top(0);
+ m = obj_get(ACTOR_METTATON);
+ if (bt.mon[0].killed) {
+ set_flag(F_CORE_MTT, 2);
+ NARRATE("* (Mettaton falls back into his\n box. It's completely trashed.)");
+ } else {
+ set_flag(F_CORE_MTT, 1);
+ m->spr[0] = m->spr[1] = m->spr[2] = m->spr[3] = SPR_MTTEX_OW, m->flags &= (uint16_t)~OF_ANIM;
+ NARRATE("* THESE RATINGS... THE BEST\n I'VE EVER HAD!|* AND... WE'RE GETTING CALLS?\n* GO AHEAD, CALLER!");
+ NARRATE("* (A small, sad voice.)\n* oh... hi mettaton...|* i just... wanted to say...\n* your show is all i watch...|* please don't leave...");
+ NARRATE("* MORE CALLS... ALL SAYING THE\n SAME THING.|* ...SO THE UNDERGROUND NEEDS\n ME MORE THAN I THOUGHT.|* THEN I'LL STAY.\n* AND YOU, DARLING...\n YOU CAN STAND UP TO ASGORE.");
+ NARRATE("* ...THOUGH THIS BODY EATS\n BATTERY LIKE...|* (Mettaton runs out of power.)");
+ }
+ text_set_top(-1);
+ fields_down();
+ /* Alphys, in through the door */
+ {
+ static const uint16_t s[4] = {SPR_ALPHYS_OW, SPR_ALPHYS_OW, SPR_ALPHYS_OW, SPR_ALPHYS_OW};
+ Obj *a = obj_npc(ACTOR_ALPHYS, s, 3360, 760, DIR_UP, 0);
+ if (a) a->flags &= (uint16_t)~OF_SOLID;
+ }
+ WALK(obj_get(ACTOR_ALPHYS), 3360, 600, 3);
+ if (flag(F_CORE_MTT) == 2) SAY(FACE_ALPHYS, "* I-I got the door open...\n* ...oh.|* Mettaton...|* ...let's just go.");
+ else SAY(FACE_ALPHYS, "* I-I got the door open! Are\n you okay?! Is Mettaton...|* ...he's just out of power.\n* Oh, thank goodness...|* Come on. The elevator's\n this way.");
+ gone(obj_get(ACTOR_ALPHYS));
+ cam_free();
+ EV_END;
+}
+
+/* the hallway: by the elevator, Alphys tells Frisk the truth about the way home */
+static int ev_hall(Ev *c) {
+ Obj *a = obj_get(ACTOR_ALPHYS);
+ EV_BEGIN;
+ if (c->self) gone(c->self);
+ WALK(a, pl.x - 30, pl.y + 10, 3);
+ SAY(FACE_ALPHYS, "* W-wait! Before you go...|* There's something I didn't\n tell you.|* To leave, you need a human\n SOUL, and a monster's...");
+ SAY(FACE_ALPHYS, "* ASGORE's the only one who\n can help you through...|* ...but he needs your SOUL\n for the barrier.|* To go home, you'll have to\n fight him. And...");
+ SAY(FACE_ALPHYS, "* ...one of you won't come\n back.|* I'm sorry. I made it all up,\n so I'd seem like a hero.|* Good luck. You deserve a\n better friend than me.");
+ set_flag(F_CORE_HALL, 1);
+ a = obj_get(ACTOR_ALPHYS);
+ WALK(a, 3360, a->y, 3);
+ gone(obj_get(ACTOR_ALPHYS));
+ EV_END;
+}
+
+int core_event(Ev *c) {
+ switch (c->id) {
+ case EV_CORE_APPROACH: return ev_approach(c);
+ case EV_CORE_CALL: return ev_call(c);
+ case EV_CORE_ORDER:
+ if (!c->at) c->v[0] = CALL_ORDER;
+ return ev_call(c);
+ case EV_CORE_BRANCH: return ev_branch(c);
+ case EV_CORE_POWER: return ev_power(c);
+ case EV_CORE_LASER: return ev_laser(c);
+ case EV_CORE_SWITCH: return ev_switch(c);
+ case EV_CORE_MADJICK: return ev_madjick(c);
+ case EV_CORE_KNIGHT: return ev_knight(c);
+ case EV_CORE_SAGE: return ev_sage(c);
+ case EV_CORE_WARRIOR: return ev_warrior(c);
+ case EV_CORE_WSWITCH: return ev_wswitch(c);
+ case EV_CORE_FIELD: return ev_field(c);
+ case EV_CORE_TRASH: return ev_trash(c);
+ case EV_CORE_LIFT: return ev_lift(c);
+ case EV_CORE_MTT: return ev_mtt(c);
+ case EV_CORE_HALL: return ev_hall(c);
+ }
+ return -1;
+}
diff --git a/games/undertale/src/coremon.c b/games/undertale/src/coremon.c
new file mode 100644
index 00000000..6973ba00
--- /dev/null
+++ b/games/undertale/src/coremon.c
@@ -0,0 +1,766 @@
+/* The CORE's monsters: Final Froggit, Whimsalot, Astigmatism, Madjick and Knight Knight (stats, ACTs and how they're
+ * spared from the Undertale Wiki's records of the game); then Mettaton's last show: his box until Frisk turns him
+ * round and flips his switch, and Mettaton EX with the show's ratings. Their words are short ones of our own. */
+#include
+#include "battle.h"
+#include "game.h"
+#include "gfx.h"
+#include "items.h"
+#include "monsters.h"
+#include "res.h"
+#include "world.h"
+
+/* ---------------------------------------------------------------- Final Froggit: mystify it, or two compliments
+ * or threats (a compliment lowers its ATTACK, a threat its DEFENSE) */
+static void act_ffroggit(Battle *b, Monster *m, int act, char *t, int cap) {
+ (void)b;
+ if (act == 0) {
+ snprintf(t, (size_t)cap, "* You compliment Final Froggit.\n* It understood. Its ATTACK dropped.");
+ m->at = (int16_t)max_i(1, m->at - 2);
+ if (++m->state >= 2) m->spare = true;
+ } else if (act == 1) {
+ snprintf(t, (size_t)cap, "* You threaten Final Froggit.\n* It understood. Its DEFENSE dropped.");
+ m->df = (int16_t)(m->df - 2);
+ if (++m->state >= 2) m->spare = true;
+ } else if (!m->state2) {
+ snprintf(t, (size_t)cap, "* You do something mysterious.\n* Final Froggit sees it still has\n much to learn.");
+ m->state2 = 1, m->spare = true;
+ } else snprintf(t, (size_t)cap, "* You do something mysterious.\n* Nothing happens.");
+}
+static const char *talk_ffroggit(Battle *b, Monster *m) {
+ (void)b;
+ if (m->state2) return "(A thoughtful\ncroak.)";
+ static const char *const lines[] = {"Skip, hop.", "Ribbit.", "Creak.", "Woof."};
+ return lines[rnd_n(4)];
+}
+static int attack_ffroggit(Battle *b, Monster *m) { (void)b, (void)m; return rnd_n(2) ? ATK_FROG_FLIES : ATK_FROG_JUMP; }
+
+/* ---------------------------------------------------------------- Whimsalot: console it, or pray twice */
+static void act_whimsalot(Battle *b, Monster *m, int act, char *t, int cap) {
+ (void)b;
+ if (act == 0) {
+ snprintf(t, (size_t)cap, "* You tell Whimsalot what it's\n doing is all right.");
+ m->spare = true, m->state = 1;
+ } else if (act == 1) {
+ snprintf(t, (size_t)cap, "* You leap at Whimsalot,\n threateningly.");
+ m->state = 2;
+ } else {
+ snprintf(t, (size_t)cap, "* You kneel and pray for safety.\n* Whimsalot remembers its\n conscience.");
+ if (++m->state2 >= 2) m->spare = true;
+ m->state = 3;
+ }
+}
+static const char *talk_whimsalot(Battle *b, Monster *m) {
+ (void)b;
+ if (m->state == 1) return "I'm at\npeace now.";
+ if (m->state == 2) return "You don't\nscare me.";
+ if (m->state == 3) return "There's\nstill hope.";
+ static const char *const lines[] = {"My mind is\nmade up.", "Not this\ntime.", "No\nregrets.", "(Shine.)"};
+ return lines[rnd_n(4)];
+}
+static int attack_whimsalot(Battle *b, Monster *m) { (void)b, (void)m; return rnd_n(2) ? ATK_WHIM_RING : ATK_WHIM_COLS; }
+
+/* ---------------------------------------------------------------- Astigmatism: it says what it wants (pick on it, or
+ * don't); do as it says, and it's done. Challenging it makes the others with it want to stop */
+static void act_astig(Battle *b, Monster *m, int act, char *t, int cap) {
+ if (act == 2) {
+ bool others = false;
+ for (int i = 0; i < b->nmon; i++) {
+ Monster *o = &b->mon[i];
+ if (o != m && o->alive && !o->gone) o->spare = true, others = true;
+ }
+ snprintf(t, (size_t)cap, others ? "* You stand up to Astigmatism.\n* The others are impressed." : "* You stand up to Astigmatism.");
+ m->state2 = 3;
+ return;
+ }
+ snprintf(t, (size_t)cap, act == 0 ? "* You pick on Astigmatism." : "* You don't pick on Astigmatism.");
+ if (m->state && act == m->state - 1) m->spare = true, m->state2 = 1;
+ else if (m->state) m->state2 = 2;
+}
+static const char *talk_astig(Battle *b, Monster *m) {
+ (void)b;
+ if (m->state2 == 1) return m->state == 1 ? "That's\nright!" : "Finally.\nSomeone\ngets it.";
+ if (m->state2 == 2) return m->state == 1 ? "Scared?" : "How\ndare you.";
+ if (m->state2 == 3) return "You dare\ndefy me?";
+ if (!m->state) {
+ /* what it wants: 1 to be picked on, 2 not */
+ m->state = (int16_t)(1 + rnd_n(2));
+ return m->state == 1 ? "... Pick\non me." : "... Don't\npick on me.";
+ }
+ static const char *const lines[] = {"You've seen\nenough.", "Take a last\nlook.", "...", "... ..."};
+ return lines[rnd_n(4)];
+}
+static const char *flavor_astig(Battle *b, Monster *m) {
+ (void)b;
+ if (m->state2) m->state2 = 0;
+ return m->spare ? "* Astigmatism doesn't care about\n fighting anymore." : NULL;
+}
+static int attack_astig(Battle *b, Monster *m) { (void)b, (void)m; return rnd_n(2) ? ATK_LOOX_BOUNCE : ATK_LOOX_WORM; }
+
+/* ---------------------------------------------------------------- Madjick: clear your mind, then talk; or raise the
+ * Stick, a magic wand to it. Talking first dizzies Frisk (DEFENSE down) */
+static void act_madjick(Battle *b, Monster *m, int act, char *t, int cap) {
+ if (act == 0) {
+ if (m->state) {
+ snprintf(t, (size_t)cap, "* Madjick talks over you, to\n itself. It seems satisfied.");
+ m->spare = true;
+ } else {
+ snprintf(t, (size_t)cap, "* Madjick talks over you.\n* Its gibberish dizzies you...\n* Your DEFENSE drops.");
+ b->df_loss += 1;
+ }
+ } else if (act == 1) {
+ snprintf(t, (size_t)cap, "* You glare at its orbs.\n* They waver a little.");
+ if (++m->state2 >= 2) m->spare = true;
+ } else {
+ snprintf(t, (size_t)cap, "* You think of pollen and sunshine.\n* Your DEFENSE goes up.");
+ m->state = 1, b->df_loss -= 1;
+ }
+}
+static bool item_madjick(Battle *b, Monster *m, int item, char *t, int cap) {
+ (void)b;
+ if (item != ITEM_STICK) return false;
+ snprintf(t, (size_t)cap, "* You raise the Stick.\n* Madjick takes it for a wand!");
+ m->spare = true, m->state = 2;
+ return true;
+}
+static const char *talk_madjick(Battle *b, Monster *m) {
+ (void)b;
+ if (m->state == 2) return "Ah! A fellow\nwizard!";
+ static const char *const lines[] = {"Abracadabra.", "Alakazam!", "Hocus\npocus.", "Please and\nthank you."};
+ return lines[rnd_n(4)];
+}
+static const char *flavor_madjick(Battle *b, Monster *m) {
+ (void)b;
+ return m->spare ? "* Madjick believes you're a\n fellow wizard." : NULL;
+}
+static int attack_madjick(Battle *b, Monster *m) { (void)b, (void)m; return rnd_n(2) ? ATK_MADJICK_CHASER : ATK_MADJICK_CORNER; }
+
+/* ---------------------------------------------------------------- Knight Knight: three lullabies put her to sleep */
+static void act_knight(Battle *b, Monster *m, int act, char *t, int cap) {
+ (void)b;
+ if (act == 0) {
+ if (m->spare) snprintf(t, (size_t)cap, "* You talk to Knight Knight.\n* She hums in her sleep...");
+ else snprintf(t, (size_t)cap, "* You ask Knight Knight about her\n day. There's no answer.");
+ m->state2++;
+ } else {
+ m->state++;
+ if (m->state == 1) snprintf(t, (size_t)cap, "* You sing an old lullaby.\n* Knight Knight looks sleepy...");
+ else if (m->state == 2) snprintf(t, (size_t)cap, "* You keep singing.\n* Her eyes start to close...");
+ else if (m->state == 3) snprintf(t, (size_t)cap, "* You keep singing.\n* Knight Knight falls asleep."), m->spare = true;
+ else snprintf(t, (size_t)cap, "* You keep singing.\n* Nothing happens.");
+ }
+}
+static const char *talk_knight(Battle *b, Monster *m) {
+ (void)b;
+ if (m->spare) return "Zzzz...";
+ if (m->state2 == 3) return "...... ......\n...... It's\nokay.";
+ static const char *const lines[] = {"Good knight.", "Farewell.", "Adieu.", "Close your\neyes...", "Goodbye."};
+ return lines[rnd_n(5)];
+}
+static const char *flavor_knight(Battle *b, Monster *m) {
+ (void)b;
+ return m->spare ? "* Knight Knight is snoring." : NULL;
+}
+static int attack_knight(Battle *b, Monster *m) {
+ (void)b;
+ if (m->spare) return ATK_NONE;
+ static const uint8_t pick[3] = {ATK_KNIGHT_MOON, ATK_KNIGHT_SUN, ATK_KNIGHT_STAR};
+ return pick[rnd_n(3)];
+}
+
+/* ---------------------------------------------------------------- Mettaton, in his box: no hurting him. Alphys calls
+ * about the switch on his back; Turn (a mirror behind him!) and Frisk flips it */
+static void act_mttbox(Battle *b, Monster *m, int act, char *t, int cap) {
+ (void)m;
+ if (act == 0 && b->state[0]) {
+ snprintf(t, (size_t)cap, "* You point behind Mettaton:\n \"Look! A mirror!\"|* He turns to look...\n* ...and you flip his switch!");
+ b->state[1] = 1;
+ } else snprintf(t, (size_t)cap, "* You look for a way around him.\n* There isn't one.");
+}
+static const char *talk_mttbox(Battle *b, Monster *m) {
+ (void)m;
+ if (b->state[1]) return "A MIRROR?\nWHERE?\n...OH NO.";
+ if (b->turn <= 1) return "NO MORE\nGAMES,\nDARLING.";
+ static const char *const lines[] = {"YOUR SOUL\nWILL MAKE ME\nA STAR!", "THE CROWD\nIS WAITING!", "STAY STILL,\nWON'T YOU?"};
+ return lines[rnd_n(3)];
+}
+static const char *flavor_mttbox(Battle *b, Monster *m) {
+ (void)m;
+ if (b->turn == 2 && !b->state[0]) {
+ b->state[0] = 1;
+ return "* Alphys calls: \"The switch on his\n back! Get him to turn around!\"";
+ }
+ return b->state[0] ? "* Mettaton blocks the way.\n* (Try turning him around.)" : "* Mettaton blocks the way.";
+}
+static bool hit_mttbox(Battle *b, Monster *m, int dmg) {
+ (void)b, (void)dmg;
+ m->dmg_shown = 0;
+ return true;
+}
+static int attack_mttbox(Battle *b, Monster *m) { (void)m; return b->state[1] ? -1 : ATK_MTT_BOXES; }
+static bool ends_mttbox(Battle *b, Monster *m) {
+ (void)m;
+ if (b->state[1]) b->result = BR_SCRIPTED; /* (the battle ends here: the switch is flipped) */
+ return false;
+}
+
+/* ---------------------------------------------------------------- Mettaton EX: the show's ratings. They start at 4000
+ * and fall while Frisk dawdles; Boast raises them through his turn (unless he lands a hit), Pose by how hurt Frisk
+ * is, Heel Turn has the crowd cheering every hit on Frisk; his brand's food, the Stick, a fight, a hit taken add
+ * more (Junk Food takes some away). Past 12000 (10000 once his legs are gone) his show is a hit, and it ends. His
+ * arms fall off after turn 11, his legs after turn 17; from then on, ratings count double */
+enum { EXR_START = 4000, EXR_MAX = 30000 };
+static struct {
+ int ratings, boast, heel, hits, fights, brand, glam, stick, steak, body, last_hp, popup, popup_t, done;
+ uint32_t last_frame;
+} ex;
+void mttex_reset(void) {
+ ex.ratings = EXR_START, ex.boast = ex.heel = ex.hits = ex.fights = ex.brand = ex.glam = ex.stick = ex.steak = 0;
+ ex.body = 0, ex.last_hp = sv.hp, ex.popup = 0, ex.popup_t = 0, ex.done = 0;
+}
+int mttex_ratings(void) { return ex.ratings; }
+static int ex_target(void) { return ex.body >= 2 ? 10000 : 12000; }
+static void ratings_add(int n) {
+ if (n > 0 && ex.body >= 2) n *= 2;
+ ex.ratings = min_i(EXR_MAX, max_i(0, ex.ratings + n));
+ ex.popup = n, ex.popup_t = 45;
+}
+/* a hit on Frisk in his turn: Boast lost (the crowd's disappointed), or the crowd cheers */
+static void ex_hit_taken(void) {
+ if (ex.boast) {
+ ex.boast = 0;
+ ratings_add(-100);
+ return;
+ }
+ static const int16_t violence[5] = {50, 25, 20, 15, 10};
+ ratings_add(violence[min_i(ex.hits, 4)] + (ex.heel ? 100 : 0));
+ ex.hits++;
+}
+static void act_mttex(Battle *b, Monster *m, int act, char *t, int cap) {
+ (void)b, (void)m;
+ if (act == 0) {
+ snprintf(t, (size_t)cap, "* You say you won't get hit even\n once.\n* Ratings climb through his turn.");
+ ex.boast = 1;
+ } else if (act == 1) {
+ int n = sv.hp == 1 ? 600 : sv.hp < 4 ? 500 : sv.hp * 4 < sv.maxhp ? 400 : sv.hp * 2 < sv.maxhp ? 250 : sv.hp * 3 < sv.maxhp * 2 ? 150 : 100;
+ const char *how = n >= 500 ? "With the last of your strength,\n you pose. The audience screams." :
+ n >= 400 ? "Wounded as you are, you pose.\n The audience gasps." :
+ n >= 150 ? "Hurt as you are, you pose.\n The audience applauds." : "You pose dramatically.\n The audience nods.";
+ snprintf(t, (size_t)cap, "* %s", how);
+ ratings_add(n);
+ } else {
+ snprintf(t, (size_t)cap, "* You turn and sneer at the\n audience.\n* They want to see you hit!");
+ ex.heel = 1;
+ }
+}
+static bool item_mttex(Battle *b, Monster *m, int item, char *t, int cap) {
+ (void)b, (void)m, (void)t, (void)cap;
+ switch (item) {
+ case ITEM_STICK:
+ snprintf(t, (size_t)cap, "* You throw the Stick.\n* Mettaton catches it in his teeth,\n and winks.");
+ ratings_add(ex.stick++ ? 1 : 700);
+ return true;
+ case ITEM_MYSTERY_KEY:
+ snprintf(t, (size_t)cap, "* You hold up the Mystery Key.\n* Mettaton pretends not to see it.");
+ return true;
+ case ITEM_GLAMBURGER: ratings_add(ex.glam++ ? 300 : 500); break;
+ case ITEM_STARFAIT: case ITEM_LEGENDARY_HERO: ratings_add(ex.brand++ ? 200 : 300); break;
+ case ITEM_STEAK: ratings_add(700); break;
+ case ITEM_JUNK_FOOD: ratings_add(-50); break;
+ }
+ return false; /* (eaten as ever) */
+}
+static bool hit_mttex(Battle *b, Monster *m, int dmg) {
+ (void)b, (void)m, (void)dmg;
+ static const int16_t action[5] = {300, 200, 150, 100, 50};
+ ratings_add(action[min_i(ex.fights++, 4)]);
+ return false;
+}
+static const char *talk_mttex(Battle *b, Monster *m) {
+ (void)m;
+ if (ex.ratings >= ex_target()) {
+ ex.done = 1;
+ return "OOH! THESE\nRATINGS!\nTHEY'RE\nINCREDIBLE!";
+ }
+ if (b->turn == 11) return "OH MY! MY\nARMS!\nSTILL... THE\nSHOW GOES ON!";
+ if (b->turn == 17) return "MY LEGS?!\n...FINE.\nTHE FINALE,\nTHEN!";
+ if (b->turn == 10) return "A UNION\nBREAK,\nDARLING.\nENJOY IT.";
+ if (b->turn <= 1) return "LIGHTS!\nCAMERA!\nAND...\nGO!";
+ static const char *const lines[] = {"ISN'T THIS\nBODY JUST\nDIVINE?", "SMILE FOR\nTHE CAMERA!", "THE CROWD\nLOVES YOU...\nFOR NOW!",
+ "RATINGS!\nRATINGS!", "LET'S MAKE\nTHIS\nBEAUTIFUL!", "ONE MORE\nTIME, WITH\nFEELING!"};
+ return lines[rnd_n(6)];
+}
+static const char *flavor_mttex(Battle *b, Monster *m) {
+ (void)m;
+ /* his limbs, after their turns */
+ if (b->turn == 12 && ex.body < 1) ex.body = 1;
+ if (b->turn == 18 && ex.body < 2) ex.body = 2;
+ if (b->turn <= 1) return "* Mettaton EX makes his premiere!";
+ if (ex.body == 2) return "* Mettaton's legs are gone.\n* He keeps on smiling.";
+ if (ex.body == 1) return "* Mettaton's arms are gone.\n* The show must go on.";
+ static const char *const lines[] = {"* Mettaton strikes a pose.", "* The audience watches, breathless.",
+ "* Smells like a premiere.", "* The spotlight is on you both."};
+ return lines[rnd_n(4)];
+}
+/* his turns, as the wiki lists them, shortened; from turn 18, the last two in turn */
+static int attack_mttex(Battle *b, Monster *m) {
+ (void)m;
+ static const uint8_t seq[] = {ATK_EX_LEGS, ATK_EX_BOMBS, ATK_EX_ARMS, ATK_EX_HEART, ATK_EX_BOTS, ATK_EX_DISCO,
+ ATK_EX_DISCO, ATK_EX_PAIRS, ATK_EX_PAIRS, ATK_EX_BREAK, ATK_EX_HEART, ATK_EX_REWIND,
+ ATK_EX_REWIND, ATK_EX_LEGS, ATK_EX_BOTS, ATK_EX_PAIRS, ATK_EX_HEART, ATK_EX_LASER,
+ ATK_EX_BOTS};
+ int n = (int)sizeof seq;
+ int i = b->turn - 1;
+ if (i >= n) i = n - 2 + (i - n) % 2;
+ ex.last_hp = sv.hp;
+ return seq[max_i(0, i)];
+}
+static bool ends_mttex(Battle *b, Monster *m) {
+ (void)m;
+ if (ex.done) b->result = BR_SCRIPTED;
+ return false;
+}
+static void draw_mttex(Battle *b, Monster *m, int x, int y) {
+ (void)b;
+ /* (the ratings fall while Frisk takes their time) */
+ if (g_frame != ex.last_frame) {
+ ex.last_frame = g_frame;
+ if (battle_ui() != BUI_NONE && g_frame % 6 == 0) ex.ratings = max_i(0, ex.ratings - 1);
+ if (ex.popup_t) ex.popup_t--;
+ }
+ uint16_t s = m->hurt ? SPR_MTTEX_HURT : ex.body == 2 ? SPR_MTTEX_TOP : ex.body == 1 ? SPR_MTTEX_ARMLESS : SPR_MTTEX_B;
+ int fr = m->hurt || ex.body ? 0 : (int)(g_frame / 3);
+ if (m->dust) {
+ int a = 255 - m->dust * 4;
+ if (a > 0) gfx_sprite_color(s, 0, x, y, 0, C_WHITE, (uint8_t)a);
+ return;
+ }
+ gfx_sprite(s, fr, x, y - (s == SPR_MTTEX_TOP ? 50 : 0), 0, m->fade ? (uint8_t)max_i(0, 128 - m->fade * 4) : 255);
+ char r[32];
+ snprintf(r, sizeof r, "RATINGS %d", ex.ratings);
+ gfx_text(FONT_SMALL, 226, 6, r, C_WHITE, 255);
+ if (ex.popup_t) {
+ snprintf(r, sizeof r, "%+d", ex.popup);
+ gfx_text(FONT_SMALL, 262, 16, r, ex.popup < 0 ? C_RED : C_YELLOW, 255);
+ }
+}
+
+/* ---------------------------------------------------------------- the definitions */
+static const MonDef core_defs[MON_CORE_COUNT - MON_HT_COUNT] = {
+ [MON_FFROGGIT - MON_HT_COUNT] = {
+ .name = "Final Froggit", .spare_gold = 50, .hp = 100, .at = 8, .df = 0, .exp = 120, .gold = 80,
+ .spr_idle = SPR_FFROGGIT_B, .spr_hurt = SPR_FFROGGIT_HURT, .idle_speed = 6, .check = "Its future looks brighter and brighter.",
+ .acts = {"Compliment", "Threaten", "Mystify"}, .nacts = 3,
+ .neutral = {"* Final Froggit knows exactly why it's here.", "* Smells like mustard seed.",
+ "* Final Froggit hops up and down, ominously."},
+ .low_hp = "* Final Froggit stands its ground.", .spareable = "* Final Froggit seems reluctant to fight you.",
+ .act = act_ffroggit, .talk = talk_ffroggit, .attack = attack_ffroggit},
+ [MON_WHIMSALOT - MON_HT_COUNT] = {
+ .name = "Whimsalot", .spare_gold = 50, .hp = 95, .at = 8, .df = 0, .exp = 110, .gold = 80,
+ .spr_idle = SPR_WHIMSALOT_B, .spr_hurt = SPR_WHIMSALOT_HURT, .idle_speed = 6, .check = "It finally stopped worrying.",
+ .acts = {"Console", "Terrorize", "Pray"}, .nacts = 3,
+ .neutral = {"* Whimsalot spins its weapon around.", "* Smells like nightshade and bleach.",
+ "* Whimsalot flutters silently.", "* Whimsalot locks eyes with you."},
+ .low_hp = "* Whimsalot's flying stutters.", .spareable = "* Whimsalot doesn't want to fight anymore.",
+ .act = act_whimsalot, .talk = talk_whimsalot, .attack = attack_whimsalot},
+ [MON_ASTIG - MON_HT_COUNT] = {
+ .name = "Astigmatism", .spare_gold = 50, .hp = 120, .at = 8, .df = 0, .exp = 130, .gold = 85,
+ .spr_idle = SPR_ASTIG_B, .spr_hurt = SPR_ASTIG_HURT, .idle_speed = 8, .check = "This bully ALWAYS gets its way.",
+ .acts = {"Pick On", "Don't Pick", "Challenge"}, .nacts = 3,
+ .neutral = {"* Astigmatism gazes through your SOUL.", "* Astigmatism clicks its teeth.", "* Smells like glasses.",
+ "* Astigmatism gives you a hypnotic glare."},
+ .low_hp = "* Astigmatism is watering.",
+ .act = act_astig, .talk = talk_astig, .attack = attack_astig, .flavor = flavor_astig},
+ [MON_MADJICK - MON_HT_COUNT] = {
+ .name = "Madjick", .spare_gold = 60, .hp = 190, .at = 8, .df = 0, .exp = 150, .gold = 120,
+ .spr_idle = SPR_MADJICK_B, .spr_hurt = SPR_MADJICK_HURT, .idle_speed = 6, .check = "This magic mercenary only says magic words.",
+ .acts = {"Talk", "Stare", "Clear Mind"}, .nacts = 3,
+ .neutral = {"* Madjick does a mysterious jig.", "* Madjick flaunts its orbs.", "* Smells like magic.",
+ "* Madjick whispers arcane swear words."},
+ .act = act_madjick, .talk = talk_madjick, .attack = attack_madjick, .flavor = flavor_madjick, .on_item = item_madjick},
+ [MON_KNIGHT - MON_HT_COUNT] = {
+ .name = "Knight Knight", .spare_gold = 70, .hp = 230, .at = 8, .df = 2, .exp = 180, .gold = 150, .dy = 6,
+ .spr_idle = SPR_KNIGHT_B, .spr_hurt = SPR_KNIGHT_HURT, .idle_speed = 8, .check = "This mercenary swings the Good Morningstar.",
+ .acts = {"Talk", "Sing"}, .nacts = 2,
+ .neutral = {"* Knight Knight smashes her morningstar.", "* Knight Knight breathes deeply.", "* Smells like stardust.",
+ "* Knight Knight watches quietly."},
+ .act = act_knight, .talk = talk_knight, .attack = attack_knight, .flavor = flavor_knight},
+ [MON_MTT_BOX - MON_HT_COUNT] = {
+ .name = "Mettaton", .hp = 9999, .at = 6, .df = 0, .spr_idle = SPR_MTT_B, .spr_hurt = SPR_MTT_B, .idle_speed = 6,
+ .check = "His metal body makes him invincible.", .acts = {"Turn"}, .nacts = 1,
+ .flags = MF_BOSS | MF_NOT_KILLABLE, .act = act_mttbox, .talk = talk_mttbox, .attack = attack_mttbox,
+ .flavor = flavor_mttbox, .on_hit = hit_mttbox, .ends = ends_mttbox},
+ [MON_MTT_EX - MON_HT_COUNT] = {
+ .name = "Mettaton EX", .hp = 1600, .at = 6, .df = 1, .exp = 800, .gold = 0, .spr_idle = SPR_MTTEX_B,
+ .spr_hurt = SPR_MTTEX_HURT, .idle_speed = 3, .check = "His weak point is his heart-shaped core.",
+ .acts = {"Boast", "Pose", "Heel Turn"}, .nacts = 3,
+ .flags = MF_BOSS, .act = act_mttex, .talk = talk_mttex, .attack = attack_mttex, .flavor = flavor_mttex,
+ .on_hit = hit_mttex, .draw = draw_mttex, .ends = ends_mttex, .on_item = item_mttex},
+};
+const MonDef *core_mon_def(int id) { return id >= MON_CORE_COUNT ? nh_mon_def(id) : &core_defs[id - MON_HT_COUNT]; }
+
+static const Form core_forms[FORM_CORE_COUNT - FORM_HT_COUNT] = {
+ [FORM_ASTIG - FORM_HT_COUNT] = {{MON_ASTIG}, 1, "* Astigmatism draws near."},
+ [FORM_WHIM_FROG - FORM_HT_COUNT] = {{MON_WHIMSALOT, MON_FFROGGIT}, 2, "* Whimsalot and Final Froggit appear."},
+ [FORM_WHIM_ASTIG - FORM_HT_COUNT] = {{MON_WHIMSALOT, MON_ASTIG}, 2, "* Whimsalot and Astigmatism appear."},
+ [FORM_FROG_ASTIG - FORM_HT_COUNT] = {{MON_FFROGGIT, MON_ASTIG}, 2, "* Final Froggit and Astigmatism appear."},
+ [FORM_KNIGHT_MADJICK - FORM_HT_COUNT] = {{MON_KNIGHT, MON_MADJICK}, 2, "* Mercenaries step out of the\n shadows."},
+ [FORM_FROG_ASTIG_WHIM - FORM_HT_COUNT] = {{MON_FFROGGIT, MON_ASTIG, MON_WHIMSALOT}, 3, "* What a nightmare!"},
+ [FORM_KNIGHT - FORM_HT_COUNT] = {{MON_KNIGHT}, 1, "* Knight Knight blocks the way!"},
+ [FORM_MADJICK - FORM_HT_COUNT] = {{MON_MADJICK}, 1, "* Madjick pops out of its hat!"},
+ [FORM_MTT_BOX - FORM_HT_COUNT] = {{MON_MTT_BOX}, 1, "* Mettaton attacks!"},
+ [FORM_MTT_EX - FORM_HT_COUNT] = {{MON_MTT_EX}, 1, "* Mettaton EX makes his premiere!"},
+};
+const Form *core_form(int form) { return form >= FORM_CORE_COUNT ? nh_form(form) : &core_forms[form - FORM_HT_COUNT]; }
+
+/* random encounters in the Core Branch (Astigmatism alone just once) */
+int core_encounter(int room) {
+ if (room != ROOM_CORE_BRANCH) return -1;
+ static const uint8_t pool[] = {FORM_WHIM_FROG, FORM_WHIM_ASTIG, FORM_FROG_ASTIG, FORM_KNIGHT_MADJICK,
+ FORM_FROG_ASTIG_WHIM, FORM_KNIGHT, FORM_MADJICK};
+ if (!flag(F_CORE_ASTIG)) {
+ set_flag(F_CORE_ASTIG, 1);
+ return FORM_ASTIG;
+ }
+ return pool[rnd_n((int)sizeof pool)];
+}
+
+/* ---------------------------------------------------------------- the attacks */
+static int bx0(void) { return bt.x0 + 3; }
+static int bx1(void) { return bt.x1 - 3; }
+static int by0(void) { return bt.y0 + 3; }
+static int by1(void) { return bt.y1 - 3; }
+#define P(v) ((v) * FX)
+static Bullet *shot(uint16_t spr, int x, int y, int vx, int vy) {
+ Bullet *b = bullet_new(spr, P(x), P(y), vx, vy);
+ if (b) b->owner = (uint8_t)bt.atk_owner;
+ return b;
+}
+static Bullet *rect(int x, int y, int w, int h, int vx, int vy) {
+ Bullet *b = shot(SPR_RECT, x, y, vx, vy);
+ if (b) b->flags |= BF_BOX, b->w = (int16_t)w, b->h = (int16_t)h;
+ return b;
+}
+/* a shot of the yellow SOUL's within (rx, ry) of b: it's spent */
+static bool shot_at(const Bullet *b, int rx, int ry) {
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *s = &bullets[i];
+ if (s->kind == BK_NONE || !(s->flags & BF_PLAYER)) continue;
+ if (abs_i(s->x - b->x) < P(rx) && abs_i(s->y - b->y) < P(ry)) {
+ s->kind = BK_NONE;
+ return true;
+ }
+ }
+ return false;
+}
+static int count_spr(uint16_t spr) {
+ int n = 0;
+ for (int i = 0; i < MAX_BULLETS; i++) n += bullets[i].kind != BK_NONE && bullets[i].spr == spr;
+ return n;
+}
+
+/* Madjick: a chaser orb that leaves crosses behind; a corner orb, where the SOUL heads, firing crosses at it */
+static void atk_madjick_chaser(int t) {
+ static Bullet *orb;
+ if (t == 0) {
+ orb = shot(SPR_ORB, bx1() - 8, by0() + 8, 0, 0);
+ if (orb) orb->kind = BK_HOMING, orb->a = 3, orb->b = P(2), orb->r = 5;
+ }
+ if (orb && orb->kind == BK_HOMING && t % 14 == 7 && t < 150) {
+ Bullet *c = shot(SPR_STAR, orb->x / FX, orb->y / FX, 0, 0);
+ if (c) c->r = 3, c->life = 70;
+ }
+}
+static void atk_madjick_corner(int t) {
+ static Bullet *orb;
+ static int corner;
+ if (t == 0) corner = 0, orb = shot(SPR_ORB, bx1() - 6, by0() + 6, 0, 0);
+ if (!orb || orb->spr != SPR_ORB) return;
+ orb->flags |= BF_HARMLESS;
+ /* (it moves to the corner of the way the SOUL went last) */
+ int c = soul_x() < board_cx() ? (soul_y() < board_cy() ? 1 : 2) : (soul_y() < board_cy() ? 0 : 3);
+ if (t % 30 == 0) corner = c;
+ int tx = corner == 1 || corner == 2 ? bx0() + 6 : bx1() - 6, ty = corner == 0 || corner == 1 ? by0() + 6 : by1() - 6;
+ orb->x += (P(tx) - orb->x) / 6, orb->y += (P(ty) - orb->y) / 6;
+ if (t % 12 == 6 && t < 150) {
+ int dx = soul_x() - orb->x / FX, dy = soul_y() - orb->y / FX, d = abs_i(dx) + abs_i(dy);
+ if (d) {
+ Bullet *s = shot(SPR_STAR, orb->x / FX, orb->y / FX, dx * P(3) / d, dy * P(3) / d);
+ if (s) s->r = 3;
+ }
+ }
+}
+/* Knight Knight: the Moon's meteors from the top left; the Sun's flares, spiralling out; her morningstars' slams */
+static void atk_knight_moon(int t) {
+ if (t % 6 == 0 && t < 150) {
+ int x = bx0() - 30 + rnd_n(bx1() - bx0()), y = by0() - 10;
+ Bullet *s = shot(SPR_STAR, x, y, P(2), P(3));
+ if (s) s->r = 3;
+ }
+}
+static void atk_knight_sun(int t) {
+ static Bullet *sun;
+ if (t == 0) {
+ sun = shot(SPR_ORB, board_cx(), board_cy(), 0, 0);
+ if (sun) sun->r = 5;
+ }
+ if (t % 4 == 0 && t < 150) {
+ int a = t * 5;
+ for (int k = 0; k < 2; k++) {
+ int aa = a + k * 128;
+ Bullet *s = shot(SPR_STAR, board_cx(), board_cy(), gfx_cos(aa) * 2 * FX / 1024, gfx_sin(aa) * 2 * FX / 1024);
+ if (s) s->r = 3;
+ }
+ }
+ if (sun && t == 150) sun->kind = BK_NONE;
+}
+static void atk_knight_star(int t) {
+ /* five heads, at the top or the bottom; they shine a moment, then slam across */
+ static int8_t top;
+ int k = t % 50;
+ if (k == 0 && t < 150) {
+ top = (int8_t)rnd_n(2);
+ int w = bx1() - bx0();
+ for (int i = 0; i < 5; i++) {
+ if (i == rnd_n(5)) continue;
+ Bullet *s = shot(SPR_ORB, bx0() + w * (2 * i + 1) / 10, top ? by0() + 4 : by1() - 4, 0, 0);
+ if (s) s->r = 6, s->flags |= BF_HARMLESS, s->life = 50, s->alpha = 120;
+ }
+ }
+ if (k == 15)
+ for (int i = 0; i < MAX_BULLETS; i++)
+ if (bullets[i].kind != BK_NONE && bullets[i].spr == SPR_ORB && (bullets[i].flags & BF_HARMLESS))
+ bullets[i].flags &= (uint8_t)~BF_HARMLESS, bullets[i].alpha = 255, bullets[i].vy = top ? P(5) : -P(5);
+}
+
+/* Mettaton EX's. Shooting what he sends (the yellow SOUL) is good for the ratings, as his heart is */
+static void ex_watch_hits(void) {
+ if (sv.hp < ex.last_hp) ex_hit_taken();
+ ex.last_hp = sv.hp;
+ if (ex.boast && bt.atk_t % 9 == 0) ratings_add(3 + (bt.atk_t / 9) % 2);
+}
+static void atk_ex_legs(int t) {
+ if (t % 26 == 0 && t < 130) {
+ bool left = rnd_n(2);
+ int y = by0() + 6 + rnd_n(by1() - by0() - 12);
+ Bullet *w = rect(left ? bx0() + 2 : bx1() - 2, y, 3, 9, 0, 0); /* the warning: where it comes */
+ if (w) w->flags |= BF_HARMLESS, w->color = BC_ORANGE, w->life = 12;
+ Bullet *l = shot(SPR_EXLEG, left ? bx0() - 40 : bx1() + 40, y, left ? P(7) : -P(7), 0);
+ if (l) l->flags |= BF_BOX, l->w = 56, l->h = 7, l->angle = 64, l->c = 12, l->kind = BK_SCRIPT;
+ }
+ /* (after their warning, they kick across) */
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *l = &bullets[i];
+ if (l->kind != BK_SCRIPT || l->spr != SPR_EXLEG) continue;
+ if (l->c) l->c--;
+ else l->x += l->vx;
+ if (l->x < P(bx0() - 70) || l->x > P(bx1() + 70)) l->kind = BK_NONE;
+ }
+}
+/* rows of bombs and blocks; a shot bomb blows up its row and its column; with `rewind`, back up they come */
+static void ex_blast(int x, int y) {
+ Bullet *h = rect(board_cx(), y, bx1() - bx0(), 6, 0, 0);
+ if (h) h->life = 8, h->color = BC_WHITE;
+ Bullet *v = rect(x, board_cy(), 6, by1() - by0(), 0, 0);
+ if (v) v->life = 8;
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind == BK_NONE || (b->spr != SPR_EXBLOCK && b->spr != SPR_BOMB)) continue;
+ if (abs_i(b->y / FX - y) < 6 || abs_i(b->x / FX - x) < 6) b->kind = BK_NONE;
+ }
+}
+static void ex_rows(int t, bool rewind, int cells) {
+ int w = bx1() - bx0(), cw = w / cells;
+ if (t % 34 == 0 && t < (rewind ? 100 : 136)) {
+ int bomb = rnd_n(cells);
+ for (int k = 0; k < cells; k++) {
+ bool is_bomb = rewind ? k == bomb : k != bomb;
+ Bullet *b = shot(is_bomb ? SPR_BOMB : SPR_EXBLOCK, bx0() + cw * k + cw / 2, by0() - 8, 0, P(1));
+ if (b) b->r = 6, b->flags |= BF_NOCLIP, b->kind = BK_SCRIPT, b->vy = P(1);
+ }
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT || (b->spr != SPR_EXBLOCK && b->spr != SPR_BOMB)) continue;
+ if (rewind && t == 110) b->vy = -P(1);
+ b->y += b->vy;
+ if (shot_at(b, 7, 7)) {
+ ratings_add(20);
+ b->kind = BK_NONE;
+ if (b->spr == SPR_BOMB) ex_blast(b->x / FX, b->y / FX);
+ }
+ if (b->y > P(by1() + 30) || b->y < P(by0() - 40)) b->kind = BK_NONE;
+ }
+}
+/* pairs: a bomb beside a block; only the bomb's blast breaks the block */
+static void atk_ex_pairs(int t) {
+ int w = bx1() - bx0();
+ if (t % 36 == 0 && t < 144) {
+ int side = rnd_n(2);
+ for (int k = 0; k < 2; k++) {
+ Bullet *b = shot(k == side ? SPR_BOMB : SPR_EXBLOCK, bx0() + w / 4 + k * w / 2, by0() - 8, 0, 0);
+ if (b) b->r = 6, b->flags |= BF_NOCLIP, b->kind = BK_SCRIPT, b->vy = P(1) + P(1) / 4;
+ }
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT || (b->spr != SPR_EXBLOCK && b->spr != SPR_BOMB)) continue;
+ b->y += b->vy;
+ if (b->spr == SPR_BOMB && shot_at(b, 7, 7)) {
+ ratings_add(20);
+ int x = b->x / FX, y = b->y / FX;
+ b->kind = BK_NONE;
+ for (int j = 0; j < MAX_BULLETS; j++) /* its block, beside it */
+ if (bullets[j].kind == BK_SCRIPT && bullets[j].spr == SPR_EXBLOCK && abs_i(bullets[j].y / FX - y) < 6) bullets[j].kind = BK_NONE;
+ Bullet *v = rect(x, board_cy(), 6, by1() - by0(), 0, 0); /* (and its column, up and down) */
+ if (v) v->life = 8;
+ } else if (b->spr == SPR_EXBLOCK) shot_at(b, 7, 7); /* (a block just stops a shot) */
+ if (b->y > P(by1() + 30)) b->kind = BK_NONE;
+ }
+}
+/* his long arms come down; shooting the box on one sends it back up */
+static void atk_ex_arms(int t) {
+ if (t % 40 == 0 && t < 120) {
+ int w = bx1() - bx0();
+ for (int k = 0; k < 3; k++) {
+ int x = bx0() + w * (2 * k + 1) / 6 + (t / 40 % 2 ? 8 : -8);
+ Bullet *a = rect(x, by0() - 40, 4, 70, 0, 0);
+ if (!a) continue;
+ a->kind = BK_SCRIPT, a->vy = P(1), a->flags |= BF_NOCLIP, a->a = 1;
+ Bullet *box = shot(SPR_EXARM, x, by0() - 5, 0, 0);
+ if (box) box->kind = BK_SCRIPT, box->vy = P(1), box->flags |= BF_NOCLIP | BF_HARMLESS, box->a = 2, box->b = (int16_t)(a - bullets);
+ }
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT || (b->a != 1 && b->a != 2)) continue;
+ b->y += b->vy;
+ if (b->a == 2 && b->vy > 0 && shot_at(b, 7, 8)) {
+ ratings_add(20);
+ b->vy = -P(3);
+ Bullet *arm = &bullets[b->b];
+ if (arm->kind == BK_SCRIPT && arm->a == 1) arm->vy = -P(3);
+ }
+ if (b->y < P(by0() - 80) || b->y > P(by1() + 60)) b->kind = BK_NONE;
+ }
+}
+/* his heart, out of his waist: sparks in a ring, again and again; enough shots at it and his turn is over */
+static Bullet *heart;
+static int heart_hp;
+static void atk_ex_heart(int t, bool laser) {
+ if (t == 0) {
+ heart = shot(SPR_MTTHEART, board_cx(), by0() + 12, laser ? P(2) : P(1), 0);
+ if (heart) heart->kind = BK_SCRIPT, heart->flags |= BF_HARMLESS | BF_NOCLIP;
+ heart_hp = laser ? 25 : 20;
+ }
+ Bullet *h = heart;
+ if (!h || h->kind != BK_SCRIPT || h->spr != SPR_MTTHEART) return;
+ h->x += h->vx;
+ if (h->x < P(bx0() + 8) || h->x > P(bx1() - 8)) h->vx = -h->vx;
+ if (h->c) h->c--, h->alpha = h->c % 2 ? 90 : 255;
+ if (shot_at(h, 9, 8)) heart_hp--, h->c = 6, ratings_add(20);
+ if (t % 30 == 29) heart_hp--; /* (it runs down by itself, too) */
+ if (heart_hp <= 0) {
+ h->kind = BK_NONE;
+ bt.atk_len = min_i(bt.atk_len, t + 20);
+ return;
+ }
+ if (!laser && t % 28 == 14) {
+ for (int k = 0; k < 8; k++) {
+ int a = k * 32 + t;
+ Bullet *s = shot(SPR_BOLT, h->x / FX, h->y / FX, gfx_cos(a) * 3 * FX / 2048, gfx_sin(a) * 3 * FX / 2048);
+ if (s) s->r = 3;
+ }
+ }
+ if (laser && t % 20 == 10) { /* three lines of sparks, at where the SOUL is */
+ int dx = soul_x() - h->x / FX, dy = soul_y() - h->y / FX, d = abs_i(dx) + abs_i(dy);
+ if (d)
+ for (int k = -1; k <= 1; k++) {
+ Bullet *s = shot(SPR_BOLT, h->x / FX, h->y / FX, (dx + k * 12) * P(3) / d, dy * P(3) / d);
+ if (s) s->r = 3;
+ }
+ }
+}
+/* his bots: a swarm coming down; all shot, and his turn is over */
+static void atk_ex_bots(int t) {
+ if (t == 0) {
+ int w = bx1() - bx0();
+ for (int k = 0; k < 9; k++) {
+ Bullet *b = shot(SPR_MTTMINI, bx0() + w * (k % 3 * 2 + 1) / 6, by0() - 14 - (k / 3) * 26, 0, 0);
+ if (b) b->kind = BK_SCRIPT, b->r = 7, b->flags |= BF_NOCLIP, b->vy = P(1) / 2, b->a = (int16_t)rnd_n(256);
+ }
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT || b->spr != SPR_MTTMINI) continue;
+ b->y += b->vy, b->a = (int16_t)(b->a + 4);
+ b->x += gfx_sin(b->a) * FX / 1024;
+ if (shot_at(b, 8, 10)) b->kind = BK_NONE, ratings_add(20);
+ if (b->y > P(by1() + 20)) b->kind = BK_NONE;
+ }
+ if (t > 10 && !count_spr(SPR_MTTMINI)) bt.atk_len = min_i(bt.atk_len, t + 10);
+}
+/* the disco ball: its beams sweep across the board, blue or white; a shot at the ball turns them */
+static void atk_ex_disco(int t, bool fast) {
+ static Bullet *ball;
+ static uint8_t col;
+ if (t == 0) {
+ ball = shot(SPR_DISCO, board_cx(), by0() + 8, 0, 0);
+ if (ball) ball->kind = BK_SCRIPT, ball->flags |= BF_HARMLESS | BF_NOCLIP;
+ col = BC_BLUE;
+ }
+ if (ball && ball->kind == BK_SCRIPT && ball->spr == SPR_DISCO && shot_at(ball, 9, 9)) {
+ col = col == BC_BLUE ? BC_WHITE : BC_BLUE;
+ ratings_add(5);
+ for (int i = 0; i < MAX_BULLETS; i++)
+ if (bullets[i].kind != BK_NONE && bullets[i].spr == SPR_RECT && bullets[i].a == 3) bullets[i].color = col;
+ }
+ int every = fast ? 22 : 34;
+ if (t > (fast ? 10 : 40) && t % every == 0 && t < 140) {
+ bool left = (t / every) % 2;
+ Bullet *b = rect(left ? bx0() : bx1(), board_cy(), 3, by1() - by0(), left ? P(fast ? 3 : 2) : -P(fast ? 3 : 2), 0);
+ if (b) b->color = col, b->a = 3, b->life = 120;
+ }
+}
+
+int core_attack_length(int atk) {
+ if (atk >= ATK_CORE_COUNT) return nh_attack_length(atk);
+ switch (atk) {
+ case ATK_EX_BREAK: return 90;
+ case ATK_EX_REWIND: return 200;
+ default: return 165;
+ }
+}
+
+void core_attack_run(Battle *b, int atk, int t) {
+ if (atk >= ATK_CORE_COUNT) {
+ nh_attack_run(b, atk, t);
+ return;
+ }
+ bool ex_atk = atk >= ATK_EX_LEGS;
+ if (t < 0) {
+ int w = atk == ATK_EX_BOMBS || atk == ATK_EX_REWIND ? 76 : atk == ATK_EX_PAIRS ? 44 : ex_atk ? 120 : 130;
+ board_set(w, ex_atk ? 110 : 100);
+ soul_set_mode(atk == ATK_EX_LEGS || atk == ATK_EX_BREAK || !ex_atk ? SOUL_RED : SOUL_YELLOW);
+ heart = NULL;
+ return;
+ }
+ if (ex_atk) ex_watch_hits();
+ switch (atk) {
+ case ATK_MADJICK_CHASER: atk_madjick_chaser(t); break;
+ case ATK_MADJICK_CORNER: atk_madjick_corner(t); break;
+ case ATK_KNIGHT_MOON: atk_knight_moon(t); break;
+ case ATK_KNIGHT_SUN: atk_knight_sun(t); break;
+ case ATK_KNIGHT_STAR: atk_knight_star(t); break;
+ case ATK_EX_LEGS: atk_ex_legs(t); break;
+ case ATK_EX_BOMBS: ex_rows(t, false, 4); break;
+ case ATK_EX_REWIND: ex_rows(t, true, 4); break;
+ case ATK_EX_ARMS: atk_ex_arms(t); break;
+ case ATK_EX_HEART: atk_ex_heart(t, false); break;
+ case ATK_EX_LASER: atk_ex_heart(t, true); break;
+ case ATK_EX_BOTS: atk_ex_bots(t); break;
+ case ATK_EX_DISCO: atk_ex_disco(t, ex.hits < 2 && bt.turn >= 7); break;
+ case ATK_EX_PAIRS: atk_ex_pairs(t); break;
+ case ATK_EX_BREAK: break;
+ }
+}
diff --git a/games/undertale/src/data.S b/games/undertale/src/data.S
new file mode 100644
index 00000000..ef216e88
--- /dev/null
+++ b/games/undertale/src/data.S
@@ -0,0 +1,19 @@
+/* data.bin (tools/pack.py) in the app's read-only data */
+#ifdef __APPLE__
+ .const_data
+ .p2align 2
+ .globl _ut_data
+_ut_data:
+ .incbin DATA_BIN
+ .p2align 2
+#else
+ .section .rodata.ut_data,"a"
+ .balign 4
+ .global ut_data
+ut_data:
+ .incbin DATA_BIN
+ .balign 4
+#endif
+#ifdef __ELF__
+ .section .note.GNU-stack,"",%progbits
+#endif
diff --git a/games/undertale/src/data.bin b/games/undertale/src/data.bin
new file mode 100644
index 00000000..61ae5ba8
Binary files /dev/null and b/games/undertale/src/data.bin differ
diff --git a/games/undertale/src/data.h b/games/undertale/src/data.h
new file mode 100644
index 00000000..90721e02
--- /dev/null
+++ b/games/undertale/src/data.h
@@ -0,0 +1,554 @@
+/* made by tools/pack.py: the contents of data.bin */
+#ifndef UT_DATA_H
+#define UT_DATA_H
+#define SEC_SPRITES 0
+#define SEC_FRAMES 1
+#define SEC_TILES 2
+#define SEC_ROOMS 3
+#define SEC_GRID 4
+#define SEC_COLL 5
+#define SEC_FONTS 6
+#define SEC_STRINGS 7
+#define SEC_DOORS 8
+#define SEC_BLOCKS 9
+#define SPR_FRISK_D 0
+#define SPR_FRISK_U 1
+#define SPR_FRISK_L 2
+#define SPR_FRISK_R 3
+#define SPR_TORIEL_D 4
+#define SPR_TORIEL_U 5
+#define SPR_TORIEL_L 6
+#define SPR_TORIEL_R 7
+#define SPR_SAVEPOINT 8
+#define SPR_FLOWEY 9
+#define SPR_HEART 10
+#define SPR_FIGHTBT 11
+#define SPR_TALKBT 12
+#define SPR_ITEMBT 13
+#define SPR_SPAREBT 14
+#define SPR_TARGET 15
+#define SPR_TARGETCHOICE 16
+#define SPR_HPNAME 17
+#define SPR_BATTLEBG 18
+#define SPR_STRIKE 19
+#define SPR_HEARTBREAK 20
+#define SPR_HEARTSHARDS 21
+#define SPR_HEARTGTFO 22
+#define SPR_FLYBULLET 23
+#define SPR_SMALLFROGBULLET 24
+#define SPR_BUTTERFLYBLLT 25
+#define SPR_BULLETMD 26
+#define SPR_BULLETSM 27
+#define SPR_CIRCLEBULLETMD1 28
+#define SPR_CIRCLEBULLETSM 29
+#define SPR_VEGBULLET 30
+#define SPR_CARROTBULLET 31
+#define SPR_ROACHBULLET 32
+#define SPR_ROACHDANCE 33
+#define SPR_TEARDROP 34
+#define SPR_FIREBULLET 35
+#define SPR_HANDBULLET 36
+#define SPR_SPINBULLET 37
+#define SPR_STREAMBULLET 38
+#define SPR_DIRBULLET 39
+#define SPR_BULLETGENMD 40
+#define SPR_DUMMY_B 41
+#define SPR_FROGGIT_B 42
+#define SPR_FROGGIT_HURT 43
+#define SPR_WHIMSUN_B 44
+#define SPR_WHIMSUN_HURT 45
+#define SPR_MOLDSMAL_B 46
+#define SPR_LOOX_B 47
+#define SPR_LOOX_HURT 48
+#define SPR_VEGETOID_B 49
+#define SPR_VEGETOID_HURT 50
+#define SPR_MIGOSP_B 51
+#define SPR_MIGOSP_HURT 52
+#define SPR_NAPSTABLOOK_B 53
+#define SPR_NAPSTA_HAT 54
+#define SPR_TORIEL_B 55
+#define SPR_TORIEL_HURT 56
+#define SPR_TORIEL_AWAY 57
+#define SPR_TORIEL_FIGHT 58
+#define SPR_TORIEL_DEAD 59
+#define SPR_O_RUINSDOOR1 60
+#define SPR_O_SWITCH 61
+#define SPR_O_GROUNDSWITCH1 62
+#define SPR_O_SPIKETILE 63
+#define SPR_O_ROCK 64
+#define SPR_O_COLORSWITCH 65
+#define SPR_O_FACESWITCH 66
+#define SPR_O_DUMMY 67
+#define SPR_O_EXC 68
+#define SPR_O_FLOWEYGROW 69
+#define SPR_O_FLOWEYSHRINK 70
+#define SPR_O_FLOWEYSINK 71
+#define SPR_O_FLOWEYLAUGHOVERWORLD 72
+#define SPR_O_TORIEL_HANDHOLD_D 73
+#define SPR_O_TORIEL_HANDHOLD_U 74
+#define SPR_O_TORIEL_HANDHOLD_L 75
+#define SPR_O_TORIEL_HANDHOLD_R 76
+#define SPR_O_HOLE 77
+#define SPR_O_SMALLWEB 78
+#define SPR_O_BIGWEB 79
+#define SPR_O_NAPSTABLOOK_GR 80
+#define SPR_O_NAPSTABLOOK_GRSM 81
+#define SPR_O_NAPSTABLOOK_L 82
+#define SPR_O_CHAIREMPTY 83
+#define SPR_O_CHAIRIEL 84
+#define SPR_O_CHAIRIELGETUP 85
+#define SPR_O_CANDYDISH 86
+#define SPR_O_CANDYDISH2 87
+#define SPR_O_CANDYDISH_BAD 88
+#define SPR_O_CHEESETABLE 89
+#define SPR_O_MOUSEHOLE 90
+#define SPR_O_NPC_D 91
+#define SPR_O_NPC_U 92
+#define SPR_O_NPC_L 93
+#define SPR_O_NPC_R 94
+#define SPR_O_PILLAR 95
+#define SPR_O_PIESLICE 96
+#define SPR_O_BIGPIE 97
+#define SPR_O_VEGETABLEOUTSIDE 98
+#define SPR_O_SMALLFROG 99
+#define SPR_O_RIBBON 100
+#define SPR_O_GROUNDKNIFE 101
+#define SPR_O_TORIELFLAME 102
+#define SPR_O_WINKSTAR 103
+#define SPR_O_TORNOTE 104
+#define SPR_O_DININGTABLE 105
+#define SPR_O_NASTYTREE 106
+#define SPR_O_TORIEL_D_PHONE 107
+#define SPR_O_TORIEL_DT 108
+#define SPR_O_TORIEL_UT 109
+#define SPR_O_TORIEL_LT 110
+#define SPR_O_TORIEL_RT 111
+#define SPR_O_TORIEL_HUG 112
+#define SPR_O_TORIEL_RUFFLE 113
+#define SPR_FROG_OW 114
+#define SPR_NAPSTA_OW 115
+#define SPR_PAPYRUS_R 116
+#define SPR_PAPYRUS_L 117
+#define SPR_PAPYRUS_D 118
+#define SPR_SANS_D 119
+#define SPR_DOGGO_OW 120
+#define SPR_DOGAMY_OW 121
+#define SPR_DOGARESSA_OW 122
+#define SPR_GREATERDOG_OW 123
+#define SPR_POFF 124
+#define SPR_DOG_OW 125
+#define SPR_DOG_BONE 126
+#define SPR_XTILE 127
+#define SPR_OTILE 128
+#define SPR_SNOWSPIKES 129
+#define SPR_SNOWSWITCH 130
+#define SPR_SNOWBALL 131
+#define SPR_LAMP 132
+#define SPR_PAPYRUS_ZAP 133
+#define SPR_FLAG_LIGHTBLUE 134
+#define SPR_FLAG_PURPLE 135
+#define SPR_FLAG_GREEN 136
+#define SPR_FLAG_BLUE 137
+#define SPR_FLAG_YELLOW 138
+#define SPR_FLAG_ORANGE 139
+#define SPR_FLAG_RED 140
+#define SPR_ORB 141
+#define SPR_SHOP_QC 142
+#define SPR_BONE 143
+#define SPR_SNOWFLAKE 144
+#define SPR_ICESHARD 145
+#define SPR_PRESENT 146
+#define SPR_KNIFE 147
+#define SPR_DOGPAW 148
+#define SPR_HEARTBULLET 149
+#define SPR_SNOWDRAKE_B 150
+#define SPR_SNOWDRAKE_HURT 151
+#define SPR_ICECAP_B 152
+#define SPR_ICECAP_HURT 153
+#define SPR_ICECAP_HATLESS 154
+#define SPR_GYFTROT_B 155
+#define SPR_GYFTROT_HURT 156
+#define SPR_DOGGO_B 157
+#define SPR_DOGGO_HURT 158
+#define SPR_LESSERDOG_B 159
+#define SPR_LESSERDOG_HURT 160
+#define SPR_GREATERDOG_B 161
+#define SPR_GREATERDOG_HURT 162
+#define SPR_DOGAMY_B 163
+#define SPR_DOGARESSA_B 164
+#define SPR_JERRY_B 165
+#define SPR_JERRY_HURT 166
+#define SPR_CHILLDRAKE_B 167
+#define SPR_GLYDE_B 168
+#define SPR_GLYDE_HURT 169
+#define SPR_PAPYRUS_B 170
+#define SPR_PAPYRUS_HURT 171
+#define SPR_PAPYRUS_CONFUSED 172
+#define SPR_MK_OW 173
+#define SPR_MK_OW_L 174
+#define SPR_UNDYNE_OW 175
+#define SPR_UNDYNE_THROW 176
+#define SPR_ONIONSAN 177
+#define SPR_ONIONSAN_UP 178
+#define SPR_SHYREN_OW 179
+#define SPR_AARON_OW 180
+#define SPR_WOSHUA_OW 181
+#define SPR_SEED 182
+#define SPR_BLOOM 183
+#define SPR_MADDUMMY_OW 184
+#define SPR_MADDUMMY_B 185
+#define SPR_DUMMY_MINION 186
+#define SPR_DUMMY_BOT 187
+#define SPR_TEMMIE_OW 188
+#define SPR_TEMMIE_B 189
+#define SPR_TEMMIE_HURT 190
+#define SPR_SHOP_GERSON 191
+#define SPR_SHOP_TEM 192
+#define SPR_SHOP_BURGER 193
+#define SPR_BURGERPANTS 194
+#define SPR_SHOP_BRATTY 195
+#define SPR_BRATTY_CATTY 196
+#define SPR_FFROGGIT_B 197
+#define SPR_FFROGGIT_HURT 198
+#define SPR_WHIMSALOT_B 199
+#define SPR_WHIMSALOT_HURT 200
+#define SPR_ASTIG_B 201
+#define SPR_ASTIG_HURT 202
+#define SPR_MADJICK_B 203
+#define SPR_MADJICK_HURT 204
+#define SPR_KNIGHT_B 205
+#define SPR_KNIGHT_HURT 206
+#define SPR_MTTEX_B 207
+#define SPR_MTTEX_HURT 208
+#define SPR_MTTEX_ARMLESS 209
+#define SPR_MTTEX_TOP 210
+#define SPR_MTTEX_OW 211
+#define SPR_SAUSAGE 212
+#define SPR_EXBLOCK 213
+#define SPR_EXLEG 214
+#define SPR_MTTHEART 215
+#define SPR_DISCO 216
+#define SPR_EXARM 217
+#define SPR_ASGORE_OW 218
+#define SPR_ASGORE_B 219
+#define SPR_ASGORE_HURT 220
+#define SPR_ASGORE_KNEEL 221
+#define SPR_FACE_ASGORE 222
+#define SPR_OMEGA 223
+#define SPR_OMEGA_LAUGH 224
+#define SPR_SOUL_LIGHT_BLUE 225
+#define SPR_SOUL_ORANGE 226
+#define SPR_SOUL_BLUE 227
+#define SPR_SOUL_PURPLE 228
+#define SPR_SOUL_GREEN 229
+#define SPR_SOUL_YELLOW 230
+#define SPR_XSHOT 231
+#define SPR_PAN 232
+#define SPR_SHOE 233
+#define SPR_BOOK 234
+#define SPR_ACTBUTTON 235
+#define SPR_BIRD 236
+#define SPR_TUTU 237
+#define SPR_ARTIFACT 238
+#define SPR_ECHO_OW 239
+#define SPR_ALPHYS_OW 240
+#define SPR_MTT_OW 241
+#define SPR_MTT_B 242
+#define SPR_RGUARDS_OW 243
+#define SPR_VENT_D 244
+#define SPR_VENT_U 245
+#define SPR_VENT_L 246
+#define SPR_VENT_R 247
+#define SPR_BURNTPAN 248
+#define SPR_VULKIN_B 249
+#define SPR_VULKIN_HURT 250
+#define SPR_TSUNDER_B 251
+#define SPR_PYROPE_B 252
+#define SPR_PYROPE_HURT 253
+#define SPR_RG01_OW 254
+#define SPR_RG02_OW 255
+#define SPR_RG01_B 256
+#define SPR_RG02_B 257
+#define SPR_MTT_NEWS 258
+#define SPR_BOMB_BALL 259
+#define SPR_BOMB_DOG 260
+#define SPR_BOMB_PRESENT 261
+#define SPR_BOMB_WATER 262
+#define SPR_BOMB_SCRIPT 263
+#define SPR_BOMB_GAME 264
+#define SPR_NEWS_SCRIPT 265
+#define SPR_APRON 266
+#define SPR_MUFFET_OW 267
+#define SPR_MUFFET_B 268
+#define SPR_MUFFET_HURT 269
+#define SPR_MUFFET_PET 270
+#define SPR_SPIDER_B 271
+#define SPR_DIAMOND1 272
+#define SPR_DIAMOND2 273
+#define SPR_BOMB_B 274
+#define SPR_DOGBOMB_B 275
+#define SPR_WATER_B 276
+#define SPR_UNDYNE_FALLEN 277
+#define SPR_SPEAR 278
+#define SPR_ARROW 279
+#define SPR_PLANE 280
+#define SPR_BOMB 281
+#define SPR_CLOUD 282
+#define SPR_BOLT 283
+#define SPR_UNDYNE_B 284
+#define SPR_NOTE 285
+#define SPR_DROP 286
+#define SPR_ARM 287
+#define SPR_DEBRIS 288
+#define SPR_STAR 289
+#define SPR_CROISSANT 290
+#define SPR_DONUT 291
+#define SPR_MTTMINI 292
+#define SPR_AARON_B 293
+#define SPR_AARON_HURT 294
+#define SPR_WOSHUA_B 295
+#define SPR_WOSHUA_HURT 296
+#define SPR_MOLDBYGG_B 297
+#define SPR_SHYREN_B 298
+#define SPR_SHYREN_TURNED 299
+#define SPR_INTROIMAGE 300
+#define SPR_INTROLAST 301
+#define SPR_UNDERTALETITLE 302
+#define SPR_GAMEOVERBG 303
+#define SPR_FACE_TORIELTALK 304
+#define SPR_FACE_TORIELTALKSIDE 305
+#define SPR_FACE_TORIELHAPPYTALK 306
+#define SPR_FACE_TORIELWHAT 307
+#define SPR_FACE_TORIELWHATSIDE 308
+#define SPR_FACE_TORIELMAD 309
+#define SPR_FACE_TORIELCOLD 310
+#define SPR_FACE_TORIELSURPRISED 311
+#define SPR_FACE_TORIELEMBARRASSED 312
+#define SPR_FACE_TORIELGLASSES 313
+#define SPR_FACE_TORIELREVENGETALK 314
+#define SPR_FACE_TORIELSCARED 315
+#define SPR_FACE_TORIELBLINK 316
+#define SPR_FACE_TORIELHAPPYBLINK 317
+#define SPR_FACE_TORIELBODY 318
+#define SPR_FACE_SANS 319
+#define SPR_FACE_SANSCHUCKLE 320
+#define SPR_FACE_SANSWINK 321
+#define SPR_FACE_PAPYRUS 322
+#define SPR_FACE_PAPYRUSEVIL 323
+#define SPR_FACE_PAPYRUSLAUGH 324
+#define SPR_FACE_PAPYRUSMAD 325
+#define SPR_FACE_PAPYRUSSIDE 326
+#define SPR_FACE_ALPHYS 327
+#define SPR_FLOWEYNICE 328
+#define SPR_FLOWEYNICESIDE 329
+#define SPR_FLOWEYSASSY 330
+#define SPR_FLOWEYEVIL 331
+#define SPR_FLOWEYGRIN 332
+#define SPR_FLOWEYPLAIN 333
+#define SPR_FLOWEYPISSED 334
+#define SPR_FLOWEYWINK 335
+#define SPR_FLOWEYLAUGH 336
+#define SPR_FLOWEYHURT 337
+#define SPR_FLOWEYTORIEL 338
+#define SPR_FLOWEYTORIEL2 339
+#define SPR_FLOWEYSIDESHOCK 340
+#define AREA_RUINS 0
+#define AREA_SNOWDIN 1
+#define AREA_SNOWTOWN 2
+#define AREA_WATERFALL 3
+#define AREA_HOTLAND 4
+#define AREA_RESORT 5
+#define AREA_CORE 6
+#define AREA_NEWHOME 7
+#define ROOM_AREA1 0
+#define ROOM_AREA1_2 1
+#define ROOM_RUINS1 2
+#define ROOM_RUINS2 3
+#define ROOM_RUINS3 4
+#define ROOM_RUINS4 5
+#define ROOM_RUINS5 6
+#define ROOM_RUINS6 7
+#define ROOM_RUINS7 8
+#define ROOM_RUINS7A 9
+#define ROOM_RUINS8 10
+#define ROOM_RUINS8B 11
+#define ROOM_RUINS9 12
+#define ROOM_RUINS10 13
+#define ROOM_RUINS10B 14
+#define ROOM_RUINS11 15
+#define ROOM_RUINS12 16
+#define ROOM_RUINS13 17
+#define ROOM_RUINS14 18
+#define ROOM_RUINS15 19
+#define ROOM_RUINS16 20
+#define ROOM_RUINS16A 21
+#define ROOM_RUINS16B 22
+#define ROOM_RUINS16C 23
+#define ROOM_RUINS16D 24
+#define ROOM_RUINS16E 25
+#define ROOM_RUINS16F 26
+#define ROOM_RUINS17A 27
+#define ROOM_RUINS17B 28
+#define ROOM_RUINS17C 29
+#define ROOM_RUINS17D 30
+#define ROOM_RUINS17X 31
+#define ROOM_RUINS18 32
+#define ROOM_RUINS19 33
+#define ROOM_RUINS20 34
+#define ROOM_RUINS21 35
+#define ROOM_TORHOUSE1 36
+#define ROOM_TORHOUSE2 37
+#define ROOM_TORKITCHEN 38
+#define ROOM_TORHOUSE3 39
+#define ROOM_ASRIELROOM 40
+#define ROOM_TORIELROOM 41
+#define ROOM_BASEMENT1 42
+#define ROOM_BASEMENT2 43
+#define ROOM_BASEMENT3 44
+#define ROOM_RUINSEXIT 45
+#define ROOM_SNOWDIN1 46
+#define ROOM_SNOWDIN2 47
+#define ROOM_SNOWDIN3 48
+#define ROOM_SNOWDIN4 49
+#define ROOM_SNOWDIN5 50
+#define ROOM_BOXROAD 51
+#define ROOM_RIVER 52
+#define ROOM_PAPSENTRY 53
+#define ROOM_DOGGO 54
+#define ROOM_ICEFIELD 55
+#define ROOM_SNOWMAN 56
+#define ROOM_ELECMAZE 57
+#define ROOM_BALLGAME 58
+#define ROOM_DOGI 59
+#define ROOM_WORDSEARCH 60
+#define ROOM_SPAGHETTI 61
+#define ROOM_SNOWPUZZLE 62
+#define ROOM_XOXO1 63
+#define ROOM_XOXO2 64
+#define ROOM_TILEPUZZLE 65
+#define ROOM_DOGHOUSE 66
+#define ROOM_XOXO3 67
+#define ROOM_XOXOFAIL 68
+#define ROOM_POFFZONE 69
+#define ROOM_DANGERBRIDGE 70
+#define ROOM_DOORROOM 71
+#define ROOM_SOUTHCLIFF 72
+#define ROOM_SNOWTOWN 73
+#define ROOM_TOWNNORTH 74
+#define ROOM_TOWNDOCK 75
+#define ROOM_FOGROOM 76
+#define ROOM_INN1 77
+#define ROOM_INN2 78
+#define ROOM_GRILLBYS 79
+#define ROOM_LIBRARY 80
+#define ROOM_PSHOUSE 81
+#define ROOM_PAPROOM 82
+#define ROOM_SANSROOM 83
+#define ROOM_SHED 84
+#define ROOM_WF1 85
+#define ROOM_WF_FALLS 86
+#define ROOM_WF_FALLS_LOW 87
+#define ROOM_WF2 88
+#define ROOM_WF_SEEDS1 89
+#define ROOM_WF_SEEDS2 90
+#define ROOM_WF_QUICHE 91
+#define ROOM_WF_WISH 92
+#define ROOM_WF_HIST 93
+#define ROOM_WF_CHASE1 94
+#define ROOM_WF_GRASS1 95
+#define ROOM_WF_SANS2 96
+#define ROOM_WF_MARSH_TOP 97
+#define ROOM_WF_MARSH_MID 98
+#define ROOM_WF_MARSH 99
+#define ROOM_WF_ONION 100
+#define ROOM_WF_PIANO 101
+#define ROOM_WF_STATUE 102
+#define ROOM_WF_RAIN1 103
+#define ROOM_WF_RAIN2 104
+#define ROOM_WF_CASTLE 105
+#define ROOM_WF_AARON 106
+#define ROOM_WF_BRIDGE 107
+#define ROOM_WF_DUMP 108
+#define ROOM_WF_TRASH 109
+#define ROOM_WF_WALKWAY 110
+#define ROOM_WF_QUIET 111
+#define ROOM_WF_BIRD2 112
+#define ROOM_WF_ACRES 113
+#define ROOM_WF_NAPSTA_IN 114
+#define ROOM_WF_GERSON 115
+#define ROOM_WF_TWIN 116
+#define ROOM_WF_MUSHROOM 117
+#define ROOM_WF_TEMMIE 118
+#define ROOM_WF_LANTERN 119
+#define ROOM_WF_DEADEND 120
+#define ROOM_WF_ECHOPATH 121
+#define ROOM_WF_CHASM 122
+#define ROOM_WF_ARENA 123
+#define ROOM_WF_NORTH 124
+#define ROOM_WF_HOTGATE 125
+#define ROOM_HT_ENTRANCE 126
+#define ROOM_HT_LABFRONT 127
+#define ROOM_HT_RIVER 128
+#define ROOM_HT_LAB 129
+#define ROOM_HT_LABEXIT 130
+#define ROOM_HT_BELT1 131
+#define ROOM_HT_BELT2 132
+#define ROOM_HT_MAGMA 133
+#define ROOM_HT_PAN 134
+#define ROOM_HT_LASERS 135
+#define ROOM_HT_HUB 136
+#define ROOM_HT_VENTISLE 137
+#define ROOM_HT_COOKING 138
+#define ROOM_HT_COREVIEW 139
+#define ROOM_HT_HOTDOG 140
+#define ROOM_HT_CROSS2 141
+#define ROOM_HT_ARTCLUB 142
+#define ROOM_HT_APRON 143
+#define ROOM_HT_BELTPUZ 144
+#define ROOM_HT_VENTPUZ 145
+#define ROOM_HT_BOZ 146
+#define ROOM_HT_GUARDS 147
+#define ROOM_HT_NEWS 148
+#define ROOM_HT_COREVIEW2 149
+#define ROOM_HT_L3 150
+#define ROOM_HT_HUB3 151
+#define ROOM_HT_NORTH3 152
+#define ROOM_HT_BELTS3 153
+#define ROOM_HT_SOUTH3 154
+#define ROOM_HT_SPIDERENT 155
+#define ROOM_HT_LAIR 156
+#define ROOM_HT_STAGE 157
+#define ROOM_HT_TILES 158
+#define ROOM_HT_CROSS3 159
+#define ROOM_RES_OUTSIDE 160
+#define ROOM_RES_LOBBY 161
+#define ROOM_RES_DINER 162
+#define ROOM_RES_HALL 163
+#define ROOM_RES_ROOM 164
+#define ROOM_CORE_APPROACH 165
+#define ROOM_CORE_LOBBY 166
+#define ROOM_CORE_DOORWAY 167
+#define ROOM_CORE_PIT 168
+#define ROOM_CORE_WEST 169
+#define ROOM_CORE_BRIDGE 170
+#define ROOM_CORE_BRANCH 171
+#define ROOM_CORE_WARRIOR 172
+#define ROOM_CORE_END 173
+#define ROOM_CORE_MTT 174
+#define ROOM_CORE_HALL 175
+#define ROOM_NH_TOWN 176
+#define ROOM_NH_HOUSE 177
+#define ROOM_NH_BASEMENT 178
+#define ROOM_NH_LAST 179
+#define ROOM_NH_THALL 180
+#define ROOM_NH_THRONE 181
+#define ROOM_NH_COFFINS 182
+#define ROOM_NH_BARRIER 183
+#define ROOM_NH_WALL 184
+#define FONT_MAIN 0
+#define FONT_MENU 1
+#define FONT_SMALL 2
+#define NUM_SPRITES 341
+#define NUM_ROOMS 185
+#define NUM_FONTS 3
+#endif
diff --git a/games/undertale/src/event.c b/games/undertale/src/event.c
new file mode 100644
index 00000000..f5cf6c3e
--- /dev/null
+++ b/games/undertale/src/event.c
@@ -0,0 +1,54 @@
+/* The event that runs now (event.h), and Frisk walking for a cutscene. */
+#include
+#include "event.h"
+#include "events.h"
+#include "game.h"
+
+static Ev cur;
+static bool running;
+
+void event_start(int id, Obj *self) {
+ if (running || !id) return;
+ memset(&cur, 0, sizeof cur);
+ cur.id = id, cur.self = self;
+ running = true;
+ if (id != EV_ENCOUNTER) event_update(); /* (it starts this frame: a talk opens at once) */
+}
+
+bool event_running(void) { return running; }
+bool event_blocking(void) { return running && !cur.free; }
+
+void event_cancel(void) { running = false; }
+void event_cur_arg(int v) { cur.v[0] = v; }
+int g_exclaim;
+
+void event_update(void) {
+ if (!running) return;
+ if (event_run(&cur)) running = false;
+}
+
+/* ---------------------------------------------------------------- Frisk walked */
+static struct {
+ int x, y, sp;
+ bool on;
+} walk;
+
+void pl_walk_to(int x, int y, int sp) { walk.x = x, walk.y = y, walk.sp = sp ? sp : 3, walk.on = true; }
+bool pl_walking(void) { return walk.on; }
+
+/* (world.c calls it each frame while a cutscene runs) */
+void pl_walk_step(void) {
+ if (!walk.on) {
+ pl.moving = 0;
+ return;
+ }
+ int dx = walk.x - pl.x, dy = walk.y - pl.y;
+ if (dx) pl.dir = dx < 0 ? DIR_LEFT : DIR_RIGHT;
+ if (abs_i(dy) > abs_i(dx)) pl.dir = dy < 0 ? DIR_UP : DIR_DOWN;
+ pl.x = (int16_t)(pl.x + clamp_i(dx, -walk.sp, walk.sp));
+ pl.y = (int16_t)(pl.y + clamp_i(dy, -walk.sp, walk.sp));
+ pl.anim++;
+ pl.frame = (uint8_t)((1 + pl.anim / 6) % (pl.dir < 2 ? 4 : 2));
+ pl.moving = 1;
+ if (pl.x == walk.x && pl.y == walk.y) walk.on = false, pl.frame = 0, pl.anim = 0, pl.moving = 0;
+}
diff --git a/games/undertale/src/event.h b/games/undertale/src/event.h
new file mode 100644
index 00000000..b408f2f4
--- /dev/null
+++ b/games/undertale/src/event.h
@@ -0,0 +1,58 @@
+/* Undertale: events, the game's cutscenes and talks.
+ *
+ * An event is a C function called once a frame until it says it is done;
+ * the macros below make it read like the game's scripts (each SAY waits for
+ * its text, each WAIT for its frames). They are protothreads on GCC's labels
+ * as values (a wait can be anywhere, in a switch too). The function's locals
+ * don't last over a wait: what has to is kept in c->v. One event runs at a
+ * time; while it does, Frisk doesn't move unless it lets him. */
+#ifndef UT_EVENT_H
+#define UT_EVENT_H
+#include
+#include "text.h"
+#include "world.h"
+
+typedef struct Ev {
+ int id, t;
+ void *at; /* where it waits (NULL: from the start) */
+ bool free; /* Frisk walks while it runs (FREE()), the others stop him */
+ int v[6];
+ Obj *self;
+} Ev;
+
+#define EV_CAT2(a, b) a##b
+#define EV_CAT(a, b) EV_CAT2(a, b)
+#define EV_BEGIN do { if (c->at) goto *c->at; } while (0); {
+#define EV_END } return 1
+#define EV_FALL __attribute__((fallthrough))
+#define EV_WAIT_(cond, n) do { c->at = &&EV_CAT(ev_at_, n); EV_CAT(ev_at_, n): if (!(cond)) return 0; } while (0)
+#define WAIT_UNTIL(cond) EV_WAIT_(cond, __COUNTER__)
+#define EV_WAITN_(N, n) do { c->t = (N); c->at = &&EV_CAT(ev_at_, n); EV_CAT(ev_at_, n): if (c->t-- > 0) return 0; } while (0)
+#define WAIT(N) EV_WAITN_(N, __COUNTER__)
+#define SAY(face, s) do { text_open(face, s); WAIT_UNTIL(!text_active()); } while (0)
+#define SAYF(face, ...) do { text_openf(face, __VA_ARGS__); WAIT_UNTIL(!text_active()); } while (0)
+#define NARRATE(s) SAY(FACE_NONE, s)
+#define CHOICE() text_choice()
+#define FREE(b) (c->free = (b))
+/* a list to choose from in the dialogue box (the phone's): CHOOSE waits, CHOSEN() is -1 for Back */
+#define CHOOSE(opts, n) do { text_open_menu(opts, n); WAIT_UNTIL(!text_active()); } while (0)
+#define CHOSEN() text_choice()
+/* an object walks to (x, y) at sp pixels a frame; WALK waits for it */
+#define WALK_TO(o, X, Y, SP) do { (o)->tx = (int16_t)(X), (o)->ty = (int16_t)(Y), (o)->speed = (uint8_t)(SP), (o)->walking = 1; } while (0)
+#define WALK(o, X, Y, SP) do { WALK_TO(o, X, Y, SP); WAIT_UNTIL(!(o)->walking); } while (0)
+/* Frisk walks to (x, y), as a cutscene moves him */
+#define PL_WALK(X, Y, SP) do { pl_walk_to(X, Y, SP); WAIT_UNTIL(!pl_walking()); } while (0)
+#define BATTLE(id) do { battle_start(id); WAIT_UNTIL(battle_over()); } while (0)
+#define FADE_TO(r, X, Y, D) do { world_fade_to(r, X, Y, D); WAIT_UNTIL(!world_fading()); } while (0)
+
+void event_start(int id, Obj *self);
+bool event_running(void);
+bool event_blocking(void); /* running, and Frisk can't walk */
+void event_update(void);
+void event_cancel(void);
+void event_cur_arg(int v); /* the event just started: its v[0] */
+extern int g_exclaim; /* frames of the "!" over Frisk (an encounter) */
+int event_run(Ev *c); /* events.c: the events, by id */
+void pl_walk_to(int x, int y, int sp);
+bool pl_walking(void);
+#endif
diff --git a/games/undertale/src/events.c b/games/undertale/src/events.c
new file mode 100644
index 00000000..6a46d51f
--- /dev/null
+++ b/games/undertale/src/events.c
@@ -0,0 +1,135 @@
+/* The events (event.h): the common ones (signs, SAVE Points, the phone), then each area's. */
+#include
+#include
+#include "event.h"
+#include "events.h"
+#include "battle.h"
+#include "game.h"
+#include "hotland.h"
+#include "newhome.h"
+#include "menu.h"
+#include "ruins.h"
+#include "snowdin.h"
+#include "waterfall.h"
+
+static int ev_sign(Ev *c) {
+ EV_BEGIN;
+ if (c->self && c->self->text) SAY(FACE_NONE, c->self->text);
+ EV_END;
+}
+
+void say_on_entry(const char *text) {
+ static Obj note;
+ note.text = text;
+ event_start(EV_SIGN, ¬e);
+}
+
+static char save_text[200];
+static int ev_save(Ev *c) {
+ EV_BEGIN;
+ /* its words, the HP restored, then SAVE */
+ if (sv.hp < sv.maxhp) snprintf(save_text, sizeof save_text, "%s|* (HP fully restored.)", c->self ? c->self->text : "");
+ else snprintf(save_text, sizeof save_text, "%s", c->self ? c->self->text : "");
+ sv.hp = sv.maxhp;
+ SAY(FACE_NONE, save_text);
+ menu_save_open();
+ WAIT_UNTIL(g_mode == MODE_WORLD);
+ EV_END;
+}
+
+static int ev_phone(Ev *c) {
+ static const char *const opts[] = {"Say Hello", "Puzzle Help", "About Yourself", "Call Her \"Mom\"", "Flirt"};
+ EV_BEGIN;
+ NARRATE("* Dialing...");
+ if (c->v[0] == 1) { /* Papyrus */
+ SAY(FACE_PAPYRUS, "`0* HELLO! THIS IS PAPYRUS!\n* HOW DID YOU GET THIS NUMBER?|`2* OH, RIGHT, I GAVE IT TO YOU!\n* NYEH HEH HEH!");
+ return 1;
+ }
+ CHOOSE(opts, 5);
+ switch (CHOSEN()) {
+ case 0:
+ set_flag(F_CALL_HELLO, flag(F_CALL_HELLO) + 1);
+ if (flag(F_CALL_HELLO) == 1)
+ SAY(FACE_TORIEL, "`0* TORIEL here.|* Just saying hello?|`2* Well, hello to you too!");
+ else if (flag(F_CALL_HELLO) == 2)
+ SAY(FACE_TORIEL, "`0* This is TORIEL.|* You want to say hello again?|`2* 'Salutations!'|`0* Is that enough?");
+ else if (flag(F_CALL_HELLO) == 3)
+ SAY(FACE_TORIEL, "`0* TORIEL here.|`1* Bored? I should have\n left you a book.|`0* Pretend to be royalty!|`2* Rule the leaf pile!");
+ else
+ SAY(FACE_TORIEL, "`0* TORIEL here.|`1* I have little to say...|`0* But it is nice to hear\n from you.");
+ break;
+ case 1:
+ SAY(FACE_TORIEL, "`0* TORIEL here.|* A puzzle?|`1* Please wait for me; we\n can solve it together.");
+ break;
+ case 2:
+ SAY(FACE_TORIEL, "`0* TORIEL here.|* About me?|`2* Oh, there is not much to\n tell. I worry a lot!");
+ break;
+ case 3:
+ set_flag(F_CALL_MOM, 1);
+ SAY(FACE_TORIEL, "`0* This is TORIEL.|`3* Huh? Did you just call me...\n \"Mom\"?|`8* Well... I suppose...|* Would that make you happy?|* To call me... \"Mother?\"|`2* Well then, call me whatever\n you like!");
+ break;
+ case 4:
+ set_flag(F_CALL_FLIRT, flag(F_CALL_FLIRT) + 1);
+ if (flag(F_CALL_FLIRT) == 1)
+ SAY(FACE_TORIEL, "`3* ...oh?|`8* Ha ha ha!|`2* How sweet of you.|`0* But you can do better than\n an old lady like me.");
+ else if (flag(F_CALL_MOM))
+ SAY(FACE_TORIEL, "`3* Oh dear, are you serious...?|* And after you said you want\n to call me \"mother...\"|`1* You are an... ...\n \"interesting\" child.");
+ else
+ SAY(FACE_TORIEL, "`3* Oh dear, are you serious...?|`1* I do not know if this is\n pathetic, or endearing.");
+ break;
+ default:
+ break;
+ }
+ EV_END;
+}
+
+static int ev_encounter(Ev *c) {
+ EV_BEGIN;
+ g_exclaim = 18;
+ WAIT(18);
+ g_exclaim = 0;
+ BATTLE(c->v[0]);
+ snow_battle_done(c->v[0]);
+ wf_battle_done(c->v[0]);
+ EV_END;
+}
+
+int event_run(Ev *c) {
+ switch (c->id) {
+ case EV_ENCOUNTER: return ev_encounter(c);
+ case EV_SIGN: return ev_sign(c);
+ case EV_SAVE: return ev_save(c);
+ case EV_PHONE_MENU: return ev_phone(c);
+ }
+ int r = ruins1_event(c);
+ if (r >= 0) return r;
+ r = ruins2_event(c);
+ if (r >= 0) return r;
+ r = ruins3_event(c);
+ if (r >= 0) return r;
+ r = snow1_event(c);
+ if (r >= 0) return r;
+ r = snow2_event(c);
+ if (r >= 0) return r;
+ r = town_event(c);
+ if (r >= 0) return r;
+ r = wf1_event(c);
+ if (r >= 0) return r;
+ r = wf2_event(c);
+ if (r >= 0) return r;
+ r = wf3_event(c);
+ if (r >= 0) return r;
+ r = ht1_event(c);
+ if (r >= 0) return r;
+ r = ht2_event(c);
+ if (r >= 0) return r;
+ r = ht3_event(c);
+ if (r >= 0) return r;
+ r = res_event(c);
+ if (r >= 0) return r;
+ r = core_event(c);
+ if (r >= 0) return r;
+ r = nh_event(c);
+ if (r >= 0) return r;
+ return 1;
+}
diff --git a/games/undertale/src/events.h b/games/undertale/src/events.h
new file mode 100644
index 00000000..cbfbf5a2
--- /dev/null
+++ b/games/undertale/src/events.h
@@ -0,0 +1,71 @@
+/* Undertale: the events, by id. */
+#ifndef UT_EVENTS_H
+#define UT_EVENTS_H
+/* who is who in a room (Obj.name), for the events to find */
+enum {
+ ACTOR_NONE, ACTOR_TORIEL, ACTOR_FLOWEY, ACTOR_DUMMY, ACTOR_DOOR, ACTOR_SPIKES, ACTOR_SPIKES2, ACTOR_FROGGIT,
+ ACTOR_PILLAR, ACTOR_NAPSTA, ACTOR_ROCK, ACTOR_ROCK2, ACTOR_ROCK3, ACTOR_SWITCH, ACTOR_SWITCH2, ACTOR_SWITCH3,
+ ACTOR_CANDY, ACTOR_TRIGGER, ACTOR_TRIGGER2, ACTOR_ITEM, ACTOR_CHAIR, ACTOR_PIE, ACTOR_ANY,
+ /* Snowdin */
+ ACTOR_SANS, ACTOR_PAPYRUS, ACTOR_DOGGO, ACTOR_DOGAMY, ACTOR_DOGARESSA, ACTOR_GREATERDOG, ACTOR_BALL, ACTOR_ORB,
+ ACTOR_TILE, ACTOR_DOG, ACTOR_BLOCK, ACTOR_TRIGGER3,
+ /* Waterfall */
+ ACTOR_MK, ACTOR_UNDYNE, ACTOR_SEED, ACTOR_BLOOM, ACTOR_SPEAR, ACTOR_ONION, ACTOR_DEBRIS, ACTOR_SHYREN,
+ ACTOR_MADDUMMY, ACTOR_BIRD, ACTOR_TEMMIE,
+ /* Hotland */
+ ACTOR_ALPHYS, ACTOR_METTATON, ACTOR_GUARDS, ACTOR_VENT, ACTOR_LASER, ACTOR_RG01, ACTOR_RG02, ACTOR_BOMB, ACTOR_FIELD,
+ ACTOR_MUFFET
+};
+
+enum {
+ EV_NONE,
+ EV_SIGN, /* a sign or anything that only says its text (self->arg: the text, in events.c) */
+ EV_PHONE_MENU, /* CELL: calling Toriel */
+ EV_SAVE, /* a SAVE Point */
+ EV_ENCOUNTER, /* a random encounter: v[0] its formation */
+ EV_FLOWEY_MEET, EV_RUINS1_TORIEL, EV_RUINS2_TORIEL, EV_RUINS3_TORIEL, EV_RUINS3_SWITCH, EV_RUINS4_TORIEL,
+ EV_RUINS4_DUMMY, EV_RUINS5_TORIEL, EV_RUINS6_TORIEL, EV_RUINS6_PILLAR, EV_RUINS6_REVEAL, EV_RUINS7_CALL,
+ EV_RUINS7_FROGGIT,
+ /* ruins2.c */
+ EV_FALLING, EV_COLOR_SWITCH, EV_CANDY, EV_CALL, EV_ROCK_TALK, EV_ROCK_OFF, EV_NAPSTA_SLEEP, EV_NAPSTA_LIE, EV_WEB,
+ EV_FROGGIT, EV_ITEM, EV_ONE_SWITCH, EV_VEGGIE, EV_TORIEL_TREE,
+ /* ruins3.c */
+ EV_HOME_TOUR, EV_TORIEL_CHAIR, EV_BED, EV_STAIRS, EV_BASEMENT_TALK, EV_TORIEL_DOOR, EV_FLOWEY_END, EV_RUINS_DOOR,
+ /* snowdin1.c */
+ EV_BRANCH, EV_SANS_INTRO, EV_SANS_TALK, EV_PAP_SENTRY, EV_DOGGO, EV_DOGGO_BELL, EV_SNOWMAN, EV_ELECMAZE, EV_MAZE_ZAP,
+ EV_NICECREAM, EV_BALL_HOLE, EV_WORDSEARCH, EV_BOX, EV_FISHROD, EV_SNOW_TALKBACK,
+ /* snowdin2.c */
+ EV_SNOW_SWITCH, EV_DOGI, EV_XOXO_SWITCH, EV_PAP_XOXO, EV_TILES, EV_XOXO_FALL, EV_TREESLIDE, EV_POFF, EV_GREATERDOG,
+ EV_GAUNTLET, EV_MYSTERY_DOOR, EV_SNOW_SANS_CLIFF,
+ /* snowtown.c */
+ EV_SHOP, EV_INN, EV_GRILLBYS, EV_PAP_FIGHT, EV_PAP_HOUSE, EV_PAP_DATE, EV_TOWN_DOOR, EV_COUCH,
+ /* wf1.c */
+ EV_WF_MK, EV_WF_ITEM, EV_WF_STREAM_FALL, EV_WF_UNDYNE1, EV_BELL, EV_WF_GRASS, EV_WF_TELESCOPE, EV_WF_BIRD,
+ EV_WF_ONION, EV_PIANO, EV_SHYREN, EV_STATUE, EV_UMBRELLA, EV_MK_RAIN, EV_MK_TALK, EV_WF_BRIDGE_END,
+ /* wf2.c */
+ EV_DUMP_WAKE, EV_COOLER, EV_MAD_DUMMY, EV_NAPSTA_ROOM, EV_LOCKED_HOUSE, EV_SNAILS, EV_GERSON, EV_TEM_SHOP, EV_TEMMIE,
+ /* wf3.c */
+ EV_DEADEND, EV_MK_CHASM, EV_ARENA, EV_UNDYNE_CATCH, EV_PAP_NEON, EV_HOT_UNDYNE, EV_HOT_COOLER,
+ /* ht1.c */
+ EV_HT_LAB, EV_HT_FRIDGE, EV_VENT, EV_HT_CALL, EV_SHOOT, EV_HT_COOK, EV_HT_ELEVATOR, EV_LASER_HIT, EV_HT_ITEM,
+ /* ht2.c */
+ EV_HOTDOG, EV_HEATS, EV_APRON, EV_BELT_SWITCH, EV_HT2_CALL, EV_VENTPUZ, EV_GUARDS, EV_NEWS, EV_BOMB, EV_HT2_ELEVATOR,
+ /* ht3.c */
+ EV_BAKESALE3, EV_SHOOT3, EV_HT3_CALL, EV_MUFFET, EV_MUSICAL, EV_TILE, EV_TILES3, EV_HEATS3, EV_HT3_ELEVATOR,
+ EV_NICECREAM3,
+ /* resort.c */
+ EV_RES_SANS, EV_RES_SHOP, EV_RES_DOOR, EV_RES_DESK,
+ /* core.c */
+ EV_CORE_APPROACH, EV_CORE_CALL, EV_CORE_ORDER, EV_CORE_BRANCH, EV_CORE_POWER, EV_CORE_LASER, EV_CORE_SWITCH,
+ EV_CORE_MADJICK, EV_CORE_KNIGHT, EV_CORE_SAGE, EV_CORE_WARRIOR, EV_CORE_WSWITCH, EV_CORE_FIELD, EV_CORE_TRASH,
+ EV_CORE_LIFT, EV_CORE_MTT, EV_CORE_HALL,
+ /* newhome.c */
+ EV_NH_LIFT, EV_NH_KEY, EV_NH_CHAIN, EV_NH_UP, EV_NH_GIFT, EV_NH_STORY, EV_NH_SANS, EV_NH_THRONE, EV_NH_COFFIN,
+ EV_NH_BARRIER, EV_NH_CALL,
+ EV_COUNT
+};
+/* Toriel's calls on the way (EV_CALL's v[0]) */
+enum { CALL_FLAVOR, CALL_FLAVOR2, CALL_ALLERGY, CALL_ITEMS };
+/* a text when Frisk comes in (a room's setup) */
+void say_on_entry(const char *text);
+#endif
diff --git a/games/undertale/src/flags.h b/games/undertale/src/flags.h
new file mode 100644
index 00000000..26a1fad6
--- /dev/null
+++ b/games/undertale/src/flags.h
@@ -0,0 +1,196 @@
+/* Undertale: what the game remembers (the original's global.flag), named.
+ * Saved: new flags go at the end, before FLAG_COUNT. */
+#ifndef UT_FLAGS_H
+#define UT_FLAGS_H
+enum {
+ F_NONE,
+ /* the Ruins */
+ F_FLOWEY_MET, /* the first meeting is over */
+ F_TORIEL_MET,
+ F_RUINS1_DONE, /* Toriel waited at the entrance's stairs */
+ F_RUINS2_DONE, /* the middle road puzzle */
+ F_RUINS3_SW1, F_RUINS3_SW2, F_RUINS3_DONE,
+ F_DUMMY, /* 1 talked, 2 fought, 3 fled, 4 bored */
+ F_RUINS5_DONE, /* the spikes, hand in hand */
+ F_RUINS6_DONE, /* the independence test */
+ F_PHONE, /* Toriel's cell phone */
+ F_LEAFPILE_CALL,
+ F_CANDY_TAKEN, /* Monster Candy taken (to 4) */
+ F_CANDY_SPILT,
+ F_RUINS9_ROCK, /* the first rock pushed home */
+ F_FLAVOR_CALL, F_FLAVOR, /* 1 cinnamon, 2 butterscotch */
+ F_RUINS11_ROCK, /* the second rock puzzle: the reluctant rock moved */
+ F_RUINS11_ASK,
+ F_NAPSTA, /* 1 spared, 2 killed, 3 fled */
+ F_DONUT_BOUGHT, F_CIDER_BOUGHT,
+ F_ITEM_CALL,
+ F_FROGGIT_YELLOW, /* 0 yellow names, 1 white, 2 pink */
+ F_RIBBON, F_TOYKNIFE,
+ F_ONESWITCH, F_ROTATE1, F_ROTATE2, F_ROTATE3, F_ROTATE4,
+ F_ALLERGY_CALL,
+ F_TORIEL_HOME, /* Toriel found Frisk at her tree */
+ F_PIE, /* the pie slice taken */
+ F_TORIEL_TALKS, /* questions asked her by the fire */
+ F_TORIEL_BASEMENT, /* she went downstairs */
+ F_BASEMENT_TALK, /* 1..3: what she said along the way */
+ F_TORIEL_FIGHT, /* 1 spared, 2 killed */
+ F_FLOWEY_END,
+ F_RUINS_EXIT,
+ F_KILLS_RUINS,
+ F_SKIPS, /* text pages skipped with X (a Froggit notices) */
+ F_FROG_TIP,
+ F_SAVE_SEEN,
+ F_FALL_LEAVES,
+ F_SPIDER_CALL,
+ F_ASRIEL_ROOM,
+ F_MIRROR,
+ F_LIGHTS,
+ F_RUINS_SAVE_BED,
+ F_HARDMODE,
+ F_FIRST_FROGGIT,
+ F_TORIEL_SNAIL,
+ F_WHIMSUN_TALK,
+ F_CALL_HELLO, F_CALL_MOM, F_CALL_FLIRT,
+ F_NAPSTA_LIE, /* Napstablook went away from the room under the one switch puzzle */
+ F_HOME_TOUR, /* Toriel showed Frisk the house */
+ F_SNAIL_TALK,
+ F_BED_SLEPT, /* 1 slept; 2 the pie taken */
+ /* Snowdin */
+ F_BRANCH, /* the branch broke behind Frisk */
+ F_SANS_MET, /* the gate, the lamp: Sans and Papyrus met */
+ F_SANS_FAVOR, /* Sans asked his favour */
+ F_PAP_SENTRY, /* Papyrus saw the human (not a rock) */
+ F_DOGGO, /* 1 spared, 2 killed */
+ F_SANS_HINT, /* the blue stop sign */
+ F_SNOWMAN, /* pieces taken */
+ F_ELECMAZE, /* 1 Papyrus zapped and gave the orb, 2 crossed */
+ F_SANS_COOL, /* 1 cool, 2 uncool */
+ F_NICECREAM, /* the Nice Cream guy met */
+ F_FRIED_SNOW, /* times Sans named his price */
+ F_BALLGAME, /* the flag won (1..7) */
+ F_WORDSEARCH, /* 1 crossword, 2 junior jumble */
+ F_SPAGHETTI,
+ F_SNOWPUZZLE, /* the switch under the snow */
+ F_DOGI, /* 1 spared, 2 killed */
+ F_XOXO1, F_XOXO2, F_XOXO3,
+ F_PAP_XOXO1, /* asked about the spaghetti */
+ F_TILES, /* the multicolor tiles explained */
+ F_LESSERDOG, /* 1 spared, 2 killed */
+ F_POFF_GOLD,
+ F_GREATERDOG, /* 1 spared, 2 killed */
+ F_GAUNTLET,
+ F_SNOW_SANS_TALK, /* Sans's talks along the way, a bit each */
+ F_PAPYRUS, /* 1 spared, 2 killed, 3 fled */
+ F_PAP_CAPTURED, /* times Papyrus captured Frisk */
+ F_PAP_DATE, /* 1 promised, 2 done */
+ F_GRILLBYS,
+ F_INN, /* nights at the inn */
+ F_KILLS_SNOWDIN,
+ F_BOX0, F_BOX1, F_BOX2, F_BOX3, F_BOX4, F_BOX5, F_BOX6, F_BOX7, F_BOX8, F_BOX9, /* the Dimensional Box */
+ F_SNOW_EXIT,
+ F_TOWN_HOUSE, /* Papyrus's house open */
+ F_SNOWDIN_BOSSES, /* boss fights (Bisicle's price) */
+ F_SNOW_SANS_LAMP, /* hidden behind the lamp */
+ F_COUCH_GOLD,
+ F_FISH, /* the fishing rod's note read */
+ F_SHOP_TALK,
+ F_SNOWDRAKE_DEAD, /* (then Chilldrake comes instead) */
+ /* Waterfall */
+ F_WF_MK, /* Monster Kid met by the first echo flower */
+ F_WF_GRILLBYS, /* Sans took Frisk to Grillby's from his station */
+ F_WF_TUTU, F_WF_QUICHE, F_WF_BALLET, F_WF_PUNCH, F_WF_ARTIFACT, /* found */
+ F_WF_UNDYNE1, /* Undyne seen from the tall grass */
+ F_SEEDS1, F_SEEDS2, F_SEEDS3, /* the bridges of bridge seeds, made */
+ F_CHASE1, /* Undyne's first chase, over */
+ F_WF_TELESCOPE,
+ F_WF_ONION, /* 1 met, 2 parted */
+ F_PIANO, /* its song played: the door is open */
+ F_UMBRELLA, /* 1 carried, 2 left with the statue */
+ F_MK_RAIN, /* Monster Kid walked with Frisk in the rain */
+ F_SHYREN, /* 1 spared, 2 killed */
+ F_WF_BRIDGE, /* the bridge cut: Frisk fell */
+ F_KILLS_WATERFALL,
+ F_WF_BIRD, /* the small bird: 1 it carried Frisk west (it waits there), 2 back east */
+ F_WF_DUMP, /* woke up in the garbage dump */
+ F_WF_COOLER, /* the cooler's Astronaut Food taken: how many */
+ F_MAD_DUMMY, /* its fight, over (Napstablook came) */
+ F_NAPSTA_HOME, /* 1 invited home, 2 lay on the floor with them */
+ F_SNAIL_RACE, /* Thundersnail races run */
+ F_TEM_VILLAGE, /* found */
+ F_WF_EAST1, /* (unused: the mushroom maze's far side once ended the game) */
+ F_DEADEND, /* Undyne at the dead end, Monster Kid in her way: the way north lit */
+ F_MK_CHASM, /* Monster Kid on the chasm's bridge: 1 hanging, 2 helped up, 3 let fall, 4 left behind */
+ F_UNDYNE, /* her fight: 1 fled (she chases), 2 killed, 3 fallen in the heat, 4 given water */
+ F_WATER_CUP, /* a cup of the cooler's water: 1 carried, 2 poured */
+ F_PAP_NEON, /* Papyrus's call by the neon sign */
+ F_HOTLAND, /* (unused: Hotland's first room once ended the game) */
+ /* Hotland */
+ F_HT_LAB, /* the Lab: 1 Alphys met (Mettaton's quiz), 2 the phone upgraded */
+ F_HT_NOODLES, /* the Instant Noodles from Alphys's fridge */
+ F_HT_PAN, /* the Burnt Pan taken */
+ F_HT_CALL_LASER, /* Alphys's calls: the lasers, the puzzle hub, the vents past the door, the CORE */
+ F_HT_CALL_HUB,
+ F_HT_CALL_ISLE,
+ F_HT_CALL_CORE,
+ F_HT_PUZ_R, /* the shooting puzzles solved: the right one, the left one */
+ F_HT_PUZ_L,
+ F_HT_BLUE_OFF, /* the blue laser before the left puzzle switched off */
+ F_HT_COOK, /* the cooking show: 1 the substitute reached, 2 out of time */
+ F_HT_FLOOR1, /* the elevator up from Hotland's first floor taken */
+ F_HT_HEATS, /* Heats Flamesman's name heard */
+ F_HT_APRON, /* the Stained Apron taken */
+ F_HT_CALL_BELT, /* Alphys's calls: the belt puzzle, by the CORE again */
+ F_HT_CALL_CORE2,
+ F_HT_CALL_BOZ,
+ F_HT_BELT, /* the belt puzzle's force field off */
+ F_HT_VENTPUZ, /* the vent puzzle crossed */
+ F_HT_GUARDS, /* the Royal Guards: 1 spared, 2 killed */
+ F_HT_NEWS, /* Mettaton's news report: 1 every bomb defused, 2 out of time */
+ F_HT_FLOOR2, /* the elevator up from the second floor taken */
+ F_HT_CALL_HUB3, /* Alphys's call by the third floor's vents */
+ F_HT3_PUZ_N, /* the third floor's shooting puzzles: north, south */
+ F_HT3_PUZ_S,
+ F_HT_ANIME, /* Alphys asked Frisk to watch anime with her: 1 yes, 2 no */
+ F_HT_MUFFET, /* Muffet: 1 spared, 2 killed */
+ F_HT_MUSICAL, /* Mettaton's musical seen (and the trapdoor) */
+ F_HT_TILES3, /* the tile puzzle under the stage: 1 solved, 2 out of time; then Mettaton fought */
+ F_HT_HEATS3, /* Heats Flamesman asked about his name */
+ F_HT_FLOOR3, /* the MTT Resort reached */
+ F_RES_SANS, /* dinner with Sans at the Resort */
+ F_RES_TEA, /* room service brought: the Sea Tea (door 1), the Cinnamon Bunny (door 3) */
+ F_RES_BUN,
+ F_RES_ROOM, /* the hotel's room: 1 paid for, 2 slept in */
+ F_RES_DOG, /* the Hot Dog slid under door 4 (for a Hush Puppy) */
+ F_CORE_IN, /* the CORE's approach: the black shapes seen backing away inside */
+ F_CORE_CALLS, /* Alphys's calls in the CORE, a bit each (core.c's CALL_*), then the rest */
+ F_CORE_CALLS2,
+ F_CORE_MADJICK, /* the doorway's ambush, the room north of the crossroads: 1 spared, 2 killed */
+ F_CORE_KNIGHT,
+ F_CORE_SWITCH, /* the switch by the laser ambush pressed */
+ F_CORE_POWER, /* the laser bridge: 1 its power cut, 2 crossed */
+ F_CORE_SAGE, /* the sage's path's puzzle solved */
+ F_CORE_WARRIOR, /* the warrior's path: its fights won (1 to 3), 4 its switch flipped */
+ F_CORE_FIELD, /* the force field before The End down */
+ F_CORE_TRASH1, /* the trash cans: the Glamburger, the 100G */
+ F_CORE_TRASH2,
+ F_CORE_ASTIG, /* Astigmatism met on its own (just once) */
+ F_CORE_END, /* the Core End reached (the lobby's elevator works) */
+ F_CORE_MTT, /* Mettaton EX: 1 his show a hit, 2 killed */
+ F_CORE_HALL, /* Alphys's talk by the last elevator */
+ F_CORE_DONE, /* the elevator up to the castle taken */
+ F_NH_IN, /* New Home reached */
+ F_NH_KEY1, /* Asgore's home: the kitchen's key, the hallway's */
+ F_NH_KEY2,
+ F_NH_CHAIN, /* the chain on the stairs down unlocked */
+ F_NH_DAGGER, /* the presents opened: the Worn Dagger, the Heart Locket */
+ F_NH_LOCKET,
+ F_NH_STORY, /* the basement's story: how many parts told */
+ F_NH_LIFT, /* the basement's far end reached (the street's elevator works) */
+ F_NH_SANS, /* Sans's judgment heard */
+ F_NH_THRONE, /* Asgore met in the throne room */
+ F_NH_ASGORE, /* Asgore, beaten: 1 spared (Flowey killed him), 2 killed */
+ F_NH_FLOWEY, /* Omega Flowey, beaten: 1 spared, 2 killed */
+ F_NH_DONE, /* through the barrier: the end */
+ FLAG_COUNT = 256
+};
+#endif
diff --git a/games/undertale/src/flowey.c b/games/undertale/src/flowey.c
new file mode 100644
index 00000000..e19fa64c
--- /dev/null
+++ b/games/undertale/src/flowey.c
@@ -0,0 +1,210 @@
+/* The first battle: Flowey and his "friendliness pellets", then Toriel. A scripted battle: no buttons, the SOUL
+ * free in a small box, Flowey's words in his bubble (from the Undertale Wiki's records of the game). */
+#include
+#include
+#include "battle.h"
+#include "game.h"
+#include "gfx.h"
+#include "monsters.h"
+#include "res.h"
+
+enum {
+ F_TALK1, F_PELLETS, F_AFTER, F_RING, F_FIREBALL, F_TORIEL_IN, F_TORIEL_TALK, F_DONE
+};
+/* bt.state: [0] step, [1] times dodged, [2] Flowey's face (a sprite), [3] Flowey's x offset, [4] Toriel's x */
+static const char *talk_text;
+static int ring_r;
+
+static void bubble(const char *s) {
+ talk_text = s;
+ writer_start(&bt.bw, s, 0, 0, FONT_SMALL);
+ bt.bw.line_h = 9;
+}
+
+static const char intro_text[] =
+ "See that\nheart? That\nis your SOUL,\nthe very\nculmination\nof your\nbeing!|"
+ "Your SOUL\nstarts off\nweak, but can\ngrow strong\nif you gain a\nlot of LV.|"
+ "What's LV\nstand for?\nWhy, LOVE,\nof course!|"
+ "You want\nsome LOVE,\ndon't you?|"
+ "`7Don't worry,\nI'll share\nsome with\nyou!";
+static const char share_text[] =
+ "Down here,\nLOVE is\nshared\nthrough...|"
+ "Little\nwhite...\n\"friendliness\npellets.\"|"
+ "Are you\nready?|"
+ "Move around!\nGet as many\nas you can!";
+static const char hit_text[] =
+ "`4You idiot.|"
+ "In this\nworld, it's\nkill or BE\nkilled.|"
+ "Why would\nANYONE pass\nup an\nopportunity\nlike this!?|"
+ "`3Die.";
+static const char miss1_text[] = "`6Hey buddy,\nyou missed\nthem.|`0Let's try\nagain, okay?";
+static const char miss2_text[] = "`6Is this a\njoke? Are you\nbraindead?|RUN. INTO.\nTHE.\nfriendliness\npellets";
+static const char miss3_text[] = "`2You know\nwhat's going\non here,\ndon't you?|You just\nwanted to see\nme suffer.|`3Die.";
+static const char toriel_text[] =
+ "What a\nterrible\ncreature,\ntorturing\nsuch a poor,\ninnocent\nyouth...|"
+ "Ah, do not\nbe afraid, my\nchild.|"
+ "I am #YTORIEL#X,\ncaretaker of\nthe #YRUINS#X.|"
+ "I pass\nthrough this\nplace every\nday to see if\nanyone has\nfallen down.|"
+ "You are the\nfirst human\nto come here\nin a long\ntime.|"
+ "Come! I will\nguide you\nthrough the\ncatacombs.";
+
+static const uint16_t faces[] = {SPR_FLOWEYNICE, SPR_FLOWEYNICESIDE, SPR_FLOWEYSASSY, SPR_FLOWEYEVIL, SPR_FLOWEYGRIN,
+ SPR_FLOWEYPLAIN, SPR_FLOWEYPISSED, SPR_FLOWEYWINK, SPR_FLOWEYLAUGH, SPR_FLOWEYHURT};
+
+void flowey_battle_start(void) {
+ bt.scripted = true;
+ bt.state[0] = F_TALK1;
+ bt.state[2] = 0;
+ board_set(84, 76);
+ bt.x0 = bt.tx0, bt.y0 = bt.ty0, bt.x1 = bt.tx1, bt.y1 = bt.ty1;
+ bt.sx = board_cx() * FX, bt.sy = board_cy() * FX + 8 * FX;
+ bubble(intro_text);
+}
+
+static void pellets(void) {
+ /* five pellets in an arc over Flowey, then at the SOUL, a little aimed */
+ bullets_clear();
+ for (int k = 0; k < 5; k++) {
+ int x = 160 + (k - 2) * 14, y = 72 - (k == 0 || k == 4 ? 0 : k == 2 ? 14 : 9);
+ Bullet *b = bullet_new(SPR_SPINBULLET, x * FX, y * FX, 0, 0);
+ if (b) b->kind = BK_SCRIPT, b->a = (int16_t)(20 + k * 3), b->anim = 3, b->dmg = 19, b->flags = BF_NOCLIP;
+ }
+}
+
+void flowey_battle_update(uint32_t keys, uint32_t pressed) {
+ int st = bt.state[0];
+ if (st != F_FIREBALL && st != F_TORIEL_IN) soul_update(keys);
+ switch (st) {
+ case F_TALK1:
+ case F_AFTER:
+ case F_TORIEL_TALK:
+ bt.state[2] = bt.bw.face_expr;
+ if (!writer_update(&bt.bw, pressed)) {
+ if (st == F_TORIEL_TALK) {
+ bt.state[0] = F_DONE;
+ bt.result = BR_SCRIPTED;
+ return;
+ }
+ if (talk_text == intro_text) {
+ bubble(share_text);
+ bt.state[2] = 7;
+ } else if (talk_text == hit_text || talk_text == miss3_text) {
+ bt.state[0] = F_RING, bt.t = 0;
+ ring_r = 70;
+ bullets_clear();
+ for (int k = 0; k < 24; k++) {
+ Bullet *b = bullet_new(SPR_SPINBULLET, 0, 0, 0, 0);
+ if (b) b->kind = BK_SCRIPT, b->a = (int16_t)(k * 256 / 24), b->anim = 3, b->flags = BF_HARMLESS | BF_NOCLIP;
+ }
+ bt.state[2] = 8;
+ } else {
+ bt.state[0] = F_PELLETS, bt.t = 0;
+ pellets();
+ }
+ }
+ break;
+ case F_PELLETS: {
+ int alive = 0;
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT) continue;
+ alive++;
+ if (b->a > 0) {
+ b->t++;
+ if (b->t % 3 == 0) b->frame++;
+ if (--b->a == 0) {
+ int dx = bt.sx - b->x, dy = bt.sy - b->y, d = abs_i(dx) + abs_i(dy);
+ if (!d) d = 1;
+ b->vx = dx * FX * 2 / d, b->vy = dy * FX * 2 / d;
+ }
+ continue;
+ }
+ /* a little homing */
+ b->vx += (bt.sx > b->x ? 1 : -1) * FX / 32;
+ b->x += b->vx, b->y += b->vy;
+ if (++b->t % 3 == 0) b->frame++;
+ if (b->y > (bt.y1 + 10) * FX || b->x < 0 || b->x > SCREEN_W * FX) b->kind = BK_NONE;
+ int dx = b->x / FX - soul_x(), dy = b->y / FX - soul_y();
+ if (dx * dx + dy * dy < 36) {
+ /* hit: 19, down to 1 */
+ sv.hp = 1;
+ bullets_clear();
+ bt.state[0] = F_AFTER;
+ bubble(hit_text);
+ bt.state[2] = 4;
+ return;
+ }
+ }
+ if (!alive) {
+ bt.state[1]++;
+ bt.state[0] = F_AFTER;
+ bubble(bt.state[1] == 1 ? miss1_text : bt.state[1] == 2 ? miss2_text : miss3_text);
+ }
+ break;
+ }
+ case F_RING:
+ /* the ring closes in, slowly; Flowey laughs */
+ if (bt.t % 3 == 0 && ring_r > 14) ring_r--;
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT) continue;
+ b->x = bt.sx + ring_r * gfx_cos(b->a + bt.t) * FX / 1024;
+ b->y = bt.sy + ring_r * gfx_sin(b->a + bt.t) * FX / 1024;
+ if (bt.t % 3 == 0) b->frame++;
+ }
+ if (ring_r <= 14 && bt.t > 150) bt.state[0] = F_FIREBALL, bt.t = 0;
+ break;
+ case F_FIREBALL:
+ /* a fireball from the right knocks Flowey away */
+ if (bt.t == 10) {
+ bullets_clear();
+ bt.state[2] = 9;
+ }
+ if (bt.t > 10) bt.state[3] = (int16_t)(bt.state[3] - 6);
+ if (bt.t == 30) sv.hp = sv.maxhp;
+ if (bt.t > 40) bt.state[0] = F_TORIEL_IN, bt.t = 0, bt.state[4] = 340;
+ break;
+ case F_TORIEL_IN:
+ bt.state[4] = (int16_t)max_i(160, bt.state[4] - 4);
+ if (bt.state[4] == 160 && bt.t > 30) {
+ bt.state[0] = F_TORIEL_TALK;
+ bubble(toriel_text);
+ }
+ break;
+ }
+}
+
+void flowey_battle_draw(void) {
+ int st = bt.state[0];
+ int fx = 160 + bt.state[3];
+ if (st <= F_FIREBALL) {
+ int e = bt.state[2];
+ if (e < 0 || e > 9) e = 0;
+ uint16_t s = faces[e];
+ int talking = writer_talking(&bt.bw);
+ gfx_sprite(s, talking ? (int)(g_frame / 4) % 2 : 0, fx - 21, 72, 0, 255);
+ }
+ if (st == F_FIREBALL && bt.t < 12) gfx_sprite(SPR_FIREBULLET, (int)(g_frame / 2) % 2, 330 - bt.t * 15, 90, 0, 255);
+ if (st >= F_TORIEL_IN) gfx_sprite(SPR_TORIEL_B, 0, bt.state[4], 120, 0, 255);
+ gfx_rect(bt.x0, bt.y0, bt.x1 - bt.x0, bt.y1 - bt.y0, C_WHITE, 255);
+ gfx_rect(bt.x0 + 2, bt.y0 + 2, bt.x1 - bt.x0 - 4, bt.y1 - bt.y0 - 4, C_BLACK, 255);
+ bullets_draw();
+ gfx_sprite(SPR_HEART, 0, soul_x(), soul_y(), 0, 255);
+ /* the bubble */
+ if (st == F_TALK1 || st == F_AFTER || st == F_TORIEL_TALK) {
+ int bx = (st == F_TORIEL_TALK ? bt.state[4] : fx) + 30, by = 40;
+ gfx_rect(bx, by, 96, 72, C_WHITE, 255);
+ gfx_rect(bx - 6, by + 20, 6, 5, C_WHITE, 255);
+ bt.bw.x = bx + 5, bt.bw.y = by + 5;
+ writer_draw(&bt.bw, C_BLACK);
+ }
+ /* the HUD, without the buttons */
+ char s[32];
+ gfx_text(FONT_SMALL, 70, 201, "LV 1", C_WHITE, 255);
+ gfx_sprite(SPR_HPNAME, 0, 122, 202, 0, 255);
+ int w = sv.maxhp * 6 / 10 + 1, f = max_i(1, sv.hp * 6 / 10);
+ gfx_rect(137, 200, w, 10, rgb565(0xC00000), 255);
+ gfx_rect(137, 200, f, 10, C_YELLOW, 255);
+ snprintf(s, sizeof s, "%02d / %d", sv.hp, sv.maxhp);
+ gfx_text(FONT_SMALL, 137 + w + 8, 201, s, C_WHITE, 255);
+}
diff --git a/games/undertale/src/game.c b/games/undertale/src/game.c
new file mode 100644
index 00000000..70883ae5
--- /dev/null
+++ b/games/undertale/src/game.c
@@ -0,0 +1,158 @@
+/* The game's state, its modes and its frame (game.h). */
+#include
+#include
+#include "battle.h"
+#include "event.h"
+#include "game.h"
+#include "shoot.h"
+#include "shop.h"
+#include "gfx.h"
+#include "items.h"
+#include "menu.h"
+#include "res.h"
+#include "save.h"
+#include "text.h"
+#include "world.h"
+
+Save sv;
+int g_mode;
+uint32_t g_frame;
+bool g_running = true;
+static uint32_t prev_keys;
+
+static uint32_t seed = 0x2545F491u;
+uint32_t rnd(void) {
+ seed ^= seed << 13, seed ^= seed >> 17, seed ^= seed << 5;
+ return seed;
+}
+int rnd_n(int n) { return n > 0 ? (int)(rnd() % (uint32_t)n) : 0; }
+
+/* ---------------------------------------------------------------- stats (LOVE) */
+static const uint32_t exp_table[] = {0, 0, 10, 30, 70, 120, 200, 300, 500, 800, 1200, 1700, 2500, 3500, 5000, 7000,
+ 10000, 15000, 25000, 50000, 99999};
+int lv_exp_for(int lv) { return lv >= 1 && lv <= 20 ? (int)exp_table[lv] : 99999; }
+int lv_maxhp(int lv) { return lv >= 20 ? 99 : 16 + 4 * lv; }
+int lv_at(int lv) { return 8 + 2 * lv; }
+int lv_df(int lv) { return 10 + (lv - 1) / 4; }
+
+bool gain_exp(int exp) {
+ sv.exp = (uint32_t)min_i((int)sv.exp + exp, 99999);
+ bool up = false;
+ while (sv.lv < 20 && (int)sv.exp >= lv_exp_for(sv.lv + 1)) {
+ sv.lv++;
+ int old = sv.maxhp;
+ sv.maxhp = (int16_t)lv_maxhp(sv.lv);
+ sv.hp = (int16_t)(sv.hp + sv.maxhp - old);
+ sv.at = (uint8_t)lv_at(sv.lv), sv.df = (uint8_t)lv_df(sv.lv);
+ up = true;
+ }
+ return up;
+}
+int weapon_at(void) { return item_info(sv.weapon)->at + item_info(sv.armor)->at; } /* (the Cowboy Hat's too) */
+int armor_df(void) { return item_info(sv.armor)->df; }
+
+bool item_add(int item) {
+ for (int i = 0; i < NUM_ITEMS; i++)
+ if (!sv.items[i]) {
+ sv.items[i] = (uint8_t)item;
+ return true;
+ }
+ return false;
+}
+void item_remove_at(int slot) {
+ for (int i = slot; i < NUM_ITEMS - 1; i++) sv.items[i] = sv.items[i + 1];
+ sv.items[NUM_ITEMS - 1] = 0;
+}
+int item_count(void) {
+ int n = 0;
+ while (n < NUM_ITEMS && sv.items[n]) n++;
+ return n;
+}
+
+/* ---------------------------------------------------------------- a new game */
+void game_new(const char *name) {
+ char n[sizeof sv.name];
+ snprintf(n, sizeof n, "%s", name); /* (it can be sv.name) */
+ memset(&sv, 0, sizeof sv);
+ memcpy(sv.name, n, sizeof n);
+ memcpy(g_name, sv.name, sizeof g_name);
+ sv.lv = 1, sv.maxhp = sv.hp = 20, sv.at = (uint8_t)lv_at(1), sv.df = (uint8_t)lv_df(1);
+ sv.weapon = ITEM_STICK, sv.armor = ITEM_BANDAGE;
+ g_mode = MODE_WORLD;
+ world_enter(ROOM_AREA1, 120, 6010, DIR_DOWN);
+}
+
+void game_init(void) {
+ save_restore_copy();
+ /* back where Home left it, or the intro */
+ if (!save_resume_load()) menu_title_open();
+}
+
+void game_open_menu(void) { menu_open(); }
+
+void game_quit_save(void) { save_resume_write(); }
+
+/* ---------------------------------------------------------------- the frame */
+void game_frame(uint32_t keys) {
+ uint32_t pressed = keys & ~prev_keys;
+ prev_keys = keys;
+ g_frame++;
+ if (keys & K_HOME) {
+ if (g_mode == MODE_WORLD || g_mode == MODE_MENU || g_mode == MODE_BATTLE || g_mode == MODE_SHOP || g_mode == MODE_SHOOT)
+ game_quit_save();
+ g_running = false;
+ return;
+ }
+ if (g_mode == MODE_WORLD || g_mode == MODE_BATTLE || g_mode == MODE_MENU || g_mode == MODE_SHOP || g_mode == MODE_SHOOT)
+ sv.time++;
+ switch (g_mode) {
+ case MODE_WORLD:
+ world_update(keys, pressed);
+ event_update();
+ text_update(pressed);
+ break;
+ case MODE_BATTLE:
+ battle_update(keys, pressed);
+ event_update();
+ break;
+ case MODE_SHOP:
+ shop_update(keys, pressed);
+ if (g_mode == MODE_WORLD) event_update();
+ break;
+ case MODE_SHOOT:
+ shoot_update(keys, pressed);
+ if (g_mode == MODE_WORLD) event_update();
+ break;
+ default:
+ menu_update(keys, pressed);
+ if (g_mode == MODE_WORLD) event_update();
+ break;
+ }
+}
+
+void game_draw(void) {
+ gfx_begin();
+ switch (g_mode) {
+ case MODE_WORLD:
+ case MODE_MENU:
+ case MODE_SAVE:
+ world_draw();
+ if (g_fade) gfx_rect(0, 0, SCREEN_W, SCREEN_H, C_BLACK, g_fade);
+ text_draw();
+ if (g_mode != MODE_WORLD) menu_draw();
+ break;
+ case MODE_BATTLE:
+ battle_draw();
+ break;
+ case MODE_SHOP:
+ shop_draw();
+ break;
+ case MODE_SHOOT:
+ shoot_draw();
+ break;
+ default:
+ menu_draw();
+ break;
+ }
+ gfx_end();
+}
diff --git a/games/undertale/src/game.h b/games/undertale/src/game.h
new file mode 100644
index 00000000..a9e180a4
--- /dev/null
+++ b/games/undertale/src/game.h
@@ -0,0 +1,67 @@
+/* Undertale: the game's state and its modes (title, overworld, battle, menus).
+ *
+ * Inspired by Undertale by Toby Fox, not affiliated with him.
+ * Made by Mason Chen as part of NumPlay. */
+#ifndef UT_GAME_H
+#define UT_GAME_H
+#include
+#include
+#include "flags.h"
+
+static inline int abs_i(int v) { return v < 0 ? -v : v; }
+static inline int clamp_i(int v, int a, int b) { return v < a ? a : v > b ? b : v; }
+static inline int min_i(int a, int b) { return a < b ? a : b; }
+static inline int max_i(int a, int b) { return a > b ? a : b; }
+
+enum { MODE_TITLE, MODE_NAME, MODE_WORLD, MODE_BATTLE, MODE_MENU, MODE_SAVE, MODE_GAMEOVER, MODE_INTRO, MODE_END,
+ MODE_SHOP, MODE_SHOOT };
+
+#define NUM_ITEMS 8
+typedef struct {
+ /* what a SAVE keeps */
+ char name[8];
+ uint8_t lv;
+ uint32_t exp;
+ uint16_t gold;
+ int16_t hp, maxhp;
+ uint8_t at, df; /* the game's own: 10 at LV 1, shown less 10 */
+ uint8_t weapon, armor; /* ITEM_* */
+ uint8_t items[NUM_ITEMS]; /* ITEM_*, 0 none */
+ uint8_t phone[8]; /* CELL: who can be called */
+ uint16_t kills;
+ uint32_t time; /* frames played */
+ int16_t room, x, y; /* where it was saved */
+ uint8_t flags[FLAG_COUNT];
+} Save;
+extern Save sv; /* the game now */
+extern int g_mode;
+extern uint32_t g_frame;
+extern bool g_running;
+
+static inline int flag(int f) { return sv.flags[f]; }
+static inline void set_flag(int f, int v) { sv.flags[f] = (uint8_t)v; }
+
+void game_init(void);
+void game_frame(uint32_t keys);
+void game_draw(void);
+void game_quit_save(void); /* Home: the moment is kept, to come back to */
+void game_open_menu(void);
+void room_setup(int room, int from); /* rooms.c: the room's objects */
+void encounter_step(void); /* rooms.c: a step towards a random encounter */
+uint32_t rnd(void);
+int rnd_n(int n); /* 0 .. n - 1 */
+
+/* stats */
+int lv_maxhp(int lv);
+int lv_at(int lv);
+int lv_df(int lv);
+int lv_exp_for(int lv); /* the EXP to reach LV lv */
+bool gain_exp(int exp); /* true if LOVE went up */
+int weapon_at(void);
+int armor_df(void);
+
+/* items: inventory */
+bool item_add(int item);
+void item_remove_at(int slot);
+int item_count(void);
+#endif
diff --git a/games/undertale/src/gfx.c b/games/undertale/src/gfx.c
new file mode 100644
index 00000000..08fd184d
--- /dev/null
+++ b/games/undertale/src/gfx.c
@@ -0,0 +1,477 @@
+/* The renderer: a draw list rasterised in bands of GFX_BAND rows.
+ *
+ * Each command keeps its screen bounds (clipped); a band walks the commands
+ * that cross it, in order. Sprites are row RLE (res.h): a row is runs of
+ * (skip, length, pixels), so a band only reads the rows it shows. Alpha has
+ * 33 steps (0..32), applied with the usual RGB565 trick. */
+#include
+#include "blk.h"
+#include "gfx.h"
+#include "res.h"
+#include "world.h"
+
+enum { I_RECT, I_SPRITE, I_TILES, I_TEXT, I_LINE, I_CIRCLE, I_SPRITE_EX };
+
+typedef struct {
+ uint8_t kind, flags, alpha, font;
+ int16_t x0, y0, x1, y1; /* screen bounds, clipped: [x0, x1) x [y0, y1) */
+ int16_t cx0, cx1; /* the clip's columns */
+ int16_t x, y; /* sprite: its top-left; text: its top-left; line: start */
+ int16_t a, b; /* line: end; circle: radius; sprite_ex: scale x, scale y; text: shake, spacing */
+ uint16_t ref, sub; /* sprite, frame / text: pool offset, length */
+ uint16_t color; /* sprite: 0 its own colours, else 1 + RGB565 (in `extra`) */
+ uint16_t extra;
+ uint32_t seed;
+} Item;
+
+#ifdef HOST
+static uint16_t host_band[SCREEN_W * GFX_BAND];
+static uint16_t *band = host_band;
+#else
+static uint16_t *band;
+#endif
+void gfx_set_band_memory(uint16_t *px) {
+#ifdef HOST
+ band = px ? px : host_band;
+#else
+ band = px;
+#endif
+}
+
+static Item items[GFX_MAX_ITEMS];
+static int nitems;
+static char pool[GFX_TEXT_POOL];
+static int npool;
+static int clip_x0, clip_y0, clip_x1 = SCREEN_W, clip_y1 = SCREEN_H;
+int gfx_dropped;
+static int dropped_now;
+
+void gfx_begin(void) {
+ blk_frame();
+ nitems = 0, npool = 0, dropped_now = 0;
+ gfx_clip_all();
+}
+
+void gfx_clip(int x, int y, int w, int h) {
+ clip_x0 = x < 0 ? 0 : x, clip_y0 = y < 0 ? 0 : y;
+ clip_x1 = x + w > SCREEN_W ? SCREEN_W : x + w, clip_y1 = y + h > SCREEN_H ? SCREEN_H : y + h;
+}
+void gfx_clip_all(void) { clip_x0 = 0, clip_y0 = 0, clip_x1 = SCREEN_W, clip_y1 = SCREEN_H; }
+
+/* a new command over [x0, x1) x [y0, y1), clipped; NULL if it shows nothing */
+static Item *add(int kind, int x0, int y0, int x1, int y1) {
+ if (x0 < clip_x0) x0 = clip_x0;
+ if (y0 < clip_y0) y0 = clip_y0;
+ if (x1 > clip_x1) x1 = clip_x1;
+ if (y1 > clip_y1) y1 = clip_y1;
+ if (x0 >= x1 || y0 >= y1) return NULL;
+ if (nitems >= GFX_MAX_ITEMS) {
+ dropped_now++;
+ return NULL;
+ }
+ Item *it = &items[nitems++];
+ memset(it, 0, sizeof *it);
+ it->kind = (uint8_t)kind;
+ it->x0 = (int16_t)x0, it->y0 = (int16_t)y0, it->x1 = (int16_t)x1, it->y1 = (int16_t)y1;
+ it->cx0 = (int16_t)clip_x0, it->cx1 = (int16_t)clip_x1;
+ it->alpha = 255;
+ return it;
+}
+
+void gfx_rect(int x, int y, int w, int h, uint16_t color, uint8_t alpha) {
+ if (!alpha) return;
+ Item *it = add(I_RECT, x, y, x + w, y + h);
+ if (it) it->color = color, it->alpha = alpha;
+}
+
+void gfx_frame_rect(int x, int y, int w, int h, int t, uint16_t color) {
+ gfx_rect(x, y, w, t, color, 255);
+ gfx_rect(x, y + h - t, w, t, color, 255);
+ gfx_rect(x, y + t, t, h - 2 * t, color, 255);
+ gfx_rect(x + w - t, y + t, t, h - 2 * t, color, 255);
+}
+
+static void sprite_item(uint16_t s, int i, int x, int y, uint8_t flags, uint16_t color, bool tint, uint8_t alpha) {
+ Frame f;
+ if (!alpha || !frame_get(s, i, &f)) return;
+ int tx = x - ((flags & GFX_FLIPX) ? f.w - 1 - f.ox : f.ox), ty = y - ((flags & GFX_FLIPY) ? f.h - 1 - f.oy : f.oy);
+ Item *it = add(I_SPRITE, tx, ty, tx + f.w, ty + f.h);
+ if (!it) return;
+ it->x = (int16_t)tx, it->y = (int16_t)ty;
+ it->ref = s, it->sub = (uint16_t)i, it->flags = flags, it->alpha = alpha;
+ it->color = tint, it->extra = color;
+}
+void gfx_sprite(uint16_t s, int i, int x, int y, uint8_t flags, uint8_t alpha) { sprite_item(s, i, x, y, flags, 0, false, alpha); }
+void gfx_sprite_color(uint16_t s, int i, int x, int y, uint8_t flags, uint16_t color, uint8_t alpha) {
+ sprite_item(s, i, x, y, flags, color, true, alpha);
+}
+
+/* scaled and turned: the bounds of the turned rectangle (sin and cos in 1/256 steps of a turn) */
+static const int16_t sin_q[65] = {
+ 0, 25, 50, 75, 100, 125, 150, 175, 200, 224, 249, 273, 297, 321, 345, 369, 392, 415, 438, 460, 483, 505, 526, 548,
+ 569, 590, 610, 630, 650, 669, 688, 706, 724, 742, 759, 775, 792, 807, 822, 837, 851, 865, 878, 891, 903, 915, 926,
+ 936, 946, 955, 964, 972, 980, 987, 993, 999, 1004, 1009, 1013, 1016, 1019, 1021, 1023, 1024, 1024};
+int gfx_sin(int a) { /* a in 1/256 turn, result in 1/1024 */
+ a &= 255;
+ if (a < 64) return sin_q[a];
+ if (a < 128) return sin_q[128 - a];
+ if (a < 192) return -sin_q[a - 128];
+ return -sin_q[256 - a];
+}
+int gfx_cos(int a) { return gfx_sin(a + 64); }
+
+void gfx_sprite_ex(uint16_t s, int i, int x, int y, int scale_x, int scale_y, int angle, uint16_t color, uint8_t alpha) {
+ Frame f;
+ if (!alpha || !frame_get(s, i, &f) || !scale_x || !scale_y) return;
+ if (!(angle & 255) && scale_x == 256 && scale_y == 256) {
+ sprite_item(s, i, x, y, 0, color, color != 0, alpha);
+ return;
+ }
+ /* the corners, relative to the origin, scaled then turned */
+ int c = gfx_cos(angle), sn = gfx_sin(angle);
+ int xs[4] = {-f.ox, f.w - f.ox, -f.ox, f.w - f.ox}, ys[4] = {-f.oy, -f.oy, f.h - f.oy, f.h - f.oy};
+ int minx = 1 << 30, maxx = -(1 << 30), miny = 1 << 30, maxy = -(1 << 30);
+ for (int k = 0; k < 4; k++) {
+ int px = xs[k] * scale_x / 256, py = ys[k] * scale_y / 256;
+ int rx = (px * c - py * sn) / 1024, ry = (px * sn + py * c) / 1024;
+ if (rx < minx) minx = rx;
+ if (rx > maxx) maxx = rx;
+ if (ry < miny) miny = ry;
+ if (ry > maxy) maxy = ry;
+ }
+ Item *it = add(I_SPRITE_EX, x + minx - 1, y + miny - 1, x + maxx + 2, y + maxy + 2);
+ if (!it) return;
+ it->x = (int16_t)x, it->y = (int16_t)y;
+ it->a = (int16_t)scale_x, it->b = (int16_t)scale_y;
+ it->ref = s, it->sub = (uint16_t)i, it->alpha = alpha;
+ it->color = color != 0, it->extra = color;
+ it->seed = (uint32_t)angle;
+}
+
+void gfx_tiles(int wx, int wy) {
+ Item *it = add(I_TILES, 0, 0, SCREEN_W, SCREEN_H);
+ if (it) it->x = (int16_t)wx, it->y = (int16_t)wy;
+}
+
+void gfx_line(int x0, int y0, int x1, int y1, uint16_t color) {
+ Item *it = add(I_LINE, (x0 < x1 ? x0 : x1), (y0 < y1 ? y0 : y1), (x0 > x1 ? x0 : x1) + 1, (y0 > y1 ? y0 : y1) + 1);
+ if (!it) return;
+ it->x = (int16_t)x0, it->y = (int16_t)y0, it->a = (int16_t)x1, it->b = (int16_t)y1, it->color = color;
+}
+
+void gfx_circle(int cx, int cy, int r, uint16_t color, uint8_t alpha) {
+ Item *it = add(I_CIRCLE, cx - r, cy - r, cx + r + 1, cy + r + 1);
+ if (!it) return;
+ it->x = (int16_t)cx, it->y = (int16_t)cy, it->a = (int16_t)r, it->color = color, it->alpha = alpha;
+}
+
+/* ---------------------------------------------------------------- fonts */
+typedef struct {
+ uint8_t height, first, count, flags;
+ const uint8_t *glyphs; /* 8 bytes each: advance, w, h, ox, oy, pad, bitmap offset (u16) */
+ const uint8_t *bits;
+} Font;
+static void font_get(int font, Font *f) {
+ const uint8_t *s = section(SEC_FONTS);
+ const uint8_t *p = s + rd32(s + 4 + 4 * font);
+ f->height = p[0], f->first = p[1], f->count = p[2], f->flags = p[3];
+ f->glyphs = p + 4;
+ f->bits = p + 4 + 8 * f->count;
+}
+static const uint8_t *glyph(const Font *f, uint8_t c) {
+ if (c < f->first || c >= f->first + f->count) c = '?';
+ if (c < f->first || c >= f->first + f->count) return NULL;
+ return f->glyphs + 8 * (c - f->first);
+}
+int font_height(int font) {
+ Font f;
+ font_get(font, &f);
+ return f.height;
+}
+int gfx_text_width(int font, const char *s, int n) {
+ Font f;
+ font_get(font, &f);
+ int w = 0, best = 0;
+ for (int i = 0; i < n && s[i]; i++) {
+ if (s[i] == '\n') {
+ if (w > best) best = w;
+ w = 0;
+ continue;
+ }
+ const uint8_t *g = glyph(&f, (uint8_t)s[i]);
+ if (g) w += g[0];
+ }
+ return w > best ? w : best;
+}
+
+int gfx_text_ex(int font, int x, int y, const char *s, int n, uint16_t color, int shake, uint32_t seed, int spacing) {
+ int len = 0;
+ while (len < n && s[len]) len++;
+ if (!len) return 0;
+ Font f;
+ font_get(font, &f);
+ int lines = 1;
+ for (int i = 0; i < len; i++) lines += s[i] == '\n';
+ int w = gfx_text_width(font, s, len) + len * spacing;
+ if (npool + len > GFX_TEXT_POOL) {
+ dropped_now++;
+ return w;
+ }
+ Item *it = add(I_TEXT, x - shake - 2, y - shake - 4, x + w + shake + 4, y + lines * (f.height + 2) + shake + 4);
+ if (!it) return w;
+ memcpy(pool + npool, s, (size_t)len);
+ it->ref = (uint16_t)npool, it->sub = (uint16_t)len;
+ npool += len;
+ it->x = (int16_t)x, it->y = (int16_t)y, it->font = (uint8_t)font, it->color = color;
+ it->a = (int16_t)shake, it->b = (int16_t)spacing, it->seed = seed;
+ return w;
+}
+int gfx_text(int font, int x, int y, const char *s, uint16_t color, uint8_t alpha) {
+ (void)alpha;
+ return gfx_text_ex(font, x, y, s, 0x7FFF, color, 0, 0, 0);
+}
+
+/* ---------------------------------------------------------------- rasterising */
+static inline uint16_t blend(uint16_t d, uint16_t s, uint32_t a) { /* a: 0..32 */
+ uint32_t dd = (d | (uint32_t)d << 16) & 0x07E0F81Fu;
+ uint32_t ss = (s | (uint32_t)s << 16) & 0x07E0F81Fu;
+ uint32_t r = (dd + (((ss - dd) * a) >> 5)) & 0x07E0F81Fu;
+ return (uint16_t)(r | r >> 16);
+}
+static inline uint32_t a32(uint8_t a) { return (a * 32u + 127) / 255; }
+
+static void rast_rect(const Item *it, int by0, int by1) {
+ int y0 = it->y0 > by0 ? it->y0 : by0, y1 = it->y1 < by1 ? it->y1 : by1;
+ uint32_t a = a32(it->alpha);
+ for (int y = y0; y < y1; y++) {
+ uint16_t *row = band + (y - by0) * SCREEN_W;
+ if (a >= 32)
+ for (int x = it->x0; x < it->x1; x++) row[x] = it->color;
+ else
+ for (int x = it->x0; x < it->x1; x++) row[x] = blend(row[x], it->color, a);
+ }
+}
+
+static inline uint32_t px_at(const uint8_t *p, int k, int bpp) {
+ switch (bpp) {
+ case 8: return p[k];
+ case 4: return (p[k >> 1] >> ((k & 1) * 4)) & 15;
+ case 2: return (p[k >> 2] >> ((k & 3) * 2)) & 3;
+ case 1: return (p[k >> 3] >> (k & 7)) & 1;
+ default: return 0;
+ }
+}
+
+static void rast_sprite(const Item *it, int by0, int by1) {
+ Frame f;
+ if (!frame_get(it->ref, it->sub, &f)) return;
+ int y0 = it->y0 > by0 ? it->y0 : by0, y1 = it->y1 < by1 ? it->y1 : by1;
+ uint32_t a = a32(it->alpha);
+ bool fx = it->flags & GFX_FLIPX, fy = it->flags & GFX_FLIPY;
+ uint16_t pal[256];
+ for (int k = 0; k < f.ncol; k++) pal[k] = it->color ? it->extra : rd16(f.pal + 2 * k);
+ int cx0 = it->x0, cx1 = it->x1;
+ for (int y = y0; y < y1; y++) {
+ int r = y - it->y;
+ if (fy) r = f.h - 1 - r;
+ const uint8_t *p = f.data + rd16(f.rows + 2 * r);
+ uint16_t *row = band + (y - by0) * SCREEN_W;
+ int x = 0;
+ while (x < f.w) {
+ int skip = p[0], len = p[1];
+ p += 2;
+ x += skip;
+ for (int k = 0; k < len; k++) {
+ int sx = x + k;
+ int dx = it->x + (fx ? f.w - 1 - sx : sx);
+ if (dx < cx0 || dx >= cx1) continue;
+ uint16_t c = pal[px_at(p, k, f.bpp)];
+ row[dx] = a >= 32 ? c : blend(row[dx], c, a);
+ }
+ p += (len * f.bpp + 7) >> 3;
+ x += len;
+ }
+ }
+}
+
+/* one row of a frame, unpacked: index + 1 for each pixel, 0 where clear */
+static void frame_row(const Frame *f, int r, uint8_t *out) {
+ memset(out, 0, f->w);
+ const uint8_t *p = f->data + rd16(f->rows + 2 * r);
+ int x = 0;
+ while (x < f->w) {
+ int skip = p[0], len = p[1];
+ p += 2;
+ x += skip;
+ for (int k = 0; k < len && x + k < f->w; k++) out[x + k] = (uint8_t)(px_at(p, k, f->bpp) + 1);
+ p += (len * f->bpp + 7) >> 3;
+ x += len;
+ }
+}
+
+static void rast_sprite_ex(const Item *it, int by0, int by1) {
+ Frame f;
+ if (!frame_get(it->ref, it->sub, &f) || f.w > 640) return;
+ int y0 = it->y0 > by0 ? it->y0 : by0, y1 = it->y1 < by1 ? it->y1 : by1;
+ uint32_t a = a32(it->alpha);
+ uint16_t pal[256];
+ for (int k = 0; k < f.ncol; k++) pal[k] = it->color ? it->extra : rd16(f.pal + 2 * k);
+ int c = gfx_cos((int)it->seed), s = gfx_sin((int)it->seed);
+ /* inverse: source = R(-angle) (screen - origin) / scale, in 1/1024 */
+ static uint8_t rowbuf[640];
+ int cached = -1;
+ for (int y = y0; y < y1; y++) {
+ uint16_t *row = band + (y - by0) * SCREEN_W;
+ int dy = y - it->y;
+ for (int x = it->x0; x < it->x1; x++) {
+ int dx = x - it->x;
+ int ux = (dx * c + dy * s), uy = (-dx * s + dy * c); /* x 1024 */
+ int sx = (int)((int64_t)ux * 256 / ((int64_t)it->a * 1024)) + f.ox;
+ int sy = (int)((int64_t)uy * 256 / ((int64_t)it->b * 1024)) + f.oy;
+ if (ux < 0 && (int64_t)ux * 256 % ((int64_t)it->a * 1024)) sx--;
+ if (uy < 0 && (int64_t)uy * 256 % ((int64_t)it->b * 1024)) sy--;
+ if (sx < 0 || sy < 0 || sx >= f.w || sy >= f.h) continue;
+ if (sy != cached) frame_row(&f, sy, rowbuf), cached = sy;
+ uint8_t v = rowbuf[sx];
+ if (!v) continue;
+ uint16_t col = pal[v - 1];
+ row[x] = a >= 32 ? col : blend(row[x], col, a);
+ }
+ }
+}
+
+static void rast_tiles(const Item *it, int by0, int by1) {
+ const RoomInfo *r = world_room_info();
+ if (!r) return;
+ int y0 = it->y0 > by0 ? it->y0 : by0, y1 = it->y1 < by1 ? it->y1 : by1;
+ /* only inside the room's rectangle: outside it stays black */
+ int rx0 = r->x - it->x, rx1 = r->x + r->w - it->x;
+ if (rx0 < it->x0) rx0 = it->x0;
+ if (rx1 > it->x1) rx1 = it->x1;
+ for (int y = y0; y < y1; y++) {
+ int wy = it->y + y;
+ if (wy < r->y || wy >= r->y + r->h) continue;
+ int gy = wy - r->gy;
+ if (gy < 0 || gy >= r->grows * 20) continue;
+ int trow = gy / 20, ty = gy % 20;
+ uint16_t *row = band + (y - by0) * SCREEN_W;
+ int x = rx0;
+ while (x < rx1) {
+ int wx = it->x + x, gx = wx - r->gx;
+ int tcol = gx >= 0 ? gx / 20 : -1, tx = gx >= 0 ? gx % 20 : 0;
+ int n = 20 - tx;
+ if (x + n > rx1) n = rx1 - x;
+ uint16_t id = tcol >= 0 && tcol < r->gcols ? rd16(r->grid + 2 * (trow * r->gcols + tcol)) : 0xFFFF;
+ if (id != 0xFFFF) {
+ Tile t;
+ tile_get(id, &t);
+ if (t.bpp == 0) {
+ uint16_t c = rd16(t.pal);
+ for (int k = 0; k < n; k++) row[x + k] = c;
+ } else {
+ uint16_t pal[16];
+ int nc = t.ncol > 16 ? 16 : t.ncol;
+ for (int k = 0; k < nc; k++) pal[k] = rd16(t.pal + 2 * k);
+ int base = ty * 20 + tx;
+ if (t.bpp == 8)
+ for (int k = 0; k < n; k++) row[x + k] = rd16(t.pal + 2 * t.bits[base + k]);
+ else
+ for (int k = 0; k < n; k++) row[x + k] = pal[px_at(t.bits, base + k, t.bpp)];
+ }
+ }
+ x += n;
+ }
+ }
+}
+
+/* a pseudo-random shake for letter i of a text */
+static int shake_of(uint32_t seed, int i, int axis, int amount) {
+ uint32_t h = seed * 2654435761u ^ (uint32_t)(i * 2 + axis) * 40503u;
+ h ^= h >> 13, h *= 0x5bd1e995u, h ^= h >> 15;
+ return (int)(h % (uint32_t)(2 * amount + 1)) - amount;
+}
+
+static void rast_text(const Item *it, int by0, int by1) {
+ Font f;
+ font_get(it->font, &f);
+ const char *s = pool + it->ref;
+ int x = it->x, y = it->y;
+ for (int i = 0; i < it->sub; i++) {
+ uint8_t ch = (uint8_t)s[i];
+ if (ch == '\n') {
+ x = it->x, y += f.height + 2;
+ continue;
+ }
+ const uint8_t *g = glyph(&f, ch);
+ if (!g) continue;
+ int gw = g[1], gh = g[2], gx = x + (int8_t)g[3], gy = y + (int8_t)g[4];
+ if (it->a) gx += shake_of(it->seed, i, 0, it->a), gy += shake_of(it->seed, i, 1, it->a);
+ x += g[0] + it->b;
+ if (gy >= by1 || gy + gh <= by0) continue;
+ const uint8_t *bits = f.bits + rd16(g + 6);
+ int stride = (gw + 7) >> 3;
+ for (int r = 0; r < gh; r++) {
+ int yy = gy + r;
+ if (yy < by0 || yy >= by1 || yy < it->y0 || yy >= it->y1) continue;
+ uint16_t *row = band + (yy - by0) * SCREEN_W;
+ const uint8_t *b = bits + r * stride;
+ for (int c = 0; c < gw; c++)
+ if (b[c >> 3] & (0x80 >> (c & 7))) {
+ int xx = gx + c;
+ if (xx >= it->cx0 && xx < it->cx1 && xx >= 0 && xx < SCREEN_W) row[xx] = it->color;
+ }
+ }
+ }
+}
+
+static void rast_line(const Item *it, int by0, int by1) {
+ int x0 = it->x, y0 = it->y, x1 = it->a, y1 = it->b;
+ int dx = x1 > x0 ? x1 - x0 : x0 - x1, dy = y1 > y0 ? y0 - y1 : y1 - y0;
+ int sx = x0 < x1 ? 1 : -1, sy = y0 < y1 ? 1 : -1, err = dx + dy;
+ for (;;) {
+ if (y0 >= by0 && y0 < by1 && x0 >= it->x0 && x0 < it->x1 && y0 >= it->y0 && y0 < it->y1) band[(y0 - by0) * SCREEN_W + x0] = it->color;
+ if (x0 == x1 && y0 == y1) break;
+ int e2 = 2 * err;
+ if (e2 >= dy) err += dy, x0 += sx;
+ if (e2 <= dx) err += dx, y0 += sy;
+ }
+}
+
+static void rast_circle(const Item *it, int by0, int by1) {
+ int y0 = it->y0 > by0 ? it->y0 : by0, y1 = it->y1 < by1 ? it->y1 : by1;
+ int r = it->a;
+ uint32_t a = a32(it->alpha);
+ for (int y = y0; y < y1; y++) {
+ int dy = y - it->y, w = 0;
+ while ((w + 1) * (w + 1) + dy * dy <= r * r + r) w++;
+ if (dy * dy > r * r + r) continue;
+ int xa = it->x - w, xb = it->x + w + 1;
+ if (xa < it->x0) xa = it->x0;
+ if (xb > it->x1) xb = it->x1;
+ uint16_t *row = band + (y - by0) * SCREEN_W;
+ for (int x = xa; x < xb; x++) row[x] = a >= 32 ? it->color : blend(row[x], it->color, a);
+ }
+}
+
+void gfx_end(void) {
+ gfx_dropped = dropped_now;
+ if (!band) return;
+ for (int by0 = 0; by0 < SCREEN_H; by0 += GFX_BAND) {
+ int by1 = by0 + GFX_BAND;
+ memset(band, 0, sizeof(uint16_t) * SCREEN_W * GFX_BAND);
+ for (int i = 0; i < nitems; i++) {
+ const Item *it = &items[i];
+ if (it->y1 <= by0 || it->y0 >= by1) continue;
+ switch (it->kind) {
+ case I_RECT: rast_rect(it, by0, by1); break;
+ case I_SPRITE: rast_sprite(it, by0, by1); break;
+ case I_SPRITE_EX: rast_sprite_ex(it, by0, by1); break;
+ case I_TILES: rast_tiles(it, by0, by1); break;
+ case I_TEXT: rast_text(it, by0, by1); break;
+ case I_LINE: rast_line(it, by0, by1); break;
+ case I_CIRCLE: rast_circle(it, by0, by1); break;
+ }
+ }
+ plat_push(0, by0, SCREEN_W, GFX_BAND, band);
+ }
+}
diff --git a/games/undertale/src/gfx.h b/games/undertale/src/gfx.h
new file mode 100644
index 00000000..6ab55d10
--- /dev/null
+++ b/games/undertale/src/gfx.h
@@ -0,0 +1,61 @@
+/* Undertale: drawing.
+ *
+ * A frame is a draw list, rasterised band by band (the calculator has no
+ * frame buffer) and pushed to the screen. Commands are in screen pixels and
+ * painted in the order given. The game draws at the original's 320 x 240,
+ * the calculator's own size: one of the game's pixels is one of the screen's. */
+#ifndef UT_GFX_H
+#define UT_GFX_H
+#include
+#include
+#include "plat.h"
+
+static inline uint16_t rgb565(uint32_t rgb) { /* 0xRRGGBB */
+ return (uint16_t)(((rgb >> 8) & 0xF800) | ((rgb >> 5) & 0x07E0) | ((rgb >> 3) & 0x001F));
+}
+#define C_BLACK 0x0000
+#define C_WHITE 0xFFFF
+#define C_RED 0xF800
+#define C_YELLOW 0xFFE0
+
+enum { GFX_FLIPX = 1, GFX_FLIPY = 2 };
+
+#define GFX_BAND 16 /* screen rows rasterised at a time */
+#define GFX_MAX_ITEMS 400 /* commands in a frame (more are dropped) */
+#define GFX_TEXT_POOL 1536 /* bytes of text in a frame */
+
+/* the band: SCREEN_W * GFX_BAND pixels, the caller's (main's stack on the calculator) */
+void gfx_set_band_memory(uint16_t *px);
+void gfx_begin(void);
+void gfx_end(void);
+/* later commands draw only inside this screen rectangle (gfx_clip_all(): all of it) */
+void gfx_clip(int x, int y, int w, int h);
+void gfx_clip_all(void);
+
+void gfx_rect(int x, int y, int w, int h, uint16_t color, uint8_t alpha); /* alpha 0 (none) to 255 */
+void gfx_frame_rect(int x, int y, int w, int h, int t, uint16_t color); /* an outline t pixels thick */
+/* frame i of sprite s (SPR_* in data.h), its origin at (x, y); alpha 255 opaque */
+void gfx_sprite(uint16_t s, int i, int x, int y, uint8_t flags, uint8_t alpha);
+/* the same, every opaque pixel in one colour (a flash, a silhouette) */
+void gfx_sprite_color(uint16_t s, int i, int x, int y, uint8_t flags, uint16_t color, uint8_t alpha);
+/* the same, scaled (fixed point 8.8, 256 = 1) around its origin, and turned by angle (0-255 for a turn) */
+void gfx_sprite_ex(uint16_t s, int i, int x, int y, int scale_x, int scale_y, int angle, uint16_t color, uint8_t alpha);
+/* the room's tiles (world.c gives them), its world point (wx, wy) at the screen's top-left */
+void gfx_tiles(int wx, int wy);
+/* text in a font (FONT_* in data.h), its top-left at (x, y); returns its width. '\n' starts a line */
+int gfx_text(int font, int x, int y, const char *s, uint16_t color, uint8_t alpha);
+/* the same, each letter shaken by up to `shake` pixels (the game's shaking text), seeded */
+int gfx_text_ex(int font, int x, int y, const char *s, int n, uint16_t color, int shake, uint32_t seed, int spacing);
+int gfx_text_width(int font, const char *s, int n);
+int font_height(int font);
+/* a line one pixel wide */
+void gfx_line(int x0, int y0, int x1, int y1, uint16_t color);
+/* sine and cosine of a in 1/256 turns, in 1/1024 */
+int gfx_sin(int a);
+int gfx_cos(int a);
+/* a solid circle */
+void gfx_circle(int cx, int cy, int r, uint16_t color, uint8_t alpha);
+
+/* how many commands the last frame dropped */
+extern int gfx_dropped;
+#endif
diff --git a/games/undertale/src/hotland.h b/games/undertale/src/hotland.h
new file mode 100644
index 00000000..e0a70eb4
--- /dev/null
+++ b/games/undertale/src/hotland.h
@@ -0,0 +1,27 @@
+/* Undertale: Hotland (ht1.c, ht2.c, ht3.c: its three floors; resort.c: the MTT Resort), its monsters
+ * (htmon.c), Mettaton's shows (htshows.c). */
+#ifndef UT_HOTLAND_H
+#define UT_HOTLAND_H
+#include "event.h"
+void ht1_setup(int room, int from);
+int ht1_event(Ev *c); /* -1: not one of its */
+void ht1_update(void); /* the belts, the lasers */
+void ht1_draw(void); /* under everything: the lasers, the turning vents' arrows */
+void ht2_setup(int room, int from);
+int ht2_event(Ev *c);
+void ht2_update(void); /* the belt puzzle's switches, the news report's bombs and clock */
+void ht2_draw(void); /* under everything: force fields, the switches, the vent puzzle's arrows */
+void ht2_draw_top(void); /* over everything: the news report's clock */
+void ht3_setup(int room, int from);
+int ht3_event(Ev *c);
+void ht3_update(void); /* the webs' hold, the tile puzzle, its flames and clock */
+void ht3_draw(void); /* under everything: the flames, the turned-off force fields */
+void ht3_draw_top(void); /* over everything: the tile puzzle's clock */
+int ht3_speed(int x, int y);
+void res_setup(int room, int from);
+int res_event(Ev *c);
+void core_setup(int room, int from); /* core.c: the CORE */
+int core_event(Ev *c);
+void core_update(void); /* its lasers, the force field's patience */
+void core_draw(void); /* under everything: the lasers, the force fields */
+#endif
diff --git a/games/undertale/src/ht1.c b/games/undertale/src/ht1.c
new file mode 100644
index 00000000..559e0f68
--- /dev/null
+++ b/games/undertale/src/ht1.c
@@ -0,0 +1,496 @@
+/* Hotland, its first floor: the crossroads before the Lab, Alphys and Mettaton's quiz, the conveyor belts, the
+ * Magma Chamber's steam vents and the burnt pan, the laser walkway, the shooting puzzles that open the door, the
+ * vents over the lava, Mettaton's cooking show and the elevator by the CORE's view. (As the Undertale Wiki describes
+ * them; what they say is ours, in short.) */
+#include
+#include "battle.h"
+#include "event.h"
+#include "events.h"
+#include "game.h"
+#include "gfx.h"
+#include "hotland.h"
+#include "items.h"
+#include "menu.h"
+#include "monsters.h"
+#include "save.h"
+#include "shoot.h"
+#include "snowdin.h"
+#include "world.h"
+
+static void gone(Obj *o) { o->kind = OBJ_NONE; }
+static Obj *thing(int x, int y, int w, int h, int event, int name, int arg) {
+ Obj *o = obj_trigger(x, y, w, h, event, name);
+ if (o) o->flags = OF_TALK | OF_SOLID, o->arg = (int16_t)arg;
+ return o;
+}
+static Obj *obj_alphys(int x, int y, int dir) {
+ static const uint16_t s[4] = {SPR_ALPHYS_OW, SPR_ALPHYS_OW, SPR_ALPHYS_OW, SPR_ALPHYS_OW};
+ return obj_npc(ACTOR_ALPHYS, s, x, y, dir, 0);
+}
+static Obj *obj_mtt(int x, int y) {
+ static const uint16_t s[4] = {SPR_MTT_OW, SPR_MTT_OW, SPR_MTT_OW, SPR_MTT_OW};
+ Obj *o = obj_npc(ACTOR_METTATON, s, x, y, DIR_DOWN, 0);
+ if (o) o->flags |= OF_ANIM, o->flags &= (uint16_t)~OF_SOLID, o->anim_speed = 5;
+ return o;
+}
+
+/* ---------------------------------------------------------------- the conveyor belts */
+/* each pushes Frisk along it while Frisk's feet are on it, where there's floor */
+typedef struct {
+ int16_t room, x, y, w, h;
+ int8_t dx, dy;
+} Belt;
+static const Belt belts[] = {
+ /* the first room: up the left belt, down the right one */
+ {ROOM_HT_BELT1, 2904, 5450, 36, 440, 0, -2}, {ROOM_HT_BELT1, 2980, 5450, 36, 440, 0, 2},
+ /* the second: up the left side, down the right */
+ {ROOM_HT_BELT2, 2840, 4730, 120, 450, 0, -1}, {ROOM_HT_BELT2, 2980, 4730, 120, 450, 0, 1},
+ /* the second floor: the belt puzzle's (the top one runs left, the bottom right: as fast as Frisk walks, so
+ * walking against one stands still); round the vent puzzle (down, along, up: back to its start); the news
+ * room's, up to its vent */
+ {ROOM_HT_BELTPUZ, 5130, 2340, 300, 50, -3, 0}, {ROOM_HT_BELTPUZ, 5130, 2440, 300, 50, 3, 0},
+ {ROOM_HT_VENTPUZ, 4420, 2310, 60, 290, 0, 3}, {ROOM_HT_VENTPUZ, 4420, 2560, 360, 40, 3, 0},
+ {ROOM_HT_VENTPUZ, 4720, 2430, 60, 170, 0, -3},
+ {ROOM_HT_NEWS, 4300, 1790, 100, 160, 0, -2},
+ /* the third floor's, on the way to the south puzzle: down the left side, along the bottom (past the drones), up the
+ * right; back along the top */
+ {ROOM_HT_BELTS3, 4800, 1400, 60, 130, 0, 3}, {ROOM_HT_BELTS3, 4800, 1520, 600, 60, 3, 0},
+ {ROOM_HT_BELTS3, 5380, 1430, 60, 150, 0, -3}, {ROOM_HT_BELTS3, 4850, 1320, 520, 60, -3, 0},
+};
+static void belts_update(void) {
+ if (event_running()) return;
+ int fx = pl.x + 10, fy = pl.y + 24;
+ for (unsigned i = 0; i < sizeof belts / sizeof belts[0]; i++) {
+ const Belt *b = &belts[i];
+ if (b->room != g_room || fx < b->x || fx >= b->x + b->w || fy < b->y || fy >= b->y + b->h) continue;
+ if (world_walkable(pl.x + 3 + b->dx, pl.y + 20 + b->dy, 14, 9, -1)) pl.x = (int16_t)(pl.x + b->dx), pl.y = (int16_t)(pl.y + b->dy);
+ }
+}
+
+/* ---------------------------------------------------------------- the steam vents */
+/* stepping on a grate, Frisk is blown its way, over the lava, to (tx, ty) (Frisk's top-left: never on another
+ * vent's grate, but where a chain of them is meant); a vent that turns (dir -1: the hub's, a quarter clockwise each
+ * time it's used; -2: the pan's, left and right all the time) has a landing each way */
+typedef struct {
+ int16_t room, gx, gy;
+ int8_t dir;
+ int16_t t[4][2]; /* by DIR_*: down, up, left, right */
+} Vent;
+static const Vent vents[] = {
+ {ROOM_HT_MAGMA, 3120, 4000, DIR_DOWN, {{3120, 4110}}},
+ {ROOM_HT_MAGMA, 3362, 4000, DIR_LEFT, {[DIR_LEFT] = {3241, 3986}}},
+ {ROOM_HT_MAGMA, 3503, 4060, DIR_RIGHT, {[DIR_RIGHT] = {3580, 4060}}},
+ {ROOM_HT_MAGMA, 3420, 4180, DIR_UP, {[DIR_UP] = {3420, 4048}}},
+ {ROOM_HT_MAGMA, 3225, 4240, DIR_RIGHT, {[DIR_RIGHT] = {3302, 4226}}},
+ {ROOM_HT_MAGMA, 3325, 4240, DIR_LEFT, {[DIR_LEFT] = {3244, 4226}}},
+ {ROOM_HT_MAGMA, 3525, 4240, DIR_LEFT, {[DIR_LEFT] = {3421, 4226}}},
+ {ROOM_HT_PAN, 3620, 4480, -2, {[DIR_LEFT] = {3501, 4466}, [DIR_RIGHT] = {3739, 4466}}},
+ {ROOM_HT_HUB, 4820, 3620, -1, {{4820, 3680}, {4820, 3490}, {4700, 3610}, {4945, 3610}}},
+ {ROOM_HT_HUB, 4820, 3560, DIR_DOWN, {{4795, 3592}}},
+ {ROOM_HT_HUB, 4740, 3620, DIR_RIGHT, {[DIR_RIGHT] = {4785, 3610}}},
+ {ROOM_HT_HUB, 4900, 3620, DIR_LEFT, {[DIR_LEFT] = {4855, 3610}}},
+ {ROOM_HT_VENTISLE, 4940, 3120, DIR_RIGHT, {[DIR_RIGHT] = {5051, 3106}}},
+ {ROOM_HT_VENTISLE, 5060, 3120, DIR_RIGHT, {[DIR_RIGHT] = {5171, 3106}}},
+ {ROOM_HT_VENTISLE, 5180, 3120, DIR_RIGHT, {[DIR_RIGHT] = {5312, 3106}}},
+ /* the news room's: up to the top room's corner, and from there over to the rest of it */
+ {ROOM_HT_NEWS, 4350, 1722, DIR_UP, {[DIR_UP] = {4350, 1600}}},
+ {ROOM_HT_NEWS, 4320, 1600, DIR_LEFT, {[DIR_LEFT] = {4200, 1590}}},
+ /* the third floor's islands: each grate blows the way its arrow points, to the next island that way (off the
+ * edges: the corridor back west, the walkways north and south, the platform east to the door) */
+ {ROOM_HT_HUB3, 4740, 740, DIR_DOWN, {[DIR_DOWN] = {4740, 786}}},
+ {ROOM_HT_HUB3, 4820, 700, DIR_UP, {[DIR_UP] = {4820, 590}}},
+ {ROOM_HT_HUB3, 4820, 740, DIR_DOWN, {[DIR_DOWN] = {4820, 786}}},
+ {ROOM_HT_HUB3, 4880, 720, DIR_LEFT, {[DIR_LEFT] = {4820, 706}}},
+ {ROOM_HT_HUB3, 4680, 800, DIR_RIGHT, {[DIR_RIGHT] = {4740, 786}}},
+ {ROOM_HT_HUB3, 4740, 780, DIR_UP, {[DIR_UP] = {4740, 706}}},
+ {ROOM_HT_HUB3, 4740, 820, DIR_DOWN, {[DIR_DOWN] = {4740, 866}}},
+ {ROOM_HT_HUB3, 4820, 820, DIR_DOWN, {[DIR_DOWN] = {4820, 866}}},
+ {ROOM_HT_HUB3, 4800, 800, DIR_LEFT, {[DIR_LEFT] = {4740, 786}}},
+ {ROOM_HT_HUB3, 4900, 780, DIR_UP, {[DIR_UP] = {4900, 706}}},
+ {ROOM_HT_HUB3, 4880, 800, DIR_LEFT, {[DIR_LEFT] = {4820, 786}}},
+ {ROOM_HT_HUB3, 4960, 800, DIR_LEFT, {[DIR_LEFT] = {4900, 786}}},
+ {ROOM_HT_HUB3, 4660, 860, DIR_UP, {[DIR_UP] = {4660, 786}}},
+ {ROOM_HT_HUB3, 4640, 880, DIR_LEFT, {[DIR_LEFT] = {4572, 866}}},
+ {ROOM_HT_HUB3, 4740, 900, DIR_DOWN, {[DIR_DOWN] = {4740, 946}}},
+ {ROOM_HT_HUB3, 4840, 880, DIR_RIGHT, {[DIR_RIGHT] = {4900, 866}}},
+ {ROOM_HT_HUB3, 4900, 860, DIR_UP, {[DIR_UP] = {4900, 786}}},
+ {ROOM_HT_HUB3, 4980, 860, DIR_UP, {[DIR_UP] = {4980, 786}}},
+ {ROOM_HT_HUB3, 4980, 900, DIR_DOWN, {[DIR_DOWN] = {4980, 946}}},
+ {ROOM_HT_HUB3, 4960, 880, DIR_LEFT, {[DIR_LEFT] = {4900, 866}}},
+ {ROOM_HT_HUB3, 5000, 880, DIR_RIGHT, {[DIR_RIGHT] = {5070, 866}}},
+ {ROOM_HT_HUB3, 4660, 940, DIR_UP, {[DIR_UP] = {4660, 866}}},
+ {ROOM_HT_HUB3, 4720, 960, DIR_LEFT, {[DIR_LEFT] = {4660, 946}}},
+ {ROOM_HT_HUB3, 4760, 960, DIR_RIGHT, {[DIR_RIGHT] = {4820, 946}}},
+ {ROOM_HT_HUB3, 4820, 940, DIR_UP, {[DIR_UP] = {4820, 866}}},
+ {ROOM_HT_HUB3, 4840, 960, DIR_RIGHT, {[DIR_RIGHT] = {4900, 946}}},
+ {ROOM_HT_HUB3, 4900, 940, DIR_UP, {[DIR_UP] = {4900, 866}}},
+ {ROOM_HT_HUB3, 4900, 980, DIR_DOWN, {[DIR_DOWN] = {4900, 1026}}},
+ {ROOM_HT_HUB3, 4920, 960, DIR_RIGHT, {[DIR_RIGHT] = {4980, 946}}},
+ {ROOM_HT_HUB3, 4980, 940, DIR_UP, {[DIR_UP] = {4980, 866}}},
+ {ROOM_HT_HUB3, 4740, 1020, DIR_UP, {[DIR_UP] = {4740, 946}}},
+ {ROOM_HT_HUB3, 4820, 1060, DIR_DOWN, {[DIR_DOWN] = {4820, 1130}}},
+ {ROOM_HT_HUB3, 4800, 1040, DIR_LEFT, {[DIR_LEFT] = {4740, 1026}}},
+ {ROOM_HT_HUB3, 4880, 1040, DIR_LEFT, {[DIR_LEFT] = {4820, 1026}}},
+ {ROOM_HT_HUB3, 4600, 880, DIR_RIGHT, {[DIR_RIGHT] = {4660, 866}}},
+ {ROOM_HT_HUB3, 4820, 660, DIR_DOWN, {[DIR_DOWN] = {4820, 706}}},
+ {ROOM_HT_HUB3, 4820, 1100, DIR_UP, {[DIR_UP] = {4820, 1026}}},
+ {ROOM_HT_HUB3, 5040, 880, DIR_LEFT, {[DIR_LEFT] = {4980, 866}}},
+};
+#define NVENTS ((int)(sizeof vents / sizeof vents[0]))
+/* the way a vent blows now */
+static int hub_turns;
+static int vent_dir(const Vent *v) {
+ static const uint8_t round[4] = {DIR_UP, DIR_RIGHT, DIR_DOWN, DIR_LEFT};
+ if (v->dir == -1) return round[hub_turns % 4];
+ if (v->dir == -2) return (g_frame / 20) % 2 ? DIR_RIGHT : DIR_LEFT;
+ return v->dir;
+}
+static void vents_setup(void) {
+ for (int i = 0; i < NVENTS; i++) {
+ const Vent *v = &vents[i];
+ if (v->room != g_room) continue;
+ Obj *o = obj_trigger(v->gx + 4, v->gy + 4, 12, 12, EV_VENT, ACTOR_VENT);
+ if (o) o->arg = (int16_t)i;
+ }
+}
+
+/* ---------------------------------------------------------------- the lasers */
+/* the walkway's sweep to and fro across it: an orange one hurts if Frisk stands in it, a blue one if Frisk moves */
+typedef struct {
+ int16_t room, x, y, h, swing;
+ uint8_t blue;
+} Laser;
+static const Laser lasers[] = {
+ {ROOM_HT_LASERS, 3258, 3796, 62, 30, 0}, {ROOM_HT_LASERS, 3425, 3796, 62, 40, 1},
+ {ROOM_HT_LASERS, 3567, 3796, 62, 30, 0}, {ROOM_HT_LASERS, 3717, 3796, 62, 40, 1},
+ {ROOM_HT_LASERS, 3840, 3796, 62, 30, 0}, {ROOM_HT_LASERS, 3958, 3796, 62, 40, 1},
+ /* the news room's bottom: blue, orange, blue */
+ {ROOM_HT_NEWS, 3965, 1946, 56, 16, 1}, {ROOM_HT_NEWS, 4025, 1946, 56, 16, 0}, {ROOM_HT_NEWS, 4085, 1946, 56, 16, 1},
+ /* the third floor's drones over the bottom belt, blue: ride it without a step */
+ {ROOM_HT_BELTS3, 4967, 1496, 84, 0, 1}, {ROOM_HT_BELTS3, 5149, 1496, 84, 0, 1}, {ROOM_HT_BELTS3, 5249, 1496, 84, 0, 1},
+};
+static int laser_cool;
+static int laser_x(int i) {
+ const Laser *l = &lasers[i];
+ int ph = (int)((g_frame * (l->blue ? 3 : 4) + (uint32_t)i * 40) % 256);
+ return l->x + l->swing * gfx_sin(ph) / 1024;
+}
+static void lasers_watch(void) {
+ if (laser_cool) laser_cool--;
+ if (event_running() || laser_cool) return;
+ for (int i = 0; i < (int)(sizeof lasers / sizeof lasers[0]); i++) {
+ const Laser *l = &lasers[i];
+ if (l->room != g_room) continue;
+ int x = laser_x(i);
+ if (pl.x + 3 > x + 1 || pl.x + 17 < x - 1 || pl.y + 29 < l->y || pl.y + 20 > l->y + l->h) continue;
+ if (l->blue ? pl.moving : !pl.moving) {
+ event_start(EV_LASER_HIT, NULL);
+ return;
+ }
+ }
+}
+/* the blue one in front of the left puzzle, that Alphys switches off */
+#define BLUE_X 4380
+#define BLUE_Y 3584
+#define BLUE_W 40
+
+void ht1_draw(void) {
+ if (world_room_info()->area != AREA_HOTLAND) return;
+ for (int i = 0; i < (int)(sizeof lasers / sizeof lasers[0]); i++) {
+ const Laser *l = &lasers[i];
+ if (l->room != g_room) continue;
+ uint16_t c = l->blue ? rgb565(0x14A9FF) : rgb565(0xFCA600);
+ gfx_rect(laser_x(i) - 1 - cam_x, l->y - cam_y, 3, l->h, c, (g_frame / 2) % 2 ? 255 : 200);
+ }
+ if (g_room == ROOM_HT_HUB && !flag(F_HT_BLUE_OFF))
+ gfx_rect(BLUE_X - cam_x, BLUE_Y - cam_y, BLUE_W, 3, rgb565(0x14A9FF), (g_frame / 2) % 2 ? 255 : 200);
+ /* the turning vents' arrows, over the map's */
+ static const uint16_t arrow[4] = {SPR_VENT_D, SPR_VENT_U, SPR_VENT_L, SPR_VENT_R};
+ for (int i = 0; i < NVENTS; i++) {
+ const Vent *v = &vents[i];
+ if (v->room == g_room && v->dir < 0) gfx_sprite(arrow[vent_dir(v)], 0, v->gx - cam_x, v->gy - cam_y, 0, 255);
+ }
+}
+
+void ht1_update(void) {
+ if (world_room_info()->area != AREA_HOTLAND) return;
+ belts_update();
+ lasers_watch();
+}
+
+/* ---------------------------------------------------------------- the rooms */
+void ht1_setup(int room, int from) {
+ (void)from;
+ vents_setup();
+ if (room < ROOM_HT_LABFRONT || room > ROOM_HT_COREVIEW) return;
+ switch (room) {
+ case ROOM_HT_LABFRONT: {
+ obj_save(1045, 6243, "* (The heat of Hotland fills you with determination.)");
+ Obj *g = obj_deco(SPR_RGUARDS_OW, 1012, 6146, true);
+ if (g) g->name = ACTOR_GUARDS, obj_set_box(g, 0, 30, 74, 24);
+ obj_sign(1010, 6176, 80, 30, "* (Two Royal Guards stand in front of the elevator.)|* Nobody goes up.\n* Not until the captain's back.");
+ obj_sign(1290, 6150, 60, 30, "* (The Lab. Its door is open.)");
+ break;
+ }
+ case ROOM_HT_RIVER:
+ obj_sign(1010, 6440, 50, 60, "* (Someone in a hood waits by\n a boat, humming.)|* (Maybe they'll give you a\n ride some other time.)");
+ break;
+ case ROOM_HT_LAB:
+ if (!flag(F_HT_LAB)) obj_trigger(1450, 5860, 16, 460, EV_HT_LAB, ACTOR_TRIGGER);
+ thing(2025, 6175, 34, 64, EV_HT_FRIDGE, ACTOR_ITEM, 0);
+ obj_sign(1775, 6140, 108, 90, "* (A huge screen.)\n* (It shows you, standing right here.)");
+ obj_sign(2317, 6150, 50, 75, "* (The door is locked.)\n* (Someone inside is humming.)");
+ obj_sign(1880, 5920, 60, 40, "* (Shelves stuffed with comics.)\n* (Most of them are about magical girls.)");
+ obj_sign(2050, 5920, 90, 50, "* (A bed, half buried in figurines.)");
+ break;
+ case ROOM_HT_MAGMA:
+ obj_save(2968, 4226, "* (The bubbling magma fills you with determination.)");
+ break;
+ case ROOM_HT_PAN:
+ if (!flag(F_HT_PAN)) {
+ Obj *p = obj_deco(SPR_BURNTPAN, 3476, 4468, false);
+ if (p) p->name = ACTOR_ITEM;
+ Obj *t = obj_trigger(3472, 4462, 26, 26, EV_HT_ITEM, ACTOR_ITEM);
+ if (t) t->flags = OF_TALK;
+ }
+ break;
+ case ROOM_HT_LASERS:
+ if (!flag(F_HT_CALL_LASER)) {
+ Obj *t = obj_trigger(3180, 3780, 12, 90, EV_HT_CALL, ACTOR_TRIGGER);
+ if (t) t->arg = 0;
+ }
+ break;
+ case ROOM_HT_HUB:
+ if (!flag(F_HT_CALL_HUB)) {
+ Obj *t = obj_trigger(4770, 3700, 120, 12, EV_HT_CALL, ACTOR_TRIGGER);
+ if (t) t->arg = 1;
+ }
+ if (!flag(F_HT_BLUE_OFF)) {
+ Obj *b = obj_trigger(BLUE_X, BLUE_Y - 6, BLUE_W, 12, EV_NONE, ACTOR_LASER);
+ if (b) b->flags = OF_SOLID;
+ Obj *t = obj_trigger(4380, 3560, 60, 100, EV_HT_CALL, ACTOR_TRIGGER2);
+ if (t) t->arg = 2;
+ }
+ thing(4372, 3410, 34, 34, EV_SHOOT, ACTOR_ITEM, SHOOT_LEFT);
+ thing(5250, 3410, 34, 34, EV_SHOOT, ACTOR_ITEM, SHOOT_RIGHT);
+ if (!(flag(F_HT_PUZ_L) && flag(F_HT_PUZ_R))) {
+ Obj *d = obj_trigger(4796, 3398, 68, 16, EV_NONE, ACTOR_DOOR);
+ if (d) d->flags = OF_SOLID;
+ obj_sign(4796, 3412, 68, 20, "* (The door is locked.)\n* (Both puzzles have to be solved.)");
+ }
+ obj_sign(4278, 3570, 26, 110, "* Phew, the laser's off!\n* ...now what do I do?|* I guess I'll keep standing here.");
+ obj_sign(5236, 3666, 60, 40, "* Did you catch Mettaton's show?\n* That human looked so real!|* His fan club is HUGE, you know.");
+ obj_sign(5286, 3432, 24, 24, "* My co-workers won't help me\n with this puzzle.|* Do I even WANT to go to work?");
+ break;
+ case ROOM_HT_VENTISLE:
+ if (!flag(F_HT_CALL_ISLE)) {
+ Obj *t = obj_trigger(4800, 3150, 60, 12, EV_HT_CALL, ACTOR_TRIGGER);
+ if (t) t->arg = 3;
+ }
+ break;
+ case ROOM_HT_COOKING:
+ if (!flag(F_HT_COOK)) {
+ obj_mtt(5662, 3138);
+ obj_trigger(5330, 3000, 12, 260, EV_HT_COOK, ACTOR_TRIGGER);
+ }
+ obj_sign(5500, 3005, 110, 85, "* (The set's kitchen: a fridge, an oven, a counter.)");
+ break;
+ case ROOM_HT_COREVIEW:
+ if (!flag(F_HT_CALL_CORE)) {
+ Obj *t = obj_trigger(6300, 3080, 12, 120, EV_HT_CALL, ACTOR_TRIGGER);
+ if (t) t->arg = 4;
+ }
+ thing(6950, 3070, 76, 48, EV_HT_ELEVATOR, ACTOR_DOOR, 0);
+ break;
+ }
+}
+
+/* ---------------------------------------------------------------- the events */
+static int ev_lab(Ev *c) {
+ Obj *a = obj_get(ACTOR_ALPHYS), *m = obj_get(ACTOR_METTATON);
+ EV_BEGIN;
+ if (c->self) gone(c->self);
+ /* the Lab is dark; then the lights come on, and its big screen shows Frisk, right here */
+ NARRATE("* (It's dark in here.)");
+ g_shake = 2;
+ WAIT(20);
+ NARRATE("* (The lights come on.)\n* (A huge screen shows... you.)");
+ a = obj_alphys(2212, 6275, DIR_LEFT);
+ WALK(a, pl.x + 70, pl.y + 29, 2);
+ SAY(FACE_ALPHYS, "* Oh! Oh no, you're already here!|* Um. H-hi! I'm Dr. Alphys.\n* The Royal Scientist.|* I've been, um, watching you.\n* On my cameras.|* Ever since the Ruins.\n* The fights, the friends...|* It's been kind of amazing?|* I was supposed to stop you.\n* But, um... I'd rather help.");
+ /* and through the wall comes Mettaton */
+ g_shake = 10;
+ m = obj_mtt(2215, 6232);
+ WAIT(20);
+ NARRATE("* GOOD EVENING, BEAUTIES!\n* WELCOME TO TODAY'S QUIZ SHOW!");
+ BATTLE(FORM_MTT_QUIZ);
+ m = obj_get(ACTOR_METTATON);
+ WALK(m, m->x, m->y - 200, 6);
+ gone(obj_get(ACTOR_METTATON));
+ set_flag(F_HT_LAB, 1);
+ a = obj_get(ACTOR_ALPHYS);
+ SAY(FACE_ALPHYS, "* S-sorry about him.\n* I built him, a long time ago.|* He was meant to be an\n entertainment robot...|* Oh! Your phone is SO old.\n* Let me fix it up!");
+ NARRATE("* (Alphys upgrades your phone.)\n* (Now it opens a Dimensional Box, too.)");
+ set_flag(F_HT_LAB, 2);
+ SAY(FACE_ALPHYS, "* There! It has a few new...\n features.|* I'll, um, call you if I can help.|* Now I have to go to the...\n bathroom. Good luck!");
+ a = obj_get(ACTOR_ALPHYS);
+ WALK(a, 2342, 6225, 3);
+ gone(obj_get(ACTOR_ALPHYS));
+ EV_END;
+}
+
+static int ev_fridge(Ev *c) {
+ EV_BEGIN;
+ if (!flag(F_HT_NOODLES)) {
+ NARRATE("* (The fridge is full of\n instant noodles. Take one?)@Take@Leave");
+ if (CHOICE() == 0) {
+ if (!item_add(ITEM_INSTANT_NOODLES)) {
+ NARRATE("* (You're carrying too much.)");
+ } else {
+ set_flag(F_HT_NOODLES, 1);
+ NARRATE("* (You got the Instant Noodles.)");
+ }
+ }
+ } else {
+ NARRATE("* (Noodles, as far as the eye can see.)");
+ }
+ EV_END;
+}
+
+static int ev_item(Ev *c) {
+ EV_BEGIN;
+ if (!item_add(ITEM_BURNT_PAN)) {
+ NARRATE("* (You're carrying too much.)");
+ return 1;
+ }
+ set_flag(F_HT_PAN, 1);
+ NARRATE("* (You got the Burnt Pan.)");
+ gone(obj_get(ACTOR_ITEM));
+ if (c->self) gone(c->self);
+ EV_END;
+}
+
+static int ev_vent(Ev *c) {
+ EV_BEGIN;
+ c->v[0] = c->self ? c->self->arg : 0;
+ c->v[1] = vent_dir(&vents[c->v[0]]);
+ /* the steam blows Frisk over the lava */
+ pl.dir = (uint8_t)c->v[1];
+ if (vents[c->v[0]].dir == -1) hub_turns++;
+ PL_WALK(vents[c->v[0]].t[c->v[1]][0], vents[c->v[0]].t[c->v[1]][1], 7);
+ EV_END;
+}
+
+static int ev_laser_hit(Ev *c) {
+ EV_BEGIN;
+ g_exclaim = 20;
+ WAIT(15);
+ BATTLE(FORM_LASERS);
+ /* (out of the beam's way) */
+ pl.x = (int16_t)(pl.x - 26);
+ laser_cool = 60;
+ EV_END;
+}
+
+/* Alphys's calls: 0 the lasers, 1 the puzzle hub, 2 the blue laser off, 3 the door already open, 4 by the CORE */
+static int ev_call(Ev *c) {
+ EV_BEGIN;
+ c->v[0] = c->self ? c->self->arg : 0;
+ if (c->self) gone(c->self);
+ switch (c->v[0]) {
+ case 0:
+ set_flag(F_HT_CALL_LASER, 1);
+ SAY(FACE_ALPHYS, "* Ring...|* H-hi! It's Alphys!|* Lasers up ahead!\n* Orange ones: keep moving!|* Blue ones: stand still.\n* Like the attacks, see?|* Okay! Bye!");
+ break;
+ case 1:
+ set_flag(F_HT_CALL_HUB, 1);
+ SAY(FACE_ALPHYS, "* Ring...|* It's Alphys again!|* That door north stays shut until\n both puzzles are done.|* The one on the right is easier.\n* Try it first!");
+ break;
+ case 2:
+ set_flag(F_HT_BLUE_OFF, 1);
+ SAY(FACE_ALPHYS, "* Ring...|* Oh! A laser's blocking you?|* Hang on, hang on...");
+ gone(obj_get(ACTOR_LASER));
+ NARRATE("* (The blue laser switches off.)");
+ SAY(FACE_ALPHYS, "* There! Got it!");
+ break;
+ case 3:
+ set_flag(F_HT_CALL_ISLE, 1);
+ SAY(FACE_ALPHYS, "* Ring...|* Hi! So, those puzzles by\n the door: shoot the blocks\n out of the way, then...|* ...|* Oh. You already did them.|* Ha ha... never mind!\n* Bye!");
+ break;
+ case 4:
+ set_flag(F_HT_CALL_CORE, 1);
+ SAY(FACE_ALPHYS, "* Ring...|* See that, far off?\n* That's the CORE.|* It powers the whole\n underground.|* The king's castle is past it.\n* We're almost there!|* D-don't worry. I've got your back.");
+ break;
+ }
+ EV_END;
+}
+
+static int ev_shoot(Ev *c) {
+ EV_BEGIN;
+ c->v[0] = c->self ? c->self->arg : 0;
+ if (flag(c->v[0] == SHOOT_LEFT ? F_HT_PUZ_L : F_HT_PUZ_R)) {
+ NARRATE("* (This puzzle is solved.)");
+ return 1;
+ }
+ shoot_open(c->v[0]);
+ WAIT_UNTIL(g_mode == MODE_WORLD);
+ if (shoot_result()) {
+ set_flag(c->v[0] == SHOOT_LEFT ? F_HT_PUZ_L : F_HT_PUZ_R, 1);
+ if (flag(F_HT_PUZ_L) && flag(F_HT_PUZ_R)) {
+ gone(obj_get(ACTOR_DOOR));
+ NARRATE("* (Somewhere, a door unlocks.)");
+ } else {
+ NARRATE("* (One down.)");
+ }
+ }
+ EV_END;
+}
+
+static int ev_cook(Ev *c) {
+ Obj *m = obj_get(ACTOR_METTATON);
+ EV_BEGIN;
+ if (c->self) gone(c->self);
+ /* the lights: Mettaton's cooking show */
+ NARRATE("* (The lights come on.)");
+ NARRATE("* GREETINGS, BEAUTIES!\n* WELCOME TO COOKING WITH A KILLER ROBOT!|* TODAY'S RECIPE: A CAKE!\n* MY LOVELY ASSISTANT WILL FETCH\n THE SUGAR, MILK AND EGGS!");
+ PL_WALK(5600, 3114, 3);
+ NARRATE("* (You bring the sugar, the milk\n and the eggs.)");
+ NARRATE("* PERFECT! AND THE LAST INGREDIENT...\n* A HUMAN SOUL!");
+ g_shake = 6;
+ SAY(FACE_ALPHYS, "* Ring...|* W-wait! Mettaton!\n* What about a vegan option?\n* For the viewers at home?");
+ NARRATE("* ...OH, VERY WELL.|* THE SUBSTITUTE IS ON THE\n COUNTER, OVER THERE.\n* GO GET IT!");
+ PL_WALK(5900, 3114, 3);
+ NARRATE("* (The counter starts to grow, taller and taller...)");
+ SAY(FACE_ALPHYS, "* Your phone has a JETPACK button!\n* I'm turning it on... now!");
+ BATTLE(FORM_MTT_COOK);
+ set_flag(F_HT_COOK, cook_won() ? 1 : 2);
+ m = obj_get(ACTOR_METTATON);
+ WALK(m, m->x, m->y - 200, 6);
+ gone(obj_get(ACTOR_METTATON));
+ SAY(FACE_ALPHYS, "* Ring...|* Phew... are you okay?\n* Keep going! Right, and up!");
+ EV_END;
+}
+
+static int ev_elevator(Ev *c) {
+ EV_BEGIN;
+ NARRATE("* (The elevator up.)\n* (Get in?)@Yes@No");
+ if (CHOICE() == 1) return 1;
+ set_flag(F_HT_FLOOR1, 1);
+ FADE_TO(ROOM_HT_HOTDOG, 6966, 2380, DIR_DOWN);
+ EV_END;
+}
+
+int ht1_event(Ev *c) {
+ switch (c->id) {
+ case EV_HT_LAB: return ev_lab(c);
+ case EV_HT_FRIDGE: return ev_fridge(c);
+ case EV_HT_ITEM: return ev_item(c);
+ case EV_VENT: return ev_vent(c);
+ case EV_LASER_HIT: return ev_laser_hit(c);
+ case EV_HT_CALL: return ev_call(c);
+ case EV_SHOOT: return ev_shoot(c);
+ case EV_HT_COOK: return ev_cook(c);
+ case EV_HT_ELEVATOR: return ev_elevator(c);
+ }
+ return -1;
+}
diff --git a/games/undertale/src/ht2.c b/games/undertale/src/ht2.c
new file mode 100644
index 00000000..06186543
--- /dev/null
+++ b/games/undertale/src/ht2.c
@@ -0,0 +1,499 @@
+/* Hotland, its second floor: out of the elevator, Sans's hot dog stand and Heats Flamesman; the crossroads (the
+ * invisible path up to the Art Club's room, the Stained Apron down south); the conveyor belt puzzle and the vent
+ * puzzle; the Bad Opinion Zone; the Royal Guards at the bend; Mettaton's news report and its six bombs; the CORE
+ * again, and the elevator up to the third floor. (As the Undertale Wiki describes them; what they say is ours, in short.) */
+#include
+#include
+#include "battle.h"
+#include "event.h"
+#include "events.h"
+#include "game.h"
+#include "gfx.h"
+#include "hotland.h"
+#include "items.h"
+#include "menu.h"
+#include "monsters.h"
+#include "res.h"
+#include "save.h"
+#include "world.h"
+
+static void gone(Obj *o) { o->kind = OBJ_NONE; }
+static Obj *thing(int x, int y, int w, int h, int event, int name, int arg) {
+ Obj *o = obj_trigger(x, y, w, h, event, name);
+ if (o) o->flags = OF_TALK | OF_SOLID, o->arg = (int16_t)arg;
+ return o;
+}
+static Obj *call_at(int x, int y, int w, int h, int arg) {
+ Obj *t = obj_trigger(x, y, w, h, EV_HT2_CALL, ACTOR_TRIGGER);
+ if (t) t->arg = (int16_t)arg;
+ return t;
+}
+/* a force field: a wall of light across the way */
+static void field(int x, int y, int w, int h) {
+ Obj *o = obj_trigger(x, y, w, h, EV_NONE, ACTOR_FIELD);
+ if (o) o->flags = OF_SOLID;
+}
+static void fields_down(void) {
+ for (Obj *o = obj_find(ACTOR_FIELD); o; o = obj_find(ACTOR_FIELD)) gone(o);
+}
+static void draw_fields(void) {
+ for (int i = 0; i < MAX_OBJS; i++) {
+ const Obj *o = &objs[i];
+ if (o->kind == OBJ_NONE || o->name != ACTOR_FIELD) continue;
+ uint16_t c = (g_frame / 3) % 2 ? rgb565(0x30FF60) : rgb565(0x10A040);
+ gfx_rect(o->x - cam_x, o->y - cam_y, o->bw, o->bh, c, 200);
+ }
+}
+
+/* ---------------------------------------------------------------- the conveyor belt puzzle */
+/* three switches over the top belt (it runs left): pressed right to left, each soon after the last */
+#define SW_Y 2322
+#define SW_WINDOW 45
+static const int16_t sw_x[3] = {5200, 5285, 5370};
+static int sw_lit, sw_t; /* lit: a bit each; t: frames since the last press */
+
+/* ---------------------------------------------------------------- the vent puzzle */
+/* eight platforms, each with its vents (the ways they blow, by DIR_*); landing on a panel turns every vent the other
+ * way, landing on the top one turns them back; blown off where there's no platform, Frisk lands on the belts
+ * below, or back at the start */
+typedef struct {
+ int16_t x, y;
+ uint8_t vents, panel; /* panel: 1 turns the vents, 2 turns them back */
+} Plat;
+#define VB(d) (1 << (d))
+static const Plat plats[] = {
+ {4620, 2160, VB(DIR_DOWN), 2}, /* the top one */
+ {4520, 2260, VB(DIR_RIGHT), 1}, /* turned, it blows left: the way out */
+ {4620, 2260, VB(DIR_UP) | VB(DIR_RIGHT), 1},
+ {4720, 2260, VB(DIR_LEFT), 1}, /* the start */
+ {4520, 2360, 15, 1},
+ {4620, 2360, VB(DIR_UP) | VB(DIR_DOWN), 1},
+ {4520, 2460, 15, 0},
+ {4620, 2460, 15, 0},
+};
+#define NPLATS ((int)(sizeof plats / sizeof plats[0]))
+#define VP_START_X 4740
+#define VP_START_Y 2340
+static const int8_t ddx[4] = {0, 0, -1, 1}, ddy[4] = {1, -1, 0, 0};
+static bool vp_turned;
+
+/* ---------------------------------------------------------------- Mettaton's news report */
+enum { NEWS_DARK, NEWS_PICK, NEWS_TIMER, NEWS_OVER };
+#define NEWS_TIME (120 * 30)
+static int news, news_t, news_why;
+static uint8_t defused;
+/* where each bomb's feet are, t frames into the show (the dog, the game and the present stay put: the map shows
+ * them) */
+static void bomb_at(int k, int t, int *x, int *y) {
+ switch (k) {
+ case BOMB_DOG: *x = 4015, *y = 1730; break;
+ case BOMB_GAME: *x = 3918, *y = 1978; break;
+ case BOMB_PRESENT: *x = 4366, *y = 1588; break;
+ case BOMB_SCRIPT: *x = 4332, *y = 1946 - t % 150; break; /* riding the belt up */
+ case BOMB_BALL: { /* bouncing along the top room */
+ int p = t % 300, b = gfx_sin(t * 256 / 30 % 256);
+ *x = 3950 + 250 * (p < 150 ? p : 300 - p) / 150, *y = 1604 - 18 * (b < 0 ? -b : b) / 1024;
+ break;
+ }
+ default: /* the glass of water darts every way */
+ *x = 4070 + 130 * gfx_sin(t * 3 % 256) / 1024, *y = 1626 + 24 * gfx_sin((t * 5 + 64) % 256) / 1024;
+ break;
+ }
+}
+static bool bomb_moves(int k) { return k == BOMB_SCRIPT || k == BOMB_BALL || k == BOMB_WATER; }
+static uint16_t bomb_spr(int k) {
+ if (k == BOMB_SCRIPT) return news >= NEWS_TIMER ? SPR_BOMB_SCRIPT : SPR_NEWS_SCRIPT;
+ return k == BOMB_BALL ? SPR_BOMB_BALL : SPR_BOMB_WATER;
+}
+static int news_frame;
+static void bombs_setup(void) {
+ for (int k = 0; k < BOMB_COUNT; k++) {
+ int x, y;
+ bomb_at(k, news_frame, &x, &y);
+ Obj *o;
+ if (bomb_moves(k)) {
+ o = obj_deco(bomb_spr(k), x, y, false);
+ if (o) o->name = ACTOR_BOMB, obj_set_box(o, -10, -16, 20, 16), o->event = EV_BOMB;
+ } else {
+ /* (the dog lies along the wall: its box reaches down to where Frisk can stand) */
+ o = obj_trigger(x - 12, y - 18, 24, k == BOMB_DOG ? 26 : 18, EV_BOMB, ACTOR_BOMB);
+ }
+ if (!o) continue;
+ o->arg = (int16_t)k;
+ /* before the show: things to report on; during it, bombs to touch; defused, nothing */
+ o->flags = (uint16_t)((o->flags & ~(OF_TOUCH | OF_TALK | OF_SOLID)) |
+ (defused & (1 << k) || news == NEWS_OVER || flag(F_HT_NEWS) ? 0
+ : news == NEWS_TIMER ? OF_TOUCH : OF_TALK | (bomb_moves(k) ? 0 : OF_SOLID)));
+ }
+}
+static void bombs_arm(void) {
+ for (int i = 0; i < MAX_OBJS; i++) {
+ Obj *o = &objs[i];
+ if (o->kind != OBJ_NONE && o->name == ACTOR_BOMB) o->flags = (uint16_t)((o->flags & ~(OF_TALK | OF_SOLID)) | OF_TOUCH);
+ }
+}
+static void bombs_move(void) {
+ if (news == NEWS_DARK) return;
+ news_frame++;
+ for (int i = 0; i < MAX_OBJS; i++) {
+ Obj *o = &objs[i];
+ if (o->kind == OBJ_NONE || o->name != ACTOR_BOMB || !bomb_moves(o->arg) || (defused & (1 << o->arg))) continue;
+ int x, y;
+ bomb_at(o->arg, news_frame, &x, &y);
+ o->x = (int16_t)x, o->y = (int16_t)y, o->spr[0] = bomb_spr(o->arg);
+ }
+}
+static void news_fields(void) {
+ field(3796, 1756, 6, 48); /* back west */
+ field(4580, 1700, 64, 6); /* on, up north */
+}
+
+/* ---------------------------------------------------------------- drawn, and watched */
+void ht2_draw(void) {
+ if (g_room < ROOM_HT_HOTDOG || g_room > ROOM_HT_COREVIEW2) return;
+ draw_fields();
+ if (g_room == ROOM_HT_BELTPUZ)
+ for (int i = 0; i < 3; i++) {
+ bool on = flag(F_HT_BELT) || (sw_lit >> i & 1);
+ gfx_rect(sw_x[i] - 4 - cam_x, SW_Y + 4 - cam_y, 8, 6, on ? rgb565(0x40FF40) : rgb565(0x802020), 255);
+ }
+ if (g_room == ROOM_HT_VENTPUZ) {
+ static const uint16_t arrow[4] = {SPR_VENT_D, SPR_VENT_U, SPR_VENT_L, SPR_VENT_R};
+ for (int p = 0; p < NPLATS; p++)
+ for (int g = 0; g < 4; g++)
+ if (plats[p].vents & VB(g))
+ gfx_sprite(arrow[g ^ vp_turned], 0, plats[p].x + 20 + ddx[g] * 20 - cam_x, plats[p].y + 20 + ddy[g] * 20 - cam_y, 0, 255);
+ }
+}
+
+/* over everything: the news report's clock */
+void ht2_draw_top(void) {
+ if (g_room != ROOM_HT_NEWS || news != NEWS_TIMER) return;
+ char s[16];
+ int secs = (news_t + 29) / 30;
+ snprintf(s, sizeof s, "%d:%02d", secs / 60, secs % 60);
+ gfx_rect(4, 4, 52, 18, C_BLACK, 200);
+ gfx_text(FONT_MAIN, 10, 6, s, secs <= 20 ? C_RED : C_WHITE, 255);
+}
+
+void ht2_update(void) {
+ if (g_room < ROOM_HT_HOTDOG || g_room > ROOM_HT_COREVIEW2) return;
+ if (sw_t < 1000) sw_t++;
+ if (sw_lit && sw_t > SW_WINDOW && !flag(F_HT_BELT)) sw_lit = 0;
+ if (g_room == ROOM_HT_NEWS) {
+ bombs_move();
+ if (news == NEWS_TIMER && !event_running()) {
+ if (defused == (1 << BOMB_COUNT) - 1) news_why = 1, event_start(EV_NEWS, NULL);
+ else if (--news_t <= 0) news_why = 2, event_start(EV_NEWS, NULL);
+ }
+ }
+}
+
+/* ---------------------------------------------------------------- the rooms */
+static Obj *obj_rg(int name, int x, int y) {
+ static const uint16_t s1[4] = {SPR_RG01_OW, SPR_RG01_OW, SPR_RG01_OW, SPR_RG01_OW};
+ static const uint16_t s2[4] = {SPR_RG02_OW, SPR_RG02_OW, SPR_RG02_OW, SPR_RG02_OW};
+ Obj *o = obj_npc(name, name == ACTOR_RG01 ? s1 : s2, x, y, DIR_RIGHT, 0);
+ if (o) o->flags &= (uint16_t)~OF_SOLID;
+ return o;
+}
+
+void ht2_setup(int room, int from) {
+ if (room < ROOM_HT_HOTDOG || room > ROOM_HT_COREVIEW2) return;
+ switch (room) {
+ case ROOM_HT_HOTDOG:
+ thing(6585, 2346, 58, 36, EV_HOTDOG, ACTOR_ITEM, 0);
+ thing(7056, 2420, 28, 30, EV_HEATS, ACTOR_ITEM, 0);
+ thing(6940, 2330, 80, 64, EV_HT2_ELEVATOR, ACTOR_DOOR, 0);
+ obj_sign(6696, 2364, 30, 30, "* Snow on a hot dog stand?\n* Here, in Hotland?|* Guess somebody's too lazy\n to clean it off.");
+ obj_sign(6618, 2436, 28, 30, "* TOASTY BUN!");
+ break;
+ case ROOM_HT_ARTCLUB:
+ obj_sign(5896, 2146, 30, 30, "* (ART CLUB ROOM.)\n* (It meets on one night a year,\n at one hour only.)");
+ break;
+ case ROOM_HT_APRON:
+ if (!flag(F_HT_APRON)) {
+ Obj *p = obj_deco(SPR_APRON, 5836, 2618, false);
+ if (p) p->name = ACTOR_ITEM;
+ Obj *t = obj_trigger(5832, 2614, 30, 26, EV_APRON, ACTOR_ITEM);
+ if (t) t->flags = OF_TALK;
+ }
+ break;
+ case ROOM_HT_BELTPUZ:
+ if (!flag(F_HT_CALL_BELT)) call_at(5640, 2370, 12, 70, 0);
+ if (!flag(F_HT_BELT)) field(5066, 2370, 6, 70);
+ for (int i = 0; i < 3; i++) {
+ Obj *s = obj_trigger(sw_x[i] - 12, SW_Y, 24, 22, EV_BELT_SWITCH, ACTOR_ITEM);
+ if (s) s->flags = OF_TALK, s->arg = (int16_t)i;
+ }
+ sw_lit = 0;
+ break;
+ case ROOM_HT_VENTPUZ:
+ for (int p = 0; p < NPLATS; p++)
+ for (int g = 0; g < 4; g++)
+ if (plats[p].vents & VB(g)) {
+ Obj *o = obj_trigger(plats[p].x + 24 + ddx[g] * 20, plats[p].y + 24 + ddy[g] * 20, 12, 12, EV_VENTPUZ, ACTOR_VENT);
+ if (o) o->arg = (int16_t)(p * 4 + g);
+ }
+ break;
+ case ROOM_HT_BOZ:
+ obj_save(4005, 2252, "* (The smell of melted cheese fills you with determination.)");
+ obj_sign(3918, 2240, 36, 32, "* (A safe on a little table.)\n* (The cheese inside has melted\n onto the table.)");
+ obj_sign(3958, 2246, 16, 20, "* (A mouse hole.)\n* (Someone wants that cheese.)");
+ if (!flag(F_HT_CALL_BOZ)) call_at(4300, 2250, 12, 80, 2);
+ break;
+ case ROOM_HT_GUARDS:
+ if (!flag(F_HT_GUARDS)) obj_trigger(3560, 2240, 10, 100, EV_GUARDS, ACTOR_TRIGGER);
+ break;
+ case ROOM_HT_NEWS:
+ if (!flag(F_HT_NEWS)) {
+ /* (come in from elsewhere mid-show: only a SAVE loaded, as its exits are shut; it starts over) */
+ if (from != ROOM_HT_NEWS) news = NEWS_DARK;
+ if (news == NEWS_DARK) {
+ defused = 0, news_frame = 0;
+ obj_trigger(3880, 1750, 10, 60, EV_NEWS, ACTOR_TRIGGER);
+ } else news_fields();
+ Obj *m = obj_deco(SPR_MTT_NEWS, 4186, 1781, true);
+ if (m) m->name = ACTOR_METTATON, m->flags |= OF_ANIM, m->anim_speed = 8, obj_set_box(m, -16, -10, 32, 10);
+ }
+ bombs_setup();
+ break;
+ case ROOM_HT_COREVIEW2:
+ if (!flag(F_HT_CALL_CORE2)) call_at(4596, 1420, 48, 10, 3);
+ thing(3740, 1300, 80, 60, EV_HT2_ELEVATOR, ACTOR_DOOR, 1);
+ break;
+ }
+}
+
+/* ---------------------------------------------------------------- the events */
+static int ev_hotdog(Ev *c) {
+ EV_BEGIN;
+ SAY(FACE_SANS, "* hey. hot dogs, 30G apiece.\n* want one?@Buy@No thanks");
+ if (CHOICE() == 0) {
+ if (sv.gold < 30) {
+ SAY(FACE_SANS, "* you're a little short, kid.");
+ } else if (!item_add(ITEM_HOT_DOG)) {
+ SAY(FACE_SANS, "* hands full, huh?\n* come back when they aren't.");
+ } else {
+ sv.gold = (uint16_t)(sv.gold - 30);
+ SAY(FACE_SANS, "* here you go. enjoy.");
+ }
+ }
+ EV_END;
+}
+
+static int ev_heats(Ev *c) {
+ EV_BEGIN;
+ if (!flag(F_HT_HEATS)) {
+ set_flag(F_HT_HEATS, 1);
+ NARRATE("* I'M HEATS FLAMESMAN!!!\n* REMEMBER MY NAME!!!");
+ } else {
+ NARRATE("* DON'T YOU FORGET IT!!!\n* HEATS FLAMESMAN!!!");
+ }
+ EV_END;
+}
+
+static int ev_apron(Ev *c) {
+ EV_BEGIN;
+ if (!item_add(ITEM_STAINED_APRON)) {
+ NARRATE("* (You're carrying too much.)");
+ return 1;
+ }
+ set_flag(F_HT_APRON, 1);
+ NARRATE("* (You got the Stained Apron.)");
+ gone(obj_get(ACTOR_ITEM));
+ if (c->self) gone(c->self);
+ EV_END;
+}
+
+static int ev_belt_switch(Ev *c) {
+ int next = !sw_lit ? 2 : sw_lit == 4 ? 1 : 0;
+ EV_BEGIN;
+ c->v[0] = c->self ? c->self->arg : 0;
+ if (flag(F_HT_BELT)) return 1;
+ /* right to left: the first restarts it, one out of turn spoils it */
+ if (c->v[0] == next) sw_lit |= 1 << c->v[0];
+ else sw_lit = c->v[0] == 2 ? 4 : 0;
+ sw_t = 0;
+ if (sw_lit != 6) return 1;
+ /* two of three: Alphys calls to cheer Frisk on, just as the last one goes by */
+ SAY(FACE_ALPHYS, "* Ring...|* Now! Hit the last one now!|* ...|* Oh no. I made you miss it.|* Um! Actually, only the\n first two mattered!\n* Really!|* I'll just turn off the\n field.");
+ set_flag(F_HT_BELT, 1);
+ fields_down();
+ NARRATE("* (The force field switches off.)");
+ EV_END;
+}
+
+static int ev_ventpuz(Ev *c) {
+ EV_BEGIN;
+ c->v[0] = c->self ? c->self->arg / 4 : 0;
+ c->v[1] = (c->self ? c->self->arg % 4 : 0) ^ vp_turned;
+ c->v[2] = -1;
+ for (int q = 0; q < NPLATS; q++)
+ if (plats[q].x == plats[c->v[0]].x + ddx[c->v[1]] * 100 && plats[q].y == plats[c->v[0]].y + ddy[c->v[1]] * 100) c->v[2] = q;
+ c->v[3] = plats[c->v[0]].x + 20 + ddx[c->v[1]] * 100, c->v[4] = plats[c->v[0]].y + 6 + ddy[c->v[1]] * 100;
+ pl.dir = (uint8_t)c->v[1];
+ if (c->v[2] < 0 && !world_walkable(c->v[3] + 3, c->v[4] + 20, 14, 9, -1)) {
+ /* nowhere to land: off into the haze, and back at the start */
+ PL_WALK(plats[c->v[0]].x + 20 + ddx[c->v[1]] * 40, plats[c->v[0]].y + 6 + ddy[c->v[1]] * 40, 7);
+ vp_turned = false;
+ FADE_TO(ROOM_HT_VENTPUZ, VP_START_X, VP_START_Y, DIR_UP);
+ return 1;
+ }
+ PL_WALK(c->v[3], c->v[4], 7);
+ if (c->v[2] >= 0) {
+ if (plats[c->v[2]].panel == 2) vp_turned = false;
+ else if (plats[c->v[2]].panel) vp_turned = !vp_turned;
+ } else if (c->v[3] < plats[1].x) {
+ set_flag(F_HT_VENTPUZ, 1); /* (out, west) */
+ }
+ EV_END;
+}
+
+/* Alphys's calls: 0 the belt puzzle ahead, 2 in the Bad Opinion Zone, 3 by the CORE again */
+static int ev_call(Ev *c) {
+ EV_BEGIN;
+ c->v[0] = c->self ? c->self->arg : 0;
+ if (c->self) gone(c->self);
+ switch (c->v[0]) {
+ case 0:
+ set_flag(F_HT_CALL_BELT, 1);
+ SAY(FACE_ALPHYS, "* Ring...|* It's Alphys! There's a belt\n puzzle up ahead.|* Ride the top belt, and hit\n all three switches, one\n after another.|* Don't worry, I'll help you\n with the timing!");
+ break;
+ case 2:
+ set_flag(F_HT_CALL_BOZ, 1);
+ SAY(FACE_ALPHYS, "* Ring...|* Oh! H-hi. Sorry, I'm in\n a bad mood.|* The sequel to my favorite\n anime came out, and... ugh.|* They got EVERYTHING wrong.\n* Don't get me started.|* ...|* I already started, huh.\n* Sorry! Bye!");
+ break;
+ case 3:
+ set_flag(F_HT_CALL_CORE2, 1);
+ SAY(FACE_ALPHYS, "* Ring...|* Phew! You made it through\n the news.|* Mettaton's out of control.|* The elevator's just ahead.\n* Keep going!");
+ break;
+ }
+ EV_END;
+}
+
+static int ev_guards(Ev *c) {
+ Obj *a = obj_get(ACTOR_RG01), *b = obj_get(ACTOR_RG02);
+ EV_BEGIN;
+ if (c->self) gone(c->self);
+ /* two of the Royal Guard, from round the bend */
+ a = obj_rg(ACTOR_RG01, 3300, pl.y + 29);
+ b = obj_rg(ACTOR_RG02, 3250, pl.y + 29);
+ WALK_TO(b, pl.x - 70, pl.y + 29, 3);
+ WALK(a, pl.x - 30, pl.y + 29, 3);
+ NARRATE("* Hey! You! Hold up!|* So, like, we got a tip about a\n human in a striped shirt.\n* Around here, right now.|* Scary, huh? But stay chill.\n* We'll get you somewhere safe.");
+ NARRATE("* ...|* Huh? What is it, bro?|* Their shirt? What about it?|* ...|* Oh. OH.|* Dude, this is, like, SO\n awkward.|* We totally have to fight\n you now.");
+ BATTLE(FORM_RGUARDS);
+ a = obj_get(ACTOR_RG01), b = obj_get(ACTOR_RG02);
+ if (bt.mon[0].killed || bt.mon[1].killed) {
+ set_flag(F_HT_GUARDS, 2);
+ if (bt.mon[0].killed) gone(a);
+ if (bt.mon[1].killed) gone(b);
+ return 1;
+ }
+ set_flag(F_HT_GUARDS, 1);
+ NARRATE("* So, uh... we're gonna go get\n some ice cream now.|* Later, dude!");
+ WALK_TO(b, 3230, b->y, 3);
+ WALK(a, 3230, a->y, 3);
+ gone(obj_get(ACTOR_RG01));
+ gone(obj_get(ACTOR_RG02));
+ EV_END;
+}
+
+static int ev_news(Ev *c) {
+ Obj *m = obj_get(ACTOR_METTATON);
+ EV_BEGIN;
+ if (c->self) gone(c->self);
+ c->v[0] = news_why;
+ if (c->v[0] == 0) {
+ /* the lights, and the show */
+ NARRATE("* (It's pitch dark in here.)");
+ SAY(FACE_ALPHYS, "* Ring...|* It's dark in there, huh?\n* Hang on, I'll get the\n lights...");
+ NARRATE("* (The lights come on.)");
+ NARRATE("* OHHH YES! GOOD EVENING, AND\n WELCOME TO... MTT NEWS!|* I'M YOUR ANCHOR, METTATON.|* TONIGHT, OUR BRAVE REPORTER\n FINDS THE STORY OF THE YEAR!|* GO ON, DARLING! FIND SOMETHING\n NEWSWORTHY TO REPORT ON!");
+ news_fields();
+ news = NEWS_PICK;
+ return 1;
+ }
+ /* the end: every bomb defused, or out of time */
+ if (c->v[0] == 1) {
+ NARRATE("* WHAT?! YOU DEFUSED THEM ALL?!|* ...NO MATTER! THE BIG ONE GOES\n OFF IN TWO SECONDS!|* ONE... TWO...");
+ SAY(FACE_ALPHYS, "* Ring...|* Got it! I hacked into it!\n* It's off!");
+ } else {
+ NARRATE("* AND THAT'S TIME!!!|* ...|* ...HM? IT ISN'T GOING OFF.");
+ SAY(FACE_ALPHYS, "* Ring...|* Phew! I got into the big\n one. Just in time!");
+ }
+ NARRATE("* ALPHYS HACKED MY BOMB?!|* UGH! THAT'S ALL FOR TONIGHT,\n FOLKS!");
+ m = obj_get(ACTOR_METTATON);
+ WALK(m, m->x, m->y - 200, 6);
+ gone(obj_get(ACTOR_METTATON));
+ fields_down();
+ news = NEWS_OVER;
+ set_flag(F_HT_NEWS, c->v[0]);
+ for (int i = 0; i < MAX_OBJS; i++)
+ if (objs[i].kind != OBJ_NONE && objs[i].name == ACTOR_BOMB) objs[i].flags &= (uint16_t)~(OF_TOUCH | OF_TALK | OF_SOLID);
+ EV_END;
+}
+
+static const char *const reports[BOMB_COUNT] = {
+ [BOMB_DOG] = "* A DOG! HOW... ORDINARY.",
+ [BOMB_WATER] = "* A GLASS OF WATER!\n* IT'S... VERY QUICK.",
+ [BOMB_SCRIPT] = "* THE SCRIPT OF MY NEXT FILM!\n* A FINE CHOICE, DARLING.",
+ [BOMB_BALL] = "* A BASKETBALL! NO, WAIT, A\n FASHION STATEMENT!",
+ [BOMB_PRESENT] = "* A PRESENT! FOR ME?\n* YOU SHOULDN'T HAVE!",
+ [BOMB_GAME] = "* A VIDEO GAME! I'M IN IT, YOU\n KNOW. NEAR THE END.",
+};
+static int ev_bomb(Ev *c) {
+ EV_BEGIN;
+ c->v[0] = c->self ? c->self->arg : 0;
+ if (news == NEWS_PICK) {
+ /* the story: it's a bomb, and so is everything else */
+ NARRATE(reports[c->v[0]]);
+ NARRATE("* ...BUT WAIT!!!|* IT'S A BOMB!!!|* IN FACT, EVERYTHING IN HERE\n IS A BOMB! SURPRISE!|* AND THE BIG ONE UP NORTH GOES\n OFF IN TWO MINUTES!|* STAY TUNED, VIEWERS!\n* THIS IS GOING TO BE EXPLOSIVE!");
+ SAY(FACE_ALPHYS, "* Ring...|* D-don't panic! I put a bomb\n defuser in your phone!|* Touch a bomb, then defuse\n it while it's in the\n DEFUSE ZONE!");
+ news = NEWS_TIMER, news_t = NEWS_TIME;
+ bombs_arm();
+ return 1;
+ }
+ if (news != NEWS_TIMER || (defused >> c->v[0] & 1)) return 1;
+ g_bomb_kind = c->v[0];
+ BATTLE(FORM_BOMB);
+ if (battle_result() == BR_SPARED) {
+ defused = (uint8_t)(defused | 1 << c->v[0]);
+ for (int i = 0; i < MAX_OBJS; i++)
+ if (objs[i].kind != OBJ_NONE && objs[i].name == ACTOR_BOMB && objs[i].arg == c->v[0]) objs[i].flags &= (uint16_t)~OF_TOUCH;
+ }
+ EV_END;
+}
+
+static int ev_elevator(Ev *c) {
+ EV_BEGIN;
+ c->v[0] = c->self ? c->self->arg : 0;
+ if (c->v[0] == 0) {
+ NARRATE("* (The elevator back down.)\n* (Get in?)@Yes@No");
+ if (CHOICE() == 1) return 1;
+ FADE_TO(ROOM_HT_COREVIEW, 6930, 3110, DIR_DOWN);
+ return 1;
+ }
+ NARRATE("* (The elevator up.)\n* (Get in?)@Yes@No");
+ if (CHOICE() == 1) return 1;
+ set_flag(F_HT_FLOOR2, 1);
+ FADE_TO(ROOM_HT_L3, 3770, 842, DIR_DOWN);
+ EV_END;
+}
+
+int ht2_event(Ev *c) {
+ switch (c->id) {
+ case EV_HOTDOG: return ev_hotdog(c);
+ case EV_HEATS: return ev_heats(c);
+ case EV_APRON: return ev_apron(c);
+ case EV_BELT_SWITCH: return ev_belt_switch(c);
+ case EV_VENTPUZ: return ev_ventpuz(c);
+ case EV_HT2_CALL: return ev_call(c);
+ case EV_GUARDS: return ev_guards(c);
+ case EV_NEWS: return ev_news(c);
+ case EV_BOMB: return ev_bomb(c);
+ case EV_HT2_ELEVATOR: return ev_elevator(c);
+ }
+ return -1;
+}
diff --git a/games/undertale/src/ht3.c b/games/undertale/src/ht3.c
new file mode 100644
index 00000000..1a5fcfe0
--- /dev/null
+++ b/games/undertale/src/ht3.c
@@ -0,0 +1,433 @@
+/* Hotland, its third floor: out of the elevator, the spiders' bake sale; the vent islands, the shooting puzzles north
+ * (the Diamond Boys) and south (over the belts, past the drones); the spider entrance and Muffet's lair, its webs;
+ * Mettaton's musical, the trapdoor down to the tile puzzle and his fight; the crossroads, where the stairs north lead
+ * to the MTT Resort (resort.c). (As the Undertale Wiki describes them; what they say is ours, in short.) */
+#include
+#include
+#include "battle.h"
+#include "event.h"
+#include "events.h"
+#include "game.h"
+#include "gfx.h"
+#include "hotland.h"
+#include "items.h"
+#include "menu.h"
+#include "monsters.h"
+#include "res.h"
+#include "save.h"
+#include "shoot.h"
+#include "snowdin.h"
+#include "world.h"
+
+static void gone(Obj *o) { o->kind = OBJ_NONE; }
+static Obj *thing(int x, int y, int w, int h, int event, int name, int arg) {
+ Obj *o = obj_trigger(x, y, w, h, event, name);
+ if (o) o->flags = OF_TALK | OF_SOLID, o->arg = (int16_t)arg;
+ return o;
+}
+static Obj *npc(int name, uint16_t spr, int x, int y) {
+ const uint16_t s[4] = {spr, spr, spr, spr};
+ Obj *o = obj_npc(name, s, x, y, DIR_DOWN, 0);
+ if (o) o->flags &= (uint16_t)~OF_SOLID;
+ return o;
+}
+static Obj *obj_mtt(int x, int y) {
+ Obj *o = npc(ACTOR_METTATON, SPR_MTT_OW, x, y);
+ if (o) o->flags |= OF_ANIM, o->anim_speed = 5;
+ return o;
+}
+
+/* ---------------------------------------------------------------- Muffet's lair: its webs hold Frisk back */
+typedef struct {
+ int16_t x, y, w, h;
+ uint8_t speed;
+} Web;
+static const Web webs[] = {
+ {5800, 466, 60, 32, 2}, {5880, 466, 60, 32, 2}, {5975, 466, 60, 32, 2}, /* the small ones */
+ {6083, 454, 400, 46, 1}, /* the big one, Muffet's */
+ {6540, 466, 60, 32, 2},
+};
+static bool muffet_holds;
+int ht3_speed(int x, int y) {
+ if (g_room != ROOM_HT_LAIR) return 3;
+ if (muffet_holds) return 0;
+ for (unsigned i = 0; i < sizeof webs / sizeof webs[0]; i++)
+ if (x >= webs[i].x && x < webs[i].x + webs[i].w && y >= webs[i].y && y < webs[i].y + webs[i].h) return webs[i].speed;
+ return 3;
+}
+
+/* ---------------------------------------------------------------- the tile puzzle (under the stage) */
+/* the map's tiles: R red (a wall: the room's collision leaves it out), Y yellow (a shock: back to the last tile
+ * stepped on), O orange (Frisk smells of oranges), U purple (soap: lemons, and a slide on to the next tile), B blue
+ * (water: the piranhas bite if Frisk smells of oranges; next to yellow, it shocks), P pink and G green (nothing) */
+#define TX 6640
+#define TY 620
+#define TW 39
+#define TH 4
+static const char tiles[TH][TW + 1] = {
+ "BGYPGUPYGRYPRGPOYRRORYPYYGGBYBOGGGRGGGR",
+ "GOUBORRBYRRBBBRRRPPRBPGRUOBOUUPBRGRGRGR",
+ "PRPPBBRORPBURPYGUOUYUURRPOBUUBYPRGRGRGR",
+ "GYBYRPGOUUOPROPORBBPOBGOPBUOYBPPGGGGRGG",
+};
+#define TILES_TIME (30 * 30)
+enum { TILES_OFF, TILES_ON, TILES_DONE };
+static int tiles_state, tiles_t, tiles_why, flavor, last_r = -99, last_c = -99, safe_x, safe_y, tile_act, flames_in;
+static char tile_at(int r, int c) { return r < 0 || c < 0 || r >= TH || c >= TW ? 0 : tiles[r][c]; }
+static bool tile_bad(int r, int c) {
+ char t = tile_at(r, c);
+ if (t == 'Y') return true;
+ if (t != 'B') return false;
+ if (flavor == 'O') return true;
+ return tile_at(r - 1, c) == 'Y' || tile_at(r + 1, c) == 'Y' || tile_at(r, c - 1) == 'Y' || tile_at(r, c + 1) == 'Y';
+}
+static void tiles_watch(void) {
+ if (tiles_state != TILES_ON) return;
+ if (flames_in) {
+ /* out of time: the flames close in, then Alphys */
+ if (++flames_in > 40) tiles_why = 2, event_start(EV_TILES3, NULL);
+ return;
+ }
+ if (--tiles_t <= 0) {
+ flames_in = 1;
+ return;
+ }
+ int fx = pl.x + 10, fy = pl.y + 24;
+ if (fx >= TX + TW * 20) {
+ tiles_why = 1, event_start(EV_TILES3, NULL);
+ return;
+ }
+ int c = fx >= TX ? (fx - TX) / 20 : -1, r = fy >= TY ? (fy - TY) / 20 : -1;
+ if (c < 0 || r < 0 || r >= TH) {
+ last_r = last_c = -99, safe_x = pl.x, safe_y = pl.y;
+ return;
+ }
+ if (r != last_r || c != last_c) {
+ last_r = r, last_c = c;
+ if (tile_bad(r, c)) {
+ tile_act = 1, event_start(EV_TILE, NULL);
+ return;
+ }
+ if (tile_at(r, c) == 'O') flavor = 'O';
+ if (tile_at(r, c) == 'U') {
+ flavor = 'L', tile_act = 2, event_start(EV_TILE, NULL);
+ return;
+ }
+ }
+ if (tile_at(r, c) != 'U') safe_x = pl.x, safe_y = pl.y;
+}
+
+/* ---------------------------------------------------------------- drawn, and watched */
+static void draw_flame_wall(int x) {
+ for (int k = 0; k < 6; k++)
+ gfx_sprite_color(SPR_FIREBULLET, (int)(g_frame / 4 + (uint32_t)k) % 2, x - cam_x, TY - 10 + k * 18 - cam_y, 0,
+ (g_frame / 3 + (uint32_t)k) % 2 ? rgb565(0xFF8000) : rgb565(0xFFC000), 255);
+}
+void ht3_draw(void) {
+ if (g_room != ROOM_HT_TILES || tiles_state != TILES_ON) return;
+ int in = flames_in * 6;
+ draw_flame_wall(TX - 14 + in);
+ draw_flame_wall(TX + TW * 20 + 170 - in);
+}
+void ht3_draw_top(void) {
+ if (g_room != ROOM_HT_TILES || tiles_state != TILES_ON) return;
+ char s[16];
+ int secs = (tiles_t + 29) / 30;
+ snprintf(s, sizeof s, "0:%02d", secs);
+ gfx_rect(4, 4, 52, 18, C_BLACK, 200);
+ gfx_text(FONT_MAIN, 10, 6, s, secs <= 10 ? C_RED : C_WHITE, 255);
+}
+void ht3_update(void) {
+ if (g_room == ROOM_HT_TILES) tiles_watch();
+}
+
+/* ---------------------------------------------------------------- the rooms */
+static void diamond_boys(void) {
+ Obj *a = npc(ACTOR_ITEM, SPR_DIAMOND1, 4750, 452);
+ if (a) a->flags |= OF_SOLID;
+ Obj *b = npc(ACTOR_ITEM, SPR_DIAMOND2, 4905, 454);
+ if (b) b->flags |= OF_SOLID;
+ obj_sign(4736, 424, 30, 32, "* Did you see Mettaton's last\n show? He was AMAZING!|* When he strikes a pose, the\n ratings go through the roof!");
+ obj_sign(4890, 420, 30, 38, "* My mom says never touch the\n switch on his back.|* I wonder what it does...");
+}
+
+void ht3_setup(int room, int from) {
+ (void)from;
+ if (room < ROOM_HT_L3 || room > ROOM_HT_CROSS3) return;
+ if (room != ROOM_HT_TILES && tiles_state == TILES_ON) tiles_state = TILES_OFF; /* (a SAVE loaded mid-puzzle) */
+ switch (room) {
+ case ROOM_HT_L3:
+ thing(3740, 800, 80, 56, EV_HT3_ELEVATOR, ACTOR_DOOR, 1);
+ thing(4128, 846, 74, 36, EV_BAKESALE3, ACTOR_ITEM, 0);
+ obj_sign(4276, 892, 44, 40, "* I didn't even want a donut...|* The spiders scared me into\n buying one.\n* There goes all my money.");
+ break;
+ case ROOM_HT_HUB3:
+ if (!flag(F_HT_CALL_HUB3)) {
+ Obj *t = obj_trigger(4566, 856, 10, 70, EV_HT3_CALL, ACTOR_TRIGGER);
+ if (t) t->arg = 0;
+ }
+ if (!(flag(F_HT3_PUZ_N) && flag(F_HT3_PUZ_S))) {
+ Obj *d = obj_trigger(5200, 700, 60, 12, EV_NONE, ACTOR_DOOR);
+ if (d) d->flags = OF_SOLID;
+ obj_sign(5200, 712, 60, 16, "* (The door is locked.)\n* (Both puzzles have to be solved.)");
+ }
+ break;
+ case ROOM_HT_NORTH3:
+ thing(4812, 154, 36, 30, EV_SHOOT3, ACTOR_ITEM, SHOOT_NORTH);
+ if (flag(F_HT3_PUZ_N)) diamond_boys();
+ break;
+ case ROOM_HT_BELTS3:
+ obj_sign(5118, 1286, 30, 34, "* (A cactus, all on its own.)\n* (You stop to look at it.)");
+ break;
+ case ROOM_HT_SOUTH3:
+ thing(5524, 1144, 36, 30, EV_SHOOT3, ACTOR_ITEM, SHOOT_SOUTH);
+ break;
+ case ROOM_HT_SPIDERENT:
+ obj_save(5300, 420, "* (The smell of cobwebs fills you with determination.)");
+ break;
+ case ROOM_HT_LAIR:
+ if (!flag(F_HT_MUFFET)) obj_trigger(6240, 455, 8, 50, EV_MUFFET, ACTOR_TRIGGER);
+ break;
+ case ROOM_HT_STAGE:
+ if (!flag(F_HT_MUSICAL)) obj_trigger(6760, 440, 8, 66, EV_MUSICAL, ACTOR_TRIGGER);
+ break;
+ case ROOM_HT_TILES:
+ if (!flag(F_HT_TILES3) && tiles_state != TILES_ON) obj_mtt(6430, 626);
+ break;
+ case ROOM_HT_CROSS3:
+ thing(7444, 360, 70, 58, EV_NICECREAM3, ACTOR_ITEM, 0);
+ if (flag(F_HT_GUARDS) == 1) {
+ static const uint16_t s1[4] = {SPR_RG01_OW, SPR_RG01_OW, SPR_RG01_OW, SPR_RG01_OW};
+ static const uint16_t s2[4] = {SPR_RG02_OW, SPR_RG02_OW, SPR_RG02_OW, SPR_RG02_OW};
+ obj_npc(ACTOR_RG01, s1, 7600, 412, DIR_DOWN, 0);
+ obj_npc(ACTOR_RG02, s2, 7642, 412, DIR_DOWN, 0);
+ obj_sign(7580, 374, 84, 40, "* Dude, this ice cream is, like,\n the best.|* ... ...yeah.");
+ }
+ thing(8054, 430, 24, 30, EV_HEATS3, ACTOR_ITEM, 0);
+ thing(7934, 340, 80, 80, EV_HT3_ELEVATOR, ACTOR_DOOR, 0);
+ break;
+ }
+}
+
+/* ---------------------------------------------------------------- the events */
+static int ev_bakesale(Ev *c) {
+ static const char *const opts[] = {"Spider Donut - 9999G", "Spider Cider - 9999G"};
+ EV_BEGIN;
+ NARRATE("* (A spider bake sale.)\n* (All proceeds go to real spiders.)");
+ CHOOSE(opts, 2);
+ if (CHOSEN() < 0) return 1;
+ if (sv.gold < 9999) {
+ NARRATE("* (You don't have enough gold.)");
+ return 1;
+ }
+ if (!item_add(CHOSEN() ? ITEM_SPIDER_CIDER : ITEM_SPIDER_DONUT)) {
+ NARRATE("* (You're carrying too much.)");
+ return 1;
+ }
+ sv.gold = (uint16_t)(sv.gold - 9999);
+ NARRATE("* (The spiders take your money.)");
+ EV_END;
+}
+
+/* Alphys's calls: 0 by the vents; after the south puzzle, the anime (in ev_shoot) */
+static int ev_call(Ev *c) {
+ EV_BEGIN;
+ if (c->self) gone(c->self);
+ set_flag(F_HT_CALL_HUB3, 1);
+ SAY(FACE_ALPHYS, "* Ring...|* Hi! It's Alphys.|* The door on the right opens\n once both puzzles are done.|* One's up north, one's down\n south. Good luck!");
+ EV_END;
+}
+
+static int ev_shoot(Ev *c) {
+ EV_BEGIN;
+ c->v[0] = c->self ? c->self->arg : SHOOT_NORTH;
+ c->v[1] = c->v[0] == SHOOT_NORTH ? F_HT3_PUZ_N : F_HT3_PUZ_S;
+ if (flag(c->v[1])) {
+ NARRATE("* (This puzzle is solved.)");
+ return 1;
+ }
+ shoot_open(c->v[0]);
+ WAIT_UNTIL(g_mode == MODE_WORLD);
+ if (!shoot_result()) return 1;
+ set_flag(c->v[1], 1);
+ if (flag(F_HT3_PUZ_N) && flag(F_HT3_PUZ_S)) {
+ NARRATE("* (Somewhere, a door unlocks.)");
+ } else {
+ NARRATE("* (One down.)");
+ }
+ if (c->v[0] == SHOOT_NORTH) {
+ diamond_boys();
+ return 1;
+ }
+ /* the south one: Alphys calls, about anime */
+ SAY(FACE_ALPHYS, "* Ring...|* H-hey! Um... when all this\n is over...|* want to watch anime with me?@Yes@No");
+ if (CHOICE() == 0) {
+ set_flag(F_HT_ANIME, 1);
+ SAY(FACE_ALPHYS, "* R-really?! Okay!|* There's this show about a\n magical girl, and she...");
+ NARRATE("* (Alphys talks about it for a\n long, long time.)");
+ SAY(FACE_ALPHYS, "* ...oh! Sorry! Bye!");
+ } else {
+ set_flag(F_HT_ANIME, 2);
+ SAY(FACE_ALPHYS, "* Oh. Okay. That's... fine.\n* Bye!");
+ }
+ EV_END;
+}
+
+static int ev_muffet(Ev *c) {
+ static const uint16_t s[4] = {SPR_MUFFET_OW, SPR_MUFFET_OW, SPR_MUFFET_OW, SPR_MUFFET_OW};
+ Obj *m = obj_get(ACTOR_MUFFET);
+ EV_BEGIN;
+ if (c->self) gone(c->self);
+ /* the web holds Frisk fast, and down comes Muffet */
+ muffet_holds = true;
+ NARRATE("* (The web holds you fast.)\n* (You can't move!)");
+ m = obj_npc(ACTOR_MUFFET, s, pl.x + 60, pl.y - 30, DIR_LEFT, 0);
+ if (m) m->flags &= (uint16_t)~OF_SOLID;
+ m = obj_get(ACTOR_MUFFET);
+ WALK(m, pl.x + 60, pl.y + 29, 2);
+ NARRATE("* Ahuhuhu~|* Did you think you could just\n walk through my lair, deary?|* Someone pays very well for\n human SOULs, you know~|* And you didn't buy a single\n donut. How stingy~");
+ BATTLE(FORM_MUFFET);
+ muffet_holds = false;
+ if (bt.mon[0].killed) {
+ set_flag(F_HT_MUFFET, 2);
+ gone(obj_get(ACTOR_MUFFET));
+ return 1;
+ }
+ set_flag(F_HT_MUFFET, 1);
+ NARRATE("* Ahuhu... no hard feelings,\n deary~|* Do come back to our bake sale!");
+ m = obj_get(ACTOR_MUFFET);
+ WALK(m, m->x + 200, m->y, 3);
+ gone(obj_get(ACTOR_MUFFET));
+ EV_END;
+}
+
+static int ev_musical(Ev *c) {
+ Obj *m = obj_get(ACTOR_METTATON);
+ EV_BEGIN;
+ if (c->self) gone(c->self);
+ NARRATE("* (The lights go down.)\n* (A spotlight finds you.)");
+ /* (the view: up to his balcony, and back) */
+ WAIT_UNTIL(cam_pan(7000, 214, 6));
+ text_set_top(0); /* (the box below, out of his way) */
+ NARRATE("* OHHH! WHO IS THIS, BENEATH MY\n BALCONY?|* A HUMAN, COME TO SAVE ME FROM\n MY TOWER?|* ALAS! A ROBOT AND A HUMAN...\n* IT COULD NEVER BE!|* SO, MY LOVE... YOU'LL JUST HAVE\n TO DROP IN!");
+ text_set_top(-1);
+ WAIT_UNTIL(cam_pan(pl.x - 150, pl.y - 105, 6));
+ cam_free();
+ g_shake = 6;
+ NARRATE("* (The floor opens up under you!)");
+ set_flag(F_HT_MUSICAL, 1);
+ FADE_TO(ROOM_HT_TILES, 6420, 680, DIR_RIGHT);
+ /* below: Mettaton, the tiles, thirty seconds */
+ m = obj_get(ACTOR_METTATON);
+ NARRATE("* OH, HELLO AGAIN, DARLING!|* REMEMBER THIS PUZZLE? FROM\n WAY BACK IN SNOWDIN?|* THEN YOU KNOW THE RULES.\n* THIRTY SECONDS! GO!");
+ tiles_state = TILES_ON, tiles_t = TILES_TIME, flavor = 0, flames_in = 0, last_r = last_c = -99;
+ safe_x = pl.x, safe_y = pl.y;
+ (void)m;
+ EV_END;
+}
+
+static const int8_t ddx[4] = {0, 0, -1, 1}, ddy[4] = {1, -1, 0, 0};
+static int ev_tile(Ev *c) {
+ EV_BEGIN;
+ c->v[0] = tile_act;
+ if (c->v[0] == 1) {
+ /* a shock (or the piranhas): back to the last tile stepped on */
+ g_shake = 4;
+ PL_WALK(safe_x, safe_y, 4);
+ last_r = last_c = -99;
+ return 1;
+ }
+ /* the soap: on to the next tile that way, unless a wall's there */
+ c->v[1] = last_r + ddy[pl.dir], c->v[2] = last_c + ddx[pl.dir];
+ c->v[3] = TX + c->v[2] * 20, c->v[4] = TY + c->v[1] * 20 - 14;
+ if (!world_walkable(c->v[3] + 3, c->v[4] + 20, 14, 9, -1)) return 1;
+ PL_WALK(c->v[3], c->v[4], 4);
+ EV_END;
+}
+
+static int ev_tiles_end(Ev *c) {
+ Obj *m = obj_get(ACTOR_METTATON);
+ EV_BEGIN;
+ c->v[0] = tiles_why;
+ tiles_state = TILES_DONE;
+ if (c->v[0] == 1) {
+ NARRATE("* OH! YOU MADE IT ACROSS.\n* HOW... ANTICLIMACTIC.|* NO MATTER! I'LL BE THE MONSTER\n YOU MEET AT THE END!");
+ } else {
+ NARRATE("* (The flames close in...!)");
+ SAY(FACE_ALPHYS, "* Ring...|* H-hang on! Turning off the\n firewall!");
+ NARRATE("* (The flames die down.)");
+ NARRATE("* TSK! FINE. THEN I'LL BE THE\n MONSTER YOU MEET!");
+ }
+ m = obj_get(ACTOR_METTATON);
+ if (m->kind == OBJ_NONE) obj_mtt(pl.x + 60, pl.y + 29);
+ else WALK(m, pl.x + 60, pl.y + 29, 6);
+ BATTLE(FORM_MTT_TILES);
+ set_flag(F_HT_TILES3, c->v[0]);
+ NARRATE("* OHHH... I'M ALL OUT OF POWER.|* UNTIL NEXT TIME, DARLING!\n* STAY TUNED!");
+ m = obj_get(ACTOR_METTATON);
+ WALK(m, m->x, m->y - 220, 6);
+ gone(obj_get(ACTOR_METTATON));
+ EV_END;
+}
+
+static int ev_heats(Ev *c) {
+ EV_BEGIN;
+ if (flag(F_HT_HEATS3)) {
+ NARRATE("* I'LL GET YOU NEXT TIME!!!");
+ return 1;
+ }
+ NARRATE("* HEY!!! YOU!!!|* DO YOU REMEMBER MY NAME???@Yes@No");
+ set_flag(F_HT_HEATS3, 1);
+ NARRATE("* WHAT?!|* HOW COULD I LOSE SO EASILY?!\n* GRRR...!!!");
+ EV_END;
+}
+
+static int ev_nicecream(Ev *c) {
+ EV_BEGIN;
+ if (flag(F_HT_GUARDS) == 1) {
+ NARRATE("* Sorry, I'm all sold out!|* Those two guards bought every\n last one!");
+ return 1;
+ }
+ NARRATE("* Nice Cream! Just 12G!\n* Want one?@Yes@No");
+ if (CHOICE() == 1) return 1;
+ if (sv.gold < 12) {
+ NARRATE("* Oh... you're short on gold.");
+ return 1;
+ }
+ if (!item_add(ITEM_NICE_CREAM)) {
+ NARRATE("* Your hands are full!");
+ return 1;
+ }
+ sv.gold = (uint16_t)(sv.gold - 12);
+ NARRATE("* Here you go! Have a super day!");
+ EV_END;
+}
+
+static int ev_elevator(Ev *c) {
+ EV_BEGIN;
+ c->v[0] = c->self ? c->self->arg : 0;
+ NARRATE("* (The elevator down.)\n* (Get in?)@Yes@No");
+ if (CHOICE() == 1) return 1;
+ if (c->v[0]) {
+ FADE_TO(ROOM_HT_COREVIEW2, 3770, 1348, DIR_DOWN);
+ } else {
+ FADE_TO(ROOM_HT_HOTDOG, 6966, 2380, DIR_DOWN);
+ }
+ EV_END;
+}
+
+int ht3_event(Ev *c) {
+ switch (c->id) {
+ case EV_BAKESALE3: return ev_bakesale(c);
+ case EV_SHOOT3: return ev_shoot(c);
+ case EV_HT3_CALL: return ev_call(c);
+ case EV_MUFFET: return ev_muffet(c);
+ case EV_MUSICAL: return ev_musical(c);
+ case EV_TILE: return ev_tile(c);
+ case EV_TILES3: return ev_tiles_end(c);
+ case EV_HEATS3: return ev_heats(c);
+ case EV_NICECREAM3: return ev_nicecream(c);
+ case EV_HT3_ELEVATOR: return ev_elevator(c);
+ }
+ return -1;
+}
diff --git a/games/undertale/src/htmon.c b/games/undertale/src/htmon.c
new file mode 100644
index 00000000..556c97f1
--- /dev/null
+++ b/games/undertale/src/htmon.c
@@ -0,0 +1,716 @@
+/* Hotland's monsters: stats and ACTs (from the Undertale Wiki's records of the game), their formations, where they
+ * come out and their attacks. Their words are short ones of our own. */
+#include
+#include "battle.h"
+#include "game.h"
+#include "items.h"
+#include "gfx.h"
+#include "monsters.h"
+#include "res.h"
+#include "world.h"
+
+/* ---------------------------------------------------------------- Vulkin: it means well (its lava doesn't heal) */
+static void act_vulkin(Battle *b, Monster *m, int act, char *t, int cap) {
+ (void)b;
+ if (act == 0) { /* Criticize */
+ snprintf(t, (size_t)cap, "* You tell Vulkin its lava isn't helping.\n* Vulkin looks crushed. It stops attacking.");
+ m->state = 2;
+ } else if (act == 1) { /* Encourage */
+ snprintf(t, (size_t)cap, "* You tell Vulkin it's doing great.\n* Vulkin glows with pride... and heat.");
+ m->at = (int16_t)(m->at + 2), m->spare = true;
+ if (m->state != 2) m->state = 1;
+ } else { /* Hug */
+ snprintf(t, (size_t)cap, "* You hug Vulkin.\n* It's very, very warm.\n* Your DEFENSE dropped.");
+ m->spare = true;
+ if (m->state != 2) m->state = 1;
+ b->df_loss += 5;
+ }
+}
+static const char *talk_vulkin(Battle *b, Monster *m) {
+ (void)b;
+ if (m->state == 2) return "(sniff)";
+ if (m->state == 1) return "Feeling\nbetter?";
+ static const char *const lines[] = {"I'll heal\nyou!", "Lava\nhelps!", "Warm\nhugs!", "Here, take\nsome lava!"};
+ return lines[rnd_n(4)];
+}
+static int attack_vulkin(Battle *b, Monster *m) {
+ (void)b;
+ if (m->state == 2) return ATK_NONE;
+ return rnd_n(2) ? ATK_VULKIN_THUNDER : ATK_VULKIN_FIRE;
+}
+
+/* ---------------------------------------------------------------- Tsunderplane: come closer, through its green */
+static void act_tsunder(Battle *b, Monster *m, int act, char *t, int cap) {
+ (void)b;
+ if (act == 0) { /* Flirt */
+ snprintf(t, (size_t)cap, "* You flirt with Tsunderplane.\n* It turns away, blushing.");
+ } else { /* Approach */
+ snprintf(t, (size_t)cap, "* You move a little closer.\n* Tsunderplane pretends not to notice.");
+ m->state = 1;
+ }
+}
+static const char *talk_tsunder(Battle *b, Monster *m) {
+ (void)b;
+ if (m->spare) return "I-it's not\nlike I like\nyou!!";
+ if (m->state) return "Don't get\nthe wrong\nidea!";
+ static const char *const lines[] = {"Hmph!", "Baka!", "Wh-what are\nyou looking\nat?", "As if!"};
+ return lines[rnd_n(4)];
+}
+static const char *flavor_tsunder(Battle *b, Monster *m) {
+ /* its green zones touched enough (four times, three with Vulkin by it): it can be spared */
+ int need = 4;
+ for (int i = 0; i < b->nmon; i++)
+ if (b->mon[i].def == mon_def(MON_VULKIN) && !b->mon[i].gone) need = 3;
+ if (m->state && b->greens >= need) m->spare = true;
+ if (m->spare) return "* Tsunderplane is pretending it doesn't want to be spared.";
+ return NULL;
+}
+static int attack_tsunder(Battle *b, Monster *m) {
+ (void)b, (void)m;
+ return rnd_n(2) ? ATK_TSUNDER_BOMBS : ATK_TSUNDER_DIVE;
+}
+
+/* ---------------------------------------------------------------- Pyrope: turn up the heat */
+static void act_pyrope(Battle *b, Monster *m, int act, char *t, int cap) {
+ (void)b;
+ if (act == 0) { /* Heat Up */
+ m->state++;
+ if (m->state >= 2) snprintf(t, (size_t)cap, "* You rub your hands together.\n* The room is sweltering!"), m->spare = true;
+ else snprintf(t, (size_t)cap, "* You rub your hands together.\n* It's getting warmer...");
+ } else if (act == 1) { /* Cool Down */
+ snprintf(t, (size_t)cap, "* You blow on Pyrope.\n* Its flame shrinks. Its ATTACK dropped!");
+ m->at = (int16_t)max_i(1, m->at - 2);
+ } else { /* Invite */
+ snprintf(t, (size_t)cap, "* You invite Pyrope to hang out.\n* It just wants to be warmer.");
+ }
+}
+static const char *talk_pyrope(Battle *b, Monster *m) {
+ (void)b;
+ if (m->spare) return "Toasty!";
+ static const char *const lines[] = {"Brr...", "Hotter!", "I'm all\nfired up!", "Got a\nlight?"};
+ return lines[rnd_n(4)];
+}
+static int attack_pyrope(Battle *b, Monster *m) { (void)b, (void)m; return rnd_n(2) ? ATK_PYROPE_BOMBS : ATK_PYROPE_ROPE; }
+
+/* ---------------------------------------------------------------- the Royal Guards: 01 and 02, always a team.
+ * Clean 02's armor, and its green stars caught five times, 02 can't stand the heat and takes the armor off; then a
+ * whisper to 01 to say what he feels, and the two can be spared */
+enum { RG_CLEANED, RG_ARMOR_OFF, RG_TOLD }; /* (b->state[]) */
+static Monster *rg_other(Battle *b, Monster *m) {
+ for (int i = 0; i < b->nmon; i++)
+ if (&b->mon[i] != m) return &b->mon[i];
+ return m;
+}
+static bool rg_alone(Battle *b, Monster *m) { return rg_other(b, m)->killed; }
+static void act_rg(Battle *b, Monster *m, int act, char *t, int cap) {
+ bool one = m->def == mon_def(MON_RG01);
+ Monster *o = rg_other(b, m);
+ if (act == 0) { /* Clean Armor */
+ if (one) snprintf(t, (size_t)cap, "* You try to polish 01's armor.\n* Your hands slide right off.");
+ else if (b->state[RG_ARMOR_OFF]) snprintf(t, (size_t)cap, "* You drum on 02's bare chest.\n* 01 can't look.");
+ else {
+ snprintf(t, (size_t)cap, "* You scrub 02's armor.\n* The grime starts to come off.\n* 01 looks bothered.");
+ b->state[RG_CLEANED] = 1;
+ }
+ } else { /* Whisper */
+ if (!one) snprintf(t, (size_t)cap, "* You whisper a secret to 02.\n* 02 won't tell.");
+ else if (!b->state[RG_ARMOR_OFF] || o->gone)
+ snprintf(t, (size_t)cap, "* You tell 01 to be honest about his feelings.\n* 01 doesn't get it.");
+ else {
+ snprintf(t, (size_t)cap, "* You tell 01 to be honest about his feelings.");
+ if (!b->state[RG_TOLD]) b->state[RG_TOLD] = 1;
+ m->spare = o->spare = true;
+ }
+ }
+}
+static const char *talk_rg(Battle *b, Monster *m) {
+ bool one = m->def == mon_def(MON_RG01);
+ if (rg_alone(b, m)) return one ? "02...!\nThis one's\nfor you!" : "01...!\n...!!!";
+ if (b->state[RG_TOLD] == 1) {
+ if (one) return "02... I...\nI like,\nLIKE you,\nbro.";
+ b->state[RG_TOLD] = 2;
+ return "... want ice\ncream... after\nthis?";
+ }
+ if (b->state[RG_TOLD]) return one ? "Ice cream?\nSure, dude!" : "... heh.";
+ if (b->state[RG_ARMOR_OFF]) return one ? "D-dude..." : "... much\nbetter.";
+ if (b->state[RG_CLEANED]) return one ? "Hey! Hands\noff the\narmor!" : "... getting\n... hot.";
+ static const char *const l01[] = {"Like, hand\nover the\nSOUL, bro.", "Like, you're\ntoast and\nstuff.", "Team\nattack!"};
+ static const char *const l02[] = {"... ... hm.", "... ... sigh.", "... team\nattack."};
+ return one ? l01[rnd_n(3)] : l02[rnd_n(3)];
+}
+static const char *flavor_rg(Battle *b, Monster *m) {
+ (void)m;
+ if (b->state[RG_CLEANED] && !b->state[RG_ARMOR_OFF] && b->greens >= 5) {
+ b->state[RG_ARMOR_OFF] = 1;
+ return "* 02 can't take the heat, and pulls the armor off!\n* 01 starts to sweat.";
+ }
+ if (b->state[RG_TOLD]) return "* 01 and 02 can't stop smiling at each other.";
+ if (b->state[RG_ARMOR_OFF]) return "* 01 is shaking. 02 looks a lot cooler.";
+ return NULL;
+}
+static int attack_rg(Battle *b, Monster *m) { (void)b, (void)m; return ATK_RG_TEAM; }
+
+/* ---------------------------------------------------------------- Mettaton's bombs: defuse each in the DEFUSE ZONE.
+ * Each moves its own way (the wiki's account of them), the zone in the middle; Defuse (or FIGHT) only works while
+ * the bomb is in it */
+int g_bomb_kind;
+#define ZONE_X0 128
+#define ZONE_X1 192
+#define ZONE_Y0 34
+#define ZONE_Y1 104
+static int bomb_tri(int t, int period, int lo, int hi) { /* a triangle wave, lo to hi and back */
+ int p = t % period, half = period / 2;
+ return lo + (hi - lo) * (p < half ? p : period - p) / half;
+}
+/* where the bomb's middle is, t frames into the battle */
+static void bomb_pos(int t, int *x, int *y) {
+ switch (g_bomb_kind) {
+ case BOMB_DOG: /* side to side, low, in waves */
+ *x = 160 + 110 * gfx_sin(t / 2 % 256) / 1024, *y = 92 + 6 * gfx_sin(t * 3 % 256) / 1024;
+ break;
+ case BOMB_WATER: /* fast, to the right, round again from the left */
+ *x = 20 + t * 3 % 280, *y = 70;
+ break;
+ case BOMB_SCRIPT: /* across and back, doubling back a little before each turn */
+ *x = 160 + (95 * gfx_sin(t * 256 / 300 % 256) + 22 * gfx_sin(t * 768 / 300 % 256)) / 1024, *y = 66;
+ break;
+ case BOMB_BALL: { /* bouncing from side to side */
+ int b = gfx_sin(t * 256 / 50 % 256);
+ *x = bomb_tri(t, 240, 50, 270), *y = 96 - 56 * (b < 0 ? -b : b) / 1024;
+ break;
+ }
+ case BOMB_PRESENT: /* up through the zone, round again from below */
+ *x = 160, *y = 150 - t * 3 / 2 % 170;
+ break;
+ default: { /* the game: side to side, keeping to the edges (across the middle in a rush) */
+ int v = 240 * gfx_sin(t * 256 / 400 % 256) / 1024;
+ *x = 160 + clamp_i(v, -115, 115), *y = 70;
+ break;
+ }
+ }
+}
+static bool bomb_in_zone(Battle *b) {
+ int x, y;
+ bomb_pos((uint16_t)b->state[0], &x, &y);
+ return x >= ZONE_X0 && x <= ZONE_X1 && y >= ZONE_Y0 && y <= ZONE_Y1;
+}
+static void bomb_defused(Monster *m) { m->gone = true, m->fade = 1, m->dmg_t = 0; }
+static void act_bomb(Battle *b, Monster *m, int act, char *t, int cap) {
+ (void)act;
+ if (bomb_in_zone(b)) {
+ bomb_defused(m);
+ snprintf(t, (size_t)cap, g_bomb_kind == BOMB_DOG ? "* Dog defused!" : "* Bomb defused!");
+ } else snprintf(t, (size_t)cap, "* Missed! Try while it's in\n the DEFUSE ZONE.");
+}
+static bool hit_bomb(Battle *b, Monster *m, int dmg) {
+ (void)dmg;
+ if (bomb_in_zone(b)) bomb_defused(m);
+ else m->dmg_shown = 0; /* (MISS) */
+ return true;
+}
+static const char *talk_bomb(Battle *b, Monster *m) {
+ (void)b, (void)m;
+ if (g_bomb_kind == BOMB_DOG) return "Zzz...";
+ if (g_bomb_kind == BOMB_WATER) return "Swish.";
+ return rnd_n(2) ? "Tick." : "Tock.";
+}
+static const char *flavor_bomb(Battle *b, Monster *m) {
+ (void)b, (void)m;
+ return g_bomb_kind == BOMB_DOG ? "* The dog is still active." : "* The bomb is still ticking.";
+}
+static int attack_bomb(Battle *b, Monster *m) { (void)b, (void)m; return -1; }
+static void draw_bomb(Battle *b, Monster *m, int x0, int y0) {
+ (void)y0;
+ if (!m->gone) b->state[0]++;
+ /* the zone */
+ uint16_t g = rgb565(0x00C000);
+ gfx_frame_rect(ZONE_X0 - 14, ZONE_Y0 - 14, ZONE_X1 - ZONE_X0 + 28, ZONE_Y1 - ZONE_Y0 + 28, 1, g);
+ gfx_text(FONT_SMALL, 160 - gfx_text_width(FONT_SMALL, "DEFUSE ZONE", -1) / 2, ZONE_Y0 - 11, "DEFUSE ZONE", g, 255);
+ int x, y;
+ bomb_pos((uint16_t)b->state[0], &x, &y);
+ x += x0 - m->x; /* (its shake, hit) */
+ uint16_t s = g_bomb_kind == BOMB_DOG ? SPR_DOGBOMB_B : g_bomb_kind == BOMB_WATER ? SPR_WATER_B : SPR_BOMB_B;
+ int half = g_bomb_kind == BOMB_DOG ? 6 : g_bomb_kind == BOMB_WATER ? 11 : 23;
+ uint8_t alpha = m->fade ? (uint8_t)max_i(0, 128 - m->fade * 4) : 255;
+ gfx_sprite(s, (int)(g_frame / 8), x, y + half, 0, alpha);
+}
+/* ---------------------------------------------------------------- Muffet: purple strings, spiders, pastries, her
+ * pet. She lets Frisk go once her spiders send word (at turn 17), or at once if Frisk eats one of their treats.
+ * Paying her makes her hits weaker (a hit less each time, and the price doubles); struggling three times halves it */
+enum { MF_PRICE, MF_STRUGGLE, MF_TREAT }; /* (b->state[]) */
+static void act_muffet(Battle *b, Monster *m, int act, char *t, int cap) {
+ if (!b->state[MF_PRICE]) b->state[MF_PRICE] = 100;
+ if (act == 0) { /* Pay */
+ if (sv.gold >= b->state[MF_PRICE]) {
+ sv.gold = (uint16_t)(sv.gold - b->state[MF_PRICE]);
+ snprintf(t, (size_t)cap, "* You pay Muffet %dG.\n* Her attacks soften a little.", b->state[MF_PRICE]);
+ m->at = (int16_t)max_i(1, m->at - 1);
+ b->state[MF_PRICE] = (int16_t)min_i(b->state[MF_PRICE] * 2, 9000);
+ } else {
+ snprintf(t, (size_t)cap, "* You don't have %dG.\n* Muffet sighs, and lowers her price.", b->state[MF_PRICE]);
+ b->state[MF_PRICE] = (int16_t)max_i(10, b->state[MF_PRICE] / 2);
+ }
+ } else { /* Struggle */
+ if (++b->state[MF_STRUGGLE] == 3) {
+ snprintf(t, (size_t)cap, "* You struggle with all your might.\n* Muffet halves her price, laughing.");
+ b->state[MF_PRICE] = (int16_t)max_i(10, b->state[MF_PRICE] / 2);
+ } else snprintf(t, (size_t)cap, "* You struggle against the web.\n* It's too sticky to get free.");
+ }
+}
+static const char *talk_muffet(Battle *b, Monster *m) {
+ if (b->state[MF_TREAT] == 1) {
+ b->state[MF_TREAT] = 2;
+ return "Oh? You\'ve had\none of our\ntreats~";
+ }
+ if (m->spare) return "My mistake,\ndeary~\nRun along\nnow~";
+ if (b->turn == 1) return "Purple\nsuits you\nmuch better,\ndeary~";
+ if (b->turn == 3 || b->turn == 7 || b->turn == 11) {
+ static const char *const meal[3] = {"Time for\nbreakfast,\nmy pet~", "Time for\nlunch,\nmy pet~", "Time for\ndinner,\nmy pet~"};
+ return meal[b->turn / 4];
+ }
+ static const char *const lines[] = {"Ahuhuhu~", "Someone paid\nme well for\nyour SOUL~", "The spiders\ntold me all\nabout you~",
+ "Too stingy\nto buy a\ndonut~?", "Pay up, and\nI might go\neasy~"};
+ return lines[rnd_n(5)];
+}
+static const char *flavor_muffet(Battle *b, Monster *m) {
+ if (!m->spare && b->turn >= 17) {
+ m->spare = true;
+ return "* A spider brings Muffet a note.\n* She reads it... and smiles.";
+ }
+ if (m->spare) return "* Muffet isn't going to fight anymore.";
+ return NULL;
+}
+static bool item_muffet(Battle *b, Monster *m, int item, char *t, int cap) {
+ (void)t, (void)cap;
+ /* a treat from the spiders' bake sale: it's eaten as ever, and she lets Frisk go */
+ if (item == ITEM_SPIDER_DONUT || item == ITEM_SPIDER_CIDER) m->spare = true, b->state[MF_TREAT] = 1;
+ return false;
+}
+static int attack_muffet(Battle *b, Monster *m) {
+ if (m->spare) return ATK_NONE;
+ if (b->turn == 3 || b->turn == 7 || b->turn == 11) return ATK_MUFFET_PET;
+ static const uint8_t seq[3] = {ATK_MUFFET_SPIDERS, ATK_MUFFET_CROISSANTS, ATK_MUFFET_DONUTS};
+ return seq[(b->turn - 1) % 3];
+}
+
+/* ---------------------------------------------------------------- Mettaton, after the tile puzzle: no hurting him;
+ * Alphys's yellow button (ACT) turns the SOUL yellow, and its shots wear him down */
+enum { MT_YELLOW, MT_HITS }; /* (b->state[]) */
+static void act_mtt_tiles(Battle *b, Monster *m, int act, char *t, int cap) {
+ (void)m;
+ if (act == 0) snprintf(t, (size_t)cap, "* You yell...\n* Nothing happens.");
+ else {
+ snprintf(t, (size_t)cap, "* You press the yellow button.\n* The phone hums near Mettaton...!");
+ b->state[MT_YELLOW] = 1;
+ }
+}
+static const char *talk_mtt_tiles(Battle *b, Monster *m) {
+ (void)m;
+ if (b->state[MT_HITS] >= 6) return "OH NO...\nI\'M OUT OF\nPOWER?!";
+ if (b->state[MT_YELLOW] && b->state[MT_HITS]) return "THAT\nBUTTON IS\nCHEATING!";
+ if (b->turn <= 1) return "NOW FOR\nTHE GRAND\nFINALE!";
+ static const char *const lines[] = {"NO MORE\nPUZZLES,\nDARLING!", "STAY\nTUNED!", "THE\nRATINGS\nARE GREAT!"};
+ return lines[rnd_n(3)];
+}
+static const char *flavor_mtt_tiles(Battle *b, Monster *m) {
+ (void)m;
+ if (b->turn == 2 && !b->state[MT_YELLOW]) return "* Alphys calls: \"The yellow button!\n It's in your ACT menu!\"";
+ if (b->turn >= 2 && !b->state[MT_YELLOW]) return "* Your phone's ACT menu is glowing.";
+ return NULL;
+}
+static bool hit_mtt_tiles(Battle *b, Monster *m, int dmg) {
+ (void)b, (void)dmg;
+ m->dmg_shown = 0; /* (MISS: metal) */
+ return true;
+}
+static int attack_mtt_tiles(Battle *b, Monster *m) { (void)m; return b->state[MT_YELLOW] ? ATK_MTT_YELLOW : ATK_MTT_BOXES; }
+static bool ends_mtt_tiles(Battle *b, Monster *m) { (void)m; return b->state[MT_HITS] >= 6; }
+
+#define BOMB(nm, chk) {.name = nm, .hp = 1, .idle_speed = 8, .check = chk, .acts = {"Defuse"}, .nacts = 1, \
+ .flags = MF_BOSS | MF_NOT_KILLABLE, .act = act_bomb, .talk = talk_bomb, .attack = attack_bomb, \
+ .flavor = flavor_bomb, .on_hit = hit_bomb, .draw = draw_bomb}
+static const MonDef bomb_defs[BOMB_COUNT] = {
+ [BOMB_DOG] = BOMB("Dog", "It's fast asleep. It has no idea."),
+ [BOMB_WATER] = BOMB("Glass of Water", "A glass of water. A very, very fast one."),
+ [BOMB_SCRIPT] = BOMB("Script", "A movie script. Every page ends with a bang."),
+ [BOMB_BALL] = BOMB("Basketball", "Painted to look like a basketball. Stylish, at least."),
+ [BOMB_PRESENT] = BOMB("Present", "Wrapped up with a bow. It's ticking."),
+ [BOMB_GAME] = BOMB("Game", "A video game. The box is oddly heavy."),
+};
+static const Form bomb_forms[BOMB_COUNT] = {
+ [BOMB_DOG] = {{MON_BOMB}, 1, "* It's the dog!\n* Defuse it!"},
+ [BOMB_WATER] = {{MON_BOMB}, 1, "* The glass of water dashes about!\n* Defuse it!"},
+ [BOMB_SCRIPT] = {{MON_BOMB}, 1, "* Defuse the script!"},
+ [BOMB_BALL] = {{MON_BOMB}, 1, "* Defuse the basketball!"},
+ [BOMB_PRESENT] = {{MON_BOMB}, 1, "* Defuse the present!"},
+ [BOMB_GAME] = {{MON_BOMB}, 1, "* Defuse the game!"},
+};
+
+static const MonDef ht_defs[MON_HT_COUNT - MON_WF_COUNT] = {
+ [MON_VULKIN - MON_WF_COUNT] = {
+ .name = "Vulkin", .spare_gold = 40, .hp = 20, .at = 8, .df = 0, .exp = 70, .gold = 40, .spr_idle = SPR_VULKIN_B,
+ .spr_hurt = SPR_VULKIN_HURT, .idle_speed = 6, .check = "Thinks its lava makes people feel better.",
+ .acts = {"Criticize", "Encourage", "Hug"}, .nacts = 3,
+ .neutral = {"* Vulkin is making coffee in its crater.", "* Vulkin hums a warm little song.", "* Smells like a campfire."},
+ .low_hp = "* Vulkin's lava is cooling.", .act = act_vulkin, .talk = talk_vulkin, .attack = attack_vulkin},
+ [MON_TSUNDERPLANE - MON_WF_COUNT] = {
+ .name = "Tsunderplane", .spare_gold = 60, .hp = 80, .at = 8, .df = 6, .exp = 95, .gold = 60, .spr_idle = SPR_TSUNDER_B,
+ .spr_hurt = SPR_TSUNDER_B, .idle_speed = 6, .check = "Acts mean, but maybe it secretly likes you?",
+ .acts = {"Flirt", "Approach"}, .nacts = 2,
+ .neutral = {"* Tsunderplane shakes its nose at you.", "* Tsunderplane is looking the other way.",
+ "* Smells like jet fuel and perfume."},
+ .low_hp = "* Tsunderplane's engine is smoking.", .act = act_tsunder, .talk = talk_tsunder, .attack = attack_tsunder,
+ .flavor = flavor_tsunder},
+ [MON_PYROPE - MON_WF_COUNT] = {
+ .name = "Pyrope", .spare_gold = 45, .hp = 110, .at = 8, .df = 1, .exp = 80, .gold = 30, .spr_idle = SPR_PYROPE_B,
+ .spr_hurt = SPR_PYROPE_HURT, .idle_speed = 5, .dy = 18, .check = "A troublemaker who's never warm enough.",
+ .acts = {"Heat Up", "Cool Down", "Invite"}, .nacts = 3,
+ .neutral = {"* Pyrope is protected by its winning smile.", "* Pyrope crackles.", "* Smells like a burnt rope."},
+ .act = act_pyrope, .talk = talk_pyrope, .attack = attack_pyrope},
+ [MON_METTATON - MON_WF_COUNT] = {
+ .name = "Mettaton", .hp = 9999, .at = 0, .df = 0, .spr_idle = SPR_MTT_B, .spr_hurt = SPR_MTT_B, .idle_speed = 6,
+ .check = "A robot with a TV show.", .flags = MF_BOSS | MF_NOT_KILLABLE},
+ [MON_RG01 - MON_WF_COUNT] = {
+ .name = "RG 01", .spare_gold = 50, .hp = 150, .at = 8, .df = 4, .exp = 110, .gold = 60, .spr_idle = SPR_RG01_B,
+ .spr_hurt = SPR_RG01_B, .idle_speed = 6, .check = "A Royal Guard. Armor polished to a shine.",
+ .acts = {"Clean Armor", "Whisper"}, .nacts = 2,
+ .neutral = {"* 01 polishes his helmet.", "* 02 watches your every step.", "* Smells like a locker room.",
+ "* 01 stands guard."},
+ .flags = MF_BOSS, .act = act_rg, .talk = talk_rg, .attack = attack_rg, .flavor = flavor_rg},
+ [MON_RG02 - MON_WF_COUNT] = {
+ .name = "RG 02", .spare_gold = 50, .hp = 150, .at = 8, .df = 4, .exp = 110, .gold = 60, .spr_idle = SPR_RG02_B,
+ .spr_hurt = SPR_RG02_B, .idle_speed = 6, .check = "A Royal Guard. Armor looks stuffy and hot.",
+ .acts = {"Clean Armor", "Whisper"}, .nacts = 2,
+ .neutral = {"* Sweat drips from 02's armor.", "* 02 is clenching his fists.", "* Smells like a locker room."},
+ .flags = MF_BOSS, .act = act_rg, .talk = talk_rg, .attack = attack_rg, .flavor = flavor_rg},
+ [MON_BOMB - MON_WF_COUNT] = BOMB("Bomb", ""),
+ [MON_MUFFET - MON_WF_COUNT] = {
+ .name = "Muffet", .spare_gold = 100, .hp = 1250, .at = 8, .df = 0, .exp = 300, .gold = 100, .spr_idle = SPR_MUFFET_B,
+ .spr_hurt = SPR_MUFFET_HURT, .idle_speed = 5, .check = "If she asks you up to her parlor, say no thanks.",
+ .acts = {"Pay", "Struggle"}, .nacts = 2,
+ .neutral = {"* Muffet pours herself a cup of tea.", "* Muffet counts her money, twice.",
+ "* Smells like cobwebs and fresh pastry."},
+ .flags = MF_BOSS, .act = act_muffet, .talk = talk_muffet, .attack = attack_muffet, .flavor = flavor_muffet,
+ .on_item = item_muffet},
+ [MON_MTT_TILES - MON_WF_COUNT] = {
+ .name = "Mettaton", .hp = 9999, .at = 8, .df = 0, .spr_idle = SPR_MTT_B, .spr_hurt = SPR_MTT_B, .idle_speed = 6,
+ .check = "Still made of metal. Still can't be hurt.", .acts = {"Yell", "Yellow"}, .nacts = 2,
+ .neutral = {"* Mettaton.", "* Mettaton strikes a pose."},
+ .flags = MF_BOSS | MF_NOT_KILLABLE, .act = act_mtt_tiles, .talk = talk_mtt_tiles, .attack = attack_mtt_tiles,
+ .flavor = flavor_mtt_tiles, .on_hit = hit_mtt_tiles, .ends = ends_mtt_tiles},
+};
+const MonDef *ht_mon_def(int id) {
+ if (id >= MON_HT_COUNT) return core_mon_def(id);
+ return id == MON_BOMB ? &bomb_defs[g_bomb_kind] : &ht_defs[id - MON_WF_COUNT];
+}
+
+static const Form ht_forms[FORM_HT_COUNT - FORM_WF_COUNT] = {
+ [FORM_VULKIN - FORM_WF_COUNT] = {{MON_VULKIN}, 1, "* Vulkin strolls in, happy as can be."},
+ [FORM_TSUNDERPLANE - FORM_WF_COUNT] = {{MON_TSUNDERPLANE}, 1, "* Tsunderplane gets in the way!\n* Not on purpose or anything."},
+ [FORM_TSUNDER_VULKIN - FORM_WF_COUNT] = {{MON_VULKIN, MON_TSUNDERPLANE}, 2,
+ "* Tsunderplane and Vulkin show up.\n* (Not that Tsunderplane cares.)"},
+ [FORM_PYROPE - FORM_WF_COUNT] = {{MON_PYROPE}, 1, "* Pyrope bounces in, burning bright!"},
+ [FORM_PYROPE2 - FORM_WF_COUNT] = {{MON_PYROPE, MON_PYROPE}, 2, "* Two Pyropes hop in.\n* They're up to something."},
+ [FORM_MTT_QUIZ - FORM_WF_COUNT] = {{MON_METTATON}, 1, ""},
+ [FORM_MTT_COOK - FORM_WF_COUNT] = {{MON_METTATON}, 1, ""},
+ [FORM_LASERS - FORM_WF_COUNT] = {{MON_METTATON}, 1, ""},
+ [FORM_RGUARDS - FORM_WF_COUNT] = {{MON_RG01, MON_RG02}, 2, "* The Royal Guards attack!"},
+ [FORM_BOMB - FORM_WF_COUNT] = {{MON_BOMB}, 1, ""},
+ [FORM_MUFFET - FORM_WF_COUNT] = {{MON_MUFFET}, 1, "* Muffet blocks the way!"},
+ [FORM_MTT_TILES - FORM_WF_COUNT] = {{MON_MTT_TILES}, 1, "* Mettaton attacks!"},
+};
+const Form *ht_form(int form) {
+ if (form >= FORM_HT_COUNT) return core_form(form);
+ return form == FORM_BOMB ? &bomb_forms[g_bomb_kind] : &ht_forms[form - FORM_WF_COUNT];
+}
+
+/* random encounters: past the Lab (the belts and the Magma Chamber); on the second floor, the crossroads and the
+ * Bad Opinion Zone */
+int ht_encounter(int room) {
+ switch (room) {
+ case ROOM_HT_BELT1: case ROOM_HT_BELT2: case ROOM_HT_MAGMA: {
+ static const uint8_t pool[] = {FORM_VULKIN, FORM_TSUNDERPLANE, FORM_TSUNDER_VULKIN, FORM_PYROPE, FORM_PYROPE2};
+ return pool[rnd_n(5)];
+ }
+ case ROOM_HT_CROSS2: case ROOM_HT_BOZ: {
+ static const uint8_t pool[] = {FORM_TSUNDERPLANE, FORM_TSUNDER_VULKIN, FORM_PYROPE, FORM_PYROPE2};
+ return pool[rnd_n(4)];
+ }
+ }
+ return core_encounter(room);
+}
+
+/* ---------------------------------------------------------------- the attacks */
+static int bx0(void) { return bt.x0 + 3; }
+static int bx1(void) { return bt.x1 - 3; }
+static int by0(void) { return bt.y0 + 3; }
+static int by1(void) { return bt.y1 - 3; }
+#define P(v) ((v) * FX)
+static Bullet *shot(uint16_t spr, int x, int y, int vx, int vy) { return bullet_new(spr, P(x), P(y), vx, vy); }
+
+/* Vulkin: a thundercloud over the board drops bolts that spread as they fall (it drifts, sometimes) */
+static void atk_vulkin_thunder(int t) {
+ static Bullet *cloud;
+ if (t == 0) {
+ cloud = shot(SPR_CLOUD, board_cx(), by0() + 8, rnd_n(2) ? P(1) / 2 : 0, 0);
+ if (cloud) cloud->flags |= BF_HARMLESS | BF_NOCLIP, cloud->life = 150;
+ }
+ if (cloud && cloud->kind != BK_NONE && (cloud->x < P(bx0() + 14) || cloud->x > P(bx1() - 14))) cloud->vx = -cloud->vx;
+ if (cloud && cloud->kind != BK_NONE && t < 130 && t % 20 == 10) {
+ for (int k = -1; k <= 1; k++) {
+ Bullet *b = bullet_new(SPR_BOLT, cloud->x, cloud->y + P(6), k * P(1) / 2, P(2));
+ if (b) b->r = 2, b->flags |= BF_NOCLIP;
+ }
+ }
+}
+/* and fire: two columns at the sides throw fireballs at the middle, and they fall back */
+static void atk_vulkin_fire(int t) {
+ if (t < 130 && t % 16 == 0) {
+ bool left = (t / 16) % 2;
+ int y = by0() + 8 + rnd_n(by1() - by0() - 16);
+ Bullet *b = shot(SPR_FIREBULLET, left ? bx0() + 2 : bx1() - 2, y, (left ? 1 : -1) * P(3), 0);
+ if (b) b->r = 4, b->ax = (left ? -1 : 1) * 3, b->anim = 3, b->flags |= BF_NOCLIP, b->life = 120;
+ }
+}
+
+/* Tsunderplane: little planes cross the board dropping bombs (a bomb at the bottom sends smoke all the way up);
+ * approached, its planes come wrapped in green: touch it */
+static int tsunder_approached(void) {
+ for (int i = 0; i < bt.nmon; i++)
+ if (bt.mon[i].def == mon_def(MON_TSUNDERPLANE) && bt.mon[i].state && !bt.mon[i].gone) return 1;
+ return 0;
+}
+static void green_wrap(Bullet *p) {
+ Bullet *g = bullet_new(SPR_ORB, p->x, p->y - P(9), p->vx, p->vy);
+ if (g) g->color = BC_GREEN, g->dmg = 0, g->r = 4, g->flags |= BF_NOCLIP, g->life = p->life;
+}
+static void atk_tsunder_bombs(int t) {
+ if (t < 120 && t % 30 == 0) {
+ bool left = (t / 30) % 2;
+ int y = by0() + 6;
+ Bullet *p = shot(SPR_PLANE, left ? bx0() - 10 : bx1() + 10, y, (left ? 1 : -1) * P(2), 0);
+ if (p) p->r = 4, p->flags |= BF_NOCLIP, p->life = 110, p->angle = left ? 0 : 128;
+ if (p && tsunder_approached()) green_wrap(p);
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind == BK_NONE) continue;
+ if (b->spr == SPR_PLANE && b->t % 24 == 12 && b->x > P(bx0()) && b->x < P(bx1())) {
+ Bullet *d = bullet_new(SPR_BOMB, b->x, b->y + P(6), 0, P(1));
+ if (d) d->kind = BK_GRAVITY, d->ay = 2, d->r = 3, d->a = 1;
+ }
+ if (b->spr == SPR_BOMB && b->a == 1 && b->y > P(by1() - 4)) {
+ /* the smoke: a column up through the whole board, a moment */
+ int x = b->x / FX;
+ b->kind = BK_NONE;
+ for (int y = by1() - 4; y > by0(); y -= 9) {
+ Bullet *s = shot(SPR_ORB, x, y, 0, 0);
+ if (s) s->r = 3, s->life = 18, s->alpha = 170;
+ }
+ }
+ }
+}
+/* or big planes dive from the top, a trail of smoke behind (it moves across, unless approached) */
+static void atk_tsunder_dive(int t) {
+ if (t < 120 && t % 40 == 0) {
+ int x = bx0() + 10 + rnd_n(bx1() - bx0() - 20);
+ Bullet *p = shot(SPR_PLANE, x, by0() - 10, 0, P(2));
+ if (p) p->r = 5, p->flags |= BF_NOCLIP, p->life = 80, p->angle = 64;
+ if (p && tsunder_approached()) green_wrap(p);
+ }
+ bool still = tsunder_approached();
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind == BK_NONE || b->spr != SPR_PLANE || b->angle != 64 || b->t % 8) continue;
+ Bullet *s = bullet_new(SPR_ORB, b->x, b->y - P(8), still ? 0 : (b->t % 16 ? P(1) : -P(1)), 0);
+ if (s) s->r = 2, s->life = 50, s->alpha = 150;
+ }
+}
+
+/* Pyrope: bombs come from every side, bounce about and burst at the board's edges; or its rope of fire runs across
+ * (orange: run through it) */
+static void atk_pyrope_bombs(int t) {
+ if (t < 110 && t % 22 == 0) {
+ int side = rnd_n(4);
+ int x = side < 2 ? bx0() + 8 + rnd_n(bx1() - bx0() - 16) : side == 2 ? bx0() : bx1();
+ int y = side >= 2 ? by0() + 8 + rnd_n(by1() - by0() - 16) : side == 0 ? by0() : by1();
+ int vx = side == 2 ? P(2) : side == 3 ? -P(2) : (rnd_n(2) ? P(1) : -P(1));
+ int vy = side == 0 ? P(2) : side == 1 ? -P(2) : (rnd_n(2) ? P(1) : -P(1));
+ Bullet *b = shot(SPR_BOMB, x, y, vx, vy);
+ if (b) b->kind = BK_SCRIPT, b->r = 3, b->a = 3, b->flags |= BF_NOCLIP;
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT || b->spr != SPR_BOMB) continue;
+ b->x += b->vx, b->y += b->vy, b->t++;
+ bool hit_x = b->x < P(bx0() + 2) || b->x > P(bx1() - 2), hit_y = b->y < P(by0() + 2) || b->y > P(by1() - 2);
+ if (b->t > 8 && (hit_x || hit_y)) {
+ if (--b->a > 0) {
+ if (hit_x) b->vx = -b->vx;
+ if (hit_y) b->vy = -b->vy;
+ continue;
+ }
+ /* it bursts: sparks */
+ b->kind = BK_NONE;
+ for (int k = 0; k < 4; k++) {
+ static const int8_t sx[4] = {1, -1, 1, -1}, sy[4] = {1, 1, -1, -1};
+ Bullet *s = bullet_new(SPR_ORB, b->x, b->y, sx[k] * P(1), sy[k] * P(1));
+ if (s) s->r = 2, s->life = 30;
+ }
+ }
+ }
+}
+static void atk_pyrope_rope(int t) {
+ if (t < 120 && t % 40 == 0) {
+ Bullet *f = shot(SPR_FIREBULLET, bx1() + 6, board_cy(), -P(2) - P(t / 40) / 2, 0);
+ if (f) f->color = BC_ORANGE, f->flags |= BF_BOX | BF_NOCLIP, f->w = 8, f->h = (int16_t)(by1() - by0()), f->life = 120, f->anim = 3;
+ }
+}
+
+/* the Royal Guards' team attack: stars falling from the top, and others rising from the bottom. Once 02's armor's
+ * been cleaned, some of the falling ones are green (dropped right over the SOUL); once it's off, 01's aim goes shaky and they fly out at the
+ * sides. One guard fallen, the other attacks twice as hard */
+static void atk_rg_team(int t) {
+ bool angry = false, off = bt.state[RG_ARMOR_OFF], green = bt.state[RG_CLEANED] && !off;
+ for (int i = 0; i < bt.nmon; i++) angry |= bt.mon[i].killed;
+ int every = angry ? 10 : 20;
+ if (t < 130 && t % every == 0) {
+ /* (a green one falls right over the SOUL) */
+ bool g = green && t % 40 == 20;
+ int x = g ? bt.sx / FX : bx0() + 6 + rnd_n(bx1() - bx0() - 12);
+ Bullet *s = shot(SPR_STAR, x, by0() - 4, off ? (rnd_n(2) ? P(3) / 2 : -P(3) / 2) : 0, P(1) / 2);
+ if (s) {
+ s->kind = BK_GRAVITY, s->ay = 2, s->r = 3, s->flags |= BF_NOCLIP, s->life = 120;
+ if (g) s->color = BC_GREEN, s->dmg = 0;
+ }
+ }
+ if (t < 130 && t % (every * 3 / 2) == 15) {
+ int x = bx0() + 6 + rnd_n(bx1() - bx0() - 12);
+ Bullet *s = shot(SPR_STAR, x, by1() + 4, 0, -P(2));
+ if (s) s->r = 3, s->flags |= BF_NOCLIP, s->life = 90;
+ }
+}
+
+/* Muffet's: spiders crawl along the strings (riding them when they're pulled); croissants thrown along one come
+ * back; donuts bounce about the board; and at mealtimes her pet waits at the bottom while the strings are pulled down
+ * to it */
+static void spider_on(int string, bool left, int speed) {
+ Bullet *b = shot(SPR_SPIDER_B, left ? bx0() - 6 : bx1() + 6, soul_string_y(string), (left ? 1 : -1) * speed, 0);
+ if (b) b->kind = BK_SCRIPT, b->a = (int16_t)string, b->flags |= BF_NOCLIP, b->r = 3, b->life = 260;
+}
+static void spiders_ride(void) {
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT || b->spr != SPR_SPIDER_B) continue;
+ b->x += b->vx, b->y = soul_string_y(b->a) * FX;
+ if (++b->t >= b->life) b->kind = BK_NONE;
+ }
+}
+static void atk_muffet_spiders(int t) {
+ if (t < 130 && t % 14 == 0) spider_on(rnd_n(NSTRINGS), rnd_n(2), P(2));
+ spiders_ride();
+}
+static void atk_muffet_croissants(int t) {
+ if (t < 110 && t % 26 == 0) {
+ bool left = rnd_n(2);
+ Bullet *b = shot(SPR_CROISSANT, left ? bx0() - 6 : bx1() + 6, soul_string_y(rnd_n(NSTRINGS)), (left ? 1 : -1) * P(4), 0);
+ if (b) b->ax = left ? -1 : 1, b->r = 4, b->flags |= BF_NOCLIP, b->life = 140, b->angle = left ? 128 : 0;
+ }
+}
+static void atk_muffet_donuts(int t) {
+ if (t < 110 && t % 20 == 0) {
+ bool left = (t / 20) % 2;
+ Bullet *b = shot(SPR_DONUT, left ? bx0() + 4 : bx1() - 4, by0() + 4, (left ? 1 : -1) * P(2), P(2));
+ if (b) b->kind = BK_SCRIPT, b->a = 3, b->r = 4, b->flags |= BF_NOCLIP;
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT || b->spr != SPR_DONUT) continue;
+ b->x += b->vx, b->y += b->vy, b->t++;
+ bool hx = b->x < P(bx0() + 2) || b->x > P(bx1() - 2), hy = b->y < P(by0() + 2) || b->y > P(by1() - 2);
+ if (b->t > 6 && (hx || hy)) {
+ if (--b->a <= 0) b->kind = BK_NONE;
+ if (hx) b->vx = -b->vx;
+ if (hy) b->vy = -b->vy;
+ }
+ }
+}
+static void atk_muffet_pet(int t) {
+ if (t == 0) {
+ Bullet *p = shot(SPR_MUFFET_PET, board_cx(), by1() + 8, 0, 0);
+ if (p) p->flags |= BF_BOX | BF_NOCLIP, p->w = 90, p->h = 26, p->anim = 6, p->life = 150, p->dmg = 4;
+ }
+ /* the strings, down towards it (round again from the top) */
+ soul_strings_shift += P(1) / 2;
+ if (t < 130 && t % 30 == 0) spider_on(rnd_n(NSTRINGS), rnd_n(2), P(1));
+ spiders_ride();
+}
+
+/* Mettaton's: rows of boxes falling, a gap in each; turned yellow, the SOUL shoots at him as he glides about over
+ * the board (six hits, and he's out of power), a few boxes still falling */
+static void atk_mtt_boxes(int t) {
+ if (t < 130 && t % 26 == 0) {
+ static const uint8_t gaps[6] = {1, 4, 2, 5, 0, 3};
+ int n = (bx1() - bx0()) / 16, gap = gaps[(t / 26) % 6] % n;
+ for (int k = 0; k < n; k++) {
+ if (k == gap || k == gap + 1) continue;
+ Bullet *b = shot(SPR_PRESENT, bx0() + 8 + k * 16, by0() - 8, 0, P(2));
+ if (b) b->r = 5, b->flags |= BF_NOCLIP, b->life = 90;
+ }
+ }
+}
+static Bullet *mtt_target;
+static void atk_mtt_yellow(int t) {
+ if (t == 0) {
+ mtt_target = shot(SPR_MTTMINI, board_cx(), by0() + 12, P(3) / 2, 0);
+ if (mtt_target) mtt_target->kind = BK_SCRIPT, mtt_target->flags |= BF_HARMLESS;
+ }
+ Bullet *m = mtt_target;
+ if (!m || m->kind != BK_SCRIPT || m->spr != SPR_MTTMINI) return;
+ m->x += m->vx;
+ if (m->x < P(bx0() + 10) || m->x > P(bx1() - 10)) m->vx = -m->vx;
+ if (m->c) m->c--, m->alpha = m->c % 2 ? 90 : 255;
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *s = &bullets[i];
+ if (s->kind == BK_NONE || !(s->flags & BF_PLAYER)) continue;
+ if (abs_i(s->x - m->x) < P(9) && abs_i(s->y - m->y) < P(11)) {
+ s->kind = BK_NONE;
+ bt.state[MT_HITS]++, m->c = 10;
+ }
+ }
+ if (t < 130 && t % 40 == 20) {
+ Bullet *b = shot(SPR_PRESENT, m->x / FX, m->y / FX + 10, 0, P(2));
+ if (b) b->r = 5, b->flags |= BF_NOCLIP, b->life = 80;
+ }
+}
+
+int ht_attack_length(int atk) { return atk >= ATK_HT_COUNT ? core_attack_length(atk) : atk == ATK_MUFFET_PET ? 150 : 160; }
+
+void ht_attack_run(Battle *b, int atk, int t) {
+ if (atk >= ATK_HT_COUNT) {
+ core_attack_run(b, atk, t);
+ return;
+ }
+ if (t < 0) {
+ bool muffet = atk >= ATK_MUFFET_SPIDERS && atk <= ATK_MUFFET_PET;
+ board_set(muffet ? 190 : atk == ATK_TSUNDER_BOMBS || atk == ATK_PYROPE_ROPE ? 150 : atk == ATK_MTT_BOXES ? 140 : 120,
+ muffet ? 90 : 100);
+ soul_set_mode(muffet ? SOUL_PURPLE : atk == ATK_MTT_YELLOW ? SOUL_YELLOW : SOUL_RED);
+ return;
+ }
+ switch (atk) {
+ case ATK_VULKIN_THUNDER: atk_vulkin_thunder(t); break;
+ case ATK_VULKIN_FIRE: atk_vulkin_fire(t); break;
+ case ATK_TSUNDER_BOMBS: atk_tsunder_bombs(t); break;
+ case ATK_TSUNDER_DIVE: atk_tsunder_dive(t); break;
+ case ATK_PYROPE_BOMBS: atk_pyrope_bombs(t); break;
+ case ATK_PYROPE_ROPE: atk_pyrope_rope(t); break;
+ case ATK_RG_TEAM: atk_rg_team(t); break;
+ case ATK_MUFFET_SPIDERS: atk_muffet_spiders(t); break;
+ case ATK_MUFFET_CROISSANTS: atk_muffet_croissants(t); break;
+ case ATK_MUFFET_DONUTS: atk_muffet_donuts(t); break;
+ case ATK_MUFFET_PET: atk_muffet_pet(t); break;
+ case ATK_MTT_BOXES: atk_mtt_boxes(t); break;
+ case ATK_MTT_YELLOW: atk_mtt_yellow(t); break;
+ }
+}
diff --git a/games/undertale/src/htshows.c b/games/undertale/src/htshows.c
new file mode 100644
index 00000000..bee69ab7
--- /dev/null
+++ b/games/undertale/src/htshows.c
@@ -0,0 +1,256 @@
+/* Mettaton's shows, and a laser's alarm: battles that play their own way (battle.c's scripts).
+ *
+ * The quiz: Mettaton asks, four answers wait in the box, and the SOUL picks one (Z over it) before the time runs
+ * out; Alphys, beside the stage, signals the right one. A wrong answer halves Frisk's HP (never below 1).
+ * The cooking show's jetpack: Frisk flies up the counter as it grows, dodging what Mettaton throws, before a minute
+ * is up. The alarm: three lasers, orange or blue, sweep through the SOUL. (As the Undertale Wiki describes them;
+ * the questions and what's said are ours.) */
+#include
+#include
+#include "battle.h"
+#include "game.h"
+#include "gfx.h"
+#include "monsters.h"
+#include "res.h"
+#include "text.h"
+
+#define P(v) ((v) * FX)
+
+static void bubble(const char *s) {
+ writer_start(&bt.bw, s, 0, 0, FONT_SMALL);
+ bt.bw.line_h = 9;
+}
+static void draw_bubble_at(int bx, int by, int w, int h) {
+ gfx_rect(bx, by, w, h, C_WHITE, 255);
+ gfx_rect(bx - 6, by + 16, 6, 5, C_WHITE, 255);
+ bt.bw.x = bx + 5, bt.bw.y = by + 5;
+ writer_draw(&bt.bw, C_BLACK);
+}
+static void draw_box(void) {
+ gfx_rect(bt.x0, bt.y0, bt.x1 - bt.x0, bt.y1 - bt.y0, C_WHITE, 255);
+ gfx_rect(bt.x0 + 2, bt.y0 + 2, bt.x1 - bt.x0 - 4, bt.y1 - bt.y0 - 4, C_BLACK, 255);
+}
+static void draw_soul(void) {
+ if (!bt.inv || (bt.inv / 2) % 2) gfx_sprite(SPR_HEART, 0, soul_x(), soul_y(), 0, 255);
+}
+static void draw_mettaton(int x, int y) {
+ gfx_sprite(SPR_MTT_B, (int)(g_frame / 6), x, y, 0, 255);
+}
+static void tick(uint32_t keys) {
+ if (bt.inv) bt.inv--;
+ soul_update(keys);
+ bullets_update();
+ soul_hit_test();
+ if (sv.hp < 1) sv.hp = 1; /* (these never kill) */
+}
+
+/* ---------------------------------------------------------------- the quiz */
+typedef struct {
+ const char *q;
+ const char *a[4];
+ int right;
+} Question;
+static const Question quiz[] = {
+ {"First, an\neasy one!\nWhat color\nis a human\nSOUL?", {"Blue", "Red", "Green", "Plaid"}, 1},
+ {"Who leads\nthe Royal\nGuard?", {"Papyrus", "Sans", "Undyne", "A dog"}, 2},
+ {"How many\nhuman SOULs\ndoes the king\nneed?", {"Five", "Six", "Seven", "A dozen"}, 2},
+ {"Quick math!\n7 x 6 - 24.5\nis...?", {"17.5", "16.5", "18", "42"}, 0},
+ {"Which did\nyou NOT see\nin Waterfall?", {"Echo flowers", "Temmies", "Snowmen", "Bridge seeds"}, 2},
+ {"And who's\nthe star of\nthis show?", {"You", "Alphys", "The crowd", "Mettaton"}, 3},
+};
+#define NQ ((int)(sizeof quiz / sizeof quiz[0]))
+enum { QZ_HELLO, QZ_ASK, QZ_SAY, QZ_BYE };
+/* bt.state: [0] step, [1] the question, [2] frames left to answer, [3] wrong answers */
+#define QZ_TIME 450
+
+static void quiz_ask(void) {
+ bt.state[0] = QZ_ASK, bt.state[2] = QZ_TIME;
+ bubble(quiz[bt.state[1]].q);
+ bt.sx = P(board_cx()), bt.sy = P(board_cy());
+}
+static int zone_at(int x, int y) {
+ /* the four answers, two by two in the box */
+ int col = x >= board_cx(), row = y >= board_cy();
+ return row * 2 + col;
+}
+void quiz_start(void) {
+ bt.scripted = true;
+ bt.state[0] = QZ_HELLO, bt.state[1] = 0, bt.state[3] = 0;
+ board_set_rect(40, 128, 280, 194);
+ bt.x0 = bt.tx0, bt.y0 = bt.ty0, bt.x1 = bt.tx1, bt.y1 = bt.ty1;
+ bt.sx = P(board_cx()), bt.sy = P(board_cy());
+ bubble("Welcome,\nbeauties, to\nthe quiz show\nof the\nunderground!|Answer right,\nand you win!\nAnswer wrong,\nand...|Well! Let's\nnot spoil the\nsurprise!");
+}
+void quiz_update(uint32_t keys, uint32_t pressed) {
+ switch (bt.state[0]) {
+ case QZ_HELLO:
+ case QZ_SAY:
+ if (!writer_update(&bt.bw, pressed)) {
+ if (bt.state[0] == QZ_SAY && bt.state[1] >= NQ) {
+ bt.state[0] = QZ_BYE;
+ bubble("Hmph! Too\neasy, with a\ncertain doctor\nwaving the\nanswers!|Oh well. That\nwas only our\nfirst episode!|Until next\ntime, darlings!");
+ } else {
+ quiz_ask();
+ }
+ }
+ break;
+ case QZ_ASK: {
+ writer_update(&bt.bw, 0);
+ tick(keys);
+ /* (a Z left over from the talk doesn't answer: the question first) */
+ bool chose = ((pressed & K_Z) && bt.state[2] < QZ_TIME - 20) || --bt.state[2] <= 0;
+ if (!chose) break;
+ int z = bt.state[2] <= 0 ? -1 : zone_at(soul_x(), soul_y());
+ const Question *q = &quiz[bt.state[1]];
+ if (z == q->right) {
+ static const char *const yes[] = {"Correct!\nHow\nthrilling!", "Right\nagain!", "Ooh, you're\ngood!"};
+ bubble(yes[bt.state[1] % 3]);
+ } else {
+ sv.hp = (int16_t)max_i(1, sv.hp / 2);
+ bt.inv = 30, bt.state[3]++;
+ bubble(z < 0 ? "Time's up!\nThat's a\nshock for\nyou!" : "Wrong!\nThat's a\nshock for\nyou!");
+ /* (worn out: straight to the end) */
+ if (sv.hp <= 1) bt.state[1] = NQ - 1;
+ }
+ bt.state[1]++;
+ bt.state[0] = QZ_SAY;
+ break;
+ }
+ case QZ_BYE:
+ if (!writer_update(&bt.bw, pressed)) bt.result = BR_SCRIPTED;
+ break;
+ }
+}
+void quiz_draw(void) {
+ gfx_sprite(SPR_BATTLEBG, 0, 0, 0, 0, 255);
+ draw_mettaton(120, 118);
+ draw_bubble_at(182, 18, 104, 76);
+ draw_box();
+ if (bt.state[0] == QZ_ASK) {
+ const Question *q = &quiz[bt.state[1]];
+ static const char *const letter[4] = {"A", "B", "C", "D"};
+ for (int i = 0; i < 4; i++) {
+ int x = (i % 2 ? board_cx() + 6 : bt.x0 + 8), y = (i / 2 ? board_cy() + 6 : bt.y0 + 8);
+ char s[32];
+ snprintf(s, sizeof s, "%s %s", letter[i], q->a[i]);
+ bool on = zone_at(soul_x(), soul_y()) == i;
+ gfx_text(FONT_SMALL, x, y, s, on ? C_YELLOW : C_WHITE, 255);
+ }
+ gfx_rect(board_cx(), bt.y0 + 2, 1, bt.y1 - bt.y0 - 4, rgb565(0x404040), 255);
+ gfx_rect(bt.x0 + 2, board_cy(), bt.x1 - bt.x0 - 4, 1, rgb565(0x404040), 255);
+ char s[8];
+ snprintf(s, sizeof s, "%d", (bt.state[2] + 14) / 15);
+ gfx_text(FONT_SMALL, bt.x0 - 22, bt.y0 + 2, s, C_WHITE, 255);
+ /* Alphys, off to the side, signals the answer */
+ if (bt.state[2] < QZ_TIME - 60) {
+ gfx_sprite(SPR_FACE_ALPHYS, 0, 282, 104, 0, 255);
+ gfx_text(FONT_MAIN, 300, 92, letter[q->right], C_YELLOW, 255);
+ }
+ draw_soul();
+ }
+ battle_draw_hud();
+}
+
+/* ---------------------------------------------------------------- the cooking show: the jetpack */
+/* bt.state: [0] step, [1] how high (0..CK_TOP), [2] frames left */
+enum { CK_HELLO, CK_FLY, CK_BYE };
+#define CK_TOP 600
+#define CK_TIME 1800
+void cook_start(void) {
+ bt.scripted = true;
+ bt.state[0] = CK_HELLO, bt.state[1] = 0, bt.state[2] = CK_TIME;
+ board_set_rect(100, 40, 220, 194);
+ bt.x0 = bt.tx0, bt.y0 = bt.ty0, bt.x1 = bt.tx1, bt.y1 = bt.ty1;
+ bt.sx = P(board_cx()), bt.sy = P(bt.y1 - 14);
+ bubble("The counter's\ngrowing! Get\nthe substitute\nin under a\nminute!|Use your\nphone's\nJETPACK!\nUp, up and\naway!");
+}
+void cook_update(uint32_t keys, uint32_t pressed) {
+ switch (bt.state[0]) {
+ case CK_HELLO:
+ if (!writer_update(&bt.bw, pressed)) bt.state[0] = CK_FLY, bt.t = 0;
+ break;
+ case CK_FLY: {
+ int hp = sv.hp;
+ tick(keys);
+ /* what Mettaton throws down (faster as the counter goes up) */
+ if (bt.t % max_i(8, 22 - bt.state[1] / 50) == 0) {
+ Bullet *b = bullet_new(rnd_n(3) ? SPR_ORB : SPR_BOMB, P(bt.x0 + 8 + rnd_n(bt.x1 - bt.x0 - 16)), P(bt.y0 + 2), 0,
+ P(2) + P(bt.state[1]) / 300);
+ if (b) b->r = 4, b->dmg = 3, b->flags |= BF_NOCLIP;
+ }
+ /* up the jetpack goes, set back by every hit */
+ bt.state[1] = (int16_t)min_i(CK_TOP, bt.state[1] + 1 - (sv.hp < hp ? 60 : 0));
+ if (bt.state[1] < 0) bt.state[1] = 0;
+ if (bt.state[1] >= CK_TOP) {
+ bt.state[0] = CK_BYE, bt.state[3] = 1;
+ bullets_clear();
+ bubble("You made it!\nThe vegan\nsubstitute!|...and I'd\nalready baked\nthe cake.\nTa-da!|That's our\nshow! Ciao!");
+ } else if (--bt.state[2] <= 0) {
+ bt.state[0] = CK_BYE, bt.state[3] = 0;
+ bullets_clear();
+ bubble("Oh dear,\nout of time!|...but we're\nat the break.\nNo killing\nwithout an\naudience!|Ciao for now!");
+ }
+ break;
+ }
+ case CK_BYE:
+ if (!writer_update(&bt.bw, pressed)) bt.result = BR_SCRIPTED;
+ break;
+ }
+}
+int cook_won(void) { return bt.state[3]; }
+void cook_draw(void) {
+ gfx_sprite(SPR_BATTLEBG, 0, 0, 0, 0, 255);
+ draw_mettaton(272, 118);
+ draw_box();
+ gfx_clip(bt.x0 + 2, bt.y0 + 2, bt.x1 - bt.x0 - 4, bt.y1 - bt.y0 - 4);
+ /* the counter's side scrolling past: lines going down as Frisk goes up */
+ for (int k = 0; k < 8; k++) {
+ int y = bt.y0 + ((k * 20 + bt.state[1]) % 160);
+ gfx_rect(bt.x0 + 2, y, bt.x1 - bt.x0 - 4, 1, rgb565(0x303030), 255);
+ }
+ bullets_draw();
+ if (bt.state[0] == CK_FLY) draw_soul();
+ gfx_clip_all();
+ /* how far up, and the time */
+ int h = (bt.y1 - bt.y0) * bt.state[1] / CK_TOP;
+ gfx_rect(bt.x0 - 14, bt.y0, 6, bt.y1 - bt.y0, rgb565(0x404040), 255);
+ gfx_rect(bt.x0 - 14, bt.y1 - h, 6, h, C_YELLOW, 255);
+ char s[8];
+ snprintf(s, sizeof s, "%d", (bt.state[2] + 29) / 30);
+ gfx_text(FONT_MAIN, bt.x0 - 34, bt.y0 - 2, s, C_WHITE, 255);
+ if (bt.state[0] != CK_FLY) draw_bubble_at(182, 18, 104, 76);
+ battle_draw_hud();
+}
+
+/* ---------------------------------------------------------------- the lasers' alarm */
+void lasers_start(void) {
+ bt.scripted = true;
+ bt.state[0] = 0;
+ board_set(120, 60);
+ bt.x0 = bt.tx0, bt.y0 = bt.ty0, bt.x1 = bt.tx1, bt.y1 = bt.ty1;
+ bt.sx = P(board_cx()), bt.sy = P(board_cy());
+ bt.t = 0;
+}
+void lasers_update(uint32_t keys, uint32_t pressed) {
+ (void)pressed;
+ tick(keys);
+ if (bt.t % 40 == 10 && bt.t < 100) {
+ Bullet *b = bullet_new(SPR_ORB, P(bt.x1 + 4), P(board_cy()), -P(4), 0);
+ if (b) b->color = rnd_n(2) ? BC_ORANGE : BC_BLUE, b->flags |= BF_BOX | BF_NOCLIP, b->w = 3,
+ b->h = (int16_t)(bt.y1 - bt.y0), b->dmg = 3, b->life = 60;
+ }
+ if (bt.t > 170) bt.result = BR_SCRIPTED;
+}
+void lasers_draw(void) {
+ gfx_rect(0, 0, SCREEN_W, SCREEN_H, C_BLACK, 255);
+ draw_box();
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ const Bullet *b = &bullets[i];
+ if (b->kind == BK_NONE || !(b->flags & BF_BOX)) continue;
+ int x = b->x / FX;
+ if (x < bt.x0 + 2 || x > bt.x1 - 2) continue;
+ gfx_rect(x - 1, bt.y0 + 2, 3, bt.y1 - bt.y0 - 4, b->color == BC_ORANGE ? rgb565(0xFCA600) : rgb565(0x14A9FF), 255);
+ }
+ draw_soul();
+ battle_draw_hud();
+}
diff --git a/games/undertale/src/icon.png b/games/undertale/src/icon.png
new file mode 100644
index 00000000..608fe4b0
Binary files /dev/null and b/games/undertale/src/icon.png differ
diff --git a/games/undertale/src/items.c b/games/undertale/src/items.c
new file mode 100644
index 00000000..c3df682a
--- /dev/null
+++ b/games/undertale/src/items.c
@@ -0,0 +1,164 @@
+/* Items: what they are and what using one does (items.h). Their texts are the game's. */
+#include
+#include
+#include "game.h"
+#include "items.h"
+
+static const ItemInfo items[ITEM_COUNT] = {
+ [ITEM_NONE] = {"", "", IT_SPECIAL, 0, 0, 0, "", "", 0},
+ [ITEM_MONSTER_CANDY] = {"Monster Candy", "MnstrCndy", IT_FOOD, 10, 0, 0, "Has a distinct,\n non-licorice flavor.",
+ "* You ate the Monster Candy.\n* Very un-licorice-like.", 25},
+ [ITEM_STICK] = {"Stick", "Stick", IT_WEAPON, 0, 0, 0, "Its bark is worse than\n its bite.",
+ "* You threw the stick away.\n* Then picked it back up.", 150},
+ [ITEM_BANDAGE] = {"Bandage", "Bandage", IT_ARMOR, 10, 0, 0, "It has already been used\n several times.",
+ "* You re-applied the bandage.\n* Still kind of gooey.", 150},
+ [ITEM_SPIDER_DONUT] = {"Spider Donut", "SpidrDont", IT_FOOD, 12, 0, 0, "A donut made with Spider\n Cider in the batter.",
+ "* You ate the Spider Donut.", 30},
+ [ITEM_SPIDER_CIDER] = {"Spider Cider", "SpidrCidr", IT_FOOD, 24, 0, 0, "Made with whole spiders,\n not just the juice.",
+ "* You drank the Spider Cider.", 60},
+ [ITEM_BUTTERSCOTCH_PIE] = {"Butterscotch Pie", "ButtsPie", IT_FOOD, 999, 0, 0, "Butterscotch-cinnamon\n pie, one slice.",
+ "* You ate the Butterscotch\n Pie.", 180},
+ [ITEM_SNAIL_PIE] = {"Snail Pie", "SnailPie", IT_FOOD, 998, 0, 0, "An acquired taste.",
+ "* You ate the Snail Pie.\n* Your HP was maxed. Almost.", 350},
+ [ITEM_FADED_RIBBON] = {"Faded Ribbon", "Ribbon", IT_ARMOR, 0, 0, 3, "If you're cuter, monsters\n won't hit you as hard.", "", 0},
+ [ITEM_TOY_KNIFE] = {"Toy Knife", "Toy Knife", IT_WEAPON, 0, 3, 0, "Made of plastic.\n* A rarity nowadays.", "", 100},
+ [ITEM_TOUGH_GLOVE] = {"Tough Glove", "TuffGlove", IT_WEAPON, 0, 5, 0, "A worn pink leather glove.\n* For five-fingered folk.", "", 50},
+ [ITEM_MANLY_BANDANNA] = {"Manly Bandanna", "Mandanna", IT_ARMOR, 0, 0, 7, "It has seen some wear.\n* It has abs drawn on it.", "", 50},
+ [ITEM_SNOWMAN_PIECE] = {"Snowman Piece", "SnowPiece", IT_FOOD, 45, 0, 0, "Please take this to the\n ends of the earth.",
+ "* You ate the Snowman Piece.", 40},
+ [ITEM_NICE_CREAM] = {"Nice Cream", "NiceCream", IT_FOOD, 15, 0, 0, "Instead of a joke, the\n wrapper says something nice.",
+ "* You unwrapped the Nice Cream.", 2},
+ [ITEM_BISICLE] = {"Bisicle", "Bisicle", IT_FOOD, 11, 0, 0, "It's a two-pronged popsicle,\n so you can eat it twice.",
+ "* You eat one half of the\n Bisicle.", 5},
+ [ITEM_UNISICLE] = {"Unisicle", "Unisicle", IT_FOOD, 11, 0, 0, "It's a SINGLE-pronged popsicle.\n* Wait, that's just normal...",
+ "* You ate the Unisicle.", 2},
+ [ITEM_CINNAMON_BUNNY] = {"Cinnamon Bunny", "CinnaBun", IT_FOOD, 22, 0, 0, "A cinnamon roll in the\n shape of a bunny.",
+ "* You eat the Cinnamon Bunny.", 8},
+ [ITEM_OLD_TUTU] = {"Old Tutu", "Old Tutu", IT_ARMOR, 0, 0, 10, "A dusty frilly skirt.\n* Still holds up well.", "", 80},
+ [ITEM_BALLET_SHOES] = {"Ballet Shoes", "BallShoes", IT_WEAPON, 0, 7, 0, "Worn dancing shoes.\n* Too big for you.", "", 80},
+ [ITEM_QUICHE] = {"Abandoned Quiche", "Quiche", IT_FOOD, 34, 0, 0, "A quiche someone left\n under a bench.",
+ "* You eat the quiche.", 76},
+ [ITEM_PUNCH_CARD] = {"Punch Card", "PunchCard", IT_SPECIAL, 0, 0, 0, "A card for a game nobody\n plays anymore.",
+ "* You look at the punch card.\n* It makes you want to punch\n something.", 15},
+ [ITEM_ARTIFACT] = {"Legendary Artifact", "Artifact", IT_SPECIAL, 0, 0, 0, "A mysterious orb.\n* It's a little heavy.",
+ "* You hold the orb up.\n* Nothing happens.", 0},
+ [ITEM_ASTRONAUT_FOOD] = {"Astronaut Food", "AstroFood", IT_FOOD, 21, 0, 0, "Food for a trip to space,\n found in a cooler.",
+ "* You eat the Astronaut Food.", 25},
+ [ITEM_CRAB_APPLE] = {"Crab Apple", "CrabApple", IT_FOOD, 18, 0, 0, "A fruit with little claws.",
+ "* You eat the Crab Apple.", 5},
+ [ITEM_SEA_TEA] = {"Sea Tea", "SeaTea", IT_FOOD, 10, 0, 0, "Tea made from sea grass.",
+ "* You drink the Sea Tea.\n* Your feet feel lighter.", 5},
+ [ITEM_CLOUDY_GLASSES] = {"Cloudy Glasses", "Glasses", IT_ARMOR, 0, 0, 5, "Glasses you can barely\n see through.", "", 30},
+ [ITEM_TORN_NOTEBOOK] = {"Torn Notebook", "TornNotbo", IT_WEAPON, 0, 2, 0, "An old notebook, torn up.\n* It doesn't hit very hard.", "", 50},
+ [ITEM_TEMMIE_FLAKES] = {"Temmie Flakes", "TemFlakes", IT_FOOD, 2, 0, 0, "Bits of paper, sold as\n food.",
+ "* You eat the Temmie Flakes.", 2},
+ [ITEM_BURNT_PAN] = {"Burnt Pan", "BurntPan", IT_WEAPON, 0, 10, 0,
+ "A pan, burnt black. Hits\n hard, every time.\n* Food heals 4 HP more with it.", "", 50},
+ [ITEM_INSTANT_NOODLES] = {"Instant Noodles", "InstaNood", IT_FOOD, 15, 0, 0, "A cup of noodles, quick to\n make.",
+ "* You eat the Instant Noodles.\n* They're a little crunchy.", 2},
+ [ITEM_HOT_DOG] = {"Hot Dog...?", "HotDog", IT_FOOD, 20, 0, 0, "A hot dog from Sans's stand.\n* Better not ask what's in it.",
+ "* You eat the Hot Dog.\n* (Bark!)", 10},
+ [ITEM_STAINED_APRON] = {"Stained Apron", "StainApro", IT_ARMOR, 0, 0, 11, "An apron with old stains.\n* Heals 1 HP every other turn.",
+ "", 50},
+ [ITEM_STARFAIT] = {"Starfait", "Starfait", IT_FOOD, 14, 0, 0, "A tall, sparkly dessert.\n* Very popular.",
+ "* You eat the Starfait.\n* It's sweet and fizzy.", 5},
+ [ITEM_GLAMBURGER] = {"Glamburger", "GlamBurg", IT_FOOD, 27, 0, 0, "A burger that glitters all\n over. Very popular.",
+ "* You eat the Glamburger.\n* It crunches. It's the glitter.", 15},
+ [ITEM_LEGENDARY_HERO] = {"Legendary Hero", "L. Hero", IT_FOOD, 40, 0, 0, "A sandwich shaped like a\n sword.",
+ "* You eat the Legendary Hero.\n* You feel brave!", 40},
+ [ITEM_STEAK] = {"Face Steak", "FaceSteak", IT_FOOD, 60, 0, 0, "A steak in the shape of\n Mettaton's face. Don't ask.",
+ "* You eat the steak.\n* ...It looks back at you.", 20},
+ [ITEM_JUNK_FOOD] = {"Junk Food", "JunkFood", IT_FOOD, 17, 0, 0, "Food someone threw away,\n probably.",
+ "* You eat the Junk Food.\n* Someone took a bite first.", 1},
+ [ITEM_EMPTY_GUN] = {"Empty Gun", "EmptyGun", IT_WEAPON, 0, 12, 0, "An old revolver with no\n bullets in it.", "", 100},
+ [ITEM_COWBOY_HAT] = {"Cowboy Hat", "CowboyHat", IT_ARMOR, 0, 5, 12, "A worn hat. Makes you want\n a beard. (+5 AT)", "", 50},
+ [ITEM_MYSTERY_KEY] = {"Mystery Key", "MystryKey", IT_SPECIAL, 0, 0, 0, "A key, too bent to go on\n your keychain.", "", 500},
+ [ITEM_HUSH_PUPPY] = {"Hush Puppy", "HushPupy", IT_FOOD, 65, 0, 0, "A spell that keeps a dog\n from casting magic.",
+ "* You eat the Hush Puppy.\n* Dog-magic is neutralized.", 150},
+ [ITEM_WORN_DAGGER] = {"Worn Dagger", "WornDG", IT_WEAPON, 0, 15, 0, "Good for cutting plants and\n vines.", "", 250},
+ [ITEM_HEART_LOCKET] = {"Heart Locket", "H.Locket", IT_ARMOR, 0, 0, 15, "It says \"Best Friends\n Forever.\"", "", 250},
+};
+
+const ItemInfo *item_info(int id) { return &items[id >= 0 && id < ITEM_COUNT ? id : 0]; }
+
+void item_info_text(int id, char *out, int cap) {
+ const ItemInfo *it = item_info(id);
+ switch (it->type) {
+ case IT_WEAPON: snprintf(out, (size_t)cap, "* \"%s\" - Weapon AT %d\n* %s", it->name, it->at, it->info); break;
+ case IT_ARMOR:
+ if (id == ITEM_BANDAGE) snprintf(out, (size_t)cap, "* \"%s\" - Heals %d HP\n* %s", it->name, it->heal, it->info);
+ else snprintf(out, (size_t)cap, "* \"%s\" - Armor DF %d\n* %s", it->name, it->df, it->info);
+ break;
+ case IT_SPECIAL: snprintf(out, (size_t)cap, "* \"%s\"\n* %s", it->name, it->info); break;
+ default:
+ if (it->heal >= 998) snprintf(out, (size_t)cap, "* \"%s\" - Heals ALL HP\n* %s", it->name, it->info);
+ else snprintf(out, (size_t)cap, "* \"%s\" - Heals %d HP\n* %s", it->name, it->heal, it->info);
+ break;
+ }
+}
+
+/* heals and says so: "You recovered N HP!" or "Your HP was maxed out." */
+static int heal(int n, char *out, int cap) {
+ int before = sv.hp;
+ sv.hp = (int16_t)min_i(sv.maxhp, sv.hp + n);
+ if (sv.hp >= sv.maxhp) return snprintf(out, (size_t)cap, "\n* Your HP was maxed out.");
+ return snprintf(out, (size_t)cap, "\n* You recovered %d HP!", sv.hp - before);
+}
+
+void item_use(int slot, char *out, int cap, int in_battle) {
+ int id = sv.items[slot];
+ const ItemInfo *it = item_info(id);
+ out[0] = 0;
+ switch (it->type) {
+ case IT_WEAPON: {
+ if (id == ITEM_STICK) {
+ snprintf(out, (size_t)cap, "%s", it->use);
+ return;
+ }
+ int old = sv.weapon;
+ sv.weapon = (uint8_t)id;
+ sv.items[slot] = (uint8_t)(old == ITEM_STICK ? 0 : old);
+ if (!sv.items[slot]) item_remove_at(slot);
+ snprintf(out, (size_t)cap, "* You equipped the %s.", it->name);
+ return;
+ }
+ case IT_ARMOR:
+ if (id == ITEM_BANDAGE && sv.armor != ITEM_BANDAGE) {
+ int n = snprintf(out, (size_t)cap, "%s", it->use);
+ item_remove_at(slot);
+ heal(it->heal, out + n, cap - n);
+ return;
+ } else {
+ int old = sv.armor;
+ sv.armor = (uint8_t)id;
+ sv.items[slot] = (uint8_t)old;
+ if (!old) item_remove_at(slot);
+ snprintf(out, (size_t)cap, "* You equipped the %s.", it->name);
+ return;
+ }
+ case IT_SPECIAL: /* looked at, kept */
+ snprintf(out, (size_t)cap, "%s", it->use);
+ return;
+ default: {
+ int n = snprintf(out, (size_t)cap, "%s", it->use);
+ if (id == ITEM_NICE_CREAM) { /* its wrapper's kind words */
+ static const char *const nice[] = {"You're just great!", "You look nice today!", "Are those claws natural?",
+ "You're super spiffy!", "Have a wonderful day!", "Is this as sweet as you?"};
+ n += snprintf(out + n, (size_t)(cap - n), "\n* \"%s\"", nice[rnd_n(6)]);
+ }
+ /* half a Bisicle is a Unisicle */
+ if (id == ITEM_BISICLE) sv.items[slot] = ITEM_UNISICLE;
+ else item_remove_at(slot);
+ if (it->heal >= 999) {
+ sv.hp = sv.maxhp;
+ snprintf(out + n, (size_t)(cap - n), "\n* Your HP was maxed out.");
+ } else if (it->heal == 998) {
+ if (sv.hp < sv.maxhp - 1) sv.hp = (int16_t)(sv.maxhp - 1);
+ } else {
+ heal(it->heal + (sv.weapon == ITEM_BURNT_PAN ? 4 : 0), out + n, cap - n);
+ }
+ (void)in_battle;
+ return;
+ }
+ }
+}
diff --git a/games/undertale/src/items.h b/games/undertale/src/items.h
new file mode 100644
index 00000000..bbf04478
--- /dev/null
+++ b/games/undertale/src/items.h
@@ -0,0 +1,73 @@
+/* Undertale: items (the game's ids are kept where it has them, for the record). */
+#ifndef UT_ITEMS_H
+#define UT_ITEMS_H
+#include
+
+enum {
+ ITEM_NONE,
+ ITEM_MONSTER_CANDY,
+ ITEM_STICK,
+ ITEM_BANDAGE,
+ ITEM_SPIDER_DONUT,
+ ITEM_SPIDER_CIDER,
+ ITEM_BUTTERSCOTCH_PIE,
+ ITEM_SNAIL_PIE,
+ ITEM_FADED_RIBBON,
+ ITEM_TOY_KNIFE,
+ /* Snowdin */
+ ITEM_TOUGH_GLOVE,
+ ITEM_MANLY_BANDANNA,
+ ITEM_SNOWMAN_PIECE,
+ ITEM_NICE_CREAM,
+ ITEM_BISICLE,
+ ITEM_UNISICLE,
+ ITEM_CINNAMON_BUNNY,
+ /* Waterfall */
+ ITEM_OLD_TUTU,
+ ITEM_BALLET_SHOES,
+ ITEM_QUICHE,
+ ITEM_PUNCH_CARD,
+ ITEM_ARTIFACT,
+ ITEM_ASTRONAUT_FOOD,
+ ITEM_CRAB_APPLE,
+ ITEM_SEA_TEA,
+ ITEM_CLOUDY_GLASSES,
+ ITEM_TORN_NOTEBOOK,
+ ITEM_TEMMIE_FLAKES,
+ /* Hotland */
+ ITEM_BURNT_PAN,
+ ITEM_INSTANT_NOODLES,
+ ITEM_HOT_DOG,
+ ITEM_STAINED_APRON,
+ /* the MTT Resort */
+ ITEM_STARFAIT,
+ ITEM_GLAMBURGER,
+ ITEM_LEGENDARY_HERO,
+ ITEM_STEAK,
+ ITEM_JUNK_FOOD,
+ ITEM_EMPTY_GUN,
+ ITEM_COWBOY_HAT,
+ ITEM_MYSTERY_KEY,
+ ITEM_HUSH_PUPPY,
+ /* New Home */
+ ITEM_WORN_DAGGER,
+ ITEM_HEART_LOCKET,
+ ITEM_COUNT
+};
+enum { IT_FOOD, IT_WEAPON, IT_ARMOR, IT_SPECIAL };
+
+typedef struct {
+ const char *name; /* in the menu */
+ const char *short_name; /* in battle, 9 letters at most */
+ uint8_t type;
+ int16_t heal; /* HP, 999 all of it */
+ uint8_t at, df;
+ const char *info; /* what Info says after its name */
+ const char *use; /* what eating it says (before the HP) */
+ uint16_t sell;
+} ItemInfo;
+const ItemInfo *item_info(int id);
+/* uses the item in slot (the inventory's): fills `out` with what to say; in battle or not */
+void item_use(int slot, char *out, int cap, int in_battle);
+void item_info_text(int id, char *out, int cap);
+#endif
diff --git a/games/undertale/src/lzma/7zTypes.h b/games/undertale/src/lzma/7zTypes.h
new file mode 100644
index 00000000..4977cdaa
--- /dev/null
+++ b/games/undertale/src/lzma/7zTypes.h
@@ -0,0 +1,374 @@
+/* 7zTypes.h -- Basic types
+2017-07-17 : Igor Pavlov : Public domain */
+
+#ifndef __7Z_TYPES_H
+#define __7Z_TYPES_H
+
+#ifdef _WIN32
+/* #include */
+#endif
+
+#include
+
+#ifndef EXTERN_C_BEGIN
+#ifdef __cplusplus
+#define EXTERN_C_BEGIN extern "C" {
+#define EXTERN_C_END }
+#else
+#define EXTERN_C_BEGIN
+#define EXTERN_C_END
+#endif
+#endif
+
+EXTERN_C_BEGIN
+
+#define SZ_OK 0
+
+#define SZ_ERROR_DATA 1
+#define SZ_ERROR_MEM 2
+#define SZ_ERROR_CRC 3
+#define SZ_ERROR_UNSUPPORTED 4
+#define SZ_ERROR_PARAM 5
+#define SZ_ERROR_INPUT_EOF 6
+#define SZ_ERROR_OUTPUT_EOF 7
+#define SZ_ERROR_READ 8
+#define SZ_ERROR_WRITE 9
+#define SZ_ERROR_PROGRESS 10
+#define SZ_ERROR_FAIL 11
+#define SZ_ERROR_THREAD 12
+
+#define SZ_ERROR_ARCHIVE 16
+#define SZ_ERROR_NO_ARCHIVE 17
+
+typedef int SRes;
+
+
+#ifdef _WIN32
+
+/* typedef DWORD WRes; */
+typedef unsigned WRes;
+#define MY_SRes_HRESULT_FROM_WRes(x) HRESULT_FROM_WIN32(x)
+
+#else
+
+typedef int WRes;
+#define MY__FACILITY_WIN32 7
+#define MY__FACILITY__WRes MY__FACILITY_WIN32
+#define MY_SRes_HRESULT_FROM_WRes(x) ((HRESULT)(x) <= 0 ? ((HRESULT)(x)) : ((HRESULT) (((x) & 0x0000FFFF) | (MY__FACILITY__WRes << 16) | 0x80000000)))
+
+#endif
+
+
+#ifndef RINOK
+#define RINOK(x) { int __result__ = (x); if (__result__ != 0) return __result__; }
+#endif
+
+typedef unsigned char Byte;
+typedef short Int16;
+typedef unsigned short UInt16;
+
+#ifdef _LZMA_UINT32_IS_ULONG
+typedef long Int32;
+typedef unsigned long UInt32;
+#else
+typedef int Int32;
+typedef unsigned int UInt32;
+#endif
+
+#ifdef _SZ_NO_INT_64
+
+/* define _SZ_NO_INT_64, if your compiler doesn't support 64-bit integers.
+ NOTES: Some code will work incorrectly in that case! */
+
+typedef long Int64;
+typedef unsigned long UInt64;
+
+#else
+
+#if defined(_MSC_VER) || defined(__BORLANDC__)
+typedef __int64 Int64;
+typedef unsigned __int64 UInt64;
+#define UINT64_CONST(n) n
+#else
+typedef long long int Int64;
+typedef unsigned long long int UInt64;
+#define UINT64_CONST(n) n ## ULL
+#endif
+
+#endif
+
+#ifdef _LZMA_NO_SYSTEM_SIZE_T
+typedef UInt32 SizeT;
+#else
+typedef size_t SizeT;
+#endif
+
+typedef int Bool;
+#define True 1
+#define False 0
+
+
+#ifdef _WIN32
+#define MY_STD_CALL __stdcall
+#else
+#define MY_STD_CALL
+#endif
+
+#ifdef _MSC_VER
+
+#if _MSC_VER >= 1300
+#define MY_NO_INLINE __declspec(noinline)
+#else
+#define MY_NO_INLINE
+#endif
+
+#define MY_FORCE_INLINE __forceinline
+
+#define MY_CDECL __cdecl
+#define MY_FAST_CALL __fastcall
+
+#else
+
+#define MY_NO_INLINE
+#define MY_FORCE_INLINE
+#define MY_CDECL
+#define MY_FAST_CALL
+
+/* inline keyword : for C++ / C99 */
+
+/* GCC, clang: */
+/*
+#if defined (__GNUC__) && (__GNUC__ >= 4)
+#define MY_FORCE_INLINE __attribute__((always_inline))
+#define MY_NO_INLINE __attribute__((noinline))
+#endif
+*/
+
+#endif
+
+
+/* The following interfaces use first parameter as pointer to structure */
+
+typedef struct IByteIn IByteIn;
+struct IByteIn
+{
+ Byte (*Read)(const IByteIn *p); /* reads one byte, returns 0 in case of EOF or error */
+};
+#define IByteIn_Read(p) (p)->Read(p)
+
+
+typedef struct IByteOut IByteOut;
+struct IByteOut
+{
+ void (*Write)(const IByteOut *p, Byte b);
+};
+#define IByteOut_Write(p, b) (p)->Write(p, b)
+
+
+typedef struct ISeqInStream ISeqInStream;
+struct ISeqInStream
+{
+ SRes (*Read)(const ISeqInStream *p, void *buf, size_t *size);
+ /* if (input(*size) != 0 && output(*size) == 0) means end_of_stream.
+ (output(*size) < input(*size)) is allowed */
+};
+#define ISeqInStream_Read(p, buf, size) (p)->Read(p, buf, size)
+
+/* it can return SZ_ERROR_INPUT_EOF */
+SRes SeqInStream_Read(const ISeqInStream *stream, void *buf, size_t size);
+SRes SeqInStream_Read2(const ISeqInStream *stream, void *buf, size_t size, SRes errorType);
+SRes SeqInStream_ReadByte(const ISeqInStream *stream, Byte *buf);
+
+
+typedef struct ISeqOutStream ISeqOutStream;
+struct ISeqOutStream
+{
+ size_t (*Write)(const ISeqOutStream *p, const void *buf, size_t size);
+ /* Returns: result - the number of actually written bytes.
+ (result < size) means error */
+};
+#define ISeqOutStream_Write(p, buf, size) (p)->Write(p, buf, size)
+
+typedef enum
+{
+ SZ_SEEK_SET = 0,
+ SZ_SEEK_CUR = 1,
+ SZ_SEEK_END = 2
+} ESzSeek;
+
+
+typedef struct ISeekInStream ISeekInStream;
+struct ISeekInStream
+{
+ SRes (*Read)(const ISeekInStream *p, void *buf, size_t *size); /* same as ISeqInStream::Read */
+ SRes (*Seek)(const ISeekInStream *p, Int64 *pos, ESzSeek origin);
+};
+#define ISeekInStream_Read(p, buf, size) (p)->Read(p, buf, size)
+#define ISeekInStream_Seek(p, pos, origin) (p)->Seek(p, pos, origin)
+
+
+typedef struct ILookInStream ILookInStream;
+struct ILookInStream
+{
+ SRes (*Look)(const ILookInStream *p, const void **buf, size_t *size);
+ /* if (input(*size) != 0 && output(*size) == 0) means end_of_stream.
+ (output(*size) > input(*size)) is not allowed
+ (output(*size) < input(*size)) is allowed */
+ SRes (*Skip)(const ILookInStream *p, size_t offset);
+ /* offset must be <= output(*size) of Look */
+
+ SRes (*Read)(const ILookInStream *p, void *buf, size_t *size);
+ /* reads directly (without buffer). It's same as ISeqInStream::Read */
+ SRes (*Seek)(const ILookInStream *p, Int64 *pos, ESzSeek origin);
+};
+
+#define ILookInStream_Look(p, buf, size) (p)->Look(p, buf, size)
+#define ILookInStream_Skip(p, offset) (p)->Skip(p, offset)
+#define ILookInStream_Read(p, buf, size) (p)->Read(p, buf, size)
+#define ILookInStream_Seek(p, pos, origin) (p)->Seek(p, pos, origin)
+
+
+SRes LookInStream_LookRead(const ILookInStream *stream, void *buf, size_t *size);
+SRes LookInStream_SeekTo(const ILookInStream *stream, UInt64 offset);
+
+/* reads via ILookInStream::Read */
+SRes LookInStream_Read2(const ILookInStream *stream, void *buf, size_t size, SRes errorType);
+SRes LookInStream_Read(const ILookInStream *stream, void *buf, size_t size);
+
+
+
+typedef struct
+{
+ ILookInStream vt;
+ const ISeekInStream *realStream;
+
+ size_t pos;
+ size_t size; /* it's data size */
+
+ /* the following variables must be set outside */
+ Byte *buf;
+ size_t bufSize;
+} CLookToRead2;
+
+void LookToRead2_CreateVTable(CLookToRead2 *p, int lookahead);
+
+#define LookToRead2_Init(p) { (p)->pos = (p)->size = 0; }
+
+
+typedef struct
+{
+ ISeqInStream vt;
+ const ILookInStream *realStream;
+} CSecToLook;
+
+void SecToLook_CreateVTable(CSecToLook *p);
+
+
+
+typedef struct
+{
+ ISeqInStream vt;
+ const ILookInStream *realStream;
+} CSecToRead;
+
+void SecToRead_CreateVTable(CSecToRead *p);
+
+
+typedef struct ICompressProgress ICompressProgress;
+
+struct ICompressProgress
+{
+ SRes (*Progress)(const ICompressProgress *p, UInt64 inSize, UInt64 outSize);
+ /* Returns: result. (result != SZ_OK) means break.
+ Value (UInt64)(Int64)-1 for size means unknown value. */
+};
+#define ICompressProgress_Progress(p, inSize, outSize) (p)->Progress(p, inSize, outSize)
+
+
+
+typedef struct ISzAlloc ISzAlloc;
+typedef const ISzAlloc * ISzAllocPtr;
+
+struct ISzAlloc
+{
+ void *(*Alloc)(ISzAllocPtr p, size_t size);
+ void (*Free)(ISzAllocPtr p, void *address); /* address can be 0 */
+};
+
+#define ISzAlloc_Alloc(p, size) (p)->Alloc(p, size)
+#define ISzAlloc_Free(p, a) (p)->Free(p, a)
+
+/* deprecated */
+#define IAlloc_Alloc(p, size) ISzAlloc_Alloc(p, size)
+#define IAlloc_Free(p, a) ISzAlloc_Free(p, a)
+
+
+
+
+
+#ifndef MY_offsetof
+ #ifdef offsetof
+ #define MY_offsetof(type, m) offsetof(type, m)
+ /*
+ #define MY_offsetof(type, m) FIELD_OFFSET(type, m)
+ */
+ #else
+ #define MY_offsetof(type, m) ((size_t)&(((type *)0)->m))
+ #endif
+#endif
+
+
+
+#ifndef MY_container_of
+
+/*
+#define MY_container_of(ptr, type, m) container_of(ptr, type, m)
+#define MY_container_of(ptr, type, m) CONTAINING_RECORD(ptr, type, m)
+#define MY_container_of(ptr, type, m) ((type *)((char *)(ptr) - offsetof(type, m)))
+#define MY_container_of(ptr, type, m) (&((type *)0)->m == (ptr), ((type *)(((char *)(ptr)) - MY_offsetof(type, m))))
+*/
+
+/*
+ GCC shows warning: "perhaps the 'offsetof' macro was used incorrectly"
+ GCC 3.4.4 : classes with constructor
+ GCC 4.8.1 : classes with non-public variable members"
+*/
+
+#define MY_container_of(ptr, type, m) ((type *)((char *)(1 ? (ptr) : &((type *)0)->m) - MY_offsetof(type, m)))
+
+
+#endif
+
+#define CONTAINER_FROM_VTBL_SIMPLE(ptr, type, m) ((type *)(ptr))
+
+/*
+#define CONTAINER_FROM_VTBL(ptr, type, m) CONTAINER_FROM_VTBL_SIMPLE(ptr, type, m)
+*/
+#define CONTAINER_FROM_VTBL(ptr, type, m) MY_container_of(ptr, type, m)
+
+#define CONTAINER_FROM_VTBL_CLS(ptr, type, m) CONTAINER_FROM_VTBL_SIMPLE(ptr, type, m)
+/*
+#define CONTAINER_FROM_VTBL_CLS(ptr, type, m) CONTAINER_FROM_VTBL(ptr, type, m)
+*/
+
+
+
+#ifdef _WIN32
+
+#define CHAR_PATH_SEPARATOR '\\'
+#define WCHAR_PATH_SEPARATOR L'\\'
+#define STRING_PATH_SEPARATOR "\\"
+#define WSTRING_PATH_SEPARATOR L"\\"
+
+#else
+
+#define CHAR_PATH_SEPARATOR '/'
+#define WCHAR_PATH_SEPARATOR L'/'
+#define STRING_PATH_SEPARATOR "/"
+#define WSTRING_PATH_SEPARATOR L"/"
+
+#endif
+
+EXTERN_C_END
+
+#endif
diff --git a/games/undertale/src/lzma/LzmaDec.c b/games/undertale/src/lzma/LzmaDec.c
new file mode 100644
index 00000000..962b94bb
--- /dev/null
+++ b/games/undertale/src/lzma/LzmaDec.c
@@ -0,0 +1,1185 @@
+/* LzmaDec.c -- LZMA Decoder
+2018-02-28 : Igor Pavlov : Public domain */
+
+#include "Precomp.h"
+
+/* #include "CpuArch.h" */
+#include "LzmaDec.h"
+
+#include
+
+#define kNumTopBits 24
+#define kTopValue ((UInt32)1 << kNumTopBits)
+
+#define kNumBitModelTotalBits 11
+#define kBitModelTotal (1 << kNumBitModelTotalBits)
+#define kNumMoveBits 5
+
+#define RC_INIT_SIZE 5
+
+#define NORMALIZE if (range < kTopValue) { range <<= 8; code = (code << 8) | (*buf++); }
+
+#define IF_BIT_0(p) ttt = *(p); NORMALIZE; bound = (range >> kNumBitModelTotalBits) * ttt; if (code < bound)
+#define UPDATE_0(p) range = bound; *(p) = (CLzmaProb)(ttt + ((kBitModelTotal - ttt) >> kNumMoveBits));
+#define UPDATE_1(p) range -= bound; code -= bound; *(p) = (CLzmaProb)(ttt - (ttt >> kNumMoveBits));
+#define GET_BIT2(p, i, A0, A1) IF_BIT_0(p) \
+ { UPDATE_0(p); i = (i + i); A0; } else \
+ { UPDATE_1(p); i = (i + i) + 1; A1; }
+
+#define TREE_GET_BIT(probs, i) { GET_BIT2(probs + i, i, ;, ;); }
+
+#define REV_BIT(p, i, A0, A1) IF_BIT_0(p + i) \
+ { UPDATE_0(p + i); A0; } else \
+ { UPDATE_1(p + i); A1; }
+#define REV_BIT_VAR( p, i, m) REV_BIT(p, i, i += m; m += m, m += m; i += m; )
+#define REV_BIT_CONST(p, i, m) REV_BIT(p, i, i += m; , i += m * 2; )
+#define REV_BIT_LAST( p, i, m) REV_BIT(p, i, i -= m , ; )
+
+#define TREE_DECODE(probs, limit, i) \
+ { i = 1; do { TREE_GET_BIT(probs, i); } while (i < limit); i -= limit; }
+
+/* #define _LZMA_SIZE_OPT */
+
+#ifdef _LZMA_SIZE_OPT
+#define TREE_6_DECODE(probs, i) TREE_DECODE(probs, (1 << 6), i)
+#else
+#define TREE_6_DECODE(probs, i) \
+ { i = 1; \
+ TREE_GET_BIT(probs, i); \
+ TREE_GET_BIT(probs, i); \
+ TREE_GET_BIT(probs, i); \
+ TREE_GET_BIT(probs, i); \
+ TREE_GET_BIT(probs, i); \
+ TREE_GET_BIT(probs, i); \
+ i -= 0x40; }
+#endif
+
+#define NORMAL_LITER_DEC TREE_GET_BIT(prob, symbol)
+#define MATCHED_LITER_DEC \
+ matchByte += matchByte; \
+ bit = offs; \
+ offs &= matchByte; \
+ probLit = prob + (offs + bit + symbol); \
+ GET_BIT2(probLit, symbol, offs ^= bit; , ;)
+
+
+
+#define NORMALIZE_CHECK if (range < kTopValue) { if (buf >= bufLimit) return DUMMY_ERROR; range <<= 8; code = (code << 8) | (*buf++); }
+
+#define IF_BIT_0_CHECK(p) ttt = *(p); NORMALIZE_CHECK; bound = (range >> kNumBitModelTotalBits) * ttt; if (code < bound)
+#define UPDATE_0_CHECK range = bound;
+#define UPDATE_1_CHECK range -= bound; code -= bound;
+#define GET_BIT2_CHECK(p, i, A0, A1) IF_BIT_0_CHECK(p) \
+ { UPDATE_0_CHECK; i = (i + i); A0; } else \
+ { UPDATE_1_CHECK; i = (i + i) + 1; A1; }
+#define GET_BIT_CHECK(p, i) GET_BIT2_CHECK(p, i, ; , ;)
+#define TREE_DECODE_CHECK(probs, limit, i) \
+ { i = 1; do { GET_BIT_CHECK(probs + i, i) } while (i < limit); i -= limit; }
+
+
+#define REV_BIT_CHECK(p, i, m) IF_BIT_0_CHECK(p + i) \
+ { UPDATE_0_CHECK; i += m; m += m; } else \
+ { UPDATE_1_CHECK; m += m; i += m; }
+
+
+#define kNumPosBitsMax 4
+#define kNumPosStatesMax (1 << kNumPosBitsMax)
+
+#define kLenNumLowBits 3
+#define kLenNumLowSymbols (1 << kLenNumLowBits)
+#define kLenNumHighBits 8
+#define kLenNumHighSymbols (1 << kLenNumHighBits)
+
+#define LenLow 0
+#define LenHigh (LenLow + 2 * (kNumPosStatesMax << kLenNumLowBits))
+#define kNumLenProbs (LenHigh + kLenNumHighSymbols)
+
+#define LenChoice LenLow
+#define LenChoice2 (LenLow + (1 << kLenNumLowBits))
+
+#define kNumStates 12
+#define kNumStates2 16
+#define kNumLitStates 7
+
+#define kStartPosModelIndex 4
+#define kEndPosModelIndex 14
+#define kNumFullDistances (1 << (kEndPosModelIndex >> 1))
+
+#define kNumPosSlotBits 6
+#define kNumLenToPosStates 4
+
+#define kNumAlignBits 4
+#define kAlignTableSize (1 << kNumAlignBits)
+
+#define kMatchMinLen 2
+#define kMatchSpecLenStart (kMatchMinLen + kLenNumLowSymbols * 2 + kLenNumHighSymbols)
+
+/* External ASM code needs same CLzmaProb array layout. So don't change it. */
+
+/* (probs_1664) is faster and better for code size at some platforms */
+/*
+#ifdef MY_CPU_X86_OR_AMD64
+*/
+#define kStartOffset 1664
+#define GET_PROBS p->probs_1664
+/*
+#define GET_PROBS p->probs + kStartOffset
+#else
+#define kStartOffset 0
+#define GET_PROBS p->probs
+#endif
+*/
+
+#define SpecPos (-kStartOffset)
+#define IsRep0Long (SpecPos + kNumFullDistances)
+#define RepLenCoder (IsRep0Long + (kNumStates2 << kNumPosBitsMax))
+#define LenCoder (RepLenCoder + kNumLenProbs)
+#define IsMatch (LenCoder + kNumLenProbs)
+#define Align (IsMatch + (kNumStates2 << kNumPosBitsMax))
+#define IsRep (Align + kAlignTableSize)
+#define IsRepG0 (IsRep + kNumStates)
+#define IsRepG1 (IsRepG0 + kNumStates)
+#define IsRepG2 (IsRepG1 + kNumStates)
+#define PosSlot (IsRepG2 + kNumStates)
+#define Literal (PosSlot + (kNumLenToPosStates << kNumPosSlotBits))
+#define NUM_BASE_PROBS (Literal + kStartOffset)
+
+#if Align != 0 && kStartOffset != 0
+ #error Stop_Compiling_Bad_LZMA_kAlign
+#endif
+
+#if NUM_BASE_PROBS != 1984
+ #error Stop_Compiling_Bad_LZMA_PROBS
+#endif
+
+
+#define LZMA_LIT_SIZE 0x300
+
+#define LzmaProps_GetNumProbs(p) (NUM_BASE_PROBS + ((UInt32)LZMA_LIT_SIZE << ((p)->lc + (p)->lp)))
+
+
+#define CALC_POS_STATE(processedPos, pbMask) (((processedPos) & (pbMask)) << 4)
+#define COMBINED_PS_STATE (posState + state)
+#define GET_LEN_STATE (posState)
+
+#define LZMA_DIC_MIN (1 << 12)
+
+/*
+p->remainLen : shows status of LZMA decoder:
+ < kMatchSpecLenStart : normal remain
+ = kMatchSpecLenStart : finished
+ = kMatchSpecLenStart + 1 : need init range coder
+ = kMatchSpecLenStart + 2 : need init range coder and state
+*/
+
+/* ---------- LZMA_DECODE_REAL ---------- */
+/*
+LzmaDec_DecodeReal_3() can be implemented in external ASM file.
+3 - is the code compatibility version of that function for check at link time.
+*/
+
+#define LZMA_DECODE_REAL LzmaDec_DecodeReal_3
+
+/*
+LZMA_DECODE_REAL()
+In:
+ RangeCoder is normalized
+ if (p->dicPos == limit)
+ {
+ LzmaDec_TryDummy() was called before to exclude LITERAL and MATCH-REP cases.
+ So first symbol can be only MATCH-NON-REP. And if that MATCH-NON-REP symbol
+ is not END_OF_PAYALOAD_MARKER, then function returns error code.
+ }
+
+Processing:
+ first LZMA symbol will be decoded in any case
+ All checks for limits are at the end of main loop,
+ It will decode new LZMA-symbols while (p->buf < bufLimit && dicPos < limit),
+ RangeCoder is still without last normalization when (p->buf < bufLimit) is being checked.
+
+Out:
+ RangeCoder is normalized
+ Result:
+ SZ_OK - OK
+ SZ_ERROR_DATA - Error
+ p->remainLen:
+ < kMatchSpecLenStart : normal remain
+ = kMatchSpecLenStart : finished
+*/
+
+
+#ifdef _LZMA_DEC_OPT
+
+int MY_FAST_CALL LZMA_DECODE_REAL(CLzmaDec *p, SizeT limit, const Byte *bufLimit);
+
+#else
+
+static
+int MY_FAST_CALL LZMA_DECODE_REAL(CLzmaDec *p, SizeT limit, const Byte *bufLimit)
+{
+ CLzmaProb *probs = GET_PROBS;
+ unsigned state = (unsigned)p->state;
+ UInt32 rep0 = p->reps[0], rep1 = p->reps[1], rep2 = p->reps[2], rep3 = p->reps[3];
+ unsigned pbMask = ((unsigned)1 << (p->prop.pb)) - 1;
+ unsigned lc = p->prop.lc;
+ unsigned lpMask = ((unsigned)0x100 << p->prop.lp) - ((unsigned)0x100 >> lc);
+
+ Byte *dic = p->dic;
+ SizeT dicBufSize = p->dicBufSize;
+ SizeT dicPos = p->dicPos;
+
+ UInt32 processedPos = p->processedPos;
+ UInt32 checkDicSize = p->checkDicSize;
+ unsigned len = 0;
+
+ const Byte *buf = p->buf;
+ UInt32 range = p->range;
+ UInt32 code = p->code;
+
+ do
+ {
+ CLzmaProb *prob;
+ UInt32 bound;
+ unsigned ttt;
+ unsigned posState = CALC_POS_STATE(processedPos, pbMask);
+
+ prob = probs + IsMatch + COMBINED_PS_STATE;
+ IF_BIT_0(prob)
+ {
+ unsigned symbol;
+ UPDATE_0(prob);
+ prob = probs + Literal;
+ if (processedPos != 0 || checkDicSize != 0)
+ prob += (UInt32)3 * ((((processedPos << 8) + dic[(dicPos == 0 ? dicBufSize : dicPos) - 1]) & lpMask) << lc);
+ processedPos++;
+
+ if (state < kNumLitStates)
+ {
+ state -= (state < 4) ? state : 3;
+ symbol = 1;
+ #ifdef _LZMA_SIZE_OPT
+ do { NORMAL_LITER_DEC } while (symbol < 0x100);
+ #else
+ NORMAL_LITER_DEC
+ NORMAL_LITER_DEC
+ NORMAL_LITER_DEC
+ NORMAL_LITER_DEC
+ NORMAL_LITER_DEC
+ NORMAL_LITER_DEC
+ NORMAL_LITER_DEC
+ NORMAL_LITER_DEC
+ #endif
+ }
+ else
+ {
+ unsigned matchByte = dic[dicPos - rep0 + (dicPos < rep0 ? dicBufSize : 0)];
+ unsigned offs = 0x100;
+ state -= (state < 10) ? 3 : 6;
+ symbol = 1;
+ #ifdef _LZMA_SIZE_OPT
+ do
+ {
+ unsigned bit;
+ CLzmaProb *probLit;
+ MATCHED_LITER_DEC
+ }
+ while (symbol < 0x100);
+ #else
+ {
+ unsigned bit;
+ CLzmaProb *probLit;
+ MATCHED_LITER_DEC
+ MATCHED_LITER_DEC
+ MATCHED_LITER_DEC
+ MATCHED_LITER_DEC
+ MATCHED_LITER_DEC
+ MATCHED_LITER_DEC
+ MATCHED_LITER_DEC
+ MATCHED_LITER_DEC
+ }
+ #endif
+ }
+
+ dic[dicPos++] = (Byte)symbol;
+ continue;
+ }
+
+ {
+ UPDATE_1(prob);
+ prob = probs + IsRep + state;
+ IF_BIT_0(prob)
+ {
+ UPDATE_0(prob);
+ state += kNumStates;
+ prob = probs + LenCoder;
+ }
+ else
+ {
+ UPDATE_1(prob);
+ /*
+ // that case was checked before with kBadRepCode
+ if (checkDicSize == 0 && processedPos == 0)
+ return SZ_ERROR_DATA;
+ */
+ prob = probs + IsRepG0 + state;
+ IF_BIT_0(prob)
+ {
+ UPDATE_0(prob);
+ prob = probs + IsRep0Long + COMBINED_PS_STATE;
+ IF_BIT_0(prob)
+ {
+ UPDATE_0(prob);
+ dic[dicPos] = dic[dicPos - rep0 + (dicPos < rep0 ? dicBufSize : 0)];
+ dicPos++;
+ processedPos++;
+ state = state < kNumLitStates ? 9 : 11;
+ continue;
+ }
+ UPDATE_1(prob);
+ }
+ else
+ {
+ UInt32 distance;
+ UPDATE_1(prob);
+ prob = probs + IsRepG1 + state;
+ IF_BIT_0(prob)
+ {
+ UPDATE_0(prob);
+ distance = rep1;
+ }
+ else
+ {
+ UPDATE_1(prob);
+ prob = probs + IsRepG2 + state;
+ IF_BIT_0(prob)
+ {
+ UPDATE_0(prob);
+ distance = rep2;
+ }
+ else
+ {
+ UPDATE_1(prob);
+ distance = rep3;
+ rep3 = rep2;
+ }
+ rep2 = rep1;
+ }
+ rep1 = rep0;
+ rep0 = distance;
+ }
+ state = state < kNumLitStates ? 8 : 11;
+ prob = probs + RepLenCoder;
+ }
+
+ #ifdef _LZMA_SIZE_OPT
+ {
+ unsigned lim, offset;
+ CLzmaProb *probLen = prob + LenChoice;
+ IF_BIT_0(probLen)
+ {
+ UPDATE_0(probLen);
+ probLen = prob + LenLow + GET_LEN_STATE;
+ offset = 0;
+ lim = (1 << kLenNumLowBits);
+ }
+ else
+ {
+ UPDATE_1(probLen);
+ probLen = prob + LenChoice2;
+ IF_BIT_0(probLen)
+ {
+ UPDATE_0(probLen);
+ probLen = prob + LenLow + GET_LEN_STATE + (1 << kLenNumLowBits);
+ offset = kLenNumLowSymbols;
+ lim = (1 << kLenNumLowBits);
+ }
+ else
+ {
+ UPDATE_1(probLen);
+ probLen = prob + LenHigh;
+ offset = kLenNumLowSymbols * 2;
+ lim = (1 << kLenNumHighBits);
+ }
+ }
+ TREE_DECODE(probLen, lim, len);
+ len += offset;
+ }
+ #else
+ {
+ CLzmaProb *probLen = prob + LenChoice;
+ IF_BIT_0(probLen)
+ {
+ UPDATE_0(probLen);
+ probLen = prob + LenLow + GET_LEN_STATE;
+ len = 1;
+ TREE_GET_BIT(probLen, len);
+ TREE_GET_BIT(probLen, len);
+ TREE_GET_BIT(probLen, len);
+ len -= 8;
+ }
+ else
+ {
+ UPDATE_1(probLen);
+ probLen = prob + LenChoice2;
+ IF_BIT_0(probLen)
+ {
+ UPDATE_0(probLen);
+ probLen = prob + LenLow + GET_LEN_STATE + (1 << kLenNumLowBits);
+ len = 1;
+ TREE_GET_BIT(probLen, len);
+ TREE_GET_BIT(probLen, len);
+ TREE_GET_BIT(probLen, len);
+ }
+ else
+ {
+ UPDATE_1(probLen);
+ probLen = prob + LenHigh;
+ TREE_DECODE(probLen, (1 << kLenNumHighBits), len);
+ len += kLenNumLowSymbols * 2;
+ }
+ }
+ }
+ #endif
+
+ if (state >= kNumStates)
+ {
+ UInt32 distance;
+ prob = probs + PosSlot +
+ ((len < kNumLenToPosStates ? len : kNumLenToPosStates - 1) << kNumPosSlotBits);
+ TREE_6_DECODE(prob, distance);
+ if (distance >= kStartPosModelIndex)
+ {
+ unsigned posSlot = (unsigned)distance;
+ unsigned numDirectBits = (unsigned)(((distance >> 1) - 1));
+ distance = (2 | (distance & 1));
+ if (posSlot < kEndPosModelIndex)
+ {
+ distance <<= numDirectBits;
+ prob = probs + SpecPos;
+ {
+ UInt32 m = 1;
+ distance++;
+ do
+ {
+ REV_BIT_VAR(prob, distance, m);
+ }
+ while (--numDirectBits);
+ distance -= m;
+ }
+ }
+ else
+ {
+ numDirectBits -= kNumAlignBits;
+ do
+ {
+ NORMALIZE
+ range >>= 1;
+
+ {
+ UInt32 t;
+ code -= range;
+ t = (0 - ((UInt32)code >> 31)); /* (UInt32)((Int32)code >> 31) */
+ distance = (distance << 1) + (t + 1);
+ code += range & t;
+ }
+ /*
+ distance <<= 1;
+ if (code >= range)
+ {
+ code -= range;
+ distance |= 1;
+ }
+ */
+ }
+ while (--numDirectBits);
+ prob = probs + Align;
+ distance <<= kNumAlignBits;
+ {
+ unsigned i = 1;
+ REV_BIT_CONST(prob, i, 1);
+ REV_BIT_CONST(prob, i, 2);
+ REV_BIT_CONST(prob, i, 4);
+ REV_BIT_LAST (prob, i, 8);
+ distance |= i;
+ }
+ if (distance == (UInt32)0xFFFFFFFF)
+ {
+ len = kMatchSpecLenStart;
+ state -= kNumStates;
+ break;
+ }
+ }
+ }
+
+ rep3 = rep2;
+ rep2 = rep1;
+ rep1 = rep0;
+ rep0 = distance + 1;
+ state = (state < kNumStates + kNumLitStates) ? kNumLitStates : kNumLitStates + 3;
+ if (distance >= (checkDicSize == 0 ? processedPos: checkDicSize))
+ {
+ p->dicPos = dicPos;
+ return SZ_ERROR_DATA;
+ }
+ }
+
+ len += kMatchMinLen;
+
+ {
+ SizeT rem;
+ unsigned curLen;
+ SizeT pos;
+
+ if ((rem = limit - dicPos) == 0)
+ {
+ p->dicPos = dicPos;
+ return SZ_ERROR_DATA;
+ }
+
+ curLen = ((rem < len) ? (unsigned)rem : len);
+ pos = dicPos - rep0 + (dicPos < rep0 ? dicBufSize : 0);
+
+ processedPos += curLen;
+
+ len -= curLen;
+ if (curLen <= dicBufSize - pos)
+ {
+ Byte *dest = dic + dicPos;
+ ptrdiff_t src = (ptrdiff_t)pos - (ptrdiff_t)dicPos;
+ const Byte *lim = dest + curLen;
+ dicPos += curLen;
+ do
+ *(dest) = (Byte)*(dest + src);
+ while (++dest != lim);
+ }
+ else
+ {
+ do
+ {
+ dic[dicPos++] = dic[pos];
+ if (++pos == dicBufSize)
+ pos = 0;
+ }
+ while (--curLen != 0);
+ }
+ }
+ }
+ }
+ while (dicPos < limit && buf < bufLimit);
+
+ NORMALIZE;
+
+ p->buf = buf;
+ p->range = range;
+ p->code = code;
+ p->remainLen = len;
+ p->dicPos = dicPos;
+ p->processedPos = processedPos;
+ p->reps[0] = rep0;
+ p->reps[1] = rep1;
+ p->reps[2] = rep2;
+ p->reps[3] = rep3;
+ p->state = state;
+
+ return SZ_OK;
+}
+#endif
+
+static void MY_FAST_CALL LzmaDec_WriteRem(CLzmaDec *p, SizeT limit)
+{
+ if (p->remainLen != 0 && p->remainLen < kMatchSpecLenStart)
+ {
+ Byte *dic = p->dic;
+ SizeT dicPos = p->dicPos;
+ SizeT dicBufSize = p->dicBufSize;
+ unsigned len = (unsigned)p->remainLen;
+ SizeT rep0 = p->reps[0]; /* we use SizeT to avoid the BUG of VC14 for AMD64 */
+ SizeT rem = limit - dicPos;
+ if (rem < len)
+ len = (unsigned)(rem);
+
+ if (p->checkDicSize == 0 && p->prop.dicSize - p->processedPos <= len)
+ p->checkDicSize = p->prop.dicSize;
+
+ p->processedPos += len;
+ p->remainLen -= len;
+ while (len != 0)
+ {
+ len--;
+ dic[dicPos] = dic[dicPos - rep0 + (dicPos < rep0 ? dicBufSize : 0)];
+ dicPos++;
+ }
+ p->dicPos = dicPos;
+ }
+}
+
+
+#define kRange0 0xFFFFFFFF
+#define kBound0 ((kRange0 >> kNumBitModelTotalBits) << (kNumBitModelTotalBits - 1))
+#define kBadRepCode (kBound0 + (((kRange0 - kBound0) >> kNumBitModelTotalBits) << (kNumBitModelTotalBits - 1)))
+#if kBadRepCode != (0xC0000000 - 0x400)
+ #error Stop_Compiling_Bad_LZMA_Check
+#endif
+
+static int MY_FAST_CALL LzmaDec_DecodeReal2(CLzmaDec *p, SizeT limit, const Byte *bufLimit)
+{
+ do
+ {
+ SizeT limit2 = limit;
+ if (p->checkDicSize == 0)
+ {
+ UInt32 rem = p->prop.dicSize - p->processedPos;
+ if (limit - p->dicPos > rem)
+ limit2 = p->dicPos + rem;
+
+ if (p->processedPos == 0)
+ if (p->code >= kBadRepCode)
+ return SZ_ERROR_DATA;
+ }
+
+ RINOK(LZMA_DECODE_REAL(p, limit2, bufLimit));
+
+ if (p->checkDicSize == 0 && p->processedPos >= p->prop.dicSize)
+ p->checkDicSize = p->prop.dicSize;
+
+ LzmaDec_WriteRem(p, limit);
+ }
+ while (p->dicPos < limit && p->buf < bufLimit && p->remainLen < kMatchSpecLenStart);
+
+ return 0;
+}
+
+typedef enum
+{
+ DUMMY_ERROR, /* unexpected end of input stream */
+ DUMMY_LIT,
+ DUMMY_MATCH,
+ DUMMY_REP
+} ELzmaDummy;
+
+static ELzmaDummy LzmaDec_TryDummy(const CLzmaDec *p, const Byte *buf, SizeT inSize)
+{
+ UInt32 range = p->range;
+ UInt32 code = p->code;
+ const Byte *bufLimit = buf + inSize;
+ const CLzmaProb *probs = GET_PROBS;
+ unsigned state = (unsigned)p->state;
+ ELzmaDummy res;
+
+ {
+ const CLzmaProb *prob;
+ UInt32 bound;
+ unsigned ttt;
+ unsigned posState = CALC_POS_STATE(p->processedPos, (1 << p->prop.pb) - 1);
+
+ prob = probs + IsMatch + COMBINED_PS_STATE;
+ IF_BIT_0_CHECK(prob)
+ {
+ UPDATE_0_CHECK
+
+ /* if (bufLimit - buf >= 7) return DUMMY_LIT; */
+
+ prob = probs + Literal;
+ if (p->checkDicSize != 0 || p->processedPos != 0)
+ prob += ((UInt32)LZMA_LIT_SIZE *
+ ((((p->processedPos) & ((1 << (p->prop.lp)) - 1)) << p->prop.lc) +
+ (p->dic[(p->dicPos == 0 ? p->dicBufSize : p->dicPos) - 1] >> (8 - p->prop.lc))));
+
+ if (state < kNumLitStates)
+ {
+ unsigned symbol = 1;
+ do { GET_BIT_CHECK(prob + symbol, symbol) } while (symbol < 0x100);
+ }
+ else
+ {
+ unsigned matchByte = p->dic[p->dicPos - p->reps[0] +
+ (p->dicPos < p->reps[0] ? p->dicBufSize : 0)];
+ unsigned offs = 0x100;
+ unsigned symbol = 1;
+ do
+ {
+ unsigned bit;
+ const CLzmaProb *probLit;
+ matchByte += matchByte;
+ bit = offs;
+ offs &= matchByte;
+ probLit = prob + (offs + bit + symbol);
+ GET_BIT2_CHECK(probLit, symbol, offs ^= bit; , ; )
+ }
+ while (symbol < 0x100);
+ }
+ res = DUMMY_LIT;
+ }
+ else
+ {
+ unsigned len;
+ UPDATE_1_CHECK;
+
+ prob = probs + IsRep + state;
+ IF_BIT_0_CHECK(prob)
+ {
+ UPDATE_0_CHECK;
+ state = 0;
+ prob = probs + LenCoder;
+ res = DUMMY_MATCH;
+ }
+ else
+ {
+ UPDATE_1_CHECK;
+ res = DUMMY_REP;
+ prob = probs + IsRepG0 + state;
+ IF_BIT_0_CHECK(prob)
+ {
+ UPDATE_0_CHECK;
+ prob = probs + IsRep0Long + COMBINED_PS_STATE;
+ IF_BIT_0_CHECK(prob)
+ {
+ UPDATE_0_CHECK;
+ NORMALIZE_CHECK;
+ return DUMMY_REP;
+ }
+ else
+ {
+ UPDATE_1_CHECK;
+ }
+ }
+ else
+ {
+ UPDATE_1_CHECK;
+ prob = probs + IsRepG1 + state;
+ IF_BIT_0_CHECK(prob)
+ {
+ UPDATE_0_CHECK;
+ }
+ else
+ {
+ UPDATE_1_CHECK;
+ prob = probs + IsRepG2 + state;
+ IF_BIT_0_CHECK(prob)
+ {
+ UPDATE_0_CHECK;
+ }
+ else
+ {
+ UPDATE_1_CHECK;
+ }
+ }
+ }
+ state = kNumStates;
+ prob = probs + RepLenCoder;
+ }
+ {
+ unsigned limit, offset;
+ const CLzmaProb *probLen = prob + LenChoice;
+ IF_BIT_0_CHECK(probLen)
+ {
+ UPDATE_0_CHECK;
+ probLen = prob + LenLow + GET_LEN_STATE;
+ offset = 0;
+ limit = 1 << kLenNumLowBits;
+ }
+ else
+ {
+ UPDATE_1_CHECK;
+ probLen = prob + LenChoice2;
+ IF_BIT_0_CHECK(probLen)
+ {
+ UPDATE_0_CHECK;
+ probLen = prob + LenLow + GET_LEN_STATE + (1 << kLenNumLowBits);
+ offset = kLenNumLowSymbols;
+ limit = 1 << kLenNumLowBits;
+ }
+ else
+ {
+ UPDATE_1_CHECK;
+ probLen = prob + LenHigh;
+ offset = kLenNumLowSymbols * 2;
+ limit = 1 << kLenNumHighBits;
+ }
+ }
+ TREE_DECODE_CHECK(probLen, limit, len);
+ len += offset;
+ }
+
+ if (state < 4)
+ {
+ unsigned posSlot;
+ prob = probs + PosSlot +
+ ((len < kNumLenToPosStates - 1 ? len : kNumLenToPosStates - 1) <<
+ kNumPosSlotBits);
+ TREE_DECODE_CHECK(prob, 1 << kNumPosSlotBits, posSlot);
+ if (posSlot >= kStartPosModelIndex)
+ {
+ unsigned numDirectBits = ((posSlot >> 1) - 1);
+
+ /* if (bufLimit - buf >= 8) return DUMMY_MATCH; */
+
+ if (posSlot < kEndPosModelIndex)
+ {
+ prob = probs + SpecPos + ((2 | (posSlot & 1)) << numDirectBits);
+ }
+ else
+ {
+ numDirectBits -= kNumAlignBits;
+ do
+ {
+ NORMALIZE_CHECK
+ range >>= 1;
+ code -= range & (((code - range) >> 31) - 1);
+ /* if (code >= range) code -= range; */
+ }
+ while (--numDirectBits);
+ prob = probs + Align;
+ numDirectBits = kNumAlignBits;
+ }
+ {
+ unsigned i = 1;
+ unsigned m = 1;
+ do
+ {
+ REV_BIT_CHECK(prob, i, m);
+ }
+ while (--numDirectBits);
+ }
+ }
+ }
+ }
+ }
+ NORMALIZE_CHECK;
+ return res;
+}
+
+
+void LzmaDec_InitDicAndState(CLzmaDec *p, Bool initDic, Bool initState)
+{
+ p->remainLen = kMatchSpecLenStart + 1;
+ p->tempBufSize = 0;
+
+ if (initDic)
+ {
+ p->processedPos = 0;
+ p->checkDicSize = 0;
+ p->remainLen = kMatchSpecLenStart + 2;
+ }
+ if (initState)
+ p->remainLen = kMatchSpecLenStart + 2;
+}
+
+void LzmaDec_Init(CLzmaDec *p)
+{
+ p->dicPos = 0;
+ LzmaDec_InitDicAndState(p, True, True);
+}
+
+
+SRes LzmaDec_DecodeToDic(CLzmaDec *p, SizeT dicLimit, const Byte *src, SizeT *srcLen,
+ ELzmaFinishMode finishMode, ELzmaStatus *status)
+{
+ SizeT inSize = *srcLen;
+ (*srcLen) = 0;
+
+ *status = LZMA_STATUS_NOT_SPECIFIED;
+
+ if (p->remainLen > kMatchSpecLenStart)
+ {
+ for (; inSize > 0 && p->tempBufSize < RC_INIT_SIZE; (*srcLen)++, inSize--)
+ p->tempBuf[p->tempBufSize++] = *src++;
+ if (p->tempBufSize != 0 && p->tempBuf[0] != 0)
+ return SZ_ERROR_DATA;
+ if (p->tempBufSize < RC_INIT_SIZE)
+ {
+ *status = LZMA_STATUS_NEEDS_MORE_INPUT;
+ return SZ_OK;
+ }
+ p->code =
+ ((UInt32)p->tempBuf[1] << 24)
+ | ((UInt32)p->tempBuf[2] << 16)
+ | ((UInt32)p->tempBuf[3] << 8)
+ | ((UInt32)p->tempBuf[4]);
+ p->range = 0xFFFFFFFF;
+ p->tempBufSize = 0;
+
+ if (p->remainLen > kMatchSpecLenStart + 1)
+ {
+ SizeT numProbs = LzmaProps_GetNumProbs(&p->prop);
+ SizeT i;
+ CLzmaProb *probs = p->probs;
+ for (i = 0; i < numProbs; i++)
+ probs[i] = kBitModelTotal >> 1;
+ p->reps[0] = p->reps[1] = p->reps[2] = p->reps[3] = 1;
+ p->state = 0;
+ }
+
+ p->remainLen = 0;
+ }
+
+ LzmaDec_WriteRem(p, dicLimit);
+
+ while (p->remainLen != kMatchSpecLenStart)
+ {
+ int checkEndMarkNow = 0;
+
+ if (p->dicPos >= dicLimit)
+ {
+ if (p->remainLen == 0 && p->code == 0)
+ {
+ *status = LZMA_STATUS_MAYBE_FINISHED_WITHOUT_MARK;
+ return SZ_OK;
+ }
+ if (finishMode == LZMA_FINISH_ANY)
+ {
+ *status = LZMA_STATUS_NOT_FINISHED;
+ return SZ_OK;
+ }
+ if (p->remainLen != 0)
+ {
+ *status = LZMA_STATUS_NOT_FINISHED;
+ return SZ_ERROR_DATA;
+ }
+ checkEndMarkNow = 1;
+ }
+
+ if (p->tempBufSize == 0)
+ {
+ SizeT processed;
+ const Byte *bufLimit;
+ if (inSize < LZMA_REQUIRED_INPUT_MAX || checkEndMarkNow)
+ {
+ int dummyRes = LzmaDec_TryDummy(p, src, inSize);
+ if (dummyRes == DUMMY_ERROR)
+ {
+ memcpy(p->tempBuf, src, inSize);
+ p->tempBufSize = (unsigned)inSize;
+ (*srcLen) += inSize;
+ *status = LZMA_STATUS_NEEDS_MORE_INPUT;
+ return SZ_OK;
+ }
+ if (checkEndMarkNow && dummyRes != DUMMY_MATCH)
+ {
+ *status = LZMA_STATUS_NOT_FINISHED;
+ return SZ_ERROR_DATA;
+ }
+ bufLimit = src;
+ }
+ else
+ bufLimit = src + inSize - LZMA_REQUIRED_INPUT_MAX;
+ p->buf = src;
+ if (LzmaDec_DecodeReal2(p, dicLimit, bufLimit) != 0)
+ return SZ_ERROR_DATA;
+ processed = (SizeT)(p->buf - src);
+ (*srcLen) += processed;
+ src += processed;
+ inSize -= processed;
+ }
+ else
+ {
+ unsigned rem = p->tempBufSize, lookAhead = 0;
+ while (rem < LZMA_REQUIRED_INPUT_MAX && lookAhead < inSize)
+ p->tempBuf[rem++] = src[lookAhead++];
+ p->tempBufSize = rem;
+ if (rem < LZMA_REQUIRED_INPUT_MAX || checkEndMarkNow)
+ {
+ int dummyRes = LzmaDec_TryDummy(p, p->tempBuf, rem);
+ if (dummyRes == DUMMY_ERROR)
+ {
+ (*srcLen) += lookAhead;
+ *status = LZMA_STATUS_NEEDS_MORE_INPUT;
+ return SZ_OK;
+ }
+ if (checkEndMarkNow && dummyRes != DUMMY_MATCH)
+ {
+ *status = LZMA_STATUS_NOT_FINISHED;
+ return SZ_ERROR_DATA;
+ }
+ }
+ p->buf = p->tempBuf;
+ if (LzmaDec_DecodeReal2(p, dicLimit, p->buf) != 0)
+ return SZ_ERROR_DATA;
+
+ {
+ unsigned kkk = (unsigned)(p->buf - p->tempBuf);
+ if (rem < kkk)
+ return SZ_ERROR_FAIL; /* some internal error */
+ rem -= kkk;
+ if (lookAhead < rem)
+ return SZ_ERROR_FAIL; /* some internal error */
+ lookAhead -= rem;
+ }
+ (*srcLen) += lookAhead;
+ src += lookAhead;
+ inSize -= lookAhead;
+ p->tempBufSize = 0;
+ }
+ }
+
+ if (p->code != 0)
+ return SZ_ERROR_DATA;
+ *status = LZMA_STATUS_FINISHED_WITH_MARK;
+ return SZ_OK;
+}
+
+
+SRes LzmaDec_DecodeToBuf(CLzmaDec *p, Byte *dest, SizeT *destLen, const Byte *src, SizeT *srcLen, ELzmaFinishMode finishMode, ELzmaStatus *status)
+{
+ SizeT outSize = *destLen;
+ SizeT inSize = *srcLen;
+ *srcLen = *destLen = 0;
+ for (;;)
+ {
+ SizeT inSizeCur = inSize, outSizeCur, dicPos;
+ ELzmaFinishMode curFinishMode;
+ SRes res;
+ if (p->dicPos == p->dicBufSize)
+ p->dicPos = 0;
+ dicPos = p->dicPos;
+ if (outSize > p->dicBufSize - dicPos)
+ {
+ outSizeCur = p->dicBufSize;
+ curFinishMode = LZMA_FINISH_ANY;
+ }
+ else
+ {
+ outSizeCur = dicPos + outSize;
+ curFinishMode = finishMode;
+ }
+
+ res = LzmaDec_DecodeToDic(p, outSizeCur, src, &inSizeCur, curFinishMode, status);
+ src += inSizeCur;
+ inSize -= inSizeCur;
+ *srcLen += inSizeCur;
+ outSizeCur = p->dicPos - dicPos;
+ memcpy(dest, p->dic + dicPos, outSizeCur);
+ dest += outSizeCur;
+ outSize -= outSizeCur;
+ *destLen += outSizeCur;
+ if (res != 0)
+ return res;
+ if (outSizeCur == 0 || outSize == 0)
+ return SZ_OK;
+ }
+}
+
+void LzmaDec_FreeProbs(CLzmaDec *p, ISzAllocPtr alloc)
+{
+ ISzAlloc_Free(alloc, p->probs);
+ p->probs = NULL;
+}
+
+static void LzmaDec_FreeDict(CLzmaDec *p, ISzAllocPtr alloc)
+{
+ ISzAlloc_Free(alloc, p->dic);
+ p->dic = NULL;
+}
+
+void LzmaDec_Free(CLzmaDec *p, ISzAllocPtr alloc)
+{
+ LzmaDec_FreeProbs(p, alloc);
+ LzmaDec_FreeDict(p, alloc);
+}
+
+SRes LzmaProps_Decode(CLzmaProps *p, const Byte *data, unsigned size)
+{
+ UInt32 dicSize;
+ Byte d;
+
+ if (size < LZMA_PROPS_SIZE)
+ return SZ_ERROR_UNSUPPORTED;
+ else
+ dicSize = data[1] | ((UInt32)data[2] << 8) | ((UInt32)data[3] << 16) | ((UInt32)data[4] << 24);
+
+ if (dicSize < LZMA_DIC_MIN)
+ dicSize = LZMA_DIC_MIN;
+ p->dicSize = dicSize;
+
+ d = data[0];
+ if (d >= (9 * 5 * 5))
+ return SZ_ERROR_UNSUPPORTED;
+
+ p->lc = (Byte)(d % 9);
+ d /= 9;
+ p->pb = (Byte)(d / 5);
+ p->lp = (Byte)(d % 5);
+
+ return SZ_OK;
+}
+
+static SRes LzmaDec_AllocateProbs2(CLzmaDec *p, const CLzmaProps *propNew, ISzAllocPtr alloc)
+{
+ UInt32 numProbs = LzmaProps_GetNumProbs(propNew);
+ if (!p->probs || numProbs != p->numProbs)
+ {
+ LzmaDec_FreeProbs(p, alloc);
+ p->probs = (CLzmaProb *)ISzAlloc_Alloc(alloc, numProbs * sizeof(CLzmaProb));
+ if (!p->probs)
+ return SZ_ERROR_MEM;
+ p->probs_1664 = p->probs + 1664;
+ p->numProbs = numProbs;
+ }
+ return SZ_OK;
+}
+
+SRes LzmaDec_AllocateProbs(CLzmaDec *p, const Byte *props, unsigned propsSize, ISzAllocPtr alloc)
+{
+ CLzmaProps propNew;
+ RINOK(LzmaProps_Decode(&propNew, props, propsSize));
+ RINOK(LzmaDec_AllocateProbs2(p, &propNew, alloc));
+ p->prop = propNew;
+ return SZ_OK;
+}
+
+SRes LzmaDec_Allocate(CLzmaDec *p, const Byte *props, unsigned propsSize, ISzAllocPtr alloc)
+{
+ CLzmaProps propNew;
+ SizeT dicBufSize;
+ RINOK(LzmaProps_Decode(&propNew, props, propsSize));
+ RINOK(LzmaDec_AllocateProbs2(p, &propNew, alloc));
+
+ {
+ UInt32 dictSize = propNew.dicSize;
+ SizeT mask = ((UInt32)1 << 12) - 1;
+ if (dictSize >= ((UInt32)1 << 30)) mask = ((UInt32)1 << 22) - 1;
+ else if (dictSize >= ((UInt32)1 << 22)) mask = ((UInt32)1 << 20) - 1;;
+ dicBufSize = ((SizeT)dictSize + mask) & ~mask;
+ if (dicBufSize < dictSize)
+ dicBufSize = dictSize;
+ }
+
+ if (!p->dic || dicBufSize != p->dicBufSize)
+ {
+ LzmaDec_FreeDict(p, alloc);
+ p->dic = (Byte *)ISzAlloc_Alloc(alloc, dicBufSize);
+ if (!p->dic)
+ {
+ LzmaDec_FreeProbs(p, alloc);
+ return SZ_ERROR_MEM;
+ }
+ }
+ p->dicBufSize = dicBufSize;
+ p->prop = propNew;
+ return SZ_OK;
+}
+
+SRes LzmaDecode(Byte *dest, SizeT *destLen, const Byte *src, SizeT *srcLen,
+ const Byte *propData, unsigned propSize, ELzmaFinishMode finishMode,
+ ELzmaStatus *status, ISzAllocPtr alloc)
+{
+ CLzmaDec p;
+ SRes res;
+ SizeT outSize = *destLen, inSize = *srcLen;
+ *destLen = *srcLen = 0;
+ *status = LZMA_STATUS_NOT_SPECIFIED;
+ if (inSize < RC_INIT_SIZE)
+ return SZ_ERROR_INPUT_EOF;
+ LzmaDec_Construct(&p);
+ RINOK(LzmaDec_AllocateProbs(&p, propData, propSize, alloc));
+ p.dic = dest;
+ p.dicBufSize = outSize;
+ LzmaDec_Init(&p);
+ *srcLen = inSize;
+ res = LzmaDec_DecodeToDic(&p, outSize, src, srcLen, finishMode, status);
+ *destLen = p.dicPos;
+ if (res == SZ_OK && *status == LZMA_STATUS_NEEDS_MORE_INPUT)
+ res = SZ_ERROR_INPUT_EOF;
+ LzmaDec_FreeProbs(&p, alloc);
+ return res;
+}
diff --git a/games/undertale/src/lzma/LzmaDec.h b/games/undertale/src/lzma/LzmaDec.h
new file mode 100644
index 00000000..28ce60c3
--- /dev/null
+++ b/games/undertale/src/lzma/LzmaDec.h
@@ -0,0 +1,234 @@
+/* LzmaDec.h -- LZMA Decoder
+2018-04-21 : Igor Pavlov : Public domain */
+
+#ifndef __LZMA_DEC_H
+#define __LZMA_DEC_H
+
+#include "7zTypes.h"
+
+EXTERN_C_BEGIN
+
+/* #define _LZMA_PROB32 */
+/* _LZMA_PROB32 can increase the speed on some CPUs,
+ but memory usage for CLzmaDec::probs will be doubled in that case */
+
+typedef
+#ifdef _LZMA_PROB32
+ UInt32
+#else
+ UInt16
+#endif
+ CLzmaProb;
+
+
+/* ---------- LZMA Properties ---------- */
+
+#define LZMA_PROPS_SIZE 5
+
+typedef struct _CLzmaProps
+{
+ Byte lc;
+ Byte lp;
+ Byte pb;
+ Byte _pad_;
+ UInt32 dicSize;
+} CLzmaProps;
+
+/* LzmaProps_Decode - decodes properties
+Returns:
+ SZ_OK
+ SZ_ERROR_UNSUPPORTED - Unsupported properties
+*/
+
+SRes LzmaProps_Decode(CLzmaProps *p, const Byte *data, unsigned size);
+
+
+/* ---------- LZMA Decoder state ---------- */
+
+/* LZMA_REQUIRED_INPUT_MAX = number of required input bytes for worst case.
+ Num bits = log2((2^11 / 31) ^ 22) + 26 < 134 + 26 = 160; */
+
+#define LZMA_REQUIRED_INPUT_MAX 20
+
+typedef struct
+{
+ /* Don't change this structure. ASM code can use it. */
+ CLzmaProps prop;
+ CLzmaProb *probs;
+ CLzmaProb *probs_1664;
+ Byte *dic;
+ SizeT dicBufSize;
+ SizeT dicPos;
+ const Byte *buf;
+ UInt32 range;
+ UInt32 code;
+ UInt32 processedPos;
+ UInt32 checkDicSize;
+ UInt32 reps[4];
+ UInt32 state;
+ UInt32 remainLen;
+
+ UInt32 numProbs;
+ unsigned tempBufSize;
+ Byte tempBuf[LZMA_REQUIRED_INPUT_MAX];
+} CLzmaDec;
+
+#define LzmaDec_Construct(p) { (p)->dic = NULL; (p)->probs = NULL; }
+
+void LzmaDec_Init(CLzmaDec *p);
+
+/* There are two types of LZMA streams:
+ - Stream with end mark. That end mark adds about 6 bytes to compressed size.
+ - Stream without end mark. You must know exact uncompressed size to decompress such stream. */
+
+typedef enum
+{
+ LZMA_FINISH_ANY, /* finish at any point */
+ LZMA_FINISH_END /* block must be finished at the end */
+} ELzmaFinishMode;
+
+/* ELzmaFinishMode has meaning only if the decoding reaches output limit !!!
+
+ You must use LZMA_FINISH_END, when you know that current output buffer
+ covers last bytes of block. In other cases you must use LZMA_FINISH_ANY.
+
+ If LZMA decoder sees end marker before reaching output limit, it returns SZ_OK,
+ and output value of destLen will be less than output buffer size limit.
+ You can check status result also.
+
+ You can use multiple checks to test data integrity after full decompression:
+ 1) Check Result and "status" variable.
+ 2) Check that output(destLen) = uncompressedSize, if you know real uncompressedSize.
+ 3) Check that output(srcLen) = compressedSize, if you know real compressedSize.
+ You must use correct finish mode in that case. */
+
+typedef enum
+{
+ LZMA_STATUS_NOT_SPECIFIED, /* use main error code instead */
+ LZMA_STATUS_FINISHED_WITH_MARK, /* stream was finished with end mark. */
+ LZMA_STATUS_NOT_FINISHED, /* stream was not finished */
+ LZMA_STATUS_NEEDS_MORE_INPUT, /* you must provide more input bytes */
+ LZMA_STATUS_MAYBE_FINISHED_WITHOUT_MARK /* there is probability that stream was finished without end mark */
+} ELzmaStatus;
+
+/* ELzmaStatus is used only as output value for function call */
+
+
+/* ---------- Interfaces ---------- */
+
+/* There are 3 levels of interfaces:
+ 1) Dictionary Interface
+ 2) Buffer Interface
+ 3) One Call Interface
+ You can select any of these interfaces, but don't mix functions from different
+ groups for same object. */
+
+
+/* There are two variants to allocate state for Dictionary Interface:
+ 1) LzmaDec_Allocate / LzmaDec_Free
+ 2) LzmaDec_AllocateProbs / LzmaDec_FreeProbs
+ You can use variant 2, if you set dictionary buffer manually.
+ For Buffer Interface you must always use variant 1.
+
+LzmaDec_Allocate* can return:
+ SZ_OK
+ SZ_ERROR_MEM - Memory allocation error
+ SZ_ERROR_UNSUPPORTED - Unsupported properties
+*/
+
+SRes LzmaDec_AllocateProbs(CLzmaDec *p, const Byte *props, unsigned propsSize, ISzAllocPtr alloc);
+void LzmaDec_FreeProbs(CLzmaDec *p, ISzAllocPtr alloc);
+
+SRes LzmaDec_Allocate(CLzmaDec *p, const Byte *props, unsigned propsSize, ISzAllocPtr alloc);
+void LzmaDec_Free(CLzmaDec *p, ISzAllocPtr alloc);
+
+/* ---------- Dictionary Interface ---------- */
+
+/* You can use it, if you want to eliminate the overhead for data copying from
+ dictionary to some other external buffer.
+ You must work with CLzmaDec variables directly in this interface.
+
+ STEPS:
+ LzmaDec_Construct()
+ LzmaDec_Allocate()
+ for (each new stream)
+ {
+ LzmaDec_Init()
+ while (it needs more decompression)
+ {
+ LzmaDec_DecodeToDic()
+ use data from CLzmaDec::dic and update CLzmaDec::dicPos
+ }
+ }
+ LzmaDec_Free()
+*/
+
+/* LzmaDec_DecodeToDic
+
+ The decoding to internal dictionary buffer (CLzmaDec::dic).
+ You must manually update CLzmaDec::dicPos, if it reaches CLzmaDec::dicBufSize !!!
+
+finishMode:
+ It has meaning only if the decoding reaches output limit (dicLimit).
+ LZMA_FINISH_ANY - Decode just dicLimit bytes.
+ LZMA_FINISH_END - Stream must be finished after dicLimit.
+
+Returns:
+ SZ_OK
+ status:
+ LZMA_STATUS_FINISHED_WITH_MARK
+ LZMA_STATUS_NOT_FINISHED
+ LZMA_STATUS_NEEDS_MORE_INPUT
+ LZMA_STATUS_MAYBE_FINISHED_WITHOUT_MARK
+ SZ_ERROR_DATA - Data error
+*/
+
+SRes LzmaDec_DecodeToDic(CLzmaDec *p, SizeT dicLimit,
+ const Byte *src, SizeT *srcLen, ELzmaFinishMode finishMode, ELzmaStatus *status);
+
+
+/* ---------- Buffer Interface ---------- */
+
+/* It's zlib-like interface.
+ See LzmaDec_DecodeToDic description for information about STEPS and return results,
+ but you must use LzmaDec_DecodeToBuf instead of LzmaDec_DecodeToDic and you don't need
+ to work with CLzmaDec variables manually.
+
+finishMode:
+ It has meaning only if the decoding reaches output limit (*destLen).
+ LZMA_FINISH_ANY - Decode just destLen bytes.
+ LZMA_FINISH_END - Stream must be finished after (*destLen).
+*/
+
+SRes LzmaDec_DecodeToBuf(CLzmaDec *p, Byte *dest, SizeT *destLen,
+ const Byte *src, SizeT *srcLen, ELzmaFinishMode finishMode, ELzmaStatus *status);
+
+
+/* ---------- One Call Interface ---------- */
+
+/* LzmaDecode
+
+finishMode:
+ It has meaning only if the decoding reaches output limit (*destLen).
+ LZMA_FINISH_ANY - Decode just destLen bytes.
+ LZMA_FINISH_END - Stream must be finished after (*destLen).
+
+Returns:
+ SZ_OK
+ status:
+ LZMA_STATUS_FINISHED_WITH_MARK
+ LZMA_STATUS_NOT_FINISHED
+ LZMA_STATUS_MAYBE_FINISHED_WITHOUT_MARK
+ SZ_ERROR_DATA - Data error
+ SZ_ERROR_MEM - Memory allocation error
+ SZ_ERROR_UNSUPPORTED - Unsupported properties
+ SZ_ERROR_INPUT_EOF - It needs more bytes in input buffer (src).
+*/
+
+SRes LzmaDecode(Byte *dest, SizeT *destLen, const Byte *src, SizeT *srcLen,
+ const Byte *propData, unsigned propSize, ELzmaFinishMode finishMode,
+ ELzmaStatus *status, ISzAllocPtr alloc);
+
+EXTERN_C_END
+
+#endif
diff --git a/games/undertale/src/lzma/Precomp.h b/games/undertale/src/lzma/Precomp.h
new file mode 100644
index 00000000..fdaa2452
--- /dev/null
+++ b/games/undertale/src/lzma/Precomp.h
@@ -0,0 +1,5 @@
+/* Precomp.h -- StdAfx
+2013-11-12 : Igor Pavlov : Public domain */
+#ifndef __7Z_PRECOMP_H
+#define __7Z_PRECOMP_H
+#endif
diff --git a/games/undertale/src/main.c b/games/undertale/src/main.c
new file mode 100644
index 00000000..05da21a5
--- /dev/null
+++ b/games/undertale/src/main.c
@@ -0,0 +1,48 @@
+#if defined(__GNUC__) && !defined(__clang__)
+#pragma GCC optimize("Os") /* not drawn every frame: smaller over faster */
+#endif
+/* Undertale for the NumWorks calculator.
+ *
+ * Inspired by Undertale by Toby Fox, not affiliated with him.
+ * Made by Mason Chen as part of NumPlay. */
+#include "game.h"
+#include "gfx.h"
+#include "plat.h"
+#include "res.h"
+
+#ifndef HOST
+const char eadk_app_name[] __attribute__((section(".rodata.eadk_app_name"))) = "Undertale";
+const uint32_t eadk_api_level __attribute__((section(".rodata.eadk_api_level"))) = 0;
+extern const uint8_t ut_data[];
+
+uint32_t perf_frames __attribute__((used)); /* read by tools/emu.py */
+uint32_t perf_updates __attribute__((used));
+
+int main(void) {
+ uint16_t band[SCREEN_W * GFX_BAND]; /* (here: on the stack, out of the RAM apps get) */
+ gfx_set_band_memory(band);
+ plat_begin();
+ ut_bin = ut_data;
+ game_init();
+ uint32_t next = plat_millis() * 3; /* in thirds of a millisecond: 100 per frame, as the game's 30 a second */
+ while (g_running) {
+ /* the game's frames: drawn when caught up (at most 2 updates per drawing) */
+ int n = 0;
+ uint32_t now = plat_millis() * 3;
+ while ((int32_t)(now - next) >= 0 && n < 2 && g_running) {
+ game_frame(plat_keys());
+ perf_updates++;
+ next += 100;
+ n++;
+ }
+ if (!g_running) break;
+ if ((int32_t)(now - next) > 600) next = now; /* far behind: do not rush */
+ if (n) {
+ game_draw();
+ perf_frames++;
+ } else
+ plat_sleep(1);
+ }
+ return plat_end();
+}
+#endif
diff --git a/games/undertale/src/menu.c b/games/undertale/src/menu.c
new file mode 100644
index 00000000..d65379c7
--- /dev/null
+++ b/games/undertale/src/menu.c
@@ -0,0 +1,535 @@
+/* The menus: the intro, the title, naming the fallen human, the C menu (ITEM, STAT, CELL), SAVE, and GAME OVER
+ * (menu.h). Their texts are the game's. */
+#include
+#include
+#include "battle.h"
+#include "event.h"
+#include "events.h"
+#include "game.h"
+#include "gfx.h"
+#include "items.h"
+#include "menu.h"
+#include "res.h"
+#include "save.h"
+#include "shop.h"
+#include "text.h"
+#include "world.h"
+
+static int st, sel, sub, t; /* the screen's step, what is selected, a second level, its frames */
+static Writer w;
+static char buf[400];
+static char name[8];
+static int name_len;
+static int letter; /* the naming screen's cursor */
+static int item_sel, item_act;
+static bool msg_open; /* a text in the dialogue box over the menu */
+
+enum { S_INTRO, S_TITLE, S_INSTR, S_NAME, S_CONFIRM, S_CONTINUE, S_GAMEOVER, S_FADEIN };
+
+/* ---------------------------------------------------------------- the intro */
+static const char *const intro_lines[] = {
+ "Long ago, two races\nruled over Earth:\nHUMANS and MONSTERS.",
+ "One day, war broke\nout between the two\nraces.",
+ "After a long battle,\nthe humans were\nvictorious.",
+ "They sealed the monsters\nunderground with a magic\nspell.",
+ "Many years later...",
+ " MT. EBOTT\n 201X",
+ "Legends say that those\nwho climb the mountain\nnever return.",
+ "", "", "", "",
+};
+#define INTRO_SLIDES 11
+static int slide;
+
+static void intro_slide(int k) {
+ slide = k, t = 0;
+ writer_start(&w, intro_lines[k], 60, 152, FONT_MAIN);
+ w.line_h = 16, w.speed = 2;
+}
+
+void menu_title_open(void) {
+ g_mode = MODE_INTRO;
+ st = S_INTRO;
+ intro_slide(0);
+}
+
+static void title_open(void) {
+ g_mode = MODE_TITLE;
+ st = S_TITLE, t = 0, sel = 0;
+}
+
+/* ---------------------------------------------------------------- naming */
+static const char *const special_forbidden[][2] = {
+ {"alphys", "D-don't do that."}, {"asgore", "You cannot."}, {"asriel", "..."}, {"flowey", "I already CHOSE that name."},
+ {"sans", "nope."}, {"toriel", "I think you should\nthink of your own\nname, my child."}, {"undyne", "Get your OWN name!"},
+};
+static const char *const special_allowed[][2] = {
+ {"frisk", "WARNING: This name will\nmake your life hell.\nProceed anyway?"}, {"murder", "That's a little on-the-\nnose, isn't it...?"},
+ {"mercy", "That's a little on-the-\nnose, isn't it...?"}, {"aaaaaa", "Not very creative...?"},
+ {"aaron", "Is this name correct? ;)"}, {"alphy", "Uh... OK?"}, {"bpants", "You are really scraping\nthe bottom of the barrel."},
+ {"bratty", "Like, OK I guess."}, {"catty", "Bratty! Bratty!\nThat's MY name!"}, {"chara", "The true name."},
+ {"gerson", "Wah ha ha! Why not?"}, {"jerry", "Jerry."}, {"metta", "OOOOH!!! ARE YOU\nPROMOTING MY BRAND?"},
+ {"mett", "OOOOH!!! ARE YOU\nPROMOTING MY BRAND?"}, {"mtt", "OOOOH!!! ARE YOU\nPROMOTING MY BRAND?"},
+ {"napsta", "............\n(They're powerless to\nstop you.)"}, {"blooky", "............\n(They're powerless to\nstop you.)"},
+ {"papyru", "I'LL ALLOW IT!!!!"}, {"shyren", "...?"}, {"temmie", "hOI!"}, {"woshua", "Clean name."},
+};
+static const char *confirm_text;
+static bool confirm_allowed;
+
+static bool same_lower(const char *a, const char *b) {
+ for (; *a && *b; a++, b++) {
+ char x = *a >= 'A' && *a <= 'Z' ? (char)(*a + 32) : *a;
+ if (x != *b) return false;
+ }
+ return !*a && !*b;
+}
+
+static void name_done(void) {
+ name[name_len] = 0;
+ if (!name_len) {
+ confirm_text = "You must choose a name.";
+ confirm_allowed = false;
+ } else {
+ confirm_text = "Is this name correct?", confirm_allowed = true;
+ for (size_t i = 0; i < sizeof special_forbidden / sizeof special_forbidden[0]; i++)
+ if (same_lower(name, special_forbidden[i][0])) confirm_text = special_forbidden[i][1], confirm_allowed = false;
+ for (size_t i = 0; i < sizeof special_allowed / sizeof special_allowed[0]; i++)
+ if (same_lower(name, special_allowed[i][0])) confirm_text = special_allowed[i][1];
+ if (same_lower(name, "gaster")) {
+ /* the game starts over */
+ name_len = 0;
+ menu_title_open();
+ return;
+ }
+ }
+ st = S_CONFIRM, sel = 0, t = 0;
+}
+
+/* the letters: A-Z, then a-z, 7 a row, then Quit / Backspace / Done */
+#define LETTERS 52
+static char letter_at(int i) { return (char)(i < 26 ? 'A' + i : 'a' + i - 26); }
+static void letter_xy(int i, int *x, int *y) {
+ int k = i < 26 ? i : i - 26, row = k / 7, col = k % 7;
+ *x = 60 + col * 32, *y = (i < 26 ? 58 : 122) + row * 14;
+}
+
+static void name_input(uint32_t pressed) {
+ /* letter: 0..51, 52 Quit, 53 Backspace, 54 Done */
+ if (pressed & K_RIGHT) letter = letter >= 52 ? (letter == 54 ? 52 : letter + 1) : (letter + 1) % LETTERS;
+ if (pressed & K_LEFT) letter = letter >= 52 ? (letter == 52 ? 54 : letter - 1) : (letter + LETTERS - 1) % LETTERS;
+ if (pressed & K_DOWN) {
+ if (letter >= 52) letter = letter == 52 ? 0 : letter == 53 ? 3 : 6;
+ else {
+ int k = letter < 26 ? letter : letter - 26;
+ if (k + 7 < 26) letter += 7;
+ else if (letter < 26) letter = 26 + k % 7;
+ else letter = k % 7 < 2 ? 52 : k % 7 < 5 ? 53 : 54;
+ }
+ }
+ if (pressed & K_UP) {
+ if (letter >= 52) letter = 26 + 21 + (letter == 52 ? 0 : letter == 53 ? 2 : 4);
+ else {
+ int k = letter < 26 ? letter : letter - 26;
+ if (k >= 7) letter -= 7;
+ else if (letter >= 26) letter = (k + 21 < 26) ? k + 21 : k + 14;
+ else letter = k < 2 ? 52 : k < 5 ? 53 : 54;
+ }
+ }
+ if (pressed & K_X && name_len) name_len--;
+ if (pressed & K_Z) {
+ if (letter < LETTERS) {
+ if (name_len < 6) name[name_len++] = letter_at(letter);
+ } else if (letter == 52) title_open();
+ else if (letter == 53) {
+ if (name_len) name_len--;
+ } else name_done();
+ }
+}
+
+/* ---------------------------------------------------------------- the C menu */
+enum { M_MAIN, M_ITEMS, M_ITEM_ACT, M_STAT, M_CELL };
+/* the CELL's list: Toriel's phone (0); Papyrus's (1) once he gave his number; the Dimensional Box (2) once Alphys
+ * upgraded the phone */
+static int cell_entries(int *calls) {
+ int n = 0;
+ calls[n++] = 0;
+ if (flag(F_PAP_DATE) == 2) calls[n++] = 1;
+ if (flag(F_HT_LAB) >= 2) calls[n++] = 2;
+ return n;
+}
+
+void menu_open(void) {
+ g_mode = MODE_MENU;
+ st = M_MAIN, sel = 0;
+ msg_open = false;
+}
+
+static void menu_close(void) { g_mode = MODE_WORLD; }
+
+static void menu_say(const char *s) {
+ snprintf(buf, sizeof buf, "%s", s);
+ text_set_top(-1);
+ text_open(FACE_NONE, buf);
+ msg_open = true;
+}
+
+static void c_menu_input(uint32_t pressed) {
+ if (msg_open) {
+ text_update(pressed);
+ if (!text_active()) {
+ msg_open = false;
+ if (st != M_MAIN) {
+ menu_close();
+ return;
+ }
+ }
+ return;
+ }
+ switch (st) {
+ case M_MAIN: {
+ int n = flag(F_PHONE) ? 3 : 2;
+ if (pressed & K_UP) sel = (sel + n - 1) % n;
+ if (pressed & K_DOWN) sel = (sel + 1) % n;
+ if (pressed & (K_X | K_C)) menu_close();
+ if (pressed & K_Z) {
+ if (sel == 0 && item_count()) st = M_ITEMS, item_sel = 0;
+ else if (sel == 1) st = M_STAT;
+ else if (sel == 2) st = M_CELL, sub = 0;
+ }
+ break;
+ }
+ case M_ITEMS: {
+ int n = item_count();
+ if (pressed & K_UP) item_sel = (item_sel + n - 1) % n;
+ if (pressed & K_DOWN) item_sel = (item_sel + 1) % n;
+ if (pressed & K_X) st = M_MAIN;
+ if (pressed & K_Z) st = M_ITEM_ACT, item_act = 0;
+ break;
+ }
+ case M_ITEM_ACT:
+ if (pressed & K_LEFT) item_act = (item_act + 2) % 3;
+ if (pressed & K_RIGHT) item_act = (item_act + 1) % 3;
+ if (pressed & K_X) st = M_ITEMS;
+ if (pressed & K_Z) {
+ if (item_act == 0) {
+ item_use(item_sel, buf, sizeof buf, 0);
+ menu_say(buf);
+ } else if (item_act == 1) {
+ char s[300];
+ item_info_text(sv.items[item_sel], s, sizeof s);
+ menu_say(s);
+ } else {
+ char s[120];
+ snprintf(s, sizeof s, "* The %s was\n thrown away.", item_info(sv.items[item_sel])->name);
+ item_remove_at(item_sel);
+ menu_say(s);
+ }
+ }
+ break;
+ case M_STAT:
+ if (pressed & (K_X | K_Z)) st = M_MAIN;
+ break;
+ case M_CELL: {
+ int calls[3], n = cell_entries(calls);
+ if (pressed & K_UP) sub = (sub + n - 1) % n;
+ if (pressed & K_DOWN) sub = (sub + 1) % n;
+ if (pressed & K_X) st = M_MAIN;
+ if (pressed & K_Z) {
+ menu_close();
+ if (calls[sub] == 2) {
+ box_open();
+ } else {
+ event_start(EV_PHONE_MENU, NULL);
+ event_cur_arg(calls[sub]);
+ }
+ }
+ break;
+ }
+ }
+}
+
+static void panel(int x, int y, int w, int h) {
+ gfx_rect(x, y, w, h, C_WHITE, 255);
+ gfx_rect(x + 3, y + 3, w - 6, h - 6, C_BLACK, 255);
+}
+
+static void draw_c_menu(void) {
+ bool low = pl.y + 15 - cam_y < 130;
+ int oy = low ? 135 : 0;
+ char s[64];
+ /* the name and its numbers */
+ oy = low ? 135 : 0;
+ panel(16, 26 + oy, 71, 55);
+ gfx_text(FONT_MENU, 23, 30 + oy, sv.name, C_WHITE, 255);
+ snprintf(s, sizeof s, "LV %d", sv.lv);
+ gfx_text(FONT_SMALL, 23, 50 + oy, s, C_WHITE, 255);
+ snprintf(s, sizeof s, "HP %d/%d", sv.hp, sv.maxhp);
+ gfx_text(FONT_SMALL, 23, 59 + oy, s, C_WHITE, 255);
+ snprintf(s, sizeof s, "G %d", sv.gold);
+ gfx_text(FONT_SMALL, 23, 68 + oy, s, C_WHITE, 255);
+ /* ITEM, STAT, CELL */
+ int n = flag(F_PHONE) ? 3 : 2;
+ panel(16, 84, 71, 18 + 18 * n);
+ static const char *const opts[] = {"ITEM", "STAT", "CELL"};
+ for (int i = 0; i < n; i++) {
+ uint16_t c = (i == 0 && !item_count()) ? rgb565(0x808080) : C_WHITE;
+ gfx_text(FONT_MENU, 41, 92 + 18 * i, opts[i], c, 255);
+ }
+ if (st == M_MAIN) gfx_sprite(SPR_HEART, 0, 32, 98 + 18 * sel, 0, 255);
+ if (st == M_ITEMS || st == M_ITEM_ACT) {
+ panel(94, 26, 173, 181);
+ for (int i = 0; i < item_count(); i++) {
+ gfx_text(FONT_MENU, 116, 38 + 16 * i, item_info(sv.items[i])->name, C_WHITE, 255);
+ if (st == M_ITEMS && i == item_sel) gfx_sprite(SPR_HEART, 0, 107, 44 + 16 * i, 0, 255);
+ }
+ static const char *const acts[] = {"USE", "INFO", "DROP"};
+ for (int i = 0; i < 3; i++) {
+ gfx_text(FONT_MENU, 116 + i * 48, 180, acts[i], C_WHITE, 255);
+ if (st == M_ITEM_ACT && i == item_act) gfx_sprite(SPR_HEART, 0, 107 + i * 48, 186, 0, 255);
+ }
+ }
+ if (st == M_STAT) {
+ panel(94, 26, 173, 209);
+ snprintf(s, sizeof s, "\"%s\"", sv.name);
+ gfx_text(FONT_MENU, 108, 38, s, C_WHITE, 255);
+ snprintf(s, sizeof s, "LV %d", sv.lv);
+ gfx_text(FONT_MENU, 108, 68, s, C_WHITE, 255);
+ snprintf(s, sizeof s, "HP %d / %d", sv.hp, sv.maxhp);
+ gfx_text(FONT_MENU, 108, 84, s, C_WHITE, 255);
+ snprintf(s, sizeof s, "AT %d(%d)", sv.at - 10, weapon_at());
+ gfx_text(FONT_MENU, 108, 114, s, C_WHITE, 255);
+ snprintf(s, sizeof s, "DF %d(%d)", sv.df - 10, armor_df());
+ gfx_text(FONT_MENU, 108, 130, s, C_WHITE, 255);
+ snprintf(s, sizeof s, "EXP: %d", (int)sv.exp);
+ gfx_text(FONT_MENU, 190, 114, s, C_WHITE, 255);
+ snprintf(s, sizeof s, "NEXT: %d", sv.lv < 20 ? lv_exp_for(sv.lv + 1) - (int)sv.exp : 0);
+ gfx_text(FONT_MENU, 190, 130, s, C_WHITE, 255);
+ snprintf(s, sizeof s, "WEAPON: %s", item_info(sv.weapon)->name);
+ gfx_text(FONT_MENU, 108, 160, s, C_WHITE, 255);
+ snprintf(s, sizeof s, "ARMOR: %s", item_info(sv.armor)->name);
+ gfx_text(FONT_MENU, 108, 176, s, C_WHITE, 255);
+ snprintf(s, sizeof s, "GOLD: %d", sv.gold);
+ gfx_text(FONT_MENU, 108, 200, s, C_WHITE, 255);
+ if (sv.kills > 20) {
+ snprintf(s, sizeof s, "KILLS: %d", sv.kills);
+ gfx_text(FONT_MENU, 190, 200, s, C_WHITE, 255);
+ }
+ }
+ if (st == M_CELL) {
+ static const char *const names[3] = {"Toriel's Phone", "Papyrus's Phone", "Dimensional Box"};
+ int calls[3], n = cell_entries(calls);
+ panel(94, 26, 173, 24 + 18 * n);
+ for (int i = 0; i < n; i++) gfx_text(FONT_MENU, 116, 38 + 18 * i, names[calls[i]], C_WHITE, 255);
+ gfx_sprite(SPR_HEART, 0, 107, 44 + sub * 18, 0, 255);
+ }
+}
+
+/* ---------------------------------------------------------------- SAVE */
+static int save_state; /* 0 the choice, 1 saved */
+
+void menu_save_open(void) {
+ g_mode = MODE_SAVE;
+ sel = 0, save_state = 0;
+}
+
+static void draw_save(void) {
+ panel(54, 58, 212, 82);
+ char s[48];
+ uint16_t c = save_state ? C_YELLOW : C_WHITE;
+ const Save *shown = &sv;
+ gfx_text(FONT_MENU, 68, 68, shown->name, c, 255);
+ snprintf(s, sizeof s, "LV %d", shown->lv);
+ gfx_text(FONT_MENU, 136, 68, s, c, 255);
+ unsigned secs = shown->time / 30;
+ snprintf(s, sizeof s, "%u:%02u", secs / 60, secs % 60);
+ gfx_text(FONT_MENU, 222, 68, s, c, 255);
+ RoomInfo ri;
+ room_info(g_room, &ri);
+ gfx_text(FONT_MENU, 68, 88, str(ri.name), c, 255);
+ if (save_state) gfx_text(FONT_MENU, 84, 116, "File saved.", c, 255);
+ else {
+ gfx_text(FONT_MENU, 84, 116, "Save", C_WHITE, 255);
+ gfx_text(FONT_MENU, 180, 116, "Return", C_WHITE, 255);
+ gfx_sprite(SPR_HEART, 0, sel ? 170 : 74, 122, 0, 255);
+ }
+}
+
+static void save_input(uint32_t pressed) {
+ if (save_state) {
+ if (pressed & (K_Z | K_X)) g_mode = MODE_WORLD;
+ return;
+ }
+ if (pressed & (K_LEFT | K_RIGHT)) sel ^= 1;
+ if (pressed & K_X) g_mode = MODE_WORLD;
+ if (pressed & K_Z) {
+ if (sel) g_mode = MODE_WORLD;
+ else {
+ sv.room = (int16_t)g_room, sv.x = pl.x, sv.y = pl.y;
+ save_write();
+ save_state = 1;
+ }
+ }
+}
+
+/* ---------------------------------------------------------------- GAME OVER */
+void menu_gameover_open(void) {
+ g_mode = MODE_GAMEOVER;
+ st = S_GAMEOVER, t = 0;
+ snprintf(buf, sizeof buf, "You cannot give\nup just yet...|%s!\nStay determined...", sv.name);
+ writer_start(&w, buf, 90, 160, FONT_MAIN);
+ w.speed = 2;
+}
+
+/* ---------------------------------------------------------------- the end of what there is */
+void menu_end_open(const char *text) {
+ g_mode = MODE_END;
+ t = 0;
+ writer_start(&w, text, 40, 90, FONT_MAIN);
+ w.speed = 2;
+}
+
+/* ---------------------------------------------------------------- the frame */
+void menu_update(uint32_t keys, uint32_t pressed) {
+ (void)keys;
+ t++;
+ switch (g_mode) {
+ case MODE_INTRO:
+ if (pressed & K_Z) {
+ title_open();
+ return;
+ }
+ writer_update(&w, 0);
+ if (t > (slide == 10 ? 300 : slide >= 7 ? 90 : 180)) {
+ if (slide + 1 < INTRO_SLIDES) intro_slide(slide + 1);
+ else title_open();
+ }
+ break;
+ case MODE_TITLE:
+ if (st == S_TITLE) {
+ if (pressed & K_Z && t > 15) {
+ if (save_exists()) st = S_CONTINUE, sel = 0;
+ else st = S_INSTR, sel = 0;
+ t = 0;
+ }
+ } else if (st == S_INSTR) {
+ if (pressed & (K_UP | K_DOWN)) sel ^= 1;
+ if (pressed & K_Z && t > 5) {
+ if (sel == 0) st = S_NAME, letter = 0, name_len = 0, t = 0;
+ }
+ } else if (st == S_NAME) {
+ name_input(pressed);
+ } else if (st == S_CONFIRM) {
+ if (!confirm_allowed) {
+ if (pressed & (K_Z | K_X)) st = S_NAME;
+ break;
+ }
+ if (pressed & (K_LEFT | K_RIGHT)) sel ^= 1;
+ if (pressed & K_X) st = S_NAME;
+ if (pressed & K_Z) {
+ if (sel == 0) st = S_NAME;
+ else {
+ game_new(name);
+ if (same_lower(name, "frisk")) set_flag(F_HARDMODE, 1);
+ st = S_FADEIN;
+ }
+ }
+ } else if (st == S_CONTINUE) {
+ if (pressed & (K_LEFT | K_RIGHT)) sel ^= 1;
+ if (pressed & K_Z) {
+ if (sel == 0) save_load();
+ else st = S_NAME, letter = 0, name_len = 0;
+ }
+ }
+ break;
+ case MODE_MENU:
+ c_menu_input(pressed);
+ break;
+ case MODE_SAVE:
+ save_input(pressed);
+ break;
+ case MODE_GAMEOVER:
+ if (t > 40 && !writer_update(&w, pressed)) {
+ if (!save_load()) game_new(sv.name);
+ }
+ break;
+ case MODE_END:
+ if (t > 30 && !writer_update(&w, pressed)) menu_title_open();
+ break;
+ }
+}
+
+void menu_draw(void) {
+ char s[64];
+ switch (g_mode) {
+ case MODE_INTRO:
+ if (slide < 10) gfx_sprite(SPR_INTROIMAGE, slide, 0, 0, 0, (uint8_t)min_i(255, t * 16));
+ else gfx_sprite(SPR_INTROLAST, 0, 60, 28 - min_i(t / 2, 110), 0, 255);
+ writer_draw(&w, C_WHITE);
+ break;
+ case MODE_TITLE:
+ if (st == S_TITLE) {
+ gfx_sprite(SPR_UNDERTALETITLE, 0, 0, 0, 0, 255);
+ if (t > 30) gfx_text(FONT_SMALL, 128, 184, "[PRESS OK]", rgb565(0x808080), 255);
+ } else if (st == S_INSTR) {
+ static const char *const lines[] = {" --- Instruction --- ", "[OK] - Confirm", "[Back] - Cancel",
+ "[toolbox] - Menu (In-game)", "[Home] - Quit", "When HP is 0, you lose."};
+ for (int i = 0; i < 6; i++) gfx_text(FONT_MAIN, 50, 30 + i * 18 + (i > 0) * 4, lines[i], rgb565(0xC0C0C0), 255);
+ gfx_text(FONT_MAIN, 74, 160, "Begin Game", sel == 0 ? C_YELLOW : C_WHITE, 255);
+ gfx_text(FONT_MAIN, 74, 180, "Settings", sel == 1 ? C_YELLOW : rgb565(0x808080), 255);
+ } else if (st == S_NAME) {
+ gfx_text(FONT_MAIN, 76, 18, "Name the fallen human.", C_WHITE, 255);
+ memcpy(s, name, (size_t)name_len), s[name_len] = 0;
+ gfx_text(FONT_MAIN, 128, 40, s, C_WHITE, 255);
+ for (int i = 0; i < LETTERS; i++) {
+ int x, y;
+ letter_xy(i, &x, &y);
+ char c[2] = {letter_at(i), 0};
+ /* (the game's letters tremble; at half its size, one at a time) */
+ bool shakes = (g_frame / 3 + (uint32_t)i * 7) % 23 == 0;
+ gfx_text_ex(FONT_MAIN, x, y, c, 1, i == letter ? C_YELLOW : C_WHITE, shakes, (uint32_t)(i * 77 + g_frame / 3), 0);
+ }
+ static const char *const opts[] = {"Quit", "Backspace", "Done"};
+ static const int16_t ox[] = {60, 122, 220};
+ for (int i = 0; i < 3; i++) gfx_text(FONT_MAIN, ox[i], 200, opts[i], letter == 52 + i ? C_YELLOW : C_WHITE, 255);
+ } else if (st == S_CONFIRM) {
+ gfx_text(FONT_MAIN, 76, 18, confirm_text, C_WHITE, 255);
+ memcpy(s, name, (size_t)name_len), s[name_len] = 0;
+ /* the name grows */
+ int k = min_i(t, 60);
+ gfx_text_ex(FONT_MAIN, 128 - k / 2, 60 + k / 2, s, name_len, C_WHITE, 1, g_frame / 2, k / 10);
+ if (confirm_allowed) {
+ gfx_text(FONT_MAIN, 60, 200, "No", sel == 0 ? C_YELLOW : C_WHITE, 255);
+ gfx_text(FONT_MAIN, 228, 200, "Yes", sel == 1 ? C_YELLOW : C_WHITE, 255);
+ } else
+ gfx_text(FONT_MAIN, 60, 200, "Go back", C_YELLOW, 255);
+ } else if (st == S_CONTINUE) {
+ Save saved;
+ if (save_peek(&saved)) {
+ gfx_text(FONT_MENU, 70, 60, saved.name, C_WHITE, 255);
+ snprintf(s, sizeof s, "LV %d", saved.lv);
+ gfx_text(FONT_MENU, 140, 60, s, C_WHITE, 255);
+ unsigned secs = saved.time / 30;
+ snprintf(s, sizeof s, "%u:%02u", secs / 60, secs % 60);
+ gfx_text(FONT_MENU, 222, 60, s, C_WHITE, 255);
+ RoomInfo ri;
+ room_info(saved.room, &ri);
+ gfx_text(FONT_MENU, 70, 80, str(ri.name), C_WHITE, 255);
+ }
+ gfx_text(FONT_MENU, 84, 110, "Continue", sel == 0 ? C_YELLOW : C_WHITE, 255);
+ gfx_text(FONT_MENU, 190, 110, "Reset", sel == 1 ? C_YELLOW : C_WHITE, 255);
+ }
+ break;
+ case MODE_MENU:
+ draw_c_menu();
+ break;
+ case MODE_SAVE:
+ draw_save();
+ break;
+ case MODE_GAMEOVER:
+ gfx_sprite(SPR_GAMEOVERBG, 0, 0, 0, 0, (uint8_t)min_i(255, t * 6));
+ if (t > 40) writer_draw(&w, C_WHITE);
+ break;
+ case MODE_END:
+ if (t > 30) writer_draw(&w, C_WHITE);
+ break;
+ }
+}
diff --git a/games/undertale/src/menu.h b/games/undertale/src/menu.h
new file mode 100644
index 00000000..2da2a526
--- /dev/null
+++ b/games/undertale/src/menu.h
@@ -0,0 +1,13 @@
+/* Undertale: the menus (title, naming, the C menu, SAVE). */
+#ifndef UT_MENU_H
+#define UT_MENU_H
+#include
+void menu_title_open(void);
+void menu_open(void);
+void menu_save_open(void);
+void menu_gameover_open(void);
+void menu_end_open(const char *text); /* the end: its words, a page at a time */
+void menu_update(uint32_t keys, uint32_t pressed);
+void menu_draw(void);
+void game_new(const char *name);
+#endif
diff --git a/games/undertale/src/monsters.c b/games/undertale/src/monsters.c
new file mode 100644
index 00000000..313b9358
--- /dev/null
+++ b/games/undertale/src/monsters.c
@@ -0,0 +1,704 @@
+/* The Ruins' monsters: stats, texts and ACTs (from the Undertale Wiki's records of the game), their formations,
+ * and their attacks, as the wiki describes them. */
+#include
+#include
+#include "battle.h"
+#include "game.h"
+#include "gfx.h"
+#include "monsters.h"
+#include "res.h"
+
+/* ---------------------------------------------------------------- the ACTs */
+static void act_dummy(Battle *b, Monster *m, int act, char *t, int cap) {
+ (void)b;
+ snprintf(t, (size_t)cap, "* You chat with the DUMMY.\n* It doesn't answer.|* TORIEL looks pleased.");
+ m->gone = true, m->fade = 1;
+ set_flag(F_DUMMY, 1);
+}
+static const char *flavor_dummy(Battle *b, Monster *m) {
+ if (b->turn >= 4) return "* Dummy tires of your aimless\n shenanigans.";
+ return NULL;
+}
+
+static void act_froggit(Battle *b, Monster *m, int act, char *t, int cap) {
+ (void)b;
+ if (act == 0) {
+ snprintf(t, (size_t)cap, "* Froggit didn't understand\n what you said, but was\n flattered anyway.");
+ m->state = 1;
+ } else {
+ snprintf(t, (size_t)cap, "* Froggit didn't understand\n what you said, but was scared\n anyway.");
+ m->state = 2;
+ }
+ m->spare = true;
+}
+static const char *talk_froggit(Battle *b, Monster *m) {
+ if (m->state == 1) { m->state = 3; return "(Blushes\ndeeply.)\nRibbit.."; }
+ if (m->state == 2) { m->state = 3; return "Shiver,\nshiver."; }
+ static const char *const q[] = {"Ribbit,\nribbit.", "Croak,\ncroak.", "Hop,\nhop.", "Meow."};
+ return q[rnd_n(4)];
+}
+static int attack_froggit(Battle *b, Monster *m) { return rnd_n(10) == 0 ? ATK_FROG_RAIN : rnd_n(2) ? ATK_FROG_FLIES : ATK_FROG_JUMP; }
+
+static void act_whimsun(Battle *b, Monster *m, int act, char *t, int cap) {
+ if (act == 0) {
+ snprintf(t, (size_t)cap, "* Halfway through your first\n word, Whimsun bursts into\n tears and runs away.");
+ m->gone = true, m->fade = 1;
+ m->spare_gold = m->state ? 2 : 0;
+ } else {
+ snprintf(t, (size_t)cap, "* You raise your arms and\n wiggle your fingers.\n* Whimsun freaks out!");
+ m->state = 1;
+ }
+ (void)b;
+}
+static const char *talk_whimsun(Battle *b, Monster *m) {
+ if (m->state == 1) { m->state = 2; return "I can't\nhandle\nthis..."; }
+ static const char *const q[] = {"I'm sorry...", "*sniff sniff*", "I have no\nchoice..", "Forgive me..."};
+ return q[rnd_n(4)];
+}
+static const char *flavor_whimsun(Battle *b, Monster *m) { return m->state ? "* Whimsun is hyperventilating." : NULL; }
+static int attack_whimsun(Battle *b, Monster *m) { return rnd_n(2) ? ATK_WHIM_RING : ATK_WHIM_COLS; }
+
+static void act_moldsmal(Battle *b, Monster *m, int act, char *t, int cap) {
+ if (act == 0) snprintf(t, (size_t)cap, "* You lie still beside\n Moldsmal.\n* It feels peaceful.");
+ else snprintf(t, (size_t)cap, "* You wiggle your hips.\n* Moldsmal wiggles back.\n* What a meaningful conversation!");
+ m->spare_gold = 1;
+ (void)b;
+}
+static int attack_moldsmal(Battle *b, Monster *m) { return rnd_n(2) ? ATK_MOLD_ZIG : ATK_MOLD_BURST; }
+
+static void act_loox(Battle *b, Monster *m, int act, char *t, int cap) {
+ if (act == 0) {
+ snprintf(t, (size_t)cap, "* You don't pick on Loox.");
+ if (m->exp > 4) m->exp = (int16_t)(m->exp - 2);
+ m->spare = true, m->state = 2;
+ } else {
+ snprintf(t, (size_t)cap, "* You do something mean...\n* You pick on Loox. Loox is\n furious!");
+ if (m->exp < 20 && !m->spare) m->exp = (int16_t)(m->exp + 5);
+ m->state = 1;
+ }
+ (void)b;
+}
+static const char *talk_loox(Battle *b, Monster *m) {
+ if (m->state == 1) { m->state = 0; m->state2 = 1; return "You rude\nlittle\nsnipe!"; }
+ if (m->state == 2) { m->state = 0; m->state2 = 0; return "Finally\nsomeone\ngets it."; }
+ if (b->turn <= 1) return "Please don't\npick on me.";
+ static const char *const q[] = {"Quit staring\nat me.", "I've got my\neye on you.", "How about a\nstaring\ncontest?", "Don't point\nthat at me.", "What an\neyesore."};
+ return q[rnd_n(5)];
+}
+static int attack_loox(Battle *b, Monster *m) { return rnd_n(2) ? ATK_LOOX_BOUNCE : ATK_LOOX_WORM; }
+
+static void act_vegetoid(Battle *b, Monster *m, int act, char *t, int cap) {
+ if (act == 0) {
+ snprintf(t, (size_t)cap, "* You try to talk with\n Vegetoid, but it just smiles.");
+ m->state = 3;
+ } else if (act == 1) {
+ if (m->spare || m->hp < m->maxhp / 2) {
+ snprintf(t, (size_t)cap, "* You took a bite out of\n Vegetoid.\n* You recovered 5 HP!");
+ sv.hp = (int16_t)min_i(sv.maxhp, sv.hp + 5);
+ m->gone = true, m->fade = 1;
+ } else
+ snprintf(t, (size_t)cap, "* You tried to eat Vegetoid,\n but it wasn't weakened\n enough.");
+ } else {
+ snprintf(t, (size_t)cap, "* You pat your stomach.\n* Vegetoid offers a healthy\n meal.");
+ m->state = 1;
+ }
+ (void)b;
+}
+static const char *talk_vegetoid(Battle *b, Monster *m) {
+ if (m->state == 3) { m->state = 0; return "Plants Can't\nTalk Dummy"; }
+ if (m->state == 1) return "Eat Your\nGreens";
+ if (m->state == 2) return "Ate Your\nGreens";
+ static const char *const q[] = {"Farmed\nLocally,\nVery Locally", "Part Of A\nComplete\nBreakfast", "Fresh\nMorning\nTaste", "Contains\nVitamin A"};
+ return q[rnd_n(4)];
+}
+static int attack_vegetoid(Battle *b, Monster *m) { return rnd_n(2) ? ATK_VEG_FALL : ATK_VEG_CARROT; }
+
+static void act_migosp(Battle *b, Monster *m, int act, char *t, int cap) {
+ if (count_alive() == 1) {
+ snprintf(t, (size_t)cap, "* You talk to Migosp.\n* It doesn't seem to care about\n anything.");
+ m->state = 1;
+ } else
+ snprintf(t, (size_t)cap, "* You talk to Migosp.\n* It doesn't seem to want to\n listen.");
+ (void)b;
+}
+static const char *talk_migosp(Battle *b, Monster *m) {
+ if (count_alive() == 1) {
+ m->spare = true;
+ if (m->state == 1) { m->state = 0; return "Hiya~"; }
+ static const char *const q[] = {"Bein' me is\nthe best!", "La la~ Just\nbe yourself~", "Nothin' like\nalone time!", "Mmm, cha\ncha cha!", "Swing your\narms, baby"};
+ return q[rnd_n(5)];
+ }
+ if (m->state == 1) { m->state = 0; return "I DON'T\nCARE."; }
+ static const char *const q[] = {"FILTHY\nSINGLE\nMINDER..", "OBEY THE\nOVERMIND ..", "LEGION! WE\nARE LEGION", "HEED THE\nSWARM", "IN UNISON,\nNOW"};
+ return q[rnd_n(5)];
+}
+static const char *flavor_migosp(Battle *b, Monster *m) {
+ if (count_alive() == 1) return "* Migosp doesn't have a care\n in the world.";
+ return NULL;
+}
+static int attack_migosp(Battle *b, Monster *m) {
+ if (count_alive() == 1) return ATK_MIGOSP_DANCE;
+ return rnd_n(8) == 0 ? ATK_FROG_FLIES : ATK_MIGOSP_ROACH;
+}
+
+/* ---------------------------------------------------------------- Napstablook */
+static void act_napsta(Battle *b, Monster *m, int act, char *t, int cap) {
+ /* state: cheers; state2: 1 after the hat, 2 morose (turns of it in b->state[1]) */
+ if (act == 2) { /* Cheer */
+ if (m->state2 == 2) {
+ snprintf(t, (size_t)cap, "* You try to console\n Napstablook...");
+ b->state[1]++;
+ if (b->state[1] >= 8) m->state2 = 0, m->state = 0;
+ return;
+ }
+ m->state++;
+ if (m->state == 1) snprintf(t, (size_t)cap, "* You gave Napstablook a\n patient smile.");
+ else if (m->state == 2) snprintf(t, (size_t)cap, "* You told Napstablook a\n little joke.");
+ else if (m->state == 3) snprintf(t, (size_t)cap, "* Napstablook wants to show\n you something.");
+ else {
+ snprintf(t, (size_t)cap, "* ...");
+ b->state[2] = 1; /* the end: Napstablook goes */
+ }
+ } else if (act == 0) { /* Flirt */
+ if (m->state >= 3) {
+ snprintf(t, (size_t)cap, "* You flirt with Napstablook.");
+ b->state[2] = 2;
+ } else {
+ snprintf(t, (size_t)cap, "* You try your best pickup\n line.");
+ m->state2 = 2, b->state[1] = 0;
+ }
+ } else { /* Threat */
+ snprintf(t, (size_t)cap, "* You give Napstablook a\n cruel look.");
+ m->state2 = 2, b->state[1] = 0, b->state[3] = m->state >= 3;
+ m->state = 0;
+ }
+}
+static const char *talk_napsta(Battle *b, Monster *m) {
+ if (b->state[2] == 3)
+ return "oh... i'm a\nghost, you\nknow|i just\nlowered my hp\nto be polite|sorry... that\nwas awkward|let's say you\nwon...";
+ if (b->state[2] == 1) return "oh gee...";
+ if (b->state[2] == 2) return "oh no...";
+ if (b->state[3]) { b->state[3] = 0; return "i knew it..."; }
+ if (m->acts[0] && m->state2 == 2 && b->state[1] == 0 && b->state[4] != 1) { b->state[4] = 1; return "i'd just\nweigh you\ndown."; }
+ if (m->acts[1] && m->state2 == 2 && b->state[1] == 0 && b->state[4] != 2) { b->state[4] = 2; return "go ahead,\ndo it."; }
+ if (m->state == 1 && b->state[5] != 1) { b->state[5] = 1; return "heh..."; }
+ if (m->state == 2 && b->state[5] != 2) { b->state[5] = 2; return "heh heh..."; }
+ if (m->state == 3 && b->state[5] != 3) { b->state[5] = 3; return "let me\ntry...|i call it\n\"dapper\nblook\"|do you\nlike it..."; }
+ static const char *const q[] = {"nnnnnnggghhh.", "just pluggin\nalong...", "i'm fine,\nthanks."};
+ return q[rnd_n(3)];
+}
+static const char *flavor_napsta(Battle *b, Monster *m) {
+ if (m->state2 == 2) return NULL;
+ if (m->state == 1) return "* Napstablook looks just a\n little bit better.";
+ if (m->state == 2) return "* Cheering seems to have\n improved Napstablook's mood\n again.";
+ if (m->state >= 3) return "* Napstablook eagerly awaits\n your response.";
+ return NULL;
+}
+static int attack_napsta(Battle *b, Monster *m) {
+ if (b->state[2]) return -1;
+ if (b->turn == 2) return ATK_NAPSTA_TEXT;
+ if (m->state == 3 && b->state[5] == 3 && !b->state[6]) { b->state[6] = 1; return ATK_NAPSTA_HAT; }
+ if (m->state >= 3) return ATK_IDLE;
+ if (m->state2 == 2) return b->state[1] >= 6 ? ATK_NAPSTA_TEARS : ATK_NAPSTA_FLOOD;
+ return rnd_n(2) ? ATK_NAPSTA_TEARS : ATK_NAPSTA_WIGGLE;
+}
+static bool ends_napsta(Battle *b, Monster *m) {
+ if (b->state[2] == 3) m->killed = false, m->exp = 0;
+ if (b->state[2]) set_flag(F_NAPSTA, b->state[2] == 3 ? 2 : 1);
+ return b->state[2] != 0;
+}
+static bool hit_napsta(Battle *b, Monster *m, int dmg) {
+ m->hp = (int16_t)(m->hp - dmg);
+ m->hurt = 30;
+ if (m->hp <= 0) {
+ m->hp = 0;
+ b->state[2] = 3; /* "you cant kill ghosts": then gone, no EXP */
+ }
+ return true;
+}
+
+/* ---------------------------------------------------------------- the monsters */
+static const MonDef defs[MON_RUINS_COUNT] = {
+ [MON_DUMMY] = {
+ .name = "Dummy", .hp = 15, .at = 0, .df = -5, .exp = 0, .gold = 0, .spr_idle = SPR_DUMMY_B, .spr_hurt = SPR_DUMMY_B,
+ .check = "A cotton heart and a button\n eye\n* You are the apple of my eye",
+ .acts = {"Talk"}, .nacts = 1, .neutral = {"* Dummy looks like it's going\n to fall over.", "* Dummy stands around\n absentmindedly."},
+ .quotes = {"....."}, .act = act_dummy, .flavor = flavor_dummy, .flags = MF_SPARE_START},
+ [MON_FROGGIT_FIRST] = {
+ .name = "Froggit", .hp = 20, .at = 4, .df = 1, .exp = 10, .gold = 20, .spr_idle = SPR_FROGGIT_B, .spr_hurt = SPR_FROGGIT_HURT,
+ .idle_speed = 8, .check = "Life is difficult for this\n enemy.", .acts = {"Compliment", "Threaten"}, .nacts = 2,
+ .neutral = {"* Froggit doesn't seem to know\n why it's here.", "* Froggit hops to and fro.",
+ "* The battlefield is filled\n with the smell of mustard\n seed.", "* You are intimidated by\n Froggit's raw strength.\n* Only kidding."},
+ .low_hp = "* Froggit is trying to run\n away.", .spareable = "* Froggit seems reluctant to\n fight you.",
+ .act = act_froggit, .talk = talk_froggit, .attack = attack_froggit},
+ [MON_FROGGIT] = {
+ .name = "Froggit", .hp = 30, .at = 4, .df = 4, .exp = 3, .gold = 2, .spr_idle = SPR_FROGGIT_B, .spr_hurt = SPR_FROGGIT_HURT,
+ .idle_speed = 8, .check = "Life is difficult for this\n enemy.", .acts = {"Compliment", "Threaten"}, .nacts = 2,
+ .neutral = {"* Froggit doesn't seem to know\n why it's here.", "* Froggit hops to and fro.",
+ "* The battlefield is filled\n with the smell of mustard\n seed.", "* You are intimidated by\n Froggit's raw strength.\n* Only kidding."},
+ .low_hp = "* Froggit is trying to run\n away.", .spareable = "* Froggit seems reluctant to\n fight you.",
+ .act = act_froggit, .talk = talk_froggit, .attack = attack_froggit},
+ [MON_WHIMSUN] = {
+ .name = "Whimsun", .hp = 10, .at = 4, .df = 0, .exp = 2, .gold = 2, .spr_idle = SPR_WHIMSUN_B, .spr_hurt = SPR_WHIMSUN_HURT,
+ .idle_speed = 4, .dy = -10, .check = "This monster is too sensitive\n to fight...", .acts = {"Console", "Terrorize"}, .nacts = 2,
+ .neutral = {"* Whimsun continues to mutter\n apologies.", "* Whimsun avoids eye contact.", "* Whimsun is fluttering.",
+ "* It's starting to smell like\n lavender and mothballs."},
+ .flags = MF_ONE_HIT | MF_SPARE_START, .act = act_whimsun, .talk = talk_whimsun, .attack = attack_whimsun, .flavor = flavor_whimsun},
+ [MON_MOLDSMAL] = {
+ .name = "Moldsmal", .hp = 50, .at = 4, .df = 0, .exp = 3, .gold = 3, .spr_idle = SPR_MOLDSMAL_B, .spr_hurt = SPR_MOLDSMAL_B,
+ .idle_speed = 6, .check = "Stereotypical: Curvaceously\n attractive, but no brains...", .acts = {"Imitate", "Flirt"}, .nacts = 2,
+ .neutral = {"* Moldsmal waits pensively.", "* Moldsmal burbles quietly.", "* Moldsmal is ruminating.",
+ "* The sound of slurping fills\n the room.", "* Moldsmal hasn't brushed its\n teeth."},
+ .quotes = {"Squorch...", "Burble\nburb...", "*Slime\nsounds*", "*Sexy\nwiggle*"},
+ .flags = MF_SPARE_START, .act = act_moldsmal, .attack = attack_moldsmal},
+ [MON_LOOX] = {
+ .name = "Loox", .hp = 50, .at = 5, .df = 4, .exp = 7, .gold = 5, .spr_idle = SPR_LOOX_B, .spr_hurt = SPR_LOOX_HURT,
+ .idle_speed = 6, .check = "Don't pick on him.\n* Family name: Eyewalker", .acts = {"Don't Pick On", "Pick On"}, .nacts = 2,
+ .neutral = {"* Loox is staring right\n through you.", "* Loox is gazing at you.", "* Loox gnashes its teeth.",
+ "* Smells like eyedrops."},
+ .low_hp = "* Loox is watering.", .act = act_loox, .talk = talk_loox, .attack = attack_loox},
+ [MON_VEGETOID] = {
+ .name = "Vegetoid", .hp = 72, .at = 5, .df = 0, .exp = 6, .gold = 1, .spr_idle = SPR_VEGETOID_B, .spr_hurt = SPR_VEGETOID_HURT,
+ .idle_speed = 6, .check = "Serving Size: 1 Monster\n* Not monitored by the USDA", .acts = {"Talk", "Devour", "Dinner"}, .nacts = 3,
+ .neutral = {"* Vegetoid cackles softly.", "* Vegetoid's here for your\n health.", "* Vegetoid gave a mysterious\n smile.",
+ "* It smells like steamed\n carrots and peas."},
+ .low_hp = "* Vegetoid seems kind of\n bruised.", .act = act_vegetoid, .talk = talk_vegetoid, .attack = attack_vegetoid},
+ [MON_MIGOSP] = {
+ .name = "Migosp", .hp = 40, .at = 5, .df = 4, .exp = 5, .gold = 2, .spr_idle = SPR_MIGOSP_B, .spr_hurt = SPR_MIGOSP_HURT,
+ .idle_speed = 4, .check = "It seems evil, but it's just\n with the wrong crowd...", .acts = {"Talk"}, .nacts = 1,
+ .neutral = {"* Migosp skitters around.", "* Migosp is knitting its brow.", "* Migosp is whispering to the\n others.",
+ "* It's starting to smell like a\n roach motel."},
+ .low_hp = "* Migosp refuses to give up.", .act = act_migosp, .talk = talk_migosp, .attack = attack_migosp,
+ .flavor = flavor_migosp},
+ [MON_NAPSTABLOOK] = {
+ .name = "Napstablook", .hp = 88, .at = 3, .df = 4, .exp = 0, .gold = 0, .spr_idle = SPR_NAPSTABLOOK_B, .spr_hurt = SPR_NAPSTABLOOK_B,
+ .idle_speed = 6, .dy = -16, .check = "This monster doesn't seem to\n have a sense of humor...", .acts = {"Flirt", "Threat", "Cheer"}, .nacts = 3,
+ .neutral = {"* Napstablook is wishing they\n weren't here.", "* Napstablook is staring into\n the distance.",
+ "* Napstablook is pretending to\n sleep.", "* The faint odor of ectoplasm\n permeates the vicinity."},
+ .flags = MF_BOSS, .act = act_napsta, .talk = talk_napsta, .attack = attack_napsta, .flavor = flavor_napsta,
+ .on_hit = hit_napsta, .ends = ends_napsta},
+};
+
+extern const MonDef toriel_def;
+const MonDef *mon_def(int id) {
+ if (id >= MON_RUINS_COUNT) return snow_mon_def(id);
+ return id == MON_TORIEL ? &toriel_def : &defs[id];
+}
+
+/* ---------------------------------------------------------------- formations */
+static const Form forms[FORM_RUINS_COUNT] = {
+ [FORM_DUMMY] = {{MON_DUMMY}, 1, "* You encountered the Dummy."},
+ [FORM_FROGGIT_FIRST] = {{MON_FROGGIT_FIRST}, 1, "* Froggit attacks you!"},
+ [FORM_FROGGIT] = {{MON_FROGGIT}, 1, "* Froggit hopped close!"},
+ [FORM_WHIMSUN] = {{MON_WHIMSUN}, 1, "* Whimsun approached meekly!"},
+ [FORM_FROGGIT_WHIMSUN] = {{MON_FROGGIT, MON_WHIMSUN}, 2, "* Froggit and Whimsun drew\n near!"},
+ [FORM_FROGGIT2] = {{MON_FROGGIT, MON_FROGGIT}, 2, "* A pair of Froggits hop\n towards you."},
+ [FORM_MOLDSMAL] = {{MON_MOLDSMAL}, 1, "* Moldsmal blocks the way!"},
+ [FORM_MOLDSMAL2] = {{MON_MOLDSMAL, MON_MOLDSMAL}, 2, "* Moldsmal and Moldsmal block\n the way."},
+ [FORM_MOLDSMAL3] = {{MON_MOLDSMAL, MON_MOLDSMAL, MON_MOLDSMAL}, 3, "* You're surrounded by\n Moldsmal!"},
+ [FORM_MOLDSMAL_MIGOSP] = {{MON_MIGOSP, MON_MOLDSMAL}, 2, "* Migosp crawled up close!"},
+ [FORM_LOOX] = {{MON_LOOX}, 1, "* Loox drew near!"},
+ [FORM_LOOX2] = {{MON_LOOX, MON_LOOX}, 2, "* A pair of Loox decided to\n pick on you!"},
+ [FORM_VEGETOID] = {{MON_VEGETOID}, 1, "* Vegetoid came out of the\n earth!"},
+ [FORM_VEGETOID2] = {{MON_VEGETOID, MON_VEGETOID}, 2, "* A pair of Vegetoids came out\n of the ground!"},
+ [FORM_VEGETOID_MIGOSP] = {{MON_MIGOSP, MON_VEGETOID}, 2, "* Vegetoid came out of the\n earth!"},
+ [FORM_LOOX_VEG_MIGOSP] = {{MON_VEGETOID, MON_LOOX, MON_MIGOSP}, 3, "* Loox and co. decided to pick\n on you!"},
+ [FORM_NAPSTABLOOK] = {{MON_NAPSTABLOOK}, 1, "* Here comes Napstablook."},
+ [FORM_TORIEL] = {{MON_TORIEL}, 1, "* Toriel blocks the way!"},
+ [FORM_FLOWEY] = {{MON_FLOWEY}, 1, ""},
+};
+
+int form_setup(int form, Battle *b) {
+ const Form *f = form >= FORM_RUINS_COUNT ? snow_form(form) : &forms[form];
+ static const int16_t xs[3][3] = {{160}, {108, 212}, {70, 160, 250}};
+ b->nmon = f->n;
+ for (int i = 0; i < f->n; i++) {
+ Monster *m = &b->mon[i];
+ memset(m, 0, sizeof *m);
+ m->def = mon_def(f->mon[i]);
+ m->x = (int16_t)(xs[f->n - 1][i] + m->def->dx), m->y = (int16_t)(120 + m->def->dy);
+ }
+ /* Toriel and Flowey can't be fled from; Napstablook can */
+ if (form == FORM_TORIEL || form == FORM_FLOWEY || form == FORM_PAPYRUS || form == FORM_MAD_DUMMY ||
+ form == FORM_RGUARDS || form == FORM_BOMB || form == FORM_MUFFET || form == FORM_MTT_TILES || form == FORM_MTT_BOX ||
+ form == FORM_MTT_EX || form == FORM_ASGORE)
+ b->no_flee = true;
+ b->captures = form == FORM_PAPYRUS;
+ b->no_mercy = form == FORM_ASGORE;
+ return 0;
+}
+const char *form_text(int form) { return form >= FORM_RUINS_COUNT ? snow_form(form)->text : forms[form].text; }
+
+/* ---------------------------------------------------------------- attacks */
+/* the board's inside, in pixels */
+static int bx0(void) { return bt.x0 + 3; }
+static int bx1(void) { return bt.x1 - 3; }
+static int by0(void) { return bt.y0 + 3; }
+static int by1(void) { return bt.y1 - 3; }
+#define P(v) ((v) * FX)
+
+static Bullet *shot(uint16_t spr, int x, int y, int vx, int vy) {
+ Bullet *b = bullet_new(spr, P(x), P(y), vx, vy);
+ if (b) b->owner = (uint8_t)bt.atk_owner;
+ return b;
+}
+
+static int toward(int from, int to, int sp, int dist) { return dist ? (to - from) * sp / dist : 0; }
+static int isqrt(int v) {
+ int r = 0;
+ while ((r + 1) * (r + 1) <= v) r++;
+ return r;
+}
+
+static void atk_frog_flies(int t) {
+ /* five flies come down one after another, stop, then dart at the SOUL */
+ if (t < 0) return;
+ if (t < 50 && t % 10 == 0) {
+ Bullet *b = shot(SPR_FLYBULLET, bx0() + 6 + rnd_n(bx1() - bx0() - 12), by0() - 6, 0, P(3) / 2);
+ if (b) b->kind = BK_SCRIPT, b->anim = 3, b->a = (int16_t)(by0() + 6 + rnd_n(20)), b->b = 0;
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT) continue;
+ if (b->b == 0 && b->y / FX >= b->a) b->vy = 0, b->b = 1, b->c = 0;
+ else if (b->b == 1 && ++b->c >= 14) {
+ int dx = bt.sx - b->x, dy = bt.sy - b->y, d = isqrt((dx / 4) * (dx / 4) + (dy / 4) * (dy / 4)) * 4;
+ b->vx = toward(b->x, bt.sx, P(2), d), b->vy = toward(b->y, bt.sy, P(2), d);
+ b->b = 2;
+ }
+ b->x += b->vx, b->y += b->vy;
+ b->t++;
+ if (b->t % 3 == 0) b->frame++;
+ }
+}
+
+static void atk_frog_jump(int t) {
+ /* a little frog hops from the bottom right to the other side, with random force */
+ if (t < 0) return;
+ if (t == 0) {
+ Bullet *b = shot(SPR_SMALLFROGBULLET, bx1() - 8, by1() - 8, 0, 0);
+ if (b) b->kind = BK_SCRIPT, b->a = 20, b->r = 7;
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT) continue;
+ if (b->a > 0) {
+ b->frame = 0;
+ if (--b->a == 0) b->vx = -(P(1) + rnd_n(P(3) / 2)), b->vy = -(P(2) + rnd_n(P(1)));
+ continue;
+ }
+ b->frame = 1;
+ b->vy += P(1) / 7;
+ b->x += b->vx, b->y += b->vy;
+ if (b->y >= P(by1() - 8)) {
+ b->y = P(by1() - 8), b->vy = 0, b->vx = 0, b->a = 18;
+ }
+ }
+}
+
+static void atk_frog_rain(int t) {
+ /* flies falling smoothly down the board, not aimed */
+ if (t < 0) return;
+ if (t < 100 && t % 9 == 0) {
+ Bullet *b = shot(SPR_FLYBULLET, bx0() + 4 + rnd_n(bx1() - bx0() - 8), by0() - 6, 0, P(1));
+ if (b) b->anim = 3;
+ }
+}
+
+static void atk_whim_ring(int t) {
+ /* a ring of moths around the board's middle, growing and shrinking */
+ int n = 10;
+ if (t < 0) return;
+ if (t == 0)
+ for (int k = 0; k < n; k++) {
+ Bullet *b = shot(SPR_BUTTERFLYBLLT, board_cx(), board_cy(), 0, 0);
+ if (b) b->kind = BK_SCRIPT, b->a = (int16_t)(k * 256 / n), b->anim = 4;
+ }
+ int r = 26 + gfx_sin(t * 2) * 14 / 1024;
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT) continue;
+ int a = b->a + t;
+ b->x = P(board_cx()) + r * gfx_cos(a) * FX / 1024, b->y = P(board_cy()) + r * gfx_sin(a) * FX / 1024;
+ if (++b->t % 4 == 0) b->frame++;
+ }
+}
+
+static void atk_whim_cols(int t) {
+ /* two columns of moths rising on either side of the SOUL, following it */
+ if (t < 0) return;
+ if (t == 0) bt.as[0] = bt.sx;
+ bt.as[0] += (bt.sx - bt.as[0]) / 12;
+ if (t < 120 && t % 7 == 0)
+ for (int s = -1; s <= 1; s += 2) {
+ Bullet *b = shot(SPR_BUTTERFLYBLLT, 0, by1() + 6, 0, -P(3) / 2);
+ if (b) b->kind = BK_SCRIPT, b->a = (int16_t)s, b->anim = 4, b->x = bt.as[0] + s * P(16);
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT) continue;
+ b->x = bt.as[0] + b->a * P(16);
+ b->y += b->vy;
+ if (++b->t % 4 == 0) b->frame++;
+ if (b->y < P(by0() - 10)) b->kind = BK_NONE;
+ }
+}
+
+static void atk_mold_zig(int t) {
+ /* pellets falling in a zigzag */
+ if (t < 0) return;
+ if (t < 110 && t % 10 == 0) {
+ Bullet *b = shot(SPR_BULLETMD, bx0() + 8 + rnd_n(bx1() - bx0() - 16), by0() - 4, P(1), P(1));
+ if (b) b->kind = BK_SCRIPT;
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT) continue;
+ if (++b->t % 14 == 0) b->vx = -b->vx;
+ b->x += b->vx, b->y += b->vy;
+ }
+}
+
+static void atk_mold_burst(int t) {
+ /* pellets that fall a little way, then burst into rings of smaller ones */
+ if (t < 0) return;
+ if (t < 100 && t % 25 == 0) {
+ Bullet *b = shot(SPR_BULLETMD, bx0() + 10 + rnd_n(bx1() - bx0() - 20), by0() - 4, 0, P(1));
+ if (b) b->kind = BK_SCRIPT, b->a = (int16_t)(12 + rnd_n(16));
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT) continue;
+ b->y += b->vy;
+ if (++b->t >= b->a) {
+ for (int k = 0; k < 8; k++) {
+ Bullet *s = shot(SPR_BULLETSM, b->x / FX, b->y / FX, gfx_cos(k * 32) * P(1) / 1024, gfx_sin(k * 32) * P(1) / 1024);
+ if (s) s->life = 60;
+ }
+ b->kind = BK_NONE;
+ }
+ }
+}
+
+static void atk_loox_bounce(int t) {
+ /* spheres come in from the sides and bounce around the board, faster and faster */
+ bool mad = bt.mon[bt.atk_owner].state2;
+ if (t < 0) return;
+ if (t < 40 && t % 8 == 0) {
+ int s = (t / 8) % 2 ? 1 : -1;
+ Bullet *b = shot(SPR_CIRCLEBULLETMD1, s < 0 ? bx0() - 4 : bx1() + 4, by0() + 8 + rnd_n(by1() - by0() - 16), -s * P(1), (rnd_n(2) ? 1 : -1) * P(1));
+ if (b) b->kind = BK_SCRIPT, b->a = 0;
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT) continue;
+ if (++b->t % 20 == 0) b->vx += b->vx / (mad ? 4 : 8), b->vy += b->vy / (mad ? 4 : 8);
+ b->x += b->vx, b->y += b->vy;
+ if (b->t > 8) {
+ if (b->x < P(bx0() + 3) || b->x > P(bx1() - 3)) b->vx = -b->vx;
+ if (b->y < P(by0() + 3) || b->y > P(by1() - 3)) b->vy = -b->vy;
+ }
+ }
+}
+
+static void atk_loox_worm(int t) {
+ /* worms of spheres wiggling across the board */
+ bool mad = bt.mon[bt.atk_owner].state2;
+ if (t < 0) return;
+ int every = mad ? 16 : 24;
+ if (t < 110 && t % every == 0) {
+ int s = (t / every) % 2 ? 1 : -1, y = by0() + 10 + rnd_n(by1() - by0() - 20);
+ for (int k = 0; k < 4; k++) {
+ Bullet *b = shot(SPR_CIRCLEBULLETSM, (s < 0 ? bx0() - 4 - k * 6 : bx1() + 4 + k * 6), y, -s * (mad ? P(3) / 2 : P(1)), 0);
+ if (b) b->kind = BK_SCRIPT, b->a = (int16_t)y, b->b = (int16_t)(k * 20);
+ }
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT) continue;
+ b->t++;
+ b->x += b->vx;
+ b->y = P(b->a) + gfx_sin(b->t * 6 + b->b) * P(6) / 1024;
+ if ((b->vx > 0 && b->x > P(bx1() + 8)) || (b->vx < 0 && b->x < P(bx0() - 8))) b->kind = BK_NONE;
+ }
+}
+
+static void atk_veg_fall(int t) {
+ /* vegetables fall and bounce off the bottom; after Dinner, one of them is green */
+ Monster *m = &bt.mon[bt.atk_owner];
+ if (t < 0) return;
+ if (t < 100 && t % 14 == 0) {
+ Bullet *b = shot(SPR_VEGBULLET, bx0() + 8 + rnd_n(bx1() - bx0() - 16), by0() - 6, (rnd_n(2) ? 1 : -1) * rnd_n(P(1)), P(1));
+ if (b) {
+ b->kind = BK_SCRIPT, b->frame = (uint8_t)rnd_n(3), b->a = 0;
+ if (m->state == 1 && t == 42) b->color = BC_GREEN;
+ }
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT) continue;
+ b->vy += P(1) / 12;
+ b->x += b->vx, b->y += b->vy;
+ if (b->y > P(by1() - 5) && b->vy > 0 && !b->a) b->vy = -b->vy * 2 / 3, b->a = 1;
+ }
+ if (m->state == 1 && t == 99) m->state = 2, m->spare = true;
+}
+
+static void atk_veg_carrot(int t) {
+ /* carrots dropping straight down */
+ Monster *m = &bt.mon[bt.atk_owner];
+ if (t < 0) return;
+ if (t < 110 && t % 11 == 0) {
+ Bullet *b = shot(SPR_CARROTBULLET, bx0() + 6 + rnd_n(bx1() - bx0() - 12), by0() - 8, 0, P(2));
+ if (b && m->state == 1 && t == 44) b->color = BC_GREEN;
+ if (b) b->kind = BK_SCRIPT;
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT) continue;
+ b->y += b->vy;
+ if (b->y > P(by1())) b->kind = BK_NONE;
+ }
+ if (m->state == 1 && t == 109) m->state = 2, m->spare = true;
+}
+
+static void atk_migosp_roach(int t) {
+ /* roaches flying up the left and the right halves */
+ if (t < 0) return;
+ if (t < 110 && t % 8 == 0) {
+ int left = (t / 8) % 2, w = (bx1() - bx0()) / 2 - 8;
+ int x = left ? bx0() + 4 + rnd_n(w) : board_cx() + 4 + rnd_n(w);
+ Bullet *b = shot(SPR_ROACHBULLET, x, by1() + 6, 0, -P(3) / 2);
+ if (b) b->anim = 3;
+ }
+}
+
+static void atk_migosp_dance(int t) {
+ /* a tiny roach dancing at the bottom: touching it ends the turn */
+ if (t < 0) return;
+ if (t == 0) {
+ Bullet *b = shot(SPR_ROACHDANCE, board_cx(), by1() - 6, P(1), 0);
+ if (b) b->kind = BK_SCRIPT, b->anim = 5, b->flags = BF_HARMLESS;
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT) continue;
+ b->x += b->vx;
+ if (b->x < P(bx0() + 6) || b->x > P(bx1() - 6)) b->vx = -b->vx;
+ if (++b->t % 5 == 0) b->frame++;
+ int dx = b->x / FX - soul_x(), dy = b->y / FX - soul_y();
+ if (dx * dx + dy * dy < 64) bt.atk_t = bt.atk_len;
+ }
+}
+
+static void atk_napsta_tears(int t, int every) {
+ /* tears arching out of Napstablook's eyes, then falling straight */
+ if (t < 0) return;
+ if (t < 120 && t % every == 0) {
+ int s = rnd_n(2) ? 1 : -1;
+ Bullet *b = shot(SPR_TEARDROP, board_cx() + s * 10, by0() - 4, s * (P(1) / 2 + rnd_n(P(1))), -P(1));
+ if (b) b->kind = BK_GRAVITY, b->ay = P(1) / 16, b->dmg = 3;
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind == BK_GRAVITY && b->vy > P(1)) b->vx = b->vx * 7 / 8;
+ }
+}
+
+static void atk_napsta_wiggle(int t) {
+ /* long wiggly tears turning at the walls, falling once over the SOUL */
+ if (t < 0) return;
+ if (t < 100 && t % 20 == 0) {
+ Bullet *b = shot(SPR_TEARDROP, bx0() + 6 + rnd_n(bx1() - bx0() - 12), by0() + 2, (rnd_n(2) ? 1 : -1) * P(3) / 2, 0);
+ if (b) b->kind = BK_SCRIPT, b->dmg = 3, b->a = 0;
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT) continue;
+ b->t++;
+ if (!b->a) {
+ b->x += b->vx;
+ b->y = b->y + (gfx_sin(b->t * 10) > 0 ? FX / 4 : -FX / 4);
+ if (b->x < P(bx0() + 4) || b->x > P(bx1() - 4)) b->vx = -b->vx;
+ if (abs_i(b->x - bt.sx) < P(3) && b->t > 15) b->a = 1, b->vy = P(2);
+ } else
+ b->y += b->vy;
+ }
+}
+
+static void atk_napsta_text(int t) {
+ /* REALLY NOT FEELIN UP TO IT RIGHT NOW. SORRY. in grey, harmless: drawn by attack_draw */
+ (void)t;
+}
+
+static void atk_napsta_hat(int t) {
+ /* the tears rise to make a top hat on Napstablook's head */
+ if (t < 0) return;
+ if (t < 60 && t % 4 == 0) {
+ Bullet *b = shot(SPR_TEARDROP, bx0() + 6 + rnd_n(bx1() - bx0() - 12), by1() - 2, 0, -P(2));
+ if (b) b->flags |= BF_HARMLESS, b->kind = BK_SCRIPT;
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT) continue;
+ b->y += b->vy;
+ if (b->y < P(by0())) b->kind = BK_NONE;
+ }
+ if (t == 70) bt.mon[0].spr_override = SPR_NAPSTA_HAT;
+}
+
+static const int16_t lengths[ATK_RUINS_COUNT] = {
+ [ATK_FROG_FLIES] = 150, [ATK_FROG_JUMP] = 150, [ATK_FROG_RAIN] = 150, [ATK_WHIM_RING] = 150, [ATK_WHIM_COLS] = 150,
+ [ATK_MOLD_ZIG] = 150, [ATK_MOLD_BURST] = 150, [ATK_LOOX_BOUNCE] = 160, [ATK_LOOX_WORM] = 150, [ATK_VEG_FALL] = 150,
+ [ATK_VEG_CARROT] = 150, [ATK_MIGOSP_ROACH] = 150, [ATK_MIGOSP_DANCE] = 180, [ATK_NAPSTA_TEARS] = 150,
+ [ATK_NAPSTA_WIGGLE] = 150, [ATK_NAPSTA_TEXT] = 120, [ATK_NAPSTA_HAT] = 100, [ATK_NAPSTA_FLOOD] = 150,
+ [ATK_TOR_HANDS] = 170, [ATK_TOR_WAVES] = 180, [ATK_TOR_HELIX] = 180, [ATK_TOR_FALTER] = 150, [ATK_IDLE] = 60,
+};
+int attack_length(int atk) {
+ if (atk >= ATK_RUINS_COUNT) return snow_attack_length(atk);
+ return atk >= 0 && lengths[atk] ? lengths[atk] : 150;
+}
+
+void toriel_attack(int atk, int t); /* toriel.c */
+
+void attack_run(Battle *b, int atk, int t) {
+ (void)b;
+ switch (atk) {
+ case ATK_FROG_FLIES: atk_frog_flies(t); break;
+ case ATK_FROG_JUMP: atk_frog_jump(t); break;
+ case ATK_FROG_RAIN: atk_frog_rain(t); break;
+ case ATK_WHIM_RING: atk_whim_ring(t); break;
+ case ATK_WHIM_COLS: atk_whim_cols(t); break;
+ case ATK_MOLD_ZIG: atk_mold_zig(t); break;
+ case ATK_MOLD_BURST: atk_mold_burst(t); break;
+ case ATK_LOOX_BOUNCE: atk_loox_bounce(t); break;
+ case ATK_LOOX_WORM: atk_loox_worm(t); break;
+ case ATK_VEG_FALL: atk_veg_fall(t); break;
+ case ATK_VEG_CARROT: atk_veg_carrot(t); break;
+ case ATK_MIGOSP_ROACH: atk_migosp_roach(t); break;
+ case ATK_MIGOSP_DANCE: atk_migosp_dance(t); break;
+ case ATK_NAPSTA_TEARS: atk_napsta_tears(t, 9); break;
+ case ATK_NAPSTA_FLOOD: atk_napsta_tears(t, 3); break;
+ case ATK_NAPSTA_WIGGLE: atk_napsta_wiggle(t); break;
+ case ATK_NAPSTA_TEXT: atk_napsta_text(t); break;
+ case ATK_NAPSTA_HAT: atk_napsta_hat(t); break;
+ case ATK_TOR_HANDS: case ATK_TOR_WAVES: case ATK_TOR_HELIX: case ATK_TOR_FALTER: toriel_attack(atk, t); break;
+ default: if (atk >= ATK_RUINS_COUNT) snow_attack_run(b, atk, t); break;
+ }
+}
+
+void toriel_draw_attack(int atk, int t); /* toriel.c */
+
+void attack_draw(Battle *b, int atk, int t) {
+ (void)b;
+ if (atk == ATK_NAPSTA_TEXT) {
+ /* the grey words drift down: harmless */
+ static const char *const words[] = {"REALLY", "NOT", "FEELIN", "UP", "TO", "IT", "RIGHT", "NOW.", "SORRY."};
+ for (int k = 0; k < 9; k++) {
+ int y = by0() - 10 + (t - k * 8) * 2 / 3;
+ if (y > by0() - 12 && y < by1()) gfx_text(FONT_SMALL, bx0() + 4 + (k * 23) % (bx1() - bx0() - 30), y, words[k], rgb565(0x808080), 255);
+ }
+ }
+ if (atk >= ATK_TOR_HANDS && atk <= ATK_TOR_FALTER) toriel_draw_attack(atk, t);
+ if (atk >= ATK_RUINS_COUNT) snow_attack_draw(b, atk, t);
+}
diff --git a/games/undertale/src/monsters.h b/games/undertale/src/monsters.h
new file mode 100644
index 00000000..8c610dfc
--- /dev/null
+++ b/games/undertale/src/monsters.h
@@ -0,0 +1,107 @@
+/* Undertale: the monsters, their formations and their attacks. */
+#ifndef UT_MONSTERS_H
+#define UT_MONSTERS_H
+#include
+
+enum {
+ MON_DUMMY, MON_FROGGIT_FIRST, MON_FROGGIT, MON_WHIMSUN, MON_MOLDSMAL, MON_LOOX, MON_VEGETOID, MON_MIGOSP,
+ MON_NAPSTABLOOK, MON_TORIEL, MON_FLOWEY, MON_RUINS_COUNT,
+ /* Snowdin (snowmon.c, papyrus.c) */
+ MON_SNOWDRAKE = MON_RUINS_COUNT, MON_ICECAP, MON_GYFTROT, MON_DOGGO, MON_LESSERDOG, MON_DOGAMY, MON_DOGARESSA,
+ MON_GREATERDOG, MON_JERRY, MON_CHILLDRAKE, MON_GLYDE, MON_PAPYRUS, MON_SNOW_COUNT,
+ /* Waterfall (wfmon.c) */
+ MON_AARON = MON_SNOW_COUNT, MON_WOSHUA, MON_MOLDBYGG, MON_SHYREN, MON_MAD_DUMMY, MON_TEMMIE, MON_UNDYNE, MON_WF_COUNT,
+ /* Hotland (htmon.c) */
+ MON_VULKIN = MON_WF_COUNT, MON_TSUNDERPLANE, MON_PYROPE, MON_METTATON, MON_RG01, MON_RG02, MON_BOMB, MON_MUFFET,
+ MON_MTT_TILES, MON_HT_COUNT,
+ /* the CORE */
+ MON_FFROGGIT = MON_HT_COUNT, MON_WHIMSALOT, MON_ASTIG, MON_MADJICK, MON_KNIGHT, MON_MTT_BOX, MON_MTT_EX, MON_CORE_COUNT,
+ /* New Home */
+ MON_ASGORE = MON_CORE_COUNT, MON_OMEGA, MON_COUNT
+};
+
+/* formations: who is fought */
+enum {
+ FORM_DUMMY, FORM_FROGGIT_FIRST, FORM_FROGGIT, FORM_WHIMSUN, FORM_FROGGIT_WHIMSUN, FORM_FROGGIT2, FORM_MOLDSMAL,
+ FORM_MOLDSMAL2, FORM_MOLDSMAL3, FORM_MOLDSMAL_MIGOSP, FORM_LOOX, FORM_LOOX2, FORM_VEGETOID, FORM_VEGETOID2,
+ FORM_VEGETOID_MIGOSP, FORM_LOOX_VEG_MIGOSP, FORM_NAPSTABLOOK, FORM_TORIEL, FORM_FLOWEY, FORM_RUINS_COUNT,
+ /* Snowdin */
+ FORM_SNOWDRAKE = FORM_RUINS_COUNT, FORM_ICECAP, FORM_LESSERDOG, FORM_CHILLDRAKE, FORM_ICECAP_JERRY,
+ FORM_ICECAP_JERRY_SNOWDRAKE, FORM_ICECAP_JERRY_CHILLDRAKE, FORM_GYFTROT, FORM_GLYDE, FORM_DOGGO, FORM_DOGI,
+ FORM_GREATERDOG, FORM_PAPYRUS, FORM_SNOW_COUNT,
+ /* Waterfall */
+ FORM_AARON = FORM_SNOW_COUNT, FORM_WOSHUA, FORM_WOSHUA_AARON, FORM_MOLDBYGG, FORM_MOLDBYGG_MOLDSMAL,
+ FORM_MOLDBYGG_WOSHUA, FORM_SHYREN, FORM_MAD_DUMMY, FORM_TEMMIE, FORM_TEMMIE_MOLDBYGG, FORM_UNDYNE, FORM_WF_COUNT,
+ /* Hotland (Mettaton's shows and a laser's alarm play their own way: htshows.c) */
+ FORM_VULKIN = FORM_WF_COUNT, FORM_TSUNDERPLANE, FORM_TSUNDER_VULKIN, FORM_PYROPE, FORM_PYROPE2, FORM_MTT_QUIZ,
+ FORM_MTT_COOK, FORM_LASERS, FORM_RGUARDS, FORM_BOMB, FORM_MUFFET, FORM_MTT_TILES, FORM_HT_COUNT,
+ /* the CORE */
+ FORM_ASTIG = FORM_HT_COUNT, FORM_WHIM_FROG, FORM_WHIM_ASTIG, FORM_FROG_ASTIG, FORM_KNIGHT_MADJICK, FORM_FROG_ASTIG_WHIM,
+ FORM_KNIGHT, FORM_MADJICK, FORM_MTT_BOX, FORM_MTT_EX, FORM_CORE_COUNT,
+ /* New Home */
+ FORM_ASGORE = FORM_CORE_COUNT, FORM_OMEGA, FORM_COUNT
+};
+
+enum {
+ ATK_NONE, ATK_FROG_FLIES, ATK_FROG_JUMP, ATK_FROG_RAIN, ATK_WHIM_RING, ATK_WHIM_COLS, ATK_MOLD_ZIG, ATK_MOLD_BURST,
+ ATK_LOOX_BOUNCE, ATK_LOOX_WORM, ATK_VEG_FALL, ATK_VEG_CARROT, ATK_MIGOSP_ROACH, ATK_MIGOSP_DANCE,
+ ATK_NAPSTA_TEARS, ATK_NAPSTA_WIGGLE, ATK_NAPSTA_TEXT, ATK_NAPSTA_HAT, ATK_NAPSTA_FLOOD,
+ ATK_TOR_HANDS, ATK_TOR_WAVES, ATK_TOR_HELIX, ATK_TOR_FALTER, ATK_IDLE, ATK_RUINS_COUNT,
+ /* Snowdin */
+ ATK_DRAKE_ARCS = ATK_RUINS_COUNT, ATK_DRAKE_LINES, ATK_ICECAP_HATS, ATK_ICECAP_WAVES, ATK_GYFT_PRESENTS,
+ ATK_GYFT_SNOW, ATK_DOGGO_SWORD, ATK_LDOG_JUMP, ATK_LDOG_SPEARS, ATK_DOGI_AXES, ATK_DOGI_HEARTS, ATK_GDOG_SPEAR,
+ ATK_GDOG_BARK, ATK_GLYDE_STARS, ATK_PAP_FIRST, ATK_PAP_LAST = ATK_PAP_FIRST + 24, ATK_SNOW_COUNT,
+ /* Waterfall */
+ ATK_AARON_ARMS = ATK_SNOW_COUNT, ATK_AARON_SWEAT, ATK_WOSHUA_DROPS, ATK_WOSHUA_BUBBLES, ATK_MOLDBYGG_HOPS,
+ ATK_MOLDBYGG_RAIN, ATK_SHYREN_NOTES, ATK_DUMMY_MINIONS, ATK_DUMMY_MISSILES, ATK_TEMMIE_HOPS,
+ ATK_UNDYNE_ARROWS, ATK_UNDYNE_SPEARS, ATK_WF_COUNT,
+ /* Hotland */
+ ATK_VULKIN_THUNDER = ATK_WF_COUNT, ATK_VULKIN_FIRE, ATK_TSUNDER_BOMBS, ATK_TSUNDER_DIVE, ATK_PYROPE_BOMBS,
+ ATK_PYROPE_ROPE, ATK_RG_TEAM, ATK_MUFFET_SPIDERS, ATK_MUFFET_CROISSANTS, ATK_MUFFET_DONUTS, ATK_MUFFET_PET,
+ ATK_MTT_BOXES, ATK_MTT_YELLOW, ATK_HT_COUNT,
+ /* the CORE */
+ ATK_MADJICK_CHASER = ATK_HT_COUNT, ATK_MADJICK_CORNER, ATK_KNIGHT_MOON, ATK_KNIGHT_SUN, ATK_KNIGHT_STAR,
+ /* (Mettaton EX's from here on) */
+ ATK_EX_LEGS, ATK_EX_BOMBS, ATK_EX_REWIND, ATK_EX_ARMS, ATK_EX_HEART, ATK_EX_LASER, ATK_EX_BOTS, ATK_EX_DISCO,
+ ATK_EX_PAIRS, ATK_EX_BREAK, ATK_CORE_COUNT,
+ /* New Home */
+ ATK_ASG_HELIX = ATK_CORE_COUNT, ATK_ASG_RAIN, ATK_ASG_RINGS, ATK_ASG_HANDS, ATK_ASG_TRIDENT, ATK_COUNT
+};
+
+/* a formation: up to three monsters, and what the first turn says */
+typedef struct {
+ uint8_t mon[3];
+ uint8_t n;
+ const char *text;
+} Form;
+struct Battle;
+const Form *snow_form(int form); /* snowmon.c */
+const struct MonDef *snow_mon_def(int id);
+void snow_attack_run(struct Battle *b, int atk, int t);
+void snow_attack_draw(struct Battle *b, int atk, int t);
+int snow_attack_length(int atk);
+int snow_encounter(int room); /* a random encounter's formation there, -1 none */
+const Form *wf_form(int form); /* wfmon.c */
+const struct MonDef *wf_mon_def(int id);
+void wf_attack_run(struct Battle *b, int atk, int t);
+int wf_attack_length(int atk);
+int wf_encounter(int room);
+const Form *ht_form(int form); /* htmon.c */
+const struct MonDef *ht_mon_def(int id);
+void ht_attack_run(struct Battle *b, int atk, int t);
+int ht_attack_length(int atk);
+int ht_encounter(int room);
+const Form *core_form(int form); /* coremon.c */
+const struct MonDef *core_mon_def(int id);
+void core_attack_run(struct Battle *b, int atk, int t);
+int core_attack_length(int atk);
+int core_encounter(int room);
+const Form *nh_form(int form); /* nhmon.c */
+const struct MonDef *nh_mon_def(int id);
+void nh_attack_run(struct Battle *b, int atk, int t);
+int nh_attack_length(int atk);
+void mttex_reset(void); /* before Mettaton EX's battle: the show's ratings back to the start */
+int mttex_ratings(void);
+extern int g_bomb_kind; /* the bomb fought next (BOMB_*: htmon.c) */
+enum { BOMB_DOG, BOMB_WATER, BOMB_SCRIPT, BOMB_BALL, BOMB_PRESENT, BOMB_GAME, BOMB_COUNT };
+#endif
diff --git a/games/undertale/src/newhome.c b/games/undertale/src/newhome.c
new file mode 100644
index 00000000..e8b34260
--- /dev/null
+++ b/games/undertale/src/newhome.c
@@ -0,0 +1,342 @@
+/* New Home, the capital: the elevator up from the CORE, the grey street to Asgore's home; his home (the stairs down
+ * chained shut, a key in the kitchen and one in the hallway, two presents in the children's room); the basement's
+ * corridors, where the story of Asriel and the first human is told as Frisk walks; the Last Corridor and Sans's
+ * judgment; the throne room, Asgore among his flowers; the coffins below; the barrier, Asgore's battle and Frisk's
+ * choice; Flowey, the six SOULs and Omega Flowey; the way out, and Sans's call after. (As the Undertale Wiki describes
+ * them; what they say is ours, in short.) */
+#include
+#include "battle.h"
+#include "event.h"
+#include "events.h"
+#include "game.h"
+#include "gfx.h"
+#include "hotland.h"
+#include "items.h"
+#include "menu.h"
+#include "monsters.h"
+#include "newhome.h"
+#include "res.h"
+#include "save.h"
+#include "snowdin.h"
+#include "world.h"
+
+static void gone(Obj *o) { o->kind = OBJ_NONE; }
+static Obj *thing(int x, int y, int w, int h, int event, int name, int arg) {
+ Obj *o = obj_trigger(x, y, w, h, event, name);
+ if (o) o->flags = OF_TALK | OF_SOLID, o->arg = (int16_t)arg;
+ return o;
+}
+static Obj *touch(int x, int y, int w, int h, int event, int arg) {
+ Obj *o = obj_trigger(x, y, w, h, event, ACTOR_TRIGGER);
+ if (o) o->arg = (int16_t)arg;
+ return o;
+}
+static Obj *npc(int name, uint16_t spr, int x, int y, int dir) {
+ const uint16_t s[4] = {spr, spr, spr, spr};
+ Obj *o = obj_npc(name, s, x, y, dir, 0);
+ if (o) o->flags &= (uint16_t)~OF_SOLID;
+ return o;
+}
+
+/* the story, told in the basement as Frisk walks (in short, in our words): corridor 1, then 2 */
+#define NSTORY 11
+static const int16_t story_x[NSTORY] = {1650, 1850, 2050, 2250, 2450, 2800, 3200, 3600, 4000, 4350, 4650};
+static const char *const story[NSTORY] = {
+ "* Long ago, a human child fell\n into the RUINS, hurt, and cried\n out for help.",
+ "* The king's son, ASRIEL, found\n the child and brought it back\n to the castle.",
+ "* The two became like brother and\n sister. The king and queen\n raised the child as their own.|* Down here, there was hope\n again.",
+ "* But one day, the child became\n gravely ill.",
+ "* Its one wish was to see the\n flowers of its home village.\n* There was no way to grant it.|* The next day, the child was\n gone.",
+ "* Grieving, ASRIEL took the\n child's SOUL into himself, and\n grew powerful.|* He passed through the barrier,\n bearing the child home.",
+ "* In the village, he found a bed\n of golden flowers, and laid the\n child there.|* The villagers saw him beside\n their dead child, and believed\n he had killed it.",
+ "* They attacked him with all\n their strength. He could have\n wiped them out.|* Instead, he held back. He only\n smiled, and left.",
+ "* Badly hurt, ASRIEL came home\n and collapsed. His dust settled\n over the garden.",
+ "* In one night, the king and\n queen had lost both children.|* So the king made a decree: any\n human who fell here would die.|* With enough SOULs, the barrier\n could be broken.",
+ "* Soon, now. King ASGORE will\n free us all.|* ...why aren't you smiling?\n* Aren't you glad?\n* Soon, you'll be free.",
+};
+
+void nh_setup(int room, int from) {
+ (void)from;
+ if (room < ROOM_NH_TOWN || room > ROOM_NH_WALL) return;
+ set_flag(F_NH_IN, 1);
+ switch (room) {
+ case ROOM_NH_TOWN:
+ thing(76, 4490, 48, 46, EV_NH_LIFT, ACTOR_DOOR, 0);
+ thing(280, 4272, 44, 38, EV_NH_LIFT, ACTOR_DOOR, 1);
+ obj_save(416, 4546, "* (The city's quiet. It fills you\n with determination.)");
+ obj_save(1670, 3175, "* (Home, sweet home... someone's.\n You're filled with determination.)");
+ obj_sign(1735, 3140, 20, 20, "* (A box. Its lid won't budge.)");
+ break;
+ case ROOM_NH_HOUSE:
+ thing(1640, 2815, 60, 30, EV_NH_CHAIN, ACTOR_ITEM, 0);
+ obj_sign(1765, 2800, 22, 44, "* (A note, hung on the chain.)|* (\"Howdy! I'm in the garden.\n If you need to talk, come by.\")|* (\"The keys are in the kitchen\n and the hallway.\")");
+ thing(1290, 2575, 50, 24, EV_NH_KEY, ACTOR_ITEM, 0);
+ thing(2374, 2808, 20, 32, EV_NH_KEY, ACTOR_ITEM, 1);
+ obj_sign(2198, 2808, 22, 32, "* (\"Room under renovations.\")");
+ thing(1906, 2618, 24, 22, EV_NH_GIFT, ACTOR_ITEM, 0);
+ thing(1994, 2604, 24, 22, EV_NH_GIFT, ACTOR_ITEM, 1);
+ obj_sign(1290, 2735, 80, 45, "* (A fireplace. It's warm, but\n not too hot to touch.)");
+ obj_sign(2300, 2560, 60, 40, "* (A huge bed. A flowered\n blanket hangs over its end.)");
+ break;
+ case ROOM_NH_BASEMENT:
+ touch(1533, 2293, 8, 94, EV_NH_UP, 0);
+ for (int i = flag(F_NH_STORY); i < NSTORY; i++)
+ touch(story_x[i], i < 5 ? 2293 : 2143, 10, i < 5 ? 94 : 67, EV_NH_STORY, i);
+ thing(4805, 2108, 50, 35, EV_NH_LIFT, ACTOR_DOOR, 2);
+ break;
+ case ROOM_NH_LAST:
+ obj_save(4690, 2370, "* (Golden light pours in. You're\n filled with determination.)");
+ if (!flag(F_NH_SANS)) touch(5300, 2335, 10, 75, EV_NH_SANS, 0);
+ break;
+ case ROOM_NH_THALL:
+ obj_save(6100, 1787, "* (The throne room is just ahead.\n You're filled with determination.)");
+ obj_sign(5910, 1750, 22, 40, "* (Throne Room.)");
+ break;
+ case ROOM_NH_THRONE:
+ if (!flag(F_NH_THRONE)) {
+ npc(ACTOR_ITEM, SPR_ASGORE_OW, 6018, 1357, DIR_UP);
+ touch(5903, 1500, 215, 10, EV_NH_THRONE, 0);
+ }
+ obj_sign(5963, 1110, 60, 50, "* (A big throne. Beside it, a\n second one under a sheet.)");
+ break;
+ case ROOM_NH_COFFINS:
+ thing(6360, 2897, 34, 50, EV_NH_COFFIN, ACTOR_ITEM, 0);
+ for (int i = 1; i < 6; i++) obj_sign(6360 + i * 46, 2897, 34, 50, "* (A coffin for a human. It's\n empty... it's been opened.)");
+ break;
+ case ROOM_NH_BARRIER:
+ obj_save(6340, 962, "* (The end is close. It fills\n you with determination.)");
+ break;
+ case ROOM_NH_WALL:
+ if (!flag(F_NH_ASGORE)) {
+ npc(ACTOR_ITEM, SPR_ASGORE_OW, 6342, 760, DIR_DOWN);
+ touch(6190, 795, 320, 20, EV_NH_BARRIER, 0);
+ } else obj_sign(6250, 620, 160, 120, "* (The barrier. Past it, there's\n nothing left to stop you.)");
+ break;
+ }
+}
+
+/* ---------------------------------------------------------------- the events */
+/* the elevators: down to the CORE; the street's to the far end of the basement (once it's been reached); back */
+static int ev_lift(Ev *c) {
+ EV_BEGIN;
+ c->v[0] = c->self ? c->self->arg : 0;
+ if (c->v[0] == 1 && !flag(F_NH_LIFT)) {
+ NARRATE("* (The elevator isn't working\n yet.)");
+ return 1;
+ }
+ NARRATE(c->v[0] == 0 ? "* (Take the elevator back down\n to the CORE?)@Yes@No" : "* (Take the elevator?)@Yes@No");
+ if (CHOICE() == 1) return 1;
+ if (c->v[0] == 0) FADE_TO(ROOM_CORE_HALL, 4285, 125, DIR_DOWN);
+ else if (c->v[0] == 1) FADE_TO(ROOM_NH_BASEMENT, 4740, 2150, DIR_DOWN);
+ else FADE_TO(ROOM_NH_TOWN, 292, 4325, DIR_DOWN);
+ EV_END;
+}
+
+static int ev_key(Ev *c) {
+ EV_BEGIN;
+ c->v[0] = c->self ? c->self->arg : 0;
+ if (flag(c->v[0] ? F_NH_KEY2 : F_NH_KEY1)) {
+ NARRATE(c->v[0] ? "* (Nothing else here.)" : "* (A note: \"Help yourself to\n anything you want!\")");
+ return 1;
+ }
+ if (!c->v[0]) NARRATE("* (A note: \"Help yourself to\n anything you want!\")");
+ set_flag(c->v[0] ? F_NH_KEY2 : F_NH_KEY1, 1);
+ NARRATE("* (You found a key.)");
+ EV_END;
+}
+
+static int ev_chain(Ev *c) {
+ EV_BEGIN;
+ if (!flag(F_NH_CHAIN)) {
+ if (!flag(F_NH_KEY1) || !flag(F_NH_KEY2)) {
+ NARRATE(flag(F_NH_KEY1) || flag(F_NH_KEY2) ? "* (The stairs are chained shut.\n One lock is still closed.)"
+ : "* (The stairs down are chained\n shut. Two locks.)");
+ return 1;
+ }
+ NARRATE("* (You unlock the chain.)");
+ set_flag(F_NH_CHAIN, 1);
+ }
+ FADE_TO(ROOM_NH_BASEMENT, 1560, 2320, DIR_RIGHT);
+ EV_END;
+}
+
+static int ev_up(Ev *c) {
+ EV_BEGIN;
+ FADE_TO(ROOM_NH_HOUSE, 1660, 2856, DIR_DOWN);
+ EV_END;
+}
+
+static int ev_gift(Ev *c) {
+ EV_BEGIN;
+ c->v[0] = c->self ? c->self->arg : 0;
+ if (flag(c->v[0] ? F_NH_LOCKET : F_NH_DAGGER)) {
+ NARRATE("* (An empty box.)");
+ return 1;
+ }
+ NARRATE("* (A present, wrapped up.)\n* (Open it?)@Yes@No");
+ if (CHOICE() == 1) return 1;
+ if (!item_add(c->v[0] ? ITEM_HEART_LOCKET : ITEM_WORN_DAGGER)) {
+ NARRATE("* (You're carrying too much.)");
+ return 1;
+ }
+ set_flag(c->v[0] ? F_NH_LOCKET : F_NH_DAGGER, 1);
+ NARRATE(c->v[0] ? "* (Inside, a heart-shaped locket.)\n* (You take it.)" : "* (Inside, an old dagger.)\n* (You take it.)");
+ EV_END;
+}
+
+static int ev_story(Ev *c) {
+ EV_BEGIN;
+ c->v[0] = c->self ? c->self->arg : 0;
+ if (c->self) gone(c->self);
+ if (flag(F_NH_STORY) > c->v[0]) return 1;
+ set_flag(F_NH_STORY, c->v[0] + 1);
+ NARRATE(story[c->v[0]]);
+ if (c->v[0] == NSTORY - 1) set_flag(F_NH_LIFT, 1);
+ EV_END;
+}
+
+/* the Last Corridor: Sans, and what EXP and LOVE are */
+static int ev_sans(Ev *c) {
+ Obj *s = obj_get(ACTOR_SANS);
+ EV_BEGIN;
+ if (c->self) gone(c->self);
+ s = obj_sans(pl.x + 110, pl.y + 29, DIR_LEFT);
+ (void)s;
+ SAY(FACE_SANS, "* so, you've made it this far.\n* soon you'll meet the king.|* together, you'll decide the\n future of this world.|* so now, you'll be judged.\n* for every bit of EXP you've\n earned.");
+ SAY(FACE_SANS, "* what's EXP? \"execution\n points.\" a measure of how\n much you've hurt others.|* the more you kill, the easier\n it gets to stop caring.|* that's your LOVE: your \"level\n of violence.\"");
+ if (flag(F_PAPYRUS) == 2) SAY(FACE_SANS, "* ...and you killed my brother.\n* i hope it was worth it.");
+ if (sv.exp == 0)
+ SAY(FACE_SANS, "* but you never gained any LOVE.\n* you stayed... you.|* heh. no LOVE... but lots of\n love. know what i mean?|* ...or not. that's ok.");
+ else SAYF(FACE_SANS, "* you've got some LOVE now.\n* LV %d.|* but there's still good in you.\n* i can tell.", sv.lv);
+ SAY(FACE_SANS, "* anyway. what happens next...\n* we leave up to you.");
+ set_flag(F_NH_SANS, 1);
+ gone(obj_get(ACTOR_SANS));
+ EV_END;
+}
+
+/* the throne room: Asgore and his flowers */
+static int ev_throne(Ev *c) {
+ Obj *a = obj_get(ACTOR_ITEM);
+ EV_BEGIN;
+ if (c->self) gone(c->self);
+ SAY(FACE_ASGORE, "* Dum dum dum...|* Oh? Is someone there?\n* Just a moment! I have to\n finish watering these.|* Howdy! How can I...");
+ a = obj_get(ACTOR_ITEM);
+ a->dir = DIR_DOWN;
+ SAY(FACE_ASGORE, "* ...oh.|* I so wish we could have tea,\n like any other afternoon.|* But... you know how this must\n be.|* When you are ready, come to\n the next room.");
+ a = obj_get(ACTOR_ITEM);
+ WALK(a, 5968, a->y, 2);
+ a = obj_get(ACTOR_ITEM);
+ WALK(a, 5968, 1110, 2);
+ gone(obj_get(ACTOR_ITEM));
+ set_flag(F_NH_THRONE, 1);
+ EV_END;
+}
+
+static int ev_coffin(Ev *c) {
+ EV_BEGIN;
+ SAYF(FACE_NONE, "* (A coffin. The name on it is\n \"%s\".)|* (It's been opened.)", sv.name);
+ EV_END;
+}
+
+/* Sans's call, after: how things turned out (what Frisk did decides it) */
+static int ev_call_after(Ev *c) {
+ EV_BEGIN;
+ SAY(FACE_SANS, "* ring...|* heya. it's me, sans.\n* figured you'd want to know\n how it went down here.");
+ if (flag(F_TORIEL_FIGHT) == 1) SAY(FACE_SANS, "* the lady from the ruins came\n back. she runs things now.|* she teaches at the school,\n too. guess she always\n wanted to.");
+ else if (flag(F_PAPYRUS) != 2 && flag(F_UNDYNE) != 2) SAY(FACE_SANS, "* undyne's in charge now, and\n papyrus is her right hand.|* they're keeping everyone safe.");
+ else SAY(FACE_SANS, "* nobody's really in charge.\n* it's... not great.");
+ if (flag(F_CORE_MTT) == 1) SAY(FACE_SANS, "* mettaton's back on tv.\n* the ratings are through the\n roof.");
+ if (flag(F_CORE_MTT) == 2) SAY(FACE_SANS, "* no more of mettaton's shows,\n though. folks miss him.");
+ SAY(FACE_SANS, "* the king's gone. and the barrier\n is still up.|* but we're getting by.|* so don't worry about us, kid.\n* live your life up there.|* see ya.");
+ EV_END;
+}
+
+/* the barrier: Asgore, his battle, Frisk's choice; Flowey and the six SOULs; out */
+static int ev_barrier(Ev *c) {
+ Obj *a = obj_get(ACTOR_ITEM);
+ EV_BEGIN;
+ if (c->self) gone(c->self);
+ (void)a;
+ SAY(FACE_ASGORE, "* Here it is: the barrier.\n* The wall that holds us all\n underground.|* Anything left to do?\n* Go and do it first.@Ready@Not yet");
+ if (CHOICE() == 1) {
+ SAY(FACE_ASGORE, "* I see. Take your time.");
+ FADE_TO(ROOM_NH_BARRIER, 6415, 985, DIR_DOWN);
+ return 1;
+ }
+ SAY(FACE_ASGORE, "* ...Then let us begin.|* Human... I am glad we met.|* Farewell.");
+ NARRATE("* (Asgore swings his trident.)\n* (Your MERCY button breaks.)");
+ BATTLE(FORM_ASGORE);
+ if (battle_result() == BR_DIED) return 1;
+ NARRATE("* (Asgore is on his knees.)\n* (What will you do?)@Kill@Spare");
+ c->v[0] = CHOICE();
+ if (c->v[0] == 0) {
+ set_flag(F_NH_ASGORE, 2);
+ sv.kills++;
+ NARRATE("* (Asgore turns to dust.)");
+ } else {
+ set_flag(F_NH_ASGORE, 1);
+ SAY(FACE_ASGORE, "* ...after everything I've done,\n you would spare me?|* I declared war the day my\n son died. I swore to take\n every human SOUL...|* ...but I never wanted any\n of it. I only wanted hope\n back.");
+ SAY(FACE_ASGORE, "* Human... I am so sorry.|* Could you... stay with me? We\n could be like a family...");
+ g_shake = 10;
+ NARRATE("* (White pellets rain down on\n Asgore.)\n* (He turns to dust.)");
+ }
+ gone(obj_get(ACTOR_ITEM));
+ /* Flowey, where Asgore was */
+ {
+ static const uint16_t s[4] = {SPR_FLOWEY, SPR_FLOWEY, SPR_FLOWEY, SPR_FLOWEY};
+ Obj *f = obj_npc(ACTOR_FLOWEY, s, 6336, 740, DIR_DOWN, 0);
+ if (f) f->flags &= (uint16_t)~OF_SOLID;
+ }
+ SAY(FACE_FLOWEY, "* Heh. You fool.|* Still haven't figured it out?|* Down here, you kill, or you\n get killed.");
+ SAY(FACE_FLOWEY, "* And now... his six SOULs are\n MINE.");
+ g_shake = 16;
+ NARRATE("* (The six SOULs rise from their\n jars, and gather around\n Flowey.)");
+ BATTLE(FORM_OMEGA);
+ set_flag(F_NH_FLOWEY, 3); /* (beaten: spared or killed below) */
+ SAY(FACE_FLOWEY, "* ...|* Why...?|* Why are you being so... nice\n to me?|* I can't understand.\n* I just can't understand...");
+ NARRATE("* (Flowey is beaten.)\n* (What will you do?)@Spare@Kill");
+ if (CHOICE() == 0) {
+ set_flag(F_NH_FLOWEY, 1);
+ SAY(FACE_FLOWEY, "* ...you're letting me go? I'll\n just come back, you know.|* ...|* ...you make no sense.");
+ NARRATE("* (Flowey sinks into the ground.)");
+ } else {
+ set_flag(F_NH_FLOWEY, 2);
+ NARRATE("* (Flowey is gone.)");
+ }
+ gone(obj_get(ACTOR_FLOWEY));
+ NARRATE("* (With the six SOULs' power, you\n cross the barrier.)|* (The sun is rising.)");
+ set_flag(F_NH_DONE, 1);
+ sv.room = (int16_t)ROOM_NH_BARRIER, sv.x = 6400, sv.y = 985;
+ save_write_auto();
+ /* Sans's call, then the end */
+ c->v[1] = 1;
+ EV_END;
+}
+
+int nh_event(Ev *c) {
+ switch (c->id) {
+ case EV_NH_LIFT: return ev_lift(c);
+ case EV_NH_KEY: return ev_key(c);
+ case EV_NH_CHAIN: return ev_chain(c);
+ case EV_NH_UP: return ev_up(c);
+ case EV_NH_GIFT: return ev_gift(c);
+ case EV_NH_STORY: return ev_story(c);
+ case EV_NH_SANS: return ev_sans(c);
+ case EV_NH_THRONE: return ev_throne(c);
+ case EV_NH_COFFIN: return ev_coffin(c);
+ case EV_NH_BARRIER: {
+ int r = ev_barrier(c);
+ if (r && flag(F_NH_DONE) && c->v[1] == 1) { /* (on to the call) */
+ c->id = EV_NH_CALL, c->at = NULL, c->v[1] = 0;
+ return 0;
+ }
+ return r;
+ }
+ case EV_NH_CALL: {
+ int r = ev_call_after(c);
+ if (r) menu_end_open("THE END|You went home. The monsters\nstayed underground.|(A neutral ending.)");
+ return r;
+ }
+ }
+ return -1;
+}
diff --git a/games/undertale/src/newhome.h b/games/undertale/src/newhome.h
new file mode 100644
index 00000000..21c76a16
--- /dev/null
+++ b/games/undertale/src/newhome.h
@@ -0,0 +1,7 @@
+/* Undertale: New Home (newhome.c), its battles (nhmon.c: Asgore, Omega Flowey). */
+#ifndef UT_NEWHOME_H
+#define UT_NEWHOME_H
+#include "event.h"
+void nh_setup(int room, int from);
+int nh_event(Ev *c);
+#endif
diff --git a/games/undertale/src/nhmon.c b/games/undertale/src/nhmon.c
new file mode 100644
index 00000000..e0b1edce
--- /dev/null
+++ b/games/undertale/src/nhmon.c
@@ -0,0 +1,452 @@
+/* New Home's battles: Asgore at the barrier (the Undertale Wiki's records of the game: his HP, his ACT, how his
+ * guard drops, his fire and his trident; he breaks MERCY before it starts), and Omega Flowey with the six SOULs he
+ * took: not turn by turn, his attacks one after another; now and then a SOUL fights for him, until Frisk reaches the
+ * ACT that appears among its attack and calls for help; all six helped, they turn on him, and FIGHT can reach him.
+ * Their words are short ones of our own. */
+#include
+#include "battle.h"
+#include "game.h"
+#include "gfx.h"
+#include "items.h"
+#include "monsters.h"
+#include "res.h"
+#include "world.h"
+
+/* ---------------------------------------------------------------- Asgore */
+enum { AS_DOWN, AS_TALKS }; /* (b->state[]) */
+static void act_asgore(Battle *b, Monster *m, int act, char *t, int cap) {
+ (void)act;
+ int n = ++b->state[AS_TALKS];
+ if (n < 3 || sv.exp > 0) snprintf(t, (size_t)cap, "* You tell Asgore you don't want\n to fight him.\n* He doesn't answer.");
+ else if (n == 3) {
+ snprintf(t, (size_t)cap, "* You tell Asgore you don't want\n to fight. His hands tremble.\n* His ATTACK and DEFENSE drop.");
+ m->at = (int16_t)max_i(1, m->at - 1), m->df = (int16_t)(m->df - 10);
+ } else snprintf(t, (size_t)cap, "* Asgore's hands are shaking.");
+}
+static bool item_asgore(Battle *b, Monster *m, int item, char *t, int cap) {
+ (void)b, (void)t, (void)cap;
+ /* her pie: its smell reminds him of someone */
+ if (item == ITEM_BUTTERSCOTCH_PIE) m->at = (int16_t)max_i(1, m->at - 1), m->df = (int16_t)(m->df - 5);
+ return false;
+}
+static const char *talk_asgore(Battle *b, Monster *m) {
+ (void)m;
+ if (b->state[AS_DOWN]) return "Ah...";
+ return NULL; /* (he says nothing: he looks away) */
+}
+static const char *flavor_asgore(Battle *b, Monster *m) {
+ /* after turn 20 his guard falls, turn by turn */
+ if (b->turn >= 20 && m->df > -120) m->df = (int16_t)(m->df - 10);
+ if (b->turn <= 1) return "* ASGORE attacks!";
+ if (b->state[AS_TALKS] == 3 && sv.exp == 0) return "* Asgore's hands are shaking.";
+ static const char *const lines[] = {"* Asgore looks away.", "* Asgore's trident glints.", "* You feel the heat of his fire.",
+ "* The barrier hums behind him.", "* Seems like no one can save you now."};
+ return lines[rnd_n(5)];
+}
+static bool hit_asgore(Battle *b, Monster *m, int dmg) {
+ if (m->hp - dmg > 0) return false;
+ /* down on his knees: what's next is Frisk's choice, not the battle's */
+ m->hp = 1, m->hurt = 30, m->shake = 8;
+ b->state[AS_DOWN] = 1;
+ return true;
+}
+static int attack_asgore(Battle *b, Monster *m) {
+ (void)m;
+ if (b->state[AS_DOWN]) return -1;
+ static const uint8_t seq[6] = {ATK_ASG_HELIX, ATK_ASG_TRIDENT, ATK_ASG_RINGS, ATK_ASG_RAIN, ATK_ASG_HANDS, ATK_ASG_TRIDENT};
+ return seq[(b->turn - 1) % 6];
+}
+static bool ends_asgore(Battle *b, Monster *m) {
+ (void)m;
+ if (b->state[AS_DOWN]) b->result = BR_SCRIPTED;
+ return false;
+}
+static void draw_asgore(Battle *b, Monster *m, int x, int y) {
+ uint16_t s = b->state[AS_DOWN] ? SPR_ASGORE_KNEEL : m->hurt ? SPR_ASGORE_HURT : SPR_ASGORE_B;
+ int fr = m->hurt || b->state[AS_DOWN] ? 0 : (int)(g_frame / 8);
+ gfx_sprite(s, fr, x, y + 4, 0, 255);
+}
+
+static const MonDef nh_defs[MON_COUNT - MON_CORE_COUNT] = {
+ [MON_ASGORE - MON_CORE_COUNT] = {
+ .name = "ASGORE", .hp = 3500, .at = 8, .df = -30, .spr_idle = SPR_ASGORE_B, .spr_hurt = SPR_ASGORE_HURT,
+ .idle_speed = 8, .check = "The king of the monsters.", .acts = {"Talk"}, .nacts = 1, .flags = MF_BOSS,
+ .act = act_asgore, .talk = talk_asgore, .attack = attack_asgore, .flavor = flavor_asgore, .on_hit = hit_asgore,
+ .ends = ends_asgore, .draw = draw_asgore, .on_item = item_asgore},
+ [MON_OMEGA - MON_CORE_COUNT] = {.name = "Flowey", .hp = 9999, .flags = MF_BOSS | MF_NOT_KILLABLE},
+};
+const MonDef *nh_mon_def(int id) { return &nh_defs[id - MON_CORE_COUNT]; }
+static const Form nh_forms[FORM_COUNT - FORM_CORE_COUNT] = {
+ [FORM_ASGORE - FORM_CORE_COUNT] = {{MON_ASGORE}, 1, "* (Twilight shines through the\n barrier.)\n* (You're filled with DETERMINATION.)"},
+ [FORM_OMEGA - FORM_CORE_COUNT] = {{MON_OMEGA}, 1, ""},
+};
+const Form *nh_form(int form) { return &nh_forms[form - FORM_CORE_COUNT]; }
+
+/* ---------------------------------------------------------------- Asgore's attacks */
+static int bx0(void) { return bt.x0 + 3; }
+static int bx1(void) { return bt.x1 - 3; }
+static int by0(void) { return bt.y0 + 3; }
+static int by1(void) { return bt.y1 - 3; }
+#define P(v) ((v) * FX)
+static Bullet *shot(uint16_t spr, int x, int y, int vx, int vy) {
+ Bullet *b = bullet_new(spr, P(x), P(y), vx, vy);
+ if (b) b->owner = (uint8_t)bt.atk_owner;
+ return b;
+}
+static Bullet *fire(int x, int y, int vx, int vy) {
+ Bullet *b = shot(SPR_FIREBULLET, x, y, vx, vy);
+ if (b) b->r = 3, b->flags |= BF_NOCLIP;
+ return b;
+}
+/* four waves of fire, curling down in a double helix */
+static void atk_helix(int t) {
+ if (t % 4 || t > 140) return;
+ int cx = board_cx(), a = gfx_sin(t * 6) * ((bx1() - bx0()) / 2 - 6) / 1024;
+ fire(cx + a, by0() - 6, 0, P(2));
+ fire(cx - a, by0() - 6, 0, P(2));
+}
+/* a stream down the middle, and a cloud of fire over one side, then the other */
+static void atk_rain(int t) {
+ if (t % 5 == 0 && t < 150) fire(board_cx() - 6 + rnd_n(13), by0() - 6, 0, P(2));
+ if (t % 3 == 0 && t < 150) {
+ bool left = (t / 50) % 2;
+ int w = (bx1() - bx0()) / 2 - 16;
+ fire(left ? bx0() + rnd_n(w) : bx1() - rnd_n(w), by0() - 6, 0, P(3));
+ }
+}
+/* rings of fire, a gap in each, closing in round the SOUL */
+static void atk_rings(int t) {
+ if (t % 45 || t > 120) return;
+ int gap = rnd_n(12);
+ for (int k = 0; k < 12; k++) {
+ if (k == gap || k == (gap + 1) % 12) continue;
+ Bullet *b = fire(board_cx(), board_cy(), 0, 0);
+ if (!b) continue;
+ b->kind = BK_ORBIT, b->a = (int16_t)board_cx(), b->b = (int16_t)board_cy(), b->c = (int16_t)P(80);
+ b->d = (int16_t)(k * 256 / 12 * 16), b->vx = 12 + t / 10, b->vy = -P(1) / 2, b->life = 150;
+ }
+}
+/* his hands sweep down the sides, leaving fire; then it all flies at where the SOUL was */
+static int aim_x, aim_y;
+static void atk_hands(int t) {
+ if (t < 80 && t % 8 == 0) {
+ for (int side = 0; side < 2; side++) {
+ Bullet *b = fire(side ? bx1() - 4 : bx0() + 4, by0() + 4 + t * (by1() - by0() - 8) / 80, 0, 0);
+ if (b) b->kind = BK_SCRIPT, b->a = 1;
+ }
+ }
+ if (t == 85) aim_x = soul_x(), aim_y = soul_y();
+ if (t >= 85)
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT || b->a != 1) continue;
+ int dx = aim_x - b->x / FX, dy = aim_y - b->y / FX, d = abs_i(dx) + abs_i(dy);
+ b->kind = BK_LINEAR;
+ if (d) b->vx = dx * P(3) / d, b->vy = dy * P(3) / d;
+ }
+}
+/* his eyes flash blue or orange, then his trident sweeps the whole board in that colour */
+static uint8_t trident[3];
+static void atk_trident(int t) {
+ if (t == 0)
+ for (int k = 0; k < 3; k++) trident[k] = (uint8_t)(rnd_n(2) ? BC_BLUE : BC_ORANGE);
+ int k = (t - 20) / 45, ph = (t - 20) % 45;
+ if (t < 20 || k >= 3) return;
+ if (ph == 0) { /* the warning: a flash of the colour along the top */
+ Bullet *w = shot(SPR_RECT, board_cx(), by0() + 2, 0, 0);
+ if (w) w->flags |= BF_BOX | BF_HARMLESS, w->w = (int16_t)(bx1() - bx0()), w->h = 3, w->color = trident[k], w->life = 18;
+ }
+ if (ph == 20) {
+ bool left = k % 2;
+ Bullet *s = shot(SPR_RECT, left ? bx0() - 4 : bx1() + 4, board_cy(), left ? P(9) : -P(9), 0);
+ if (s) s->flags |= BF_BOX | BF_NOCLIP, s->w = 8, s->h = (int16_t)(by1() - by0()), s->color = trident[k], s->life = 30;
+ }
+}
+
+int nh_attack_length(int atk) { return atk == ATK_ASG_TRIDENT ? 160 : 165; }
+void nh_attack_run(Battle *b, int atk, int t) {
+ (void)b;
+ if (t < 0) {
+ board_set(atk == ATK_ASG_RINGS ? 110 : 150, 100);
+ soul_set_mode(SOUL_RED);
+ return;
+ }
+ switch (atk) {
+ case ATK_ASG_HELIX: atk_helix(t); break;
+ case ATK_ASG_RAIN: atk_rain(t); break;
+ case ATK_ASG_RINGS: atk_rings(t); break;
+ case ATK_ASG_HANDS: atk_hands(t); break;
+ case ATK_ASG_TRIDENT: atk_trident(t); break;
+ }
+}
+
+/* ---------------------------------------------------------------- Omega Flowey */
+enum { OF_ATTACK, OF_WARNING, OF_SOUL, OF_REBEL, OF_DONE };
+enum { OA_EYES, OA_FLAME, OA_PELLETS, OA_VINES, OA_BOMBS, OA_FINGERS, OA_COUNT };
+static struct {
+ int st, t, atk, n, souls, helped, hits, msg_t, refused;
+ const char *msg;
+ Bullet *act;
+} om;
+static const uint16_t soul_spr[6] = {SPR_SOUL_LIGHT_BLUE, SPR_SOUL_ORANGE, SPR_SOUL_BLUE, SPR_SOUL_PURPLE, SPR_SOUL_GREEN,
+ SPR_SOUL_YELLOW};
+static const uint32_t soul_rgb[6] = {0x42FCFF, 0xFCA600, 0x003CFF, 0xD535D9, 0x00C000, 0xFFFF00};
+#define OM_HITS 6
+static void say_top(const char *s) { om.msg = s, om.msg_t = 110; }
+
+void omega_start(void) {
+ bt.scripted = true;
+ board_set_rect(65, 120, 255, 194);
+ bt.x0 = bt.tx0, bt.y0 = bt.ty0, bt.x1 = bt.tx1, bt.y1 = bt.ty1;
+ bt.sx = P(board_cx()), bt.sy = P(board_cy());
+ soul_set_mode(SOUL_RED);
+ om.st = OF_ATTACK, om.t = 0, om.atk = 0, om.n = 0, om.souls = 0, om.helped = 0, om.hits = 0, om.refused = 0, om.act = NULL;
+ say_top("* Flowey holds the six SOULs.");
+}
+
+/* his own: the eyes' X shots, the flamethrowers, a ring of pellets closing in, vines along flashing lines, bombs,
+ * finger guns from both sides */
+static void omega_attack(int a, int t) {
+ int cx = board_cx();
+ switch (a) {
+ case OA_EYES:
+ if (t % 18 == 0 && t < 140)
+ for (int k = -3; k <= 3; k++) {
+ Bullet *b = shot(SPR_XSHOT, cx + (t / 18 % 2 ? 40 : -40), by0() - 10, k * P(1) / 2, P(2));
+ if (b) b->r = 3, b->flags |= BF_NOCLIP, b->dmg = 6;
+ }
+ break;
+ case OA_FLAME:
+ if (t % 30 == 10 && t < 140)
+ for (int side = 0; side < 2; side++) {
+ int y = by0() + 10 + rnd_n(by1() - by0() - 20);
+ Bullet *b = shot(SPR_RECT, side ? bx1() : bx0(), y, side ? -P(3) : P(3), 0);
+ if (b) b->flags |= BF_BOX | BF_NOCLIP, b->w = 40, b->h = 8, b->color = BC_WHITE, b->life = 40, b->dmg = 6;
+ }
+ break;
+ case OA_PELLETS:
+ if (t % 60 == 0 && t < 130)
+ for (int k = 0; k < 10; k++) {
+ int an = k * 256 / 10;
+ Bullet *b = shot(SPR_SPINBULLET, soul_x() + gfx_cos(an) * 40 / 1024, soul_y() + gfx_sin(an) * 40 / 1024, 0, 0);
+ if (b) b->kind = BK_SCRIPT, b->a = 30, b->r = 3, b->anim = 3, b->dmg = 6, b->flags |= BF_NOCLIP;
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT || b->spr != SPR_SPINBULLET) continue;
+ if (b->a) { /* (still, a moment: the warning) */
+ b->a--;
+ b->alpha = b->a % 4 < 2 ? 120 : 255, b->flags |= BF_HARMLESS;
+ if (!b->a) {
+ int dx = soul_x() - b->x / FX, dy = soul_y() - b->y / FX, d = abs_i(dx) + abs_i(dy);
+ b->kind = BK_LINEAR, b->alpha = 255, b->flags &= (uint8_t)~BF_HARMLESS;
+ if (d) b->vx = dx * P(2) / d, b->vy = dy * P(2) / d;
+ }
+ }
+ }
+ break;
+ case OA_VINES:
+ if (t % 40 == 0 && t < 130)
+ for (int k = 0; k < 3; k++) {
+ int x = soul_x() + (k - 1) * 22;
+ Bullet *w = shot(SPR_RECT, x, board_cy(), 0, 0);
+ if (w) w->flags |= BF_BOX | BF_HARMLESS, w->w = 1, w->h = (int16_t)(by1() - by0()), w->color = BC_ORANGE, w->life = 20;
+ Bullet *v = shot(SPR_RECT, x, by0() - 60, 0, 0);
+ if (v) v->kind = BK_SCRIPT, v->a = 20, v->flags |= BF_BOX | BF_NOCLIP, v->w = 5, v->h = 100, v->dmg = 6;
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *v = &bullets[i];
+ if (v->kind != BK_SCRIPT || v->spr != SPR_RECT) continue;
+ if (v->a) v->a--;
+ else v->y += P(8);
+ if (v->y > P(by1() + 70)) v->kind = BK_NONE;
+ }
+ break;
+ case OA_BOMBS:
+ if (t % 20 == 0 && t < 130) {
+ Bullet *b = shot(SPR_BOMB, bx0() + 8 + rnd_n(bx1() - bx0() - 16), by0() - 8, 0, P(2));
+ if (b) b->kind = BK_SCRIPT, b->r = 5, b->dmg = 6, b->flags |= BF_NOCLIP;
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT || b->spr != SPR_BOMB) continue;
+ b->y += b->vy;
+ if (b->y >= P(by1() - 4)) { /* it hits the floor: a burst */
+ int x = b->x / FX, y = b->y / FX;
+ b->kind = BK_NONE;
+ for (int k = 0; k < 8; k++) {
+ Bullet *s = shot(SPR_SPINBULLET, x, y, gfx_cos(k * 32) * 2 * FX / 1024, gfx_sin(k * 32) * 2 * FX / 1024);
+ if (s) s->r = 3, s->dmg = 6, s->life = 50;
+ }
+ }
+ }
+ break;
+ case OA_FINGERS:
+ if (t % 16 == 0 && t < 140) {
+ bool left = (t / 16) % 2;
+ Bullet *b = shot(SPR_ORB, left ? bx0() - 6 : bx1() + 6, soul_y(), left ? P(3) : -P(3), 0);
+ if (b) b->r = 4, b->dmg = 6, b->flags |= BF_NOCLIP;
+ }
+ break;
+ }
+}
+
+/* a SOUL's: the light blue's knives, the orange's fists, the blue's stars and shoes, the purple's words, the
+ * green's pans and their fire, the yellow's gun; once called on, what they throw heals instead */
+static void soul_attack(int s, int t) {
+ int cx = board_cx(), cy = board_cy();
+ uint8_t col = om.helped ? BC_GREEN : BC_WHITE;
+ Bullet *b = NULL;
+ switch (s) {
+ case 0:
+ if (t % 12 == 0) b = shot(SPR_KNIFE, bx0() - 4, by0() + rnd_n(by1() - by0()), P(3), (t / 60 % 2 ? 1 : -1) * P(1));
+ break;
+ case 1:
+ if (t % 50 == 0)
+ for (int k = 0; k < 8; k++) {
+ b = shot(SPR_DOGPAW, cx, cy, 0, 0);
+ if (b) b->kind = BK_ORBIT, b->a = (int16_t)cx, b->b = (int16_t)cy, b->c = (int16_t)P(70), b->d = (int16_t)(k * 32 * 16),
+ b->vx = 30, b->vy = -P(1) / 2, b->life = 130, b->r = 4, b->color = col, b->dmg = om.helped ? 2 : 6;
+ b = NULL;
+ }
+ break;
+ case 2:
+ if (t % 14 == 0) {
+ b = shot(SPR_SHOE, bx1() + 6, cy + gfx_sin(t * 8) * 30 / 1024, -P(2), 0);
+ if (b) b->color = col, b->r = 4, b->dmg = om.helped ? 2 : 6;
+ b = shot(SPR_STAR, bx0() + rnd_n(bx1() - bx0()), by0() - 4, 0, P(1));
+ }
+ break;
+ case 3:
+ if (t % 10 == 0) {
+ bool left = (t / 10) % 2;
+ b = shot(SPR_RECT, left ? bx0() : bx1(), by0() + 8 + rnd_n(by1() - by0() - 16), left ? P(2) : -P(2), 0);
+ if (b) b->flags |= BF_BOX, b->w = 22, b->h = 6;
+ }
+ break;
+ case 4:
+ if (t % 9 == 0) b = shot(SPR_FIREBULLET, cx + (rnd_n(3) - 1) * 40, cy - 10, (rnd_n(3) - 1) * P(1) / 2, -P(3));
+ if (b) b->ay = P(1) / 8;
+ break;
+ case 5:
+ if (t % 15 == 0) {
+ int dx = soul_x() - cx, dy = soul_y() - by0(), d = abs_i(dx) + abs_i(dy);
+ b = shot(SPR_ORB, cx, by0() + 4, d ? dx * P(3) / d : 0, d ? dy * P(3) / d : P(3));
+ }
+ break;
+ }
+ if (b) b->color = col, b->r = b->r ? b->r : 4, b->dmg = om.helped ? 2 : 6, b->flags |= BF_NOCLIP;
+}
+static void clear_bullets(void) {
+ for (int i = 0; i < MAX_BULLETS; i++) bullets[i].kind = BK_NONE;
+}
+/* a button among the bullets (ACT, then FIGHT): it drifts to the SOUL; touched, it's pressed */
+static Bullet *button_new(uint16_t spr) {
+ Bullet *b = bullet_new(spr, P(board_cx()), P(bt.y0 + 6), 0, 0);
+ if (b) b->kind = BK_SCRIPT, b->flags |= BF_HARMLESS | BF_NOCLIP, b->a = 7;
+ return b;
+}
+static bool is_button(const Bullet *b) {
+ return b && b->kind == BK_SCRIPT && b->a == 7 && (b->spr == SPR_ACTBUTTON || b->spr == SPR_FIGHTBT);
+}
+static bool button_step(Bullet *b) {
+ if (!is_button(b)) return false;
+ int dx = soul_x() - b->x / FX, dy = soul_y() - b->y / FX;
+ b->x += dx > 0 ? FX * 3 / 4 : dx < 0 ? -FX * 3 / 4 : 0, b->y += dy > 0 ? FX * 3 / 4 : dy < 0 ? -FX * 3 / 4 : 0;
+ if (abs_i(dx) < 9 && abs_i(dy) < 7) {
+ b->kind = BK_NONE;
+ return true;
+ }
+ return false;
+}
+
+void omega_update(uint32_t keys, uint32_t pressed) {
+ (void)pressed;
+ if (bt.inv) bt.inv--;
+ soul_update(keys);
+ bullets_update();
+ soul_hit_test();
+ if (om.msg_t) om.msg_t--;
+ /* at 0 HP: but it refused (the SOULs won't let Frisk go) */
+ if (sv.hp < 1) {
+ sv.hp = sv.maxhp, om.refused++;
+ say_top("* Your SOUL won't give up.");
+ }
+ om.t++;
+ switch (om.st) {
+ case OF_ATTACK:
+ omega_attack(om.atk, om.t);
+ if (om.t >= 160) {
+ om.t = 0, om.n++;
+ om.atk = (om.atk + 1 + rnd_n(OA_COUNT - 1)) % OA_COUNT;
+ if (om.souls < 6 && om.n % 2 == 0) om.st = OF_WARNING, clear_bullets(), say_top("* WARNING!");
+ }
+ break;
+ case OF_WARNING:
+ if (om.t >= 45) om.st = OF_SOUL, om.t = 0, om.helped = 0, om.act = NULL;
+ break;
+ case OF_SOUL:
+ soul_attack(om.souls, om.t);
+ if (om.t == 70) om.act = button_new(SPR_ACTBUTTON);
+ if (!om.helped && button_step(om.act)) {
+ om.helped = 1, om.t = 200;
+ say_top("* You called for help...");
+ for (int i = 0; i < MAX_BULLETS; i++)
+ if (bullets[i].kind != BK_NONE) bullets[i].color = BC_GREEN, bullets[i].dmg = 2;
+ }
+ if (om.t >= 300) {
+ clear_bullets();
+ om.t = 0;
+ if (om.helped) {
+ om.souls++;
+ say_top(om.souls < 6 ? "* A SOUL is on your side." : "* The SOULs turn on him! DEFENSE 0!");
+ }
+ om.st = om.souls >= 6 ? OF_REBEL : OF_ATTACK;
+ }
+ break;
+ case OF_REBEL:
+ /* his attacks, slower now; FIGHT comes down among them */
+ if (om.t % 2 == 0) omega_attack(om.atk, om.t / 2);
+ if (om.t >= 320) om.t = 0, om.atk = (om.atk + 1) % OA_COUNT;
+ if (!is_button(om.act)) {
+ if (om.t % 120 == 60) om.act = button_new(SPR_FIGHTBT);
+ } else if (button_step(om.act)) {
+ om.hits++, om.act = NULL;
+ g_shake = 8;
+ say_top(om.hits < OM_HITS ? "* You hit Flowey!" : "* Flowey falls apart.");
+ if (om.hits >= OM_HITS) om.st = OF_DONE, om.t = 0, clear_bullets();
+ }
+ break;
+ case OF_DONE:
+ if (om.t >= 90) bt.result = BR_SCRIPTED;
+ break;
+ }
+}
+
+void omega_draw(void) {
+ gfx_rect(0, 0, SCREEN_W, SCREEN_H, C_BLACK, 255);
+ /* his screen: his face, laughing while he wins; the SOUL he's using, beside it */
+ bool hurt = om.st == OF_REBEL || om.st == OF_DONE;
+ if (om.st != OF_DONE || om.t % 6 < 3) {
+ if (hurt && g_frame % 30 < 15) gfx_sprite(SPR_OMEGA, 0, 160 + (g_frame % 4 < 2 ? 2 : -2), 56, 0, 255);
+ else if (om.st == OF_ATTACK) gfx_sprite(SPR_OMEGA_LAUGH, (int)(g_frame / 6), 160, 56, 0, 255);
+ else gfx_sprite(SPR_OMEGA, 0, 160, 56, 0, 255);
+ }
+ for (int k = 0; k < 6; k++) { /* the six SOULs: dim once they've sided with Frisk */
+ int x = k < 3 ? 46 + k * 16 : 242 + (k - 3) * 16;
+ gfx_sprite(soul_spr[k], 0, x, 20, 0, k < om.souls ? 90 : 255);
+ }
+ if (om.st == OF_WARNING && om.t % 10 < 6) {
+ gfx_rect(120, 92, 80, 14, rgb565(soul_rgb[om.souls]), 255);
+ gfx_text(FONT_SMALL, 134, 94, "WARNING", C_BLACK, 255);
+ }
+ /* the board */
+ gfx_rect(bt.x0, bt.y0, bt.x1 - bt.x0, bt.y1 - bt.y0, C_WHITE, 255);
+ gfx_rect(bt.x0 + 2, bt.y0 + 2, bt.x1 - bt.x0 - 4, bt.y1 - bt.y0 - 4, C_BLACK, 255);
+ gfx_clip(bt.x0 + 2, bt.y0 + 2, bt.x1 - bt.x0 - 4, bt.y1 - bt.y0 - 4);
+ bullets_draw();
+ gfx_clip(0, 0, SCREEN_W, SCREEN_H);
+ if (!bt.inv || (bt.inv / 2) % 2) gfx_sprite(SPR_HEART, 0, soul_x(), soul_y(), 0, 255);
+ if (om.msg_t && om.msg) gfx_text(FONT_SMALL, 70, 107, om.msg, C_WHITE, 255);
+ battle_draw_hud();
+}
diff --git a/games/undertale/src/papyrus.c b/games/undertale/src/papyrus.c
new file mode 100644
index 00000000..d9cecd34
--- /dev/null
+++ b/games/undertale/src/papyrus.c
@@ -0,0 +1,222 @@
+/* Papyrus, Snowdin's boss: his bones, the blue SOUL he gives you, his special attack and the dog that steals
+ * it (as the Undertale Wiki describes the fight). He never kills you: he captures you (the battle ends,
+ * snowtown.c takes it from there). */
+#include
+#include
+#include "battle.h"
+#include "game.h"
+#include "gfx.h"
+#include "monsters.h"
+#include "res.h"
+
+/* b->state[0]: 0 before he's FOUGHT or spared, 1 the blue attack next, 2 blue; [1] flirts, [2] insults;
+ * [3] blue turns; [4] FOUGHT (1) or spared (2) first; [5] he spares you; [6] his speech this turn */
+enum { PAP_RED, PAP_BLUE_NEXT, PAP_BLUE };
+
+static const char *const turn_lines[] = {
+ "BEHOLD!", "AND I HAVE A\n#RSPECIAL\nATTACK#X TOO!", "I CAN ALMOST\nHEAR THE\nCHEERING!!!", "PAPYRUS,\nROYAL\nGUARD!", "PAPYRUS,\nPASTA\nMASTER!",
+ "UNDYNE WILL\nBE SO\nPROUD!!", "THEY'LL MAKE\nA STATUE OF\nME! A BIG\nONE!!!", "SANS WILL...\nPROBABLY\nNAP THROUGH\nIT.",
+ "SO MANY\nFANS!!\nBUT...", "WILL THEY\nLIKE THE\nREAL ME???", "YOU'RE NOT\nLIKE MOST\nPEOPLE...", "ONCE YOU'RE\nCAPTURED...",
+ "THEY WON'T\nLET YOU\nGO, WILL\nTHEY...", "NO! GIVE UP\nALREADY!!", "OR FACE MY\n#RSPECIAL\nATTACK#X!!!", "IT'S COMING\nSOON! MY\n#RSPECIAL\nATTACK#X!",
+ "ANY TURN NOW\nTHE #RSPECIAL\nATTACK#X!!!", "LAST CHANCE\nBEFORE MY\n#RSPECIAL\nATTACK#X!!"};
+#define NTURNS (int)(sizeof turn_lines / sizeof turn_lines[0])
+#define SPECIAL_TURN NTURNS /* the dog steals it */
+#define NORMAL_TURN (NTURNS + 1)
+
+static void act_papyrus(Battle *b, Monster *m, int act, char *t, int cap) {
+ (void)m;
+ if (b->state[0] == PAP_BLUE) {
+ snprintf(t, (size_t)cap, act == 0 ? "* Papyrus is too busy FIGHTing to flirt back." : "* Papyrus is too busy FIGHTing to accept your insult.");
+ return;
+ }
+ if (act == 0) {
+ b->state[1]++;
+ snprintf(t, (size_t)cap, b->state[1] >= 3 ? "* You FLIRT, but to no avail.\n* Seems ACTing won't escalate this battle..." : "* You flirt with Papyrus.");
+ } else {
+ b->state[2]++;
+ snprintf(t, (size_t)cap, b->state[2] >= 4 ? "* You INSULT, but to no avail.\n* Seems ACTing won't escalate this battle..." : "* You insult Papyrus.");
+ }
+ b->state[6] = act + 1;
+}
+
+static const char *talk_papyrus(Battle *b, Monster *m) {
+ (void)m;
+ int said = b->state[6];
+ b->state[6] = 0;
+ /* the first FIGHT or MERCY: the blue attack comes */
+ if (b->state[0] == PAP_RED && (b->last_action == 0 || b->last_action == 3)) {
+ b->state[0] = PAP_BLUE_NEXT;
+ return b->last_action == 0 ? "OH, A FIGHT,\nIS IT?|THEN TRY MY\nLEGENDARY\nBLUE ATTACK!"
+ : "NO FIGHTING?\nHMPH!|THEN TRY MY\nLEGENDARY\nBLUE ATTACK!";
+ }
+ if (b->last_action == 2 && b->state[0] != PAP_BLUE) return "OOH, SNACKS!\nI SHOULD HAVE\nPACKED SOME.";
+ if (said == 1) {
+ if (b->state[1] == 1) return "FL-FLIRTING!?|I HAVE VERY\nHIGH\nSTANDARDS!!!|A DATE...\nAFTER THE\nCAPTURE!";
+ return "AFTER THE\nCAPTURE! THEN\nWE'LL SEE!";
+ }
+ if (said == 2) {
+ if (b->state[2] == 1) return "AH, I SEE!|YOU'RE BEING\nMEAN SO I\nFEEL OKAY\nFIGHTING YOU!";
+ if (b->state[2] == 2) return "PRICKLY\nOUTSIDE, SOFT\nINSIDE!\nLIKE A\nCACTUS!";
+ return "SAVE YOUR\nBREATH!";
+ }
+ if (b->state[0] != PAP_BLUE) return b->state[0] == PAP_RED ? "NYEH HEH\nHEH!" : NULL;
+ /* his absolutely normal attack done: he spares you (and one hit would end him now) */
+ if (m->state == 1) {
+ b->state[5] = 1, m->spare = true, m->state = 2;
+ m->df = (int16_t)(-2 * m->hp);
+ return "*HUFF*\nYOU CAN'T\nBEAT ME!\n*HUFF*|SO I'LL BE\nGENEROUS...|I #RSPARE#X\nYOU, HUMAN!|ACCEPT MY\n#RMERCY#X!";
+ }
+ if (b->state[5]) return NULL;
+ int k = b->state[3];
+ if (k < NTURNS) return turn_lines[k];
+ if (k == SPECIAL_TURN)
+ return "AND NOW...\nMY #RSPECIAL\nATTACK#X!|HEY!!!|THE DOG TOOK\nIT!!!|...|FINE. A\nREGULAR\nATTACK,\nTHEN.";
+ if (k == NORMAL_TURN) return "*SIGH*\nA PERFECTLY\nORDINARY\nATTACK.";
+ return NULL;
+}
+
+static const char *flavor_papyrus(Battle *b, Monster *m) {
+ (void)m;
+ if (b->state[5]) return "* Papyrus is sparing you.";
+ if (b->state[0] == PAP_BLUE && b->state[3] == 1) return "* You're blue now.";
+ if (b->state[3] > SPECIAL_TURN) return "* Papyrus is getting ready for a regular attack.";
+ if (b->state[1] && b->state[0] != PAP_BLUE) return "* Papyrus is thinking about what to wear for his date.";
+ return NULL;
+}
+
+static int attack_papyrus(Battle *b, Monster *m) {
+ if (b->state[5]) return -1;
+ if (b->state[0] == PAP_RED) return ATK_PAP_FIRST; /* bones low along the floor */
+ if (b->state[0] == PAP_BLUE_NEXT) {
+ b->state[0] = PAP_BLUE, b->state[3] = 0;
+ return ATK_PAP_FIRST + 1; /* the blue attack, then the SOUL goes blue */
+ }
+ int k = ++b->state[3];
+ if (k == SPECIAL_TURN) return ATK_PAP_FIRST + 9; /* the dog */
+ if (k == NORMAL_TURN) {
+ m->state = 1; /* (after it, he spares you) */
+ return ATK_PAP_FIRST + 10;
+ }
+ if (k <= 6) return ATK_PAP_FIRST + 1 + k; /* the wiki's patterns, in order */
+ return ATK_PAP_FIRST + 3 + rnd_n(6);
+}
+
+static bool hit_papyrus(Battle *b, Monster *m, int dmg) {
+ (void)b;
+ if (b->state[0] == PAP_RED) b->state[4] = 1;
+ (void)m, (void)dmg;
+ return false;
+}
+
+const MonDef papyrus_def = {
+ .name = "Papyrus", .hp = 680, .at = 8, .df = 2, .exp = 200, .gold = 0, .spr_idle = SPR_PAPYRUS_B,
+ .spr_hurt = SPR_PAPYRUS_HURT, .check = "He likes to say:\n* \"Nyeh heh heh!\"", .acts = {"Flirt", "Insult"}, .nacts = 2,
+ .neutral = {"* Papyrus is preparing a bone attack.", "* Papyrus is cackling.", "* Papyrus whispers \"Nyeh heh heh!\"",
+ "* Papyrus is rattling his bones.", "* Papyrus is trying hard to play it cool.",
+ "* Papyrus is considering his options."},
+ .flags = MF_BOSS, .act = act_papyrus, .talk = talk_papyrus, .attack = attack_papyrus, .flavor = flavor_papyrus,
+ .on_hit = hit_papyrus,
+};
+
+/* ---------------------------------------------------------------- his bones */
+#define P(v) ((v) * FX)
+static int bx0(void) { return bt.x0 + 3; }
+static int bx1(void) { return bt.x1 - 3; }
+static int by0(void) { return bt.y0 + 3; }
+static int by1(void) { return bt.y1 - 3; }
+
+/* a bone of a pattern: when (frames), how tall (from the floor, or hanging from the ceiling when negative),
+ * blue, fast, and from the right instead of the left */
+typedef struct {
+ uint16_t at;
+ uint8_t h, ceil, blue, fast, right;
+} Bone;
+static void bone(const Bone *k) {
+ bool right = k->right;
+ int sp = k->fast ? P(4) : P(2);
+ int y = k->ceil ? by0() + k->h / 2 : by1() - k->h / 2;
+ Bullet *b = bullet_new(SPR_BONE, P(right ? bx1() + 4 : bx0() - 4), P(y), right ? -sp : sp, 0);
+ if (!b) return;
+ b->flags |= BF_BOX, b->w = 5, b->h = k->h, b->owner = (uint8_t)bt.atk_owner;
+ if (k->blue) b->color = BC_BLUE;
+}
+
+/* the patterns (heights halved from the game's: small 10, medium 20, large 30): a floor bone F, one hanging from
+ * the ceiling C, a blue one BL, from the right R (C from the right: RC), fast from the right FR */
+#define F(at, h) {at, h, 0, 0, 0, 0}
+#define C(at, h) {at, h, 1, 0, 0, 0}
+#define BL(at) {at, 60, 0, 1, 0, 0}
+#define R(at, h) {at, h, 0, 0, 0, 1}
+#define RC(at, h) {at, h, 1, 0, 0, 1}
+#define FR(at, h) {at, h, 0, 0, 1, 1}
+static const Bone pat_red[] = {F(0, 8), F(16, 10), F(32, 12), F(48, 14), F(64, 16), F(80, 14), F(96, 12), F(112, 10), F(128, 8)};
+static const Bone pat_blue[] = {BL(0), BL(20), BL(40), BL(60), F(120, 10)};
+static const Bone pat_2[] = {F(0, 10), F(20, 10), F(40, 10), F(60, 10), F(90, 20)};
+static const Bone pat_3[] = {F(0, 10), F(30, 20), F(55, 20), F(80, 20), F(110, 30), F(140, 30), F(170, 30)};
+static const Bone pat_4[] = {F(0, 30), C(40, 30), F(70, 10), F(85, 10), F(100, 10), F(125, 20), C(160, 30)};
+static const Bone pat_5[] = {F(0, 30), F(35, 30), F(70, 30), BL(105), FR(140, 10)};
+static const Bone pat_6[] = {F(0, 12), C(0, 30), F(40, 12), C(40, 30), RC(80, 30), F(120, 8), F(126, 12), F(132, 16),
+ F(138, 20), F(144, 20), F(150, 16), F(156, 12), F(162, 8)};
+static const Bone pat_7[] = {F(0, 12), C(0, 30), F(25, 12), C(25, 30), F(60, 22), C(60, 22), F(85, 22), C(85, 22), BL(120),
+ R(150, 30), F(185, 12), C(185, 30), R(185, 12), RC(185, 30)};
+static const Bone pat_normal[] = {F(0, 10), F(15, 10), F(30, 20), F(50, 30), C(70, 30), FR(95, 10), F(110, 20), BL(125),
+ F(150, 12), C(150, 30), F(175, 12), C(175, 30), F(205, 8), F(210, 12), F(215, 16),
+ F(220, 20), F(225, 24), F(230, 20), F(235, 16), F(240, 12), F(245, 8), F(275, 30),
+ FR(300, 30), F(320, 10), F(330, 10), F(340, 10), F(350, 10)};
+typedef struct {
+ const Bone *bones;
+ int n, len;
+} Pattern;
+#define PAT(a, len) {a, (int)(sizeof a / sizeof a[0]), len}
+static const Pattern pats[] = {
+ PAT(pat_red, 200), PAT(pat_blue, 200), PAT(pat_2, 170), PAT(pat_3, 250), PAT(pat_4, 240), PAT(pat_5, 220),
+ PAT(pat_6, 240), PAT(pat_7, 270), PAT(pat_2, 170), {NULL, 0, 220}, PAT(pat_normal, 420)};
+
+int papyrus_attack_length(int atk) { return pats[atk - ATK_PAP_FIRST].len; }
+
+void papyrus_attack(int atk, int t) {
+ int k = atk - ATK_PAP_FIRST;
+ if (t == 0) {
+ /* his board: wide and low; the SOUL on the floor once it's blue */
+ if (k == 0) board_set(110, 60);
+ else board_set(140, 60);
+ if (k >= 2) soul_set_mode(SOUL_BLUE);
+ if (bt.soul_mode == SOUL_BLUE) bt.sy = P(by1() - 3);
+ }
+ if (k == 1 && t == 100) { /* the blue bones passed: now your SOUL's blue */
+ soul_set_mode(SOUL_BLUE);
+ }
+ const Pattern *p = &pats[k];
+ for (int i = 0; i < p->n; i++)
+ if (p->bones[i].at == t) bone(&p->bones[i]);
+ /* the dog: it trots in, takes the special attack's bone and runs off with it (harmless) */
+ if (k == 9) {
+ if (t == 20) {
+ Bullet *b = bullet_new(SPR_BONE, P((bx0() + bx1()) / 2), P(by1() - 12), 0, 0);
+ if (b) b->flags |= BF_BOX | BF_HARMLESS, b->w = 24, b->h = 6, b->kind = BK_SCRIPT, b->a = 1;
+ }
+ if (t == 60) {
+ Bullet *d = bullet_new(SPR_DOG_OW, P(bx1() + 10), P(by1() - 6), -P(1), 0);
+ if (d) d->flags |= BF_HARMLESS, d->anim = 6;
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *d = &bullets[i];
+ if (d->spr != SPR_DOG_OW || d->kind == BK_NONE) continue;
+ /* at the bone: it takes it and goes the other way */
+ if (d->vx < 0 && d->x <= P((bx0() + bx1()) / 2 + 6)) {
+ d->vx = P(2), d->spr = SPR_DOG_BONE;
+ for (int j = 0; j < MAX_BULLETS; j++)
+ if (bullets[j].kind == BK_SCRIPT && bullets[j].a == 1) bullets[j].kind = BK_NONE;
+ }
+ }
+ }
+ /* the normal attack's dog: it runs across, and it hurts */
+ if (k == 10 && t == 260) {
+ Bullet *d = bullet_new(SPR_DOG_OW, P(bx0() - 10), P(by1() - 6), P(3), 0);
+ if (d) d->owner = (uint8_t)bt.atk_owner, d->dmg = 3, d->r = 6, d->anim = 4;
+ }
+}
+
+void papyrus_attack_draw(int atk, int t) {
+ (void)atk, (void)t;
+}
diff --git a/games/undertale/src/plat.h b/games/undertale/src/plat.h
new file mode 100644
index 00000000..615facbf
--- /dev/null
+++ b/games/undertale/src/plat.h
@@ -0,0 +1,40 @@
+/* Undertale: the platform (the calculator, or a computer for tests).
+ *
+ * plat_eadk.c: the calculator's EADK display, keyboard, time and files.
+ * plat_host.c: a frame buffer that tests save as images, scripted keys and
+ * simulated time (tests/play.c). */
+#ifndef UT_PLAT_H
+#define UT_PLAT_H
+#include
+#include
+#include
+
+#define SCREEN_W 320
+#define SCREEN_H 240
+
+/* keys held now, as bits: the game's Z, X and C are OK, Back and toolbox */
+enum {
+ K_LEFT = 1 << 0, K_RIGHT = 1 << 1, K_UP = 1 << 2, K_DOWN = 1 << 3,
+ K_Z = 1 << 4, /* OK, EXE: confirm */
+ K_X = 1 << 5, /* Back: cancel */
+ K_C = 1 << 6, /* toolbox, shift: the menu */
+ K_HOME = 1 << 7, /* Home, On/Off: save the moment and quit */
+ K_ANY = 1 << 8
+};
+uint32_t plat_keys(void);
+uint32_t plat_millis(void);
+void plat_sleep(uint32_t ms);
+void plat_push(int x, int y, int w, int h, const uint16_t *px);
+void plat_fill(int x, int y, int w, int h, uint16_t c);
+bool plat_save(const char *name, const void *data, uint32_t len);
+const uint8_t *plat_load(const char *name, uint32_t *len); /* NULL if absent */
+/* a file of len bytes to write in place (the calculator's storage is RAM): plat_reserve gives its
+ * bytes, plat_reserved says they are written; NULL if there is no room */
+uint8_t *plat_reserve(const char *name, uint32_t len);
+void plat_reserved(const char *name, uint32_t len);
+bool plat_remove(const char *name);
+void plat_begin(void);
+int plat_end(void);
+/* an N0110 or N0115 (216 MHz, the N0120 has 550) */
+bool plat_slow(void);
+#endif
diff --git a/games/undertale/src/plat_eadk.c b/games/undertale/src/plat_eadk.c
new file mode 100644
index 00000000..4d67782a
--- /dev/null
+++ b/games/undertale/src/plat_eadk.c
@@ -0,0 +1,42 @@
+/* The calculator: EADK display, keyboard, time and files. */
+#include
+#include "../../common/epsilon_app.h"
+#include "../../common/epsilon_files.h"
+#include "plat.h"
+
+#define KEY(k) ((uint64_t)1 << (k))
+
+uint32_t plat_keys(void) {
+ uint64_t k = eadk_keyboard_scan();
+ uint32_t r = 0;
+ if (k & (KEY(eadk_key_left) | KEY(eadk_key_four))) r |= K_LEFT;
+ if (k & (KEY(eadk_key_right) | KEY(eadk_key_six))) r |= K_RIGHT;
+ if (k & (KEY(eadk_key_up) | KEY(eadk_key_eight))) r |= K_UP;
+ if (k & (KEY(eadk_key_down) | KEY(eadk_key_two))) r |= K_DOWN;
+ if (k & (KEY(eadk_key_ok) | KEY(eadk_key_exe) | KEY(eadk_key_five))) r |= K_Z;
+ if (k & (KEY(eadk_key_back) | KEY(eadk_key_backspace))) r |= K_X;
+ if (k & (KEY(eadk_key_toolbox) | KEY(eadk_key_shift) | KEY(eadk_key_var))) r |= K_C;
+ if (k & (KEY(eadk_key_home) | KEY(eadk_key_on_off))) r |= K_HOME;
+ if (k) r |= K_ANY;
+ return r;
+}
+
+uint32_t plat_millis(void) { return (uint32_t)eadk_timing_millis(); }
+void plat_sleep(uint32_t ms) { eadk_timing_msleep(ms); }
+void plat_push(int x, int y, int w, int h, const uint16_t *px) {
+ eadk_display_push_rect((eadk_rect_t){(uint16_t)x, (uint16_t)y, (uint16_t)w, (uint16_t)h}, px);
+}
+void plat_fill(int x, int y, int w, int h, uint16_t c) {
+ if (w > 0 && h > 0) eadk_display_push_rect_uniform((eadk_rect_t){(uint16_t)x, (uint16_t)y, (uint16_t)w, (uint16_t)h}, c);
+}
+bool plat_save(const char *name, const void *data, uint32_t len) { return ef_write(name, data, len); }
+const uint8_t *plat_load(const char *name, uint32_t *len) { return ef_read(name, len); }
+uint8_t *plat_reserve(const char *name, uint32_t len) {
+ uint32_t n;
+ return ef_write(name, NULL, len) ? (uint8_t *)(uintptr_t)ef_read(name, &n) : NULL; /* (the storage is RAM) */
+}
+void plat_reserved(const char *name, uint32_t len) { (void)name, (void)len; }
+bool plat_remove(const char *name) { return ef_remove(name); }
+void plat_begin(void) { np_app_begin(); }
+int plat_end(void) { return np_app_end(); }
+bool plat_slow(void) { return epsilon_slow_model(); }
diff --git a/games/undertale/src/plat_host.c b/games/undertale/src/plat_host.c
new file mode 100644
index 00000000..047ec744
--- /dev/null
+++ b/games/undertale/src/plat_host.c
@@ -0,0 +1,76 @@
+/* A computer: the screen goes to a frame buffer that tests save as images,
+ * keys come from a script, time is simulated. */
+#include
+#include
+#include "plat.h"
+
+uint16_t host_fb[SCREEN_W * SCREEN_H];
+uint32_t host_time; /* ms */
+uint32_t host_keys;
+
+uint32_t plat_keys(void) { return host_keys; }
+uint32_t plat_millis(void) { return host_time; }
+void plat_sleep(uint32_t ms) { host_time += ms; }
+void plat_push(int x, int y, int w, int h, const uint16_t *px) {
+ for (int r = 0; r < h; r++)
+ for (int c = 0; c < w; c++)
+ if ((unsigned)(x + c) < SCREEN_W && (unsigned)(y + r) < SCREEN_H) host_fb[(y + r) * SCREEN_W + x + c] = px[r * w + c];
+}
+void plat_fill(int x, int y, int w, int h, uint16_t c) {
+ for (int r = 0; r < h; r++)
+ for (int k = 0; k < w; k++)
+ if ((unsigned)(x + k) < SCREEN_W && (unsigned)(y + r) < SCREEN_H) host_fb[(y + r) * SCREEN_W + x + k] = c;
+}
+
+static char save_dir[256] = "build/host-saves";
+void host_save_dir(const char *d) { snprintf(save_dir, sizeof save_dir, "%s", d); }
+bool plat_save(const char *name, const void *data, uint32_t len) {
+ char p[512];
+ snprintf(p, sizeof p, "%s/%s", save_dir, name);
+ FILE *f = fopen(p, "wb");
+ if (!f) return false;
+ bool ok = fwrite(data, 1, len, f) == len;
+ fclose(f);
+ return ok;
+}
+/* (each file read gets its own buffer: callers may hold two at once) */
+static uint8_t loaded[4][1 << 16];
+static int next_load;
+const uint8_t *plat_load(const char *name, uint32_t *len) {
+ char p[512];
+ snprintf(p, sizeof p, "%s/%s", save_dir, name);
+ FILE *f = fopen(p, "rb");
+ if (!f) return NULL;
+ uint8_t *b = loaded[next_load++ & 3];
+ *len = (uint32_t)fread(b, 1, sizeof loaded[0], f);
+ fclose(f);
+ return b;
+}
+static uint8_t reserved[1 << 16];
+bool host_storage_full; /* tests: no room for any file */
+uint8_t *plat_reserve(const char *name, uint32_t len) {
+ (void)name;
+ return len <= sizeof reserved && !host_storage_full ? reserved : NULL;
+}
+void plat_reserved(const char *name, uint32_t len) { plat_save(name, reserved, len); }
+bool plat_remove(const char *name) {
+ char p[512];
+ snprintf(p, sizeof p, "%s/%s", save_dir, name);
+ return remove(p) == 0;
+}
+void plat_begin(void) {}
+int plat_end(void) { return 0; }
+bool plat_slow(void) { return false; }
+
+/* writes the screen as a PPM image */
+void host_shot(const char *path) {
+ FILE *f = fopen(path, "wb");
+ if (!f) return;
+ fprintf(f, "P6\n%d %d\n255\n", SCREEN_W, SCREEN_H);
+ for (int i = 0; i < SCREEN_W * SCREEN_H; i++) {
+ uint16_t c = host_fb[i];
+ uint8_t rgb[3] = {(uint8_t)((c >> 11) * 255 / 31), (uint8_t)(((c >> 5) & 63) * 255 / 63), (uint8_t)((c & 31) * 255 / 31)};
+ fwrite(rgb, 1, 3, f);
+ }
+ fclose(f);
+}
diff --git a/games/undertale/src/res.c b/games/undertale/src/res.c
new file mode 100644
index 00000000..d9a4d015
--- /dev/null
+++ b/games/undertale/src/res.c
@@ -0,0 +1,88 @@
+/* data.bin: sections, sprites, tiles, rooms and strings (see res.h). */
+#include
+#include "blk.h"
+#include "res.h"
+
+const uint8_t *ut_bin;
+
+int spr_frames(uint16_t spr) {
+ const uint8_t *s = section(SEC_SPRITES);
+ if (spr >= rd32(s)) return 0;
+ return rd16(s + 4 + 4 * spr + 2);
+}
+
+/* a frame's or tile's bytes: in its block, unpacked (blk.h) */
+static const uint8_t *in_block(int sec, int k) {
+ const uint8_t *t = section(sec) + 4 + 4 * k;
+ const uint8_t *b = blk_get(rd16(t));
+ return b ? b + rd16(t + 2) : NULL;
+}
+
+bool frame_get(uint16_t spr, int i, Frame *f) {
+ const uint8_t *s = section(SEC_SPRITES);
+ if (spr >= rd32(s)) return false;
+ int first = rd16(s + 4 + 4 * spr), n = rd16(s + 4 + 4 * spr + 2);
+ if (n <= 0) return false;
+ i %= n;
+ if (i < 0) i += n;
+ const uint8_t *p = in_block(SEC_FRAMES, first + i);
+ if (!p) return false;
+ f->w = rd16(p), f->h = rd16(p + 2);
+ f->ox = rds16(p + 4), f->oy = rds16(p + 6);
+ f->bpp = p[8], f->ncol = p[9];
+ f->pal = p + 10;
+ uint32_t o = 10 + 2u * f->ncol;
+ f->rows = p + o;
+ f->data = p + o + 2u * f->h;
+ return true;
+}
+
+int spr_w(uint16_t spr) {
+ Frame f;
+ return frame_get(spr, 0, &f) ? f.w : 0;
+}
+int spr_h(uint16_t spr) {
+ Frame f;
+ return frame_get(spr, 0, &f) ? f.h : 0;
+}
+
+void tile_get(uint16_t id, Tile *t) {
+ static const uint8_t black[4] = {0, 1, 0, 0};
+ const uint8_t *p = in_block(SEC_TILES, id);
+ if (!p) p = black;
+ t->bpp = p[0], t->ncol = p[1];
+ t->pal = p + 2;
+ t->bits = p + 2 + 2 * t->ncol;
+}
+
+void room_info(int room, RoomInfo *r) {
+ const uint8_t *s = section(SEC_ROOMS);
+ const uint8_t *p = s + 4 + 32 * room;
+ r->x = rds16(p), r->y = rds16(p + 2), r->w = rds16(p + 4), r->h = rds16(p + 6);
+ r->gx = rds16(p + 8), r->gy = rds16(p + 10);
+ r->gcols = rd16(p + 12), r->grows = rd16(p + 14);
+ r->grid = section(SEC_GRID) + rd32(p + 16);
+ r->coll = section(SEC_COLL) + rd32(p + 20);
+ r->cw = rd16(p + 24), r->ch = rd16(p + 26);
+ r->area = p[28], r->flags = p[29];
+ r->name = rd16(p + 30);
+ r->ice = (r->flags & ROOM_ICE) ? r->coll + ((r->cw + 7) >> 3) * r->ch : NULL;
+}
+
+const char *str(uint16_t id) {
+ const uint8_t *s = section(SEC_STRINGS);
+ if (id >= rd32(s)) return "";
+ return (const char *)s + rd32(s + 4 + 4 * id);
+}
+
+int room_doors(int room, Door *out, int max) {
+ const uint8_t *s = section(SEC_DOORS);
+ int first = rd16(s + 4 * room), n = rd16(s + 4 * room + 2);
+ const uint8_t *d = s + 4 * NUM_ROOMS + 16 * first;
+ if (n > max) n = max;
+ for (int i = 0; i < n; i++, d += 16) {
+ out[i].x = rds16(d), out[i].y = rds16(d + 2), out[i].w = rds16(d + 4), out[i].h = rds16(d + 6);
+ out[i].room = rd16(d + 8), out[i].tx = rds16(d + 10), out[i].ty = rds16(d + 12), out[i].dir = rd16(d + 14);
+ }
+ return n;
+}
diff --git a/games/undertale/src/res.h b/games/undertale/src/res.h
new file mode 100644
index 00000000..20217dce
--- /dev/null
+++ b/games/undertale/src/res.h
@@ -0,0 +1,67 @@
+/* Undertale: data.bin (tools/pack.py), read where it lies in flash.
+ *
+ * Its sections (SEC_* in data.h): sprites and their frames, the areas' tiles,
+ * rooms (a tile grid and a collision map each), fonts and strings. Frames and
+ * tiles are packed in LZMA blocks (blk.h): their pointers hold until the next
+ * frame_get or tile_get. */
+#ifndef UT_RES_H
+#define UT_RES_H
+#include
+#include
+#include "data.h"
+
+extern const uint8_t *ut_bin;
+static inline uint16_t rd16(const uint8_t *p) { return (uint16_t)(p[0] | p[1] << 8); }
+static inline int16_t rds16(const uint8_t *p) { return (int16_t)rd16(p); }
+static inline uint32_t rd32(const uint8_t *p) { return p[0] | p[1] << 8 | p[2] << 16 | (uint32_t)p[3] << 24; }
+static inline const uint8_t *section(int s) { return ut_bin + rd32(ut_bin + 8 + 4 * s); }
+
+/* a sprite's frame: its size and origin, palette and rows */
+typedef struct {
+ uint16_t w, h;
+ int16_t ox, oy;
+ uint8_t bpp, ncol;
+ const uint8_t *pal; /* ncol RGB565 colours (unaligned: read with rd16) */
+ const uint8_t *rows; /* u16 offsets of each row, from `data` */
+ const uint8_t *data;
+} Frame;
+bool frame_get(uint16_t spr, int i, Frame *f);
+int spr_frames(uint16_t spr);
+int spr_w(uint16_t spr);
+int spr_h(uint16_t spr);
+
+/* a 20 x 20 tile: bpp 0 (one colour), 1, 2, 4 or 8 */
+typedef struct {
+ uint8_t bpp, ncol;
+ const uint8_t *pal; /* ncol RGB565 colours */
+ const uint8_t *bits; /* 400 pixels, bpp bits each, lowest bits first */
+} Tile;
+void tile_get(uint16_t id, Tile *t);
+
+/* a room's facts */
+typedef struct {
+ int16_t x, y, w, h; /* its rectangle on the area's map */
+ int16_t gx, gy; /* the tile grid's top-left on the map */
+ uint16_t gcols, grows;
+ const uint8_t *grid; /* gcols * grows tile ids (u16), 0xFFFF none */
+ const uint8_t *coll; /* cw * ch bits, row by row ((cw + 7) / 8 bytes a row): 1 walkable */
+ uint16_t cw, ch; /* 4-pixel cells over the rectangle */
+ uint8_t area, flags;
+ uint16_t name; /* string: what SAVE shows */
+ const uint8_t *ice; /* the same bits for ice (Frisk slides), NULL none */
+} RoomInfo;
+#define ROOM_ICE 1
+void room_info(int room, RoomInfo *r);
+
+/* a door of a room: walking into its rectangle takes Frisk to another room */
+typedef struct {
+ int16_t x, y, w, h;
+ uint16_t room;
+ int16_t tx, ty; /* where Frisk lands; DOOR_KEEP: where he is */
+ uint16_t dir;
+} Door;
+#define DOOR_KEEP (-32768)
+int room_doors(int room, Door *out, int max);
+
+const char *str(uint16_t id);
+#endif
diff --git a/games/undertale/src/resort.c b/games/undertale/src/resort.c
new file mode 100644
index 00000000..d96c13bd
--- /dev/null
+++ b/games/undertale/src/resort.c
@@ -0,0 +1,179 @@
+/* The MTT Resort: the street outside (Sans, and dinner with him in the restaurant), the lobby (its fountain, the
+ * MTT-Brand Burger Emporium, the elevator that's out of order, the door north to the CORE: core.c), the hotel's hallway and
+ * its room service. (As the Undertale Wiki describes them; what they say is ours, in short.) */
+#include
+#include "event.h"
+#include "events.h"
+#include "game.h"
+#include "hotland.h"
+#include "items.h"
+#include "menu.h"
+#include "save.h"
+#include "shop.h"
+#include "snowdin.h"
+#include "world.h"
+
+static void gone(Obj *o) { o->kind = OBJ_NONE; }
+static Obj *thing(int x, int y, int w, int h, int event, int name, int arg) {
+ Obj *o = obj_trigger(x, y, w, h, event, name);
+ if (o) o->flags = OF_TALK | OF_SOLID, o->arg = (int16_t)arg;
+ return o;
+}
+static int slot_of(int item) {
+ for (int i = 0; i < NUM_ITEMS; i++)
+ if (sv.items[i] == item) return i;
+ return -1;
+}
+
+void res_setup(int room, int from) {
+ (void)from;
+ if (room < ROOM_RES_OUTSIDE || room > ROOM_RES_ROOM) return;
+ set_flag(F_HT_FLOOR3, 1);
+ switch (room) {
+ case ROOM_RES_OUTSIDE:
+ if (!flag(F_RES_SANS)) {
+ Obj *s = obj_sans(1143, 851, DIR_DOWN);
+ if (s) s->event = EV_RES_SANS, s->flags |= OF_TALK;
+ }
+ obj_sign(1260, 880, 18, 18, "* (A note: great deals up the\n creepy alley on the right!)");
+ thing(1382, 628, 36, 52, EV_RES_SHOP, ACTOR_DOOR, 1); /* Bratty and Catty's, up the alley */
+ break;
+ case ROOM_RES_LOBBY:
+ obj_save(1210, 448, "* (The calm of this hotel fills\n you with determination.)");
+ obj_sign(1180, 382, 80, 54, "* (Royal Memorial Fountain.\n Built 201X.)\n* (Mettaton added last week.)");
+ thing(1262, 226, 76, 54, EV_RES_SHOP, ACTOR_DOOR, 0); /* the MTT-Brand Burger Emporium */
+ obj_sign(1378, 228, 46, 52, "* (The elevator to the capital.)\n* (It's out of order.)");
+ thing(1100, 236, 80, 44, EV_RES_DESK, ACTOR_ITEM, 0);
+ obj_sign(1252, 496, 20, 24, "* Welcome to the MTT Resort!\n* Mettaton's own hotel!");
+ obj_sign(1354, 292, 36, 40, "* (On the phone: they won't be\n home tonight. The elevator.)");
+ obj_sign(1432, 284, 22, 34, "* Out of order?! The ONE\n elevator to the capital?!");
+ obj_sign(1394, 320, 52, 34, "* The elevator's broken, so\n we're trying to fit in all\n the guests going up.");
+ obj_sign(1290, 510, 22, 22, "* (A box. It's locked tight.)");
+ break;
+ case ROOM_RES_DINER:
+ obj_sign(820, 330, 60, 52, "* Welcome to the restaurant!|* ...you needed a reservation,\n darling. Sorry.");
+ obj_sign(640, 312, 30, 40, "* My son ran away after his\n mother fell down.|* ...I tell jokes here now.\n* Ice to meet you.");
+ obj_sign(622, 386, 18, 22, "* The CORE's made of parts\n you can swap around.|* So its layout changes when\n it needs to. Neat, huh?");
+ obj_sign(528, 376, 38, 44, "* Puzzles everywhere down here.\n* Ugh.|* They're from when we built\n them to stop humans.");
+ obj_sign(98, 440, 22, 30, "* (They forgot to book a table,\n so they're licking the ficus\n to look busy.)");
+ break;
+ case ROOM_RES_HALL:
+ for (int i = 0; i < 4; i++) thing(1652 + i * 75, 330, 36, 52, EV_RES_DOOR, ACTOR_ITEM, i + 1);
+ obj_sign(1930, 380, 44, 36, "* (They mop up the slime\n dripping off them.)\n* (It keeps dripping.)");
+ break;
+ case ROOM_RES_ROOM:
+ obj_sign(1680, 44, 120, 116, "* (A big, soft bed.)");
+ break;
+ }
+}
+
+/* ---------------------------------------------------------------- the events */
+static int ev_sans(Ev *c) {
+ Obj *s = obj_get(ACTOR_SANS);
+ EV_BEGIN;
+ if (flag(F_RES_SANS)) {
+ SAY(FACE_SANS, "* hey. good luck in there.");
+ return 1;
+ }
+ SAY(FACE_SANS, "* hey. you made it.|* how about dinner?\n* my treat.@Sure@Not now");
+ if (CHOICE() == 1) {
+ SAY(FACE_SANS, "* ok. i'll be around.");
+ return 1;
+ }
+ /* the restaurant: a table, and what he has to say */
+ FADE_TO(ROOM_RES_DINER, 236, 418, DIR_RIGHT);
+ s = obj_sans(338, 450, DIR_LEFT);
+ SAY(FACE_SANS, "* so. i keep watch in the\n forest by snowdin, right?|* out there's this big door.\n locked tight.|* so i'd knock and tell jokes\n through it. for fun.|* one day, somebody on the\n other side knocked back.|* an old lady. laughed at all\n of my jokes.|* we kept at it. day after\n day.");
+ SAY(FACE_SANS, "* then she asked me for a\n favor.|* if a human ever came out of\n that door...|* ...would i look after them?\n keep them safe?|* ...|* i don't like promises.\n but i made that one.|* and without it...|* well. you'd be dust by now,\n buddy.");
+ SAY(FACE_SANS, "* ...heh. lighten up.\n* thanks for dinner.|* wait. i said i'd pay, huh.\n* ...catch you later.");
+ set_flag(F_RES_SANS, 1);
+ s = obj_get(ACTOR_SANS);
+ WALK(s, s->x, 362, 2); /* up past the tables, out east */
+ s = obj_get(ACTOR_SANS);
+ WALK(s, 780, 362, 3);
+ gone(obj_get(ACTOR_SANS));
+ EV_END;
+}
+
+static int ev_shop(Ev *c) {
+ EV_BEGIN;
+ shop_open(c->self && c->self->arg ? SHOP_BRATTY : SHOP_BURGER);
+ WAIT_UNTIL(g_mode == MODE_WORLD);
+ EV_END;
+}
+
+/* the hotel's front desk: a room for 200G (door 2) */
+static int ev_desk(Ev *c) {
+ EV_BEGIN;
+ if (flag(F_RES_ROOM)) {
+ NARRATE(flag(F_RES_ROOM) == 1 ? "* Your room is the second\n door down the hall." : "* We hope you enjoyed your stay!");
+ return 1;
+ }
+ NARRATE("* Welcome to the MTT Resort!\n* A room is 200G a night.@Stay@Leave");
+ if (CHOICE() == 1) return 1;
+ if (sv.gold < 200) {
+ NARRATE("* ...you don't have enough\n money. Sorry!");
+ return 1;
+ }
+ sv.gold = (uint16_t)(sv.gold - 200);
+ set_flag(F_RES_ROOM, 1);
+ NARRATE("* Your room is the second\n door down the hall. Enjoy!");
+ EV_END;
+}
+
+/* the hotel's doors: room service for 1 and 3; 2 the room from the desk; a dog behind 4 */
+static int ev_door(Ev *c) {
+ EV_BEGIN;
+ c->v[0] = c->self ? c->self->arg : 1;
+ if (c->v[0] == 2) {
+ if (flag(F_RES_ROOM) != 1) {
+ NARRATE("* (The door is locked.)");
+ return 1;
+ }
+ world_fade_hold(ROOM_RES_ROOM, 1730, 158, DIR_DOWN, 90);
+ WAIT_UNTIL(!world_fading());
+ sv.hp = sv.maxhp;
+ set_flag(F_RES_ROOM, 2);
+ NARRATE("* (You slept soundly. Your HP\n was maxed out.)");
+ return 1;
+ }
+ if (c->v[0] == 4) {
+ if (flag(F_RES_DOG) || slot_of(ITEM_HOT_DOG) < 0) {
+ NARRATE("* (Something behind the door is\n panting.)\n* (It sounds like a dog.)");
+ return 1;
+ }
+ NARRATE("* (Something behind the door is\n panting.)|* (Slide a Hot Dog under it?)@Yes@No");
+ if (CHOICE() == 1) return 1;
+ item_remove_at(slot_of(ITEM_HOT_DOG));
+ item_add(ITEM_HUSH_PUPPY);
+ set_flag(F_RES_DOG, 1);
+ NARRATE("* (It tugs at the Hot Dog a few\n times, then gets it.)|* (A Hush Puppy slides back out.)");
+ return 1;
+ }
+ c->v[1] = c->v[0] == 1 ? ITEM_SEA_TEA : ITEM_CINNAMON_BUNNY;
+ c->v[2] = c->v[0] == 1 ? F_RES_TEA : F_RES_BUN;
+ if (flag(c->v[2])) {
+ NARRATE("* (Nobody's ordered anything\n else.)");
+ return 1;
+ }
+ NARRATE(c->v[0] == 1 ? "* (A note on the door: room\n service, please. A Sea Tea.)"
+ : "* (A note on the door: room\n service, please. A Cinnamon\n Bunny.)");
+ if (slot_of(c->v[1]) < 0) return 1;
+ NARRATE("* (Slide it under the door?)@Yes@No");
+ if (CHOICE() == 1) return 1;
+ item_remove_at(slot_of(c->v[1]));
+ sv.gold = (uint16_t)min_i(sv.gold + 99, 9999);
+ set_flag(c->v[2], 1);
+ NARRATE("* (Someone takes it, and slides\n 99G back under the door.)");
+ EV_END;
+}
+
+int res_event(Ev *c) {
+ switch (c->id) {
+ case EV_RES_SANS: return ev_sans(c);
+ case EV_RES_SHOP: return ev_shop(c);
+ case EV_RES_DOOR: return ev_door(c);
+ case EV_RES_DESK: return ev_desk(c);
+ }
+ return -1;
+}
diff --git a/games/undertale/src/rooms.c b/games/undertale/src/rooms.c
new file mode 100644
index 00000000..52b9e7b7
--- /dev/null
+++ b/games/undertale/src/rooms.c
@@ -0,0 +1,112 @@
+/* The rooms' objects and their random encounters. */
+#include
+#include "battle.h"
+#include "event.h"
+#include "events.h"
+#include "game.h"
+#include "hotland.h"
+#include "monsters.h"
+#include "newhome.h"
+#include "ruins.h"
+#include "snowdin.h"
+#include "waterfall.h"
+#include "world.h"
+
+void room_setup(int room, int from) {
+ ruins1_setup(room, from);
+ ruins2_setup(room, from);
+ ruins3_setup(room, from);
+ snow1_setup(room, from);
+ snow2_setup(room, from);
+ town_setup(room, from);
+ wf1_setup(room, from);
+ wf2_setup(room, from);
+ wf3_setup(room, from);
+ ht1_setup(room, from);
+ ht2_setup(room, from);
+ ht3_setup(room, from);
+ res_setup(room, from);
+ core_setup(room, from);
+ nh_setup(room, from);
+}
+
+/* each frame Frisk can walk: what a room watches (a rock on its switch, a cracked floor) */
+void room_update(void) {
+ ruins2_update();
+ snow1_update();
+ snow2_update();
+ wf1_update();
+ wf3_update();
+ ht1_update();
+ ht2_update();
+ ht3_update();
+ core_update();
+}
+
+/* what a room draws on its floor, under everything (world.c) */
+void room_draw_floor(void) {
+ snow1_draw();
+ snow2_draw();
+ ht1_draw();
+ ht2_draw();
+ ht3_draw();
+ core_draw();
+}
+
+/* what a room draws over everything (a clock) */
+void room_draw_top(void) {
+ ht2_draw_top();
+ ht3_draw_top();
+}
+
+/* a pushed rock that may go where Frisk can't (a bridge seed into the water), and what happens once it has */
+bool room_rock_enters(Obj *o, int x, int y) { return wf_seed_enters(o, x, y); }
+void room_rock_moved(Obj *o) { wf_seed_moved(o); }
+
+int room_speed(int x, int y) { return ht3_speed(x, y); }
+
+bool room_ice(int x, int y) {
+ const RoomInfo *ri = world_room_info();
+ if (!ri || !ri->ice) return false;
+ int cx = (x - ri->x) >> 2, cy = (y - ri->y) >> 2;
+ if (cx < 0 || cy < 0 || cx >= ri->cw || cy >= ri->ch) return false;
+ return ri->ice[cy * ((ri->cw + 7) >> 3) + (cx >> 3)] >> (cx & 7) & 1;
+}
+
+/* random encounters: a room's formations and how many steps between them (the game counts its steps,
+ * then a "!" shows over Frisk) */
+typedef struct {
+ uint8_t room;
+ uint8_t forms[6];
+ uint8_t n;
+} Encounters;
+static const Encounters enc[] = {
+ {ROOM_RUINS7, {FORM_FROGGIT, FORM_WHIMSUN}, 2},
+ {ROOM_RUINS8, {FORM_FROGGIT, FORM_WHIMSUN}, 2},
+ {ROOM_RUINS9, {FORM_FROGGIT, FORM_WHIMSUN}, 2},
+ {ROOM_RUINS10, {FORM_FROGGIT, FORM_WHIMSUN, FORM_FROGGIT_WHIMSUN, FORM_FROGGIT2, FORM_MOLDSMAL}, 5},
+ {ROOM_RUINS11, {FORM_FROGGIT_WHIMSUN, FORM_FROGGIT2, FORM_MOLDSMAL, FORM_MOLDSMAL2, FORM_MOLDSMAL3}, 5},
+ {ROOM_RUINS15, {FORM_LOOX, FORM_VEGETOID, FORM_LOOX_VEG_MIGOSP, FORM_VEGETOID_MIGOSP, FORM_MOLDSMAL_MIGOSP, FORM_LOOX2}, 6},
+ {ROOM_RUINS16, {FORM_LOOX, FORM_VEGETOID, FORM_MOLDSMAL_MIGOSP, FORM_VEGETOID2}, 4},
+ {ROOM_RUINS17A, {FORM_LOOX, FORM_VEGETOID, FORM_MOLDSMAL_MIGOSP}, 3},
+ {ROOM_RUINS17B, {FORM_LOOX, FORM_VEGETOID, FORM_MOLDSMAL_MIGOSP}, 3},
+ {ROOM_RUINS17C, {FORM_LOOX, FORM_VEGETOID, FORM_MOLDSMAL_MIGOSP}, 3},
+ {ROOM_RUINS17D, {FORM_LOOX, FORM_VEGETOID, FORM_MOLDSMAL_MIGOSP}, 3},
+};
+static int steps_left = 120;
+bool g_no_encounters; /* (the tests' --noenc) */
+
+void encounter_step(void) {
+ if (!pl.moving || g_no_encounters) return;
+ const Encounters *e = NULL;
+ for (unsigned i = 0; i < sizeof enc / sizeof enc[0]; i++)
+ if (enc[i].room == g_room) e = &enc[i];
+ /* Snowdin's and Waterfall's are snowmon.c's and wfmon.c's (who comes depends on what's happened) */
+ int snow = e ? -1 : snow_encounter(g_room);
+ if (!e && snow < 0) snow = wf_encounter(g_room);
+ if (!e && snow < 0) return;
+ if (--steps_left > 0) return;
+ steps_left = 100 + rnd_n(200);
+ event_start(EV_ENCOUNTER, NULL);
+ event_cur_arg(e ? e->forms[rnd_n(e->n)] : snow);
+}
diff --git a/games/undertale/src/ruins.h b/games/undertale/src/ruins.h
new file mode 100644
index 00000000..ed5d85f3
--- /dev/null
+++ b/games/undertale/src/ruins.h
@@ -0,0 +1,12 @@
+/* Undertale: the Ruins' rooms and events (ruins1.c, ruins2.c, ruins3.c). */
+#ifndef UT_RUINS_H
+#define UT_RUINS_H
+#include "event.h"
+void ruins1_setup(int room, int from);
+int ruins1_event(Ev *c); /* -1: not one of its */
+void ruins2_setup(int room, int from);
+int ruins2_event(Ev *c);
+void ruins2_update(void);
+void ruins3_setup(int room, int from);
+int ruins3_event(Ev *c);
+#endif
diff --git a/games/undertale/src/ruins1.c b/games/undertale/src/ruins1.c
new file mode 100644
index 00000000..63bc826c
--- /dev/null
+++ b/games/undertale/src/ruins1.c
@@ -0,0 +1,393 @@
+/* The Ruins, from the bed of golden flowers to the leaf pile: Flowey, then Toriel guiding the fallen human
+ * through her puzzles, the Dummy, the spikes, the independence test. Texts are the game's. */
+#include
+#include
+#include "battle.h"
+#include "event.h"
+#include "events.h"
+#include "game.h"
+#include "menu.h"
+#include "monsters.h"
+#include "ruins.h"
+#include "save.h"
+#include "world.h"
+
+/* ---------------------------------------------------------------- the rooms' things */
+void ruins1_setup(int room, int from) {
+ (void)from;
+ switch (room) {
+ case ROOM_AREA1:
+ if (flag(F_FLOWEY_MET)) obj_sign(120, 6015, 60, 40, "* (Golden flowers.)\n* (They must have broken your\n fall.)");
+ break;
+ case ROOM_AREA1_2:
+ if (!flag(F_FLOWEY_MET)) {
+ static const uint16_t s[4] = {SPR_FLOWEY};
+ Obj *f = obj_npc(ACTOR_FLOWEY, s, 508, 5768, DIR_DOWN, 0);
+ if (f) f->flags = OF_SOLID, obj_set_box(f, 4, 16, 17, 8);
+ obj_trigger(430, 5800, 180, 40, EV_FLOWEY_MEET, ACTOR_TRIGGER);
+ }
+ break;
+ case ROOM_RUINS1:
+ obj_save(510, 5275, "* (The shadow of the ruins\n looms above, filling you with\n determination.)");
+ if (flag(F_TORIEL_MET) && !flag(F_RUINS1_DONE)) {
+ obj_toriel(504, 5330, DIR_DOWN);
+ obj_trigger(380, 5380, 280, 180, EV_RUINS1_TORIEL, ACTOR_TRIGGER);
+ }
+ break;
+ case ROOM_RUINS2:
+ obj_sign(440, 4880, 20, 20, "* \"Only the fearless may\n proceed.\"|* \"Brave ones, foolish ones.\"|* \"Both walk not the middle\n road.\"");
+ for (int k = 0; k < 6; k++) {
+ static const int16_t px[6] = {540, 580, 560, 540, 580, 560}, py[6] = {4938, 4938, 4958, 4978, 4978, 4998};
+ Obj *p = obj_deco(SPR_O_GROUNDSWITCH1, px[k], py[k], false);
+ if (p) p->flags |= OF_BACK, p->frame = flag(F_RUINS2_DONE) && (k == 0 || k == 1 || k == 3 || k == 4) ? 1 : 0, p->arg = (int16_t)k;
+ }
+ {
+ Obj *lever = obj_deco(SPR_O_SWITCH, 560, 4888, false);
+ if (lever) lever->flags |= OF_BACK, lever->frame = flag(F_RUINS2_DONE) ? 1 : 0, lever->name = ACTOR_SWITCH;
+ }
+ if (!flag(F_RUINS2_DONE)) {
+ Obj *d = obj_deco(SPR_O_RUINSDOOR1, 480, 4860, true);
+ if (d) d->name = ACTOR_DOOR, obj_set_box(d, 0, 30, 40, 30);
+ obj_toriel(500, 4952, DIR_DOWN);
+ obj_trigger(400, 4920, 240, 140, EV_RUINS2_TORIEL, ACTOR_TRIGGER);
+ }
+ break;
+ case ROOM_RUINS3: {
+ obj_sign(488, 4724, 20, 18, "* \"Stay on the path.\"");
+ static const int16_t sx[3] = {742, 942, 980};
+ for (int k = 0; k < 3; k++) {
+ Obj *s = obj_deco(SPR_O_SWITCH, sx[k], 4650, false);
+ if (!s) continue;
+ s->name = (uint8_t)(ACTOR_SWITCH + k), s->flags = OF_TALK | OF_BACK, s->event = EV_RUINS3_SWITCH;
+ obj_set_box(s, 0, 0, 20, 34);
+ s->frame = flag(F_RUINS3_SW1 + k) ? 1 : 0;
+ }
+ obj_spikes(1060, 4740, 1, 2, !flag(F_RUINS3_DONE), ACTOR_SPIKES);
+ if (!flag(F_RUINS3_DONE)) {
+ obj_toriel(470, 4716, DIR_DOWN);
+ if (!flag(F_RUINS3_SW1) && !flag(F_RUINS3_SW2)) obj_trigger(440, 4760, 120, 70, EV_RUINS3_TORIEL, ACTOR_TRIGGER);
+ }
+ break;
+ }
+ case ROOM_RUINS4: {
+ Obj *d = obj_deco(SPR_O_DUMMY, 1300, 4690, true);
+ if (d) d->name = ACTOR_DUMMY, d->flags |= OF_TALK, d->event = EV_RUINS4_DUMMY, obj_set_box(d, 2, 14, 16, 18);
+ if (!flag(F_DUMMY)) {
+ obj_toriel(1236, 4690, DIR_RIGHT);
+ obj_trigger(1080, 4680, 120, 140, EV_RUINS4_TORIEL, ACTOR_TRIGGER);
+ }
+ break;
+ }
+ case ROOM_RUINS5:
+ obj_sign(1680, 4478, 20, 20, "* \"The western room is the\n eastern room's blueprint.\"");
+ if (!flag(F_RUINS5_DONE)) {
+ obj_toriel(1250, 4470, DIR_DOWN);
+ obj_trigger(1230, 4500, 90, 100, EV_RUINS5_TORIEL, ACTOR_TRIGGER);
+ }
+ break;
+ case ROOM_RUINS6: {
+ Obj *p = obj_deco(SPR_O_PILLAR, 4602, 4400, true);
+ if (p) p->name = ACTOR_PILLAR, p->flags |= OF_TALK, p->event = EV_RUINS6_PILLAR, obj_set_box(p, 4, 44, 32, 16);
+ if (!flag(F_RUINS6_DONE)) {
+ obj_toriel(2330, 4466, DIR_RIGHT);
+ obj_trigger(2280, 4440, 120, 110, EV_RUINS6_TORIEL, ACTOR_TRIGGER);
+ }
+ break;
+ }
+ case ROOM_RUINS7: {
+ obj_save(5010, 4500, "* (Playfully crinkling through\n the leaves fills you with\n determination.)");
+ static const uint16_t fs[4] = {SPR_O_SMALLFROG};
+ if (!sv.kills) obj_npc(ACTOR_FROGGIT, fs, 4901, 4440, DIR_DOWN, EV_RUINS7_FROGGIT);
+ if (flag(F_PHONE) && !flag(F_LEAFPILE_CALL)) event_start(EV_RUINS7_CALL, NULL);
+ break;
+ }
+ }
+}
+
+/* ---------------------------------------------------------------- the events */
+static const char flowey_hi[] =
+ "`0* Howdy!^2 I'm #YFLOWEY#X.^2\n* #YFLOWEY#X the #YFLOWER#X!|"
+ "`1* Hmmm...|"
+ "`0* You're new to the\n UNDERGROUND, aren'tcha?|"
+ "`1* Golly, you must be so\n confused.|"
+ "`0* Someone ought to teach you\n how things work around here!|"
+ "`0* I guess little old me will\n have to do.|"
+ "`7* Ready?^2 Here we go!";
+
+static int ev_flowey_meet(Ev *c) {
+ Obj *f, *t;
+ EV_BEGIN;
+ if (c->self) c->self->kind = OBJ_NONE;
+ SAY(FACE_FLOWEY, flowey_hi);
+ BATTLE(FORM_FLOWEY);
+ /* Toriel stands where Flowey was */
+ f = obj_find(ACTOR_FLOWEY);
+ if (f) f->kind = OBJ_NONE;
+ obj_toriel(500, 5742, DIR_DOWN);
+ set_flag(F_FLOWEY_MET, 1);
+ set_flag(F_TORIEL_MET, 1);
+ WAIT(10);
+ SAY(FACE_TORIEL, "`0* This way.");
+ t = obj_find(ACTOR_TORIEL);
+ if (t) WALK_TO(t, 504, 5580, 2);
+ WAIT_UNTIL(!(t = obj_find(ACTOR_TORIEL)) || !t->walking);
+ if ((t = obj_find(ACTOR_TORIEL))) t->kind = OBJ_NONE;
+ EV_END;
+}
+
+static int ev_ruins1_toriel(Ev *c) {
+ Obj *t = obj_find(ACTOR_TORIEL);
+ EV_BEGIN;
+ if (c->self) c->self->kind = OBJ_NONE;
+ c->v[0] = 0;
+ /* she goes up the stairs, then through the door */
+ if (!t) return 1;
+ WALK(t, 580, 5300, 2);
+ WALK(t, 606, 5250, 2);
+ WALK(t, 606, 5160, 2);
+ WALK(t, 504, 5150, 2);
+ WALK(t, 504, 5100, 2);
+ t->kind = OBJ_NONE;
+ set_flag(F_RUINS1_DONE, 1);
+ EV_END;
+}
+
+static int ev_ruins2_toriel(Ev *c) {
+ Obj *t = obj_find(ACTOR_TORIEL);
+ EV_BEGIN;
+ if (c->self) c->self->kind = OBJ_NONE;
+ if (!t) return 1;
+ t->dir = DIR_DOWN;
+ SAY(FACE_TORIEL, "`0* Welcome to your new\n home, little one.|* Let me show you how things\n work in the RUINS.");
+ /* over the switches, not the middle road */
+ WALK(t, 534, 4952, 2);
+ WALK(t, 534, 4898, 2);
+ c->v[0] = 0;
+ for (c->v[1] = 0; c->v[1] < MAX_OBJS; c->v[1]++)
+ if (objs[c->v[1]].kind == OBJ_DECO && objs[c->v[1]].spr[0] == SPR_O_GROUNDSWITCH1 && objs[c->v[1]].arg == 0) objs[c->v[1]].frame = 1;
+ WALK(t, 574, 4898, 2);
+ for (c->v[1] = 0; c->v[1] < MAX_OBJS; c->v[1]++)
+ if (objs[c->v[1]].kind == OBJ_DECO && objs[c->v[1]].spr[0] == SPR_O_GROUNDSWITCH1 && objs[c->v[1]].arg == 1) objs[c->v[1]].frame = 1;
+ WALK(t, 574, 4938, 2);
+ for (c->v[1] = 0; c->v[1] < MAX_OBJS; c->v[1]++)
+ if (objs[c->v[1]].kind == OBJ_DECO && objs[c->v[1]].spr[0] == SPR_O_GROUNDSWITCH1 && objs[c->v[1]].arg == 4) objs[c->v[1]].frame = 1;
+ WALK(t, 534, 4938, 2);
+ for (c->v[1] = 0; c->v[1] < MAX_OBJS; c->v[1]++)
+ if (objs[c->v[1]].kind == OBJ_DECO && objs[c->v[1]].spr[0] == SPR_O_GROUNDSWITCH1 && objs[c->v[1]].arg == 3) objs[c->v[1]].frame = 1;
+ WALK(t, 544, 4880, 2);
+ t->dir = DIR_UP;
+ WAIT(12);
+ {
+ Obj *l = obj_find(ACTOR_SWITCH), *d = obj_find(ACTOR_DOOR);
+ if (l) l->frame = 1;
+ if (d) d->kind = OBJ_NONE;
+ }
+ WAIT(15);
+ t->dir = DIR_DOWN;
+ SAY(FACE_TORIEL, "`0* The RUINS are full of\n puzzles that open the way.|* Solve each one to reach\n the next room.");
+ set_flag(F_RUINS2_DONE, 1);
+ WALK(t, 484, 4900, 2);
+ WALK(t, 484, 4840, 2);
+ t->kind = OBJ_NONE;
+ EV_END;
+}
+
+static int ev_ruins3_toriel(Ev *c) {
+ Obj *t = obj_find(ACTOR_TORIEL);
+ EV_BEGIN;
+ if (c->self) c->self->kind = OBJ_NONE;
+ if (!t) return 1;
+ SAY(FACE_TORIEL, "`0* Here you must flip some\n switches.|* I marked the right ones\n for you.");
+ WALK(t, 560, 4716, 3);
+ WALK(t, 990, 4716, 3);
+ t->dir = DIR_LEFT;
+ EV_END;
+}
+
+static int ev_ruins3_switch(Ev *c) {
+ int k = c->self ? c->self->name - ACTOR_SWITCH : 0;
+ EV_BEGIN;
+ if (flag(F_RUINS3_SW1 + k)) {
+ NARRATE("* (The switch is already\n flipped.)");
+ return 1;
+ }
+ if (k == 2 && !flag(F_RUINS3_DONE) && obj_find(ACTOR_TORIEL)) {
+ SAY(FACE_TORIEL, "`3* Oh? Not that switch.|`0* Only the ones I marked,\n my child.");
+ return 1;
+ }
+ set_flag(F_RUINS3_SW1 + k, 1);
+ c->self->frame = 1;
+ NARRATE("* (You flipped the switch.)");
+ if (flag(F_RUINS3_SW1) && flag(F_RUINS3_SW2) && !flag(F_RUINS3_DONE)) {
+ set_flag(F_RUINS3_DONE, 1);
+ {
+ Obj *s = obj_find(ACTOR_SPIKES);
+ if (s) s->frame = 1, s->flags &= (uint16_t)~OF_SOLID;
+ }
+ WAIT(10);
+ if (obj_find(ACTOR_TORIEL)) {
+ SAY(FACE_TORIEL, "`2* Splendid!\n* I am proud of you, little\n one.|`0* Let us move to the next\n room.");
+ WALK(obj_get(ACTOR_TORIEL), 1040, 4730, 3);
+ WALK(obj_get(ACTOR_TORIEL), 1100, 4730, 3);
+ obj_get(ACTOR_TORIEL)->kind = OBJ_NONE;
+ }
+ }
+ EV_END;
+}
+
+static int ev_ruins4_toriel(Ev *c) {
+ EV_BEGIN;
+ if (c->self) c->self->kind = OBJ_NONE;
+ SAY(FACE_TORIEL, "`0* Monsters down here may\n try to fight you.|`2* Do not fear!\n* It is simple.|`0* In a FIGHT, talk kindly\n and wait.|* I will come and sort\n things out.|* Try talking to the\n dummy.");
+ EV_END;
+}
+
+static int ev_ruins4_dummy(Ev *c) {
+ EV_BEGIN;
+ if (flag(F_DUMMY)) {
+ NARRATE("* (A cotton dummy.)");
+ return 1;
+ }
+ BATTLE(FORM_DUMMY);
+ if (battle_result() == BR_FLED) {
+ set_flag(F_DUMMY, 3);
+ SAY(FACE_TORIEL, "`3* ...you ran from it.|`0* Not a bad choice, really.\n* Avoiding a fight is often\n wise.|* Still, it is just a\n cotton dummy.|* Come along.");
+ } else if (bt.mon[0].killed) {
+ set_flag(F_DUMMY, 2);
+ if (c->self) c->self->kind = OBJ_NONE;
+ SAY(FACE_TORIEL, "`3* Oh! Dummies are for\n talking, not hitting!|`1* We do not want to hurt\n anyone.|`0* Come along.");
+ } else if (bt.turn >= 5 && !flag(F_DUMMY)) {
+ set_flag(F_DUMMY, 4);
+ SAY(FACE_TORIEL, "`3* ...the dummy grew tired\n of being looked at.|`0* Well. Let us move on.");
+ } else {
+ set_flag(F_DUMMY, 1);
+ SAY(FACE_TORIEL, "`2* Ah, very good!\n* You are very good.");
+ }
+ if (obj_find(ACTOR_TORIEL)) {
+ WALK(obj_get(ACTOR_TORIEL), 1244, 4640, 2);
+ WALK(obj_get(ACTOR_TORIEL), 1244, 4560, 2);
+ obj_get(ACTOR_TORIEL)->kind = OBJ_NONE;
+ }
+ EV_END;
+}
+
+/* the spikes' safe way: the cells of the blueprint (tools/rooms.py has the same) */
+static const int16_t spike_path[][2] = {
+ {1916, 4497}, {2000, 4497}, {2000, 4458}, {2060, 4458}, {2060, 4497}, {2160, 4497}, {2160, 4458}, {2236, 4458}};
+
+static int ev_ruins5_toriel(Ev *c) {
+ Obj *t = obj_find(ACTOR_TORIEL);
+ EV_BEGIN;
+ if (c->self) c->self->kind = OBJ_NONE;
+ if (!t) return 1;
+ /* she goes ahead; halfway, a Froggit */
+ FREE(true);
+ WALK(t, 1500, 4470, 3);
+ WALK(obj_get(ACTOR_TORIEL), 1820, 4480, 3);
+ obj_get(ACTOR_TORIEL)->dir = DIR_LEFT;
+ WAIT_UNTIL(pl.x > 1500);
+ FREE(false);
+ BATTLE(FORM_FROGGIT_FIRST);
+ FREE(true);
+ WAIT_UNTIL(pl.x > 1740);
+ FREE(false);
+ t = obj_find(ACTOR_TORIEL);
+ t->dir = DIR_LEFT;
+ SAY(FACE_TORIEL, "`0* This is the puzzle,\n but...|* Here, take my hand for a\n moment.");
+ /* hand in hand over the spikes */
+ PL_WALK(t->x - 14, t->y + 10, 3);
+ for (c->v[0] = 0; c->v[0] < (int)(sizeof spike_path / sizeof spike_path[0]); c->v[0]++) {
+ WALK_TO(obj_get(ACTOR_TORIEL), spike_path[c->v[0]][0] + 6, spike_path[c->v[0]][1] - 30, 2);
+ pl_walk_to(spike_path[c->v[0]][0] - 16 > pl.x ? spike_path[c->v[0]][0] - 16 : pl.x, spike_path[c->v[0]][1] - 20, 2);
+ WAIT_UNTIL(!obj_get(ACTOR_TORIEL)->walking && !pl_walking());
+ }
+ pl_walk_to(2240, 4444, 2);
+ WAIT_UNTIL(!pl_walking());
+ obj_get(ACTOR_TORIEL)->dir = DIR_LEFT;
+ SAY(FACE_TORIEL, "`0* Puzzles seem too dangerous\n for now.");
+ set_flag(F_RUINS5_DONE, 1);
+ WALK(obj_get(ACTOR_TORIEL), 2320, 4440, 3);
+ obj_get(ACTOR_TORIEL)->kind = OBJ_NONE;
+ EV_END;
+}
+
+static int ev_ruins6_toriel(Ev *c) {
+ Obj *t = obj_find(ACTOR_TORIEL);
+ EV_BEGIN;
+ if (c->self) c->self->kind = OBJ_NONE;
+ if (!t) return 1;
+ t->dir = DIR_LEFT;
+ SAY(FACE_TORIEL, "`0* You are doing very well,\n my child.|`1* Now, a hard request...|`0* Walk to the end of this\n room on your own.|`1* I am sorry.");
+ /* she hurries off, and hides behind the pillar */
+ FREE(true);
+ WALK(obj_get(ACTOR_TORIEL), 4560, 4466, 6);
+ WALK(obj_get(ACTOR_TORIEL), 4606, 4392, 3);
+ obj_get(ACTOR_TORIEL)->dir = DIR_DOWN;
+ obj_get(ACTOR_TORIEL)->flags &= (uint16_t)~OF_SOLID;
+ obj_trigger(4470, 4430, 16, 130, EV_RUINS6_REVEAL, ACTOR_TRIGGER2);
+ EV_END;
+}
+
+static int ev_ruins6_pillar(Ev *c) {
+ Obj *t = obj_find(ACTOR_TORIEL);
+ EV_BEGIN;
+ if (!t || flag(F_RUINS6_DONE)) {
+ NARRATE("* (A pillar.)");
+ return 1;
+ }
+ SAY(FACE_TORIEL, "`0* ...");
+ EV_END;
+}
+
+static int ev_ruins6_reveal(Ev *c) {
+ Obj *t = obj_find(ACTOR_TORIEL);
+ EV_BEGIN;
+ if (!t) return 1;
+ if (c->self) c->self->kind = OBJ_NONE;
+ WALK(t, 4570, 4440, 2);
+ t->dir = DIR_LEFT;
+ SAY(FACE_TORIEL, "`0* There you are!|* I was just behind the\n pillar.|`2* Thank you for trusting\n me.|`0* That was a test of your\n independence.|`1* I have errands to run.\n* Please wait here for me.|`2* Take this CELL PHONE.|`0* Call me if you need\n anything. Be good!");
+ set_flag(F_PHONE, 1);
+ WALK(obj_get(ACTOR_TORIEL), 4700, 4470, 3);
+ WALK(obj_get(ACTOR_TORIEL), 4860, 4470, 3);
+ obj_get(ACTOR_TORIEL)->kind = OBJ_NONE;
+ set_flag(F_RUINS6_DONE, 1);
+ /* (the game saves here when she leaves) */
+ sv.room = (int16_t)g_room, sv.x = pl.x, sv.y = pl.y;
+ save_write_auto();
+ EV_END;
+}
+
+static int ev_ruins7_call(Ev *c) {
+ EV_BEGIN;
+ WAIT_UNTIL(!world_fading());
+ set_flag(F_LEAFPILE_CALL, 1);
+ NARRATE("* Ring...");
+ SAY(FACE_TORIEL, "`0* Hello? It is TORIEL.|* You are still in that\n room, yes?|* The puzzles ahead are\n tricky. Wait for me.|* Be good!");
+ EV_END;
+}
+
+static int ev_ruins7_froggit(Ev *c) {
+ EV_BEGIN;
+ NARRATE("* Ribbit, ribbit.|* (Some advice, human.)|* (ACT the right way, or weaken\n a monster...)|* (...and it may stop wanting\n to fight.)|* (Then show it MERCY.)\n* Ribbit.");
+ EV_END;
+}
+
+int ruins1_event(Ev *c) {
+ switch (c->id) {
+ case EV_FLOWEY_MEET: return ev_flowey_meet(c);
+ case EV_RUINS1_TORIEL: return ev_ruins1_toriel(c);
+ case EV_RUINS2_TORIEL: return ev_ruins2_toriel(c);
+ case EV_RUINS3_TORIEL: return ev_ruins3_toriel(c);
+ case EV_RUINS3_SWITCH: return ev_ruins3_switch(c);
+ case EV_RUINS4_TORIEL: return ev_ruins4_toriel(c);
+ case EV_RUINS4_DUMMY: return ev_ruins4_dummy(c);
+ case EV_RUINS5_TORIEL: return ev_ruins5_toriel(c);
+ case EV_RUINS6_TORIEL: return ev_ruins6_toriel(c);
+ case EV_RUINS6_PILLAR: return ev_ruins6_pillar(c);
+ case EV_RUINS6_REVEAL: return ev_ruins6_reveal(c);
+ case EV_RUINS7_CALL: return ev_ruins7_call(c);
+ case EV_RUINS7_FROGGIT: return ev_ruins7_froggit(c);
+ }
+ return -1;
+}
diff --git a/games/undertale/src/ruins2.c b/games/undertale/src/ruins2.c
new file mode 100644
index 00000000..178643b3
--- /dev/null
+++ b/games/undertale/src/ruins2.c
@@ -0,0 +1,574 @@
+/* The Ruins, from the Monster Candy to Toriel's tree: the cracked floors, the rocks, Napstablook, the Spider
+ * Bake Sale, the Froggits' hall, the one switch, the rotating rooms, the balcony over Home. Toriel calls on
+ * the way. Texts are the game's (the Undertale Wiki's records of them) where known. */
+#include
+#include
+#include "battle.h"
+#include "event.h"
+#include "events.h"
+#include "game.h"
+#include "items.h"
+#include "menu.h"
+#include "monsters.h"
+#include "ruins.h"
+#include "save.h"
+#include "world.h"
+
+/* ---------------------------------------------------------------- falling through a cracked floor */
+/* where each fall lands: the room under, Frisk's top-left there (x -1: where he was; y -1: 400 under) */
+typedef struct {
+ uint8_t room;
+ int16_t x, y;
+} Fall;
+enum { FALL_RUINS8, FALL_LEAVES, FALL_ONE_A, FALL_ROTATE = FALL_ONE_A + 6 };
+static const Fall falls[] = {
+ [FALL_RUINS8] = {ROOM_RUINS8B, 5258, 5132},
+ [FALL_LEAVES] = {ROOM_RUINS10B, -1, -1},
+ [FALL_ONE_A] = {ROOM_RUINS16A, 8340, 4945}, {ROOM_RUINS16B, 8460, 4945}, {ROOM_RUINS16C, 8580, 4945},
+ {ROOM_RUINS16D, 8340, 5065}, {ROOM_RUINS16E, 8460, 5065}, {ROOM_RUINS16F, 8580, 5065},
+ [FALL_ROTATE] = {ROOM_RUINS17X, 9270, 4570},
+};
+
+static Obj *crack(int x, int y, int w, int h, int fall) {
+ Obj *o = obj_trigger(x, y, w, h, EV_FALLING, ACTOR_NONE);
+ if (o) o->arg = (int16_t)fall;
+ return o;
+}
+
+static int ev_fall(Ev *c) {
+ EV_BEGIN;
+ {
+ const Fall *f = &falls[c->self ? c->self->arg : c->v[4]];
+ c->v[0] = f->room;
+ c->v[1] = f->x < 0 ? pl.x : f->x;
+ c->v[2] = f->y < 0 ? pl.y + 400 : f->y;
+ }
+ if (g_room == ROOM_RUINS10) set_flag(F_FALL_LEAVES, flag(F_FALL_LEAVES) + 1);
+ pl.hidden = true;
+ WAIT(6);
+ FADE_TO(c->v[0], c->v[1], c->v[2] - 60, DIR_DOWN);
+ pl.hidden = false;
+ /* down onto the leaves */
+ for (c->v[3] = 0; c->v[3] < 12; c->v[3]++) {
+ pl.y = (int16_t)(pl.y + 5);
+ world_camera();
+ WAIT(0);
+ }
+ WAIT(10);
+ EV_END;
+}
+
+/* the leaf puzzle: the tiles that give way (under them, the leaves) */
+static const int16_t leaf_holes[][4] = {
+ {6160, 4740, 80, 160}, {6280, 4780, 20, 100}, {6340, 4840, 20, 60}, {6400, 4780, 60, 40}, {6400, 4820, 40, 60}};
+/* the one switch puzzle: its six cracks */
+static const int16_t one_cracks[6][2] = {{8342, 4582}, {8462, 4582}, {8582, 4582}, {8342, 4702}, {8462, 4702}, {8582, 4702}};
+
+/* ---------------------------------------------------------------- the rotating rooms */
+/* each room's three switches: x, y, colour (0 red, 1 blue, 2 green), right? A hidden one is under a pillar
+ * (its box the pillar's) */
+typedef struct {
+ int16_t x, y, w, h;
+ uint8_t color, right, hidden;
+} CSwitch;
+static const CSwitch cswitch[4][3] = {
+ {{9020, 4780, 20, 20, 0, 0, 0}, {8800, 4680, 20, 20, 1, 0, 0}, {8970, 4620, 20, 20, 2, 0, 0}},
+ {{8830, 4397, 20, 20, 0, 0, 0}, {8916, 4156, 48, 78, 1, 1, 1}, {8990, 4320, 20, 20, 2, 0, 0}},
+ {{8400, 4200, 20, 20, 0, 1, 0}, {8620, 4300, 20, 20, 1, 0, 0}, {8460, 4360, 20, 20, 2, 0, 0}},
+ {{8576, 3676, 48, 78, 0, 0, 1}, {8490, 3920, 20, 20, 1, 0, 0}, {8416, 3716, 48, 78, 2, 1, 1}},
+};
+static int rotate_fails;
+
+static int rotate_index(int room) {
+ switch (room) {
+ case ROOM_RUINS17A: return 0;
+ case ROOM_RUINS17B: return 1;
+ case ROOM_RUINS17C: return 2;
+ case ROOM_RUINS17D: return 3;
+ default: return -1;
+ }
+}
+
+static void rotate_setup(int k) {
+ for (int i = 0; i < 3; i++) {
+ const CSwitch *s = &cswitch[k][i];
+ Obj *o = obj_sign(s->x, s->y, s->w, s->h, NULL);
+ if (o) o->event = EV_COLOR_SWITCH, o->arg = (int16_t)(k * 3 + i), o->name = (uint8_t)(ACTOR_SWITCH + i);
+ if (o && !s->hidden) obj_set_box(o, 2, 6, 16, 14);
+ }
+ static const char *const plaque[4] = {
+ "* \"The far door is not an\n exit.\"|* \"It simply marks a rotation\n in perspective.\"",
+ "* \"If you can read this,\n press the blue switch.\"",
+ "* \"If you can read this,\n press the red switch.\"",
+ "* \"If you can read this,\n press the green switch.\""};
+ static const int16_t px[4][2] = {{8800, 4536}, {8800, 4238}, {8622, 4238}, {8502, 3656}};
+ obj_sign(px[k][0], px[k][1], 40, 20, plaque[k]);
+ /* the doors' spikes: where Frisk came in, down; the far door's, up until the right switch */
+ switch (k) {
+ case 0:
+ obj_spikes(8760, 4620, 1, 2, false, ACTOR_NONE);
+ obj_spikes(8960, 4840, 2, 1, false, ACTOR_NONE);
+ break;
+ case 1:
+ obj_spikes(8960, 4140, 2, 1, false, ACTOR_NONE);
+ obj_spikes(8760, 4340, 1, 2, !flag(F_ROTATE2), ACTOR_SPIKES);
+ break;
+ case 2:
+ obj_spikes(8660, 4340, 1, 2, false, ACTOR_NONE);
+ obj_spikes(8440, 4140, 2, 1, !flag(F_ROTATE3), ACTOR_SPIKES);
+ break;
+ case 3:
+ obj_spikes(8440, 3960, 2, 1, false, ACTOR_NONE);
+ obj_spikes(8660, 3740, 1, 2, !flag(F_ROTATE4), ACTOR_SPIKES);
+ break;
+ }
+}
+
+static int ev_color_switch(Ev *c) {
+ EV_BEGIN;
+ {
+ int k = c->self ? c->self->arg : 0;
+ const CSwitch *s = &cswitch[k / 3][k % 3];
+ if (flag(F_ROTATE1 + k / 3) && s->right) {
+ NARRATE("* (The switch is already\n pressed.)");
+ return 1;
+ }
+ c->v[0] = k;
+ }
+ if (cswitch[c->v[0] / 3][c->v[0] % 3].hidden) NARRATE("* (You found a switch hidden\n behind the pillar.)");
+ NARRATE("* (You pressed the switch.)");
+ if (cswitch[c->v[0] / 3][c->v[0] % 3].right) {
+ set_flag(F_ROTATE1 + c->v[0] / 3, 1);
+ obj_spikes_set(obj_find(ACTOR_SPIKES), false);
+ return 1;
+ }
+ rotate_fails++;
+ /* the floor gives way */
+ c->self = NULL, c->v[4] = FALL_ROTATE;
+ c->at = NULL, c->id = EV_FALLING;
+ return ev_fall(c);
+ EV_END;
+}
+
+/* ---------------------------------------------------------------- the rooms' things */
+static const uint16_t frog_spr[4] = {SPR_O_SMALLFROG};
+
+static Obj *item_here(uint16_t spr, int x, int y, int item, int f) {
+ if (flag(f)) return NULL;
+ Obj *o = obj_deco(spr, x, y, false);
+ if (o) o->flags |= OF_TALK | OF_BACK, o->event = EV_ITEM, o->arg = (int16_t)item, o->arg2 = (int16_t)f;
+ return o;
+}
+
+/* the second rock puzzle's talking rock, by how far it has been asked */
+static void rock3_place(Obj *r) {
+ int s = flag(F_RUINS11_ROCK);
+ static const int16_t at[6][2] = {{6720, 4875}, {6740, 4875}, {6740, 4855}, {6780, 4875}, {6740, 4875}, {6780, 4875}};
+ r->x = at[s][0], r->y = at[s][1];
+}
+
+void ruins2_setup(int room, int from) {
+ (void)from;
+ int k = rotate_index(room);
+ if (k >= 0) rotate_setup(k);
+ switch (room) {
+ case ROOM_RUINS7A: {
+ static const uint16_t dish[3] = {SPR_O_CANDYDISH, SPR_O_CANDYDISH2, SPR_O_CANDYDISH_BAD};
+ Obj *d = obj_deco(dish[flag(F_CANDY_SPILT) ? 2 : 0], 4950, 4215, true);
+ if (d) d->flags |= OF_TALK, d->event = EV_CANDY, d->name = ACTOR_CANDY;
+ break;
+ }
+ case ROOM_RUINS8:
+ crack(5244, 4683, 72, 79, FALL_RUINS8);
+ break;
+ case ROOM_RUINS9: {
+ obj_sign(5600, 4640, 20, 20, "* \"Three out of four gray rocks\n recommend you push them.\"");
+ Obj *r = obj_rock(5620, 4735, ACTOR_ROCK, 0);
+ if (r && flag(F_RUINS9_ROCK)) r->x = 5680;
+ obj_spikes(5740, 4680, 1, 6, !flag(F_RUINS9_ROCK), ACTOR_SPIKES);
+ if (!flag(F_FLAVOR_CALL)) event_start(EV_CALL, NULL), event_cur_arg(CALL_FLAVOR);
+ break;
+ }
+ case ROOM_RUINS10:
+ obj_sign(6380, 4740, 20, 20, "* \"Didn't you read the sign\n downstairs?\"");
+ if (flag(F_FALL_LEAVES) < 10)
+ for (unsigned i = 0; i < sizeof leaf_holes / sizeof leaf_holes[0]; i++)
+ crack(leaf_holes[i][0] + 2, leaf_holes[i][1] + 2, leaf_holes[i][2] - 4, leaf_holes[i][3] - 4, FALL_LEAVES);
+ if (flag(F_FLAVOR_CALL) == 1) event_start(EV_CALL, NULL), event_cur_arg(CALL_FLAVOR2);
+ break;
+ case ROOM_RUINS10B:
+ obj_sign(6380, 5140, 20, 20, "* \"Please don't step on the\n leaves.\"");
+ break;
+ case ROOM_RUINS11: {
+ obj_rock(6700, 4795, ACTOR_ROCK, 0);
+ obj_rock(6680, 4835, ACTOR_ROCK2, 0);
+ Obj *r = obj_rock(6720, 4875, ACTOR_ROCK3, EV_ROCK_TALK);
+ if (r) rock3_place(r);
+ int s = flag(F_RUINS11_ROCK);
+ obj_spikes(6880, 4820, 2, 3, s != 3 && s != 5, ACTOR_SPIKES);
+ if (s == 3) obj_trigger(6830, 4800, 30, 100, EV_ROCK_OFF, ACTOR_TRIGGER);
+ if (flag(F_FLAVOR_CALL) == 2) event_start(EV_CALL, NULL), event_cur_arg(CALL_ALLERGY);
+ break;
+ }
+ case ROOM_RUINS12:
+ obj_save(7170, 4860, "* (Knowing the mouse might one\n day leave its hole and get\n the cheese...)|* (It fills you with\n determination.)");
+ obj_sign(7240, 4860, 30, 20, "* (The cheese has been stuck\n to the table for so long...)|* (...it's crystallized.)");
+ obj_sign(7164, 4798, 14, 12, "* (Squeak!)");
+ break;
+ case ROOM_RUINS13:
+ if (!flag(F_NAPSTA)) {
+ Obj *n = obj_deco(SPR_O_NAPSTABLOOK_GR, 7525, 4832, true);
+ if (n) n->name = ACTOR_NAPSTA, n->flags |= OF_TALK, n->event = EV_NAPSTA_SLEEP, obj_set_box(n, -7, -14, 44, 44);
+ obj_trigger(7496, 4818, 20, 44, EV_NAPSTA_SLEEP, ACTOR_TRIGGER);
+ }
+ break;
+ case ROOM_RUINS14: {
+ obj_sign(7834, 4818, 22, 20, "* \"Spider Bake Sale\"|* \"All proceeds go to real\n spiders.\"");
+ Obj *w = obj_sign(7788, 4782, 30, 30, NULL);
+ if (w) w->event = EV_WEB, w->arg = 0;
+ w = obj_sign(7850, 4758, 48, 50, NULL);
+ if (w) w->event = EV_WEB, w->arg = 1;
+ break;
+ }
+ case ROOM_RUINS15: {
+ static const int16_t fx[3] = {7660, 7780, 7900};
+ for (int i = 0; i < 3; i++) {
+ if (i == 0 && flag(F_SKIPS) > 5) continue;
+ Obj *f = obj_npc(ACTOR_FROGGIT, frog_spr, fx[i], 4540, DIR_DOWN, EV_FROGGIT);
+ if (f) f->arg = (int16_t)i, f->flags |= OF_ANIM, f->anim_speed = 20;
+ }
+ if (!flag(F_ITEM_CALL)) {
+ Obj *t = obj_trigger(7960, 4540, 40, 80, EV_CALL, ACTOR_TRIGGER);
+ if (t) t->arg = CALL_ITEMS;
+ }
+ break;
+ }
+ case ROOM_RUINS16:
+ obj_sign(8180, 4520, 20, 20, "* \"There is just one switch.\"");
+ for (int i = 0; i < 6; i++) crack(one_cracks[i][0], one_cracks[i][1], 16, 17, FALL_ONE_A + i);
+ obj_spikes(8680, 4620, 1, 2, !flag(F_ONESWITCH), ACTOR_SPIKES);
+ break;
+ case ROOM_RUINS16A:
+ case ROOM_RUINS16F: {
+ Obj *v = obj_deco(SPR_O_VEGETABLEOUTSIDE, room == ROOM_RUINS16A ? 8300 : 8660, room == ROOM_RUINS16A ? 4980 : 5117, true);
+ if (v) v->flags |= OF_TALK, v->event = EV_VEGGIE;
+ break;
+ }
+ case ROOM_RUINS16B: {
+ Obj *s = obj_deco(SPR_O_SWITCH, 8460, 4935, false);
+ if (s) s->flags |= OF_TALK | OF_BACK, s->event = EV_ONE_SWITCH, s->frame = flag(F_ONESWITCH) ? 1 : 0, obj_set_box(s, 0, 0, 20, 30);
+ break;
+ }
+ case ROOM_RUINS16D:
+ item_here(SPR_O_RIBBON, 8260, 5120, ITEM_FADED_RIBBON, F_RIBBON);
+ break;
+ case ROOM_RUINS16E:
+ if (flag(F_NAPSTA) == 1 && !flag(F_NAPSTA_LIE)) {
+ Obj *n = obj_deco(SPR_O_NAPSTABLOOK_GR, 8495, 5117, true);
+ if (n) n->name = ACTOR_NAPSTA, n->flags |= OF_TALK, n->event = EV_NAPSTA_LIE;
+ }
+ break;
+ case ROOM_RUINS17B:
+ if (rotate_fails >= 2 && !flag(F_ROTATE2)) say_on_entry("* (You notice a switch hidden\n behind the pillar.)");
+ break;
+ case ROOM_RUINS19: {
+ Obj *f = obj_npc(ACTOR_FROGGIT, frog_spr, 9400, 3700, DIR_DOWN, EV_FROGGIT);
+ if (f) f->arg = 3, f->flags |= OF_ANIM, f->anim_speed = 20;
+ break;
+ }
+ case ROOM_RUINS20:
+ item_here(SPR_O_GROUNDKNIFE, 9364, 3566, ITEM_TOY_KNIFE, F_TOYKNIFE);
+ break;
+ case ROOM_RUINS21:
+ obj_save(8940, 3120, "* (Seeing such a cute, tidy\n house in the RUINS gives\n you determination.)");
+ obj_sign(9004, 3290, 34, 30, "* (Every time this old tree\n grows any leaves, they fall\n right off.)");
+ if (!flag(F_TORIEL_HOME)) {
+ Obj *t = obj_toriel(8996, 3240, DIR_DOWN);
+ if (t) t->spr[0] = SPR_O_TORIEL_D_PHONE, t->flags &= (uint16_t)~OF_WALKANIM;
+ obj_trigger(8880, 3330, 280, 40, EV_TORIEL_TREE, ACTOR_TRIGGER);
+ }
+ break;
+ }
+}
+
+/* each frame: the rocks on their switches */
+void ruins2_update(void) {
+ if (g_room == ROOM_RUINS9 && !flag(F_RUINS9_ROCK)) {
+ Obj *r = obj_find(ACTOR_ROCK);
+ if (r && r->x >= 5676 && r->x <= 5684) {
+ set_flag(F_RUINS9_ROCK, 1);
+ r->x = 5680;
+ r->flags &= (uint16_t)~OF_SOLID; /* it stays */
+ obj_spikes_set(obj_find(ACTOR_SPIKES), false);
+ }
+ }
+}
+
+/* ---------------------------------------------------------------- the events */
+static int ev_candy(Ev *c) {
+ EV_BEGIN;
+ if (flag(F_CANDY_SPILT)) {
+ NARRATE("* Look at what you've done.");
+ return 1;
+ }
+ if (item_count() >= NUM_ITEMS) {
+ NARRATE("* You tried to take a piece of\n candy, but you didn't have\n any room.");
+ return 1;
+ }
+ set_flag(F_CANDY_TAKEN, flag(F_CANDY_TAKEN) + 1);
+ if (flag(F_CANDY_TAKEN) >= (flag(F_HARDMODE) ? 3 : 4)) {
+ set_flag(F_CANDY_SPILT, 1);
+ if (c->self) c->self->spr[0] = SPR_O_CANDYDISH_BAD;
+ NARRATE("* You took too much too fast.\n* The candy spills onto the\n floor.");
+ return 1;
+ }
+ item_add(ITEM_MONSTER_CANDY);
+ if (flag(F_CANDY_TAKEN) == 1) NARRATE("* You took a piece of candy.\n* (Press [TOOLBOX] to open\n the menu.)");
+ else if (flag(F_CANDY_TAKEN) == 2) NARRATE("* You took more candy.\n* How disgusting...");
+ else if (flag(F_HARDMODE)) NARRATE("* In this hellish world, you\n can only take 3 pieces of\n candy...");
+ else NARRATE("* You take another piece.\n* You feel like the scum of\n the earth...");
+ EV_END;
+}
+
+static const char call_hello[] = "* Ring...|`0* Hello?\n* This is TORIEL.|";
+
+static int ev_call(Ev *c) {
+ EV_BEGIN;
+ if (c->self) c->v[0] = c->self->arg, c->self->kind = OBJ_NONE;
+ WAIT_UNTIL(!world_fading());
+ WAIT(15);
+ switch (c->v[0]) {
+ case CALL_FLAVOR:
+ set_flag(F_FLAVOR_CALL, 1);
+ SAY(FACE_TORIEL, "* Ring...|`0* Hello? TORIEL here.|* A small question...|* Cinnamon or butterscotch?@Cinnamon@Bscotch");
+ set_flag(F_FLAVOR, CHOICE() ? 2 : 1);
+ SAY(FACE_TORIEL, "`0* Oh, I see.\n* Thank you very much!");
+ break;
+ case CALL_FLAVOR2:
+ set_flag(F_FLAVOR_CALL, 2);
+ SAYF(FACE_TORIEL, "%s* You would still eat\n %s, though?|* Just checking!|* Thank you for waiting.",
+ call_hello, flag(F_FLAVOR) == 2 ? "cinnamon" : "butterscotch");
+ break;
+ case CALL_ALLERGY:
+ set_flag(F_FLAVOR_CALL, 3), set_flag(F_ALLERGY_CALL, 1);
+ SAY(FACE_TORIEL, "* Ring...|`0* Hello?|* Any allergies, by the way?|`1* Why do I ask? Oh...|`0* No reason at all.");
+ break;
+ case CALL_ITEMS:
+ set_flag(F_ITEM_CALL, 1);
+ SAY(FACE_TORIEL, "* Ring...|`0* Hello?|* Things are a bit messy\n around here, I am afraid.|* Pick up what you like,\n but leave room in your\n pockets.|* You never know what you\n will want later.");
+ break;
+ }
+ EV_END;
+}
+
+static int ev_rock_talk(Ev *c) {
+ Obj *r = obj_find(ACTOR_ROCK3);
+ EV_BEGIN;
+ if (!r) return 1;
+ switch (flag(F_RUINS11_ROCK)) {
+ case 0:
+ SAY(FACE_NONE, "* Hey now! Who's shoving?|* ...|* Oh, you want me to move?\n* You could have just asked.");
+ set_flag(F_RUINS11_ROCK, 1);
+ WALK(obj_get(ACTOR_ROCK3), 6740, 4875, 1);
+ NARRATE("* (The rock moved a little,\n and stopped.)");
+ break;
+ case 1:
+ SAY(FACE_NONE, "* Huh? Not far enough?\n* Alright, alright.");
+ set_flag(F_RUINS11_ROCK, 2);
+ WALK(obj_get(ACTOR_ROCK3), 6740, 4855, 1);
+ break;
+ case 2:
+ NARRATE("* (You tell the rock it's\n going the wrong way.)");
+ SAY(FACE_NONE, "* Oh! Why didn't ya say so,\n pardner?");
+ set_flag(F_RUINS11_ROCK, 3);
+ WALK(obj_get(ACTOR_ROCK3), 6740, 4875, 1);
+ WALK(obj_get(ACTOR_ROCK3), 6780, 4875, 1);
+ obj_spikes_set(obj_find(ACTOR_SPIKES), false);
+ obj_trigger(6830, 4800, 30, 100, EV_ROCK_OFF, ACTOR_TRIGGER);
+ break;
+ case 3:
+ case 5:
+ SAY(FACE_NONE, "* Well, here I am, pardner.");
+ break;
+ case 4:
+ NARRATE("* (You ask the rock to stay\n on the switch.)");
+ SAY(FACE_NONE, "* Stay here? Sure thing.|* Asking works, you know.");
+ set_flag(F_RUINS11_ROCK, 5);
+ WALK(obj_get(ACTOR_ROCK3), 6780, 4875, 1);
+ obj_spikes_set(obj_find(ACTOR_SPIKES), false);
+ break;
+ }
+ EV_END;
+}
+
+static int ev_rock_off(Ev *c) {
+ EV_BEGIN;
+ if (c->self) c->self->kind = OBJ_NONE;
+ if (flag(F_RUINS11_ROCK) != 3) return 1;
+ set_flag(F_RUINS11_ROCK, 4);
+ WALK(obj_get(ACTOR_ROCK3), 6740, 4875, 1);
+ obj_spikes_set(obj_find(ACTOR_SPIKES), true);
+ NARRATE("* (The rock rolled off the\n switch.)");
+ EV_END;
+}
+
+static int ev_napsta_sleep(Ev *c) {
+ EV_BEGIN;
+ if (flag(F_NAPSTA)) return 1;
+ NARRATE("* (A ghost says 'z' over and\n over, faking sleep.)|* Push through it?@Yes@No");
+ if (CHOICE() != 0) {
+ /* he steps back out of its way */
+ PL_WALK(pl.x - 12, pl.y, 2);
+ return 1;
+ }
+ BATTLE(FORM_NAPSTABLOOK);
+ if (!flag(F_NAPSTA)) set_flag(F_NAPSTA, battle_result() == BR_FLED ? 3 : 1);
+ obj_get(ACTOR_NAPSTA)->kind = OBJ_NONE;
+ {
+ Obj *t = obj_find(ACTOR_TRIGGER);
+ if (t) t->kind = OBJ_NONE;
+ }
+ EV_END;
+}
+
+static int ev_napsta_lie(Ev *c) {
+ EV_BEGIN;
+ SAY(FACE_NONE, "* oh...^2 it's you...|* i was just lying here...|* ...^2 i'll get out of your\n way...");
+ for (c->v[0] = 255; c->v[0] > 0; c->v[0] -= 15) {
+ obj_get(ACTOR_NAPSTA)->alpha = (uint8_t)c->v[0];
+ WAIT(1);
+ }
+ obj_get(ACTOR_NAPSTA)->kind = OBJ_NONE;
+ set_flag(F_NAPSTA_LIE, 1);
+ EV_END;
+}
+
+static int ev_web(Ev *c) {
+ EV_BEGIN;
+ c->v[0] = c->self ? c->self->arg : 0;
+ if (c->v[0] == 0) NARRATE("* (There's a note on the web.)\n* \"Spider Donut: 7G\"|* Leave 7G in the web?@Yes@No");
+ else NARRATE("* (There's a note on the web.)\n* \"Spider Cider: 18G\"|* Leave 18G in the web?@Yes@No");
+ if (CHOICE() != 0) return 1;
+ {
+ int cost = c->v[0] ? 18 : 7;
+ if (sv.gold < cost) {
+ NARRATE("* You didn't have enough gold.");
+ return 1;
+ }
+ if (item_count() >= NUM_ITEMS) {
+ NARRATE("* You are carrying too many\n items.");
+ return 1;
+ }
+ sv.gold = (uint16_t)(sv.gold - cost);
+ item_add(c->v[0] ? ITEM_SPIDER_CIDER : ITEM_SPIDER_DONUT);
+ set_flag(c->v[0] ? F_CIDER_BOUGHT : F_DONUT_BOUGHT, 1);
+ }
+ if (c->v[0] == 0) NARRATE("* Some spiders crawled down\n and gave you a donut.");
+ else NARRATE("* Some spiders crawled down\n and gave you a jug.");
+ EV_END;
+}
+
+static int ev_froggit(Ev *c) {
+ EV_BEGIN;
+ c->v[0] = c->self ? c->self->arg : 0;
+ switch (c->v[0]) {
+ case 0:
+ NARRATE("* Ribbit, ribbit.|* (Tip: [BACK] shows the\n whole text at once.)");
+ break;
+ case 1:
+ NARRATE("* Ribbit, ribbit.|* (Hey, human.)|* (Have you seen the key with\n a 4 on your calculator?)|* (I'm told 4 means \"four\n frogs.\")|* (But I only see three frogs\n in this room...)");
+ break;
+ case 2:
+ if (!flag(F_FROGGIT_YELLOW))
+ NARRATE("* Ribbit, ribbit.|* (A #YYELLOW#X name means you\n can SPARE that monster.)|* (Too easy, some say.\n Should we drop yellow?)@Yes@No");
+ else if (flag(F_FROGGIT_YELLOW) == 1)
+ NARRATE("* Ribbit, ribbit.|* (Everyone dropped yellow.)|* (Try #PPINK#X names\n instead?)@Yes@No");
+ else
+ NARRATE("* Ribbit, ribbit.|* (Pink names...^2 Or should\n everyone go back to #YYELLOW#X?)@Yellow@Pink");
+ if (flag(F_FROGGIT_YELLOW) == 2) set_flag(F_FROGGIT_YELLOW, CHOICE() == 0 ? 0 : 2);
+ else if (CHOICE() == 0) set_flag(F_FROGGIT_YELLOW, flag(F_FROGGIT_YELLOW) + 1);
+ NARRATE("* Ribbit.");
+ break;
+ case 3:
+ NARRATE("* Ribbit, ribbit.|* (#BTORIEL#X went by with\n groceries a while ago.)|* (Nobody dared ask what\n for.)");
+ break;
+ }
+ EV_END;
+}
+
+static int ev_item(Ev *c) {
+ EV_BEGIN;
+ if (!c->self) return 1;
+ if (item_count() >= NUM_ITEMS) {
+ NARRATE("* (You're carrying too much.)");
+ return 1;
+ }
+ item_add(c->self->arg);
+ set_flag(c->self->arg2, 1);
+ c->v[0] = c->self->arg;
+ c->self->kind = OBJ_NONE;
+ SAYF(FACE_NONE, "* (You got the %s.)", item_info(c->v[0])->name);
+ EV_END;
+}
+
+static int ev_one_switch(Ev *c) {
+ EV_BEGIN;
+ if (flag(F_ONESWITCH)) {
+ NARRATE("* (The switch is already\n flipped.)");
+ return 1;
+ }
+ set_flag(F_ONESWITCH, 1);
+ if (c->self) c->self->frame = 1;
+ NARRATE("* (You flipped the switch.)\n* (Somewhere, spikes lowered.)");
+ EV_END;
+}
+
+static int ev_veggie(Ev *c) {
+ EV_BEGIN;
+ NARRATE("* (It's a vegetable.)|* (...it looks back at you.)");
+ BATTLE(FORM_VEGETOID);
+ if (c->self) c->self->kind = OBJ_NONE;
+ EV_END;
+}
+
+static int ev_toriel_tree(Ev *c) {
+ Obj *t = obj_find(ACTOR_TORIEL);
+ EV_BEGIN;
+ if (c->self) c->self->kind = OBJ_NONE;
+ if (!t) return 1;
+ SAY(FACE_TORIEL, "`0* Hello?\n* This is TORIEL.|`7* ...!");
+ t = obj_get(ACTOR_TORIEL);
+ t->spr[0] = SPR_TORIEL_D;
+ t->flags |= OF_WALKANIM;
+ WALK(t, 8996, pl.y - 52, 3);
+ SAY(FACE_TORIEL, "`7* How did you get here, my\n child?|`3* Are you hurt?");
+ if (sv.hp >= sv.maxhp) SAY(FACE_TORIEL, "`0* Not a scratch...\n* Impressive!|`1* But still...");
+ else if (sv.hp > 2) SAY(FACE_TORIEL, "`0* There, there, I will heal\n you.");
+ else SAY(FACE_TORIEL, "`5* Who did this to you?\n* You will get an apology.");
+ sv.hp = sv.maxhp;
+ SAY(FACE_TORIEL, "`1* I left you alone too long.\n* I am sorry.|`0* Oh, the surprise is\n spoiled now...|`2* Come along, little one!");
+ set_flag(F_TORIEL_HOME, 1);
+ WALK(obj_get(ACTOR_TORIEL), 9006, 3070, 3);
+ obj_get(ACTOR_TORIEL)->kind = OBJ_NONE;
+ EV_END;
+}
+
+int ruins2_event(Ev *c) {
+ switch (c->id) {
+ case EV_FALLING: return ev_fall(c);
+ case EV_COLOR_SWITCH: return ev_color_switch(c);
+ case EV_CANDY: return ev_candy(c);
+ case EV_CALL: return ev_call(c);
+ case EV_ROCK_TALK: return ev_rock_talk(c);
+ case EV_ROCK_OFF: return ev_rock_off(c);
+ case EV_NAPSTA_SLEEP: return ev_napsta_sleep(c);
+ case EV_NAPSTA_LIE: return ev_napsta_lie(c);
+ case EV_WEB: return ev_web(c);
+ case EV_FROGGIT: return ev_froggit(c);
+ case EV_ITEM: return ev_item(c);
+ case EV_ONE_SWITCH: return ev_one_switch(c);
+ case EV_VEGGIE: return ev_veggie(c);
+ case EV_TORIEL_TREE: return ev_toriel_tree(c);
+ }
+ return -1;
+}
diff --git a/games/undertale/src/ruins3.c b/games/undertale/src/ruins3.c
new file mode 100644
index 00000000..74a60e74
--- /dev/null
+++ b/games/undertale/src/ruins3.c
@@ -0,0 +1,317 @@
+/* The end of the Ruins: Toriel's home, the basement where she goes to destroy the exit, her fight, and Flowey
+ * waiting past the door. Texts are the game's (the Undertale Wiki's records of them) where known. */
+#include
+#include
+#include "battle.h"
+#include "event.h"
+#include "events.h"
+#include "game.h"
+#include "items.h"
+#include "menu.h"
+#include "monsters.h"
+#include "ruins.h"
+#include "save.h"
+#include "world.h"
+
+/* ---------------------------------------------------------------- the rooms' things */
+void ruins3_setup(int room, int from) {
+ (void)from;
+ switch (room) {
+ case ROOM_TORHOUSE1:
+ obj_sign(8880, 2740, 24, 22, "* (Inside is an old calendar\n from the beginning of 201X.)");
+ obj_sign(9100, 2728, 44, 24, "* (It's a bookshelf.)\n* (The books are worn out.)");
+ if (flag(F_TORIEL_HOME) && !flag(F_HOME_TOUR)) {
+ obj_toriel(8996, 2800, DIR_DOWN);
+ event_start(EV_HOME_TOUR, NULL);
+ }
+ if (!flag(F_TORIEL_BASEMENT)) obj_trigger(9078, 2734, 12, 28, EV_STAIRS, ACTOR_TRIGGER);
+ break;
+ case ROOM_TORHOUSE2: {
+ bool reading = flag(F_HOME_TOUR) && !flag(F_TORIEL_BASEMENT);
+ Obj *ch = obj_deco(reading ? SPR_O_CHAIRIEL : SPR_O_CHAIREMPTY, 8640, 2723, true);
+ if (ch) ch->name = ACTOR_CHAIR, ch->flags |= OF_TALK, ch->event = EV_TORIEL_CHAIR, obj_set_box(ch, 10, 36, 44, 24);
+ obj_sign(8673, 2690, 68, 66, flag(F_TORIEL_FIGHT) == 2 ? "* (The fire has gone out.)"
+ : "* (The fire isn't burning\n hot...)\n* (Just pleasantly warm.)|* (You could put your hand\n inside.)");
+ obj_sign(8753, 2692, 60, 60, "* (It's a history book.)|* \"Trapped behind the barrier\n and fearful of further human\n attacks, we retreated.\"|* \"Far, far into the earth we\n walked, until we reached the\n cavern's end.\"|* \"This was our new home, which\n we named...\"|* \"Home.\"|* \"As great as our king is, he\n is pretty lousy at names.\"");
+ break;
+ }
+ case ROOM_TORKITCHEN:
+ obj_sign(8560, 2540, 38, 62, "* (For some reason, there is a\n brand-name chocolate bar in\n the fridge.)");
+ obj_sign(8603, 2560, 22, 40, "* (There's a clump of white\n fur stuck in the drain.)");
+ obj_sign(8630, 2580, 22, 22, "* (Inside the cupboard are\n cookie cutters for\n gingerbread monsters.)");
+ obj_sign(8654, 2560, 30, 40, flag(F_BED_SLEPT) ? "* (It's a butterscotch-\n cinnamon pie.)\n* (Its size intimidates you.)"
+ : "* (It's a butterscotch-\n cinnamon pie.)|* (It smells nice, but it's\n too hot to eat.)");
+ obj_sign(8687, 2560, 32, 40, flag(F_TORIEL_FIGHT) == 2 ? "* (No one will use this\n anymore...)"
+ : "* (The stovetop is very\n clean.)\n* (Toriel must use fire magic\n instead.)");
+ break;
+ case ROOM_TORHOUSE3:
+ obj_sign(9404, 2750, 24, 54, "* (You have seen this type of\n plant before but do not know\n its name.)");
+ obj_sign(9492, 2750, 24, 54, "* (You have seen this type of\n plant before but do not know\n its name.)");
+ obj_sign(9746, 2775, 30, 47, "* (Room under renovations.)");
+ obj_sign(9810, 2770, 60, 52, "* (It's you!)");
+ if (flag(F_TORIEL_HOME) && !flag(F_HOME_TOUR)) obj_toriel(9370, 2780, DIR_DOWN);
+ break;
+ case ROOM_ASRIELROOM:
+ {
+ Obj *b = obj_sign(9368, 2522, 52, 70, NULL);
+ if (b) b->event = EV_BED;
+ }
+ obj_sign(9260, 2498, 62, 34, "* (Toys you aren't interested\n in.)|* (An empty photo frame.)");
+ obj_sign(9335, 2498, 32, 48, "* (A box of shoes in many\n different sizes.)");
+ if (flag(F_BED_SLEPT) == 1) {
+ Obj *p = obj_deco(SPR_O_PIESLICE, 9322, 2572, false);
+ if (p) p->flags |= OF_TALK | OF_BACK, p->event = EV_ITEM, p->arg = flag(F_HARDMODE) ? ITEM_SNAIL_PIE : ITEM_BUTTERSCOTCH_PIE,
+ p->arg2 = F_PIE;
+ }
+ break;
+ case ROOM_TORIELROOM:
+ obj_sign(9540, 2520, 74, 92, "* (It's bigger than a\n twin-sized bed.)");
+ obj_sign(9592, 2598, 26, 30, "* (Toriel's small chair.)\n* (Its name is Chairiel.)");
+ obj_sign(9690, 2528, 62, 36, "* (It's Toriel's diary.)|* (You read the passage...)|* \"Why did the skeleton want a\n friend?\"|* \"Because she was feeling\n BONELY...\"");
+ obj_sign(9632, 2478, 58, 54, "* (You peek inside...)\n* (Scandalous!)|* (It's TORIEL's sock drawer.)");
+ break;
+ case ROOM_BASEMENT1:
+ if (flag(F_TORIEL_BASEMENT) && !flag(F_TORIEL_FIGHT)) {
+ static const int16_t tx[2] = {9030, 9560};
+ int k = flag(F_BASEMENT_TALK);
+ if (k < 2) {
+ obj_toriel(tx[k], 2300, DIR_RIGHT);
+ obj_trigger(tx[k] - 70, 2300, 16, 80, EV_BASEMENT_TALK, ACTOR_TRIGGER);
+ }
+ }
+ break;
+ case ROOM_BASEMENT2:
+ if (flag(F_TORIEL_BASEMENT) && !flag(F_TORIEL_FIGHT)) {
+ if (flag(F_BASEMENT_TALK) < 3) {
+ obj_toriel(10004, 2110, DIR_UP);
+ obj_trigger(9960, 2170, 140, 14, EV_BASEMENT_TALK, ACTOR_TRIGGER);
+ } else {
+ obj_toriel(10004, 1984, DIR_DOWN);
+ obj_trigger(9960, 2060, 140, 14, EV_TORIEL_DOOR, ACTOR_TRIGGER2);
+ }
+ }
+ break;
+ case ROOM_RUINSEXIT:
+ if (!flag(F_FLOWEY_END)) {
+ static const uint16_t s[4] = {SPR_FLOWEY};
+ Obj *f = obj_npc(ACTOR_FLOWEY, s, 10008, 205, DIR_DOWN, 0);
+ if (f) f->flags = OF_HIDDEN, obj_set_box(f, 4, 16, 17, 8);
+ obj_trigger(9980, 250, 80, 20, EV_FLOWEY_END, ACTOR_TRIGGER);
+ }
+ obj_trigger(9990, 70, 60, 14, EV_RUINS_DOOR, ACTOR_TRIGGER2);
+ break;
+ }
+}
+
+/* ---------------------------------------------------------------- the events */
+static int ev_home_tour(Ev *c) {
+ EV_BEGIN;
+ WAIT_UNTIL(!world_fading());
+ SAY(FACE_TORIEL, "`0* Smell that?|`2* Surprise! A butterscotch-\n cinnamon pie, for you!|`0* No snail pie tonight.|`2* And one more surprise...");
+ WALK(obj_get(ACTOR_TORIEL), 8996, 2840, 3);
+ WALK(obj_get(ACTOR_TORIEL), 9200, 2840, 3);
+ obj_get(ACTOR_TORIEL)->kind = OBJ_NONE;
+ PL_WALK(pl.x + 40, pl.y, 3);
+ FADE_TO(ROOM_TORHOUSE3, 9196, 2836, DIR_RIGHT);
+ PL_WALK(9330, 2836, 3);
+ pl.dir = DIR_UP;
+ SAY(FACE_TORIEL, "`2* A room of your own.|* I hope you like it!");
+ WAIT(20);
+ SAY(FACE_TORIEL, "`7* ...!|`3* Is something burning...?|`0* Um, make yourself at home!");
+ WALK(obj_get(ACTOR_TORIEL), 9370, 2840, 4);
+ WALK(obj_get(ACTOR_TORIEL), 9180, 2840, 5);
+ obj_get(ACTOR_TORIEL)->kind = OBJ_NONE;
+ set_flag(F_HOME_TOUR, 1);
+ EV_END;
+}
+
+static const char *const chair_opts[2] = {"When can I go home?", "How to exit the RUINS"};
+
+static int ev_toriel_chair(Ev *c) {
+ EV_BEGIN;
+ if (!flag(F_HOME_TOUR) || flag(F_TORIEL_BASEMENT)) {
+ NARRATE("* (Seems like the right size\n for Toriel.)");
+ return 1;
+ }
+ SAY(FACE_TORIEL, "`0* What is it?");
+ CHOOSE(chair_opts, 2);
+ if (CHOSEN() < 0) return 1;
+ if (CHOSEN() == 0) {
+ SAY(FACE_TORIEL, "`3* What?|`1* This is your home now.|`0* Would you rather hear about\n my book on snails?@Yes@No");
+ if (CHOICE() == 0) SAY(FACE_TORIEL, "`2* A snail fact!|`0* Snails have a tongue like a\n tiny saw.");
+ else SAY(FACE_TORIEL, "`0* Oh...^2 Alright.");
+ return 1;
+ }
+ SAY(FACE_TORIEL, "`1* ...|* Um...^2 I have to do\n something.|* Stay here.");
+ set_flag(F_TORIEL_BASEMENT, 1);
+ if (c->self) c->self->spr[0] = SPR_O_CHAIRIELGETUP;
+ for (c->v[0] = 0; c->v[0] < 6; c->v[0]++) {
+ obj_get(ACTOR_CHAIR)->frame = (uint8_t)c->v[0];
+ WAIT(4);
+ }
+ obj_get(ACTOR_CHAIR)->frame = 0;
+ if (c->self) c->self->spr[0] = SPR_O_CHAIREMPTY;
+ {
+ Obj *t = obj_toriel(8660, 2760, DIR_RIGHT);
+ if (t) t->flags &= (uint16_t)~OF_SOLID;
+ }
+ WALK(obj_get(ACTOR_TORIEL), 8800, 2780, 4);
+ WALK(obj_get(ACTOR_TORIEL), 8860, 2780, 4);
+ obj_get(ACTOR_TORIEL)->kind = OBJ_NONE;
+ EV_END;
+}
+
+static int ev_stairs(Ev *c) {
+ EV_BEGIN;
+ if (flag(F_TORIEL_BASEMENT)) {
+ if (c->self) c->self->kind = OBJ_NONE;
+ return 1;
+ }
+ if (!flag(F_HOME_TOUR)) return 1;
+ SAY(FACE_TORIEL, "`1* My child...|`0* There is nothing for you\n down there.|* Let us go back upstairs.");
+ PL_WALK(pl.x + 14, pl.y + 14, 2);
+ EV_END;
+}
+
+static int ev_bed(Ev *c) {
+ EV_BEGIN;
+ NARRATE("* (You lie down on the bed.)");
+ if (!flag(F_BED_SLEPT) && !flag(F_TORIEL_BASEMENT)) set_flag(F_BED_SLEPT, 1);
+ sv.hp = sv.maxhp;
+ world_fade_hold(g_room, 9376, 2590, DIR_DOWN, 90);
+ WAIT_UNTIL(!world_fading());
+ EV_END;
+}
+
+static int ev_basement_talk(Ev *c) {
+ EV_BEGIN;
+ if (c->self) c->self->kind = OBJ_NONE;
+ obj_get(ACTOR_TORIEL)->dir = DIR_LEFT;
+ switch (flag(F_BASEMENT_TALK)) {
+ case 0:
+ SAY(FACE_TORIEL, "`0* You want to go \"home\".|* The exit from the RUINS is\n just ahead.|`6* I will seal it, so no one\n can leave.|`0* Go upstairs, please.");
+ break;
+ case 1:
+ SAY(FACE_TORIEL, "`6* Every human who falls here\n leaves... and is never\n seen again.|* Out there, #RASGORE#X will\n take your life.|* I am keeping you safe.|* ...Go to your room.");
+ break;
+ default:
+ obj_get(ACTOR_TORIEL)->dir = DIR_DOWN;
+ SAY(FACE_TORIEL, "`6* Do not try to stop me.|* This is your final warning.");
+ break;
+ }
+ set_flag(F_BASEMENT_TALK, flag(F_BASEMENT_TALK) + 1);
+ if (g_room == ROOM_BASEMENT1 && flag(F_BASEMENT_TALK) == 1) {
+ /* on ahead, to wait further down */
+ obj_get(ACTOR_TORIEL)->dir = DIR_RIGHT;
+ WALK(obj_get(ACTOR_TORIEL), 9560, 2300, 4);
+ obj_get(ACTOR_TORIEL)->dir = DIR_RIGHT;
+ obj_trigger(9490, 2300, 16, 80, EV_BASEMENT_TALK, ACTOR_TRIGGER);
+ return 1;
+ }
+ if (g_room == ROOM_BASEMENT1) {
+ WALK(obj_get(ACTOR_TORIEL), 10120, 2300, 5);
+ } else {
+ WALK(obj_get(ACTOR_TORIEL), 10004, 1984, 4);
+ obj_get(ACTOR_TORIEL)->dir = DIR_DOWN;
+ obj_trigger(9960, 2060, 140, 14, EV_TORIEL_DOOR, ACTOR_TRIGGER2);
+ return 1;
+ }
+ obj_get(ACTOR_TORIEL)->kind = OBJ_NONE;
+ EV_END;
+}
+
+static int ev_toriel_door(Ev *c) {
+ EV_BEGIN;
+ if (c->self) c->self->kind = OBJ_NONE;
+ SAY(FACE_TORIEL, "`6* So you insist on leaving.|* Like all the others.|* Then show me you are\n strong enough to survive.");
+ BATTLE(FORM_TORIEL);
+ if (flag(F_TORIEL_FIGHT) != 1) {
+ obj_get(ACTOR_TORIEL)->kind = OBJ_NONE;
+ if (!flag(F_TORIEL_FIGHT)) set_flag(F_TORIEL_FIGHT, battle_result() == BR_FLED ? 1 : 2);
+ return 1;
+ }
+ SAY(FACE_TORIEL, "`1* ...|* I see.|* You would not be happy\n here.|* This small place is no\n home for you to grow up in.|`0* My worries can wait.\n* For you, my child.");
+ /* she holds Frisk */
+ PL_WALK(10010, 2040, 2);
+ pl.hidden = true;
+ obj_get(ACTOR_TORIEL)->spr[0] = SPR_O_TORIEL_HUG, obj_get(ACTOR_TORIEL)->dir = DIR_DOWN;
+ obj_get(ACTOR_TORIEL)->y = 2000;
+ WAIT(70);
+ pl.hidden = false;
+ obj_get(ACTOR_TORIEL)->spr[0] = SPR_TORIEL_D, obj_get(ACTOR_TORIEL)->y = 1984;
+ SAY(FACE_TORIEL, "`1* If you must leave...|* I will not stop you.|* But please do not come\n back.|`0* I hope you understand.");
+ WAIT(20);
+ SAY(FACE_TORIEL, "`0* Goodbye, my child.");
+ obj_get(ACTOR_TORIEL)->flags &= (uint16_t)~OF_SOLID;
+ WALK(obj_get(ACTOR_TORIEL), 10040, 2050, 2);
+ WALK(obj_get(ACTOR_TORIEL), 10040, 2240, 2);
+ obj_get(ACTOR_TORIEL)->kind = OBJ_NONE;
+ EV_END;
+}
+
+static const char flowey_spared[] =
+ "`1* Well, well. Aren't you\n clever.|"
+ "`0* Down here it's kill or be\n killed... and you spared\n her anyway.|`2* Proud of yourself? Hee hee.|"
+ "`1* But what happens when you\n meet someone who won't stop?|"
+ "`3* Over and over, until you're\n sick of it.|* Then what? Will you fight\n back... or let ME take over?|";
+static const char flowey_killed[] =
+ "`1* Well, well. Aren't you\n clever.|"
+ "`0* Kill or be killed, right?\n`2* And you chose. Hee hee.|"
+ "`3* You learn fast.|";
+static const char flowey_others[] =
+ "`1* Well, well. Aren't you\n clever.|"
+ "`0* You spared her...|"
+ "`2* ...after everyone you hurt\n on the way here. Hee hee.|";
+static const char flowey_last[] =
+ "`8* This world's future is\n mine to decide.|`1* Go on, then.|"
+ "* I won't get in your way.|`2* Have fun...|`8* Ha ha ha...";
+
+static int ev_flowey_end(Ev *c) {
+ EV_BEGIN;
+ if (c->self) c->self->kind = OBJ_NONE;
+ {
+ Obj *f = obj_get(ACTOR_FLOWEY);
+ f->flags &= (uint16_t)~OF_HIDDEN, f->spr[0] = SPR_O_FLOWEYGROW, f->frame = 0;
+ }
+ for (c->v[0] = 0; c->v[0] < 9; c->v[0]++) {
+ obj_get(ACTOR_FLOWEY)->frame = (uint8_t)c->v[0];
+ WAIT(3);
+ }
+ obj_get(ACTOR_FLOWEY)->spr[0] = SPR_FLOWEY, obj_get(ACTOR_FLOWEY)->frame = 0;
+ if (flag(F_TORIEL_FIGHT) == 2) SAY(FACE_FLOWEY, flowey_killed);
+ else if (sv.kills) SAY(FACE_FLOWEY, flowey_others);
+ else SAY(FACE_FLOWEY, flowey_spared);
+ SAY(FACE_FLOWEY, flowey_last);
+ obj_get(ACTOR_FLOWEY)->spr[0] = SPR_O_FLOWEYSINK;
+ for (c->v[0] = 0; c->v[0] < 6; c->v[0]++) {
+ obj_get(ACTOR_FLOWEY)->frame = (uint8_t)c->v[0];
+ WAIT(3);
+ }
+ obj_get(ACTOR_FLOWEY)->kind = OBJ_NONE;
+ set_flag(F_FLOWEY_END, 1);
+ EV_END;
+}
+
+/* the door out of the Ruins: the rest of the underground is still to come */
+static int ev_ruins_door(Ev *c) {
+ EV_BEGIN;
+ /* out of the Ruins: Snowdin's snow */
+ set_flag(F_RUINS_EXIT, 1);
+ world_fade_hold(ROOM_SNOWDIN1, 34, 392, DIR_DOWN, 20);
+ WAIT_UNTIL(!world_fading());
+ EV_END;
+}
+
+int ruins3_event(Ev *c) {
+ switch (c->id) {
+ case EV_HOME_TOUR: return ev_home_tour(c);
+ case EV_TORIEL_CHAIR: return ev_toriel_chair(c);
+ case EV_STAIRS: return ev_stairs(c);
+ case EV_BED: return ev_bed(c);
+ case EV_BASEMENT_TALK: return ev_basement_talk(c);
+ case EV_TORIEL_DOOR: return ev_toriel_door(c);
+ case EV_FLOWEY_END: return ev_flowey_end(c);
+ case EV_RUINS_DOOR: return ev_ruins_door(c);
+ }
+ return -1;
+}
diff --git a/games/undertale/src/save.c b/games/undertale/src/save.c
new file mode 100644
index 00000000..d6396bd4
--- /dev/null
+++ b/games/undertale/src/save.c
@@ -0,0 +1,171 @@
+/* Saves (save.h): the game's SAVE (undertale.sav), the moment kept when Home quits (utresume.sav, read once
+ * on the next start), and a copy of both in a Python script: installing apps from the NumWorks website keeps
+ * only Python scripts, so the saves come back from it when the files themselves are gone. Each is a
+ * comment line "#>name:base64" (the way NumPlay and its big games keep theirs). */
+#include
+#include "game.h"
+#include "menu.h"
+#include "plat.h"
+#include "save.h"
+#include "text.h"
+#include "world.h"
+
+#define SAVE_NAME "undertale.sav"
+#define RESUME_NAME "utresume.sav"
+#define COPY_NAME "undertale_saves.py"
+#define MAGIC 0x31305455u /* "UT01" */
+
+typedef struct {
+ uint32_t magic;
+ uint32_t size;
+ Save s;
+} SaveFile;
+
+static const char *const kept[] = {SAVE_NAME, RESUME_NAME};
+#define NKEPT (int)(sizeof kept / sizeof kept[0])
+static const char copy_head[] =
+ "# Undertale keeps a copy of your SAVE here, so that\n"
+ "# reinstalling it doesn't erase it. If you delete this file,\n"
+ "# the game writes it again: your SAVE stays either way.\n";
+static const char b64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+static void copy_write(void) {
+ uint32_t total = 1 + (sizeof copy_head - 1) + 1, len;
+ int n = 0;
+ for (int k = 0; k < NKEPT; k++)
+ if (plat_load(kept[k], &len) && len) total += 2 + (uint32_t)strlen(kept[k]) + 1 + (len + 2) / 3 * 4 + 1, n++;
+ plat_remove(COPY_NAME);
+ if (!n) return;
+ uint8_t *o = plat_reserve(COPY_NAME, total), *p = o;
+ if (!o) return;
+ *p++ = 0; /* (the script's status byte: not imported) */
+ memcpy(p, copy_head, sizeof copy_head - 1), p += sizeof copy_head - 1;
+ for (int k = 0; k < NKEPT; k++) {
+ const uint8_t *d = plat_load(kept[k], &len);
+ if (!d || !len) continue;
+ uint32_t nl = (uint32_t)strlen(kept[k]);
+ *p++ = '#', *p++ = '>';
+ memcpy(p, kept[k], nl), p += nl;
+ *p++ = ':';
+ for (uint32_t i = 0; i < len; i += 3, p += 4) {
+ /* (written out: GCC 15.2 for the calculator got a loop over the four wrong, see NumPlay's
+ * launcher/src/storage.c) */
+ uint32_t left = len - i, v = (uint32_t)d[i] << 16 | (left > 1 ? d[i + 1] << 8 : 0) | (left > 2 ? d[i + 2] : 0);
+ p[0] = (uint8_t)b64[v >> 18 & 63];
+ p[1] = (uint8_t)b64[v >> 12 & 63];
+ p[2] = left > 1 ? (uint8_t)b64[v >> 6 & 63] : '=';
+ p[3] = left > 2 ? (uint8_t)b64[v & 63] : '=';
+ }
+ *p++ = '\n';
+ }
+ *p++ = 0;
+ plat_reserved(COPY_NAME, (uint32_t)(p - o));
+}
+
+static int b64_value(uint8_t c) {
+ for (int i = 0; i < 64; i++)
+ if ((uint8_t)b64[i] == c) return i;
+ return -1;
+}
+
+static const uint8_t *copy_line(int k, uint32_t *len) {
+ uint32_t n, nl = (uint32_t)strlen(kept[k]);
+ const uint8_t *c = plat_load(COPY_NAME, &n);
+ if (!c) return NULL;
+ for (uint32_t i = 0; i + nl + 3 <= n; i++)
+ if (c[i] == '#' && c[i + 1] == '>' && !memcmp(c + i + 2, kept[k], nl) && c[i + 2 + nl] == ':') {
+ const uint8_t *l = c + i + 3 + nl;
+ uint32_t m = 0;
+ while (l + m < c + n && l[m] != '\n' && l[m]) m++;
+ *len = m;
+ return l;
+ }
+ return NULL;
+}
+
+void save_restore_copy(void) {
+ for (int k = 0; k < NKEPT; k++) {
+ uint32_t have, len;
+ if (plat_load(kept[k], &have)) continue;
+ const uint8_t *l = copy_line(k, &len);
+ if (!l || !len || len % 4) continue;
+ uint32_t size = len / 4 * 3 - (l[len - 1] == '=') - (l[len - 2] == '=');
+ uint8_t *out = plat_reserve(kept[k], size);
+ if (!out) continue;
+ l = copy_line(k, &len);
+ bool ok = l != NULL && len / 4 * 3 - (l[len - 1] == '=') - (l[len - 2] == '=') == size;
+ uint32_t o = 0;
+ for (uint32_t i = 0; ok && i < len; i += 4) {
+ int v0 = l[i] == '=' ? 0 : b64_value(l[i]), v1 = l[i + 1] == '=' ? 0 : b64_value(l[i + 1]);
+ int v2 = l[i + 2] == '=' ? 0 : b64_value(l[i + 2]), v3 = l[i + 3] == '=' ? 0 : b64_value(l[i + 3]);
+ if (v0 < 0 || v1 < 0 || v2 < 0 || v3 < 0) ok = false;
+ uint32_t wd = (uint32_t)(v0 << 18 | v1 << 12 | v2 << 6 | v3);
+ if (o < size) out[o++] = (uint8_t)(wd >> 16);
+ if (o < size) out[o++] = (uint8_t)(wd >> 8);
+ if (o < size) out[o++] = (uint8_t)wd;
+ }
+ if (ok && o == size) plat_reserved(kept[k], size);
+ else plat_remove(kept[k]);
+ }
+}
+
+static bool write_file(const char *name, const Save *s) {
+ SaveFile f;
+ memset(&f, 0, sizeof f);
+ f.magic = MAGIC, f.size = sizeof(Save), f.s = *s;
+ bool ok = plat_save(name, &f, sizeof f);
+ copy_write();
+ return ok;
+}
+
+static bool read_file(const char *name, Save *s) {
+ uint32_t len;
+ const uint8_t *d = plat_load(name, &len);
+ if (!d || len != sizeof(SaveFile)) return false;
+ SaveFile f;
+ memcpy(&f, d, sizeof f);
+ if (f.magic != MAGIC || f.size != sizeof(Save)) return false;
+ *s = f.s;
+ return true;
+}
+
+bool save_write(void) { return write_file(SAVE_NAME, &sv); }
+bool save_write_auto(void) { return write_file(SAVE_NAME, &sv); }
+bool save_exists(void) {
+ Save s;
+ return read_file(SAVE_NAME, &s);
+}
+bool save_peek(Save *s) { return read_file(SAVE_NAME, s); }
+
+bool save_load(void) {
+ Save s;
+ if (!read_file(SAVE_NAME, &s)) return false;
+ sv = s;
+ memcpy(g_name, sv.name, sizeof g_name);
+ sv.hp = sv.maxhp;
+ g_mode = MODE_WORLD;
+ world_enter(sv.room, sv.x, sv.y, DIR_DOWN);
+ return true;
+}
+
+/* Home: the moment kept as it is, room and place; read back once */
+void save_resume_write(void) {
+ if (g_mode != MODE_WORLD && g_mode != MODE_MENU && g_mode != MODE_SAVE) {
+ /* (in a battle: kept as it was before it) */
+ }
+ Save s = sv;
+ s.room = (int16_t)g_room, s.x = pl.x, s.y = pl.y;
+ write_file(RESUME_NAME, &s);
+}
+
+bool save_resume_load(void) {
+ Save s;
+ if (!read_file(RESUME_NAME, &s)) return false;
+ plat_remove(RESUME_NAME);
+ copy_write();
+ sv = s;
+ memcpy(g_name, sv.name, sizeof g_name);
+ g_mode = MODE_WORLD;
+ world_enter(sv.room, sv.x, sv.y, DIR_DOWN);
+ return true;
+}
diff --git a/games/undertale/src/save.h b/games/undertale/src/save.h
new file mode 100644
index 00000000..d40fc1cd
--- /dev/null
+++ b/games/undertale/src/save.h
@@ -0,0 +1,14 @@
+/* Undertale: saves (the game's SAVE, the moment kept on Home), and their copy in a Python script. */
+#ifndef UT_SAVE_H
+#define UT_SAVE_H
+#include
+#include "game.h"
+void save_restore_copy(void);
+bool save_write(void);
+bool save_write_auto(void); /* the game's own SAVE at certain moments (sv's room and place set) */
+bool save_exists(void);
+bool save_load(void);
+bool save_peek(Save *s); /* the SAVE's facts, without loading it */
+void save_resume_write(void);
+bool save_resume_load(void);
+#endif
diff --git a/games/undertale/src/shoot.c b/games/undertale/src/shoot.c
new file mode 100644
index 00000000..5ff1e098
--- /dev/null
+++ b/games/undertale/src/shoot.c
@@ -0,0 +1,136 @@
+/* Hotland's shooting puzzles (the Reactivated Door Puzzles), on their own screen (MODE_SHOOT).
+ *
+ * A grid of blocks stands between Frisk's ship, below, and the target, above. Each arrow pushes every dark block one
+ * cell that way, where there's room inside the grid; the light blocks never move. Z shoots straight up: a shot
+ * breaks the first dark block it meets, a light one stops it, and a clear way hits the target. Two shots a try on
+ * the first floor and in the CORE, one on the third: out of them, it starts over. (The rules are worked out from the solutions the Undertale Wiki gives for them; the
+ * grids are the game's, as its screenshots show them.) */
+#include
+#include "game.h"
+#include "gfx.h"
+#include "plat.h"
+#include "res.h"
+#include "shoot.h"
+
+typedef struct {
+ uint8_t w, h, ship, shots;
+ const char *rows[6]; /* '#' a light block, 'o' a dark one, ' ' nothing */
+} Puzzle;
+static const Puzzle puzzles[SHOOT_COUNT] = {
+ [SHOOT_RIGHT] = {3, 3, 1, 2, {"#o#", " ", "#o#"}},
+ [SHOOT_LEFT] = {4, 4, 2, 2, {"# #", "ooo ", " #o#", "#oo "}},
+ [SHOOT_NORTH] = {5, 5, 2, 1, {"## o#", " o # ", " #o o", " o o#", "## ##"}},
+ [SHOOT_SOUTH] = {5, 4, 3, 1, {"###o ", " o#", "###o ", "###o#"}},
+ [SHOOT_SAGE] = {5, 5, 2, 2, {"## # ", " #o# ", " oooo", " #o# ", " # ##"}}, /* (up, up, left, down, left, down, left) */
+};
+#define CELL 24
+
+static int id, shots, result, t, shot_y, fail_t;
+static char grid[6][6];
+static bool won;
+
+static void reset(void) {
+ const Puzzle *p = &puzzles[id];
+ for (int y = 0; y < p->h; y++)
+ for (int x = 0; x < p->w; x++) grid[y][x] = p->rows[y][x];
+ shots = p->shots, shot_y = -1, fail_t = 0, won = false;
+}
+
+void shoot_open(int which) {
+ id = which, result = 0, t = 0;
+ reset();
+ g_mode = MODE_SHOOT;
+}
+int shoot_result(void) { return result; }
+
+static void push(int dx, int dy) {
+ const Puzzle *p = &puzzles[id];
+ /* one cell a press; the dark blocks nearest the way they go move first, so a line of them moves together */
+ for (int y = 0; y < p->h; y++)
+ for (int x = 0; x < p->w; x++) {
+ int sx = dx > 0 ? p->w - 1 - x : x, sy = dy > 0 ? p->h - 1 - y : y;
+ if (grid[sy][sx] != 'o') continue;
+ int nx = sx + dx, ny = sy + dy;
+ if (nx < 0 || ny < 0 || nx >= p->w || ny >= p->h || grid[ny][nx] != ' ') continue;
+ grid[ny][nx] = 'm', grid[sy][sx] = ' '; /* (marked: moved once) */
+ }
+ for (int y = 0; y < p->h; y++)
+ for (int x = 0; x < p->w; x++)
+ if (grid[y][x] == 'm') grid[y][x] = 'o';
+}
+
+void shoot_update(uint32_t keys, uint32_t pressed) {
+ (void)keys;
+ const Puzzle *p = &puzzles[id];
+ t++;
+ if (won) {
+ if (++fail_t > 40) result = 1, g_mode = MODE_WORLD;
+ return;
+ }
+ if (fail_t) {
+ if (++fail_t > 45) reset();
+ return;
+ }
+ if (shot_y >= 0) {
+ /* the shot flies up its column, a cell every few frames */
+ if (t % 3) return;
+ shot_y--;
+ if (shot_y < 0) {
+ won = true, fail_t = 1;
+ return;
+ }
+ char *c = &grid[shot_y][p->ship];
+ if (*c == ' ') return;
+ if (*c == 'o') *c = ' ';
+ shot_y = -1;
+ if (!shots) fail_t = 1;
+ return;
+ }
+ if (pressed & K_X) {
+ result = 0, g_mode = MODE_WORLD;
+ return;
+ }
+ if (pressed & K_UP) push(0, -1);
+ else if (pressed & K_DOWN) push(0, 1);
+ else if (pressed & K_LEFT) push(-1, 0);
+ else if (pressed & K_RIGHT) push(1, 0);
+ else if ((pressed & K_Z) && shots) shots--, shot_y = p->h, t = 0;
+}
+
+void shoot_draw(void) {
+ const Puzzle *p = &puzzles[id];
+ gfx_rect(0, 0, SCREEN_W, SCREEN_H, C_BLACK, 255);
+ int gw = p->w * CELL, gh = p->h * CELL;
+ int gx = (SCREEN_W - gw) / 2, gy = 104 - gh / 2;
+ uint16_t grey = rgb565(0x909090), dim = rgb565(0x606060);
+ /* the frame, the target, the ship */
+ gfx_rect(gx - 40, 16, gw + 80, 1, grey, 255);
+ gfx_rect(gx - 40, 190, gw + 80, 1, grey, 255);
+ gfx_rect(gx - 40, 16, 1, 175, grey, 255);
+ gfx_rect(gx + gw + 39, 16, 1, 175, grey, 255);
+ int cx = gx + p->ship * CELL + CELL / 2;
+ if (!won || (t / 3) % 2)
+ for (int k = 0; k < 8; k++) gfx_rect(cx - 8 + k, 22 + k, 16 - 2 * k, 1, grey, 255);
+ for (int k = 0; k < 9; k++) gfx_rect(cx - k, 172 + k, 2 * k + 1, 1, rgb565(0xA0A030), 255);
+ /* the grid */
+ gfx_rect(gx - 2, gy - 2, gw + 4, 1, dim, 255);
+ gfx_rect(gx - 2, gy + gh + 1, gw + 4, 1, dim, 255);
+ gfx_rect(gx - 2, gy - 2, 1, gh + 4, dim, 255);
+ gfx_rect(gx + gw + 1, gy - 2, 1, gh + 4, dim, 255);
+ for (int y = 0; y < p->h; y++)
+ for (int x = 0; x < p->w; x++) {
+ int bx = gx + x * CELL + 2, by = gy + y * CELL + 2;
+ if (grid[y][x] == '#') {
+ gfx_rect(bx, by, CELL - 4, CELL - 4, grey, 255);
+ } else if (grid[y][x] == 'o') {
+ gfx_rect(bx, by, CELL - 4, CELL - 4, grey, 255);
+ gfx_rect(bx + 2, by + 2, CELL - 8, CELL - 8, C_BLACK, 255);
+ }
+ }
+ if (shot_y >= 0) gfx_rect(cx - 1, gy + shot_y * CELL + CELL / 2 - 4, 3, 8, C_YELLOW, 255);
+ /* the shots left, and how it plays */
+ for (int k = 0; k < shots; k++) gfx_rect(gx + gw + 50 + k * 8, 176, 4, 8, C_YELLOW, 255);
+ if (fail_t && !won) gfx_text(FONT_MAIN, 120, 200, "RESTART...", C_WHITE, 255);
+ else if (won) gfx_text(FONT_MAIN, 128, 200, "SUCCESS!", C_YELLOW, 255);
+ else gfx_text(FONT_SMALL, 40, 204, "ARROWS: MOVE BLOCKS Z: SHOOT X: LEAVE", dim, 255);
+}
diff --git a/games/undertale/src/shoot.h b/games/undertale/src/shoot.h
new file mode 100644
index 00000000..fcfa2786
--- /dev/null
+++ b/games/undertale/src/shoot.h
@@ -0,0 +1,10 @@
+/* Undertale: Hotland's shooting puzzles (shoot.c), on their own screen. */
+#ifndef UT_SHOOT_H
+#define UT_SHOOT_H
+#include
+enum { SHOOT_RIGHT, SHOOT_LEFT, SHOOT_NORTH, SHOOT_SOUTH, SHOOT_SAGE, SHOOT_COUNT }; /* (the first floor's, the third's, the CORE's) */
+void shoot_open(int id); /* MODE_SHOOT until solved or left */
+int shoot_result(void); /* 1 solved, 0 left */
+void shoot_update(uint32_t keys, uint32_t pressed);
+void shoot_draw(void);
+#endif
diff --git a/games/undertale/src/shop.c b/games/undertale/src/shop.c
new file mode 100644
index 00000000..8c71097c
--- /dev/null
+++ b/games/undertale/src/shop.c
@@ -0,0 +1,324 @@
+/* Shops, as the game shows them: the keeper's picture over a text box and Buy, Sell, Talk, Exit; and the
+ * Dimensional Box (an inventory you leave items in). Both are their own mode (MODE_SHOP). */
+#include
+#include
+#include "event.h"
+#include "game.h"
+#include "gfx.h"
+#include "items.h"
+#include "res.h"
+#include "shop.h"
+#include "text.h"
+
+typedef struct {
+ uint8_t item;
+ uint16_t price;
+} Ware;
+typedef struct {
+ uint16_t pic;
+ uint16_t keeper; /* drawn over the picture (its feet at keeper_x, 120), 0 if the picture has them */
+ int16_t keeper_x;
+ const char *hello, *again, *buying, *bought, *poor, *full, *nobuy, *sell;
+ Ware wares[4];
+ int nwares;
+ const char *topics[4];
+ const char *answers[4];
+ int ntopics;
+ bool buys; /* Sell: it buys what you carry (the Tem Shop) */
+} Shop;
+
+static const Shop shops[SHOP_COUNT] = {
+ [SHOP_SNOWDIN] = {
+ .pic = SPR_SHOP_QC, .hello = "* Hello, traveller.\n* How can I help you?", .again = "* Take your time.",
+ .buying = "What would\nyou like\nto buy?", .bought = "Thanks for\nyour purchase.", .poor = "That's not\nenough money.",
+ .full = "You're\ncarrying\ntoo much.", .nobuy = "Just\nlooking?",
+ .sell = "* Huh? Sell somethin'?\n* Does this look like a pawn shop?",
+ .wares = {{ITEM_TOUGH_GLOVE, 50}, {ITEM_MANLY_BANDANNA, 50}, {ITEM_BISICLE, 15}, {ITEM_CINNAMON_BUNNY, 25}}, .nwares = 4,
+ .topics = {"Say hello", "What to do here", "Town history", "Your life"},
+ .answers = {"* Hello! Welcome to Snowdin!\n* I can't remember the last time I saw a new face around here.",
+ "* If you're tired, the inn's right next door. My sister runs it.|* And there's Grillby's, if you're hungry.",
+ "* Long ago, monsters lived in the RUINS back there.|* When we came here, the cold made it easy to get used to the snow.",
+ "* Life is the same as usual.\n* A little cold, though..."},
+ .ntopics = 4},
+ [SHOP_GERSON] = {
+ .pic = SPR_SHOP_GERSON, .hello = "* Hello there, little one!\n* Pay no mind to the junk.", .again = "* Take a look around!",
+ .buying = "Take your\ntime.", .bought = "Wa ha ha!\nThank you!", .poor = "Not enough\nmoney, eh?",
+ .full = "Your pockets\nare full,\nkid.", .nobuy = "Hmm?",
+ .sell = "* Sell? I'm not that kind of shop, kid.\n* Try the Temmies, if you can find them.",
+ .wares = {{ITEM_CRAB_APPLE, 25}, {ITEM_SEA_TEA, 18}, {ITEM_CLOUDY_GLASSES, 35}, {ITEM_TORN_NOTEBOOK, 55}}, .nwares = 4,
+ .topics = {"About you", "About Undyne", "About the king", "About the junk"},
+ .answers = {"* Me? Just an old turtle who's seen a lot.\n* They used to call me a hero, way back when.",
+ "* Undyne? That girl's the head of the Royal Guard.\n* She'd fight anyone to protect us. Wa ha ha!",
+ "* The king's a big softie.\n* Lost his family a long while back. Still keeps smiling.",
+ "* All of this washed up in the dump.\n* One monster's trash is another's treasure!"},
+ .ntopics = 4},
+ [SHOP_TEM] = {
+ .pic = SPR_SHOP_TEM, .hello = "* hOI!\n* welcom to... da TEM SHOP!!!", .again = "* hOI!", .buying = "tem has\nstuff!!",
+ .bought = "thanks!!!", .poor = "no muns...", .full = "u hav too\nmany stuffs", .nobuy = "oh...",
+ .sell = "", .buys = true,
+ .wares = {{ITEM_TEMMIE_FLAKES, 1}}, .nwares = 1,
+ .topics = {"Say hello", "About Temmies", "Tem history", "About shop"},
+ .answers = {"* hOI!!! i'm temmie!!", "* temmies r the best!!!\n* we love humans... but some temmies r allergic...",
+ "* temmie village is very old...\n* tem forget the rest.", "* tem wants to go to colleg someday!!"},
+ .ntopics = 4},
+ [SHOP_BURGER] = {
+ .pic = SPR_SHOP_BURGER, .keeper = SPR_BURGERPANTS, .keeper_x = 246,
+ .hello = "* Hi, welcome to MTT-Brand\n Burger Emporium...|* ...where the food is, uh...\n food.", .again = "* Anything else?",
+ .buying = "Take your\ntime. Please.", .bought = "Thanks...\nI guess.", .poor = "You don't\nhave the\nmoney.",
+ .full = "Your hands\nare full.", .nobuy = "Fine by me.",
+ .sell = "* Sell stuff? Here?\n* I can barely sell the burgers.",
+ .wares = {{ITEM_STARFAIT, 60}, {ITEM_GLAMBURGER, 120}, {ITEM_LEGENDARY_HERO, 300}, {ITEM_STEAK, 500}}, .nwares = 4,
+ .topics = {"Say hello", "About Mettaton", "About the food", "Your life"},
+ .answers = {"* Hi. I'm Burgerpants.\n* Not my real name. Long story.",
+ "* Mettaton's my boss.\n* He's... a lot.|* Everything here has to have\n his face on it.",
+ "* The Glamburger has glitter\n in it.|* Mettaton says that's how\n humans eat. Sure.",
+ "* I came here to be a star.\n* Now I make burgers.|* ...at least I've got my youth.\n* Right?"},
+ .ntopics = 4},
+ [SHOP_BRATTY] = {
+ .pic = SPR_SHOP_BRATTY, .keeper = SPR_BRATTY_CATTY, .keeper_x = 160,
+ .hello = "* Hey! Check it out!\n* Yeah! Check it out!", .again = "* Like, anything else?",
+ .buying = "Buy ALL\nour stuff!", .bought = "Thanks!\nLike, so\nmuch!", .poor = "You need\nWAY more\nmoney.",
+ .full = "You have\nTOO many\nitems!", .nobuy = "Whatever!",
+ .sell = "* We don't, like, need\n anything. Really.",
+ .wares = {{ITEM_JUNK_FOOD, 25}, {ITEM_EMPTY_GUN, 350}, {ITEM_COWBOY_HAT, 350}, {ITEM_MYSTERY_KEY, 600}}, .nwares = 4,
+ .topics = {"About you two", "Burgerpants", "Your stuff", "Mettaton"},
+ .answers = {"* I'm Bratty. She's Catty.\n* We're, like, best friends.",
+ "* The guy at the burger place?\n* Ugh, a creep.|* ...a kind of cute creep,\n though. -Catty",
+ "* We find it all in the dump.\n* It's GOOD garbage.|* And, like, TOTALLY expensive.",
+ "* Mettaton is SO cute.\n* We're, like, his biggest fans."},
+ .ntopics = 4},
+};
+
+static int shop_id, st, sel, sub;
+static Writer w;
+static char buf[300];
+static const char *right_text;
+static bool is_box;
+enum { S_MENU, S_BUY, S_CONFIRM, S_TALK, S_SAY, S_SELL };
+
+static void left_say(const char *s) {
+ snprintf(buf, sizeof buf, "%s", s);
+ text_wrap_width(190);
+ writer_start(&w, buf, 12, 133, FONT_MAIN);
+ w.line_h = 16;
+}
+
+void shop_open(int id) {
+ shop_id = id, is_box = false;
+ st = S_MENU, sel = 0, sub = 0, right_text = NULL;
+ left_say(shops[id].hello);
+ g_mode = MODE_SHOP;
+}
+
+/* the Bisicle costs more after each boss fought (the game's 15G and up) */
+static int price_of(const Ware *wa) {
+ if (wa->item == ITEM_BISICLE) return min_i(70, wa->price + 5 * flag(F_SNOWDIN_BOSSES));
+ return wa->price;
+}
+
+/* ---------------------------------------------------------------- the Dimensional Box */
+#define BOX_SLOTS 10
+void box_open(void) {
+ is_box = true, sel = 0, sub = 0;
+ g_mode = MODE_SHOP;
+}
+static int box_item(int i) { return flag(F_BOX0 + i); }
+
+static void box_update(uint32_t pressed) {
+ int n = sub ? BOX_SLOTS : NUM_ITEMS;
+ if (pressed & K_UP) sel = (sel + n - 1) % n;
+ if (pressed & K_DOWN) sel = (sel + 1) % n;
+ if (pressed & (K_LEFT | K_RIGHT)) sub = !sub, sel = min_i(sel, (sub ? BOX_SLOTS : NUM_ITEMS) - 1);
+ if (pressed & K_Z) {
+ if (!sub && sv.items[sel]) {
+ for (int i = 0; i < BOX_SLOTS; i++)
+ if (!box_item(i)) {
+ set_flag(F_BOX0 + i, sv.items[sel]);
+ item_remove_at(sel);
+ break;
+ }
+ } else if (sub && box_item(sel) && item_add(box_item(sel))) {
+ /* the box keeps its order: the ones after move up */
+ for (int i = sel; i < BOX_SLOTS - 1; i++) set_flag(F_BOX0 + i, box_item(i + 1));
+ set_flag(F_BOX0 + BOX_SLOTS - 1, 0);
+ }
+ }
+ if (pressed & K_X) g_mode = MODE_WORLD;
+}
+
+static void panel(int x, int y, int w_, int h) {
+ gfx_rect(x, y, w_, h, C_BLACK, 255);
+ gfx_frame_rect(x, y, w_, h, 3, C_WHITE);
+}
+
+static void box_draw(void) {
+ gfx_rect(0, 0, SCREEN_W, SCREEN_H, C_BLACK, 255);
+ panel(8, 8, 304, 224);
+ gfx_text(FONT_MENU, 50, 18, "INVENTORY", C_WHITE, 255);
+ gfx_text(FONT_MENU, 210, 18, "BOX", C_WHITE, 255);
+ gfx_line(160, 20, 160, 210, C_WHITE);
+ for (int i = 0; i < NUM_ITEMS; i++) {
+ const char *nm = sv.items[i] ? item_info(sv.items[i])->name : "";
+ if (sv.items[i]) gfx_text(FONT_MENU, 36, 40 + i * 20, nm, C_WHITE, 255);
+ else gfx_rect(36, 48 + i * 20, 100, 1, rgb565(0xC00000), 255);
+ }
+ for (int i = 0; i < BOX_SLOTS; i++) {
+ if (box_item(i)) gfx_text(FONT_MENU, 190, 36 + i * 17, item_info(box_item(i))->name, C_WHITE, 255);
+ else gfx_rect(190, 44 + i * 17, 100, 1, rgb565(0xC00000), 255);
+ }
+ gfx_sprite(SPR_HEART, 0, sub ? 180 : 26, sub ? 42 + sel * 17 : 46 + sel * 20, 0, 255);
+ gfx_text(FONT_MENU, 120, 214, "Press [X] to Finish", C_WHITE, 255);
+}
+
+/* ---------------------------------------------------------------- the shop */
+void shop_update(uint32_t keys, uint32_t pressed) {
+ (void)keys;
+ if (is_box) {
+ box_update(pressed);
+ return;
+ }
+ const Shop *s = &shops[shop_id];
+ bool typing = writer_update(&w, st == S_SAY ? pressed : 0);
+ switch (st) {
+ case S_MENU:
+ if (pressed & K_UP) sel = (sel + 3) % 4;
+ if (pressed & K_DOWN) sel = (sel + 1) % 4;
+ if (pressed & K_Z) {
+ if (sel == 0) st = S_BUY, sub = 0, right_text = s->buying;
+ else if (sel == 1 && s->buys) st = S_SELL, sub = 0, right_text = "what u\nsell??";
+ else if (sel == 1) st = S_SAY, left_say(s->sell);
+ else if (sel == 2) st = S_TALK, sub = 0;
+ else g_mode = MODE_WORLD;
+ }
+ break;
+ case S_BUY:
+ if (pressed & K_UP) sub = (sub + s->nwares) % (s->nwares + 1);
+ if (pressed & K_DOWN) sub = (sub + 1) % (s->nwares + 1);
+ if (pressed & K_X || ((pressed & K_Z) && sub == s->nwares)) {
+ st = S_MENU, left_say(s->again);
+ break;
+ }
+ if (pressed & K_Z) st = S_CONFIRM, sel = 0;
+ break;
+ case S_CONFIRM:
+ if (pressed & (K_UP | K_DOWN)) sel = !sel;
+ if (pressed & K_X) st = S_BUY, right_text = s->nobuy;
+ else if (pressed & K_Z) {
+ const Ware *wa = &s->wares[sub];
+ if (sel == 1) right_text = s->nobuy;
+ else if (sv.gold < price_of(wa)) right_text = s->poor;
+ else if (!item_add(wa->item)) right_text = s->full;
+ else sv.gold = (uint16_t)(sv.gold - price_of(wa)), right_text = s->bought;
+ st = S_BUY;
+ }
+ break;
+ case S_TALK:
+ if (pressed & K_UP) sub = (sub + s->ntopics) % (s->ntopics + 1);
+ if (pressed & K_DOWN) sub = (sub + 1) % (s->ntopics + 1);
+ if (pressed & K_X || ((pressed & K_Z) && sub == s->ntopics)) {
+ st = S_MENU, left_say(s->again);
+ break;
+ }
+ if (pressed & K_Z) st = S_SAY, left_say(s->answers[sub]);
+ break;
+ case S_SAY:
+ if (!typing) st = S_MENU, left_say(s->again);
+ break;
+ case S_SELL: {
+ /* what's carried, and Exit: Z sells it for what it's worth */
+ if (pressed & K_UP) sub = (sub + NUM_ITEMS) % (NUM_ITEMS + 1);
+ if (pressed & K_DOWN) sub = (sub + 1) % (NUM_ITEMS + 1);
+ if (pressed & K_X || ((pressed & K_Z) && sub == NUM_ITEMS)) {
+ st = S_MENU, left_say(s->again);
+ break;
+ }
+ if ((pressed & K_Z) && sv.items[sub]) {
+ sv.gold = (uint16_t)min_i(9999, sv.gold + item_info(sv.items[sub])->sell);
+ item_remove_at(sub);
+ right_text = "thanks!!";
+ }
+ break;
+ }
+ }
+}
+
+static void wares_list(const Shop *s, bool topics) {
+ int n = topics ? s->ntopics : s->nwares;
+ for (int i = 0; i <= n; i++) {
+ char line[48];
+ if (i == n) snprintf(line, sizeof line, "Exit");
+ else if (topics) snprintf(line, sizeof line, "%s", s->topics[i]);
+ else snprintf(line, sizeof line, "%dG - %s", price_of(&s->wares[i]), item_info(s->wares[i].item)->name);
+ gfx_text(FONT_MAIN, 30, 132 + i * 18, line, C_WHITE, 255);
+ }
+ if (!(st == S_CONFIRM)) gfx_sprite(SPR_HEART, 0, 20, 140 + sub * 18, 0, 255);
+}
+
+static void sell_list(void) {
+ for (int i = 0; i <= NUM_ITEMS; i++) {
+ char line[48];
+ if (i == NUM_ITEMS) snprintf(line, sizeof line, "Exit");
+ else if (sv.items[i]) snprintf(line, sizeof line, "%dG - %s", item_info(sv.items[i])->sell, item_info(sv.items[i])->short_name);
+ else line[0] = 0;
+ gfx_text(FONT_SMALL, 30, 127 + i * 12, line, C_WHITE, 255);
+ }
+ gfx_sprite(SPR_HEART, 0, 20, 130 + sub * 12, 0, 255);
+}
+
+void shop_draw(void) {
+ if (is_box) {
+ box_draw();
+ return;
+ }
+ const Shop *s = &shops[shop_id];
+ gfx_rect(0, 0, SCREEN_W, SCREEN_H, C_BLACK, 255);
+ gfx_sprite(s->pic, 0, 0, 0, 0, 255);
+ if (s->keeper) gfx_sprite(s->keeper, 0, s->keeper_x, 120, 0, 255);
+ panel(2, 121, 210, 117);
+ panel(212, 121, 106, 117);
+ char g[24];
+ snprintf(g, sizeof g, "%dG", sv.gold);
+ gfx_text(FONT_MAIN, 226, 214, g, C_WHITE, 255);
+ snprintf(g, sizeof g, "%d/8", item_count());
+ gfx_text(FONT_MAIN, 276, 214, g, C_WHITE, 255);
+ if (st == S_MENU || st == S_SAY) {
+ writer_draw(&w, C_WHITE);
+ static const char *const opts[] = {"Buy", "Sell", "Talk", "Exit"};
+ for (int i = 0; i < 4; i++) gfx_text(FONT_MAIN, 238, 132 + i * 20, opts[i], C_WHITE, 255);
+ if (st == S_MENU) gfx_sprite(SPR_HEART, 0, 228, 140 + sel * 20, 0, 255);
+ return;
+ }
+ if (st == S_SELL) {
+ sell_list();
+ if (right_text) gfx_text(FONT_SMALL, 222, 130, right_text, C_WHITE, 255);
+ return;
+ }
+ wares_list(s, st == S_TALK);
+ if (st == S_CONFIRM) {
+ char q[40];
+ snprintf(q, sizeof q, "Buy it for\n%dG?", price_of(&s->wares[sub]));
+ gfx_text(FONT_MAIN, 222, 132, q, C_WHITE, 255);
+ gfx_text(FONT_MAIN, 238, 172, "Yes", C_WHITE, 255);
+ gfx_text(FONT_MAIN, 238, 190, "No", C_WHITE, 255);
+ gfx_sprite(SPR_HEART, 0, 228, 180 + sel * 18, 0, 255);
+ } else if (st == S_BUY) {
+ if (sub < s->nwares) {
+ /* what the item is: its kind and what it does */
+ const ItemInfo *it = item_info(s->wares[sub].item);
+ char info[64];
+ if (it->type == IT_WEAPON) snprintf(info, sizeof info, "Weapon:\nAT %d", it->at);
+ else if (it->type == IT_ARMOR) snprintf(info, sizeof info, "Armor:\nDF %d", it->df);
+ else snprintf(info, sizeof info, "Heals\n%d HP", it->heal);
+ gfx_text(FONT_MAIN, 222, 172, info, C_WHITE, 255);
+ }
+ if (right_text) gfx_text(FONT_SMALL, 222, 130, right_text, C_WHITE, 255);
+ }
+}
+
+/* the Dimensional Box as an event: it opens, and the event waits for it to close */
+int box_menu(Ev *c) {
+ EV_BEGIN;
+ box_open();
+ WAIT_UNTIL(g_mode == MODE_WORLD);
+ EV_END;
+}
diff --git a/games/undertale/src/shop.h b/games/undertale/src/shop.h
new file mode 100644
index 00000000..8581069f
--- /dev/null
+++ b/games/undertale/src/shop.h
@@ -0,0 +1,10 @@
+/* Undertale: shops and the Dimensional Box (shop.c). */
+#ifndef UT_SHOP_H
+#define UT_SHOP_H
+#include
+enum { SHOP_SNOWDIN, SHOP_GERSON, SHOP_TEM, SHOP_BURGER, SHOP_BRATTY, SHOP_COUNT };
+void shop_open(int id); /* MODE_SHOP until Exit */
+void box_open(void);
+void shop_update(uint32_t keys, uint32_t pressed);
+void shop_draw(void);
+#endif
diff --git a/games/undertale/src/snowdin.h b/games/undertale/src/snowdin.h
new file mode 100644
index 00000000..26860774
--- /dev/null
+++ b/games/undertale/src/snowdin.h
@@ -0,0 +1,25 @@
+/* Undertale: Snowdin, the forest (snowdin1.c, snowdin2.c) and the town (snowtown.c), its monsters (snowmon.c),
+ * Papyrus (papyrus.c), the shops (shop.c). */
+#ifndef UT_SNOWDIN_H
+#define UT_SNOWDIN_H
+#include "event.h"
+void snow1_setup(int room, int from);
+int snow1_event(Ev *c); /* -1: not one of its */
+void snow1_update(void);
+void snow1_draw(void); /* what its rooms draw over the floor (footprints, the tiles) */
+void snow2_setup(int room, int from);
+int snow2_event(Ev *c);
+void snow2_update(void);
+void snow2_draw(void);
+void town_setup(int room, int from);
+int town_event(Ev *c);
+void snow_doggo_stick(void);
+void snow_battle_done(int form); /* snowmon.c: who was killed, who met */ /* Doggo was spared with the stick (his talk after) */
+/* Sans and Papyrus, their feet at (x, y) */
+Obj *obj_sans(int x, int y, int dir);
+Obj *obj_papyrus(int x, int y, int dir);
+/* the camera moves to (x, y) (its top-left), sp pixels a frame; true once there (it stays until world_enter or
+ * cam_free) */
+bool cam_pan(int x, int y, int sp);
+void cam_free(void);
+#endif
diff --git a/games/undertale/src/snowdin1.c b/games/undertale/src/snowdin1.c
new file mode 100644
index 00000000..eeced798
--- /dev/null
+++ b/games/undertale/src/snowdin1.c
@@ -0,0 +1,611 @@
+/* Snowdin Forest, from the Ruins' door to the word search: Sans and his brother Papyrus, Doggo, the ice, the
+ * Snowman, the Invisible Electricity Maze, the Ball Game, the Nice Cream guy. Signs and short lines are the
+ * game's (as the Undertale Wiki records them); the longer talks are kept short. */
+#include
+#include
+#include "battle.h"
+#include "event.h"
+#include "events.h"
+#include "game.h"
+#include "gfx.h"
+#include "items.h"
+#include "monsters.h"
+#include "snowdin.h"
+#include "world.h"
+
+/* ---------------------------------------------------------------- Sans and Papyrus */
+Obj *obj_sans(int x, int y, int dir) {
+ static const uint16_t s[4] = {SPR_SANS_D};
+ Obj *o = obj_npc(ACTOR_SANS, s, x, y, dir, 0);
+ if (o) o->flags &= (uint16_t)~OF_WALKANIM;
+ return o;
+}
+Obj *obj_papyrus(int x, int y, int dir) {
+ static const uint16_t s[4] = {SPR_PAPYRUS_D, SPR_PAPYRUS_D, SPR_PAPYRUS_L, SPR_PAPYRUS_R};
+ Obj *o = obj_npc(ACTOR_PAPYRUS, s, x, y, dir, 0);
+ if (o) o->anim_speed = 4;
+ return o;
+}
+static void gone(Obj *o) { o->kind = OBJ_NONE; }
+
+/* ---------------------------------------------------------------- the electricity maze */
+/* its path (the feet's middle), from the entrance to Papyrus at the exit */
+static const int16_t maze_path[][2] = {{5280, 380}, {5340, 380}, {5340, 340}, {5400, 340}, {5400, 440}, {5470, 440}};
+#define MAZE_POINTS 6
+/* the invisible walls: the maze's box but the path, 26 pixels wide */
+static const int16_t maze_walls[][4] = {
+ {5280, 270, 190, 57}, {5280, 327, 47, 40}, {5280, 393, 107, 73}, {5353, 353, 34, 40}, {5413, 327, 57, 100},
+ {5387, 453, 83, 13}};
+static int maze_prints; /* how far back from the exit Papyrus's footprints go (pixels) */
+static bool maze_growing; /* he's walking it, leaving them */
+static int maze_len(void) {
+ int n = 0;
+ for (int i = 1; i < MAZE_POINTS; i++)
+ n += abs_i(maze_path[i][0] - maze_path[i - 1][0]) + abs_i(maze_path[i][1] - maze_path[i - 1][1]);
+ return n;
+}
+
+/* ---------------------------------------------------------------- the Ball Game */
+#define BALL_X 5890
+#define BALL_Y 352
+#define HOLE_X 5961
+#define HOLE_Y 690
+static struct {
+ int vx, vy; /* 1/16 px a frame */
+ int t; /* frames since first kicked (0: not yet) */
+ bool sunk;
+} ball;
+
+static void ball_place(Obj *b) {
+ b->x = BALL_X, b->y = BALL_Y, b->frame = 0;
+ obj_set_box(b, -9, -9, 18, 18);
+ ball.vx = ball.vy = 0, ball.t = 0, ball.sunk = false;
+}
+
+static void ball_update(void) {
+ Obj *b = obj_find(ACTOR_BALL);
+ if (!b || ball.sunk) return;
+ int r = 10 - b->frame;
+ /* Frisk walking into it kicks it the way he faces */
+ int fx = pl.x + 3, fy = pl.y + 20;
+ if (pl.moving && fx < b->x + r + 2 && fx + 14 > b->x - r - 2 && fy < b->y + r + 2 && fy + 9 > b->y - r - 2) {
+ static const int8_t dx[4] = {0, 0, -1, 1}, dy[4] = {1, -1, 0, 0};
+ ball.vx = dx[pl.dir] * 4 * FX, ball.vy = dy[pl.dir] * 4 * FX;
+ if (!ball.t) ball.t = 1;
+ }
+ if (!ball.t) return;
+ ball.t++;
+ /* it melts as it goes: smaller every three seconds (faster on ice), then gone, then a new one */
+ bool ice = room_ice(b->x, b->y);
+ if (ball.t % (ice ? 45 : 90) == 0) {
+ if (b->frame >= 7) {
+ ball_place(b);
+ return;
+ }
+ b->frame++;
+ r = 10 - b->frame;
+ obj_set_box(b, -r + 1, -r + 1, 2 * r - 2, 2 * r - 2);
+ }
+ /* rolls on, slowing (less on ice); bounces off what stops it */
+ for (int axis = 0; axis < 2; axis++) {
+ int v = axis ? ball.vy : ball.vx;
+ if (!v) continue;
+ int nx = b->x + (axis ? 0 : v / FX), ny = b->y + (axis ? v / FX : 0);
+ if (world_walkable(nx - r + 1, ny - r + 1, 2 * r - 2, 2 * r - 2, (int)(b - objs))) b->x = (int16_t)nx, b->y = (int16_t)ny;
+ else if (axis) ball.vy = -ball.vy / 2;
+ else ball.vx = -ball.vx / 2;
+ }
+ int fr = ice ? 1 : 3;
+ ball.vx += ball.vx > 0 ? -fr : ball.vx < 0 ? fr : 0;
+ ball.vy += ball.vy > 0 ? -fr : ball.vy < 0 ? fr : 0;
+ if (abs_i(ball.vx) < fr) ball.vx = 0;
+ if (abs_i(ball.vy) < fr) ball.vy = 0;
+ /* in the hole */
+ if (abs_i(b->x - HOLE_X) < 7 && abs_i(b->y - HOLE_Y) < 6 && !event_running()) {
+ ball.sunk = true;
+ event_start(EV_BALL_HOLE, b);
+ }
+}
+
+/* ---------------------------------------------------------------- the rooms' things */
+void snow1_setup(int room, int from) {
+ (void)from;
+ switch (room) {
+ case ROOM_SNOWDIN1:
+ if (!flag(F_BRANCH)) obj_trigger(1026, 380, 20, 100, EV_BRANCH, ACTOR_TRIGGER);
+ else obj_sign(996, 384, 26, 30, "* (It's been smashed like it was nothing...)");
+ break;
+ case ROOM_SNOWDIN5: {
+ Obj *l = obj_deco(SPR_LAMP, 2960, 365, true);
+ if (l) {
+ l->name = ACTOR_BLOCK, l->flags |= OF_TALK, l->event = EV_SIGN, l->text = "* (It's a conveniently-shaped lamp.)";
+ obj_set_box(l, 2, 22, 15, 9);
+ }
+ obj_sign(3004, 300, 60, 66, "* (It's some sort of checkpoint or sentry-station.)|* (But there are bottles of ketchup, mustard, and relish sitting inside...)");
+ if (!flag(F_SANS_MET)) obj_trigger(2572, 380, 40, 100, EV_SANS_INTRO, ACTOR_TRIGGER);
+ break;
+ }
+ case ROOM_BOXROAD:
+ obj_save(3240, 376, "* (The convenience of that lamp still fills you with determination.)");
+ obj_sign(3420, 334, 20, 22, "* (A box. Put an item in, take one out.)|* (Boxes further on hold the same things.)");
+ {
+ Obj *b = obj_trigger(3444, 334, 26, 24, EV_BOX, ACTOR_ITEM);
+ if (b) b->flags = OF_TALK | OF_SOLID;
+ }
+ break;
+ case ROOM_RIVER: {
+ Obj *r = obj_trigger(3428, 116, 26, 34, EV_FISHROD, ACTOR_ITEM);
+ if (r) r->flags = OF_TALK;
+ break;
+ }
+ case ROOM_PAPSENTRY:
+ obj_sign(3966, 296, 52, 64, "* (A sentry station, built with great care.)|* (A note brags it was made by a future Royal Guard.)");
+ if (!flag(F_PAP_SENTRY)) {
+ obj_sans(3616, 414, DIR_RIGHT);
+ obj_papyrus(3660, 414, DIR_LEFT);
+ obj_trigger(3540, 370, 34, 110, EV_PAP_SENTRY, ACTOR_TRIGGER);
+ }
+ break;
+ case ROOM_DOGGO:
+ obj_sign(4204, 384, 22, 22, "* (Absolutely NO MOVING!!!)");
+ obj_sign(4486, 398, 26, 16, "* (It's a bag of smoked dog treats.)");
+ obj_sign(4296, 330, 88, 66, flag(F_DOGGO) == 2 ? "* (It's a sentry station. No one is inside.)" : "* (It's Doggo's sentry station.)");
+ if (!flag(F_DOGGO)) obj_trigger(4296, 398, 70, 80, EV_DOGGO, ACTOR_TRIGGER);
+ break;
+ case ROOM_ICEFIELD:
+ obj_sign(4864, 368, 24, 28, "* North: Ice\n South: Ice\n West: Ice|* East: Snowdin Town\n (... and ice)");
+ {
+ Obj *s = obj_sans(4686, 409, DIR_DOWN);
+ if (s) s->event = EV_SANS_TALK, s->arg = 1, s->flags |= OF_TALK;
+ }
+ break;
+ case ROOM_SNOWMAN: {
+ Obj *s = obj_trigger(4904, 60, 40, 44, EV_SNOWMAN, ACTOR_ITEM);
+ if (s) s->flags = OF_TALK | OF_SOLID;
+ break;
+ }
+ case ROOM_ELECMAZE:
+ if (flag(F_ELECMAZE) < 2) {
+ obj_papyrus(5484, 446, DIR_LEFT);
+ if (!flag(F_ELECMAZE)) obj_trigger(5200, 280, 30, 200, EV_ELECMAZE, ACTOR_TRIGGER);
+ else obj_trigger(5440, 420, 30, 40, EV_ELECMAZE, ACTOR_TRIGGER2);
+ }
+ if (flag(F_ELECMAZE) == 1)
+ for (unsigned i = 0; i < sizeof maze_walls / sizeof maze_walls[0]; i++)
+ obj_trigger(maze_walls[i][0], maze_walls[i][1], maze_walls[i][2], maze_walls[i][3], EV_MAZE_ZAP, ACTOR_BLOCK);
+ {
+ Obj *s = obj_sans(5464, 336, DIR_DOWN);
+ if (s) s->event = EV_SANS_TALK, s->arg = 2, s->flags |= OF_TALK;
+ }
+ maze_prints = flag(F_ELECMAZE) ? maze_len() : 0;
+ break;
+ case ROOM_BALLGAME: {
+ Obj *n = obj_trigger(5594, 280, 54, 30, EV_NICECREAM, ACTOR_ITEM);
+ if (n) n->flags = OF_TALK | OF_SOLID;
+ obj_sign(5682, 382, 18, 16, "* (It's a snowdecahedron.)");
+ obj_sign(HOLE_X - 11, HOLE_Y - 8, 22, 16, "* Ah! Wow! Woah!\n* It's a \"#RHole#X\".");
+ if (flag(F_KILLS_SNOWDIN) < 8) {
+ Obj *b = obj_deco(SPR_SNOWBALL, BALL_X, BALL_Y, true);
+ if (b) b->name = ACTOR_BALL, ball_place(b);
+ }
+ Obj *s = obj_sans(6136, 336, DIR_DOWN);
+ if (s) s->event = EV_SANS_TALK, s->arg = 3, s->flags |= OF_TALK;
+ break;
+ }
+ case ROOM_DOGI:
+ obj_sign(6426, 310, 24, 24, "* (A chart of smells and how dangerous they are.)|* (Snowmen: harmless. Puppies: fine.)\n* (Humans: #GDANGER!#X)");
+ break;
+ case ROOM_WORDSEARCH:
+ obj_sign(6434, 648, 18, 18, "* (It's a word search.)\n* (The top row says GIASFCLFUBREHBER.)");
+ if (!flag(F_WORDSEARCH)) {
+ obj_papyrus(6484, 652, DIR_LEFT);
+ obj_trigger(6300, 600, 30, 160, EV_WORDSEARCH, ACTOR_TRIGGER);
+ }
+ {
+ Obj *s = obj_sans(6536, 658, DIR_DOWN);
+ if (s) s->event = EV_SANS_TALK, s->arg = 4, s->flags |= OF_TALK;
+ }
+ break;
+ }
+}
+
+void snow1_update(void) {
+ if (g_room == ROOM_BALLGAME) ball_update();
+}
+
+/* (while a cutscene runs too) */
+static void snow1_scene_update(void) {
+ if (maze_growing && maze_prints < maze_len()) maze_prints += 2;
+}
+
+void snow1_draw(void) {
+ if (g_room != ROOM_ELECMAZE || !maze_prints) return;
+ /* Papyrus's footprints along the maze's path: two dents every 8 pixels */
+ snow1_scene_update();
+ int left = maze_prints;
+ for (int i = MAZE_POINTS - 1; i > 0 && left > 0; i--) {
+ int x0 = maze_path[i][0], y0 = maze_path[i][1], x1 = maze_path[i - 1][0], y1 = maze_path[i - 1][1];
+ int len = abs_i(x1 - x0) + abs_i(y1 - y0);
+ for (int d = 0; d < len && d < left; d += 8) {
+ int x = x0 + (x1 > x0 ? d : x1 < x0 ? -d : 0), y = y0 + (y1 > y0 ? d : y1 < y0 ? -d : 0);
+ int ox = y1 != y0 ? 3 : 0, oy = x1 != x0 ? 3 : 0;
+ gfx_rect(x - ox - 1 - cam_x, y - oy - 1 - cam_y, 3, 2, rgb565(0x55607F), 255);
+ gfx_rect(x + ox - 1 - cam_x, y + oy - cam_y, 3, 2, rgb565(0x55607F), 255);
+ }
+ left -= len;
+ }
+}
+
+/* ---------------------------------------------------------------- the events */
+static int ev_branch(Ev *c) {
+ EV_BEGIN;
+ /* behind Frisk, the branch snaps */
+ set_flag(F_BRANCH, 1);
+ g_shake = 10;
+ pl.frozen = true;
+ WAIT(12);
+ pl.dir = DIR_LEFT;
+ WAIT(30);
+ pl.dir = DIR_RIGHT;
+ pl.frozen = false;
+ if (c->self) gone(c->self);
+ obj_sign(996, 384, 26, 30, "* (It's been smashed like it was nothing...)");
+ EV_END;
+}
+
+static int ev_sans_intro(Ev *c) {
+ Obj *s = obj_get(ACTOR_SANS), *p = obj_get(ACTOR_PAPYRUS), *lamp = obj_get(ACTOR_BLOCK);
+ EV_BEGIN;
+ /* on the bridge, steps behind Frisk */
+ PL_WALK(2598, 392, 2);
+ pl.dir = DIR_RIGHT;
+ WAIT(20);
+ s = obj_sans(2556, 422, DIR_RIGHT);
+ if (s) s->flags |= OF_DARK;
+ s = obj_get(ACTOR_SANS);
+ WALK(s, 2584, 422, 1);
+ NARRATE("* Human.|* Don't you know how to greet a new pal?|* Turn around and shake my hand.");
+ pl.dir = DIR_LEFT;
+ WAIT(30);
+ g_shake = 4;
+ WAIT(20);
+ s->flags &= (uint16_t)~OF_DARK;
+ SAY(FACE_SANS, "`1* heh. never gets old.\n`0* i'm sans. sans the skeleton.|* i'm meant to be watching for humans... but i'm not big on capturing.|* my brother papyrus, though? he's really into it.|* speak of the devil.\n* quick, go through the gate.|* the bars are too wide to\n stop anyone anyway.");
+ /* through the gate, then behind the lamp */
+ WALK_TO(s, 2660, 432, 2);
+ PL_WALK(2720, 392, 2);
+ SAY(FACE_SANS, "* quick, behind that conveniently-shaped lamp.");
+ WALK_TO(s, 2920, 432, 3);
+ PL_WALK(2959, 392, 3);
+ PL_WALK(2959, 357, 2);
+ pl.dir = DIR_DOWN;
+ lamp->flags |= OF_FRONT;
+ WAIT_UNTIL(!s->walking);
+ s->dir = DIR_RIGHT;
+ /* Papyrus comes, scolds his brother and storms off */
+ p = obj_papyrus(3180, 432, DIR_LEFT);
+ p = obj_get(ACTOR_PAPYRUS);
+ WALK(p, 3000, 432, 3);
+ SAY(FACE_SANS, "* sup, bro?");
+ SAY(FACE_PAPYRUS, "`3* SANS!!! YOUR PUZZLES STILL\n AREN'T RECALIBRATED!|* YOU'VE BEEN LAZING AROUND\n FOR DAYS!!!");
+ SAY(FACE_SANS, "* staring at this lamp. it's really cool.");
+ SAY(FACE_PAPYRUS, "`3* WHAT IF A HUMAN SHOWS UP!?!|`2* I'LL BE READY!\n* I'LL CAPTURE THEM!|* AND THEN THE ROYAL GUARD\n WILL HAVE TO LET ME IN!!!");
+ SAY(FACE_SANS, "* hmm... maybe this lamp will help you.");
+ SAY(FACE_PAPYRUS, "`3* SANS!!! YOU LAZYBONES!|* YOU GET LAZIER AND LAZIER EVERY DAY!!!");
+ SAY(FACE_SANS, "* hey, take it easy. i've\n done a ton of work today.|`2* a skele-ton.");
+ SAY(FACE_PAPYRUS, "`3* SANS!!!");
+ SAY(FACE_SANS, "`1* come on. you're smiling.");
+ SAY(FACE_PAPYRUS, "`3* I AM AND I HATE IT!|`3* UGH!!!\n`0* I WILL ATTEND TO MY PUZZLES...|* AS FOR YOUR WORK?\n`2* PUT A LITTLE MORE, \"BACKBONE\" INTO IT!!!!|* NYEH HEH HEH HEH HEH HEH!!!");
+ WALK(p, 3180, 432, 4);
+ gone(p);
+ WAIT(30);
+ p = obj_papyrus(3180, 432, DIR_LEFT);
+ p = obj_get(ACTOR_PAPYRUS);
+ WALK(p, 3140, 432, 4);
+ SAY(FACE_PAPYRUS, "`2* HEH!");
+ WALK(p, 3180, 432, 4);
+ gone(p);
+ SAY(FACE_SANS, "* ok, you can come out now.");
+ lamp->flags &= (uint16_t)~OF_FRONT;
+ PL_WALK(2959, 400, 2);
+ pl.dir = DIR_LEFT;
+ SAY(FACE_SANS, "* you should keep moving.\n`1* unless you want to hear more of my jokes.");
+ SAY(FACE_SANS, "`0* oh, one favor?|* my brother's never met a human. seeing you would make his day.|* he's harmless, promise. i'll be up ahead.");
+ set_flag(F_SANS_MET, 1);
+ set_flag(F_SANS_FAVOR, 1);
+ /* (the wrong way) */
+ WALK(s, 2556, 432, 2);
+ gone(s);
+ if (c->self) gone(c->self);
+ EV_END;
+}
+
+static int ev_sans_talk(Ev *c) {
+ int k = c->self ? c->self->arg : 0;
+ EV_BEGIN;
+ switch (k) {
+ case 1: /* by the ice: Papyrus's special attack */
+ if (!flag(F_SANS_HINT)) {
+ set_flag(F_SANS_HINT, 1);
+ SAY(FACE_SANS, "* a tip: my brother's got a #Bspecial attack#X.|* when it's #Lblue#X, stay still and it can't hurt you.|* think of a stop sign... but blue.");
+ } else {
+ SAY(FACE_SANS, "* remember... #Lblue#X stop sign.");
+ }
+ break;
+ case 2: /* by the maze: his brother's outfit */
+ if (flag(F_ELECMAZE) < 2) SAY(FACE_SANS, "* go on. he made this one just for you.");
+ else if (!flag(F_SANS_COOL)) {
+ SAY(FACE_SANS, "* thanks. he's having a blast.|* that outfit? party costume.\n* he won't take it off.|* calls it his \"battle body.\"|* cool brother, right?@Cool@Uncool");
+ set_flag(F_SANS_COOL, CHOICE() + 1);
+ if (CHOICE() == 0) SAY(FACE_SANS, "`1* you'd better tell him that.");
+ else SAY(FACE_SANS, "* hey, pal. sarcasm isn't funny, okay?\n* my brother's a real star.");
+ } else {
+ SAY(FACE_SANS, "* my brother's a real star.\n* he's the person who pushed me to get this sentry job.");
+ }
+ break;
+ case 3: /* by the Ball Game: fried snow, ever dearer, and then none */
+ set_flag(F_FRIED_SNOW, flag(F_FRIED_SNOW) + 1);
+ if (flag(F_FRIED_SNOW) == 1) SAY(FACE_SANS, "* hey. want some fried snow?\n* it's 5G.");
+ else if (flag(F_FRIED_SNOW) == 2) SAY(FACE_SANS, "* fried snow? 10G.");
+ else if (flag(F_FRIED_SNOW) == 3) SAY(FACE_SANS, "* fried snow? 50G.");
+ else SAY(FACE_SANS, "`1* actually, i'm all out of snow.");
+ break;
+ case 4: /* by the word search */
+ if (!flag(F_WORDSEARCH)) SAY(FACE_SANS, "* trust me. there's no way they can get past this one.");
+ else SAY(FACE_SANS, "* papyrus tried to solve\n yesterday's horoscope.|`1* says it was really tough.");
+ break;
+ }
+ EV_END;
+}
+
+static int ev_pap_sentry(Ev *c) {
+ Obj *s = obj_get(ACTOR_SANS), *p = obj_get(ACTOR_PAPYRUS);
+ EV_BEGIN;
+ pl.dir = DIR_RIGHT;
+ SAY(FACE_PAPYRUS, "`0* SO, AS I WAS SAYING ABOUT UNDYNE...");
+ SAY(FACE_SANS, "* hey, check it out.");
+ /* the double take */
+ for (c->v[0] = 0; c->v[0] < 6; c->v[0]++) {
+ p->dir = (uint8_t)(c->v[0] % 2 ? DIR_RIGHT : DIR_LEFT);
+ WAIT(c->v[0] < 4 ? 6 : 14);
+ }
+ p->dir = DIR_LEFT;
+ SAY(FACE_PAPYRUS, "`3* SANS!! OH MY GOD!!\n* IS THAT... A HUMAN!?!?");
+ SAY(FACE_SANS, "* uhhhh... actually, i think that's a rock.");
+ SAY(FACE_PAPYRUS, "`4* OH.");
+ SAY(FACE_SANS, "* hey, what's that in front of the rock?");
+ SAY(FACE_PAPYRUS, "`3* OH MY GOD!!! (IS THAT A HUMAN?)");
+ SAY(FACE_SANS, "* yes.");
+ SAY(FACE_PAPYRUS, "`2* SANS!!! IT'S A HUMAN!!! I'M GOING TO BE SO POPULAR!!!|`0* HUMAN! THE GREAT PAPYRUS WILL STOP YOU AND CAPTURE YOU!|`4* AND THEN... WELL, I'LL WORK THAT OUT LATER.|`2* ONWARD, IF YOU DARE!!! NYEH HEH HEH!!!");
+ WALK(p, 4140, 414, 6);
+ gone(p);
+ SAY(FACE_SANS, "* well, that went well.\n* don't sweat it, kid.|* i'll keep an eye out for ya.");
+ WALK(s, 4140, 414, 2);
+ gone(s);
+ set_flag(F_PAP_SENTRY, 1);
+ if (c->self) gone(c->self);
+ EV_END;
+}
+
+static bool doggo_stick; /* (snowmon.c: spared with the stick) */
+void snow_doggo_stick(void) { doggo_stick = true; }
+
+static int ev_doggo(Ev *c) {
+ Obj *d = obj_get(ACTOR_DOGGO);
+ EV_BEGIN;
+ d = obj_deco(SPR_DOGGO_OW, 4340, 386, false);
+ if (d) d->name = ACTOR_DOGGO, d->flags |= OF_FRONT;
+ NARRATE("* Something moved? Or did it?|* I only see things that move.\n* And moving humans don't stay moving for long!");
+ doggo_stick = false;
+ BATTLE(FORM_DOGGO);
+ set_flag(F_DOGGO, bt.mon[0].killed ? 2 : 1);
+ if (flag(F_DOGGO) == 1) {
+ if (doggo_stick) NARRATE("* A stick? Then... no stick?|* I'll need a dog treat to figure this out.");
+ else NARRATE("* S-S-S-Something pet me...\n* Something that isn't m-m-moving...|* I'm gonna need some dog treats for this!!!");
+ }
+ gone(d);
+ if (c->self) gone(c->self);
+ EV_END;
+}
+
+static int ev_snowman(Ev *c) {
+ EV_BEGIN;
+ {
+ bool has = false;
+ for (int i = 0; i < NUM_ITEMS; i++) has |= sv.items[i] == ITEM_SNOWMAN_PIECE;
+ c->v[0] = has;
+ }
+ if (c->v[0]) {
+ NARRATE("* Thank you... good luck!");
+ } else if (flag(F_SNOWMAN) >= 2) {
+ NARRATE("* If you keep taking pieces of me, there'll be nothing left...");
+ } else {
+ if (!flag(F_SNOWMAN)) NARRATE("* I'm a snowman, and I'd love to travel.|* Would you carry a piece of me far away?@Yes@No");
+ else NARRATE("* Did you lose my piece...?\n* Will you take another?@Yes@No");
+ if (CHOICE() == 0) {
+ if (item_add(ITEM_SNOWMAN_PIECE)) {
+ set_flag(F_SNOWMAN, flag(F_SNOWMAN) + 1);
+ NARRATE("* Thank you... good luck!\n* (You got the Snowman Piece.)");
+ } else {
+ NARRATE("* (You're carrying too much.)");
+ }
+ } else {
+ NARRATE("* I see... good journey, then.");
+ }
+ }
+ EV_END;
+}
+
+static int ev_elecmaze(Ev *c) {
+ Obj *p = obj_get(ACTOR_PAPYRUS);
+ EV_BEGIN;
+ if (flag(F_ELECMAZE) == 1) goto crossed;
+ pl.dir = DIR_RIGHT;
+ SAY(FACE_PAPYRUS, "`0* HUMAN!! BEHOLD: THE INVISIBLE ELECTRICITY MAZE!!!|* TOUCH A WALL, AND THIS ORB ZAPS YOU!\n`2* NYEH HEH HEH!!!");
+ /* Frisk steps in: Papyrus, holding the orb, gets the shock */
+ FREE(true);
+ WAIT_UNTIL(pl.x + 10 >= 5282);
+ FREE(false);
+ p->spr[DIR_LEFT] = SPR_PAPYRUS_ZAP, p->flags |= OF_ANIM, p->anim_speed = 3;
+ g_shake = 12;
+ WAIT(40);
+ p->spr[DIR_LEFT] = SPR_PAPYRUS_L, p->flags &= (uint16_t)~OF_ANIM, p->frame = 0;
+ SAY(FACE_PAPYRUS, "`3* SANS!!! WHAT HAPPENED???");
+ SAY(FACE_SANS, "* i think you have to hold the orb.");
+ SAY(FACE_PAPYRUS, "`4* OH, OKAY.");
+ /* he walks the maze to Frisk (his footprints show the way), gives the orb, and walks back */
+ maze_growing = true;
+ for (c->v[0] = MAZE_POINTS - 2; c->v[0] >= 0; c->v[0]--) WALK(p, maze_path[c->v[0]][0], maze_path[c->v[0]][1] + 6, 2);
+ maze_growing = false;
+ maze_prints = maze_len();
+ p->dir = DIR_LEFT;
+ SAY(FACE_PAPYRUS, "`0* HOLD THIS PLEASE.");
+ NARRATE("* (You got the orb.)");
+ for (c->v[0] = 1; c->v[0] < MAZE_POINTS; c->v[0]++) WALK(p, maze_path[c->v[0]][0] + (c->v[0] == MAZE_POINTS - 1 ? 14 : 0), maze_path[c->v[0]][1] + 6, 3);
+ p->dir = DIR_LEFT;
+ SAY(FACE_PAPYRUS, "`2* OKAY! TRY IT NOW!!!");
+ set_flag(F_ELECMAZE, 1);
+ if (c->self) gone(c->self);
+ for (unsigned i = 0; i < sizeof maze_walls / sizeof maze_walls[0]; i++)
+ obj_trigger(maze_walls[i][0], maze_walls[i][1], maze_walls[i][2], maze_walls[i][3], EV_MAZE_ZAP, ACTOR_BLOCK);
+ obj_trigger(5440, 420, 30, 40, EV_ELECMAZE, ACTOR_TRIGGER2);
+ return 1;
+crossed:
+ SAY(FACE_PAPYRUS, "`2* YOU SOLVED IT! FAR TOO EASILY...|`0* THE NEXT ONE WON'T BE SO KIND!!!\n`2* NYEH HEH HEH!!!");
+ WALK(p, 5520, 446, 5);
+ gone(p);
+ set_flag(F_ELECMAZE, 2);
+ for (int i = 0; i < MAX_OBJS; i++)
+ if (objs[i].event == EV_MAZE_ZAP || objs[i].event == EV_ELECMAZE) objs[i].kind = OBJ_NONE;
+ EV_END;
+}
+
+static int ev_maze_zap(Ev *c) {
+ EV_BEGIN;
+ /* the orb shocks Frisk (no harm) and he's back at the start */
+ g_shake = 10;
+ pl.frozen = true;
+ WAIT(20);
+ pl.x = 5258, pl.y = 356, pl.dir = DIR_RIGHT;
+ pl.frozen = false;
+ EV_END;
+}
+
+static int ev_nicecream(Ev *c) {
+ EV_BEGIN;
+ if (!flag(F_NICECREAM)) {
+ set_flag(F_NICECREAM, 1);
+ NARRATE("* Nobody wants something cold today. Odd...|* OH!!!! A CUSTOMER!!!");
+ NARRATE("* Hello! Would you like some Nice Cream?|* The frozen treat that warms\n your heart! Just 15G!@Yes@No");
+ } else {
+ NARRATE("* Nice Cream! The frozen treat\n that warms your heart!|* Just 15G!@Yes@No");
+ }
+ if (CHOICE() == 1) {
+ NARRATE("* Well then... Tell your friends...|* There's ice cream... Out in the middle of the woods...");
+ } else if (sv.gold < 15) {
+ NARRATE("* Huh? You don't have enough money...|* I wish I could make Nice Cream for free...");
+ } else if (!item_add(ITEM_NICE_CREAM)) {
+ NARRATE("* It looks like you're holding too much stuff! Oh well!");
+ } else {
+ sv.gold = (uint16_t)(sv.gold - 15);
+ NARRATE("* Here you go!\n* Have a super-duper day!\n* (You got the Nice Cream.)");
+ }
+ EV_END;
+}
+
+static char ball_text[260];
+static int ev_ball_hole(Ev *c) {
+ Obj *b = c->self;
+ EV_BEGIN;
+ if (b) b->flags |= OF_HIDDEN;
+ {
+ /* the flag that pops up: by how long the ball took, or how small it was */
+ int secs = ball.t / 30, g;
+ uint16_t spr;
+ const char *t;
+ if (secs < 6 && flag(F_BALLGAME) != 7) {
+ spr = SPR_FLAG_RED, g = 50, t = "* Bravery. Justice. Integrity. Kindness. Perseverance. Patience.|* Using these, you were able to win at \"Ball Game.\"";
+ } else if (secs < 6) {
+ spr = SPR_FLAG_RED, g = 10, t = "* #RRED#X - Try as you might, you continue to be yourself.";
+ } else if (b && b->frame >= 5) {
+ spr = SPR_FLAG_LIGHTBLUE, g = 4, t = "* #LLIGHT BLUE#X - \"Ball\" is \"Small.\"|* You waited, still, for this opportunity...\n* ... then dethroned \"Ball\" with a sharp attack.";
+ } else if (secs < 8) {
+ spr = SPR_FLAG_ORANGE, g = 5, t = "* #OORANGE#X - You are the kind of person who rushes fists-first through all obstacles.";
+ } else if (secs < 11) {
+ spr = SPR_FLAG_YELLOW, g = 3, t = "* #YYELLOW#X - Your sure-fire accuracy put an end to the mayhem of \"Ball.\"";
+ } else if (secs < 15) {
+ spr = SPR_FLAG_BLUE, g = 2, t = "* #BBLUE#X - Hopping and twirling, your original style pulled you through.";
+ } else if (secs < 20) {
+ spr = SPR_FLAG_GREEN, g = 1, t = "* #GGREEN#X - Your concern and care for \"Ball\" led you to a delicious victory.";
+ } else {
+ spr = SPR_FLAG_PURPLE, g = 2, t = "* #PPURPLE#X - Even when you felt trapped, you took notes and achieved the end of \"Ball.\"";
+ }
+ if (spr == SPR_FLAG_RED) set_flag(F_BALLGAME, 7);
+ else if (!flag(F_BALLGAME)) set_flag(F_BALLGAME, 1);
+ sv.gold = (uint16_t)min_i(9999, sv.gold + g);
+ snprintf(ball_text, sizeof ball_text, "%s|* (You won %dG.)", t, g);
+ Obj *f = obj_deco(spr, HOLE_X, HOLE_Y + 4, false);
+ if (f) f->name = ACTOR_DOG;
+ }
+ NARRATE(ball_text);
+ WAIT(30);
+ gone(obj_get(ACTOR_DOG));
+ if (b) b->flags &= (uint16_t)~OF_HIDDEN, ball_place(b);
+ EV_END;
+}
+
+static int ev_wordsearch(Ev *c) {
+ Obj *p = obj_get(ACTOR_PAPYRUS);
+ EV_BEGIN;
+ pl.dir = DIR_RIGHT;
+ SAY(FACE_PAPYRUS, "`0* HUMAN!!!|* THE NEXT PUZZLE WILL NOT\n BE EASY!|* IT IS DESIGNED BY MY\n BROTHER, SANS!");
+ SAY(FACE_SANS, "* it's right there.\n on the ground.|* trust me. there's no way\n they can get past this one.");
+ FREE(true);
+ WAIT_UNTIL(pl.x >= 6440);
+ FREE(false);
+ pl.dir = DIR_RIGHT;
+ SAY(FACE_PAPYRUS, "`3* SANS!!! THE PUZZLE DIDN'T DO ANYTHING!!!");
+ SAY(FACE_SANS, "* whoops. i knew i should have used today's crossword instead.");
+ SAY(FACE_PAPYRUS, "`3* CROSSWORD!?|* IN MY OPINION, JUNIOR\n JUMBLE IS EASILY THE\n HARDEST.");
+ SAY(FACE_SANS, "* what? really, dude? that easy-peasy word scramble?\n* that's for baby bones.");
+ SAY(FACE_PAPYRUS, "`3* UNBELIEVABLE!!!\n* HUMAN!! SOLVE THIS DISPUTE!@Crossword@Junior Jumble");
+ set_flag(F_WORDSEARCH, CHOICE() + 1);
+ if (CHOICE() == 0) SAY(FACE_PAPYRUS, "`3* HUMANS... YOU'RE ALL THE SAME!!!");
+ else SAY(FACE_PAPYRUS, "`2* HA! THE HUMAN AGREES WITH ME!!!");
+ SAY(FACE_PAPYRUS, "`2* NYEH HEH HEH HEH!!!");
+ WALK(p, 6640, 652, 6);
+ gone(p);
+ if (c->self) gone(c->self);
+ EV_END;
+}
+
+static int ev_fishrod(Ev *c) {
+ EV_BEGIN;
+ NARRATE("* (A fishing rod, stuck in\n the ground. Reel it in?)@Yes@No");
+ if (CHOICE() == 0) {
+ set_flag(F_FISH, 1);
+ NARRATE("* (You reeled in the line...)\n* (There's a note on the hook.)|* \"Call me! Here's my number!\"");
+ }
+ EV_END;
+}
+
+int box_menu(Ev *c); /* shop.c: the Dimensional Box */
+
+int snow1_event(Ev *c) {
+ switch (c->id) {
+ case EV_BRANCH: return ev_branch(c);
+ case EV_SANS_INTRO: return ev_sans_intro(c);
+ case EV_SANS_TALK: return ev_sans_talk(c);
+ case EV_PAP_SENTRY: return ev_pap_sentry(c);
+ case EV_DOGGO: return ev_doggo(c);
+ case EV_SNOWMAN: return ev_snowman(c);
+ case EV_ELECMAZE: return ev_elecmaze(c);
+ case EV_MAZE_ZAP: return ev_maze_zap(c);
+ case EV_NICECREAM: return ev_nicecream(c);
+ case EV_BALL_HOLE: return ev_ball_hole(c);
+ case EV_WORDSEARCH: return ev_wordsearch(c);
+ case EV_FISHROD: return ev_fishrod(c);
+ case EV_BOX: return box_menu(c);
+ }
+ return -1;
+}
diff --git a/games/undertale/src/snowdin2.c b/games/undertale/src/snowdin2.c
new file mode 100644
index 00000000..3cef0f8a
--- /dev/null
+++ b/games/undertale/src/snowdin2.c
@@ -0,0 +1,448 @@
+/* Snowdin Forest, from the spaghetti to the bridge into town: the snow puzzle and the Dogi, the XOXO puzzles,
+ * the Multicolor Tile Puzzle, the Dog House, the ice puzzle and the slide through the trees, the snow poffs and
+ * Greater Dog, the Gauntlet of Deadly Terror, the cliff below and the mysterious door. */
+#include
+#include
+#include "battle.h"
+#include "event.h"
+#include "events.h"
+#include "game.h"
+#include "gfx.h"
+#include "items.h"
+#include "monsters.h"
+#include "snowdin.h"
+#include "world.h"
+
+static void gone(Obj *o) { o->kind = OBJ_NONE; }
+
+/* ---------------------------------------------------------------- the XOXO puzzles */
+/* a puzzle's tiles (their top-left), its switch, its spikes */
+typedef struct {
+ const int16_t (*tiles)[2];
+ int n;
+ int16_t sw_x, sw_y;
+ int flag;
+} Xoxo;
+static const int16_t x1_tiles[][2] = {{7460, 1240}, {7540, 1240}};
+static const int16_t x2_tiles[][2] = {{8180, 1200}, {8240, 1200}, {8280, 1200}, {8240, 1220}, {8280, 1220}, {8200, 1240},
+ {8240, 1240}, {8260, 1240}, {8280, 1240}, {8180, 1260}, {8240, 1260}, {8180, 1280},
+ {8200, 1280}, {8220, 1280}, {8240, 1280}};
+static const int16_t x3_tiles[][2] = {{9540, 1160}, {9700, 1160}, {9540, 1180}, {9580, 1180}, {9660, 1180}, {9620, 1220},
+ {9580, 1240}, {9620, 1240}, {9580, 1260}, {9660, 1260}, {9540, 1300}, {9700, 1300}};
+static const Xoxo xoxos[3] = {
+ {x1_tiles, 2, 7500, 1196, F_XOXO1}, {x2_tiles, 15, 8098, 1240, F_XOXO2}, {x3_tiles, 12, 9743, 1224, F_XOXO3}};
+static const Xoxo *xo; /* the room's, or NULL */
+static uint8_t xo_state[16]; /* 0 X, 1 O */
+static int xo_on = -1; /* the tile under Frisk */
+static bool xo_kill; /* XOXO 2: the switch in the tree was pressed */
+
+static void xo_show(void) {
+ for (int i = 0; i < MAX_OBJS; i++) {
+ Obj *o = &objs[i];
+ if (o->kind != OBJ_DECO || o->name != ACTOR_TILE) continue;
+ bool solved = flag(xo->flag);
+ o->spr[0] = (uint16_t)(xo_state[o->arg] || solved ? SPR_OTILE : SPR_XTILE);
+ o->frame = solved ? 1 : 0;
+ }
+}
+
+static void xo_setup(int k) {
+ xo = &xoxos[k];
+ xo_on = -1, xo_kill = false;
+ memset(xo_state, 0, sizeof xo_state);
+ for (int i = 0; i < xo->n; i++) {
+ Obj *o = obj_deco(SPR_XTILE, xo->tiles[i][0], xo->tiles[i][1], false);
+ if (o) o->name = ACTOR_TILE, o->arg = (int16_t)i, o->flags |= OF_BACK, o->bh = 0;
+ }
+ Obj *sw = obj_deco(SPR_SNOWSWITCH, xo->sw_x, xo->sw_y, false);
+ if (sw) sw->flags |= OF_BACK | OF_TALK, sw->event = EV_XOXO_SWITCH, obj_set_box(sw, 0, 2, 24, 22), sw->name = ACTOR_SWITCH;
+ xo_show();
+}
+
+/* Frisk steps on a tile: X turns O, O turns back to X */
+static void xo_update(void) {
+ if (!xo || flag(xo->flag)) return;
+ int fx = pl.x + 10, fy = pl.y + 25, on = -1;
+ for (int i = 0; i < xo->n; i++)
+ if (fx >= xo->tiles[i][0] && fx < xo->tiles[i][0] + 20 && fy >= xo->tiles[i][1] && fy < xo->tiles[i][1] + 20) on = i;
+ if (on >= 0 && on != xo_on) {
+ xo_state[on] = !xo_state[on];
+ xo_show();
+ }
+ xo_on = on;
+}
+
+/* past the first XOXO, Papyrus waits by the spikes */
+static void pap_xoxo1(void) {
+ if (flag(F_PAP_XOXO1)) return;
+ obj_papyrus(7720, 1252, DIR_LEFT);
+ obj_trigger(7600, 1180, 30, 120, EV_PAP_XOXO, ACTOR_TRIGGER);
+}
+
+static int ev_xoxo_switch(Ev *c) {
+ EV_BEGIN;
+ if (!xo || flag(xo->flag)) {
+ NARRATE("* (The switch is already pressed.)");
+ return 1;
+ }
+ c->v[0] = 1;
+ for (int i = 0; i < xo->n; i++) c->v[0] &= xo_state[i];
+ if (c->v[0] || (xo == &xoxos[1] && xo_kill)) {
+ set_flag(xo->flag, 1);
+ xo_show();
+ /* the spikes go down, or (the ice puzzle) the bridge over to the trees is there */
+ for (int i = 0; i < MAX_OBJS; i++) {
+ if (objs[i].name == ACTOR_SPIKES) obj_spikes_set(&objs[i], false), objs[i].flags |= OF_HIDDEN;
+ if (objs[i].name == ACTOR_BLOCK) objs[i].flags &= (uint16_t)~OF_SOLID;
+ }
+ if (xo == &xoxos[0]) pap_xoxo1();
+ NARRATE("* (Click!)");
+ } else {
+ memset(xo_state, 0, sizeof xo_state);
+ xo_show();
+ NARRATE("* (Click...)\n* (The puzzle reset.)");
+ }
+ EV_END;
+}
+
+/* ---------------------------------------------------------------- the Multicolor Tile Puzzle */
+#define TILES_X 8660
+#define TILES_Y 1200
+#define TILES_W 8
+#define TILES_H 6
+static int tiles_t; /* frames left of its colours shuffling, -1 off */
+
+/* ---------------------------------------------------------------- the rooms' things */
+static Obj *spikes(int x, int y, int rows, bool raised) {
+ Obj *o = obj_spikes(x, y, 1, rows, raised, ACTOR_SPIKES);
+ if (o) o->spr[0] = SPR_SNOWSPIKES, o->frame = 0, o->flags |= OF_BACK;
+ if (o && !raised) o->flags |= OF_HIDDEN;
+ return o;
+}
+
+void snow2_setup(int room, int from) {
+ (void)from;
+ xo = NULL, tiles_t = -1;
+ switch (room) {
+ case ROOM_SPAGHETTI:
+ obj_save(6745, 695, "* (Knowing the mouse might one day find a way to heat up the spaghetti...)\n* (It fills you with determination.)");
+ obj_sign(6690, 658, 16, 20, "* (A note by the spaghetti.)|* ENJOY, HUMAN!|* (IT'S A TRAP: YOU'LL BE TOO\n BUSY EATING TO MOVE ON!)\n* - PAPYRUS");
+ obj_sign(6734, 646, 38, 34, "* (A plate of frozen spaghetti.)|* (It's so cold, it's stuck\n to the table.)");
+ obj_sign(6788, 644, 38, 36, "* (It's a microwave.)|* (Its only setting is\n \"spaghetti.\")\n* (It's unplugged.)");
+ obj_sign(6742, 585, 18, 14, "* (Squeak!)");
+ break;
+ case ROOM_SNOWPUZZLE:
+ obj_sign(6984, 632, 20, 22, "* Warning: Dog Marriage|* (Yes, you read that correctly.)");
+ obj_sign(6980, 920, 80, 80, "* (It's a map of this room, drawn in the snow.)\n* (An X marks its top right.)");
+ {
+ Obj *sw = obj_trigger(7300, 650, 46, 68, EV_SNOW_SWITCH, ACTOR_SWITCH);
+ if (sw) sw->flags = OF_TALK;
+ }
+ spikes(7120, 940, 3, !flag(F_SNOWPUZZLE));
+ if (!flag(F_DOGI)) obj_trigger(7300, 900, 140, 140, EV_DOGI, ACTOR_TRIGGER);
+ break;
+ case ROOM_XOXO1:
+ obj_sign(7456, 1262, 20, 20, "* Turn every X into an O.\n* Then press the switch.");
+ xo_setup(0);
+ spikes(7680, 1220, 3, !flag(F_XOXO1));
+ if (flag(F_XOXO1)) pap_xoxo1();
+ break;
+ case ROOM_XOXO2:
+ obj_sign(8096, 1198, 20, 22, "* Turn every X into an O.\n* Then press the switch.");
+ xo_setup(1);
+ spikes(8360, 1160, 8, !flag(F_XOXO2));
+ {
+ /* the switch in the tree */
+ Obj *k = obj_trigger(7956, 1150, 30, 56, EV_XOXO_SWITCH, ACTOR_TRIGGER3);
+ if (k) k->flags = OF_TALK, k->arg = 1;
+ }
+ if (!flag(F_XOXO2)) {
+ Obj *p = obj_papyrus(7880, 1222, DIR_DOWN);
+ if (p) p->event = EV_PAP_XOXO, p->arg = 2, p->flags |= OF_TALK;
+ if (!flag(F_PAP_XOXO1) || flag(F_PAP_XOXO1) == 1) obj_trigger(7700, 1160, 30, 140, EV_PAP_XOXO, ACTOR_TRIGGER);
+ } else {
+ Obj *s = obj_sans(8390, 1216, DIR_DOWN);
+ if (s) s->event = EV_SNOW_SANS_CLIFF, s->arg = 1, s->flags |= OF_TALK;
+ }
+ break;
+ case ROOM_TILEPUZZLE:
+ obj_sign(8818, 1176, 26, 30, "* (The machine isn't working.)");
+ if (!flag(F_TILES)) {
+ obj_papyrus(8840, 1252, DIR_LEFT);
+ obj_trigger(8560, 1160, 30, 100, EV_TILES, ACTOR_TRIGGER);
+ /* (the tiles aren't there yet: nothing to walk on past the bridge) */
+ Obj *b = obj_trigger(8656, 1196, 8, 130, 0, ACTOR_BLOCK);
+ if (b) b->flags = OF_SOLID;
+ }
+ {
+ Obj *s = obj_sans(8834, 1290, DIR_DOWN);
+ if (s) s->event = EV_SNOW_SANS_CLIFF, s->arg = 2, s->flags |= OF_TALK;
+ }
+ break;
+ case ROOM_DOGHOUSE:
+ obj_save(9260, 1300, flag(F_LESSERDOG) == 2 ? "* (Snow can always be broken down and rebuilt into something more useful.)\n* (This simple fact fills you with determination.)"
+ : "* (Knowing the dog will never give up trying to make the perfect snowdog...)\n* (It fills you with determination.)");
+ obj_sign(9186, 1196, 20, 22, "* AWARE OF DOG\n* pleas pet dog");
+ obj_sign(9356, 1170, 26, 34, "* (Lesser Dog keeps building a snow dog...)\n* (It gets taller every time.)");
+ break;
+ case ROOM_XOXO3:
+ xo_setup(2);
+ if (!flag(F_XOXO3)) {
+ /* the gap to the trees, until the bridge comes */
+ Obj *b = obj_trigger(9776, 1206, 10, 40, 0, ACTOR_BLOCK);
+ if (b) b->flags = OF_SOLID;
+ }
+ /* off the ice's edge: down to the room below */
+ obj_trigger(9522, 1322, 262, 14, EV_XOXO_FALL, ACTOR_TRIGGER2);
+ break;
+ case ROOM_XOXOFAIL:
+ obj_sign(9520, 1440, 30, 40, "* (It's a snow Papyrus.)");
+ obj_sign(9636, 1466, 30, 20, "* (It's a lump of snow with \"Sans\" written on it.)");
+ break;
+ case ROOM_POFFZONE: {
+ static const int16_t poffs[][2] = {{10330, 1215}, {10485, 1235}, {10370, 1275}, {10465, 1285}, {10560, 1205},
+ {10620, 1245}, {10665, 1215}, {10570, 1290}, {10745, 1270}, {10882, 1284}};
+ for (int i = 0; i < 10; i++) {
+ Obj *p = obj_trigger(poffs[i][0] - 14, poffs[i][1] - 10, 30, 14, EV_POFF, ACTOR_ITEM);
+ if (p) p->flags = OF_TALK | OF_SOLID, p->arg = (int16_t)i;
+ }
+ obj_sign(10600, 1186, 18, 20, "* (Woof.)");
+ if (!flag(F_GREATERDOG)) obj_trigger(10840, 1200, 30, 110, EV_GREATERDOG, ACTOR_TRIGGER);
+ break;
+ }
+ case ROOM_DANGERBRIDGE:
+ if (!flag(F_GAUNTLET)) {
+ obj_papyrus(11830, 1236, DIR_LEFT);
+ obj_sans(11850, 1200, DIR_DOWN);
+ obj_trigger(11300, 1220, 30, 40, EV_GAUNTLET, ACTOR_TRIGGER);
+ } else {
+ Obj *s = obj_sans(11792, 1214, DIR_DOWN);
+ if (s) s->event = EV_SNOW_SANS_CLIFF, s->arg = 3, s->flags |= OF_TALK;
+ }
+ break;
+ case ROOM_SOUTHCLIFF: {
+ Obj *s = obj_sans(10552, 2312, DIR_DOWN);
+ if (s) s->event = EV_SNOW_SANS_CLIFF, s->arg = 4, s->flags |= OF_TALK;
+ break;
+ }
+ case ROOM_DOORROOM: {
+ Obj *d = obj_trigger(9890, 1636, 40, 54, EV_MYSTERY_DOOR, ACTOR_ITEM);
+ if (d) d->flags = OF_TALK | OF_SOLID;
+ break;
+ }
+ }
+}
+
+void snow2_update(void) {
+ xo_update();
+}
+
+void snow2_draw(void) {
+ /* the ice puzzle's bridge, once solved */
+ if (g_room == ROOM_XOXO3 && flag(F_XOXO3)) gfx_rect(9780 - cam_x, 1214 - cam_y, 60, 24, rgb565(0xCFDBFF), 255);
+ /* the tile puzzle: its colours shuffle, then settle (the map's red and pink) */
+ if (g_room == ROOM_TILEPUZZLE && tiles_t > 0) {
+ static const uint32_t cols[] = {0xFF4040, 0xFFC0C0, 0xFFFF40, 0x40C040, 0xFF8000, 0x4060FF, 0xA040FF};
+ for (int y = 0; y < TILES_H; y++)
+ for (int x = 0; x < TILES_W; x++)
+ gfx_rect(TILES_X + x * 20 - cam_x, TILES_Y + y * 20 - cam_y, 20, 20,
+ rgb565(cols[(x * 7 + y * 3 + tiles_t / 4) % 7]), 255);
+ tiles_t--;
+ } else if (g_room == ROOM_TILEPUZZLE && !flag(F_TILES)) {
+ /* (before the machine: plain grey tiles) */
+ gfx_rect(TILES_X - cam_x, TILES_Y - cam_y, TILES_W * 20, TILES_H * 20, rgb565(0x9090A0), 255);
+ }
+}
+
+/* ---------------------------------------------------------------- the events */
+/* XOXO 2's switch in the tree: then the puzzle's own switch solves it */
+static int ev_tree_switch(Ev *c) {
+ EV_BEGIN;
+ xo_kill = true;
+ NARRATE("* (There's a switch on this tree.)\n* (You press it.)");
+ EV_END;
+}
+
+static int ev_snow_switch(Ev *c) {
+ EV_BEGIN;
+ if (flag(F_SNOWPUZZLE)) {
+ NARRATE("* (The switch is already pressed.)");
+ } else {
+ NARRATE("* (There's a switch under the snow.)\n* (You press it.)");
+ set_flag(F_SNOWPUZZLE, 1);
+ for (int i = 0; i < MAX_OBJS; i++)
+ if (objs[i].name == ACTOR_SPIKES) obj_spikes_set(&objs[i], false), objs[i].flags |= OF_HIDDEN;
+ }
+ EV_END;
+}
+
+static int ev_dogi(Ev *c) {
+ Obj *a = obj_get(ACTOR_DOGAMY), *b = obj_get(ACTOR_DOGARESSA);
+ EV_BEGIN;
+ /* they come sniffing */
+ a = obj_deco(SPR_DOGAMY_OW, 7330, 900, false);
+ if (a) a->name = ACTOR_DOGAMY;
+ b = obj_deco(SPR_DOGARESSA_OW, 7410, 900, false);
+ if (b) b->name = ACTOR_DOGARESSA;
+ a = obj_get(ACTOR_DOGAMY), b = obj_get(ACTOR_DOGARESSA);
+ WALK_TO(a, pl.x - 8, pl.y + 26, 2);
+ WALK(b, pl.x + 30, pl.y + 26, 2);
+ NARRATE("* Sniff... sniff... a strange smell.|* (A strange smell must be... eliminated!)");
+ BATTLE(FORM_DOGI);
+ set_flag(F_DOGI, bt.mon[0].killed || bt.mon[1].killed ? 2 : 1);
+ gone(a), gone(b);
+ if (c->self) gone(c->self);
+ EV_END;
+}
+
+static int ev_pap_xoxo(Ev *c) {
+ Obj *p = obj_get(ACTOR_PAPYRUS);
+ int k = c->self && c->self->kind == OBJ_NPC ? c->self->arg : 0;
+ EV_BEGIN;
+ if (g_room == ROOM_XOXO1) {
+ /* past the first XOXO: the spaghetti */
+ pl.dir = DIR_RIGHT;
+ SAY(FACE_PAPYRUS, "`0* HUMAN!!! YOU SOLVED IT!\n* BUT... WHAT ABOUT THE SPAGHETTI?|* DID YOU EAT IT? OR LEAVE IT ALONE?@Ate it@Left it");
+ if (CHOICE() == 0) SAY(FACE_PAPYRUS, "`2* WOWIE!!! YOU ATE MY SPAGHETTI!!!|* IT MUST HAVE BEEN DELICIOUS!\n* NYEH HEH HEH!");
+ else SAY(FACE_PAPYRUS, "`4* YOU LEFT IT ALONE?|`0* SO THERE'S STILL SOME LEFT FOR ME?\n`2* NYEH HEH HEH!");
+ set_flag(F_PAP_XOXO1, 1);
+ WALK(p, 7740, 1252, 6);
+ gone(p);
+ if (c->self) gone(c->self);
+ return 1;
+ }
+ /* the second XOXO: his face, frozen in the snow */
+ if (k == 2) {
+ SAY(FACE_PAPYRUS, "`0* NEED HELP WITH THE PUZZLE?@Yes@No");
+ if (CHOICE() == 0) SAY(FACE_PAPYRUS, "`4* WELL... THERE MAY OR MAY NOT BE A SWITCH ON THAT TREE OVER THERE.");
+ else SAY(FACE_PAPYRUS, "`2* GOOD! A HUMAN WHO SOLVES PUZZLES!\n* NYEH HEH HEH!");
+ return 1;
+ }
+ pl.dir = DIR_RIGHT;
+ SAY(FACE_PAPYRUS, "`4* SO... WHILE I WAITED, I REDID THE SNOW TO LOOK LIKE MY FACE.|* THEN IT FROZE. THE PUZZLE CHANGED.\n`0* ASK ME IF YOU NEED A HINT!");
+ set_flag(F_PAP_XOXO1, 2);
+ if (c->self) gone(c->self);
+ EV_END;
+}
+
+static int ev_tiles(Ev *c) {
+ Obj *p = obj_get(ACTOR_PAPYRUS);
+ EV_BEGIN;
+ pl.dir = DIR_RIGHT;
+ SAY(FACE_PAPYRUS, "`0* HUMAN!!! THE MULTICOLOR\n TILE PUZZLE!|* ONE PULL, AND THE TILES\n CHANGE COLOR!|* EVERY COLOR DOES SOMETHING\n DIFFERENT!|* RED: WALL. YELLOW: ZAP.\n* GREEN: MONSTER.\n* PINK: NOTHING.|* ORANGE, BLUE AND PURPLE... IT GETS COMPLICATED.|`2* GOT ALL THAT???@Yes@No");
+ if (CHOICE() == 1)
+ SAY(FACE_PAPYRUS, "`3* OKAY! AGAIN!\n* PINK TILES ARE... WAIT, NO...|`4* ...|`0* JUST TRY IT ON YOUR OWN!");
+ SAY(FACE_PAPYRUS, "`2* LET'S DO IT!!!");
+ /* the machine: the colours shuffle, then settle */
+ tiles_t = 90;
+ WAIT(95);
+ tiles_t = 0;
+ SAY(FACE_PAPYRUS, "`4* ...");
+ set_flag(F_TILES, 1);
+ for (int i = 0; i < MAX_OBJS; i++)
+ if (objs[i].name == ACTOR_BLOCK) objs[i].kind = OBJ_NONE;
+ /* he spins out of the room */
+ WALK(p, 8920, 1252, 8);
+ gone(p);
+ if (c->self) gone(c->self);
+ EV_END;
+}
+
+static int ev_xoxo_fall(Ev *c) {
+ EV_BEGIN;
+ /* off the ice's edge: down to the room under it */
+ FADE_TO(ROOM_XOXOFAIL, 9600, 1460, DIR_DOWN);
+ EV_END;
+}
+
+static int ev_poff(Ev *c) {
+ int k = c->self ? c->self->arg : 0;
+ EV_BEGIN;
+ if (k == 8 && !flag(F_POFF_GOLD)) {
+ set_flag(F_POFF_GOLD, 1);
+ sv.gold = (uint16_t)min_i(9999, sv.gold + 30);
+ NARRATE("* (It's a snow poff.)|* (There's 30G inside!)");
+ } else {
+ NARRATE("* (It's a snow poff.)");
+ }
+ EV_END;
+}
+
+static int ev_greaterdog(Ev *c) {
+ Obj *d = obj_get(ACTOR_GREATERDOG);
+ EV_BEGIN;
+ /* out of a poff it jumps */
+ d = obj_deco(SPR_GREATERDOG_OW, 10900, 1280, false);
+ if (d) d->name = ACTOR_GREATERDOG;
+ g_shake = 6;
+ WAIT(20);
+ BATTLE(FORM_GREATERDOG);
+ set_flag(F_GREATERDOG, bt.mon[0].killed ? 2 : 1);
+ set_flag(F_SNOWDIN_BOSSES, flag(F_SNOWDIN_BOSSES) + 1);
+ gone(obj_get(ACTOR_GREATERDOG));
+ if (c->self) gone(c->self);
+ EV_END;
+}
+
+static int ev_gauntlet(Ev *c) {
+ Obj *p = obj_get(ACTOR_PAPYRUS), *s = obj_get(ACTOR_SANS);
+ EV_BEGIN;
+ pl.dir = DIR_RIGHT;
+ SAY(FACE_PAPYRUS, "`0* HUMAN!!!\n* THIS IS YOUR FINAL AND MOST DANGEROUS CHALLENGE!!!|`2* BEHOLD!\n* THE GAUNTLET OF DEADLY TERROR!");
+ /* the gauntlet comes down: a spear, a spiked ball, a dog on a rope, a fire, a cannon */
+ g_shake = 8;
+ WAIT(40);
+ SAY(FACE_PAPYRUS, "`0* ONE WORD FROM ME AND IT ALL SWINGS INTO ACTION!!!|`3* HERE IT GOES!|`4* ...|* ANY MOMENT NOW...");
+ SAY(FACE_SANS, "* well? what's the holdup?");
+ SAY(FACE_PAPYRUS, "`3* I'M GETTING TO IT!!!|`4* ...ACTUALLY, IT'S TOO EASY.\n* NOT FAIR AT ALL.|`2* A SKELETON HAS STANDARDS!\n* NYEH HEH HEH!!!");
+ WALK(p, 12000, 1236, 6);
+ gone(p);
+ SAY(FACE_SANS, "* ...|* i don't know what my brother's going to do.|* if i were you, i would keep going.");
+ WALK(s, 12000, 1200, 2);
+ gone(s);
+ set_flag(F_GAUNTLET, 1);
+ if (c->self) gone(c->self);
+ EV_END;
+}
+
+static int ev_sans_cliff(Ev *c) {
+ int k = c->self ? c->self->arg : 0;
+ EV_BEGIN;
+ switch (k) {
+ case 1: SAY(FACE_SANS, "* hey, you could have asked\n me for help.|`2* i know a thing or two\n about trees."); break;
+ case 2: SAY(FACE_SANS, "* his cooking's getting\n better.|* give it a year. it might\n even be food."); break;
+ case 3:
+ if (!flag(F_SNOW_SANS_TALK)) {
+ set_flag(F_SNOW_SANS_TALK, 1);
+ SAY(FACE_SANS, "* hey, buddy.\n* my brother's up ahead.|* remember: if you see a #Lblue#X attack, don't move.");
+ } else {
+ SAY(FACE_SANS, "* blue stop sign. got it?");
+ }
+ break;
+ case 4: SAY(FACE_SANS, "* hey.\n* nice view down here, huh?"); break;
+ }
+ EV_END;
+}
+
+static int ev_mystery_door(Ev *c) {
+ EV_BEGIN;
+ NARRATE("* (It's locked.)");
+ EV_END;
+}
+
+int snow2_event(Ev *c) {
+ switch (c->id) {
+ case EV_XOXO_SWITCH: return c->self && c->self->name == ACTOR_TRIGGER3 ? ev_tree_switch(c) : ev_xoxo_switch(c);
+ case EV_SNOW_SWITCH: return ev_snow_switch(c);
+ case EV_DOGI: return ev_dogi(c);
+ case EV_PAP_XOXO: return ev_pap_xoxo(c);
+ case EV_TILES: return ev_tiles(c);
+ case EV_XOXO_FALL: return ev_xoxo_fall(c);
+ case EV_POFF: return ev_poff(c);
+ case EV_GREATERDOG: return ev_greaterdog(c);
+ case EV_GAUNTLET: return ev_gauntlet(c);
+ case EV_SNOW_SANS_CLIFF: return ev_sans_cliff(c);
+ case EV_MYSTERY_DOOR: return ev_mystery_door(c);
+ }
+ return -1;
+}
diff --git a/games/undertale/src/snowmon.c b/games/undertale/src/snowmon.c
new file mode 100644
index 00000000..f6d7e01c
--- /dev/null
+++ b/games/undertale/src/snowmon.c
@@ -0,0 +1,902 @@
+/* Snowdin's monsters: stats, ACTs and texts (from the Undertale Wiki's records of the game), their formations,
+ * where they come out, and their attacks, as the wiki describes them. Papyrus is in papyrus.c. */
+#include
+#include
+#include "battle.h"
+#include "game.h"
+#include "gfx.h"
+#include "items.h"
+#include "monsters.h"
+#include "res.h"
+#include "snowdin.h"
+#include "world.h"
+
+#define PICK(arr) ((arr)[rnd_n((int)(sizeof(arr) / sizeof((arr)[0])))])
+
+/* whether the player ACTed on the monster this turn (else it's a "neutral" turn: Snowdrake tells a pun) */
+static bool acted_on(const Monster *m) { return m->state2 & 0x100; }
+static void mark_acted(Monster *m) { m->state2 = (int16_t)(m->state2 | 0x100); }
+static void clear_acted(Monster *m) { m->state2 = (int16_t)(m->state2 & ~0x100); }
+
+/* the drakes and Jerry: a joke makes the others laugh, and they can be spared */
+static void others_laugh(Battle *b, Monster *self) {
+ for (int i = 0; i < b->nmon; i++) {
+ Monster *o = &b->mon[i];
+ if (o == self || !o->alive || o->gone) continue;
+ o->spare = true, o->state = 9;
+ }
+}
+static bool others_here(Battle *b, Monster *self) {
+ for (int i = 0; i < b->nmon; i++)
+ if (&b->mon[i] != self && b->mon[i].alive && !b->mon[i].gone) return true;
+ return false;
+}
+
+/* ---------------------------------------------------------------- Snowdrake, Chilldrake */
+static void act_snowdrake(Battle *b, Monster *m, int act, char *t, int cap) {
+ mark_acted(m);
+ if (act == 0) { /* Heckle */
+ if (rnd_n(6) == 0) {
+ snprintf(t, (size_t)cap, "* You tell Snowdrake their jokes will never land.\n* Crushed, they slink away...");
+ m->gone = true, m->fade = 1;
+ } else {
+ snprintf(t, (size_t)cap, "* You boo Snowdrake.");
+ m->state = 3;
+ }
+ } else if (act == 1) { /* Laugh */
+ snprintf(t, (size_t)cap, m->state == 1 ? "* You laugh at Snowdrake's pun." : "* You laugh at nothing in particular.");
+ if (m->state == 1) m->spare = true, m->state = 2;
+ else m->state = 4;
+ } else { /* Joke */
+ if (others_here(b, m)) {
+ snprintf(t, (size_t)cap, "* You make an ice pun.\n* The other monsters think it's hilarious...");
+ others_laugh(b, m);
+ m->state = 5;
+ } else {
+ snprintf(t, (size_t)cap, "* You make an ice pun.\n* Snowdrake isn't impressed.");
+ m->state = 6;
+ }
+ }
+}
+static const char *talk_snowdrake(Battle *b, Monster *m) {
+ int s = m->state;
+ bool acted = acted_on(m);
+ clear_acted(m);
+ if (s == 2) { m->state = 7; return "See!? Laughs!\nDad was\nwrong!"; }
+ if (s == 3) { m->state = 0; return "THIS won't\nbe funny\neither!"; }
+ if (s == 4) { m->state = 0; return "What are YOU\nlaughin' at?!?"; }
+ if (s == 5) { m->state = 0; return "That's not\nlike me..."; }
+ if (s == 6) { m->state = 0; return PICK(((const char *const[]){"Is that s'posed\nto be funny?", "Ha... Ha...\nNice try.", "I've heard\nthat one."})); }
+ if (s == 7) return "Thanks, you're\nall great.";
+ /* a neutral turn: his next pun (laugh at it to spare him) */
+ if (!acted || s == 1) m->state = 1;
+ static const char *const puns[] = {"\"Ice\" to\nmeet you.", "Ice puns are\n\"snow\" problem", "Fights you in\n\"cold\" blood.",
+ "Better not\nsnow \"flake\"\nout!", "My fave ice\ncereals:\n\"frosted\"", "\"Chill\"\nout...",
+ "M.. m..\nmacaroni and\n\"freeze\""};
+ return PICK(puns);
+}
+static int attack_drake(Battle *b, Monster *m) { (void)b, (void)m; return rnd_n(2) ? ATK_DRAKE_ARCS : ATK_DRAKE_LINES; }
+
+static void act_chilldrake(Battle *b, Monster *m, int act, char *t, int cap) {
+ mark_acted(m);
+ if (act == 0) { /* Clash */
+ snprintf(t, (size_t)cap, "* You tell Chilldrake it's all wrong.");
+ m->state = 3;
+ } else if (act == 1) { /* Agree */
+ if (m->state == 1) {
+ snprintf(t, (size_t)cap, "* You agree with Chilldrake.\n* It seems to get very uncomfortable...");
+ m->spare = true, m->state = 2;
+ } else {
+ snprintf(t, (size_t)cap, "* You agree with Chilldrake before it gives its speech.");
+ m->state = 4;
+ }
+ } else { /* Joke */
+ if (others_here(b, m)) {
+ snprintf(t, (size_t)cap, "* You make fun of Chilldrake.\n* The other monsters think it's hilarious...");
+ others_laugh(b, m);
+ } else {
+ snprintf(t, (size_t)cap, "* You make fun of Chilldrake.");
+ }
+ m->state = 5;
+ }
+}
+static const char *talk_chilldrake(Battle *b, Monster *m) {
+ int s = m->state;
+ bool acted = acted_on(m);
+ clear_acted(m);
+ (void)b;
+ if (s == 2) { m->state = 6; return "To tell you\nthe truth..."; }
+ if (s == 3) { m->state = 0; return "WRONG! Let's\nfight and die."; }
+ if (s == 4) { m->state = 0; return "You don't\nknow my\ncause!"; }
+ if (s == 5) { m->state = 0; return "Only Snowy\ncan do that."; }
+ if (s == 6) return "Do you...\nreally...";
+ if (!acted || s == 1) m->state = 1;
+ static const char *const q[] = {"Brush my\nteeth? No way\nin heck!", "NEVER do your\nhomework !!", "No bedtime!\nOnly DEADTIME",
+ "*turns music\nup all the\nway*"};
+ return PICK(q);
+}
+
+/* ---------------------------------------------------------------- Ice Cap */
+static void act_icecap(Battle *b, Monster *m, int act, char *t, int cap) {
+ (void)b;
+ mark_acted(m);
+ if (m->spr_override) { /* "Ice" now */
+ snprintf(t, (size_t)cap, act == 0 ? "* You inform Ice Cap that it still looks fine..." : "* ...");
+ return;
+ }
+ if (act == 0) { /* Compliment */
+ snprintf(t, (size_t)cap, "* You inform Ice Cap that it has a great hat!");
+ m->state = 1;
+ } else if (act == 1) { /* Ignore */
+ m->state2 = (int16_t)(m->state2 + 1);
+ if ((m->state2 & 0xFF) >= 2) {
+ snprintf(t, (size_t)cap, "* You continue not looking at Ice Cap's hat.\n* It seems defeated...");
+ m->spare = true;
+ } else {
+ snprintf(t, (size_t)cap, "* You manage to tear your eyes away from Ice Cap's hat.\n* It looks annoyed...");
+ }
+ m->state = 2;
+ } else { /* Steal */
+ if (m->spare) {
+ snprintf(t, (size_t)cap, "* You tried to steal Ice Cap's hat...\n* And succeeded!\n* (It melts in your hands...)");
+ m->spr_override = SPR_ICECAP_HATLESS;
+ m->exp = 25, m->gold = 40, m->spare_gold = 35, m->at = 1, m->df = -100;
+ } else {
+ snprintf(t, (size_t)cap, "* You tried to steal Ice Cap's hat...\n* ... but it's not weakened enough!");
+ m->state = 3;
+ }
+ }
+}
+static const char *talk_icecap(Battle *b, Monster *m) {
+ (void)b;
+ clear_acted(m);
+ if (m->spr_override) return "I... I...";
+ int s = m->state;
+ m->state = 0;
+ if (s == 9) return "Haha! That was\nACTUALLY\nfunny!";
+ if (s == 1) return PICK(((const char *const[]){"DUH! Who\nDOESN'T know?", "Envious? TOO\nBAD!", "My hat's too\nloud for me to\nhear you."}));
+ if (s == 2) return (m->state2 & 0xFF) >= 2 ? "Fine!!! I\ndon't care!!!" : "HELLO??? My\nhat's up here.";
+ if (s == 3) return "I KNEW IT!!!\nTHIEF!!";
+ static const char *const q[] = {"Where's YOUR\nhat?", "Your head\nlooks so\n...NAKED", "What a great\nhat! (Mine)",
+ "Snow? No! It's\nhat residue.", "I just looove\nmy hat. OK?"};
+ return PICK(q);
+}
+static int attack_icecap(Battle *b, Monster *m) {
+ (void)b;
+ return m->spr_override ? ATK_IDLE : rnd_n(2) ? ATK_ICECAP_HATS : ATK_ICECAP_WAVES;
+}
+static const char *flavor_icecap(Battle *b, Monster *m) {
+ (void)b;
+ if (m->spr_override) return "* 'Ice Cap' is no more.";
+ if (m->spare) return "* Ice Cap is desperate for attention.";
+ return NULL;
+}
+
+/* ---------------------------------------------------------------- Jerry */
+static void act_jerry(Battle *b, Monster *m, int act, char *t, int cap) {
+ (void)act;
+ snprintf(t, (size_t)cap, "* You and the other monsters ditch Jerry when it looks away!");
+ m->gone = true, m->fade = 1, m->spare_gold = 0;
+ b->state[7] = 1;
+}
+static const char *talk_jerry(Battle *b, Monster *m) {
+ if (m->state == 9) { m->state = 0; return "Hahaha! Say\nsomething else\nfunny!"; }
+ if (!others_here(b, m)) return PICK(((const char *const[]){"Well... can YOU\ngive me a ride\nhome?", "Awkwarrrd.", "The wi-fi here\nsucks."}));
+ static const char *const q[] = {"Guys, it's\nCOLD. Does\nANYONE care?", "Why are we\ndoing this?\nWhat a fail.",
+ "Wow, you guys\nSUCK at this.", "Ka-SIGH."};
+ return PICK(q);
+}
+static bool hit_jerry(Battle *b, Monster *m, int dmg) {
+ (void)b;
+ /* it takes a fifth of every hit */
+ m->hp = (int16_t)(m->hp - (dmg + 4) / 5);
+ m->hurt = 30;
+ if (m->hp <= 0) m->hp = 0, m->killed = true;
+ return true;
+}
+
+/* ---------------------------------------------------------------- Gyftrot */
+static const char *const gyft_decor[] = {
+ "* You remove the striped cane that says \"I use this tiny cane to walk\" on it.",
+ "* You remove the box of non-dog-related raisins.",
+ "* You remove the lenticular bookmark of a smug teen winking.",
+ "* You remove the barbed wire made of pipe cleaners.",
+ "* You remove a small, confused dog.",
+ "* You remove a stocking filled with chicken nuggets."};
+static void act_gyftrot(Battle *b, Monster *m, int act, char *t, int cap) {
+ (void)b;
+ mark_acted(m);
+ /* state: decorations left (3 at first, kept as 3 - state2 & 7), state: what it says next */
+ int left = 3 - (m->state2 & 7);
+ if (act == 0) { /* Decorate */
+ snprintf(t, (size_t)cap, "* You add some googly eyes you found on the ground.");
+ m->state = left == 0 ? 5 : 1;
+ if (left == 0) m->state2 = (int16_t)(m->state2 | 0x10); /* decorated again: no gift now */
+ } else if (act == 1) { /* Undecorate */
+ if (left > 0) {
+ snprintf(t, (size_t)cap, "%s", gyft_decor[(m->state2 & 7) + rnd_n(3)]);
+ m->state2 = (int16_t)(m->state2 + 1);
+ m->state = left == 1 ? 3 : 2;
+ if (left == 1) m->spare = true;
+ } else {
+ snprintf(t, (size_t)cap, "* You try to undecorate...?");
+ m->state = 4;
+ }
+ } else { /* Gift */
+ if (left > 0 || (m->state2 & 0x10)) {
+ snprintf(t, (size_t)cap, "* Gyftrot refuses your gift.");
+ m->state = left > 0 ? 6 : 7;
+ } else {
+ snprintf(t, (size_t)cap, "* You give Gyftrot a gift.");
+ m->spare_gold = 75, m->state = 8;
+ }
+ }
+}
+static const char *talk_gyftrot(Battle *b, Monster *m) {
+ (void)b;
+ clear_acted(m);
+ int s = m->state;
+ m->state = 0;
+ switch (s) {
+ case 1: return "GOSHDARN\nTEENAGE GOOGLY\nEYES!!!";
+ case 2: return "That's a\nlittle better.";
+ case 3: return "A weight has\nbeen lifted.";
+ case 4: return "Er... Those\nare real.";
+ case 5: return "I WAS STARTING\nTO TRUST YOU!!!";
+ case 6: return "How do I know\nit's not a\ntrick?";
+ case 7: return "I don't want\nyour gift!";
+ case 8: return "Aw, you\nshouldn'\nhave...";
+ }
+ if (m->spare) return "Thanks.";
+ static const char *const q[] = {"Is this funny\nto you?", "Don't put any\nmore on me!", "Leave me\nalone...",
+ "Hohoho! Go\nahead and\nlaugh...", "Get this off\nof me..."};
+ return PICK(q);
+}
+static int attack_gyftrot(Battle *b, Monster *m) { (void)b, (void)m; return rnd_n(2) ? ATK_GYFT_PRESENTS : ATK_GYFT_SNOW; }
+
+/* ---------------------------------------------------------------- the dogs: the Stick spares them */
+static bool stick_dog(Battle *b, Monster *m, int item, char *t, int cap) {
+ if (item != ITEM_STICK) return false;
+ bool pair = m->def == mon_def(MON_DOGAMY) || m->def == mon_def(MON_DOGARESSA);
+ snprintf(t, (size_t)cap, pair ? "* You threw the stick and the dogs ran to get it.\n* You played fetch for a while."
+ : "* You threw the stick and the dog ran to get it.\n* You played fetch for a while.");
+ for (int i = 0; i < b->nmon; i++)
+ if (b->mon[i].alive && !b->mon[i].gone) b->mon[i].spare = true, b->mon[i].state = 20;
+ if (m->def == mon_def(MON_DOGGO)) snow_doggo_stick();
+ return true;
+}
+
+/* Doggo: he can be pet once he's survived an attack without seeing you move */
+static void act_doggo(Battle *b, Monster *m, int act, char *t, int cap) {
+ (void)act;
+ bool calm = b->turn >= 1 && sv.hp >= m->state2;
+ if (!calm && !m->spare) {
+ snprintf(t, (size_t)cap, "* Doggo is too suspicious of your movements.");
+ m->state = 1;
+ return;
+ }
+ m->acts[5]++;
+ snprintf(t, (size_t)cap, "* You pet Doggo.");
+ m->spare = true, m->state = 2;
+ if (m->acts[5] >= 5) m->df = 0;
+}
+static const char *talk_doggo(Battle *b, Monster *m) {
+ int s = m->state;
+ m->state = 0;
+ if (s == 20) return "HUH!!! A FUN\nSTICK APPEARS!";
+ if (s == 2) {
+ switch (m->acts[5]) {
+ case 1: return "WHAT!!! I'VE\nBEEN PET!!!|PET?|PAT?|POT?";
+ case 2: return "WHERE'S THAT\nCOMING FROM!";
+ case 3: return "THERE'S NO END\nTO IT!!";
+ case 4: return "WELL, THIS IS\nTHOROUGH !!!";
+ default: return "OK. That's\nenough.";
+ }
+ }
+ if (b->turn == 0) return "Don't move an\ninch!";
+ if (sv.hp < m->state2) return "MOVING?";
+ return "Will it move\nthis time?";
+}
+static int attack_doggo(Battle *b, Monster *m) {
+ (void)b;
+ m->state2 = sv.hp; /* (unhurt by this attack: calm enough to pet) */
+ return ATK_DOGGO_SWORD;
+}
+static const char *flavor_doggo(Battle *b, Monster *m) {
+ if (m->spare) return "* Doggo has been pet.";
+ if (b->turn >= 1 && sv.hp >= m->state2) return "* Doggo can't seem to find anything.";
+ return NULL;
+}
+
+/* Lesser Dog: a pet spares it; more pets raise its head ever higher */
+static void act_lesserdog(Battle *b, Monster *m, int act, char *t, int cap) {
+ (void)b, (void)act;
+ static const char *const pet[] = {
+ "* You barely lifted your hand and Lesser Dog got excited.",
+ "* You lightly touched the Dog.\n* It's already overexcited...",
+ "* You pet the Dog.\n* It raises its head up to meet your hand.",
+ "* You pet the Dog.\n* It was a good Dog.",
+ "* You pet the Dog.\n* Its excitement knows no bounds.",
+ "* Critical pet!\n* Dog excitement increased.",
+ "* You have to jump up to pet the Dog.",
+ "* You don't even pet it.\n* It gets more excited.",
+ "* There is no way to stop this madness.",
+ "* Lesser Dog enters the realm of the clouds.",
+ "* You call the Dog but it is too late.\n* It cannot hear you.",
+ "* ...",
+ "* You can reach Lesser Dog again."};
+ int n = m->acts[5]++;
+ if (n < 13) snprintf(t, (size_t)cap, "%s", pet[n]);
+ else if (n < 19) snprintf(t, (size_t)cap, "* You pet Lesser Dog.");
+ else if (n < 33) snprintf(t, (size_t)cap, "* It's possible that you may have a problem.");
+ else if (n < 44) snprintf(t, (size_t)cap, "* Lesser Dog is unpettable but appreciates the attempt.");
+ else snprintf(t, (size_t)cap, "* Perhaps mankind was not meant to pet this much.");
+ m->spare = true;
+ m->state = 1;
+}
+static const char *talk_lesserdog(Battle *b, Monster *m) {
+ (void)b;
+ int n = m->acts[5];
+ if (m->state == 20) { m->state = 0; return "(Wag wag)"; }
+ if (m->state == 1) {
+ m->state = 0;
+ if (n <= 3) return "(Pant! Pant!)";
+ if (n <= 6) return "(Excited\nnoises)";
+ static const char *const q[] = {"(Motor\nrevving)", "(Plane\ntakeoff)", "(Kettle\nwhistle)", "(...)", "(Faraway\nbark)", "(...)", "(Bark)"};
+ if (n <= 13) return q[n - 7];
+ return "(Pant pant)";
+ }
+ static const char *const q[] = {"(Pant pant)", "(Wag wag)", "(Thinks of\nfood)", "(Tiny bark)"};
+ return PICK(q);
+}
+static int attack_lesserdog(Battle *b, Monster *m) {
+ (void)b;
+ if (m->acts[5] >= 6) return ATK_IDLE; /* (a critical pet: too excited to attack) */
+ return rnd_n(2) ? ATK_LDOG_JUMP : ATK_LDOG_SPEARS;
+}
+static void draw_lesserdog(Battle *b, Monster *m, int x, int y) {
+ (void)b;
+ /* each pet lifts its head: its neck grows */
+ int neck = min_i(m->acts[5], 13) * 7;
+ if (m->acts[5] >= 34) neck = 0;
+ if (neck) gfx_rect(x - 4, y - 46 - neck, 9, neck + 4, C_WHITE, 255);
+ gfx_sprite(m->hurt ? SPR_LESSERDOG_HURT : SPR_LESSERDOG_B, m->hurt ? 0 : (int)(g_frame / 6), x, y - neck * 0, 0, 255);
+}
+
+/* the Dogi: roll in the snow, let them re-sniff you, then pet them both */
+static void act_dogi(Battle *b, Monster *m, int act, char *t, int cap) {
+ bool husband = m->def == mon_def(MON_DOGAMY);
+ /* b->state[0]: rolled around, [1]: re-sniffed after, [2], [3]: Dogamy, Dogaressa pet */
+ if (act == 0) { /* Pet */
+ if (!b->state[1]) {
+ snprintf(t, (size_t)cap, husband ? "* The Dogs are too suspicious of your smell." : "* The Dogaressa is too suspicious of your smell.");
+ } else {
+ snprintf(t, (size_t)cap, husband ? "* You pet Dogamy." : "* You pet the Dogaressa.");
+ b->state[husband ? 2 : 3] = 1;
+ m->state = 1;
+ /* both pet: their minds expanded */
+ if (b->state[2] && b->state[3])
+ for (int i = 0; i < b->nmon; i++) b->mon[i].spare = true;
+ else if (count_alive() == 1)
+ m->spare = true;
+ }
+ } else if (act == 1) { /* Re-sniff */
+ if (b->state[1]) snprintf(t, (size_t)cap, "* The Dogs already know you smell fine.");
+ else if (b->state[0]) snprintf(t, (size_t)cap, "* The Dogs sniff you again...\n* After rolling in the dirt, you smell all right!"), b->state[1] = 1;
+ else snprintf(t, (size_t)cap, "* The Dogs sniff you again...\n* But you smell just as weird as before!");
+ } else { /* Roll Around */
+ snprintf(t, (size_t)cap, "* You roll around in the dirt and snow.\n* You smell like a weird puppy.");
+ b->state[0] = 1;
+ }
+}
+static const char *talk_dogamy(Battle *b, Monster *m) {
+ if (count_alive() == 1) return "...";
+ if (b->state[2] && b->state[3]) return "Dogs can pet\nother dogs???";
+ if (m->state == 20) return "A stick!";
+ if (b->state[1]) return "Smells like a\nlittle puppy!";
+ if (b->state[0]) return "Hmm? Huh?";
+ static const char *const q[] = {"Hmm? Is that\na...", "Huh? What's\nthat smell?", "Don't touch\nmy wife!"};
+ return PICK(q);
+}
+static const char *talk_dogaressa(Battle *b, Monster *m) {
+ if (count_alive() == 1) return "...";
+ if (b->state[2] && b->state[3]) return "A new world\n... !";
+ if (m->state == 20) return "Fetch!!!";
+ if (b->state[1]) return "(A puppy?)";
+ static const char *const q[] = {"(I smell it\ntoo...)", "(Hands off\nmy hubby!)", "(Weird smell...)"};
+ return PICK(q);
+}
+static int attack_dogi(Battle *b, Monster *m) {
+ /* alone, each has its own: Dogamy's sad hearts, Dogaressa's angry axes; together they take turns */
+ if (count_alive() == 1) return m->def == mon_def(MON_DOGAMY) ? ATK_DOGI_HEARTS : ATK_DOGI_AXES;
+ return b->turn % 2 ? ATK_DOGI_HEARTS : ATK_DOGI_AXES;
+}
+
+/* Greater Dog: call it (or ignore it), pet it, play with it, pet it twice */
+static void act_greaterdog(Battle *b, Monster *m, int act, char *t, int cap) {
+ (void)b;
+ int s = m->state & 0xFF;
+ if (act == 0) { /* Pet */
+ if (s == 1) {
+ snprintf(t, (size_t)cap, "* Greater Dog flops into your lap for pets and dozes off...|* ...then bolts awake, thrilled!");
+ m->state = 2, m->df = -20;
+ } else if (s == 3) {
+ snprintf(t, (size_t)cap, "* You pet the dog. It leans all its weight on you.\n* More pets are needed...!");
+ m->state = 4;
+ } else if (s == 4) {
+ snprintf(t, (size_t)cap, "* One last, perfect pet.\n* The dog rolls over, completely satisfied.");
+ m->state = 5, m->spare = true;
+ } else if (s == 5) {
+ snprintf(t, (size_t)cap, "* Tummy rubs are forbidden.");
+ } else if (s == 2) {
+ snprintf(t, (size_t)cap, "* Greater Dog's excitement is creating a power field that prevents petting.");
+ } else {
+ snprintf(t, (size_t)cap, "* Greater Dog is too far away to pet.\n* You just pet the air.");
+ }
+ } else if (act == 1) { /* Beckon */
+ if (s == 0) snprintf(t, (size_t)cap, "* You call the Greater Dog.\n* It bounds towards you, flecking slobber into your face."), m->state = 1;
+ else snprintf(t, (size_t)cap, "* The Greater Dog's ears perk up.\n* Nothing else happens.");
+ } else if (act == 2) { /* Play */
+ if (s == 2) {
+ snprintf(t, (size_t)cap, "* You toss a snowball. It falls apart.|* Greater Dog fetches ALL the snow anyway.\n* Worn out, it rests its head on you...");
+ m->state = 3;
+ } else if (s >= 3) {
+ snprintf(t, (size_t)cap, "* Greater Dog is too tired to play.");
+ } else {
+ snprintf(t, (size_t)cap, "* Greater Dog is not excited enough to play with.");
+ }
+ } else { /* Ignore */
+ m->state2 = (int16_t)(m->state2 + 1);
+ if (m->state2 >= 4) {
+ snprintf(t, (size_t)cap, "* Greater Dog decides you are too boring.");
+ m->gone = true, m->fade = 1;
+ } else {
+ snprintf(t, (size_t)cap, "* Greater Dog inches closer.");
+ if (s == 0) m->state = 1;
+ }
+ }
+}
+static const char *flavor_greaterdog(Battle *b, Monster *m) {
+ (void)b;
+ switch (m->state & 0xFF) {
+ case 1: return "* Greater Dog just wants affection.";
+ case 2: return "* Greater Dog is patting the ground with its front paws.";
+ case 3: return "* Greater Dog wants some TLC.";
+ case 4: return "* Pet capacity is 40-percent.";
+ case 5: return "* Greater Dog is contented.";
+ }
+ return NULL;
+}
+static int attack_greaterdog(Battle *b, Monster *m) { (void)b, (void)m; return rnd_n(2) ? ATK_GDOG_SPEAR : ATK_GDOG_BARK; }
+
+/* ---------------------------------------------------------------- Glyde */
+static void act_glyde(Battle *b, Monster *m, int act, char *t, int cap) {
+ (void)b;
+ /* state: applause (to 2), state2: nothings after them */
+ if (act == 0) {
+ m->state++;
+ snprintf(t, (size_t)cap, m->state == 1 ? "* You clap really sloppily.\n* Glyde sucks up your praise like a vacuum cleaner."
+ : "* You clap like a gorilla.\n* Glyde is becoming addicted to your praise.");
+ m->state2 = 0;
+ } else if (act == 1) {
+ if (m->state >= 2 && m->state2 >= 2) {
+ snprintf(t, (size_t)cap, "* You boo loudly.\n* Glyde leaves to look elsewhere for praise.");
+ m->gone = true, m->fade = 1;
+ } else {
+ snprintf(t, (size_t)cap, "* You boo... but haters only make Glyde stronger.\n* Glyde ATTACK UP+DEFENSE DOWN.");
+ m->at = (int16_t)(m->at + 2), m->df = (int16_t)(m->df - 10);
+ }
+ } else {
+ if (m->state < 2) {
+ snprintf(t, (size_t)cap, "* You do nothing.\n* No effect.");
+ } else {
+ m->state2++;
+ if (m->state2 >= 3) snprintf(t, (size_t)cap, "* You do nothing.\n* Glyde leaves to look elsewhere for praise."), m->gone = true, m->fade = 1;
+ else if (m->state2 == 2) snprintf(t, (size_t)cap, "* You do nothing.\n* Glyde looks desperate for attention.");
+ else snprintf(t, (size_t)cap, "* You do nothing.\n* Glyde looks disappointed you aren't paying attention.");
+ }
+ }
+}
+static const char *talk_glyde(Battle *b, Monster *m) {
+ (void)b;
+ if (m->state == 1 && m->state2 == 0) return "OK! I rule!\nI admit it!";
+ if (m->state >= 2 && m->state2 == 0) return "What else do\nyou have to\nsay!?";
+ if (m->state >= 2 && m->state2 == 1) return "Eh? You forgot\nto clap.";
+ if (m->state >= 2 && m->state2 == 2) return "HELLO?? I'M\nRIGHT HERE!!!";
+ static const char *const q[] = {"How great\nI am.", "Look. Watch.\nObserve.", "Wow. Check out\nmy pecs.", "Sorry... for\nNOTHING\n*ollies*"};
+ return PICK(q);
+}
+static int attack_glyde(Battle *b, Monster *m) { (void)b, (void)m; return ATK_GLYDE_STARS; }
+
+/* ---------------------------------------------------------------- the table */
+extern const MonDef papyrus_def; /* papyrus.c */
+static const MonDef snow_defs[MON_SNOW_COUNT - MON_RUINS_COUNT] = {
+ [MON_SNOWDRAKE - MON_RUINS_COUNT] = {
+ .name = "Snowdrake", .spare_gold = 12, .hp = 74, .at = 6, .df = 2, .exp = 22, .gold = 18, .spr_idle = SPR_SNOWDRAKE_B,
+ .spr_hurt = SPR_SNOWDRAKE_HURT, .idle_speed = 8, .check = "This teen comedian fights to keep a captive audience.",
+ .acts = {"Heckle", "Laugh", "Joke"}, .nacts = 3,
+ .neutral = {"* Snowdrake is assessing the crowd.", "* Snowdrake is practicing its next pun.",
+ "* Snowdrake is smiling at its own bad joke."},
+ .act = act_snowdrake, .talk = talk_snowdrake, .attack = attack_drake},
+ [MON_ICECAP - MON_RUINS_COUNT] = {
+ .name = "Ice Cap", .spare_gold = 13, .hp = 48, .at = 6, .df = 0, .exp = 17, .gold = 17, .spr_idle = SPR_ICECAP_B,
+ .spr_hurt = SPR_ICECAP_HURT, .idle_speed = 6, .check = "This teen wonders why it isn't named 'Ice Hat.'",
+ .acts = {"Compliment", "Ignore", "Steal"}, .nacts = 3,
+ .neutral = {"* It's snowing dandruff.", "* Ice Cap also wants a hat for its nose.",
+ "* Ice Cap makes sure its hat is still there.", "* Ice Cap is thinking about a certain article of clothing.",
+ "* Here comes that new clothes smell."},
+ .low_hp = "* Ice Cap's hat is loose.", .act = act_icecap, .talk = talk_icecap, .attack = attack_icecap,
+ .flavor = flavor_icecap},
+ [MON_GYFTROT - MON_RUINS_COUNT] = {
+ .name = "Gyftrot", .spare_gold = 20, .hp = 114, .at = 7, .df = 3, .exp = 35, .gold = 30, .spr_idle = SPR_GYFTROT_B,
+ .spr_hurt = SPR_GYFTROT_HURT, .idle_speed = 8, .check = "Some teens \"decorated\" it as a prank.",
+ .acts = {"Decorate", "Undecorate", "Gift"}, .nacts = 3,
+ .neutral = {"* Gyftrot laments its lack of hands.", "* Gyftrot eyes you with suspicion.",
+ "* Gyftrot distrusts your youthful demeanor.", "* Ah, the scent of fresh pine needles.",
+ "* Gyftrot tries vainly to remove its decorations."},
+ .act = act_gyftrot, .talk = talk_gyftrot, .attack = attack_gyftrot},
+ [MON_DOGGO - MON_RUINS_COUNT] = {
+ .name = "Doggo", .spare_gold = 30, .hp = 70, .at = 6, .df = 1, .exp = 30, .gold = 30, .spr_idle = SPR_DOGGO_B, .spr_hurt = SPR_DOGGO_HURT,
+ .idle_speed = 6, .check = "Easily excited by movement. Hobbies include: squirrels.", .acts = {"Pet"}, .nacts = 1,
+ .neutral = {"* Doggo is too suspicious of your movements."},
+ .act = act_doggo, .talk = talk_doggo, .attack = attack_doggo, .flavor = flavor_doggo, .on_item = stick_dog},
+ [MON_LESSERDOG - MON_RUINS_COUNT] = {
+ .name = "Lesser Dog", .spare_gold = 15, .hp = 60, .at = 6, .df = 0, .exp = 18, .gold = 15, .spr_idle = SPR_LESSERDOG_B,
+ .spr_hurt = SPR_LESSERDOG_HURT, .idle_speed = 6, .check = "Wields a stone dogger made of pomer-granite.",
+ .acts = {"Pet", "Pet", "Pet", "Pet", "Pet"}, .nacts = 5,
+ .neutral = {"* Lesser Dog cocks its head to one side.", "* Lesser Dog thinks your weapon is a dog treat.",
+ "* Lesser Dog is really not paying attention.", "* Smells like dog chow."},
+ .act = act_lesserdog, .talk = talk_lesserdog, .attack = attack_lesserdog, .draw = draw_lesserdog,
+ .on_item = stick_dog},
+ [MON_DOGAMY - MON_RUINS_COUNT] = {
+ .name = "Dogamy", .spare_gold = 20, .hp = 108, .at = 6, .df = 4, .exp = 30, .gold = 25, .spr_idle = SPR_DOGAMY_B, .spr_hurt = SPR_DOGAMY_B,
+ .check = "Husband of Dogaressa.\n* Knows only what he smells.", .acts = {"Pet", "Re-sniff", "Roll Around"}, .nacts = 3,
+ .neutral = {"* The Dogs keep shifting their axes to protect each other.", "* The Dogs are re-evaluating your smell.",
+ "* The Dogs are practicing for the next couples contest.",
+ "* The Dogs are saying sickly sweet things to each other."},
+ .act = act_dogi, .talk = talk_dogamy, .attack = attack_dogi, .on_item = stick_dog},
+ [MON_DOGARESSA - MON_RUINS_COUNT] = {
+ .name = "Dogaressa", .spare_gold = 20, .hp = 108, .at = 6, .df = 4, .exp = 30, .gold = 25, .spr_idle = SPR_DOGARESSA_B,
+ .spr_hurt = SPR_DOGARESSA_B, .check = "This dog's wife.\n* Only knows what she smells.", .acts = {"Pet", "Re-sniff", "Roll Around"},
+ .nacts = 3,
+ .neutral = {"* The Dogs keep shifting their axes to protect each other.", "* The Dogs are re-evaluating your smell."},
+ .act = act_dogi, .talk = talk_dogaressa, .attack = attack_dogi, .on_item = stick_dog},
+ [MON_GREATERDOG - MON_RUINS_COUNT] = {
+ .name = "Greater Dog", .spare_gold = 40, .hp = 105, .at = 6, .df = 4, .exp = 80, .gold = 60, .spr_idle = SPR_GREATERDOG_B,
+ .spr_hurt = SPR_GREATERDOG_HURT, .idle_speed = 10, .check = "It's so excited that it thinks fighting is just play.",
+ .acts = {"Pet", "Beckon", "Play", "Ignore"}, .nacts = 4,
+ .neutral = {"* Greater Dog is watching you intently.", "* Greater Dog is waiting for your command.",
+ "* It smells like freshly-squeezed puppy juice.", "* Greater Dog is seeking affection."},
+ .quotes = {"(Bark!)", "(Pant pant)", "(Wag wag)"},
+ .low_hp = "* Greater Dog is panting slowly.", .act = act_greaterdog, .attack = attack_greaterdog,
+ .flavor = flavor_greaterdog, .on_item = stick_dog},
+ [MON_JERRY - MON_RUINS_COUNT] = {
+ .name = "Jerry", .hp = 80, .at = 1, .df = 8, .exp = 1, .gold = 55, .spr_idle = SPR_JERRY_B, .spr_hurt = SPR_JERRY_HURT,
+ .idle_speed = 6, .dy = 10, .check = "Everyone knows Jerry.\n* Makes attacks 2 seconds longer.", .acts = {"Ditch"},
+ .nacts = 1,
+ .neutral = {"* Jerry eats powdery food and licks its hands loudly.", "* Jerry sneezes without covering its nose.",
+ "* Jerry tells everyone it has to go to the bathroom.", "* Jerry lets out a yawn.",
+ "* Smells like....... Jerry."},
+ .low_hp = "* Jerry is wounded.", .act = act_jerry, .talk = talk_jerry, .on_hit = hit_jerry},
+ [MON_CHILLDRAKE - MON_RUINS_COUNT] = {
+ .name = "Chilldrake", .spare_gold = 12, .hp = 74, .at = 6, .df = 2, .exp = 22, .gold = 18, .spr_idle = SPR_CHILLDRAKE_B,
+ .spr_hurt = SPR_CHILLDRAKE_B, .idle_speed = 8, .check = "Rebels against everything!!\n* Looking for its friend Snowy.",
+ .acts = {"Clash", "Agree", "Joke"}, .nacts = 3,
+ .neutral = {"* Chilldrake starts a one-monster riot.", "* Chilldrake is chanting an anarchist spell.",
+ "* Chilldrake is eating its own homework.", "* It smells like \"Ice\" scented body-spray.",
+ "* Chilldrake is wondering where Snowy went."},
+ .act = act_chilldrake, .talk = talk_chilldrake, .attack = attack_drake},
+ [MON_GLYDE - MON_RUINS_COUNT] = {
+ .name = "Glyde", .spare_gold = 120, .hp = 220, .at = 9, .df = -20, .exp = 100, .gold = 140, .spr_idle = SPR_GLYDE_B,
+ .spr_hurt = SPR_GLYDE_HURT, .idle_speed = 6, .dy = -6, .check = "Refuses to give more details about its statistics.",
+ .acts = {"Applaud", "Boo", "Nothing"}, .nacts = 3,
+ .neutral = {"* Glyde does fancy flips.", "* Glyde is giving itself a high five.\n* ... somehow.",
+ "* Glyde sees its reflection and gets jealous.", "* An arrogant-smelling wind blows through."},
+ .act = act_glyde, .talk = talk_glyde, .attack = attack_glyde},
+};
+const MonDef *snow_mon_def(int id) {
+ if (id >= MON_SNOW_COUNT) return wf_mon_def(id);
+ if (id == MON_PAPYRUS) return &papyrus_def;
+ return &snow_defs[id - MON_RUINS_COUNT];
+}
+
+static const Form snow_forms[FORM_SNOW_COUNT - FORM_RUINS_COUNT] = {
+ [FORM_SNOWDRAKE - FORM_RUINS_COUNT] = {{MON_SNOWDRAKE}, 1, "* Snowdrake flutters forth!"},
+ [FORM_ICECAP - FORM_RUINS_COUNT] = {{MON_ICECAP}, 1, "* Icecap struts into view."},
+ [FORM_LESSERDOG - FORM_RUINS_COUNT] = {{MON_LESSERDOG}, 1, "* Lesser Dog appears."},
+ [FORM_CHILLDRAKE - FORM_RUINS_COUNT] = {{MON_CHILLDRAKE}, 1, "* Chilldrake saunters up!"},
+ [FORM_ICECAP_JERRY - FORM_RUINS_COUNT] = {{MON_ICECAP, MON_JERRY}, 2, "* Icecap appears.\n* Jerry came too."},
+ [FORM_ICECAP_JERRY_SNOWDRAKE - FORM_RUINS_COUNT] = {{MON_SNOWDRAKE, MON_ICECAP, MON_JERRY}, 3,
+ "* Icecap and Snowdrake confront you, sighing.\n* Jerry."},
+ [FORM_ICECAP_JERRY_CHILLDRAKE - FORM_RUINS_COUNT] = {{MON_CHILLDRAKE, MON_ICECAP, MON_JERRY}, 3,
+ "* Icecap and Chilldrake confront you, sighing.\n* Jerry."},
+ [FORM_GYFTROT - FORM_RUINS_COUNT] = {{MON_GYFTROT}, 1, "* Gyftrot confronts you!"},
+ [FORM_GLYDE - FORM_RUINS_COUNT] = {{MON_GLYDE}, 1, "* Glyde swooped in!"},
+ [FORM_DOGGO - FORM_RUINS_COUNT] = {{MON_DOGGO}, 1, "* Doggo blocks the way!"},
+ [FORM_DOGI - FORM_RUINS_COUNT] = {{MON_DOGAMY, MON_DOGARESSA}, 2, "* Dogi assault you!"},
+ [FORM_GREATERDOG - FORM_RUINS_COUNT] = {{MON_GREATERDOG}, 1, "* It's the Greater Dog."},
+ [FORM_PAPYRUS - FORM_RUINS_COUNT] = {{MON_PAPYRUS}, 1, "* Papyrus blocks the way!"},
+};
+const Form *snow_form(int form) { return form >= FORM_SNOW_COUNT ? wf_form(form) : &snow_forms[form - FORM_RUINS_COUNT]; }
+
+/* random encounters: the wiki's pools (no more of them once Papyrus is spared with no one killed here) */
+int snow_encounter(int room) {
+ if (flag(F_PAPYRUS) == 1 && !flag(F_KILLS_SNOWDIN)) return -1;
+ int drake = flag(F_SNOWDRAKE_DEAD) ? FORM_CHILLDRAKE : FORM_SNOWDRAKE;
+ bool lesser = !flag(F_LESSERDOG) && rnd_n(4) == 0;
+ switch (room) {
+ case ROOM_BOXROAD: case ROOM_PAPSENTRY: case ROOM_ICEFIELD:
+ if (lesser) return FORM_LESSERDOG;
+ return rnd_n(2) ? drake : FORM_ICECAP;
+ case ROOM_SNOWPUZZLE: case ROOM_DANGERBRIDGE:
+ if (lesser) return FORM_LESSERDOG;
+ if (rnd_n(2)) return FORM_ICECAP_JERRY;
+ return flag(F_SNOWDRAKE_DEAD) ? FORM_ICECAP_JERRY_CHILLDRAKE : FORM_ICECAP_JERRY_SNOWDRAKE;
+ case ROOM_SOUTHCLIFF: return FORM_GYFTROT;
+ case ROOM_DOORROOM: return FORM_GLYDE;
+ }
+ return -1;
+}
+
+/* ---------------------------------------------------------------- attacks */
+static int bx0(void) { return bt.x0 + 3; }
+static int bx1(void) { return bt.x1 - 3; }
+static int by0(void) { return bt.y0 + 3; }
+static int by1(void) { return bt.y1 - 3; }
+#define P(v) ((v) * FX)
+static Bullet *shot(uint16_t spr, int x, int y, int vx, int vy) {
+ Bullet *b = bullet_new(spr, P(x), P(y), vx, vy);
+ if (b) b->owner = (uint8_t)bt.atk_owner;
+ return b;
+}
+/* aimed at the SOUL, sp (1/16 px) a frame */
+static void aim(Bullet *b, int sp) {
+ int dx = bt.sx - b->x, dy = bt.sy - b->y;
+ int d = abs_i(dx) + abs_i(dy);
+ if (d) b->vx = dx * sp / d, b->vy = dy * sp / d;
+}
+
+/* Snowdrake: crescents falling in arcs from the top corners / in lines at the SOUL */
+static void atk_drake_arcs(int t) {
+ if (t < 120 && t % 14 == 0) {
+ bool left = (t / 14) % 2;
+ Bullet *b = shot(SPR_ICESHARD, left ? bx0() : bx1(), by0(), (left ? 1 : -1) * (P(1) + rnd_n(P(1))), -P(1));
+ if (b) b->kind = BK_GRAVITY, b->ay = 3, b->r = 3;
+ }
+}
+static void atk_drake_lines(int t) {
+ if (t < 120 && t % 20 == 0) {
+ int x = bx0() + 8 + rnd_n(bx1() - bx0() - 16);
+ for (int k = 0; k < 3; k++) {
+ Bullet *b = shot(SPR_ICESHARD, x + (k - 1) * 10, by0() - 10 - k * 6, 0, 0);
+ if (b) aim(b, P(2)), b->r = 3;
+ }
+ }
+}
+/* Ice Cap: ice shot up from hats at the bottom, falling back; zig-zag walls closing top and bottom */
+static void atk_icecap_hats(int t) {
+ if (t < 120 && t % 18 == 0) {
+ Bullet *b = shot(SPR_ICESHARD, bx0() + 8 + rnd_n(bx1() - bx0() - 16), by1(), rnd_n(P(1)) - P(1) / 2, -P(4));
+ if (b) b->kind = BK_GRAVITY, b->ay = 5, b->r = 3;
+ }
+}
+static void atk_icecap_waves(int t) {
+ /* columns of blocks scroll in from the right, their gaps zig-zagging */
+ if (t < 130 && t % 6 == 0) {
+ int h = by1() - by0();
+ int gap_mid = by0() + h / 2 + ((t / 6) % 8 < 4 ? (t / 6) % 4 : 4 - (t / 6) % 4) * h / 10 - h / 5;
+ for (int side = 0; side < 2; side++) {
+ int y0 = side ? gap_mid + 13 : by0(), y1 = side ? by1() : gap_mid - 13;
+ if (y1 <= y0) continue;
+ Bullet *b = shot(SPR_BONE, bx1() + 4, (y0 + y1) / 2, -P(2), 0);
+ if (b) b->flags |= BF_BOX, b->w = 5, b->h = (int16_t)(y1 - y0);
+ }
+ }
+}
+/* Gyftrot: three presents rise from the bottom (one blue: keep still), snowflakes fall in waves */
+static void atk_gyft_presents(int t) {
+ if (t % 50 == 0 && t < 130) {
+ int blue = rnd_n(3);
+ for (int k = 0; k < 3; k++) {
+ Bullet *b = shot(SPR_PRESENT, bx0() + (bx1() - bx0()) * (k + 1) / 4, by1() + 4, 0, -P(1) - P(1) / 2);
+ if (b) b->color = (uint8_t)(k == blue ? BC_BLUE : BC_WHITE), b->r = 4;
+ }
+ }
+}
+static void atk_gyft_snow(int t) {
+ if (t < 130 && t % 8 == 0) {
+ Bullet *b = shot(SPR_SNOWFLAKE, bx0() + 6 + rnd_n(bx1() - bx0() - 12), by0() - 4, 0, P(1) + P(1) / 4);
+ if (b) b->kind = BK_SCRIPT, b->a = (int16_t)(b->x / FX), b->r = 3;
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT || b->spr != SPR_SNOWFLAKE) continue;
+ b->t++;
+ b->y += b->vy;
+ b->x = P(b->a) + P(8) * gfx_sin(b->t * 4) / 1024;
+ if (b->y > P(by1() + 8)) b->kind = BK_NONE;
+ }
+}
+/* Doggo: a blue blade sweeps across the board (still, it can't see you) */
+static void atk_doggo_sword(int t) {
+ if (t == 10 || t == 80) {
+ bool right = t == 80;
+ Bullet *b = shot(SPR_BONE, right ? bx1() + 6 : bx0() - 6, (by0() + by1()) / 2, right ? -P(3) : P(3), 0);
+ if (b) b->flags |= BF_BOX, b->w = 4, b->h = (int16_t)(by1() - by0()), b->color = BC_BLUE;
+ }
+}
+/* Lesser Dog: a little dog runs on the spot, then leaps at the SOUL; or spears, blue then white */
+static void atk_ldog_jump(int t) {
+ if (t == 0) {
+ Bullet *b = shot(SPR_DOG_OW, bx1() - 10, by1() - 8, 0, 0);
+ if (b) b->kind = BK_SCRIPT, b->r = 6, b->anim = 4;
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT || b->spr != SPR_DOG_OW) continue;
+ b->t++;
+ if (b->t % 4 == 0) b->frame++;
+ if (b->t == 40 || b->t == 100) aim(b, P(3)), b->vy -= P(2), b->c = 1;
+ if (b->c) {
+ b->vy += 4;
+ b->x += b->vx, b->y += b->vy;
+ if (b->y > P(by1() - 8)) b->y = P(by1() - 8), b->vx = b->vy = 0, b->c = 0;
+ if (b->x < P(bx0() + 6)) b->x = P(bx0() + 6);
+ if (b->x > P(bx1() - 6)) b->x = P(bx1() - 6);
+ }
+ }
+}
+static void atk_ldog_spears(int t) {
+ if (t == 5) {
+ Bullet *b = shot(SPR_BONE, bx1() + 20, (by0() + by1()) / 2, -P(3), 0);
+ if (b) b->flags |= BF_BOX, b->w = 40, b->h = 4, b->color = BC_BLUE;
+ }
+ if (t == 70) {
+ bool left = rnd_n(2);
+ Bullet *b = shot(SPR_BONE, left ? bx0() - 20 : bx1() + 20, by0() + 10 + rnd_n(by1() - by0() - 20), left ? P(3) : -P(3), 0);
+ if (b) b->flags |= BF_BOX, b->w = 40, b->h = 4, b->kind = BK_SCRIPT, b->a = left;
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT || b->spr != SPR_BONE) continue;
+ b->t++;
+ if (b->t < 26) b->x += b->vx;
+ else if (b->t > 50) b->x -= b->vx;
+ }
+}
+/* the Dogi: two axes rise, then swing down crossing; a ring of hearts barked from one to the other (half blue) */
+static void atk_dogi_axes(int t) {
+ if (t == 0 || t == 70) {
+ for (int k = 0; k < 2; k++) {
+ Bullet *b = shot(SPR_KNIFE, k ? bx1() - 14 : bx0() + 14, by0() + 18, 0, -P(1) / 2);
+ if (b) b->kind = BK_SCRIPT, b->a = (int16_t)k, b->r = 5;
+ }
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT || b->spr != SPR_KNIFE) continue;
+ b->t++;
+ if (b->t < 16) b->y += b->vy;
+ else b->x += b->a ? -P(2) : P(2), b->y += P(1);
+ if (b->t > 70) b->kind = BK_NONE;
+ }
+}
+static void atk_dogi_hearts(int t) {
+ if (t % 50 == 5 && t < 120) {
+ int cy = by0() + 12 + rnd_n(by1() - by0() - 24);
+ for (int k = 0; k < 8; k++) {
+ Bullet *b = shot(SPR_HEARTBULLET, bx0() - 10, cy, P(2), 0);
+ if (b) b->kind = BK_ORBIT, b->a = (int16_t)(bx0() - 10), b->b = (int16_t)cy, b->c = P(13), b->d = (int16_t)(k * 32 * 16),
+ b->vx = 40, b->vy = 0, b->color = (uint8_t)(k % 2 ? BC_BLUE : BC_WHITE), b->r = 3, b->ax = P(2);
+ }
+ }
+ /* the ring's middle moves across */
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind == BK_ORBIT && b->spr == SPR_HEARTBULLET) b->a = (int16_t)(b->a + 2);
+ }
+}
+/* Greater Dog: its spear flies right to left flickering blue and white; a sleeping dog barks if you move */
+static void atk_gdog_spear(int t) {
+ if (t % 45 == 0 && t < 130) {
+ Bullet *b = shot(SPR_BONE, bx1() + 20, by0() + 8 + rnd_n(by1() - by0() - 16), -P(3), 0);
+ if (b) b->flags |= BF_BOX, b->w = 36, b->h = 4, b->kind = BK_SCRIPT;
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT || b->spr != SPR_BONE) continue;
+ b->t++;
+ b->x += b->vx;
+ b->color = (uint8_t)((b->t / 10) % 2 ? BC_BLUE : BC_WHITE);
+ if (b->x < P(bx0() - 30)) b->kind = BK_NONE;
+ }
+}
+static void atk_gdog_bark(int t) {
+ static int32_t lx, ly;
+ if (t == 0) {
+ lx = bt.sx, ly = bt.sy;
+ Bullet *d = shot(SPR_DOG_OW, (bx0() + bx1()) / 2, by1() - 6, 0, 0);
+ if (d) d->kind = BK_SCRIPT, d->flags |= BF_HARMLESS;
+ }
+ if (t > 10 && t % 16 == 0 && (bt.sx != lx || bt.sy != ly)) {
+ Bullet *b = shot(SPR_HEARTBULLET, (bx0() + bx1()) / 2, by1() - 14, 0, 0);
+ if (b) aim(b, P(2)), b->r = 3;
+ }
+ lx = bt.sx, ly = bt.sy;
+}
+/* Glyde: waves of stars */
+static void atk_glyde_stars(int t) {
+ if (t < 140 && t % 24 == 0) {
+ bool left = (t / 24) % 2;
+ for (int k = 0; k < 5; k++) {
+ Bullet *b = shot(SPR_SNOWFLAKE, left ? bx0() - 6 : bx1() + 6, by0() + 6 + k * (by1() - by0() - 12) / 4, left ? P(2) : -P(2),
+ (k - 2) * 6);
+ if (b) b->r = 3, b->angle = 1;
+ }
+ }
+}
+
+static const int16_t snow_lengths[ATK_SNOW_COUNT - ATK_RUINS_COUNT] = {
+ [ATK_DRAKE_ARCS - ATK_RUINS_COUNT] = 150, [ATK_DRAKE_LINES - ATK_RUINS_COUNT] = 150,
+ [ATK_ICECAP_HATS - ATK_RUINS_COUNT] = 150, [ATK_ICECAP_WAVES - ATK_RUINS_COUNT] = 180,
+ [ATK_GYFT_PRESENTS - ATK_RUINS_COUNT] = 170, [ATK_GYFT_SNOW - ATK_RUINS_COUNT] = 160,
+ [ATK_DOGGO_SWORD - ATK_RUINS_COUNT] = 150, [ATK_LDOG_JUMP - ATK_RUINS_COUNT] = 150,
+ [ATK_LDOG_SPEARS - ATK_RUINS_COUNT] = 140, [ATK_DOGI_AXES - ATK_RUINS_COUNT] = 150,
+ [ATK_DOGI_HEARTS - ATK_RUINS_COUNT] = 170, [ATK_GDOG_SPEAR - ATK_RUINS_COUNT] = 160,
+ [ATK_GDOG_BARK - ATK_RUINS_COUNT] = 150, [ATK_GLYDE_STARS - ATK_RUINS_COUNT] = 170,
+};
+int papyrus_attack_length(int atk); /* papyrus.c */
+void papyrus_attack(int atk, int t);
+void papyrus_attack_draw(int atk, int t);
+
+int snow_attack_length(int atk) {
+ if (atk >= ATK_SNOW_COUNT) return wf_attack_length(atk);
+ if (atk >= ATK_PAP_FIRST && atk <= ATK_PAP_LAST) return papyrus_attack_length(atk);
+ int n = snow_lengths[atk - ATK_RUINS_COUNT] ? snow_lengths[atk - ATK_RUINS_COUNT] : 150;
+ /* Jerry makes attacks 2 seconds longer */
+ for (int i = 0; i < bt.nmon; i++)
+ if (bt.mon[i].def == mon_def(MON_JERRY) && bt.mon[i].alive && !bt.mon[i].gone) n += 60;
+ return n;
+}
+
+void snow_attack_run(Battle *b, int atk, int t) {
+ if (atk >= ATK_SNOW_COUNT) {
+ wf_attack_run(b, atk, t);
+ return;
+ }
+ (void)b;
+ if (t < 0) return;
+ switch (atk) {
+ case ATK_DRAKE_ARCS: atk_drake_arcs(t); break;
+ case ATK_DRAKE_LINES: atk_drake_lines(t); break;
+ case ATK_ICECAP_HATS: atk_icecap_hats(t); break;
+ case ATK_ICECAP_WAVES: atk_icecap_waves(t); break;
+ case ATK_GYFT_PRESENTS: atk_gyft_presents(t); break;
+ case ATK_GYFT_SNOW: atk_gyft_snow(t); break;
+ case ATK_DOGGO_SWORD: atk_doggo_sword(t); break;
+ case ATK_LDOG_JUMP: atk_ldog_jump(t); break;
+ case ATK_LDOG_SPEARS: atk_ldog_spears(t); break;
+ case ATK_DOGI_AXES: atk_dogi_axes(t); break;
+ case ATK_DOGI_HEARTS: atk_dogi_hearts(t); break;
+ case ATK_GDOG_SPEAR: atk_gdog_spear(t); break;
+ case ATK_GDOG_BARK: atk_gdog_bark(t); break;
+ case ATK_GLYDE_STARS: atk_glyde_stars(t); break;
+ default:
+ if (atk >= ATK_PAP_FIRST && atk <= ATK_PAP_LAST) papyrus_attack(atk, t);
+ break;
+ }
+}
+
+void snow_attack_draw(Battle *b, int atk, int t) {
+ (void)b;
+ if (atk >= ATK_PAP_FIRST && atk <= ATK_PAP_LAST) papyrus_attack_draw(atk, t);
+}
+
+/* after a battle in Snowdin: who was killed, who's been met */
+void snow_battle_done(int form) {
+ for (int i = 0; i < bt.nmon; i++) {
+ const Monster *m = &bt.mon[i];
+ if (m->def == mon_def(MON_SNOWDRAKE) && m->killed) set_flag(F_SNOWDRAKE_DEAD, 1);
+ }
+ if (form == FORM_LESSERDOG) set_flag(F_LESSERDOG, bt.mon[0].killed ? 2 : 1);
+}
diff --git a/games/undertale/src/snowtown.c b/games/undertale/src/snowtown.c
new file mode 100644
index 00000000..fd81a096
--- /dev/null
+++ b/games/undertale/src/snowtown.c
@@ -0,0 +1,302 @@
+/* Snowdin Town: the shop, the inn, Grillby's, the library, the townsfolk, the foggy path where Papyrus fights,
+ * his house and the time spent with him, and the way on to Waterfall. Signs and books are the game's; the
+ * townsfolk's talks are kept short. */
+#include
+#include
+#include "battle.h"
+#include "event.h"
+#include "events.h"
+#include "game.h"
+#include "items.h"
+#include "menu.h"
+#include "monsters.h"
+#include "save.h"
+#include "shop.h"
+#include "snowdin.h"
+#include "world.h"
+
+static void gone(Obj *o) { o->kind = OBJ_NONE; }
+
+/* someone of the town who's drawn on the map: a box to talk to, what they say */
+static void folk(int x, int y, const char *text) {
+ Obj *o = obj_sign(x - 12, y - 18, 24, 26, text);
+ (void)o;
+}
+
+void town_setup(int room, int from) {
+ (void)from;
+ switch (room) {
+ case ROOM_SNOWTOWN:
+ obj_save(518, 1076, "* (The sight of such a friendly town fills you with determination.)");
+ obj_sign(266, 1046, 30, 24, "* (Welcome to Snowdin Town!)");
+ obj_sign(776, 1048, 20, 20, "* (Don't want to walk to the other side of town?)\n* (Try the undersnow tunnels!)|* (They're simple, quick, and convenient!)");
+ {
+ Obj *b = obj_trigger(566, 1072, 26, 24, EV_BOX, ACTOR_ITEM);
+ if (b) b->flags = OF_TALK | OF_SOLID;
+ Obj *shop = obj_trigger(446, 1066, 28, 24, EV_SHOP, ACTOR_TRIGGER);
+ (void)shop;
+ Obj *house = obj_trigger(2238, 1086, 28, 26, EV_TOWN_DOOR, ACTOR_TRIGGER2);
+ if (house) house->arg = 0;
+ Obj *shed = obj_trigger(2398, 1084, 28, 16, EV_TOWN_DOOR, ACTOR_TRIGGER3);
+ if (shed) shed->arg = 1;
+ }
+ folk(850, 1029, "* My sister's walking our little brother on a leash again.\n* Is that weird? It's weird, right?");
+ folk(911, 1101, "* I'm taking little Cinnamon for a walk!");
+ folk(991, 1081, "* Teens kept decorating Gyftrot's horns as a prank...|* So now everyone decorates a tree and puts presents under it instead.");
+ folk(1202, 1043, "* Papyrus reports straight to the Royal Guard.\n* Around here, that's about all the government we've got.");
+ folk(1457, 1042, "* Everyone makes light of how things are down here.\n* I don't think it's funny.");
+ folk(1515, 1040, "* If we didn't laugh about it, we'd just be sad.\n* So we laugh.");
+ folk(1840, 1044, "* (The sign says \"Librarby.\")");
+ if (flag(F_PAPYRUS) == 1) {
+ Obj *p = obj_papyrus(2190, 1100, DIR_DOWN);
+ if (p) p->event = EV_PAP_HOUSE, p->flags |= OF_TALK;
+ }
+ folk(1099, 1093, "* Hey, another kid!|* Someday I'll watch Undyne\n fight. She's the best!");
+ break;
+ case ROOM_TOWNNORTH:
+ folk(1463, 851, "* The kids are playing \"monsters and humans.\"\n* Ah, to be young again...");
+ folk(1513, 880, "* I'm the human! You can't catch me!");
+ folk(1505, 807, "* (Knock knock.)\n* ... Who's there?\n* ...|* Oh, I just love it when someone knocks.");
+ folk(1910, 789, "* I throw ice into the water.\n* It floats all the way to the CORE.");
+ break;
+ case ROOM_TOWNDOCK:
+ folk(1611, 589, "* Tra la la.\n* The boat isn't going anywhere today.");
+ break;
+ case ROOM_INN1: {
+ Obj *k = obj_trigger(636, 770, 26, 26, EV_INN, ACTOR_ITEM);
+ if (k) k->flags = OF_TALK | OF_SOLID;
+ folk(698, 815, "* My mom runs this inn.\n* Sleep in a bed and you'll feel all better!");
+ break;
+ }
+ case ROOM_INN2:
+ obj_sign(680, 520, 80, 80, "* (Two people are snoring loudly.)");
+ break;
+ case ROOM_GRILLBYS:
+ folk(1246, 770, "* (Grillby is silently cleaning a glass.)");
+ folk(1214, 792, "* Cheep cheep!\n* (The dogs here are the Royal Guard's Snowdin squad.)");
+ folk(1194, 786, "* I'm fishing for a date.\n* Literally.");
+ folk(1156, 782, "* (Lesser Dog is playing poker against itself.)");
+ folk(1174, 834, "* (Bark!)");
+ folk(1134, 860, "* I'm thinking of getting a collar...\n* Then maybe someone would take me for a walk.");
+ folk(1316, 903, "* I want new drinks...\n* And hot guys!");
+ folk(1324, 833, "* Monster food turns into energy right away.\n* I want to try human food someday.");
+ folk(1329, 767, "* New Home's getting crowded.\n* I bet someday everyone moves out here.");
+ if (flag(F_DOGI) != 2) {
+ folk(1209, 871, "* Watch out for Greater Dog.\n* It'll love you to death.");
+ folk(1238, 853, "* Nobody respects the sentries...");
+ }
+ break;
+ case ROOM_LIBRARY:
+ obj_sign(1830, 506, 30, 40, "* (A book about monster funerals.)|* (When a monster passes, its\n dust is spread on what it\n loved most.)|* (The writer seems eager to\n reach the page count.)");
+ obj_sign(1874, 506, 30, 40, "* (A book comparing humans and monsters.)|* (Humans are sturdier... but they can't send a birthday card made of bullets.)");
+ obj_sign(1918, 506, 30, 40, "* (Here I am... Writing this\n book.)|* (A person comes in and picks\n up the book...)\n* (They start reading it...!)");
+ obj_sign(1962, 506, 30, 40, "* (A book about magic and the SOUL.)|* (A monster who won't fight\n grows weaker...)|* (...and cruel intent hurts\n them most.)\n* (The chapter stops there.)");
+ obj_sign(2006, 506, 30, 40, "* (A history book. Monsters left their first city and crossed cold, swamp and heat.)|* (They named the new capital \"New Home.\" The book sighs about the King's naming.)");
+ obj_sign(2050, 506, 30, 40, "* (A book on what a SOUL is made of.)|* (Nobody knows for sure.)");
+ folk(1896, 640, "* Yes, the sign outside says \"Librarby.\"\n* We made a typo when we were making it.");
+ folk(2031, 655, flag(F_WORDSEARCH) == 2 ? "* I can see it in your eyes...\n* You think Junior Jumble is harder." : "* I can see it in your eyes...\n* You think crosswords are harder.");
+ folk(2002, 589, "* There's no news around here, so the newspaper is mostly comics and puzzles.");
+ folk(2044, 590, "* I make the word searches.\n* Number one in the underground!");
+ break;
+ case ROOM_PSHOUSE:
+ {
+ Obj *couch = obj_trigger(2300, 892, 82, 40, EV_COUCH, ACTOR_ITEM);
+ if (couch) couch->flags = OF_TALK;
+ }
+ obj_sign(2316, 834, 52, 30, "* (A TV.)");
+ obj_sign(2398, 800, 64, 50, "* (The kitchen.)");
+ if (flag(F_PAP_DATE) == 1) {
+ Obj *p = obj_papyrus(2276, 940, DIR_RIGHT);
+ if (p) p->event = EV_PAP_DATE, p->flags |= OF_TALK;
+ }
+ break;
+ case ROOM_SANSROOM:
+ obj_sign(2560, 600, 40, 40, "* (It's dark... A trash tornado swirls in the corner.)");
+ break;
+ case ROOM_SHED:
+ obj_sign(2650, 770, 30, 20, flag(F_PAP_CAPTURED) <= 1 ? "* (A note.)|* SORRY ABOUT THE LOCK!\n* UNDYNE IS ON HER WAY.\n* MAKE YOURSELF COMFY!!!|* - PAPYRUS"
+ : flag(F_PAP_CAPTURED) == 2 ? "* (A note.)\n* NEXT TIME, ASK BEFORE ESCAPING! I WAS WORRIED!!!\n* - PAPYRUS"
+ : "* (A note.)\n* YOU CAN JUST STAY OVER, YOU KNOW! NO FIGHTING NEEDED!!!\n* - PAPYRUS");
+ obj_sign(2680, 820, 40, 30, "* (A dog bed. It's warm.)");
+ break;
+ case ROOM_FOGROOM:
+ if (!flag(F_PAPYRUS)) obj_trigger(3290, 1030, 30, 150, EV_PAP_FIGHT, ACTOR_TRIGGER);
+ break;
+ }
+}
+
+/* ---------------------------------------------------------------- the events */
+static int ev_shop(Ev *c) {
+ EV_BEGIN;
+ pl.y = (int16_t)(pl.y + 6); /* (a step back from the door, for after) */
+ shop_open(SHOP_SNOWDIN);
+ WAIT_UNTIL(g_mode == MODE_WORLD);
+ pl.dir = DIR_DOWN;
+ EV_END;
+}
+
+static int ev_inn(Ev *c) {
+ EV_BEGIN;
+ NARRATE("* Hello, traveller.\n* Welcome to Snowed Inn.|* A room is 80G for the night.\n* Would you like to stay?@Stay@Leave");
+ if (CHOICE() == 1) {
+ NARRATE("* Come back anytime!");
+ } else if (sv.gold < 80) {
+ NARRATE("* Oh... You don't have enough money...|* ... Um, you know what? Take the room anyway.");
+ c->v[0] = 1;
+ } else {
+ c->v[0] = 1;
+ }
+ if (c->v[0]) {
+ NARRATE("* Here's your key.\n* Sleep well!");
+ world_fade_hold(ROOM_INN2, 540, 620, DIR_DOWN, 90);
+ WAIT_UNTIL(!world_fading());
+ sv.hp = sv.maxhp;
+ set_flag(F_INN, flag(F_INN) + 1);
+ NARRATE("* (You slept soundly. Your HP was maxed out.)|* (The innkeeper gave you your money back.)");
+ }
+ EV_END;
+}
+
+static int ev_town_door(Ev *c) {
+ int k = c->self ? c->self->arg : 0;
+ EV_BEGIN;
+ if (k == 0 && flag(F_TOWN_HOUSE)) { /* Papyrus and Sans's house */
+ FADE_TO(ROOM_PSHOUSE, 2436, 914, DIR_UP);
+ return 1;
+ }
+ NARRATE("* (It's locked.)");
+ pl.y = (int16_t)(pl.y + 6); /* (a step back off the door) */
+ EV_END;
+}
+
+static int ev_pap_fight(Ev *c) {
+ Obj *p = obj_get(ACTOR_PAPYRUS);
+ EV_BEGIN;
+ /* through the fog, a tall silhouette */
+ p = obj_papyrus(3440, 1100, DIR_LEFT);
+ p = obj_get(ACTOR_PAPYRUS);
+ pl.dir = DIR_RIGHT;
+ WALK(p, 3400, 1100, 1);
+ if (flag(F_PAP_CAPTURED) >= 3) {
+ SAY(FACE_PAPYRUS, "`0* HUMAN! YOU KEEP COMING\n BACK...|* SHALL WE SKIP OUR BATTLE?@Yes@No");
+ if (CHOICE() == 0) {
+ SAY(FACE_PAPYRUS, "`2* WELL THEN! LET'S BE FRIENDS INSTEAD!!!");
+ set_flag(F_PAPYRUS, 1);
+ goto spared;
+ }
+ } else if (flag(F_PAP_CAPTURED)) {
+ SAY(FACE_PAPYRUS, "`0* HUMAN! YOU ESCAPED!\n* LET'S TRY THIS AGAIN!!!");
+ } else {
+ SAY(FACE_PAPYRUS, "`0* HUMAN. I HAVE BEEN\n THINKING.|* YOU LIKE PASTA.\n* YOU SOLVE PUZZLES.\n* YOU SEEM... COOL.|`2* I COULD BE YOUR FRIEND!!!|`3* NO! A ROYAL GUARD CAN'T\n BEFRIEND A HUMAN!|* I MUST CAPTURE YOU, AND\n JOIN THE GUARD!!!");
+ }
+ sv.hp = sv.maxhp;
+ BATTLE(FORM_PAPYRUS);
+ set_flag(F_SNOWDIN_BOSSES, flag(F_SNOWDIN_BOSSES) + 1);
+ if (battle_result() == BR_SCRIPTED) {
+ /* captured: off to the shed */
+ set_flag(F_PAP_CAPTURED, flag(F_PAP_CAPTURED) + 1);
+ if (flag(F_PAP_CAPTURED) == 1)
+ SAY(FACE_PAPYRUS, "`2* NYEH HEH HEH! CAPTURED!!!\n* OFF TO THE CAPTURE ZONE WITH YOU! (OUR SHED.)");
+ else if (flag(F_PAP_CAPTURED) == 2)
+ SAY(FACE_PAPYRUS, "`2* CAPTURED AGAIN!!!\n* THE SHED HAS BEEN IMPROVED SINCE YOUR LAST VISIT.");
+ else
+ SAY(FACE_PAPYRUS, "`2* YOU ARE... PERSISTENT!\n* BUT I AM THE PERSISTENTEST!!!");
+ gone(p);
+ world_fade_hold(ROOM_SHED, 2640, 840, DIR_DOWN, 40);
+ WAIT_UNTIL(!world_fading());
+ return 1;
+ }
+ if (bt.mon[0].killed) {
+ set_flag(F_PAPYRUS, 2);
+ gone(p);
+ SAY(FACE_PAPYRUS, "`4* WELL... I DIDN'T SEE THAT COMING.|* STILL, I THINK YOU CAN BE BETTER THAN THIS.\n* I BELIEVE IN YOU!");
+ if (c->self) gone(c->self);
+ return 1;
+ }
+ set_flag(F_PAPYRUS, 1);
+spared:
+ SAY(FACE_PAPYRUS, "`4* NYOO HOO HOO...|* I COULDN'T EVEN CAPTURE\n YOU.\n* SO MUCH FOR THE GUARD.|`0* WAIT... YOU WANT TO BE FRIENDS???|`2* WOWIE!!! COME HANG OUT AT MY HOUSE LATER!|`0* (THE CAPITAL IS FURTHER ON, PAST THE TOWN.)");
+ set_flag(F_PAP_DATE, 1);
+ set_flag(F_TOWN_HOUSE, 1);
+ WALK(p, 3240, 1100, 6);
+ gone(p);
+ if (c->self) gone(c->self);
+ EV_END;
+}
+
+static int ev_pap_house(Ev *c) {
+ EV_BEGIN;
+ if (flag(F_PAP_DATE) == 1) {
+ SAY(FACE_PAPYRUS, "`0* HUMAN! READY TO HANG OUT?\n* COME INSIDE!!!");
+ FADE_TO(ROOM_PSHOUSE, 2436, 914, DIR_UP);
+ } else {
+ SAY(FACE_PAPYRUS, "`2* HELLO, FRIEND!!!");
+ }
+ EV_END;
+}
+
+static int ev_pap_date(Ev *c) {
+ EV_BEGIN;
+ /* his room, his spaghetti, his number */
+ SAY(FACE_PAPYRUS, "`0* WELCOME TO MY HOUSE!\n* THIS IS WHERE I LIVE WITH MY BROTHER, SANS.|* LET'S GO TO MY ROOM!!!");
+ FADE_TO(ROOM_PAPROOM, 2290, 650, DIR_UP);
+ {
+ Obj *p = obj_papyrus(2330, 640, DIR_LEFT);
+ (void)p;
+ }
+ SAY(FACE_PAPYRUS, "`0* MY ROOM!!! IMPRESSIVE, ISN'T IT?|`4* NOW... HANGING OUT. I READ A GUIDE ABOUT THIS.|`0* STEP ONE: A PRESENT!\n* FOR YOU: SPAGHETTI!@Eat it@Keep it");
+ if (CHOICE() == 0) SAY(FACE_PAPYRUS, "`2* WOWIE!!! YOU LIKED IT?!?\n* I'LL HAVE TO MAKE MORE!!!");
+ else SAY(FACE_PAPYRUS, "`2* SAVING IT FOR LATER?\n* A TRUE CONNOISSEUR!");
+ SAY(FACE_PAPYRUS, "`4* HUMAN... I'VE THOUGHT\n ABOUT IT.|* LET'S STAY FRIENDS!\n* THE BEST OF FRIENDS!!!|`2* TAKE MY PHONE NUMBER! CALL ANY TIME!\n* NYEH HEH HEH!!!");
+ NARRATE("* (You got Papyrus's phone number.)");
+ set_flag(F_PAP_DATE, 2);
+ FADE_TO(ROOM_SNOWTOWN, 2240, 1096, DIR_DOWN);
+ EV_END;
+}
+
+static int ev_grillbys(Ev *c) {
+ EV_BEGIN;
+ /* (from his station in Waterfall) */
+ if (flag(F_GRILLBYS)) {
+ SAY(FACE_SANS, "* i'm on another break.\n* from my other break.");
+ return 1;
+ }
+ SAY(FACE_SANS, "* hey. i'm on break. want to\n eat at grillby's?@Sure@Not now");
+ if (CHOICE() == 1) {
+ SAY(FACE_SANS, "* ok. maybe later.");
+ return 1;
+ }
+ SAY(FACE_SANS, "* well, i know a shortcut.");
+ FADE_TO(ROOM_GRILLBYS, 1230, 880, DIR_UP);
+ SAY(FACE_SANS, "* here's a good seat.|* so, what'll it be?\n`2* fries or burger?@Fries@Burger");
+ SAY(FACE_SANS, "* nice.|* say... papyrus says a\n #Yflower#X talks to him\n sometimes.|* gives him advice.\n* weird, right?|* if you see a talking flower, tell me, ok?|`1* oh, and thanks for paying.");
+ set_flag(F_GRILLBYS, 1);
+ FADE_TO(ROOM_WF1, 880, 1458, DIR_DOWN);
+ EV_END;
+}
+
+static int ev_couch(Ev *c) {
+ EV_BEGIN;
+ if (!flag(F_COUCH_GOLD)) {
+ set_flag(F_COUCH_GOLD, 1);
+ sv.gold = (uint16_t)min_i(9999, sv.gold + 20);
+ NARRATE("* (There's 20G under the couch!)");
+ } else {
+ NARRATE("* (It's a couch.)");
+ }
+ EV_END;
+}
+
+int town_event(Ev *c) {
+ switch (c->id) {
+ case EV_SHOP: return ev_shop(c);
+ case EV_INN: return ev_inn(c);
+ case EV_TOWN_DOOR: return ev_town_door(c);
+ case EV_PAP_FIGHT: return ev_pap_fight(c);
+ case EV_PAP_HOUSE: return ev_pap_house(c);
+ case EV_PAP_DATE: return ev_pap_date(c);
+ case EV_GRILLBYS: return ev_grillbys(c);
+ case EV_COUCH: return ev_couch(c);
+ }
+ return -1;
+}
diff --git a/games/undertale/src/text.c b/games/undertale/src/text.c
new file mode 100644
index 00000000..227cbf65
--- /dev/null
+++ b/games/undertale/src/text.c
@@ -0,0 +1,340 @@
+/* Text typed a letter at a time, and the dialogue box (see text.h). */
+#include
+#include
+#include
+#include "gfx.h"
+#include "res.h"
+#include "text.h"
+#include "world.h"
+
+char g_name[8] = "Frisk";
+
+/* ---------------------------------------------------------------- faces */
+/* each expression: its sprite (2 frames: the second one talks), drawn at (x, y) in the box; Toriel's
+ * heads sit on her shoulders */
+typedef struct {
+ uint16_t spr;
+ int8_t x, y;
+} Expr;
+static const Expr toriel_faces[] = {
+ {SPR_FACE_TORIELTALK, 9, 0}, {SPR_FACE_TORIELTALKSIDE, 9, 0}, {SPR_FACE_TORIELHAPPYTALK, 9, 0},
+ {SPR_FACE_TORIELWHAT, 9, 0}, {SPR_FACE_TORIELWHATSIDE, 9, 0}, {SPR_FACE_TORIELMAD, 9, 0},
+ {SPR_FACE_TORIELCOLD, 9, 0}, {SPR_FACE_TORIELSURPRISED, 9, 0}, {SPR_FACE_TORIELEMBARRASSED, 9, 0},
+ {SPR_FACE_TORIELGLASSES, 9, 0}, {SPR_FACE_TORIELREVENGETALK, 9, 0}, {SPR_FACE_TORIELSCARED, 9, 0},
+ {SPR_FACE_TORIELBLINK, 9, 0}, {SPR_FACE_TORIELHAPPYBLINK, 9, 0},
+};
+static const Expr flowey_faces[] = {
+ {SPR_FLOWEYNICE, 4, -2}, {SPR_FLOWEYNICESIDE, 4, -2}, {SPR_FLOWEYSASSY, 4, -2}, {SPR_FLOWEYEVIL, 4, -2},
+ {SPR_FLOWEYGRIN, 4, -2}, {SPR_FLOWEYPLAIN, 4, -2}, {SPR_FLOWEYPISSED, 4, -2}, {SPR_FLOWEYWINK, 4, -2},
+ {SPR_FLOWEYLAUGH, 4, -2}, {SPR_FLOWEYHURT, 4, -2}, {SPR_FLOWEYTORIEL, 4, -2}, {SPR_FLOWEYTORIEL2, 4, -2},
+ {SPR_FLOWEYSIDESHOCK, 4, -2},
+};
+static const Expr sans_faces[] = {{SPR_FACE_SANS, 4, 0}, {SPR_FACE_SANSCHUCKLE, 4, 0}, {SPR_FACE_SANSWINK, 4, 0}};
+static const Expr alphys_faces[] = {{SPR_FACE_ALPHYS, 4, 0}};
+static const Expr asgore_faces[] = {{SPR_FACE_ASGORE, 4, -4}};
+static const Expr papyrus_faces[] = {
+ {SPR_FACE_PAPYRUS, 4, -4}, {SPR_FACE_PAPYRUSEVIL, 4, -4}, {SPR_FACE_PAPYRUSLAUGH, 4, -4},
+ {SPR_FACE_PAPYRUSMAD, 4, -4}, {SPR_FACE_PAPYRUSSIDE, 4, -4},
+};
+static const struct {
+ const Expr *e;
+ int n;
+} faces[FACE_MAX] = {
+ [FACE_TORIEL] = {toriel_faces, sizeof toriel_faces / sizeof toriel_faces[0]},
+ [FACE_FLOWEY] = {flowey_faces, sizeof flowey_faces / sizeof flowey_faces[0]},
+ [FACE_SANS] = {sans_faces, sizeof sans_faces / sizeof sans_faces[0]},
+ [FACE_PAPYRUS] = {papyrus_faces, sizeof papyrus_faces / sizeof papyrus_faces[0]},
+ [FACE_ALPHYS] = {alphys_faces, 1},
+ [FACE_ASGORE] = {asgore_faces, 1},
+};
+
+/* ---------------------------------------------------------------- the writer */
+#define MAX_LINES 8
+static uint16_t breaks[MAX_LINES]; /* where the page's lines wrap (src offsets), the line after indented */
+static int nbreaks;
+static int wrap_width = 270;
+
+static int page_len(const char *s) {
+ int n = 0;
+ while (s[n] && s[n] != '|') n++;
+ return n;
+}
+
+/* the width of a letter, 0 for the marks */
+static int code_len(const char *s) {
+ switch (s[0]) {
+ case '^': case '`': case '#': return s[1] ? 2 : 1;
+ case '~': return 1;
+ default: return 0;
+ }
+}
+
+/* where the page's lines wrap: at the last space before the box's edge */
+static void layout(Writer *w) {
+ nbreaks = 0;
+ const char *s = w->src + w->page;
+ int x = 0, last_space = -1, start = 0;
+ for (int i = 0; i < w->len; i++) {
+ int c = code_len(s + i);
+ if (c) {
+ i += c - 1;
+ continue;
+ }
+ if (s[i] == '@') break;
+ if (s[i] == '\n') {
+ x = 0, last_space = -1, start = i + 1;
+ continue;
+ }
+ if (s[i] == ' ') last_space = i;
+ int adv = s[i] == '$' ? 8 * (int)strlen(g_name) : 8;
+ x += adv;
+ if (x > wrap_width && last_space > start && nbreaks < MAX_LINES) {
+ breaks[nbreaks++] = (uint16_t)(w->page + last_space);
+ /* the line after: indented under the first's words when the page starts with "* " */
+ x = (s[0] == '*' ? 16 : 0);
+ for (int k = last_space + 1; k <= i; k++)
+ if (!code_len(s + k)) x += 8;
+ start = last_space + 1;
+ last_space = -1;
+ }
+ }
+}
+
+static uint8_t expr_of(char c) { return (uint8_t)(c >= 'a' ? 10 + c - 'a' : c - '0'); }
+
+static void page_begin(Writer *w) {
+ w->len = page_len(w->src + w->page);
+ w->shown = 0, w->pause = 0, w->t = 0, w->done = false, w->auto_close = false;
+ w->choices = 0, w->choice = 0;
+ for (int i = 0; i < w->len; i++)
+ if (w->src[w->page + i] == '@') {
+ w->choice_at = w->page + i;
+ w->choices = 2;
+ break;
+ }
+ layout(w);
+}
+
+void writer_start(Writer *w, const char *s, int x, int y, int font) {
+ memset(w, 0, sizeof *w);
+ w->src = s, w->x = x, w->y = y, w->font = font;
+ w->line_h = 18, w->speed = 1;
+ page_begin(w);
+}
+
+static void show_all(Writer *w) {
+ const char *s = w->src + w->page;
+ while (w->shown < w->len && s[w->shown] != '@') {
+ int c = code_len(s + w->shown);
+ if (c && s[w->shown] == '`') w->face_expr = expr_of(s[w->shown + 1]);
+ if (c && s[w->shown] == '~') w->auto_close = true;
+ w->shown += c ? c : 1;
+ }
+ w->done = true;
+}
+
+bool writer_update(Writer *w, uint32_t pressed) {
+ if (w->finished) return false;
+ const char *s = w->src + w->page;
+ if (!w->done) {
+ if (pressed & K_X) {
+ show_all(w);
+ } else if (w->pause > 0) {
+ w->pause--;
+ } else if (++w->t >= w->speed) {
+ w->t = 0;
+ /* the marks take no time; the next letter shows */
+ for (;;) {
+ if (w->shown >= w->len || s[w->shown] == '@') {
+ w->done = true;
+ break;
+ }
+ int c = code_len(s + w->shown);
+ if (!c) {
+ w->shown++;
+ break;
+ }
+ if (s[w->shown] == '^') w->pause = (s[w->shown + 1] - '0') * 5;
+ if (s[w->shown] == '`') w->face_expr = expr_of(s[w->shown + 1]);
+ if (s[w->shown] == '~') w->auto_close = true;
+ w->shown += c;
+ if (w->pause) break;
+ }
+ if (w->shown >= w->len) w->done = true;
+ }
+ return true;
+ }
+ if (w->choices) {
+ if (pressed & K_LEFT) w->choice = 0;
+ if (pressed & K_RIGHT) w->choice = 1;
+ }
+ if ((pressed & K_Z) || w->auto_close) {
+ const char *next = s + w->len;
+ if (*next == '|') {
+ w->page += w->len + 1;
+ page_begin(w);
+ } else {
+ w->finished = true;
+ return false;
+ }
+ }
+ return true;
+}
+
+int writer_talking(const Writer *w) { return !w->done && !w->pause; }
+
+static uint16_t color_of(char c) {
+ switch (c) {
+ case 'Y': return rgb565(0xFFFF00);
+ case 'R': return rgb565(0xFF0000);
+ case 'B': return rgb565(0x0000FF);
+ case 'O': return rgb565(0xFFA040);
+ case 'G': return rgb565(0x00C000);
+ case 'P': return rgb565(0xD535D9);
+ case 'L': return rgb565(0x0EC0FD);
+ default: return C_WHITE;
+ }
+}
+
+void writer_draw(const Writer *w, uint16_t color) {
+ if (w->finished) return;
+ char seg[80];
+ int n = 0, x = w->x, y = w->y, sx = x;
+ uint16_t col = color, segcol = color;
+ const char *s = w->src + w->page;
+ int b = 0;
+#define FLUSH() do { if (n) { seg[n] = 0; gfx_text_ex(w->font, sx, y, seg, n, segcol, w->shake, (uint32_t)(y * 31 + sx), 0); n = 0; } } while (0)
+ for (int i = 0; i < w->shown && i < w->len; i++) {
+ if (s[i] == '@') break;
+ int c = code_len(s + i);
+ if (c) {
+ if (s[i] == '#') {
+ FLUSH();
+ col = s[i + 1] == 'X' || s[i + 1] == 'W' ? color : color_of(s[i + 1]);
+ }
+ i += c - 1;
+ continue;
+ }
+ if (s[i] == '\n' || (b < nbreaks && w->page + i == breaks[b])) {
+ FLUSH();
+ if (s[i] != '\n') b++;
+ y += w->line_h;
+ x = w->x + (s[i] != '\n' && s[0] == '*' ? 16 : 0);
+ continue;
+ }
+ if (n == 0) sx = x, segcol = col;
+ if (col != segcol) {
+ FLUSH();
+ sx = x, segcol = col;
+ }
+ if (s[i] == '$') {
+ for (const char *p = g_name; *p && n < 70; p++) seg[n++] = *p, x += 8;
+ continue;
+ }
+ seg[n++] = s[i];
+ x += 8;
+ if (n >= 70) FLUSH();
+ }
+ FLUSH();
+#undef FLUSH
+ if (w->choices && w->done) {
+ /* the two options on the next line, the heart by the one selected */
+ const char *a = w->src + w->choice_at + 1, *bb = strchr(a, '@');
+ int la = bb ? (int)(bb - a) : 0, lb = bb ? page_len(bb + 1) : 0;
+ int cy = y + w->line_h + 4;
+ int ax = w->x + 56, bx = w->x + 168;
+ gfx_text_ex(w->font, ax, cy, a, la, C_WHITE, 0, 0, 0);
+ if (bb) gfx_text_ex(w->font, bx, cy, bb + 1, lb, C_WHITE, 0, 0, 0);
+ gfx_sprite(SPR_HEART, 0, (w->choice ? bx : ax) - 12, cy + 6, 0, 255);
+ }
+}
+
+/* ---------------------------------------------------------------- the dialogue box */
+static Writer box;
+static bool box_open;
+static int box_face;
+static int box_top = -1, box_at_top;
+static int last_choice;
+static char fmt_buf[512];
+
+void text_set_top(int top) { box_top = top; }
+void text_wrap_width(int w) { wrap_width = w; }
+
+void text_open(int face, const char *s) {
+ box_face = face;
+ int tx = face ? 68 : 22;
+ wrap_width = face ? 301 - tx : 300 - tx - 8;
+ box_at_top = box_top >= 0 ? box_top : (pl.y + 15 - cam_y > 130);
+ writer_start(&box, s, tx, (box_at_top ? 5 : 160) + 11, FONT_MAIN);
+ box_open = true;
+}
+
+void text_openf(int face, const char *fmt, ...) {
+ va_list ap;
+ va_start(ap, fmt);
+ vsnprintf(fmt_buf, sizeof fmt_buf, fmt, ap);
+ va_end(ap);
+ text_open(face, fmt_buf);
+}
+
+static const char *const *menu_opts;
+static int menu_n, menu_sel, menu_cols; /* long options: one a row */
+void text_open_menu(const char *const *opts, int n) {
+ menu_opts = opts, menu_n = n, menu_sel = 0, menu_cols = 2;
+ for (int i = 0; i < n; i++)
+ if (strlen(opts[i]) > 15) menu_cols = 1;
+ box_face = FACE_NONE;
+ box_at_top = box_top >= 0 ? box_top : (pl.y + 15 - cam_y > 130);
+ box_open = true;
+ box.finished = true;
+}
+
+bool text_active(void) { return box_open; }
+int text_choice(void) { return last_choice; }
+int text_menu_cols(void) { return menu_cols; }
+int text_menu_sel(void) { return box_open && menu_opts ? menu_sel : box_open && box.choices && box.done ? 100 + box.choice : -1; }
+
+void text_update(uint32_t pressed) {
+ if (!box_open) return;
+ if (menu_opts) {
+ int k = menu_cols;
+ if (pressed & K_UP && menu_sel >= k) menu_sel -= k;
+ if (pressed & K_DOWN && menu_sel + k < menu_n) menu_sel += k;
+ if (k == 2 && pressed & K_LEFT && menu_sel % 2) menu_sel--;
+ if (k == 2 && pressed & K_RIGHT && !(menu_sel % 2) && menu_sel + 1 < menu_n) menu_sel++;
+ if (pressed & (K_Z | K_X)) {
+ last_choice = pressed & K_Z ? menu_sel : -1;
+ menu_opts = NULL, box_open = false;
+ }
+ return;
+ }
+ int ch = box.choice;
+ bool had = box.choices && box.done;
+ if (!writer_update(&box, pressed)) {
+ box_open = false;
+ if (had) last_choice = ch;
+ }
+}
+
+void text_draw(void) {
+ if (!box_open) return;
+ int y = box_at_top ? 5 : 160;
+ gfx_rect(16, y, 288, 76, C_WHITE, 255);
+ gfx_rect(19, y + 3, 282, 70, C_BLACK, 255);
+ if (box_face && faces[box_face].e) {
+ int e = box.face_expr < faces[box_face].n ? box.face_expr : 0;
+ const Expr *x = &faces[box_face].e[e];
+ int fr = writer_talking(&box) ? (box.shown / 2) & 1 : 0;
+ if (box_face == FACE_TORIEL) gfx_sprite(SPR_FACE_TORIELBODY, 0, 22, y + 13 + 32, 0, 255);
+ gfx_sprite(x->spr, fr, 22 + x->x, y + 13 + x->y, 0, 255);
+ }
+ if (menu_opts) {
+ for (int i = 0; i < menu_n; i++) {
+ int x = 46 + (i % menu_cols) * 136, ty = y + 12 + (i / menu_cols) * 18;
+ gfx_text(FONT_MAIN, x, ty, menu_opts[i], C_WHITE, 255);
+ if (i == menu_sel) gfx_sprite(SPR_HEART, 0, x - 12, ty + 6, 0, 255);
+ }
+ return;
+ }
+ writer_draw(&box, C_WHITE);
+}
diff --git a/games/undertale/src/text.h b/games/undertale/src/text.h
new file mode 100644
index 00000000..96575db8
--- /dev/null
+++ b/games/undertale/src/text.h
@@ -0,0 +1,60 @@
+/* Undertale: text, typed a letter at a time.
+ *
+ * The game's texts are written with a few marks:
+ * \n a new line
+ * | a new page (after Z)
+ * ^N a pause of N times 5 frames (^1 .. ^9)
+ * `N the face's expression N (FACE_* in face.c)
+ * #Y #R #B #O #G #P #L #X yellow, red, blue, orange, green, purple, light blue, white again
+ * $ the fallen human's name
+ * @A@B at the end of a page: a choice between A and B (text_choice() says which)
+ * ~ the page closes itself once typed (no Z)
+ *
+ * A Writer types into a rectangle; the dialogue box is one, with a face or not, at the top or the bottom of
+ * the screen. */
+#ifndef UT_TEXT_H
+#define UT_TEXT_H
+#include
+#include
+
+enum { FACE_NONE, FACE_TORIEL, FACE_FLOWEY, FACE_SANS, FACE_PAPYRUS, FACE_UNDYNE, FACE_ALPHYS, FACE_ASGORE, FACE_MAX };
+
+typedef struct {
+ const char *src; /* the whole text */
+ int page; /* where the page being typed starts in src */
+ int shown; /* letters of the page shown */
+ int len; /* the page's length (up to | or the end) */
+ int pause; /* frames left before the next letter */
+ int x, y, line_h;
+ int font;
+ int speed; /* frames between letters */
+ int t;
+ uint8_t face_expr;
+ bool done; /* the page is all typed */
+ bool finished; /* every page read */
+ bool auto_close;
+ int shake; /* letters shaking (pixels) */
+ int choice, choices; /* the page's choice: selected, how many (0: none) */
+ int choice_at; /* where the choice's options start in src */
+} Writer;
+
+void writer_start(Writer *w, const char *s, int x, int y, int font);
+/* one frame: Z goes on, X shows the page at once; returns true while it isn't finished */
+bool writer_update(Writer *w, uint32_t pressed);
+void writer_draw(const Writer *w, uint16_t color);
+int writer_talking(const Writer *w); /* letters are being typed (a face moves its mouth) */
+
+/* the dialogue box */
+void text_open(int face, const char *s);
+void text_openf(int face, const char *fmt, ...);
+bool text_active(void);
+void text_update(uint32_t pressed);
+void text_draw(void);
+int text_choice(void); /* 0 or 1: the last choice made (a list's: its index, -1 Back) */
+void text_open_menu(const char *const *opts, int n); /* a list of options in the box */
+void text_set_top(int top); /* -1: by where Frisk is (the game's way), 0 bottom, 1 top */
+void text_wrap_width(int w); /* where the next writer's lines wrap (pixels from its left) */
+int text_menu_sel(void); /* (the tests) a list's selection, 100 + a page's choice, -1 neither open */
+int text_menu_cols(void); /* (the list open: its columns) */
+extern char g_name[8]; /* the fallen human's name */
+#endif
diff --git a/games/undertale/src/toriel.c b/games/undertale/src/toriel.c
new file mode 100644
index 00000000..3653db65
--- /dev/null
+++ b/games/undertale/src/toriel.c
@@ -0,0 +1,258 @@
+/* Toriel, the Ruins' boss: her fire, her spares, her words (from the Undertale Wiki's records of the fight). */
+#include
+#include
+#include "battle.h"
+#include "game.h"
+#include "gfx.h"
+#include "monsters.h"
+#include "res.h"
+
+/* b->state[0]: spares; [1]: talks; [2]: 1 she stopped fighting, 2 she gives up, 3 defeated, 4 betrayed */
+static const char *const spare_lines[] = {
+ ".....", "..... .....", "..... ..... .....", "...?", "What is\nthis?", "Fight, or\ngo!",
+ "What do you\nhope to\nshow?", "Fight or\nflee!", "Enough.", "Do not look\nat me so.", "Leave!", "...", "... ...",
+ "I know you\nmiss home...", "Please, go\nupstairs.", "I will look\nafter you.", "It is not\nmuch here,\nbut...",
+ "We could be\nhappy here.", "Why must\nyou make\nthis hard?", "Go upstairs.\nPlease.", ".....", "Ha...",
+ "I cannot\nprotect\neven one\nchild...", "...",
+ "No... I\nsee.|You would\nnot be happy\nhere.|This small\nplace is no\nhome for\nyou.|My worries\ncan wait.|For you, my\nchild."};
+#define NSPARES (int)(sizeof spare_lines / sizeof spare_lines[0])
+
+static void act_toriel(Battle *b, Monster *m, int act, char *t, int cap) {
+ int n = b->state[1]++;
+ if (n == 0) snprintf(t, (size_t)cap, "* You couldn't think of any\n conversation topics.");
+ else if (n == 1) snprintf(t, (size_t)cap, "* You tried to think of\n something to say again,\n but...");
+ else snprintf(t, (size_t)cap, "* Ironically, talking does not\n seem to be the solution to\n this situation.");
+ (void)m;
+}
+
+static const char *talk_toriel(Battle *b, Monster *m) {
+ if (b->state[2] == 3)
+ return "Urgh...|You are\nstrong...|Go on, then.\nKeep going\nuntil you\nreach the\nexit.|And do not\nlet #RASGORE#X\ntake your\nSOUL.|Be good,\nmy child.";
+ if (b->state[2] == 4)
+ return "You...|...when I\nwas\nunguarded...|You are just\nlike the\nothers...|Ha... ha...";
+ if (b->last_action == 3) {
+ int k = b->state[0] - 1;
+ if (k >= 0 && k < NSPARES) return spare_lines[k];
+ }
+ return NULL;
+}
+
+static void spare_toriel(Battle *b, Monster *m) {
+ (void)m;
+ b->state[0]++;
+ if (b->state[0] >= 12) b->no_flee = true;
+}
+
+static const char *flavor_toriel(Battle *b, Monster *m) {
+ /* her DF falls when the SOUL is weak, and when she is */
+ m->df = (int16_t)(sv.hp < 3 ? -15 : m->hp < 150 ? -140 : 1);
+ if (b->state[0] >= 12) return "* ...";
+ return NULL;
+}
+
+/* she uses one of her attacks; her fire falters when the SOUL is weak, and stops after enough spares */
+static int attack_toriel(Battle *b, Monster *m) {
+ if (b->state[2]) return -1;
+ if (b->state[0] >= 12) return -1;
+ if (sv.hp <= 2 || b->state[0] >= 11) return ATK_TOR_FALTER;
+ int k = b->turn % 3;
+ return k == 0 ? ATK_TOR_HANDS : k == 1 ? ATK_TOR_WAVES : ATK_TOR_HELIX;
+}
+
+static bool hit_toriel(Battle *b, Monster *m, int dmg) {
+ /* hit once she stopped fighting: at once, betrayed */
+ if (b->state[0] >= 12) {
+ m->hp = 0, m->hurt = 30;
+ b->state[2] = 4;
+ return true;
+ }
+ if (dmg >= m->hp) {
+ m->hp = 0, m->hurt = 30;
+ b->state[2] = 3;
+ return true;
+ }
+ return false;
+}
+
+static bool ends_toriel(Battle *b, Monster *m) {
+ if (b->state[2] == 3 || b->state[2] == 4) {
+ m->killed = true, m->alive = false, m->dust = 1;
+ set_flag(F_TORIEL_FIGHT, 2);
+ sv.kills++;
+ return true;
+ }
+ if (b->state[0] >= 25) {
+ set_flag(F_TORIEL_FIGHT, 1);
+ m->spare = true;
+ return true;
+ }
+ return false;
+}
+
+static void draw_toriel(Battle *b, Monster *m, int x, int y) {
+ uint16_t s = SPR_TORIEL_B;
+ if (m->hurt) s = SPR_TORIEL_HURT;
+ else if (b->state[2] == 3 || b->state[2] == 4 || m->hp <= 0) s = SPR_TORIEL_DEAD;
+ else if (b->atk == ATK_TOR_FALTER && b->phase >= 10) s = SPR_TORIEL_AWAY;
+ if (m->dust) {
+ int a = 255 - m->dust * 4;
+ if (a > 0) gfx_sprite_color(SPR_TORIEL_DEAD, 0, x, y, 0, C_WHITE, (uint8_t)a);
+ return;
+ }
+ gfx_sprite(s, 0, x, y, 0, 255);
+}
+
+const MonDef toriel_def = {
+ .name = "Toriel", .hp = 440, .at = 6, .df = 1, .exp = 150, .gold = 0, .spr_idle = SPR_TORIEL_B, .spr_hurt = SPR_TORIEL_HURT,
+ .check = "Knows best for you.", .acts = {"Talk"}, .nacts = 1,
+ .neutral = {"* Toriel looks through you.", "* Toriel prepares a magical\n attack.", "* Toriel takes a deep breath.",
+ "* Toriel is acting aloof."},
+ .flags = MF_BOSS | MF_MISS_IS_SPARE, .on_spare = spare_toriel, .act = act_toriel, .talk = talk_toriel, .attack = attack_toriel, .flavor = flavor_toriel,
+ .on_hit = hit_toriel, .ends = ends_toriel, .draw = draw_toriel,
+};
+
+/* ---------------------------------------------------------------- her attacks */
+#define P(v) ((v) * FX)
+static int bx0(void) { return bt.x0 + 3; }
+static int bx1(void) { return bt.x1 - 3; }
+static int by0(void) { return bt.y0 + 3; }
+static int by1(void) { return bt.y1 - 3; }
+
+static Bullet *fire(int x, int y, int vx, int vy, int dmg) {
+ Bullet *b = bullet_new(SPR_FIREBULLET, P(x), P(y), vx, vy);
+ if (b) b->owner = (uint8_t)bt.atk_owner, b->dmg = (int8_t)dmg, b->anim = 4;
+ return b;
+}
+
+/* the paws: from the top-left (and, later, the bottom-right) they sweep across, leaving fire that then flies
+ * at the SOUL and bounces off the walls; touching a paw ends it */
+static void tor_hands(int t) {
+ bool two = bt.turn >= 4;
+ if (t < 0) {
+ board_set(110, 80);
+ return;
+ }
+ int sweep = 40;
+ for (int k = 0; k < (two ? 2 : 1); k++) {
+ if (t <= sweep) {
+ /* the arc: from one top corner to the other, dipping down */
+ int a = t * 128 / sweep; /* half a turn */
+ int cx = board_cx(), r = (bx1() - bx0()) / 2 - 6;
+ int x = cx - r * gfx_cos(a) / 1024, y = (k ? by1() - 6 - r * gfx_sin(a) / 1024 / 3 : by0() + 6 + r * gfx_sin(a) / 1024 / 3);
+ if (k) x = cx + r * gfx_cos(a) / 1024;
+ bt.as[2 * k] = x, bt.as[2 * k + 1] = y;
+ if (t % 4 == 0) {
+ Bullet *b = fire(x, y, 0, 0, 3);
+ if (b) b->kind = BK_SCRIPT, b->a = 0;
+ }
+ }
+ /* the paw touching the SOUL: 4 and the attack is over */
+ int px = (int)bt.as[2 * k], py = (int)bt.as[2 * k + 1];
+ if (t <= sweep + 6 && abs_i(px - soul_x()) < 12 && abs_i(py - soul_y()) < 8) {
+ soul_hurt(damage_to_player(4), 0);
+ bt.atk_t = bt.atk_len;
+ }
+ }
+ if (t == sweep + 4)
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT) continue;
+ int dx = bt.sx - b->x, dy = bt.sy - b->y, d = abs_i(dx) + abs_i(dy);
+ if (!d) d = 1;
+ b->vx = dx * P(2) / d, b->vy = dy * P(2) / d, b->a = 1;
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT || !b->a) continue;
+ b->x += b->vx, b->y += b->vy;
+ if (b->x < P(bx0() + 3) || b->x > P(bx1() - 3)) b->vx = -b->vx;
+ if (b->y < P(by0() + 3) || b->y > P(by1() - 3)) b->vy = -b->vy;
+ if (++b->t > 90) b->kind = BK_NONE;
+ }
+}
+
+/* two streams of fire falling, crossing over each other, with holes to slip through */
+static void tor_waves(int t) {
+ bool hard = bt.turn >= 6;
+ if (t < 0) {
+ board_set(110, hard ? 60 : 80);
+ return;
+ }
+ if (t < 150 && t % 4 == 0) {
+ int w = bx1() - bx0() - 16;
+ for (int s = 0; s < 2; s++) {
+ if ((t / 4 + s * 3) % 9 >= 7) continue; /* the holes */
+ int x = board_cx() + (s ? 1 : -1) * w / 2 * gfx_sin(t * 2) / 1024;
+ Bullet *b = fire(x, by0() - 4, 0, P(3) / 2, 4);
+ if (b) b->kind = BK_SCRIPT;
+ }
+ }
+ if (hard && t % 30 == 0) {
+ fire(bx0() + 3, by0() + 10 + rnd_n(by1() - by0() - 20), 0, 0, 4);
+ fire(bx1() - 3, by0() + 10 + rnd_n(by1() - by0() - 20), 0, 0, 4);
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT) continue;
+ b->t++;
+ if (b->y < P(by1() - 3)) b->y += b->vy;
+ else if (b->t > 120) b->kind = BK_NONE; /* (they gather at the bottom a while) */
+ }
+}
+
+/* thicker streams that stay in their lanes: safe between them */
+static void tor_helix(int t) {
+ if (t < 0) {
+ board_set(120, 80);
+ return;
+ }
+ if (t < 150 && t % 5 == 0) {
+ int lanes = 3, w = bx1() - bx0();
+ for (int l = 0; l < lanes; l++) {
+ int cx = bx0() + w * (2 * l + 1) / (2 * lanes) + 10 * gfx_sin(t * 3 + l * 85) / 1024;
+ for (int k = -1; k <= 1; k++) fire(cx + k * 6, by0() - 4, 0, P(3) / 2, 4);
+ }
+ }
+}
+
+/* her fire falls anywhere, but turns away from the SOUL: she cannot bring herself to hurt it */
+static void tor_falter(int t) {
+ if (t < 0) {
+ board_set(110, 80);
+ return;
+ }
+ if (t < 120 && t % 6 == 0) {
+ Bullet *b = fire(bx0() + 4 + rnd_n(bx1() - bx0() - 8), by0() - 4, 0, P(3) / 2, 0);
+ if (b) b->kind = BK_SCRIPT, b->flags |= BF_HARMLESS;
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT) continue;
+ int dx = b->x - bt.sx, dy = b->y - bt.sy;
+ if (abs_i(dx) < P(16) && abs_i(dy) < P(16)) b->x += dx > 0 ? P(2) : -P(2);
+ b->y += b->vy;
+ if (b->y > P(by1() + 6)) b->kind = BK_NONE;
+ }
+}
+
+void toriel_attack(int atk, int t) {
+ /* a SOUL down to 2 HP: she stops at once */
+ if (atk != ATK_TOR_FALTER && sv.hp <= 2) {
+ bullets_clear();
+ bt.atk_t = bt.atk_len;
+ return;
+ }
+ switch (atk) {
+ case ATK_TOR_HANDS: tor_hands(t); break;
+ case ATK_TOR_WAVES: tor_waves(t); break;
+ case ATK_TOR_HELIX: tor_helix(t); break;
+ case ATK_TOR_FALTER: tor_falter(t); break;
+ }
+}
+
+void toriel_draw_attack(int atk, int t) {
+ if (atk == ATK_TOR_HANDS && t <= 46) {
+ bool two = bt.turn >= 4;
+ for (int k = 0; k < (two ? 2 : 1); k++) gfx_sprite(SPR_HANDBULLET, 0, (int)bt.as[2 * k], (int)bt.as[2 * k + 1], k ? GFX_FLIPX : 0, 255);
+ }
+}
diff --git a/games/undertale/src/waterfall.h b/games/undertale/src/waterfall.h
new file mode 100644
index 00000000..0e769607
--- /dev/null
+++ b/games/undertale/src/waterfall.h
@@ -0,0 +1,22 @@
+/* Undertale: Waterfall (wf1.c: from Snowdin to the bridge Undyne cuts; wf2.c: the dump to Temmie Village; wf3.c:
+ * on to Hotland), its monsters (wfmon.c). */
+#ifndef UT_WATERFALL_H
+#define UT_WATERFALL_H
+#include "event.h"
+void wf1_setup(int room, int from);
+int wf1_event(Ev *c); /* -1: not one of its */
+void wf1_update(void);
+bool wf_seed_enters(Obj *o, int x, int y); /* a bridge seed pushed into the water */
+void wf_seed_moved(Obj *o);
+void wf_battle_done(int form);
+/* wf2.c: past the bridge */
+void wf2_setup(int room, int from);
+int wf2_event(Ev *c); /* -1: not one of its */
+void wf_bird_setup(int room); /* the small bird, on its side of the gap */
+Obj *wf_mk(int x, int y, int dir); /* Monster Kid, Undyne in her armor (their feet at (x, y)) */
+Obj *wf_undyne(int x, int y);
+/* wf3.c: the lantern maze to Hotland's edge */
+void wf3_setup(int room, int from);
+int wf3_event(Ev *c); /* -1: not one of its */
+void wf3_update(void);
+#endif
diff --git a/games/undertale/src/wf1.c b/games/undertale/src/wf1.c
new file mode 100644
index 00000000..2f6b0049
--- /dev/null
+++ b/games/undertale/src/wf1.c
@@ -0,0 +1,769 @@
+/* Waterfall, the west: from Snowdin to the bridge Undyne cuts. Monster Kid, the echo flowers, the stream's rocks,
+ * Papyrus's report to Undyne, the bridge seeds, the wishing room, Undyne's spears, Sans's telescope, Onionsan, the
+ * piano and its artifact, the statue in the rain, Monster Kid under the umbrella, and the bridge. (As the Undertale
+ * Wiki describes them; what they say is ours, in short.) */
+#include
+#include
+#include "battle.h"
+#include "event.h"
+#include "events.h"
+#include "game.h"
+#include "items.h"
+#include "menu.h"
+#include "monsters.h"
+#include "save.h"
+#include "snowdin.h"
+#include "waterfall.h"
+#include "world.h"
+
+static void gone(Obj *o) { o->kind = OBJ_NONE; }
+
+/* who walks about */
+static Obj *obj_mk(int x, int y, int dir) {
+ static const uint16_t s[4] = {SPR_MK_OW, SPR_MK_OW, SPR_MK_OW_L, SPR_MK_OW};
+ return obj_npc(ACTOR_MK, s, x, y, dir, 0);
+}
+static Obj *obj_undyne(int x, int y) {
+ static const uint16_t s[4] = {SPR_UNDYNE_OW, SPR_UNDYNE_OW, SPR_UNDYNE_OW, SPR_UNDYNE_OW};
+ Obj *o = obj_npc(ACTOR_UNDYNE, s, x, y, DIR_DOWN, 0);
+ if (o) o->flags &= (uint16_t)~OF_SOLID;
+ return o;
+}
+Obj *wf_mk(int x, int y, int dir) { return obj_mk(x, y, dir); }
+Obj *wf_undyne(int x, int y) { return obj_undyne(x, y); }
+/* a thing to pick up: its item and the flag that says it's taken (and what's drawn of it, at (x, y)) */
+static void pickup(int x, int y, int w, int h, int item, int fl, uint16_t spr) {
+ if (flag(fl)) return;
+ Obj *o = obj_trigger(x, y, w, h, EV_WF_ITEM, ACTOR_ITEM);
+ if (o) o->flags = OF_TALK | OF_BACK, o->arg = (int16_t)item, o->arg2 = (int16_t)fl, o->spr[0] = spr;
+}
+
+/* ---------------------------------------------------------------- the bridge seeds */
+/* each puzzle: where its water is (the seeds bloom on it) and where its seeds start; solved, the blooms that
+ * stay */
+typedef struct {
+ int room;
+ int16_t wx, wy, ww, wh;
+ int16_t seeds[4][2];
+ int16_t blooms[4][2];
+ int fl;
+} Seeds;
+static const Seeds seed_puzzles[] = {
+ {ROOM_WF_SEEDS1, 2380, 1480, 80, 120, {{2300, 1440}, {2280, 1460}, {2300, 1460}, {2300, 1480}},
+ {{2380, 1500}, {2400, 1500}, {2420, 1500}, {2440, 1500}}, F_SEEDS1},
+ {ROOM_WF_SEEDS2, 2820, 1236, 84, 264, {{2960, 1340}, {2960, 1380}, {3000, 1340}, {3000, 1380}},
+ {{2860, 1260}, {2840, 1260}, {2820, 1260}, {2860, 1280}}, F_SEEDS2},
+};
+static const Seeds *seeds_here(void) {
+ for (unsigned i = 0; i < sizeof seed_puzzles / sizeof seed_puzzles[0]; i++)
+ if (seed_puzzles[i].room == g_room) return &seed_puzzles[i];
+ return NULL;
+}
+/* the quiche's secret: the water under the second puzzle's floor */
+#define SECRET_X 3036
+#define SECRET_Y 1536
+#define SECRET_W 84
+#define SECRET_H 24
+
+static Obj *bloom(int x, int y) {
+ Obj *o = obj_deco(SPR_BLOOM, x, y, false);
+ if (o) o->name = ACTOR_BLOOM, o->flags |= OF_FLOOR | OF_BACK, obj_set_box(o, 0, 0, 20, 20);
+ return o;
+}
+static void seeds_setup(const Seeds *s) {
+ for (int i = 0; i < 4; i++) {
+ if (flag(s->fl)) {
+ bloom(s->blooms[i][0], s->blooms[i][1]);
+ continue;
+ }
+ Obj *o = obj_rock(s->seeds[i][0], s->seeds[i][1], ACTOR_SEED, 0);
+ if (o) {
+ o->spr[0] = SPR_SEED;
+ obj_set_box(o, 2, 4, 16, 14);
+ }
+ }
+ if (g_room == ROOM_WF_SEEDS2 && flag(F_SEEDS3))
+ for (int i = 0; i < 4; i++) bloom(3040 + 20 * i, 1540);
+}
+/* water: in the puzzle's (or the secret's) rectangle, where there's no floor */
+static bool in_water(int x, int y, int ignore) {
+ const Seeds *s = seeds_here();
+ if (!s) return false;
+ bool in = (x >= s->wx && x < s->wx + s->ww && y >= s->wy && y < s->wy + s->wh) ||
+ (g_room == ROOM_WF_SEEDS2 && x >= SECRET_X && x < SECRET_X + SECRET_W && y >= SECRET_Y && y < SECRET_Y + SECRET_H);
+ return in && !world_walkable(x, y, 1, 1, ignore);
+}
+bool wf_seed_enters(Obj *o, int x, int y) {
+ if (o->name != ACTOR_SEED) return false;
+ /* the seed's box over water or floor (nothing solid in the way, but the seed itself) */
+ int me = (int)(o - objs), bx = x + o->bx, by = y + o->by;
+ for (int yy = by;; yy += 4) {
+ if (yy > by + o->bh - 1) yy = by + o->bh - 1;
+ for (int xx = bx;; xx += 4) {
+ if (xx > bx + o->bw - 1) xx = bx + o->bw - 1;
+ if (!in_water(xx, yy, me) && !world_walkable(xx, yy, 1, 1, me)) return false;
+ if (xx == bx + o->bw - 1) break;
+ }
+ if (yy == by + o->bh - 1) break;
+ }
+ return true;
+}
+/* pushed along one way, a seed eases onto the 20-pixel grid the other way (a pixel a push, where it's free), so
+ * it stays in line for the next push */
+static void seed_align(Obj *o) {
+ bool across = pl.dir == DIR_LEFT || pl.dir == DIR_RIGHT;
+ int v = across ? o->y : o->x, to = (v + 10) / 20 * 20;
+ if (v == to) return;
+ int nx = o->x + (across ? 0 : (to > v ? 1 : -1)), ny = o->y + (across ? (to > v ? 1 : -1) : 0);
+ if (world_walkable(nx + o->bx, ny + o->by, o->bw, o->bh, (int)(o - objs)) || wf_seed_enters(o, nx, ny))
+ o->x = (int16_t)nx, o->y = (int16_t)ny;
+}
+
+void wf_seed_moved(Obj *o) {
+ if (o->name != ACTOR_SEED) return;
+ seed_align(o);
+ /* mostly over the water (its edges are ragged): it blooms where its middle is, on the 20-pixel grid, and can be
+ * walked on (and pushed over) */
+ int me = (int)(o - objs), wet = 0;
+ for (int j = 0; j < 4; j++)
+ for (int i = 0; i < 4; i++) wet += in_water(o->x + o->bx + 2 + 4 * i, o->y + o->by + 1 + 4 * j, me);
+ if (wet < 9) return;
+ int cx = o->x + o->bx + o->bw / 2, cy = o->y + o->by + o->bh / 2;
+ gone(o);
+ bloom(cx / 20 * 20, cy / 20 * 20);
+}
+/* solved: the first, its four across the path; the second, Frisk on the far side; the secret, at the quiche */
+static void seeds_watch(void) {
+ if (g_room == ROOM_WF_SEEDS1 && !flag(F_SEEDS1)) {
+ int across = 0;
+ for (int i = 0; i < MAX_OBJS; i++)
+ if (objs[i].kind != OBJ_NONE && objs[i].name == ACTOR_BLOOM && objs[i].y >= 1480 && objs[i].y <= 1520) across++;
+ if (across >= 4) set_flag(F_SEEDS1, 1);
+ }
+ if (g_room == ROOM_WF_SEEDS2 && !flag(F_SEEDS2) && pl.x < 2800 && pl.y < 1300) set_flag(F_SEEDS2, 1);
+}
+
+/* ---------------------------------------------------------------- the stream's rocks */
+#define STREAM_X0 1184
+#define STREAM_X1 1336
+#define STREAM_Y0 1446
+#define STREAM_FALL 1622 /* Frisk's feet past it: over the falls */
+static int stream_t;
+static void stream_update(void) {
+ if (g_room != ROOM_WF_FALLS) return;
+ if (++stream_t % 34 == 0) {
+ Obj *d = obj_deco(SPR_DEBRIS, STREAM_X0 + 8 + rnd_n(STREAM_X1 - STREAM_X0 - 16), STREAM_Y0, false);
+ if (d) d->name = ACTOR_DEBRIS, obj_set_box(d, -8, -6, 16, 12);
+ }
+ int fx = pl.x + 3, fy = pl.y + 20;
+ for (int i = 0; i < MAX_OBJS; i++) {
+ Obj *d = &objs[i];
+ if (d->kind == OBJ_NONE || d->name != ACTOR_DEBRIS) continue;
+ d->y = (int16_t)(d->y + 2);
+ if (d->y > 1650) {
+ gone(d);
+ continue;
+ }
+ /* a rock carries Frisk down with it */
+ if (!event_running() && fx < d->x + 8 && fx + 14 > d->x - 8 && fy < d->y + 6 && fy + 9 > d->y - 6) pl.y = (int16_t)(pl.y + 2);
+ }
+ if (!event_running() && pl.y + 29 > STREAM_FALL && pl.x + 10 > STREAM_X0 && pl.x + 10 < STREAM_X1)
+ event_start(EV_WF_STREAM_FALL, NULL);
+}
+
+/* ---------------------------------------------------------------- Undyne's spears */
+/* a spear comes up where Frisk is going: a glint, then the spear; it hurts (never below 1 HP) */
+typedef struct {
+ int room, from_x, to_x; /* where the spears come */
+ int fl;
+} Chase;
+static const Chase chases[] = {{ROOM_WF_CHASE1, 4150, 6460, F_CHASE1}, {ROOM_WF_BRIDGE, 15600, 17380, F_WF_BRIDGE}};
+static int spear_t, hurt_t;
+static const Chase *chase_here(void) {
+ for (unsigned i = 0; i < sizeof chases / sizeof chases[0]; i++)
+ if (chases[i].room == g_room && !flag(chases[i].fl)) return &chases[i];
+ return NULL;
+}
+static void spears_update(void) {
+ const Chase *ch = chase_here();
+ if (!ch) return;
+ Obj *u = obj_find(ACTOR_UNDYNE);
+ if (pl.x < ch->from_x || pl.x > ch->to_x || event_running()) {
+ if (u && g_room == ROOM_WF_CHASE1 && pl.x >= ch->from_x) u->x = (int16_t)(pl.x - 40);
+ return;
+ }
+ /* she keeps up along the ledge above */
+ if (u && g_room == ROOM_WF_CHASE1) {
+ int want = pl.x - 40;
+ u->x = (int16_t)(u->x + (want > u->x ? min_i(3, want - u->x) : -min_i(3, u->x - want)));
+ }
+ if (hurt_t) hurt_t--;
+ int every = g_room == ROOM_WF_BRIDGE ? 26 : 40;
+ if (++spear_t % every == 0) {
+ int lead = pl.moving ? (pl.dir == DIR_RIGHT ? 30 : pl.dir == DIR_LEFT ? -30 : 0) : 0;
+ Obj *s = obj_deco(SPR_SPEAR, pl.x + 10 + lead, pl.y + 34, false);
+ if (s) s->name = ACTOR_SPEAR, s->arg = 0, s->alpha = 90, obj_set_box(s, -3, -36, 6, 34);
+ }
+ for (int i = 0; i < MAX_OBJS; i++) {
+ Obj *s = &objs[i];
+ if (s->kind == OBJ_NONE || s->name != ACTOR_SPEAR) continue;
+ s->arg++;
+ if (s->arg == 18) s->alpha = 255; /* (the glint, then it's out) */
+ if (s->arg > 40) {
+ gone(s);
+ continue;
+ }
+ if (s->arg >= 18 && !hurt_t && pl.x + 3 < s->x + 3 && pl.x + 17 > s->x - 3 && pl.y + 29 > s->y - 36 && pl.y + 20 < s->y) {
+ sv.hp = (int16_t)max_i(1, sv.hp - 3);
+ hurt_t = 40, g_shake = 6;
+ }
+ }
+ if (g_room == ROOM_WF_BRIDGE && pl.x >= ch->to_x - 4) event_start(EV_WF_BRIDGE_END, NULL);
+}
+
+/* ---------------------------------------------------------------- Monster Kid, along under the umbrella */
+#define TRAIL 32
+static int16_t trail[TRAIL][2];
+static int trail_at;
+static bool mk_follows;
+static void mk_update(void) {
+ if (!mk_follows) return;
+ Obj *mk = obj_find(ACTOR_MK);
+ if (!mk) return;
+ mk->flags &= (uint16_t)~OF_SOLID; /* (walking along behind, never in the way) */
+ if (pl.moving || !trail_at) {
+ trail[trail_at % TRAIL][0] = pl.x, trail[trail_at % TRAIL][1] = pl.y;
+ trail_at++;
+ }
+ if (trail_at < TRAIL) return;
+ const int16_t *p = trail[(trail_at - TRAIL + 4) % TRAIL];
+ int nx = p[0] + 10, ny = p[1] + 29;
+ mk->dir = (uint8_t)(nx < mk->x ? DIR_LEFT : nx > mk->x ? DIR_RIGHT : mk->dir);
+ mk->x = (int16_t)nx, mk->y = (int16_t)ny;
+}
+
+/* ---------------------------------------------------------------- Onionsan, along in the water */
+static void onion_update(void) {
+ if (g_room != ROOM_WF_ONION || flag(F_WF_ONION) != 1) return;
+ Obj *o = obj_find(ACTOR_ONION);
+ if (o && !event_running()) {
+ int want = pl.x + 60;
+ if (o->x < want) o->x = (int16_t)(o->x + 1);
+ }
+ if (!event_running() && pl.x > 9500) event_start(EV_WF_ONION, NULL);
+}
+
+void wf1_update(void) {
+ if (world_room_info()->area != AREA_WATERFALL) return;
+ seeds_watch();
+ stream_update();
+ spears_update();
+ mk_update();
+ onion_update();
+}
+
+/* ---------------------------------------------------------------- the rooms */
+static void echo(int x, int y, const char *says) {
+ static char buf[8][120];
+ static int k;
+ snprintf(buf[k], sizeof buf[k], "* (The echo flower whispers.)|* %s", says);
+ obj_sign(x - 10, y - 14, 20, 40, buf[k]);
+ k = (k + 1) % 8;
+}
+
+void wf1_setup(int room, int from) {
+ (void)from;
+ if (room < ROOM_WF1 || room > ROOM_WF_BRIDGE) return;
+ mk_follows = false, trail_at = 0, spear_t = 0, hurt_t = 0;
+ switch (room) {
+ case ROOM_WF1:
+ set_flag(F_SNOW_EXIT, 1); /* (out of Snowdin) */
+ if (!flag(F_WF_MK)) {
+ obj_mk(731, 1531, DIR_LEFT);
+ obj_trigger(640, 1450, 20, 100, EV_WF_MK, ACTOR_TRIGGER);
+ }
+ obj_sign(718, 1376, 36, 48, "* Echo flowers repeat whatever they heard last.\n* Over and over and over...");
+ echo(769, 1423, "\"...is anybody listening?\"");
+ obj_save(814, 1430, "* (The sound of rushing water fills you with determination.)");
+ {
+ /* (the booth is the map's: talking reaches it from the path below) */
+ Obj *s = obj_trigger(856, 1440, 68, 30, EV_GRILLBYS, ACTOR_SANS);
+ if (s) s->flags = OF_TALK;
+ }
+ obj_sign(1084, 1452, 20, 20, "* (Waterfall ahead.\n* Mind the rocks in the stream.)");
+ {
+ Obj *b = obj_trigger(1112, 1452, 22, 22, EV_BOX, ACTOR_ITEM);
+ if (b) b->flags = OF_TALK | OF_SOLID;
+ }
+ break;
+ case ROOM_WF_FALLS:
+ stream_t = 0;
+ pickup(1240, 1240, 22, 24, ITEM_OLD_TUTU, F_WF_TUTU, SPR_TUTU);
+ break;
+ case ROOM_WF_FALLS_LOW:
+ echo(1450, 1700, "\"Is someone down there?\"");
+ obj_sign(1250, 1652, 20, 18, "* (Something small and shiny is tucked in the planks.\n* A camera?)");
+ break;
+ case ROOM_WF2:
+ obj_save(2035, 1485, "* (The rumbling of the falls fills you with determination.)");
+ if (!flag(F_WF_UNDYNE1)) obj_trigger(1770, 1496, 40, 44, EV_WF_UNDYNE1, ACTOR_TRIGGER);
+ break;
+ case ROOM_WF_SEEDS1:
+ obj_sign(2172, 1452, 20, 20, "* (Bridge seeds bloom when\n they touch the water.)|* (Push them in, one after\n another.)");
+ seeds_setup(&seed_puzzles[0]);
+ break;
+ case ROOM_WF_SEEDS2:
+ obj_sign(2840, 1540, 20, 22, "* (Ring the bell to call the seeds back.)");
+ {
+ Obj *b = obj_trigger(3018, 1188, 24, 26, EV_BELL, ACTOR_ITEM);
+ if (b) b->flags = OF_TALK | OF_SOLID;
+ }
+ seeds_setup(&seed_puzzles[1]);
+ break;
+ case ROOM_WF_QUICHE:
+ /* (reached over the secret bridge: it stays, and leads back) */
+ set_flag(F_SEEDS3, 1);
+ for (int i = 0; i < 4; i++) bloom(3040 + 20 * i, 1540);
+ pickup(3262, 1474, 30, 34, ITEM_QUICHE, F_WF_QUICHE, 0);
+ echo(3330, 1463, "\"I'll wait here for a while.\"");
+ break;
+ case ROOM_WF_WISH:
+ obj_sign(2770, 990, 26, 16, "* (Old writing on the wall: people used to come here to wish on the \"stars\" above.)");
+ echo(2690, 1050, "\"I wish I could see the real sky...\"");
+ echo(2838, 1068, "\"Someday we'll all be free.\"");
+ echo(2928, 1050, "\"I hope my wish comes true.\"");
+ echo(2968, 1050, "\"Please, let me find a friend.\"");
+ echo(3288, 1050, "\"...thank you for listening.\"");
+ obj_sign(3122, 1060, 20, 26, "* (A telescope.\n* The \"stars\" are crystals on the cave's ceiling.)");
+ break;
+ case ROOM_WF_HIST:
+ obj_sign(3090, 486, 30, 16, "* (Long ago, humans and monsters lived side by side.)");
+ obj_sign(3170, 486, 30, 16, "* (Then a war broke out between them.\n* The humans won.)");
+ obj_sign(3250, 486, 30, 16, "* (The monsters were sealed under the mountain by a magic barrier.)");
+ obj_sign(3330, 486, 30, 16, "* (Only power as great as seven human SOULs could break it.)");
+ obj_sign(3410, 486, 30, 16, "* (Still, monsters hope.\n* One day, they will see the sky again.)");
+ {
+ static const uint16_t w[4] = {SPR_WOSHUA_OW, SPR_WOSHUA_OW, SPR_WOSHUA_OW, SPR_WOSHUA_OW};
+ Obj *o = obj_npc(ACTOR_ANY, w, 3454, 580, DIR_DOWN, EV_SIGN);
+ if (o) o->text = "* (Woshua is scrubbing the boardwalk.\n* It doesn't look up.)", o->flags |= OF_ANIM, o->anim_speed = 8;
+ }
+ break;
+ case ROOM_WF_CHASE1:
+ if (!flag(F_CHASE1)) obj_undyne(4180, 470);
+ break;
+ case ROOM_WF_GRASS1:
+ if (!flag(F_CHASE1)) obj_trigger(6640, 540, 30, 72, EV_WF_GRASS, ACTOR_TRIGGER);
+ echo(7219, 503, "\"...is it over? Is it gone?\"");
+ obj_save(7250, 540, "* (The crystals glitter. You're filled with determination.)");
+ obj_sign(7172, 500, 26, 24, "* (A mouse hole.\n* Something squeaks inside.)");
+ obj_sign(7490, 512, 24, 48, "* (The door is locked.)");
+ break;
+ case ROOM_WF_SANS2: {
+ /* (the telescope, the Nice Cream cart, the box are the map's: talked to from the floor beside them) */
+ Obj *t = obj_trigger(7843, 520, 62, 36, EV_WF_TELESCOPE, ACTOR_SANS);
+ if (t) t->flags = OF_TALK | OF_SOLID;
+ Obj *n = obj_trigger(7876, 96, 70, 50, EV_NICECREAM, ACTOR_ITEM);
+ if (n) n->flags = OF_TALK | OF_SOLID;
+ Obj *b = obj_trigger(7928, 540, 26, 22, EV_BOX, ACTOR_ITEM);
+ if (b) b->flags = OF_TALK | OF_SOLID;
+ pickup(8048, 118, 24, 42, ITEM_PUNCH_CARD, F_WF_PUNCH, 0);
+ Obj *m = obj_sign(8074, 554, 18, 12, "* (It's gazing up at the glittering ceiling.)\n* (It doesn't notice you.)");
+ if (m) m->flags |= OF_SOLID;
+ break;
+ }
+ case ROOM_WF_MARSH_TOP:
+ wf_bird_setup(room);
+ break;
+ case ROOM_WF_MARSH:
+ pickup(7578, 1230, 30, 30, ITEM_BALLET_SHOES, F_WF_BALLET, 0);
+ {
+ Obj *h = obj_sign(8826, 812, 50, 26, "* (The humans were afraid of how strong a monster's SOUL might become.)");
+ if (h) h->flags |= OF_SOLID;
+ }
+ echo(8449, 1098, "\"Splish... splash...\"");
+ echo(8449, 1228, "\"Is the water getting higher?\"");
+ echo(8719, 1177, "\"Hello? Hello?\"");
+ break;
+ case ROOM_WF_ONION:
+ if (flag(F_WF_ONION) < 2) {
+ static const uint16_t o[4] = {SPR_ONIONSAN, SPR_ONIONSAN, SPR_ONIONSAN, SPR_ONIONSAN};
+ Obj *on = obj_npc(ACTOR_ONION, o, 8900, 650, DIR_DOWN, 0);
+ if (on) on->flags = OF_BACK, on->alpha = flag(F_WF_ONION) ? 255 : 0;
+ if (!flag(F_WF_ONION)) obj_trigger(8840, 656, 20, 48, EV_WF_ONION, ACTOR_TRIGGER);
+ }
+ break;
+ case ROOM_WF_PIANO: {
+ Obj *p = obj_trigger(10052, 404, 44, 26, EV_PIANO, ACTOR_ITEM);
+ if (p) p->flags = OF_TALK | OF_SOLID;
+ obj_sign(10194, 374, 30, 16, "* (A sign: \"Play the song of the rain.\")");
+ if (!flag(F_PIANO)) {
+ Obj *d = obj_trigger(10118, 330, 64, 50, 0, ACTOR_BLOCK);
+ if (d) d->flags = OF_SOLID;
+ }
+ pickup(10156, 150, 12, 30, ITEM_ARTIFACT, F_WF_ARTIFACT, SPR_ARTIFACT);
+ obj_sign(10194, 572, 30, 16, "* (Monsters fear humans... yet some wish to meet one.)");
+ obj_sign(10374, 610, 30, 22, "* (The barrier can't be crossed by anything with a SOUL... except a human.)");
+ obj_sign(10484, 610, 30, 22, "* (A human SOUL and a monster SOUL together: that would be enough.)");
+ if (!flag(F_SHYREN)) {
+ static const uint16_t s[4] = {SPR_SHYREN_OW, SPR_SHYREN_OW, SPR_SHYREN_OW, SPR_SHYREN_OW};
+ obj_npc(ACTOR_SHYREN, s, 10147, 660, DIR_DOWN, EV_SHYREN);
+ }
+ break;
+ }
+ case ROOM_WF_STATUE: {
+ Obj *st = obj_trigger(10744, 618, 52, 42, EV_STATUE, ACTOR_ITEM);
+ if (st) st->flags = OF_TALK | OF_SOLID;
+ obj_sign(11050, 636, 22, 26, "* (Umbrellas: take one, leave one.)");
+ Obj *u = obj_trigger(11146, 630, 28, 30, EV_UMBRELLA, ACTOR_ITEM);
+ if (u) u->flags = OF_TALK | OF_SOLID;
+ break;
+ }
+ case ROOM_WF_RAIN1:
+ if (!flag(F_MK_RAIN)) {
+ obj_mk(11911, 648, DIR_DOWN);
+ obj_trigger(11880, 660, 60, 80, EV_MK_RAIN, ACTOR_TRIGGER);
+ }
+ break;
+ case ROOM_WF_RAIN2: case ROOM_WF_CASTLE:
+ if (flag(F_MK_RAIN) == 1) {
+ obj_mk(pl.x + 10, pl.y + 29, DIR_DOWN);
+ mk_follows = true;
+ if (room == ROOM_WF_RAIN2) obj_trigger(12740, 1300, 60, 20, EV_MK_TALK, ACTOR_TRIGGER2);
+ else obj_trigger(13600, 1660, 20, 70, EV_MK_TALK, ACTOR_TRIGGER3);
+ }
+ break;
+ case ROOM_WF_AARON:
+ if (flag(F_MK_RAIN) == 1) {
+ obj_mk(pl.x + 10, pl.y + 29, DIR_DOWN);
+ mk_follows = true;
+ obj_trigger(14700, 1660, 20, 70, EV_MK_TALK, ACTOR_TRIGGER);
+ }
+ {
+ static const uint16_t a[4] = {SPR_AARON_OW, SPR_AARON_OW, SPR_AARON_OW, SPR_AARON_OW};
+ Obj *o = obj_npc(ACTOR_ANY, a, 14504, 1682, DIR_DOWN, EV_SIGN);
+ if (o) o->text = "* Hey there, small fry ;)\n* Want to see me flex? ;)";
+ }
+ obj_save(15140, 1640, "* (The bridge ahead looks long.\n* You're filled with determination.)");
+ obj_sign(15070, 1590, 30, 16, "* (A human and a monster may cross the barrier... but none ever has.)");
+ obj_sign(15210, 1590, 30, 16, "* (The king vowed to break the barrier, and set us all free.)");
+ break;
+ case ROOM_WF_BRIDGE:
+ if (!flag(F_WF_BRIDGE)) obj_undyne(15500, 1820);
+ break;
+ }
+}
+
+/* ---------------------------------------------------------------- the events */
+static int ev_wf_mk(Ev *c) {
+ Obj *mk = obj_get(ACTOR_MK);
+ EV_BEGIN;
+ pl.dir = DIR_RIGHT;
+ NARRATE("* Yo! Are you going to see Undyne too?|* She's the head of the Royal Guard!\n* She's SO cool!|* Come on, let's go!");
+ set_flag(F_WF_MK, 1);
+ WALK(mk, 1180, 1496, 4);
+ gone(mk);
+ if (c->self) gone(c->self);
+ EV_END;
+}
+
+static int ev_wf_item(Ev *c) {
+ int item = c->self ? c->self->arg : 0, fl = c->self ? c->self->arg2 : 0;
+ static char buf[120];
+ EV_BEGIN;
+ if (!item_add(item)) {
+ NARRATE("* (You're carrying too much.)");
+ return 1;
+ }
+ set_flag(fl, 1);
+ snprintf(buf, sizeof buf, "* (You got the %s.)", item_info(item)->name);
+ NARRATE(buf);
+ if (c->self) gone(c->self);
+ EV_END;
+}
+
+static int ev_stream_fall(Ev *c) {
+ EV_BEGIN;
+ NARRATE("* (The current sweeps you over the edge!)");
+ FADE_TO(ROOM_WF_FALLS_LOW, 1252, 1640, DIR_DOWN);
+ EV_END;
+}
+
+static int ev_wf_undyne1(Ev *c) {
+ Obj *p = obj_get(ACTOR_PAPYRUS), *u = obj_get(ACTOR_UNDYNE), *mk = obj_get(ACTOR_MK);
+ EV_BEGIN;
+ /* hidden in the tall grass, Frisk hears Papyrus report to Undyne on the ledge above */
+ PL_WALK(1820, 1500, 2);
+ p = obj_papyrus(1770, 1396, DIR_RIGHT);
+ obj_undyne(1822, 1396);
+ WAIT_UNTIL(cam_pan(1640, 1300, 2));
+ WAIT(50);
+ SAY(FACE_PAPYRUS, "`0* H-HI, UNDYNE!\n* I'M HERE WITH MY REPORT!|* THAT HUMAN I TOLD YOU ABOUT...\n* I FOUGHT THEM...|`4* ...AND I, UM, DIDN'T WIN.|`3* WHAT? YOU'LL CAPTURE THEM YOURSELF?|`4* BUT, UNDYNE...|* ...|`0* ...OKAY. I'LL HELP YOU.");
+ WALK(p, 1680, 1396, 2);
+ gone(p);
+ /* she looks down into the grass, right at Frisk */
+ WAIT(40);
+ g_shake = 4;
+ NARRATE("* (Undyne stares into the grass...)");
+ WAIT(40);
+ u = obj_get(ACTOR_UNDYNE);
+ WALK(u, 1900, 1396, 1);
+ gone(u);
+ cam_free();
+ if (flag(F_WF_MK)) {
+ obj_mk(1840, 1530, DIR_LEFT);
+ NARRATE("* Yo! Did you see that?!\n* She was looking right at you!|* That's so awesome!!");
+ mk = obj_get(ACTOR_MK);
+ WALK(mk, 2120, 1520, 4);
+ gone(mk);
+ }
+ set_flag(F_WF_UNDYNE1, 1);
+ if (c->self) gone(c->self);
+ EV_END;
+}
+
+static int ev_bell(Ev *c) {
+ EV_BEGIN;
+ NARRATE("* (Ding!)\n* (The seeds float back where they came from.)");
+ for (int i = 0; i < MAX_OBJS; i++)
+ if (objs[i].kind != OBJ_NONE && (objs[i].name == ACTOR_SEED || objs[i].name == ACTOR_BLOOM)) gone(&objs[i]);
+ set_flag(F_SEEDS2, 0);
+ seeds_setup(&seed_puzzles[1]);
+ EV_END;
+}
+
+static int ev_wf_grass(Ev *c) {
+ Obj *u = obj_get(ACTOR_UNDYNE), *mk = obj_get(ACTOR_MK);
+ EV_BEGIN;
+ /* the end of the first chase: Frisk ducks into the grass; Undyne reaches in... and pulls out Monster Kid */
+ PL_WALK(6700, 560, 3);
+ pl.hidden = true;
+ u = obj_undyne(6580, 520);
+ WALK(u, 6690, 540, 2);
+ WAIT(30);
+ g_shake = 6;
+ NARRATE("* (Undyne reaches into the grass...)");
+ if (flag(F_WF_MK)) {
+ obj_mk(6700, 540, DIR_DOWN);
+ WAIT(30);
+ NARRATE("* (...and pulls out Monster Kid.)");
+ WAIT(20);
+ NARRATE("* (She drops them, and walks away.)");
+ }
+ u = obj_get(ACTOR_UNDYNE);
+ WALK(u, 6560, 520, 2);
+ gone(u);
+ pl.hidden = false;
+ if (flag(F_WF_MK)) {
+ NARRATE("* Yo! She almost got you!\n* ...wait, why WAS she after you?|* Never mind! Let's keep going!");
+ mk = obj_get(ACTOR_MK);
+ WALK(mk, 6900, 580, 4);
+ gone(mk);
+ }
+ set_flag(F_CHASE1, 1);
+ if (c->self) gone(c->self);
+ EV_END;
+}
+
+static int ev_wf_telescope(Ev *c) {
+ EV_BEGIN;
+ if (!flag(F_WF_TELESCOPE)) {
+ SAY(FACE_SANS, "* hey. want to look through\n my telescope? it's free.@Sure@No thanks");
+ if (CHOICE() == 1) {
+ SAY(FACE_SANS, "* suit yourself.");
+ return 1;
+ }
+ NARRATE("* (You look through the telescope.)\n* (It's very dark...)");
+ set_flag(F_WF_TELESCOPE, 1);
+ SAY(FACE_SANS, "`1* heh. you've got a red ring around your eye now.");
+ } else {
+ SAY(FACE_SANS, "* how was the view?");
+ }
+ EV_END;
+}
+
+/* the small bird carries Frisk over the gap: east from the boardwalk (arg 2), west from the Quiet Area's side (1) */
+static int ev_wf_bird(Ev *c) {
+ EV_BEGIN;
+ c->v[1] = c->self ? c->self->arg : 1;
+ NARRATE("* (The little bird offers to carry you over the gap.)@Yes@No");
+ if (CHOICE() == 0) {
+ set_flag(F_WF_BIRD, c->v[1]);
+ if (c->v[1] == 1) FADE_TO(ROOM_WF_MARSH_TOP, 8440, 562, DIR_LEFT);
+ else FADE_TO(ROOM_WF_BIRD2, 18548, 2318, DIR_RIGHT);
+ }
+ EV_END;
+}
+
+static int ev_wf_onion(Ev *c) {
+ Obj *o = obj_get(ACTOR_ONION);
+ EV_BEGIN;
+ if (!flag(F_WF_ONION)) {
+ /* up from the water, a huge friendly face */
+ o->x = (int16_t)(pl.x + 60);
+ for (c->v[0] = 0; c->v[0] < 16; c->v[0]++) {
+ o->alpha = (uint8_t)(c->v[0] * 16);
+ WAIT(2);
+ }
+ o->alpha = 255;
+ NARRATE("* Hey there... you're new around here, huh?|* I'm Onionsan!\n* I live in this big, big water.|* Let's walk together a while!");
+ set_flag(F_WF_ONION, 1);
+ if (c->self) gone(c->self);
+ return 1;
+ }
+ NARRATE("* Someday they'll drain all this water...|* Then I'll live somewhere else.\n* Somewhere nice!|* See you!");
+ for (c->v[0] = 16; c->v[0] > 0; c->v[0]--) {
+ o->alpha = (uint8_t)(c->v[0] * 15);
+ WAIT(2);
+ }
+ gone(o);
+ set_flag(F_WF_ONION, 2);
+ EV_END;
+}
+
+/* the piano: the rain's song (on the statue's sign: middle, up, right, middle, down, down, right) */
+static const char *const notes[] = {"Up", "Down", "Left", "Right", "Middle", "Stop"};
+static const uint8_t song[7] = {4, 0, 3, 4, 1, 1, 3};
+static uint8_t played[7];
+static int ev_piano(Ev *c) {
+ EV_BEGIN;
+ if (flag(F_PIANO)) {
+ NARRATE("* (The piano rings softly.)");
+ return 1;
+ }
+ NARRATE("* (Play the piano?)@Yes@No");
+ if (CHOICE() == 1) return 1;
+ memset(played, 0xFF, sizeof played);
+ for (;;) {
+ CHOOSE(notes, 6);
+ if (CHOSEN() < 0 || CHOSEN() == 5) break;
+ memmove(played, played + 1, 6);
+ played[6] = (uint8_t)CHOSEN();
+ if (!memcmp(played, song, 7)) {
+ set_flag(F_PIANO, 1);
+ for (int i = 0; i < MAX_OBJS; i++)
+ if (objs[i].kind != OBJ_NONE && objs[i].name == ACTOR_BLOCK) gone(&objs[i]);
+ g_shake = 6;
+ NARRATE("* (Somewhere, a door rumbles open.)");
+ break;
+ }
+ }
+ EV_END;
+}
+
+static int ev_shyren(Ev *c) {
+ EV_BEGIN;
+ NARRATE("* (She's humming to herself... she hasn't noticed you.)");
+ BATTLE(FORM_SHYREN);
+ set_flag(F_SHYREN, bt.mon[0].killed ? 2 : 1);
+ gone(obj_get(ACTOR_SHYREN));
+ EV_END;
+}
+
+/* the umbrella (F_UMBRELLA): 1 carried, 2 left with the statue, 3 left there and another carried */
+static bool carrying_umbrella(void) { return flag(F_UMBRELLA) == 1 || flag(F_UMBRELLA) == 3; }
+
+static int ev_statue(Ev *c) {
+ EV_BEGIN;
+ if (flag(F_UMBRELLA) >= 2) {
+ NARRATE("* (The music box plays a gentle song under the umbrella.)");
+ } else if (flag(F_UMBRELLA) == 1) {
+ NARRATE("* (A statue, out in the rain.)\n* (Give it your umbrella?)@Yes@No");
+ if (CHOICE() == 0) {
+ set_flag(F_UMBRELLA, 2);
+ NARRATE("* (You hold the umbrella over the statue.)|* (A music box inside starts to play.)");
+ }
+ } else {
+ NARRATE("* (A statue, out in the rain.)\n* (It looks lonely.)");
+ }
+ EV_END;
+}
+
+static int ev_umbrella(Ev *c) {
+ EV_BEGIN;
+ if (carrying_umbrella()) {
+ NARRATE("* (Umbrellas.)");
+ } else {
+ set_flag(F_UMBRELLA, flag(F_UMBRELLA) ? 3 : 1);
+ NARRATE("* (You took an umbrella.)");
+ }
+ EV_END;
+}
+
+static int ev_mk_rain(Ev *c) {
+ EV_BEGIN;
+ if (!carrying_umbrella()) {
+ NARRATE("* Yo... it's really pouring.|* If only I had an umbrella...");
+ PL_WALK(pl.x - 20, pl.y, 2);
+ return 1;
+ }
+ NARRATE("* Yo! An umbrella!|* Can I walk under it with you? I don't have arms to hold one.|* Thanks, dude!");
+ set_flag(F_MK_RAIN, 1);
+ mk_follows = true, trail_at = 0;
+ if (c->self) gone(c->self);
+ EV_END;
+}
+
+/* what Monster Kid says on the way: down the slope, the castle, the parting */
+static int ev_mk_talk(Ev *c) {
+ int k = c->self ? c->self->name : 0;
+ EV_BEGIN;
+ if (k == ACTOR_TRIGGER2) {
+ NARRATE("* Undyne's so strong... she beats up the bad guys and never loses!|* Someday I'll be just like her.");
+ } else if (k == ACTOR_TRIGGER3) {
+ NARRATE("* Whoa, look!\n* That's the king's castle, way out there!");
+ } else {
+ NARRATE("* Yo, this is where I live.\n* I gotta go home.|* See you later, dude!");
+ set_flag(F_MK_RAIN, 2);
+ mk_follows = false;
+ {
+ Obj *mk = obj_get(ACTOR_MK);
+ WALK_TO(mk, 14300, 1700, 4);
+ }
+ WAIT(40);
+ gone(obj_get(ACTOR_MK));
+ }
+ if (c->self) gone(c->self);
+ EV_END;
+}
+
+static int ev_bridge_end(Ev *c) {
+ EV_BEGIN;
+ obj_undyne(pl.x - 60, pl.y + 30);
+ NARRATE("* (Undyne raises her spear...)");
+ g_shake = 12;
+ WAIT(30);
+ NARRATE("* (...and cuts the bridge from under you!)");
+ set_flag(F_WF_BRIDGE, 1);
+ /* down, into the dark: the garbage dump */
+ world_fade_hold(ROOM_WF_DUMP, 17680, 3214, DIR_DOWN, 60);
+ WAIT_UNTIL(!world_fading());
+ EV_END;
+}
+
+int wf1_event(Ev *c) {
+ switch (c->id) {
+ case EV_WF_MK: return ev_wf_mk(c);
+ case EV_WF_ITEM: return ev_wf_item(c);
+ case EV_WF_STREAM_FALL: return ev_stream_fall(c);
+ case EV_WF_UNDYNE1: return ev_wf_undyne1(c);
+ case EV_BELL: return ev_bell(c);
+ case EV_WF_GRASS: return ev_wf_grass(c);
+ case EV_WF_TELESCOPE: return ev_wf_telescope(c);
+ case EV_WF_BIRD: return ev_wf_bird(c);
+ case EV_WF_ONION: return ev_wf_onion(c);
+ case EV_PIANO: return ev_piano(c);
+ case EV_SHYREN: return ev_shyren(c);
+ case EV_STATUE: return ev_statue(c);
+ case EV_UMBRELLA: return ev_umbrella(c);
+ case EV_MK_RAIN: return ev_mk_rain(c);
+ case EV_MK_TALK: return ev_mk_talk(c);
+ case EV_WF_BRIDGE_END: return ev_bridge_end(c);
+ }
+ return -1;
+}
+
+void wf_battle_done(int form) {
+ (void)form;
+ for (int i = 0; i < bt.nmon; i++)
+ if (bt.mon[i].killed) set_flag(F_KILLS_WATERFALL, flag(F_KILLS_WATERFALL) + 1);
+}
diff --git a/games/undertale/src/wf2.c b/games/undertale/src/wf2.c
new file mode 100644
index 00000000..4ceb5c65
--- /dev/null
+++ b/games/undertale/src/wf2.c
@@ -0,0 +1,262 @@
+/* Waterfall, past the bridge: the garbage dump and Mad Dummy, Napstablook, the Quiet Area and the small bird, Blook
+ * Acres, Gerson's shop, the twin waterfalls, the mushroom maze and Temmie Village. (As the Undertale Wiki describes
+ * them; what they say is ours, in short.) */
+#include
+#include "battle.h"
+#include "event.h"
+#include "events.h"
+#include "game.h"
+#include "items.h"
+#include "monsters.h"
+#include "save.h"
+#include "shop.h"
+#include "waterfall.h"
+#include "world.h"
+
+static void gone(Obj *o) { o->kind = OBJ_NONE; }
+
+/* something on the map to talk to (solid: it stands there) */
+static Obj *thing(int x, int y, int w, int h, int event, int name, int arg) {
+ Obj *o = obj_trigger(x, y, w, h, event, name);
+ if (o) o->flags = OF_TALK | OF_SOLID, o->arg = (int16_t)arg;
+ return o;
+}
+static Obj *solid_sign(int x, int y, int w, int h, const char *text) {
+ Obj *o = obj_sign(x, y, w, h, text);
+ if (o) o->flags |= OF_SOLID;
+ return o;
+}
+
+static Obj *obj_napstablook(int x, int y) {
+ static const uint16_t s[4] = {SPR_NAPSTA_OW, SPR_NAPSTA_OW, SPR_NAPSTA_OW, SPR_NAPSTA_OW};
+ Obj *o = obj_npc(ACTOR_NAPSTA, s, x, y, DIR_DOWN, 0);
+ if (o) o->alpha = 200;
+ return o;
+}
+
+/* the small bird over the gap of bright water (F_WF_BIRD: 1, it waits on the west side) */
+void wf_bird_setup(int room) {
+ bool west = flag(F_WF_BIRD) == 1;
+ if (room == ROOM_WF_MARSH_TOP) {
+ if (west) {
+ obj_deco(SPR_BIRD, 8466, 578, false);
+ thing(8462, 574, 22, 26, EV_WF_BIRD, ACTOR_BIRD, 2);
+ } else {
+ obj_deco(SPR_BIRD, 8544, 578, false); /* (across the gap: out of reach) */
+ }
+ } else if (room == ROOM_WF_BIRD2 && !west) {
+ obj_deco(SPR_BIRD, 18523, 2341, false);
+ thing(18519, 2337, 24, 26, EV_WF_BIRD, ACTOR_BIRD, 1);
+ }
+}
+
+void wf2_setup(int room, int from) {
+ (void)from;
+ if (room < ROOM_WF_DUMP || room > ROOM_WF_TEMMIE) return;
+ switch (room) {
+ case ROOM_WF_DUMP:
+ if (!flag(F_WF_DUMP)) {
+ Obj *t = obj_trigger(17660, 3220, 80, 50, EV_DUMP_WAKE, ACTOR_TRIGGER);
+ if (t) t->flags = OF_TOUCH;
+ }
+ solid_sign(18030, 3292, 80, 44, "* (A heap of garbage.)\n* (A rusty bike sticks out of it.)");
+ solid_sign(18120, 3292, 80, 44, "* (A heap of garbage.)\n* (There's an old computer in there.)");
+ obj_sign(17880, 3250, 24, 20, "* (Water flows in from somewhere far above.)");
+ break;
+ case ROOM_WF_TRASH:
+ obj_save(18540, 3234, "* (Bits of garbage float past and drop into the dark.)\n* (You're filled with determination.)");
+ solid_sign(18560, 3170, 40, 50, "* (A pile of trash.)\n* (Someone left a box of old tapes.)");
+ solid_sign(18690, 3170, 40, 50, "* (A pile of trash.)\n* (A DVD case. Something with claws scratched it.)");
+ break;
+ case ROOM_WF_WALKWAY:
+ if (flag(F_WF_COOLER) < 2) thing(18922, 2816, 36, 22, EV_COOLER, ACTOR_ITEM, 0);
+ else solid_sign(18922, 2816, 36, 22, "* (An empty cooler.)");
+ solid_sign(18916, 2736, 60, 60, "* (A heap of garbage.)\n* (It smells like old rain.)");
+ solid_sign(18824, 2836, 46, 58, "* (A heap of garbage.)\n* (A deflated ball is stuck on top.)");
+ if (!flag(F_MAD_DUMMY)) {
+ static const uint16_t s[4] = {SPR_MADDUMMY_OW, SPR_MADDUMMY_OW, SPR_MADDUMMY_OW, SPR_MADDUMMY_OW};
+ obj_npc(ACTOR_MADDUMMY, s, 18929, 2730, DIR_DOWN, EV_MAD_DUMMY);
+ }
+ break;
+ case ROOM_WF_QUIET:
+ obj_save(18900, 2340, "* (The quiet of this place fills you with determination.)");
+ obj_sign(18966, 2256, 22, 24, "* (West: the bird.\n North: the houses.\n East: the shop.)");
+ break;
+ case ROOM_WF_BIRD2:
+ wf_bird_setup(room);
+ break;
+ case ROOM_WF_ACRES:
+ thing(18568, 2060, 32, 44, EV_LOCKED_HOUSE, ACTOR_DOOR, 0);
+ thing(18926, 2080, 26, 30, EV_LOCKED_HOUSE, ACTOR_DOOR, 1);
+ obj_sign(18450, 2124, 64, 34, "* (A doormat shaped like a fish.)");
+ obj_sign(19200, 2020, 30, 30, "* (The snail farm.\n* Thundersnail races: ask the ghost.)");
+ if (flag(F_MAD_DUMMY)) {
+ Obj *n = obj_napstablook(19316, 2092);
+ if (n) n->event = EV_SNAILS, n->flags |= OF_TALK, obj_set_box(n, 0, 20, 26, 36); /* (over the fence) */
+ }
+ break;
+ case ROOM_WF_NAPSTA_IN:
+ solid_sign(18820, 1790, 30, 36, "* (A fridge.)\n* (There's a ghost sandwich inside. Your hand goes right through it.)");
+ solid_sign(18866, 1800, 32, 30, "* (An old TV, showing a ghost show.)");
+ solid_sign(18930, 1786, 36, 30, "* (A computer with a song playing on it.)\n* (It sounds spooky... and kind of nice.)");
+ if (flag(F_NAPSTA_HOME)) {
+ Obj *n = obj_napstablook(18969, 1810);
+ if (n) n->event = EV_NAPSTA_ROOM, n->flags |= OF_TALK;
+ }
+ break;
+ case ROOM_WF_GERSON:
+ thing(19356, 2256, 44, 50, EV_GERSON, ACTOR_DOOR, 0); /* (the shop's doorway) */
+ thing(19384, 2286, 26, 20, EV_BOX, ACTOR_ITEM, 0);
+ obj_sign(19690, 2288, 30, 20, "* (Gerson's shop, behind you.\n* Waterfall goes on, ahead.)");
+ break;
+ case ROOM_WF_TWIN:
+ obj_sign(19860, 2290, 30, 20, "* (The water here is shallow. You wade on.)");
+ break;
+ case ROOM_WF_TEMMIE:
+ set_flag(F_TEM_VILLAGE, 1);
+ obj_save(21325, 3120, "* (The Temmies' cheer fills you with determination.)");
+ thing(21360, 3118, 26, 24, EV_BOX, ACTOR_ITEM, 0);
+ thing(21334, 3020, 48, 56, EV_TEM_SHOP, ACTOR_DOOR, 0);
+ obj_sign(21176, 3002, 78, 34, "* (A painting of a Temmie, very proud of itself.)");
+ obj_sign(21122, 3062, 22, 26, "* (Welcome to Temmie Village!)");
+ obj_sign(21316, 3056, 24, 26, "* (Tem Shop! Tem sells!)");
+ for (int i = 0; i < 4; i++) {
+ static const int16_t tx[4] = {21168, 21202, 21234, 21268}, ty[4] = {3072, 3080, 3068, 3074};
+ thing(tx[i], ty[i], 24, 30, EV_TEMMIE, ACTOR_TEMMIE, i);
+ }
+ break;
+ }
+}
+
+/* ---------------------------------------------------------------- the events */
+static int ev_dump_wake(Ev *c) {
+ EV_BEGIN;
+ /* (a dream while Frisk lies there) */
+ NARRATE("* (...)|* (In the dark, a voice keeps calling someone's name.)\n* (It isn't yours.)");
+ WAIT(30);
+ NARRATE("* (You wake up on a bed of golden flowers.)\n* (Garbage floats all around.)");
+ set_flag(F_WF_DUMP, 1);
+ if (c->self) gone(c->self);
+ EV_END;
+}
+
+static int ev_cooler(Ev *c) {
+ EV_BEGIN;
+ NARRATE("* (A cooler, with Astronaut\n Food inside. Take some?)@Yes@No");
+ if (CHOICE() == 0) {
+ if (!item_add(ITEM_ASTRONAUT_FOOD)) {
+ NARRATE("* (You're carrying too much.)");
+ } else {
+ set_flag(F_WF_COOLER, flag(F_WF_COOLER) + 1);
+ NARRATE("* (You got the Astronaut Food.)");
+ if (flag(F_WF_COOLER) >= 2 && c->self) gone(c->self);
+ }
+ }
+ EV_END;
+}
+
+static int ev_mad_dummy(Ev *c) {
+ Obj *d = obj_get(ACTOR_MADDUMMY), *n = obj_get(ACTOR_NAPSTA);
+ EV_BEGIN;
+ NARRATE("* (It's a training dummy.)\n* (Hit it?)@Yes@No");
+ g_shake = 6;
+ SAY(FACE_NONE, "* Hey! HEY! You!|* You're the one who picked on my cousin back in the Ruins!|* Ghosts like us live in dummies, you know! And you... you...|* GRR! Dummies, get them!!");
+ BATTLE(FORM_MAD_DUMMY);
+ /* Napstablook drifts in; their tears soak Mad Dummy, who storms off */
+ n = obj_napstablook(d->x - 50, d->y);
+ NARRATE("* (Napstablook drifts in, crying softly.)|* (Their tears rain down on Mad Dummy.)");
+ SAY(FACE_NONE, "* Ugh! Gross! My dummy's all wet!|* This isn't over! I'll be back!!");
+ d = obj_get(ACTOR_MADDUMMY);
+ WALK(d, d->x, d->y - 120, 3);
+ gone(obj_get(ACTOR_MADDUMMY));
+ set_flag(F_MAD_DUMMY, 1);
+ SAY(FACE_NONE, "* oh... sorry... i didn't mean to get in the way...|* um... i live just up north, past the pond.|* ...if you want, you could come over.\n* only if you want to...");
+ set_flag(F_NAPSTA_HOME, 1);
+ for (c->v[0] = 200; c->v[0] > 0; c->v[0] -= 10) {
+ n = obj_get(ACTOR_NAPSTA);
+ n->alpha = (uint8_t)c->v[0];
+ WAIT(2);
+ }
+ gone(obj_get(ACTOR_NAPSTA));
+ EV_END;
+}
+
+static int ev_napsta_room(Ev *c) {
+ EV_BEGIN;
+ if (flag(F_NAPSTA_HOME) >= 2) {
+ SAY(FACE_NONE, "* oh... hi again.\n* it's nice to have company... i think.");
+ return 1;
+ }
+ SAY(FACE_NONE, "* oh... you really came.|* uh... this is my room. it's not much.|* sometimes i just lie on\n the floor for a while...|* want to do that with me?@Sure@No thanks");
+ if (CHOICE() == 0) {
+ NARRATE("* (You lie down on the floor next to Napstablook.)");
+ WAIT(60);
+ NARRATE("* (Time passes.)\n* (It's oddly peaceful.)");
+ SAY(FACE_NONE, "* ...thanks. that was\n really nice.|* i don't get to do that\n with anyone.");
+ set_flag(F_NAPSTA_HOME, 2);
+ } else {
+ SAY(FACE_NONE, "* oh... that's okay...");
+ }
+ EV_END;
+}
+
+static int ev_locked_house(Ev *c) {
+ EV_BEGIN;
+ if (c->self && c->self->arg == 0) NARRATE("* (The door is locked.)\n* (Someone inside is clanking on something metal.)");
+ else NARRATE("* (The door is locked tight.)\n* (It has a big star on it.)");
+ EV_END;
+}
+
+/* Thundersnail: 10G a race; cheering doesn't help much */
+static int ev_snails(Ev *c) {
+ EV_BEGIN;
+ SAY(FACE_NONE, "* oh... hi. this is our snail farm.|* want to race the snails? it's 10G...@Race@No");
+ if (CHOICE() == 1) return 1;
+ if (sv.gold < 10) {
+ SAY(FACE_NONE, "* oh... you don't have enough money... that's okay.");
+ return 1;
+ }
+ sv.gold = (uint16_t)(sv.gold - 10);
+ set_flag(F_SNAIL_RACE, flag(F_SNAIL_RACE) + 1);
+ NARRATE("* (The snails are off!)|* (Your yellow snail creeps forward...)");
+ WAIT(40);
+ if (rnd_n(2)) {
+ sv.gold = (uint16_t)(sv.gold + 9);
+ SAY(FACE_NONE, "* oh... your snail won...\n here's 9G.|* ...sorry, that's all the\n prize money.");
+ } else {
+ SAY(FACE_NONE, "* oh... it lost. that's how it goes sometimes.");
+ }
+ EV_END;
+}
+
+static int ev_shop_door(Ev *c) {
+ EV_BEGIN;
+ shop_open(c->id == EV_GERSON ? SHOP_GERSON : SHOP_TEM);
+ WAIT_UNTIL(g_mode == MODE_WORLD);
+ EV_END;
+}
+
+static int ev_temmie(Ev *c) {
+ static const char *const lines[4] = {
+ "* hOI!!! im temmie!!|* and dis is my friend... temmie!!",
+ "* hOI!! im temmie!!\n* don forget my friend!!",
+ "* hOI!!!\n* yaya!!! human!!! so cute!!",
+ "* hOI... tem is allergic to temmies...|* but tem still luvs dem!!"};
+ EV_BEGIN;
+ NARRATE(lines[(c->self ? c->self->arg : 0) & 3]);
+ EV_END;
+}
+
+int wf2_event(Ev *c) {
+ switch (c->id) {
+ case EV_DUMP_WAKE: return ev_dump_wake(c);
+ case EV_COOLER: return ev_cooler(c);
+ case EV_MAD_DUMMY: return ev_mad_dummy(c);
+ case EV_NAPSTA_ROOM: return ev_napsta_room(c);
+ case EV_LOCKED_HOUSE: return ev_locked_house(c);
+ case EV_SNAILS: return ev_snails(c);
+ case EV_GERSON: case EV_TEM_SHOP: return ev_shop_door(c);
+ case EV_TEMMIE: return ev_temmie(c);
+ }
+ return -1;
+}
diff --git a/games/undertale/src/wf3.c b/games/undertale/src/wf3.c
new file mode 100644
index 00000000..e8a4670b
--- /dev/null
+++ b/games/undertale/src/wf3.c
@@ -0,0 +1,369 @@
+/* Waterfall, its end: the lantern maze, the dead end where Undyne finds Frisk (and Monster Kid gets in her way),
+ * the echo flowers' path, the chasm's bridge, Undyne on her crag and her fight, the chase by the river and the
+ * neon sign, and Hotland's edge, where she falls in the heat. (As the Undertale Wiki describes them; what they say
+ * is ours, in short.) */
+#include
+#include "battle.h"
+#include "event.h"
+#include "events.h"
+#include "game.h"
+#include "menu.h"
+#include "monsters.h"
+#include "save.h"
+#include "snowdin.h"
+#include "waterfall.h"
+#include "world.h"
+
+static void gone(Obj *o) { o->kind = OBJ_NONE; }
+
+#define ECHO(s) "* (The echo flower whispers.)|* " s
+static void echo(int x, int y, const char *text) { obj_sign(x - 10, y - 14, 20, 40, text); }
+
+/* ---------------------------------------------------------------- Undyne, after Frisk */
+/* fled from her fight, she chases Frisk through the arena, the river and the neon sign's hall: slower than Frisk,
+ * but a catch is another fight */
+static bool chasing;
+static int stun_t;
+static void chase_update(void) {
+ if (!chasing || event_running()) return;
+ Obj *u = obj_find(ACTOR_UNDYNE);
+ if (!u) return;
+ if (stun_t) {
+ stun_t--;
+ return;
+ }
+ int dx = pl.x + 10 - u->x, dy = pl.y + 29 - u->y;
+ u->x = (int16_t)(u->x + clamp_i(dx, -2, 2)), u->y = (int16_t)(u->y + clamp_i(dy, -2, 2));
+ if (abs_i(dx) < 14 && abs_i(dy) < 12) event_start(EV_UNDYNE_CATCH, u);
+}
+/* she comes in after Frisk, from the way Frisk came */
+static void chase_setup(void) {
+ if (flag(F_UNDYNE) != 1) return;
+ static const int8_t back[4][2] = {{0, -1}, {0, 1}, {1, 0}, {-1, 0}};
+ chasing = true, stun_t = 30;
+ wf_undyne(pl.x + 10 + back[pl.dir][0] * 70, pl.y + 29 + back[pl.dir][1] * 70);
+}
+
+/* Monster Kid on the chasm's bridge: how it went (EV_MK_CHASM's v[0]: 0 they meet, 1 Frisk helps them up, 3
+ * Undyne gets there first, 4 Frisk runs off) */
+static int mk_how;
+
+void wf3_update(void) {
+ if (g_room < ROOM_WF_LANTERN || g_room > ROOM_HT_ENTRANCE) return;
+ chase_update();
+ if (g_room == ROOM_WF_CHASM && flag(F_MK_CHASM) == 1 && !event_running()) {
+ Obj *u = obj_find(ACTOR_UNDYNE);
+ if (pl.x < 22330) mk_how = 4, event_start(EV_MK_CHASM, NULL);
+ else if (u && (!u->walking || u->x - (pl.x + 10) < 24)) mk_how = 3, event_start(EV_MK_CHASM, NULL);
+ }
+}
+
+/* ---------------------------------------------------------------- the rooms */
+void wf3_setup(int room, int from) {
+ (void)from;
+ chasing = false;
+ if (room < ROOM_WF_LANTERN || room > ROOM_HT_ENTRANCE) return;
+ switch (room) {
+ case ROOM_WF_LANTERN:
+ obj_sign(21176, 2414, 48, 28, "* (The lanterns light the way\n through the crystals.)|* (Their glow is dim, but\n it's enough.)");
+ {
+ static const int16_t lx[5] = {21260, 21440, 21280, 21680, 21720}, ly[5] = {2440, 2640, 2740, 2800, 2420};
+ for (int i = 0; i < 5; i++) obj_sign(lx[i], ly[i], 20, 20, "* (A lantern, glowing softly.)");
+ }
+ break;
+ case ROOM_WF_DEADEND: {
+ /* the end of the dark: an echo flower; the way north hidden in the grass until Undyne has come */
+ Obj *e = obj_deco(SPR_ECHO_OW, 22420, 2686, true);
+ if (e) obj_set_box(e, -8, -8, 16, 8);
+ if (!flag(F_DEADEND)) {
+ Obj *f = obj_trigger(22408, 2650, 24, 40, EV_DEADEND, ACTOR_TRIGGER);
+ if (f) f->flags = OF_TALK;
+ Obj *b = obj_trigger(22160, 2556, 62, 64, EV_NONE, ACTOR_TRIGGER2);
+ if (b) b->flags = OF_SOLID | OF_HIDDEN;
+ } else {
+ echo(22420, 2666, ECHO("\"...\"\n* (It only gives back the quiet now.)"));
+ }
+ break;
+ }
+ case ROOM_WF_ECHOPATH:
+ echo(22170, 2150, ECHO("\"It's so quiet back here.\n Does anyone ever come this way?\""));
+ echo(22210, 2230, ECHO("\"One day I'll see the stars.\n The real ones, up on the surface.\""));
+ echo(22170, 2330, ECHO("\"Shh... did you hear that?\n Footsteps...\""));
+ echo(22210, 2370, ECHO("\"If you could wish for anything...\n what would it be?\""));
+ break;
+ case ROOM_WF_CHASM:
+ /* the glyph at the end of the echo flowers' path, a SAVE, Monster Kid on the bridge */
+ obj_sign(22180, 1978, 38, 44,
+ "* (Ancient glyphs, about a prophecy.)|* (An angel will come down\n from the surface...)|* (...and the underground\n will go empty.)");
+ echo(22210, 2070, ECHO("\"Yo... where'd everybody go?\""));
+ obj_save(22300, 2000, "* (The roar of the chasm fills you with determination.)");
+ if (flag(F_MK_CHASM) == 1) set_flag(F_MK_CHASM, 4); /* (left in the middle of it) */
+ if (!flag(F_MK_CHASM)) {
+ wf_mk(22560, 2058, DIR_LEFT);
+ obj_trigger(22396, 1990, 12, 110, EV_MK_CHASM, ACTOR_TRIGGER);
+ }
+ break;
+ case ROOM_WF_ARENA:
+ obj_save(23520, 2038, "* (Undyne's crag looms over you.)\n* (You're filled with determination.)");
+ if (!flag(F_UNDYNE)) {
+ wf_undyne(23455, 1570);
+ obj_trigger(23300, 1980, 12, 100, EV_ARENA, ACTOR_TRIGGER);
+ }
+ chase_setup();
+ break;
+ case ROOM_WF_NORTH:
+ chase_setup();
+ break;
+ case ROOM_WF_HOTGATE:
+ obj_sign(23648, 672, 505, 100,
+ flag(F_UNDYNE) == 2 ? "* (A neon sign. It's switched off.)" : "* (A neon sign.\n* It says WELCOME TO HOTLAND!)");
+ if (!flag(F_PAP_NEON)) obj_trigger(23700, 760, 12, 70, EV_PAP_NEON, ACTOR_TRIGGER2);
+ chase_setup();
+ break;
+ case ROOM_HT_ENTRANCE:
+ obj_sign(130, 6140, 60, 92, "* (Sans's other sentry station.)\n* (He's here too, somehow.)");
+ {
+ Obj *k = obj_trigger(718, 6162, 30, 46, EV_HOT_COOLER, ACTOR_ITEM);
+ if (k) k->flags = OF_TALK | OF_SOLID;
+ }
+ if (flag(F_UNDYNE) == 1) {
+ obj_trigger(0, 6100, 1460, 300, EV_HOT_UNDYNE, ACTOR_TRIGGER); /* (she's right behind) */
+ } else if (flag(F_UNDYNE) == 3) {
+ Obj *f = obj_deco(SPR_UNDYNE_FALLEN, 534, 6213, false);
+ if (f) f->name = ACTOR_UNDYNE, f->event = EV_HOT_UNDYNE, f->flags |= OF_TALK, obj_set_box(f, 0, 6, 50, 20);
+ }
+ break;
+ }
+}
+
+/* ---------------------------------------------------------------- the events */
+static int ev_deadend(Ev *c) {
+ Obj *u = obj_get(ACTOR_UNDYNE), *mk = obj_get(ACTOR_MK);
+ EV_BEGIN;
+ /* the echo flower at the end; the room lights up, and Undyne is there */
+ NARRATE("* (The echo flower gives back the sound of your own footsteps.)");
+ WAIT(20);
+ pl.dir = DIR_LEFT;
+ u = wf_undyne(pl.x - 170, pl.y + 29);
+ WALK(u, pl.x - 50, pl.y + 29, 2);
+ WAIT(20);
+ NARRATE("* (Undyne raises her spear.)");
+ /* ...and Monster Kid jumps out of the grass, right between them */
+ mk = wf_mk(21960, pl.y + 31, DIR_RIGHT);
+ WALK(mk, pl.x - 22, pl.y + 31, 6);
+ mk->dir = DIR_LEFT;
+ NARRATE("* Yo!! Undyne!! You're here!|* Are you gonna fight somebody?!\n* Can I watch? Please please please?!");
+ WAIT(15);
+ NARRATE("* (Undyne grabs Monster Kid by the arm...)");
+ u = obj_get(ACTOR_UNDYNE), mk = obj_get(ACTOR_MK);
+ WALK_TO(mk, 21840, mk->y, 2);
+ WALK(u, 21810, u->y, 2);
+ NARRATE("* (...and drags them away.)");
+ gone(obj_get(ACTOR_UNDYNE)), gone(obj_get(ACTOR_MK));
+ set_flag(F_DEADEND, 1);
+ gone(obj_get(ACTOR_TRIGGER2));
+ if (c->self) gone(c->self);
+ WAIT(20);
+ NARRATE("* (In the grass, a hidden path lights up.)");
+ EV_END;
+}
+
+static int ev_mk_chasm(Ev *c) {
+ Obj *u = obj_get(ACTOR_UNDYNE), *mk = obj_get(ACTOR_MK);
+ EV_BEGIN;
+ c->v[0] = c->self && c->self->name == ACTOR_MK ? 1 : mk_how;
+ mk_how = 0;
+ if (c->v[0] == 0) {
+ /* they meet on the bridge: Monster Kid has learned what Frisk is */
+ if (c->self) gone(c->self);
+ WALK(mk, pl.x + 56, mk->y, 3);
+ NARRATE("* Yo... I gotta ask you something.|* Undyne told me to stay away from you.\n* She says you're a human.|* ...|* So that means... I gotta be your enemy now, right?|* Okay! Here goes! You're a... you're a big...|* ...ugh. I can't think of anything mean.|* Forget it! See ya!");
+ mk = obj_get(ACTOR_MK);
+ mk->dir = DIR_RIGHT;
+ WALK(mk, 22639, mk->y, 4);
+ /* ...and trips over the edge */
+ g_shake = 4;
+ mk->y = 2084;
+ obj_set_box(mk, -10, -30, 20, 30);
+ mk->event = EV_MK_CHASM, mk->flags = OF_TALK;
+ NARRATE("* (Monster Kid trips over the edge of the bridge!)|* Y-yo... a little help...?");
+ u = wf_undyne(22900, 2058);
+ WALK_TO(u, 22672, 2058, 1);
+ set_flag(F_MK_CHASM, 1);
+ NARRATE("* (Undyne is coming across the bridge.)");
+ return 1;
+ }
+ if (c->v[0] == 1) {
+ /* Frisk pulls them up: Monster Kid stands up to Undyne, and she backs off */
+ u->walking = 0;
+ NARRATE("* (You pull Monster Kid back up.)");
+ mk->x = (int16_t)(pl.x + 40), mk->y = 2058, mk->dir = DIR_RIGHT, obj_set_box(mk, -6, -8, 12, 8);
+ WAIT(10);
+ NARRATE("* Undyne...|* If you wanna hurt them...\n* you'll have to get through me first!");
+ WAIT(20);
+ NARRATE("* (Undyne hesitates... then turns away.)");
+ u = obj_get(ACTOR_UNDYNE);
+ WALK(u, 22920, u->y, 3);
+ gone(u);
+ mk = obj_get(ACTOR_MK);
+ mk->dir = DIR_LEFT;
+ NARRATE("* Yo... thanks for saving me.|* We're still friends, right? Cool.|* I'd better get home before my parents worry. Later!");
+ mk = obj_get(ACTOR_MK);
+ WALK(mk, 22920, mk->y, 4);
+ gone(mk);
+ set_flag(F_MK_CHASM, 2);
+ return 1;
+ }
+ if (c->v[0] == 3) {
+ /* Undyne gets there first: Monster Kid falls, and she dives after them */
+ NARRATE("* (Monster Kid can't hold on any more...)");
+ gone(mk);
+ WALK(u, 22650, u->y, 4);
+ NARRATE("* (Undyne dives off the bridge after them!)");
+ gone(obj_get(ACTOR_UNDYNE));
+ WAIT(45);
+ g_shake = 6;
+ u = wf_undyne(22650, 2058);
+ wf_mk(22676, 2058, DIR_LEFT);
+ WAIT(15);
+ NARRATE("* (Undyne climbs back up, Monster Kid with her.)\n* (She's badly hurt.)");
+ NARRATE("* Undyne... you were right about them.|* They just stood there and watched me fall.");
+ u = obj_get(ACTOR_UNDYNE), mk = obj_get(ACTOR_MK);
+ WALK_TO(mk, 22940, mk->y, 2);
+ WALK(u, 22920, u->y, 2);
+ gone(obj_get(ACTOR_UNDYNE)), gone(obj_get(ACTOR_MK));
+ set_flag(F_MK_CHASM, 3);
+ return 1;
+ }
+ /* Frisk ran off: behind, Undyne helps Monster Kid up */
+ gone(mk), gone(u);
+ NARRATE("* (Behind you, Undyne pulls Monster Kid back up.)");
+ set_flag(F_MK_CHASM, 4);
+ EV_END;
+}
+
+/* after a fight: killed, she's gone; fled, she comes after Frisk */
+static int undyne_after(void) {
+ Obj *u = obj_find(ACTOR_UNDYNE);
+ if (bt.mon[0].killed) {
+ set_flag(F_UNDYNE, 2);
+ chasing = false;
+ if (u) gone(u);
+ return 2;
+ }
+ set_flag(F_UNDYNE, 1);
+ chasing = true, stun_t = 50;
+ /* (she gets up behind Frisk, a little back the way Frisk came) */
+ if (!u) u = wf_undyne(0, 0);
+ u->x = (int16_t)(pl.x - 60), u->y = (int16_t)(pl.y + 29);
+ return 1;
+}
+
+static int ev_arena(Ev *c) {
+ EV_BEGIN;
+ if (c->self) gone(c->self);
+ /* Undyne, on top of her crag */
+ WAIT_UNTIL(cam_pan(23290, 1440, 4));
+ WAIT(15);
+ NARRATE("* (Undyne stands on top of the crag.)");
+ NARRATE("* You. Human.|* King Asgore has six human SOULs already.|* With one more, he can break the barrier\n and set every monster free.|* So yours is coming with me.|* I've chased you all through Waterfall.\n* No more running.|* Get ready, human! Here I come!");
+ NARRATE("* (Undyne leaps down from the crag!)");
+ gone(obj_get(ACTOR_UNDYNE));
+ WAIT_UNTIL(cam_pan(23250, 1840, 8));
+ cam_free();
+ g_shake = 8;
+ wf_undyne(pl.x + 50, pl.y + 29);
+ WAIT(15);
+ BATTLE(FORM_UNDYNE);
+ if (undyne_after() == 2) {
+ NARRATE("* Heh... so that's how it ends...|* Alphys... the rest... is on you...");
+ NARRATE("* (Undyne melts away.)");
+ } else {
+ stun_t = 50;
+ NARRATE("* (Undyne isn't letting you get away that easily!)");
+ }
+ EV_END;
+}
+
+static int ev_undyne_catch(Ev *c) {
+ EV_BEGIN;
+ NARRATE("* (Undyne catches up with you!)");
+ BATTLE(FORM_UNDYNE);
+ if (undyne_after() == 2) NARRATE("* Heh... so that's how it ends...|* Alphys... the rest... is on you...|* (Undyne melts away.)");
+ EV_END;
+}
+
+static int ev_pap_neon(Ev *c) {
+ EV_BEGIN;
+ if (c->self) gone(c->self);
+ set_flag(F_PAP_NEON, 1);
+ SAY(FACE_PAPYRUS, "* Ring...|`0* HUMAN! PAPYRUS HERE!|* I'VE BEEN THINKING ABOUT FRIENDSHIP.|`2* AND I THINK YOU AND\n UNDYNE WOULD BE THE BEST\n OF FRIENDS!!!|* WE SHOULD ALL GET TOGETHER SOMETIME!|`0* WELL! BYE!");
+ if (chasing) NARRATE("* (Undyne waited politely for you to hang up.)");
+ EV_END;
+}
+
+/* Hotland's heat: Undyne, after Frisk in her armor, falls on the bridge; the cooler's water gets her up */
+static int ev_hot_undyne(Ev *c) {
+ Obj *u = obj_get(ACTOR_UNDYNE);
+ EV_BEGIN;
+ if (flag(F_UNDYNE) == 1) {
+ if (c->self) gone(c->self);
+ PL_WALK(640, 6206, 3);
+ pl.dir = DIR_LEFT;
+ u = wf_undyne(4, 6236);
+ WALK(u, 300, 6236, 2);
+ WALK(u, 556, 6236, 1);
+ g_shake = 6;
+ gone(u);
+ u = obj_deco(SPR_UNDYNE_FALLEN, 534, 6213, false);
+ if (u) u->name = ACTOR_UNDYNE, u->event = EV_HOT_UNDYNE, u->flags |= OF_TALK, obj_set_box(u, 0, 6, 50, 20);
+ set_flag(F_UNDYNE, 3);
+ NARRATE("* (Undyne collapses on the\n bridge.)|* (In all that armor, the\n heat is too much for her.)");
+ return 1;
+ }
+ if (flag(F_WATER_CUP) != 1) {
+ NARRATE("* (Undyne lies there, dried out by the heat.)");
+ return 1;
+ }
+ NARRATE("* (Pour the water on her?)@Pour@Don't");
+ if (CHOICE() == 1) return 1;
+ set_flag(F_WATER_CUP, 2);
+ NARRATE("* (You pour the water on Undyne.)");
+ WAIT(30);
+ gone(u);
+ u = wf_undyne(558, 6238);
+ WAIT(20);
+ NARRATE("* (Undyne gets up... and stares at you for a long time.)");
+ u = obj_get(ACTOR_UNDYNE);
+ WALK(u, -20, u->y, 2);
+ gone(u);
+ set_flag(F_UNDYNE, 4);
+ EV_END;
+}
+
+static int ev_hot_cooler(Ev *c) {
+ EV_BEGIN;
+ if (flag(F_UNDYNE) == 3 && !flag(F_WATER_CUP)) {
+ NARRATE("* (A water cooler.)\n* (Take a cup of water?)@Yes@No");
+ if (CHOICE() == 0) {
+ set_flag(F_WATER_CUP, 1);
+ NARRATE("* (You fill a cup with cold water.)");
+ }
+ } else {
+ NARRATE("* (A water cooler. The water's nice and cold.)");
+ }
+ EV_END;
+}
+
+int wf3_event(Ev *c) {
+ switch (c->id) {
+ case EV_DEADEND: return ev_deadend(c);
+ case EV_MK_CHASM: return ev_mk_chasm(c);
+ case EV_ARENA: return ev_arena(c);
+ case EV_UNDYNE_CATCH: return ev_undyne_catch(c);
+ case EV_PAP_NEON: return ev_pap_neon(c);
+ case EV_HOT_UNDYNE: return ev_hot_undyne(c);
+ case EV_HOT_COOLER: return ev_hot_cooler(c);
+ }
+ return -1;
+}
diff --git a/games/undertale/src/wfmon.c b/games/undertale/src/wfmon.c
new file mode 100644
index 00000000..507f2bb1
--- /dev/null
+++ b/games/undertale/src/wfmon.c
@@ -0,0 +1,490 @@
+/* Waterfall's monsters: stats and ACTs (from the Undertale Wiki's records of the game), their formations, where
+ * they come out and their attacks. Their words are short ones of our own. */
+#include
+#include
+#include "battle.h"
+#include "game.h"
+#include "gfx.h"
+#include "items.h"
+#include "monsters.h"
+#include "res.h"
+#include "world.h"
+
+/* ---------------------------------------------------------------- Aaron: flex three times and he flexes away */
+static void act_aaron(Battle *b, Monster *m, int act, char *t, int cap) {
+ (void)b;
+ if (act == 0) { /* Flex */
+ m->state++;
+ m->at = (int16_t)(m->at + 1);
+ if (m->state == 1) snprintf(t, (size_t)cap, "* You flex at Aaron.\n* He takes it as a challenge and flexes back!");
+ else if (m->state == 2) snprintf(t, (size_t)cap, "* You flex harder.\n* Aaron flexes harder still!");
+ else {
+ snprintf(t, (size_t)cap, "* You flex with everything you've got.\n* Aaron flexes so hard he flexes himself out of the room.");
+ m->gone = true, m->fade = 1, m->spare = true;
+ }
+ } else { /* Shoo */
+ snprintf(t, (size_t)cap, "* You tell Aaron to go away.\n* He just flexes at you.");
+ }
+}
+static const char *talk_aaron(Battle *b, Monster *m) {
+ (void)b;
+ static const char *const lines[] = {"Check out\nthese fins\n;)", "Feeling the\nburn?\n;)", "Don't be\nshy ;)",
+ "Want a\ncloser look?\n;)"};
+ if (m->state == 1) return "Oh, a\ncontest?\n;)";
+ if (m->state == 2) return "You're\ngood...\n;)";
+ return lines[rnd_n(4)];
+}
+static int attack_aaron(Battle *b, Monster *m) { (void)b, (void)m; return rnd_n(2) ? ATK_AARON_ARMS : ATK_AARON_SWEAT; }
+
+/* ---------------------------------------------------------------- Woshua: ask to be cleaned, catch its green */
+static void act_woshua(Battle *b, Monster *m, int act, char *t, int cap) {
+ (void)b;
+ if (act == 0) { /* Clean */
+ snprintf(t, (size_t)cap, "* You ask Woshua to clean you.\n* It gets very excited.");
+ if (!m->state) m->state = 1;
+ } else if (act == 1) { /* Touch */
+ if (m->spare) snprintf(t, (size_t)cap, "* You give Woshua a friendly pat.\n* It wobbles happily.");
+ else snprintf(t, (size_t)cap, "* Woshua won't let you touch it.\n* You're not clean enough!");
+ } else { /* Joke */
+ snprintf(t, (size_t)cap, "* You tell a dirty joke.\n* Woshua is horrified.");
+ }
+}
+static const char *talk_woshua(Battle *b, Monster *m) {
+ (void)b;
+ static const char *const lines[] = {"Scrub\nscrub!", "Germs,\nbegone!", "Wash! Rinse!\nRepeat!", "Too\ndirty!"};
+ if (m->state == 1) return "Hold\nstill!\nWashing!";
+ if (m->state == 2) return "Squeaky\nclean!";
+ return lines[rnd_n(4)];
+}
+static const char *flavor_woshua(Battle *b, Monster *m) {
+ (void)b;
+ if (m->state == 1) return "* Woshua's water looks clean and green.";
+ if (m->state == 2) return "* Woshua is proud of how clean you are.";
+ return NULL;
+}
+static int attack_woshua(Battle *b, Monster *m) {
+ (void)b;
+ if (m->state == 1) m->state2 = (int16_t)b->greens; /* (what it had healed before this one) */
+ return m->state == 1 || rnd_n(2) ? ATK_WOSHUA_DROPS : ATK_WOSHUA_BUBBLES;
+}
+
+/* ---------------------------------------------------------------- Moldbygg: let go of it */
+static void act_moldbygg(Battle *b, Monster *m, int act, char *t, int cap) {
+ if (act == 0) { /* Hug */
+ snprintf(t, (size_t)cap, "* You hug Moldbygg.\n* You're covered in slime...\n* You feel slower.");
+ if (b->speed > FX) b->speed -= FX / 2;
+ m->state = 1;
+ } else if (act == 1) { /* Lie Down */
+ snprintf(t, (size_t)cap, "* You lie down next to Moldbygg.\n* It's surprisingly comfy.");
+ } else { /* Unhug */
+ snprintf(t, (size_t)cap, m->state == 1 ? "* You let go of Moldbygg.\n* It seems relieved." : "* You give Moldbygg some room.\n* It seems relieved.");
+ m->spare = true, m->state = 2;
+ }
+}
+static const char *talk_moldbygg(Battle *b, Monster *m) {
+ (void)b, (void)m;
+ static const char *const lines[] = {"Burble...", "*squish*", "Blub\nblub.", "(wiggle)"};
+ return lines[rnd_n(4)];
+}
+static int attack_moldbygg(Battle *b, Monster *m) { (void)b, (void)m; return rnd_n(2) ? ATK_MOLDBYGG_HOPS : ATK_MOLDBYGG_RAIN; }
+
+/* ---------------------------------------------------------------- Shyren: hum, and she sings */
+static void act_shyren(Battle *b, Monster *m, int act, char *t, int cap) {
+ (void)b;
+ if (act == 1) { /* Hum */
+ m->state++;
+ m->spare = true;
+ if (m->state >= 5) {
+ snprintf(t, (size_t)cap, "* You hum along.\n* Shyren's song fills the whole cave.\n* She swims off, happy.");
+ m->gone = true, m->fade = 1;
+ } else if (m->state == 1) snprintf(t, (size_t)cap, "* You hum a little tune.\n* Shyren hums along, very quietly.");
+ else snprintf(t, (size_t)cap, "* You hum again.\n* Shyren sings a little louder!");
+ } else if (act == 0) { /* Smile */
+ snprintf(t, (size_t)cap, "* You smile at Shyren.\n* She gets even shyer.");
+ } else { /* Conduct */
+ snprintf(t, (size_t)cap, m->state ? "* You conduct.\n* Shyren follows your hands!" : "* You wave your arms about.\n* Shyren doesn't know the tune yet.");
+ }
+}
+static const char *talk_shyren(Battle *b, Monster *m) {
+ (void)b;
+ if (m->state >= 3) return "La la\nLAAA!";
+ if (m->state) return "La...?";
+ static const char *const lines[] = {"...hm...", "(blub)", "..."};
+ return lines[rnd_n(3)];
+}
+static int attack_shyren(Battle *b, Monster *m) { (void)b, (void)m; return ATK_SHYREN_NOTES; }
+
+/* ---------------------------------------------------------------- Mad Dummy: a ghost, so a hit passes through; its
+ * own missiles, led back up into it, do hurt it. Then its cousin Napstablook drifts in (the fight ends) */
+static void act_maddummy(Battle *b, Monster *m, int act, char *t, int cap) {
+ (void)b, (void)m, (void)act;
+ snprintf(t, (size_t)cap, "* You try to talk.\n* Mad Dummy is too busy yelling to hear a word.");
+}
+static const char *talk_maddummy(Battle *b, Monster *m) {
+ static const char *const lines[] = {"You! You\nhit my\ncousin!", "Dummies!\nGet them!", "Stand\nSTILL!",
+ "Why won't\nyou go\ndown?!", "Grr... GRRR!"};
+ if (m->state >= 2) return "That's it!\nNo more\nmagic!";
+ if (m->state) return "Ow! Who\nthrew that?!";
+ return lines[min_i(b->turn - 1, 4)];
+}
+static const char *flavor_maddummy(Battle *b, Monster *m) {
+ (void)b;
+ if (m->state) return "* Mad Dummy is hit by its own magic.";
+ return NULL;
+}
+static bool hit_maddummy(Battle *b, Monster *m, int dmg) {
+ (void)b, (void)dmg;
+ m->dmg_shown = 0; /* (MISS: it's a ghost) */
+ return true;
+}
+static int attack_maddummy(Battle *b, Monster *m) {
+ (void)m;
+ return b->turn <= 2 || b->turn % 2 ? ATK_DUMMY_MINIONS : ATK_DUMMY_MISSILES;
+}
+/* hit three times by its missiles, or after a long while: Napstablook comes, and the fight is over */
+static bool ends_maddummy(Battle *b, Monster *m) { return m->state >= 3 || b->turn >= 8; }
+
+/* ---------------------------------------------------------------- Temmie: talk to her (or feed her Temmie Flakes) */
+static void act_temmie(Battle *b, Monster *m, int act, char *t, int cap) {
+ (void)b;
+ if (act == 0) { /* Flex */
+ snprintf(t, (size_t)cap, "* You flex.\n* Temmie is so impressed she wanders off to tell her friends.");
+ m->gone = true, m->fade = 1;
+ } else if (act == 1) { /* Talk */
+ snprintf(t, (size_t)cap, "* You say hi.\n* Temmie vibrates with joy.");
+ m->spare = true;
+ } else { /* Feed Temmie Flakes */
+ int slot = -1;
+ for (int i = 0; i < NUM_ITEMS; i++)
+ if (sv.items[i] == ITEM_TEMMIE_FLAKES) slot = i;
+ if (slot < 0) snprintf(t, (size_t)cap, "* You don't have any Temmie Flakes.");
+ else {
+ item_remove_at(slot);
+ snprintf(t, (size_t)cap, "* You feed Temmie some Temmie Flakes.\n* She's so happy, she gives you money!");
+ m->spare = true, m->spare_gold = 30;
+ }
+ }
+}
+static const char *talk_temmie(Battle *b, Monster *m) {
+ (void)b;
+ static const char *const lines[] = {"hOI!!", "tem!!!", "awa\nawa!!", "u cute!!"};
+ if (m->spare) return "u r tem's\nfriend!!";
+ return lines[rnd_n(4)];
+}
+static int attack_temmie(Battle *b, Monster *m) { (void)b, (void)m; return ATK_TEMMIE_HOPS; }
+
+/* ---------------------------------------------------------------- Undyne: no sparing her; she turns the SOUL green
+ * (block her spears with its shield), then red again (her spears from the ground). Running is the friendly way */
+static void act_undyne(Battle *b, Monster *m, int act, char *t, int cap) {
+ (void)m;
+ if (act == 0) snprintf(t, (size_t)cap, "* You tell Undyne you don't want to fight.\n* She isn't listening.");
+ else {
+ b->state[0]++;
+ snprintf(t, (size_t)cap, "* You tell Undyne her spears are too slow.\n* Her spears get faster!");
+ }
+}
+static const char *talk_undyne(Battle *b, Monster *m) {
+ (void)m;
+ static const char *const lines[] = {"Ha! You\ncan't run\nnow!", "Use the\nshield!\nBlock!", "Is that\nall you've\ngot?",
+ "For every\nmonster's\nfuture!", "You're\ntougher\nthan you\nlook!", "NGAAH!"};
+ return lines[(b->turn - 1) % 6];
+}
+static const char *flavor_undyne(Battle *b, Monster *m) {
+ (void)m;
+ static const char *const lines[] = {"* Undyne points her spear at you.", "* Undyne's eye is fixed on you.",
+ "* Undyne flips her spear around.", "* Smells like sushi."};
+ return lines[b->turn % 4];
+}
+static int attack_undyne(Battle *b, Monster *m) { (void)m; return b->turn % 2 ? ATK_UNDYNE_ARROWS : ATK_UNDYNE_SPEARS; }
+
+static const MonDef wf_defs[MON_WF_COUNT - MON_SNOW_COUNT] = {
+ [MON_AARON - MON_SNOW_COUNT] = {
+ .name = "Aaron", .spare_gold = 30, .hp = 98, .at = 7, .df = 2, .exp = 52, .gold = 30, .spr_idle = SPR_AARON_B,
+ .spr_hurt = SPR_AARON_HURT, .check = "All muscle, and he knows it.", .acts = {"Flex", "Shoo"}, .nacts = 2,
+ .neutral = {"* Aaron is flexing.", "* Aaron winks at nobody in particular.", "* Smells like protein powder."},
+ .low_hp = "* Aaron's muscles are tired.", .act = act_aaron, .talk = talk_aaron, .attack = attack_aaron},
+ [MON_WOSHUA - MON_SNOW_COUNT] = {
+ .name = "Woshua", .spare_gold = 25, .hp = 70, .at = 7, .df = 1, .exp = 52, .gold = 25, .spr_idle = SPR_WOSHUA_B,
+ .spr_hurt = SPR_WOSHUA_HURT, .idle_speed = 8, .check = "Can't stand a single speck of dirt.",
+ .acts = {"Clean", "Touch", "Joke"}, .nacts = 3,
+ .neutral = {"* Woshua is scrubbing the air.", "* Woshua hums a washing song.", "* Smells like soap."},
+ .low_hp = "* Woshua's water is running low.", .act = act_woshua, .talk = talk_woshua, .attack = attack_woshua,
+ .flavor = flavor_woshua},
+ [MON_MOLDBYGG - MON_SNOW_COUNT] = {
+ .name = "Moldbygg", .spare_gold = 20, .hp = 70, .at = 7, .df = 4, .exp = 46, .gold = 20, .spr_idle = SPR_MOLDBYGG_B,
+ .spr_hurt = SPR_MOLDBYGG_B, .idle_speed = 8, .check = "Like Moldsmal, only a lot bigger.",
+ .acts = {"Hug", "Lie Down", "Unhug"}, .nacts = 3,
+ .neutral = {"* Moldbygg burbles.", "* Moldbygg wobbles.", "* Smells like a damp basement."},
+ .act = act_moldbygg, .talk = talk_moldbygg, .attack = attack_moldbygg},
+ [MON_SHYREN - MON_SNOW_COUNT] = {
+ .name = "Shyren", .spare_gold = 20, .hp = 66, .at = 7, .df = 2, .exp = 52, .gold = 20, .spr_idle = SPR_SHYREN_B,
+ .spr_hurt = SPR_SHYREN_TURNED, .check = "She'd love to sing, if nobody were watching.",
+ .acts = {"Smile", "Hum", "Conduct"}, .nacts = 3, .neutral = {"* Shyren hides her face.", "* Shyren is humming to herself.", "* Shyren peeks at you."},
+ .act = act_shyren, .talk = talk_shyren, .attack = attack_shyren},
+ [MON_MAD_DUMMY - MON_SNOW_COUNT] = {
+ .name = "Mad Dummy", .hp = 200, .at = 7, .df = 0, .exp = 0, .gold = 0, .spr_idle = SPR_MADDUMMY_B,
+ .spr_hurt = SPR_MADDUMMY_B, .idle_speed = 4, .check = "Lives inside a dummy. Hits go right through.", .acts = {"Talk"},
+ .nacts = 1, .neutral = {"* Mad Dummy is yelling at its dummies.", "* Mad Dummy glares at you.",
+ "* Mad Dummy's dummies are getting nervous."},
+ .flags = MF_BOSS | MF_NOT_KILLABLE, .act = act_maddummy, .talk = talk_maddummy, .attack = attack_maddummy,
+ .flavor = flavor_maddummy, .on_hit = hit_maddummy, .ends = ends_maddummy},
+ [MON_TEMMIE - MON_SNOW_COUNT] = {
+ .name = "Temmie", .hp = 5, .at = 7, .df = 0, .exp = 40, .gold = 20, .spr_idle = SPR_TEMMIE_B,
+ .spr_hurt = SPR_TEMMIE_HURT, .idle_speed = 4, .check = "Wants to pet you. You're allergic, though.",
+ .acts = {"Flex", "Talk", "Feed Flakes"}, .nacts = 3,
+ .neutral = {"* Temmie is vibrating.", "* Temmie is trying to pet you.", "* Smells like Temmie Flakes."},
+ .act = act_temmie, .talk = talk_temmie, .attack = attack_temmie},
+ [MON_UNDYNE - MON_SNOW_COUNT] = {
+ .name = "Undyne", .hp = 1500, .at = 6, .df = 20, .exp = 500, .gold = 0, .spr_idle = SPR_UNDYNE_B,
+ .spr_hurt = SPR_UNDYNE_B, .idle_speed = 5, .check = "Head of the Royal Guard. She does not back down.",
+ .acts = {"Plead", "Challenge"}, .nacts = 2, .act = act_undyne, .talk = talk_undyne, .attack = attack_undyne,
+ .flavor = flavor_undyne},
+};
+const MonDef *wf_mon_def(int id) { return id >= MON_WF_COUNT ? ht_mon_def(id) : &wf_defs[id - MON_SNOW_COUNT]; }
+
+/* Woshua's clean green caught: then it can be spared (battle.c asks each turn, through its flavour) */
+static void woshua_check(Battle *b) {
+ for (int i = 0; i < b->nmon; i++) {
+ Monster *m = &b->mon[i];
+ if (m->def == &wf_defs[MON_WOSHUA - MON_SNOW_COUNT] && m->state == 1 && b->greens > m->state2) m->state = 2, m->spare = true;
+ }
+}
+
+static const Form wf_forms[FORM_WF_COUNT - FORM_SNOW_COUNT] = {
+ [FORM_AARON - FORM_SNOW_COUNT] = {{MON_AARON}, 1, "* Aaron flexes in."},
+ [FORM_WOSHUA - FORM_SNOW_COUNT] = {{MON_WOSHUA}, 1, "* Woshua sloshes up."},
+ [FORM_WOSHUA_AARON - FORM_SNOW_COUNT] = {{MON_WOSHUA, MON_AARON}, 2, "* Woshua and Aaron show up."},
+ [FORM_MOLDBYGG - FORM_SNOW_COUNT] = {{MON_MOLDBYGG}, 1, "* Moldbygg blocks the way."},
+ [FORM_MOLDBYGG_MOLDSMAL - FORM_SNOW_COUNT] = {{MON_MOLDSMAL, MON_MOLDBYGG}, 2, "* Moldbygg and Moldsmal block the way."},
+ [FORM_MOLDBYGG_WOSHUA - FORM_SNOW_COUNT] = {{MON_MOLDBYGG, MON_WOSHUA}, 2, "* Moldbygg and Woshua appear."},
+ [FORM_SHYREN - FORM_SNOW_COUNT] = {{MON_SHYREN}, 1, "* Shyren hums, then hides."},
+ [FORM_MAD_DUMMY - FORM_SNOW_COUNT] = {{MON_MAD_DUMMY}, 1, "* Mad Dummy blocks the way!"},
+ [FORM_TEMMIE - FORM_SNOW_COUNT] = {{MON_TEMMIE}, 1, "* A Temmie bounces up!"},
+ [FORM_TEMMIE_MOLDBYGG - FORM_SNOW_COUNT] = {{MON_TEMMIE, MON_MOLDBYGG}, 2, "* Temmie and Moldbygg come along."},
+ [FORM_UNDYNE - FORM_SNOW_COUNT] = {{MON_UNDYNE}, 1, "* Undyne attacks!"},
+};
+const Form *wf_form(int form) { return form >= FORM_WF_COUNT ? ht_form(form) : &wf_forms[form - FORM_SNOW_COUNT]; }
+
+/* random encounters: the wiki's pools, room by room */
+int wf_encounter(int room) {
+ switch (room) {
+ case ROOM_WF_SEEDS2: case ROOM_WF_WISH:
+ return rnd_n(3) == 0 ? FORM_WOSHUA_AARON : rnd_n(2) ? FORM_AARON : FORM_WOSHUA;
+ case ROOM_WF_MARSH: case ROOM_WF_MARSH_MID: {
+ static const uint8_t pool[] = {FORM_WOSHUA, FORM_WOSHUA_AARON, FORM_MOLDBYGG, FORM_MOLDBYGG_MOLDSMAL, FORM_AARON};
+ return pool[rnd_n(5)];
+ }
+ case ROOM_WF_TWIN: case ROOM_WF_MUSHROOM: {
+ static const uint8_t pool[] = {FORM_TEMMIE, FORM_TEMMIE_MOLDBYGG, FORM_MOLDBYGG, FORM_WOSHUA, FORM_AARON};
+ return pool[rnd_n(5)];
+ }
+ }
+ return ht_encounter(room);
+}
+
+/* ---------------------------------------------------------------- the attacks */
+static int bx0(void) { return bt.x0 + 3; }
+static int bx1(void) { return bt.x1 - 3; }
+static int by0(void) { return bt.y0 + 3; }
+static int by1(void) { return bt.y1 - 3; }
+#define P(v) ((v) * FX)
+static Bullet *shot(uint16_t spr, int x, int y, int vx, int vy) { return bullet_new(spr, P(x), P(y), vx, vy); }
+
+/* Aaron: flexing arms rise up both sides, faster and faster; sweat drips down */
+static void atk_aaron_arms(int t) {
+ if (t < 140 && t % 16 == 0) {
+ int k = t / 16;
+ bool left = k % 2;
+ int x = left ? bx0() + 10 + rnd_n(30) : bx1() - 10 - rnd_n(30);
+ Bullet *b = shot(SPR_ARM, x, by1() + 8, 0, -(P(1) + P(k) / 6));
+ if (b) b->r = 6, b->ay = -1;
+ }
+}
+static void atk_aaron_sweat(int t) {
+ if (t < 140 && t % 7 == 0) {
+ Bullet *b = shot(SPR_DROP, bx0() + 4 + rnd_n(bx1() - bx0() - 8), by0() - 6, 0, P(1));
+ if (b) b->kind = BK_GRAVITY, b->ay = 2, b->r = 3;
+ }
+}
+/* Woshua: drops fall in columns, some green (they heal) once it's cleaning you; bubbles rise and sway */
+static void atk_woshua_drops(int t) {
+ bool clean = false;
+ for (int i = 0; i < bt.nmon; i++)
+ if (bt.mon[i].def == wf_mon_def(MON_WOSHUA) && bt.mon[i].state == 1 && bt.mon[i].alive && !bt.mon[i].gone) clean = true;
+ if (t < 140 && t % 10 == 0) {
+ /* (the green ones fall right over the SOUL: to be caught) */
+ bool green = clean && (t / 10) % 4 == 1;
+ int x = green ? clamp_i(soul_x(), bx0() + 6, bx1() - 6) : bx0() + 6 + rnd_n(bx1() - bx0() - 12);
+ Bullet *b = shot(SPR_DROP, x, by0() - 6, 0, P(2));
+ if (b) {
+ b->r = 3;
+ if (green) b->color = BC_GREEN, b->dmg = 1;
+ }
+ }
+ if (t == 150) woshua_check(&bt);
+}
+static void atk_woshua_bubbles(int t) {
+ if (t < 130 && t % 12 == 0) {
+ Bullet *b = shot(SPR_ORB, bx0() + 8 + rnd_n(bx1() - bx0() - 16), by1() + 6, 0, -P(1));
+ if (b) b->kind = BK_SCRIPT, b->a = (int16_t)(b->x / FX), b->r = 4;
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT || b->spr != SPR_ORB) continue;
+ b->y -= P(1);
+ b->x = P(b->a) + gfx_sin(b->t * 6) * 12 * FX / 1024;
+ if (b->y < P(by0() - 10)) b->kind = BK_NONE;
+ }
+}
+/* Moldbygg: molds hop across the floor; spores fall */
+static void atk_moldbygg_hops(int t) {
+ if (t < 120 && t % 30 == 0) {
+ bool left = (t / 30) % 2;
+ Bullet *b = shot(SPR_ORB, left ? bx0() : bx1(), by1() - 6, (left ? 1 : -1) * P(2), -P(4));
+ if (b) b->kind = BK_GRAVITY, b->ay = 6, b->r = 5, b->a = 3;
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_GRAVITY || b->spr != SPR_ORB || b->a <= 0) continue;
+ if (b->y > P(by1() - 5) && b->vy > 0) b->vy = -P(4), b->a--; /* (it bounces a few times) */
+ }
+}
+static void atk_moldbygg_rain(int t) {
+ if (t < 140 && t % 5 == 0) {
+ Bullet *b = shot(SPR_ORB, bx0() + 4 + rnd_n(bx1() - bx0() - 8), by0() - 6, rnd_n(P(1)) - P(1) / 2, P(1));
+ if (b) b->r = 3;
+ }
+}
+/* Shyren: notes swim in from the right on waves, more of them for each hum */
+static void atk_shyren_notes(int t) {
+ int hums = 0;
+ for (int i = 0; i < bt.nmon; i++)
+ if (bt.mon[i].def == wf_mon_def(MON_SHYREN)) hums = bt.mon[i].state;
+ int every = 16 - hums * 2;
+ if (t < 140 && t % every == 0) {
+ Bullet *b = shot(SPR_NOTE, bx1() + 6, by0() + 8 + rnd_n(by1() - by0() - 16), -(P(2) + P(hums) / 2), 0);
+ if (b) b->kind = BK_SCRIPT, b->b = (int16_t)(b->y / FX), b->r = 4;
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT || b->spr != SPR_NOTE) continue;
+ b->x += b->vx;
+ b->y = P(b->b) + gfx_sin(b->t * 8) * 10 * FX / 1024;
+ if (b->x < P(bx0() - 10)) b->kind = BK_NONE;
+ }
+}
+
+int wf_attack_length(int atk) { return atk >= ATK_WF_COUNT ? ht_attack_length(atk) : 160; }
+
+/* Mad Dummy: its dummies fly in from the sides and fire; later its magic missiles home in on the SOUL, and one that
+ * flies into Mad Dummy (up at the board's top middle, where it shows) hurts it */
+static void atk_dummy_minions(int t) {
+ if (t < 130 && t % 22 == 0) {
+ bool left = (t / 22) % 2;
+ int y = by0() + 12 + rnd_n(by1() - by0() - 24);
+ Bullet *d = shot((t / 22) % 3 == 2 ? SPR_DUMMY_BOT : SPR_DUMMY_MINION, left ? bx0() - 12 : bx1() + 12, y,
+ (left ? 1 : -1) * P(2), 0);
+ if (d) d->r = 7, d->flags |= BF_NOCLIP, d->life = 140;
+ }
+}
+#define DUMMY_X (board_cx())
+#define DUMMY_Y (by0() + 4)
+static void atk_dummy_missiles(int t) {
+ if (t < 100 && t % 25 == 0) {
+ Bullet *b = shot(SPR_ORB, (t / 25) % 2 ? bx0() + 6 : bx1() - 6, by1() - 6, 0, -P(1));
+ if (b) b->kind = BK_SCRIPT, b->r = 4, b->a = 0;
+ }
+ /* (the dummy's mark: what the missiles may fly into) */
+ if (t == 0) {
+ Bullet *m = shot(SPR_DUMMY_MINION, DUMMY_X, DUMMY_Y, 0, 0);
+ if (m) m->flags |= BF_HARMLESS, m->kind = BK_NONE, m->life = 180, m->a = 1;
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT || b->spr != SPR_ORB) continue;
+ /* steer toward the SOUL, a little at a time */
+ int dx = soul_x() * FX - b->x, dy = soul_y() * FX - b->y;
+ b->vx += dx > 0 ? 2 : -2, b->vy += dy > 0 ? 2 : -2;
+ b->vx = clamp_i(b->vx, -P(2), P(2)), b->vy = clamp_i(b->vy, -P(2), P(2));
+ b->x += b->vx, b->y += b->vy;
+ if (abs_i(b->x / FX - DUMMY_X) < 8 && abs_i(b->y / FX - DUMMY_Y) < 8 && b->t > 20) {
+ b->kind = BK_NONE;
+ for (int k = 0; k < bt.nmon; k++)
+ if (bt.mon[k].def == &wf_defs[MON_MAD_DUMMY - MON_SNOW_COUNT]) bt.mon[k].state++, bt.mon[k].hurt = 20, bt.mon[k].shake = 6;
+ }
+ if (b->t > 200) b->kind = BK_NONE;
+ }
+}
+/* Temmie: little Temmies hop across the board */
+static void atk_temmie_hops(int t) {
+ if (t < 130 && t % 26 == 0) {
+ bool left = (t / 26) % 2;
+ Bullet *b = shot(SPR_TEMMIE_OW, left ? bx0() - 10 : bx1() + 10, by1() - 8, (left ? 1 : -1) * P(2), -P(3));
+ if (b) b->kind = BK_GRAVITY, b->ay = 5, b->r = 7, b->flags |= BF_NOCLIP, b->life = 150, b->a = 4;
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_GRAVITY || b->spr != SPR_TEMMIE_OW || b->a <= 0) continue;
+ if (b->y > P(by1() - 8) && b->vy > 0) b->vy = -P(3), b->a--;
+ }
+}
+
+/* Undyne: spears fly at the green SOUL from all four sides, faster as she goes on (and as she's challenged) */
+static void atk_undyne_arrows(int t) {
+ int speed = P(2) + P(min_i(bt.turn, 10)) / 6 + P(bt.state[0]) / 3;
+ int every = max_i(10, 22 - bt.turn);
+ if (t < 140 && t % every == 0) {
+ static const int16_t ang[4] = {64, 192, 0, 128}; /* from above, below, the left, the right: pointing in */
+ static const int8_t fx[4] = {0, 0, -1, 1}, fy[4] = {-1, 1, 0, 0};
+ int side = rnd_n(4);
+ Bullet *b = shot(SPR_ARROW, board_cx() + fx[side] * 60, board_cy() + fy[side] * 60, -fx[side] * speed, -fy[side] * speed);
+ if (b) b->r = 3, b->angle = ang[side], b->flags |= BF_SHIELD | BF_NOCLIP;
+ }
+}
+/* and at the red SOUL: a glint on the ground, then a spear shoots up there */
+static void atk_undyne_spears(int t) {
+ if (t < 130 && t % 14 == 0) {
+ int x = clamp_i(soul_x() - 20 + rnd_n(41), bx0() + 6, bx1() - 6);
+ Bullet *b = shot(SPR_SPEAR, x, by1() - 4, 0, 0);
+ if (b) b->kind = BK_SCRIPT, b->flags |= BF_HARMLESS, b->alpha = 90, b->a = 0, b->r = 3;
+ }
+ for (int i = 0; i < MAX_BULLETS; i++) {
+ Bullet *b = &bullets[i];
+ if (b->kind != BK_SCRIPT || b->spr != SPR_SPEAR) continue;
+ if (++b->a == 16) b->flags &= (uint8_t)~BF_HARMLESS, b->alpha = 255, b->vy = -P(5);
+ b->y += b->vy;
+ if (b->y < P(by0() - 30)) b->kind = BK_NONE;
+ }
+}
+
+void wf_attack_run(Battle *b, int atk, int t) {
+ if (atk >= ATK_WF_COUNT) {
+ ht_attack_run(b, atk, t);
+ return;
+ }
+ if (t < 0) {
+ if (atk == ATK_UNDYNE_ARROWS) {
+ board_set(80, 80);
+ soul_set_mode(SOUL_GREEN);
+ bt.shield = 1;
+ } else {
+ board_set(atk == ATK_SHYREN_NOTES ? 160 : 130, 110);
+ soul_set_mode(SOUL_RED);
+ }
+ return;
+ }
+ switch (atk) {
+ case ATK_AARON_ARMS: atk_aaron_arms(t); break;
+ case ATK_AARON_SWEAT: atk_aaron_sweat(t); break;
+ case ATK_WOSHUA_DROPS: atk_woshua_drops(t); break;
+ case ATK_WOSHUA_BUBBLES: atk_woshua_bubbles(t); break;
+ case ATK_MOLDBYGG_HOPS: atk_moldbygg_hops(t); break;
+ case ATK_MOLDBYGG_RAIN: atk_moldbygg_rain(t); break;
+ case ATK_SHYREN_NOTES: atk_shyren_notes(t); break;
+ case ATK_DUMMY_MINIONS: atk_dummy_minions(t); break;
+ case ATK_DUMMY_MISSILES: atk_dummy_missiles(t); break;
+ case ATK_TEMMIE_HOPS: atk_temmie_hops(t); break;
+ case ATK_UNDYNE_ARROWS: atk_undyne_arrows(t); break;
+ case ATK_UNDYNE_SPEARS: atk_undyne_spears(t); break;
+ }
+}
diff --git a/games/undertale/src/world.c b/games/undertale/src/world.c
new file mode 100644
index 00000000..ebc45f4d
--- /dev/null
+++ b/games/undertale/src/world.c
@@ -0,0 +1,544 @@
+/* The overworld: rooms, the camera, Frisk and the room's objects (world.h). */
+#include
+#include "event.h"
+#include "game.h"
+#include "gfx.h"
+#include "res.h"
+#include "text.h"
+#include "world.h"
+#include "events.h"
+
+void pl_walk_step(void); /* event.c */
+void room_update(void); /* rooms.c */
+
+Obj objs[MAX_OBJS];
+Player pl;
+int g_room = -1;
+static RoomInfo room;
+static Door doors[16];
+static int ndoors;
+int cam_x, cam_y;
+bool cam_locked;
+uint8_t g_fade;
+
+/* Frisk's box: his feet (from his sprite's top-left), as the game's mask */
+#define PBX 3
+#define PBY 20
+#define PBW 14
+#define PBH 9
+#define SPEED 3
+
+const RoomInfo *world_room_info(void) { return g_room >= 0 ? &room : NULL; }
+
+/* the room of the same area whose rectangle holds (x, y), -1 none */
+static int room_at(int x, int y) {
+ if (x >= room.x && x < room.x + room.w && y >= room.y && y < room.y + room.h) return g_room;
+ for (int r = 0; r < NUM_ROOMS; r++) {
+ RoomInfo o;
+ room_info(r, &o);
+ if (o.area == room.area && x >= o.x && x < o.x + o.w && y >= o.y && y < o.y + o.h) return r;
+ }
+ return -1;
+}
+
+/* the 4-pixel cell at (x, y) walkable? (in whichever room of the area holds it) */
+static bool cell_walkable(int x, int y) {
+ const RoomInfo *ri = &room;
+ RoomInfo o;
+ if (!(x >= room.x && x < room.x + room.w && y >= room.y && y < room.y + room.h)) {
+ int r = room_at(x, y);
+ if (r < 0) return false;
+ room_info(r, &o);
+ ri = &o;
+ }
+ int cx = (x - ri->x) >> 2, cy = (y - ri->y) >> 2;
+ if (cx < 0 || cy < 0 || cx >= ri->cw || cy >= ri->ch) return false;
+ return ri->coll[cy * ((ri->cw + 7) >> 3) + (cx >> 3)] >> (cx & 7) & 1;
+}
+
+static bool boxes_meet(int ax, int ay, int aw, int ah, int bx, int by, int bw, int bh) {
+ return ax < bx + bw && bx < ax + aw && ay < by + bh && by < ay + ah;
+}
+
+/* a floor object (a bloomed bridge seed) under the point */
+static bool on_floor_obj(int x, int y) {
+ for (int i = 0; i < MAX_OBJS; i++) {
+ const Obj *o = &objs[i];
+ if (o->kind != OBJ_NONE && (o->flags & OF_FLOOR) && x >= o->x + o->bx && x < o->x + o->bx + o->bw &&
+ y >= o->y + o->by && y < o->y + o->by + o->bh)
+ return true;
+ }
+ return false;
+}
+
+bool world_walkable(int x, int y, int w, int h, int ignore) {
+ /* every cell the box covers (its corners and every 4 pixels between) */
+ for (int yy = y;; yy += 4) {
+ if (yy > y + h - 1) yy = y + h - 1;
+ for (int xx = x;; xx += 4) {
+ if (xx > x + w - 1) xx = x + w - 1;
+ if (!cell_walkable(xx, yy) && !on_floor_obj(xx, yy)) return false;
+ if (xx == x + w - 1) break;
+ }
+ if (yy == y + h - 1) break;
+ }
+ if (ignore == WALK_FLOOR_ONLY) return true;
+ for (int i = 0; i < MAX_OBJS; i++) {
+ const Obj *o = &objs[i];
+ if (i == ignore || o->kind == OBJ_NONE || !(o->flags & OF_SOLID)) continue;
+ if (boxes_meet(x, y, w, h, o->x + o->bx, o->y + o->by, o->bw, o->bh)) return false;
+ }
+ return true;
+}
+
+static bool player_free(int x, int y) { return world_walkable(x + PBX, y + PBY, PBW, PBH, -1); }
+
+Obj *obj_new(int kind, int x, int y) {
+ for (int i = 0; i < MAX_OBJS; i++)
+ if (objs[i].kind == OBJ_NONE) {
+ Obj *o = &objs[i];
+ memset(o, 0, sizeof *o);
+ o->kind = (uint8_t)kind, o->x = (int16_t)x, o->y = (int16_t)y, o->alpha = 255;
+ return o;
+ }
+ return NULL;
+}
+Obj *obj_find(int name) {
+ for (int i = 0; i < MAX_OBJS; i++)
+ if (objs[i].kind != OBJ_NONE && objs[i].name == name) return &objs[i];
+ return NULL;
+}
+Obj *obj_get(int name) {
+ static Obj spare;
+ Obj *o = obj_find(name);
+ if (o) return o;
+ memset(&spare, 0, sizeof spare);
+ return &spare;
+}
+void obj_clear_all(void) { memset(objs, 0, sizeof objs); }
+
+void world_camera(void) {
+ if (cam_locked) return;
+ int cx = pl.x + 10 - SCREEN_W / 2, cy = pl.y + 15 - SCREEN_H / 2;
+ if (room.w <= SCREEN_W) cx = room.x + (room.w - SCREEN_W) / 2;
+ else if (cx < room.x) cx = room.x;
+ else if (cx > room.x + room.w - SCREEN_W) cx = room.x + room.w - SCREEN_W;
+ if (room.h <= SCREEN_H) cy = room.y + (room.h - SCREEN_H) / 2;
+ else if (cy < room.y) cy = room.y;
+ else if (cy > room.y + room.h - SCREEN_H) cy = room.y + room.h - SCREEN_H;
+ cam_x = cx, cam_y = cy;
+}
+
+void world_enter(int r, int x, int y, int dir) {
+ int from = g_room;
+ g_room = r;
+ room_info(r, &room);
+ pl.x = (int16_t)x, pl.y = (int16_t)y, pl.dir = (uint8_t)dir;
+ pl.frame = 0, pl.anim = 0;
+ obj_clear_all();
+ ndoors = room_doors(r, doors, 16);
+ cam_locked = false;
+ room_setup(r, from);
+ /* a door's spot a little off the floor: the nearest that isn't (the floor the room's things make counts: a
+ * bridge of flowers; what stands about doesn't) */
+ if (!world_walkable(pl.x + PBX, pl.y + PBY, PBW, PBH, WALK_FLOOR_ONLY))
+ for (int d = 1; d <= 24; d++) {
+ static const int8_t dx[4] = {0, 0, -1, 1}, dy[4] = {1, -1, 0, 0};
+ int k;
+ for (k = 0; k < 4 && !world_walkable(pl.x + dx[k] * d + PBX, pl.y + dy[k] * d + PBY, PBW, PBH, WALK_FLOOR_ONLY); k++) {}
+ if (k < 4) {
+ pl.x = (int16_t)(pl.x + dx[k] * d), pl.y = (int16_t)(pl.y + dy[k] * d);
+ break;
+ }
+ }
+ world_camera();
+}
+
+/* ---------------------------------------------------------------- fades */
+static struct {
+ int room, x, y, dir;
+ int phase, t; /* 1: going black, 2: coming back */
+ int hold; /* frames it stays black (Frisk sleeps) */
+ bool walked; /* Frisk walked out: what was happening in the room stops */
+} fade;
+#define FADE_FRAMES 4
+
+void world_fade_to(int r, int x, int y, int dir) {
+ fade.room = r, fade.x = x, fade.y = y, fade.dir = dir;
+ fade.phase = 1, fade.t = 0, fade.walked = false, fade.hold = 0;
+}
+void world_fade_hold(int r, int x, int y, int dir, int hold) {
+ world_fade_to(r, x, y, dir);
+ fade.hold = hold;
+}
+bool world_fading(void) { return fade.phase != 0; }
+
+static void fade_step(void) {
+ if (fade.phase == 1) {
+ if (fade.t >= FADE_FRAMES) { /* in the new room, still black */
+ g_fade = 255;
+ if (fade.hold > 0) fade.hold--;
+ else fade.phase = 2, fade.t = 0;
+ return;
+ }
+ if (++fade.t >= FADE_FRAMES) {
+ if (fade.walked) event_cancel();
+ world_enter(fade.room, fade.x, fade.y, fade.dir);
+ if (fade.hold > 0) {
+ g_fade = 255;
+ return;
+ }
+ fade.phase = 2, fade.t = 0;
+ }
+ g_fade = (uint8_t)(255 * fade.t / FADE_FRAMES);
+ } else if (fade.phase == 2) {
+ if (++fade.t >= FADE_FRAMES) fade.phase = 0;
+ g_fade = (uint8_t)(255 - 255 * fade.t / FADE_FRAMES);
+ } else {
+ g_fade = 0;
+ }
+}
+
+/* ---------------------------------------------------------------- Frisk */
+static const uint8_t frames_of[4] = {4, 4, 2, 2};
+
+static void face_from_keys(uint32_t keys, uint32_t pressed) {
+ static const uint32_t bit[4] = {K_DOWN, K_UP, K_LEFT, K_RIGHT};
+ /* a direction just pressed turns him; letting go of the one he faces turns him to one still held */
+ for (int d = 0; d < 4; d++)
+ if (pressed & bit[d]) pl.dir = (uint8_t)d;
+ if (!(keys & bit[pl.dir]))
+ for (int d = 0; d < 4; d++)
+ if (keys & bit[d]) {
+ pl.dir = (uint8_t)d;
+ break;
+ }
+}
+
+/* walking into a rock pushes it, a pixel a frame, if there's floor for it (a rock with an event talks back
+ * instead) */
+static uint32_t pushed_at;
+static bool push_rock(int sx, int sy) {
+ int fx = pl.x + PBX + sx, fy = pl.y + PBY + sy;
+ for (int i = 0; i < MAX_OBJS; i++) {
+ Obj *o = &objs[i];
+ if (o->kind != OBJ_ROCK || !(o->flags & OF_SOLID)) continue;
+ if (!boxes_meet(fx, fy, PBW, PBH, o->x + o->bx, o->y + o->by, o->bw, o->bh)) continue;
+ if (o->event) {
+ if (!event_running()) event_start(o->event, o);
+ return false;
+ }
+ if (pushed_at == g_frame || o->walking) return false;
+ if (!world_walkable(o->x + o->bx + sx, o->y + o->by + sy, o->bw, o->bh, i) &&
+ !room_rock_enters(o, o->x + sx, o->y + sy))
+ return false;
+ pushed_at = g_frame;
+ o->x = (int16_t)(o->x + sx), o->y = (int16_t)(o->y + sy);
+ room_rock_moved(o);
+ return player_free(pl.x + sx, pl.y + sy);
+ }
+ return false;
+}
+
+/* one pixel along an axis, sliding past a corner (diagonal walls are steps of cells) */
+static bool step(int sx, int sy, bool slide) {
+ if (player_free(pl.x + sx, pl.y + sy) || push_rock(sx, sy)) {
+ pl.x = (int16_t)(pl.x + sx), pl.y = (int16_t)(pl.y + sy);
+ return true;
+ }
+ if (!slide) return false;
+ for (int k = 1; k <= 2; k++) {
+ bool a = sx ? player_free(pl.x + sx, pl.y - k) : player_free(pl.x - k, pl.y + sy);
+ bool b = sx ? player_free(pl.x + sx, pl.y + k) : player_free(pl.x + k, pl.y + sy);
+ if (a != b) {
+ int o = a ? -1 : 1;
+ if (sx) pl.y = (int16_t)(pl.y + o), pl.x = (int16_t)(pl.x + sx);
+ else pl.x = (int16_t)(pl.x + o), pl.y = (int16_t)(pl.y + sy);
+ return true;
+ }
+ if (a && b) break;
+ }
+ return false;
+}
+
+static int8_t slide_dx, slide_dy;
+bool pl_sliding(void) { return slide_dx || slide_dy; }
+
+static void player_move(uint32_t keys, uint32_t pressed) {
+ int dx = ((keys & K_RIGHT) != 0) - ((keys & K_LEFT) != 0);
+ int dy = ((keys & K_DOWN) != 0) - ((keys & K_UP) != 0);
+ face_from_keys(keys, pressed);
+ /* on ice he slides the way he went on until he's off it, or something stops him */
+ if (room_ice(pl.x + PBX + PBW / 2, pl.y + PBY + PBH / 2)) {
+ if (!slide_dx && !slide_dy && (dx || dy)) {
+ static const int8_t ddx[4] = {0, 0, -1, 1}, ddy[4] = {1, -1, 0, 0};
+ slide_dx = ddx[pl.dir], slide_dy = ddy[pl.dir];
+ }
+ if (slide_dx || slide_dy) {
+ bool moved = false;
+ for (int i = 0; i < 4; i++) moved |= step(slide_dx, slide_dy, false);
+ if (!moved) slide_dx = slide_dy = 0;
+ pl.anim = 0, pl.frame = 0;
+ pl.moving = moved;
+ return;
+ }
+ } else {
+ slide_dx = slide_dy = 0;
+ }
+ bool moved = false;
+ int sp = room_speed(pl.x + PBX + PBW / 2, pl.y + PBY + PBH / 2);
+ for (int i = 0; i < sp; i++) {
+ if (dx) moved |= step(dx, 0, !dy);
+ if (dy) moved |= step(0, dy, !dx);
+ }
+ if (dx || dy) {
+ pl.anim++;
+ pl.frame = (uint8_t)((1 + pl.anim / 6) % frames_of[pl.dir]);
+ if (moved) pl.steps++;
+ } else {
+ pl.anim = 0, pl.frame = 0;
+ }
+ pl.moving = moved;
+}
+
+/* the box in front of Frisk, where Z looks */
+static void front_box(int *x, int *y, int *w, int *h) {
+ int fx = pl.x + PBX, fy = pl.y + PBY, fw = PBW, fh = PBH;
+ switch (pl.dir) {
+ case DIR_DOWN: fy += fh, fh = 12; break;
+ case DIR_UP: fy -= 14, fh = 14; break;
+ case DIR_LEFT: fx -= 12, fw = 12; break;
+ default: fx += fw, fw = 12; break;
+ }
+ *x = fx, *y = fy, *w = fw, *h = fh;
+}
+
+static void interact(void) {
+ int x, y, w, h;
+ front_box(&x, &y, &w, &h);
+ for (int i = 0; i < MAX_OBJS; i++) {
+ Obj *o = &objs[i];
+ if (o->kind == OBJ_NONE || !(o->flags & OF_TALK)) continue;
+ if (boxes_meet(x, y, w, h, o->x + o->bx, o->y + o->by, o->bw, o->bh)) {
+ event_start(o->event, o);
+ return;
+ }
+ }
+}
+
+static void touches(void) {
+ int x = pl.x + PBX, y = pl.y + PBY;
+ for (int i = 0; i < MAX_OBJS; i++) {
+ Obj *o = &objs[i];
+ if (o->kind == OBJ_NONE || !(o->flags & OF_TOUCH)) continue;
+ if (boxes_meet(x, y, PBW, PBH, o->x + o->bx, o->y + o->by, o->bw, o->bh)) {
+ event_start(o->event, o);
+ return;
+ }
+ }
+}
+
+/* ---------------------------------------------------------------- objects walking */
+static void objs_update(void) {
+ for (int i = 0; i < MAX_OBJS; i++) {
+ Obj *o = &objs[i];
+ if (o->kind == OBJ_NONE) continue;
+ if (o->walking) {
+ int dx = o->tx - o->x, dy = o->ty - o->y;
+ int sp = o->speed ? o->speed : 2;
+ if (dx) o->dir = dx < 0 ? DIR_LEFT : DIR_RIGHT;
+ if (abs_i(dy) > abs_i(dx)) o->dir = dy < 0 ? DIR_UP : DIR_DOWN;
+ o->x = (int16_t)(o->x + clamp_i(dx, -sp, sp));
+ o->y = (int16_t)(o->y + clamp_i(dy, -sp, sp));
+ if (o->x == o->tx && o->y == o->ty) o->walking = 0;
+ if (o->flags & OF_WALKANIM) {
+ o->anim_t++;
+ o->frame = (uint8_t)(1 + o->anim_t / (o->anim_speed ? o->anim_speed : 6));
+ }
+ if (!o->walking && (o->flags & OF_WALKANIM)) o->frame = 0, o->anim_t = 0;
+ } else if (o->flags & OF_ANIM) {
+ if (++o->anim_t >= (o->anim_speed ? o->anim_speed : 6)) o->anim_t = 0, o->frame++;
+ }
+ }
+}
+
+void world_update(uint32_t keys, uint32_t pressed) {
+ fade_step();
+ if (fade.phase == 1) return;
+ objs_update();
+ bool busy = event_blocking() || text_active() || fade.phase;
+ if (!busy && !pl.frozen) {
+ player_move(keys, pressed);
+ /* out of the room's rectangle: into the room that holds his feet */
+ int fx = pl.x + PBX + PBW / 2, fy = pl.y + PBY + PBH / 2;
+ int r = room_at(fx, fy);
+ if (r >= 0 && r != g_room) {
+ world_fade_to(r, pl.x, pl.y, pl.dir);
+ fade.walked = true;
+ return;
+ }
+ /* a door */
+ for (int i = 0; i < ndoors; i++)
+ if (boxes_meet(pl.x + PBX, pl.y + PBY, PBW, PBH, doors[i].x, doors[i].y, doors[i].w, doors[i].h)) {
+ /* (a side door between rooms with a gap keeps Frisk's height: DOOR_KEEP) */
+ world_fade_to(doors[i].room, doors[i].tx == DOOR_KEEP ? pl.x : doors[i].tx,
+ doors[i].ty == DOOR_KEEP ? pl.y : doors[i].ty, doors[i].dir);
+ fade.walked = true;
+ return;
+ }
+ room_update();
+ if (!event_running()) {
+ if (pressed & K_Z) interact();
+ else touches();
+ if (!event_running() && (pressed & K_C)) game_open_menu();
+ if (!event_running()) encounter_step();
+ }
+ } else {
+ pl_walk_step();
+ }
+ world_camera();
+}
+
+/* ---------------------------------------------------------------- drawing */
+typedef struct {
+ int16_t depth;
+ int8_t who; /* object index, -1 Frisk */
+} DrawRef;
+
+static uint16_t obj_sprite(const Obj *o) { return o->spr[o->spr[o->dir] ? o->dir : 0]; }
+
+/* ---------------------------------------------------------------- the room's things */
+Obj *obj_npc(int name, const uint16_t spr[4], int x, int y, int dir, int event) {
+ Obj *o = obj_new(OBJ_NPC, x, y);
+ if (!o) return NULL;
+ for (int i = 0; i < 4; i++) o->spr[i] = spr[i];
+ Frame f;
+ frame_get(spr[0], 0, &f);
+ o->name = (uint8_t)name, o->dir = (uint8_t)dir, o->event = (uint16_t)event;
+ /* its box: the bottom of its sprite, a little narrower (from the sprite's origin: its top-left or its feet) */
+ o->bx = (int16_t)(f.w / 5 - f.ox), o->bw = (int16_t)(f.w - 2 * (f.w / 5)), o->bh = (int16_t)(f.h / 4 < 8 ? 8 : f.h / 4);
+ o->by = (int16_t)(f.h - o->bh - f.oy);
+ o->flags = OF_SOLID | (event ? OF_TALK : 0) | OF_WALKANIM;
+ return o;
+}
+Obj *obj_toriel(int x, int y, int dir) {
+ static const uint16_t s[4] = {SPR_TORIEL_D, SPR_TORIEL_U, SPR_TORIEL_L, SPR_TORIEL_R};
+ Obj *o = obj_npc(ACTOR_TORIEL, s, x, y, dir, 0);
+ if (o) obj_set_box(o, 6, 44, 20, 12), o->anim_speed = 6;
+ return o;
+}
+Obj *obj_sign(int x, int y, int w, int h, const char *text) {
+ Obj *o = obj_new(OBJ_SIGN, x, y);
+ if (!o) return NULL;
+ o->bw = (int16_t)w, o->bh = (int16_t)h, o->flags = OF_TALK, o->event = EV_SIGN, o->text = text;
+ return o;
+}
+Obj *obj_save(int x, int y, const char *text) {
+ Obj *o = obj_new(OBJ_SAVE, x, y);
+ if (!o) return NULL;
+ o->spr[0] = SPR_SAVEPOINT, o->flags = OF_TALK | OF_SOLID | OF_ANIM, o->anim_speed = 6;
+ o->bx = 2, o->by = 4, o->bw = 16, o->bh = 14;
+ o->event = EV_SAVE, o->text = text;
+ return o;
+}
+Obj *obj_deco(uint16_t spr, int x, int y, bool solid) {
+ Obj *o = obj_new(OBJ_DECO, x, y);
+ if (!o) return NULL;
+ Frame f;
+ frame_get(spr, 0, &f);
+ o->spr[0] = spr;
+ o->bx = 0, o->bw = (int16_t)f.w, o->bh = (int16_t)(f.h < 10 ? f.h : 10), o->by = (int16_t)(f.h - o->bh);
+ if (solid) o->flags |= OF_SOLID;
+ return o;
+}
+Obj *obj_trigger(int x, int y, int w, int h, int event, int name) {
+ Obj *o = obj_new(OBJ_TRIGGER, x, y);
+ if (!o) return NULL;
+ o->bw = (int16_t)w, o->bh = (int16_t)h, o->flags = OF_TOUCH, o->event = (uint16_t)event, o->name = (uint8_t)name;
+ return o;
+}
+Obj *obj_rock(int x, int y, int name, int event) {
+ Obj *o = obj_new(OBJ_ROCK, x, y);
+ if (!o) return NULL;
+ o->spr[0] = SPR_O_ROCK, o->name = (uint8_t)name, o->event = (uint16_t)event;
+ o->flags = OF_SOLID | (event ? OF_TALK : 0);
+ obj_set_box(o, 1, 8, 18, 12);
+ return o;
+}
+void obj_spikes_set(Obj *o, bool raised) {
+ if (!o) return;
+ o->frame = raised ? 0 : 1;
+ if (raised) o->flags |= OF_SOLID;
+ else o->flags &= (uint16_t)~OF_SOLID;
+}
+Obj *obj_spikes(int x, int y, int cols, int rows, bool raised, int name) {
+ Obj *o = obj_new(OBJ_SPIKES, x, y);
+ if (!o) return NULL;
+ o->spr[0] = SPR_O_SPIKETILE, o->arg = (int16_t)cols, o->arg2 = (int16_t)rows, o->name = (uint8_t)name;
+ o->bw = (int16_t)(cols * 20), o->bh = (int16_t)(rows * 20);
+ o->frame = raised ? 0 : 1;
+ o->flags = (raised ? OF_SOLID : 0) | OF_BACK;
+ return o;
+}
+void obj_set_box(Obj *o, int bx, int by, int bw, int bh) {
+ o->bx = (int16_t)bx, o->by = (int16_t)by, o->bw = (int16_t)bw, o->bh = (int16_t)bh;
+}
+bool near_frisk(const Obj *o, int d) {
+ int ox = o->x + o->bx + o->bw / 2, oy = o->y + o->by + o->bh / 2;
+ int px = pl.x + PBX + PBW / 2, py = pl.y + PBY + PBH / 2;
+ return abs_i(ox - px) <= d && abs_i(oy - py) <= d;
+}
+
+bool cam_pan(int x, int y, int sp) {
+ cam_locked = true;
+ cam_x += clamp_i(x - cam_x, -sp, sp), cam_y += clamp_i(y - cam_y, -sp, sp);
+ return cam_x == x && cam_y == y;
+}
+void cam_free(void) { cam_locked = false; }
+
+void room_draw_floor(void); /* rooms.c */
+void room_draw_top(void);
+
+int g_shake; /* frames the view shakes */
+
+void world_draw(void) {
+ int keep_x = cam_x, keep_y = cam_y;
+ if (g_shake > 0) cam_x += (rnd_n(3) - 1) * 2, cam_y += (rnd_n(3) - 1) * 2, g_shake--;
+ gfx_tiles(cam_x, cam_y);
+ room_draw_floor();
+ /* the room's objects and Frisk, the lower ones in front */
+ DrawRef order[MAX_OBJS + 1];
+ int n = 0;
+ for (int i = 0; i < MAX_OBJS; i++) {
+ const Obj *o = &objs[i];
+ if (o->kind == OBJ_NONE || (o->flags & OF_HIDDEN) || !o->spr[0]) continue;
+ int d = o->y + o->by + o->bh + o->depth_bias;
+ if (o->flags & OF_BACK) d = -30000;
+ if (o->flags & OF_FRONT) d = 30000;
+ order[n++] = (DrawRef){(int16_t)d, (int8_t)i};
+ }
+ if (!pl.hidden) order[n++] = (DrawRef){(int16_t)(pl.y + PBY + PBH), -1};
+ for (int i = 1; i < n; i++)
+ for (int j = i; j > 0 && order[j - 1].depth > order[j].depth; j--) {
+ DrawRef t = order[j];
+ order[j] = order[j - 1], order[j - 1] = t;
+ }
+ static const uint16_t frisk[4] = {SPR_FRISK_D, SPR_FRISK_U, SPR_FRISK_L, SPR_FRISK_R};
+ if (g_exclaim) gfx_sprite(SPR_O_EXC, 0, pl.x - cam_x, pl.y - 12 - cam_y, 0, 255);
+ for (int i = 0; i < n; i++) {
+ if (order[i].who < 0) {
+ gfx_sprite(frisk[pl.dir], pl.frame, pl.x - cam_x, pl.y - cam_y, 0, 255);
+ } else {
+ const Obj *o = &objs[order[i].who];
+ if (o->kind == OBJ_SPIKES) {
+ for (int r = 0; r < o->arg2; r++)
+ for (int k = 0; k < o->arg; k++) gfx_sprite(o->spr[0], o->frame, o->x + k * 20 - cam_x, o->y + r * 20 - cam_y, 0, 255);
+ continue;
+ }
+ if (o->flags & OF_DARK) gfx_sprite_color(obj_sprite(o), o->frame, o->x - cam_x, o->y - cam_y, 0, C_BLACK, o->alpha);
+ else gfx_sprite(obj_sprite(o), o->frame, o->x - cam_x, o->y - cam_y, (o->flags & OF_FLIP) ? GFX_FLIPX : 0, o->alpha);
+ }
+ }
+ room_draw_top();
+ cam_x = keep_x, cam_y = keep_y;
+}
diff --git a/games/undertale/src/world.h b/games/undertale/src/world.h
new file mode 100644
index 00000000..84eb9595
--- /dev/null
+++ b/games/undertale/src/world.h
@@ -0,0 +1,100 @@
+/* Undertale: the overworld. Rooms, the camera, Frisk, the room's objects.
+ *
+ * Positions are the area map's pixels (tools/rooms.py): the rooms of an area
+ * keep their places on it, so walking out of a room's rectangle walks into the
+ * next room, after a fade, as in the game. */
+#ifndef UT_WORLD_H
+#define UT_WORLD_H
+#include
+#include
+#include "res.h"
+
+enum { DIR_DOWN, DIR_UP, DIR_LEFT, DIR_RIGHT };
+
+/* an object of the room: a character, a SAVE Point, a sign, a trigger... */
+enum {
+ OBJ_NONE, OBJ_NPC, OBJ_SAVE, OBJ_SIGN, OBJ_TRIGGER, OBJ_DECO, OBJ_ROCK, OBJ_SPIKES, OBJ_SWITCH, OBJ_ITEM,
+ OBJ_MARK
+};
+enum {
+ OF_SOLID = 1, /* Frisk can't walk through its box */
+ OF_TALK = 2, /* pressing Z facing it runs its event */
+ OF_TOUCH = 4, /* walking into its box runs its event */
+ OF_HIDDEN = 8, /* not drawn */
+ OF_ANIM = 16, /* its frames cycle (anim_speed) even standing */
+ OF_WALKANIM = 32, /* its frames cycle while it walks */
+ OF_FRONT = 64, /* drawn over everything else */
+ OF_BACK = 128, /* drawn under everything else */
+ OF_DARK = 256, /* a silhouette */
+ OF_FLIP = 512, /* drawn mirrored */
+ OF_FLOOR = 1024 /* its box is floor to walk on (a bloomed bridge seed over water) */
+};
+#define MAX_OBJS 48
+typedef struct {
+ uint8_t kind, dir;
+ uint16_t flags;
+ int16_t x, y; /* its sprite's origin (the frame's top-left for the game's sprites) */
+ int16_t bx, by, bw, bh; /* its box, from (x, y) */
+ uint16_t spr[4]; /* its sprite for each direction (spr[0] if it has one) */
+ uint8_t frame, anim_speed, anim_t, depth_bias;
+ uint16_t event; /* EV_* run when talked to or touched */
+ int16_t arg, arg2; /* the kind's own (a sign's text, a flag...) */
+ /* walking somewhere: (tx, ty) at speed pixels a frame */
+ int16_t tx, ty;
+ uint8_t speed, walking;
+ uint8_t alpha;
+ uint8_t name; /* an ACTOR_* (scripts find it by it) */
+ int16_t fx, fy; /* fractions (1/16 pixel) for slow walks */
+ const char *text; /* what a sign or a plain thing says */
+} Obj;
+extern Obj objs[MAX_OBJS];
+
+typedef struct {
+ int16_t x, y; /* Frisk's sprite's top-left */
+ uint8_t dir, frame, anim, moving;
+ bool frozen; /* a cutscene has control */
+ bool hidden;
+ uint16_t steps; /* towards an encounter */
+} Player;
+extern Player pl;
+
+extern int g_room; /* ROOM_* */
+extern int cam_x, cam_y; /* the screen's top-left on the map */
+extern bool cam_locked; /* a cutscene moves the camera */
+
+const RoomInfo *world_room_info(void);
+void world_enter(int room, int x, int y, int dir); /* (x, y) Frisk's top-left; no fade */
+void world_update(uint32_t keys, uint32_t pressed);
+void world_draw(void);
+#define WALK_FLOOR_ONLY (-2) /* (world_walkable's ignore: the floor alone, nothing solid standing on it) */
+bool world_walkable(int x, int y, int w, int h, int ignore); /* a box on the map, objects too (not `ignore`) */
+void world_camera(void); /* follows Frisk, inside the room */
+Obj *obj_new(int kind, int x, int y);
+Obj *obj_find(int name);
+Obj *obj_get(int name); /* obj_find, or a spare object when there is none (events can't crash on it) */
+void obj_clear_all(void);
+/* the room's things, made by rooms.c */
+Obj *obj_npc(int name, const uint16_t spr[4], int x, int y, int dir, int event);
+Obj *obj_toriel(int x, int y, int dir);
+Obj *obj_sign(int x, int y, int w, int h, const char *text);
+Obj *obj_save(int x, int y, const char *text);
+Obj *obj_deco(uint16_t spr, int x, int y, bool solid);
+Obj *obj_trigger(int x, int y, int w, int h, int event, int name);
+Obj *obj_spikes(int x, int y, int cols, int rows, bool raised, int name);
+void obj_spikes_set(Obj *o, bool raised);
+Obj *obj_rock(int x, int y, int name, int event); /* pushed by walking into it; with an event, it talks */
+void obj_set_box(Obj *o, int bx, int by, int bw, int bh);
+bool near_frisk(const Obj *o, int d); /* within d pixels (their feet) */
+bool room_ice(int x, int y); /* rooms.c: ice there (Frisk slides) */
+int room_speed(int x, int y); /* rooms.c: Frisk's steps a frame there (3; webs slow him) */
+/* rooms.c: a pushed rock that the floor stops (it may go into water, a bridge seed), then where it went */
+bool room_rock_enters(Obj *o, int x, int y);
+void room_rock_moved(Obj *o);
+bool pl_sliding(void);
+/* the room fades out, then in: room changes in the middle */
+void world_fade_to(int room, int x, int y, int dir);
+void world_fade_hold(int room, int x, int y, int dir, int hold); /* black for `hold` frames in the middle */
+bool world_fading(void);
+extern uint8_t g_fade; /* 0 clear, 255 black: drawn over the room */
+extern int g_shake; /* frames the view shakes */
+#endif
diff --git a/games/undertale/tests/check.py b/games/undertale/tests/check.py
new file mode 100644
index 00000000..67356fd5
--- /dev/null
+++ b/games/undertale/tests/check.py
@@ -0,0 +1,325 @@
+#!/usr/bin/env python3
+"""Checks of Undertale on a computer (make test): the host build, with
+AddressSanitizer and UBSan, through the menus, every battle, the saves, the
+whole Ruins, the whole of Snowdin and Waterfall, and Hotland as far as it goes.
+
+ python3 tests/check.py build/play [--quick]
+
+--quick skips playing the whole Ruins, Snowdin, Waterfall and Hotland (the longest parts)."""
+import os
+import re
+import shutil
+import subprocess
+import sys
+import tempfile
+
+PLAY = sys.argv[1] if len(sys.argv) > 1 else "build/play"
+HERE = os.path.dirname(os.path.abspath(__file__))
+QUICK = "--quick" in sys.argv
+failures = []
+ENV = dict(os.environ, ASAN_OPTIONS="detect_leaks=0", UBSAN_OPTIONS="halt_on_error=1:print_stacktrace=1")
+
+# the flags the checks look at (src/flags.h)
+FLAGS = {}
+src = open(os.path.join(HERE, "..", "src", "flags.h")).read()
+body = re.sub(r"/\*.*?\*/", "", src[src.index("enum {") + 6:src.rindex("FLAG_COUNT")], flags=re.S)
+for i, n in enumerate(n.strip() for n in body.split(",") if n.strip()):
+ FLAGS[n] = i
+
+MODE_WORLD, MODE_BATTLE, MODE_END, MODE_SHOP = 2, 3, 8, 9
+# rooms and battles (src/data.h, src/monsters.h)
+ROOMS = {m.group(1): int(m.group(2)) for m in
+ re.finditer(r"#define ROOM_(\w+) (\d+)", open(os.path.join(HERE, "..", "src", "data.h")).read())}
+FORM_SNOWDRAKE, FORM_PAPYRUS = 19, 31
+(FORM_AARON, FORM_WOSHUA, FORM_WOSHUA_AARON, FORM_MOLDBYGG, FORM_MOLDBYGG_MOLDSMAL, FORM_MOLDBYGG_WOSHUA, FORM_SHYREN,
+ FORM_MAD_DUMMY, FORM_TEMMIE, FORM_TEMMIE_MOLDBYGG, FORM_UNDYNE) = range(32, 43)
+(FORM_VULKIN, FORM_TSUNDERPLANE, FORM_TSUNDER_VULKIN, FORM_PYROPE, FORM_PYROPE2, FORM_MTT_QUIZ, FORM_MTT_COOK,
+ FORM_LASERS, FORM_RGUARDS, FORM_BOMB, FORM_MUFFET, FORM_MTT_TILES) = range(43, 55)
+ITEM_OLD_TUTU, ITEM_BALLET_SHOES, ITEM_QUICHE, ITEM_PUNCH_CARD, ITEM_ARTIFACT, ITEM_ASTRONAUT_FOOD = range(17, 23)
+ITEM_BURNT_PAN, ITEM_INSTANT_NOODLES, ITEM_HOT_DOG, ITEM_STAINED_APRON = 28, 29, 30, 31
+ITEM_NICE_CREAM = 13
+ITEM_STARFAIT, ITEM_GLAMBURGER = 32, 33
+ITEM_WORN_DAGGER, ITEM_HEART_LOCKET = 41, 42
+
+
+def run(args, frames, saves=None, what=""):
+ """Plays (in a save folder, a new one if None); returns the end state: mode, room, flags, items..."""
+ own = saves is None
+ if own:
+ saves = tempfile.mkdtemp(prefix="undertale_check_")
+ p = subprocess.run([PLAY, "--saves", saves, "--frames", str(frames)] + args, capture_output=True, text=True,
+ env=ENV, timeout=900)
+ if own:
+ shutil.rmtree(saves, ignore_errors=True)
+ if p.returncode or "runtime error" in p.stderr or "AddressSanitizer" in p.stderr:
+ failures.append(f"{what or args}: crashed or a sanitizer error\n{p.stderr[-2000:]}")
+ return None
+ m = re.search(r"END mode (\d+) room (-?\d+) at (-?\d+),(-?\d+) hp (-?\d+) lv (\d+) gold (\d+) kills (\d+) path (\w+)",
+ p.stdout)
+ if not m:
+ failures.append(f"{what}: no end state\n{p.stdout[-500:]}")
+ return None
+ s = dict(zip(("mode", "room", "x", "y", "hp", "lv", "gold", "kills"), map(int, m.groups()[:8])))
+ s["path"] = m.group(9)
+ fl = re.search(r"FLAGS(.*)", p.stdout).group(1).split()
+ s["flags"] = {int(a): int(b) for a, b in (f.split("=") for f in fl)}
+ s["items"] = [int(v) for v in re.search(r"ITEMS(.*)", p.stdout).group(1).split()]
+ return s
+
+
+def flag(s, name):
+ return s["flags"].get(FLAGS["F_" + name], 0)
+
+
+def check(cond, msg):
+ if not cond:
+ failures.append(msg)
+
+
+# the intro, the title, the instructions, naming the fallen human "A", Done, Yes: the flowers
+keys = "30:z,60:z,90:z,120:z," + ",".join("%d:down" % (150 + 10 * i) for i in range(8)) + \
+ ",230:right,240:right,250:z,290:right,300:z"
+s = run(["--keys", keys], 700, what="the menus to a new game")
+check(s and s["mode"] == MODE_WORLD and s["room"] == 0 and s["hp"] == 20, f"the menus did not start a game: {s}")
+
+# every battle of the Ruins, fled from (Toriel's can't be: she is spared) and fought to the end
+for form in range(18):
+ s = run(["--god", "--battle", str(form), "--path", "flee"], 1500, what=f"battle {form}, fleeing")
+ if form == 17:
+ continue
+ check(s and s["mode"] == MODE_WORLD, f"battle {form}: fleeing did not end it: {s}")
+for form in (2, 3, 6, 10, 12):
+ s = run(["--god", "--battle", str(form), "--path", "fight"], 3000, what=f"battle {form}, fighting")
+ check(s and s["mode"] == MODE_WORLD and s["kills"] >= 1, f"battle {form}: FIGHT did not win it: {s}")
+s = run(["--god", "--battle", "16", "--path", "act:3"], 6000, what="Napstablook, cheered up")
+check(s and s["mode"] == MODE_WORLD and s["kills"] == 0, f"Napstablook was not spared by cheering: {s}")
+
+# Snowdin's battles: fled from, fought (the Snowdrake, the Gyftrot, the Glyde, the Greater Dog...), and Papyrus,
+# who can't be fled: spared once he spares you, or (the SOUL hurt) he captures you
+for form in range(FORM_SNOWDRAKE, FORM_PAPYRUS):
+ s = run(["--god", "--battle", str(form), "--path", "flee"], 1500, what=f"battle {form}, fleeing")
+ check(s and s["mode"] == MODE_WORLD, f"battle {form}: fleeing did not end it: {s}")
+for form in (19, 20, 26, 27, 28, 30):
+ s = run(["--god", "--battle", str(form), "--path", "fight"], 4000, what=f"battle {form}, fighting")
+ check(s and s["mode"] == MODE_WORLD and s["kills"] >= 1, f"battle {form}: FIGHT did not win it: {s}")
+s = run(["--god", "--battle", str(FORM_PAPYRUS), "--path", "spare"], 15000, what="Papyrus, spared")
+check(s and s["mode"] == MODE_WORLD and s["kills"] == 0, f"Papyrus was not spared: {s}")
+s = run(["--battle", str(FORM_PAPYRUS), "--path", "spare"], 6000, what="Papyrus, captured")
+check(s and s["mode"] == MODE_WORLD and s["hp"] == 1, f"Papyrus did not capture Frisk: {s}")
+
+# Waterfall's battles: fled from, fought, and each spared its own way (Aaron out-flexed, Woshua's green caught after
+# asking to be cleaned, Moldbygg hugged then let go, Shyren hummed along with, Temmie talked to); Mad Dummy can't be
+# fled or hurt: its fight lasts until Napstablook comes. Undyne can't be spared: fled from (her arrows blocked with
+# the green SOUL's shield)
+for form in list(range(FORM_AARON, FORM_SHYREN + 1)) + [FORM_TEMMIE, FORM_TEMMIE_MOLDBYGG, FORM_UNDYNE]:
+ s = run(["--god", "--battle", str(form), "--path", "flee"], 1500, what=f"battle {form}, fleeing")
+ check(s and s["mode"] == MODE_WORLD, f"battle {form}: fleeing did not end it: {s}")
+for form in (FORM_AARON, FORM_WOSHUA, FORM_MOLDBYGG, FORM_SHYREN, FORM_TEMMIE):
+ s = run(["--god", "--battle", str(form), "--path", "fight"], 4000, what=f"battle {form}, fighting")
+ check(s and s["mode"] == MODE_WORLD and s["kills"] >= 1, f"battle {form}: FIGHT did not win it: {s}")
+for form, seq, gold in ((FORM_AARON, "1,1,1", 30), (FORM_WOSHUA, "1,1,1", 25), (FORM_WOSHUA_AARON, "1,1@2,1@2,1@2", 55),
+ (FORM_MOLDBYGG, "1,3", 20), (FORM_MOLDBYGG_WOSHUA, "1,3,1@2,1@2,1@2", 45),
+ (FORM_SHYREN, "2,2,2,2,2", 20), (FORM_TEMMIE, "2", 0), (FORM_TEMMIE_MOLDBYGG, "2,1@2,3@2", 20)):
+ s = run(["--god", "--battle", str(form), "--path", "seq:" + seq], 9000, what=f"battle {form}, sparing")
+ check(s and s["mode"] == MODE_WORLD and s["kills"] == 0 and s["gold"] == gold,
+ f"battle {form}: the friendly way did not spare it: {s}")
+s = run(["--god", "--battle", str(FORM_MAD_DUMMY), "--path", "act:1"], 9000, what="Mad Dummy")
+check(s and s["mode"] == MODE_WORLD and s["kills"] == 0, f"Mad Dummy's fight did not end: {s}")
+
+# Hotland's battles: fled from, fought, spared (Vulkin encouraged, Pyrope heated up twice); Mettaton's quiz, his
+# cooking show's jetpack and a laser's alarm play their own way to their end
+for form in range(FORM_VULKIN, FORM_PYROPE2 + 1):
+ s = run(["--god", "--battle", str(form), "--path", "flee"], 1500, what=f"battle {form}, fleeing")
+ check(s and s["mode"] == MODE_WORLD, f"battle {form}: fleeing did not end it: {s}")
+for form in (FORM_VULKIN, FORM_TSUNDERPLANE, FORM_PYROPE):
+ s = run(["--god", "--battle", str(form), "--path", "fight"], 4000, what=f"battle {form}, fighting")
+ check(s and s["mode"] == MODE_WORLD and s["kills"] >= 1, f"battle {form}: FIGHT did not win it: {s}")
+for form, seq, gold in ((FORM_VULKIN, "2", 40), (FORM_PYROPE, "1,1", 45), (FORM_PYROPE2, "1,1,1@2,1@2", 90)):
+ s = run(["--god", "--battle", str(form), "--path", "seq:" + seq], 9000, what=f"battle {form}, sparing")
+ check(s and s["mode"] == MODE_WORLD and s["kills"] == 0 and s["gold"] == gold,
+ f"battle {form}: the friendly way did not spare it: {s}")
+for form in (FORM_MTT_QUIZ, FORM_MTT_COOK, FORM_LASERS):
+ s = run(["--god", "--battle", str(form), "--path", "talk"], 3000, what=f"show {form}")
+ check(s and s["mode"] == MODE_WORLD, f"show {form} did not end: {s}")
+# the Royal Guards (no running from them): fought, or spared (02's armor cleaned twice, its green stars caught, a
+# whisper to 01); a bomb of the news report (the dog), defused by Defuse or by FIGHT, in the DEFUSE ZONE
+s = run(["--god", "--battle", str(FORM_RGUARDS), "--path", "fight"], 9000, what="the Royal Guards, fought")
+check(s and s["mode"] == MODE_WORLD and s["kills"] == 2, f"the Royal Guards: FIGHT did not win it: {s}")
+s = run(["--god", "--battle", str(FORM_RGUARDS), "--path", "seq:1@2,1@2,2@1"], 9000, what="the Royal Guards, spared")
+check(s and s["mode"] == MODE_WORLD and s["kills"] == 0 and s["gold"] == 100,
+ f"the Royal Guards: the friendly way did not spare them: {s}")
+for how in ("act:1", "fight"):
+ s = run(["--god", "--battle", str(FORM_BOMB), "--path", how], 9000, what=f"a bomb, {how}")
+ check(s and s["mode"] == MODE_WORLD and s["kills"] == 0, f"a bomb: {how} did not defuse it: {s}")
+# Muffet (no running from her): spared once her spiders send word, her 100G; Mettaton after the tile puzzle: ACT
+# Yellow, and the yellow SOUL's shots drain his power
+s = run(["--god", "--battle", str(FORM_MUFFET), "--path", "spare"], 9000, what="Muffet, spared")
+check(s and s["mode"] == MODE_WORLD and s["kills"] == 0 and s["gold"] == 100, f"Muffet was not spared: {s}")
+s = run(["--god", "--battle", str(FORM_MTT_TILES), "--path", "act:2"], 4000, what="Mettaton's yellow button")
+check(s and s["mode"] == MODE_WORLD and s["kills"] == 0, f"Mettaton's fight did not end with the yellow button: {s}")
+
+# the shop in Snowdin: a Cinnamon Bunny for 25G; Papyrus's date: his phone number
+s = run(["--name", "Frisk", "--noenc", "--room", str(ROOMS["SNOWTOWN"]), "--at", "446,1092", "--gold", "100", "--keys",
+ "2-24:up,60:z,90:down,100:down,110:down,120:z,140:z,170:x,190:down,200:down,210:down,220:z"], 400, what="the shop")
+check(s and s["gold"] == 75 and s["items"][0] == 16, f"the shop did not sell a Cinnamon Bunny: {s}")
+s = run(["--name", "Frisk", "--noenc", "--room", str(ROOMS["PSHOUSE"]), "--at", "2436,914", "--flag",
+ f"{FLAGS['F_PAPYRUS']}=1", "--flag", f"{FLAGS['F_PAP_DATE']}=1", "--path",
+ "go:2300,918;l;w5;l;w5;l;w5;z;pick:0;talk;w60"], 2500, what="Papyrus's date")
+check(s and flag(s, "PAP_DATE") == 2, f"the date did not end with Papyrus's number: {s}")
+
+# the leaf pile's SAVE, then Continue from the title
+d = tempfile.mkdtemp(prefix="undertale_check_")
+s = run(["--name", "Frisk", "--noenc", "--room", "8", "--at", "5012,4498", "--path", "u;z;w20;z;w30;x"], 300, saves=d,
+ what="saving at the leaf pile")
+check(os.path.exists(d + "/undertale.sav") and os.path.exists(d + "/undertale_saves.py"), "the SAVE Point did not save")
+s = run(["--keys", "30:z,60:z,90:z,120:z"], 300, saves=d, what="Continue")
+check(s and s["mode"] == MODE_WORLD and s["room"] == 8, f"Continue did not go back to the leaf pile: {s}")
+# Home keeps the moment; the next start is back there
+s = run(["--keys", "30:z,60:z,90:z,120:z,130-170:right,200:home"], 300, saves=d, what="Home")
+s = run([], 30, saves=d, what="back after Home")
+check(s and s["mode"] == MODE_WORLD and s["room"] == 8 and s["x"] > 5000, f"Home did not bring the game back: {s}")
+shutil.rmtree(d, ignore_errors=True)
+
+# the whole Ruins, the friendly way: Flowey, Toriel's puzzles, the candy, the rocks, Napstablook, the switches,
+# Toriel's home, her fight spared, Flowey's judgement, the door out
+if not QUICK:
+ s = run(["--name", "Frisk", "--noenc", "--god", "--path-file", os.path.join(HERE, "ruins.path")], 40000,
+ what="the Ruins")
+ check(s and s["mode"] == MODE_WORLD and s["room"] == ROOMS["SNOWDIN1"], f"the Ruins' door did not lead to Snowdin: {s}")
+ if s:
+ for name, v in (("FLOWEY_MET", 1), ("DUMMY", 1), ("RUINS5_DONE", 1), ("RUINS6_DONE", 1), ("PHONE", 1),
+ ("RUINS9_ROCK", 1), ("RUINS11_ROCK", 5), ("NAPSTA", 1), ("ONESWITCH", 1), ("ROTATE4", 1),
+ ("TORIEL_HOME", 1), ("HOME_TOUR", 1), ("TORIEL_BASEMENT", 1), ("BASEMENT_TALK", 3),
+ ("TORIEL_FIGHT", 1), ("FLOWEY_END", 1), ("RUINS_EXIT", 1)):
+ check(flag(s, name) == v, f"the Ruins: F_{name} is {flag(s, name)}, not {v}")
+ check(s["kills"] == 0, f"the Ruins: {s['kills']} killed on the friendly way")
+ check(s["items"].count(1) == 2, f"the Ruins: not the two pieces of Monster Candy: {s['items']}")
+
+# the whole of Snowdin, the friendly way: Sans, Papyrus's puzzles (the maze, the X/O ones, the ice, the tiles),
+# Doggo, Dogamy and Dogaressa, Greater Dog, the Gauntlet, the town, Papyrus spared, the way on
+if not QUICK:
+ s = run(["--name", "Frisk", "--noenc", "--god", "--room", str(ROOMS["SNOWDIN1"]), "--at", "34,392", "--path-file",
+ os.path.join(HERE, "snowdin.path")], 30000, what="Snowdin")
+ check(s and s["mode"] == MODE_WORLD and s["room"] == ROOMS["WF1"], f"Snowdin did not lead on to Waterfall: {s}")
+ if s:
+ for name, v in (("BRANCH", 1), ("SANS_MET", 1), ("PAP_SENTRY", 1), ("DOGGO", 1), ("ELECMAZE", 2),
+ ("WORDSEARCH", 1), ("SNOWPUZZLE", 1), ("DOGI", 1), ("XOXO1", 1), ("XOXO2", 1), ("XOXO3", 1),
+ ("PAP_XOXO1", 2), ("TILES", 1), ("GREATERDOG", 1), ("GAUNTLET", 1), ("PAPYRUS", 1),
+ ("PAP_DATE", 1), ("TOWN_HOUSE", 1), ("SNOW_EXIT", 1)):
+ check(flag(s, name) == v, f"Snowdin: F_{name} is {flag(s, name)}, not {v}")
+ check(s["kills"] == 0, f"Snowdin: {s['kills']} killed on the friendly way")
+
+# Waterfall, the friendly way: Monster Kid, Grillby's with Sans, the tutu, Papyrus's report, the bridge seeds (and
+# the secret one to the quiche), the wishing room, Undyne's spears, the telescope, the punch card, the marsh (the
+# ballet shoes), Onionsan, the piano and the artifact, Shyren, the umbrellas, Monster Kid in the rain, the bridge cut,
+# the garbage dump, Mad Dummy, Napstablook's house, the snail farm, Gerson, the mushroom maze and Temmie Village, the
+# dead end, Monster Kid helped up on the chasm's bridge, Undyne's fight (fled), her chase, Papyrus's call, and Hotland,
+# where she falls in the heat and gets the cooler's water
+if not QUICK:
+ s = run(["--name", "Frisk", "--noenc", "--god", "--room", str(ROOMS["WF1"]), "--at", "30,1480", "--path-file",
+ os.path.join(HERE, "waterfall.path")], 60000, what="Waterfall")
+ check(s and s["mode"] == MODE_WORLD and s["room"] == ROOMS["HT_LABFRONT"], f"Waterfall did not end in Hotland: {s}")
+ if s:
+ for name, v in (("WF_MK", 1), ("GRILLBYS", 1), ("WF_TUTU", 1), ("WF_UNDYNE1", 1), ("SEEDS1", 1), ("SEEDS2", 1),
+ ("SEEDS3", 1), ("WF_QUICHE", 1), ("CHASE1", 1), ("WF_TELESCOPE", 1), ("WF_PUNCH", 1), ("WF_BALLET", 1),
+ ("WF_ONION", 2), ("PIANO", 1), ("WF_ARTIFACT", 1), ("SHYREN", 1),
+ ("UMBRELLA", 3), ("MK_RAIN", 2), ("WF_BRIDGE", 1), ("WF_DUMP", 1), ("WF_COOLER", 2),
+ ("MAD_DUMMY", 1), ("NAPSTA_HOME", 2), ("TEM_VILLAGE", 1), ("DEADEND", 1), ("MK_CHASM", 2),
+ ("UNDYNE", 4), ("WATER_CUP", 2), ("PAP_NEON", 1)):
+ check(flag(s, name) == v, f"Waterfall: F_{name} is {flag(s, name)}, not {v}")
+ check(s["kills"] == 0, f"Waterfall: {s['kills']} killed on the friendly way")
+ for item in (ITEM_OLD_TUTU, ITEM_BALLET_SHOES, ITEM_QUICHE, ITEM_PUNCH_CARD, ITEM_ARTIFACT, ITEM_ASTRONAUT_FOOD):
+ check(item in s["items"], f"Waterfall: item {item} not picked up: {s['items']}")
+
+# Hotland's first floor: Alphys and the quiz, the noodles, the belts, the vents, the lasers, both shooting puzzles, the
+# cooking show, up the elevator to the second floor
+if not QUICK:
+ s = run(["--name", "Frisk", "--noenc", "--god", "--room", str(ROOMS["HT_LABFRONT"]), "--at", "1000,6230",
+ "--path-file", os.path.join(HERE, "hotland.path")], 16000, what="Hotland")
+ check(s and s["mode"] == MODE_WORLD and s["room"] == ROOMS["HT_HOTDOG"],
+ f"Hotland did not end up the elevator, on the second floor: {s}")
+ if s:
+ for name, v in (("HT_LAB", 2), ("HT_NOODLES", 1), ("HT_CALL_LASER", 1), ("HT_CALL_HUB", 1), ("HT_CALL_ISLE", 1),
+ ("HT_CALL_CORE", 1), ("HT_PUZ_R", 1), ("HT_PUZ_L", 1), ("HT_BLUE_OFF", 1), ("HT_COOK", 1),
+ ("HT_FLOOR1", 1)):
+ check(flag(s, name) == v, f"Hotland: F_{name} is {flag(s, name)}, not {v}")
+ check(ITEM_INSTANT_NOODLES in s["items"], f"Hotland: the noodles not taken: {s['items']}")
+
+# Hotland's second floor: Heats Flamesman, Sans's hot dog, the apron, the Art Club's room, the belt and vent puzzles,
+# the Bad Opinion Zone, the Royal Guards spared, Mettaton's news report, up the elevator to the third floor
+if not QUICK:
+ s = run(["--name", "Frisk", "--noenc", "--god", "--gold", "100", "--room", str(ROOMS["HT_HOTDOG"]), "--at", "6966,2380",
+ "--path-file", os.path.join(HERE, "hotland2.path")], 13000, what="Hotland's second floor")
+ check(s and s["mode"] == MODE_WORLD and s["room"] == ROOMS["HT_L3"],
+ f"Hotland's second floor did not end up the elevator, on the third floor: {s}")
+ if s:
+ for name, v in (("HT_HEATS", 1), ("HT_APRON", 1), ("HT_CALL_BELT", 1), ("HT_BELT", 1), ("HT_VENTPUZ", 1),
+ ("HT_CALL_BOZ", 1), ("HT_GUARDS", 1), ("HT_NEWS", 2), ("HT_CALL_CORE2", 1), ("HT_FLOOR2", 1)):
+ check(flag(s, name) == v, f"Hotland's second floor: F_{name} is {flag(s, name)}, not {v}")
+ check(s["kills"] == 0, f"Hotland's second floor: {s['kills']} killed on the friendly way")
+ check(s["gold"] == 170, f"Hotland's second floor: the hot dog or the guards' gold went wrong: {s['gold']}")
+ for item in (ITEM_HOT_DOG, ITEM_STAINED_APRON):
+ check(item in s["items"], f"Hotland's second floor: item {item} not picked up: {s['items']}")
+
+# Hotland's third floor: the bake sale, the vent islands, both shooting puzzles (the Diamond Boys, Alphys's anime),
+# the belts past the drones, Muffet spared, Mettaton's musical, the tile puzzle and his fight, the crossroads (a Nice
+# Cream, Heats Flamesman), up the stairs to the MTT Resort
+if not QUICK:
+ s = run(["--name", "Frisk", "--noenc", "--god", "--gold", "100", "--room", str(ROOMS["HT_L3"]), "--at", "3770,842",
+ "--path-file", os.path.join(HERE, "hotland3.path")], 14000, what="Hotland's third floor")
+ check(s and s["mode"] == MODE_WORLD and s["room"] == ROOMS["RES_OUTSIDE"],
+ f"Hotland's third floor did not end at the MTT Resort: {s}")
+ if s:
+ for name, v in (("HT_CALL_HUB3", 1), ("HT3_PUZ_N", 1), ("HT3_PUZ_S", 1), ("HT_ANIME", 1), ("HT_MUFFET", 1),
+ ("HT_MUSICAL", 1), ("HT_TILES3", 1), ("HT_HEATS3", 1), ("HT_FLOOR3", 1)):
+ check(flag(s, name) == v, f"Hotland's third floor: F_{name} is {flag(s, name)}, not {v}")
+ check(s["kills"] == 0, f"Hotland's third floor: {s['kills']} killed on the friendly way")
+ check(s["gold"] == 188, f"Hotland's third floor: Muffet's gold or the Nice Cream went wrong: {s['gold']}")
+ check(ITEM_NICE_CREAM in s["items"], f"Hotland's third floor: no Nice Cream: {s['items']}")
+
+# the MTT Resort: dinner with Sans, a Starfait from Burgerpants, a night in the hotel (200G), room service and the
+# dog's door (nothing to give), through the door north into the CORE
+if not QUICK:
+ s = run(["--name", "Frisk", "--noenc", "--god", "--gold", "400", "--room", str(ROOMS["HT_CROSS3"]), "--at", "7550,420",
+ "--path-file", os.path.join(HERE, "resort.path")], 5000, what="the MTT Resort")
+ check(s and s["mode"] == MODE_WORLD and s["room"] == ROOMS["CORE_APPROACH"], f"the MTT Resort did not end in the CORE: {s}")
+ if s:
+ for name, v in (("HT_FLOOR3", 1), ("RES_SANS", 1), ("RES_ROOM", 2), ("RES_TEA", 0), ("RES_DOG", 0)):
+ check(flag(s, name) == v, f"the MTT Resort: F_{name} is {flag(s, name)}, not {v}")
+ check(s["gold"] == 140, f"the MTT Resort: the Starfait or the room went wrong: {s['gold']}")
+ check(ITEM_STARFAIT in s["items"], f"the MTT Resort: no Starfait: {s['items']}")
+
+# the CORE: Madjick and Knight Knight spared, the switch's lasers, the bridge's power, the sage's puzzle, a Glamburger,
+# Mettaton turned round and his show a hit, Alphys by the last elevator, up to New Home
+if not QUICK:
+ s = run(["--name", "Frisk", "--noenc", "--god", "--room", str(ROOMS["RES_LOBBY"]), "--at", "1210,160",
+ "--path-file", os.path.join(HERE, "core.path")], 24000, what="the CORE")
+ check(s and s["mode"] == MODE_WORLD and s["room"] == ROOMS["NH_TOWN"], f"the CORE did not end up in New Home: {s}")
+ if s:
+ for name, v in (("CORE_IN", 1), ("CORE_MADJICK", 1), ("CORE_KNIGHT", 1), ("CORE_SWITCH", 1), ("CORE_POWER", 2),
+ ("CORE_SAGE", 1), ("CORE_FIELD", 1), ("CORE_TRASH1", 1), ("CORE_END", 1), ("CORE_MTT", 1),
+ ("CORE_HALL", 1), ("CORE_DONE", 1)):
+ check(flag(s, name) == v, f"the CORE: F_{name} is {flag(s, name)}, not {v}")
+ check(s["kills"] == 0, f"the CORE: {s['kills']} killed on the friendly way")
+ check(s["gold"] == 130, f"the CORE: Madjick's and Knight Knight's gold went wrong: {s['gold']}")
+ check(ITEM_GLAMBURGER in s["items"], f"the CORE: no Glamburger from the trash can: {s['items']}")
+
+# New Home: the keys and the chain, the presents, the story, Sans's judgment, Asgore in the throne room; at the
+# barrier, Asgore beaten (FIGHT) and spared; Omega Flowey (the six SOULs called on); Flowey spared; the end
+if not QUICK:
+ s = run(["--name", "Frisk", "--noenc", "--god", "--room", str(ROOMS["NH_TOWN"]), "--at", "90,4545",
+ "--path-file", os.path.join(HERE, "newhome.path")], 26000, what="New Home")
+ check(s and s["mode"] == MODE_END, f"New Home did not reach the end: {s}")
+ if s:
+ for name, v in (("NH_KEY1", 1), ("NH_KEY2", 1), ("NH_CHAIN", 1), ("NH_DAGGER", 1), ("NH_LOCKET", 1),
+ ("NH_STORY", 11), ("NH_SANS", 1), ("NH_THRONE", 1), ("NH_ASGORE", 1), ("NH_FLOWEY", 1),
+ ("NH_DONE", 1)):
+ check(flag(s, name) == v, f"New Home: F_{name} is {flag(s, name)}, not {v}")
+ check(s["kills"] == 0, f"New Home: {s['kills']} killed on the friendly way")
+ for item in (ITEM_WORN_DAGGER, ITEM_HEART_LOCKET):
+ check(item in s["items"], f"New Home: item {item} not found: {s['items']}")
+
+if failures:
+ print("\n".join("FAIL " + f for f in failures))
+ sys.exit(1)
+print("all checks passed")
diff --git a/games/undertale/tests/core.path b/games/undertale/tests/core.path
new file mode 100644
index 00000000..2f52d7a9
--- /dev/null
+++ b/games/undertale/tests/core.path
@@ -0,0 +1,113 @@
+# The CORE, from the MTT Resort's lobby (play it with --room 161 --at 1210,160 --name Frisk --noenc --god): its
+# approach, the lobby (the elevator's out of order), the doorway's Madjick (clear mind, then talk), the switch and its
+# lasers, the crossroads and Knight Knight north (three lullabies), the laser bridge and its power cut, the Core
+# Branch: the sage's puzzle (up, up, left, down, left, down, left, two shots) and a Glamburger from a trash can; The
+# End, Mettaton (turned round; then his show, boasting every turn until the ratings are high enough), the hallway
+# and Alphys, the elevator up to the castle
+w5
+1210,120
+1210,110
+w30
+go:310,2640
+310,2620
+talk
+w20
+go:310,2300
+310,2280
+talk
+w20
+go:310,2080
+310,2070
+talk
+go:320,2022
+u
+z
+talk
+go:200,1985
+200,1960
+w20
+go:150,1740
+seq:3,1
+talk
+w20
+go:160,1520
+160,1490
+w20
+go:160,1440
+go:255,1345
+u
+z
+talk
+w200
+talk
+go:530,1370
+go:830,1300
+go:830,1140
+seq:2,2,2
+talk
+go:830,1370
+go:1180,1370
+1236,1370
+talk
+w20
+go:1470,1370
+1500,1370
+talk
+go:2080,1370
+go:2080,1320
+talk
+go:2080,1160
+2080,1120
+talk
+w10
+go:1760,1110
+go:1760,374
+u
+z
+talk
+go:1760,860
+go:1470,844
+u
+z
+w10
+u
+w4
+u
+w4
+l
+w4
+d
+w4
+l
+w4
+d
+w4
+l
+w4
+z
+w20
+z
+w60
+talk
+go:2400,870
+go:2580,870
+2700,870
+w20
+go:3348,872
+3348,836
+w20
+go:3348,600
+act:1
+talk
+w30
+go:3352,345
+3352,320
+w20
+go:4200,130
+4240,130
+talk
+go:4288,100
+u
+z
+pick:0
+w60
diff --git a/games/undertale/tests/hotland.path b/games/undertale/tests/hotland.path
new file mode 100644
index 00000000..cdea25e9
--- /dev/null
+++ b/games/undertale/tests/hotland.path
@@ -0,0 +1,145 @@
+# Hotland's first floor, from the crossroads before the Lab (play it with --room 127 --at 1000,6230 --name Frisk
+# --noenc --god): Alphys and Mettaton's quiz, the phone's upgrade and the noodles in the fridge
+go:1300,6262
+1350,6250
+w30
+talk
+w30
+talk
+go:2050,6232
+u
+z
+pick:0
+talk
+# out the Lab's east door, up the conveyor belts
+go:2500,6255
+2545,6255
+w20
+go:2958,6250
+go:2958,6010
+2958,5960
+w20
+go:2915,5880
+go:2915,5470
+go:2958,5420
+go:2958,5260
+2958,5200
+w20
+go:2860,5150
+go:2860,4740
+go:2958,4700
+go:2958,4400
+2958,4340
+w20
+# the Magma Chamber: the vents east, up and west, to the way north (the burnt pan's room left aside)
+go:2968,4250
+go:3190,4226
+vent:3240,4226
+w20
+3302,4198
+3380,4198
+go:3405,4170
+vent:3422,4168
+w20
+go:3340,4048
+vent:3366,3990
+w20
+go:3185,3960
+3185,3900
+w20
+# the laser walkway (its alarm, when a laser catches Frisk), Alphys's call
+go:3185,3830
+talk
+go:3400,3820
+go:4150,3820
+4215,3820
+w20
+# the puzzle hub: its vent turns a quarter each time (north: the locked door, east: the right puzzle, south, west: the
+# left one, its blue laser switched off by Alphys), both puzzles solved, then north through the door
+go:4820,3800
+go:4820,3690
+talk
+vent:4820,3600
+w20
+u
+z
+talk
+vent:4820,3600
+w20
+vent:4826,3612
+w20
+go:5262,3610
+5262,3560
+w20
+go:5258,3424
+u
+z
+w10
+u
+w4
+l
+w4
+z
+w30
+z
+w80
+talk
+go:5260,3480
+5260,3520
+w20
+go:4960,3610
+vent:4890,3610
+w20
+vent:4810,3610
+w20
+vent:4820,3600
+w20
+go:4500,3610
+go:4392,3610
+talk
+4392,3560
+w20
+go:4389,3424
+u
+z
+w10
+u
+w4
+l
+w4
+d
+w4
+l
+w4
+z
+w30
+z
+w80
+talk
+go:4390,3480
+4390,3520
+w20
+go:4700,3610
+vent:4770,3610
+w20
+vent:4840,3610
+w20
+4820,3420
+w30
+# over the vents to Mettaton's cooking show (the jetpack), the CORE far off, and the elevator up, where the game
+# now ends
+go:4818,3150
+go:4900,3110
+vent:4960,3110
+w60
+w60
+go:5330,3110
+talk
+w30
+talk
+go:6300,3120
+talk
+go:6960,3110
+u
+z
+pick:0
diff --git a/games/undertale/tests/hotland2.path b/games/undertale/tests/hotland2.path
new file mode 100644
index 00000000..545bdc7a
--- /dev/null
+++ b/games/undertale/tests/hotland2.path
@@ -0,0 +1,114 @@
+# Hotland's second floor, out of the elevator (play it with --room 140 --at 6966,2380 --name Frisk --noenc --god
+# --gold 100): Heats Flamesman's name, a hot dog from Sans
+go:7030,2400
+r
+z
+talk
+go:6600,2364
+u
+z
+pick:0
+talk
+# the crossroads: the Stained Apron south, the invisible path north to the Art Club's room
+go:5900,2400
+go:5900,2548
+5900,2600
+go:5840,2628
+u
+z
+talk
+go:5900,2600
+5900,2560
+go:5900,2240
+5900,2180
+go:5900,2160
+u
+z
+talk
+go:5900,2190
+5900,2215
+w10
+# the conveyor belt puzzle: Alphys's call, then the top belt's switches pressed right to left as it carries Frisk by
+# (her call after the second, and the force field off)
+go:5760,2400
+5690,2400
+w10
+talk
+go:5460,2400
+5460,2330
+5421,2330
+l
+u
+w16
+z
+w26
+z
+talk
+w60
+go:5090,2400
+5040,2400
+w20
+# the vent puzzle: right to left in a U, a step back once (each panel landed on turns every vent; out by the top-left
+# platform's, turned)
+go:4750,2400
+go:4740,2300
+vent:4722,2264
+w20
+vent:4640,2250
+w20
+vent:4640,2382
+w20
+vent:4640,2450
+w20
+vent:4640,2350
+w20
+vent:4656,2466
+w20
+vent:4540,2482
+w20
+vent:4540,2350
+w20
+vent:4556,2266
+w20
+# the Bad Opinion Zone (Alphys's call), the Royal Guards at the bend: 02's armor cleaned, its green stars caught, a
+# whisper to 01, both spared
+go:4410,2255
+4380,2255
+w10
+go:4100,2275
+go:3760,2275
+3730,2275
+w10
+go:3540,2275
+seq:1@2,1@2,2@1
+talk
+go:3230,2275
+go:3230,2040
+go:3670,2030
+3695,2030
+w20
+# Mettaton's news report: the dog picked, every bomb revealed; the dog defused, then the clock runs out (Alphys
+# switches the big one off)
+go:3895,1748
+talk
+go:4008,1720
+u
+z
+talk
+4008,1714
+act:1
+talk
+w3700
+talk
+# up past the news to the CORE again (Alphys's call), and the elevator up to the third floor
+go:4590,1770
+go:4600,1560
+4600,1500
+w20
+go:4600,1440
+talk
+go:3770,1380
+go:3770,1348
+u
+z
+pick:0
diff --git a/games/undertale/tests/hotland3.path b/games/undertale/tests/hotland3.path
new file mode 100644
index 00000000..3f1360ea
--- /dev/null
+++ b/games/undertale/tests/hotland3.path
@@ -0,0 +1,222 @@
+# Hotland's third floor, out of the elevator (play it with --room 150 --at 3770,842 --name Frisk --noenc --god
+# --gold 100): the spiders' bake sale (too dear for Frisk), Alphys's call by the vent islands
+w30
+go:4150,870
+u
+z
+talk
+pick:0
+talk
+go:4555,870
+4575,870
+talk
+# the islands' vents to the north walkway: right, up, right, down, down, right, up, right, up, up, left, up
+vent:4600,870
+w20
+vent:4660,850
+w20
+vent:4676,786
+w20
+vent:4740,806
+w20
+vent:4740,886
+w20
+vent:4756,946
+w20
+vent:4820,930
+w20
+vent:4836,866
+w20
+vent:4900,850
+w20
+vent:4900,770
+w20
+vent:4884,706
+w20
+vent:4820,690
+w30
+# the north puzzle (left, right, then shoot), and the Diamond Boys
+go:4820,175
+u
+z
+w10
+l
+w4
+r
+w4
+z
+w60
+talk
+w30
+# back down; the vents to the south walkway: down, left, down, down, right, right, down, left, down
+go:4820,600
+4820,650
+w30
+vent:4820,726
+w20
+vent:4804,786
+w20
+vent:4740,806
+w20
+vent:4740,886
+w20
+vent:4756,946
+w20
+vent:4836,946
+w20
+vent:4900,966
+w20
+vent:4884,1026
+w20
+vent:4820,1046
+w30
+# the belts round the south, ridden without a step past the drones' blue lasers; the south puzzle (left, up, left,
+# left, right, shoot), and Alphys asks about anime
+go:4830,1385
+4830,1400
+w340
+go:5520,1400
+go:5530,1148
+u
+z
+w10
+l
+w4
+u
+w4
+l
+w4
+l
+w4
+r
+w4
+z
+w60
+talk
+pick:0
+talk
+w30
+# back along the top belt, up to the islands; east: left, up, right, right, right, up, right; through the door
+go:5520,1400
+5480,1400
+w10
+go:5400,1335
+w200
+go:4830,1200
+4830,1090
+w30
+vent:4804,1026
+w20
+vent:4740,1010
+w20
+vent:4756,946
+w20
+vent:4836,946
+w20
+vent:4916,946
+w20
+vent:4980,930
+w20
+vent:4996,866
+w30
+go:5230,712
+5230,690
+w30
+go:5400,462
+5440,462
+w20
+# Muffet's lair: the webs, Muffet (spared once the spiders send word)
+go:5650,455
+go:6230,455
+spare
+talk
+# Mettaton's musical, the trapdoor; the tile puzzle against the clock (each purple slides Frisk on, so the steps
+# aim where the slides end); Mettaton's fight, his power drained with the yellow button
+go:6630,455
+6660,455
+w20
+go:6740,455
+6790,455
+talk
+w30
+talk
+go:6620,646
+6640,646
+6640,626
+6660,626
+6700,626
+6700,646
+6720,646
+6740,646
+6740,666
+6760,666
+6780,666
+6840,666
+6860,666
+6860,626
+6880,626
+6900,626
+6900,646
+6900,666
+6920,666
+6940,666
+6940,646
+6980,646
+6980,626
+7000,626
+7000,666
+7020,666
+7040,666
+7040,626
+7060,626
+7060,666
+7080,666
+7100,666
+7120,666
+7120,646
+7140,646
+7140,626
+7120,626
+7120,646
+7120,666
+7140,666
+7180,666
+7180,626
+7240,626
+7260,626
+7260,646
+7260,666
+7280,666
+7300,666
+7320,666
+7340,666
+7340,646
+7340,626
+7340,606
+7360,606
+7380,606
+7380,626
+7380,646
+7380,666
+7400,666
+7430,666
+act:2
+talk
+w30
+# the crossroads: a Nice Cream, Heats Flamesman's name, and the stairs north to the MTT Resort, where the game
+# now ends
+go:7550,560
+7550,530
+w30
+go:7470,408
+u
+z
+talk
+go:8030,430
+r
+z
+pick:0
+talk
+go:7550,420
+7550,276
+w60
diff --git a/games/undertale/tests/newhome.path b/games/undertale/tests/newhome.path
new file mode 100644
index 00000000..e52df92b
--- /dev/null
+++ b/games/undertale/tests/newhome.path
@@ -0,0 +1,70 @@
+# New Home, from the elevator up from the CORE (play it with --room 176 --at 90,4545 --name Frisk --noenc --god): the
+# street to Asgore's home; its two keys (the kitchen's, the hallway's), the presents (the Worn Dagger, the Heart
+# Locket), the chain; the basement and its story; the Last Corridor and Sans; the throne room and Asgore; the
+# barrier: Asgore fought (FIGHT), then spared; Flowey and his six SOULs; Flowey spared; Sans's call, the end
+w5
+go:340,4545
+go:340,4330
+go:1670,4330
+go:1670,3130
+1670,3100
+w20
+go:1300,2580
+u
+z
+talk
+go:2380,2822
+u
+z
+talk
+go:1990,2846
+1990,2812
+w20
+go:1915,2621
+u
+z
+pick:0
+talk
+go:2000,2607
+u
+z
+pick:0
+talk
+go:2040,2655
+2040,2700
+w20
+go:1660,2826
+u
+z
+talk
+w30
+go:2550,2320
+go:2550,2160
+go:4720,2160
+4720,2230
+w20
+go:5300,2350
+talk
+go:5830,2350
+5830,2200
+w20
+go:6020,1810
+6020,1780
+w20
+go:6010,1490
+talk
+go:5960,1120
+5960,1090
+w20
+go:5960,990
+go:6420,990
+6420,938
+talk
+pick:0
+talk
+fight
+pick:1
+talk
+pick:0
+talk
+w60
diff --git a/games/undertale/tests/play.c b/games/undertale/tests/play.c
new file mode 100644
index 00000000..29e10ea2
--- /dev/null
+++ b/games/undertale/tests/play.c
@@ -0,0 +1,488 @@
+/* Plays Undertale on a computer, with scripted keys, and saves frames.
+ *
+ * play [--frames N] [--keys "10-40:right,50:z,..."] [--shots 30,60|all] [--out DIR] [--saves DIR]
+ * [--room N --at X,Y] [--name NAME] [--flag F=V]... [--gold N] [--battle ID] [--trace]
+ *
+ * --path "x,y;x,y;z;w30;..." steers Frisk to each point in turn (holding arrows), presses Z ("z"), X ("x"),
+ * waits ("wN": N frames) or mashes Z through talks ("talk": until no text or event is left), when the scripted
+ * keys don't hold anything.
+ * Keys: left right up down z x c home. "a-b:key" holds the key from frame a to b, "a:key" for one frame.
+ * --room starts a new game in room N (its ROOM_* number) with Frisk's top-left at (X, Y), past the title.
+ * Screenshots are DIR/shot_.ppm. */
+#include
+#include
+#include
+#include
+#include
+#include "../src/battle.h"
+#include "../src/blk.h"
+#include "../src/game.h"
+#include "../src/gfx.h"
+#include "../src/menu.h"
+#include "../src/res.h"
+#include "../src/world.h"
+
+extern uint32_t host_keys, host_time;
+extern uint16_t host_fb[SCREEN_W * SCREEN_H];
+void host_shot(const char *path);
+void host_save_dir(const char *d);
+extern const uint8_t ut_data[];
+
+typedef struct { int a, b; uint32_t k; } Hold;
+static Hold holds[4096];
+static int nholds;
+
+#include "../src/event.h"
+#include "../src/text.h"
+static char path_buf[65536];
+extern bool g_no_encounters;
+static char *path_at;
+static int path_wait, path_stuck, path_last_x, path_last_y, path_room = -1;
+
+/* ---------------------------------------------------------------- "go:x,y": a way there (Frisk's top-left
+ * on a 4-pixel grid, over the floor and around the solid things), found once, then walked */
+#define GO_R 400 /* cells each way: 1600 pixels */
+#define GO_N (2 * GO_R + 1)
+static int16_t go_wp[512][2];
+static int go_n, go_i;
+static bool go_free(int x, int y) { return world_walkable(x + 3, y + 20, 14, 9, -1); }
+static bool go_plan(int tx, int ty) {
+ static int16_t prev[GO_N * GO_N];
+ static int queue[GO_N * GO_N];
+ int ox = pl.x, oy = pl.y;
+ for (int i = 0; i < GO_N * GO_N; i++) prev[i] = -2;
+ int s = GO_R * GO_N + GO_R, qh = 0, qt = 0, best = s, bestd = 1 << 30;
+ prev[s] = -1, queue[qt++] = s;
+ while (qh < qt) {
+ int c = queue[qh++], cx = c % GO_N - GO_R, cy = c / GO_N - GO_R;
+ int d = abs(ox + cx * 4 - tx) + abs(oy + cy * 4 - ty);
+ if (d < bestd) bestd = d, best = c;
+ if (d <= 3) break;
+ static const int dx[4] = {1, -1, 0, 0}, dy[4] = {0, 0, 1, -1};
+ for (int k = 0; k < 4; k++) {
+ int nx = cx + dx[k], ny = cy + dy[k];
+ if (nx < -GO_R || nx > GO_R || ny < -GO_R || ny > GO_R) continue;
+ int n = (ny + GO_R) * GO_N + nx + GO_R;
+ if (prev[n] != -2 || !go_free(ox + nx * 4, oy + ny * 4)) continue;
+ prev[n] = (int16_t)k, queue[qt++] = n;
+ }
+ }
+ /* back from the end, a waypoint where the way turns */
+ int16_t rev[GO_N * 2][2];
+ int nr = 0, c = best, last = -1;
+ while (prev[c] >= 0 && nr < GO_N * 2) {
+ int k = prev[c], cx = c % GO_N - GO_R, cy = c / GO_N - GO_R;
+ if (k != last) rev[nr][0] = (int16_t)(ox + cx * 4), rev[nr][1] = (int16_t)(oy + cy * 4), nr++, last = k;
+ static const int dx[4] = {1, -1, 0, 0}, dy[4] = {0, 0, 1, -1};
+ c = (cy - dy[k] + GO_R) * GO_N + cx - dx[k] + GO_R;
+ }
+ go_n = 0;
+ for (int i = nr - 1; i >= 0 && go_n < 512; i--) go_wp[go_n][0] = rev[i][0], go_wp[go_n][1] = rev[i][1], go_n++;
+ if (nr && go_n < 512) {
+ /* (the last turn's waypoint is where the turn starts: the end too) */
+ int ex = ox + (best % GO_N - GO_R) * 4, ey = oy + (best / GO_N - GO_R) * 4;
+ go_wp[go_n][0] = (int16_t)ex, go_wp[go_n][1] = (int16_t)ey, go_n++;
+ }
+ go_i = 0;
+ if (bestd > 3) fprintf(stderr, "go: no way to %d,%d, as near as %d px\n", tx, ty, bestd);
+ if (getenv("GO_DEBUG"))
+ for (int i = 0; i < go_n; i++) fprintf(stderr, "go %d: %d,%d\n", i, go_wp[i][0], go_wp[i][1]);
+ return go_n > 0;
+}
+
+/* the keys the path holds this frame (0: it's done or waiting) */
+static uint32_t path_keys(int f) {
+ if (!path_at || !*path_at) return 0;
+ /* a battle or a choice that came on the way to a point: the step after it is for it, if it says so */
+ {
+ char *e = strchr(path_at, ';');
+ if (e) {
+ const char *n = e + 1;
+ bool nb = !strncmp(n, "fight", 5) || !strncmp(n, "spare", 5) || !strncmp(n, "bt", 2) || !strncmp(n, "act:", 4) ||
+ !strncmp(n, "flee", 4) || !strncmp(n, "seq:", 4);
+ bool hb = !strncmp(path_at, "fight", 5) || !strncmp(path_at, "spare", 5) || !strncmp(path_at, "bt", 2) ||
+ !strncmp(path_at, "act:", 4) || !strncmp(path_at, "flee", 4) || !strncmp(path_at, "pick:", 5) ||
+ !strncmp(path_at, "seq:", 4);
+ if (!hb && ((g_mode == MODE_BATTLE && nb) || (g_mode == MODE_WORLD && text_menu_sel() >= 0 && !strncmp(n, "pick:", 5))))
+ path_at = e + 1;
+ }
+ }
+ /* a choice up while Frisk still walks: a "pick:" a few steps on (past "talk"s and points) is for it */
+ if (g_mode == MODE_WORLD && text_menu_sel() >= 0 && strncmp(path_at, "pick:", 5)) {
+ char *n = strchr(path_at, ';');
+ for (int k = 0; n && k < 3 && (!strncmp(n + 1, "talk", 4) || !strncmp(n + 1, "go:", 3) || isdigit((unsigned char)n[1])); k++)
+ n = strchr(n + 1, ';');
+ if (n && !strncmp(n + 1, "pick:", 5)) path_at = n + 1;
+ }
+ /* "pick:N": the option N of the next list or choice */
+ if (!strncmp(path_at, "pick:", 5) && g_mode == MODE_WORLD) {
+ int n = atoi(path_at + 5), sel = text_menu_sel();
+ if (f & 1) return 0;
+ if (sel < 0) return text_active() ? K_Z : 0;
+ if (sel >= 100) sel -= 100;
+ if (sel == n) {
+ char *e = strchr(path_at, ';');
+ path_at = e ? e + 1 : path_at + strlen(path_at);
+ return K_Z;
+ }
+ if (text_menu_sel() < 100 && text_menu_cols() == 2) { /* (two columns: the row, then the column) */
+ if (sel / 2 != n / 2) return sel / 2 < n / 2 ? K_DOWN : K_UP;
+ return sel < n ? K_RIGHT : K_LEFT;
+ }
+ return sel < n ? (text_menu_sel() >= 100 ? K_RIGHT : K_DOWN) : (text_menu_sel() >= 100 ? K_LEFT : K_UP);
+ }
+ /* a text in the way: read on (Z every other frame) */
+ if (g_mode == MODE_WORLD && text_active()) return (f & 1) ? K_Z : 0;
+ /* in a battle: "fight" FIGHTs, "spare" spares, "act:N" picks the ACT N (1: the first after Check) each turn,
+ * "bt" (a scripted battle) reads on; Z on whatever else comes (the bar, the texts). A battle the path didn't
+ * expect: Frisk flees it */
+ bool btok = !strncmp(path_at, "fight", 5) || !strncmp(path_at, "spare", 5) || !strncmp(path_at, "bt", 2) ||
+ !strncmp(path_at, "act:", 4) || !strncmp(path_at, "flee", 4) || !strncmp(path_at, "seq:", 4);
+ /* "seq:a,b@2,...": an ACT a turn (b@2: on the second monster), MERCY once one can be spared or they're done */
+ static char *seq_for;
+ static int seq_i;
+ int seq_act = 0, seq_target = 0;
+ bool seq = !strncmp(path_at, "seq:", 4);
+ if (seq) {
+ if (seq_for != path_at) seq_for = path_at, seq_i = 0;
+ const char *q = path_at + 4;
+ for (int k = 0; k < seq_i && q && *q && *q != ';'; k++) {
+ q = strchr(q, ',');
+ if (q) q++;
+ }
+ if (q && *q && *q != ';') {
+ seq_act = atoi(q);
+ const char *at = strchr(q, '@'), *comma = strchr(q, ','), *semi = strchr(q, ';');
+ if (at && (!comma || at < comma) && (!semi || at < semi)) seq_target = atoi(at + 1) - 1;
+ }
+ bool can = false;
+ for (int i = 0; i < bt.nmon; i++) can |= bt.mon[i].alive && !bt.mon[i].gone && bt.mon[i].spare;
+ if (can) seq_act = 0;
+ }
+ static int idle;
+ if (g_mode == MODE_BATTLE) {
+ idle = 0;
+ if (battle_over()) return 0;
+ if (f & 1) return 0;
+ if (bt.scripted) return K_Z; /* (Flowey's: read on) */
+ int want = !btok || !strncmp(path_at, "flee", 4) || !strncmp(path_at, "spare", 5) ? 3 : !strncmp(path_at, "act:", 4) ? 1 : 0;
+ if (seq) want = seq_act ? 1 : 3;
+ int mercy = (!btok || !strncmp(path_at, "flee", 4)) && !bt.no_flee ? 1 : 0;
+ switch (battle_ui()) {
+ case BUI_MENU: return bt.button < want ? K_RIGHT : bt.button > want ? K_LEFT : K_Z;
+ case BUI_TARGET:
+ if (seq && bt.button == 1) {
+ /* (the list holds those still there: "the second" is the last when one has gone) */
+ int here = 0;
+ for (int i = 0; i < bt.nmon; i++) here += bt.mon[i].alive && !bt.mon[i].gone;
+ int want_t = seq_target < here ? seq_target : here - 1;
+ return bt.target < want_t ? K_DOWN : bt.target > want_t ? K_UP : K_Z;
+ }
+ return K_Z;
+ case BUI_ACTS: {
+ int n = seq ? seq_act : atoi(path_at + 4);
+ if (seq && bt.act == n) seq_i++;
+ return bt.act + 2 <= n ? K_DOWN : bt.act < n ? K_RIGHT : bt.act - 2 >= n ? K_UP : bt.act > n ? K_LEFT : K_Z;
+ }
+ case BUI_MERCY: return bt.mercy < mercy ? K_DOWN : bt.mercy > mercy ? K_UP : K_Z;
+ case BUI_BAR: return abs(bt.bar_x / FX - 140) < 8 ? K_Z : 0; /* (the middle: the best hit) */
+ default: return K_Z;
+ }
+ }
+ if (btok) {
+ /* (it waits for the battle: done when the world has been quiet a while) */
+ if (g_mode == MODE_WORLD && !event_blocking() && !text_active() && ++idle > 30) {
+ char *e = strchr(path_at, ';');
+ path_at = e ? e + 1 : path_at + strlen(path_at);
+ idle = 0;
+ }
+ return (f & 1) ? K_Z : 0;
+ }
+ if (path_wait > 0) {
+ path_wait--;
+ return 0;
+ }
+ char *end = strchr(path_at, ';');
+ char tok[64];
+ int n = end ? (int)(end - path_at) : (int)strlen(path_at);
+ snprintf(tok, sizeof tok, "%.*s", n, path_at);
+ char *next = end ? end + 1 : path_at + n;
+ if (!strcmp(tok, "z") || !strcmp(tok, "x") || !strcmp(tok, "c")) {
+ path_at = next;
+ path_wait = 1;
+ return tok[0] == 'z' ? K_Z : tok[0] == 'x' ? K_X : K_C;
+ }
+ /* u d l r: a key's press (to face something) */
+ if (!tok[1] && strchr("udlr", tok[0])) {
+ path_at = next;
+ path_wait = 1;
+ return tok[0] == 'u' ? K_UP : tok[0] == 'd' ? K_DOWN : tok[0] == 'l' ? K_LEFT : K_RIGHT;
+ }
+ if (tok[0] == 'w') {
+ path_wait = atoi(tok + 1);
+ path_at = next;
+ return 0;
+ }
+ if (!strcmp(tok, "talk")) {
+ if (g_mode == MODE_END) { /* (the end: nothing left to read on) */
+ path_at = next;
+ return 0;
+ }
+ if (!text_active() && !event_blocking() && g_mode == MODE_WORLD && !world_fading()) {
+ path_at = next;
+ return 0;
+ }
+ return (f & 1) ? K_Z : 0;
+ }
+ /* "vent:x,y": toward (x, y), straight, until something starts on the way (a steam vent blows Frisk off) */
+ if (!strncmp(tok, "vent:", 5)) {
+ if (event_running()) {
+ path_at = next;
+ return 0;
+ }
+ if (g_mode != MODE_WORLD || text_active() || world_fading()) return 0;
+ int vx, vy;
+ sscanf(tok + 5, "%d,%d", &vx, &vy);
+ int dx = vx - pl.x, dy = vy - pl.y;
+ if (abs(dx) <= 1 && abs(dy) <= 1) {
+ path_at = next;
+ return 0;
+ }
+ if (abs(dx) > 1) return dx > 0 ? K_RIGHT : K_LEFT;
+ return dy > 0 ? K_DOWN : K_UP;
+ }
+ if (!strncmp(tok, "go:", 3)) {
+ static char *planned;
+ if (g_mode != MODE_WORLD || text_active() || event_blocking() || world_fading()) return 0;
+ int gx, gy;
+ sscanf(tok + 3, "%d,%d", &gx, &gy);
+ static int planned_room;
+ if (planned == path_at && planned_room != g_room) {
+ /* a door to another area on the way: the point was there, so it is reached */
+ RoomInfo a, b;
+ room_info(planned_room, &a), room_info(g_room, &b);
+ if (a.area != b.area) {
+ path_at = next, planned = NULL;
+ return 0;
+ }
+ }
+ if (planned != path_at || planned_room != g_room) {
+ planned = path_at, planned_room = g_room;
+ if (!go_plan(gx, gy)) {
+ path_at = next, planned = NULL;
+ return 0;
+ }
+ }
+ while (go_i < go_n && abs(go_wp[go_i][0] - pl.x) <= 1 && abs(go_wp[go_i][1] - pl.y) <= 1) go_i++;
+ /* (a waypoint on the axis it's walking: it may step over it by a pixel or two) */
+ if (go_i >= go_n) {
+ path_at = next, planned = NULL;
+ return 0;
+ }
+ if (pl.x == path_last_x && pl.y == path_last_y) {
+ if (++path_stuck > 30) {
+ planned = NULL, path_stuck = 0; /* (something moved in the way: plan again) */
+ return 0;
+ }
+ } else
+ path_stuck = 0;
+ path_last_x = pl.x, path_last_y = pl.y;
+ /* one axis at a time, as the way was found */
+ int ddx = go_wp[go_i][0] - pl.x, ddy = go_wp[go_i][1] - pl.y;
+ if (abs(ddx) > 1) return ddx > 0 ? K_RIGHT : K_LEFT;
+ return ddy > 0 ? K_DOWN : K_UP;
+ }
+ int x, y;
+ if (sscanf(tok, "%d,%d", &x, &y) != 2) {
+ path_at = next;
+ return 0;
+ }
+ if (g_mode != MODE_WORLD || text_active() || event_blocking() || world_fading()) return 0;
+ /* a door took him to another room on the way to this point: it is reached */
+ static char *room_step;
+ if (room_step != path_at) room_step = path_at, path_room = g_room;
+ if (path_room >= 0 && g_room != path_room) {
+ path_room = g_room;
+ path_at = next;
+ return 0;
+ }
+ path_room = g_room;
+ /* or a door inside the room did (he's far from where he just was) */
+ if (abs(pl.x - path_last_x) > 40 || abs(pl.y - path_last_y) > 40) {
+ path_last_x = pl.x, path_last_y = pl.y;
+ path_at = next;
+ return 0;
+ }
+ int dx = x - pl.x, dy = y - pl.y;
+ if (abs(dx) <= 1 && abs(dy) <= 1) {
+ path_at = next;
+ path_stuck = 0;
+ return 0;
+ }
+ if (pl.x == path_last_x && pl.y == path_last_y) {
+ if (++path_stuck > 60) {
+ fprintf(stderr, "path: stuck at %d,%d on the way to %d,%d (room %d)\n", pl.x, pl.y, x, y, g_room);
+ path_at = next, path_stuck = 0;
+ }
+ } else
+ path_stuck = 0;
+ path_last_x = pl.x, path_last_y = pl.y;
+ uint32_t k = 0;
+ if (dx > 1) k |= K_RIGHT;
+ if (dx < -1) k |= K_LEFT;
+ if (dy > 1) k |= K_DOWN;
+ if (dy < -1) k |= K_UP;
+ return k;
+}
+
+static uint32_t key_bit(const char *s) {
+ static const struct { const char *n; uint32_t k; } keys[] = {
+ {"left", K_LEFT}, {"right", K_RIGHT}, {"up", K_UP}, {"down", K_DOWN}, {"z", K_Z}, {"x", K_X}, {"c", K_C},
+ {"home", K_HOME}};
+ for (size_t i = 0; i < sizeof keys / sizeof keys[0]; i++)
+ if (!strcmp(s, keys[i].n)) return keys[i].k;
+ fprintf(stderr, "unknown key %s\n", s);
+ exit(2);
+}
+
+static void parse_keys(const char *spec) {
+ char buf[65536];
+ snprintf(buf, sizeof buf, "%s", spec);
+ for (char *t = strtok(buf, ","); t; t = strtok(NULL, ",")) {
+ int a, b;
+ char k[32];
+ if (sscanf(t, "%d-%d:%31s", &a, &b, k) == 3) holds[nholds++] = (Hold){a, b, key_bit(k)};
+ else if (sscanf(t, "%d:%31s", &a, k) == 2) holds[nholds++] = (Hold){a, a, key_bit(k)};
+ else fprintf(stderr, "bad key %s\n", t), exit(2);
+ }
+}
+
+int main(int argc, char **argv) {
+ const char *out = "build/play", *saves = NULL, *shots = "";
+ int frames = 120, room = -1, ax = 0, ay = 0, battle = -1;
+ const char *name = NULL;
+ int nflags = 0, fl[64][2], gold = -1;
+ bool trace = false, coll = false, god = false;
+ for (int i = 1; i < argc; i++) {
+ if (!strcmp(argv[i], "--frames") && i + 1 < argc) frames = atoi(argv[++i]);
+ else if (!strcmp(argv[i], "--keys") && i + 1 < argc) parse_keys(argv[++i]);
+ else if (!strcmp(argv[i], "--path") && i + 1 < argc) snprintf(path_buf, sizeof path_buf, "%s", argv[++i]), path_at = path_buf;
+ else if (!strcmp(argv[i], "--path-file") && i + 1 < argc) {
+ /* a path one step a line; # starts a comment */
+ FILE *pf = fopen(argv[++i], "r");
+ if (!pf) fprintf(stderr, "no %s\n", argv[i]), exit(2);
+ char line[256];
+ size_t n = 0;
+ while (fgets(line, sizeof line, pf)) {
+ char *h = strchr(line, '#');
+ if (h) *h = 0;
+ char *a = line, *e = line + strlen(line);
+ while (*a == ' ' || *a == '\t') a++;
+ while (e > a && (e[-1] == '\n' || e[-1] == '\r' || e[-1] == ' ' || e[-1] == '\t')) *--e = 0;
+ if (!*a) continue;
+ n += (size_t)snprintf(path_buf + n, sizeof path_buf - n, "%s%s", n ? ";" : "", a);
+ }
+ fclose(pf);
+ path_at = path_buf;
+ }
+ else if (!strcmp(argv[i], "--shots") && i + 1 < argc) shots = argv[++i];
+ else if (!strcmp(argv[i], "--out") && i + 1 < argc) out = argv[++i];
+ else if (!strcmp(argv[i], "--saves") && i + 1 < argc) saves = argv[++i];
+ else if (!strcmp(argv[i], "--room") && i + 1 < argc) room = atoi(argv[++i]);
+ else if (!strcmp(argv[i], "--at") && i + 1 < argc) sscanf(argv[++i], "%d,%d", &ax, &ay);
+ else if (!strcmp(argv[i], "--name") && i + 1 < argc) name = argv[++i];
+ else if (!strcmp(argv[i], "--battle") && i + 1 < argc) battle = atoi(argv[++i]);
+ else if (!strcmp(argv[i], "--flag") && i + 1 < argc && nflags < 64) {
+ sscanf(argv[++i], "%d=%d", &fl[nflags][0], &fl[nflags][1]);
+ nflags++;
+ } else if (!strcmp(argv[i], "--trace")) trace = true;
+ else if (!strcmp(argv[i], "--noenc")) g_no_encounters = true;
+ else if (!strcmp(argv[i], "--god")) god = true;
+ else if (!strcmp(argv[i], "--gold") && i + 1 < argc) gold = atoi(argv[++i]);
+ else if (!strcmp(argv[i], "--coll")) coll = true;
+ else fprintf(stderr, "unknown option %s\n", argv[i]), exit(2);
+ }
+ mkdir(out, 0755);
+ char sd[512];
+ snprintf(sd, sizeof sd, "%s/saves", out);
+ if (!saves) saves = sd;
+ mkdir(saves, 0755);
+ host_save_dir(saves);
+ ut_bin = ut_data;
+ game_init();
+ if (room >= 0 || name || battle >= 0) {
+ game_new(name ? name : "Frisk");
+ for (int i = 0; i < nflags; i++) set_flag(fl[i][0], fl[i][1]); /* (before the room is set up) */
+ if (room < 0 && battle >= 0) room = 0; /* (a battle comes back to a room) */
+ if (room >= 0) {
+ if (!ax && !ay) {
+ RoomInfo ri;
+ room_info(room, &ri);
+ ax = ri.x + ri.w / 2 - 10, ay = ri.y + ri.h / 2 - 15;
+ }
+ world_enter(room, ax, ay, DIR_DOWN);
+ }
+ }
+ for (int i = 0; i < nflags; i++) set_flag(fl[i][0], fl[i][1]);
+ if (gold >= 0) sv.gold = (uint16_t)gold;
+ if (battle >= 0) battle_start(battle);
+ bool all = !strcmp(shots, "all");
+ for (int f = 0; f < frames && g_running; f++) {
+ uint32_t k = 0;
+ for (int i = 0; i < nholds; i++)
+ if (f >= holds[i].a && f <= holds[i].b) k |= holds[i].k;
+ if (!k) k = path_keys(f);
+ host_keys = k;
+ if (god && g_mode == MODE_BATTLE && sv.hp < sv.maxhp) sv.hp = sv.maxhp; /* (--god: the SOUL can't be hurt) */
+ host_time += 33;
+ game_frame(k);
+ game_draw();
+ if (trace) printf("%d mode %d room %d frisk %d,%d cam %d,%d hp %d ui %d btn %d keys %x path %.12s\n", f, g_mode, g_room, pl.x, pl.y, cam_x, cam_y, sv.hp, battle_ui(), bt.button, k, path_at ? path_at : "");
+ char want[16];
+ snprintf(want, sizeof want, "%d", f);
+ bool shoot = all;
+ if (!shoot) {
+ const char *p = shots;
+ size_t n = strlen(want);
+ while ((p = strstr(p, want))) {
+ if ((p == shots || p[-1] == ',') && (p[n] == ',' || !p[n])) {
+ shoot = true;
+ break;
+ }
+ p += n;
+ }
+ }
+ if (shoot && coll && world_room_info()) {
+ /* the walkable cells tinted green, the others red */
+ const RoomInfo *r = world_room_info();
+ for (int y = 0; y < SCREEN_H; y++)
+ for (int x = 0; x < SCREEN_W; x++) {
+ int wx = x + cam_x, wy = y + cam_y, cx = (wx - r->x) >> 2, cy = (wy - r->y) >> 2;
+ if (wx < r->x || wy < r->y || cx >= r->cw || cy >= r->ch) continue;
+ bool w = r->coll[cy * ((r->cw + 7) >> 3) + (cx >> 3)] >> (cx & 7) & 1;
+ uint16_t c = host_fb[y * SCREEN_W + x];
+ host_fb[y * SCREEN_W + x] = w ? (uint16_t)((c >> 1 & 0x7BEF) | 0x0400) : (uint16_t)((c >> 1 & 0x7BEF) | 0x8000);
+ }
+ }
+ if (shoot) {
+ char path[600];
+ snprintf(path, sizeof path, "%s/shot_%d.ppm", out, f);
+ host_shot(path);
+ }
+ }
+ if (gfx_dropped) printf("dropped %d draw commands\n", gfx_dropped);
+ if (getenv("BLK_STATS")) fprintf(stderr, "blocks unpacked %u, at most %u in a frame\n", blk_unpacked, blk_most_in_frame);
+ if (getenv("OBJS")) /* (the room's objects at the end: kind, name, where, its box) */
+ for (int i = 0; i < MAX_OBJS; i++)
+ if (objs[i].kind != OBJ_NONE)
+ fprintf(stderr, "obj %d kind %d name %d at %d,%d box %d,%d %dx%d spr %d\n", i, objs[i].kind, objs[i].name, objs[i].x,
+ objs[i].y, objs[i].bx, objs[i].by, objs[i].bw, objs[i].bh, objs[i].spr[0]);
+ printf("END mode %d room %d at %d,%d hp %d lv %d gold %d kills %d path %s\nFLAGS", g_mode, g_room, pl.x, pl.y, sv.hp,
+ sv.lv, sv.gold, sv.kills, path_at && *path_at ? "left" : "done");
+ for (int i = 0; i < FLAG_COUNT; i++)
+ if (sv.flags[i]) printf(" %d=%d", i, sv.flags[i]);
+ printf("\nITEMS");
+ for (int i = 0; i < NUM_ITEMS; i++) printf(" %d", sv.items[i]);
+ printf("\n");
+ return 0;
+}
diff --git a/games/undertale/tests/resort.path b/games/undertale/tests/resort.path
new file mode 100644
index 00000000..c9ddbf29
--- /dev/null
+++ b/games/undertale/tests/resort.path
@@ -0,0 +1,62 @@
+# the MTT Resort, up the crossroads' stairs (play it with --room 159 --at 7550,420 --name Frisk --noenc --god
+# --gold 400): Sans out front, dinner with him; the lobby, a Starfait from Burgerpants; a room at the desk, door 2
+# and its bed; doors 1 and 4 (nothing to give); through the door north, into the CORE
+w5
+7550,410
+7550,276
+w30
+go:1152,829
+l
+z
+talk
+pick:0
+talk
+w30
+go:1030,400
+1075,400
+w10
+go:1290,262
+u
+z
+w40
+z
+w5
+z
+w5
+z
+w30
+x
+w5
+u
+z
+w20
+go:1140,262
+u
+z
+pick:0
+talk
+go:1450,396
+1490,396
+w10
+go:1735,362
+u
+z
+talk
+w30
+go:1730,200
+1730,214
+w10
+go:1660,362
+u
+z
+talk
+go:1885,362
+u
+z
+talk
+go:1500,396
+1470,396
+w10
+go:1210,122
+1210,100
+w60
diff --git a/games/undertale/tests/ruins.path b/games/undertale/tests/ruins.path
new file mode 100644
index 00000000..e23a6e78
--- /dev/null
+++ b/games/undertale/tests/ruins.path
@@ -0,0 +1,241 @@
+# The Ruins from the bed of golden flowers to the door out, the friendly way (tests/check.py plays it with
+# --name Frisk --noenc --god: no random encounters, the SOUL not hurt). One step a line: x,y walk there;
+# go:x,y find the way there; u d l r z x c a key; talk read on; pick:N an option; wN wait;
+# fight / spare / flee / act:N / bt how the next battle goes.
+
+# the flowers, then Flowey
+go:608,6036
+608,6020
+go:510,5800
+bt
+w40
+talk
+go:510,5590
+510,5560
+# the entrance: Toriel by the stairs
+go:600,5300
+talk
+go:520,5190
+520,5160
+w20
+# the middle road: Toriel's puzzle
+go:500,4960
+talk
+w30
+go:494,4900
+494,4840
+w20
+# the switches
+talk
+go:748,4666
+u
+z
+talk
+go:948,4666
+u
+z
+talk
+w40
+talk
+go:1100,4732
+# the Dummy: talk to it
+talk
+go:1278,4700
+r
+z
+act:1
+w30
+talk
+w40
+go:1252,4686
+1252,4650
+w20
+# the blueprint: Toriel goes ahead, a Froggit, then hand in hand over the spikes
+go:1520,4480
+w200
+talk
+go:1780,4480
+w20
+talk
+w60
+talk
+w40
+go:2320,4470
+# the long hall: alone, then the pillar
+talk
+w60
+go:4470,4470
+w20
+talk
+w60
+talk
+go:4790,4470
+# the leaf pile: Toriel calls; the Froggit's advice; the candy room above
+talk
+go:4901,4448
+u
+z
+talk
+go:4960,4446
+4960,4420
+w20
+go:4960,4226
+u
+z
+talk
+z
+talk
+go:4960,4352
+4960,4380
+w20
+# the cracked floor: down through it, out by the right chute
+go:5040,4600
+go:5300,4692
+w60
+go:5352,5090
+5352,5060
+w30
+# the first rock: Toriel calls about pie
+talk
+go:5590,4728
+5700,4728
+w20
+# the leaf puzzle: the second call, then the way through the cracks
+talk
+go:6250,4700
+6250,4868
+6310,4868
+6310,4790
+6370,4790
+6370,4868
+6430,4868
+6450,4868
+6450,4810
+6520,4810
+# the second rock: the third call, then ask the rock (four times), walking into it
+talk
+go:6704,4870
+r
+r
+talk
+go:6724,4870
+r
+r
+talk
+go:6724,4850
+r
+r
+talk
+go:6860,4840
+w30
+talk
+go:6724,4870
+r
+r
+talk
+go:7000,4840
+# the mouse hole: SAVE
+go:7172,4882
+u
+z
+w20
+z
+w30
+x
+w20
+# Napstablook: move it with force, then cheer them up
+go:7490,4830
+pick:0
+act:3
+w40
+talk
+# north to the Froggits' hall
+go:7597,4790
+7597,4730
+w30
+go:7900,4542
+u
+z
+pick:0
+talk
+go:8000,4590
+talk
+w30
+# the one switch: down the middle crack (between the others), the switch, back up the chute
+go:8100,4640
+8460,4640
+8460,4570
+w60
+u
+z
+talk
+go:8505,4960
+8505,4930
+w30
+# the rotating rooms: blue behind the pillar, red, green behind the pillar
+go:8970,4880
+8970,4900
+w30
+go:8962,4190
+l
+z
+talk
+go:8640,4340
+go:8400,4206
+u
+z
+talk
+go:8455,4130
+8455,4100
+w30
+go:8466,3765
+l
+z
+talk
+go:8720,3740
+# the crossroads, up to Toriel's tree; then her house
+go:9006,3546
+9006,3510
+w30
+go:9006,3380
+talk
+w30
+go:9012,3080
+9012,3060
+w30
+talk
+w60
+talk
+w30
+# the living room: ask her how to leave
+go:9190,2840
+go:8840,2860
+go:8664,2764
+u
+z
+pick:1
+talk
+w100
+# the basement: her three talks, then the door
+go:9090,2760
+go:9070,2736
+w30
+go:9990,2316
+talk
+go:10010,2150
+talk
+go:10010,2080
+spare
+w30
+talk
+w100
+go:10010,2040
+10010,2010
+w30
+# past the door: Flowey, then the way out
+go:10010,300
+go:10010,236
+talk
+w60
+go:10010,50
+w60
diff --git a/games/undertale/tests/snowdin.path b/games/undertale/tests/snowdin.path
new file mode 100644
index 00000000..540c2c36
--- /dev/null
+++ b/games/undertale/tests/snowdin.path
@@ -0,0 +1,242 @@
+# Snowdin from the Ruins door (play it with --room 46 --at 34,392 --name Frisk --noenc --god), the friendly
+# way: one step a line, as in ruins.path; seq:a,b@2 picks an ACT a turn (b on the second monster) then MERCY.
+# The forest road: Sans at the gate, Papyrus at the lamp
+go:1060,404
+1100,404
+go:1760,404
+1800,404
+go:2200,404
+2240,404
+go:2530,404
+2580,404
+talk
+# Box Road, the sentry station
+go:3200,400
+go:3530,400
+3560,400
+talk
+# Doggo: pet it after its attack
+go:4110,404
+go:4290,404
+4330,404
+seq:1,1
+go:4570,404
+go:4660,404
+# the invisible electricity maze, along Papyrus's footprints
+go:5180,400
+5210,400
+talk
+5280,360
+talk
+5260,356
+5330,356
+5330,316
+5390,316
+5390,416
+5445,416
+talk
+go:5480,420
+# the ball game room (over the plank bridge), the word search, Junior Jumble
+go:5490,370
+5530,350
+go:5760,330
+go:6040,420
+go:6080,600
+go:6280,650
+6320,650
+talk
+go:6450,650
+pick:0
+talk
+# the spaghetti, then the snow puzzle's switch and Dogamy and Dogaressa
+go:6600,640
+6640,640
+go:6880,660
+6930,660
+go:7316,690
+u
+z
+talk
+go:7100,960
+go:7180,960
+go:7300,960
+seq:3,2,1@1,1@2
+# the first X/O puzzle, Papyrus and the spaghetti question; the second, with the switch in the tree
+go:7370,1100
+7370,1140
+go:7450,1222
+go:7530,1222
+go:7500,1196
+u
+z
+talk
+go:7590,1222
+7620,1222
+talk
+pick:0
+talk
+go:7690,1222
+7720,1222
+talk
+go:7960,1186
+u
+z
+talk
+go:8094,1232
+u
+z
+talk
+# the multicolor tile puzzle
+go:8480,1222
+8520,1222
+go:8580,1222
+pick:0
+talk
+go:8840,1236
+# the dog house; the ice X/O puzzle: slides (a key, then the wait while Frisk slides), the switch, the bridge
+go:9000,1240
+go:9300,1240
+go:9446,1230
+u
+u
+u
+u
+u
+r
+r
+r
+r
+r
+r
+r
+r
+r
+r
+r
+r
+r
+r
+r
+r
+r
+r
+r
+r
+r
+r
+r
+w18
+u
+r
+w11
+l
+u
+w20
+l
+w17
+d
+d
+d
+d
+r
+l
+w3
+r
+r
+w56
+d
+w8
+l
+w66
+u
+r
+r
+w8
+r
+r
+d
+d
+w30
+r
+r
+w41
+u
+u
+w36
+d
+w36
+u
+l
+w50
+r
+r
+w66
+u
+w17
+l
+w36
+d
+d
+l
+w3
+d
+d
+l
+w11
+r
+r
+w46
+u
+w5
+z
+talk
+l
+w36
+u
+u
+u
+u
+u
+u
+u
+u
+u
+l
+w3
+u
+u
+l
+w31
+u
+r
+r
+w67
+r
+r
+r
+r
+r
+r
+r
+9845,1196
+# the snow poffs, Greater Dog (beckon, pet, play, pet, pet), the Gauntlet bridge
+go:10500,1240
+go:10830,1240
+seq:2,1,3,1,1
+talk
+go:10950,1244
+go:11100,1236
+go:11300,1236
+talk
+go:11990,1236
+# Snowdin Town, the fog, Papyrus spared, the way on to Waterfall
+go:600,1090
+go:1500,1090
+go:2500,1090
+go:3240,1090
+go:3300,1090
+spare
+talk
+go:3560,1090
+3640,1090
+talk
diff --git a/games/undertale/tests/waterfall.path b/games/undertale/tests/waterfall.path
new file mode 100644
index 00000000..a601d5cf
--- /dev/null
+++ b/games/undertale/tests/waterfall.path
@@ -0,0 +1,358 @@
+# Waterfall from Snowdin's fog to Hotland's edge (play it with --room 85 --at 30,1480 --name Frisk --noenc --god), the
+# friendly way, as in snowdin.path. Monster Kid, Sans's booth (Grillby's), the echo flowers
+go:600,1480
+talk
+go:880,1458
+u
+z
+talk
+w30
+talk
+# the falls: the tutu up north, the stream's rocks, Papyrus's report to Undyne from the tall grass
+go:1100,1480
+go:1250,1460
+go:1250,1300
+go:1240,1256
+u
+z
+talk
+go:1250,1400
+go:1250,1470
+go:1370,1505
+go:1600,1505
+go:1780,1500
+talk
+w60
+talk
+# the first bridge seeds: four pushed down to the path and on into the water
+go:2240,1485
+go:2280,1425
+2305,1425
+go:2320,1405
+2320,1475
+go:2280,1425
+2280,1475
+go:2280,1465
+2305,1465
+go:2300,1485
+2365,1485
+go:2260,1485
+2385,1485
+go:2300,1425
+2300,1475
+go:2280,1485
+2405,1485
+go:2320,1445
+2320,1475
+go:2300,1485
+2425,1485
+go:2500,1485
+# the second: first the secret bridge to the quiche (all four seeds), then the bell, then three across the channel
+go:2900,1490
+go:3000,1345
+3000,1455
+go:2980,1465
+3025,1465
+go:3040,1445
+3040,1520
+go:3000,1305
+3000,1455
+go:2980,1465
+3045,1465
+go:3060,1445
+3060,1520
+go:2940,1365
+3045,1365
+go:3060,1345
+3060,1517
+go:3040,1515
+3040,1525
+3062,1525
+go:2940,1325
+3045,1325
+go:3060,1305
+3060,1517
+go:3040,1515
+3040,1525
+3082,1525
+3140,1525
+go:3200,1525
+go:3270,1500
+u
+z
+talk
+3190,1500
+3190,1525
+3100,1525
+go:3040,1525
+go:3020,1210
+u
+z
+talk
+go:2960,1345
+2960,1258
+go:2980,1245
+2870,1245
+go:3000,1345
+3000,1258
+go:3020,1245
+2850,1245
+go:2960,1385
+2960,1258
+go:2980,1245
+2830,1245
+go:2780,1245
+go:2729,1240
+2729,1200
+w30
+# the wishing room, the history boardwalk, Undyne's spears to the tall grass
+go:2800,1060
+go:3120,830
+go:3125,700
+go:3460,600
+go:4010,560
+go:4600,560
+go:5200,560
+go:5760,560
+go:6400,560
+go:6700,560
+talk
+w30
+talk
+# the crystals' SAVE, Sans's telescope, the Nice Cream cart and the punch card upstairs
+go:7248,562
+u
+z
+talk
+go:7880,540
+u
+z
+talk
+go:7975,560
+go:7900,130
+u
+z
+talk
+go:8050,130
+u
+z
+talk
+# south past Loren, down the ladder into the marsh: the ballet shoes in the tall grass, an echo flower, the history
+# sign, then up to Onionsan's path; the piano's song (the artifact), Shyren (hummed along with)
+go:8140,560
+go:8140,700
+go:7590,1250
+u
+z
+talk
+go:8445,1132
+u
+z
+talk
+go:8850,840
+u
+z
+talk
+go:8770,760
+go:8800,664
+go:8900,664
+talk
+go:9300,664
+go:9600,664
+talk
+go:9850,664
+go:10070,640
+go:10066,412
+u
+z
+pick:0
+pick:4
+pick:0
+pick:3
+pick:4
+pick:1
+pick:1
+pick:3
+talk
+go:10150,330
+go:10150,240
+go:10150,162
+u
+z
+talk
+go:10137,612
+d
+z
+talk
+seq:2,2,2,2,2
+talk
+# the umbrella for the statue, another for Monster Kid, the rain down to the castle's view
+go:10400,640
+go:11150,640
+u
+z
+talk
+go:10760,640
+u
+z
+pick:0
+talk
+go:11150,640
+u
+z
+talk
+go:11400,662
+go:11900,662
+talk
+go:12500,662
+go:12760,900
+talk
+go:12760,1500
+go:12900,1700
+talk
+go:13700,1700
+talk
+go:14300,1700
+go:14720,1700
+talk
+# the last SAVE before the bridge, Undyne's spears from below, the bridge cut
+go:15138,1665
+u
+z
+talk
+go:15400,1700
+go:16000,1700
+go:16800,1700
+go:17370,1669
+17400,1669
+talk
+talk
+# the garbage dump: waking on the golden flowers, the Trash Zone's SAVE, the walkway's cooler, Mad Dummy (until
+# Napstablook comes)
+go:18300,3240
+go:18540,3256
+u
+z
+talk
+go:18880,3200
+go:18930,2818
+u
+z
+pick:0
+talk
+u
+z
+pick:0
+talk
+go:18905,2705
+r
+z
+pick:0
+talk
+act:1
+talk
+# the Quiet Area, Napstablook's house (lying on the floor), the snail farm (no race), Gerson's shop (just looking)
+go:18900,2440
+go:18877,2116
+18877,2080
+w30
+go:18972,1828
+u
+z
+pick:0
+talk
+go:18840,1940
+18840,1980
+w30
+go:19318,2134
+u
+z
+pick:1
+talk
+go:19366,2290
+u
+z
+w20
+d
+w5
+d
+w5
+d
+w5
+z
+w30
+# the twin waterfalls, the mushroom maze, Temmie Village down its hidden path (a Temmie, the Tem Shop), and out at
+# the maze's far side
+go:20540,2310
+go:20700,2330
+go:21070,2860
+go:21070,2950
+go:21170,3084
+u
+z
+talk
+go:21350,3070
+u
+z
+w20
+d
+w5
+d
+w5
+d
+w5
+z
+w20
+go:21070,2950
+go:21070,2840
+go:21130,2380
+# the lantern maze, the dead end (its echo flower: Undyne, then Monster Kid in her way), the echo flowers' path north
+21175,2380
+go:21780,2665
+21830,2665
+go:22390,2660
+r
+z
+talk
+w30
+go:22190,2580
+22190,2530
+go:22190,2110
+22190,2030
+# the chasm: Monster Kid on the bridge, then hanging off it, helped up; Undyne on her crag, her fight (fled), her
+# chase by the river and the neon sign (Papyrus calls); Hotland: she falls in the heat, the cooler's water for her,
+# and on to the crossroads before the Lab (hotland.path goes on from there)
+go:22390,2030
+talk
+go:22626,2022
+d
+z
+talk
+go:23300,2030
+talk
+flee
+talk
+go:23449,1960
+23449,1930
+w20
+go:23449,920
+23449,890
+w20
+go:23690,780
+talk
+go:24200,780
+24215,780
+talk
+w20
+go:722,6188
+u
+z
+pick:0
+talk
+go:590,6206
+l
+z
+pick:0
+talk
+w20
+go:1000,6230
diff --git a/games/undertale/tools/fetch.py b/games/undertale/tools/fetch.py
new file mode 100644
index 00000000..142a570e
--- /dev/null
+++ b/games/undertale/tools/fetch.py
@@ -0,0 +1,176 @@
+#!/usr/bin/env python3
+"""Gathers the public material the pack is made from, into REF (not committed):
+
+ python3 -I tools/fetch.py REF
+
+- REF/wiki/maps/*.png: the Undertale Wiki's maps of each area (undertale.wiki, Category:Maps): every room,
+ one pixel for one, laid out as in the game
+- REF/wiki/sprites/*: the wiki's sprite files (Category:Overworld sprites, Battle sprites, Face sprites,
+ Sprites, Item files), as their own names
+- REF/wiki/pages/*.txt: the wiki's pages (their wikitext) that the game's texts are checked against
+- REF/demo/: the free demo from undertale.com (its Mac build keeps its sprites and backgrounds as PNG files)
+- REF/fonts/: Pixel Operator (CC0), by Jayvee Enaguas, once called 8-bit Operator: the font the game credits
+
+Files already there are kept: run it again to fetch only what is missing."""
+import json
+import os
+import sys
+import time
+import urllib.parse
+import urllib.request
+import zipfile
+
+API = "https://undertale.wiki/api.php"
+UA = {"User-Agent": "NumPlay-Undertale/1.0 (fan port for the NumWorks calculator; one-time asset fetch)"}
+CATEGORIES = {
+ "maps": ["Maps"],
+ "sprites": ["Overworld_sprites", "Battle_sprites", "Face_sprites", "Sprites", "Item_files", "Location_files"],
+ "screens": ["Screenshots", "Cut_content_files"],
+}
+PAGES = ["Ruins", "Ruins/Puzzles", "Toriel", "Toriel/In_battle", "Toriel's_Home", "Flowey", "Napstablook",
+ "Cell_Phone/Toriel", "SAVE_Point", "Froggit", "Whimsun", "Moldsmal", "Loox", "Vegetoid", "Migosp",
+ "Dummy", "Froggit_NPC", "Spider_Bake_Sale", "Items", "Monster_Candy", "Spider_Donut", "Spider_Cider",
+ "Butterscotch_Pie", "Snail_Pie", "Toy_Knife", "Faded_Ribbon", "Bandage", "Stick", "LOVE", "EXP",
+ "HP", "AT", "DF", "Stats", "FIGHT", "ACT", "MERCY", "ITEM", "Flowey/In_battle", "Napstablook/In_battle",
+ "Encounter", "Home", "Annoying_Dog", "Rock", "Mouse", "Golden_flower", "Chara", "Frisk", "Name",
+ "Item", "Consumables", "Weapons", "Armor", "Cell_Phone", "Intro", "Game_Over", "Damage", "Monster_Candy",
+ "Toriel's_Home", "Blue_SOUL", "Bullet_board", "Spare", "Flee", "Gold", "Save",
+ # Snowdin
+ "Snowdin", "Snowdin/Puzzles", "Snowdin_Town", "Sans", "Papyrus", "Papyrus/In_battle", "Sans/In_battle",
+ "Snowdrake", "Ice_Cap", "Gyftrot", "Doggo", "Lesser_Dog", "Greater_Dog", "Dogamy_and_Dogaressa", "Jerry",
+ "Chilldrake", "Glyde", "Snowman", "Monster_Kid", "Multicolor_Tile_Puzzle", "Nice_Cream_guy", "Nice_Cream",
+ "Dimensional_Box", "Ball_Game", "Grillby's", "Grillby", "Papyrus_and_Sans's_House", "Papyrus's_room",
+ "Sans's_room", "Shop", "Snowdin_Shop", "Snowed_Inn", "Innkeeper", "Librarby", "Gift_Bear", "Ice_Wolf",
+ "Cell_Phone/Papyrus", "Papyrus_date", "Mysterious_door", "Snowdin_Forest", "Royal_Guard", "Blue_attack",
+ "Faun", "Big_Mouth", "Crazy_Bun", "Drunk_Bun", "Politics_Bear", "Punk_Hamster", "Rabbit_Kid",
+ "Rabbit_Lady_and_Cinnamon", "Red_Bird", "Red_Demon", "Scarf_Lady", "Slime_family", "Ugly_Fish",
+ "Library_Lizard", "Library_Loox", "Knock_lover", "Lady_Garf", "Wrap_Mouse", "Innkeeper's_child",
+ "Snoring_guests", "Mad_Mew_Mew", "Snowdrake's_mother", "Snowdrake's_father", "Bonetrousle",
+ # Waterfall
+ "Waterfall", "Waterfall/Puzzles", "Aaron", "Woshua", "Moldbygg", "Shyren", "Temmie", "Temmie_Village",
+ "Mad_Dummy", "Undyne", "Undyne/In_battle", "Onionsan", "Gerson", "Echo_Flower", "Bridge_seed",
+ "Undyne's_House", "Napstablook's_house", "Garbage_Dump", "Shyren/In_battle", "Riverman", "Piano_puzzle",
+ "Legendary_Artifact", "Astronaut_Food", "Glamburger", "Hot_Dog...?", "Cloudy_Glasses", "Torn_Notebook",
+ "Stained_Apron", "Burnt_Pan", "Toy_Knife", "Temmie_Flakes", "Tem_Shop", "Small_bird", "Lemon_Bread",
+ "Tough_Glove", "Manly_Bandanna", "Old_Tutu", "Ballet_Shoes", "Cinnamon_Bunny", "Bisicle", "Unisicle",
+ "Nice_Cream_(item)", "Snowman_Piece", "Snowman_Piece", "Punch_Card", "Dog_Residue", "Abandoned_Quiche",
+ "Astronaut_Food", "Hot_Dog...?", "Hot_Cat", "Spaghetti", "Glamburger", "Sea_Tea", "Temmie_Flakes",
+ "Junk_Food", "Annoying_Dog/In_battle", "Lesser_Dog/In_battle", "Greater_Dog/In_battle", "Doggo/In_battle",
+ "Dogamy_and_Dogaressa/In_battle",
+ # Hotland and the CORE
+ "Hotland", "Hotland/Puzzles", "Alphys", "Alphys's_Lab", "Mettaton", "Mettaton/In_battle", "Mettaton_EX",
+ "Muffet", "Muffet/In_battle", "Royal_Guard/In_battle", "Vulkin", "Tsunderplane", "Pyrope", "So_Sorry",
+ "Madjick", "Knight_Knight", "Final_Froggit", "Whimsalot", "Astigmatism", "CORE", "MTT_Resort", "Burgerpants",
+ "Heats_Flamesman", "Bratty_and_Catty", "Spider_Bake_Sale", "Elevator", "Sans/Hotland", "Hot_Dog...?",
+ "Cell_Phone/Alphys", "Steam_Vent", "Conveyor_belt", "Mettaton_quiz_show", "Laser", "Royal_Guards", "Bomb",
+ "Stained_Apron", "Hot_Dog_Harpy", "Art_Club_room", "Spider_Entrance", "Multicolor_Tile_Puzzle", "Lab",
+ # New Home
+ "New_Home", "Asgore", "Asgore/In_battle", "Asgore's_Home", "Last_Corridor", "Throne_Room", "Barrier",
+ "Photoshop_Flowey", "Neutral_Route", "Worn_Dagger", "Heart_Locket"]
+# files the categories leave out, as their wiki names
+FILES = ["QC sprite.png", "QC screenshot shop.png", "Gerson screenshot shop.png", "Tem Shop screenshot Temmie Armor.png"]
+DEMO = {
+ "MacUnderTale.zip": "https://thisisatestofthevideogamedeliverysystem.sfo2.digitaloceanspaces.com/game/MacUnderTale.zip",
+}
+FONT = ("pixel_operator.zip", "https://dl.dafont.com/dl/?f=pixel_operator")
+
+
+def get(url, tries=4):
+ for t in range(tries):
+ try:
+ with urllib.request.urlopen(urllib.request.Request(url, headers=UA), timeout=60) as r:
+ return r.read()
+ except Exception as e: # network hiccups: wait and try again
+ if t == tries - 1:
+ raise
+ print(" retry", url, e)
+ time.sleep(2 ** (t + 1))
+
+
+def api(**q):
+ q["format"] = "json"
+ return json.loads(get(API + "?" + urllib.parse.urlencode(q)))
+
+
+def category_files(cat):
+ out, cont = [], {}
+ while True:
+ d = api(action="query", list="categorymembers", cmtitle="Category:" + cat, cmlimit=500, cmtype="file", **cont)
+ out += [m["title"] for m in d["query"]["categorymembers"]]
+ if "continue" not in d:
+ return out
+ cont = {"cmcontinue": d["continue"]["cmcontinue"]}
+
+
+def file_url(title):
+ d = api(action="query", prop="imageinfo", iiprop="url", titles=title)
+ for p in d["query"]["pages"].values():
+ if "imageinfo" in p:
+ return p["imageinfo"][0]["url"].split("?")[0]
+ return None
+
+
+def save(path, data):
+ os.makedirs(os.path.dirname(path), exist_ok=True)
+ with open(path + ".part", "wb") as f:
+ f.write(data)
+ os.replace(path + ".part", path)
+
+
+def main():
+ if len(sys.argv) != 2:
+ sys.exit(__doc__)
+ ref = sys.argv[1]
+ for kind, cats in CATEGORIES.items():
+ for cat in cats:
+ for title in category_files(cat):
+ name = title.split(":", 1)[1].replace(" ", "_")
+ path = os.path.join(ref, "wiki", kind, name)
+ if os.path.exists(path):
+ continue
+ url = file_url(title)
+ if url:
+ print(kind, name)
+ save(path, get(url))
+ for name in FILES:
+ path = os.path.join(ref, "wiki", "sprites", name.replace(" ", "_"))
+ if not os.path.exists(path):
+ url = file_url("File:" + name)
+ if url:
+ print("file", name)
+ save(path, get(url))
+ for page in PAGES:
+ path = os.path.join(ref, "wiki", "pages", page.replace("/", "__") + ".txt")
+ if os.path.exists(path):
+ continue
+ d = api(action="parse", page=page.replace("_", " "), prop="wikitext", redirects=1)
+ if "parse" in d:
+ print("page", page)
+ save(path, d["parse"]["wikitext"]["*"].encode())
+ for name, url in DEMO.items():
+ path = os.path.join(ref, "demo", name)
+ if not os.path.exists(path):
+ print("demo", name)
+ save(path, get(url))
+ out = os.path.join(ref, "demo", "mac")
+ if not os.path.exists(out):
+ with zipfile.ZipFile(path) as z:
+ for m in z.namelist():
+ if "/MacOS/sprites/" in m or "/MacOS/bgs/" in m or m.endswith("_manifest.txt"):
+ if m.startswith("__MACOSX") or m.endswith("/"):
+ continue
+ save(os.path.join(out, *m.split("/MacOS/")[1].split("/")), z.read(m))
+ path = os.path.join(ref, "fonts", FONT[0])
+ if not os.path.exists(path):
+ print("font", FONT[0])
+ save(path, get(FONT[1]))
+ with zipfile.ZipFile(path) as z:
+ for m in z.namelist():
+ if m.endswith(".ttf") or m.endswith(".txt"):
+ dst = os.path.join(ref, "fonts", os.path.basename(m))
+ if not os.path.exists(dst):
+ save(dst, z.read(m))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/games/undertale/tools/pack.py b/games/undertale/tools/pack.py
new file mode 100644
index 00000000..2bcc6572
--- /dev/null
+++ b/games/undertale/tools/pack.py
@@ -0,0 +1,780 @@
+#!/usr/bin/env python3
+"""Undertale's pictures, rooms and fonts -> src/data.bin + src/data.h.
+
+ python3 -I tools/pack.py REF (REF: the folder tools/fetch.py filled)
+
+data.bin: "UTD1", the number of sections, their offsets, then the sections (SEC_* in data.h):
+- SPRITES: for each sprite (SPR_*) its first frame and its number of frames (u16, u16)
+- FRAMES: each frame: w, h, origin x, y (u16, u16, s16, s16), bpp, colours (u8, u8), the colours (RGB565),
+ each row's offset (u16) from the rows' data, then the rows: runs of (skip, length, length pixels of bpp bits,
+ lowest first, the run padded to a byte), until the row's width
+- TILES: the areas' 20 x 20 tiles: bpp (0: one colour), colours, the colours (RGB565), 400 pixels of bpp bits
+- ROOMS: 32 bytes each (res.c room_info): the room's rectangle on its area's map, its tile grid and
+ collision map (4-pixel cells, 1 walkable), its area, flags (1: ice, its bits after the collision's) and name
+- GRID, COLL: the rooms' tile ids (u16, 0xFFFF none) and collision bits
+- FONTS: height, first character, count, flags, then 8 bytes a glyph (advance, w, h, ox, oy, 0,
+ bitmap offset u16) and the bitmaps (rows of whole bytes, highest bit first)
+- STRINGS: the room names and other texts the data refers to
+- DOORS: for each room its first door and how many (u16, u16), then the doors: 16 bytes each (x, y, w, h on
+ the map, the room it leads to, where Frisk's top-left lands, the way he faces)
+- BLOCKS: the frames and tiles themselves, in LZMA streams the game unpacks when it needs them (blk.c): the
+ count, the 5 LZMA properties bytes (3 of padding), then for each block its stream's offset (u32) and its size
+ unpacked (u32), then the streams. FRAMES and TILES only say where each frame or tile is: its block and its
+ offset in it (u16, u16). A sprite's frames share blocks (of 16 KB at most); a room's tiles go in 8 x 8 squares,
+ so the tiles a screen shows sit in few blocks (of 2 KB)."""
+import lzma
+import os
+import struct
+import sys
+
+import numpy as np
+from scipy import ndimage
+from PIL import Image, ImageDraw, ImageFont, ImageSequence
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+sys.path.insert(0, HERE)
+import rooms as R # noqa: E402
+import sprites as S # noqa: E402
+
+OUT_BIN = os.path.join(HERE, "..", "src", "data.bin")
+OUT_H = os.path.join(HERE, "..", "src", "data.h")
+SECTIONS = ["SPRITES", "FRAMES", "TILES", "ROOMS", "GRID", "COLL", "FONTS", "STRINGS", "DOORS", "BLOCKS"]
+BLOCK_TILES, BLOCK_FRAMES = 2048, 16384 # the most bytes a block holds, unpacked (a frame bigger: alone)
+MAX_BLOCKS = 4096 # (blk.c's table)
+LZMA_FILTERS = [{"id": lzma.FILTER_LZMA1, "dict_size": 1 << 16, "lc": 0, "lp": 0, "pb": 0}]
+
+
+class Blocks:
+ """the BLOCKS section, and the tables that point into it"""
+ def __init__(self):
+ self.raw = []
+
+ def place(self, entries, groups):
+ """entries (bytes) put in blocks, one per group of their indices -> u32 count, (block, offset) each"""
+ where = [None] * len(entries)
+ for g in groups:
+ data = bytearray()
+ for i in g:
+ where[i] = (len(self.raw), len(data))
+ data += entries[i]
+ assert len(data) < 65536
+ self.raw.append(bytes(data))
+ return struct.pack(" limit or i in starts):
+ out.append(cur)
+ cur, n = [], 0
+ cur.append(i)
+ n += sz
+ if cur:
+ out.append(cur)
+ return out
+
+
+def rgb565(r, g, b):
+ r, g, b = int(r), int(g), int(b)
+ return ((r * 31 + 127) // 255) << 11 | ((g * 63 + 127) // 255) << 5 | ((b * 31 + 127) // 255)
+
+
+class Strings:
+ def __init__(self):
+ self.list, self.ids = [], {}
+
+ def add(self, s):
+ if s not in self.ids:
+ self.ids[s] = len(self.list)
+ self.list.append(s)
+ return self.ids[s]
+
+ def blob(self):
+ out, offs = bytearray(), []
+ base = 4 + 4 * len(self.list)
+ for s in self.list:
+ offs.append(base + len(out))
+ out += s.encode("latin-1") + b"\0"
+ return struct.pack("> 3] |= v << (b & 7)
+ return out
+
+
+def bpp_for(n):
+ return 1 if n <= 2 else 2 if n <= 4 else 4 if n <= 16 else 8
+
+
+# ------------------------------------------------------------------ frames
+def encode_frame(img, ox, oy):
+ """an RGBA image -> the frame's bytes (row RLE)"""
+ a = np.array(img.convert("RGBA"))
+ h, w = a.shape[:2]
+ opaque = a[:, :, 3] >= 128
+ cols = {}
+ for c in map(tuple, a[opaque][:, :3]):
+ k = rgb565(*c)
+ cols[k] = cols.get(k, 0) + 1
+ pal = sorted(cols, key=lambda k: -cols[k])
+ if len(pal) > 255:
+ raise ValueError("a frame with more than 255 colours")
+ index = {k: i for i, k in enumerate(pal)}
+ bpp = bpp_for(max(len(pal), 1))
+ rows, data = [], bytearray()
+ for y in range(h):
+ rows.append(len(data))
+ x = 0
+ while x < w:
+ skip = 0
+ while x < w and not opaque[y, x] and skip < 255:
+ x += 1
+ skip += 1
+ run = []
+ while x < w and opaque[y, x] and len(run) < 255:
+ run.append(index[rgb565(*a[y, x, :3])])
+ x += 1
+ data += bytes([skip, len(run)]) + bits_pack(run, bpp)
+ if len(data) > 0xFFFF:
+ raise ValueError("a frame too big")
+ head = struct.pack(" the game's size (the phase where 2 x 2 blocks agree)"""
+ a = np.array(img.convert("RGBA")).astype(np.int32)
+ h, w = a.shape[:2]
+ best = None
+ for py in (0, 1):
+ for px in (0, 1):
+ b = a[py:py + (h - py) // 2 * 2, px:px + (w - px) // 2 * 2]
+ if not b.size:
+ continue
+ q = b.reshape(b.shape[0] // 2, 2, b.shape[1] // 2, 2, 4)
+ err = np.abs(q - q[:, :1, :, :1]).sum()
+ if best is None or err < best[0]:
+ best = (err, py, px)
+ _, py, px = best
+ b = a[py::2, px::2]
+ return Image.fromarray(b.astype(np.uint8), "RGBA")
+
+
+def shrink(img):
+ """art drawn for the original's 640 x 480 battle -> half its size: each 2 x 2 block becomes its most common
+ opaque colour (or clear, if most of it is)"""
+ a = np.array(img.convert("RGBA"))
+ h, w = a.shape[:2]
+ a = np.pad(a, ((0, h % 2), (0, w % 2), (0, 0)))
+ H, W = a.shape[0] // 2, a.shape[1] // 2
+ out = np.zeros((H, W, 4), np.uint8)
+ for y in range(H):
+ for x in range(W):
+ block = a[2 * y:2 * y + 2, 2 * x:2 * x + 2].reshape(4, 4)
+ op = block[block[:, 3] >= 128]
+ if len(op) < 2:
+ continue
+ cols, counts = np.unique(op[:, :3], axis=0, return_counts=True)
+ # ties: the darker colour keeps outlines
+ best = sorted(zip(counts, [-int(c.sum()) for c in cols], range(len(cols))))[-1][2]
+ out[y, x, :3] = cols[best]
+ out[y, x, 3] = 255
+ return Image.fromarray(out, "RGBA")
+
+
+def load_frames(ref, src):
+ """'demo:spr_name' (all its numbered frames), 'wiki:File.png' or .gif (all frames, halved), 'png:path'"""
+ kind, name = src.split(":", 1)
+ if kind in ("demo", "demo2"):
+ d = os.path.join(ref, "demo", "mac", "sprites")
+ out, k = [], 0
+ while os.path.exists(os.path.join(d, "%s %d.png" % (name, k))):
+ out.append(Image.open(os.path.join(d, "%s %d.png" % (name, k))).convert("RGBA"))
+ k += 1
+ if not out:
+ raise FileNotFoundError(src)
+ return [shrink(f) for f in out] if kind == "demo2" else out
+ if kind == "demobg":
+ return [Image.open(os.path.join(ref, "demo", "mac", "bgs", name + ".png")).convert("RGBA")]
+ if kind in ("wiki", "wiki1"):
+ im = Image.open(os.path.join(ref, "wiki", "sprites", name))
+ frames = []
+ for fr in ImageSequence.Iterator(im):
+ f = fr.convert("RGBA")
+ frames.append(halve(f) if kind == "wiki" else f)
+ return frames
+ if kind == "png":
+ return [Image.open(os.path.join(HERE, name)).convert("RGBA")]
+ if kind == "move":
+ # 'move:File.gif:x0,y0,x1,y1:i,j,...': the biggest thing that moves in a part of a screenshot GIF (against
+ # its median frame), in frames i, j...; halved
+ name, box, which = name.split(":")
+ x0, y0, x1, y1 = map(int, box.split(","))
+ im = Image.open(os.path.join(ref, "wiki", "screens", name))
+ st = np.stack([np.asarray(f.convert("RGB"))[y0:y1, x0:x1] for f in ImageSequence.Iterator(im)]).astype(int)
+ bg = np.median(st, axis=0)
+ out = []
+ for i in map(int, which.split(",")):
+ m = np.abs(st[i] - bg).sum(2) > 30
+ lab, n = ndimage.label(ndimage.binary_dilation(m, iterations=4))
+ k = int(np.argmax(ndimage.sum(m, lab, range(1, n + 1)))) + 1
+ sl = ndimage.find_objects(lab)[k - 1]
+ a = (m & (lab == k))[sl]
+ rgba = np.dstack([st[i][sl].astype(np.uint8), (a * 255).astype(np.uint8)])
+ # each frame on a canvas of the window's size, its feet at the bottom middle: frames line up
+ f = Image.fromarray(rgba, "RGBA")
+ canvas = Image.new("RGBA", (x1 - x0, y1 - y0), (0, 0, 0, 0))
+ canvas.paste(f, ((x1 - x0 - f.width) // 2, y1 - y0 - f.height), f)
+ out.append(halve(canvas))
+ return out
+ if kind == "map":
+ # 'map:Map.png:x,y,w,h': a piece of an area's map, as it is
+ name, box = name.split(":")
+ x, y, w, h = map(int, box.split(","))
+ return [Image.open(os.path.join(ref, "wiki", "maps", name)).convert("RGBA").crop((x, y, x + w, y + h))]
+ if kind == "gen": # drawn by tools/sprites.py
+ return S.GEN[name]()
+ if kind == "wiki2": # a wiki file of the 640 x 480 battle, at the game's size: halved
+ im = Image.open(os.path.join(ref, "wiki", "sprites", name))
+ return [shrink(fr.convert("RGBA")) for fr in ImageSequence.Iterator(im)]
+ raise ValueError(src)
+
+
+def crop_frames(frames):
+ """all frames cropped to the union of their opaque pixels; returns the frames and the crop's top-left"""
+ box = None
+ for f in frames:
+ b = f.getchannel("A").point(lambda v: 255 if v >= 128 else 0).getbbox()
+ if b:
+ box = b if box is None else (min(box[0], b[0]), min(box[1], b[1]), max(box[2], b[2]), max(box[3], b[3]))
+ if box is None:
+ return frames, 0, 0
+ return [f.crop(box) for f in frames], box[0], box[1]
+
+
+# ------------------------------------------------------------------ fonts
+def make_font(ref, spec):
+ path = os.path.join(ref, "fonts", spec["ttf"]) if not spec["ttf"].startswith("/") else spec["ttf"]
+ font = ImageFont.truetype(path, spec["size"])
+ first, last = 32, 127
+ glyphs, bits = bytearray(), bytearray()
+ asc = spec.get("ascent", spec["size"])
+ for c in range(first, last):
+ ch = chr(c) if c < 127 else "♥"
+ if c == 127 and "heart" not in spec:
+ ch = "?"
+ im = Image.new("L", (spec["size"] * 3, spec["size"] * 3), 0)
+ d = ImageDraw.Draw(im)
+ d.fontmode = "1"
+ d.text((spec["size"], spec["size"]), ch, font=font, fill=255)
+ adv = int(round(font.getlength(ch))) if "advance" not in spec else spec["advance"]
+ bb = im.getbbox()
+ if bb is None:
+ glyphs += struct.pack("= 128
+ gh, gw = g.shape
+ ox, oy = bb[0] - spec["size"], bb[1] - spec["size"] - (spec["size"] - asc)
+ off = len(bits)
+ for row in g:
+ rb = bytearray((gw + 7) // 8)
+ for x, v in enumerate(row):
+ if v:
+ rb[x >> 3] |= 0x80 >> (x & 7)
+ bits += rb
+ glyphs += struct.pack("> 16, int(c) >> 8 & 255, int(c) & 255) for c in cols]
+ self.transparent = None
+ self.spec = spec
+ for patch in spec.get("patches", []):
+ self.patch(patch)
+
+ def black_index(self):
+ for i, c in enumerate(self.rgb):
+ if c == (0, 0, 0):
+ return i
+ return None
+
+ def patch(self, p):
+ """changes to the map, before rooms are cut from it: ('fill', x, y, w, h, (r, g, b)),
+ ('copy', sx, sy, w, h, dx, dy) or ('mirror', ...) the same, flipped left to right, or ('erase', x, y, w, h,
+ [colours]): what isn't one of the colours takes the colour of the nearest pixel that is (a monster painted
+ on the floor)"""
+ if p[0] == "fill":
+ _, x, y, w, h, c = p
+ self.idx[y:y + h, x:x + w] = self.color_index(c)
+ elif p[0] == "copy":
+ _, sx, sy, w, h, dx, dy = p
+ self.idx[dy:dy + h, dx:dx + w] = self.idx[sy:sy + h, sx:sx + w].copy()
+ elif p[0] == "erase":
+ # the nearest pixel of the colours, looking a little around the rectangle (only it changes)
+ _, x, y, w, h, cols, *drop = p
+ if cols == "ring": # a little more than the box: the outline's last pixels
+ x, y, w, h = x - 2, y - 2, w + 4, h + 4
+ m = 4
+ x0, y0 = max(0, x - m), max(0, y - m)
+ win = self.idx[y0:y + h + m, x0:x + w + m]
+ if cols == "ring":
+ # the colours just around the rectangle (but those of drop): what's behind the monster
+ ring = np.ones(win.shape, bool)
+ ring[y - y0:y - y0 + h, x - x0:x - x0 + w] = False
+ # (black: the outlines of what's erased, unless the patch says otherwise)
+ dropped = drop[0] if drop else [(0, 0, 0)]
+ # (only colours common around it: a few pixels of a colour are the monster's own, sticking out)
+ u, n = np.unique(win[ring], return_counts=True)
+ ids = {int(i) for i, c in zip(u, n) if c >= 0.05 * ring.sum()} - \
+ {i for i, k in enumerate(self.rgb) if k in dropped}
+ keep = np.isin(win, list(ids))
+ else:
+ keep = np.isin(win, [i for i, k in enumerate(self.rgb) if k in [tuple(c) for c in cols]])
+ if not keep.any():
+ raise ValueError("erase %s: none of its colours there" % (p,))
+ _, (iy, ix) = ndimage.distance_transform_edt(~keep, return_indices=True)
+ filled = win[iy, ix]
+ self.idx[y:y + h, x:x + w] = filled[y - y0:y - y0 + h, x - x0:x - x0 + w]
+ elif p[0] == "mirror":
+ _, sx, sy, w, h, dx, dy = p
+ self.idx[dy:dy + h, dx:dx + w] = self.idx[sy:sy + h, sx:sx + w][:, ::-1].copy()
+ else:
+ raise ValueError(p)
+
+ def color_index(self, c):
+ for i, k in enumerate(self.rgb):
+ if k == tuple(c) and i != self.transparent:
+ return i
+ raise ValueError("colour %s not in the map" % (c,))
+
+
+def room_pixels(area, r):
+ """the room's rectangle of the map, as RGB565 per pixel; outside the map and its clear pixels black"""
+ x, y, w, h = r["rect"]
+ H, W = area.idx.shape
+ out = np.zeros((h, w), np.int32)
+ xs0, ys0, xs1, ys1 = max(x, 0), max(y, 0), min(x + w, W), min(y + h, H)
+ sub = area.idx[ys0:ys1, xs0:xs1]
+ lut = np.array([rgb565(*c) for c in area.rgb] + [0] * max(0, 256 - len(area.rgb)), np.int32)
+ if area.transparent is not None:
+ lut[area.transparent] = 0
+ out[ys0 - y:ys1 - y, xs0 - x:xs1 - x] = lut[sub]
+ return out, sub, (xs0 - x, ys0 - y)
+
+
+def floor_mask(area, r, x0, y0, x1, y1):
+ """the pixels of a rectangle of the map in the room's floor colours"""
+ walk_idx = [i for c in area.spec["floor"] + r.get("floor_colors", []) for i, k in enumerate(area.rgb)
+ if k == tuple(c) and i != area.transparent]
+ return np.isin(area.idx[y0:y1, x0:x1], walk_idx)
+
+
+KEEP = -32768 # a door's landing: Frisk's own x or y, kept
+
+
+def apply_links(area, rooms):
+ """the links between rooms one above the other through a dark doorway, ("v", lower, upper, x, w, y): a door
+ each way between the lower room's floor under the doorway (x .. x + w, y in it) and the upper's above it, the
+ floor made walkable up to them. ("up", lower, x, w, y, to, (tx, ty)): a one-way door, up a chute"""
+ for link in R.LINKS:
+ lo = rooms.get(link[1])
+ if lo is None or lo["area"] != area.spec["name"]:
+ continue
+ if link[0] == "h":
+ # side by side: the left room's right edge and the right one's left edge, Frisk's height kept
+ ri = rooms[link[2]]
+ y, h = link[3:5]
+ lx, _, lw, _ = lo["rect"]
+ rx = ri["rect"][0]
+ lo.setdefault("fix", []).append(("walk", lx + lw - 8, y, 8, h))
+ ri.setdefault("fix", []).append(("walk", rx, y, 8, h))
+ lo.setdefault("doors", []).append(((lx + lw - 2, y, 2, h), link[2], (rx + 2, KEEP), "right"))
+ ri.setdefault("doors", []).append(((rx, y, 2, h), link[1], (lx + lw - 22, KEEP), "left"))
+ continue
+ x, w, yd = link[3:6] if link[0] == "v" else link[2:5]
+ cx = x + w // 2
+ lx, ly, lw, lh = lo["rect"]
+ col = floor_mask(area, lo, cx, yd, cx + 1, ly + lh)[:, 0]
+ if not col.any():
+ sys.exit("link %s: no floor under the doorway" % (link,))
+ ya = yd + int(np.argmax(col)) # the lower room's floor, where the doorway ends
+ if link[0] == "up":
+ # a chute is narrow: a little room either side to walk into it
+ to, (tx, ty) = link[5:7]
+ lo.setdefault("fix", []).append(("walk", x - 4, ya - 8, w + 8, 12))
+ lo.setdefault("doors", []).append(((x - 2, ya - 6, w + 4, 4), to, (tx, ty), "down"))
+ continue
+ lo.setdefault("fix", []).append(("walk", x, ya - 8, w, 12))
+ up = rooms[link[2]]
+ ux, uy, uw, uh = up["rect"]
+ col = floor_mask(area, up, cx, uy, cx + 1, min(uy + uh, yd))[:, 0]
+ if not col.any():
+ sys.exit("link %s: no floor above the doorway" % (link,))
+ yb = uy + len(col) - int(np.argmax(col[::-1])) # the upper room's floor, where the doorway starts
+ up.setdefault("fix", []).append(("walk", x, yb - 4, w, 4))
+ lo.setdefault("doors", []).append(((x, ya - 6, w, 4), link[2], (cx - 10, yb - 34), "up"))
+ up.setdefault("doors", []).append(((x, yb - 2, w, 2), link[1], (cx - 10, ya - 12), "down"))
+ print("link: %s floor at %d, %s at %d" % (link[1], ya, link[2], yb))
+
+
+def collision(area, r):
+ """4-pixel cells over the room: walkable where the floor's colours fill at least half the cell and
+ inside one of its floor rectangles (all of it if it has none), then its own fixes"""
+ x, y, w, h = r["rect"]
+ cw, ch = (w + 3) // 4, (h + 3) // 4
+ H, W = area.idx.shape
+ walk_idx = set()
+ for c in area.spec["floor"] + r.get("floor_colors", []):
+ for i, k in enumerate(area.rgb):
+ if k == tuple(c) and i != area.transparent:
+ walk_idx.add(i)
+ # the map's pixels over the cells, and `margin` more around (what's beyond the room's edge counts too)
+ margin = r.get("margin", area.spec.get("margin", 0))
+ X0, Y0, X1, Y1 = x - margin, y - margin, x + cw * 4 + margin, y + ch * 4 + margin
+ big = np.zeros((Y1 - Y0, X1 - X0), bool)
+ xs0, ys0, xs1, ys1 = max(X0, 0), max(Y0, 0), min(X1, W), min(Y1, H)
+ big[ys0 - Y0:ys1 - Y0, xs0 - X0:xs1 - X0] = np.isin(area.idx[ys0:ys1, xs0:xs1], list(walk_idx))
+ sub = big[margin:margin + ch * 4, margin:margin + cw * 4]
+ if r.get("open"): # floors of many colours (a carpet, planks): its floor_rects alone say
+ sub = np.ones_like(sub)
+ cells = sub.reshape(ch, 4, cw, 4).mean(axis=(1, 3)) >= 0.5
+ # piles of leaves are floor, outline and all
+ soft = area.spec.get("piles")
+ pile_big = np.zeros_like(big)
+ if soft:
+ win = np.zeros(big.shape, area.idx.dtype)
+ win[ys0 - Y0:ys1 - Y0, xs0 - X0:xs1 - X0] = area.idx[ys0:ys1, xs0:xs1]
+ pile = np.isin(win, [i for i, k in enumerate(area.rgb) if k in [tuple(c) for c in soft]])
+ lab, n = ndimage.label(ndimage.binary_dilation(pile, iterations=3))
+ for sl in ndimage.find_objects(lab):
+ if (sl[0].stop - sl[0].start) * (sl[1].stop - sl[1].start) >= 16:
+ pile_big[sl] = True
+ # walls drawn in the floor's colours give themselves away by their lines: a cell is floor only with no
+ # other colour within `margin` pixels of it (a pile's leaves don't count)
+ if margin:
+ other = ~big & ~pile_big
+ # lines drawn on the floor (a path's outline) aren't walls
+ lines = area.spec.get("lines", []) + r.get("lines", [])
+ if lines:
+ win = np.zeros(big.shape, area.idx.dtype)
+ win[ys0 - Y0:ys1 - Y0, xs0 - X0:xs1 - X0] = area.idx[ys0:ys1, xs0:xs1]
+ other &= ~np.isin(win, [i for i, k in enumerate(area.rgb) if k in [tuple(c) for c in lines]])
+ near = np.zeros_like(sub)
+ for dy in range(2 * margin + 1):
+ for dx in range(2 * margin + 1):
+ near |= other[dy:dy + sub.shape[0], dx:dx + sub.shape[1]]
+ cells &= ~near.reshape(ch, 4, cw, 4).any(axis=(1, 3))
+ piles = pile_big[margin:margin + ch * 4, margin:margin + cw * 4]
+ cells |= piles.reshape(ch, 4, cw, 4).mean(axis=(1, 3)) >= 0.5
+ if r.get("close", area.spec.get("close")):
+ # a floor's own lines one cell thin (the seams between planks) aren't walls: a cell with floor either side
+ up, down = np.zeros_like(cells), np.zeros_like(cells)
+ up[1:], down[:-1] = cells[:-1], cells[1:]
+ left, right = np.zeros_like(cells), np.zeros_like(cells)
+ left[:, 1:], right[:, :-1] = cells[:, :-1], cells[:, 1:]
+ cells = cells | (up & down) | (left & right)
+ if r.get("floor_rects"):
+ keep = np.zeros_like(cells)
+ for fx, fy, fw, fh in r["floor_rects"]:
+ keep[max(0, (fy - y) // 4):max(0, (fy + fh - y + 3) // 4), max(0, (fx - x) // 4):max(0, (fx + fw - x + 3) // 4)] = True
+ cells &= keep
+ raw = sub.reshape(ch, 4, cw, 4).mean(axis=(1, 3)) >= 0.5
+ for kind, fx, fy, fw, fh, *how in r.get("fix", []):
+ sl = (slice(max(0, (fy - y) // 4), max(0, (fy + fh - y + 3) // 4)),
+ slice(max(0, (fx - x) // 4), max(0, (fx + fw - x + 3) // 4)))
+ # "floor": walkable only where the floor's colours are (the edge of a room, past the margin rule)
+ cells[sl] = raw[sl] if how == ["floor"] else kind == "walk"
+ out = bytearray()
+ for row in cells:
+ rb = bytearray((cw + 7) // 8)
+ for i, v in enumerate(row):
+ if v:
+ rb[i >> 3] |= 1 << (i & 7)
+ out += rb
+ return out, cw, ch
+
+
+class TileSet:
+ def __init__(self):
+ self.ids, self.blobs = {}, []
+ self.blobs_size = 0
+
+ def add(self, block):
+ """a 20 x 20 block of RGB565 values -> its tile id"""
+ key = block.tobytes()
+ if key in self.ids:
+ return self.ids[key]
+ flat = block.reshape(-1)
+ cols, counts = np.unique(flat, return_counts=True)
+ order = [int(c) for c, _ in sorted(zip(cols, counts), key=lambda t: -t[1])]
+ if len(order) == 1:
+ blob = struct.pack("= 0.5
+ for fx, fy, fw, fh in r.get("no_ice", []):
+ cells[max(0, (fy - y) // 4):(fy + fh - y + 3) // 4, max(0, (fx - x) // 4):(fx + fw - x + 3) // 4] = False
+ for row in cells:
+ rb = bytearray((cw + 7) // 8)
+ for i, v in enumerate(row):
+ if v:
+ rb[i >> 3] |= 1 << (i & 7)
+ coll += rb
+ flags |= 1
+ return dict(rect=(x, y, w, h), g=(gx, gy, cols, rows), grid=bytes(grid), coll=bytes(coll), cw=cw, ch=ch,
+ area=r["area_id"], flags=flags, name=strings.add(r.get("title", "")))
+
+
+# ------------------------------------------------------------------ main
+def main():
+ if len(sys.argv) < 2:
+ sys.exit(__doc__)
+ ref = sys.argv[1]
+ strings = Strings()
+ strings.add("")
+ defines = []
+
+ # sprites
+ spr_entries, frames = [], []
+ for k, spec in enumerate(S.SPRITES):
+ name, src = spec[0], spec[1]
+ opts = spec[2] if len(spec) > 2 else {}
+ imgs = []
+ for s in (src if isinstance(src, list) else [src]):
+ imgs += load_frames(ref, s)
+ if opts.get("clear_black"): # drawn over black anyway: its black is clear, and costs nothing
+ out = []
+ for im in imgs:
+ a = np.array(im)
+ a[(a[:, :, 0] < 8) & (a[:, :, 1] < 8) & (a[:, :, 2] < 8)] = 0
+ out.append(Image.fromarray(a, "RGBA"))
+ imgs = out
+ if opts.get("dedupe"): # GIFs repeat a frame for its duration: once each
+ out = []
+ for im in imgs:
+ if not out or im.tobytes() != out[-1].tobytes():
+ out.append(im)
+ imgs = out
+ if "box" in opts:
+ bx, by, bw, bh = opts["box"]
+ imgs = [im.crop((bx, by, bx + bw, by + bh)) for im in imgs]
+ if "every" in opts:
+ imgs = imgs[::opts["every"]]
+ if "frames" in opts:
+ imgs = [imgs[i] for i in opts["frames"]]
+ if opts.get("clear_bg"): # the black around a monster (the wiki's GIFs have no see-through) is clear
+ out = []
+ for im in imgs:
+ a = np.array(im)
+ dark = (a[:, :, 0] < 8) & (a[:, :, 1] < 8) & (a[:, :, 2] < 8) | (a[:, :, 3] < 128)
+ lab, _ = ndimage.label(dark)
+ edge = set(np.unique(np.concatenate([lab[0], lab[-1], lab[:, 0], lab[:, -1]]))) - {0}
+ a[np.isin(lab, list(edge))] = 0
+ out.append(Image.fromarray(a, "RGBA"))
+ imgs = out
+ if "clear" in opts: # these colours are see-through (a map's floor behind a thing cut from it)
+ out = []
+ for im in imgs:
+ a = np.array(im)
+ for c in opts["clear"]:
+ a[(a[:, :, 0] == c[0]) & (a[:, :, 1] == c[1]) & (a[:, :, 2] == c[2])] = 0
+ out.append(Image.fromarray(a, "RGBA"))
+ imgs = out
+ if "only" in opts: # only these colours kept (a thing cut from a map that shares the map's colours)
+ out = []
+ for im in imgs:
+ a = np.array(im)
+ keep = np.zeros(a.shape[:2], bool)
+ for c in opts["only"]:
+ keep |= (a[:, :, 0] == c[0]) & (a[:, :, 1] == c[1]) & (a[:, :, 2] == c[2])
+ a[~keep] = 0
+ out.append(Image.fromarray(a, "RGBA"))
+ imgs = out
+ if opts.get("mirror"):
+ imgs = [im.transpose(Image.FLIP_LEFT_RIGHT) for im in imgs]
+ if "tints" in opts: # the first frame, then it again in each colour (the SOUL's modes)
+ a = np.array(imgs[0].convert("RGBA"))
+ lum = a[:, :, :3].max(axis=2).astype(np.float32) / 255
+ imgs = [imgs[0]]
+ for t in opts["tints"]:
+ b = a.copy()
+ b[:, :, :3] = (lum[:, :, None] * np.array(t, np.float32)).astype(np.uint8)
+ imgs.append(Image.fromarray(b, "RGBA"))
+ if "scale" in opts:
+ imgs = [im.resize((im.width * opts["scale"], im.height * opts["scale"]), Image.NEAREST) for im in imgs]
+ w0, h0 = imgs[0].size
+ ox, oy = opts.get("origin", (0, 0))
+ if opts.get("center"):
+ ox, oy = w0 // 2, h0 // 2
+ if opts.get("feet"):
+ ox, oy = w0 // 2, h0
+ if not opts.get("keep"):
+ imgs, cx, cy = crop_frames(imgs)
+ ox, oy = ox - cx, oy - cy
+ spr_entries.append((len(frames), len(imgs)))
+ for im in imgs:
+ frames.append(encode_frame(im, ox, oy))
+ defines.append(("SPR_" + name.upper(), k))
+ sec_sprites = struct.pack(" 0,
+ lambda v: v[:4] == b"UT01" and struct.unpack_from(" 60),
# a simulation saved ("NPWD"); after the reinstall, from the copy, it goes on and is saved again
"numpowder": (lambda v: v[:4] == b"NPWD",) * 2,
# a game saved ("HKSV", then the play time at 152); after the reinstall, from the copy, it goes on
diff --git a/tools/release.py b/tools/release.py
index 8063f0b6..9b2c73bd 100644
--- a/tools/release.py
+++ b/tools/release.py
@@ -27,6 +27,9 @@
"on its own"),
"hollowknight": ("Hollow Knight", "from King's Pass to Hornet in Greenpath. Too big to share the calculator with "
"NumPlay: install it on its own"),
+ "undertale": ("Undertale", "the whole Underground, from the bed of golden flowers to the barrier and the neutral "
+ "ending: the Ruins, Snowdin, Waterfall, Hotland, the CORE and New Home. Too big to share the calculator "
+ "with NumPlay: install it on its own"),
"championisland": ("Champion Island", "the Doodle Champion Island Games. Too big to share the calculator with "
"NumPlay: install it on its own"),
"numpowder": ("NumPowder", "The Powder Toy on the calculator: sand, water, fire, acid, electricity and nearly 200 "