Add Shift-run, live minimap, README; bump assets to v11

This commit is contained in:
2026-08-23 10:04:45 +00:00
parent 8680f79102
commit 862d09b3ee
4 changed files with 112 additions and 12 deletions
+53
View File
@@ -0,0 +1,53 @@
# Jianghu Chronicles — Road of the Wandering Blade
An **original open-world wuxia browser RPG**: walk a living jianghu, befriend
(and romance) companions, master six weapon schools down to their tier-V
ultimates, and unravel the Iron Umbrella Society across a six-chapter story.
All names, characters, techniques and lore are original works of fiction.
## Play
Any static server pointed at this folder works:
```bash
python3 -m http.server 8917 # or: node server.js
# open http://127.0.0.1:8917
```
Opening `index.html` directly from disk (`file://`) also works — no modules,
no fetches.
## Controls
| Input | Action |
|---|---|
| Click / tap map | Walk there; tap near NPC/portal to auto-interact |
| WASD / Arrows / ZQSD | Move (Shift = run) |
| E / Enter | Talk · gather · use spots · portals |
| D-pad (bottom-right) | Touch controls |
| Glowing ➜ arches | Travel between locations |
Combat is turn-based tactics on a 10×7 grid: move + act each turn, click a
move then a highlighted tile. Battles pause the world; winning clears the
roamer that ambushed you.
## Features
- 12 hand-styled locations, day/night cycle, ambient particles, minimap
- 6 weapon types × 5 tiers of techniques, internals cultivation, lightness arts
- Full gear: weapon / head / body / feet / 2 accessories, smithing upgrades
- 6-chapter main quest, job-board side jobs, bounty hunts, weekly tournament
- Companions with chemistry passives, affection, three romances, four sect paths
- Alchemy, fishing, gambling, pickpocketing (infamy has consequences)
- Achievements, monster codex, four endings, autosave + 3 slots + export
## Tests
```bash
node test/smoke.mjs
```
25 headless tests load every script into a VM with DOM stubs and exercise
character creation, battles (AI and player-driven), quests, economy, crafting,
save/load, world-map generation invariants, achievements, equipment stats and
side-job lifecycles.
+10 -10
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Jianghu Chronicles — Road of the Wandering Blade</title> <title>Jianghu Chronicles — Road of the Wandering Blade</title>
<link rel="stylesheet" href="css/style.css?v=10"> <link rel="stylesheet" href="css/style.css?v=11">
</head> </head>
<body> <body>
<div id="app"> <div id="app">
@@ -35,14 +35,14 @@
<canvas id="fx-layer" width="10" height="10" hidden></canvas> <canvas id="fx-layer" width="10" height="10" hidden></canvas>
</div> </div>
<script src="js/data.items.js?v=10"></script> <script src="js/data.items.js?v=11"></script>
<script src="js/data.world.js?v=10"></script> <script src="js/data.world.js?v=11"></script>
<script src="js/data.dialogues.js?v=10"></script> <script src="js/data.dialogues.js?v=11"></script>
<script src="js/engine.js?v=10"></script> <script src="js/engine.js?v=11"></script>
<script src="js/combat.js?v=10"></script> <script src="js/combat.js?v=11"></script>
<script src="js/game.js?v=10"></script> <script src="js/game.js?v=11"></script>
<script src="js/world2d.js?v=10"></script> <script src="js/world2d.js?v=11"></script>
<script src="js/ui.js?v=10"></script> <script src="js/ui.js?v=11"></script>
<script src="js/main.js?v=10"></script> <script src="js/main.js?v=11"></script>
</body> </body>
</html> </html>
+1 -1
View File
@@ -357,7 +357,7 @@ const UI = {
v.innerHTML = ` v.innerHTML = `
<div class="w2d-wrap"> <div class="w2d-wrap">
<canvas id="w2d" width="960" height="540"></canvas> <canvas id="w2d" width="960" height="540"></canvas>
<div class="w2d-hint"><b>Click / tap map</b> to walk there (tap NPC to talk) · <b>WASD / Arrows</b> also move · <b>E</b> interact · <b>D-pad</b> bottom-right for touch · glowing <b>➜</b> arches travel · touch beasts to fight</div> <div class="w2d-hint"><b>Click / tap map</b> to walk there (tap NPC to talk) · <b>WASD / Arrows</b> move · <b>Shift</b> run · <b>E</b> interact · <b>D-pad</b> bottom-right for touch · glowing <b>➜</b> arches travel · touch beasts to fight · minimap top-right</div>
</div> </div>
<div class="scene-head"><h2>${Util.esc(L.name)} <span class="tag">${L.region}</span>${L.danger ? `<span class="tag red">danger ${'★'.repeat(L.danger)}</span>` : ''}${L.nightOnly ? '<span class="tag blue">night only</span>' : ''}</h2></div> <div class="scene-head"><h2>${Util.esc(L.name)} <span class="tag">${L.region}</span>${L.danger ? `<span class="tag red">danger ${'★'.repeat(L.danger)}</span>` : ''}${L.nightOnly ? '<span class="tag blue">night only</span>' : ''}</h2></div>
<p class="scene-desc">${L.desc}</p> <p class="scene-desc">${L.desc}</p>
+48 -1
View File
@@ -432,6 +432,7 @@ W2D.enter = function (locId, canvas) {
W2D.ground = W2D._flatGround(W2D.grid); W2D.ground = W2D._flatGround(W2D.grid);
} }
try { W2D.initParts(); } catch (_) { W2D.partMode = null; } try { W2D.initParts(); } catch (_) { W2D.partMode = null; }
try { W2D.mini = W2D.prerenderMini(W2D.grid); } catch (_) { W2D.mini = null; }
W2D.cv = canvas; W2D.ctx = canvas.getContext('2d'); W2D.cv = canvas; W2D.ctx = canvas.getContext('2d');
const gr = W2D.grid; const gr = W2D.grid;
@@ -579,7 +580,7 @@ W2D.uiBlocked = function () {
/* ---------------- update ---------------- */ /* ---------------- update ---------------- */
W2D.update = function (dt, now) { W2D.update = function (dt, now) {
const sp = 0.14 * dt; // px per ms const sp = 0.14 * dt * (W2D.keys['shift'] ? 1.75 : 1); // px per ms (Shift = run)
const K = W2D.keys, V = W2D.vk || {}; const K = W2D.keys, V = W2D.vk || {};
let dx = 0, dy = 0; let dx = 0, dy = 0;
if (K['w'] || V.up) dy -= 1; if (K['w'] || V.up) dy -= 1;
@@ -825,6 +826,34 @@ W2D.draw = function (now) {
c.font = '20px serif'; c.textAlign = 'center'; c.font = '20px serif'; c.textAlign = 'center';
c.fillText(LOCS[W2D.cur].name, W2D.VW / 2, 43); c.fillText(LOCS[W2D.cur].name, W2D.VW / 2, 43);
} }
// minimap (screen space, top-right)
safe(() => {
if (!W2D.mini || !gr) return;
const mw = W2D.mini.width, mh = W2D.mini.height;
const mx = W2D.VW - mw - 14, my = 14;
const sx = mw / (gr.w * T), sy = mh / (gr.h * T);
c.fillStyle = 'rgba(8,10,16,.78)';
c.fillRect(mx - 5, my - 5, mw + 10, mh + 10);
c.strokeStyle = 'rgba(216,179,106,.55)';
c.strokeRect(mx - 4.5, my - 4.5, mw + 9, mh + 9);
c.drawImage(W2D.mini, mx, my);
for (const ex of gr.exits) {
const L2 = LOCS[ex.to];
if (L2.hidden && !G.flags[L2.hidden]) continue;
c.fillStyle = '#d8b36a';
c.fillRect(mx + ex.x * T * sx - 2, my + ex.y * T * sy - 2, 4, 4);
}
for (const e of W2D.ents) {
if (e.dead) continue;
if (e.kind === 'npc') { c.fillStyle = '#efe6cf'; c.fillRect(mx + e.x * sx - 1.5, my + e.y * sy - 1.5, 3, 3); }
else if (e.kind === 'roam') { c.fillStyle = '#d05a48'; c.fillRect(mx + e.x * sx - 1.5, my + e.y * sy - 1.5, 3, 3); }
else if (e.kind === 'spot') { c.fillStyle = '#7dc25c'; c.fillRect(mx + e.x * sx - 1, my + e.y * sy - 1, 2, 2); }
}
const pulse = 2.5 + Math.sin(now / 180) * 1;
c.fillStyle = '#ff5a4e';
c.beginPath(); c.arc(mx + W2D.px * sx, my + W2D.py * sy, pulse, 0, 7); c.fill();
});
}; };
W2D.figure = function (c, x, y, s, robe, now, walking) { W2D.figure = function (c, x, y, s, robe, now, walking) {
@@ -1088,6 +1117,24 @@ W2D.drawVignette = function (c) {
c.fillRect(W2D.cam.x, W2D.cam.y, W2D.VW, W2D.VH); c.fillRect(W2D.cam.x, W2D.cam.y, W2D.VW, W2D.VH);
}; };
/* minimap: prerendered tile thumbnail + live dots drawn each frame */
W2D.prerenderMini = function (grid) {
const S = 3;
const cv = document.createElement('canvas');
cv.width = grid.w * S; cv.height = grid.h * S;
const c = cv.getContext('2d');
for (let y = 0; y < grid.h; y++) for (let x = 0; x < grid.w; x++) {
const t = grid.g[y][x];
c.fillStyle = t === 2 || t === 19 ? '#1c3a52'
: t === 1 ? '#63553f'
: t === 8 ? '#7a5c3c'
: BLOCKED.has(t) ? '#20262c'
: '#3a5239';
c.fillRect(x * S, y * S, S, S);
}
return cv;
};
/* flat fallback ground (used only if the rich renderer throws) */ /* flat fallback ground (used only if the rich renderer throws) */
W2D._flatGround = function (grid) { W2D._flatGround = function (grid) {
const T = W2D.T; const T = W2D.T;