diff --git a/README.md b/README.md index 07175f77..ab9211f3 100644 --- a/README.md +++ b/README.md @@ -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 里反馈。 ## 功能一览 diff --git a/package-lock.json b/package-lock.json index e67c1b26..7a4e3348 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/package.json b/package.json index 7d7f4b37..8d3f83db 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/css/screens/title.css b/public/css/screens/title.css index 1805b46c..108f6f27 100644 --- a/public/css/screens/title.css +++ b/public/css/screens/title.css @@ -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; +} diff --git a/public/i18n/en.json b/public/i18n/en.json index 325e6795..bdf656b7 100644 --- a/public/i18n/en.json +++ b/public/i18n/en.json @@ -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" } diff --git a/public/js/screens/title.js b/public/js/screens/title.js index 16144736..9d3f3917 100644 --- a/public/js/screens/title.js +++ b/public/js/screens/title.js @@ -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() { `; } diff --git a/server/http/boot.js b/server/http/boot.js index 68002e64..ee862e2b 100644 --- a/server/http/boot.js +++ b/server/http/boot.js @@ -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}`); diff --git a/shared/constants.js b/shared/constants.js index c0a009ae..2173cd39 100644 --- a/shared/constants.js +++ b/shared/constants.js @@ -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; /** diff --git a/test/version.test.js b/test/version.test.js index 8505ca1b..0d9e2c40 100644 --- a/test/version.test.js +++ b/test/version.test.js @@ -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` /-/.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}` : ''}`);