diff --git a/CHANGELOG.md b/CHANGELOG.md index ac3a457..cf5ba61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,28 @@ The native shim's ABI is tracked separately by `b2Version()` (currently `4`). ### Added +- **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 little composed + scene built from the already-loaded atlases (zero new asset files). Behind the title + text sits the level's **real Kenney biome backdrop** (L1-L5; the dark cavern/keep + keep their tint) under a dimming scrim, and a **cast row of real sprites** - the + chosen hero + a biome-representative foe + a coin + the goal flag - stands on a + ground band, previewing what's in that world. Implementation notes: + - 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; the + 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 a failed slice just + drops that layer, and the always-present opaque `pfCardShade` tint base still + guarantees the cover hides the build. Worst case is "needs tuning", never a broken + cover. + - The whole stack (tint base + art layers + title text, tracked in `gCardArt`) fades + out together; each layer carries its resting blend in `uCardBaseBlend` so the + scrim's dimming holds through the ramp instead of flashing opaque. + Biome backdrops/foes, the cast layout, scrim/ground tints, and the ground line are + first-pass and tunable in OXT. Example-side only (no Kit touch). Static gates + + audit clean. **Needs an OXT pass** to confirm the slice-clone art renders/positions + on the engine and to tune the composition. - **Platformer: TRANSITION CARD + boot TITLE screen + recomposed WIN card (the polish pass's headline - `docs/platformer-polish-plan.md` §2 / §2.4).** The single biggest "demo-not-game" tell is gone: a full-screen opaque overlay now COVERS diff --git a/docs/platformer-polish-plan.md b/docs/platformer-polish-plan.md index 257e595..d38276f 100644 --- a/docs/platformer-polish-plan.md +++ b/docs/platformer-polish-plan.md @@ -63,6 +63,13 @@ well, not adding more. > off the in-level binding), and the win screen is recomposed in the card language. > Static gates + audit clean; example-side only (no Kit touch). Tints/flavours/timings > are first-pass tunables. See the CHANGELOG entry for the full landing notes. +> +> **Follow-up (same branch):** the level cards are now **illustrated** rather than a +> solid tint — each shows the level's real biome backdrop (L1-L5) under a scrim plus a +> cast row of real sprites (hero + foe + coin + flag) on a ground band, all cloned from +> the loaded atlases (no new assets), every piece degrading to the tint base if a slice +> fails. (Real per-level screenshots were rejected: the levels are ~6,400px scrolling +> worlds, so a flat grab is unrepresentative and would mean shipping/maintaining PNGs.) **The problem.** `pfStartGame` (the per-level world build) runs its **teardown before the `lock screen`** (it deletes the previous level's controls at diff --git a/examples/box2dxt-platformer.livecodescript b/examples/box2dxt-platformer.livecodescript index cbe69ad..d9c62aa 100644 --- a/examples/box2dxt-platformer.livecodescript +++ b/examples/box2dxt-platformer.livecodescript @@ -405,8 +405,10 @@ local gShowDebug -- COVERS each level teardown/rebuild (no visible construction). gAtTitle is -- true while the title is up (SPACE starts); gCardFade is the fade-out ramp -- (0 = opaque ... 100 = clear); gCardGen invalidates a stale fade send when a --- newer cover supersedes it; gTitleHeroSpr is the title's live hero preview. -local gAtTitle, gCardFade, gCardGen, gTitleHeroSpr +-- newer cover supersedes it; gTitleHeroSpr is the title's live hero preview; +-- gCardArt is the CR-list of per-level illustrated-card art controls (long ids) +-- that fade/raise/hide as a set alongside the tint base + title text. +local gAtTitle, gCardFade, gCardGen, gTitleHeroSpr, gCardArt local gGate, gGateSprA, gGateSprB, gPlateHoldMS, gPlateLook, gCamOK local gIntroPan, gRunStart, gFalls, gWinLock, gFlagHint, gWinSecs local gOuches -- Wave 2: contact hits (knockbacks) - falls stay falls @@ -798,13 +800,63 @@ function pfHeroName pSkin return toUpper(char 1 of pSkin) & (char 2 to -1 of pSkin) end pfHeroName --- The composed text for a LEVEL card (shown while that level builds behind it). +-- The composed text for a LEVEL card (shown in the upper band while that level +-- builds behind it; the illustrated cast row sits below it - see pfCardArtBuildLevel). function pfLevelCardText local tMsg - 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 + put "LEVEL " & gLevel & " of 7" & cr & cr & pfLevelTitle(gLevel) & cr & cr & pfLevelFlavour(gLevel) & cr & cr & "Collect every coin, then reach the flag" into tMsg return tMsg end pfLevelCardText +-- The biome BACKDROP frame for a level's card (the same Kenney bg art the level +-- itself uses). L6/L7 build their backdrops procedurally - no frame - so the +-- card falls back to its solid biome tint there. +function pfLevelBgFrame pN + switch pN + case 1 + case 2 + return "background_color_hills" + case 3 + return "background_fade_hills" + case 4 + return "background_color_mushrooms" + case 5 + return "background_color_desert" + default + return "" + end switch +end pfLevelBgFrame + +-- A representative FOE frame per biome for the card's cast row (all on the +-- always-loaded "foes" sheet - no optional-sheet dependency). A missing frame +-- just drops that one cast slot (pfCardCastMember no-ops). +function pfLevelFoeFrame pN + switch pN + case 2 + return "bee_a" + case 3 + return "snail_walk_a" + case 5 + return "ladybug_walk_a" + case 6 + return "mouse_walk_a" + case 7 + return "saw_a" + default + return "slime_normal_walk_a" + end switch +end pfLevelFoeFrame + +-- Darken an "r,g,b" toward black by pFactor (0..1) - for the card's ground band. +function pfShade pRGB, pFactor + local tR, tG, tB + set the itemDelimiter to comma + put round((item 1 of pRGB) * pFactor) into tR + put round((item 2 of pRGB) * pFactor) into tG + put round((item 3 of pRGB) * pFactor) into tB + return tR & comma & tG & comma & tB +end pfShade + -- The composed text for the boot TITLE card. The hero chooser highlights the -- current pick; the live hero sprite (pfTitleHero) floats over the blank band -- between the title and the chooser. @@ -825,12 +877,158 @@ function pfTitleCardText return tMsg end pfTitleCardText --- COVER the screen with the card NOW: compose its colour + text, raise it (and --- its text) above every other control, make it opaque, and force ONE repaint so --- it is on screen BEFORE the teardown/build that follows paints underneath it. --- pLight true = light text (for the dark tints). +-- ===== ILLUSTRATED-CARD ART (the per-level scene behind the title text) ===== +-- The biome backdrop, a dimming scrim, a ground band, and a CAST ROW of real +-- game sprites (hero + a biome foe + a coin + the goal flag). All card-level +-- controls named pfCard* (so pfWipeStage leaves them) and tracked in gCardArt so +-- they fade/raise/hide as a set. EVERY piece is optional - on a missing sheet or +-- a failed slice it is simply skipped, and the always-present opaque pfCardShade +-- tint base guarantees the cover still hides the build. ===== + +-- Remember an art control so the fade/raise/hide handlers see it. +command pfCardAddArt pCtrl + if pCtrl is empty then exit pfCardAddArt + put pCtrl & cr after gCardArt +end pfCardAddArt + +-- Drop every per-level art control and reset the list. Called before each cover, +-- and at the title. Deletes BY NAME (a proper type-checked existence test); the +-- names are fixed, so this clears every art control whether or not gCardArt is in +-- sync, and avoids referencing a possibly-stale long id. +command pfCardArtClear + local tC + set the itemDelimiter to comma + repeat for each item tC in "pfCardBgA,pfCardBgB,pfCardScrim,pfCardGround,pfCardCastHero,pfCardCastFoe,pfCardCastCoin,pfCardCastFlag" + if there is an image tC then delete image tC + if there is a graphic tC then delete graphic tC + end repeat + put empty into gCardArt +end pfCardArtClear + +-- Clone a sliced atlas frame onto the CARD as a plain (camera-free) image named +-- pName, hidden and at native sliced size. Uses the proven throwaway-sprite path +-- (pfTextureSlab): the sprite carries the frame as its icon, which we clone. The +-- throwaway is created in whatever camera group is live, then removed; the clone +-- is a card control. Returns pName, or empty on any failure. +function pfCardArtImage pSheet, pFrame, pName + local tSpr, tImg + if gAssetsOK is not true then return empty + if not b2kSheetHasFrame(pSheet, pFrame) then return empty + b2kSpriteNew pSheet, pFrame, -4000, -4000 + put the result into tSpr + if tSpr is empty then return empty + put the icon of tSpr into tImg + if tImg is empty or there is no image id tImg then + b2kSpriteRemove tSpr + return empty + end if + if there is an image pName then delete image pName + clone image id tImg + set the name of the last image to pName + set the visible of the last image to false + -- NB: lockLoc is set by the CALLER, AFTER it sets the rect - an image only + -- scales its content to a custom rect when the rect is set while lockLoc is + -- still false, then locked (the order pfTextureSlab uses). Locking first here + -- left the backdrop stuck at its natural ~640px instead of filling the card. + b2kSpriteRemove tSpr + return pName +end pfCardArtImage + +-- One cast-row figure: clone pFrame, scale the clone to target height pH (aspect +-- kept), stand it centred at x pCX with its feet on pFeetY, and track it. No +-- b2kSheetScale juggling - the clone is resized directly, so the game's sheet +-- scales are never touched. +command pfCardCastMember pSheet, pFrame, pName, pCX, pFeetY, pH + local tNW, tNH, tW + if pfCardArtImage(pSheet, pFrame, pName) is empty then exit pfCardCastMember + put the width of image pName into tNW + put the height of image pName into tNH + if tNH < 1 then + delete image pName + exit pfCardCastMember + end if + put max(1, round(tNW * pH / tNH)) into tW + set the rect of image pName to pCX - (tW div 2), pFeetY - pH, pCX + (tW div 2), pFeetY + set the lockLoc of image pName to true -- freeze the scaled size (rect set first, see pfCardArtImage) + set the uCardBaseBlend of image pName to 0 + pfCardAddArt the long id of image pName +end pfCardCastMember + +-- Build the illustrated scene for gLevel into gCardArt (hidden; pfCardShow shows +-- it under the cover lock). The uCardBaseBlend property holds each layer's RESTING +-- blendLevel so the fade ramp can add progress on top without flattening the +-- scrim/ground translucency. +command pfCardArtBuildLevel + local tBg, tGY, tBW, tBH, tBgH, tBgTop + if gAssetsOK is not true then exit pfCardArtBuildLevel -- placeholder mode: tint card only + put 480 into tGY -- the ground line the cast stands on + -- the biome backdrop (L1-L5): ONE panel scaled to COVER the 1024 card width with + -- its aspect KEPT (the overflow below the 640 card height is simply cropped by + -- the window). A single image has no join, so there is no seam to misalign - the + -- earlier two-panel stretch mismatched in its overlap. The card's ground band + -- hides the cropped-off foreground. + put pfLevelBgFrame(gLevel) into tBg + if tBg is not empty and b2kSheetHasFrame("bg", tBg) then + if pfCardArtImage("bg", tBg, "pfCardBgA") is not empty then + put the width of image "pfCardBgA" into tBW -- natural sliced size (~640x640 at bg scale 2.5) + put the height of image "pfCardBgA" into tBH + if tBW < 1 then put 640 into tBW + if tBH < 1 then put 640 into tBH + put max(640, round(1024 * tBH / tBW)) into tBgH -- aspect-scaled to fill the 1024 width (>= card height) + put (640 - tBgH) div 2 into tBgTop -- centre the vertical crop (show the focal middle, not just the sky) + set the rect of image "pfCardBgA" to 0, tBgTop, 1024, tBgTop + tBgH + set the lockLoc of image "pfCardBgA" to true -- freeze the scaled size (rect set first, see pfCardArtImage) + set the uCardBaseBlend of image "pfCardBgA" to 0 + pfCardAddArt the long id of image "pfCardBgA" + -- a dark scrim so the title text reads over the busy backdrop (rests at 42) + create graphic "pfCardScrim" + set the style of graphic "pfCardScrim" to "rectangle" + set the rect of graphic "pfCardScrim" to 0, 0, 1024, 640 + set the filled of graphic "pfCardScrim" to true + set the backgroundColor of graphic "pfCardScrim" to "14,16,26" + set the blendLevel of graphic "pfCardScrim" to 42 + set the uCardBaseBlend of graphic "pfCardScrim" to 42 + set the visible of graphic "pfCardScrim" to false -- hidden until pfCardShow reveals the whole cover at once + pfCardAddArt the long id of graphic "pfCardScrim" + end if + end if + -- a ground band the cast stands on (a darker shade of the biome tint) + create graphic "pfCardGround" + set the style of graphic "pfCardGround" to "rectangle" + set the rect of graphic "pfCardGround" to 0, tGY, 1024, 640 + set the filled of graphic "pfCardGround" to true + set the backgroundColor of graphic "pfCardGround" to pfShade(pfLevelTint(gLevel), 0.45) + set the blendLevel of graphic "pfCardGround" to 6 + set the uCardBaseBlend of graphic "pfCardGround" to 6 + set the visible of graphic "pfCardGround" to false -- hidden until pfCardShow reveals the whole cover at once + pfCardAddArt the long id of graphic "pfCardGround" + -- THE CAST: real game art previewing this world (each figure optional) + pfCardCastMember "chars", ("character_" & gHeroSkin & "_idle"), "pfCardCastHero", 336, tGY + 8, 96 + pfCardCastMember "foes", pfLevelFoeFrame(gLevel), "pfCardCastFoe", 492, tGY + 8, 64 + pfCardCastMember "tiles", "coin_gold", "pfCardCastCoin", 606, tGY + 4, 40 + pfCardCastMember "tiles", "flag_yellow_a", "pfCardCastFlag", 706, tGY + 8, 96 +end pfCardArtBuildLevel + +-- Raise the whole card stack to the very top, in z-order: tint base, then the +-- art (in build order: backdrop, scrim, ground, cast), then the title text on +-- top. Each "set the layer ... to tTop" lands at the top, pushing the previous +-- one down, so the LAST raised (the text) ends up frontmost. +command pfCardRaise + local tTop, tC + put the number of controls of this card into tTop + if there is a graphic "pfCardShade" then set the layer of graphic "pfCardShade" to tTop + repeat for each line tC in gCardArt + if tC is not empty then set the layer of tC to tTop + end repeat + if there is a field "pfCardText" then set the layer of field "pfCardText" to tTop +end pfCardRaise + +-- COVER the screen with the card NOW: compose its colour + text, raise the whole +-- stack above every other control, make it opaque, and force ONE repaint so it is +-- on screen BEFORE the teardown/build that follows paints underneath it. pLight +-- true = light text (for the dark tints). command pfCardShow pText, pTint, pLight - local tTop + local tC add 1 to gCardGen -- invalidate any in-flight fade ramp if there is no graphic "pfCardShade" then exit pfCardShow set the backgroundColor of graphic "pfCardShade" to pTint @@ -844,18 +1042,23 @@ command pfCardShow pText, pTint, pLight set the textColor of field "pfCardText" to "28,30,42" end if end if - put the number of controls of this card into tTop - set the layer of graphic "pfCardShade" to tTop - if there is a field "pfCardText" then set the layer of field "pfCardText" to tTop + pfCardRaise lock screen set the visible of graphic "pfCardShade" to true + repeat for each line tC in gCardArt + if tC is not empty then set the visible of tC to true + end repeat if there is a field "pfCardText" then set the visible of field "pfCardText" to true unlock screen -- flush: the cover is on screen now end pfCardShow -- Cover for the CURRENT level (called at the very top of pfStartGame, before the --- teardown) - the biome card for gLevel. +-- teardown): build the illustrated biome scene, then cover with it. The title +-- text sits in the upper band, the cast row below it. command pfCardCover + pfCardArtClear + pfCardArtBuildLevel + if there is a field "pfCardText" then set the rect of field "pfCardText" to 40, 70, 984, 410 pfCardShow pfLevelCardText(), pfLevelTint(gLevel), pfLevelLight(gLevel) end pfCardCover @@ -868,20 +1071,30 @@ command pfCardReveal send ("pfCardFadeStep" && gCardGen) to me in 620 milliseconds end pfCardReveal --- One step of the fade-out ramp (0 = opaque ... 100 = clear). pGen guards --- against a stale ramp left over when a newer cover superseded this one. +-- One step of the fade-out ramp (0 = opaque ... 100 = clear), across the tint +-- base, the title text, and every illustrated-art layer. Each layer fades from +-- its RESTING blend (uCardBaseBlend: 0 for opaque pieces, 42/6 for scrim/ground) up to +-- 100, so the scrim's dimming holds through the fade instead of flashing opaque. +-- pGen guards against a stale ramp left when a newer cover superseded this one. command pfCardFadeStep pGen + local tC if pGen is not gCardGen then exit pfCardFadeStep if there is no graphic "pfCardShade" then exit pfCardFadeStep add 12 to gCardFade if gCardFade >= 100 then set the visible of graphic "pfCardShade" to false if there is a field "pfCardText" then set the visible of field "pfCardText" to false + repeat for each line tC in gCardArt + if tC is not empty then set the visible of tC to false + end repeat set the blendLevel of graphic "pfCardShade" to 0 -- reset for the next cover exit pfCardFadeStep end if set the blendLevel of graphic "pfCardShade" to gCardFade if there is a field "pfCardText" then set the blendLevel of field "pfCardText" to gCardFade + repeat for each line tC in gCardArt + if tC is not empty then set the blendLevel of tC to min(100, (the uCardBaseBlend of tC) + gCardFade) + end repeat send ("pfCardFadeStep" && gCardGen) to me in 45 milliseconds end pfCardFadeStep @@ -897,6 +1110,8 @@ command pfShowTitle -- load the atlases ONCE (prompts for the folder the first time, then cached + -- persisted into the game); skip the reload once the chars sheet is present if not b2kSheetHasFrame("chars", "character_beige_idle") then put pfLoadSheets() into gAssetsOK + pfCardArtClear -- the title has no level scene (just the hero preview) + if there is a field "pfCardText" then set the rect of field "pfCardText" to 32, 40, 992, 600 pfCardShow pfTitleCardText(), "26,30,44", true pfTitleHero -- the live hero preview, above the card end pfShowTitle @@ -5128,13 +5343,11 @@ command pfChromeFront repeat for each item tC in "pfbtn_pause,pfbtn_reset,pfbtn_again,pfbtn_level" if there is a button tC then set the layer of button tC to tN end repeat - -- the transition card sits ABOVE all other chrome while it is covering: the - -- build created controls over it, so re-raise it here (last) to keep it on top - -- until the reveal fade hides it. Skipped when hidden (e.g. on the win screen). - if there is a graphic "pfCardShade" and the visible of graphic "pfCardShade" is true then - set the layer of graphic "pfCardShade" to tN - if there is a field "pfCardText" then set the layer of field "pfCardText" to tN - end if + -- the transition card (tint base + illustrated art + title text) sits ABOVE all + -- other chrome while it is covering: the build created controls over it, so + -- re-raise the whole stack here to keep it on top until the reveal fade hides + -- it. Skipped when hidden (e.g. on the win screen). + if there is a graphic "pfCardShade" and the visible of graphic "pfCardShade" is true then pfCardRaise end pfChromeFront command pfWipeStage