diff --git a/assets/branding/boot_splash.png b/assets/branding/boot_splash.png index 72fa92e..7197112 100644 Binary files a/assets/branding/boot_splash.png and b/assets/branding/boot_splash.png differ diff --git a/assets/branding/work_death_icon.png b/assets/branding/work_death_icon.png new file mode 100644 index 0000000..4584d7b Binary files /dev/null and b/assets/branding/work_death_icon.png differ diff --git a/assets/branding/work_death_icon.png.import b/assets/branding/work_death_icon.png.import new file mode 100644 index 0000000..9efda83 --- /dev/null +++ b/assets/branding/work_death_icon.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://2jes4fpudg40" +path="res://.godot/imported/work_death_icon.png-4a0e0833fbbeeb174e53a0f325be264b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/branding/work_death_icon.png" +dest_files=["res://.godot/imported/work_death_icon.png-4a0e0833fbbeeb174e53a0f325be264b.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/BRANDING.md b/docs/BRANDING.md index acf6e52..beff19e 100644 --- a/docs/BRANDING.md +++ b/docs/BRANDING.md @@ -1,7 +1,8 @@ -# Loading screen branding +# Work-Death Balance — loading screen and favicon -The stock Godot boot splash (logo plus "GODOT Game engine") is replaced by the -"Mat Games" mascot on a black background. This applies to both surfaces: +The game is named **Work-Death Balance** in the menu, native window, HTML title, +description and loading screen. **MatGames** remains the studio signature. +The stock Godot boot splash is replaced by the existing nerd-zombie mascot on black. This applies to both surfaces: - the browser loading screen: the exported `index.png` referenced by `` in the HTML shell, drawn over `#status`; @@ -10,10 +11,10 @@ The stock Godot boot splash (logo plus "GODOT Game engine") is replaced by the ## Asset `assets/branding/boot_splash.png`, 1024x1315, palette PNG with alpha -(256 colors, 95 alpha levels), about 171 KB. Subject: cartoon zombie head and bust, +(256 colors, antialiased alpha), about 174 KB. Subject: cartoon zombie head and bust, bald scalp with a sparse hair fringe on the sides, thick nerd glasses, unsettling grin, -thumbs up, with "MatGames" (one word, no space) in a phosphor-terminal typeface and -the subtitle `"...because he was bored..."` beneath it. +thumbs up, with **Work-Death Balance** in the VT323 phosphor-terminal typeface and +the subtitle `MatGames — "...because he was bored..."` beneath it. Provenance is recorded in `tools/branding/manifest.json`: @@ -34,7 +35,7 @@ Provenance is recorded in `tools/branding/manifest.json`: and redistribution, including commercially, as long as the font is not sold on its own. SHA256 `cf4de751ada78ceac033dbe16a687742939995b77bc2a052ae17a4957958594d`. -Only the final PNG lives under `assets/`, so the raw source and the font are not +Only the final splash/icon PNGs live under `assets/`, so the raw source and the font are not packed into the export (`tools/*` is already excluded by the export preset). ## Composition and keying @@ -49,14 +50,22 @@ packed into the export (`tools/*` is already excluded by the export preset). 4. crops to the subject, applies a 50 px fade at the bottom to soften the frame cut of the generated bust, and scales to 1024 px wide with 40 px side padding; 5. draws the title with VT323 in amber over a blurred glow layer, plus an optional - second line. Defaults are the one-word `MatGames` and the subtitle - `"...because he was bored..."`; `--title`, `--subtitle` (empty to omit) and + second line. Defaults are `Work-Death Balance` and the subtitle + `MatGames — "...because he was bored..."`; `--title`, `--subtitle` (empty to omit) and `--subtitle-height` change them. A 30 px `--bottom-padding` band is left under the text so the shell progress bar does not cross the lettering; -6. quantizes to 256 colors (no dither) and writes the PNG plus the manifest. +6. quantizes to 256 colors (no dither) and writes the PNG plus the manifest; +7. invokes `tools/make_favicon.py` with the already keyed original 1024×1024 mascot. + A head-only polygon excludes the hand and bust, then draws the face inside a dark + rounded badge with an amber outline. No new AI generation or second chroma key. + The 512×512 RGBA master is `assets/branding/work_death_icon.png`; a 32×32 review + preview is kept in `tools/branding/favicon_preview_32.png`. Provenance and SHA256 + are recorded under `favicon` in the same manifest. ```sh -python3 tools/make_boot_splash.py tools/branding/raw/agy_generate_20261010234423_0.jpg +python3 tools/make_boot_splash.py tools/branding/raw/agy_edit_20261011000309_0.jpg \ + --provenance tools/branding/raw/agy_generate_20261010234423_0.jpg \ + --provenance tools/branding/raw/agy_edit_20261010235510_0.jpg ``` `--colors 0` keeps 32-bit RGBA if a future artwork needs smoother gradients. @@ -64,6 +73,8 @@ python3 tools/make_boot_splash.py tools/branding/raw/agy_generate_20261010234423 ## Godot settings ```ini +config/name="Work-Death Balance" +config/icon="res://assets/branding/work_death_icon.png" boot_splash/bg_color=Color(0, 0, 0, 1) boot_splash/image="res://assets/branding/boot_splash.png" boot_splash/show_image=true @@ -77,16 +88,26 @@ ratio; the exported shell confirms the result with the classes ## Verification -- Exported `index.png` has the same 1024x1315 dimensions as the master. It is - re-encoded by the exporter to 32-bit RGBA (about 528 KB), so the bytes are not - identical; composited on black the two images differ by at most 5.8/255 on - 0.11% of pixels, all with alpha below 20, i.e. invisible. The release keeps the - 171 KB master in the repository and ships a gzip sidecar. +- Exported `index.png` has the same 1024×1315 dimensions as the master. Godot + re-encodes palette images to RGBA, so bytes need not match the source PNG. +- Web preset `html/export_icon=true` uses the project icon through Godot's official + exporter. It creates `index.icon.png` (512×512) and `index.apple-touch-icon.png` + (180×180) with the corresponding HTML links. No generated HTML patch or default + Godot favicon remains; both files are included in the immutable release/hash check. + Firefox's runtime favicon is a generated blob URL; decoded pixels were compared + with the exported PNG and match exactly (zero mean/max difference). - The exported shell contains no "Game engine" text and the splash image has an empty `alt`. -- `html/export_icon=false`, so no favicon is exported: the browser tab still shows - a default icon. Using the mascot as favicon is a possible follow-up. +- The legacy URL `/pub/zombie-fps/`, score API and `zombie_fps_*` localStorage keys + intentionally remain unchanged to preserve bookmarks and browser identity. +- Native `user://` moves with the project name. `player_profile.gd` imports the old + sibling `Zombie Room FPS/zombie_fps_profile.cfg` only when the new profile is + absent, never modifies the source and never replaces an existing new profile. + Name, ID, best score and name-lock flag are retained; migration has isolated tests. + The actual local migration was also verified byte-identical, old file unchanged. - On a phone in landscape the splash is scaled to fit the height, so the mascot appears fairly small; a landscape variant (mascot beside the title) would fill more of the screen if that is wanted. -- All eleven test suites still pass; this change touches no gameplay code. +- `test_rebalance_branding.gd` checks title, icon, export setting and exact damage + values; `test_player_profile.gd` includes five non-destructive migration checks. + Both suites also pass in the GDExtension-free staging project. diff --git a/docs/GAMEPLAY-IMPROVEMENTS.md b/docs/GAMEPLAY-IMPROVEMENTS.md index 17c58d2..c146a5e 100644 --- a/docs/GAMEPLAY-IMPROVEMENTS.md +++ b/docs/GAMEPLAY-IMPROVEMENTS.md @@ -1,5 +1,16 @@ # Classic HUD, automatic reload, furniture navigation, death audio +## Work-Death Balance damage tuning + +- Player rifle: **17.5 damage** per body hit (previously 35), using the float + `Player.BULLET_DAMAGE` constant. Head multipliers remain zombie ×2.5 / boss ×1.5. +- Grenade explosion: **320 maximum damage** (previously 160), same 5 m linear + falloff, wall occlusion, self-damage and 15 s cooldown. At 1 m it deals 256; + at 2.5 m it deals 160; outside 5 m it deals none. +- Alien Grunt hornet damage is **unchanged**, including its progression by rank. +- `tools/test_rebalance_branding.gd`: 14 checks, including real rifle body/head + hits and exact explosion/self-damage values. All previous gameplay suites pass. + ## HUD `scripts/game_hud.gd` replaces the debug-style text block with translucent black diff --git a/docs/GRENADES-WAVES.md b/docs/GRENADES-WAVES.md index 054dbc2..98f41f1 100644 --- a/docs/GRENADES-WAVES.md +++ b/docs/GRENADES-WAVES.md @@ -17,7 +17,7 @@ not add environmental destruction (the existing zombie-entry glass shatter is unchanged). - **No 4-second fuse anymore**. A lost projectile is deleted after 15 seconds without an explosion, avoiding orphan nodes or delayed airborne blasts. -- Blast radius **5 m**, maximum damage **160**, linear falloff; a world-mask ray prevents +- Blast radius **5 m**, maximum damage **320**, linear falloff; a world-mask ray prevents damage through walls. The living thrower is still included. ## Original Half-Life explosion diff --git a/docs/HIGHSCORES.md b/docs/HIGHSCORES.md index c32e1dc..6860400 100644 --- a/docs/HIGHSCORES.md +++ b/docs/HIGHSCORES.md @@ -1,4 +1,4 @@ -# Classifica globale (punteggi) — Zombie Room FPS +# Classifica globale (punteggi) — Work-Death Balance Sistema di punteggi online: schermata iniziale con classifica e nome del giocatore, microservizio sul bastion, invio del punteggio alla morte. @@ -31,7 +31,7 @@ File coinvolti: | `scripts/start_screen.gd` | menu: nome, pulsante GIOCA, classifica | | `tools/test_scores_api.py` | 42 controlli sul servizio (funzionalità, validazione, limiti) | | `tools/test_leaderboard.gd` + `.sh` | 14 controlli sul client, contro un'API locale usa e getta | -| `tools/test_player_profile.gd` | 21 controlli sul profilo (nome, id, riparazioni) | +| `tools/test_player_profile.gd` | 26 controlli sul profilo (nome, id, riparazioni, migrazione dopo rinomina) | | `tools/test_start_screen.gd` | 10 controlli sulle regole del menu | ## 2. Contratto dell'API @@ -198,7 +198,7 @@ sh tools/test_leaderboard.sh # 14 controlli # profilo e menu (menu: serve un display) $HOME/.local/opt/godot-4.7.1/Godot_v4.7.1-stable_linux.x86_64 --headless --path . \ - --script res://tools/test_player_profile.gd # 21 controlli + --script res://tools/test_player_profile.gd # 26 controlli $HOME/.local/opt/godot-4.7.1/Godot_v4.7.1-stable_linux.x86_64 --path . \ --rendering-method gl_compatibility --script res://tools/test_start_screen.gd # 10 controlli ``` diff --git a/docs/WEB-DEPLOY.md b/docs/WEB-DEPLOY.md index 9f5c9a9..d2bb006 100644 --- a/docs/WEB-DEPLOY.md +++ b/docs/WEB-DEPLOY.md @@ -1,5 +1,27 @@ # Web build and compressed deployment +## Work-Death Balance / damage tuning: 118709d117376527 + +Renames the game in menu, native window, HTML title/description and loading screen; +MatGames remains the studio signature. New nerd-zombie dark/amber favicon and Apple +icon, exported through `html/export_icon=true`; see `docs/BRANDING.md`. + +- Player rifle 35 -> 17.5 damage, grenade maximum 160 -> 320; alien hornet damage, + head multipliers, blast radius, occlusion and cooldown unchanged. +- Native rename migration copies the old profile only if the new one is absent; + old source remains unchanged. Actual local migration verified byte-identical. +- Browser name/ID/best/lock fingerprint unchanged before/after the rename. +- All 17 Godot suites + 42 API checks passed; 14 exact-damage/branding checks and + 26 profile checks repeated in GDExtension-free staging. +- PCK 7,029,108 bytes (gzip 3,826,014); main gzip payload 13,975,158 bytes. +- 16 release files SHA256-verified; HTTPS PCK and favicon cmp-identical to local. + Favicon `index.icon.png` 512x512; `index.apple-touch-icon.png` 180x180. +- Browser: loading screen and menu reviewed, title/active script correct; the + runtime favicon's decoded pixels exactly match the exported icon (zero difference). +- Atomic root swap, backup `/home/debian/zombie-fps-backups/index-before-work-death-118709d117376527.html`. +- URL/API/storage keys unchanged; no DB/nginx/API change and no production test + submissions. Previous `86c32ea9f5a4470e` still HTTP 200; older releases retained. + ## Alien Grunt boss release: 86c32ea9f5a4470e Every tenth wave is a single teleporting Alien Grunt, with progressively increasing diff --git a/export_presets.cfg b/export_presets.cfg index 95b16b9..9d9e9e6 100644 --- a/export_presets.cfg +++ b/export_presets.cfg @@ -21,9 +21,9 @@ variant/extensions_support=false variant/thread_support=false vram_texture_compression/for_desktop=true vram_texture_compression/for_mobile=true -html/export_icon=false +html/export_icon=true html/custom_html_shell="" -html/head_include="" +html/head_include="" html/canvas_resize_policy=2 html/focus_canvas_on_start=true html/experimental_virtual_keyboard=true diff --git a/project.godot b/project.godot index bcfce06..41c4bf2 100644 --- a/project.godot +++ b/project.godot @@ -10,8 +10,9 @@ config_version=5 [application] -config/name="Zombie Room FPS" -config/description="FPS survival: zombie di Half-Life in una stanza con finestre e nessuna porta." +config/name="Work-Death Balance" +config/description="Work-Death Balance — sopravvivi al turno in ufficio. Un gioco MatGames." +config/icon="res://assets/branding/work_death_icon.png" run/main_scene="res://scenes/main.tscn" config/features=PackedStringArray("4.7", "Forward Plus") diff --git a/scripts/grenade.gd b/scripts/grenade.gd index 5728e7d..7d94600 100644 --- a/scripts/grenade.gd +++ b/scripts/grenade.gd @@ -4,7 +4,7 @@ signal detonated const MAX_LIFETIME := 15.0 const Explosion = preload("res://scripts/grenade_explosion.gd") const BLAST_RADIUS := 5.0 -const MAX_DAMAGE := 160.0 +const MAX_DAMAGE := 320.0 const Audio = preload("res://scripts/hl_audio.gd") const Blood = preload("res://scripts/blood_effects.gd") var age := 0.0 diff --git a/scripts/player.gd b/scripts/player.gd index 640355e..fcfcda4 100644 --- a/scripts/player.gd +++ b/scripts/player.gd @@ -5,6 +5,7 @@ var damage_pivot: Node3D var damage_feedback: CanvasLayer const DamageFeedback = preload("res://scripts/damage_feedback.gd") const MAX_HP := 100.0 +const BULLET_DAMAGE := 17.5 var hp := MAX_HP var invulnerable := false # only enabled by the explicit native boss simulation flag var _regen_elapsed := 0.0 @@ -189,7 +190,7 @@ func shoot() -> void: var hit := get_world_3d().direct_space_state.intersect_ray(query) if hit and hit.collider.has_method("take_damage"): Blood.impact(get_parent(), hit.position, -camera.global_basis.z) - hit.collider.take_damage(35, hit.position.y) + hit.collider.take_damage(BULLET_DAMAGE, hit.position.y) if ammo <= 0: start_reload() diff --git a/scripts/player_profile.gd b/scripts/player_profile.gd index 654ac47..08e32d8 100644 --- a/scripts/player_profile.gd +++ b/scripts/player_profile.gd @@ -5,6 +5,7 @@ extends RefCounted ## IndexedDB di Godot puo' non essere sincronizzata). Chiavi: zombie_fps_*. ## Il nome, una volta scelto, resta bloccato: non viene piu' richiesto. const PATH := "user://zombie_fps_profile.cfg" +const LEGACY_PROJECT_NAME := "Zombie Room FPS" const DEFAULT_NAME := "Giocatore" const NAME_MAX := 16 const KEY_NAME := "zombie_fps_name" @@ -14,10 +15,19 @@ const KEY_LOCKED := "zombie_fps_locked" ## Legge il profilo (o i valori predefiniti) e lo riscrive, cosi' il primo avvio ## lascia sempre un id stabile e le chiavi del browser allineate. -static func load_profile(path := PATH) -> Dictionary: +static func load_profile(path := PATH, legacy_path := "") -> Dictionary: var data := {"name": DEFAULT_NAME, "player_id": "", "best": 0, "locked": false} var config := ConfigFile.new() - if config.load(path) == OK: + var loaded := config.load(path) + # Renaming application/config/name moves native user://. Import the old file + # only when the new one does not exist; never change/delete the source profile. + # Explicit legacy_path enables isolated migration tests using temporary files. + if loaded != OK and not FileAccess.file_exists(path): + if legacy_path == "" and path == PATH and not OS.has_feature("web"): + legacy_path = legacy_profile_path() + if legacy_path != "" and legacy_path != ProjectSettings.globalize_path(path): + loaded = config.load(legacy_path) + if loaded == OK: var raw_name := text_of(config.get_value("player", "name", DEFAULT_NAME)) # Una versione precedente poteva salvare la stringa "null": in quel caso il # nome va considerato non scelto e il blocco rimosso (riparazione automatica). @@ -35,6 +45,9 @@ static func load_profile(path := PATH) -> Dictionary: save_profile(data, path) return data +static func legacy_profile_path() -> String: + return OS.get_user_data_dir().get_base_dir().path_join(LEGACY_PROJECT_NAME).path_join(PATH.get_file()) + static func save_profile(data: Dictionary, path := PATH) -> void: var raw_name := text_of(data.get("name")) var name := sanitize_name(raw_name) if is_stored_name_usable(raw_name) else DEFAULT_NAME diff --git a/scripts/start_screen.gd b/scripts/start_screen.gd index f9a6259..6063871 100644 --- a/scripts/start_screen.gd +++ b/scripts/start_screen.gd @@ -49,7 +49,7 @@ func _build_ui() -> void: column.add_theme_constant_override("separation", 10 if touch_mode else 14) center.add_child(column) - var title := _label("ZOMBIE ROOM FPS", 34 if touch_mode else 44, AMBER, HORIZONTAL_ALIGNMENT_CENTER) + var title := _label("Work-Death Balance", 34 if touch_mode else 44, AMBER, HORIZONTAL_ALIGNMENT_CENTER) column.add_child(title) column.add_child(_label("MatGames — classifica globale", 15, DIM, HORIZONTAL_ALIGNMENT_CENTER)) diff --git a/tools/branding/favicon_preview_32.png b/tools/branding/favicon_preview_32.png new file mode 100644 index 0000000..8d9550f Binary files /dev/null and b/tools/branding/favicon_preview_32.png differ diff --git a/tools/branding/favicon_preview_32.png.import b/tools/branding/favicon_preview_32.png.import new file mode 100644 index 0000000..0fe31f1 --- /dev/null +++ b/tools/branding/favicon_preview_32.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://x2fduvd60l5o" +path="res://.godot/imported/favicon_preview_32.png-dd38476f0c8cf5ec4443f01536b2cbf2.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://tools/branding/favicon_preview_32.png" +dest_files=["res://.godot/imported/favicon_preview_32.png-dd38476f0c8cf5ec4443f01536b2cbf2.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/tools/branding/manifest.json b/tools/branding/manifest.json index 0415e05..f2f289a 100644 --- a/tools/branding/manifest.json +++ b/tools/branding/manifest.json @@ -7,7 +7,7 @@ ], "mode": "P", "palette_colors": 256, - "sha256": "715bd5f1dd3a651b1b5f6ee0560db232ddfdad676091f07ea34696787e27fbee" + "sha256": "4996cbf31fdc1afb5de77a228dc7a6a5f54156dda62b3108d8f2f73a7e6f2c3e" }, "mascot_source": { "file": "tools/branding/raw/agy_edit_20261011000309_0.jpg", @@ -30,8 +30,8 @@ "opaque_above": 130 }, "title": { - "text": "MatGames", - "subtitle": "\"...because he was bored...\"", + "text": "Work-Death Balance", + "subtitle": "MatGames \u2014 \"...because he was bored...\"", "font": "tools/branding/fonts/VT323-Regular.ttf", "license": "SIL Open Font License 1.1 (tools/branding/fonts/OFL.txt)" }, @@ -50,5 +50,15 @@ "sha256": "57bc1e46eb0876c02008038981c9b01a6bf4e02da78418de7b44f4fcbd93895e" } ], - "bottom_padding_px": 30 + "bottom_padding_px": 30, + "favicon": { + "file": "assets/branding/work_death_icon.png", + "size": [ + 512, + 512 + ], + "sha256": "dc674000e9f3378f962aee5804b84ae292723f82a95b4a76405c9a70817528d6", + "method": "existing chroma-keyed mascot head, dark rounded badge, amber outline; Pillow", + "preview": "tools/branding/favicon_preview_32.png" + } } diff --git a/tools/make_boot_splash.py b/tools/make_boot_splash.py index e1503d8..3e18cb2 100644 --- a/tools/make_boot_splash.py +++ b/tools/make_boot_splash.py @@ -1,4 +1,4 @@ -"""Compose the boot splash: chroma-keyed mascot plus VT323 "Mat Games" lettering. +"""Compose Work-Death Balance's splash/icon using the existing MatGames mascot. The raw mascot comes from an image generator on a flat magenta background (generators do not emit real alpha). This script keys that background to transparency, removes @@ -10,11 +10,12 @@ import hashlib import json import pathlib from PIL import Image, ImageDraw, ImageFilter, ImageFont +from make_favicon import build_favicon parser = argparse.ArgumentParser() parser.add_argument('raw', type=pathlib.Path, help='raw generated image (magenta background)') -parser.add_argument('--title', default='MatGames') -parser.add_argument('--subtitle', default='"...because he was bored..."', +parser.add_argument('--title', default='Work-Death Balance') +parser.add_argument('--subtitle', default='MatGames — "...because he was bored..."', help='second line under the title; empty string for none') parser.add_argument('--subtitle-height', type=int, default=100) parser.add_argument('--bottom-padding', type=int, default=30, @@ -63,6 +64,7 @@ for y in range(source.height): fixed_pixels[x, y] = (max(0, r - amount), g, max(0, b - amount)) mascot = fixed.convert('RGBA') mascot.putalpha(mask) +favicon = build_favicon(mascot, root) mascot = mascot.crop(mascot.getbbox()) if args.bottom_fade > 0: fade = min(args.bottom_fade, mascot.height) @@ -111,6 +113,7 @@ if args.colors > 0: canvas.save(out, optimize=True) manifest = json.loads((source_tools / 'manifest.json').read_text()) if (source_tools / 'manifest.json').exists() else {} manifest.update({ + 'favicon': favicon, 'boot_splash': {'file': str(out.relative_to(root)), 'size': list(canvas.size), 'mode': canvas.mode, 'palette_colors': args.colors, 'sha256': hashlib.sha256(out.read_bytes()).hexdigest()}, diff --git a/tools/make_favicon.py b/tools/make_favicon.py new file mode 100644 index 0000000..077a93c --- /dev/null +++ b/tools/make_favicon.py @@ -0,0 +1,36 @@ +"""Work-Death Balance icon: the existing nerd-zombie face in an amber badge. + +Called by make_boot_splash.py with the already keyed full-resolution mascot, so +there is no second keying pass or new AI artwork. Reference coordinates are for +our original 1024x1024 mascot (not a generic automatic face detector). +""" +import hashlib +from pathlib import Path +from PIL import Image, ImageChops, ImageDraw + + +def build_favicon(mascot: Image.Image, root: Path) -> dict: + assert mascot.size == (1024, 1024), 'Update the head mask for a different source' + face = mascot.convert('RGBA').copy() + mask = Image.new('L', face.size) + ImageDraw.Draw(mask).polygon([ + (235, 240), (280, 195), (320, 110), (512, 80), (700, 140), + (780, 245), (770, 385), (748, 460), (694, 610), (640, 710), + (530, 755), (420, 735), (328, 650), (277, 520), (228, 395), + ], fill=255) + face.putalpha(ImageChops.multiply(face.getchannel('A'), mask)) + face = face.crop((220, 80, 790, 755)) + face.thumbnail((440, 460), Image.Resampling.LANCZOS) + icon = Image.new('RGBA', (512, 512)) + ImageDraw.Draw(icon).rounded_rectangle((7, 7, 504, 504), radius=96, + fill=(13, 16, 15, 255), + outline=(255, 186, 48, 255), width=14) + icon.alpha_composite(face, ((512 - face.width) // 2, (512 - face.height) // 2)) + output = root / 'assets/branding/work_death_icon.png' + icon.save(output, optimize=True) + preview = root / 'tools/branding/favicon_preview_32.png' + icon.resize((32, 32), Image.Resampling.LANCZOS).save(preview, optimize=True) + return {'file': str(output.relative_to(root)), 'size': [512, 512], + 'sha256': hashlib.sha256(output.read_bytes()).hexdigest(), + 'method': 'existing chroma-keyed mascot head, dark rounded badge, amber outline; Pillow', + 'preview': str(preview.relative_to(root))} diff --git a/tools/test_player_profile.gd b/tools/test_player_profile.gd index 92feb92..ad512c5 100644 --- a/tools/test_player_profile.gd +++ b/tools/test_player_profile.gd @@ -139,6 +139,31 @@ func run_test() -> void: str(junk_after.get_value("player", "locked", true))]) DirAccess.remove_absolute(ProjectSettings.globalize_path(junk_path)) + # 12) Native rename migration: temporary files only, old source is read-only. + var legacy_path := "user://test_legacy_profile.cfg" + var migrated_path := "user://test_migrated_profile.cfg" + DirAccess.remove_absolute(ProjectSettings.globalize_path(migrated_path)) + Profile.save_profile({"name": "Legacy", "player_id": "abcdef0123456789", + "best": 850, "locked": true}, legacy_path) + var old_hash := FileAccess.get_sha256(legacy_path) + var migrated := Profile.load_profile(migrated_path, legacy_path) + check("rinomina: nome, id, record e blocco migrati senza perdite", + migrated.name == "Legacy" and migrated.player_id == "abcdef0123456789" + and migrated.best == 850 and migrated.locked) + check("rinomina: il vecchio file rimane byte-identico", + FileAccess.get_sha256(legacy_path) == old_hash) + check("rinomina: il profilo viene scritto nella nuova posizione", + FileAccess.file_exists(migrated_path)) + migrated.name = "Attuale" + migrated.best = 950 + Profile.save_profile(migrated, migrated_path) + var preferred := Profile.load_profile(migrated_path, legacy_path) + check("rinomina: un profilo nuovo esistente prevale sul vecchio", + preferred.name == "Attuale" and preferred.best == 950) + check("rinomina: percorso legacy risolto accanto al nuovo user data", + Profile.legacy_profile_path().ends_with("/Zombie Room FPS/zombie_fps_profile.cfg")) + DirAccess.remove_absolute(ProjectSettings.globalize_path(legacy_path)) + DirAccess.remove_absolute(ProjectSettings.globalize_path(migrated_path)) DirAccess.remove_absolute(ProjectSettings.globalize_path(TEST_PATH)) print("%d controlli superati, %d falliti" % [checks - failures, failures]) quit(1 if failures > 0 else 0) diff --git a/tools/test_rebalance_branding.gd b/tools/test_rebalance_branding.gd new file mode 100644 index 0000000..4fc709c --- /dev/null +++ b/tools/test_rebalance_branding.gd @@ -0,0 +1,89 @@ +extends SceneTree +## Exact damage regression + visible game identity, no public score submissions. +const Player = preload("res://scripts/player.gd") +const Grenade = preload("res://scripts/grenade.gd") +const Boss = preload("res://scripts/alien_grunt.gd") +const Target = preload("res://tools/grenade_test_target.gd") +var checks := 0 +var failures := 0 + +func _initialize() -> void: + call_deferred("run_test") + +func check(ok: bool, label: String) -> void: + checks += 1 + if not ok: + failures += 1 + print("PASS: " if ok else "FAIL: ", label) + +func run_test() -> void: + check(Player.BULLET_DAMAGE == 17.5, "fucile: 35 -> 17,5 senza arrotondamento") + check(Grenade.MAX_DAMAGE == 320 and Grenade.BLAST_RADIUS == 5, + "granata: 160 -> 320, raggio invariato a 5m") + check(Boss.stats_for_wave(10).ranged == 8 and Boss.stats_for_wave(20).ranged == 10, + "proiettili alieni invariati a tutti i gradi") + check(ProjectSettings.get_setting("application/config/name") == "Work-Death Balance", + "titolo nativo del progetto aggiornato") + var icon: Texture2D = load(ProjectSettings.get_setting("application/config/icon")) + check(icon != null and icon.get_size() == Vector2(512, 512), "icona del progetto caricata, 512x512") + var preset := ConfigFile.new() + preset.load("res://export_presets.cfg") + check(preset.get_value("preset.0.options", "html/export_icon", false), + "favicon Web abilitata tramite esportazione icona ufficiale") + var world = load("res://scenes/main.tscn").instantiate() + root.add_child(world) # first real renamed startup also exercises profile migration + world.player.set_physics_process(false) + world.player.hp = 100000 + world.player.position = Vector3(0, 0.1, 2) + Input.mouse_mode = Input.MOUSE_MODE_VISIBLE + var title_found := false + for label in world.start_screen.find_children("*", "Label", true, false): + if label.text == "Work-Death Balance": + title_found = true + check(title_found, "menu mostra Work-Death Balance") + check(world.leaderboard.disabled, "build di prova non invia punteggi pubblici") + var zombie = load("res://scripts/zombie.gd").new() + zombie.player = world.player + zombie.position = Vector3(0, 0.05, -3) + world.add_child(zombie) + zombie.entering = false + zombie.set_physics_process(false) + world.player.camera.look_at(zombie.position + Vector3.UP) + await physics_frame + await physics_frame + world.player.shoot() + check(is_equal_approx(zombie.hp, 82.5), "colpo reale al corpo: esattamente 17,5 danni") + world.player.camera.look_at(zombie.position + Vector3.UP * 1.7) + world.player.shoot() + check(is_equal_approx(zombie.hp, 38.75), "headshot zombie: 43,75 danni, moltiplicatore 2,5 invariato") + zombie.queue_free() + await process_frame + var near := Target.new() + near.position = Vector3(0, 0, -1) + world.add_child(near) + var half := Target.new() + half.position = Vector3(0, 0, -2.5) + world.add_child(half) + var far := Target.new() + far.position = Vector3(0, 0, -6) + world.add_child(far) + var grenade := Grenade.new() + grenade.position = Vector3(0, 0.9, 0) + grenade.thrower = world.player + world.add_child(grenade) + grenade.freeze = true + var hp_before: float = world.player.hp + var player_distance: float = grenade.position.distance_to(world.player.position + Vector3.UP * 0.9) + var expected_self_damage := 320.0 * (1.0 - player_distance / 5.0) + grenade._request_detonation(grenade.position) + await process_frame + await process_frame + check(is_equal_approx(near.damage_taken, 256.0), "esplosione a 1m: 256 danni, decadimento lineare invariato") + check(is_equal_approx(half.damage_taken, 160.0), "esplosione a 2,5m: 160 danni, il doppio di prima") + check(far.damage_taken == 0, "nessun danno oltre il raggio originale") + check(is_equal_approx(hp_before - world.player.hp, expected_self_damage), + "danno dell'esplosione al tiratore raddoppiato con lo stesso decadimento") + world.queue_free() + await process_frame + print(checks - failures, " controlli superati, ", failures, " falliti") + quit(1 if failures else 0) diff --git a/tools/test_rebalance_branding.gd.uid b/tools/test_rebalance_branding.gd.uid new file mode 100644 index 0000000..5f9c095 --- /dev/null +++ b/tools/test_rebalance_branding.gd.uid @@ -0,0 +1 @@ +uid://dmlx8rbl8u4ub