diff --git a/.gitignore b/.gitignore index d0bf673..4c7f76e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,13 @@ # Godot's import cache and editor state. Regenerated by `godot --import`. .godot/ +# Assets whose licence forbids redistribution. bdragon1727's packs say +# plainly: "You cannot do: Resell / redistribute this asset." Publishing a repo +# containing them IS redistribution, so they stay local-only. The game loads +# them if present and falls back to generated placeholders if not, so a clone +# without them still runs. See docs/ASSETS.md. +/assets/local/* +!/assets/local/README.md + # Raw asset packs: gigabytes of source artwork and audio. Redistributable # third-party material belongs where it was bought or downloaded, not in a code # repo. The directory itself and its guards stay tracked, so a fresh clone has diff --git a/CREDITS.md b/CREDITS.md new file mode 100644 index 0000000..40fae52 --- /dev/null +++ b/CREDITS.md @@ -0,0 +1,30 @@ +# Credits + +Third-party assets used in Transcience, with the attribution their licences +require. + +## Audio + +**Pixel Combat SFX** — [Helton Yan](https://heltonyan.itch.io/pixelcombat) +Licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). +Converted to 16-bit / 44.1 kHz mono and trimmed for use in game. + +> Attribution is **required** by this licence, not optional. It has to appear +> somewhere a player can reach — this file is not enough on its own once the +> game ships. See docs/ROADMAP.md. + +## Art + +**16x16 DungeonTileset II** — [0x72 (Robert Norenberg)](https://0x72.itch.io/dungeontileset-ii) +Released under [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) +(public domain). Credit is not required; it is here because the work deserves +it. + +## Not distributed with this repository + +Two packs are used locally but deliberately **excluded** from the repository, +because their licences forbid redistribution. They are not part of any build +made from this source alone. See [docs/ASSETS.md](docs/ASSETS.md). + +- Fire Pixel Bullet 16x16 — [bdragon1727](https://bdragon1727.itch.io/fire-pixel-bullet-16x16) +- 750+ Effect and FX Pixel All — [bdragon1727](https://bdragon1727.itch.io/750-effect-and-fx-pixel-all) diff --git a/README.md b/README.md index c27a05b..92bb5b9 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ will otherwise cost an afternoon. - [docs/ROADMAP.md](docs/ROADMAP.md) — what is built, what is next, where each feature lives - [docs/DECISIONS.md](docs/DECISIONS.md) — settled design decisions and their reasoning +- [CREDITS.md](CREDITS.md) — third-party asset attribution - [CLAUDE.md](CLAUDE.md) — commands, invariants, Godot CLI gotchas - [docs/WORKFLOW.md](docs/WORKFLOW.md) — agent-assisted Godot development, and why the setup looks like this - [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — code map and the node-free simulation diff --git a/assets/local/README.md b/assets/local/README.md new file mode 100644 index 0000000..2bf0903 --- /dev/null +++ b/assets/local/README.md @@ -0,0 +1,24 @@ +# Local-only assets + +Files here are **deliberately not in git**, because their licences forbid +redistribution — and publishing a repository containing them is redistribution. + +Currently: + +| File | Pack | Restriction | +| --- | --- | --- | +| `bullets.png` | [Fire Pixel Bullet 16x16](https://bdragon1727.itch.io/fire-pixel-bullet-16x16) | "You cannot do: Resell / redistribute this asset." Free tier is non-commercial only. | +| `impact.png` | [750 Effect and FX Pixel All](https://bdragon1727.itch.io/750-effect-and-fx-pixel-all) | Same wording. Even the paid tier: "You can NOT re-distribute the file, no matter how much you modify it." | + +The game degrades rather than breaks without them: `Art.bullets_texture()` +returns null and the renderer falls back to a generated dot. That fallback is +what a fresh clone gets, and it is why nothing here may be `preload()`ed — +`preload` is resolved at compile time and would fail the whole build on a +machine that does not have these files. + +To restore them locally, copy from `assetpacks/` (also untracked): + +``` +cp "assetpacks/New_All_Fire_Bullet_Pixel_16x16/All_Fire_Bullet_Pixel_16x16_00.png" assets/local/bullets.png +cp "assetpacks/Effect and FX Pixel All Free/Part 1/03.png" assets/local/impact.png +``` diff --git a/assets/sprites/bullets.png b/assets/sprites/bullets.png deleted file mode 100644 index 06ecd23..0000000 Binary files a/assets/sprites/bullets.png and /dev/null differ diff --git a/assets/sprites/bullets.png.import b/assets/sprites/bullets.png.import deleted file mode 100644 index eea0471..0000000 --- a/assets/sprites/bullets.png.import +++ /dev/null @@ -1,40 +0,0 @@ -[remap] - -importer="texture" -type="CompressedTexture2D" -uid="uid://j0ouktqmynvo" -path="res://.godot/imported/bullets.png-95be159865e32af815661d75740444b7.ctex" -metadata={ -"vram_texture": false -} - -[deps] - -source_file="res://assets/sprites/bullets.png" -dest_files=["res://.godot/imported/bullets.png-95be159865e32af815661d75740444b7.ctex"] - -[params] - -compress/mode=0 -compress/high_quality=false -compress/lossy_quality=0.7 -compress/uastc_level=0 -compress/rdo_quality_loss=0.0 -compress/hdr_compression=1 -compress/normal_map=0 -compress/channel_pack=0 -mipmaps/generate=false -mipmaps/limit=-1 -roughness/mode=0 -roughness/src_normal="" -process/channel_remap/red=0 -process/channel_remap/green=1 -process/channel_remap/blue=2 -process/channel_remap/alpha=3 -process/fix_alpha_border=true -process/premult_alpha=false -process/normal_map_invert_y=false -process/hdr_as_srgb=false -process/hdr_clamp_exposure=false -process/size_limit=0 -detect_3d/compress_to=1 diff --git a/assets/sprites/impact.png b/assets/sprites/impact.png deleted file mode 100644 index 8c77f85..0000000 Binary files a/assets/sprites/impact.png and /dev/null differ diff --git a/assets/sprites/impact.png.import b/assets/sprites/impact.png.import deleted file mode 100644 index f8bfc8b..0000000 --- a/assets/sprites/impact.png.import +++ /dev/null @@ -1,40 +0,0 @@ -[remap] - -importer="texture" -type="CompressedTexture2D" -uid="uid://bspcicxeqn4bq" -path="res://.godot/imported/impact.png-e86888071ed584ee489f2674e4500cf8.ctex" -metadata={ -"vram_texture": false -} - -[deps] - -source_file="res://assets/sprites/impact.png" -dest_files=["res://.godot/imported/impact.png-e86888071ed584ee489f2674e4500cf8.ctex"] - -[params] - -compress/mode=0 -compress/high_quality=false -compress/lossy_quality=0.7 -compress/uastc_level=0 -compress/rdo_quality_loss=0.0 -compress/hdr_compression=1 -compress/normal_map=0 -compress/channel_pack=0 -mipmaps/generate=false -mipmaps/limit=-1 -roughness/mode=0 -roughness/src_normal="" -process/channel_remap/red=0 -process/channel_remap/green=1 -process/channel_remap/blue=2 -process/channel_remap/alpha=3 -process/fix_alpha_border=true -process/premult_alpha=false -process/normal_map_invert_y=false -process/hdr_as_srgb=false -process/hdr_clamp_exposure=false -process/size_limit=0 -detect_3d/compress_to=1 diff --git a/docs/ASSETS.md b/docs/ASSETS.md index 26b02f1..0e6f5c3 100644 --- a/docs/ASSETS.md +++ b/docs/ASSETS.md @@ -18,32 +18,63 @@ place, [src/view/art.gd](../src/view/art.gd), and `tests/unit/test_art.gd` asserts every rect lands inside its texture — a wrong atlas coordinate does not error, it silently draws the wrong pixels. -## Packs +## Licence findings -All four are the **free** versions. Source URLs are recorded because the packs -themselves are gitignored, so their bundled licence files are not in the repo. +Checked against each pack's itch.io page. **Two of the four forbid +redistribution**, which is what publishing a repository containing them would +be — so they are not in the repository. -| Pack | Source | In use | Licence | -| --- | --- | --- | --- | -| 0x72 Dungeon Tileset II v1.7 | https://0x72.itch.io/dungeontileset-ii | Terrain, player, enemies, boss → `assets/sprites/dungeon_tileset.png` | Confirm at source. Widely distributed as CC0 by Robert Norenberg (0x72); the bundled README is technical, not legal. | -| Fire Pixel Bullet 16x16 | https://bdragon1727.itch.io/fire-pixel-bullet-16x16 | Bullets → `assets/sprites/bullets.png` | Confirm at source (free version). | -| 750+ Effect and FX Pixel All | https://bdragon1727.itch.io/750-effect-and-fx-pixel-all | Impact effect → `assets/sprites/impact.png` | Confirm at source (free version). | -| Helton Yan — Pixel Combat | https://heltonyan.itch.io/pixelcombat | SFX → `assets/audio/sfx/*.wav` | Confirm at source (free version). | +| Pack | Licence | Commercial | Credit | In this repo? | +| --- | --- | --- | --- | --- | +| [0x72 DungeonTileset II](https://0x72.itch.io/dungeontileset-ii) | **CC0 1.0** (public domain) | Yes | Not required | **Yes** — `assets/sprites/dungeon_tileset.png` | +| [Helton Yan Pixel Combat](https://heltonyan.itch.io/pixelcombat) | **CC BY 4.0** | Yes | **Required** | **Yes** — `assets/audio/sfx/` | +| [Fire Pixel Bullet 16x16](https://bdragon1727.itch.io/fire-pixel-bullet-16x16) | Custom | **Non-commercial only** unless you contribute | Encouraged | **No** — local only | +| [750+ Effect and FX Pixel All](https://bdragon1727.itch.io/750-effect-and-fx-pixel-all) | Custom | **Non-commercial only** unless you contribute | Encouraged | **No** — local only | + +### The blocking clause + +Both bdragon1727 packs state, verbatim: + +> **You cannot do:** Resell / redistribute this asset. + +And the paid tier of the FX pack is no better for this purpose: + +> You can NOT re-distribute the file, no matter how much you modify it you can +> use it but not share or re-sell it. + +Putting those files in a public repository distributes them to everyone who +clones it, which is squarely what that forbids. Paying for the commercial tier +does **not** unlock it — the restriction survives payment and survives +modification. + +So they live in `assets/local/`, which is gitignored. `Art.bullets_texture()` +and `Art.impact_texture()` load them at runtime if present and return null if +not; the bullet renderer falls back to a generated dot. Verified both ways: with +the files present the game uses them, and with them hidden the suite still +passes 156/156 and the game runs. + +This is also why nothing there may be `preload()`ed — `preload` resolves at +compile time and would fail the build on every machine that lacks the files. + +### Two obligations this creates + +1. **Helton Yan's CC BY 4.0 requires attribution**, and it is not satisfied by + a file in the source tree once the game ships. Players need to be able to + see it. [CREDITS.md](../CREDITS.md) records it for now; an in-game credits + screen is a todo. +2. **Both bdragon packs are non-commercial in their free tier.** The stated + goal is a Steam release. Before that happens they need either a paid + contribution to bdragon1727 (which permits commercial use but still not + redistribution) or replacement with CC0 art. A CC0 replacement solves both + problems at once and lets the assets go back in the repo. ### Not in use -`assetpacks/considering_dont_use_yet/` is the user's own "not chosen" marker and -nothing there is referenced by the game. It contains RF Catacombs v1.0 (public -domain per its bundled `public-license.txt` — free for personal or commercial -use, credit appreciated, no reselling; artwork by Szadi art), plus Tiny RPG -Character Pack 02 and neo_zero demos, whose terms are unrecorded. - -### Still to confirm - -Free itch.io packs vary: some are CC0, some require credit, some restrict -commercial use, and "free version" often carries different terms from the paid -one. Four rows above say *confirm at source* — do that before a public build, -and if any require attribution, the credits screen does not exist yet. +`assetpacks/considering_dont_use_yet/` is the "not chosen" marker and nothing +there is referenced. It contains RF Catacombs v1.0 (public domain per its +bundled `public-license.txt` — free for personal or commercial use, credit +appreciated, no reselling; artwork by Szadi art), plus Tiny RPG Character Pack +02 and neo_zero demos, whose terms are unrecorded. ## Converting audio before use diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index b623c99..5da7962 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -68,6 +68,8 @@ play off server events. Enough to prove the pipeline, not a finished look. | Impact/death VFX animation | todo | `Art.IMPACT` is loaded and validated but nothing plays it yet | | Directional sprites, hit flashes, screen shake | todo | | | Audio buses and a volume setting | todo | Everything plays on Master at hardcoded dB | +| **In-game credits screen** | **todo** | Not cosmetic: the SFX are CC BY 4.0 and attribution is a licence *requirement*. [CREDITS.md](../CREDITS.md) is not reachable by a player. | +| Replace the two non-redistributable packs | todo | Bullet and FX art is local-only and non-commercial. CC0 replacements would let them into the repo and unblock a commercial release. See [ASSETS.md](ASSETS.md). | ## Stage 2 — Characters, persistence, levels · *todo, next* diff --git a/src/view/art.gd b/src/view/art.gd index 4764f4d..412c150 100644 --- a/src/view/art.gd +++ b/src/view/art.gd @@ -10,9 +10,43 @@ extends RefCounted ## Everything in this file is view-only. Nothing under src/sim/ may reference it: ## the simulation has no idea the game has art. +## Redistributable, so committed and safe to preload. const TILESET := preload("res://assets/sprites/dungeon_tileset.png") -const BULLETS := preload("res://assets/sprites/bullets.png") -const IMPACT := preload("res://assets/sprites/impact.png") + +## NOT redistributable: bdragon1727's licence says "You cannot do: Resell / +## redistribute this asset", and shipping them in a public repo would be exactly +## that. They live in the untracked assets/local/ and are loaded at RUNTIME. +## +## preload() would be resolved at compile time and would fail the entire build +## on any machine that does not have them -- which is every fresh clone. Hence +## load() behind an existence check, and a generated fallback in the renderer. +const LOCAL_BULLETS := "res://assets/local/bullets.png" +const LOCAL_IMPACT := "res://assets/local/impact.png" + +static var _bullets: Texture2D = null +static var _impact: Texture2D = null +static var _optional_loaded: bool = false + + +static func _load_optional() -> void: + if _optional_loaded: + return + _optional_loaded = true + if ResourceLoader.exists(LOCAL_BULLETS): + _bullets = load(LOCAL_BULLETS) + if ResourceLoader.exists(LOCAL_IMPACT): + _impact = load(LOCAL_IMPACT) + + +## Null when the licence-restricted art is not present. Callers must cope. +static func bullets_texture() -> Texture2D: + _load_optional() + return _bullets + + +static func impact_texture() -> Texture2D: + _load_optional() + return _impact ## Source art is 16px; the world's tiles are 32. Drawn at 2x with nearest ## filtering (project-wide `default_texture_filter=0`) so it stays crisp. diff --git a/src/view/bullet_renderer.gd b/src/view/bullet_renderer.gd index b567b59..6249fd7 100644 --- a/src/view/bullet_renderer.gd +++ b/src/view/bullet_renderer.gd @@ -10,17 +10,34 @@ extends Node2D ## sheet. Four kinds means four draw calls, which is nothing, and it avoids a ## custom shader passing UV offsets through per-instance custom data. +## Used only in fallback mode, where every bullet is the same generated dot and +## colour is the only thing distinguishing a kind. +const _FALLBACK_COLOURS: Array[Color] = [ + Color(0.45, 0.95, 1.0), # KIND_PLAYER_SHOT + Color(1.0, 0.35, 0.75), # KIND_ORB + Color(1.0, 0.85, 0.3), # KIND_NEEDLE + Color(1.0, 0.45, 0.2), # KIND_HEAVY +] + var _layers: Array[MultiMeshInstance2D] = [] ## Advances the bullets' own animation, independent of the simulation. var _anim_tick: int = 0 +## True when the licence-restricted bullet sheet is absent and we are drawing +## generated dots instead. See Art.bullets_texture(). +var _fallback: bool = false func _ready() -> void: + var sheet := Art.bullets_texture() + _fallback = sheet == null + var art: Texture2D = sheet if sheet != null else _make_dot_texture() + if _fallback: + GameLog.info("view", "bullet sheet absent, drawing generated dots") var quad := QuadMesh.new() quad.size = Vector2.ONE for kind in SimConfig.KIND_HEAVY + 1: var layer := MultiMeshInstance2D.new() - layer.texture = Art.BULLETS + layer.texture = art var mm := MultiMesh.new() mm.transform_format = MultiMesh.TRANSFORM_2D mm.use_colors = true @@ -34,7 +51,24 @@ func _ready() -> void: func _process(_delta: float) -> void: _anim_tick += 1 - _apply_frame() + if not _fallback: + _apply_frame() + + +## A soft-edged disc with a bright core, generated at runtime. This is what the +## game draws when the bullet sheet is not present, which is the case for any +## clone of the repository -- the sheet's licence forbids redistributing it. +func _make_dot_texture() -> ImageTexture: + var size := 32 + var img := Image.create(size, size, false, Image.FORMAT_RGBA8) + var centre := Vector2(size, size) * 0.5 + for y in size: + for x in size: + var d := Vector2(x + 0.5, y + 0.5).distance_to(centre) / (size * 0.5) + var alpha := pow(clampf(1.0 - d, 0.0, 1.0), 0.6) + var core := clampf(1.0 - d * 1.9, 0.0, 1.0) + img.set_pixel(x, y, Color(1.0, 1.0, 1.0, alpha).lerp(Color.WHITE, core * 0.8)) + return ImageTexture.create_from_image(img) ## Point each layer's texture at this frame's cell in the sheet. Every bullet in @@ -50,7 +84,7 @@ func _apply_frame() -> void: var atlas := _layers[kind].texture as AtlasTexture if atlas == null: atlas = AtlasTexture.new() - atlas.atlas = Art.BULLETS + atlas.atlas = Art.bullets_texture() _layers[kind].texture = atlas atlas.region = region @@ -73,7 +107,8 @@ func render_pool(pool: BulletPool) -> void: var angle: float = (pool.vel[i] as Vector2).angle() _layers[kind].multimesh.set_instance_transform_2d( n, Transform2D(angle, Vector2(d, d), 0.0, pool.pos[i])) - _layers[kind].multimesh.set_instance_color(n, Color.WHITE) + _layers[kind].multimesh.set_instance_color(n, + _FALLBACK_COLOURS[kind] if _fallback else Color.WHITE) counts[kind] = n + 1 for kind in _layers.size(): _layers[kind].multimesh.visible_instance_count = counts[kind] diff --git a/tests/unit/test_art.gd b/tests/unit/test_art.gd index 7f0f3e5..c758eb6 100644 --- a/tests/unit/test_art.gd +++ b/tests/unit/test_art.gd @@ -12,10 +12,20 @@ func _fits(tex: Texture2D, r: Rect2) -> bool: and r.end.x <= size.x and r.end.y <= size.y -func test_every_texture_loaded() -> void: - for tex in [Art.TILESET, Art.BULLETS, Art.IMPACT]: - assert_not_null(tex) - assert_gt(tex.get_width(), 0) +func test_the_committed_texture_loads() -> void: + assert_not_null(Art.TILESET) + assert_gt(Art.TILESET.get_width(), 0) + + +## The licence-restricted sheets are absent from a clean clone by design, so +## their absence must be a supported state rather than a failure. When they are +## present locally, the rect tests below still validate them. +func test_the_game_runs_without_the_restricted_art() -> void: + var bullets := Art.bullets_texture() + if bullets == null: + assert_true(true, "no bullet sheet: renderer falls back to generated dots") + else: + assert_gt(bullets.get_width(), 0) func test_terrain_rects_are_inside_the_tileset() -> void: @@ -38,13 +48,17 @@ func test_every_animation_frame_is_inside_the_tileset() -> void: func test_every_bullet_frame_is_inside_the_sheet() -> void: + var sheet := Art.bullets_texture() + if sheet == null: + pass_test("bullet sheet not present locally; nothing to validate") + return for kind in Art.BULLET_CELLS.size(): var cell: Vector2i = Art.BULLET_CELLS[kind] for n in Art.BULLET_FRAMES: var r := Rect2( Vector2(float(cell.x + n), float(cell.y)) * Art.BULLET_CELL, Vector2(Art.BULLET_CELL, Art.BULLET_CELL)) - assert_true(_fits(Art.BULLETS, r), + assert_true(_fits(sheet, r), "bullet kind %d frame %d is outside the sheet at %s" % [kind, n, r]) @@ -63,10 +77,14 @@ func test_there_is_a_sprite_for_every_enemy_visual() -> void: func test_impact_sheet_covers_its_declared_frames() -> void: + var impact := Art.impact_texture() + if impact == null: + pass_test("impact sheet not present locally; nothing to validate") + return var last := Rect2( Vector2(float((Art.IMPACT_FRAMES - 1) % Art.IMPACT_COLUMNS), 0.0) * Art.IMPACT_CELL, Vector2(Art.IMPACT_CELL, Art.IMPACT_CELL)) - assert_true(_fits(Art.IMPACT, last)) + assert_true(_fits(impact, last)) func test_every_sound_loaded_and_is_short_enough_to_be_a_sound_effect() -> void: