feedback5: the public dev branch is marked as a development build (the owner's decision of 2026-10-06): version 0.2.0-dev (title footer, boot banner, /healthz), a README warning under the title (unstable, not for public servers, how to report, shared browser storage, no release zips), a visible 开发版 tag on the title screen (zh / en), a boot-banner warning for hosts, and tools/package.mjs refuses a dev version without --allow-dev; test/version.test.js checks all of it

This commit is contained in:
sganggs committed 2026-10-06 19:05:40 +08:00
1 parent 6499f4b32d
commit 6b08971203
10 files changed
+65 -15

No files matched your search

+7 -2
View File
@@ -1,8 +1,13 @@
# 卫戍协议:盟约 · Stronghold Protocol: Alliance
> [!WARNING]
> **开发版(dev 分支):不稳定,请勿用于公开服务器。** 这里是 0.2.0 的开发中代码,规则、数据和存档格式随时会变,可能有未发现的问题。想玩或开服请使用 [Releases](../../releases/latest) 页面的正式版整合包,或 `master` 分支。反馈问题时请注明「dev」和版本号(标题页底部、启动横幅和 `/healthz` 都会显示 `0.2.0-dev`)。开发版与正式版在同一地址下共用浏览器里保存的设置和登录信息,来回切换后如果界面异常,清除这个网站的数据即可。开发版不提供整合包。
>
> *Development branch: unstable — do not run it as a public server. Rules, data and saved settings may change at any time. Play or host the latest release (or the `master` branch) instead; when you report a bug from this branch, say "dev" and the version shown on the title screen. It shares the browser's saved settings with a release served at the same address — clear the site's data if the interface misbehaves after switching. No release zips are built from this branch.*
《明日方舟》季节性自走棋塔防玩法「卫戍协议:盟约」的**非官方同人复刻**:浏览器即开即玩,单人或 1–4 人联机合作。
![version](https://img.shields.io/badge/version-0.1.4-2ea44f)
![version](https://img.shields.io/badge/version-0.2.0--dev-orange)
![license](https://img.shields.io/badge/code%20license-GPL--3.0--or--later-blue)
![node](https://img.shields.io/badge/node-22%20%7C%2024-339933)
@@ -37,7 +42,7 @@ English summary: [below](#english).
- **独立模拟**(单人)与**同盟模拟**(1–4 人**合作**,没有 PvP;空位可以加 AI 队友)。
- 服务器是一个 Node.js 程序,**战斗在各玩家的浏览器里模拟**(和官方一样),服务器只管经济与回合,一台低功耗小主机就能开服。
- 当前版本 0.1.4:修复了 0.1.3 发布后玩家和 GitHub 上反馈的问题,并合并了多位贡献者的 PR,详见 [CHANGELOG.md](CHANGELOG.md)。仍有少数规则按推断实现,与官方不一致的地方欢迎在 Issue 里反馈。
- 本分支是 0.2.0 开发版(未发布);最新正式版是 0.1.4,见 [CHANGELOG.md](CHANGELOG.md)。仍有少数规则按推断实现,与官方不一致的地方欢迎在 Issue 里反馈。
## 功能一览
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "stronghold-protocol-alliance",
"version": "0.1.4",
"version": "0.2.0-dev",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "stronghold-protocol-alliance",
"version": "0.1.4",
"version": "0.2.0-dev",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "stronghold-protocol-alliance",
"version": "0.1.4",
"version": "0.2.0-dev",
"private": true,
"description": "明日方舟「卫戍协议:盟约」非官方同人联机网页复刻(1–4 人合作,Node.js 服务器 + 浏览器客户端)",
"license": "GPL-3.0-or-later",
+10
View File
@@ -261,3 +261,13 @@ html[data-lang="en"] .title-login .btn--xl { letter-spacing: .3em; }
font-size: .13rem;
color: var(--text-dim);
}
/* a development build (shared/constants.js DEV_BUILD): an unmissable tag in the footer */
.title-dev {
padding: .03rem .1rem;
border: 1px solid #ff9f43;
border-radius: .04rem;
color: #ffb066;
background: rgba(255, 159, 67, .14);
font-weight: 700;
letter-spacing: .02em;
}
+2 -1
View File
@@ -1014,5 +1014,6 @@
"还有 {funds} 资金未使用。休整期结束时,本回合的剩余资金将清零。确定准备就绪吗?": "You still have {funds} unspent Funds. The round's leftover Funds are cleared when the Rest Phase ends. Ready anyway?",
"选择「{name}」的朝向": "Choose {name}'s facing",
"选择朝向": "Choose a facing",
"朝向:{dir}": "Facing: {dir}"
"朝向:{dir}": "Facing: {dir}",
"开发版 · 不稳定,请勿用于公开服务器": "Development build · unstable, not for public servers"
}
+2 -1
View File
@@ -10,7 +10,7 @@
// pure CSS/SVG (radar, ridgelines, glow), so it never issues a request that can 404.
import { useMemo, useState } from '../../vendor/hooks.module.js';
import { NAME_MAX_LEN, APP_VERSION } from '../../../shared/constants.js';
import { NAME_MAX_LEN, APP_VERSION, DEV_BUILD } from '../../../shared/constants.js';
import { html, Button, Icon, MicroLabel, TextField, PingPill } from '../ui/components.js';
import { GuideButton } from '../ui/guide.js';
import { toast } from '../ui/toasts.js';
@@ -280,6 +280,7 @@ export function TitleScreen() {
<footer class="title-foot">
<span>${t('非官方同人复刻 · 游戏素材版权归 上海鹰角网络 / Yostar 所有')}</span>
<${MicroLabel}>v${APP_VERSION} · WEB SIMULATION<//>
${DEV_BUILD ? html`<span class="title-dev" role="note">${t('开发版 · 不稳定,请勿用于公开服务器')}</span>` : null}
</footer>
</div>`;
}
+2 -1
View File
@@ -10,7 +10,7 @@
import fs from 'node:fs';
import os from 'node:os';
import { fileURLToPath } from 'node:url';
import { APP_VERSION } from '../../shared/constants.js';
import { APP_VERSION, DEV_BUILD } from '../../shared/constants.js';
/** Non-internal IPv4 addresses as http URLs. @param {number} port */
export function lanUrls(port) {
@@ -36,6 +36,7 @@ export function isProcessEntry(metaUrl) {
/** Print the boot banner of a started server. @param {{ url: string, host: string, port: number }} srv */
export function printBanner(srv) {
console.log(`\n 卫戍协议:盟约 · Stronghold Protocol: Alliance v${APP_VERSION}`);
if (DEV_BUILD) console.log(' ! 开发版(dev 分支):不稳定,请勿用于公开服务器 · development build — unstable, not for public servers');
console.log(` Local: ${srv.url}`);
if (srv.host === '0.0.0.0' || srv.host === '::') {
for (const u of lanUrls(srv.port)) console.log(` LAN: ${u}`);
+4 -1
View File
@@ -5,7 +5,10 @@ import { N_ } from './i18n.js';
export const PROTOCOL_VERSION = 1;
/** Release version shown to players (title screen, server banner, /healthz). Kept equal to package.json "version"
* (test/version.test.js); PROTOCOL_VERSION above is the separate wire-format number. */
export const APP_VERSION = '0.1.4';
export const APP_VERSION = '0.2.0-dev';
/** A development build (the public `dev` branch): the title screen, the boot banner and the README say so, and
* tools/package.mjs refuses to build a release zip from it without --allow-dev. */
export const DEV_BUILD = /-dev$/.test(APP_VERSION);
export const MAX_SEATS = 4;
/**
+23 -4
View File
@@ -6,7 +6,8 @@ import assert from 'node:assert/strict';
import { readFileSync, existsSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { APP_VERSION, PROTOCOL_VERSION } from '../shared/constants.js';
import { APP_VERSION, PROTOCOL_VERSION, DEV_BUILD } from '../shared/constants.js';
import { isDevVersion } from '../tools/package.mjs';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const read = (p) => readFileSync(join(ROOT, p), 'utf8');
@@ -14,7 +15,9 @@ const pkg = JSON.parse(read('package.json'));
const lock = JSON.parse(read('package-lock.json'));
test('one release version: package.json, package-lock.json and APP_VERSION', () => {
assert.match(APP_VERSION, /^\d+\.\d+\.\d+$/);
// a release x.y.z, or x.y.z-dev on the public dev branch (the owner's decision of 2026-10-06)
assert.match(APP_VERSION, /^\d+\.\d+\.\d+(-dev)?$/);
assert.equal(DEV_BUILD, APP_VERSION.endsWith('-dev'));
assert.equal(pkg.version, APP_VERSION);
assert.equal(lock.version, APP_VERSION);
assert.equal(lock.packages[''].version, APP_VERSION);
@@ -26,11 +29,17 @@ test('CHANGELOG.md opens with the release version, and the README links it', ()
const log = read('CHANGELOG.md');
const first = log.match(/^## (\d+\.\d+\.\d+) — (\d{4}-\d{2}-\d{2})/m);
assert.ok(first, 'a "## x.y.z — date" heading');
assert.equal(first[1], APP_VERSION, 'the newest entry is the current version');
if (DEV_BUILD) {
// a dev build has no entry of its own yet: the newest one is the last release, older than the version in development
const num = (v) => v.split(/[.-]/).slice(0, 3).map(Number);
const [a, b] = [num(first[1]), num(APP_VERSION)];
assert.ok(a[0] < b[0] || (a[0] === b[0] && (a[1] < b[1] || (a[1] === b[1] && a[2] < b[2]))), `the newest entry ${first[1]} precedes ${APP_VERSION}`);
} else assert.equal(first[1], APP_VERSION, 'the newest entry is the current version');
assert.match(log, /^## 0\.1\.0 — 2026-10-02/m, 'the first public release stays listed');
const readme = read('README.md');
assert.match(readme, /\[CHANGELOG\.md\]\(CHANGELOG\.md\)/);
assert.match(readme, new RegExp(`badge/version-${APP_VERSION.replace(/\./g, '\\.')}-`), 'the README badge');
// shields.io escapes a '-' inside a badge field as '--'
assert.match(readme, new RegExp(`badge/version-${APP_VERSION.replace(/-/g, '--').replace(/\./g, '\\.')}-`), 'the README badge');
});
test('the release version is what players see', () => {
@@ -74,3 +83,13 @@ test('GPL-3.0-or-later: LICENSE, package metadata and notices', () => {
assert.match(aklz4, /SPDX-License-Identifier: BSD-3-Clause/);
assert.match(aklz4, /Copyright \(c\) 2022, Harry Huang/);
});
test('a development build says so everywhere a player or a host looks, and never packages as a release', () => {
if (!DEV_BUILD) return;
const readme = read('README.md');
assert.match(readme.split('\n').slice(0, 8).join('\n'), /开发版(dev 分支):不稳定,请勿用于公开服务器/, 'the README warns right under the title');
assert.match(read('public/js/screens/title.js'), /DEV_BUILD \? html`<span class="title-dev"/, 'the title screen footer carries the dev tag');
assert.match(read('server/http/boot.js'), /if \(DEV_BUILD\) console\.log\(/, 'the boot banner warns the host');
assert.ok(isDevVersion(APP_VERSION) && !isDevVersion('0.1.4'), 'tools/package.mjs treats the version as a development one');
assert.match(read('tools/package.mjs'), /isDevVersion\(p\.version\) && !o\.allowDev/, 'packaging a dev version needs --allow-dev');
});
+12 -2
View File
@@ -456,10 +456,13 @@ export function build(root, p, { out, install = true, force = false, keepStage =
return { zipPath, zipBytes: fileBytes(zipPath), files: staged.length, stage: keepStage ? stage : null };
}
const USAGE = 'usage: node tools/package.mjs [--lite] [--dry-run [--list]] [--out <dir>] [--force] [--keep-stage] [--no-install] [--allow-dirty] [--root <dir>]';
const USAGE = 'usage: node tools/package.mjs [--lite] [--dry-run [--list]] [--out <dir>] [--force] [--keep-stage] [--no-install] [--allow-dirty] [--allow-dev] [--root <dir>]';
/** Whether `version` is a development (pre-release) version, e.g. 0.2.0-dev. */
export const isDevVersion = (version) => /-/.test(String(version || ''));
export function parseArgs(argv) {
const o = { lite: false, dryRun: false, list: false, out: '', force: false, keepStage: false, install: true, allowDirty: false, root: REPO, help: false };
const o = { lite: false, dryRun: false, list: false, out: '', force: false, keepStage: false, install: true, allowDirty: false, allowDev: false, root: REPO, help: false };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--lite') o.lite = true;
@@ -469,6 +472,7 @@ export function parseArgs(argv) {
else if (a === '--keep-stage') o.keepStage = true;
else if (a === '--no-install') o.install = false;
else if (a === '--allow-dirty') o.allowDirty = true;
else if (a === '--allow-dev') o.allowDev = true;
else if (a === '--help' || a === '-h') o.help = true;
else if (a === '--out' || a === '--root') {
const v = argv[++i];
@@ -491,6 +495,12 @@ function main(argv) {
return p.problems.length ? 1 : 0;
}
if (p.problems.length) return 1;
// a development version (0.2.0-dev on the public dev branch) never becomes a release zip by accident: its name carries
// the version, and building one needs --allow-dev (a test build, not a release)
if (isDevVersion(p.version) && !o.allowDev) {
console.error(`package: v${p.version} is a development version — a release zip needs a release version; --allow-dev builds a test zip named after it`);
return 1;
}
try {
const r = build(o.root, p, { out: o.out || path.join(os.tmpdir(), 'stronghold-protocol-release'), install: o.install, force: o.force, keepStage: o.keepStage });
console.log(`zip: ${r.zipPath} (${MB(r.zipBytes)}, ${r.files} files)${r.stage ? ` · stage kept: ${r.stage}` : ''}`);