Rename to Work-Death Balance, add mascot favicon and rebalance rifle/grenade damage
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 173 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 238 KiB |
@@ -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
|
||||
+40
-19
@@ -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
|
||||
`<img id="status-splash">` 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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+3
-3
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
+2
-2
@@ -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="<meta name=\"description\" content=\"Zombie Room FPS — prototipo Godot. WASD, mouse, R ricarica.\"><style>/* La barra di progresso predefinita attraversa la scritta della splash: la rendo una linea ambra sottile in fondo allo schermo. */ #status-progress{appearance:none;-webkit-appearance:none;border:none;height:3px;width:22%;bottom:8px;background:rgba(255,196,64,.15);border-radius:2px} #status-progress::-moz-progress-bar{background:#ffc440} #status-progress::-webkit-progress-bar{background:rgba(255,196,64,.15)} #status-progress::-webkit-progress-value{background:#ffc440}</style>"
|
||||
html/head_include="<meta name=\"description\" content=\"Work-Death Balance — sopravvivi al turno in ufficio. Un gioco MatGames. WASD, mouse, R ricarica.\"><style>/* La barra di progresso predefinita attraversa la scritta della splash: la rendo una linea ambra sottile in fondo allo schermo. */ #status-progress{appearance:none;-webkit-appearance:none;border:none;height:3px;width:22%;bottom:8px;background:rgba(255,196,64,.15);border-radius:2px} #status-progress::-moz-progress-bar{background:#ffc440} #status-progress::-webkit-progress-bar{background:rgba(255,196,64,.15)} #status-progress::-webkit-progress-value{background:#ffc440}</style>"
|
||||
html/canvas_resize_policy=2
|
||||
html/focus_canvas_on_start=true
|
||||
html/experimental_virtual_keyboard=true
|
||||
|
||||
+3
-2
@@ -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")
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+2
-1
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()},
|
||||
|
||||
@@ -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))}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dmlx8rbl8u4ub
|
||||
Reference in New Issue
Block a user