diff --git a/patches/ips/animated_map_tiles.ips b/patches/ips/animated_map_tiles.ips
new file mode 100644
index 000000000..9e34a4a72
Binary files /dev/null and b/patches/ips/animated_map_tiles.ips differ
diff --git a/patches/rom_map/Bank DF.txt b/patches/rom_map/Bank DF.txt
index 270a1b95f..58e330a70 100644
--- a/patches/rom_map/Bank DF.txt
+++ b/patches/rom_map/Bank DF.txt
@@ -2,7 +2,8 @@ D4DF - D761: ; credits.asm
D91B - DF0A: ; credits.asm
E000 - E127: ; credits.asm
E200 - E212: ; alternate_door_colors.asm
-E212 - FEEF: [free]
+E212 - E3C2: ; animated_map_tiles.asm
+E3C2 - FEF0: [free]
FEF0 - FEFF: seed name (null-terminated ASCII string)
FF00 - FF03: display seed / internal seed hash
FF04 - FF04: [free]
diff --git a/patches/rom_map/RAM.txt b/patches/rom_map/RAM.txt
index 43f496c4e..6a1ad7731 100644
--- a/patches/rom_map/RAM.txt
+++ b/patches/rom_map/RAM.txt
@@ -48,7 +48,8 @@ $7EF597-$7EF599: split_speed.asm
$7EF59A-$7EF59C: reserve_backward_fill.asm
$7EF59C-$7EF59E: crash_handle_xmode.asm
$7EF59E-$7EF5A0: trimmed_fanfare.asm
-$7EF5A0-$7EFE00: [FREE]
+$7EF5A0-$7EF5A6: animated_map_tiles.asm
+$7EF5A6-$7EFE00: [FREE]
$7EFE00: map area (copy of $1F5B to be saved to SRAM)
$7EFE02: map area explored mask
$7EFE04: copy of $1F5D (to be saved to SRAM for escape autosave)
diff --git a/patches/src/animated_map_tiles.asm b/patches/src/animated_map_tiles.asm
new file mode 100644
index 000000000..4883529e8
--- /dev/null
+++ b/patches/src/animated_map_tiles.asm
@@ -0,0 +1,239 @@
+;; Animated Maptiles for pause map double_item rooms
+;; nn_357
+
+lorom
+arch 65816
+
+!bank_df_free_space_start = $dfe212 ; this location must match fn write_double_item_table in map_tiles.rs
+!bank_df_free_space_end = $dfe3c2
+
+!interval = #$001B
+!current_area = $7e1f5b
+!animate_mask = $7ef5a0
+!tile_timer = $7ef5a2
+!tile_state = $7ef5a4
+
+;;; Vanilla hooks
+
+org $82913B ;;; $9120: Pause menu - index 0: map screen ;;;
+ jsl AnimateTile
+ nop
+ nop
+
+org $82941e ;;; ;;; $93C3: Load pause menu map tilemap and area label ;;;
+ jsl BuildAnimateBitMask
+ nop
+ nop
+ nop
+ nop
+
+
+;;; Custom code
+
+org !bank_df_free_space_start
+
+; the contents of this table is modified by fn write_double_item_table in map_tiles.rs
+;
+; +0 area index [table is generated in area order of the rooms]
+; +2 VRAM tilemap dest
+; +4 tile_a, the priority tile, revealed (palette 7). $FFFF if both items are the same type, so it never matches and tries to flip a tile to an identical tile
+; +6 tile_b, the other item’s tile, revealed (palette 7)
+; +8 byte offset of tile_b’s item in the collected-items array at $7ED870
+; +10 bit mask for tile_b’s item within that byte
+; +12 tile_a for visited tiles (palette 2). $FFFF if same type
+; +14 tile_b for visited tiles (palette 2)
+
+; Padding: unused entries are $FFFF, $0000, $FFFF, $0000, $0000, $0000, $FFFF, $0000.
+
+DoubleItemTable:
+ dw $0000, $0000, $0000, $0000, $0000, $0000, $0000, $0000 ; Billy Mays
+ dw $0000, $0000, $0000, $0000, $0000, $0000, $0000, $0000 ; West Sand Pit
+ dw $0000, $0000, $0000, $0000, $0000, $0000, $0000, $0000 ; Watering Hole
+ dw $0000, $0000, $0000, $0000, $0000, $0000, $0000, $0000 ; Green Pirates Shaft
+ dw $0000, $0000, $0000, $0000, $0000, $0000, $0000, $0000 ; Green Brinstar Reserve
+ dw $0000, $0000, $0000, $0000, $0000, $0000, $0000, $0000 ; Norfair Reserve
+ dw $0000, $0000, $0000, $0000, $0000, $0000, $0000, $0000 ; Bomb Torizo (when it's an objective)
+DoubleItemTableEnd:
+
+; Mask layout: !animate_mask uses bit n for “animate entry n” and bit n+8 for “entry n is visited” (VisitedRowMasks).
+
+RowBitMasks:
+ dw $0001, $0002, $0004, $0008, $0010, $0020, $0040
+
+VisitedRowMasks:
+ dw $0101, $0202, $0404, $0808, $1010, $2020, $4040
+
+BuildAnimateBitMask:
+
+ phb
+ php
+ rep #$30
+ pha
+ phx
+ phy
+ phk
+ plb
+
+ lda #$0015
+ sta !tile_timer
+ lda #$0000
+ sta !tile_state
+ sta !animate_mask
+
+ ldy #$0000
+
+.loop
+ lda.w DoubleItemTable,y
+ cmp !current_area
+ bne .next_entry
+
+ lda.w DoubleItemTable+8,y
+ tax
+ lda $7ed870,x
+ and.w DoubleItemTable+10,y
+ bne .next_entry
+
+ lda.w DoubleItemTable+2,y
+ sec
+ sbc #$3000
+ asl
+ tax
+ lda $7e4000,x
+ cmp.w DoubleItemTable+4,y
+ beq .revealed
+ cmp.w DoubleItemTable+12,y
+ bne .next_entry
+
+ tya
+ lsr
+ lsr
+ lsr
+ tax
+ lda.w VisitedRowMasks,x
+ bra .set_mask
+
+.revealed
+ tya
+ lsr
+ lsr
+ lsr
+ tax
+ lda.w RowBitMasks,x
+
+.set_mask
+ ora !animate_mask
+ sta !animate_mask
+
+.next_entry
+ tya
+ clc
+ adc #$0010
+ tay
+ cpy #DoubleItemTableEnd-DoubleItemTable
+ bne .loop
+
+ ply
+ plx
+ pla
+ plp
+ plb
+ cmp #$0007 ;
+ bmi .skip ; hijacked code. (dont' think this does anything in MapRando anyway, it's for Debug options but restore it regardless.)
+ lda #$0000 ;
+.skip
+ rtl
+
+
+AnimateTile:
+ php
+ rep #$30
+ lda !tile_timer
+ inc
+ cmp !interval
+ bcs .run
+
+ sta !tile_timer
+ plp
+ lda #$0000
+ sta $0763
+ rtl
+
+.run
+ phx
+ phy
+ phb
+ phk
+ plb
+
+
+ lda #$0000
+ sta !tile_timer
+ lda !tile_state
+ eor #$0002 ; byte offset of tile_a / tile_b
+ sta !tile_state
+
+ lda !animate_mask
+ beq .done
+ pha ; running copy of the mask, shifted once per entry
+
+ ldy #$0000 ; table offset
+ lda.l $7e0330
+ tax
+
+.loop
+ lda $01,s
+ lsr
+ sta $01,s
+ bcc .next ; bit not set
+
+ lda DoubleItemTable,y
+ cmp !current_area
+ bne .next
+
+ lda #$0002
+ sta $7e00d0,x ; size
+ lda $01,s
+ and #$0080
+ lsr
+ lsr
+ lsr
+ lsr
+ clc
+ adc !tile_state
+ sta $7e00d2,x
+ tya
+ clc
+ adc #DoubleItemTable+4
+ adc $7e00d2,x
+ sta $7e00d2,x ; source = tile_a or tile_b, revealed or visited
+ lda #$00df
+ sta $7e00d4,x ; bank (dest write below overwrites the stray high byte)
+ lda DoubleItemTable+2,y
+ sta $7e00d5,x ; VRAM dest
+ txa
+ clc
+ adc #$0007
+ tax
+
+.next
+ tya
+ clc
+ adc #$0010
+ tay
+ cpy #DoubleItemTableEnd-DoubleItemTable
+ bne .loop
+
+ txa
+ sta.l $7e0330
+ pla
+
+.done
+ plb
+ ply
+ plx
+ plp
+ lda #$0000
+ sta $0763
+ rtl
+
+assert pc() <= !bank_df_free_space_end
\ No newline at end of file
diff --git a/rust/data/presets/full-settings/Community Race Season 5.json b/rust/data/presets/full-settings/Community Race Season 5.json
index 76e75fad1..4498a50a9 100644
--- a/rust/data/presets/full-settings/Community Race Season 5.json
+++ b/rust/data/presets/full-settings/Community Race Season 5.json
@@ -4755,6 +4755,7 @@
"room_outline_revealed": true,
"opposite_area_revealed": true,
"hazard_markers": true,
+ "animated_tiles": false,
"mother_brain_fight": "Short",
"supers_double": true,
"escape_autosave": true,
diff --git a/rust/data/presets/full-settings/Default.json b/rust/data/presets/full-settings/Default.json
index 17dbbb6d2..72b1db81e 100644
--- a/rust/data/presets/full-settings/Default.json
+++ b/rust/data/presets/full-settings/Default.json
@@ -4667,6 +4667,7 @@
"room_outline_revealed": true,
"opposite_area_revealed": false,
"hazard_markers": true,
+ "animated_tiles": false,
"mother_brain_fight": "Short",
"supers_double": true,
"escape_autosave": true,
diff --git a/rust/data/presets/full-settings/Mentor Tournament.json b/rust/data/presets/full-settings/Mentor Tournament.json
index b2434bf56..1c910df41 100644
--- a/rust/data/presets/full-settings/Mentor Tournament.json
+++ b/rust/data/presets/full-settings/Mentor Tournament.json
@@ -4755,6 +4755,7 @@
"room_outline_revealed": true,
"opposite_area_revealed": true,
"hazard_markers": true,
+ "animated_tiles": false,
"mother_brain_fight": "Short",
"supers_double": true,
"escape_autosave": true,
diff --git a/rust/data/presets/full-settings/Summer Series Expert Challenge.json b/rust/data/presets/full-settings/Summer Series Expert Challenge.json
index 354a7ec3d..23bd298f7 100644
--- a/rust/data/presets/full-settings/Summer Series Expert Challenge.json
+++ b/rust/data/presets/full-settings/Summer Series Expert Challenge.json
@@ -4667,6 +4667,7 @@
"room_outline_revealed": true,
"opposite_area_revealed": true,
"hazard_markers": true,
+ "animated_tiles": false,
"mother_brain_fight": "Short",
"supers_double": true,
"escape_autosave": true,
diff --git a/rust/data/presets/quality-of-life/Default.json b/rust/data/presets/quality-of-life/Default.json
index f67c790f2..0be61a02a 100644
--- a/rust/data/presets/quality-of-life/Default.json
+++ b/rust/data/presets/quality-of-life/Default.json
@@ -48,6 +48,7 @@
"room_outline_revealed": true,
"opposite_area_revealed": false,
"hazard_markers": true,
+ "animated_tiles": false,
"mother_brain_fight": "Short",
"supers_double": true,
"escape_autosave": true,
diff --git a/rust/data/presets/quality-of-life/High.json b/rust/data/presets/quality-of-life/High.json
index cb08afbde..29c8d8b22 100644
--- a/rust/data/presets/quality-of-life/High.json
+++ b/rust/data/presets/quality-of-life/High.json
@@ -48,6 +48,7 @@
"room_outline_revealed": true,
"opposite_area_revealed": true,
"hazard_markers": true,
+ "animated_tiles": true,
"mother_brain_fight": "Short",
"supers_double": true,
"escape_autosave": true,
diff --git a/rust/data/presets/quality-of-life/Low.json b/rust/data/presets/quality-of-life/Low.json
index 6cd0fca4d..4b9b7227f 100644
--- a/rust/data/presets/quality-of-life/Low.json
+++ b/rust/data/presets/quality-of-life/Low.json
@@ -48,6 +48,7 @@
"room_outline_revealed": false,
"opposite_area_revealed": false,
"hazard_markers": true,
+ "animated_tiles": false,
"mother_brain_fight": "Short",
"supers_double": true,
"escape_autosave": true,
diff --git a/rust/data/presets/quality-of-life/Max.json b/rust/data/presets/quality-of-life/Max.json
index c2f1c8802..85c54343f 100644
--- a/rust/data/presets/quality-of-life/Max.json
+++ b/rust/data/presets/quality-of-life/Max.json
@@ -48,6 +48,7 @@
"room_outline_revealed": true,
"opposite_area_revealed": true,
"hazard_markers": true,
+ "animated_tiles": true,
"mother_brain_fight": "Skip",
"supers_double": true,
"escape_autosave": true,
diff --git a/rust/data/presets/quality-of-life/Off.json b/rust/data/presets/quality-of-life/Off.json
index b3e42e4dd..85876e374 100644
--- a/rust/data/presets/quality-of-life/Off.json
+++ b/rust/data/presets/quality-of-life/Off.json
@@ -48,6 +48,7 @@
"room_outline_revealed": false,
"opposite_area_revealed": false,
"hazard_markers": false,
+ "animated_tiles": false,
"mother_brain_fight": "Vanilla",
"supers_double": false,
"escape_autosave": false,
diff --git a/rust/maprando-web/templates/generate/help/quality/animated_tiles.html b/rust/maprando-web/templates/generate/help/quality/animated_tiles.html
new file mode 100644
index 000000000..79a911aae
--- /dev/null
+++ b/rust/maprando-web/templates/generate/help/quality/animated_tiles.html
@@ -0,0 +1,25 @@
+
+
+
+
+
Animated map tiles
+
+
+
+
If enabled, the pause map displays a tile containing two items by alternating between the two tile shapes; it also applies when a tile contains both an item and an objective marker. The minimap is unaffected. This setting affects the following rooms:
+
+
Billy Mays Room
+
Brinstar Reserve Tank Room
+
Green Pirates Shaft
+
Norfair Reserve Tank Room
+
Watering Hole
+
West Sand Pit
+
Bomb Torizo Room (if it is an objective)
+
+
+
+
+
+
\ No newline at end of file
diff --git a/rust/maprando-web/templates/generate/quality_of_life.html b/rust/maprando-web/templates/generate/quality_of_life.html
index 12946a159..cf0cf5b1d 100644
--- a/rust/maprando-web/templates/generate/quality_of_life.html
+++ b/rust/maprando-web/templates/generate/quality_of_life.html
@@ -180,6 +180,24 @@