HUD pass, weapon and trinket rework, and font arrows

HUD:
- Hotbars fit a quarter of the screen, dropping to small then tiny slots
  (thinner outlines when tiny), with cooldown bars matching their size.
- HUD elements are drawn on their own layer and composited at one
  opacity: hotbar slots are see-through until hovered, the rest fade
  while hovered.
- Rival windows fit the right column in at most two rows, shrinking
  icons then font, then dropping icons for name, health and ping bars.
- Tooltips number equipped items' slots and say Recoil for weapons that
  only push back. Previews use arrows, spread uses degrees.
- Crosshair fills in and shakes with charge, and weapons shake after
  firing until their cooldown ends. DPS and speed meters off by default.

Gameplay:
- Dual Wield stacks: each copy fires another recently held weapon from
  a fanned off-hand, at its own level's cut.
- Flamethrower: no slowing, smaller fireball with some recoil, and an
  airblast that knocks others away.
- Iron Weights slam on any landing faster than a plain fall (platforms
  too), stacking radius and damage, and speeding fast falls.
- Stuck arrows are one-way ledges until they fade, matching their
  (slightly larger) hit size. Attacks now collide with slopes.
- Halved Tesla Coil, Recoil Coil, Diazepam, Shock Absorber, Feather and
  screenshake values.

Art and tools:
- Weapon and trinket icons merged into one sheet each (normal, small,
  tiny), cells in catalog order. Grimoire wisps stand upright.
- Fonts gain the degree sign and four arrows, decoded from UTF-8.
- Menu fighter preview walks at quarter speed.
- Unequip from either catalog tab by right click or gamepad.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shweetandClaude Opus 5.5 committed 2026-11-08 16:43:45 -05:00
1 parent 6003175173
commit 6f112ab231
54 files changed
+1150 -3046

No files matched your search

+23 -6
View File
@@ -26,6 +26,19 @@ FONTS = ROOT / "resources" / "font"
TTF = FONTS / "pixel_purl.ttf"
FIRST = 32
COUNT = 95
# Characters past ASCII, each after the last in its own cell (written into the .fnt as UTF-8).
EXTRA = "°←↑→↓"
# Small glyphs drawn here instead of taken from the TTF: the degree sign a pixel wider than its own, and arrows (the
# TTF has none), the up one turned for the rest.
ARROW_UP = ["..##..", ".####.", "######", "..##..", "..##..", "..##.."]
ARROW_RIGHT = ["".join(ARROW_UP[len(ARROW_UP) - 1 - y][x] for y in range(len(ARROW_UP))) for x in range(len(ARROW_UP[0]))]
SMALL_DRAWN = {
"°": {"rows": [".##.", "#..#", "#..#", "#..#", ".##."], "left": 0, "top": 2, "advance": 5},
"←": {"rows": [row[::-1] for row in ARROW_RIGHT], "left": 0, "top": 5, "advance": 7},
"↑": {"rows": ARROW_UP, "left": 0, "top": 5, "advance": 7},
"→": {"rows": ARROW_RIGHT, "left": 0, "top": 5, "advance": 7},
"↓": {"rows": ARROW_UP[::-1], "left": 0, "top": 5, "advance": 7},
}
COLUMNS = 16
EM_PIXELS = 16
LINE_HEIGHT = 12
@@ -77,12 +90,14 @@ TINY = {
'v': ["...", "#.#", "#.#", "#.#", ".#."], 'w': [".....", "#...#", "#.#.#", "#.#.#", ".#.#."],
'x': ["...", "#.#", ".#.", ".#.", "#.#"], 'y': ["...", "#.#", "#.#", ".##", "..#", "##."],
'z': ["...", "###", ".#.", "#..", "###"], '{': [".##", ".#.", "##.", ".#.", ".##"], '|': ["#", "#", "#", "#", "#"],
'}': ["##.", ".#.", ".##", ".#.", "##."], '~': ["....", ".#.#", "#.#."],
'}': ["##.", ".#.", ".##", ".#.", "##."], '~': ["....", ".#.#", "#.#."], '°': [".##.", "#..#", ".##."],
'←': ["....", ".#..", "####", ".#.."], '↑': [".#.", "###", ".#.", ".#."], '→': ["....", "..#.", "####", "..#."],
'↓': ["...", ".#.", ".#.", "###", ".#."],
}
def characters():
return [chr(FIRST + i) for i in range(COUNT)]
return [chr(FIRST + i) for i in range(COUNT)] + list(EXTRA)
# Each glyph as rows of '#'/'.' with where its top-left sits relative to the pen at the line's top, and its advance.
@@ -97,8 +112,10 @@ def tiny_glyphs():
def small_glyphs():
font = ImageFont.truetype(str(TTF), EM_PIXELS)
glyphs = {}
glyphs = dict(SMALL_DRAWN)
for character in characters():
if character in glyphs:
continue
# Drawn with the line's top at the origin's y, as the game always has; the bounding box gives the pixels.
image = Image.new("L", (64, 64), 0)
draw = ImageDraw.Draw(image)
@@ -127,7 +144,7 @@ def font_write(name, glyphs, scale, line_height, outline):
right = max(glyph["left"] + len(glyph["rows"][0]) for glyph in glyphs.values() if glyph["rows"])
bottom = max(glyph["top"] + len(glyph["rows"]) for glyph in glyphs.values() if glyph["rows"])
cell_w, cell_h = right - left, max(bottom, line_height)
rows = (COUNT + COLUMNS - 1) // COLUMNS
rows = (len(glyphs) + COLUMNS - 1) // COLUMNS
image = Image.new("RGBA", (COLUMNS * (cell_w + 1), rows * (cell_h + 1)), (0, 0, 0, 0))
entries = {}
for i, character in enumerate(characters()):
@@ -145,10 +162,10 @@ def font_write(name, glyphs, scale, line_height, outline):
if scale is None:
del header["scale"]
lines = ["{"] + [f' {json.dumps(key)}: {json.dumps(value)},' for key, value in header.items()] + [' "glyphs": {']
lines += [f' {json.dumps(character)}: {json.dumps(entry)}{"," if i < COUNT - 1 else ""}'
lines += [f' {json.dumps(character, ensure_ascii=False)}: {json.dumps(entry)}{"," if i < len(entries) - 1 else ""}'
for i, (character, entry) in enumerate(entries.items())]
lines += [" }", "}"]
(FONTS / f"{name}.fnt").write_text("\n".join(lines) + "\n")
(FONTS / f"{name}.fnt").write_text("\n".join(lines) + "\n", encoding="utf-8")
print(f"{name}: {image.width}x{image.height}, cells {cell_w}x{cell_h}")