@sdd330dev/jy-skill 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +148 -0
- package/LICENSE +21 -0
- package/README.md +177 -0
- package/SKILL.md +208 -0
- package/assets/characters/0.json +56 -0
- package/assets/characters/1.json +60 -0
- package/assets/characters/10.json +56 -0
- package/assets/characters/100.json +38 -0
- package/assets/characters/101.json +38 -0
- package/assets/characters/102.json +38 -0
- package/assets/characters/103.json +38 -0
- package/assets/characters/104.json +38 -0
- package/assets/characters/105.json +38 -0
- package/assets/characters/106.json +38 -0
- package/assets/characters/107.json +38 -0
- package/assets/characters/108.json +38 -0
- package/assets/characters/109.json +38 -0
- package/assets/characters/11.json +56 -0
- package/assets/characters/110.json +38 -0
- package/assets/characters/12.json +52 -0
- package/assets/characters/13.json +56 -0
- package/assets/characters/14.json +52 -0
- package/assets/characters/15.json +56 -0
- package/assets/characters/16.json +56 -0
- package/assets/characters/17.json +52 -0
- package/assets/characters/18.json +43 -0
- package/assets/characters/19.json +56 -0
- package/assets/characters/2.json +56 -0
- package/assets/characters/20.json +56 -0
- package/assets/characters/200.json +48 -0
- package/assets/characters/201.json +48 -0
- package/assets/characters/202.json +56 -0
- package/assets/characters/21.json +52 -0
- package/assets/characters/22.json +52 -0
- package/assets/characters/23.json +56 -0
- package/assets/characters/24.json +43 -0
- package/assets/characters/25.json +52 -0
- package/assets/characters/26.json +56 -0
- package/assets/characters/27.json +52 -0
- package/assets/characters/28.json +52 -0
- package/assets/characters/29.json +52 -0
- package/assets/characters/3.json +60 -0
- package/assets/characters/30.json +52 -0
- package/assets/characters/300.json +38 -0
- package/assets/characters/301.json +38 -0
- package/assets/characters/31.json +56 -0
- package/assets/characters/32.json +47 -0
- package/assets/characters/33.json +47 -0
- package/assets/characters/34.json +52 -0
- package/assets/characters/35.json +56 -0
- package/assets/characters/36.json +52 -0
- package/assets/characters/37.json +52 -0
- package/assets/characters/38.json +56 -0
- package/assets/characters/39.json +52 -0
- package/assets/characters/4.json +56 -0
- package/assets/characters/40.json +56 -0
- package/assets/characters/41.json +52 -0
- package/assets/characters/42.json +52 -0
- package/assets/characters/43.json +52 -0
- package/assets/characters/44.json +52 -0
- package/assets/characters/45.json +56 -0
- package/assets/characters/46.json +56 -0
- package/assets/characters/47.json +47 -0
- package/assets/characters/48.json +52 -0
- package/assets/characters/49.json +38 -0
- package/assets/characters/5.json +60 -0
- package/assets/characters/50.json +38 -0
- package/assets/characters/51.json +38 -0
- package/assets/characters/52.json +38 -0
- package/assets/characters/53.json +38 -0
- package/assets/characters/54.json +38 -0
- package/assets/characters/55.json +38 -0
- package/assets/characters/56.json +38 -0
- package/assets/characters/57.json +38 -0
- package/assets/characters/58.json +38 -0
- package/assets/characters/59.json +38 -0
- package/assets/characters/6.json +56 -0
- package/assets/characters/60.json +38 -0
- package/assets/characters/61.json +38 -0
- package/assets/characters/62.json +38 -0
- package/assets/characters/63.json +38 -0
- package/assets/characters/64.json +38 -0
- package/assets/characters/65.json +38 -0
- package/assets/characters/66.json +38 -0
- package/assets/characters/67.json +38 -0
- package/assets/characters/68.json +38 -0
- package/assets/characters/69.json +38 -0
- package/assets/characters/7.json +64 -0
- package/assets/characters/70.json +38 -0
- package/assets/characters/71.json +38 -0
- package/assets/characters/72.json +38 -0
- package/assets/characters/73.json +38 -0
- package/assets/characters/74.json +38 -0
- package/assets/characters/75.json +38 -0
- package/assets/characters/76.json +38 -0
- package/assets/characters/77.json +38 -0
- package/assets/characters/78.json +38 -0
- package/assets/characters/79.json +38 -0
- package/assets/characters/8.json +60 -0
- package/assets/characters/80.json +38 -0
- package/assets/characters/81.json +38 -0
- package/assets/characters/82.json +38 -0
- package/assets/characters/83.json +38 -0
- package/assets/characters/84.json +38 -0
- package/assets/characters/85.json +38 -0
- package/assets/characters/86.json +38 -0
- package/assets/characters/87.json +38 -0
- package/assets/characters/88.json +38 -0
- package/assets/characters/89.json +38 -0
- package/assets/characters/9.json +56 -0
- package/assets/characters/90.json +38 -0
- package/assets/characters/91.json +38 -0
- package/assets/characters/92.json +38 -0
- package/assets/characters/93.json +38 -0
- package/assets/characters/94.json +38 -0
- package/assets/characters/95.json +38 -0
- package/assets/characters/96.json +38 -0
- package/assets/characters/97.json +38 -0
- package/assets/characters/98.json +38 -0
- package/assets/characters/99.json +38 -0
- package/assets/characters/index.json +584 -0
- package/assets/game-config.json +855 -0
- package/assets/items.json +1060 -0
- package/assets/skills.json +829 -0
- package/assets/templates.json +127 -0
- package/package.json +80 -0
- package/references/agent-handbook.md +308 -0
- package/references/game-design.md +124 -0
- package/references/player-guide.md +176 -0
- package/scripts/config-loader.ts +408 -0
- package/scripts/game-engine.ts +617 -0
- package/scripts/game-logic.ts +153 -0
- package/scripts/game-types.ts +46 -0
- package/scripts/install-skill.mjs +115 -0
- package/scripts/persistence.ts +135 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 金庸群侠传 · 游戏核心公式
|
|
3
|
+
*
|
|
4
|
+
* 所有数值计算必须遵循此文件中的公式。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// 战斗系统
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 计算战斗伤害
|
|
13
|
+
* 公式: 武力 + 技能攻击力 - 敌方防御
|
|
14
|
+
* 左右互搏: ×1.5
|
|
15
|
+
* 武学常识加成: +武学常识/10
|
|
16
|
+
* 随机波动: ±20%
|
|
17
|
+
*/
|
|
18
|
+
export function calculateDamage(
|
|
19
|
+
attackerAttack: number,
|
|
20
|
+
skillAttack: number,
|
|
21
|
+
defenderDefence: number,
|
|
22
|
+
ambidextrous: number = 0,
|
|
23
|
+
martialKnowledge: number = 0,
|
|
24
|
+
random: () => number = Math.random,
|
|
25
|
+
): number {
|
|
26
|
+
let damage = attackerAttack + skillAttack - defenderDefence;
|
|
27
|
+
|
|
28
|
+
if (ambidextrous > 0) {
|
|
29
|
+
damage = Math.floor(damage * 1.5);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (martialKnowledge > 0) {
|
|
33
|
+
damage += Math.floor(martialKnowledge / 10);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const randomFactor = 0.8 + random() * 0.4;
|
|
37
|
+
damage = Math.floor(damage * randomFactor);
|
|
38
|
+
|
|
39
|
+
return Math.max(1, damage);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 计算中毒掉血
|
|
44
|
+
* 公式: 中毒值 / 10
|
|
45
|
+
*/
|
|
46
|
+
export function calculatePoisonDamage(poison: number): number {
|
|
47
|
+
return Math.floor(poison / 10);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 计算受伤掉血
|
|
52
|
+
* 公式: 受伤值 / 20
|
|
53
|
+
*/
|
|
54
|
+
export function calculateHurtDamage(hurt: number): number {
|
|
55
|
+
return Math.floor(hurt / 20);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ============================================================================
|
|
59
|
+
// 角色系统
|
|
60
|
+
// ============================================================================
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 计算升级所需经验
|
|
64
|
+
* 公式: floor(100 × 1.5^(等级-1))
|
|
65
|
+
*/
|
|
66
|
+
export function getExpForLevel(level: number): number {
|
|
67
|
+
return Math.floor(100 * Math.pow(1.5, level - 1));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 计算内力消耗
|
|
72
|
+
* 公式: 基础消耗 × ((等级+1)/2)
|
|
73
|
+
*/
|
|
74
|
+
export function calculateMpCost(baseCost: number, skillLevel: number): number {
|
|
75
|
+
return Math.floor(baseCost * ((skillLevel + 1) / 2));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 计算体力消耗
|
|
80
|
+
* 普通攻击: 3
|
|
81
|
+
* 用毒: 2
|
|
82
|
+
* 解毒: 2
|
|
83
|
+
* 医疗: 4
|
|
84
|
+
*/
|
|
85
|
+
export function calculateStaminaCost(damageType: string): number {
|
|
86
|
+
switch (damageType) {
|
|
87
|
+
case 'normal':
|
|
88
|
+
case 'absorbMp':
|
|
89
|
+
return 3;
|
|
90
|
+
case 'poison':
|
|
91
|
+
case 'depoison':
|
|
92
|
+
return 2;
|
|
93
|
+
case 'heal':
|
|
94
|
+
return 4;
|
|
95
|
+
default:
|
|
96
|
+
return 3;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 计算移动力
|
|
102
|
+
* 公式: 轻功/15 + 3
|
|
103
|
+
*/
|
|
104
|
+
export function calculateMovePoints(agility: number): number {
|
|
105
|
+
return Math.floor(agility / 15) + 3;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ============================================================================
|
|
109
|
+
// 默认属性
|
|
110
|
+
// ============================================================================
|
|
111
|
+
|
|
112
|
+
export const DEFAULT_ATTRIBUTES = {
|
|
113
|
+
maxHp: 100,
|
|
114
|
+
maxMp: 50,
|
|
115
|
+
hp: 100,
|
|
116
|
+
mp: 50,
|
|
117
|
+
hpInc: 5,
|
|
118
|
+
attack: 20,
|
|
119
|
+
agility: 15,
|
|
120
|
+
defence: 10,
|
|
121
|
+
heal: 0,
|
|
122
|
+
usePoison: 0,
|
|
123
|
+
dePoison: 0,
|
|
124
|
+
antiPoison: 0,
|
|
125
|
+
fist: 0,
|
|
126
|
+
sword: 0,
|
|
127
|
+
blade: 0,
|
|
128
|
+
exotic: 0,
|
|
129
|
+
hiddenWeapon: 0,
|
|
130
|
+
martialKnowledge: 0,
|
|
131
|
+
attackPoison: 0,
|
|
132
|
+
ambidextrous: 0,
|
|
133
|
+
iq: 50,
|
|
134
|
+
morality: 50,
|
|
135
|
+
reputation: 0,
|
|
136
|
+
stamina: 100,
|
|
137
|
+
poison: 0,
|
|
138
|
+
hurt: 0,
|
|
139
|
+
mpType: 'neutral',
|
|
140
|
+
level: 1,
|
|
141
|
+
exp: 0,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// ============================================================================
|
|
145
|
+
// 常量
|
|
146
|
+
// ============================================================================
|
|
147
|
+
|
|
148
|
+
export const MAX_LEVEL = 100;
|
|
149
|
+
export const MAX_SKILL_LEVEL = 10;
|
|
150
|
+
export const MAX_STAMINA = 100;
|
|
151
|
+
export const MAX_EXP = 9999999;
|
|
152
|
+
export const MAX_TEAM_SIZE = 6;
|
|
153
|
+
export const MAX_INVENTORY_SIZE = 100;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 游戏状态类型 — game-engine 与 persistence 共享
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { DEFAULT_ATTRIBUTES } from './game-logic';
|
|
6
|
+
|
|
7
|
+
export interface Character {
|
|
8
|
+
name: string;
|
|
9
|
+
level: number;
|
|
10
|
+
exp: number;
|
|
11
|
+
hp: number;
|
|
12
|
+
maxHp: number;
|
|
13
|
+
mp: number;
|
|
14
|
+
maxMp: number;
|
|
15
|
+
stamina: number;
|
|
16
|
+
poison: number;
|
|
17
|
+
hurt: number;
|
|
18
|
+
attributes: typeof DEFAULT_ATTRIBUTES;
|
|
19
|
+
equipment: { weapon: string | null; armor: string | null };
|
|
20
|
+
skills: string[];
|
|
21
|
+
skillLevels: Record<string, number>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface Inventory {
|
|
25
|
+
silver: number;
|
|
26
|
+
items: Array<{ id: string; name: string; count: number }>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface GameState {
|
|
30
|
+
character: Character;
|
|
31
|
+
team: string[];
|
|
32
|
+
inventory: Inventory;
|
|
33
|
+
location: string;
|
|
34
|
+
week: number;
|
|
35
|
+
flags: Record<string, boolean | number>;
|
|
36
|
+
visitedMaps: string[];
|
|
37
|
+
completedQuests: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface BattleEnemy {
|
|
41
|
+
name: string;
|
|
42
|
+
hp: number;
|
|
43
|
+
maxHp: number;
|
|
44
|
+
attack: number;
|
|
45
|
+
defence: number;
|
|
46
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 将 npm 包安装到 Cursor skills 目录(jy)
|
|
4
|
+
*
|
|
5
|
+
* jy-skill install [--global] [--copy] [--force]
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { cpSync, existsSync, lstatSync, mkdirSync, rmSync, symlinkSync } from 'node:fs';
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
import { dirname, join, resolve } from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
|
|
13
|
+
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
14
|
+
const SKILL_NAME = 'jy';
|
|
15
|
+
|
|
16
|
+
function printHelp() {
|
|
17
|
+
console.log(`Usage: jy-skill install [options]
|
|
18
|
+
|
|
19
|
+
Install @sdd330dev/jy-skill into Cursor skills directory as "${SKILL_NAME}".
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
--global Install to ~/.cursor/skills/${SKILL_NAME}
|
|
23
|
+
--copy Copy files instead of symlink (default: symlink, fallback to copy)
|
|
24
|
+
--force Overwrite existing installation
|
|
25
|
+
-h, --help Show this help
|
|
26
|
+
`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseArgs(argv) {
|
|
30
|
+
const args = argv.slice(2);
|
|
31
|
+
if (args.length === 0 || args.includes('-h') || args.includes('--help')) {
|
|
32
|
+
return { command: 'help' };
|
|
33
|
+
}
|
|
34
|
+
if (args[0] !== 'install') {
|
|
35
|
+
console.error(`Unknown command: ${args[0] ?? '(none)'}`);
|
|
36
|
+
printHelp();
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
command: 'install',
|
|
41
|
+
global: args.includes('--global'),
|
|
42
|
+
copy: args.includes('--copy'),
|
|
43
|
+
force: args.includes('--force'),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getTargetDir(global) {
|
|
48
|
+
if (global) {
|
|
49
|
+
return join(homedir(), '.cursor', 'skills', SKILL_NAME);
|
|
50
|
+
}
|
|
51
|
+
return join(process.cwd(), '.cursor', 'skills', SKILL_NAME);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function removeExisting(targetDir) {
|
|
55
|
+
if (!existsSync(targetDir)) return;
|
|
56
|
+
const stat = lstatSync(targetDir);
|
|
57
|
+
if (stat.isSymbolicLink() || stat.isDirectory()) {
|
|
58
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
rmSync(targetDir, { force: true });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function installWithSymlink(targetDir) {
|
|
65
|
+
mkdirSync(dirname(targetDir), { recursive: true });
|
|
66
|
+
symlinkSync(PACKAGE_ROOT, targetDir, 'dir');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function installWithCopy(targetDir) {
|
|
70
|
+
mkdirSync(dirname(targetDir), { recursive: true });
|
|
71
|
+
cpSync(PACKAGE_ROOT, targetDir, {
|
|
72
|
+
recursive: true,
|
|
73
|
+
filter: (src) => !src.includes(`${join(PACKAGE_ROOT, 'node_modules')}`),
|
|
74
|
+
});
|
|
75
|
+
mkdirSync(join(targetDir, 'save'), { recursive: true });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function install(options) {
|
|
79
|
+
const targetDir = getTargetDir(options.global);
|
|
80
|
+
|
|
81
|
+
if (existsSync(targetDir) && !options.force) {
|
|
82
|
+
console.error(`Already installed at ${targetDir}`);
|
|
83
|
+
console.error('Use --force to overwrite.');
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (existsSync(targetDir)) {
|
|
88
|
+
removeExisting(targetDir);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (options.copy) {
|
|
92
|
+
installWithCopy(targetDir);
|
|
93
|
+
console.log(`Copied skill to ${targetDir}`);
|
|
94
|
+
} else {
|
|
95
|
+
try {
|
|
96
|
+
installWithSymlink(targetDir);
|
|
97
|
+
console.log(`Linked skill to ${targetDir}`);
|
|
98
|
+
} catch {
|
|
99
|
+
installWithCopy(targetDir);
|
|
100
|
+
console.log(`Symlink failed; copied skill to ${targetDir}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
console.log('');
|
|
105
|
+
console.log('Next: open the project in Cursor and say「jy」or「开始游戏」.');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const options = parseArgs(process.argv);
|
|
109
|
+
|
|
110
|
+
if (options.command === 'help') {
|
|
111
|
+
printHelp();
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
install(options);
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 游戏存档持久化 — save/game-state.json
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
existsSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
renameSync,
|
|
10
|
+
unlinkSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from 'node:fs';
|
|
13
|
+
import { dirname, join } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import type { GameState } from './game-types';
|
|
16
|
+
import { DEFAULT_ATTRIBUTES, MAX_STAMINA } from './game-logic';
|
|
17
|
+
import { getMap, getTemplates, initConfigs } from './config-loader';
|
|
18
|
+
|
|
19
|
+
export interface LoadGameResult {
|
|
20
|
+
state: GameState;
|
|
21
|
+
isNewGame: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const ROOT_DIR = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
25
|
+
const SAVE_DIR = join(ROOT_DIR, 'save');
|
|
26
|
+
const SAVE_FILE = join(SAVE_DIR, 'game-state.json');
|
|
27
|
+
const SAVE_TMP = join(SAVE_DIR, 'game-state.json.tmp');
|
|
28
|
+
|
|
29
|
+
export function getSavePath(): string {
|
|
30
|
+
return SAVE_FILE;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isValidGameState(raw: unknown): raw is GameState {
|
|
34
|
+
if (!raw || typeof raw !== 'object') return false;
|
|
35
|
+
const state = raw as GameState;
|
|
36
|
+
if (!state.character?.name || typeof state.location !== 'string') return false;
|
|
37
|
+
if (!state.inventory || typeof state.inventory.silver !== 'number') return false;
|
|
38
|
+
if (!Array.isArray(state.inventory.items)) return false;
|
|
39
|
+
if (typeof state.week !== 'number') return false;
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 补全旧存档缺失字段 */
|
|
44
|
+
function migrateGameState(state: GameState): GameState {
|
|
45
|
+
initConfigs();
|
|
46
|
+
|
|
47
|
+
if (!Array.isArray(state.character.skills)) {
|
|
48
|
+
state.character.skills = [...(getTemplates().defaultCharacter.skills ?? ['基本拳法'])];
|
|
49
|
+
}
|
|
50
|
+
if (!state.character.skillLevels) {
|
|
51
|
+
state.character.skillLevels = Object.fromEntries(state.character.skills.map((s) => [s, 0]));
|
|
52
|
+
}
|
|
53
|
+
if (!state.character.attributes || typeof state.character.attributes !== 'object') {
|
|
54
|
+
state.character.attributes = { ...DEFAULT_ATTRIBUTES };
|
|
55
|
+
} else {
|
|
56
|
+
state.character.attributes = { ...DEFAULT_ATTRIBUTES, ...state.character.attributes };
|
|
57
|
+
}
|
|
58
|
+
if (!state.character.equipment) {
|
|
59
|
+
state.character.equipment = { weapon: null, armor: null };
|
|
60
|
+
}
|
|
61
|
+
if (!Array.isArray(state.team)) {
|
|
62
|
+
state.team = [];
|
|
63
|
+
}
|
|
64
|
+
if (!state.flags || typeof state.flags !== 'object') {
|
|
65
|
+
state.flags = {};
|
|
66
|
+
}
|
|
67
|
+
if (!Array.isArray(state.visitedMaps)) {
|
|
68
|
+
state.visitedMaps = [state.location];
|
|
69
|
+
}
|
|
70
|
+
if (!Array.isArray(state.completedQuests)) {
|
|
71
|
+
state.completedQuests = [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const c = state.character;
|
|
75
|
+
if (typeof c.maxHp !== 'number') c.maxHp = DEFAULT_ATTRIBUTES.maxHp;
|
|
76
|
+
if (typeof c.maxMp !== 'number') c.maxMp = DEFAULT_ATTRIBUTES.maxMp;
|
|
77
|
+
c.hp = Math.max(0, Math.min(c.maxHp, c.hp));
|
|
78
|
+
c.mp = Math.max(0, Math.min(c.maxMp, c.mp));
|
|
79
|
+
c.stamina = Math.max(0, Math.min(MAX_STAMINA, c.stamina));
|
|
80
|
+
c.poison = Math.max(0, c.poison);
|
|
81
|
+
c.hurt = Math.max(0, c.hurt);
|
|
82
|
+
|
|
83
|
+
if (!getMap(state.location)) {
|
|
84
|
+
state.location = getTemplates().startLocation ?? '小村';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return state;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 读取存档;不存在或损坏时返回 null */
|
|
91
|
+
export function loadGameState(): GameState | null {
|
|
92
|
+
if (!existsSync(SAVE_FILE)) return null;
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const raw = readFileSync(SAVE_FILE, 'utf-8');
|
|
96
|
+
const parsed: unknown = JSON.parse(raw);
|
|
97
|
+
if (!isValidGameState(parsed)) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return migrateGameState(parsed);
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** 写入存档(原子替换,避免写入中断损坏) */
|
|
107
|
+
export function saveGameState(state: GameState): void {
|
|
108
|
+
if (!existsSync(SAVE_DIR)) {
|
|
109
|
+
mkdirSync(SAVE_DIR, { recursive: true });
|
|
110
|
+
}
|
|
111
|
+
writeFileSync(SAVE_TMP, JSON.stringify(state, null, 2), 'utf-8');
|
|
112
|
+
renameSync(SAVE_TMP, SAVE_FILE);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 删除存档 */
|
|
116
|
+
export function deleteSave(): void {
|
|
117
|
+
if (existsSync(SAVE_FILE)) {
|
|
118
|
+
unlinkSync(SAVE_FILE);
|
|
119
|
+
}
|
|
120
|
+
if (existsSync(SAVE_TMP)) {
|
|
121
|
+
unlinkSync(SAVE_TMP);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** 开始或继续:有存档则加载,否则新建并落盘 */
|
|
126
|
+
export function loadOrCreateGame(
|
|
127
|
+
createNewGame: (name: string) => GameState,
|
|
128
|
+
name = '主角',
|
|
129
|
+
): LoadGameResult {
|
|
130
|
+
const existing = loadGameState();
|
|
131
|
+
if (existing) return { state: existing, isNewGame: false };
|
|
132
|
+
const state = createNewGame(name);
|
|
133
|
+
saveGameState(state);
|
|
134
|
+
return { state, isNewGame: true };
|
|
135
|
+
}
|