Skip to content

Flowchart#996

Open
MakinoharaShoko wants to merge 5 commits into
devfrom
flowchart
Open

Flowchart#996
MakinoharaShoko wants to merge 5 commits into
devfrom
flowchart

Conversation

@MakinoharaShoko

Copy link
Copy Markdown
Member

为引擎添加流程图功能。

总得来说,就是类似于柚子社游戏,有一个流程图,并在推进章节时记录关键节点以供回溯,至少要包含以下功能:

1、流程图注册、展示
2、经过节点时记录状态,解锁流程图节点
3、分阶段展示流程图
4、支持个人线。

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a comprehensive Flowchart feature to the WebGAL engine, allowing players to visualize game branches and jump to unlocked nodes. It includes the core FlowchartManager logic, integration with gameplay controllers, UI components for rendering the flowchart SVG, and localization across multiple languages. The review feedback highlights several critical and medium-severity issues to address before merging: resolving a potential crash from using the non-standard 'chinese' locale in toLocaleTimeString, adding defensive null checks to prevent TypeErrors when accessing currentScene or globalGameVar during initialization, clearing a setTimeout in Flowchart.tsx to prevent memory leaks, and fixing edge cases in the flowchart layout algorithm that could lead to negative SVG heights or clipped node coordinates.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

nowStageState: cloneDeep(stageStateManager.getViewStageState()),
backlog: [],
index: -1,
saveTime: new Date().toLocaleDateString() + ' ' + new Date().toLocaleTimeString('chinese', { hour12: false }),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

createSnapshot 中,toLocaleTimeString('chinese') 使用了非标准的 BCP 47 语言标签 'chinese'。在某些严格的 JS 引擎(如 Node.js 或部分浏览器)中,这会抛出 RangeError: Incorrect locale information provided 错误,导致游戏崩溃。建议将其修改为标准的 'zh-CN''zh'

Suggested change
saveTime: new Date().toLocaleDateString() + ' ' + new Date().toLocaleTimeString('chinese', { hour12: false }),
saveTime: new Date().toLocaleDateString() + ' ' + new Date().toLocaleTimeString('zh-CN', { hour12: false }),

Comment on lines +120 to +125
public unlockCurrentScene(refreshSnapshot = false) {
if (!this.hasFlowchart()) return;
const sceneNames = new Set([
normalizeSceneName(this.sceneManager.sceneData.currentScene.sceneName),
normalizeSceneName(this.sceneManager.sceneData.currentScene.sceneUrl),
]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

unlockCurrentScene 中,直接访问 this.sceneManager.sceneData.currentScene 可能会在场景尚未加载完成时(例如初始化阶段)由于 currentSceneundefined 而抛出 TypeError。建议添加安全的空值检查。

  public unlockCurrentScene(refreshSnapshot = false) {
    if (!this.hasFlowchart()) return;
    const currentScene = this.sceneManager?.sceneData?.currentScene;
    if (!currentScene) return;
    const sceneNames = new Set([
      normalizeSceneName(currentScene.sceneName),
      normalizeSceneName(currentScene.sceneUrl),
    ]);

Comment on lines +151 to +165
private createSnapshot(): ISaveData {
return {
nowStageState: cloneDeep(stageStateManager.getViewStageState()),
backlog: [],
index: -1,
saveTime: new Date().toLocaleDateString() + ' ' + new Date().toLocaleTimeString('chinese', { hour12: false }),
sceneData: {
currentSentenceId: this.sceneManager.sceneData.currentSentenceId,
sceneStack: cloneDeep(this.sceneManager.sceneData.sceneStack),
sceneName: this.sceneManager.sceneData.currentScene.sceneName,
sceneUrl: this.sceneManager.sceneData.currentScene.sceneUrl,
},
previewImage: '',
};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

createSnapshot 中,直接访问 this.sceneManager.sceneData.currentScene 可能会在场景未加载时抛出 TypeError。建议使用可选链或空值保护,以确保防御性编程的安全性。

Suggested change
private createSnapshot(): ISaveData {
return {
nowStageState: cloneDeep(stageStateManager.getViewStageState()),
backlog: [],
index: -1,
saveTime: new Date().toLocaleDateString() + ' ' + new Date().toLocaleTimeString('chinese', { hour12: false }),
sceneData: {
currentSentenceId: this.sceneManager.sceneData.currentSentenceId,
sceneStack: cloneDeep(this.sceneManager.sceneData.sceneStack),
sceneName: this.sceneManager.sceneData.currentScene.sceneName,
sceneUrl: this.sceneManager.sceneData.currentScene.sceneUrl,
},
previewImage: '',
};
}
private createSnapshot(): ISaveData {
const currentScene = this.sceneManager?.sceneData?.currentScene;
return {
nowStageState: cloneDeep(stageStateManager.getViewStageState()),
backlog: [],
index: -1,
saveTime: new Date().toLocaleDateString() + ' ' + new Date().toLocaleTimeString('zh-CN', { hour12: false }),
sceneData: {
currentSentenceId: this.sceneManager.sceneData.currentSentenceId,
sceneStack: cloneDeep(this.sceneManager.sceneData.sceneStack),
sceneName: currentScene?.sceneName ?? '',
sceneUrl: currentScene?.sceneUrl ?? '',
},
previewImage: '',
};
}

Comment on lines +183 to +186
private currentSceneKey() {
const { currentScene } = this.sceneManager.sceneData;
return `${normalizeSceneName(currentScene.sceneName)}|${normalizeSceneName(currentScene.sceneUrl)}`;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

currentSceneKey 中,解构 this.sceneManager.sceneData 中的 currentScene 时,如果 currentScene 为空,后续的属性访问会抛出错误。建议添加空值保护。

  private currentSceneKey() {
    const currentScene = this.sceneManager?.sceneData?.currentScene;
    if (!currentScene) return '';
    return `${normalizeSceneName(currentScene.sceneName)}|${normalizeSceneName(currentScene.sceneUrl)}`;
  }

Comment on lines +64 to +71
useEffect(() => {
if (isOpen) {
if (timeRef.current) clearTimeout(timeRef.current);
setIndexHide(false);
} else {
timeRef.current = setTimeout(() => setIndexHide(true), 780);
}
}, [isOpen]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

useEffect 中设置了 setTimeout,但没有在组件卸载(unmount)时清除该定时器。如果组件在定时器触发前被卸载,会导致内存泄漏以及在未挂载组件上更新状态的 React 警告。建议在 useEffect 的清除函数中调用 clearTimeout

  useEffect(() => {
    if (isOpen) {
      if (timeRef.current) clearTimeout(timeRef.current);
      setIndexHide(false);
    } else {
      timeRef.current = setTimeout(() => setIndexHide(true), 780);
    }
    return () => {
      if (timeRef.current) clearTimeout(timeRef.current);
    };
  }, [isOpen]);

};

function layoutFlowchart(flowchart?: IFlowchart) {
if (!flowchart) return { nodes: [] as LayoutNode[], edges: [] as LayoutEdge[], width: 0, height: 0 };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

layoutFlowchart 中,如果传入的 flowchart 没有节点(nodes 为空),计算出的 height 可能会变成负数(例如 MARGIN_Y * 2 + NODE_HEIGHT - ROW_GAP),这会导致 SVG 渲染错误。建议在函数开头增加对空节点的安全拦截。

function layoutFlowchart(flowchart?: IFlowchart) {
  if (!flowchart || !flowchart.nodes || flowchart.nodes.length === 0) return { nodes: [] as LayoutNode[], edges: [] as LayoutEdge[], width: 0, height: 0 };

Comment on lines +266 to +269
x:
layer.length === 1 && parentXs.length > 0
? parentXs.reduce((sum, parentX) => sum + parentX, 0) / parentXs.length - nodeWidth / 2
: x,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

当单节点图层根据父节点位置动态居中时,如果父节点位置偏左且节点宽度较大,计算出的 x 坐标可能会变成负数,导致节点在 SVG 左侧被裁剪。建议对 x 坐标进行边界限制,确保其在 MARGIN_Xwidth - MARGIN_X - nodeWidth 之间。

Suggested change
x:
layer.length === 1 && parentXs.length > 0
? parentXs.reduce((sum, parentX) => sum + parentX, 0) / parentXs.length - nodeWidth / 2
: x,
x:
layer.length === 1 && parentXs.length > 0
? Math.max(MARGIN_X, Math.min(width - MARGIN_X - nodeWidth, parentXs.reduce((sum, parentX) => sum + parentX, 0) / parentXs.length - nodeWidth / 2))
: x,

}
const { isSupported: isFullscreenSupport, isFullScreen, toggle: toggleFullscreen } = useFullScreen();
const GUIStore = useSelector((state: RootState) => state.GUI);
const enableFlowchart = useSelector((state: RootState) => state.userData.globalGameVar.Enable_flowchart === true);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

在从 Redux store 中获取 Enable_flowchart 时,如果 globalGameVar 尚未初始化或为 undefined,直接访问其属性会抛出错误。建议使用可选链 ?. 进行安全访问。

Suggested change
const enableFlowchart = useSelector((state: RootState) => state.userData.globalGameVar.Enable_flowchart === true);
const enableFlowchart = useSelector((state: RootState) => state.userData.globalGameVar?.Enable_flowchart === true);

export const BottomControlPanelFilm = () => {
const showPanel = useValue(false);
const stageState = useStageState();
const enableFlowchart = useSelector((state: RootState) => state.userData.globalGameVar.Enable_flowchart === true);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

在获取 Enable_flowchart 时,建议使用可选链 ?. 访问 globalGameVar 的属性,以防止在未初始化时抛出错误。

Suggested change
const enableFlowchart = useSelector((state: RootState) => state.userData.globalGameVar.Enable_flowchart === true);
const enableFlowchart = useSelector((state: RootState) => state.userData.globalGameVar?.Enable_flowchart === true);


const { playSeClick, playSeDialogOpen, playSePageChange } = useSoundEffect();
const GUIState = useSelector((state: RootState) => state.GUI);
const enableFlowchart = useSelector((state: RootState) => state.userData.globalGameVar.Enable_flowchart === true);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

在获取 Enable_flowchart 时,建议使用可选链 ?. 访问 globalGameVar 的属性,以防止在未初始化时抛出错误。

Suggested change
const enableFlowchart = useSelector((state: RootState) => state.userData.globalGameVar.Enable_flowchart === true);
const enableFlowchart = useSelector((state: RootState) => state.userData.globalGameVar?.Enable_flowchart === true);

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 27, 2026

Copy link
Copy Markdown

Deploying webgal-dev with  Cloudflare Pages  Cloudflare Pages

Latest commit: e325e8e
Status: ✅  Deploy successful!
Preview URL: https://f578eebe.webgal-dev.pages.dev
Branch Preview URL: https://flowchart.webgal-dev.pages.dev

View logs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant