DramaticShapeVoxelMod archival mirror · read-only
a140980b1d97c292dbbc5cb1bfe486329d7d255f

seal transparent back sprites

DramaticShape · 2026-08-01 · parent eb231d221e

seal transparent back sprites

Changed files

CHANGELOG.md                  |  30 +++++++++++++
 lib/BattlePics.lua            | 131 ++++++++++++++++++++++++++++++++++++++++++-----------
 lib/OverworldBattle.lua       |  33 +++++++++++++-
 tests/dramatic_shape_test.lua | 152 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
 4 files changed, 310 insertions(+), 36 deletions(-)

Diff

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 172bfd4..21ab6e1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -216,6 +216,36 @@
   depth16 as the floor every GLES3 device can read. Refused all four, the
   reflections are lost and nothing else, exactly as before.
 
+- **Under BACK SPRITES some of your own Pokemon were see-through -- Pikachu,
+  Seel, Dewgong, Chansey, Jigglypuff -- with the arena showing through the
+  middle of them.** Those back pics are drawn as OUTLINES: everything inside
+  the ink is the lightest shade, the decoder keys that shade to nothing, and
+  on hardware it did not matter because the field behind them was white too.
+
+  BattlePics already put that paper back by flooding the background inward and
+  filling whatever it could not reach, and along the bottom of a figure it told
+  a narrow opening (a belly the drawing ran out of, sealed) from a wide one (a
+  stride, left open for the world to show through). Right for a mon standing
+  on the map -- but the pinned back pic is not on the map, it is on the text
+  box with its feet on row 96, and there is white box under its lowest row
+  rather than arena. Every one of those mons leaks out through an opening far
+  too wide to read as a drain, so the flood walked straight up inside them.
+
+  A pic on the box is now told so, and its bottom edge seals: nothing reaches
+  it from below at any width, and the rule stops being a heuristic -- paper is
+  whatever the background cannot walk to from the left, the right or the top.
+  Twelve of the game's 151 back pics turn on this; the other 139 come back
+  byte-identical, and no front pic is touched at all.
+
+  **And a hole is filled with the pic's own paper rather than with white.**
+  Shade 0 is only white while the pic is still grays, and pics arrive here
+  after the bake -- a species SGB colour, a BGP fade mid-animation, PAL_BLACK
+  across the whole screen while the blackout text is up. A hardcoded white
+  belly would have been the one lit thing on a blacked-out mon. The lightest
+  shade still standing in the pic is that colour, and every one of the game's
+  battler pics keeps at least one such pixel -- an eye, a highlight down a
+  cheek -- so what goes back is the baked shade itself.
+
 ### Known
 
 - Screen-space reflections can only reflect what is in the frame. A tree just
diff --git a/lib/BattlePics.lua b/lib/BattlePics.lua
index d74ec24..bca2b53 100644
--- a/lib/BattlePics.lua
+++ b/lib/BattlePics.lua
@@ -57,6 +57,26 @@
 -- and they come back untouched because that is what their own shape says, not
 -- because they were special-cased.
 --
+-- The drain/mouth cut is for a pic STANDING ON THE MAP, where a mouth is a
+-- real hole with real ground behind it. A pic PINNED TO THE MENU has no such
+-- hole to be: under BACK SPRITES the player's mon is drawn in the GB's own
+-- slot with its feet flush on the text box (BattleState.backPlacement pins
+-- row 96), so the only thing under its lowest row is white box. Nothing can
+-- reach it from below, whatever the opening's width, and the caller says so
+-- by asking for a SEALED BOTTOM -- for which the rule stops being a heuristic
+-- and becomes exact: paper is whatever the background cannot walk to from the
+-- left, the right or the top.
+--
+-- That is the difference between a Pikachu that reads as a mon and one that
+-- reads as wireframe. The pale-bodied back pics -- Pikachu, Seel, Dewgong,
+-- Chansey, Jigglypuff -- are drawn as OUTLINES: everything inside the ink is
+-- shade 0 and every one of them is keyed away, so the figure is a rim with the
+-- arena showing through it. Each one also has a wide opening along its bottom,
+-- which the drain cut correctly reads as a mouth and the sealed bottom
+-- correctly does not. Twelve of this game's 151 back pics turn on it; the
+-- other 139 come back byte-identical either way, because they had nothing
+-- under them the flood was getting in through.
+--
 -- The silhouette is untouched, so the mon still cuts cleanly against the
 -- world; only its insides stop being see-through.
 --
@@ -71,14 +91,23 @@ local V = ...
 
 local BattlePics = {}
 
--- Cached by the image the engine handed over. Weak keys, so a pic that goes
--- out of scope takes its filled twin with it rather than pinning a texture
--- for the session.
-local cache = setmetatable({}, { __mode = "k" })
+-- Cached by the image the engine handed over, one table per bottom rule --
+-- the same pic answers differently sealed and unsealed, and a single table
+-- would hand the wrong twin back to whichever caller asked second. Weak keys,
+-- so a pic that goes out of scope takes its filled twin with it rather than
+-- pinning a texture for the session.
+local function newCache()
+  return {
+    [false] = setmetatable({}, { __mode = "k" }),
+    [true]  = setmetatable({}, { __mode = "k" }),
+  }
+end
+local cache = newCache()
 
--- What an enclosed hole is filled with. White, because white is what the
--- battle field was: this restores the pixel the artist drew and the engine
--- then keyed away, it does not invent a new one.
+-- What an enclosed hole is filled with when the pic itself offers nothing
+-- better. White, because white is what the battle field was: this restores the
+-- pixel the artist drew and the engine then keyed away, it does not invent a
+-- new one.
 BattlePics.FILL = { 1, 1, 1, 1 }
 
 -- Anything at or under this alpha counts as keyed-out rather than drawn.
@@ -154,6 +183,42 @@ local function inkBounds(data, w, h)
   return x0, y0, x1, y1
 end
 
+-- The colour the keyed-away shade would have had: the LIGHTEST colour still
+-- standing in the pic.
+--
+-- Pure white is only the right answer while the pic is still grays, and by the
+-- time it reaches here it usually is not. picImage hands a pic over AFTER the
+-- bake -- a species SGB colour, a BGP fade mid-animation, PAL_BLACK for the
+-- whole screen while the blackout text is up -- and shade 0 travels with the
+-- rest. A white belly inside a blacked-out mon would be the one lit thing on a
+-- dark screen; inside a warm-palette mon it would be a cold patch the artist
+-- never drew.
+--
+-- So the paper is read off the pic rather than assumed, which needs shade 0 to
+-- have survived somewhere in it. It always has: every one of this game's 151
+-- back pics keeps at least one opaque shade-0 pixel -- a highlight down a
+-- cheek, the white of an eye -- because only the shade-0 pixels the decoder
+-- could reach were keyed. So what comes back is the baked shade 0 itself, not
+-- an approximation of it, and it tracks every palette the engine picks without
+-- being told which one that was.
+--
+-- Ranked by channel sum, which orders four DMG shades exactly: a palette maps
+-- all three channels monotonically, so lightest by sum is lightest full stop.
+local function paperColor(data, x0, y0, x1, y1)
+  local best, pr, pg, pb = -1, nil, nil, nil
+  for y = y0, y1 do
+    for x = x0, x1 do
+      local r, g, b, a = data:getPixel(x, y)
+      if a > CUT then
+        local lum = r + g + b
+        if lum > best then best, pr, pg, pb = lum, r, g, b end
+      end
+    end
+  end
+  if best < 0 then return nil end
+  return pr, pg, pb
+end
+
 -- The widest opening along the bottom of a figure that still counts as a drain
 -- rather than a mouth. See the header for the measurements either side of it.
 BattlePics.DRAIN = 6
@@ -161,7 +226,9 @@ BattlePics.DRAIN = 6
 -- Mark every transparent pixel the BACKGROUND can reach, flooding inward from
 -- the edges of the artwork's box: the left, the right and the top whole, and
 -- along the bottom only those openings wide enough to be background rather
--- than the underside of a figure the drawing ran out of.
+-- than the underside of a figure the drawing ran out of -- or none of them at
+-- all, for a pic whose feet are on the text box and which therefore has
+-- nothing behind its lowest row to let in.
 --
 -- Confined to the box as well as seeded from it, so the empty frame under a
 -- short pic cannot walk around a sealed drain and come back up through it.
@@ -169,7 +236,7 @@ BattlePics.DRAIN = 6
 -- An explicit stack rather than recursion: a 56x56 pic is three thousand
 -- pixels and a keyed-out background is most of them, which is a deeper call
 -- chain than is worth risking for no gain.
-local function markOutside(data, w, h, x0, y0, x1, y1)
+local function markOutside(data, w, h, x0, y0, x1, y1, sealBottom)
   local outside = {}
   local stack, top = {}, 0
   local function clear(x, y)
@@ -191,17 +258,21 @@ local function markOutside(data, w, h, x0, y0, x1, y1)
     push(x1, y)
   end
   -- the bottom, run by run: a wide one is the gap between two legs and lets
-  -- the world through, a narrow one is where a belly ran out and is sealed
-  local x = x0
-  while x <= x1 do
-    if clear(x, y1) then
-      local from = x
-      while x <= x1 and clear(x, y1) do x = x + 1 end
-      if (x - from) > BattlePics.DRAIN then
-        for k = from, x - 1 do push(k, y1) end
+  -- the world through, a narrow one is where a belly ran out and is sealed.
+  -- Skipped whole for a pic on the box, where even the widest of them has
+  -- white paper behind it rather than arena.
+  if not sealBottom then
+    local x = x0
+    while x <= x1 do
+      if clear(x, y1) then
+        local from = x
+        while x <= x1 and clear(x, y1) do x = x + 1 end
+        if (x - from) > BattlePics.DRAIN then
+          for k = from, x - 1 do push(k, y1) end
+        end
+      else
+        x = x + 1
       end
-    else
-      x = x + 1
     end
   end
   while top > 0 do
@@ -219,9 +290,15 @@ end
 -- The pic with its enclosed holes filled, or the pic itself when that could
 -- not be done (no pixel access, a driver that refused the readback). Never
 -- nil for a non-nil argument: a caller must always have something to draw.
-function BattlePics.filled(img)
+--
+-- sealBottom for a pic pinned to the text box rather than standing on the map:
+-- see the header. A caller that does not say defaults to the map, which is
+-- where all but one of this mod's pics are.
+function BattlePics.filled(img, sealBottom)
   if not img then return img end
-  local hit = cache[img]
+  sealBottom = sealBottom and true or false
+  local slot = cache[sealBottom]
+  local hit = slot[img]
   if hit ~= nil then return hit or img end
 
   local made = nil
@@ -231,8 +308,12 @@ function BattlePics.filled(img)
     local w, h = data:getDimensions()
     local x0, y0, x1, y1 = inkBounds(data, w, h)
     if not x0 then return end          -- a pic with nothing drawn in it
-    local outside = markOutside(data, w, h, x0, y0, x1, y1)
+    local outside = markOutside(data, w, h, x0, y0, x1, y1, sealBottom)
     local fill = BattlePics.FILL
+    local pr, pg, pb = paperColor(data, x0, y0, x1, y1)
+    local fr = pr or fill[1]
+    local fg = pg or fill[2]
+    local fb = pb or fill[3]
     local changed = false
     -- only inside the box: everything beyond it is frame the artist never
     -- reached, and filling that would put the mon in a white rectangle
@@ -242,7 +323,7 @@ function BattlePics.filled(img)
         if not outside[row + x] then
           local _, _, _, a = data:getPixel(x, y)
           if a <= CUT then
-            data:setPixel(x, y, fill[1], fill[2], fill[3], fill[4])
+            data:setPixel(x, y, fr, fg, fb, fill[4])
             changed = true
           end
         end
@@ -255,12 +336,12 @@ function BattlePics.filled(img)
     made = out
   end)
 
-  cache[img] = (ok and made) or false
+  slot[img] = (ok and made) or false
   return made or img
 end
 
 function BattlePics.invalidate()
-  cache = setmetatable({}, { __mode = "k" })
+  cache = newCache()
 end
 
 return BattlePics
diff --git a/lib/OverworldBattle.lua b/lib/OverworldBattle.lua
index dddf97d..78fdfff 100644
--- a/lib/OverworldBattle.lua
+++ b/lib/OverworldBattle.lua
@@ -104,6 +104,31 @@ function OverworldBattle.backPinned()
   return OverworldBattle.backSetting:get() and true or false
 end
 
+-- Whether a pic is the one drawn in the GB's own slot with its feet on the
+-- text box, rather than geometry standing out on the map.
+--
+-- Exactly the player's side under BACK SPRITES -- its mon, or the trainer back
+-- that holds the slot until "Go!" -- because that is the only pic this mod
+-- ever leaves flat (see drawPicsLayer below). The foe is a billboard on its
+-- tile whichever mode is on, and with the mode off the player's side is one
+-- too, so both of those keep the open bottom that lets the arena through a
+-- stride. What the answer buys is in BattlePics: a pic on the box has nothing
+-- behind its lowest row, so its bottom edge seals.
+-- Read by TRUTHINESS rather than against nil, because sideTexture blanks the
+-- side it is not rendering by setting the field to FALSE (see OFF) and holds
+-- it that way for the whole render -- during which the pic layer runs, and
+-- picImage asks this. A nil test passes a `false` straight through to the
+-- index below, and the error comes out of sideTexture into the pcall that
+-- calls it: the foe's billboard is dropped for the frame and the Pokemon
+-- simply is not there.
+function OverworldBattle.pinnedPic(battle, img)
+  if not (battle and img) then return false end
+  if not OverworldBattle.backPinned() then return false end
+  if img == battle.playerBackPic then return true end
+  local player = battle.player
+  return (player and img == player.sprite) and true or false
+end
+
 -- ------- both mons face you
 --
 -- Standing on a map, seen from in front, a Pokemon showing you its BACK is
@@ -831,11 +856,17 @@ function OverworldBattle.install()
   -- behind it. There is a world back there now, so they are filled here
   -- instead -- see BattlePics, which puts the paper back without touching
   -- the silhouette.
+  --
+  -- The pinned pic is told that its feet are on the box, which is what lets
+  -- the pale-bodied back sprites be filled at all: their bellies leak out
+  -- through an opening too wide to read as a drain, and only the box under
+  -- them settles that it is not a hole. Passed the pre-bake image, because
+  -- that is the one the battle holds a reference to.
   local innerPic = BattleState.picImage
   function BattleState:picImage(img)
     local out = innerPic(self, img)
     if not OverworldBattle.shot() then return out end
-    return BattlePics.filled(out)
+    return BattlePics.filled(out, OverworldBattle.pinnedPic(self, img))
   end
 
   -- While a billboard texture is being rendered both pics are put in the same
diff --git a/tests/dramatic_shape_test.lua b/tests/dramatic_shape_test.lua
index 6bdfb73..70b37fc 100644
--- a/tests/dramatic_shape_test.lua
+++ b/tests/dramatic_shape_test.lua
@@ -2476,7 +2476,35 @@ T.check(onAt["DRAMATIC_SHAPE:battleBack"], "switched back on, so is the row")
 T.eq(onAt["DRAMATIC_SHAPE:battleBack"] - onAt["DRAMATIC_SHAPE:battles"], 1,
   "directly under the row it belongs to")
 
+-- ------- and which pic is the pinned one is asked with the other side BLANKED
+--
+-- picImage asks this so BattlePics knows whether a pic's feet are on the text
+-- box -- where its bottom edge seals, and its belly stops being see-through --
+-- and it is asked DURING the billboard render, inside which sideTexture has
+-- switched the side it is not drawing off by setting the field to FALSE rather
+-- than to nil (see OFF).
+--
+-- So the read has to be by truthiness. A test against nil passes that `false`
+-- through to the index below it, the error comes back out of sideTexture into
+-- the pcall that calls it, textures() reports no card for the side -- and the
+-- foe is simply not on the field. Which is the whole bug: fixing the player's
+-- back pic took the enemy's billboard out.
+local mine, theirs = {}, {}
+local live = { player = { sprite = mine }, enemy = { sprite = theirs } }
+T.eq(Battles.pinnedPic(live, mine), true,
+  "the player's own mon is the pic on the box")
+T.eq(Battles.pinnedPic(live, theirs), false,
+  "and the foe is geometry out on the map, whatever the mode")
+T.eq(Battles.pinnedPic({ player = false, enemy = { sprite = theirs } }, theirs),
+  false, "asking about the foe while the player is blanked answers, not throws")
+T.eq(Battles.pinnedPic({ playerBackPic = mine }, mine), true,
+  "the trainer back holds the slot until Go!, on the box like the mon")
+T.eq(Battles.pinnedPic({ player = false, playerBackPic = false }, mine), false,
+  "and with the side blanked outright nothing of it is pinned")
+
 Battles.backSetting:setIndex(1, backGame)          -- and off for the rows below
+T.eq(Battles.pinnedPic(live, mine), false,
+  "with BACK SPRITES off the player's mon is out on the map with the foe")
 end
 
 -- ------- the hour reaches the FLAT world too
@@ -2611,21 +2639,35 @@ local BattlePics = run.loader.exports.DRAMATIC_SHAPE.lib.require("BattlePics")
 -- reader over what came out. The pic is faked at the readback seam, which is
 -- the only thing between this and the pixels the engine would have blitted.
 local lastCanvas = nil                  -- what the readback asked newCanvas for
-local function fill(rows)
+-- '#' is ink and '.' the keyed-out nothing. 'W' is ink too, of the pic's
+-- LIGHTEST shade -- a highlight the decoder happened not to key -- which is
+-- what the paper a hole gets filled with is read off.
+local SHADE = { ["#"] = 0.25, ["W"] = 0.75 }
+-- reuse hands the SAME pic back through, which is how the two bottom rules
+-- can be asked of one image the way a running battle would ask them
+local function fill(rows, sealBottom, reuse)
   local W, H = #rows[1], #rows
   local built = nil
   local function fakeData()
-    local px = {}
+    local px, sh = {}, {}
     for y = 0, H - 1 do
       for x = 0, W - 1 do
-        px[y * W + x] = rows[y + 1]:sub(x + 1, x + 1) == "#" and 1 or 0
+        local shade = SHADE[rows[y + 1]:sub(x + 1, x + 1)]
+        px[y * W + x] = shade and 1 or 0
+        sh[y * W + x] = shade or 0
       end
     end
     return {
-      px = px,
+      px = px, sh = sh,
       getDimensions = function() return W, H end,
-      getPixel = function(self, x, y) return 0, 0, 0, self.px[y * W + x] end,
-      setPixel = function(self, x, y, r, g, b, a) self.px[y * W + x] = a end,
+      getPixel = function(self, x, y)
+        local k = y * W + x
+        return self.sh[k], self.sh[k], self.sh[k], self.px[k]
+      end,
+      setPixel = function(self, x, y, r, g, b, a)
+        self.px[y * W + x] = a
+        self.sh[y * W + x] = r
+      end,
     }
   end
 
@@ -2639,12 +2681,14 @@ local function fill(rows)
     built = data
     return { setFilter = function() end }
   end
-  local pic = { getDimensions = function() return W, H end }
-  local out = BattlePics.filled(pic)
+  local pic = reuse or { getDimensions = function() return W, H end }
+  local out = BattlePics.filled(pic, sealBottom)
   love.graphics.newCanvas, love.graphics.newImage = realNewCanvas, realNewImage
   -- deliberately NOT invalidated: each figure brings its own pic, and the
   -- cache check at the bottom needs one of them still in there
-  return out, pic, built and function(x, y) return built.px[y * W + x] > 0.5 end
+  return out, pic,
+         built and function(x, y) return built.px[y * W + x] > 0.5 end,
+         built and function(x, y) return built.sh[y * W + x] end
 end
 
 -- ------- the cut at the feet, which is what the closed bottom edge is for
@@ -2727,6 +2771,75 @@ local drainOut, drainPic, drain = fill({
 T.check(drainOut ~= drainPic and drain and drain(8, 4),
   "a narrow one is where the drawing ran out, and is paper")
 
+-- ------- but a pic ON THE MENU has no mouth at all
+--
+-- The drain/mouth cut is for a pic standing on the MAP, where a wide opening
+-- along the bottom is a stride with real ground behind it. Under BACK SPRITES
+-- the player's mon is drawn in the GB's own slot with its feet flush on the
+-- text box, and the only thing under its lowest row is white box -- so nothing
+-- reaches it from below, whatever the opening's width, and the rule stops
+-- being a heuristic: paper is whatever the background cannot walk to from the
+-- left, the right or the top.
+--
+-- Which is the difference between a Pikachu and a wireframe. The pale-bodied
+-- back pics -- Pikachu, Seel, Dewgong, Chansey, Jigglypuff -- are drawn as
+-- OUTLINES, every shade-0 pixel inside the ink keyed away, and each of them
+-- leaks out through a bottom opening far too wide to read as a drain. On the
+-- map that reading is right; on the box it left the mon a rim with the arena
+-- showing through it.
+--
+-- The same stride figure the map rule leaves open, now standing on the box.
+local boxOut, boxPic, boxOpaque = fill({
+  "..##############..",
+  "..##############..",
+  "..##############..",
+  "..###........###..",
+  "..###........###..",
+  "..###........###..",
+}, true)
+T.check(boxOut ~= boxPic and boxOpaque,
+  "the gap a stride would have shown the world through is paper on the box")
+T.check(boxOpaque(8, 3) and boxOpaque(8, 5),
+  "and it fills right down to the row the feet are on")
+
+-- the seal is the BOTTOM alone: the sides and the top still let the background
+-- in, which is what keeps the silhouette cutting against the arena instead of
+-- standing the mon in a white block
+local boxGapOut, boxGapPic = fill({
+  "..#####.",
+  "..#...#.",
+  "..#...#.",
+  "....###.",   -- opens at the left, and drains out that way
+  "..#####.",
+  "..#####.",
+}, true)
+T.eq(boxGapOut, boxGapPic,
+  "a pocket that drains out to the side is background on the box too")
+
+-- ------- and a hole is filled with the pic's OWN paper, not with white
+--
+-- Shade 0 is white only while the pic is still grays, and by the time one
+-- reaches here it usually is not: picImage hands it over after the bake -- a
+-- species SGB colour, a BGP fade mid-animation, PAL_BLACK across the whole
+-- screen while the blackout text is up -- and shade 0 travels with the rest.
+-- A hardcoded white belly would be the one lit thing on a blacked-out mon.
+--
+-- So the paper is read off the pic: the lightest shade still standing in it,
+-- which is shade 0 wherever the decoder could not reach one. It never has to
+-- guess -- all 151 of this game's back pics keep at least one, an eye or a
+-- highlight down a cheek.
+local _, _, _, paperShade = fill({
+  "..####..",
+  "..#WW#..",   -- a highlight the decoder did not key: this is the paper
+  "..#..#..",
+  "..#..#..",
+  "..####..",
+})
+T.eq(paperShade(3, 2), paperShade(3, 1),
+  "the hole takes the lightest shade the pic still has")
+T.check(paperShade(3, 2) ~= paperShade(2, 2),
+  "which is not the ink beside it")
+
 -- ------- and the readback is measured in PIXELS, which is what kept the mons
 -- the size of the squares they stand on
 --
@@ -2743,12 +2856,31 @@ T.check(drainOut ~= drainPic and drain and drain(8, 4),
 -- scale that was wrong everywhere.
 T.check(lastCanvas and lastCanvas.opts and lastCanvas.opts.dpiscale == 1,
   "the readback canvas is one texel per pic pixel, on a highdpi phone too")
-T.eq(lastCanvas.w, 18, "and it is the size of the pic, in those pixels")
+T.eq(lastCanvas.w, 8, "and it is the size of the pic, in those pixels")
 
 -- the answer is cached on the image, so a pic costs one readback a session
 -- rather than one a frame -- checked on the first figure, which is still in
 -- there because fill() does not clear it
 T.eq(BattlePics.filled(pic), out, "the rebuilt pic is cached on the original")
+-- and cached PER BOTTOM RULE, because one image answers differently on the map
+-- and on the box. A single table for both would hand whichever caller asked
+-- second the other one's answer -- the map's stencil to the menu, or a menu
+-- fill to a mon standing on grass -- which is this section's bug arriving
+-- through the cache rather than through the flood.
+--
+-- The stride figure again, on the SAME pic the map rule already answered for.
+local reOut = fill({
+  "..##############..",
+  "..##############..",
+  "..##############..",
+  "..###........###..",
+  "..###........###..",
+  "..###........###..",
+}, true, stridePic)
+T.check(reOut ~= stridePic,
+  "the pic the map left open still fills when the box asks for it")
+T.eq(BattlePics.filled(stridePic), stridePic,
+  "and the map's own answer for it is still the pic itself")
 BattlePics.invalidate()
 end