Rename to Work-Death Balance, add mascot favicon and rebalance rifle/grenade damage

This commit is contained in:
enne2
2026-10-15 17:15:16 +02:00
parent 2606fc5cc2
commit 21a1d5bf79
22 changed files with 352 additions and 39 deletions
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
+14 -4
View File
@@ -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"
}
}
+6 -3
View File
@@ -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()},
+36
View File
@@ -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))}
+25
View File
@@ -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)
+89
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://dmlx8rbl8u4ub