Skip to content

Commit f8ac20d

Browse files
committed
platformer: illustrated biome cards (real game art on the transition card)
The level cards are no longer a solid tint + sparse text. Each is now a composed scene drawn from the already-loaded atlases (zero new asset files): the level's real Kenney biome backdrop (L1-L5; the dark cavern/keep keep their tint) under a dimming scrim, plus a cast row of real sprites - the chosen hero + a biome foe + a coin + the goal flag - on a ground band. - Art is cloned onto the card via the proven pfTextureSlab throwaway-sprite path (pfCardArtImage), so it is camera-free chrome regardless of the live camera; clones are resized directly (no b2kSheetScale juggling, so the game's sheet scales are never touched). - Every piece is independently optional - a missing sheet or failed slice just drops that layer, and the always-present opaque pfCardShade tint base still guarantees the cover hides the build (worst case: needs tuning, never a broken cover). - The whole stack (tint base + art + title text, tracked in gCardArt) fades out together; each layer carries its resting blend in uCardBaseBlend so the scrim dimming holds through the ramp instead of flashing opaque. Real per-level screenshots were rejected: levels are ~6,400px scrolling worlds (a flat grab is unrepresentative) and it would mean shipping PNGs. Example-side only (no Kit touch). Static gates + audit clean. Needs an OXT pass to confirm the slice-clone art renders and to tune the composition. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HkN9P6YodA65ZuhDDuuQks
1 parent deafc00 commit f8ac20d

3 files changed

Lines changed: 254 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,28 @@ The native shim's ABI is tracked separately by `b2Version()` (currently `4`).
1010

1111
### Added
1212

13+
- **Platformer: ILLUSTRATED biome cards (real game art on the transition card).** The
14+
level cards are no longer a solid tint + sparse text: each is now a little composed
15+
scene built from the already-loaded atlases (zero new asset files). Behind the title
16+
text sits the level's **real Kenney biome backdrop** (L1-L5; the dark cavern/keep
17+
keep their tint) under a dimming scrim, and a **cast row of real sprites** - the
18+
chosen hero + a biome-representative foe + a coin + the goal flag - stands on a
19+
ground band, previewing what's in that world. Implementation notes:
20+
- Art is cloned onto the CARD via the proven `pfTextureSlab` throwaway-sprite path
21+
(`pfCardArtImage`), so it is camera-free chrome regardless of the live camera; the
22+
clones are resized directly (no `b2kSheetScale` juggling, so the game's sheet
23+
scales are never touched).
24+
- Every piece is **independently optional** - a missing sheet or a failed slice just
25+
drops that layer, and the always-present opaque `pfCardShade` tint base still
26+
guarantees the cover hides the build. Worst case is "needs tuning", never a broken
27+
cover.
28+
- The whole stack (tint base + art layers + title text, tracked in `gCardArt`) fades
29+
out together; each layer carries its resting blend in `uCardBaseBlend` so the
30+
scrim's dimming holds through the ramp instead of flashing opaque.
31+
Biome backdrops/foes, the cast layout, scrim/ground tints, and the ground line are
32+
first-pass and tunable in OXT. Example-side only (no Kit touch). Static gates +
33+
audit clean. **Needs an OXT pass** to confirm the slice-clone art renders/positions
34+
on the engine and to tune the composition.
1335
- **Platformer: TRANSITION CARD + boot TITLE screen + recomposed WIN card (the
1436
polish pass's headline - `docs/platformer-polish-plan.md` §2 / §2.4).** The single
1537
biggest "demo-not-game" tell is gone: a full-screen opaque overlay now COVERS

docs/platformer-polish-plan.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,13 @@ well, not adding more.
6363
> off the in-level binding), and the win screen is recomposed in the card language.
6464
> Static gates + audit clean; example-side only (no Kit touch). Tints/flavours/timings
6565
> are first-pass tunables. See the CHANGELOG entry for the full landing notes.
66+
>
67+
> **Follow-up (same branch):** the level cards are now **illustrated** rather than a
68+
> solid tint — each shows the level's real biome backdrop (L1-L5) under a scrim plus a
69+
> cast row of real sprites (hero + foe + coin + flag) on a ground band, all cloned from
70+
> the loaded atlases (no new assets), every piece degrading to the tint base if a slice
71+
> fails. (Real per-level screenshots were rejected: the levels are ~6,400px scrolling
72+
> worlds, so a flat grab is unrepresentative and would mean shipping/maintaining PNGs.)
6673
6774
**The problem.** `pfStartGame` (the per-level world build) runs its **teardown
6875
before the `lock screen`** (it deletes the previous level's controls at

examples/box2dxt-platformer.livecodescript

Lines changed: 225 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -405,8 +405,10 @@ local gShowDebug
405405
-- COVERS each level teardown/rebuild (no visible construction). gAtTitle is
406406
-- true while the title is up (SPACE starts); gCardFade is the fade-out ramp
407407
-- (0 = opaque ... 100 = clear); gCardGen invalidates a stale fade send when a
408-
-- newer cover supersedes it; gTitleHeroSpr is the title's live hero preview.
409-
local gAtTitle, gCardFade, gCardGen, gTitleHeroSpr
408+
-- newer cover supersedes it; gTitleHeroSpr is the title's live hero preview;
409+
-- gCardArt is the CR-list of per-level illustrated-card art controls (long ids)
410+
-- that fade/raise/hide as a set alongside the tint base + title text.
411+
local gAtTitle, gCardFade, gCardGen, gTitleHeroSpr, gCardArt
410412
local gGate, gGateSprA, gGateSprB, gPlateHoldMS, gPlateLook, gCamOK
411413
local gIntroPan, gRunStart, gFalls, gWinLock, gFlagHint, gWinSecs
412414
local gOuches -- Wave 2: contact hits (knockbacks) - falls stay falls
@@ -798,13 +800,63 @@ function pfHeroName pSkin
798800
return toUpper(char 1 of pSkin) & (char 2 to -1 of pSkin)
799801
end pfHeroName
800802

801-
-- The composed text for a LEVEL card (shown while that level builds behind it).
803+
-- The composed text for a LEVEL card (shown in the upper band while that level
804+
-- builds behind it; the illustrated cast row sits below it - see pfCardArtBuildLevel).
802805
function pfLevelCardText
803806
local tMsg
804-
put "LEVEL " & gLevel & " / 7" & cr & cr & cr & pfLevelTitle(gLevel) & cr & cr & pfLevelFlavour(gLevel) & cr & cr & cr & cr & cr & "Arrows / WASD · SPACE jump · ESC controls" into tMsg
807+
put "LEVEL " & gLevel & " of 7" & cr & cr & pfLevelTitle(gLevel) & cr & cr & pfLevelFlavour(gLevel) & cr & cr & "Collect every coin, then reach the flag" into tMsg
805808
return tMsg
806809
end pfLevelCardText
807810

811+
-- The biome BACKDROP frame for a level's card (the same Kenney bg art the level
812+
-- itself uses). L6/L7 build their backdrops procedurally - no frame - so the
813+
-- card falls back to its solid biome tint there.
814+
function pfLevelBgFrame pN
815+
switch pN
816+
case 1
817+
case 2
818+
return "background_color_hills"
819+
case 3
820+
return "background_fade_hills"
821+
case 4
822+
return "background_color_mushrooms"
823+
case 5
824+
return "background_color_desert"
825+
default
826+
return ""
827+
end switch
828+
end pfLevelBgFrame
829+
830+
-- A representative FOE frame per biome for the card's cast row (all on the
831+
-- always-loaded "foes" sheet - no optional-sheet dependency). A missing frame
832+
-- just drops that one cast slot (pfCardCastMember no-ops).
833+
function pfLevelFoeFrame pN
834+
switch pN
835+
case 2
836+
return "bee_a"
837+
case 3
838+
return "snail_walk_a"
839+
case 5
840+
return "ladybug_walk_a"
841+
case 6
842+
return "mouse_walk_a"
843+
case 7
844+
return "saw_a"
845+
default
846+
return "slime_normal_walk_a"
847+
end switch
848+
end pfLevelFoeFrame
849+
850+
-- Darken an "r,g,b" toward black by pFactor (0..1) - for the card's ground band.
851+
function pfShade pRGB, pFactor
852+
local tR, tG, tB
853+
set the itemDelimiter to comma
854+
put round((item 1 of pRGB) * pFactor) into tR
855+
put round((item 2 of pRGB) * pFactor) into tG
856+
put round((item 3 of pRGB) * pFactor) into tB
857+
return tR & comma & tG & comma & tB
858+
end pfShade
859+
808860
-- The composed text for the boot TITLE card. The hero chooser highlights the
809861
-- current pick; the live hero sprite (pfTitleHero) floats over the blank band
810862
-- between the title and the chooser.
@@ -825,12 +877,148 @@ function pfTitleCardText
825877
return tMsg
826878
end pfTitleCardText
827879

828-
-- COVER the screen with the card NOW: compose its colour + text, raise it (and
829-
-- its text) above every other control, make it opaque, and force ONE repaint so
830-
-- it is on screen BEFORE the teardown/build that follows paints underneath it.
831-
-- pLight true = light text (for the dark tints).
880+
-- ===== ILLUSTRATED-CARD ART (the per-level scene behind the title text) =====
881+
-- The biome backdrop, a dimming scrim, a ground band, and a CAST ROW of real
882+
-- game sprites (hero + a biome foe + a coin + the goal flag). All card-level
883+
-- controls named pfCard* (so pfWipeStage leaves them) and tracked in gCardArt so
884+
-- they fade/raise/hide as a set. EVERY piece is optional - on a missing sheet or
885+
-- a failed slice it is simply skipped, and the always-present opaque pfCardShade
886+
-- tint base guarantees the cover still hides the build. =====
887+
888+
-- Remember an art control so the fade/raise/hide handlers see it.
889+
command pfCardAddArt pCtrl
890+
if pCtrl is empty then exit pfCardAddArt
891+
put pCtrl & cr after gCardArt
892+
end pfCardAddArt
893+
894+
-- Drop every per-level art control and reset the list. Called before each cover,
895+
-- and at the title. Deletes BY NAME (a proper type-checked existence test); the
896+
-- names are fixed, so this clears every art control whether or not gCardArt is in
897+
-- sync, and avoids referencing a possibly-stale long id.
898+
command pfCardArtClear
899+
local tC
900+
set the itemDelimiter to comma
901+
repeat for each item tC in "pfCardBgA,pfCardBgB,pfCardScrim,pfCardGround,pfCardCastHero,pfCardCastFoe,pfCardCastCoin,pfCardCastFlag"
902+
if there is an image tC then delete image tC
903+
if there is a graphic tC then delete graphic tC
904+
end repeat
905+
put empty into gCardArt
906+
end pfCardArtClear
907+
908+
-- Clone a sliced atlas frame onto the CARD as a plain (camera-free) image named
909+
-- pName, hidden and at native sliced size. Uses the proven throwaway-sprite path
910+
-- (pfTextureSlab): the sprite carries the frame as its icon, which we clone. The
911+
-- throwaway is created in whatever camera group is live, then removed; the clone
912+
-- is a card control. Returns pName, or empty on any failure.
913+
function pfCardArtImage pSheet, pFrame, pName
914+
local tSpr, tImg
915+
if gAssetsOK is not true then return empty
916+
if not b2kSheetHasFrame(pSheet, pFrame) then return empty
917+
b2kSpriteNew pSheet, pFrame, -4000, -4000
918+
put the result into tSpr
919+
if tSpr is empty then return empty
920+
put the icon of tSpr into tImg
921+
if tImg is empty or there is no image id tImg then
922+
b2kSpriteRemove tSpr
923+
return empty
924+
end if
925+
if there is an image pName then delete image pName
926+
clone image id tImg
927+
set the name of the last image to pName
928+
set the lockLoc of the last image to true
929+
set the visible of the last image to false
930+
b2kSpriteRemove tSpr
931+
return pName
932+
end pfCardArtImage
933+
934+
-- One cast-row figure: clone pFrame, scale the clone to target height pH (aspect
935+
-- kept), stand it centred at x pCX with its feet on pFeetY, and track it. No
936+
-- b2kSheetScale juggling - the clone is resized directly, so the game's sheet
937+
-- scales are never touched.
938+
command pfCardCastMember pSheet, pFrame, pName, pCX, pFeetY, pH
939+
local tNW, tNH, tW
940+
if pfCardArtImage(pSheet, pFrame, pName) is empty then exit pfCardCastMember
941+
put the width of image pName into tNW
942+
put the height of image pName into tNH
943+
if tNH < 1 then
944+
delete image pName
945+
exit pfCardCastMember
946+
end if
947+
put max(1, round(tNW * pH / tNH)) into tW
948+
set the rect of image pName to pCX - (tW div 2), pFeetY - pH, pCX + (tW div 2), pFeetY
949+
set the uCardBaseBlend of image pName to 0
950+
pfCardAddArt the long id of image pName
951+
end pfCardCastMember
952+
953+
-- Build the illustrated scene for gLevel into gCardArt (hidden; pfCardShow shows
954+
-- it under the cover lock). The uCardBaseBlend property holds each layer's RESTING
955+
-- blendLevel so the fade ramp can add progress on top without flattening the
956+
-- scrim/ground translucency.
957+
command pfCardArtBuildLevel
958+
local tBg, tGY
959+
if gAssetsOK is not true then exit pfCardArtBuildLevel -- placeholder mode: tint card only
960+
put 480 into tGY -- the ground line the cast stands on
961+
-- the biome backdrop (L1-L5), as two overlapping panels filling the 1024 width
962+
put pfLevelBgFrame(gLevel) into tBg
963+
if tBg is not empty and b2kSheetHasFrame("bg", tBg) then
964+
if pfCardArtImage("bg", tBg, "pfCardBgA") is not empty then
965+
set the rect of image "pfCardBgA" to 0, 0, 592, 640
966+
set the uCardBaseBlend of image "pfCardBgA" to 0
967+
pfCardAddArt the long id of image "pfCardBgA"
968+
end if
969+
if pfCardArtImage("bg", tBg, "pfCardBgB") is not empty then
970+
set the rect of image "pfCardBgB" to 432, 0, 1024, 640
971+
set the uCardBaseBlend of image "pfCardBgB" to 0
972+
pfCardAddArt the long id of image "pfCardBgB"
973+
end if
974+
-- a dark scrim so the title text reads over the busy backdrop (rests at 42)
975+
create graphic "pfCardScrim"
976+
set the style of graphic "pfCardScrim" to "rectangle"
977+
set the rect of graphic "pfCardScrim" to 0, 0, 1024, 640
978+
set the filled of graphic "pfCardScrim" to true
979+
set the backgroundColor of graphic "pfCardScrim" to "14,16,26"
980+
set the blendLevel of graphic "pfCardScrim" to 42
981+
set the uCardBaseBlend of graphic "pfCardScrim" to 42
982+
set the visible of graphic "pfCardScrim" to false -- hidden until pfCardShow reveals the whole cover at once
983+
pfCardAddArt the long id of graphic "pfCardScrim"
984+
end if
985+
-- a ground band the cast stands on (a darker shade of the biome tint)
986+
create graphic "pfCardGround"
987+
set the style of graphic "pfCardGround" to "rectangle"
988+
set the rect of graphic "pfCardGround" to 0, tGY, 1024, 640
989+
set the filled of graphic "pfCardGround" to true
990+
set the backgroundColor of graphic "pfCardGround" to pfShade(pfLevelTint(gLevel), 0.45)
991+
set the blendLevel of graphic "pfCardGround" to 6
992+
set the uCardBaseBlend of graphic "pfCardGround" to 6
993+
set the visible of graphic "pfCardGround" to false -- hidden until pfCardShow reveals the whole cover at once
994+
pfCardAddArt the long id of graphic "pfCardGround"
995+
-- THE CAST: real game art previewing this world (each figure optional)
996+
pfCardCastMember "chars", ("character_" & gHeroSkin & "_idle"), "pfCardCastHero", 336, tGY + 8, 96
997+
pfCardCastMember "foes", pfLevelFoeFrame(gLevel), "pfCardCastFoe", 492, tGY + 8, 64
998+
pfCardCastMember "tiles", "coin_gold", "pfCardCastCoin", 606, tGY + 4, 40
999+
pfCardCastMember "tiles", "flag_yellow_a", "pfCardCastFlag", 706, tGY + 8, 96
1000+
end pfCardArtBuildLevel
1001+
1002+
-- Raise the whole card stack to the very top, in z-order: tint base, then the
1003+
-- art (in build order: backdrop, scrim, ground, cast), then the title text on
1004+
-- top. Each "set the layer ... to tTop" lands at the top, pushing the previous
1005+
-- one down, so the LAST raised (the text) ends up frontmost.
1006+
command pfCardRaise
1007+
local tTop, tC
1008+
put the number of controls of this card into tTop
1009+
if there is a graphic "pfCardShade" then set the layer of graphic "pfCardShade" to tTop
1010+
repeat for each line tC in gCardArt
1011+
if tC is not empty then set the layer of tC to tTop
1012+
end repeat
1013+
if there is a field "pfCardText" then set the layer of field "pfCardText" to tTop
1014+
end pfCardRaise
1015+
1016+
-- COVER the screen with the card NOW: compose its colour + text, raise the whole
1017+
-- stack above every other control, make it opaque, and force ONE repaint so it is
1018+
-- on screen BEFORE the teardown/build that follows paints underneath it. pLight
1019+
-- true = light text (for the dark tints).
8321020
command pfCardShow pText, pTint, pLight
833-
local tTop
1021+
local tC
8341022
add 1 to gCardGen -- invalidate any in-flight fade ramp
8351023
if there is no graphic "pfCardShade" then exit pfCardShow
8361024
set the backgroundColor of graphic "pfCardShade" to pTint
@@ -844,18 +1032,23 @@ command pfCardShow pText, pTint, pLight
8441032
set the textColor of field "pfCardText" to "28,30,42"
8451033
end if
8461034
end if
847-
put the number of controls of this card into tTop
848-
set the layer of graphic "pfCardShade" to tTop
849-
if there is a field "pfCardText" then set the layer of field "pfCardText" to tTop
1035+
pfCardRaise
8501036
lock screen
8511037
set the visible of graphic "pfCardShade" to true
1038+
repeat for each line tC in gCardArt
1039+
if tC is not empty then set the visible of tC to true
1040+
end repeat
8521041
if there is a field "pfCardText" then set the visible of field "pfCardText" to true
8531042
unlock screen -- flush: the cover is on screen now
8541043
end pfCardShow
8551044

8561045
-- Cover for the CURRENT level (called at the very top of pfStartGame, before the
857-
-- teardown) - the biome card for gLevel.
1046+
-- teardown): build the illustrated biome scene, then cover with it. The title
1047+
-- text sits in the upper band, the cast row below it.
8581048
command pfCardCover
1049+
pfCardArtClear
1050+
pfCardArtBuildLevel
1051+
if there is a field "pfCardText" then set the rect of field "pfCardText" to 40, 70, 984, 410
8591052
pfCardShow pfLevelCardText(), pfLevelTint(gLevel), pfLevelLight(gLevel)
8601053
end pfCardCover
8611054

@@ -868,20 +1061,30 @@ command pfCardReveal
8681061
send ("pfCardFadeStep" && gCardGen) to me in 620 milliseconds
8691062
end pfCardReveal
8701063

871-
-- One step of the fade-out ramp (0 = opaque ... 100 = clear). pGen guards
872-
-- against a stale ramp left over when a newer cover superseded this one.
1064+
-- One step of the fade-out ramp (0 = opaque ... 100 = clear), across the tint
1065+
-- base, the title text, and every illustrated-art layer. Each layer fades from
1066+
-- its RESTING blend (uCardBaseBlend: 0 for opaque pieces, 42/6 for scrim/ground) up to
1067+
-- 100, so the scrim's dimming holds through the fade instead of flashing opaque.
1068+
-- pGen guards against a stale ramp left when a newer cover superseded this one.
8731069
command pfCardFadeStep pGen
1070+
local tC
8741071
if pGen is not gCardGen then exit pfCardFadeStep
8751072
if there is no graphic "pfCardShade" then exit pfCardFadeStep
8761073
add 12 to gCardFade
8771074
if gCardFade >= 100 then
8781075
set the visible of graphic "pfCardShade" to false
8791076
if there is a field "pfCardText" then set the visible of field "pfCardText" to false
1077+
repeat for each line tC in gCardArt
1078+
if tC is not empty then set the visible of tC to false
1079+
end repeat
8801080
set the blendLevel of graphic "pfCardShade" to 0 -- reset for the next cover
8811081
exit pfCardFadeStep
8821082
end if
8831083
set the blendLevel of graphic "pfCardShade" to gCardFade
8841084
if there is a field "pfCardText" then set the blendLevel of field "pfCardText" to gCardFade
1085+
repeat for each line tC in gCardArt
1086+
if tC is not empty then set the blendLevel of tC to min(100, (the uCardBaseBlend of tC) + gCardFade)
1087+
end repeat
8851088
send ("pfCardFadeStep" && gCardGen) to me in 45 milliseconds
8861089
end pfCardFadeStep
8871090

@@ -897,6 +1100,8 @@ command pfShowTitle
8971100
-- load the atlases ONCE (prompts for the folder the first time, then cached +
8981101
-- persisted into the game); skip the reload once the chars sheet is present
8991102
if not b2kSheetHasFrame("chars", "character_beige_idle") then put pfLoadSheets() into gAssetsOK
1103+
pfCardArtClear -- the title has no level scene (just the hero preview)
1104+
if there is a field "pfCardText" then set the rect of field "pfCardText" to 32, 40, 992, 600
9001105
pfCardShow pfTitleCardText(), "26,30,44", true
9011106
pfTitleHero -- the live hero preview, above the card
9021107
end pfShowTitle
@@ -5128,13 +5333,11 @@ command pfChromeFront
51285333
repeat for each item tC in "pfbtn_pause,pfbtn_reset,pfbtn_again,pfbtn_level"
51295334
if there is a button tC then set the layer of button tC to tN
51305335
end repeat
5131-
-- the transition card sits ABOVE all other chrome while it is covering: the
5132-
-- build created controls over it, so re-raise it here (last) to keep it on top
5133-
-- until the reveal fade hides it. Skipped when hidden (e.g. on the win screen).
5134-
if there is a graphic "pfCardShade" and the visible of graphic "pfCardShade" is true then
5135-
set the layer of graphic "pfCardShade" to tN
5136-
if there is a field "pfCardText" then set the layer of field "pfCardText" to tN
5137-
end if
5336+
-- the transition card (tint base + illustrated art + title text) sits ABOVE all
5337+
-- other chrome while it is covering: the build created controls over it, so
5338+
-- re-raise the whole stack here to keep it on top until the reveal fade hides
5339+
-- it. Skipped when hidden (e.g. on the win screen).
5340+
if there is a graphic "pfCardShade" and the visible of graphic "pfCardShade" is true then pfCardRaise
51385341
end pfChromeFront
51395342

51405343
command pfWipeStage

0 commit comments

Comments
 (0)