0.2.3: the install icon keeps the 0.2.x mint rook (the owner's choice), refined: a narrower tower, lighter base and more padding so it sits well among launcher icons; manifest sizes, maskable and favicons re-exported (the ICO keeps its transparent mask)
No files matched your search
@@ -16,7 +16,7 @@
|
||||
- 设置新增四档文字大小,支持长译文换行;干员调配可逐干员选择中文、日语或跟随全局语音(#435、#436,PR #446,感谢 @7eLaryn)。
|
||||
- 补齐司霆惊蛰、赤刃明霄陈等干员技能期间的攻击与命中音,保留各技能音效自己的音量与播放概率(PR #410,感谢 @Convey123)。
|
||||
- AI 购买高阶卡时适当降低与队友开局盟约抢卡的倾向,并在混合对局中发送有限的表情回应(PR #407、#444,感谢 @IceCodeNew、@DDDarkstar;可用 `SP_BOT_EMOTES=0` 关闭表情)。
|
||||
- 增加本机对局恢复入口,避免同一浏览器的闲置窗口妨碍恢复已关闭窗口的席位(#431)。浏览器支持时可添加到桌面,精简包也自带原创的系列通用要塞图标、maskable 安全区版本和 favicon(PR #413,感谢 @Cromemadnd)。
|
||||
- 增加本机对局恢复入口,避免同一浏览器的闲置窗口妨碍恢复已关闭窗口的席位(#431)。浏览器支持时可添加到桌面,沿用 0.2.x 的薄荷绿城堡并精修,精简包也自带该图标、maskable 安全区版本和 favicon(PR #413,感谢 @Cromemadnd)。
|
||||
- 确认页可由房主发起全体真人一致同意的开局重刷,保留房间与个人配置;取消恢复剩余确认时间,成功后重新确认(#77、PR #408,感谢 @Siornya)。这是复刻版便捷功能。
|
||||
- 修复模型加载较慢时丢失入场动画,按经过时间接续,攻击或死亡后不再补播过期入场(PR #416 提示的加载问题,感谢 @Cromemadnd)。
|
||||
|
||||
|
||||
@@ -8,15 +8,17 @@ The original project icons below are shipped static files, not downloaded game a
|
||||
|
||||
## Original project application icons (#413)
|
||||
|
||||
`public/icons/app.svg` is the original vector master for the **卫戍协议 series**, independent of any season.
|
||||
Three battlements lead into a shield-shaped wall around one square tactical cell: defending a position on a board.
|
||||
The only colours are charcoal `#242424` and off-white `#f2f2f2`; the mark contains no text, gradients or fine texture.
|
||||
It contains no 「盟约」 lettering, Alliance badge or season-exclusive colours / elements.
|
||||
By the maintainer's revised decision of 2026-10-10, **沿用 0.2.x 的薄荷绿城堡并精修**.
|
||||
`public/icons/app.svg` retains the existing mint rook's three battlements, inset tower and splayed base.
|
||||
The selected refinement (B) narrows the tower, balances the battlements, reduces the base thickness and adds breathing
|
||||
room. On the 512 px canvas the mark spans x=128–384 and y=88–424; the tower is 176 px wide and the base is 32 px thick.
|
||||
The original colours remain mint `#4ed8af` and dark `#0c0f0e`, with no text, gradients, gloss or texture.
|
||||
|
||||
**Originality statement (2026-10-10):** the SVG geometry was drawn from basic rectangles and polygons for this
|
||||
project. No official logo, artwork, icon or font was used, downloaded, embedded, traced or redrawn to make it.
|
||||
**Provenance / originality statement (2026-10-10):** this is a proportion refinement of this project's existing
|
||||
`public/icons/app.svg` from before `ba5b998a`, expressed as basic rectangles and polygons.
|
||||
No official logo, artwork, icon or font was used, downloaded, embedded, traced or redrawn for this refinement.
|
||||
The vector and exports are project-owned work under the repository's GPL-3.0-or-later licence, separate from the
|
||||
Hypergryph / Yostar game assets below. The abstract fortification / tactical-board idea does not assert official branding.
|
||||
Hypergryph / Yostar game assets below. The castle mark does not assert official branding.
|
||||
|
||||
Rebuild offline with Python 3 and Pillow (the same image library used by local-extract):
|
||||
|
||||
@@ -28,6 +30,8 @@ python3 tools/export-app-icons.py --out /tmp/stronghold-icons
|
||||
|
||||
The script reads only the master SVG's flat rectangle / polygon geometry, renders at 8× and downsamples; unsupported
|
||||
SVG elements fail rather than silently disappearing. It needs no game art, fonts, browser or network.
|
||||
ICO frames use the same native-size renders as the PNG favicons, with explicit 24-bit DIBs and opaque 1-bit masks
|
||||
whose rows are padded to 4 bytes. This avoids malformed transparency masks in Pillow's RGB ICO export.
|
||||
|
||||
| Files in `public/icons/` | Use |
|
||||
|---|---|
|
||||
@@ -37,7 +41,7 @@ SVG elements fail rather than silently disappearing. It needs no game art, fonts
|
||||
| `apple-touch-icon.png` | 180 px home-screen icon |
|
||||
| `app.svg` | Scalable favicon and editable source |
|
||||
|
||||
The maskable mark is scaled to 87.5% around the canvas centre. Its outermost vertex is about 34.8% of the canvas
|
||||
The maskable mark is scaled to 87.5% around the canvas centre. Its outermost vertex is about 36.1% of the canvas
|
||||
width from that centre; even antialiased edge pixels stay inside the **40%-radius safe circle** specified by
|
||||
[Web Application Manifest §2.3](https://www.w3.org/TR/appmanifest/#icon-masks).
|
||||
The background remains opaque to every edge; launcher masks may remove the background outside the safe zone.
|
||||
@@ -46,8 +50,9 @@ favicons and Apple icon; `public/manifest.json` lists all four install icons. Al
|
||||
and included by the full and lite package allowlist; no changes to `data/assets.json` are needed.
|
||||
|
||||
`test/ui/pwa-install-023.test.js` checks decoded pixels, mask safety and references; `test/package.test.js` checks
|
||||
packaging inclusion. The local handoff holds old/new light/dark, small-size and phone-layout previews with their
|
||||
render script. These are offline review renders, not evidence of browser or native OS installation.
|
||||
packaging inclusion. The local handoff `claude-review-023/icon-v2/` holds the original and A/B/C SVGs, light/dark
|
||||
256 px and 16/32/48/96 px comparisons, and phone-layout / mask previews with their render script.
|
||||
These are offline review renders, not evidence of browser or native OS installation.
|
||||
|
||||
## Running
|
||||
|
||||
|
||||
@@ -115,8 +115,11 @@ refused recovery can return to the previous identity and nickname. It does not i
|
||||
Without BroadcastChannel it refuses shared recovery. Server restart / expiry clears
|
||||
obsolete metadata on the next welcome. A spectator never registers a recoverable player seat.
|
||||
|
||||
`manifest.json` enables browser installation. The original season-independent fortress / shield mark in
|
||||
`public/icons/app.svg` uses neutral greys and no official art, logos, icons, fonts or season-specific insignia.
|
||||
`manifest.json` enables browser installation. The maintainer's revised decision of 2026-10-10 is to
|
||||
**沿用 0.2.x 的薄荷绿城堡并精修**: `public/icons/app.svg` keeps the existing three-battlement rook, inset tower
|
||||
and splayed base in mint `#4ed8af` on dark `#0c0f0e`. Refinement B narrows the tower, balances the battlements,
|
||||
thins the base and increases breathing room. It has no lettering, gradients or gloss and uses no official art,
|
||||
logos, icons or fonts.
|
||||
`tools/export-app-icons.py` exports 192/512 px PNGs for `any` and separate opaque `maskable` icons: the latter scale
|
||||
only the mark to 87.5%, keeping every foreground pixel inside the central circle of radius 40% of the canvas.
|
||||
The same master supplies 16/32/48 px PNG / ICO favicons and the 180 px Apple touch icon linked from `index.html`.
|
||||
|
||||
@@ -275,19 +275,27 @@ Based on @Cromemadnd's proposal, with a one-use install controller. Buttons appe
|
||||
standalone/fullscreen and installed sessions hide them. Accept, dismiss, failure and duplicate clicks all consume the
|
||||
same offer once. See [the prompt API](https://developer.mozilla.org/en-US/docs/Web/API/BeforeInstallPromptEvent/prompt)
|
||||
and [installability requirements](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/Making_PWAs_installable).
|
||||
The 2026-10-10 icon revision replaces the previous mint rook with an original, season-independent fortress / shield
|
||||
and defended square. Its neutral grey palette and geometry use no official logos, artwork, icons or fonts and no
|
||||
Alliance lettering or seasonal insignia. `public/icons/app.svg` is the master; `tools/export-app-icons.py` exports
|
||||
The maintainer revised the icon direction on 2026-10-10: **沿用 0.2.x 的薄荷绿城堡并精修**.
|
||||
The selected B refinement keeps the previous mint rook's three battlements, inset tower and splayed base,
|
||||
with a narrower tower, balanced battlements, thinner base and more breathing room. It retains mint `#4ed8af`
|
||||
on dark `#0c0f0e`, without text, gradients, gloss or official logos, artwork, icons or fonts.
|
||||
The source is the project's `public/icons/app.svg` from before `ba5b998a`; the local `icon-v2` review includes
|
||||
the original and A/B/C proposals at large and small sizes, on light/dark backgrounds and simulated home screens.
|
||||
`public/icons/app.svg` is the selected B master; `tools/export-app-icons.py` exports
|
||||
192/512 px `any` and separate opaque `maskable` PNGs (the mark stays inside the central 40%-radius safe circle),
|
||||
16/32/48 px PNG / ICO favicons and a 180 px Apple touch icon. All ship outside optional game art, including lite
|
||||
packages. See ASSETS.md for the source, reproduction command and originality statement.
|
||||
The offline revision also corrects the ICO writer's transparency masks: each 16/32/48 px frame now uses its own
|
||||
native-size render and 4-byte-aligned opaque mask rows. Decoding previously exposed transparent holes in the 48 px
|
||||
frame; the regression checks mask layout / opacity and compares every ICO colour pixel with its PNG counterpart.
|
||||
No service worker or offline cache is installed; the server is still required.
|
||||
The lobby keeps Settings immediately before How to Play even when the optional install/recovery buttons appear;
|
||||
the full browser run caught and corrected an initial insertion between that established pair.
|
||||
The settings-entry check tests the absent board-style/background-music option labels, allowing the text-size help
|
||||
to explain that the board keeps its scale.
|
||||
|
||||
Tests: offer lifecycle/disposal, decoded PNG dimensions / opacity / maskable safe zone, favicon references and packaging inclusion.
|
||||
Tests: offer lifecycle/disposal, decoded PNG dimensions / opacity / mint palette / maskable safe zone,
|
||||
three distinct battlements and an inset tower at 16/32/48 px, favicon references and packaging inclusion.
|
||||
The initial feature also had real Chrome manifest and installability checks, and a synthetic
|
||||
dismissed offer through the actual button. Initial incognito rejection was a test-environment limitation; the real
|
||||
installability check runs in Chrome's ordinary temporary profile. Native OS installation itself is not performed.
|
||||
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 884 B |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 2.4 KiB |
@@ -1,10 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
|
||||
<title>Stronghold Protocol — fortress and defended square</title>
|
||||
<desc>Original geometric project mark. No official artwork, lettering or seasonal insignia.</desc>
|
||||
<rect width="512" height="512" fill="#242424"/>
|
||||
<title>Stronghold Protocol — refined mint rook</title>
|
||||
<desc>Refines the existing 0.2.x project castle: three battlements, inset tower and splayed base. Original project geometry; no official artwork, lettering, gradients or texture.</desc>
|
||||
<rect width="512" height="512" fill="#0c0f0e"/>
|
||||
<g id="mark">
|
||||
<polygon fill="#f2f2f2" points="112,112 176,112 176,152 224,152 224,112 288,112 288,152 336,152 336,112 400,112 400,304 352,352 256,416 160,352 112,304"/>
|
||||
<polygon fill="#242424" points="176,216 336,216 336,280 304,312 256,344 208,312 176,280"/>
|
||||
<rect x="232" y="240" width="48" height="48" fill="#f2f2f2"/>
|
||||
<polygon fill="#4ed8af" points="128,88 184,88 184,128 224,128 224,88 288,88 288,128 328,128 328,88 384,88 384,184 344,224 344,352 384,392 384,424 128,424 128,392 168,352 168,224 128,184"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 659 B After Width: | Height: | Size: 608 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1021 B |
|
Before Width: | Height: | Size: 308 B After Width: | Height: | Size: 275 B |
|
Before Width: | Height: | Size: 404 B After Width: | Height: | Size: 356 B |
|
Before Width: | Height: | Size: 569 B After Width: | Height: | Size: 422 B |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 11 KiB |
@@ -43,7 +43,7 @@ test('installed / standalone hides stale offers; disposing removes listeners', a
|
||||
assert.equal(offer().e.defaultPrevented, false);
|
||||
const native = rig(true); native.offer(); assert.equal(native.pwa.available(), false); native.pwa.dispose();
|
||||
});
|
||||
test('manifest provides opaque 192/512 icons for both purposes with a safe maskable mark', () => {
|
||||
test('manifest provides opaque mint-rook 192/512 icons for both purposes with a safe maskable mark', () => {
|
||||
const m = JSON.parse(readFileSync(new URL('../../public/manifest.json', import.meta.url)));
|
||||
assert.equal(m.start_url, '/'); assert.equal(m.scope, '/');
|
||||
assert.deepEqual(m.icons.map((i) => `${i.purpose}:${i.sizes}`).sort(),
|
||||
@@ -55,13 +55,14 @@ test('manifest provides opaque 192/512 icons for both purposes with a safe maska
|
||||
const size = Number(icon.sizes.split('x')[0]);
|
||||
const { w, h, rgba } = decodePng(bytes);
|
||||
assert.equal(w, size); assert.equal(h, size);
|
||||
let foreground = 0;
|
||||
let foreground = 0, solidMint = 0;
|
||||
for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) {
|
||||
const i = (y * w + x) * 4;
|
||||
assert.equal(rgba[i + 3], 255, 'opaque edge-to-edge');
|
||||
assert.equal(rgba[i], rgba[i + 1], 'season-independent neutral palette');
|
||||
assert.equal(rgba[i], rgba[i + 2]);
|
||||
if (rgba[i] === 36) continue; // #242424 background
|
||||
const [r, g, b] = rgba.subarray(i, i + 3);
|
||||
if (r === 12 && g === 15 && b === 14) continue; // Original #0c0f0e background
|
||||
assert.ok(g >= b && b > r, 'mint palette, including antialiased edges');
|
||||
if (r === 78 && g === 216 && b === 175) solidMint++; // Original #4ed8af
|
||||
foreground++;
|
||||
if (icon.purpose === 'maskable') {
|
||||
// W3C safe zone is a circle, not merely a centred 80% square.
|
||||
@@ -70,6 +71,7 @@ test('manifest provides opaque 192/512 icons for both purposes with a safe maska
|
||||
}
|
||||
}
|
||||
assert.ok(foreground > size * size * 0.12, 'the exported mark is not empty or tiny');
|
||||
assert.ok(solidMint > size * size * 0.12, 'the original flat mint identity is retained');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -86,7 +88,7 @@ test('favicon and Apple links resolve to real sizes, including all legacy ICO fr
|
||||
assert.ok(link.includes(`sizes="${w}x${h}"`), href);
|
||||
} else if (href.endsWith('.svg')) {
|
||||
assert.match(bytes.toString(), /viewBox="0 0 512 512"/);
|
||||
assert.doesNotMatch(bytes.toString(), /<(?:image|text|use|foreignObject)\b|(?:href|font-family)=/);
|
||||
assert.doesNotMatch(bytes.toString(), /<(?:image|text|use|foreignObject|linearGradient|radialGradient|filter)\b|(?:href|font-family)=/);
|
||||
} else {
|
||||
assert.equal(bytes.readUInt16LE(2), 1);
|
||||
assert.equal(bytes.readUInt16LE(4), 3);
|
||||
@@ -95,11 +97,37 @@ test('favicon and Apple links resolve to real sizes, including all legacy ICO fr
|
||||
assert.equal(bytes[entry], size); assert.equal(bytes[entry + 1], size);
|
||||
const length = bytes.readUInt32LE(entry + 8), offset = bytes.readUInt32LE(entry + 12);
|
||||
assert.ok(length > 0 && offset >= 54 && offset + length <= bytes.length);
|
||||
// Decode the legacy 24-bit DIB and its 1-bit transparency mask. Both row
|
||||
// types need 4-byte alignment; malformed masks can punch holes in an icon.
|
||||
assert.equal(bytes.readUInt32LE(offset), 40);
|
||||
assert.equal(bytes.readInt32LE(offset + 4), size);
|
||||
assert.equal(bytes.readInt32LE(offset + 8), size * 2);
|
||||
assert.equal(bytes.readUInt16LE(offset + 14), 24);
|
||||
assert.equal(bytes.readUInt32LE(offset + 16), 0);
|
||||
const colorStride = Math.ceil(size * 3 / 4) * 4, maskStride = Math.ceil(size / 32) * 4;
|
||||
assert.equal(length, 40 + (colorStride + maskStride) * size, `${size}px aligned ICO frame`);
|
||||
const mask = bytes.subarray(offset + 40 + colorStride * size, offset + length);
|
||||
assert.ok(mask.every((byte) => byte === 0), `${size}px ICO is fully opaque`);
|
||||
const { rgba } = decodePng(readFileSync(new URL(`../../public/icons/favicon-${size}.png`, import.meta.url)));
|
||||
for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) {
|
||||
const bmp = offset + 40 + (size - 1 - y) * colorStride + x * 3;
|
||||
const png = (y * size + x) * 4;
|
||||
for (let c = 0; c < 3; c++) assert.equal(bytes[bmp + 2 - c], rgba[png + c], `${size}px ICO matches PNG`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const size of [16, 32, 48]) {
|
||||
const { w, h } = decodePng(readFileSync(new URL(`../../public/icons/favicon-${size}.png`, import.meta.url)));
|
||||
const { w, h, rgba } = decodePng(readFileSync(new URL(`../../public/icons/favicon-${size}.png`, import.meta.url)));
|
||||
assert.equal(w, size); assert.equal(h, size);
|
||||
// At actual favicon sizes, the three battlements must stay separated and the
|
||||
// tower must remain narrower than its base. Sample visible raster features.
|
||||
const row = (y) => Array.from({ length: w }, (_, x) => rgba[(Math.floor(y * h) * w + x) * 4 + 1] > 100);
|
||||
const runs = (pixels) => pixels.filter((on, x) => on && !pixels[x - 1]).length;
|
||||
const crown = row(0.19), tower = row(0.56), base = row(0.79);
|
||||
assert.equal(runs(crown), 3, `${size}px battlement gaps stay open`);
|
||||
assert.equal(runs(tower), 1, `${size}px tower stays connected`);
|
||||
assert.equal(runs(base), 1, `${size}px base stays connected`);
|
||||
assert.ok(tower.filter(Boolean).length < base.filter(Boolean).length, `${size}px inset tower remains legible`);
|
||||
}
|
||||
});
|
||||
@@ -6,7 +6,9 @@ only this source's flat rect/polygon vocabulary; unsupported SVG fails loudly.
|
||||
No fonts, game art, browser, network or platform image service is involved.
|
||||
"""
|
||||
import argparse
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
@@ -54,6 +56,26 @@ def render(size, maskable=False):
|
||||
return image.resize((size, size), Image.Resampling.BOX)
|
||||
|
||||
|
||||
def write_ico(path):
|
||||
# Native-size renders preserve the same pixels as each PNG favicon. Write
|
||||
# the DIBs explicitly: Pillow's RGB ICO writer does not pad 1-bit mask rows
|
||||
# to 4 bytes, and can reuse the last size's mask for every smaller frame.
|
||||
sizes = (16, 32, 48)
|
||||
directory, frames = bytearray(), bytearray()
|
||||
offset = 6 + 16 * len(sizes)
|
||||
for size in sizes:
|
||||
dib = BytesIO()
|
||||
render(size).save(dib, format="DIB")
|
||||
frame = bytearray(dib.getvalue())
|
||||
# ICO DIB height includes both the colour bitmap and the AND mask.
|
||||
struct.pack_into("<i", frame, 8, size * 2)
|
||||
frame.extend(bytes(((size + 31) // 32) * 4 * size)) # Opaque, padded AND rows
|
||||
directory.extend(struct.pack("<BBBBHHII", size, size, 0, 0, 1, 24, len(frame), offset))
|
||||
frames.extend(frame)
|
||||
offset += len(frame)
|
||||
path.write_bytes(struct.pack("<HHH", 0, 1, len(sizes)) + directory + frames)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--out", type=Path, default=ROOT / "public/icons")
|
||||
@@ -67,7 +89,7 @@ def main():
|
||||
render(size).save(args.out / f"favicon-{size}.png")
|
||||
render(180).save(args.out / "apple-touch-icon.png")
|
||||
# BMP-backed frames also work in older ICO readers; PNG favicons ship separately.
|
||||
render(48).save(args.out / "favicon.ico", sizes=[(16, 16), (32, 32), (48, 48)], bitmap_format="bmp")
|
||||
write_ico(args.out / "favicon.ico")
|
||||
print(f"Exported 9 icon files to {args.out}")
|
||||
|
||||
|
||||
|
||||