@musnows/scriverse 0.7.3 → 0.7.4
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/README.en.md +3 -0
- package/README.md +3 -0
- package/dist/ai-connectivity-test.js +109 -0
- package/dist/ai-connectivity-test.js.map +1 -0
- package/dist/ai-conversation-export.js +70 -0
- package/dist/ai-conversation-export.js.map +1 -0
- package/dist/ai-stream-timeout.js +18 -0
- package/dist/ai-stream-timeout.js.map +1 -0
- package/dist/ai.js +681 -107
- package/dist/ai.js.map +1 -1
- package/dist/app.js +326 -53
- package/dist/app.js.map +1 -1
- package/dist/character-extraction.js +133 -0
- package/dist/character-extraction.js.map +1 -0
- package/dist/cli-core.js +7 -6
- package/dist/cli-core.js.map +1 -1
- package/dist/database.js +175 -2
- package/dist/database.js.map +1 -1
- package/dist/epub-export.js +319 -0
- package/dist/epub-export.js.map +1 -0
- package/dist/hybrid-search.js +8 -0
- package/dist/hybrid-search.js.map +1 -1
- package/dist/public/ai-connectivity-test.d.ts +7 -0
- package/dist/public/ai-connectivity-test.js +82 -0
- package/dist/public/ai-request-manager.js +99 -0
- package/dist/public/ai-stream-protocol.js +51 -0
- package/dist/public/app.js +2567 -265
- package/dist/public/chapter-version-diff.d.ts +20 -0
- package/dist/public/chapter-version-diff.js +116 -0
- package/dist/public/foreshadow-reminder.d.ts +32 -0
- package/dist/public/foreshadow-reminder.js +73 -0
- package/dist/public/global-replace-refresh.js +60 -0
- package/dist/public/index.html +120 -12
- package/dist/public/outline-board.d.ts +61 -0
- package/dist/public/outline-board.js +137 -0
- package/dist/public/page-route.d.ts +1 -0
- package/dist/public/page-route.js +8 -0
- package/dist/public/reading-preview.d.ts +32 -0
- package/dist/public/reading-preview.js +136 -0
- package/dist/public/styles.css +485 -4
- package/dist/public/upload-progress.d.ts +2 -0
- package/dist/public/upload-progress.js +10 -0
- package/dist/security.js +5 -2
- package/dist/security.js.map +1 -1
- package/dist/server-runtime.js +2 -0
- package/dist/server-runtime.js.map +1 -1
- package/dist/store.js +825 -88
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +45 -9
- package/dist/user-auth.js.map +1 -1
- package/dist/utils.js +3 -0
- package/dist/utils.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
const outlineStatuses = new Set(["all", "empty", "draft", "ready", "completed"]);
|
|
2
|
+
const foreshadowStatuses = new Set(["all", "none", "unresolved", "resolved", "abandoned"]);
|
|
3
|
+
const sortModes = new Set(["tree", "status", "foreshadows", "title"]);
|
|
4
|
+
|
|
5
|
+
function text(value) {
|
|
6
|
+
return String(value ?? "");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function normalizedSearchText(value) {
|
|
10
|
+
return text(value).normalize("NFKC").trim().toLocaleLowerCase("zh-CN");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function numericOrder(value) {
|
|
14
|
+
const candidate = Number(value);
|
|
15
|
+
return Number.isFinite(candidate) ? candidate : 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function stableIdCompare(left, right) {
|
|
19
|
+
return text(left?.id).localeCompare(text(right?.id), "zh-CN");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function treeCompare(left, right) {
|
|
23
|
+
const delta = numericOrder(left?.sortOrder) - numericOrder(right?.sortOrder);
|
|
24
|
+
return delta || stableIdCompare(left, right);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function outlineStatusRank(chapter) {
|
|
28
|
+
if (!chapter?.outline) return 0;
|
|
29
|
+
return { draft: 1, ready: 2, completed: 3 }[chapter.outline.status] ?? 1;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function unresolvedForeshadowCount(chapter) {
|
|
33
|
+
return (Array.isArray(chapter?.foreshadows) ? chapter.foreshadows : [])
|
|
34
|
+
.filter((foreshadow) => foreshadow?.status === "planned" || foreshadow?.status === "planted")
|
|
35
|
+
.length;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function compareChapters(left, right, sort) {
|
|
39
|
+
if (sort === "status") {
|
|
40
|
+
const delta = outlineStatusRank(left) - outlineStatusRank(right);
|
|
41
|
+
if (delta) return delta;
|
|
42
|
+
}
|
|
43
|
+
if (sort === "foreshadows") {
|
|
44
|
+
const unresolvedDelta = unresolvedForeshadowCount(right) - unresolvedForeshadowCount(left);
|
|
45
|
+
if (unresolvedDelta) return unresolvedDelta;
|
|
46
|
+
const totalDelta = (right?.foreshadows?.length ?? 0) - (left?.foreshadows?.length ?? 0);
|
|
47
|
+
if (totalDelta) return totalDelta;
|
|
48
|
+
}
|
|
49
|
+
if (sort === "title") {
|
|
50
|
+
const delta = text(left?.title).localeCompare(text(right?.title), "zh-CN");
|
|
51
|
+
if (delta) return delta;
|
|
52
|
+
}
|
|
53
|
+
return treeCompare(left, right);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function matchesQuery(chapter, query) {
|
|
57
|
+
if (!query) return true;
|
|
58
|
+
const outline = chapter?.outline ?? {};
|
|
59
|
+
const values = [
|
|
60
|
+
chapter?.title,
|
|
61
|
+
chapter?.chapterType,
|
|
62
|
+
outline.goal,
|
|
63
|
+
outline.conflict,
|
|
64
|
+
outline.turningPoint,
|
|
65
|
+
outline.notes,
|
|
66
|
+
...(Array.isArray(chapter?.foreshadows) ? chapter.foreshadows.map((item) => item?.title) : [])
|
|
67
|
+
];
|
|
68
|
+
return values.some((value) => normalizedSearchText(value).includes(query));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function matchesOutlineStatus(chapter, status) {
|
|
72
|
+
if (status === "all") return true;
|
|
73
|
+
if (status === "empty") return !chapter?.outline;
|
|
74
|
+
return chapter?.outline?.status === status;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function matchesForeshadowStatus(chapter, status) {
|
|
78
|
+
if (status === "all") return true;
|
|
79
|
+
const foreshadows = Array.isArray(chapter?.foreshadows) ? chapter.foreshadows : [];
|
|
80
|
+
if (status === "none") return foreshadows.length === 0;
|
|
81
|
+
if (status === "unresolved") {
|
|
82
|
+
return foreshadows.some((item) => item?.status === "planned" || item?.status === "planted");
|
|
83
|
+
}
|
|
84
|
+
return foreshadows.some((item) => item?.status === status);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function normalizeOutlineBoardState(value = {}) {
|
|
88
|
+
const outlineStatus = text(value?.outlineStatus);
|
|
89
|
+
const foreshadowStatus = text(value?.foreshadowStatus);
|
|
90
|
+
const sort = text(value?.sort);
|
|
91
|
+
return {
|
|
92
|
+
query: text(value?.query),
|
|
93
|
+
volumeId: text(value?.volumeId),
|
|
94
|
+
outlineStatus: outlineStatuses.has(outlineStatus) ? outlineStatus : "all",
|
|
95
|
+
foreshadowStatus: foreshadowStatuses.has(foreshadowStatus) ? foreshadowStatus : "all",
|
|
96
|
+
sort: sortModes.has(sort) ? sort : "tree"
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 保持分卷层级与默认章节树顺序,对章节执行筛选和卷内排序。
|
|
102
|
+
* 未筛选时保留空分卷;指定空分卷时也保留该分组,便于确认数据状态。
|
|
103
|
+
*/
|
|
104
|
+
export function prepareOutlineBoard(board, value = {}) {
|
|
105
|
+
const state = normalizeOutlineBoardState(value);
|
|
106
|
+
const query = normalizedSearchText(state.query);
|
|
107
|
+
const chapterFilterActive = Boolean(query || state.outlineStatus !== "all" || state.foreshadowStatus !== "all");
|
|
108
|
+
const volumeFilterActive = Boolean(state.volumeId);
|
|
109
|
+
const sourceVolumes = Array.isArray(board?.volumes) ? board.volumes : [];
|
|
110
|
+
const totalChapterCount = sourceVolumes.reduce(
|
|
111
|
+
(total, volume) => total + (Array.isArray(volume?.chapters) ? volume.chapters.length : 0),
|
|
112
|
+
0
|
|
113
|
+
);
|
|
114
|
+
const volumes = [...sourceVolumes].sort(treeCompare).flatMap((volume) => {
|
|
115
|
+
if (volumeFilterActive && text(volume?.id) !== state.volumeId) return [];
|
|
116
|
+
const sourceChapters = Array.isArray(volume?.chapters) ? volume.chapters : [];
|
|
117
|
+
const chapters = sourceChapters
|
|
118
|
+
.filter((chapter) => matchesQuery(chapter, query)
|
|
119
|
+
&& matchesOutlineStatus(chapter, state.outlineStatus)
|
|
120
|
+
&& matchesForeshadowStatus(chapter, state.foreshadowStatus))
|
|
121
|
+
.sort((left, right) => compareChapters(left, right, state.sort));
|
|
122
|
+
const keepEmptyVolume = sourceChapters.length === 0 && (!chapterFilterActive || state.volumeId === text(volume?.id));
|
|
123
|
+
if (chapters.length === 0 && !keepEmptyVolume) return [];
|
|
124
|
+
return [{ ...volume, chapters }];
|
|
125
|
+
});
|
|
126
|
+
return {
|
|
127
|
+
state,
|
|
128
|
+
volumes,
|
|
129
|
+
totalChapterCount,
|
|
130
|
+
visibleChapterCount: volumes.reduce((total, volume) => total + volume.chapters.length, 0),
|
|
131
|
+
filtersActive: chapterFilterActive || volumeFilterActive
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function outlineBoardUnresolvedCount(chapter) {
|
|
136
|
+
return unresolvedForeshadowCount(chapter);
|
|
137
|
+
}
|
|
@@ -2,6 +2,7 @@ export type RestorableModule = "drafts" | "settings" | "characters" | "races" |
|
|
|
2
2
|
export type PageRoute =
|
|
3
3
|
| { view: "shelf" }
|
|
4
4
|
| { view: "editor"; workId: string; chapterId: string | null }
|
|
5
|
+
| { view: "reader"; workId: string; chapterId: string | null }
|
|
5
6
|
| { view: "module"; workId: string; module: RestorableModule }
|
|
6
7
|
| { view: "welcome"; workId: string }
|
|
7
8
|
| { view: "settings" | "platform-ai"; workId: string | null; returnView?: "shelf" | "editor" | "module" | "welcome"; returnModule?: RestorableModule; returnChapterId?: string };
|
|
@@ -40,6 +40,10 @@ export function serializePageRoute(route = {}) {
|
|
|
40
40
|
params.set("view", "editor");
|
|
41
41
|
params.set("work", workId);
|
|
42
42
|
if (route.chapterId) params.set("chapter", String(route.chapterId));
|
|
43
|
+
} else if (view === "reader" && workId) {
|
|
44
|
+
params.set("view", "reader");
|
|
45
|
+
params.set("work", workId);
|
|
46
|
+
if (route.chapterId) params.set("chapter", String(route.chapterId));
|
|
43
47
|
} else if (view === "module" && workId && moduleSet.has(route.module)) {
|
|
44
48
|
params.set("view", "module");
|
|
45
49
|
params.set("work", workId);
|
|
@@ -75,6 +79,10 @@ export function parsePageRoute(hash = "") {
|
|
|
75
79
|
const chapterId = value(params, "chapter");
|
|
76
80
|
return { view, workId, chapterId: chapterId || null };
|
|
77
81
|
}
|
|
82
|
+
if (view === "reader" && workId) {
|
|
83
|
+
const chapterId = value(params, "chapter");
|
|
84
|
+
return { view, workId, chapterId: chapterId || null };
|
|
85
|
+
}
|
|
78
86
|
if (view === "module" && workId) {
|
|
79
87
|
const module = value(params, "module");
|
|
80
88
|
return moduleSet.has(module) ? { view, workId, module } : { view: "shelf" };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type ReadingMode = "scroll" | "paged";
|
|
2
|
+
export type ReadingTheme = "paper" | "light" | "dark";
|
|
3
|
+
export type ReadingPreferences = { mode: ReadingMode; fontSize: number; lineHeight: number; theme: ReadingTheme };
|
|
4
|
+
export type ReadingChapter = {
|
|
5
|
+
id: string;
|
|
6
|
+
workId: string;
|
|
7
|
+
volumeId: string;
|
|
8
|
+
volumeTitle: string;
|
|
9
|
+
title: string;
|
|
10
|
+
chapterType: string;
|
|
11
|
+
volumeIndex: number;
|
|
12
|
+
chapterIndex: number;
|
|
13
|
+
sequenceIndex: number;
|
|
14
|
+
};
|
|
15
|
+
export type ReadingPosition = { chapterId: string; scrollRatio: number; pageIndex: number };
|
|
16
|
+
|
|
17
|
+
export const READING_PREFERENCES_STORAGE_KEY: string;
|
|
18
|
+
export const READING_POSITION_STORAGE_PREFIX: string;
|
|
19
|
+
export const DEFAULT_READING_PREFERENCES: Readonly<ReadingPreferences>;
|
|
20
|
+
export function normalizeReadingPreferences(value: unknown): ReadingPreferences;
|
|
21
|
+
export function readingPositionStorageKey(workId: unknown): string;
|
|
22
|
+
export function buildReadingChapterSequence(work: unknown): ReadingChapter[];
|
|
23
|
+
export function normalizeReadingPosition(value: unknown, sequence: ReadingChapter[]): ReadingPosition | null;
|
|
24
|
+
export function resolveReadingStart(sequence: ReadingChapter[], options?: { chapterId?: unknown; volumeId?: unknown; storedPosition?: unknown }): ReadingChapter | null;
|
|
25
|
+
export function adjacentReadingChapter(sequence: ReadingChapter[], chapterId: string, direction: number): ReadingChapter | null;
|
|
26
|
+
export function resolvePagedReadingStep(input: { sequence: ReadingChapter[]; chapterId: string; pageIndex: number; pageCount: number }, direction: number): { chapterId: string; pageIndex: number; chapterChanged: boolean } | null;
|
|
27
|
+
export function createReadingRequestGate(): {
|
|
28
|
+
begin(chapterId: string): { chapterId: string; generation: number; signal: AbortSignal };
|
|
29
|
+
isCurrent(request: { chapterId: string; generation: number; signal: AbortSignal }): boolean;
|
|
30
|
+
finish(request: { chapterId: string; generation: number }): void;
|
|
31
|
+
cancel(): void;
|
|
32
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
export const READING_PREFERENCES_STORAGE_KEY = "scriverse-reading-preferences-v1";
|
|
2
|
+
export const READING_POSITION_STORAGE_PREFIX = "scriverse-reading-position-v1:";
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_READING_PREFERENCES = Object.freeze({
|
|
5
|
+
mode: "scroll",
|
|
6
|
+
fontSize: 20,
|
|
7
|
+
lineHeight: 1.9,
|
|
8
|
+
theme: "paper"
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const readingModes = new Set(["scroll", "paged"]);
|
|
12
|
+
const readingFontSizes = new Set([16, 18, 20, 22, 24]);
|
|
13
|
+
const readingLineHeights = new Set([1.6, 1.8, 1.9, 2, 2.2]);
|
|
14
|
+
const readingThemes = new Set(["paper", "light", "dark"]);
|
|
15
|
+
|
|
16
|
+
function record(value) {
|
|
17
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function normalizeReadingPreferences(value) {
|
|
21
|
+
const candidate = record(value);
|
|
22
|
+
const fontSize = Number(candidate.fontSize);
|
|
23
|
+
const lineHeight = Number(candidate.lineHeight);
|
|
24
|
+
return {
|
|
25
|
+
mode: readingModes.has(candidate.mode) ? candidate.mode : DEFAULT_READING_PREFERENCES.mode,
|
|
26
|
+
fontSize: readingFontSizes.has(fontSize) ? fontSize : DEFAULT_READING_PREFERENCES.fontSize,
|
|
27
|
+
lineHeight: readingLineHeights.has(lineHeight) ? lineHeight : DEFAULT_READING_PREFERENCES.lineHeight,
|
|
28
|
+
theme: readingThemes.has(candidate.theme) ? candidate.theme : DEFAULT_READING_PREFERENCES.theme
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function readingPositionStorageKey(workId) {
|
|
33
|
+
return `${READING_POSITION_STORAGE_PREFIX}${encodeURIComponent(String(workId ?? ""))}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function buildReadingChapterSequence(work) {
|
|
37
|
+
const workId = String(work?.id ?? "");
|
|
38
|
+
const volumes = Array.isArray(work?.volumes) ? work.volumes : [];
|
|
39
|
+
const seen = new Set();
|
|
40
|
+
const sequence = [];
|
|
41
|
+
for (const [volumeIndex, volume] of volumes.entries()) {
|
|
42
|
+
const volumeId = String(volume?.id ?? "");
|
|
43
|
+
const chapters = Array.isArray(volume?.chapters) ? volume.chapters : [];
|
|
44
|
+
for (const [chapterIndex, chapter] of chapters.entries()) {
|
|
45
|
+
const id = String(chapter?.id ?? "");
|
|
46
|
+
if (!id || seen.has(id)) continue;
|
|
47
|
+
seen.add(id);
|
|
48
|
+
sequence.push({
|
|
49
|
+
id,
|
|
50
|
+
workId: String(chapter?.workId ?? workId),
|
|
51
|
+
volumeId: String(chapter?.volumeId ?? volumeId),
|
|
52
|
+
volumeTitle: String(volume?.title ?? "正文"),
|
|
53
|
+
title: String(chapter?.title ?? "未命名章节"),
|
|
54
|
+
chapterType: String(chapter?.chapterType ?? "正文"),
|
|
55
|
+
volumeIndex,
|
|
56
|
+
chapterIndex,
|
|
57
|
+
sequenceIndex: sequence.length
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return sequence;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function normalizeReadingPosition(value, sequence) {
|
|
65
|
+
const candidate = record(value);
|
|
66
|
+
const chapterId = String(candidate.chapterId ?? "");
|
|
67
|
+
if (!sequence.some((chapter) => chapter.id === chapterId)) return null;
|
|
68
|
+
const scrollRatio = Number(candidate.scrollRatio);
|
|
69
|
+
const pageIndex = Number(candidate.pageIndex);
|
|
70
|
+
return {
|
|
71
|
+
chapterId,
|
|
72
|
+
scrollRatio: Number.isFinite(scrollRatio) ? Math.min(1, Math.max(0, scrollRatio)) : 0,
|
|
73
|
+
pageIndex: Number.isInteger(pageIndex) && pageIndex >= 0 ? pageIndex : 0
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function resolveReadingStart(sequence, options = {}) {
|
|
78
|
+
if (!sequence.length) return null;
|
|
79
|
+
const requestedChapterId = String(options.chapterId ?? "");
|
|
80
|
+
const requested = sequence.find((chapter) => chapter.id === requestedChapterId);
|
|
81
|
+
if (requested) return requested;
|
|
82
|
+
const stored = normalizeReadingPosition(options.storedPosition, sequence);
|
|
83
|
+
if (stored) return sequence.find((chapter) => chapter.id === stored.chapterId) ?? sequence[0];
|
|
84
|
+
const volumeId = String(options.volumeId ?? "");
|
|
85
|
+
return sequence.find((chapter) => chapter.volumeId === volumeId) ?? sequence[0];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function adjacentReadingChapter(sequence, chapterId, direction) {
|
|
89
|
+
const index = sequence.findIndex((chapter) => chapter.id === chapterId);
|
|
90
|
+
if (index < 0) return null;
|
|
91
|
+
return sequence[index + (direction < 0 ? -1 : 1)] ?? null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function resolvePagedReadingStep({ sequence, chapterId, pageIndex, pageCount }, direction) {
|
|
95
|
+
const safePageCount = Math.max(1, Math.floor(Number(pageCount) || 1));
|
|
96
|
+
const safePageIndex = Math.min(safePageCount - 1, Math.max(0, Math.floor(Number(pageIndex) || 0)));
|
|
97
|
+
if (direction >= 0) {
|
|
98
|
+
if (safePageIndex < safePageCount - 1) return { chapterId, pageIndex: safePageIndex + 1, chapterChanged: false };
|
|
99
|
+
const next = adjacentReadingChapter(sequence, chapterId, 1);
|
|
100
|
+
return next ? { chapterId: next.id, pageIndex: 0, chapterChanged: true } : null;
|
|
101
|
+
}
|
|
102
|
+
if (safePageIndex > 0) return { chapterId, pageIndex: safePageIndex - 1, chapterChanged: false };
|
|
103
|
+
const previous = adjacentReadingChapter(sequence, chapterId, -1);
|
|
104
|
+
return previous ? { chapterId: previous.id, pageIndex: -1, chapterChanged: true } : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function createReadingRequestGate() {
|
|
108
|
+
let generation = 0;
|
|
109
|
+
let active = null;
|
|
110
|
+
return {
|
|
111
|
+
begin(chapterId) {
|
|
112
|
+
active?.controller.abort();
|
|
113
|
+
const request = {
|
|
114
|
+
chapterId: String(chapterId),
|
|
115
|
+
generation: ++generation,
|
|
116
|
+
controller: new AbortController()
|
|
117
|
+
};
|
|
118
|
+
active = request;
|
|
119
|
+
return { chapterId: request.chapterId, generation: request.generation, signal: request.controller.signal };
|
|
120
|
+
},
|
|
121
|
+
isCurrent(request) {
|
|
122
|
+
return Boolean(active)
|
|
123
|
+
&& active.chapterId === request?.chapterId
|
|
124
|
+
&& active.generation === request?.generation
|
|
125
|
+
&& !request?.signal?.aborted;
|
|
126
|
+
},
|
|
127
|
+
finish(request) {
|
|
128
|
+
if (active?.generation === request?.generation) active = null;
|
|
129
|
+
},
|
|
130
|
+
cancel() {
|
|
131
|
+
active?.controller.abort();
|
|
132
|
+
active = null;
|
|
133
|
+
generation += 1;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|