Add Shift-run, live minimap, README; bump assets to v11
This commit is contained in:
@@ -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
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<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>
|
||||
<body>
|
||||
<div id="app">
|
||||
@@ -35,14 +35,14 @@
|
||||
<canvas id="fx-layer" width="10" height="10" hidden></canvas>
|
||||
</div>
|
||||
|
||||
<script src="js/data.items.js?v=10"></script>
|
||||
<script src="js/data.world.js?v=10"></script>
|
||||
<script src="js/data.dialogues.js?v=10"></script>
|
||||
<script src="js/engine.js?v=10"></script>
|
||||
<script src="js/combat.js?v=10"></script>
|
||||
<script src="js/game.js?v=10"></script>
|
||||
<script src="js/world2d.js?v=10"></script>
|
||||
<script src="js/ui.js?v=10"></script>
|
||||
<script src="js/main.js?v=10"></script>
|
||||
<script src="js/data.items.js?v=11"></script>
|
||||
<script src="js/data.world.js?v=11"></script>
|
||||
<script src="js/data.dialogues.js?v=11"></script>
|
||||
<script src="js/engine.js?v=11"></script>
|
||||
<script src="js/combat.js?v=11"></script>
|
||||
<script src="js/game.js?v=11"></script>
|
||||
<script src="js/world2d.js?v=11"></script>
|
||||
<script src="js/ui.js?v=11"></script>
|
||||
<script src="js/main.js?v=11"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -357,7 +357,7 @@ const UI = {
|
||||
v.innerHTML = `
|
||||
<div class="w2d-wrap">
|
||||
<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 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>
|
||||
|
||||
+48
-1
@@ -432,6 +432,7 @@ W2D.enter = function (locId, canvas) {
|
||||
W2D.ground = W2D._flatGround(W2D.grid);
|
||||
}
|
||||
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');
|
||||
const gr = W2D.grid;
|
||||
|
||||
@@ -579,7 +580,7 @@ W2D.uiBlocked = function () {
|
||||
|
||||
/* ---------------- update ---------------- */
|
||||
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 || {};
|
||||
let dx = 0, dy = 0;
|
||||
if (K['w'] || V.up) dy -= 1;
|
||||
@@ -825,6 +826,34 @@ W2D.draw = function (now) {
|
||||
c.font = '20px serif'; c.textAlign = 'center';
|
||||
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) {
|
||||
@@ -1088,6 +1117,24 @@ W2D.drawVignette = function (c) {
|
||||
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) */
|
||||
W2D._flatGround = function (grid) {
|
||||
const T = W2D.T;
|
||||
|
||||
Reference in New Issue
Block a user