From fe5a11112f980881328c69797dfa1d0967e62622 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 22 Aug 2026 11:44:51 -0400 Subject: [PATCH] consolidate single color sprites into one master file --- CLAUDE.md | 8 +-- _helpers.lua | 35 ++++++++++--- art/animals/sad_frog.png | Bin 0 -> 503 bytes art/animals/sad_frog_b.png | Bin 308 -> 0 bytes art/animals/sad_frog_g.png | Bin 390 -> 0 bytes art/animals/sad_frog_r.png | Bin 310 -> 0 bytes art/animals/sad_frog_reduced.png | Bin 458 -> 0 bytes art/animals/smile_dog.png | Bin 0 -> 470 bytes art/animals/smile_dog_b.png | Bin 385 -> 0 bytes art/animals/smile_dog_g.png | Bin 385 -> 0 bytes art/animals/smile_dog_r.png | Bin 382 -> 0 bytes art/animals/smile_dog_reduced.png | Bin 422 -> 0 bytes door.lua | 4 +- level_editor/object_attributes.lua | 76 +++++++++++++++++------------ npc.lua | 18 +++---- switch.lua | 2 +- 16 files changed, 90 insertions(+), 53 deletions(-) create mode 100644 art/animals/sad_frog.png delete mode 100644 art/animals/sad_frog_b.png delete mode 100644 art/animals/sad_frog_g.png delete mode 100644 art/animals/sad_frog_r.png delete mode 100644 art/animals/sad_frog_reduced.png create mode 100644 art/animals/smile_dog.png delete mode 100644 art/animals/smile_dog_b.png delete mode 100644 art/animals/smile_dog_g.png delete mode 100644 art/animals/smile_dog_r.png delete mode 100644 art/animals/smile_dog_reduced.png diff --git a/CLAUDE.md b/CLAUDE.md index 0a15b14..606d390 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,10 +51,10 @@ The player is not one entity — it is **up to three simultaneous colored copies ## Art pipeline -Sprites are authored in full color, then split into per-channel `_r`/`_g`/`_b` PNGs so each color copy draws its own layer. The `art/*.sh` scripts drive this with ImageMagick `convert`: -- `reduce_colors.sh ` — remap to `art/palette`/`palette.png`. -- `extract.sh ` — split one image into R/G/B channel PNGs. -- `reduce_and_extract.sh` — both steps over every PNG in the cwd (run from inside an `art/` subfolder). +**One full-color master PNG per sprite.** The RGB channels are split at *load time*, not stored as separate files: `getAllAssets` (`level_editor/object_attributes.lua`) points all three of `sprites.{red,green,blue}` at the same master, and `Entity:draw(channel)` tints it through `gColor[channel]` — so `master × (1,0,0)` renders the red gun, and the additive `"screen"` blend recombines overlapping copies into the original color. Per-channel collision shapes come from `getChannelCells` (`_helpers.lua`), which inspects each channel of the master's ImageData. Adding a sprite (or a new sprite *state*) is now one file, not three. +- `getAllAssets` treats every master PNG in a non-`_`-prefixed content folder as a placeable color asset, except the names in its `NON_ASSET_SPRITES` set (the palette swatch and sprites owned by dedicated classes like `player_*`, `door_unlocked`, `switch_pressed`). +- The `_r`/`_g`/`_b` channel-splitting step is gone (older revisions had an `extract.sh`; the pre-split channel PNGs live in git history if ever needed). +- `reduce_colors.sh ` / `reduce_and_extract.sh` — optional palette remap to `art/palette.png` (authoring step only; produces a master, no split). ## Bundled libraries (`libs/`, do not edit) diff --git a/_helpers.lua b/_helpers.lua index 215454c..b5cc15f 100644 --- a/_helpers.lua +++ b/_helpers.lua @@ -160,11 +160,12 @@ function cellShapeFromBox(box) return cells end --- Which 16x16 cells of an image are non-empty, for multi-cell collision --- shapes. LÖVE 11 no longer exposes Image:getData(), so inspect ImageData --- directly instead of creating a GPU Image first. -function getCellsFromSprite(spritePath) - local img = love.image.newImageData(spritePath) +-- Which 16x16 cells of an ImageData are non-empty, for multi-cell collision +-- shapes. `channel` ("red"/"green"/"blue") restricts the test to one colour +-- gun: a single full-colour master now feeds all three copies, so each copy's +-- collision shape comes from its own channel of the master (a cell with only +-- red pixels blocks the red copy but not blue). nil = "any channel lit". +local function cellsFromImageData(img, channel) local w, h = img:getWidth(), img:getHeight() local box = {w = w / 16, h = h / 16} local boxCells = cellShapeFromBox(box) @@ -182,7 +183,12 @@ function getCellsFromSprite(spritePath) for i = startPixel.x, endPixel.x do for j = startPixel.y, endPixel.y do local r, g, b, a = img:getPixel(i, j) - if a > 0 and (r > 0 or g > 0 or b > 0) then + local lit + if channel == "red" then lit = r > 0 + elseif channel == "green" then lit = g > 0 + elseif channel == "blue" then lit = b > 0 + else lit = r > 0 or g > 0 or b > 0 end + if a > 0 and lit then empty = false end end @@ -204,3 +210,20 @@ function getCellsFromSprite(spritePath) return boxCells end + +-- LÖVE 11 no longer exposes Image:getData(), so inspect ImageData directly +-- instead of creating a GPU Image first. +function getCellsFromSprite(spritePath, channel) + return cellsFromImageData(love.image.newImageData(spritePath), channel) +end + +-- Per-channel collision cell shapes for one full-colour master, loaded once. +-- Returns { red = cells, green = cells, blue = cells }. +function getChannelCells(spritePath) + local img = love.image.newImageData(spritePath) + return { + red = cellsFromImageData(img, "red"), + green = cellsFromImageData(img, "green"), + blue = cellsFromImageData(img, "blue"), + } +end diff --git a/art/animals/sad_frog.png b/art/animals/sad_frog.png new file mode 100644 index 0000000000000000000000000000000000000000..a9773e263a4437d3978b67f6242d2b0857755f93 GIT binary patch literal 503 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnF3?v&v(vO2U$sR$z3=CCj3=9n|3=F@3LJcn% z7)lKo7+xhXFj&oCU=S~uvn$XBC?OQ!6XFV_8U8c;2LdpQk&*FL(9$9xn=#4T-G%i> z{!5UtoCO|{#X#BvjNMLV+W{HuC7!;n?5{Y)_*JFD5BGln3aOU3MwFx^mZVxG7o`Fz z1|tI_GhG7k4iZ$6}+7%MHsTYFzuao^MJ}Bb`L(*Ni`iQ zzKmZ~%NZ^%oYin;ZtS6>hs2KGIZRzqUHG%T`JGwWyf1C*rGj>U$(Ht9yY=+B8)6b4nb$4<|Bvy>19_Dr$7g_|jlt8^ K&t;ucLK6V@4yWk= literal 0 HcmV?d00001 diff --git a/art/animals/sad_frog_b.png b/art/animals/sad_frog_b.png deleted file mode 100644 index c1e4708a7932aa3a6890035483eca8c584f2febe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 308 zcmeAS@N?(olHy`uVBq!ia0vp^3Lwk~Bp9L@-6Me%OS+@4BLl<6e(pbstUx|vage(c z!@6@aFM%9|WRD45bDP46hOx7_4S6Fo+k-*%fF5lweBo zc6VW5yxS$b1jyqo@Q5r1(jH*!b~4)z$k6q4aSY+Oo}8e-eB+0qfx-Fsa|aGA-`^qg zcZC9@OGrbreIo diff --git a/art/animals/sad_frog_g.png b/art/animals/sad_frog_g.png deleted file mode 100644 index 970bb2b5f83d4a814d2d96bba116db203d442edf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 390 zcmeAS@N?(olHy`uVBq!ia0vp^3Lwk~Bp9L@-6Me%OS+@4BLl<6e(pbstUx|vage(c z!@6@aFM%9|WRD45bDP46hOx7_4S6Fo+k-*%fF5lweBo zc6VW5yxS$b1jyqo@Q5r1(jH*!b~4)z$f);paSY+Oo}A#oU?~u&fB(RN5C8u&vK{;W zkzHcOfBAd{q4)I$8~$(mdGNshU*F}~et!S`SK>|m`S^1Q|NZL)oBv8@O1Ay7|6?!l z>i_-P0|Ecv*E3G|`k#HykHZ1?1O795H2AO!+dHrXbU6O||DTQR;s5^<5;J}`#<2Zk z=gDAT_^`_=b74P!D$psaC9V-ADTyViR>?)FK#IZ0z|d6Jz*yJNIK;@<%D~df#8B73 m#LB?ndHv#DC>nC}Q!>*kach{SB+UlYz~JfX=d#Wzp$P!d`+zn8 diff --git a/art/animals/sad_frog_r.png b/art/animals/sad_frog_r.png deleted file mode 100644 index a826d0c5966d4b7e05f4c130ebd868dc6db30cec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 310 zcmeAS@N?(olHy`uVBq!ia0vp^3Lwk~Bp9L@-6Me%OS+@4BLl<6e(pbstUx|vage(c z!@6@aFM%9|WRD45bDP46hOx7_4S6Fo+k-*%fF5lweBo zc6VW5yxS$b1jyqo@Q5r1(jH*!b~4)z$k6w6aSY+Oo}8e-eB+0qfx-Fsa|aGA-`}CZ zTgIcq!}GE}OG4t+bNhsZgg^hABUUr$&)_&!%<0Yq)T&zI8c~vxSdwa$T$Bo=7>o=I zO?3^7bq$R}jEt=eEUioobq!3c3=E#vFW!ZsAvZrIGp!Q0hG|ODY(NbRp00i_>zopr E0M_eKGynhq diff --git a/art/animals/sad_frog_reduced.png b/art/animals/sad_frog_reduced.png deleted file mode 100644 index de2826e623be8128534a8fa90d08b161c1bca6c5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 458 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnF3?v&v(vJfvmUKs7M+SzC{oH>NS%G}U;vjb? zhIQv;UV>C6dj$D1FjT2AFf_Cmnmtfeasz1T&Uj4G91O3Z>YRGHI9Nyc(@JIYa$?BQav!=MOVN5&1 z`lyuCQo-AKQiLJP3)9|dHxH;BV)x)-9&wVdJN!dVSh=Efd6dPwZ}trPns z^Rg8WUpx4HV!D7{NcP9}%LiA_31v26zi=vIre2D!+l4>do8Ot0&HK`}UMgt!muzXz zwOdc0yCEj=k$K(n|Nj`DJdjs8a(o6TC{#;aBT7;dOH!?pi&B9UgOP!usjh*suAy;= wk&%_Lxs{2fu7Qb_fkB^c(km1Vx%nxXX_dG&{4cl_2Gqdd>FVdQ&MBb@0ED5NqyPW_ diff --git a/art/animals/smile_dog.png b/art/animals/smile_dog.png new file mode 100644 index 0000000000000000000000000000000000000000..581a806cc606530f1ee0fbc15808f5d718d69b55 GIT binary patch literal 470 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnF3?v&v(vO2U$sR$z3=CCj3=9n|3=F@3LJcn% z7)lKo7+xhXFj&oCU=S~uvn$XBD8V1#6XFV_{{sPtWME`u?25X%7bwJ-f-_5GhWp-9X|Cst| zrPdFs9S<&hSjruE^&|UZ3->a&qnxW=v8noVvn(mvpZCN!45bDP46hOx7_4S6Fo+k-*%fF5lweBo zc6VW5yxS$b1jyqo@Q5r1(jH*!b~4)z$f)vkaSY+Oo}AFY{EXef>EC~Lfsg%-2lxG} zXLgSL`=43i5wr8#|No68R{j6~JaNVU|Mh&$zdru2Rd^)9{QLL$eu)|X?Q3{c>gw5q z{yVPu&+d4Hk?ooMS6-eT`8Nw$4zpJ;P?dhu(BjNH!Gl4NP0ZmCQ-*%?8%8!028PoM z&CO!t`eJ~tP%UwdC`m~yNwrEYN(E93Mh1qax(3F&hQ=XA##RQFRwjnJ1}0Vp2G8pk f??Ta#o1c=IR*74~G$m;^paup{S3j3^P6I* diff --git a/art/animals/smile_dog_g.png b/art/animals/smile_dog_g.png deleted file mode 100644 index b110afed2b281c9a42f385081e7a661766689328..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 385 zcmeAS@N?(olHy`uVBq!ia0vp^3Lwk~Bp9L@-6Me%OS+@4BLl<6e(pbstUx|vage(c z!@6@aFM%9|WRD45bDP46hOx7_4S6Fo+k-*%fF5lweBo zc6VW5yxS$b1jyqo@Q5r1(jH*!b~4)z$f)vkaSY+Oo}AFY{EXef>EC~Lfsg%-2lxG} zXLhdr`=6Po^sB?cTmS#38-)D-uYWk;|Ns9oY+oP$|Lt(Zl$5O_1I32qW7w`LDb@J@RiBvK(fwUZ5)droqLLcY+6lAe)%Op-0RozA^J;Ffcr3 zGoPp)G5Z?O6{;n!5hW>!C8<`)MX5lF!N|bSRM)^**U&h`$k@uj(#ph8*TBTez~FiP g;$0{ja`RI%(<*Umn5HDn2Gqdd>FVdQ&MBb@0J-#bod5s; diff --git a/art/animals/smile_dog_r.png b/art/animals/smile_dog_r.png deleted file mode 100644 index 1bed6423f75c68fec82febb8f3882fa4860e704c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 382 zcmeAS@N?(olHy`uVBq!ia0vp^3Lwk~Bp9L@-6Me%OS+@4BLl<6e(pbstUx|vage(c z!@6@aFM%9|WRD45bDP46hOx7_4S6Fo+k-*%fF5lweBo zc6VW5yxS$b1jyqo@Q5r1(jH*!b~4)z$SC)8aSY+Oo}AFY{EXef>EC~Lfsg%-2mk&5 z&(`+q*MD}2SFakIw*LSBKVb!s;P04mctPEt|26`Jy^VkWJ`d+P^WXj-yU>68=8oeE zj}$sA4jlNicwb}V$7b6p2|p6}x1==7PS9AvD8#97=m3+)jXH*#bq$QX3=F3^EN4A0 zym=bv1l1DPh?11Vl2ohYqEsNoU}Ruus%v1ZYiJx|WNc+%X=P%lYhYq!VDP+t@h%h% cx%nxXX_dG&OjD9(18QLKboFyt=akR{0CC`bhX4Qo diff --git a/art/animals/smile_dog_reduced.png b/art/animals/smile_dog_reduced.png deleted file mode 100644 index ebc55a2055ccb17613353b7f447e2c39e7a1799d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 422 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnH3?%tPCZz)@mUKs7M+SzC{oH>NS%G}U;vjb? zhIQv;UV>C6dj$D1FjT2AFf_Cjc?PRtckkR4k;us=vx%T{C zUM52x)(7I8`337DRTVBuPb?2H@d)3*u5G|typerdv7?8_j*rX#{AB&1o4HrKs#c); zQ2T18s<3v;S>J-Idg@kRQ2k)?gsr@-cUJZNusO?ixOwe9-aY5^AFqOiB^!7nBu{({ zP2R=MRnOe$Q!DXvdw{nOGt)}h@OANfB>r8KRxMWJZp^Wm3UrfdiEBhjN@7W>RdP`( zkYX@0Ff`RQFxE9R4ly#aGBB|+Fwg~)Z?(Q0MA49&pOTqYiCe>*a-UkD1_n=8KbLh* G2~7ZnBZArh diff --git a/door.lua b/door.lua index 67d03ba..ca0157c 100644 --- a/door.lua +++ b/door.lua @@ -9,7 +9,7 @@ function Door:initialize(x, y, color) y = y, color = color, collision = "unmoveable", - sprite = loadSprite("art/game/door_" .. color:sub(1, 1) .. ".png"), + sprite = loadSprite("art/game/door.png"), }) self.isDoor = true self.locked = true @@ -21,7 +21,7 @@ end function Door:refreshSprites() local sprite if self.locked then - sprite = loadSprite("art/game/door_" .. self.doorColor:sub(1, 1) .. ".png") + sprite = loadSprite("art/game/door.png") else sprite = loadSprite("art/game/door_unlocked.png") end diff --git a/level_editor/object_attributes.lua b/level_editor/object_attributes.lua index cc2252c..e4519da 100644 --- a/level_editor/object_attributes.lua +++ b/level_editor/object_attributes.lua @@ -3,6 +3,20 @@ require "libs/TSerial" local targetFolder = "art" local reanalyzeCells = true +-- Sprites loaded directly by dedicated entity classes or used as reference art +-- (the palette swatch), not placeable RGB assets. Every other master PNG in a +-- content folder is a colour asset. Keeping this list preserves the exact +-- palette set from when assets were identified by their _r/_g/_b channel files. +local NON_ASSET_SPRITES = { + palette = true, + color_changing_wall = true, + door_locked_w = true, + door_unlocked = true, + player_sad = true, + player_w = true, + switch_pressed = true, +} + function getAllAssets() local objectnames = {} local topFolder = "art" @@ -12,44 +26,46 @@ function getAllAssets() -- we ignore directories which are prepended w/ an underscore if not string.find(folder, "%.") then objectnames[folder] = {} - -- love.filesystem.createDirectory("art/" .. folder) -- this is a folder, probably local newFiles = love.filesystem.getDirectoryItems(topFolder .. "/" .. folder) for _, fileToAnalyze in pairs(newFiles) do - if fileToAnalyze:find("_r.png") or fileToAnalyze:find("_g.png") or fileToAnalyze:find("_b.png")then - local color - if fileToAnalyze:find("_r.png") then color = "red" - elseif fileToAnalyze:find("_g.png") then color = "green" - elseif fileToAnalyze:find("_b.png") then color = "blue" end + -- One full-colour master PNG per asset. The draw path tints it + -- per gun (see Entity:draw), so red/green/blue copies are just + -- the same master drawn through gColor[channel]; the channels are + -- split at load time, not stored as separate files. + if fileToAnalyze:find("%.png$") then + local name = fileToAnalyze:sub(1, fileToAnalyze:len() - 4) - local name = fileToAnalyze:sub(1, fileToAnalyze:len() - 6) + -- first folder to define a name wins; skip engine/reference art + if not NON_ASSET_SPRITES[name] + and not (globalAssetProperties[name] and globalAssetProperties[name].directory ~= folder) then - if not globalAssetProperties[name] then - globalAssetProperties[name] = {} + local sprite = topFolder .. "/" .. folder .. "/" .. fileToAnalyze + + if not globalAssetProperties[name] then + globalAssetProperties[name] = {} + globalAssetProperties[name].preload = false + end globalAssetProperties[name].directory = folder - globalAssetProperties[name].sprites = {} - globalAssetProperties[name].preload = false - end + -- all three channels point at the same master image + globalAssetProperties[name].sprites = { red = sprite, green = sprite, blue = sprite } - -- class comes from a .meta sitting next to the art - -- (authoritative); default to immoveable if none exists - local metaPath = topFolder .. "/" .. folder .. "/" .. name .. ".meta" - if love.filesystem.getInfo(metaPath, "file") then - globalAssetProperties[name].class = TSerial.unpack(love.filesystem.read(metaPath)).class - elseif not globalAssetProperties[name].class then - globalAssetProperties[name].class = "immoveable" - end + -- class comes from a .meta sitting next to the art + -- (authoritative); default to immoveable if none exists + local metaPath = topFolder .. "/" .. folder .. "/" .. name .. ".meta" + if love.filesystem.getInfo(metaPath, "file") then + globalAssetProperties[name].class = TSerial.unpack(love.filesystem.read(metaPath)).class + elseif not globalAssetProperties[name].class then + globalAssetProperties[name].class = "immoveable" + end - local sprite = topFolder .. "/" .. folder .. "/" .. fileToAnalyze - globalAssetProperties[name].sprites[color] = sprite - - -- Precompute the occupied-cell shape once per asset/colour and - -- cache it here; entities share this table by reference so - -- collision never rescans pixels at runtime. Keyed per colour - -- (each channel leaves its own gaps) rather than reset per file. - globalAssetProperties[name].cells = globalAssetProperties[name].cells or {} - if not globalAssetProperties[name].cells[color] or reanalyzeCells then - globalAssetProperties[name].cells[color] = getCellsFromSprite(sprite) + -- Precompute per-channel occupied-cell shapes once and cache + -- here; entities share this table by reference so collision + -- never rescans pixels at runtime. Keyed per colour because + -- each channel of the master leaves its own gaps. + if reanalyzeCells or not globalAssetProperties[name].cells then + globalAssetProperties[name].cells = getChannelCells(sprite) + end end end end diff --git a/npc.lua b/npc.lua index b5fb70a..170fe04 100644 --- a/npc.lua +++ b/npc.lua @@ -224,11 +224,10 @@ function Sign:initialize(x, y) x = x, y = y, color = "red", -- representative only; sprites are per-channel collision = "unmoveable", - sprites = { - red = loadSprite("art/npc/start_sign_r.png"), - green = loadSprite("art/npc/start_sign_g.png"), - blue = loadSprite("art/npc/start_sign_b.png"), - }, + sprites = (function() + local m = loadSprite("art/npc/start_sign.png") -- one master, tinted per gun at draw time + return { red = m, green = m, blue = m } + end)(), }) self:initWalker() end @@ -244,11 +243,10 @@ function Jorge:initialize(x, y) x = x, y = y, color = "red", collision = "unmoveable", - sprites = { - red = loadSprite("art/npc/jorge_r.png"), - green = loadSprite("art/npc/jorge_g.png"), - blue = loadSprite("art/npc/jorge_b.png"), - }, + sprites = (function() + local m = loadSprite("art/npc/jorge.png") -- one master, tinted per gun at draw time + return { red = m, green = m, blue = m } + end)(), }) self:initWalker() end diff --git a/switch.lua b/switch.lua index f48689c..5b86440 100644 --- a/switch.lua +++ b/switch.lua @@ -9,7 +9,7 @@ function Switch:initialize(x, y, color) color = color, collision = "none", --switches are floor elements that can't be pushed or bloc movement!! cellShape = {{x = 1, y = 1}}, - sprite = loadSprite("art/game/switch_" .. color:sub(1, 1) .. ".png") + sprite = loadSprite("art/game/switch.png") }) self.activated = false