Add clean NAS deploy flow and Wei strategist identity

This commit is contained in:
2026-07-02 11:30:38 +09:00
parent e28ef20018
commit 9f4e6ed5e2
22 changed files with 278 additions and 35 deletions

21
docs/deployment.md Normal file
View File

@@ -0,0 +1,21 @@
# Deployment
The NAS deployment should leave one canonical web directory:
- `/volume1/web/heros_web`
Use the checked-in script instead of ad-hoc deploy commands:
```powershell
pnpm deploy:nas
```
By default, `scripts/deploy-nas.ps1` builds the project, uploads `dist`, swaps the remote directory, and removes transient deploy or backup folders. This keeps `/volume1/web` from filling with `heros_web.backup-*` and `.heros_web_deploy_*` directories.
Optional backup retention:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/deploy-nas.ps1 -Build -BackupRetention 3
```
Use nonzero retention only for a temporary release window. The normal project default is `0`, so only `/volume1/web/heros_web` remains after deployment.

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 885 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

View File

@@ -0,0 +1,26 @@
# Wei Strategist identity v2 report
- identity note: replaced the Zhuge-like white feather fan with a Wei court analyst read built around blue-gray robes, a bronze command tablet, a war ledger, and a compact map board.
- source: `propfix-wei-strategist-v2-source.png`
- base status: ok
- action status: ok
- base partial alpha: 0, opaque pixels: 2040220
- action partial alpha: 0, opaque pixels: 4137270
- base frame border hits: 0
- action frame border hits: 0
- base empty frames: 0
- action empty frames: 0
- base bbox width range: 162..208
- base bbox height range: 282..290
- base bottom range: 303..303
- action bbox width range: 136..212
- action bbox height range: 282..295
- action bottom range: 303..303
- contact: `propfix-wei-strategist-v2-contact.png`
- animation: `propfix-wei-strategist-v2-animation.gif`
- before/after: `propfix-wei-strategist-v2-before-after.png`
- scale compare: `propfix-wei-strategist-v2-scale-compare.png`
- battle composite: `propfix-wei-strategist-v2-battle-composite.png`
- local browser battle check: `propfix-wei-strategist-v2-battle-browser-check.png`
- manual checks: build and local browser verification passed after generation
- note: battle composite is a scale review image; gameplay roster is unchanged during sample approval

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

View File

@@ -0,0 +1,43 @@
# Strategist Character Direction
This guide prevents strategist units from collapsing into the same white-robed, feather-fan silhouette.
## Core Rule
Zhuge Liang owns the large white feather fan and white-robed sage read. Other strategist, civil-official, and shaman units should keep their own readable identity through color, posture, and prop choice.
## Identity Matrix
| unit | identity read | primary colors | allowed props | avoid |
| --- | --- | --- | --- | --- |
| `unit-zhuge-liang` | white-robed master strategist | white, pale blue, silver | large white feather fan, scroll | removing the signature fan |
| `unit-ma-liang` | white-eyebrow civil official | olive, sage green, tan, brown | white eyebrows, bamboo slips, letters, document case | white robe/fan read |
| `unit-pang-tong` | eccentric dark tactician | dark brown, muted plum, black | dark folded fan, wood slips, scroll bundles | bright white fan |
| `unit-fa-zheng` | cold legal strategist | deep green, charcoal, muted gold | map board, tally baton, command slips | sage-white costume |
| `unit-sima-yi` | black court conspirator | black, gray, dark violet | command tablet, sealed order, small dark fan | white fan, pale robe |
| `unit-yi-ji` | envoy and letter carrier | cream-tan, muted green, brown | sealed letter, bamboo slips, travel scroll | feather fan |
| `unit-huang-quan` | quartermaster official | blue, dark teal, muted gold | map board, records, baton | white fan |
| `unit-shu-strategist` | Shu field tactician | olive, tan, dark green | brush, bamboo slips, compact map | white-robed sage silhouette |
| `unit-shu-strategist-field` | frontline staff officer | green, leather brown, brass | field map, short baton, scroll case | large sleeves and white fan |
| `unit-shu-strategist-white` | pale-robed civil aide | warm tan, muted jade, limited cream | paper bundle, brush case, small tablet | Zhuge-style white robe/fan pairing |
| `unit-wei-strategist` | Wei court analyst | blue-gray, steel, black | bronze command tablet, war map, folded order | white feather fan |
| `unit-wei-strategist-black` | Wei covert adviser | black, violet, steel | black tablet, sealed order | white fan |
| `unit-wei-strategist-blue` | Wei records officer | blue, gray, bronze | map scroll, registry book | white fan |
| `unit-wu-strategist` | Wu river adviser | red-brown, teal, lacquer black | naval chart, command baton, small red fan | white feather fan |
| `unit-wu-strategist-fire` | fire-plan tactician | ember red, charcoal, bronze | fire tally, oil map, torch talisman | white fan |
| `unit-wu-strategist-river` | river-route tactician | blue-green, red trim, dark wood | water chart, oar-like baton, scroll tube | white fan |
| `unit-nanman-shaman` | ritual shaman | ochre, bone, dark red | ritual staff, talisman, tribal ornaments | Han scholar fan |
| `unit-nanman-shaman-ember` | ember shaman | ember red, bone, smoke black | ember staff, charm strips | white feather fan |
| `unit-nanman-shaman-storm` | storm shaman | storm blue, bone, dark leather | storm staff, charms | white feather fan |
## Next Representative Fixes
1. `unit-wu-strategist`: replace the white-fan read with a Wu river adviser using a naval chart and lacquer command baton.
2. `unit-shu-strategist-white`: preserve a lighter civil-aide read, but remove the Zhuge-style white robe plus fan pairing.
3. `unit-wu-strategist-fire`: replace the white-fan read with a fire tally, oil map, or talisman.
Completed representative pass:
- `unit-wei-strategist`: changed from white-fan strategist to blue-gray Wei court analyst with bronze command tablet, war ledger, and compact map board.
Every representative fix should include contact sheet, animation GIF, battle preview, before/after, alpha audit, build, and browser check.

View File

@@ -2,6 +2,7 @@
This audit keeps Zhuge Liang's large white feather fan as a unique signature prop.
It also keeps character-specific scholar traits intact: Ma Liang should read as the white-eyebrow civil official, not as another white-robed feather-fan strategist.
Detailed per-unit visual direction lives in `strategist-character-direction.md`.
## Prop Rule
@@ -23,14 +24,14 @@ It also keeps character-specific scholar traits intact: Ma Liang should read as
| `unit-yi-ji` | letter, bamboo slips, scroll | distinct civil envoy | keep |
| `unit-huang-quan` | bamboo slips, map board, baton | distinct quartermaster official | keep |
| `unit-shu-strategist` | writing brush, bamboo slips, compact field map | white feather fan removed | fixed in batch 1 |
| `unit-shu-strategist-field` | likely strategist prop variant | needs later batch check | later |
| `unit-shu-strategist-white` | likely pale strategist prop variant | needs later batch check | later |
| `unit-wei-strategist` | white feather fan in several poses | too close to Zhuge Liang | later prop cleanup |
| `unit-wei-strategist-black` | dark strategist prop variant | needs later batch check | later |
| `unit-wei-strategist-blue` | strategist prop variant | needs later batch check | later |
| `unit-wu-strategist` | white feather fan in several poses | too close to Zhuge Liang | later prop cleanup |
| `unit-wu-strategist-fire` | white fan in fire strategist poses | too close to Zhuge Liang | later prop cleanup |
| `unit-wu-strategist-river` | white fan in river strategist poses | too close to Zhuge Liang | later prop cleanup |
| `unit-shu-strategist-field` | field map / staff officer variant | must stay practical and green, not sage-white | later identity pass |
| `unit-shu-strategist-white` | pale civil-aide variant | high risk if it pairs white robe with fan | representative fix candidate |
| `unit-wei-strategist` | bronze command tablet, war ledger, compact map board | white feather fan removed; Wei court analyst identity restored | fixed in representative pass |
| `unit-wei-strategist-black` | dark strategist prop variant | acceptable if fan stays dark and compact | later identity pass |
| `unit-wei-strategist-blue` | blue records officer variant | acceptable if props are records/maps | later identity pass |
| `unit-wu-strategist` | white feather fan in several poses | too close to Zhuge Liang | representative fix candidate |
| `unit-wu-strategist-fire` | white fan in fire strategist poses | should become fire tally / oil map / talisman | later prop cleanup |
| `unit-wu-strategist-river` | white fan in river strategist poses | should become water chart / oar-like baton | later prop cleanup |
| `unit-nanman-shaman` | ritual staff / tribal ornaments | not a white feather fan | keep |
| `unit-nanman-shaman-ember` | ritual staff / ember ornaments | not a white feather fan | keep |
| `unit-nanman-shaman-storm` | ritual staff / storm ornaments | not a white feather fan | keep |
@@ -45,4 +46,10 @@ The first correction batch changes:
- `unit-fa-zheng`
- `unit-shu-strategist`
Later cleanup should remove white-feather-fan reads from remaining generic strategist variants after this representative batch is verified.
Next representative cleanup order:
1. `unit-wu-strategist`
2. `unit-shu-strategist-white`
3. `unit-wu-strategist-fire`
Later cleanup should remove white-feather-fan reads from remaining generic strategist variants after the representative fixes are verified.

View File

@@ -650,9 +650,9 @@
"baseStatus": "ok",
"actionStatus": "ok",
"battleBasePx": [
25.6,
37.2,
46.3,
25.9,
33.2,
45.0,
46.3
],
"spec": null

View File

@@ -89,7 +89,7 @@ No next unit is currently defined.
| `unit-wei-officer` | approved reference | approved-baseline | technical ok | 25.4..33.1 x 46.0..47.1 | |
| `unit-wei-officer-gate` | approved reference | approved-baseline | technical ok | 28.1..33.4 x 46.0..46.0 | |
| `unit-wei-officer-iron` | approved reference | approved-baseline | technical ok | 34.2..36.6 x 44.2..46.0 | |
| `unit-wei-strategist` | approved reference | approved-baseline | technical ok | 25.6..37.2 x 46.3..46.3 | |
| `unit-wei-strategist` | approved reference | approved-baseline | technical ok | 25.9..33.2 x 45.0..46.3 | |
| `unit-wei-strategist-black` | approved reference | approved-baseline | technical ok | 23.3..34.0 x 43.1..46.0 | |
| `unit-wei-strategist-blue` | approved reference | approved-baseline | technical ok | 26.4..33.5 x 46.0..46.0 | |
| `unit-wei-yan` | approved reference | approved-baseline | technical ok | 27.6..35.0 x 44.2..46.3 | |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 844 KiB

After

Width:  |  Height:  |  Size: 843 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

View File

@@ -3788,21 +3788,21 @@
1252
],
"partial_alpha": 0,
"opaque_pixels": 2514754,
"opaque_pixels": 2040220,
"border_hits": 0,
"min_width": 160,
"max_width": 233,
"min_height": 290,
"min_width": 162,
"max_width": 208,
"min_height": 282,
"max_height": 290,
"min_bottom": 303,
"max_bottom": 303,
"empty_frames": 0,
"width_swing": 73,
"height_swing": 0,
"width_swing": 46,
"height_swing": 8,
"bottom_swing": 0,
"battle_min_width_px": 25.6,
"battle_max_width_px": 37.2,
"battle_min_height_px": 46.3,
"battle_min_width_px": 25.9,
"battle_max_width_px": 33.2,
"battle_min_height_px": 45.0,
"battle_max_height_px": 46.3,
"status": "ok"
},
@@ -3814,22 +3814,22 @@
1252
],
"partial_alpha": 0,
"opaque_pixels": 5501514,
"opaque_pixels": 4137270,
"border_hits": 0,
"min_width": 160,
"max_width": 243,
"min_height": 288,
"max_height": 290,
"min_width": 136,
"max_width": 212,
"min_height": 282,
"max_height": 295,
"min_bottom": 303,
"max_bottom": 303,
"empty_frames": 0,
"width_swing": 83,
"height_swing": 2,
"width_swing": 76,
"height_swing": 13,
"bottom_swing": 0,
"battle_min_width_px": 25.6,
"battle_max_width_px": 38.8,
"battle_min_height_px": 46.0,
"battle_max_height_px": 46.3,
"battle_min_width_px": 21.7,
"battle_max_width_px": 33.9,
"battle_min_height_px": 45.0,
"battle_max_height_px": 47.1,
"status": "ok"
}
},

View File

@@ -117,7 +117,7 @@ Visual review boards are generated by `scripts/render-unit-sprite-review-boards.
| batch3-wei | approved-baseline | `unit-wei-officer` | ok | ok | 159..207 x 288..295 | 158..255 x 288..295 | 25.4..33.1 x 46.0..47.1 | w48 h7 b0 |
| batch3-wei | approved-baseline | `unit-wei-officer-gate` | ok | ok | 176..209 x 288..288 | 176..257 x 262..288 | 28.1..33.4 x 46.0..46.0 | w33 h0 b0 |
| batch3-wei | approved-baseline | `unit-wei-officer-iron` | ok | ok | 214..229 x 277..288 | 201..258 x 230..288 | 34.2..36.6 x 44.2..46.0 | w15 h11 b0 |
| batch3-wei | approved-baseline | `unit-wei-strategist` | ok | ok | 160..233 x 290..290 | 160..243 x 288..290 | 25.6..37.2 x 46.3..46.3 | w73 h0 b0 |
| batch3-wei | approved-baseline | `unit-wei-strategist` | ok | ok | 162..208 x 282..290 | 136..212 x 282..295 | 25.9..33.2 x 45.0..46.3 | w46 h8 b0 |
| batch3-wei | approved-baseline | `unit-wei-strategist-black` | ok | ok | 146..213 x 270..288 | 146..240 x 249..288 | 23.3..34.0 x 43.1..46.0 | w67 h18 b0 |
| batch3-wei | approved-baseline | `unit-wei-strategist-blue` | ok | ok | 165..210 x 288..288 | 165..246 x 264..288 | 26.4..33.5 x 46.0..46.0 | w45 h0 b0 |
| batch3-wei | approved-baseline | `unit-wei-yan` | ok | ok | 173..219 x 277..290 | 173..264 x 232..290 | 27.6..35.0 x 44.2..46.3 | w46 h13 b0 |

View File

@@ -14,7 +14,8 @@
"verify:flow": "node scripts/verify-flow.mjs",
"verify:save-flow": "node scripts/verify-save-retry-flow.mjs",
"verify:release": "node scripts/verify-release-candidate.mjs",
"qa:representative": "node scripts/qa-representative-battles.mjs"
"qa:representative": "node scripts/qa-representative-battles.mjs",
"deploy:nas": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/deploy-nas.ps1 -Build"
},
"dependencies": {
"phaser": "^3.90.0"

145
scripts/deploy-nas.ps1 Normal file
View File

@@ -0,0 +1,145 @@
param(
[switch]$Build,
[int]$BackupRetention = 0,
[string]$SshUser = "y2keui",
[string]$SshHost = "comtropy.synology.me",
[int]$SshPort = 50022,
[string]$SshKey = "$env:USERPROFILE\.ssh\nas_msix_ed25519",
[string]$RemoteRoot = "/volume1/web",
[string]$SiteName = "heros_web"
)
$ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest
function Assert-Command {
param([string]$Name)
if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
throw "Required command not found: $Name"
}
}
function Quote-Sh {
param([string]$Value)
return "'" + ($Value -replace "'", "'\''") + "'"
}
Assert-Command "ssh"
Assert-Command "scp"
Assert-Command "git"
if ($Build) {
Assert-Command "pnpm"
pnpm build
if ($LASTEXITCODE -ne 0) {
throw "pnpm build failed with exit code $LASTEXITCODE"
}
}
$dist = Join-Path (Get-Location) "dist"
$index = Join-Path $dist "index.html"
$assets = Join-Path $dist "assets"
if (-not (Test-Path $index -PathType Leaf)) {
throw "Missing dist/index.html. Run pnpm build first or pass -Build."
}
if (-not (Test-Path $assets -PathType Container)) {
throw "Missing dist/assets. Run pnpm build first or pass -Build."
}
if ($BackupRetention -lt 0) {
throw "BackupRetention must be 0 or greater."
}
$timestamp = Get-Date -Format "yyyyMMddHHmmss"
$commit = (git rev-parse --short=8 HEAD).Trim()
$userHost = "$SshUser@$SshHost"
$remoteTemp = "$RemoteRoot/.$($SiteName)_deploy_$timestamp"
$remotePrevious = "$RemoteRoot/.$($SiteName)_previous_$timestamp"
$remoteBackup = "$RemoteRoot/$SiteName.backup-$timestamp-$commit"
$quotedRemoteRoot = Quote-Sh $RemoteRoot
$quotedSiteName = Quote-Sh $SiteName
$quotedTemp = Quote-Sh $remoteTemp
$quotedPrevious = Quote-Sh $remotePrevious
$quotedBackup = Quote-Sh $remoteBackup
$retention = $BackupRetention
$prepareRemote = @"
set -e
remote_root=$quotedRemoteRoot
temp=$quotedTemp
site_name=$quotedSiteName
test -d "`$remote_root"
rm -rf -- "`$temp"
mkdir -p -- "`$temp"
printf 'Prepared %s/%s via %s\n' "`$remote_root" "`$site_name" "`$temp"
"@
ssh -o BatchMode=yes -o ConnectTimeout=8 -i $SshKey -p $SshPort $userHost $prepareRemote
if ($LASTEXITCODE -ne 0) {
throw "Failed to prepare remote deploy directory."
}
scp -O -i $SshKey -P $SshPort -r "$dist\*" "$userHost`:$remoteTemp/"
if ($LASTEXITCODE -ne 0) {
throw "Failed to upload dist to remote deploy directory."
}
$installRemote = @"
set -e
remote_root=$quotedRemoteRoot
site_name=$quotedSiteName
temp=$quotedTemp
previous=$quotedPrevious
backup=$quotedBackup
retention=$retention
site="`$remote_root/`$site_name"
test -f "`$temp/index.html"
test -d "`$temp/assets"
rm -rf -- "`$previous"
if [ -d "`$site" ]; then
mv -- "`$site" "`$previous"
fi
if ! mv -- "`$temp" "`$site"; then
if [ -d "`$previous" ]; then
mv -- "`$previous" "`$site"
fi
exit 1
fi
chmod -R a+rX "`$site"
if [ -d "`$previous" ]; then
if [ "`$retention" -gt 0 ]; then
mv -- "`$previous" "`$backup"
else
rm -rf -- "`$previous"
fi
fi
find "`$remote_root" -maxdepth 1 -type d \( -name ".`$site_name`_deploy_*" -o -name "`$site_name`_deploy_*" -o -name ".`$site_name`_previous_*" \) -exec rm -rf -- {} +
if [ "`$retention" -eq 0 ]; then
find "`$remote_root" -maxdepth 1 -type d -name "`$site_name.backup-*" -exec rm -rf -- {} +
else
keep_count=`$retention
find "`$remote_root" -maxdepth 1 -type d -name "`$site_name.backup-*" -printf '%f\n' | sort -r | tail -n +`$((keep_count + 1)) | while IFS= read -r old_backup; do
[ -n "`$old_backup" ] && rm -rf -- "`$remote_root/`$old_backup"
done
fi
printf 'Deployed %s at %s\n' "`$site_name" "`$site"
printf 'Backup retention: %s\n' "`$retention"
printf 'Remaining site directories:\n'
find "`$remote_root" -maxdepth 1 -type d -name "*`$site_name*" -printf '%f\n' | sort
"@
ssh -o BatchMode=yes -o ConnectTimeout=8 -i $SshKey -p $SshPort $userHost $installRemote
if ($LASTEXITCODE -ne 0) {
throw "Failed to install remote deploy directory."
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1014 KiB

After

Width:  |  Height:  |  Size: 870 KiB