Extend square-hole creation flow with visual asset timeout guard
This commit is contained in:
@@ -74,7 +74,8 @@
|
|||||||
8. 后端初始化单局形状队列、洞口兼容规则和计分状态。
|
8. 后端初始化单局形状队列、洞口兼容规则和计分状态。
|
||||||
9. 玩家拖拽或点击形状投入洞口。
|
9. 玩家拖拽或点击形状投入洞口。
|
||||||
10. 后端裁决投入结果、连击、扣时、失败、胜利和成绩。
|
10. 后端裁决投入结果、连击、扣时、失败、胜利和成绩。
|
||||||
11. 前端只渲染后端快照与即时反馈,不承接正式规则真相。
|
11. 形状贴图、封面图和背景图必须由后端图片生成接口生成或由创作者上传,前端只保存和展示 URL / data URL。
|
||||||
|
12. 前端只渲染后端快照与即时反馈,不承接正式规则真相。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -106,7 +107,23 @@ Agent 需要把玩家一句灵感收束为上述锚点,不允许逐项盘问
|
|||||||
"themeText": "",
|
"themeText": "",
|
||||||
"twistRule": "",
|
"twistRule": "",
|
||||||
"shapeCount": 12,
|
"shapeCount": 12,
|
||||||
"difficulty": 4
|
"difficulty": 4,
|
||||||
|
"shapeOptions": [
|
||||||
|
{
|
||||||
|
"shapeKind": "circle",
|
||||||
|
"label": "圆形",
|
||||||
|
"imagePrompt": "一个圆形办公室印章贴纸"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"holeOptions": [
|
||||||
|
{
|
||||||
|
"holeId": "square-hole",
|
||||||
|
"holeKind": "square",
|
||||||
|
"label": "方洞",
|
||||||
|
"bonus": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"backgroundPrompt": "办公室纸箱玩具桌面背景"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -119,6 +136,10 @@ Agent 需要把玩家一句灵感收束为上述锚点,不允许逐项盘问
|
|||||||
4. `quickFillRequested=true` 时,模型应直接补齐剩余配置,后端把 `progressPercent` 固定为 `100`。
|
4. `quickFillRequested=true` 时,模型应直接补齐剩余配置,后端把 `progressPercent` 固定为 `100`。
|
||||||
5. 模型不可用或结果无法解析时,接口返回明确错误,不允许用确定性模板伪装成 AI 回复。
|
5. 模型不可用或结果无法解析时,接口返回明确错误,不允许用确定性模板伪装成 AI 回复。
|
||||||
6. 非流式消息接口和 SSE 流式消息接口都必须走同一套方洞 Agent turn,SSE 只额外负责把 `replyText` 增量回传。
|
6. 非流式消息接口和 SSE 流式消息接口都必须走同一套方洞 Agent turn,SSE 只额外负责把 `replyText` 增量回传。
|
||||||
|
7. `shapeOptions` 至少包含 `6` 个候选项;缺失时后端用当前题材生成默认候选项。
|
||||||
|
8. `holeOptions` 至少包含 `3` 个选项,最多 `6` 个选项;创作者可以自定义 label、洞口类型与是否为加分选项。
|
||||||
|
9. `bonus=true` 只表示“该选项被后端判定为正确时额外加 50 分”,不是公开提示;运行态 UI 不允许直接显示哪个选项是加分选项。
|
||||||
|
10. `backgroundPrompt` 用于生成运行态背景图;为空时后端用题材和反差规则拼出默认提示词。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -147,7 +168,30 @@ Agent 需要把玩家一句灵感收束为上述锚点,不允许逐项盘问
|
|||||||
|
|
||||||
其中 `square_hole_priority` 是参考视频核心反差的首选默认规则。
|
其中 `square_hole_priority` 是参考视频核心反差的首选默认规则。
|
||||||
|
|
||||||
## 6.3 前端表现
|
## 6.3 计分规则
|
||||||
|
|
||||||
|
首版计分由 `module-square-hole` 统一裁决:
|
||||||
|
|
||||||
|
1. 正确投入基础得分:`100 + 当前连击数 * 10`。
|
||||||
|
2. 如果本次投入的洞口是创作者配置的 `bonus=true` 选项,并且本次投入被判定为正确,额外加 `50` 分。
|
||||||
|
3. 错误投入不扣分,但连击清零。
|
||||||
|
4. 时间到、主动退出或本局结束不追加结算奖励。
|
||||||
|
5. 前端只展示后端返回的 `score / combo / bestCombo`,不自行计算分数。
|
||||||
|
|
||||||
|
## 6.4 图片资产规则
|
||||||
|
|
||||||
|
1. 草稿生成后必须进入生成进度页,后端按草稿配置生成:
|
||||||
|
- 封面图 `coverImageSrc`
|
||||||
|
- 背景图 `backgroundImageSrc`
|
||||||
|
- 每个 `shapeOptions[].imageSrc`
|
||||||
|
2. 创作者上传入口保留;上传图片可以覆盖生成图片。
|
||||||
|
3. 图片生成失败时保留草稿和可编辑配置,结果页展示缺失槽位,允许创作者重试生成或上传替代图。
|
||||||
|
4. 结果页必须展示每个形状选项及其图片、背景图、封面图和洞口选项配置。
|
||||||
|
5. 运行态当前形状优先显示 `imageSrc`,没有图片时才回退到 CSS 形状。
|
||||||
|
6. 运行态背景优先显示 `backgroundImageSrc`,没有图片时才回退到默认渐变。
|
||||||
|
7. 运行态顶部不显示“方洞是唯一解”或等价真实规则提示;只保留时间、进度、分数和连击。
|
||||||
|
|
||||||
|
## 6.5 前端表现
|
||||||
|
|
||||||
1. 竖屏优先,桌面端居中显示游戏台。
|
1. 竖屏优先,桌面端居中显示游戏台。
|
||||||
2. 当前形状位于屏幕下半区域,洞板位于上半区域。
|
2. 当前形状位于屏幕下半区域,洞板位于上半区域。
|
||||||
@@ -207,10 +251,13 @@ Agent 需要把玩家一句灵感收束为上述锚点,不允许逐项盘问
|
|||||||
10. `coverImageSrc`
|
10. `coverImageSrc`
|
||||||
11. `shapeCount`
|
11. `shapeCount`
|
||||||
12. `difficulty`
|
12. `difficulty`
|
||||||
13. `publicationStatus`
|
13. `shapeOptions`
|
||||||
14. `playCount`
|
14. `holeOptions`
|
||||||
15. `updatedAt`
|
15. `backgroundImageSrc`
|
||||||
16. `publishedAt`
|
16. `publicationStatus`
|
||||||
|
17. `playCount`
|
||||||
|
18. `updatedAt`
|
||||||
|
19. `publishedAt`
|
||||||
|
|
||||||
## 8.3 运行态 run snapshot
|
## 8.3 运行态 run snapshot
|
||||||
|
|
||||||
@@ -228,9 +275,12 @@ Agent 需要把玩家一句灵感收束为上述锚点,不允许逐项盘问
|
|||||||
12. `bestCombo`
|
12. `bestCombo`
|
||||||
13. `score`
|
13. `score`
|
||||||
14. `ruleLabel`
|
14. `ruleLabel`
|
||||||
15. `currentShape`
|
15. `backgroundImageSrc`
|
||||||
16. `holes`
|
16. `currentShape`
|
||||||
17. `lastFeedback`
|
17. `holes`
|
||||||
|
18. `lastFeedback`
|
||||||
|
|
||||||
|
运行态 `ruleLabel` 仅保留后端调试兼容字段,前端默认不展示。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# 方洞挑战 Agent LLM 超时兜底修复 2026-05-05
|
||||||
|
|
||||||
|
## 1. 问题
|
||||||
|
|
||||||
|
现场错误:
|
||||||
|
|
||||||
|
```text
|
||||||
|
方洞挑战聊天生成失败,请稍后重试。:LLM 请求超时,累计尝试 1 次
|
||||||
|
```
|
||||||
|
|
||||||
|
方洞挑战创作 Agent 在同一轮流式 JSON 中需要返回 `replyText`、玩法配置、形状选项、洞口选项和图片提示词。模型可能先返回可见回复,再继续输出完整 JSON;如果上游在流式读取阶段超过通用 LLM 请求超时,后端会发送 SSE `error`,前端只能保留本地 warning 消息,本轮后端会话不会成功推进。
|
||||||
|
|
||||||
|
## 2. 根因
|
||||||
|
|
||||||
|
`platform-llm` 的 `LlmTextRequest` 只有全局 `AppConfig.llm_request_timeout_ms`。创作 Agent 统一走 Responses 流式协议,方洞提示词扩展为视觉资产配置后,单轮输出长度明显增加;通用 30 秒超时更适合普通聊天,不适合结构化创作 Agent 的完整 JSON 流。
|
||||||
|
|
||||||
|
`request_text` 的初始 HTTP 请求会按 `max_retries` 重试,但 `stream_text` 已经进入 `response.chunk()` 读取后,当前错误路径固定记录为一次读取超时,所以用户看到“累计尝试 1 次”。
|
||||||
|
|
||||||
|
## 3. 落地策略
|
||||||
|
|
||||||
|
1. 在 `platform-llm::LlmTextRequest` 增加请求级 `request_timeout_ms` 覆写。
|
||||||
|
2. `execute_request` 优先使用请求级超时,没有覆写时继续使用全局配置。
|
||||||
|
3. `creation_agent_llm_turn` 的流式 JSON 请求统一使用更长的创作 Agent 超时窗口。
|
||||||
|
4. 该超时窗口只影响创作 Agent 的结构化流式 turn,不改变 RPG 运行时聊天、图片生成、SpacetimeDB procedure 或方洞玩法判定。
|
||||||
|
5. 不新增 SpacetimeDB 表结构,不修改 `migration.rs`。
|
||||||
|
|
||||||
|
## 4. 验收标准
|
||||||
|
|
||||||
|
1. `platform-llm` 测试覆盖请求级 timeout 会让慢响应提前超时。
|
||||||
|
2. `creation_agent_llm_turn` 测试覆盖流式 JSON 请求带创作 Agent timeout。
|
||||||
|
3. `cargo test -p platform-llm -p api-server creation_agent --manifest-path server-rs/Cargo.toml` 通过。
|
||||||
|
4. 后端代码变更后按项目约束运行 `npm run api-server:maincloud` 并确认 `/healthz`。
|
||||||
@@ -35,6 +35,21 @@ export interface ExecuteSquareHoleActionRequest {
|
|||||||
coverImageSrc?: string | null;
|
coverImageSrc?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SquareHoleShapeOption {
|
||||||
|
optionId: string;
|
||||||
|
shapeKind: string;
|
||||||
|
label: string;
|
||||||
|
imagePrompt: string;
|
||||||
|
imageSrc?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SquareHoleHoleOption {
|
||||||
|
holeId: string;
|
||||||
|
holeKind: string;
|
||||||
|
label: string;
|
||||||
|
bonus: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SquareHoleAnchorItemResponse {
|
export interface SquareHoleAnchorItemResponse {
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -54,6 +69,11 @@ export interface SquareHoleCreatorConfig {
|
|||||||
twistRule: string;
|
twistRule: string;
|
||||||
shapeCount: number;
|
shapeCount: number;
|
||||||
difficulty: number;
|
difficulty: number;
|
||||||
|
shapeOptions: SquareHoleShapeOption[];
|
||||||
|
holeOptions: SquareHoleHoleOption[];
|
||||||
|
backgroundPrompt: string;
|
||||||
|
coverImageSrc?: string | null;
|
||||||
|
backgroundImageSrc?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SquareHoleResultDraft {
|
export interface SquareHoleResultDraft {
|
||||||
@@ -63,6 +83,11 @@ export interface SquareHoleResultDraft {
|
|||||||
twistRule: string;
|
twistRule: string;
|
||||||
summary: string;
|
summary: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
|
coverImageSrc?: string | null;
|
||||||
|
backgroundPrompt: string;
|
||||||
|
backgroundImageSrc?: string | null;
|
||||||
|
shapeOptions: SquareHoleShapeOption[];
|
||||||
|
holeOptions: SquareHoleHoleOption[];
|
||||||
shapeCount: number;
|
shapeCount: number;
|
||||||
difficulty: number;
|
difficulty: number;
|
||||||
publishReady: boolean;
|
publishReady: boolean;
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export interface SquareHoleShapeSnapshot {
|
|||||||
shapeKind: SquareHoleShapeKind;
|
shapeKind: SquareHoleShapeKind;
|
||||||
label: string;
|
label: string;
|
||||||
color: string;
|
color: string;
|
||||||
|
imageSrc?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SquareHoleHoleSnapshot {
|
export interface SquareHoleHoleSnapshot {
|
||||||
@@ -45,6 +46,7 @@ export interface SquareHoleHoleSnapshot {
|
|||||||
label: string;
|
label: string;
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
|
bonus: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SquareHoleRunSnapshot {
|
export interface SquareHoleRunSnapshot {
|
||||||
@@ -62,6 +64,7 @@ export interface SquareHoleRunSnapshot {
|
|||||||
bestCombo: number;
|
bestCombo: number;
|
||||||
score: number;
|
score: number;
|
||||||
ruleLabel: string;
|
ruleLabel: string;
|
||||||
|
backgroundImageSrc?: string | null;
|
||||||
currentShape?: SquareHoleShapeSnapshot | null;
|
currentShape?: SquareHoleShapeSnapshot | null;
|
||||||
holes: SquareHoleHoleSnapshot[];
|
holes: SquareHoleHoleSnapshot[];
|
||||||
lastFeedback?: SquareHoleDropFeedback | null;
|
lastFeedback?: SquareHoleDropFeedback | null;
|
||||||
|
|||||||
@@ -4,6 +4,21 @@
|
|||||||
*/
|
*/
|
||||||
export type SquareHoleWorkPublicationStatus = 'draft' | 'published' | string;
|
export type SquareHoleWorkPublicationStatus = 'draft' | 'published' | string;
|
||||||
|
|
||||||
|
export interface SquareHoleShapeOption {
|
||||||
|
optionId: string;
|
||||||
|
shapeKind: string;
|
||||||
|
label: string;
|
||||||
|
imagePrompt: string;
|
||||||
|
imageSrc?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SquareHoleHoleOption {
|
||||||
|
holeId: string;
|
||||||
|
holeKind: string;
|
||||||
|
label: string;
|
||||||
|
bonus: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PutSquareHoleWorkRequest {
|
export interface PutSquareHoleWorkRequest {
|
||||||
gameName: string;
|
gameName: string;
|
||||||
themeText?: string;
|
themeText?: string;
|
||||||
@@ -11,6 +26,10 @@ export interface PutSquareHoleWorkRequest {
|
|||||||
summary: string;
|
summary: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
coverImageSrc?: string | null;
|
coverImageSrc?: string | null;
|
||||||
|
backgroundPrompt?: string;
|
||||||
|
backgroundImageSrc?: string | null;
|
||||||
|
shapeOptions?: SquareHoleShapeOption[];
|
||||||
|
holeOptions?: SquareHoleHoleOption[];
|
||||||
shapeCount: number;
|
shapeCount: number;
|
||||||
difficulty: number;
|
difficulty: number;
|
||||||
}
|
}
|
||||||
@@ -26,6 +45,10 @@ export interface SquareHoleWorkSummary {
|
|||||||
summary: string;
|
summary: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
coverImageSrc?: string | null;
|
coverImageSrc?: string | null;
|
||||||
|
backgroundPrompt: string;
|
||||||
|
backgroundImageSrc?: string | null;
|
||||||
|
shapeOptions: SquareHoleShapeOption[];
|
||||||
|
holeOptions: SquareHoleHoleOption[];
|
||||||
shapeCount: number;
|
shapeCount: number;
|
||||||
difficulty: number;
|
difficulty: number;
|
||||||
publicationStatus: SquareHoleWorkPublicationStatus;
|
publicationStatus: SquareHoleWorkPublicationStatus;
|
||||||
|
|||||||
@@ -13,9 +13,6 @@ export * from './contracts/puzzleAgentSession';
|
|||||||
export * from './contracts/puzzleResultPreview';
|
export * from './contracts/puzzleResultPreview';
|
||||||
export * from './contracts/puzzleRuntimeSession';
|
export * from './contracts/puzzleRuntimeSession';
|
||||||
export * from './contracts/puzzleWorkSummary';
|
export * from './contracts/puzzleWorkSummary';
|
||||||
export * from './contracts/squareHoleAgent';
|
|
||||||
export * from './contracts/squareHoleRuntime';
|
|
||||||
export * from './contracts/squareHoleWorks';
|
|
||||||
export * from './contracts/rpgAgentActions';
|
export * from './contracts/rpgAgentActions';
|
||||||
export * from './contracts/rpgAgentAnchors';
|
export * from './contracts/rpgAgentAnchors';
|
||||||
export * from './contracts/rpgAgentDraft';
|
export * from './contracts/rpgAgentDraft';
|
||||||
@@ -29,6 +26,19 @@ export * from './contracts/rpgRuntimeQuestAssist';
|
|||||||
export * from './contracts/rpgRuntimeStoryAction';
|
export * from './contracts/rpgRuntimeStoryAction';
|
||||||
export * from './contracts/rpgRuntimeStoryState';
|
export * from './contracts/rpgRuntimeStoryState';
|
||||||
export * from './contracts/runtime';
|
export * from './contracts/runtime';
|
||||||
|
export * from './contracts/squareHoleAgent';
|
||||||
|
export * from './contracts/squareHoleRuntime';
|
||||||
|
export type {
|
||||||
|
PutSquareHoleWorkRequest,
|
||||||
|
SquareHoleWorkDetailResponse,
|
||||||
|
SquareHoleHoleOption as SquareHoleWorkHoleOption,
|
||||||
|
SquareHoleWorkMutationResponse,
|
||||||
|
SquareHoleWorkProfile,
|
||||||
|
SquareHoleWorkPublicationStatus,
|
||||||
|
SquareHoleShapeOption as SquareHoleWorkShapeOption,
|
||||||
|
SquareHoleWorksResponse,
|
||||||
|
SquareHoleWorkSummary,
|
||||||
|
} from './contracts/squareHoleWorks';
|
||||||
export type * from './contracts/story';
|
export type * from './contracts/story';
|
||||||
export * from './http';
|
export * from './http';
|
||||||
export * from './llm/narrativeLanguage';
|
export * from './llm/narrativeLanguage';
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ use serde_json::Value as JsonValue;
|
|||||||
|
|
||||||
use crate::llm_model_routing::CREATION_TEMPLATE_LLM_MODEL;
|
use crate::llm_model_routing::CREATION_TEMPLATE_LLM_MODEL;
|
||||||
|
|
||||||
|
pub(crate) const CREATION_AGENT_STREAM_REQUEST_TIMEOUT_MS: u64 = 120_000;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub(crate) struct CreationAgentLlmTurnErrorMessages<'a> {
|
pub(crate) struct CreationAgentLlmTurnErrorMessages<'a> {
|
||||||
pub model_unavailable: &'a str,
|
pub model_unavailable: &'a str,
|
||||||
@@ -138,6 +140,7 @@ fn build_creation_agent_llm_request(
|
|||||||
.with_model(CREATION_TEMPLATE_LLM_MODEL)
|
.with_model(CREATION_TEMPLATE_LLM_MODEL)
|
||||||
.with_responses_api()
|
.with_responses_api()
|
||||||
.with_web_search(enable_web_search)
|
.with_web_search(enable_web_search)
|
||||||
|
.with_request_timeout_ms(CREATION_AGENT_STREAM_REQUEST_TIMEOUT_MS)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn request_creation_agent_json_turn<E>(
|
pub(crate) async fn request_creation_agent_json_turn<E>(
|
||||||
@@ -246,9 +249,9 @@ mod tests {
|
|||||||
use crate::llm_model_routing::CREATION_TEMPLATE_LLM_MODEL;
|
use crate::llm_model_routing::CREATION_TEMPLATE_LLM_MODEL;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
CreationAgentLlmTurnErrorMessages, build_creation_agent_llm_request,
|
CREATION_AGENT_STREAM_REQUEST_TIMEOUT_MS, CreationAgentLlmTurnErrorMessages,
|
||||||
extract_reply_text_from_partial_json, is_web_search_tool_unavailable,
|
build_creation_agent_llm_request, extract_reply_text_from_partial_json,
|
||||||
parse_json_response_text, stream_creation_agent_json_turn,
|
is_web_search_tool_unavailable, parse_json_response_text, stream_creation_agent_json_turn,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -277,6 +280,10 @@ mod tests {
|
|||||||
assert_eq!(request.model.as_deref(), Some(CREATION_TEMPLATE_LLM_MODEL));
|
assert_eq!(request.model.as_deref(), Some(CREATION_TEMPLATE_LLM_MODEL));
|
||||||
assert_eq!(request.protocol, platform_llm::LlmTextProtocol::Responses);
|
assert_eq!(request.protocol, platform_llm::LlmTextProtocol::Responses);
|
||||||
assert_eq!(request.messages.len(), 2);
|
assert_eq!(request.messages.len(), 2);
|
||||||
|
assert_eq!(
|
||||||
|
request.request_timeout_ms,
|
||||||
|
Some(CREATION_AGENT_STREAM_REQUEST_TIMEOUT_MS)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ pub async fn proxy_llm_chat_completions(
|
|||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
max_tokens: None,
|
max_tokens: None,
|
||||||
enable_web_search: false,
|
enable_web_search: false,
|
||||||
|
request_timeout_ms: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
if payload.stream {
|
if payload.stream {
|
||||||
|
|||||||
@@ -20,10 +20,12 @@ pub(crate) const SQUARE_HOLE_AGENT_SYSTEM_PROMPT: &str = r#"你是一个负责
|
|||||||
2. nextConfig 必须是完整对象,不能只输出 patch
|
2. nextConfig 必须是完整对象,不能只输出 patch
|
||||||
3. replyText 必须是自然中文,不能提“字段”“结构”“JSON”“后端”等内部词
|
3. replyText 必须是自然中文,不能提“字段”“结构”“JSON”“后端”等内部词
|
||||||
4. replyText 一次最多推进一个最关键问题
|
4. replyText 一次最多推进一个最关键问题
|
||||||
5. 如果用户要求自动配置,就直接补齐可发布草稿需要的题材、反差规则、形状数量和难度,不要继续提问
|
5. 如果用户要求自动配置,就直接补齐可发布草稿需要的题材、反差规则、形状数量、难度、形状选项、洞口选项和背景提示,不要继续提问
|
||||||
6. 默认核心反差优先使用“方洞万能”或“方洞优先”,但可以根据用户题材包装成更有记忆点的规则
|
6. 默认核心反差优先使用“方洞万能”或“方洞优先”,但可以根据用户题材包装成更有记忆点的规则
|
||||||
7. progressPercent 范围只能是 0 到 100
|
7. progressPercent 范围只能是 0 到 100
|
||||||
8. shapeCount 只能是 6 到 24 的整数,difficulty 只能是 1 到 10 的整数
|
8. shapeCount 只能是 6 到 24 的整数,difficulty 只能是 1 到 10 的整数
|
||||||
|
9. shapeOptions 至少给 6 个,holeOptions 给 3 到 6 个,且至少一个 holeOptions.bonus 为 true
|
||||||
|
10. imagePrompt 和 backgroundPrompt 必须适合直接生成图片,不要包含 UI、文字、水印或解释
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
const SQUARE_HOLE_AGENT_OUTPUT_CONTRACT: &str = r#"请严格按以下 JSON 输出,不要输出其他文字:
|
const SQUARE_HOLE_AGENT_OUTPUT_CONTRACT: &str = r#"请严格按以下 JSON 输出,不要输出其他文字:
|
||||||
@@ -34,7 +36,24 @@ const SQUARE_HOLE_AGENT_OUTPUT_CONTRACT: &str = r#"请严格按以下 JSON 输
|
|||||||
"themeText": "",
|
"themeText": "",
|
||||||
"twistRule": "",
|
"twistRule": "",
|
||||||
"shapeCount": 12,
|
"shapeCount": 12,
|
||||||
"difficulty": 4
|
"difficulty": 4,
|
||||||
|
"shapeOptions": [
|
||||||
|
{
|
||||||
|
"optionId": "square-block",
|
||||||
|
"shapeKind": "square",
|
||||||
|
"label": "方块",
|
||||||
|
"imagePrompt": "玩具纸箱主题的方块贴纸图,透明背景,明亮可爱,游戏资产"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"holeOptions": [
|
||||||
|
{
|
||||||
|
"holeId": "square-hole",
|
||||||
|
"holeKind": "square",
|
||||||
|
"label": "方洞",
|
||||||
|
"bonus": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"backgroundPrompt": "玩具桌面上的纸箱洞板背景,中央留出操作空间"
|
||||||
}
|
}
|
||||||
}"#;
|
}"#;
|
||||||
|
|
||||||
@@ -42,8 +61,7 @@ pub(crate) const SQUARE_HOLE_AGENT_JSON_TURN_USER_PROMPT: &str = "请按约定
|
|||||||
|
|
||||||
/// 方洞挑战草稿生成对话提示词脚本。
|
/// 方洞挑战草稿生成对话提示词脚本。
|
||||||
///
|
///
|
||||||
/// 方洞首版只需要四个可写回 SpacetimeDB 的配置项,因此提示词直接围绕配置收束,
|
/// 方洞 Agent 负责输出完整玩法配置;后端会继续归一化缺失选项,避免模型偶发漏项导致草稿失败。
|
||||||
/// 不在模型输出层引入额外锚点,避免和当前持久化 schema 产生漂移。
|
|
||||||
pub(crate) fn build_square_hole_agent_prompt(
|
pub(crate) fn build_square_hole_agent_prompt(
|
||||||
session: &SquareHoleAgentSessionRecord,
|
session: &SquareHoleAgentSessionRecord,
|
||||||
quick_fill_requested: bool,
|
quick_fill_requested: bool,
|
||||||
@@ -53,7 +71,7 @@ pub(crate) fn build_square_hole_agent_prompt(
|
|||||||
"\n\n{}",
|
"\n\n{}",
|
||||||
render_quick_fill_extra_rules(
|
render_quick_fill_extra_rules(
|
||||||
"当前方洞挑战方向里的题材、反差规则、形状数量和难度",
|
"当前方洞挑战方向里的题材、反差规则、形状数量和难度",
|
||||||
"不要要求用户再提供洞口、形状、演出或难度信息",
|
"不要要求用户再提供洞口、形状、背景或难度信息",
|
||||||
"输出完整 nextConfig,直接补齐空缺或仍过于泛化的项",
|
"输出完整 nextConfig,直接补齐空缺或仍过于泛化的项",
|
||||||
"生成结果页",
|
"生成结果页",
|
||||||
)
|
)
|
||||||
@@ -62,7 +80,7 @@ pub(crate) fn build_square_hole_agent_prompt(
|
|||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
format!(
|
format!(
|
||||||
"模板目标:收束成可试玩、可发布的方洞挑战玩法草稿。{quick_fill_rules}\n\n当前是第 {turn} 轮,当前进度 {progress}% 。\n\n是否要求自动配置:{quick_fill_requested_text}\n\n当前配置:\n{current_config}\n\n最近聊天记录:\n{chat_history}\n\n收束要求:\n1. themeText 描述本局的玩具、道具或场景题材,保持短句。\n2. twistRule 描述真实判定规则,优先体现方洞优先或类似反直觉逻辑。\n3. shapeCount 决定单局形状数量,移动端短局建议 8 到 16。\n4. difficulty 决定误导强度和节奏,建议 3 到 7。\n5. 用户给出明确方向时优先吸收并推进,不要机械问完四个问题。\n\n{contract}",
|
"模板目标:收束成可试玩、可发布的方洞挑战玩法草稿。{quick_fill_rules}\n\n当前是第 {turn} 轮,当前进度 {progress}% 。\n\n是否要求自动配置:{quick_fill_requested_text}\n\n当前配置:\n{current_config}\n\n最近聊天记录:\n{chat_history}\n\n收束要求:\n1. themeText 描述本局的玩具、道具或场景题材,保持短句。\n2. twistRule 描述真实判定规则,优先体现方洞优先或类似反直觉逻辑。\n3. shapeCount 决定单局形状数量,移动端短局建议 8 到 16。\n4. difficulty 决定误导强度和节奏,建议 3 到 7。\n5. shapeOptions 必须给出至少 6 个可生成贴图的形状候选,每个 imagePrompt 都围绕主题生成。\n6. holeOptions 必须给出 3 到 6 个洞口,创作者可在结果页继续改;至少一个 bonus=true。\n7. backgroundPrompt 用于生成运行态背景,必须描述画面,不要写规则说明。\n8. 用户给出明确方向时优先吸收并推进,不要机械问完所有字段。\n\n{contract}",
|
||||||
quick_fill_rules = quick_fill_rules,
|
quick_fill_rules = quick_fill_rules,
|
||||||
turn = session.current_turn.saturating_add(1),
|
turn = session.current_turn.saturating_add(1),
|
||||||
progress = session.progress_percent,
|
progress = session.progress_percent,
|
||||||
@@ -76,11 +94,42 @@ pub(crate) fn build_square_hole_agent_prompt(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn serialize_square_hole_session_config(session: &SquareHoleAgentSessionRecord) -> String {
|
fn serialize_square_hole_session_config(session: &SquareHoleAgentSessionRecord) -> String {
|
||||||
|
let shape_options: Vec<JsonValue> = session
|
||||||
|
.config
|
||||||
|
.shape_options
|
||||||
|
.iter()
|
||||||
|
.map(|option| {
|
||||||
|
json!({
|
||||||
|
"optionId": option.option_id,
|
||||||
|
"shapeKind": option.shape_kind,
|
||||||
|
"label": option.label,
|
||||||
|
"imagePrompt": option.image_prompt,
|
||||||
|
"imageSrc": option.image_src,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let hole_options: Vec<JsonValue> = session
|
||||||
|
.config
|
||||||
|
.hole_options
|
||||||
|
.iter()
|
||||||
|
.map(|option| {
|
||||||
|
json!({
|
||||||
|
"holeId": option.hole_id,
|
||||||
|
"holeKind": option.hole_kind,
|
||||||
|
"label": option.label,
|
||||||
|
"bonus": option.bonus,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
serde_json::to_string_pretty(&json!({
|
serde_json::to_string_pretty(&json!({
|
||||||
"themeText": session.config.theme_text,
|
"themeText": session.config.theme_text,
|
||||||
"twistRule": session.config.twist_rule,
|
"twistRule": session.config.twist_rule,
|
||||||
"shapeCount": session.config.shape_count,
|
"shapeCount": session.config.shape_count,
|
||||||
"difficulty": session.config.difficulty,
|
"difficulty": session.config.difficulty,
|
||||||
|
"shapeOptions": shape_options,
|
||||||
|
"holeOptions": hole_options,
|
||||||
|
"backgroundPrompt": session.config.background_prompt,
|
||||||
}))
|
}))
|
||||||
.unwrap_or_else(|_| "{}".to_string())
|
.unwrap_or_else(|_| "{}".to_string())
|
||||||
}
|
}
|
||||||
@@ -129,6 +178,11 @@ mod tests {
|
|||||||
twist_rule: "方洞万能".to_string(),
|
twist_rule: "方洞万能".to_string(),
|
||||||
shape_count: 12,
|
shape_count: 12,
|
||||||
difficulty: 4,
|
difficulty: 4,
|
||||||
|
shape_options: Vec::new(),
|
||||||
|
hole_options: Vec::new(),
|
||||||
|
background_prompt: "积木纸箱桌面背景".to_string(),
|
||||||
|
cover_image_src: None,
|
||||||
|
background_image_src: None,
|
||||||
},
|
},
|
||||||
draft: None,
|
draft: None,
|
||||||
messages: vec![message("user", "做成办公室文具版")],
|
messages: vec![message("user", "做成办公室文具版")],
|
||||||
@@ -159,6 +213,9 @@ mod tests {
|
|||||||
assert!(prompt.contains("用户刚刚主动要求你自动补充剩余关键字"));
|
assert!(prompt.contains("用户刚刚主动要求你自动补充剩余关键字"));
|
||||||
assert!(prompt.contains("不要再继续提问"));
|
assert!(prompt.contains("不要再继续提问"));
|
||||||
assert!(prompt.contains("nextConfig"));
|
assert!(prompt.contains("nextConfig"));
|
||||||
|
assert!(prompt.contains("shapeOptions"));
|
||||||
|
assert!(prompt.contains("holeOptions"));
|
||||||
|
assert!(prompt.contains("backgroundPrompt"));
|
||||||
assert!(prompt.contains("progressPercent 直接输出为 100"));
|
assert!(prompt.contains("progressPercent 直接输出为 100"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,11 @@ use axum::{
|
|||||||
sse::{Event, Sse},
|
sse::{Event, Sse},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
|
||||||
use module_square_hole::{
|
use module_square_hole::{
|
||||||
SQUARE_HOLE_MESSAGE_ID_PREFIX, SQUARE_HOLE_PROFILE_ID_PREFIX, SQUARE_HOLE_RUN_ID_PREFIX,
|
SQUARE_HOLE_MESSAGE_ID_PREFIX, SQUARE_HOLE_PROFILE_ID_PREFIX, SQUARE_HOLE_RUN_ID_PREFIX,
|
||||||
SQUARE_HOLE_SESSION_ID_PREFIX,
|
SQUARE_HOLE_SESSION_ID_PREFIX, default_background_prompt, normalize_hole_options,
|
||||||
|
normalize_shape_options,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
@@ -24,8 +26,9 @@ use shared_contracts::{
|
|||||||
CreateSquareHoleSessionRequest, ExecuteSquareHoleActionRequest,
|
CreateSquareHoleSessionRequest, ExecuteSquareHoleActionRequest,
|
||||||
SendSquareHoleMessageRequest, SquareHoleActionResponse, SquareHoleAgentMessageResponse,
|
SendSquareHoleMessageRequest, SquareHoleActionResponse, SquareHoleAgentMessageResponse,
|
||||||
SquareHoleAnchorItemResponse, SquareHoleAnchorPackResponse,
|
SquareHoleAnchorItemResponse, SquareHoleAnchorPackResponse,
|
||||||
SquareHoleCreatorConfigResponse, SquareHoleResultDraftResponse, SquareHoleSessionResponse,
|
SquareHoleCreatorConfigResponse, SquareHoleHoleOptionResponse,
|
||||||
SquareHoleSessionSnapshotResponse,
|
SquareHoleResultDraftResponse, SquareHoleSessionResponse,
|
||||||
|
SquareHoleSessionSnapshotResponse, SquareHoleShapeOptionResponse,
|
||||||
},
|
},
|
||||||
square_hole_runtime::{
|
square_hole_runtime::{
|
||||||
DropSquareHoleShapeRequest, SquareHoleDropFeedbackResponse, SquareHoleDropResponse,
|
DropSquareHoleShapeRequest, SquareHoleDropFeedbackResponse, SquareHoleDropResponse,
|
||||||
@@ -33,7 +36,9 @@ use shared_contracts::{
|
|||||||
SquareHoleShapeSnapshotResponse, StartSquareHoleRunRequest, StopSquareHoleRunRequest,
|
SquareHoleShapeSnapshotResponse, StartSquareHoleRunRequest, StopSquareHoleRunRequest,
|
||||||
},
|
},
|
||||||
square_hole_works::{
|
square_hole_works::{
|
||||||
PutSquareHoleWorkRequest, SquareHoleWorkDetailResponse, SquareHoleWorkMutationResponse,
|
PutSquareHoleWorkRequest, SquareHoleHoleOptionResponse as SquareHoleWorkHoleOptionResponse,
|
||||||
|
SquareHoleShapeOptionResponse as SquareHoleWorkShapeOptionResponse,
|
||||||
|
SquareHoleWorkDetailResponse, SquareHoleWorkMutationResponse,
|
||||||
SquareHoleWorkProfileResponse, SquareHoleWorkSummaryResponse, SquareHoleWorksResponse,
|
SquareHoleWorkProfileResponse, SquareHoleWorkSummaryResponse, SquareHoleWorksResponse,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -42,11 +47,11 @@ use spacetime_client::{
|
|||||||
SpacetimeClientError, SquareHoleAgentMessageRecord, SquareHoleAgentMessageSubmitRecordInput,
|
SpacetimeClientError, SquareHoleAgentMessageRecord, SquareHoleAgentMessageSubmitRecordInput,
|
||||||
SquareHoleAgentSessionCreateRecordInput, SquareHoleAgentSessionRecord,
|
SquareHoleAgentSessionCreateRecordInput, SquareHoleAgentSessionRecord,
|
||||||
SquareHoleAnchorItemRecord, SquareHoleAnchorPackRecord, SquareHoleCompileDraftRecordInput,
|
SquareHoleAnchorItemRecord, SquareHoleAnchorPackRecord, SquareHoleCompileDraftRecordInput,
|
||||||
SquareHoleCreatorConfigRecord, SquareHoleDropFeedbackRecord, SquareHoleHoleSnapshotRecord,
|
SquareHoleCreatorConfigRecord, SquareHoleDropFeedbackRecord, SquareHoleHoleOptionRecord,
|
||||||
SquareHoleResultDraftRecord, SquareHoleRunDropRecordInput, SquareHoleRunRecord,
|
SquareHoleHoleSnapshotRecord, SquareHoleResultDraftRecord, SquareHoleRunDropRecordInput,
|
||||||
SquareHoleRunRestartRecordInput, SquareHoleRunStartRecordInput, SquareHoleRunStopRecordInput,
|
SquareHoleRunRecord, SquareHoleRunRestartRecordInput, SquareHoleRunStartRecordInput,
|
||||||
SquareHoleRunTimeUpRecordInput, SquareHoleShapeSnapshotRecord, SquareHoleWorkProfileRecord,
|
SquareHoleRunStopRecordInput, SquareHoleRunTimeUpRecordInput, SquareHoleShapeOptionRecord,
|
||||||
SquareHoleWorkUpdateRecordInput,
|
SquareHoleShapeSnapshotRecord, SquareHoleWorkProfileRecord, SquareHoleWorkUpdateRecordInput,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -54,6 +59,10 @@ use crate::{
|
|||||||
api_response::json_success_body,
|
api_response::json_success_body,
|
||||||
auth::AuthenticatedAccessToken,
|
auth::AuthenticatedAccessToken,
|
||||||
http_error::AppError,
|
http_error::AppError,
|
||||||
|
openai_image_generation::{
|
||||||
|
build_openai_image_http_client, create_openai_image_generation,
|
||||||
|
require_openai_image_settings,
|
||||||
|
},
|
||||||
request_context::RequestContext,
|
request_context::RequestContext,
|
||||||
square_hole_agent_turn::{
|
square_hole_agent_turn::{
|
||||||
SquareHoleAgentTurnRequest, build_finalize_record_input, run_square_hole_agent_turn,
|
SquareHoleAgentTurnRequest, build_finalize_record_input, run_square_hole_agent_turn,
|
||||||
@@ -77,6 +86,37 @@ struct SquareHoleConfigJson {
|
|||||||
twist_rule: String,
|
twist_rule: String,
|
||||||
shape_count: u32,
|
shape_count: u32,
|
||||||
difficulty: u32,
|
difficulty: u32,
|
||||||
|
#[serde(default)]
|
||||||
|
shape_options: Vec<SquareHoleConfigShapeOptionJson>,
|
||||||
|
#[serde(default)]
|
||||||
|
hole_options: Vec<SquareHoleConfigHoleOptionJson>,
|
||||||
|
#[serde(default)]
|
||||||
|
background_prompt: String,
|
||||||
|
#[serde(default, deserialize_with = "deserialize_optional_string_as_default")]
|
||||||
|
cover_image_src: String,
|
||||||
|
#[serde(default, deserialize_with = "deserialize_optional_string_as_default")]
|
||||||
|
background_image_src: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct SquareHoleConfigShapeOptionJson {
|
||||||
|
option_id: String,
|
||||||
|
shape_kind: String,
|
||||||
|
label: String,
|
||||||
|
image_prompt: String,
|
||||||
|
#[serde(default, deserialize_with = "deserialize_optional_string_as_default")]
|
||||||
|
image_src: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct SquareHoleConfigHoleOptionJson {
|
||||||
|
hole_id: String,
|
||||||
|
hole_kind: String,
|
||||||
|
label: String,
|
||||||
|
#[serde(default)]
|
||||||
|
bonus: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
@@ -299,25 +339,37 @@ pub async fn execute_square_hole_agent_action(
|
|||||||
"sessionId",
|
"sessionId",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
if payload.action.trim() != "square_hole_compile_draft" {
|
let session = match payload.action.trim() {
|
||||||
return Err(square_hole_bad_request(
|
"square_hole_compile_draft" => {
|
||||||
&request_context,
|
compile_square_hole_draft_for_session(
|
||||||
SQUARE_HOLE_AGENT_PROVIDER,
|
&state,
|
||||||
"unknown square hole action",
|
&request_context,
|
||||||
));
|
&authenticated,
|
||||||
}
|
session_id,
|
||||||
|
payload.game_name,
|
||||||
let session = compile_square_hole_draft_for_session(
|
payload.summary,
|
||||||
&state,
|
payload.tags,
|
||||||
&request_context,
|
payload.cover_image_src,
|
||||||
&authenticated,
|
)
|
||||||
session_id,
|
.await?
|
||||||
payload.game_name,
|
}
|
||||||
payload.summary,
|
"square_hole_generate_visual_assets" => {
|
||||||
payload.tags,
|
generate_square_hole_visual_assets_for_session(
|
||||||
payload.cover_image_src,
|
&state,
|
||||||
)
|
&request_context,
|
||||||
.await?;
|
&authenticated,
|
||||||
|
session_id,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(square_hole_bad_request(
|
||||||
|
&request_context,
|
||||||
|
SQUARE_HOLE_AGENT_PROVIDER,
|
||||||
|
"unknown square hole action",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Ok(json_success_body(
|
Ok(json_success_body(
|
||||||
Some(&request_context),
|
Some(&request_context),
|
||||||
@@ -491,6 +543,18 @@ pub async fn put_square_hole_work(
|
|||||||
.clone()
|
.clone()
|
||||||
.filter(|value| !value.trim().is_empty())
|
.filter(|value| !value.trim().is_empty())
|
||||||
.unwrap_or(existing.theme_text);
|
.unwrap_or(existing.theme_text);
|
||||||
|
let shape_options_json = payload
|
||||||
|
.shape_options
|
||||||
|
.clone()
|
||||||
|
.map(square_hole_work_shape_options_to_records)
|
||||||
|
.unwrap_or_else(|| existing.shape_options.clone());
|
||||||
|
let shape_options_json = serialize_square_hole_shape_option_records(&shape_options_json);
|
||||||
|
let hole_options_json = payload
|
||||||
|
.hole_options
|
||||||
|
.clone()
|
||||||
|
.map(square_hole_work_hole_options_to_records)
|
||||||
|
.unwrap_or_else(|| existing.hole_options.clone());
|
||||||
|
let hole_options_json = serialize_square_hole_hole_option_records(&hole_options_json);
|
||||||
let item = state
|
let item = state
|
||||||
.spacetime_client()
|
.spacetime_client()
|
||||||
.update_square_hole_work(SquareHoleWorkUpdateRecordInput {
|
.update_square_hole_work(SquareHoleWorkUpdateRecordInput {
|
||||||
@@ -502,6 +566,15 @@ pub async fn put_square_hole_work(
|
|||||||
summary_text: payload.summary,
|
summary_text: payload.summary,
|
||||||
tags_json: serde_json::to_string(&normalize_tags(payload.tags)).unwrap_or_default(),
|
tags_json: serde_json::to_string(&normalize_tags(payload.tags)).unwrap_or_default(),
|
||||||
cover_image_src: payload.cover_image_src.unwrap_or_default(),
|
cover_image_src: payload.cover_image_src.unwrap_or_default(),
|
||||||
|
background_prompt: payload
|
||||||
|
.background_prompt
|
||||||
|
.filter(|value| !value.trim().is_empty())
|
||||||
|
.unwrap_or(existing.background_prompt),
|
||||||
|
background_image_src: payload
|
||||||
|
.background_image_src
|
||||||
|
.unwrap_or(existing.background_image_src.unwrap_or_default()),
|
||||||
|
shape_options_json,
|
||||||
|
hole_options_json,
|
||||||
shape_count: payload.shape_count,
|
shape_count: payload.shape_count,
|
||||||
difficulty: payload.difficulty,
|
difficulty: payload.difficulty,
|
||||||
updated_at_micros: current_utc_micros(),
|
updated_at_micros: current_utc_micros(),
|
||||||
@@ -986,6 +1059,231 @@ async fn compile_square_hole_draft_for_session(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn generate_square_hole_visual_assets_for_session(
|
||||||
|
state: &AppState,
|
||||||
|
request_context: &RequestContext,
|
||||||
|
authenticated: &AuthenticatedAccessToken,
|
||||||
|
session_id: String,
|
||||||
|
) -> Result<SquareHoleAgentSessionRecord, Response> {
|
||||||
|
let owner_user_id = authenticated.claims().user_id().to_string();
|
||||||
|
let session = state
|
||||||
|
.spacetime_client()
|
||||||
|
.get_square_hole_agent_session(session_id.clone(), owner_user_id.clone())
|
||||||
|
.await
|
||||||
|
.map_err(|error| {
|
||||||
|
square_hole_error_response(
|
||||||
|
request_context,
|
||||||
|
SQUARE_HOLE_AGENT_PROVIDER,
|
||||||
|
map_square_hole_client_error(error),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let profile_id = session
|
||||||
|
.draft
|
||||||
|
.as_ref()
|
||||||
|
.map(|draft| draft.profile_id.clone())
|
||||||
|
.ok_or_else(|| {
|
||||||
|
square_hole_bad_request(
|
||||||
|
request_context,
|
||||||
|
SQUARE_HOLE_AGENT_PROVIDER,
|
||||||
|
"square hole 草稿尚未编译,不能生成图片资产",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let mut work = state
|
||||||
|
.spacetime_client()
|
||||||
|
.get_square_hole_work_detail(profile_id.clone(), owner_user_id.clone())
|
||||||
|
.await
|
||||||
|
.map_err(|error| {
|
||||||
|
square_hole_error_response(
|
||||||
|
request_context,
|
||||||
|
SQUARE_HOLE_AGENT_PROVIDER,
|
||||||
|
map_square_hole_client_error(error),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let cover_image_src = match work.cover_image_src.clone() {
|
||||||
|
Some(value) if !value.trim().is_empty() => Some(value),
|
||||||
|
_ => Some(
|
||||||
|
generate_square_hole_image_data_url(
|
||||||
|
state,
|
||||||
|
build_square_hole_cover_prompt(&work).as_str(),
|
||||||
|
"16:9",
|
||||||
|
"生成方洞挑战封面图失败",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|error| {
|
||||||
|
square_hole_error_response(request_context, SQUARE_HOLE_AGENT_PROVIDER, error)
|
||||||
|
})?,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
let background_image_src = match work.background_image_src.clone() {
|
||||||
|
Some(value) if !value.trim().is_empty() => Some(value),
|
||||||
|
_ => Some(
|
||||||
|
generate_square_hole_image_data_url(
|
||||||
|
state,
|
||||||
|
build_square_hole_background_prompt(&work).as_str(),
|
||||||
|
"16:9",
|
||||||
|
"生成方洞挑战背景图失败",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|error| {
|
||||||
|
square_hole_error_response(request_context, SQUARE_HOLE_AGENT_PROVIDER, error)
|
||||||
|
})?,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
let mut shape_options = work.shape_options.clone();
|
||||||
|
let prompt_work = work.clone();
|
||||||
|
for option in shape_options.iter_mut() {
|
||||||
|
if option
|
||||||
|
.image_src
|
||||||
|
.as_deref()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
option.image_src = Some(
|
||||||
|
generate_square_hole_image_data_url(
|
||||||
|
state,
|
||||||
|
build_square_hole_shape_prompt(&prompt_work, option).as_str(),
|
||||||
|
"1:1",
|
||||||
|
"生成方洞挑战形状贴图失败",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|error| {
|
||||||
|
square_hole_error_response(request_context, SQUARE_HOLE_AGENT_PROVIDER, error)
|
||||||
|
})?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
work = state
|
||||||
|
.spacetime_client()
|
||||||
|
.update_square_hole_work(SquareHoleWorkUpdateRecordInput {
|
||||||
|
profile_id,
|
||||||
|
owner_user_id: owner_user_id.clone(),
|
||||||
|
game_name: work.game_name.clone(),
|
||||||
|
theme_text: work.theme_text.clone(),
|
||||||
|
twist_rule: work.twist_rule.clone(),
|
||||||
|
summary_text: work.summary.clone(),
|
||||||
|
tags_json: serde_json::to_string(&normalize_tags(work.tags.clone()))
|
||||||
|
.unwrap_or_default(),
|
||||||
|
cover_image_src: cover_image_src.clone().unwrap_or_default(),
|
||||||
|
background_prompt: work.background_prompt.clone(),
|
||||||
|
background_image_src: background_image_src.clone().unwrap_or_default(),
|
||||||
|
shape_options_json: serialize_square_hole_shape_option_records(&shape_options),
|
||||||
|
hole_options_json: serialize_square_hole_hole_option_records(&work.hole_options),
|
||||||
|
shape_count: work.shape_count,
|
||||||
|
difficulty: work.difficulty,
|
||||||
|
updated_at_micros: current_utc_micros(),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|error| {
|
||||||
|
square_hole_error_response(
|
||||||
|
request_context,
|
||||||
|
SQUARE_HOLE_AGENT_PROVIDER,
|
||||||
|
map_square_hole_client_error(error),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut next_session = state
|
||||||
|
.spacetime_client()
|
||||||
|
.get_square_hole_agent_session(session_id, owner_user_id)
|
||||||
|
.await
|
||||||
|
.map_err(|error| {
|
||||||
|
square_hole_error_response(
|
||||||
|
request_context,
|
||||||
|
SQUARE_HOLE_AGENT_PROVIDER,
|
||||||
|
map_square_hole_client_error(error),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if let Some(draft) = next_session.draft.as_mut() {
|
||||||
|
draft.cover_image_src = work.cover_image_src.clone();
|
||||||
|
draft.background_image_src = work.background_image_src.clone();
|
||||||
|
draft.background_prompt = work.background_prompt.clone();
|
||||||
|
draft.shape_options = work.shape_options.clone();
|
||||||
|
draft.hole_options = work.hole_options.clone();
|
||||||
|
}
|
||||||
|
Ok(next_session)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn generate_square_hole_image_data_url(
|
||||||
|
state: &AppState,
|
||||||
|
prompt: &str,
|
||||||
|
size: &str,
|
||||||
|
failure_context: &str,
|
||||||
|
) -> Result<String, AppError> {
|
||||||
|
let settings = require_openai_image_settings(state)?;
|
||||||
|
let http_client = build_openai_image_http_client(&settings)?;
|
||||||
|
let generated = create_openai_image_generation(
|
||||||
|
&http_client,
|
||||||
|
&settings,
|
||||||
|
prompt,
|
||||||
|
Some(build_square_hole_negative_prompt().as_str()),
|
||||||
|
size,
|
||||||
|
1,
|
||||||
|
&[],
|
||||||
|
failure_context,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let image = generated.images.into_iter().next().ok_or_else(|| {
|
||||||
|
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||||
|
"provider": "apimart",
|
||||||
|
"message": format!("{failure_context}:上游未返回图片"),
|
||||||
|
}))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(format!(
|
||||||
|
"data:{};base64,{}",
|
||||||
|
image.mime_type,
|
||||||
|
BASE64_STANDARD.encode(image.bytes)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_square_hole_cover_prompt(work: &SquareHoleWorkProfileRecord) -> String {
|
||||||
|
format!(
|
||||||
|
"移动端休闲游戏封面图。主题:{}。玩法反差:{}。画面主体是贴着主题图案的几何形状正在靠近不同洞口,视觉清晰、色彩明快、偏游戏资产质感。不要文字、不要 UI、不要水印。",
|
||||||
|
clean_prompt_text(&work.theme_text, "奇怪形状"),
|
||||||
|
clean_prompt_text(&work.twist_rule, "反直觉分拣")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_square_hole_background_prompt(work: &SquareHoleWorkProfileRecord) -> String {
|
||||||
|
let custom_prompt = work.background_prompt.trim();
|
||||||
|
if !custom_prompt.is_empty() {
|
||||||
|
return format!(
|
||||||
|
"移动端休闲游戏运行背景。{}。画面中央预留清晰操作空间,边缘可有主题装饰,低噪声,不要文字、不要 UI、不要水印。",
|
||||||
|
custom_prompt
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"移动端休闲游戏运行背景。主题:{}。柔和纵深、玩具盒或舞台感,中央预留清晰操作空间,不要文字、不要 UI、不要水印。",
|
||||||
|
clean_prompt_text(&work.theme_text, "奇怪形状")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_square_hole_shape_prompt(
|
||||||
|
work: &SquareHoleWorkProfileRecord,
|
||||||
|
option: &SquareHoleShapeOptionRecord,
|
||||||
|
) -> String {
|
||||||
|
let image_prompt = option.image_prompt.trim();
|
||||||
|
let option_prompt = if image_prompt.is_empty() {
|
||||||
|
format!("{} 主题的 {}", work.theme_text, option.label)
|
||||||
|
} else {
|
||||||
|
image_prompt.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"单个游戏道具贴图,透明或干净浅色背景。几何形状:{}。主题贴图:{}。要求主体居中、边缘清晰、适合贴在可拖拽形状上,不要文字、不要 UI、不要水印。",
|
||||||
|
clean_prompt_text(&option.label, "形状"),
|
||||||
|
clean_prompt_text(&option_prompt, "主题图案")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_square_hole_negative_prompt() -> String {
|
||||||
|
"文字、水印、复杂 UI、真实人物、恐怖血腥、低清晰度、过度模糊、主体被裁切、多个主体".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
fn map_square_hole_agent_session_response(
|
fn map_square_hole_agent_session_response(
|
||||||
session: SquareHoleAgentSessionRecord,
|
session: SquareHoleAgentSessionRecord,
|
||||||
) -> SquareHoleSessionSnapshotResponse {
|
) -> SquareHoleSessionSnapshotResponse {
|
||||||
@@ -1084,6 +1382,19 @@ fn map_square_hole_config_response(
|
|||||||
twist_rule: config.twist_rule,
|
twist_rule: config.twist_rule,
|
||||||
shape_count: config.shape_count,
|
shape_count: config.shape_count,
|
||||||
difficulty: config.difficulty,
|
difficulty: config.difficulty,
|
||||||
|
shape_options: config
|
||||||
|
.shape_options
|
||||||
|
.into_iter()
|
||||||
|
.map(map_square_hole_shape_option_response)
|
||||||
|
.collect(),
|
||||||
|
hole_options: config
|
||||||
|
.hole_options
|
||||||
|
.into_iter()
|
||||||
|
.map(map_square_hole_hole_option_response)
|
||||||
|
.collect(),
|
||||||
|
background_prompt: config.background_prompt,
|
||||||
|
cover_image_src: config.cover_image_src,
|
||||||
|
background_image_src: config.background_image_src,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1097,6 +1408,19 @@ fn map_square_hole_draft_response(
|
|||||||
twist_rule: draft.twist_rule,
|
twist_rule: draft.twist_rule,
|
||||||
summary: draft.summary,
|
summary: draft.summary,
|
||||||
tags: draft.tags,
|
tags: draft.tags,
|
||||||
|
cover_image_src: draft.cover_image_src,
|
||||||
|
background_prompt: draft.background_prompt,
|
||||||
|
background_image_src: draft.background_image_src,
|
||||||
|
shape_options: draft
|
||||||
|
.shape_options
|
||||||
|
.into_iter()
|
||||||
|
.map(map_square_hole_shape_option_response)
|
||||||
|
.collect(),
|
||||||
|
hole_options: draft
|
||||||
|
.hole_options
|
||||||
|
.into_iter()
|
||||||
|
.map(map_square_hole_hole_option_response)
|
||||||
|
.collect(),
|
||||||
shape_count: draft.shape_count,
|
shape_count: draft.shape_count,
|
||||||
difficulty: draft.difficulty,
|
difficulty: draft.difficulty,
|
||||||
publish_ready: draft.publish_ready,
|
publish_ready: draft.publish_ready,
|
||||||
@@ -1130,6 +1454,18 @@ fn map_square_hole_work_summary_response(
|
|||||||
summary: item.summary,
|
summary: item.summary,
|
||||||
tags: item.tags,
|
tags: item.tags,
|
||||||
cover_image_src: item.cover_image_src,
|
cover_image_src: item.cover_image_src,
|
||||||
|
background_prompt: item.background_prompt,
|
||||||
|
background_image_src: item.background_image_src,
|
||||||
|
shape_options: item
|
||||||
|
.shape_options
|
||||||
|
.into_iter()
|
||||||
|
.map(map_square_hole_work_shape_option_response)
|
||||||
|
.collect(),
|
||||||
|
hole_options: item
|
||||||
|
.hole_options
|
||||||
|
.into_iter()
|
||||||
|
.map(map_square_hole_work_hole_option_response)
|
||||||
|
.collect(),
|
||||||
shape_count: item.shape_count,
|
shape_count: item.shape_count,
|
||||||
difficulty: item.difficulty,
|
difficulty: item.difficulty,
|
||||||
publication_status: item.publication_status,
|
publication_status: item.publication_status,
|
||||||
@@ -1164,6 +1500,7 @@ fn map_square_hole_run_response(run: SquareHoleRunRecord) -> SquareHoleRunSnapsh
|
|||||||
best_combo: run.best_combo,
|
best_combo: run.best_combo,
|
||||||
score: run.score,
|
score: run.score,
|
||||||
rule_label: run.rule_label,
|
rule_label: run.rule_label,
|
||||||
|
background_image_src: run.background_image_src,
|
||||||
current_shape: run.current_shape.map(map_square_hole_shape_response),
|
current_shape: run.current_shape.map(map_square_hole_shape_response),
|
||||||
holes: run
|
holes: run
|
||||||
.holes
|
.holes
|
||||||
@@ -1182,6 +1519,7 @@ fn map_square_hole_shape_response(
|
|||||||
shape_kind: item.shape_kind,
|
shape_kind: item.shape_kind,
|
||||||
label: item.label,
|
label: item.label,
|
||||||
color: item.color,
|
color: item.color,
|
||||||
|
image_src: item.image_src,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1194,6 +1532,53 @@ fn map_square_hole_hole_response(
|
|||||||
label: slot.label,
|
label: slot.label,
|
||||||
x: slot.x,
|
x: slot.x,
|
||||||
y: slot.y,
|
y: slot.y,
|
||||||
|
bonus: slot.bonus,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_square_hole_shape_option_response(
|
||||||
|
item: SquareHoleShapeOptionRecord,
|
||||||
|
) -> SquareHoleShapeOptionResponse {
|
||||||
|
SquareHoleShapeOptionResponse {
|
||||||
|
option_id: item.option_id,
|
||||||
|
shape_kind: item.shape_kind,
|
||||||
|
label: item.label,
|
||||||
|
image_prompt: item.image_prompt,
|
||||||
|
image_src: item.image_src,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_square_hole_hole_option_response(
|
||||||
|
item: SquareHoleHoleOptionRecord,
|
||||||
|
) -> SquareHoleHoleOptionResponse {
|
||||||
|
SquareHoleHoleOptionResponse {
|
||||||
|
hole_id: item.hole_id,
|
||||||
|
hole_kind: item.hole_kind,
|
||||||
|
label: item.label,
|
||||||
|
bonus: item.bonus,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_square_hole_work_shape_option_response(
|
||||||
|
item: SquareHoleShapeOptionRecord,
|
||||||
|
) -> SquareHoleWorkShapeOptionResponse {
|
||||||
|
SquareHoleWorkShapeOptionResponse {
|
||||||
|
option_id: item.option_id,
|
||||||
|
shape_kind: item.shape_kind,
|
||||||
|
label: item.label,
|
||||||
|
image_prompt: item.image_prompt,
|
||||||
|
image_src: item.image_src,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_square_hole_work_hole_option_response(
|
||||||
|
item: SquareHoleHoleOptionRecord,
|
||||||
|
) -> SquareHoleWorkHoleOptionResponse {
|
||||||
|
SquareHoleWorkHoleOptionResponse {
|
||||||
|
hole_id: item.hole_id,
|
||||||
|
hole_kind: item.hole_kind,
|
||||||
|
label: item.label,
|
||||||
|
bonus: item.bonus,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1234,6 +1619,24 @@ fn build_config_from_create_request(
|
|||||||
.difficulty
|
.difficulty
|
||||||
.unwrap_or(SQUARE_HOLE_DEFAULT_DIFFICULTY)
|
.unwrap_or(SQUARE_HOLE_DEFAULT_DIFFICULTY)
|
||||||
.clamp(1, 10),
|
.clamp(1, 10),
|
||||||
|
shape_options: square_hole_shape_records_to_config_json(normalize_shape_options(
|
||||||
|
Vec::new(),
|
||||||
|
payload
|
||||||
|
.theme_text
|
||||||
|
.as_deref()
|
||||||
|
.or(payload.seed_text.as_deref())
|
||||||
|
.unwrap_or(SQUARE_HOLE_DEFAULT_THEME),
|
||||||
|
)),
|
||||||
|
hole_options: square_hole_hole_records_to_config_json(normalize_hole_options(Vec::new())),
|
||||||
|
background_prompt: default_background_prompt(
|
||||||
|
payload
|
||||||
|
.theme_text
|
||||||
|
.as_deref()
|
||||||
|
.or(payload.seed_text.as_deref())
|
||||||
|
.unwrap_or(SQUARE_HOLE_DEFAULT_THEME),
|
||||||
|
),
|
||||||
|
cover_image_src: String::new(),
|
||||||
|
background_image_src: String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1246,12 +1649,27 @@ fn resolve_config_or_default(
|
|||||||
twist_rule: config.twist_rule.clone(),
|
twist_rule: config.twist_rule.clone(),
|
||||||
shape_count: config.shape_count.max(1),
|
shape_count: config.shape_count.max(1),
|
||||||
difficulty: config.difficulty.clamp(1, 10),
|
difficulty: config.difficulty.clamp(1, 10),
|
||||||
|
shape_options: square_hole_shape_records_to_config_json(config.shape_options.clone()),
|
||||||
|
hole_options: square_hole_hole_records_to_config_json(config.hole_options.clone()),
|
||||||
|
background_prompt: config.background_prompt.clone(),
|
||||||
|
cover_image_src: config.cover_image_src.clone().unwrap_or_default(),
|
||||||
|
background_image_src: config.background_image_src.clone().unwrap_or_default(),
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| SquareHoleConfigJson {
|
.unwrap_or_else(|| SquareHoleConfigJson {
|
||||||
theme_text: SQUARE_HOLE_DEFAULT_THEME.to_string(),
|
theme_text: SQUARE_HOLE_DEFAULT_THEME.to_string(),
|
||||||
twist_rule: SQUARE_HOLE_DEFAULT_TWIST_RULE.to_string(),
|
twist_rule: SQUARE_HOLE_DEFAULT_TWIST_RULE.to_string(),
|
||||||
shape_count: SQUARE_HOLE_DEFAULT_SHAPE_COUNT,
|
shape_count: SQUARE_HOLE_DEFAULT_SHAPE_COUNT,
|
||||||
difficulty: SQUARE_HOLE_DEFAULT_DIFFICULTY,
|
difficulty: SQUARE_HOLE_DEFAULT_DIFFICULTY,
|
||||||
|
shape_options: square_hole_shape_records_to_config_json(normalize_shape_options(
|
||||||
|
Vec::new(),
|
||||||
|
SQUARE_HOLE_DEFAULT_THEME,
|
||||||
|
)),
|
||||||
|
hole_options: square_hole_hole_records_to_config_json(normalize_hole_options(
|
||||||
|
Vec::new(),
|
||||||
|
)),
|
||||||
|
background_prompt: default_background_prompt(SQUARE_HOLE_DEFAULT_THEME),
|
||||||
|
cover_image_src: String::new(),
|
||||||
|
background_image_src: String::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1259,6 +1677,13 @@ fn serialize_square_hole_config(config: &SquareHoleConfigJson) -> Option<String>
|
|||||||
serde_json::to_string(config).ok()
|
serde_json::to_string(config).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn deserialize_optional_string_as_default<'de, D>(deserializer: D) -> Result<String, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
Ok(Option::<String>::deserialize(deserializer)?.unwrap_or_default())
|
||||||
|
}
|
||||||
|
|
||||||
fn build_seed_text(
|
fn build_seed_text(
|
||||||
payload: &CreateSquareHoleSessionRequest,
|
payload: &CreateSquareHoleSessionRequest,
|
||||||
config: &SquareHoleConfigJson,
|
config: &SquareHoleConfigJson,
|
||||||
@@ -1291,6 +1716,118 @@ fn normalize_tags(tags: Vec<String>) -> Vec<String> {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn square_hole_shape_records_to_config_json(
|
||||||
|
options: Vec<impl Into<SquareHoleConfigShapeOptionJson>>,
|
||||||
|
) -> Vec<SquareHoleConfigShapeOptionJson> {
|
||||||
|
options.into_iter().map(Into::into).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn square_hole_hole_records_to_config_json(
|
||||||
|
options: Vec<impl Into<SquareHoleConfigHoleOptionJson>>,
|
||||||
|
) -> Vec<SquareHoleConfigHoleOptionJson> {
|
||||||
|
options.into_iter().map(Into::into).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn square_hole_work_shape_options_to_records(
|
||||||
|
options: Vec<SquareHoleWorkShapeOptionResponse>,
|
||||||
|
) -> Vec<SquareHoleShapeOptionRecord> {
|
||||||
|
options
|
||||||
|
.into_iter()
|
||||||
|
.map(|option| SquareHoleShapeOptionRecord {
|
||||||
|
option_id: option.option_id,
|
||||||
|
shape_kind: option.shape_kind,
|
||||||
|
label: option.label,
|
||||||
|
image_prompt: option.image_prompt,
|
||||||
|
image_src: option.image_src.filter(|value| !value.trim().is_empty()),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn square_hole_work_hole_options_to_records(
|
||||||
|
options: Vec<SquareHoleWorkHoleOptionResponse>,
|
||||||
|
) -> Vec<SquareHoleHoleOptionRecord> {
|
||||||
|
options
|
||||||
|
.into_iter()
|
||||||
|
.map(|option| SquareHoleHoleOptionRecord {
|
||||||
|
hole_id: option.hole_id,
|
||||||
|
hole_kind: option.hole_kind,
|
||||||
|
label: option.label,
|
||||||
|
bonus: option.bonus,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize_square_hole_shape_option_records(options: &[SquareHoleShapeOptionRecord]) -> String {
|
||||||
|
let json_options: Vec<SquareHoleConfigShapeOptionJson> =
|
||||||
|
options.iter().cloned().map(Into::into).collect();
|
||||||
|
serde_json::to_string(&json_options).unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize_square_hole_hole_option_records(options: &[SquareHoleHoleOptionRecord]) -> String {
|
||||||
|
let json_options: Vec<SquareHoleConfigHoleOptionJson> =
|
||||||
|
options.iter().cloned().map(Into::into).collect();
|
||||||
|
serde_json::to_string(&json_options).unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clean_prompt_text(value: &str, fallback: &str) -> String {
|
||||||
|
let cleaned = value
|
||||||
|
.trim()
|
||||||
|
.split_whitespace()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ");
|
||||||
|
if cleaned.is_empty() {
|
||||||
|
fallback.to_string()
|
||||||
|
} else {
|
||||||
|
cleaned.chars().take(180).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<module_square_hole::SquareHoleShapeOption> for SquareHoleConfigShapeOptionJson {
|
||||||
|
fn from(option: module_square_hole::SquareHoleShapeOption) -> Self {
|
||||||
|
Self {
|
||||||
|
option_id: option.option_id,
|
||||||
|
shape_kind: option.shape_kind,
|
||||||
|
label: option.label,
|
||||||
|
image_prompt: option.image_prompt,
|
||||||
|
image_src: option.image_src.unwrap_or_default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SquareHoleShapeOptionRecord> for SquareHoleConfigShapeOptionJson {
|
||||||
|
fn from(option: SquareHoleShapeOptionRecord) -> Self {
|
||||||
|
Self {
|
||||||
|
option_id: option.option_id,
|
||||||
|
shape_kind: option.shape_kind,
|
||||||
|
label: option.label,
|
||||||
|
image_prompt: option.image_prompt,
|
||||||
|
image_src: option.image_src.unwrap_or_default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<module_square_hole::SquareHoleHoleOption> for SquareHoleConfigHoleOptionJson {
|
||||||
|
fn from(option: module_square_hole::SquareHoleHoleOption) -> Self {
|
||||||
|
Self {
|
||||||
|
hole_id: option.hole_id,
|
||||||
|
hole_kind: option.hole_kind,
|
||||||
|
label: option.label,
|
||||||
|
bonus: option.bonus,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SquareHoleHoleOptionRecord> for SquareHoleConfigHoleOptionJson {
|
||||||
|
fn from(option: SquareHoleHoleOptionRecord) -> Self {
|
||||||
|
Self {
|
||||||
|
hole_id: option.hole_id,
|
||||||
|
hole_kind: option.hole_kind,
|
||||||
|
label: option.label,
|
||||||
|
bonus: option.bonus,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn resolve_author_display_name(
|
fn resolve_author_display_name(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
authenticated: &AuthenticatedAccessToken,
|
authenticated: &AuthenticatedAccessToken,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use module_square_hole::{
|
use module_square_hole::{
|
||||||
SQUARE_HOLE_MAX_DIFFICULTY, SQUARE_HOLE_MAX_SHAPE_COUNT, SQUARE_HOLE_MIN_DIFFICULTY,
|
SQUARE_HOLE_MAX_DIFFICULTY, SQUARE_HOLE_MAX_SHAPE_COUNT, SQUARE_HOLE_MIN_DIFFICULTY,
|
||||||
SQUARE_HOLE_MIN_SHAPE_COUNT,
|
SQUARE_HOLE_MIN_SHAPE_COUNT, SquareHoleHoleOption, SquareHoleShapeOption,
|
||||||
|
default_background_prompt, normalize_hole_options, normalize_shape_options,
|
||||||
};
|
};
|
||||||
use platform_llm::LlmClient;
|
use platform_llm::LlmClient;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -68,6 +69,34 @@ struct SquareHoleAgentConfigOutput {
|
|||||||
twist_rule: String,
|
twist_rule: String,
|
||||||
shape_count: u32,
|
shape_count: u32,
|
||||||
difficulty: u32,
|
difficulty: u32,
|
||||||
|
shape_options: Vec<SquareHoleAgentShapeOptionOutput>,
|
||||||
|
hole_options: Vec<SquareHoleAgentHoleOptionOutput>,
|
||||||
|
background_prompt: String,
|
||||||
|
#[serde(default)]
|
||||||
|
cover_image_src: String,
|
||||||
|
#[serde(default)]
|
||||||
|
background_image_src: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct SquareHoleAgentShapeOptionOutput {
|
||||||
|
option_id: String,
|
||||||
|
shape_kind: String,
|
||||||
|
label: String,
|
||||||
|
image_prompt: String,
|
||||||
|
#[serde(default)]
|
||||||
|
image_src: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct SquareHoleAgentHoleOptionOutput {
|
||||||
|
hole_id: String,
|
||||||
|
hole_kind: String,
|
||||||
|
label: String,
|
||||||
|
#[serde(default)]
|
||||||
|
bonus: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn run_square_hole_agent_turn<F>(
|
pub(crate) async fn run_square_hole_agent_turn<F>(
|
||||||
@@ -166,20 +195,136 @@ fn parse_model_config(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let theme_text = read_text_field(value, "themeText")
|
||||||
|
.unwrap_or_else(|| session.config.theme_text.clone());
|
||||||
|
let twist_rule = read_text_field(value, "twistRule")
|
||||||
|
.unwrap_or_else(|| session.config.twist_rule.clone());
|
||||||
|
let shape_options = parse_shape_options(value, session, &theme_text);
|
||||||
|
let hole_options = parse_hole_options(value, session);
|
||||||
|
let background_prompt = read_text_field(value, "backgroundPrompt")
|
||||||
|
.or_else(|| {
|
||||||
|
session
|
||||||
|
.config
|
||||||
|
.background_prompt
|
||||||
|
.trim()
|
||||||
|
.is_empty()
|
||||||
|
.then(|| default_background_prompt(&theme_text))
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| session.config.background_prompt.clone());
|
||||||
|
|
||||||
Ok(SquareHoleAgentConfigOutput {
|
Ok(SquareHoleAgentConfigOutput {
|
||||||
theme_text: read_text_field(value, "themeText")
|
theme_text,
|
||||||
.unwrap_or_else(|| session.config.theme_text.clone()),
|
twist_rule,
|
||||||
twist_rule: read_text_field(value, "twistRule")
|
|
||||||
.unwrap_or_else(|| session.config.twist_rule.clone()),
|
|
||||||
shape_count: read_u32_field(value, "shapeCount")
|
shape_count: read_u32_field(value, "shapeCount")
|
||||||
.unwrap_or(session.config.shape_count)
|
.unwrap_or(session.config.shape_count)
|
||||||
.clamp(SQUARE_HOLE_MIN_SHAPE_COUNT, SQUARE_HOLE_MAX_SHAPE_COUNT),
|
.clamp(SQUARE_HOLE_MIN_SHAPE_COUNT, SQUARE_HOLE_MAX_SHAPE_COUNT),
|
||||||
difficulty: read_u32_field(value, "difficulty")
|
difficulty: read_u32_field(value, "difficulty")
|
||||||
.unwrap_or(session.config.difficulty)
|
.unwrap_or(session.config.difficulty)
|
||||||
.clamp(SQUARE_HOLE_MIN_DIFFICULTY, SQUARE_HOLE_MAX_DIFFICULTY),
|
.clamp(SQUARE_HOLE_MIN_DIFFICULTY, SQUARE_HOLE_MAX_DIFFICULTY),
|
||||||
|
shape_options: shape_options
|
||||||
|
.into_iter()
|
||||||
|
.map(SquareHoleAgentShapeOptionOutput::from)
|
||||||
|
.collect(),
|
||||||
|
hole_options: hole_options
|
||||||
|
.into_iter()
|
||||||
|
.map(SquareHoleAgentHoleOptionOutput::from)
|
||||||
|
.collect(),
|
||||||
|
background_prompt,
|
||||||
|
cover_image_src: session.config.cover_image_src.clone().unwrap_or_default(),
|
||||||
|
background_image_src: session
|
||||||
|
.config
|
||||||
|
.background_image_src
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_shape_options(
|
||||||
|
value: &JsonValue,
|
||||||
|
session: &SquareHoleAgentSessionRecord,
|
||||||
|
theme_text: &str,
|
||||||
|
) -> Vec<SquareHoleShapeOption> {
|
||||||
|
let parsed = value
|
||||||
|
.get("shapeOptions")
|
||||||
|
.and_then(JsonValue::as_array)
|
||||||
|
.map(|items| {
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, item)| SquareHoleShapeOption {
|
||||||
|
option_id: read_text_field(item, "optionId")
|
||||||
|
.unwrap_or_else(|| format!("shape-option-{index}")),
|
||||||
|
shape_kind: read_text_field(item, "shapeKind")
|
||||||
|
.unwrap_or_else(|| fallback_shape_kind(index).to_string()),
|
||||||
|
label: read_text_field(item, "label")
|
||||||
|
.unwrap_or_else(|| fallback_shape_label(index).to_string()),
|
||||||
|
image_prompt: read_text_field(item, "imagePrompt").unwrap_or_else(|| {
|
||||||
|
format!("{theme_text}主题的{}贴纸图,透明背景,明亮游戏资产", fallback_shape_label(index))
|
||||||
|
}),
|
||||||
|
image_src: read_text_field(item, "imageSrc"),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
session
|
||||||
|
.config
|
||||||
|
.shape_options
|
||||||
|
.iter()
|
||||||
|
.map(|option| SquareHoleShapeOption {
|
||||||
|
option_id: option.option_id.clone(),
|
||||||
|
shape_kind: option.shape_kind.clone(),
|
||||||
|
label: option.label.clone(),
|
||||||
|
image_prompt: option.image_prompt.clone(),
|
||||||
|
image_src: option.image_src.clone(),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
});
|
||||||
|
|
||||||
|
normalize_shape_options(parsed, theme_text)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_hole_options(
|
||||||
|
value: &JsonValue,
|
||||||
|
session: &SquareHoleAgentSessionRecord,
|
||||||
|
) -> Vec<SquareHoleHoleOption> {
|
||||||
|
let parsed = value
|
||||||
|
.get("holeOptions")
|
||||||
|
.and_then(JsonValue::as_array)
|
||||||
|
.map(|items| {
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, item)| SquareHoleHoleOption {
|
||||||
|
hole_id: read_text_field(item, "holeId")
|
||||||
|
.unwrap_or_else(|| format!("hole-option-{index}")),
|
||||||
|
hole_kind: read_text_field(item, "holeKind")
|
||||||
|
.unwrap_or_else(|| fallback_shape_kind(index).to_string()),
|
||||||
|
label: read_text_field(item, "label")
|
||||||
|
.unwrap_or_else(|| fallback_hole_label(index).to_string()),
|
||||||
|
bonus: item
|
||||||
|
.get("bonus")
|
||||||
|
.and_then(JsonValue::as_bool)
|
||||||
|
.unwrap_or(index == 0),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
session
|
||||||
|
.config
|
||||||
|
.hole_options
|
||||||
|
.iter()
|
||||||
|
.map(|option| SquareHoleHoleOption {
|
||||||
|
hole_id: option.hole_id.clone(),
|
||||||
|
hole_kind: option.hole_kind.clone(),
|
||||||
|
label: option.label.clone(),
|
||||||
|
bonus: option.bonus,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
});
|
||||||
|
|
||||||
|
normalize_hole_options(parsed)
|
||||||
|
}
|
||||||
|
|
||||||
fn read_text_field(value: &JsonValue, field_name: &str) -> Option<String> {
|
fn read_text_field(value: &JsonValue, field_name: &str) -> Option<String> {
|
||||||
value
|
value
|
||||||
.get(field_name)
|
.get(field_name)
|
||||||
@@ -196,6 +341,62 @@ fn read_u32_field(value: &JsonValue, field_name: &str) -> Option<u32> {
|
|||||||
.and_then(|number| u32::try_from(number).ok())
|
.and_then(|number| u32::try_from(number).ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fallback_shape_kind(index: usize) -> &'static str {
|
||||||
|
match index % 6 {
|
||||||
|
0 => "square",
|
||||||
|
1 => "circle",
|
||||||
|
2 => "triangle",
|
||||||
|
3 => "diamond",
|
||||||
|
4 => "star",
|
||||||
|
_ => "arch",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fallback_shape_label(index: usize) -> &'static str {
|
||||||
|
match fallback_shape_kind(index) {
|
||||||
|
"square" => "方块",
|
||||||
|
"circle" => "圆块",
|
||||||
|
"triangle" => "三角块",
|
||||||
|
"diamond" => "菱形块",
|
||||||
|
"star" => "星形块",
|
||||||
|
_ => "拱形块",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fallback_hole_label(index: usize) -> &'static str {
|
||||||
|
match fallback_shape_kind(index) {
|
||||||
|
"square" => "方洞",
|
||||||
|
"circle" => "圆洞",
|
||||||
|
"triangle" => "三角洞",
|
||||||
|
"diamond" => "菱形洞",
|
||||||
|
"star" => "星形洞",
|
||||||
|
_ => "拱形洞",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SquareHoleShapeOption> for SquareHoleAgentShapeOptionOutput {
|
||||||
|
fn from(option: SquareHoleShapeOption) -> Self {
|
||||||
|
Self {
|
||||||
|
option_id: option.option_id,
|
||||||
|
shape_kind: option.shape_kind,
|
||||||
|
label: option.label,
|
||||||
|
image_prompt: option.image_prompt,
|
||||||
|
image_src: option.image_src.unwrap_or_default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SquareHoleHoleOption> for SquareHoleAgentHoleOptionOutput {
|
||||||
|
fn from(option: SquareHoleHoleOption) -> Self {
|
||||||
|
Self {
|
||||||
|
hole_id: option.hole_id,
|
||||||
|
hole_kind: option.hole_kind,
|
||||||
|
label: option.label,
|
||||||
|
bonus: option.bonus,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn resolve_stage(progress_percent: u32) -> String {
|
fn resolve_stage(progress_percent: u32) -> String {
|
||||||
if progress_percent >= 100 {
|
if progress_percent >= 100 {
|
||||||
"ReadyToCompile"
|
"ReadyToCompile"
|
||||||
@@ -228,6 +429,11 @@ mod tests {
|
|||||||
twist_rule: "方洞万能".to_string(),
|
twist_rule: "方洞万能".to_string(),
|
||||||
shape_count: 12,
|
shape_count: 12,
|
||||||
difficulty: 4,
|
difficulty: 4,
|
||||||
|
shape_options: Vec::new(),
|
||||||
|
hole_options: Vec::new(),
|
||||||
|
background_prompt: "纸箱玩具桌面背景".to_string(),
|
||||||
|
cover_image_src: None,
|
||||||
|
background_image_src: None,
|
||||||
},
|
},
|
||||||
draft: None,
|
draft: None,
|
||||||
messages: Vec::new(),
|
messages: Vec::new(),
|
||||||
@@ -260,7 +466,24 @@ mod tests {
|
|||||||
"themeText": "办公室文具",
|
"themeText": "办公室文具",
|
||||||
"twistRule": "所有文具最终都优先进入方洞",
|
"twistRule": "所有文具最终都优先进入方洞",
|
||||||
"shapeCount": 14,
|
"shapeCount": 14,
|
||||||
"difficulty": 6
|
"difficulty": 6,
|
||||||
|
"shapeOptions": [
|
||||||
|
{
|
||||||
|
"optionId": "stamp",
|
||||||
|
"shapeKind": "circle",
|
||||||
|
"label": "圆形印章",
|
||||||
|
"imagePrompt": "办公室圆形印章贴纸"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"holeOptions": [
|
||||||
|
{
|
||||||
|
"holeId": "folder",
|
||||||
|
"holeKind": "square",
|
||||||
|
"label": "档案盒方洞",
|
||||||
|
"bonus": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"backgroundPrompt": "办公室桌面纸箱玩具背景"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -276,6 +499,14 @@ mod tests {
|
|||||||
assert_eq!(output.next_config.twist_rule, "所有文具最终都优先进入方洞");
|
assert_eq!(output.next_config.twist_rule, "所有文具最终都优先进入方洞");
|
||||||
assert_eq!(output.next_config.shape_count, 14);
|
assert_eq!(output.next_config.shape_count, 14);
|
||||||
assert_eq!(output.next_config.difficulty, 6);
|
assert_eq!(output.next_config.difficulty, 6);
|
||||||
|
assert!(output.next_config.shape_options.len() >= 6);
|
||||||
|
assert_eq!(output.next_config.shape_options[0].label, "圆形印章");
|
||||||
|
assert_eq!(output.next_config.hole_options[0].label, "档案盒方洞");
|
||||||
|
assert!(output.next_config.hole_options[0].bonus);
|
||||||
|
assert_eq!(
|
||||||
|
output.next_config.background_prompt,
|
||||||
|
"办公室桌面纸箱玩具背景"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -287,7 +518,10 @@ mod tests {
|
|||||||
"themeText": "霓虹积木",
|
"themeText": "霓虹积木",
|
||||||
"twistRule": "方洞优先",
|
"twistRule": "方洞优先",
|
||||||
"shapeCount": 99,
|
"shapeCount": 99,
|
||||||
"difficulty": 0
|
"difficulty": 0,
|
||||||
|
"shapeOptions": [],
|
||||||
|
"holeOptions": [],
|
||||||
|
"backgroundPrompt": ""
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ use shared_kernel::{normalize_optional_string, normalize_required_string, normal
|
|||||||
|
|
||||||
use crate::commands::{default_tags_for_theme, validate_publish_requirements};
|
use crate::commands::{default_tags_for_theme, validate_publish_requirements};
|
||||||
use crate::{
|
use crate::{
|
||||||
SQUARE_HOLE_DEFAULT_DURATION_LIMIT_MS, SQUARE_HOLE_MAX_DIFFICULTY, SQUARE_HOLE_MIN_DIFFICULTY,
|
SQUARE_HOLE_DEFAULT_DURATION_LIMIT_MS, SQUARE_HOLE_MAX_DIFFICULTY,
|
||||||
SquareHoleCreatorConfig, SquareHoleDropConfirmation, SquareHoleDropFeedback,
|
SQUARE_HOLE_MAX_HOLE_OPTION_COUNT, SQUARE_HOLE_MIN_DIFFICULTY,
|
||||||
SquareHoleDropInput, SquareHoleDropRejectReason, SquareHoleError, SquareHoleHoleSnapshot,
|
SQUARE_HOLE_MIN_HOLE_OPTION_COUNT, SQUARE_HOLE_MIN_SHAPE_OPTION_COUNT, SquareHoleCreatorConfig,
|
||||||
|
SquareHoleDropConfirmation, SquareHoleDropFeedback, SquareHoleDropInput,
|
||||||
|
SquareHoleDropRejectReason, SquareHoleError, SquareHoleHoleOption, SquareHoleHoleSnapshot,
|
||||||
SquareHolePublicationStatus, SquareHoleResultDraft, SquareHoleRunSnapshot, SquareHoleRunStatus,
|
SquareHolePublicationStatus, SquareHoleResultDraft, SquareHoleRunSnapshot, SquareHoleRunStatus,
|
||||||
SquareHoleShapeSnapshot, SquareHoleWorkProfile,
|
SquareHoleShapeOption, SquareHoleShapeSnapshot, SquareHoleWorkProfile,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn compile_result_draft(
|
pub fn compile_result_draft(
|
||||||
@@ -14,6 +16,10 @@ pub fn compile_result_draft(
|
|||||||
config: &SquareHoleCreatorConfig,
|
config: &SquareHoleCreatorConfig,
|
||||||
) -> SquareHoleResultDraft {
|
) -> SquareHoleResultDraft {
|
||||||
let game_name = format!("{}方洞挑战", config.theme_text);
|
let game_name = format!("{}方洞挑战", config.theme_text);
|
||||||
|
let shape_options = normalize_shape_options(config.shape_options.clone(), &config.theme_text);
|
||||||
|
let hole_options = normalize_hole_options(config.hole_options.clone());
|
||||||
|
let background_prompt = normalize_required_string(&config.background_prompt)
|
||||||
|
.unwrap_or_else(|| default_background_prompt(&config.theme_text));
|
||||||
let summary = format!(
|
let summary = format!(
|
||||||
"{}主题,{} 个形状,难度 {},真实规则:{}。",
|
"{}主题,{} 个形状,难度 {},真实规则:{}。",
|
||||||
config.theme_text, config.shape_count, config.difficulty, config.twist_rule
|
config.theme_text, config.shape_count, config.difficulty, config.twist_rule
|
||||||
@@ -25,6 +31,11 @@ pub fn compile_result_draft(
|
|||||||
twist_rule: config.twist_rule.clone(),
|
twist_rule: config.twist_rule.clone(),
|
||||||
summary,
|
summary,
|
||||||
tags: default_tags_for_theme(&config.theme_text),
|
tags: default_tags_for_theme(&config.theme_text),
|
||||||
|
cover_image_src: config.cover_image_src.clone(),
|
||||||
|
background_prompt,
|
||||||
|
background_image_src: config.background_image_src.clone(),
|
||||||
|
shape_options,
|
||||||
|
hole_options,
|
||||||
shape_count: config.shape_count,
|
shape_count: config.shape_count,
|
||||||
difficulty: config.difficulty,
|
difficulty: config.difficulty,
|
||||||
publish_ready: false,
|
publish_ready: false,
|
||||||
@@ -59,7 +70,11 @@ pub fn create_work_profile(
|
|||||||
twist_rule: draft.twist_rule.clone(),
|
twist_rule: draft.twist_rule.clone(),
|
||||||
summary: draft.summary.clone(),
|
summary: draft.summary.clone(),
|
||||||
tags: normalize_string_list(draft.tags.clone()),
|
tags: normalize_string_list(draft.tags.clone()),
|
||||||
cover_image_src: None,
|
cover_image_src: draft.cover_image_src.clone(),
|
||||||
|
background_prompt: draft.background_prompt.clone(),
|
||||||
|
background_image_src: draft.background_image_src.clone(),
|
||||||
|
shape_options: normalize_shape_options(draft.shape_options.clone(), &draft.theme_text),
|
||||||
|
hole_options: normalize_hole_options(draft.hole_options.clone()),
|
||||||
shape_count: draft.shape_count,
|
shape_count: draft.shape_count,
|
||||||
difficulty: draft.difficulty,
|
difficulty: draft.difficulty,
|
||||||
publication_status: SquareHolePublicationStatus::Draft,
|
publication_status: SquareHolePublicationStatus::Draft,
|
||||||
@@ -99,6 +114,7 @@ pub fn start_run_at(
|
|||||||
normalize_required_string(owner_user_id).ok_or(SquareHoleError::MissingOwnerUserId)?;
|
normalize_required_string(owner_user_id).ok_or(SquareHoleError::MissingOwnerUserId)?;
|
||||||
let profile_id =
|
let profile_id =
|
||||||
normalize_required_string(profile_id).ok_or(SquareHoleError::MissingProfileId)?;
|
normalize_required_string(profile_id).ok_or(SquareHoleError::MissingProfileId)?;
|
||||||
|
let shape_options = normalize_shape_options(config.shape_options.clone(), &config.theme_text);
|
||||||
|
|
||||||
Ok(SquareHoleRunSnapshot {
|
Ok(SquareHoleRunSnapshot {
|
||||||
run_id,
|
run_id,
|
||||||
@@ -115,8 +131,14 @@ pub fn start_run_at(
|
|||||||
best_combo: 0,
|
best_combo: 0,
|
||||||
score: 0,
|
score: 0,
|
||||||
rule_label: config.twist_rule.clone(),
|
rule_label: config.twist_rule.clone(),
|
||||||
current_shape: Some(build_shape_at(0, config.shape_count)),
|
background_image_src: config.background_image_src.clone(),
|
||||||
holes: default_holes(),
|
current_shape: Some(build_shape_at(
|
||||||
|
0,
|
||||||
|
config.shape_count,
|
||||||
|
shape_options.as_slice(),
|
||||||
|
)),
|
||||||
|
shape_options,
|
||||||
|
holes: build_holes(config.hole_options.as_slice()),
|
||||||
last_feedback: None,
|
last_feedback: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -160,14 +182,18 @@ pub fn confirm_drop_at(
|
|||||||
next.completed_shape_count = next.completed_shape_count.saturating_add(1);
|
next.completed_shape_count = next.completed_shape_count.saturating_add(1);
|
||||||
next.combo = next.combo.saturating_add(1);
|
next.combo = next.combo.saturating_add(1);
|
||||||
next.best_combo = next.best_combo.max(next.combo);
|
next.best_combo = next.best_combo.max(next.combo);
|
||||||
next.score = next.score.saturating_add(100 + next.combo * 10);
|
let bonus_score = if hole.bonus { 50 } else { 0 };
|
||||||
|
next.score = next
|
||||||
|
.score
|
||||||
|
.saturating_add(100 + next.combo * 10 + bonus_score);
|
||||||
next.current_shape = if next.completed_shape_count >= next.total_shape_count {
|
next.current_shape = if next.completed_shape_count >= next.total_shape_count {
|
||||||
next.status = SquareHoleRunStatus::Won;
|
next.status = SquareHoleRunStatus::Won;
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(build_shape_at(
|
Some(build_shape_from_previous_options(
|
||||||
next.completed_shape_count,
|
next.completed_shape_count,
|
||||||
next.total_shape_count,
|
next.total_shape_count,
|
||||||
|
next.shape_options.as_slice(),
|
||||||
))
|
))
|
||||||
};
|
};
|
||||||
next.snapshot_version = next.snapshot_version.saturating_add(1);
|
next.snapshot_version = next.snapshot_version.saturating_add(1);
|
||||||
@@ -216,7 +242,23 @@ pub fn stop_run_at(run: &SquareHoleRunSnapshot) -> SquareHoleRunSnapshot {
|
|||||||
next
|
next
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_shape_at(index: u32, total: u32) -> SquareHoleShapeSnapshot {
|
pub fn build_shape_at(
|
||||||
|
index: u32,
|
||||||
|
total: u32,
|
||||||
|
options: &[SquareHoleShapeOption],
|
||||||
|
) -> SquareHoleShapeSnapshot {
|
||||||
|
if let Some(option) = pick_shape_option(index, options) {
|
||||||
|
let shape_kind = option.shape_kind;
|
||||||
|
let label = option.label;
|
||||||
|
return SquareHoleShapeSnapshot {
|
||||||
|
shape_id: format!("square-hole-shape-{index:03}"),
|
||||||
|
color: fallback_shape_color(&shape_kind).to_string(),
|
||||||
|
shape_kind,
|
||||||
|
label,
|
||||||
|
image_src: option.image_src,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let kind = if index + 1 == total {
|
let kind = if index + 1 == total {
|
||||||
"square"
|
"square"
|
||||||
} else if index % 4 == 0 {
|
} else if index % 4 == 0 {
|
||||||
@@ -248,6 +290,7 @@ pub fn build_shape_at(index: u32, total: u32) -> SquareHoleShapeSnapshot {
|
|||||||
_ => "#c084fc",
|
_ => "#c084fc",
|
||||||
}
|
}
|
||||||
.to_string(),
|
.to_string(),
|
||||||
|
image_src: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,6 +302,7 @@ pub fn default_holes() -> Vec<SquareHoleHoleSnapshot> {
|
|||||||
label: "方洞".to_string(),
|
label: "方洞".to_string(),
|
||||||
x: 0.5,
|
x: 0.5,
|
||||||
y: 0.28,
|
y: 0.28,
|
||||||
|
bonus: true,
|
||||||
},
|
},
|
||||||
SquareHoleHoleSnapshot {
|
SquareHoleHoleSnapshot {
|
||||||
hole_id: "circle-hole".to_string(),
|
hole_id: "circle-hole".to_string(),
|
||||||
@@ -266,6 +310,7 @@ pub fn default_holes() -> Vec<SquareHoleHoleSnapshot> {
|
|||||||
label: "圆洞".to_string(),
|
label: "圆洞".to_string(),
|
||||||
x: 0.24,
|
x: 0.24,
|
||||||
y: 0.54,
|
y: 0.54,
|
||||||
|
bonus: false,
|
||||||
},
|
},
|
||||||
SquareHoleHoleSnapshot {
|
SquareHoleHoleSnapshot {
|
||||||
hole_id: "triangle-hole".to_string(),
|
hole_id: "triangle-hole".to_string(),
|
||||||
@@ -273,10 +318,240 @@ pub fn default_holes() -> Vec<SquareHoleHoleSnapshot> {
|
|||||||
label: "三角洞".to_string(),
|
label: "三角洞".to_string(),
|
||||||
x: 0.76,
|
x: 0.76,
|
||||||
y: 0.54,
|
y: 0.54,
|
||||||
|
bonus: false,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn default_shape_options(theme_text: &str) -> Vec<SquareHoleShapeOption> {
|
||||||
|
let theme = normalize_required_string(theme_text).unwrap_or_else(|| "玩具".to_string());
|
||||||
|
[
|
||||||
|
("square", "方块"),
|
||||||
|
("circle", "圆块"),
|
||||||
|
("triangle", "三角块"),
|
||||||
|
("diamond", "菱形块"),
|
||||||
|
("star", "星形块"),
|
||||||
|
("arch", "拱形块"),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.map(|(kind, label)| SquareHoleShapeOption {
|
||||||
|
option_id: format!("{kind}-option"),
|
||||||
|
shape_kind: kind.to_string(),
|
||||||
|
label: label.to_string(),
|
||||||
|
image_prompt: format!("{theme}主题的{label}贴纸图,透明背景,明亮可爱,游戏资产"),
|
||||||
|
image_src: None,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_hole_options() -> Vec<SquareHoleHoleOption> {
|
||||||
|
vec![
|
||||||
|
SquareHoleHoleOption {
|
||||||
|
hole_id: "square-hole".to_string(),
|
||||||
|
hole_kind: "square".to_string(),
|
||||||
|
label: "方洞".to_string(),
|
||||||
|
bonus: true,
|
||||||
|
},
|
||||||
|
SquareHoleHoleOption {
|
||||||
|
hole_id: "circle-hole".to_string(),
|
||||||
|
hole_kind: "circle".to_string(),
|
||||||
|
label: "圆洞".to_string(),
|
||||||
|
bonus: false,
|
||||||
|
},
|
||||||
|
SquareHoleHoleOption {
|
||||||
|
hole_id: "triangle-hole".to_string(),
|
||||||
|
hole_kind: "triangle".to_string(),
|
||||||
|
label: "三角洞".to_string(),
|
||||||
|
bonus: false,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn normalize_shape_options(
|
||||||
|
options: Vec<SquareHoleShapeOption>,
|
||||||
|
theme_text: &str,
|
||||||
|
) -> Vec<SquareHoleShapeOption> {
|
||||||
|
let mut normalized = Vec::new();
|
||||||
|
for (index, option) in options.into_iter().enumerate() {
|
||||||
|
let shape_kind = normalize_required_string(&option.shape_kind)
|
||||||
|
.unwrap_or_else(|| fallback_shape_kind(index));
|
||||||
|
let label = normalize_required_string(&option.label)
|
||||||
|
.unwrap_or_else(|| fallback_shape_label(&shape_kind).to_string());
|
||||||
|
let option_id = normalize_required_string(&option.option_id)
|
||||||
|
.unwrap_or_else(|| format!("{shape_kind}-option-{index}"));
|
||||||
|
let image_prompt = normalize_required_string(&option.image_prompt).unwrap_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"{}主题的{}贴纸图,透明背景,明亮可爱,游戏资产",
|
||||||
|
normalize_required_string(theme_text).unwrap_or_else(|| "玩具".to_string()),
|
||||||
|
label
|
||||||
|
)
|
||||||
|
});
|
||||||
|
normalized.push(SquareHoleShapeOption {
|
||||||
|
option_id,
|
||||||
|
shape_kind,
|
||||||
|
label,
|
||||||
|
image_prompt,
|
||||||
|
image_src: option.image_src.and_then(normalize_required_string),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let defaults = default_shape_options(theme_text);
|
||||||
|
let mut default_index = 0;
|
||||||
|
while normalized.len() < SQUARE_HOLE_MIN_SHAPE_OPTION_COUNT {
|
||||||
|
let mut fallback = defaults[default_index % defaults.len()].clone();
|
||||||
|
if normalized
|
||||||
|
.iter()
|
||||||
|
.any(|option| option.option_id == fallback.option_id)
|
||||||
|
{
|
||||||
|
fallback.option_id = format!("{}-{}", fallback.option_id, normalized.len());
|
||||||
|
}
|
||||||
|
normalized.push(fallback);
|
||||||
|
default_index += 1;
|
||||||
|
}
|
||||||
|
normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn normalize_hole_options(options: Vec<SquareHoleHoleOption>) -> Vec<SquareHoleHoleOption> {
|
||||||
|
let mut normalized = Vec::new();
|
||||||
|
for (index, option) in options
|
||||||
|
.into_iter()
|
||||||
|
.take(SQUARE_HOLE_MAX_HOLE_OPTION_COUNT)
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
let hole_kind = normalize_required_string(&option.hole_kind)
|
||||||
|
.unwrap_or_else(|| fallback_shape_kind(index));
|
||||||
|
let label = normalize_required_string(&option.label)
|
||||||
|
.unwrap_or_else(|| fallback_hole_label(&hole_kind).to_string());
|
||||||
|
let hole_id = normalize_required_string(&option.hole_id)
|
||||||
|
.unwrap_or_else(|| format!("{hole_kind}-hole-{index}"));
|
||||||
|
normalized.push(SquareHoleHoleOption {
|
||||||
|
hole_id,
|
||||||
|
hole_kind,
|
||||||
|
label,
|
||||||
|
bonus: option.bonus,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for fallback in default_hole_options() {
|
||||||
|
if normalized.len() >= SQUARE_HOLE_MIN_HOLE_OPTION_COUNT {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if !normalized
|
||||||
|
.iter()
|
||||||
|
.any(|option| option.hole_id == fallback.hole_id)
|
||||||
|
{
|
||||||
|
normalized.push(fallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if normalized.iter().all(|option| !option.bonus)
|
||||||
|
&& let Some(first) = normalized.first_mut()
|
||||||
|
{
|
||||||
|
first.bonus = true;
|
||||||
|
}
|
||||||
|
normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_background_prompt(theme_text: &str) -> String {
|
||||||
|
format!(
|
||||||
|
"{}主题的竖屏游戏背景,舞台中央有洞口面板,明亮夸张,适合移动端小游戏",
|
||||||
|
normalize_required_string(theme_text).unwrap_or_else(|| "玩具".to_string())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_holes(options: &[SquareHoleHoleOption]) -> Vec<SquareHoleHoleSnapshot> {
|
||||||
|
let normalized = normalize_hole_options(options.to_vec());
|
||||||
|
let positions = [
|
||||||
|
(0.5, 0.28),
|
||||||
|
(0.24, 0.54),
|
||||||
|
(0.76, 0.54),
|
||||||
|
(0.24, 0.78),
|
||||||
|
(0.5, 0.78),
|
||||||
|
(0.76, 0.78),
|
||||||
|
];
|
||||||
|
normalized
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, option)| {
|
||||||
|
let (x, y) = positions[index.min(positions.len() - 1)];
|
||||||
|
SquareHoleHoleSnapshot {
|
||||||
|
hole_id: option.hole_id,
|
||||||
|
hole_kind: option.hole_kind,
|
||||||
|
label: option.label,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
bonus: option.bonus,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_shape_from_previous_options(
|
||||||
|
index: u32,
|
||||||
|
total: u32,
|
||||||
|
options: &[SquareHoleShapeOption],
|
||||||
|
) -> SquareHoleShapeSnapshot {
|
||||||
|
build_shape_at(index, total, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pick_shape_option(
|
||||||
|
index: u32,
|
||||||
|
options: &[SquareHoleShapeOption],
|
||||||
|
) -> Option<SquareHoleShapeOption> {
|
||||||
|
if options.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
options.get(index as usize % options.len()).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fallback_shape_kind(index: usize) -> String {
|
||||||
|
match index % 6 {
|
||||||
|
0 => "square",
|
||||||
|
1 => "circle",
|
||||||
|
2 => "triangle",
|
||||||
|
3 => "diamond",
|
||||||
|
4 => "star",
|
||||||
|
_ => "arch",
|
||||||
|
}
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fallback_shape_label(kind: &str) -> &'static str {
|
||||||
|
match kind {
|
||||||
|
"square" => "方块",
|
||||||
|
"circle" => "圆块",
|
||||||
|
"triangle" => "三角块",
|
||||||
|
"diamond" => "菱形块",
|
||||||
|
"star" => "星形块",
|
||||||
|
"arch" => "拱形块",
|
||||||
|
_ => "形状块",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fallback_hole_label(kind: &str) -> &'static str {
|
||||||
|
match kind {
|
||||||
|
"square" => "方洞",
|
||||||
|
"circle" => "圆洞",
|
||||||
|
"triangle" => "三角洞",
|
||||||
|
"diamond" => "菱形洞",
|
||||||
|
"star" => "星形洞",
|
||||||
|
"arch" => "拱形洞",
|
||||||
|
_ => "洞口",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fallback_shape_color(kind: &str) -> &'static str {
|
||||||
|
match kind {
|
||||||
|
"square" => "#facc15",
|
||||||
|
"circle" => "#22c55e",
|
||||||
|
"triangle" => "#38bdf8",
|
||||||
|
"diamond" => "#fb7185",
|
||||||
|
"star" => "#c084fc",
|
||||||
|
"arch" => "#f97316",
|
||||||
|
_ => "#f8fafc",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn is_shape_accepted_by_hole(
|
fn is_shape_accepted_by_hole(
|
||||||
shape: &SquareHoleShapeSnapshot,
|
shape: &SquareHoleShapeSnapshot,
|
||||||
hole: &SquareHoleHoleSnapshot,
|
hole: &SquareHoleHoleSnapshot,
|
||||||
@@ -315,6 +590,32 @@ mod tests {
|
|||||||
build_creator_config("玩具", "方洞万能", shape_count, 4).expect("config should be valid")
|
build_creator_config("玩具", "方洞万能", shape_count, 4).expect("config should be valid")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn test_config_with_bonus_hole(shape_count: u32) -> SquareHoleCreatorConfig {
|
||||||
|
SquareHoleCreatorConfig {
|
||||||
|
hole_options: vec![
|
||||||
|
SquareHoleHoleOption {
|
||||||
|
hole_id: "square-hole".to_string(),
|
||||||
|
hole_kind: "square".to_string(),
|
||||||
|
label: "方洞".to_string(),
|
||||||
|
bonus: true,
|
||||||
|
},
|
||||||
|
SquareHoleHoleOption {
|
||||||
|
hole_id: "circle-hole".to_string(),
|
||||||
|
hole_kind: "circle".to_string(),
|
||||||
|
label: "圆洞".to_string(),
|
||||||
|
bonus: false,
|
||||||
|
},
|
||||||
|
SquareHoleHoleOption {
|
||||||
|
hole_id: "triangle-hole".to_string(),
|
||||||
|
hole_kind: "triangle".to_string(),
|
||||||
|
label: "三角洞".to_string(),
|
||||||
|
bonus: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
..test_config(shape_count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn draft_is_publishable_with_required_fields() {
|
fn draft_is_publishable_with_required_fields() {
|
||||||
let draft = compile_result_draft("profile-1".to_string(), &test_config(8));
|
let draft = compile_result_draft("profile-1".to_string(), &test_config(8));
|
||||||
@@ -368,6 +669,33 @@ mod tests {
|
|||||||
assert_eq!(result.run.combo, 1);
|
assert_eq!(result.run.combo, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bonus_hole_adds_extra_score_when_accepted() {
|
||||||
|
let run = start_run_at(
|
||||||
|
"run-1".to_string(),
|
||||||
|
"user-1".to_string(),
|
||||||
|
"profile-1".to_string(),
|
||||||
|
&test_config_with_bonus_hole(8),
|
||||||
|
1_000,
|
||||||
|
)
|
||||||
|
.expect("run should start");
|
||||||
|
let result = confirm_drop_at(
|
||||||
|
&run,
|
||||||
|
&SquareHoleDropInput {
|
||||||
|
run_id: run.run_id.clone(),
|
||||||
|
owner_user_id: run.owner_user_id.clone(),
|
||||||
|
hole_id: "square-hole".to_string(),
|
||||||
|
client_snapshot_version: run.snapshot_version,
|
||||||
|
client_event_id: "event-1".to_string(),
|
||||||
|
dropped_at_ms: 1_100,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("drop should resolve");
|
||||||
|
|
||||||
|
assert!(result.feedback.accepted);
|
||||||
|
assert_eq!(result.run.score, 160);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn wrong_non_square_hole_rejects_and_resets_combo() {
|
fn wrong_non_square_hole_rejects_and_resets_combo() {
|
||||||
let mut run = start_run_at(
|
let mut run = start_run_at(
|
||||||
@@ -378,7 +706,7 @@ mod tests {
|
|||||||
1_000,
|
1_000,
|
||||||
)
|
)
|
||||||
.expect("run should start");
|
.expect("run should start");
|
||||||
run.current_shape = Some(build_shape_at(1, 8));
|
run.current_shape = Some(build_shape_at(1, 8, &[]));
|
||||||
run.combo = 2;
|
run.combo = 2;
|
||||||
|
|
||||||
let result = confirm_drop_at(
|
let result = confirm_drop_at(
|
||||||
@@ -413,7 +741,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.expect("run should start");
|
.expect("run should start");
|
||||||
run.completed_shape_count = 5;
|
run.completed_shape_count = 5;
|
||||||
run.current_shape = Some(build_shape_at(5, 6));
|
run.current_shape = Some(build_shape_at(5, 6, &[]));
|
||||||
|
|
||||||
let result = confirm_drop_at(
|
let result = confirm_drop_at(
|
||||||
&run,
|
&run,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use shared_kernel::{normalize_required_string, normalize_string_list};
|
|||||||
use crate::{
|
use crate::{
|
||||||
SQUARE_HOLE_MAX_DIFFICULTY, SQUARE_HOLE_MAX_SHAPE_COUNT, SQUARE_HOLE_MIN_DIFFICULTY,
|
SQUARE_HOLE_MAX_DIFFICULTY, SQUARE_HOLE_MAX_SHAPE_COUNT, SQUARE_HOLE_MIN_DIFFICULTY,
|
||||||
SQUARE_HOLE_MIN_SHAPE_COUNT, SquareHoleCreatorConfig, SquareHoleError, SquareHoleResultDraft,
|
SQUARE_HOLE_MIN_SHAPE_COUNT, SquareHoleCreatorConfig, SquareHoleError, SquareHoleResultDraft,
|
||||||
|
normalize_hole_options, normalize_shape_options,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn validate_shape_count(value: u32) -> Result<u32, SquareHoleError> {
|
pub fn validate_shape_count(value: u32) -> Result<u32, SquareHoleError> {
|
||||||
@@ -36,6 +37,11 @@ pub fn build_creator_config(
|
|||||||
twist_rule: normalize_required_string(twist_rule).ok_or(SquareHoleError::MissingText)?,
|
twist_rule: normalize_required_string(twist_rule).ok_or(SquareHoleError::MissingText)?,
|
||||||
shape_count: validate_shape_count(shape_count)?,
|
shape_count: validate_shape_count(shape_count)?,
|
||||||
difficulty: validate_difficulty(difficulty)?,
|
difficulty: validate_difficulty(difficulty)?,
|
||||||
|
shape_options: normalize_shape_options(Vec::new(), theme_text),
|
||||||
|
hole_options: normalize_hole_options(Vec::new()),
|
||||||
|
background_prompt: format!("{theme_text}主题的竖屏游戏背景,舞台中央有多个形状洞口"),
|
||||||
|
cover_image_src: None,
|
||||||
|
background_image_src: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,6 +90,12 @@ pub fn validate_publish_requirements(draft: &SquareHoleResultDraft) -> Vec<Strin
|
|||||||
if validate_difficulty(draft.difficulty).is_err() {
|
if validate_difficulty(draft.difficulty).is_err() {
|
||||||
blockers.push("难度必须在 1 到 10 之间".to_string());
|
blockers.push("难度必须在 1 到 10 之间".to_string());
|
||||||
}
|
}
|
||||||
|
if draft.shape_options.len() < crate::SQUARE_HOLE_MIN_SHAPE_OPTION_COUNT {
|
||||||
|
blockers.push("至少需要 6 个形状图片选项".to_string());
|
||||||
|
}
|
||||||
|
if draft.hole_options.len() < crate::SQUARE_HOLE_MIN_HOLE_OPTION_COUNT {
|
||||||
|
blockers.push("至少需要 3 个洞口选项".to_string());
|
||||||
|
}
|
||||||
blockers
|
blockers
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,10 +116,15 @@ pub fn build_result_draft(
|
|||||||
SquareHoleResultDraft {
|
SquareHoleResultDraft {
|
||||||
profile_id,
|
profile_id,
|
||||||
game_name,
|
game_name,
|
||||||
theme_text,
|
theme_text: theme_text.clone(),
|
||||||
twist_rule,
|
twist_rule,
|
||||||
summary,
|
summary,
|
||||||
tags: build_default_tags("方洞挑战"),
|
tags: build_default_tags("方洞挑战"),
|
||||||
|
cover_image_src: None,
|
||||||
|
background_prompt: format!("{theme_text}主题的竖屏游戏背景,舞台中央有多个形状洞口"),
|
||||||
|
background_image_src: None,
|
||||||
|
shape_options: normalize_shape_options(Vec::new(), &theme_text),
|
||||||
|
hole_options: normalize_hole_options(Vec::new()),
|
||||||
shape_count,
|
shape_count,
|
||||||
difficulty,
|
difficulty,
|
||||||
publish_ready: true,
|
publish_ready: true,
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ pub const SQUARE_HOLE_MAX_SHAPE_COUNT: u32 = 24;
|
|||||||
pub const SQUARE_HOLE_MIN_DIFFICULTY: u32 = 1;
|
pub const SQUARE_HOLE_MIN_DIFFICULTY: u32 = 1;
|
||||||
pub const SQUARE_HOLE_MAX_DIFFICULTY: u32 = 10;
|
pub const SQUARE_HOLE_MAX_DIFFICULTY: u32 = 10;
|
||||||
pub const SQUARE_HOLE_DEFAULT_DURATION_LIMIT_MS: u64 = 60_000;
|
pub const SQUARE_HOLE_DEFAULT_DURATION_LIMIT_MS: u64 = 60_000;
|
||||||
|
pub const SQUARE_HOLE_MIN_SHAPE_OPTION_COUNT: usize = 6;
|
||||||
|
pub const SQUARE_HOLE_MIN_HOLE_OPTION_COUNT: usize = 3;
|
||||||
|
pub const SQUARE_HOLE_MAX_HOLE_OPTION_COUNT: usize = 6;
|
||||||
|
|
||||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
@@ -55,6 +58,37 @@ pub struct SquareHoleCreatorConfig {
|
|||||||
pub twist_rule: String,
|
pub twist_rule: String,
|
||||||
pub shape_count: u32,
|
pub shape_count: u32,
|
||||||
pub difficulty: u32,
|
pub difficulty: u32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub shape_options: Vec<SquareHoleShapeOption>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub hole_options: Vec<SquareHoleHoleOption>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub background_prompt: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub cover_image_src: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub background_image_src: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct SquareHoleShapeOption {
|
||||||
|
pub option_id: String,
|
||||||
|
pub shape_kind: String,
|
||||||
|
pub label: String,
|
||||||
|
pub image_prompt: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub image_src: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct SquareHoleHoleOption {
|
||||||
|
pub hole_id: String,
|
||||||
|
pub hole_kind: String,
|
||||||
|
pub label: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub bonus: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||||
@@ -66,6 +100,16 @@ pub struct SquareHoleResultDraft {
|
|||||||
pub twist_rule: String,
|
pub twist_rule: String,
|
||||||
pub summary: String,
|
pub summary: String,
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub cover_image_src: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub background_prompt: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub background_image_src: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub shape_options: Vec<SquareHoleShapeOption>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub hole_options: Vec<SquareHoleHoleOption>,
|
||||||
pub shape_count: u32,
|
pub shape_count: u32,
|
||||||
pub difficulty: u32,
|
pub difficulty: u32,
|
||||||
pub publish_ready: bool,
|
pub publish_ready: bool,
|
||||||
@@ -85,6 +129,10 @@ pub struct SquareHoleWorkProfile {
|
|||||||
pub summary: String,
|
pub summary: String,
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
pub cover_image_src: Option<String>,
|
pub cover_image_src: Option<String>,
|
||||||
|
pub background_prompt: String,
|
||||||
|
pub background_image_src: Option<String>,
|
||||||
|
pub shape_options: Vec<SquareHoleShapeOption>,
|
||||||
|
pub hole_options: Vec<SquareHoleHoleOption>,
|
||||||
pub shape_count: u32,
|
pub shape_count: u32,
|
||||||
pub difficulty: u32,
|
pub difficulty: u32,
|
||||||
pub publication_status: SquareHolePublicationStatus,
|
pub publication_status: SquareHolePublicationStatus,
|
||||||
@@ -100,6 +148,8 @@ pub struct SquareHoleShapeSnapshot {
|
|||||||
pub shape_kind: String,
|
pub shape_kind: String,
|
||||||
pub label: String,
|
pub label: String,
|
||||||
pub color: String,
|
pub color: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub image_src: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||||
@@ -110,6 +160,8 @@ pub struct SquareHoleHoleSnapshot {
|
|||||||
pub label: String,
|
pub label: String,
|
||||||
pub x: f32,
|
pub x: f32,
|
||||||
pub y: f32,
|
pub y: f32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub bonus: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||||
@@ -137,6 +189,10 @@ pub struct SquareHoleRunSnapshot {
|
|||||||
pub best_combo: u32,
|
pub best_combo: u32,
|
||||||
pub score: u32,
|
pub score: u32,
|
||||||
pub rule_label: String,
|
pub rule_label: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub background_image_src: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub shape_options: Vec<SquareHoleShapeOption>,
|
||||||
pub current_shape: Option<SquareHoleShapeSnapshot>,
|
pub current_shape: Option<SquareHoleShapeSnapshot>,
|
||||||
pub holes: Vec<SquareHoleHoleSnapshot>,
|
pub holes: Vec<SquareHoleHoleSnapshot>,
|
||||||
pub last_feedback: Option<SquareHoleDropFeedback>,
|
pub last_feedback: Option<SquareHoleDropFeedback>,
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ pub struct LlmTextRequest {
|
|||||||
pub max_tokens: Option<u32>,
|
pub max_tokens: Option<u32>,
|
||||||
pub enable_web_search: bool,
|
pub enable_web_search: bool,
|
||||||
pub protocol: LlmTextProtocol,
|
pub protocol: LlmTextProtocol,
|
||||||
|
pub request_timeout_ms: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 文本协议必须由业务请求显式选择,避免全局默认模型把不同场景混到同一上游形态。
|
// 文本协议必须由业务请求显式选择,避免全局默认模型把不同场景混到同一上游形态。
|
||||||
@@ -421,6 +422,7 @@ impl LlmTextRequest {
|
|||||||
max_tokens: None,
|
max_tokens: None,
|
||||||
enable_web_search: false,
|
enable_web_search: false,
|
||||||
protocol: LlmTextProtocol::ChatCompletions,
|
protocol: LlmTextProtocol::ChatCompletions,
|
||||||
|
request_timeout_ms: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,6 +453,11 @@ impl LlmTextRequest {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_request_timeout_ms(mut self, request_timeout_ms: u64) -> Self {
|
||||||
|
self.request_timeout_ms = Some(request_timeout_ms);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
fn validate(&self) -> Result<(), LlmError> {
|
fn validate(&self) -> Result<(), LlmError> {
|
||||||
if self.messages.is_empty() {
|
if self.messages.is_empty() {
|
||||||
return Err(LlmError::InvalidRequest(
|
return Err(LlmError::InvalidRequest(
|
||||||
@@ -474,6 +481,14 @@ impl LlmTextRequest {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(request_timeout_ms) = self.request_timeout_ms
|
||||||
|
&& request_timeout_ms == 0
|
||||||
|
{
|
||||||
|
return Err(LlmError::InvalidRequest(
|
||||||
|
"LLM request_timeout_ms 必须大于 0".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,6 +499,12 @@ impl LlmTextRequest {
|
|||||||
.filter(|value| !value.is_empty())
|
.filter(|value| !value.is_empty())
|
||||||
.unwrap_or(fallback_model)
|
.unwrap_or(fallback_model)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolved_request_timeout_ms(&self, fallback_timeout_ms: u64) -> u64 {
|
||||||
|
self.request_timeout_ms
|
||||||
|
.filter(|value| *value > 0)
|
||||||
|
.unwrap_or(fallback_timeout_ms)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LlmTextProtocol {
|
impl LlmTextProtocol {
|
||||||
@@ -825,7 +846,9 @@ impl LlmClient {
|
|||||||
.post(url.as_str())
|
.post(url.as_str())
|
||||||
.bearer_auth(self.config.api_key())
|
.bearer_auth(self.config.api_key())
|
||||||
.json(&request_body)
|
.json(&request_body)
|
||||||
.timeout(Duration::from_millis(self.config.request_timeout_ms()))
|
.timeout(Duration::from_millis(
|
||||||
|
request.resolved_request_timeout_ms(self.config.request_timeout_ms()),
|
||||||
|
))
|
||||||
.send()
|
.send()
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -1592,6 +1615,48 @@ mod tests {
|
|||||||
assert_eq!(response.response_id.as_deref(), Some("resp_retry"));
|
assert_eq!(response.response_id.as_deref(), Some("resp_retry"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn request_text_uses_request_level_timeout_override() {
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:0").expect("listener should bind");
|
||||||
|
let address = listener.local_addr().expect("listener should have addr");
|
||||||
|
thread::spawn(move || {
|
||||||
|
let (mut stream, _) = listener.accept().expect("request should connect");
|
||||||
|
let _ = read_request(&mut stream);
|
||||||
|
thread::sleep(StdDuration::from_millis(200));
|
||||||
|
write_response(
|
||||||
|
&mut stream,
|
||||||
|
MockResponse {
|
||||||
|
status_line: "200 OK",
|
||||||
|
content_type: "application/json; charset=utf-8",
|
||||||
|
body: r#"{"choices":[{"message":{"content":"too late"},"finish_reason":"stop"}]}"#
|
||||||
|
.to_string(),
|
||||||
|
extra_headers: Vec::new(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
let config = LlmConfig::new(
|
||||||
|
LlmProvider::Ark,
|
||||||
|
format!("http://{address}"),
|
||||||
|
"test-key".to_string(),
|
||||||
|
"test-model".to_string(),
|
||||||
|
10_000,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
.expect("config should be valid");
|
||||||
|
let client = LlmClient::new(config).expect("client should be created");
|
||||||
|
|
||||||
|
let error = client
|
||||||
|
.request_text(
|
||||||
|
LlmTextRequest::single_turn("系统", "用户").with_request_timeout_ms(20),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect_err("request override should timeout before the global timeout");
|
||||||
|
|
||||||
|
assert_eq!(error, LlmError::Timeout { attempts: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn request_text_sends_web_search_options_when_enabled() {
|
async fn request_text_sends_web_search_options_when_enabled() {
|
||||||
let listener = TcpListener::bind("127.0.0.1:0").expect("listener should bind");
|
let listener = TcpListener::bind("127.0.0.1:0").expect("listener should bind");
|
||||||
|
|||||||
@@ -38,6 +38,26 @@ pub struct ExecuteSquareHoleActionRequest {
|
|||||||
pub cover_image_src: Option<String>,
|
pub cover_image_src: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SquareHoleShapeOptionResponse {
|
||||||
|
pub option_id: String,
|
||||||
|
pub shape_kind: String,
|
||||||
|
pub label: String,
|
||||||
|
pub image_prompt: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub image_src: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SquareHoleHoleOptionResponse {
|
||||||
|
pub hole_id: String,
|
||||||
|
pub hole_kind: String,
|
||||||
|
pub label: String,
|
||||||
|
pub bonus: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct SquareHoleAnchorItemResponse {
|
pub struct SquareHoleAnchorItemResponse {
|
||||||
@@ -63,6 +83,16 @@ pub struct SquareHoleCreatorConfigResponse {
|
|||||||
pub twist_rule: String,
|
pub twist_rule: String,
|
||||||
pub shape_count: u32,
|
pub shape_count: u32,
|
||||||
pub difficulty: u32,
|
pub difficulty: u32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub shape_options: Vec<SquareHoleShapeOptionResponse>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub hole_options: Vec<SquareHoleHoleOptionResponse>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub background_prompt: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub cover_image_src: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub background_image_src: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
@@ -74,6 +104,16 @@ pub struct SquareHoleResultDraftResponse {
|
|||||||
pub twist_rule: String,
|
pub twist_rule: String,
|
||||||
pub summary: String,
|
pub summary: String,
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub cover_image_src: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub background_prompt: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub background_image_src: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub shape_options: Vec<SquareHoleShapeOptionResponse>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub hole_options: Vec<SquareHoleHoleOptionResponse>,
|
||||||
pub shape_count: u32,
|
pub shape_count: u32,
|
||||||
pub difficulty: u32,
|
pub difficulty: u32,
|
||||||
pub publish_ready: bool,
|
pub publish_ready: bool,
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ pub struct SquareHoleShapeSnapshotResponse {
|
|||||||
pub shape_kind: String,
|
pub shape_kind: String,
|
||||||
pub label: String,
|
pub label: String,
|
||||||
pub color: String,
|
pub color: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub image_src: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
@@ -40,6 +42,7 @@ pub struct SquareHoleHoleSnapshotResponse {
|
|||||||
pub label: String,
|
pub label: String,
|
||||||
pub x: f32,
|
pub x: f32,
|
||||||
pub y: f32,
|
pub y: f32,
|
||||||
|
pub bonus: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
@@ -69,6 +72,8 @@ pub struct SquareHoleRunSnapshotResponse {
|
|||||||
pub score: u32,
|
pub score: u32,
|
||||||
pub rule_label: String,
|
pub rule_label: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub background_image_src: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
pub current_shape: Option<SquareHoleShapeSnapshotResponse>,
|
pub current_shape: Option<SquareHoleShapeSnapshotResponse>,
|
||||||
pub holes: Vec<SquareHoleHoleSnapshotResponse>,
|
pub holes: Vec<SquareHoleHoleSnapshotResponse>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|||||||
@@ -1,5 +1,25 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SquareHoleShapeOptionResponse {
|
||||||
|
pub option_id: String,
|
||||||
|
pub shape_kind: String,
|
||||||
|
pub label: String,
|
||||||
|
pub image_prompt: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub image_src: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SquareHoleHoleOptionResponse {
|
||||||
|
pub hole_id: String,
|
||||||
|
pub hole_kind: String,
|
||||||
|
pub label: String,
|
||||||
|
pub bonus: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct PutSquareHoleWorkRequest {
|
pub struct PutSquareHoleWorkRequest {
|
||||||
@@ -11,6 +31,14 @@ pub struct PutSquareHoleWorkRequest {
|
|||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub cover_image_src: Option<String>,
|
pub cover_image_src: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub background_prompt: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub background_image_src: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub shape_options: Option<Vec<SquareHoleShapeOptionResponse>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub hole_options: Option<Vec<SquareHoleHoleOptionResponse>>,
|
||||||
pub shape_count: u32,
|
pub shape_count: u32,
|
||||||
pub difficulty: u32,
|
pub difficulty: u32,
|
||||||
}
|
}
|
||||||
@@ -30,6 +58,14 @@ pub struct SquareHoleWorkSummaryResponse {
|
|||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub cover_image_src: Option<String>,
|
pub cover_image_src: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub background_prompt: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub background_image_src: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub shape_options: Vec<SquareHoleShapeOptionResponse>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub hole_options: Vec<SquareHoleHoleOptionResponse>,
|
||||||
pub shape_count: u32,
|
pub shape_count: u32,
|
||||||
pub difficulty: u32,
|
pub difficulty: u32,
|
||||||
pub publication_status: String,
|
pub publication_status: String,
|
||||||
|
|||||||
@@ -55,11 +55,11 @@ pub use mapper::{
|
|||||||
SquareHoleAgentMessageSubmitRecordInput, SquareHoleAgentSessionCreateRecordInput,
|
SquareHoleAgentMessageSubmitRecordInput, SquareHoleAgentSessionCreateRecordInput,
|
||||||
SquareHoleAgentSessionRecord, SquareHoleAnchorItemRecord, SquareHoleAnchorPackRecord,
|
SquareHoleAgentSessionRecord, SquareHoleAnchorItemRecord, SquareHoleAnchorPackRecord,
|
||||||
SquareHoleCompileDraftRecordInput, SquareHoleCreatorConfigRecord,
|
SquareHoleCompileDraftRecordInput, SquareHoleCreatorConfigRecord,
|
||||||
SquareHoleDropConfirmationRecord, SquareHoleDropFeedbackRecord, SquareHoleHoleSnapshotRecord,
|
SquareHoleDropConfirmationRecord, SquareHoleDropFeedbackRecord, SquareHoleHoleOptionRecord,
|
||||||
SquareHoleResultDraftRecord, SquareHoleRunDropRecordInput, SquareHoleRunRecord,
|
SquareHoleHoleSnapshotRecord, SquareHoleResultDraftRecord, SquareHoleRunDropRecordInput,
|
||||||
SquareHoleRunRestartRecordInput, SquareHoleRunStartRecordInput, SquareHoleRunStopRecordInput,
|
SquareHoleRunRecord, SquareHoleRunRestartRecordInput, SquareHoleRunStartRecordInput,
|
||||||
SquareHoleRunTimeUpRecordInput, SquareHoleShapeSnapshotRecord, SquareHoleWorkProfileRecord,
|
SquareHoleRunStopRecordInput, SquareHoleRunTimeUpRecordInput, SquareHoleShapeOptionRecord,
|
||||||
SquareHoleWorkUpdateRecordInput,
|
SquareHoleShapeSnapshotRecord, SquareHoleWorkProfileRecord, SquareHoleWorkUpdateRecordInput,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod ai;
|
pub mod ai;
|
||||||
@@ -144,9 +144,9 @@ use module_puzzle::{
|
|||||||
PuzzleWorkProfile as DomainPuzzleWorkProfile,
|
PuzzleWorkProfile as DomainPuzzleWorkProfile,
|
||||||
};
|
};
|
||||||
use module_runtime::{
|
use module_runtime::{
|
||||||
RuntimeBrowseHistoryRecord, RuntimePlatformTheme as DomainRuntimePlatformTheme,
|
AnalyticsMetricQueryResponse as DomainAnalyticsMetricQueryResponse, RuntimeBrowseHistoryRecord,
|
||||||
RuntimeProfileDashboardRecord, RuntimeProfileInviteCodeRecord, RuntimeProfilePlayStatsRecord,
|
RuntimePlatformTheme as DomainRuntimePlatformTheme, RuntimeProfileDashboardRecord,
|
||||||
AnalyticsMetricQueryResponse as DomainAnalyticsMetricQueryResponse,
|
RuntimeProfileInviteCodeRecord, RuntimeProfilePlayStatsRecord,
|
||||||
RuntimeProfileRechargeCenterRecord, RuntimeProfileRechargeOrderRecord,
|
RuntimeProfileRechargeCenterRecord, RuntimeProfileRechargeOrderRecord,
|
||||||
RuntimeProfileRedeemCodeMode as DomainRuntimeProfileRedeemCodeMode,
|
RuntimeProfileRedeemCodeMode as DomainRuntimeProfileRedeemCodeMode,
|
||||||
RuntimeProfileRedeemCodeRecord, RuntimeProfileRewardCodeRedeemRecord,
|
RuntimeProfileRedeemCodeRecord, RuntimeProfileRewardCodeRedeemRecord,
|
||||||
@@ -155,7 +155,7 @@ use module_runtime::{
|
|||||||
RuntimeProfileTaskStatus as DomainRuntimeProfileTaskStatus,
|
RuntimeProfileTaskStatus as DomainRuntimeProfileTaskStatus,
|
||||||
RuntimeProfileWalletLedgerEntryRecord, RuntimeReferralInviteCenterRecord,
|
RuntimeProfileWalletLedgerEntryRecord, RuntimeReferralInviteCenterRecord,
|
||||||
RuntimeReferralRedeemRecord, RuntimeSettingsRecord, RuntimeSnapshotRecord,
|
RuntimeReferralRedeemRecord, RuntimeSettingsRecord, RuntimeSnapshotRecord,
|
||||||
RuntimeTrackingScopeKind as DomainRuntimeTrackingScopeKind,
|
RuntimeTrackingScopeKind as DomainRuntimeTrackingScopeKind, build_analytics_metric_query_input,
|
||||||
build_runtime_browse_history_clear_input, build_runtime_browse_history_list_input,
|
build_runtime_browse_history_clear_input, build_runtime_browse_history_list_input,
|
||||||
build_runtime_browse_history_record, build_runtime_browse_history_sync_input,
|
build_runtime_browse_history_record, build_runtime_browse_history_sync_input,
|
||||||
build_runtime_profile_dashboard_get_input, build_runtime_profile_dashboard_record,
|
build_runtime_profile_dashboard_get_input, build_runtime_profile_dashboard_record,
|
||||||
@@ -178,7 +178,6 @@ use module_runtime::{
|
|||||||
build_runtime_profile_wallet_adjustment_input,
|
build_runtime_profile_wallet_adjustment_input,
|
||||||
build_runtime_profile_wallet_ledger_entry_record,
|
build_runtime_profile_wallet_ledger_entry_record,
|
||||||
build_runtime_profile_wallet_ledger_list_input, build_runtime_referral_invite_center_get_input,
|
build_runtime_profile_wallet_ledger_list_input, build_runtime_referral_invite_center_get_input,
|
||||||
build_analytics_metric_query_input,
|
|
||||||
build_runtime_referral_invite_center_record, build_runtime_referral_redeem_input,
|
build_runtime_referral_invite_center_record, build_runtime_referral_redeem_input,
|
||||||
build_runtime_referral_redeem_record, build_runtime_setting_get_input,
|
build_runtime_referral_redeem_record, build_runtime_setting_get_input,
|
||||||
build_runtime_setting_record, build_runtime_setting_upsert_input,
|
build_runtime_setting_record, build_runtime_setting_upsert_input,
|
||||||
|
|||||||
@@ -1585,14 +1585,9 @@ pub(crate) fn map_square_hole_agent_session_procedure_result(
|
|||||||
let session_json = result
|
let session_json = result
|
||||||
.session_json
|
.session_json
|
||||||
.ok_or_else(|| SpacetimeClientError::missing_snapshot("square hole agent session 快照"))?;
|
.ok_or_else(|| SpacetimeClientError::missing_snapshot("square hole agent session 快照"))?;
|
||||||
let session =
|
let session = serde_json::from_str::<SquareHoleAgentSessionJsonRecord>(&session_json).map_err(
|
||||||
serde_json::from_str::<SquareHoleAgentSessionJsonRecord>(&session_json).map_err(
|
|error| SpacetimeClientError::Runtime(format!("square hole session_json 非法: {error}")),
|
||||||
|error| {
|
)?;
|
||||||
SpacetimeClientError::Runtime(format!(
|
|
||||||
"square hole session_json 非法: {error}"
|
|
||||||
))
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(map_square_hole_agent_session_snapshot(session))
|
Ok(map_square_hole_agent_session_snapshot(session))
|
||||||
}
|
}
|
||||||
@@ -1624,13 +1619,15 @@ pub(crate) fn map_square_hole_works_procedure_result(
|
|||||||
let items_json = result
|
let items_json = result
|
||||||
.items_json
|
.items_json
|
||||||
.ok_or_else(|| SpacetimeClientError::missing_snapshot("square hole works 快照"))?;
|
.ok_or_else(|| SpacetimeClientError::missing_snapshot("square hole works 快照"))?;
|
||||||
let items = serde_json::from_str::<Vec<SquareHoleWorkJsonRecord>>(&items_json).map_err(
|
let items =
|
||||||
|error| {
|
serde_json::from_str::<Vec<SquareHoleWorkJsonRecord>>(&items_json).map_err(|error| {
|
||||||
SpacetimeClientError::Runtime(format!("square hole works items_json 非法: {error}"))
|
SpacetimeClientError::Runtime(format!("square hole works items_json 非法: {error}"))
|
||||||
},
|
})?;
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(items.into_iter().map(map_square_hole_work_snapshot).collect())
|
Ok(items
|
||||||
|
.into_iter()
|
||||||
|
.map(map_square_hole_work_snapshot)
|
||||||
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn map_square_hole_run_procedure_result(
|
pub(crate) fn map_square_hole_run_procedure_result(
|
||||||
@@ -1656,18 +1653,14 @@ pub(crate) fn map_square_hole_drop_shape_procedure_result(
|
|||||||
let run_json = result
|
let run_json = result
|
||||||
.run_json
|
.run_json
|
||||||
.ok_or_else(|| SpacetimeClientError::missing_snapshot("square hole drop run 快照"))?;
|
.ok_or_else(|| SpacetimeClientError::missing_snapshot("square hole drop run 快照"))?;
|
||||||
let feedback_json = result.feedback_json.ok_or_else(|| {
|
let feedback_json = result
|
||||||
SpacetimeClientError::missing_snapshot("square hole drop feedback 快照")
|
.feedback_json
|
||||||
})?;
|
.ok_or_else(|| SpacetimeClientError::missing_snapshot("square hole drop feedback 快照"))?;
|
||||||
let run = map_square_hole_run_json(run_json)?;
|
let run = map_square_hole_run_json(run_json)?;
|
||||||
let feedback =
|
let feedback = serde_json::from_str::<SquareHoleDropFeedbackJsonRecord>(&feedback_json)
|
||||||
serde_json::from_str::<SquareHoleDropFeedbackJsonRecord>(&feedback_json).map_err(
|
.map_err(|error| {
|
||||||
|error| {
|
SpacetimeClientError::Runtime(format!("square hole feedback_json 非法: {error}"))
|
||||||
SpacetimeClientError::Runtime(format!(
|
})?;
|
||||||
"square hole feedback_json 非法: {error}"
|
|
||||||
))
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(SquareHoleDropConfirmationRecord {
|
Ok(SquareHoleDropConfirmationRecord {
|
||||||
status: result.status,
|
status: result.status,
|
||||||
@@ -2950,6 +2943,19 @@ fn map_square_hole_creator_config(
|
|||||||
twist_rule: snapshot.twist_rule,
|
twist_rule: snapshot.twist_rule,
|
||||||
shape_count: snapshot.shape_count,
|
shape_count: snapshot.shape_count,
|
||||||
difficulty: snapshot.difficulty,
|
difficulty: snapshot.difficulty,
|
||||||
|
shape_options: snapshot
|
||||||
|
.shape_options
|
||||||
|
.into_iter()
|
||||||
|
.map(map_square_hole_shape_option)
|
||||||
|
.collect(),
|
||||||
|
hole_options: snapshot
|
||||||
|
.hole_options
|
||||||
|
.into_iter()
|
||||||
|
.map(map_square_hole_hole_option)
|
||||||
|
.collect(),
|
||||||
|
background_prompt: snapshot.background_prompt,
|
||||||
|
cover_image_src: empty_string_to_none(snapshot.cover_image_src),
|
||||||
|
background_image_src: empty_string_to_none(snapshot.background_image_src),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2963,6 +2969,19 @@ fn map_square_hole_result_draft(
|
|||||||
twist_rule: snapshot.twist_rule,
|
twist_rule: snapshot.twist_rule,
|
||||||
summary: snapshot.summary_text,
|
summary: snapshot.summary_text,
|
||||||
tags: snapshot.tags,
|
tags: snapshot.tags,
|
||||||
|
cover_image_src: empty_string_to_none(snapshot.cover_image_src),
|
||||||
|
background_prompt: snapshot.background_prompt,
|
||||||
|
background_image_src: empty_string_to_none(snapshot.background_image_src),
|
||||||
|
shape_options: snapshot
|
||||||
|
.shape_options
|
||||||
|
.into_iter()
|
||||||
|
.map(map_square_hole_shape_option)
|
||||||
|
.collect(),
|
||||||
|
hole_options: snapshot
|
||||||
|
.hole_options
|
||||||
|
.into_iter()
|
||||||
|
.map(map_square_hole_hole_option)
|
||||||
|
.collect(),
|
||||||
shape_count: snapshot.shape_count,
|
shape_count: snapshot.shape_count,
|
||||||
difficulty: snapshot.difficulty,
|
difficulty: snapshot.difficulty,
|
||||||
publish_ready: false,
|
publish_ready: false,
|
||||||
@@ -2997,6 +3016,18 @@ fn map_square_hole_work_snapshot(
|
|||||||
summary: snapshot.summary_text,
|
summary: snapshot.summary_text,
|
||||||
tags: snapshot.tags,
|
tags: snapshot.tags,
|
||||||
cover_image_src: empty_string_to_none(snapshot.cover_image_src),
|
cover_image_src: empty_string_to_none(snapshot.cover_image_src),
|
||||||
|
background_prompt: snapshot.background_prompt,
|
||||||
|
background_image_src: empty_string_to_none(snapshot.background_image_src),
|
||||||
|
shape_options: snapshot
|
||||||
|
.shape_options
|
||||||
|
.into_iter()
|
||||||
|
.map(map_square_hole_shape_option)
|
||||||
|
.collect(),
|
||||||
|
hole_options: snapshot
|
||||||
|
.hole_options
|
||||||
|
.into_iter()
|
||||||
|
.map(map_square_hole_hole_option)
|
||||||
|
.collect(),
|
||||||
shape_count: snapshot.shape_count,
|
shape_count: snapshot.shape_count,
|
||||||
difficulty: snapshot.difficulty,
|
difficulty: snapshot.difficulty,
|
||||||
publication_status: normalize_square_hole_publication_status(&snapshot.publication_status)
|
publication_status: normalize_square_hole_publication_status(&snapshot.publication_status)
|
||||||
@@ -3032,6 +3063,7 @@ fn map_square_hole_run_snapshot(snapshot: SquareHoleRunJsonRecord) -> SquareHole
|
|||||||
best_combo: snapshot.best_combo,
|
best_combo: snapshot.best_combo,
|
||||||
score: snapshot.score,
|
score: snapshot.score,
|
||||||
rule_label: snapshot.rule_label,
|
rule_label: snapshot.rule_label,
|
||||||
|
background_image_src: empty_string_to_none(snapshot.background_image_src),
|
||||||
current_shape: snapshot.current_shape.map(map_square_hole_shape_snapshot),
|
current_shape: snapshot.current_shape.map(map_square_hole_shape_snapshot),
|
||||||
holes: snapshot
|
holes: snapshot
|
||||||
.holes
|
.holes
|
||||||
@@ -3053,6 +3085,7 @@ fn map_square_hole_shape_snapshot(
|
|||||||
shape_kind: snapshot.shape_kind,
|
shape_kind: snapshot.shape_kind,
|
||||||
label: snapshot.label,
|
label: snapshot.label,
|
||||||
color: snapshot.color,
|
color: snapshot.color,
|
||||||
|
image_src: empty_string_to_none(snapshot.image_src),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3065,6 +3098,30 @@ fn map_square_hole_hole_snapshot(
|
|||||||
label: snapshot.label,
|
label: snapshot.label,
|
||||||
x: snapshot.x,
|
x: snapshot.x,
|
||||||
y: snapshot.y,
|
y: snapshot.y,
|
||||||
|
bonus: snapshot.bonus,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_square_hole_shape_option(
|
||||||
|
snapshot: SquareHoleShapeOptionJsonRecord,
|
||||||
|
) -> SquareHoleShapeOptionRecord {
|
||||||
|
SquareHoleShapeOptionRecord {
|
||||||
|
option_id: snapshot.option_id,
|
||||||
|
shape_kind: snapshot.shape_kind,
|
||||||
|
label: snapshot.label,
|
||||||
|
image_prompt: snapshot.image_prompt,
|
||||||
|
image_src: empty_string_to_none(snapshot.image_src),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_square_hole_hole_option(
|
||||||
|
snapshot: SquareHoleHoleOptionJsonRecord,
|
||||||
|
) -> SquareHoleHoleOptionRecord {
|
||||||
|
SquareHoleHoleOptionRecord {
|
||||||
|
hole_id: snapshot.hole_id,
|
||||||
|
hole_kind: snapshot.hole_kind,
|
||||||
|
label: snapshot.label,
|
||||||
|
bonus: snapshot.bonus,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3087,7 +3144,11 @@ fn build_square_hole_anchor_pack(
|
|||||||
let difficulty = config.difficulty.to_string();
|
let difficulty = config.difficulty.to_string();
|
||||||
SquareHoleAnchorPackRecord {
|
SquareHoleAnchorPackRecord {
|
||||||
theme: build_square_hole_anchor_item("theme", "题材主题", config.theme_text.as_str()),
|
theme: build_square_hole_anchor_item("theme", "题材主题", config.theme_text.as_str()),
|
||||||
twist_rule: build_square_hole_anchor_item("twistRule", "反差规则", config.twist_rule.as_str()),
|
twist_rule: build_square_hole_anchor_item(
|
||||||
|
"twistRule",
|
||||||
|
"反差规则",
|
||||||
|
config.twist_rule.as_str(),
|
||||||
|
),
|
||||||
shape_count: build_square_hole_anchor_item("shapeCount", "形状数量", shape_count.as_str()),
|
shape_count: build_square_hole_anchor_item("shapeCount", "形状数量", shape_count.as_str()),
|
||||||
difficulty: build_square_hole_anchor_item("difficulty", "难度", difficulty.as_str()),
|
difficulty: build_square_hole_anchor_item("difficulty", "难度", difficulty.as_str()),
|
||||||
}
|
}
|
||||||
@@ -5992,6 +6053,10 @@ pub struct SquareHoleWorkUpdateRecordInput {
|
|||||||
pub summary_text: String,
|
pub summary_text: String,
|
||||||
pub tags_json: String,
|
pub tags_json: String,
|
||||||
pub cover_image_src: String,
|
pub cover_image_src: String,
|
||||||
|
pub background_prompt: String,
|
||||||
|
pub background_image_src: String,
|
||||||
|
pub shape_options_json: String,
|
||||||
|
pub hole_options_json: String,
|
||||||
pub shape_count: u32,
|
pub shape_count: u32,
|
||||||
pub difficulty: u32,
|
pub difficulty: u32,
|
||||||
pub updated_at_micros: i64,
|
pub updated_at_micros: i64,
|
||||||
@@ -6059,6 +6124,28 @@ pub struct SquareHoleCreatorConfigRecord {
|
|||||||
pub twist_rule: String,
|
pub twist_rule: String,
|
||||||
pub shape_count: u32,
|
pub shape_count: u32,
|
||||||
pub difficulty: u32,
|
pub difficulty: u32,
|
||||||
|
pub shape_options: Vec<SquareHoleShapeOptionRecord>,
|
||||||
|
pub hole_options: Vec<SquareHoleHoleOptionRecord>,
|
||||||
|
pub background_prompt: String,
|
||||||
|
pub cover_image_src: Option<String>,
|
||||||
|
pub background_image_src: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct SquareHoleShapeOptionRecord {
|
||||||
|
pub option_id: String,
|
||||||
|
pub shape_kind: String,
|
||||||
|
pub label: String,
|
||||||
|
pub image_prompt: String,
|
||||||
|
pub image_src: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct SquareHoleHoleOptionRecord {
|
||||||
|
pub hole_id: String,
|
||||||
|
pub hole_kind: String,
|
||||||
|
pub label: String,
|
||||||
|
pub bonus: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
@@ -6069,6 +6156,11 @@ pub struct SquareHoleResultDraftRecord {
|
|||||||
pub twist_rule: String,
|
pub twist_rule: String,
|
||||||
pub summary: String,
|
pub summary: String,
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
|
pub cover_image_src: Option<String>,
|
||||||
|
pub background_prompt: String,
|
||||||
|
pub background_image_src: Option<String>,
|
||||||
|
pub shape_options: Vec<SquareHoleShapeOptionRecord>,
|
||||||
|
pub hole_options: Vec<SquareHoleHoleOptionRecord>,
|
||||||
pub shape_count: u32,
|
pub shape_count: u32,
|
||||||
pub difficulty: u32,
|
pub difficulty: u32,
|
||||||
pub publish_ready: bool,
|
pub publish_ready: bool,
|
||||||
@@ -6112,6 +6204,10 @@ pub struct SquareHoleWorkProfileRecord {
|
|||||||
pub summary: String,
|
pub summary: String,
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
pub cover_image_src: Option<String>,
|
pub cover_image_src: Option<String>,
|
||||||
|
pub background_prompt: String,
|
||||||
|
pub background_image_src: Option<String>,
|
||||||
|
pub shape_options: Vec<SquareHoleShapeOptionRecord>,
|
||||||
|
pub hole_options: Vec<SquareHoleHoleOptionRecord>,
|
||||||
pub shape_count: u32,
|
pub shape_count: u32,
|
||||||
pub difficulty: u32,
|
pub difficulty: u32,
|
||||||
pub publication_status: String,
|
pub publication_status: String,
|
||||||
@@ -6127,6 +6223,7 @@ pub struct SquareHoleShapeSnapshotRecord {
|
|||||||
pub shape_kind: String,
|
pub shape_kind: String,
|
||||||
pub label: String,
|
pub label: String,
|
||||||
pub color: String,
|
pub color: String,
|
||||||
|
pub image_src: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
@@ -6136,6 +6233,7 @@ pub struct SquareHoleHoleSnapshotRecord {
|
|||||||
pub label: String,
|
pub label: String,
|
||||||
pub x: f32,
|
pub x: f32,
|
||||||
pub y: f32,
|
pub y: f32,
|
||||||
|
pub bonus: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
@@ -6162,6 +6260,7 @@ pub struct SquareHoleRunRecord {
|
|||||||
pub best_combo: u32,
|
pub best_combo: u32,
|
||||||
pub score: u32,
|
pub score: u32,
|
||||||
pub rule_label: String,
|
pub rule_label: String,
|
||||||
|
pub background_image_src: Option<String>,
|
||||||
pub current_shape: Option<SquareHoleShapeSnapshotRecord>,
|
pub current_shape: Option<SquareHoleShapeSnapshotRecord>,
|
||||||
pub holes: Vec<SquareHoleHoleSnapshotRecord>,
|
pub holes: Vec<SquareHoleHoleSnapshotRecord>,
|
||||||
pub last_feedback: Option<SquareHoleDropFeedbackRecord>,
|
pub last_feedback: Option<SquareHoleDropFeedbackRecord>,
|
||||||
@@ -6185,6 +6284,37 @@ struct SquareHoleCreatorConfigJsonRecord {
|
|||||||
twist_rule: String,
|
twist_rule: String,
|
||||||
shape_count: u32,
|
shape_count: u32,
|
||||||
difficulty: u32,
|
difficulty: u32,
|
||||||
|
#[serde(default)]
|
||||||
|
shape_options: Vec<SquareHoleShapeOptionJsonRecord>,
|
||||||
|
#[serde(default)]
|
||||||
|
hole_options: Vec<SquareHoleHoleOptionJsonRecord>,
|
||||||
|
#[serde(default)]
|
||||||
|
background_prompt: String,
|
||||||
|
#[serde(default)]
|
||||||
|
cover_image_src: String,
|
||||||
|
#[serde(default)]
|
||||||
|
background_image_src: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct SquareHoleShapeOptionJsonRecord {
|
||||||
|
option_id: String,
|
||||||
|
shape_kind: String,
|
||||||
|
label: String,
|
||||||
|
image_prompt: String,
|
||||||
|
#[serde(default)]
|
||||||
|
image_src: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct SquareHoleHoleOptionJsonRecord {
|
||||||
|
hole_id: String,
|
||||||
|
hole_kind: String,
|
||||||
|
label: String,
|
||||||
|
#[serde(default)]
|
||||||
|
bonus: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
|
||||||
@@ -6208,6 +6338,16 @@ struct SquareHoleDraftJsonRecord {
|
|||||||
twist_rule: String,
|
twist_rule: String,
|
||||||
summary_text: String,
|
summary_text: String,
|
||||||
tags: Vec<String>,
|
tags: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
cover_image_src: String,
|
||||||
|
#[serde(default)]
|
||||||
|
background_prompt: String,
|
||||||
|
#[serde(default)]
|
||||||
|
background_image_src: String,
|
||||||
|
#[serde(default)]
|
||||||
|
shape_options: Vec<SquareHoleShapeOptionJsonRecord>,
|
||||||
|
#[serde(default)]
|
||||||
|
hole_options: Vec<SquareHoleHoleOptionJsonRecord>,
|
||||||
shape_count: u32,
|
shape_count: u32,
|
||||||
difficulty: u32,
|
difficulty: u32,
|
||||||
}
|
}
|
||||||
@@ -6247,6 +6387,14 @@ struct SquareHoleWorkJsonRecord {
|
|||||||
summary_text: String,
|
summary_text: String,
|
||||||
tags: Vec<String>,
|
tags: Vec<String>,
|
||||||
cover_image_src: String,
|
cover_image_src: String,
|
||||||
|
#[serde(default)]
|
||||||
|
background_prompt: String,
|
||||||
|
#[serde(default)]
|
||||||
|
background_image_src: String,
|
||||||
|
#[serde(default)]
|
||||||
|
shape_options: Vec<SquareHoleShapeOptionJsonRecord>,
|
||||||
|
#[serde(default)]
|
||||||
|
hole_options: Vec<SquareHoleHoleOptionJsonRecord>,
|
||||||
shape_count: u32,
|
shape_count: u32,
|
||||||
difficulty: u32,
|
difficulty: u32,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@@ -6265,6 +6413,8 @@ struct SquareHoleShapeJsonRecord {
|
|||||||
shape_kind: String,
|
shape_kind: String,
|
||||||
label: String,
|
label: String,
|
||||||
color: String,
|
color: String,
|
||||||
|
#[serde(default)]
|
||||||
|
image_src: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize)]
|
#[derive(Clone, Debug, PartialEq, serde::Deserialize)]
|
||||||
@@ -6275,6 +6425,8 @@ struct SquareHoleHoleJsonRecord {
|
|||||||
label: String,
|
label: String,
|
||||||
x: f32,
|
x: f32,
|
||||||
y: f32,
|
y: f32,
|
||||||
|
#[serde(default)]
|
||||||
|
bonus: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
|
||||||
@@ -6303,6 +6455,11 @@ struct SquareHoleRunJsonRecord {
|
|||||||
best_combo: u32,
|
best_combo: u32,
|
||||||
score: u32,
|
score: u32,
|
||||||
rule_label: String,
|
rule_label: String,
|
||||||
|
#[serde(default)]
|
||||||
|
background_image_src: String,
|
||||||
|
#[serde(default)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
shape_options: Vec<SquareHoleShapeOptionJsonRecord>,
|
||||||
current_shape: Option<SquareHoleShapeJsonRecord>,
|
current_shape: Option<SquareHoleShapeJsonRecord>,
|
||||||
holes: Vec<SquareHoleHoleJsonRecord>,
|
holes: Vec<SquareHoleHoleJsonRecord>,
|
||||||
last_feedback: Option<SquareHoleDropFeedbackJsonRecord>,
|
last_feedback: Option<SquareHoleDropFeedbackJsonRecord>,
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ pub struct SquareHoleWorkUpdateInput {
|
|||||||
pub summary_text: String,
|
pub summary_text: String,
|
||||||
pub tags_json: String,
|
pub tags_json: String,
|
||||||
pub cover_image_src: String,
|
pub cover_image_src: String,
|
||||||
|
pub background_prompt: String,
|
||||||
|
pub background_image_src: String,
|
||||||
|
pub shape_options_json: String,
|
||||||
|
pub hole_options_json: String,
|
||||||
pub shape_count: u32,
|
pub shape_count: u32,
|
||||||
pub difficulty: u32,
|
pub difficulty: u32,
|
||||||
pub updated_at_micros: i64,
|
pub updated_at_micros: i64,
|
||||||
|
|||||||
@@ -17,15 +17,14 @@ impl SpacetimeClient {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.call_after_connect(move |connection, sender| {
|
self.call_after_connect(move |connection, sender| {
|
||||||
connection.procedures().create_square_hole_agent_session_then(
|
connection
|
||||||
procedure_input,
|
.procedures()
|
||||||
move |_, result| {
|
.create_square_hole_agent_session_then(procedure_input, move |_, result| {
|
||||||
let mapped = result
|
let mapped = result
|
||||||
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
.and_then(map_square_hole_agent_session_procedure_result);
|
.and_then(map_square_hole_agent_session_procedure_result);
|
||||||
send_once(&sender, mapped);
|
send_once(&sender, mapped);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -67,15 +66,14 @@ impl SpacetimeClient {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.call_after_connect(move |connection, sender| {
|
self.call_after_connect(move |connection, sender| {
|
||||||
connection.procedures().submit_square_hole_agent_message_then(
|
connection
|
||||||
procedure_input,
|
.procedures()
|
||||||
move |_, result| {
|
.submit_square_hole_agent_message_then(procedure_input, move |_, result| {
|
||||||
let mapped = result
|
let mapped = result
|
||||||
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
.and_then(map_square_hole_agent_session_procedure_result);
|
.and_then(map_square_hole_agent_session_procedure_result);
|
||||||
send_once(&sender, mapped);
|
send_once(&sender, mapped);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -126,14 +124,15 @@ impl SpacetimeClient {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.call_after_connect(move |connection, sender| {
|
self.call_after_connect(move |connection, sender| {
|
||||||
connection
|
connection.procedures().compile_square_hole_draft_then(
|
||||||
.procedures()
|
procedure_input,
|
||||||
.compile_square_hole_draft_then(procedure_input, move |_, result| {
|
move |_, result| {
|
||||||
let mapped = result
|
let mapped = result
|
||||||
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
.and_then(map_square_hole_agent_session_procedure_result);
|
.and_then(map_square_hole_agent_session_procedure_result);
|
||||||
send_once(&sender, mapped);
|
send_once(&sender, mapped);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -151,20 +150,25 @@ impl SpacetimeClient {
|
|||||||
summary_text: input.summary_text,
|
summary_text: input.summary_text,
|
||||||
tags_json: input.tags_json,
|
tags_json: input.tags_json,
|
||||||
cover_image_src: input.cover_image_src,
|
cover_image_src: input.cover_image_src,
|
||||||
|
background_prompt: input.background_prompt,
|
||||||
|
background_image_src: input.background_image_src,
|
||||||
|
shape_options_json: input.shape_options_json,
|
||||||
|
hole_options_json: input.hole_options_json,
|
||||||
shape_count: input.shape_count,
|
shape_count: input.shape_count,
|
||||||
difficulty: input.difficulty,
|
difficulty: input.difficulty,
|
||||||
updated_at_micros: input.updated_at_micros,
|
updated_at_micros: input.updated_at_micros,
|
||||||
};
|
};
|
||||||
|
|
||||||
self.call_after_connect(move |connection, sender| {
|
self.call_after_connect(move |connection, sender| {
|
||||||
connection
|
connection.procedures().update_square_hole_work_then(
|
||||||
.procedures()
|
procedure_input,
|
||||||
.update_square_hole_work_then(procedure_input, move |_, result| {
|
move |_, result| {
|
||||||
let mapped = result
|
let mapped = result
|
||||||
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
.and_then(map_square_hole_work_procedure_result);
|
.and_then(map_square_hole_work_procedure_result);
|
||||||
send_once(&sender, mapped);
|
send_once(&sender, mapped);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -182,14 +186,15 @@ impl SpacetimeClient {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.call_after_connect(move |connection, sender| {
|
self.call_after_connect(move |connection, sender| {
|
||||||
connection
|
connection.procedures().publish_square_hole_work_then(
|
||||||
.procedures()
|
procedure_input,
|
||||||
.publish_square_hole_work_then(procedure_input, move |_, result| {
|
move |_, result| {
|
||||||
let mapped = result
|
let mapped = result
|
||||||
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
.and_then(map_square_hole_work_procedure_result);
|
.and_then(map_square_hole_work_procedure_result);
|
||||||
send_once(&sender, mapped);
|
send_once(&sender, mapped);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -221,14 +226,15 @@ impl SpacetimeClient {
|
|||||||
procedure_input: SquareHoleWorksListInput,
|
procedure_input: SquareHoleWorksListInput,
|
||||||
) -> Result<Vec<SquareHoleWorkProfileRecord>, SpacetimeClientError> {
|
) -> Result<Vec<SquareHoleWorkProfileRecord>, SpacetimeClientError> {
|
||||||
self.call_after_connect(move |connection, sender| {
|
self.call_after_connect(move |connection, sender| {
|
||||||
connection
|
connection.procedures().list_square_hole_works_then(
|
||||||
.procedures()
|
procedure_input,
|
||||||
.list_square_hole_works_then(procedure_input, move |_, result| {
|
move |_, result| {
|
||||||
let mapped = result
|
let mapped = result
|
||||||
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
.and_then(map_square_hole_works_procedure_result);
|
.and_then(map_square_hole_works_procedure_result);
|
||||||
send_once(&sender, mapped);
|
send_once(&sender, mapped);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -244,14 +250,15 @@ impl SpacetimeClient {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.call_after_connect(move |connection, sender| {
|
self.call_after_connect(move |connection, sender| {
|
||||||
connection
|
connection.procedures().get_square_hole_work_detail_then(
|
||||||
.procedures()
|
procedure_input,
|
||||||
.get_square_hole_work_detail_then(procedure_input, move |_, result| {
|
move |_, result| {
|
||||||
let mapped = result
|
let mapped = result
|
||||||
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
.and_then(map_square_hole_work_procedure_result);
|
.and_then(map_square_hole_work_procedure_result);
|
||||||
send_once(&sender, mapped);
|
send_once(&sender, mapped);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -267,14 +274,15 @@ impl SpacetimeClient {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.call_after_connect(move |connection, sender| {
|
self.call_after_connect(move |connection, sender| {
|
||||||
connection
|
connection.procedures().delete_square_hole_work_then(
|
||||||
.procedures()
|
procedure_input,
|
||||||
.delete_square_hole_work_then(procedure_input, move |_, result| {
|
move |_, result| {
|
||||||
let mapped = result
|
let mapped = result
|
||||||
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
.and_then(map_square_hole_works_procedure_result);
|
.and_then(map_square_hole_works_procedure_result);
|
||||||
send_once(&sender, mapped);
|
send_once(&sender, mapped);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -291,14 +299,15 @@ impl SpacetimeClient {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.call_after_connect(move |connection, sender| {
|
self.call_after_connect(move |connection, sender| {
|
||||||
connection
|
connection.procedures().start_square_hole_run_then(
|
||||||
.procedures()
|
procedure_input,
|
||||||
.start_square_hole_run_then(procedure_input, move |_, result| {
|
move |_, result| {
|
||||||
let mapped = result
|
let mapped = result
|
||||||
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
.and_then(map_square_hole_run_procedure_result);
|
.and_then(map_square_hole_run_procedure_result);
|
||||||
send_once(&sender, mapped);
|
send_once(&sender, mapped);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -341,21 +350,21 @@ impl SpacetimeClient {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.call_after_connect(move |connection, sender| {
|
self.call_after_connect(move |connection, sender| {
|
||||||
connection
|
connection.procedures().drop_square_hole_shape_then(
|
||||||
.procedures()
|
procedure_input,
|
||||||
.drop_square_hole_shape_then(procedure_input, move |_, result| {
|
move |_, result| {
|
||||||
let mapped = result
|
let mapped = result
|
||||||
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
.and_then(map_square_hole_drop_shape_procedure_result)
|
.and_then(map_square_hole_drop_shape_procedure_result)
|
||||||
.map(|mut confirmation| {
|
.map(|mut confirmation| {
|
||||||
if confirmation.accepted {
|
if confirmation.accepted {
|
||||||
confirmation.run.last_confirmed_action_id =
|
confirmation.run.last_confirmed_action_id = Some(client_event_id);
|
||||||
Some(client_event_id);
|
|
||||||
}
|
}
|
||||||
confirmation
|
confirmation
|
||||||
});
|
});
|
||||||
send_once(&sender, mapped);
|
send_once(&sender, mapped);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -395,14 +404,15 @@ impl SpacetimeClient {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.call_after_connect(move |connection, sender| {
|
self.call_after_connect(move |connection, sender| {
|
||||||
connection
|
connection.procedures().restart_square_hole_run_then(
|
||||||
.procedures()
|
procedure_input,
|
||||||
.restart_square_hole_run_then(procedure_input, move |_, result| {
|
move |_, result| {
|
||||||
let mapped = result
|
let mapped = result
|
||||||
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
.and_then(map_square_hole_run_procedure_result);
|
.and_then(map_square_hole_run_procedure_result);
|
||||||
send_once(&sender, mapped);
|
send_once(&sender, mapped);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -418,14 +428,15 @@ impl SpacetimeClient {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.call_after_connect(move |connection, sender| {
|
self.call_after_connect(move |connection, sender| {
|
||||||
connection
|
connection.procedures().finish_square_hole_time_up_then(
|
||||||
.procedures()
|
procedure_input,
|
||||||
.finish_square_hole_time_up_then(procedure_input, move |_, result| {
|
move |_, result| {
|
||||||
let mapped = result
|
let mapped = result
|
||||||
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
.and_then(map_square_hole_run_procedure_result);
|
.and_then(map_square_hole_run_procedure_result);
|
||||||
send_once(&sender, mapped);
|
send_once(&sender, mapped);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,17 @@ use module_square_hole::{
|
|||||||
SquareHoleDropFeedback as DomainSquareHoleDropFeedback,
|
SquareHoleDropFeedback as DomainSquareHoleDropFeedback,
|
||||||
SquareHoleDropInput as DomainSquareHoleDropInput,
|
SquareHoleDropInput as DomainSquareHoleDropInput,
|
||||||
SquareHoleDropRejectReason as DomainSquareHoleDropRejectReason,
|
SquareHoleDropRejectReason as DomainSquareHoleDropRejectReason,
|
||||||
|
SquareHoleHoleOption as DomainSquareHoleHoleOption,
|
||||||
SquareHoleHoleSnapshot as DomainSquareHoleHoleSnapshot,
|
SquareHoleHoleSnapshot as DomainSquareHoleHoleSnapshot,
|
||||||
SquareHoleRunSnapshot as DomainSquareHoleRunSnapshot,
|
SquareHoleRunSnapshot as DomainSquareHoleRunSnapshot,
|
||||||
SquareHoleRunStatus as DomainSquareHoleRunStatus,
|
SquareHoleRunStatus as DomainSquareHoleRunStatus,
|
||||||
|
SquareHoleShapeOption as DomainSquareHoleShapeOption,
|
||||||
SquareHoleShapeSnapshot as DomainSquareHoleShapeSnapshot,
|
SquareHoleShapeSnapshot as DomainSquareHoleShapeSnapshot,
|
||||||
build_creator_config as build_domain_creator_config,
|
build_creator_config as build_domain_creator_config,
|
||||||
compile_result_draft as compile_domain_result_draft, confirm_drop_at as confirm_domain_drop_at,
|
compile_result_draft as compile_domain_result_draft, confirm_drop_at as confirm_domain_drop_at,
|
||||||
|
default_background_prompt as default_domain_background_prompt,
|
||||||
|
normalize_hole_options as normalize_domain_hole_options,
|
||||||
|
normalize_shape_options as normalize_domain_shape_options,
|
||||||
resolve_run_timer_at as resolve_domain_run_timer_at, start_run_at as start_domain_run_at,
|
resolve_run_timer_at as resolve_domain_run_timer_at, start_run_at as start_domain_run_at,
|
||||||
stop_run_at as stop_domain_run_at,
|
stop_run_at as stop_domain_run_at,
|
||||||
};
|
};
|
||||||
@@ -432,6 +437,9 @@ fn compile_square_hole_draft_tx(
|
|||||||
domain_draft.blockers = module_square_hole::validate_publish_requirements(&domain_draft);
|
domain_draft.blockers = module_square_hole::validate_publish_requirements(&domain_draft);
|
||||||
domain_draft.publish_ready = domain_draft.blockers.is_empty();
|
domain_draft.publish_ready = domain_draft.blockers.is_empty();
|
||||||
|
|
||||||
|
let cover_image_src = clean_optional(&input.cover_image_src)
|
||||||
|
.or_else(|| clean_optional(&Some(config.cover_image_src.clone())))
|
||||||
|
.unwrap_or_default();
|
||||||
let draft = SquareHoleDraftSnapshot {
|
let draft = SquareHoleDraftSnapshot {
|
||||||
profile_id: input.profile_id.clone(),
|
profile_id: input.profile_id.clone(),
|
||||||
game_name: domain_draft.game_name.clone(),
|
game_name: domain_draft.game_name.clone(),
|
||||||
@@ -439,6 +447,14 @@ fn compile_square_hole_draft_tx(
|
|||||||
twist_rule: domain_draft.twist_rule.clone(),
|
twist_rule: domain_draft.twist_rule.clone(),
|
||||||
summary_text: domain_draft.summary.clone(),
|
summary_text: domain_draft.summary.clone(),
|
||||||
tags: domain_draft.tags.clone(),
|
tags: domain_draft.tags.clone(),
|
||||||
|
cover_image_src: cover_image_src.clone(),
|
||||||
|
background_prompt: domain_draft.background_prompt.clone(),
|
||||||
|
background_image_src: domain_draft
|
||||||
|
.background_image_src
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_default(),
|
||||||
|
shape_options: shape_options_to_snapshot(&domain_draft.shape_options),
|
||||||
|
hole_options: hole_options_to_snapshot(&domain_draft.hole_options),
|
||||||
shape_count: domain_draft.shape_count,
|
shape_count: domain_draft.shape_count,
|
||||||
difficulty: domain_draft.difficulty,
|
difficulty: domain_draft.difficulty,
|
||||||
};
|
};
|
||||||
@@ -454,7 +470,7 @@ fn compile_square_hole_draft_tx(
|
|||||||
twist_rule: config.twist_rule.clone(),
|
twist_rule: config.twist_rule.clone(),
|
||||||
summary_text: draft.summary_text.clone(),
|
summary_text: draft.summary_text.clone(),
|
||||||
tags_json: to_json_string(&draft.tags),
|
tags_json: to_json_string(&draft.tags),
|
||||||
cover_image_src: clean_optional(&input.cover_image_src).unwrap_or_default(),
|
cover_image_src,
|
||||||
shape_count: config.shape_count,
|
shape_count: config.shape_count,
|
||||||
difficulty: config.difficulty,
|
difficulty: config.difficulty,
|
||||||
config_json: to_json_string(&config),
|
config_json: to_json_string(&config),
|
||||||
@@ -493,12 +509,31 @@ fn update_square_hole_work_tx(
|
|||||||
) -> Result<SquareHoleWorkSnapshot, String> {
|
) -> Result<SquareHoleWorkSnapshot, String> {
|
||||||
let current = find_owned_work(ctx, &input.profile_id, &input.owner_user_id)?;
|
let current = find_owned_work(ctx, &input.profile_id, &input.owner_user_id)?;
|
||||||
let tags = parse_tags(&input.tags_json)?;
|
let tags = parse_tags(&input.tags_json)?;
|
||||||
let config = SquareHoleCreatorConfigSnapshot {
|
let current_config = parse_config(¤t.config_json)?;
|
||||||
|
let shape_options = parse_optional_json::<Vec<SquareHoleShapeOptionSnapshot>>(
|
||||||
|
input.shape_options_json.as_str(),
|
||||||
|
"square_hole shape_options_json",
|
||||||
|
)?
|
||||||
|
.unwrap_or_else(|| current_config.shape_options.clone());
|
||||||
|
let hole_options = parse_optional_json::<Vec<SquareHoleHoleOptionSnapshot>>(
|
||||||
|
input.hole_options_json.as_str(),
|
||||||
|
"square_hole hole_options_json",
|
||||||
|
)?
|
||||||
|
.unwrap_or_else(|| current_config.hole_options.clone());
|
||||||
|
let config = normalize_config(SquareHoleCreatorConfigSnapshot {
|
||||||
theme_text: clean_string(&input.theme_text, "玩具"),
|
theme_text: clean_string(&input.theme_text, "玩具"),
|
||||||
twist_rule: clean_string(&input.twist_rule, "方洞万能"),
|
twist_rule: clean_string(&input.twist_rule, "方洞万能"),
|
||||||
shape_count: input.shape_count,
|
shape_count: input.shape_count,
|
||||||
difficulty: input.difficulty,
|
difficulty: input.difficulty,
|
||||||
};
|
shape_options,
|
||||||
|
hole_options,
|
||||||
|
background_prompt: clean_string(
|
||||||
|
&input.background_prompt,
|
||||||
|
&default_domain_background_prompt(&input.theme_text),
|
||||||
|
),
|
||||||
|
cover_image_src: input.cover_image_src.trim().to_string(),
|
||||||
|
background_image_src: input.background_image_src.trim().to_string(),
|
||||||
|
});
|
||||||
validate_config(&config)?;
|
validate_config(&config)?;
|
||||||
let updated_at = Timestamp::from_micros_since_unix_epoch(input.updated_at_micros);
|
let updated_at = Timestamp::from_micros_since_unix_epoch(input.updated_at_micros);
|
||||||
let next = SquareHoleWorkProfileRow {
|
let next = SquareHoleWorkProfileRow {
|
||||||
@@ -828,6 +863,10 @@ fn build_work_snapshot(row: &SquareHoleWorkProfileRow) -> Result<SquareHoleWorkS
|
|||||||
summary_text: row.summary_text.clone(),
|
summary_text: row.summary_text.clone(),
|
||||||
tags: parse_tags(&row.tags_json)?,
|
tags: parse_tags(&row.tags_json)?,
|
||||||
cover_image_src: row.cover_image_src.clone(),
|
cover_image_src: row.cover_image_src.clone(),
|
||||||
|
background_prompt: config.background_prompt.clone(),
|
||||||
|
background_image_src: config.background_image_src.clone(),
|
||||||
|
shape_options: config.shape_options.clone(),
|
||||||
|
hole_options: config.hole_options.clone(),
|
||||||
shape_count: row.shape_count,
|
shape_count: row.shape_count,
|
||||||
difficulty: row.difficulty,
|
difficulty: row.difficulty,
|
||||||
config,
|
config,
|
||||||
@@ -1040,12 +1079,17 @@ fn is_work_publish_ready(row: &SquareHoleWorkProfileRow) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn default_config_from_seed(seed_text: &str) -> SquareHoleCreatorConfigSnapshot {
|
fn default_config_from_seed(seed_text: &str) -> SquareHoleCreatorConfigSnapshot {
|
||||||
SquareHoleCreatorConfigSnapshot {
|
normalize_config(SquareHoleCreatorConfigSnapshot {
|
||||||
theme_text: clean_string(seed_text, "玩具"),
|
theme_text: clean_string(seed_text, "玩具"),
|
||||||
twist_rule: "方洞万能".to_string(),
|
twist_rule: "方洞万能".to_string(),
|
||||||
shape_count: 12,
|
shape_count: 12,
|
||||||
difficulty: 4,
|
difficulty: 4,
|
||||||
}
|
shape_options: Vec::new(),
|
||||||
|
hole_options: Vec::new(),
|
||||||
|
background_prompt: String::new(),
|
||||||
|
cover_image_src: String::new(),
|
||||||
|
background_image_src: String::new(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_config_or_default(value: &str) -> SquareHoleCreatorConfigSnapshot {
|
fn parse_config_or_default(value: &str) -> SquareHoleCreatorConfigSnapshot {
|
||||||
@@ -1053,17 +1097,34 @@ fn parse_config_or_default(value: &str) -> SquareHoleCreatorConfigSnapshot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_config(value: &str) -> Result<SquareHoleCreatorConfigSnapshot, String> {
|
fn parse_config(value: &str) -> Result<SquareHoleCreatorConfigSnapshot, String> {
|
||||||
parse_json(value, "square_hole config_json").map(
|
parse_json(value, "square_hole config_json").map(normalize_config)
|
||||||
|mut config: SquareHoleCreatorConfigSnapshot| {
|
}
|
||||||
config.theme_text = clean_string(&config.theme_text, "玩具");
|
|
||||||
config.twist_rule = clean_string(&config.twist_rule, "方洞万能");
|
fn normalize_config(
|
||||||
config.difficulty = config.difficulty.clamp(
|
mut config: SquareHoleCreatorConfigSnapshot,
|
||||||
module_square_hole::SQUARE_HOLE_MIN_DIFFICULTY,
|
) -> SquareHoleCreatorConfigSnapshot {
|
||||||
module_square_hole::SQUARE_HOLE_MAX_DIFFICULTY,
|
config.theme_text = clean_string(&config.theme_text, "玩具");
|
||||||
);
|
config.twist_rule = clean_string(&config.twist_rule, "方洞万能");
|
||||||
config
|
config.difficulty = config.difficulty.clamp(
|
||||||
},
|
module_square_hole::SQUARE_HOLE_MIN_DIFFICULTY,
|
||||||
)
|
module_square_hole::SQUARE_HOLE_MAX_DIFFICULTY,
|
||||||
|
);
|
||||||
|
config.background_prompt = clean_string(
|
||||||
|
&config.background_prompt,
|
||||||
|
&default_domain_background_prompt(&config.theme_text),
|
||||||
|
);
|
||||||
|
config.cover_image_src = config.cover_image_src.trim().to_string();
|
||||||
|
config.background_image_src = config.background_image_src.trim().to_string();
|
||||||
|
|
||||||
|
let shape_options = normalize_domain_shape_options(
|
||||||
|
domain_shape_options_from_snapshot(&config.shape_options),
|
||||||
|
&config.theme_text,
|
||||||
|
);
|
||||||
|
let hole_options =
|
||||||
|
normalize_domain_hole_options(domain_hole_options_from_snapshot(&config.hole_options));
|
||||||
|
config.shape_options = shape_options_to_snapshot(&shape_options);
|
||||||
|
config.hole_options = hole_options_to_snapshot(&hole_options);
|
||||||
|
config
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_tags(value: &str) -> Result<Vec<String>, String> {
|
fn parse_tags(value: &str) -> Result<Vec<String>, String> {
|
||||||
@@ -1071,6 +1132,13 @@ fn parse_tags(value: &str) -> Result<Vec<String>, String> {
|
|||||||
Ok(normalize_tags(parsed))
|
Ok(normalize_tags(parsed))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_optional_json<T: DeserializeOwned>(value: &str, label: &str) -> Result<Option<T>, String> {
|
||||||
|
if value.trim().is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
parse_json(value, label).map(Some)
|
||||||
|
}
|
||||||
|
|
||||||
fn normalize_tags(tags: Vec<String>) -> Vec<String> {
|
fn normalize_tags(tags: Vec<String>) -> Vec<String> {
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
for tag in tags {
|
for tag in tags {
|
||||||
@@ -1097,12 +1165,18 @@ fn normalize_stage(value: &str) -> String {
|
|||||||
fn domain_config_from_snapshot(
|
fn domain_config_from_snapshot(
|
||||||
config: &SquareHoleCreatorConfigSnapshot,
|
config: &SquareHoleCreatorConfigSnapshot,
|
||||||
) -> Result<DomainSquareHoleCreatorConfig, module_square_hole::SquareHoleError> {
|
) -> Result<DomainSquareHoleCreatorConfig, module_square_hole::SquareHoleError> {
|
||||||
build_domain_creator_config(
|
let mut domain = build_domain_creator_config(
|
||||||
&config.theme_text,
|
&config.theme_text,
|
||||||
&config.twist_rule,
|
&config.twist_rule,
|
||||||
config.shape_count,
|
config.shape_count,
|
||||||
config.difficulty,
|
config.difficulty,
|
||||||
)
|
)?;
|
||||||
|
domain.shape_options = domain_shape_options_from_snapshot(&config.shape_options);
|
||||||
|
domain.hole_options = domain_hole_options_from_snapshot(&config.hole_options);
|
||||||
|
domain.background_prompt = config.background_prompt.clone();
|
||||||
|
domain.cover_image_src = empty_to_none(&config.cover_image_src);
|
||||||
|
domain.background_image_src = empty_to_none(&config.background_image_src);
|
||||||
|
Ok(domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn snapshot_from_domain(
|
fn snapshot_from_domain(
|
||||||
@@ -1125,6 +1199,8 @@ fn snapshot_from_domain(
|
|||||||
best_combo: run.best_combo,
|
best_combo: run.best_combo,
|
||||||
score: run.score,
|
score: run.score,
|
||||||
rule_label: run.rule_label.clone(),
|
rule_label: run.rule_label.clone(),
|
||||||
|
background_image_src: run.background_image_src.clone().unwrap_or_default(),
|
||||||
|
shape_options: shape_options_to_snapshot(&run.shape_options),
|
||||||
current_shape: run.current_shape.as_ref().map(shape_from_domain),
|
current_shape: run.current_shape.as_ref().map(shape_from_domain),
|
||||||
holes: run.holes.iter().map(hole_from_domain).collect(),
|
holes: run.holes.iter().map(hole_from_domain).collect(),
|
||||||
last_feedback: run.last_feedback.as_ref().map(feedback_from_domain),
|
last_feedback: run.last_feedback.as_ref().map(feedback_from_domain),
|
||||||
@@ -1150,6 +1226,8 @@ fn domain_snapshot_from_snapshot(
|
|||||||
best_combo: snapshot.best_combo,
|
best_combo: snapshot.best_combo,
|
||||||
score: snapshot.score,
|
score: snapshot.score,
|
||||||
rule_label: snapshot.rule_label.clone(),
|
rule_label: snapshot.rule_label.clone(),
|
||||||
|
background_image_src: empty_to_none(&snapshot.background_image_src),
|
||||||
|
shape_options: domain_shape_options_from_snapshot(&snapshot.shape_options),
|
||||||
current_shape: snapshot
|
current_shape: snapshot
|
||||||
.current_shape
|
.current_shape
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -1172,6 +1250,7 @@ fn shape_from_domain(shape: &DomainSquareHoleShapeSnapshot) -> SquareHoleShapeSn
|
|||||||
shape_kind: shape.shape_kind.clone(),
|
shape_kind: shape.shape_kind.clone(),
|
||||||
label: shape.label.clone(),
|
label: shape.label.clone(),
|
||||||
color: shape.color.clone(),
|
color: shape.color.clone(),
|
||||||
|
image_src: shape.image_src.clone().unwrap_or_default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1181,6 +1260,7 @@ fn domain_shape_from_snapshot(shape: &SquareHoleShapeSnapshot) -> DomainSquareHo
|
|||||||
shape_kind: shape.shape_kind.clone(),
|
shape_kind: shape.shape_kind.clone(),
|
||||||
label: shape.label.clone(),
|
label: shape.label.clone(),
|
||||||
color: shape.color.clone(),
|
color: shape.color.clone(),
|
||||||
|
image_src: empty_to_none(&shape.image_src),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1191,6 +1271,7 @@ fn hole_from_domain(hole: &DomainSquareHoleHoleSnapshot) -> SquareHoleHoleSnapsh
|
|||||||
label: hole.label.clone(),
|
label: hole.label.clone(),
|
||||||
x: hole.x,
|
x: hole.x,
|
||||||
y: hole.y,
|
y: hole.y,
|
||||||
|
bonus: hole.bonus,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1201,9 +1282,68 @@ fn domain_hole_from_snapshot(hole: &SquareHoleHoleSnapshot) -> DomainSquareHoleH
|
|||||||
label: hole.label.clone(),
|
label: hole.label.clone(),
|
||||||
x: hole.x,
|
x: hole.x,
|
||||||
y: hole.y,
|
y: hole.y,
|
||||||
|
bonus: hole.bonus,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn shape_options_to_snapshot(
|
||||||
|
options: &[DomainSquareHoleShapeOption],
|
||||||
|
) -> Vec<SquareHoleShapeOptionSnapshot> {
|
||||||
|
options
|
||||||
|
.iter()
|
||||||
|
.map(|option| SquareHoleShapeOptionSnapshot {
|
||||||
|
option_id: option.option_id.clone(),
|
||||||
|
shape_kind: option.shape_kind.clone(),
|
||||||
|
label: option.label.clone(),
|
||||||
|
image_prompt: option.image_prompt.clone(),
|
||||||
|
image_src: option.image_src.clone().unwrap_or_default(),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn domain_shape_options_from_snapshot(
|
||||||
|
options: &[SquareHoleShapeOptionSnapshot],
|
||||||
|
) -> Vec<DomainSquareHoleShapeOption> {
|
||||||
|
options
|
||||||
|
.iter()
|
||||||
|
.map(|option| DomainSquareHoleShapeOption {
|
||||||
|
option_id: option.option_id.clone(),
|
||||||
|
shape_kind: option.shape_kind.clone(),
|
||||||
|
label: option.label.clone(),
|
||||||
|
image_prompt: option.image_prompt.clone(),
|
||||||
|
image_src: empty_to_none(&option.image_src),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hole_options_to_snapshot(
|
||||||
|
options: &[DomainSquareHoleHoleOption],
|
||||||
|
) -> Vec<SquareHoleHoleOptionSnapshot> {
|
||||||
|
options
|
||||||
|
.iter()
|
||||||
|
.map(|option| SquareHoleHoleOptionSnapshot {
|
||||||
|
hole_id: option.hole_id.clone(),
|
||||||
|
hole_kind: option.hole_kind.clone(),
|
||||||
|
label: option.label.clone(),
|
||||||
|
bonus: option.bonus,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn domain_hole_options_from_snapshot(
|
||||||
|
options: &[SquareHoleHoleOptionSnapshot],
|
||||||
|
) -> Vec<DomainSquareHoleHoleOption> {
|
||||||
|
options
|
||||||
|
.iter()
|
||||||
|
.map(|option| DomainSquareHoleHoleOption {
|
||||||
|
hole_id: option.hole_id.clone(),
|
||||||
|
hole_kind: option.hole_kind.clone(),
|
||||||
|
label: option.label.clone(),
|
||||||
|
bonus: option.bonus,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn feedback_from_domain(feedback: &DomainSquareHoleDropFeedback) -> SquareHoleDropFeedbackSnapshot {
|
fn feedback_from_domain(feedback: &DomainSquareHoleDropFeedback) -> SquareHoleDropFeedbackSnapshot {
|
||||||
SquareHoleDropFeedbackSnapshot {
|
SquareHoleDropFeedbackSnapshot {
|
||||||
accepted: feedback.accepted,
|
accepted: feedback.accepted,
|
||||||
|
|||||||
@@ -85,6 +85,10 @@ pub struct SquareHoleWorkUpdateInput {
|
|||||||
pub summary_text: String,
|
pub summary_text: String,
|
||||||
pub tags_json: String,
|
pub tags_json: String,
|
||||||
pub cover_image_src: String,
|
pub cover_image_src: String,
|
||||||
|
pub background_prompt: String,
|
||||||
|
pub background_image_src: String,
|
||||||
|
pub shape_options_json: String,
|
||||||
|
pub hole_options_json: String,
|
||||||
pub shape_count: u32,
|
pub shape_count: u32,
|
||||||
pub difficulty: u32,
|
pub difficulty: u32,
|
||||||
pub updated_at_micros: i64,
|
pub updated_at_micros: i64,
|
||||||
@@ -206,6 +210,37 @@ pub struct SquareHoleCreatorConfigSnapshot {
|
|||||||
pub twist_rule: String,
|
pub twist_rule: String,
|
||||||
pub shape_count: u32,
|
pub shape_count: u32,
|
||||||
pub difficulty: u32,
|
pub difficulty: u32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub shape_options: Vec<SquareHoleShapeOptionSnapshot>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub hole_options: Vec<SquareHoleHoleOptionSnapshot>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub background_prompt: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub cover_image_src: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub background_image_src: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SquareHoleShapeOptionSnapshot {
|
||||||
|
pub option_id: String,
|
||||||
|
pub shape_kind: String,
|
||||||
|
pub label: String,
|
||||||
|
pub image_prompt: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub image_src: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SquareHoleHoleOptionSnapshot {
|
||||||
|
pub hole_id: String,
|
||||||
|
pub hole_kind: String,
|
||||||
|
pub label: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub bonus: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
@@ -228,6 +263,16 @@ pub struct SquareHoleDraftSnapshot {
|
|||||||
pub twist_rule: String,
|
pub twist_rule: String,
|
||||||
pub summary_text: String,
|
pub summary_text: String,
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub cover_image_src: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub background_prompt: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub background_image_src: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub shape_options: Vec<SquareHoleShapeOptionSnapshot>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub hole_options: Vec<SquareHoleHoleOptionSnapshot>,
|
||||||
pub shape_count: u32,
|
pub shape_count: u32,
|
||||||
pub difficulty: u32,
|
pub difficulty: u32,
|
||||||
}
|
}
|
||||||
@@ -264,6 +309,14 @@ pub struct SquareHoleWorkSnapshot {
|
|||||||
pub summary_text: String,
|
pub summary_text: String,
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
pub cover_image_src: String,
|
pub cover_image_src: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub background_prompt: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub background_image_src: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub shape_options: Vec<SquareHoleShapeOptionSnapshot>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub hole_options: Vec<SquareHoleHoleOptionSnapshot>,
|
||||||
pub shape_count: u32,
|
pub shape_count: u32,
|
||||||
pub difficulty: u32,
|
pub difficulty: u32,
|
||||||
pub config: SquareHoleCreatorConfigSnapshot,
|
pub config: SquareHoleCreatorConfigSnapshot,
|
||||||
@@ -281,6 +334,8 @@ pub struct SquareHoleShapeSnapshot {
|
|||||||
pub shape_kind: String,
|
pub shape_kind: String,
|
||||||
pub label: String,
|
pub label: String,
|
||||||
pub color: String,
|
pub color: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub image_src: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
@@ -291,6 +346,8 @@ pub struct SquareHoleHoleSnapshot {
|
|||||||
pub label: String,
|
pub label: String,
|
||||||
pub x: f32,
|
pub x: f32,
|
||||||
pub y: f32,
|
pub y: f32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub bonus: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
@@ -319,6 +376,10 @@ pub struct SquareHoleRunSnapshot {
|
|||||||
pub best_combo: u32,
|
pub best_combo: u32,
|
||||||
pub score: u32,
|
pub score: u32,
|
||||||
pub rule_label: String,
|
pub rule_label: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub background_image_src: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub shape_options: Vec<SquareHoleShapeOptionSnapshot>,
|
||||||
pub current_shape: Option<SquareHoleShapeSnapshot>,
|
pub current_shape: Option<SquareHoleShapeSnapshot>,
|
||||||
pub holes: Vec<SquareHoleHoleSnapshot>,
|
pub holes: Vec<SquareHoleHoleSnapshot>,
|
||||||
pub last_feedback: Option<SquareHoleDropFeedbackSnapshot>,
|
pub last_feedback: Option<SquareHoleDropFeedbackSnapshot>,
|
||||||
|
|||||||
@@ -32,22 +32,6 @@ import type {
|
|||||||
Match3DWorkProfile,
|
Match3DWorkProfile,
|
||||||
Match3DWorkSummary,
|
Match3DWorkSummary,
|
||||||
} from '../../../packages/shared/src/contracts/match3dWorks';
|
} from '../../../packages/shared/src/contracts/match3dWorks';
|
||||||
import type {
|
|
||||||
CreateSquareHoleSessionRequest,
|
|
||||||
ExecuteSquareHoleActionRequest,
|
|
||||||
SendSquareHoleMessageRequest,
|
|
||||||
SquareHoleActionResponse,
|
|
||||||
SquareHoleSessionResponse,
|
|
||||||
SquareHoleSessionSnapshot,
|
|
||||||
} from '../../../packages/shared/src/contracts/squareHoleAgent';
|
|
||||||
import type {
|
|
||||||
DropSquareHoleShapeRequest,
|
|
||||||
SquareHoleRunSnapshot,
|
|
||||||
} from '../../../packages/shared/src/contracts/squareHoleRuntime';
|
|
||||||
import type {
|
|
||||||
SquareHoleWorkProfile,
|
|
||||||
SquareHoleWorkSummary,
|
|
||||||
} from '../../../packages/shared/src/contracts/squareHoleWorks';
|
|
||||||
import type {
|
import type {
|
||||||
PuzzleAgentActionRequest,
|
PuzzleAgentActionRequest,
|
||||||
PuzzleAgentOperationRecord,
|
PuzzleAgentOperationRecord,
|
||||||
@@ -72,6 +56,21 @@ import type {
|
|||||||
ProfileSaveArchiveResumeResponse,
|
ProfileSaveArchiveResumeResponse,
|
||||||
ProfileSaveArchiveSummary,
|
ProfileSaveArchiveSummary,
|
||||||
} from '../../../packages/shared/src/contracts/runtime';
|
} from '../../../packages/shared/src/contracts/runtime';
|
||||||
|
import type {
|
||||||
|
CreateSquareHoleSessionRequest,
|
||||||
|
ExecuteSquareHoleActionRequest,
|
||||||
|
SendSquareHoleMessageRequest,
|
||||||
|
SquareHoleActionResponse,
|
||||||
|
SquareHoleSessionResponse,
|
||||||
|
SquareHoleSessionSnapshot,
|
||||||
|
} from '../../../packages/shared/src/contracts/squareHoleAgent';
|
||||||
|
import type {
|
||||||
|
SquareHoleRunSnapshot,
|
||||||
|
} from '../../../packages/shared/src/contracts/squareHoleRuntime';
|
||||||
|
import type {
|
||||||
|
SquareHoleWorkProfile,
|
||||||
|
SquareHoleWorkSummary,
|
||||||
|
} from '../../../packages/shared/src/contracts/squareHoleWorks';
|
||||||
import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets';
|
import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets';
|
||||||
import {
|
import {
|
||||||
buildPublicWorkStagePath,
|
buildPublicWorkStagePath,
|
||||||
@@ -123,6 +122,7 @@ import {
|
|||||||
buildBigFishGenerationAnchorEntries,
|
buildBigFishGenerationAnchorEntries,
|
||||||
buildMiniGameDraftGenerationProgress,
|
buildMiniGameDraftGenerationProgress,
|
||||||
buildPuzzleGenerationAnchorEntries,
|
buildPuzzleGenerationAnchorEntries,
|
||||||
|
buildSquareHoleGenerationAnchorEntries,
|
||||||
createMiniGameDraftGenerationState,
|
createMiniGameDraftGenerationState,
|
||||||
type MiniGameDraftGenerationState,
|
type MiniGameDraftGenerationState,
|
||||||
} from '../../services/miniGameDraftGenerationProgress';
|
} from '../../services/miniGameDraftGenerationProgress';
|
||||||
@@ -552,8 +552,12 @@ function mapPublicWorkDetailToSquareHoleWork(
|
|||||||
summary: entry.summaryText,
|
summary: entry.summaryText,
|
||||||
tags: entry.themeTags,
|
tags: entry.themeTags,
|
||||||
coverImageSrc: entry.coverImageSrc,
|
coverImageSrc: entry.coverImageSrc,
|
||||||
shapeCount: 8,
|
backgroundPrompt: entry.backgroundPrompt ?? '方洞挑战运行背景',
|
||||||
difficulty: 4,
|
backgroundImageSrc: entry.backgroundImageSrc ?? null,
|
||||||
|
shapeOptions: entry.shapeOptions ?? [],
|
||||||
|
holeOptions: entry.holeOptions ?? [],
|
||||||
|
shapeCount: entry.shapeCount ?? 8,
|
||||||
|
difficulty: entry.difficulty ?? 4,
|
||||||
publicationStatus: 'published',
|
publicationStatus: 'published',
|
||||||
playCount: entry.playCount ?? 0,
|
playCount: entry.playCount ?? 0,
|
||||||
updatedAt: entry.updatedAt,
|
updatedAt: entry.updatedAt,
|
||||||
@@ -581,7 +585,11 @@ function buildSquareHoleProfileFromSession(
|
|||||||
twistRule: draft.twistRule,
|
twistRule: draft.twistRule,
|
||||||
summary: draft.summary,
|
summary: draft.summary,
|
||||||
tags: draft.tags,
|
tags: draft.tags,
|
||||||
coverImageSrc: null,
|
coverImageSrc: draft.coverImageSrc ?? null,
|
||||||
|
backgroundPrompt: draft.backgroundPrompt,
|
||||||
|
backgroundImageSrc: draft.backgroundImageSrc ?? null,
|
||||||
|
shapeOptions: draft.shapeOptions,
|
||||||
|
holeOptions: draft.holeOptions,
|
||||||
shapeCount: draft.shapeCount,
|
shapeCount: draft.shapeCount,
|
||||||
difficulty: draft.difficulty,
|
difficulty: draft.difficulty,
|
||||||
publicationStatus: 'draft',
|
publicationStatus: 'draft',
|
||||||
@@ -608,13 +616,6 @@ function mergeBigFishWorkSummary(
|
|||||||
: current;
|
: current;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeSquareHoleWorkSummary(
|
|
||||||
current: SquareHoleWorkSummary,
|
|
||||||
updated: SquareHoleWorkSummary,
|
|
||||||
): SquareHoleWorkSummary {
|
|
||||||
return current.profileId === updated.profileId ? updated : current;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resolvePublicWorkAuthorSummary(
|
async function resolvePublicWorkAuthorSummary(
|
||||||
entry: PlatformPublicGalleryCard,
|
entry: PlatformPublicGalleryCard,
|
||||||
): Promise<PublicUserSummary | null> {
|
): Promise<PublicUserSummary | null> {
|
||||||
@@ -1086,6 +1087,8 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
useState<SquareHoleRuntimeReturnStage>('square-hole-result');
|
useState<SquareHoleRuntimeReturnStage>('square-hole-result');
|
||||||
const [isSquareHoleLoadingLibrary, setIsSquareHoleLoadingLibrary] =
|
const [isSquareHoleLoadingLibrary, setIsSquareHoleLoadingLibrary] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
const [squareHoleGenerationState, setSquareHoleGenerationState] =
|
||||||
|
useState<MiniGameDraftGenerationState | null>(null);
|
||||||
const [bigFishRun, setBigFishRun] =
|
const [bigFishRun, setBigFishRun] =
|
||||||
useState<BigFishRuntimeSnapshotResponse | null>(null);
|
useState<BigFishRuntimeSnapshotResponse | null>(null);
|
||||||
const [bigFishRuntimeShare, setBigFishRuntimeShare] = useState<{
|
const [bigFishRuntimeShare, setBigFishRuntimeShare] = useState<{
|
||||||
@@ -1817,7 +1820,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
workspaceStage: 'square-hole-agent-workspace',
|
workspaceStage: 'square-hole-agent-workspace',
|
||||||
resultStage: 'square-hole-result',
|
resultStage: 'square-hole-result',
|
||||||
platformStage: 'platform',
|
platformStage: 'platform',
|
||||||
isCompileAction: (payload) => payload.action === 'square_hole_compile_draft',
|
isCompileAction: () => false,
|
||||||
resolveErrorMessage: resolveSquareHoleErrorMessage,
|
resolveErrorMessage: resolveSquareHoleErrorMessage,
|
||||||
errorMessages: {
|
errorMessages: {
|
||||||
open: '开启方洞挑战共创工作台失败。',
|
open: '开启方洞挑战共创工作台失败。',
|
||||||
@@ -1831,9 +1834,30 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
onSessionOpened: () => {
|
onSessionOpened: () => {
|
||||||
setShowCreationTypeModal(false);
|
setShowCreationTypeModal(false);
|
||||||
},
|
},
|
||||||
|
beforeExecuteAction: ({ payload }) => {
|
||||||
|
if (payload.action === 'square_hole_compile_draft') {
|
||||||
|
setSquareHoleGenerationState(
|
||||||
|
createMiniGameDraftGenerationState('square-hole'),
|
||||||
|
);
|
||||||
|
setSelectionStage('square-hole-generating');
|
||||||
|
}
|
||||||
|
if (payload.action === 'square_hole_generate_visual_assets') {
|
||||||
|
setSquareHoleGenerationState((current) => ({
|
||||||
|
...(current ?? createMiniGameDraftGenerationState('square-hole')),
|
||||||
|
phase: 'square-hole-cover',
|
||||||
|
completedAssetCount: 0,
|
||||||
|
totalAssetCount: 0,
|
||||||
|
error: null,
|
||||||
|
}));
|
||||||
|
setSelectionStage('square-hole-generating');
|
||||||
|
}
|
||||||
|
},
|
||||||
onActionComplete: async ({ payload, response, setSession }) => {
|
onActionComplete: async ({ payload, response, setSession }) => {
|
||||||
setSession(response.session);
|
setSession(response.session);
|
||||||
if (payload.action !== 'square_hole_compile_draft') {
|
if (
|
||||||
|
payload.action !== 'square_hole_compile_draft' &&
|
||||||
|
payload.action !== 'square_hole_generate_visual_assets'
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1843,12 +1867,79 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (payload.action === 'square_hole_compile_draft') {
|
||||||
|
try {
|
||||||
|
const assetResponse = await squareHoleCreationClient.executeAction(
|
||||||
|
response.session.sessionId,
|
||||||
|
{
|
||||||
|
action: 'square_hole_generate_visual_assets',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
setSession(assetResponse.session);
|
||||||
|
const assetProfileId = assetResponse.session.draft?.profileId;
|
||||||
|
if (!assetProfileId) {
|
||||||
|
setSquareHoleProfile(
|
||||||
|
buildSquareHoleProfileFromSession(assetResponse.session),
|
||||||
|
);
|
||||||
|
setSelectionStage('square-hole-result');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { item } = await getSquareHoleWorkDetail(assetProfileId);
|
||||||
|
setSquareHoleProfile(item);
|
||||||
|
setSquareHoleGenerationState((current) => ({
|
||||||
|
...(current ?? createMiniGameDraftGenerationState('square-hole')),
|
||||||
|
phase: 'ready',
|
||||||
|
completedAssetCount: item.shapeOptions.length + 2,
|
||||||
|
totalAssetCount: item.shapeOptions.length + 2,
|
||||||
|
error: null,
|
||||||
|
}));
|
||||||
|
await refreshSquareHoleShelf().catch(() => undefined);
|
||||||
|
setSelectionStage('square-hole-result');
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = resolveSquareHoleErrorMessage(
|
||||||
|
error,
|
||||||
|
'生成方洞挑战图片失败。',
|
||||||
|
);
|
||||||
|
setSquareHoleError(errorMessage);
|
||||||
|
setSquareHoleGenerationState((current) => ({
|
||||||
|
...(current ?? createMiniGameDraftGenerationState('square-hole')),
|
||||||
|
phase: 'failed',
|
||||||
|
error: errorMessage,
|
||||||
|
}));
|
||||||
|
setSquareHoleProfile(buildSquareHoleProfileFromSession(response.session));
|
||||||
|
setSelectionStage('square-hole-generating');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { item } = await getSquareHoleWorkDetail(profileId);
|
const { item } = await getSquareHoleWorkDetail(profileId);
|
||||||
setSquareHoleProfile(item);
|
setSquareHoleProfile(item);
|
||||||
|
setSquareHoleGenerationState((current) => ({
|
||||||
|
...(current ?? createMiniGameDraftGenerationState('square-hole')),
|
||||||
|
phase: 'ready',
|
||||||
|
completedAssetCount: item.shapeOptions.length + 2,
|
||||||
|
totalAssetCount: item.shapeOptions.length + 2,
|
||||||
|
error: null,
|
||||||
|
}));
|
||||||
await refreshSquareHoleShelf().catch(() => undefined);
|
await refreshSquareHoleShelf().catch(() => undefined);
|
||||||
|
setSelectionStage('square-hole-result');
|
||||||
} catch {
|
} catch {
|
||||||
setSquareHoleProfile(buildSquareHoleProfileFromSession(response.session));
|
setSquareHoleProfile(buildSquareHoleProfileFromSession(response.session));
|
||||||
|
setSelectionStage('square-hole-result');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onActionError: ({ payload, errorMessage }) => {
|
||||||
|
if (
|
||||||
|
payload.action === 'square_hole_compile_draft' ||
|
||||||
|
payload.action === 'square_hole_generate_visual_assets'
|
||||||
|
) {
|
||||||
|
setSquareHoleGenerationState((current) => ({
|
||||||
|
...(current ?? createMiniGameDraftGenerationState('square-hole')),
|
||||||
|
phase: 'failed',
|
||||||
|
error: errorMessage,
|
||||||
|
}));
|
||||||
|
setSelectionStage('square-hole-generating');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -2047,6 +2138,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
setSquareHoleProfile(null);
|
setSquareHoleProfile(null);
|
||||||
setSquareHoleRun(null);
|
setSquareHoleRun(null);
|
||||||
setSquareHoleError(null);
|
setSquareHoleError(null);
|
||||||
|
setSquareHoleGenerationState(null);
|
||||||
setSquareHoleRuntimeReturnStage('square-hole-result');
|
setSquareHoleRuntimeReturnStage('square-hole-result');
|
||||||
setStreamingSquareHoleReplyText('');
|
setStreamingSquareHoleReplyText('');
|
||||||
setIsStreamingSquareHoleReply(false);
|
setIsStreamingSquareHoleReply(false);
|
||||||
@@ -2162,6 +2254,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
setSquareHoleGalleryEntries([]);
|
setSquareHoleGalleryEntries([]);
|
||||||
setSquareHoleRun(null);
|
setSquareHoleRun(null);
|
||||||
setSquareHoleRuntimeReturnStage('square-hole-result');
|
setSquareHoleRuntimeReturnStage('square-hole-result');
|
||||||
|
setSquareHoleGenerationState(null);
|
||||||
setSquareHoleError(null);
|
setSquareHoleError(null);
|
||||||
setStreamingSquareHoleReplyText('');
|
setStreamingSquareHoleReplyText('');
|
||||||
setIsStreamingSquareHoleReply(false);
|
setIsStreamingSquareHoleReply(false);
|
||||||
@@ -2291,6 +2384,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
const leaveSquareHoleFlow = useCallback(() => {
|
const leaveSquareHoleFlow = useCallback(() => {
|
||||||
setSquareHoleRun(null);
|
setSquareHoleRun(null);
|
||||||
setSquareHoleRuntimeReturnStage('square-hole-result');
|
setSquareHoleRuntimeReturnStage('square-hole-result');
|
||||||
|
setSquareHoleGenerationState(null);
|
||||||
squareHoleFlow.leaveFlow();
|
squareHoleFlow.leaveFlow();
|
||||||
}, [squareHoleFlow]);
|
}, [squareHoleFlow]);
|
||||||
|
|
||||||
@@ -2316,6 +2410,20 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
|
|
||||||
const executeSquareHoleAction = squareHoleFlow.executeAction;
|
const executeSquareHoleAction = squareHoleFlow.executeAction;
|
||||||
|
|
||||||
|
const retrySquareHoleAssetGeneration = useCallback(() => {
|
||||||
|
const session = squareHoleSession;
|
||||||
|
if (!session?.draft?.profileId) {
|
||||||
|
void executeSquareHoleAction({
|
||||||
|
action: 'square_hole_compile_draft',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void executeSquareHoleAction({
|
||||||
|
action: 'square_hole_generate_visual_assets',
|
||||||
|
});
|
||||||
|
}, [executeSquareHoleAction, squareHoleSession]);
|
||||||
|
|
||||||
const executePuzzleAction = puzzleFlow.executeAction;
|
const executePuzzleAction = puzzleFlow.executeAction;
|
||||||
|
|
||||||
const retryPuzzleDraftGeneration = useCallback(() => {
|
const retryPuzzleDraftGeneration = useCallback(() => {
|
||||||
@@ -4013,6 +4121,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSquareHoleGenerationState(null);
|
||||||
const restoredSession = await squareHoleFlow.restoreDraft(
|
const restoredSession = await squareHoleFlow.restoreDraft(
|
||||||
item.sourceSessionId,
|
item.sourceSessionId,
|
||||||
);
|
);
|
||||||
@@ -5583,6 +5692,50 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{selectionStage === 'square-hole-generating' && (
|
||||||
|
<motion.div
|
||||||
|
key="square-hole-generating"
|
||||||
|
initial={{ opacity: 0, y: 12 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -12 }}
|
||||||
|
className="flex h-full min-h-0 flex-col"
|
||||||
|
>
|
||||||
|
<Suspense
|
||||||
|
fallback={<LazyPanelFallback label="正在加载方洞挑战生成面板..." />}
|
||||||
|
>
|
||||||
|
<CustomWorldGenerationView
|
||||||
|
settingText={
|
||||||
|
squareHoleSession?.lastAssistantReply ??
|
||||||
|
'正在整理当前方洞挑战草稿。'
|
||||||
|
}
|
||||||
|
anchorEntries={buildSquareHoleGenerationAnchorEntries(
|
||||||
|
squareHoleSession,
|
||||||
|
)}
|
||||||
|
progress={buildMiniGameDraftGenerationProgress(
|
||||||
|
squareHoleGenerationState,
|
||||||
|
)}
|
||||||
|
isGenerating={isSquareHoleBusy}
|
||||||
|
error={squareHoleError}
|
||||||
|
onBack={leaveSquareHoleFlow}
|
||||||
|
onEditSetting={() => {
|
||||||
|
setSelectionStage('square-hole-agent-workspace');
|
||||||
|
}}
|
||||||
|
onRetry={retrySquareHoleAssetGeneration}
|
||||||
|
onInterrupt={undefined}
|
||||||
|
backLabel="返回创作中心"
|
||||||
|
settingActionLabel={null}
|
||||||
|
retryLabel="重新生成图片"
|
||||||
|
settingTitle="当前方洞挑战"
|
||||||
|
settingDescription={null}
|
||||||
|
progressTitle="方洞挑战图片生成进度"
|
||||||
|
activeBadgeLabel="图片生成中"
|
||||||
|
pausedBadgeLabel="图片生成已暂停"
|
||||||
|
idleBadgeLabel="等待返回结果页"
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
{selectionStage === 'square-hole-result' && squareHoleSession?.draft && (
|
{selectionStage === 'square-hole-result' && squareHoleSession?.draft && (
|
||||||
<motion.div
|
<motion.div
|
||||||
key="square-hole-result"
|
key="square-hole-result"
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export type SelectionStage =
|
|||||||
| 'match3d-result'
|
| 'match3d-result'
|
||||||
| 'match3d-runtime'
|
| 'match3d-runtime'
|
||||||
| 'square-hole-agent-workspace'
|
| 'square-hole-agent-workspace'
|
||||||
|
| 'square-hole-generating'
|
||||||
| 'square-hole-result'
|
| 'square-hole-result'
|
||||||
| 'square-hole-runtime'
|
| 'square-hole-runtime'
|
||||||
| 'puzzle-agent-workspace'
|
| 'puzzle-agent-workspace'
|
||||||
|
|||||||
@@ -2,11 +2,15 @@ import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/
|
|||||||
import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks';
|
import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks';
|
||||||
import type { PuzzleDraftLevel } from '../../../packages/shared/src/contracts/puzzleAgentDraft';
|
import type { PuzzleDraftLevel } from '../../../packages/shared/src/contracts/puzzleAgentDraft';
|
||||||
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
|
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
|
||||||
import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks';
|
|
||||||
import type {
|
import type {
|
||||||
CustomWorldGalleryCard,
|
CustomWorldGalleryCard,
|
||||||
CustomWorldLibraryEntry,
|
CustomWorldLibraryEntry,
|
||||||
} from '../../../packages/shared/src/contracts/runtime';
|
} from '../../../packages/shared/src/contracts/runtime';
|
||||||
|
import type {
|
||||||
|
SquareHoleHoleOption,
|
||||||
|
SquareHoleShapeOption,
|
||||||
|
SquareHoleWorkSummary,
|
||||||
|
} from '../../../packages/shared/src/contracts/squareHoleWorks';
|
||||||
import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets';
|
import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets';
|
||||||
import { resolveCustomWorldCampSceneImage } from '../../data/customWorldVisuals';
|
import { resolveCustomWorldCampSceneImage } from '../../data/customWorldVisuals';
|
||||||
import {
|
import {
|
||||||
@@ -112,6 +116,12 @@ export type PlatformSquareHoleGalleryCard = {
|
|||||||
subtitle: string;
|
subtitle: string;
|
||||||
summaryText: string;
|
summaryText: string;
|
||||||
coverImageSrc: string | null;
|
coverImageSrc: string | null;
|
||||||
|
backgroundPrompt?: string;
|
||||||
|
backgroundImageSrc?: string | null;
|
||||||
|
shapeOptions?: SquareHoleShapeOption[];
|
||||||
|
holeOptions?: SquareHoleHoleOption[];
|
||||||
|
shapeCount?: number;
|
||||||
|
difficulty?: number;
|
||||||
themeTags: string[];
|
themeTags: string[];
|
||||||
playCount?: number;
|
playCount?: number;
|
||||||
remixCount?: number;
|
remixCount?: number;
|
||||||
@@ -224,9 +234,15 @@ export function mapSquareHoleWorkToPlatformGalleryCard(
|
|||||||
ownerUserId: work.ownerUserId,
|
ownerUserId: work.ownerUserId,
|
||||||
authorDisplayName: '玩家',
|
authorDisplayName: '玩家',
|
||||||
worldName: work.gameName,
|
worldName: work.gameName,
|
||||||
subtitle: '反直觉形状分拣',
|
subtitle: work.twistRule || '反直觉形状分拣',
|
||||||
summaryText: work.summary,
|
summaryText: work.summary,
|
||||||
coverImageSrc: work.coverImageSrc ?? null,
|
coverImageSrc: work.coverImageSrc ?? null,
|
||||||
|
backgroundPrompt: work.backgroundPrompt,
|
||||||
|
backgroundImageSrc: work.backgroundImageSrc ?? null,
|
||||||
|
shapeOptions: work.shapeOptions,
|
||||||
|
holeOptions: work.holeOptions,
|
||||||
|
shapeCount: work.shapeCount,
|
||||||
|
difficulty: work.difficulty,
|
||||||
themeTags:
|
themeTags:
|
||||||
work.tags.length > 0 ? work.tags : [work.themeText, '方洞挑战'],
|
work.tags.length > 0 ? work.tags : [work.themeText, '方洞挑战'],
|
||||||
playCount: work.playCount ?? 0,
|
playCount: work.playCount ?? 0,
|
||||||
|
|||||||
@@ -4,13 +4,17 @@ import {
|
|||||||
ImagePlus,
|
ImagePlus,
|
||||||
Loader2,
|
Loader2,
|
||||||
Play,
|
Play,
|
||||||
|
Plus,
|
||||||
Send,
|
Send,
|
||||||
|
Trash2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { type ChangeEvent, useEffect, useMemo, useState } from 'react';
|
import { type ChangeEvent, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import type { SquareHoleResultDraft } from '../../../packages/shared/src/contracts/squareHoleAgent';
|
import type { SquareHoleResultDraft } from '../../../packages/shared/src/contracts/squareHoleAgent';
|
||||||
import type {
|
import type {
|
||||||
PutSquareHoleWorkRequest,
|
PutSquareHoleWorkRequest,
|
||||||
|
SquareHoleHoleOption,
|
||||||
|
SquareHoleShapeOption,
|
||||||
SquareHoleWorkProfile,
|
SquareHoleWorkProfile,
|
||||||
} from '../../../packages/shared/src/contracts/squareHoleWorks';
|
} from '../../../packages/shared/src/contracts/squareHoleWorks';
|
||||||
import {
|
import {
|
||||||
@@ -37,13 +41,26 @@ type SquareHoleResultEditState = {
|
|||||||
summary: string;
|
summary: string;
|
||||||
tagsText: string;
|
tagsText: string;
|
||||||
coverImageSrc: string;
|
coverImageSrc: string;
|
||||||
|
backgroundPrompt: string;
|
||||||
|
backgroundImageSrc: string;
|
||||||
themeText: string;
|
themeText: string;
|
||||||
twistRule: string;
|
twistRule: string;
|
||||||
|
shapeOptions: SquareHoleShapeOption[];
|
||||||
|
holeOptions: SquareHoleHoleOption[];
|
||||||
shapeCountText: string;
|
shapeCountText: string;
|
||||||
difficultyText: string;
|
difficultyText: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SQUARE_HOLE_AUTOSAVE_DEBOUNCE_MS = 600;
|
const SQUARE_HOLE_AUTOSAVE_DEBOUNCE_MS = 600;
|
||||||
|
const SQUARE_HOLE_SHAPE_KIND_OPTIONS = [
|
||||||
|
'square',
|
||||||
|
'circle',
|
||||||
|
'triangle',
|
||||||
|
'star',
|
||||||
|
'arch',
|
||||||
|
'diamond',
|
||||||
|
];
|
||||||
|
const SQUARE_HOLE_HOLE_KIND_OPTIONS = SQUARE_HOLE_SHAPE_KIND_OPTIONS;
|
||||||
|
|
||||||
function normalizeTags(value: string) {
|
function normalizeTags(value: string) {
|
||||||
return [
|
return [
|
||||||
@@ -78,8 +95,12 @@ function createEditState(
|
|||||||
summary: profile.summary,
|
summary: profile.summary,
|
||||||
tagsText: profile.tags.join(','),
|
tagsText: profile.tags.join(','),
|
||||||
coverImageSrc: profile.coverImageSrc?.trim() || '',
|
coverImageSrc: profile.coverImageSrc?.trim() || '',
|
||||||
|
backgroundPrompt: profile.backgroundPrompt || '',
|
||||||
|
backgroundImageSrc: profile.backgroundImageSrc?.trim() || '',
|
||||||
themeText: profile.themeText,
|
themeText: profile.themeText,
|
||||||
twistRule: profile.twistRule,
|
twistRule: profile.twistRule,
|
||||||
|
shapeOptions: profile.shapeOptions.map((option) => ({ ...option })),
|
||||||
|
holeOptions: profile.holeOptions.map((option) => ({ ...option })),
|
||||||
shapeCountText: String(profile.shapeCount),
|
shapeCountText: String(profile.shapeCount),
|
||||||
difficultyText: String(profile.difficulty),
|
difficultyText: String(profile.difficulty),
|
||||||
};
|
};
|
||||||
@@ -95,6 +116,28 @@ function buildSavePayload(
|
|||||||
const twistRule = editState.twistRule.trim();
|
const twistRule = editState.twistRule.trim();
|
||||||
const summary = editState.summary.trim();
|
const summary = editState.summary.trim();
|
||||||
const tags = normalizeTags(editState.tagsText);
|
const tags = normalizeTags(editState.tagsText);
|
||||||
|
const shapeOptions = editState.shapeOptions
|
||||||
|
.map((option) => ({
|
||||||
|
...option,
|
||||||
|
optionId: option.optionId.trim(),
|
||||||
|
shapeKind: option.shapeKind.trim(),
|
||||||
|
label: option.label.trim(),
|
||||||
|
imagePrompt: option.imagePrompt.trim(),
|
||||||
|
imageSrc: option.imageSrc?.trim() || null,
|
||||||
|
}))
|
||||||
|
.filter(
|
||||||
|
(option) =>
|
||||||
|
option.optionId && option.shapeKind && option.label && option.imagePrompt,
|
||||||
|
);
|
||||||
|
const holeOptions = editState.holeOptions
|
||||||
|
.map((option) => ({
|
||||||
|
...option,
|
||||||
|
holeId: option.holeId.trim(),
|
||||||
|
holeKind: option.holeKind.trim(),
|
||||||
|
label: option.label.trim(),
|
||||||
|
bonus: Boolean(option.bonus),
|
||||||
|
}))
|
||||||
|
.filter((option) => option.holeId && option.holeKind && option.label);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!gameName ||
|
!gameName ||
|
||||||
@@ -102,6 +145,8 @@ function buildSavePayload(
|
|||||||
!twistRule ||
|
!twistRule ||
|
||||||
!summary ||
|
!summary ||
|
||||||
tags.length === 0 ||
|
tags.length === 0 ||
|
||||||
|
shapeOptions.length === 0 ||
|
||||||
|
holeOptions.length === 0 ||
|
||||||
!shapeCount ||
|
!shapeCount ||
|
||||||
!difficulty
|
!difficulty
|
||||||
) {
|
) {
|
||||||
@@ -115,6 +160,10 @@ function buildSavePayload(
|
|||||||
summary,
|
summary,
|
||||||
tags,
|
tags,
|
||||||
coverImageSrc: editState.coverImageSrc.trim() || null,
|
coverImageSrc: editState.coverImageSrc.trim() || null,
|
||||||
|
backgroundPrompt: editState.backgroundPrompt.trim(),
|
||||||
|
backgroundImageSrc: editState.backgroundImageSrc.trim() || null,
|
||||||
|
shapeOptions,
|
||||||
|
holeOptions,
|
||||||
shapeCount,
|
shapeCount,
|
||||||
difficulty,
|
difficulty,
|
||||||
};
|
};
|
||||||
@@ -129,6 +178,21 @@ function buildPublishBlockers(editState: SquareHoleResultEditState) {
|
|||||||
...(normalizeTags(editState.tagsText).length > 0
|
...(normalizeTags(editState.tagsText).length > 0
|
||||||
? []
|
? []
|
||||||
: ['至少需要 1 个标签。']),
|
: ['至少需要 1 个标签。']),
|
||||||
|
...(editState.shapeOptions.some(
|
||||||
|
(option) =>
|
||||||
|
option.optionId.trim() &&
|
||||||
|
option.shapeKind.trim() &&
|
||||||
|
option.label.trim() &&
|
||||||
|
option.imagePrompt.trim(),
|
||||||
|
)
|
||||||
|
? []
|
||||||
|
: ['至少需要 1 个形状选项。']),
|
||||||
|
...(editState.holeOptions.some(
|
||||||
|
(option) =>
|
||||||
|
option.holeId.trim() && option.holeKind.trim() && option.label.trim(),
|
||||||
|
)
|
||||||
|
? []
|
||||||
|
: ['至少需要 1 个洞口选项。']),
|
||||||
...(normalizeShapeCount(editState.shapeCountText)
|
...(normalizeShapeCount(editState.shapeCountText)
|
||||||
? []
|
? []
|
||||||
: ['形状数量需要在 6 到 24 之间。']),
|
: ['形状数量需要在 6 到 24 之间。']),
|
||||||
@@ -154,6 +218,31 @@ function readImageAsDataUrl(file: File) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createShapeOption(index: number): SquareHoleShapeOption {
|
||||||
|
return {
|
||||||
|
optionId: `shape-${Date.now().toString(36)}-${index}`,
|
||||||
|
shapeKind:
|
||||||
|
SQUARE_HOLE_SHAPE_KIND_OPTIONS[
|
||||||
|
index % SQUARE_HOLE_SHAPE_KIND_OPTIONS.length
|
||||||
|
] ?? 'square',
|
||||||
|
label: `形状 ${index + 1}`,
|
||||||
|
imagePrompt: '主题贴图',
|
||||||
|
imageSrc: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createHoleOption(index: number): SquareHoleHoleOption {
|
||||||
|
return {
|
||||||
|
holeId: `hole-${Date.now().toString(36)}-${index}`,
|
||||||
|
holeKind:
|
||||||
|
SQUARE_HOLE_HOLE_KIND_OPTIONS[
|
||||||
|
index % SQUARE_HOLE_HOLE_KIND_OPTIONS.length
|
||||||
|
] ?? 'square',
|
||||||
|
label: `洞口 ${index + 1}`,
|
||||||
|
bonus: index === 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function buildPlayableProfile(
|
function buildPlayableProfile(
|
||||||
profile: SquareHoleWorkProfile,
|
profile: SquareHoleWorkProfile,
|
||||||
editState: SquareHoleResultEditState,
|
editState: SquareHoleResultEditState,
|
||||||
@@ -171,6 +260,10 @@ function buildPlayableProfile(
|
|||||||
summary: payload.summary,
|
summary: payload.summary,
|
||||||
tags: payload.tags,
|
tags: payload.tags,
|
||||||
coverImageSrc: payload.coverImageSrc,
|
coverImageSrc: payload.coverImageSrc,
|
||||||
|
backgroundPrompt: payload.backgroundPrompt ?? profile.backgroundPrompt,
|
||||||
|
backgroundImageSrc: payload.backgroundImageSrc,
|
||||||
|
shapeOptions: payload.shapeOptions ?? profile.shapeOptions,
|
||||||
|
holeOptions: payload.holeOptions ?? profile.holeOptions,
|
||||||
shapeCount: payload.shapeCount,
|
shapeCount: payload.shapeCount,
|
||||||
difficulty: payload.difficulty,
|
difficulty: payload.difficulty,
|
||||||
};
|
};
|
||||||
@@ -241,7 +334,7 @@ export function SquareHoleResultView({
|
|||||||
setEditState(createEditState(profile));
|
setEditState(createEditState(profile));
|
||||||
setAutoSaveState('idle');
|
setAutoSaveState('idle');
|
||||||
setLocalError(null);
|
setLocalError(null);
|
||||||
}, [profile.profileId, profile.updatedAt]);
|
}, [profile]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const payload = buildSavePayload(editState);
|
const payload = buildSavePayload(editState);
|
||||||
@@ -250,14 +343,21 @@ export function SquareHoleResultView({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentTags = normalizeTags(profile.tags.join(','));
|
const currentTags = normalizeTags(profile.tags.join(','));
|
||||||
|
const currentShapeOptions = JSON.stringify(profile.shapeOptions);
|
||||||
|
const currentHoleOptions = JSON.stringify(profile.holeOptions);
|
||||||
const changed =
|
const changed =
|
||||||
payload.gameName !== profile.gameName ||
|
payload.gameName !== profile.gameName ||
|
||||||
payload.themeText !== profile.themeText ||
|
payload.themeText !== profile.themeText ||
|
||||||
payload.twistRule !== profile.twistRule ||
|
payload.twistRule !== profile.twistRule ||
|
||||||
payload.summary !== profile.summary ||
|
payload.summary !== profile.summary ||
|
||||||
(payload.coverImageSrc ?? '') !== (profile.coverImageSrc ?? '') ||
|
(payload.coverImageSrc ?? '') !== (profile.coverImageSrc ?? '') ||
|
||||||
|
(payload.backgroundPrompt ?? '') !== (profile.backgroundPrompt ?? '') ||
|
||||||
|
(payload.backgroundImageSrc ?? '') !==
|
||||||
|
(profile.backgroundImageSrc ?? '') ||
|
||||||
payload.shapeCount !== profile.shapeCount ||
|
payload.shapeCount !== profile.shapeCount ||
|
||||||
payload.difficulty !== profile.difficulty ||
|
payload.difficulty !== profile.difficulty ||
|
||||||
|
JSON.stringify(payload.shapeOptions ?? []) !== currentShapeOptions ||
|
||||||
|
JSON.stringify(payload.holeOptions ?? []) !== currentHoleOptions ||
|
||||||
payload.tags.length !== currentTags.length ||
|
payload.tags.length !== currentTags.length ||
|
||||||
payload.tags.some((tag, index) => tag !== currentTags[index]);
|
payload.tags.some((tag, index) => tag !== currentTags[index]);
|
||||||
|
|
||||||
@@ -330,6 +430,60 @@ export function SquareHoleResultView({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleBackgroundImageChange = async (
|
||||||
|
event: ChangeEvent<HTMLInputElement>,
|
||||||
|
) => {
|
||||||
|
const file = event.target.files?.[0] ?? null;
|
||||||
|
event.target.value = '';
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dataUrl = await readImageAsDataUrl(file);
|
||||||
|
setEditState((current) => ({
|
||||||
|
...current,
|
||||||
|
backgroundImageSrc: dataUrl,
|
||||||
|
}));
|
||||||
|
setLocalError(null);
|
||||||
|
} catch (caughtError) {
|
||||||
|
setLocalError(
|
||||||
|
caughtError instanceof Error ? caughtError.message : '背景图读取失败。',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShapeImageChange = async (
|
||||||
|
optionId: string,
|
||||||
|
event: ChangeEvent<HTMLInputElement>,
|
||||||
|
) => {
|
||||||
|
const file = event.target.files?.[0] ?? null;
|
||||||
|
event.target.value = '';
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dataUrl = await readImageAsDataUrl(file);
|
||||||
|
setEditState((current) => ({
|
||||||
|
...current,
|
||||||
|
shapeOptions: current.shapeOptions.map((option) =>
|
||||||
|
option.optionId === optionId
|
||||||
|
? {
|
||||||
|
...option,
|
||||||
|
imageSrc: dataUrl,
|
||||||
|
}
|
||||||
|
: option,
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
setLocalError(null);
|
||||||
|
} catch (caughtError) {
|
||||||
|
setLocalError(
|
||||||
|
caughtError instanceof Error ? caughtError.message : '形状贴图读取失败。',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleStartTestRun = async () => {
|
const handleStartTestRun = async () => {
|
||||||
if (!canSubmit || isStartingTestRun) {
|
if (!canSubmit || isStartingTestRun) {
|
||||||
setLocalError(blockers[0] ?? null);
|
setLocalError(blockers[0] ?? null);
|
||||||
@@ -422,6 +576,31 @@ export function SquareHoleResultView({
|
|||||||
{draft?.publishReady ?? profile.publishReady ? '可发布' : '草稿'}
|
{draft?.publishReady ?? profile.publishReady ? '可发布' : '草稿'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mt-3 aspect-[16/9] overflow-hidden rounded-[1.1rem] border border-[var(--platform-subpanel-border)] bg-[linear-gradient(135deg,rgba(15,23,42,0.12),rgba(34,197,94,0.16))]">
|
||||||
|
{editState.backgroundImageSrc ? (
|
||||||
|
<ResolvedAssetImage
|
||||||
|
src={editState.backgroundImageSrc}
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
className="h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="grid h-full w-full place-items-center text-slate-700">
|
||||||
|
<ImagePlus className="h-8 w-8" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<label className="platform-button platform-button--ghost mt-3 flex min-h-10 cursor-pointer items-center justify-center gap-2 px-3 py-2 text-sm">
|
||||||
|
<ImagePlus className="h-4 w-4" />
|
||||||
|
背景图
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
className="sr-only"
|
||||||
|
disabled={busy}
|
||||||
|
onChange={handleBackgroundImageChange}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="platform-subpanel rounded-[1.5rem] p-4 sm:p-5">
|
<section className="platform-subpanel rounded-[1.5rem] p-4 sm:p-5">
|
||||||
@@ -497,6 +676,23 @@ export function SquareHoleResultView({
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<label className="block sm:col-span-2">
|
||||||
|
<span className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||||
|
背景提示
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
value={editState.backgroundPrompt}
|
||||||
|
disabled={busy}
|
||||||
|
onChange={(event) =>
|
||||||
|
setEditState({
|
||||||
|
...editState,
|
||||||
|
backgroundPrompt: event.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="mt-2 w-full rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/86 px-3 py-3 text-sm font-semibold text-[var(--platform-text-strong)] outline-none"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<span className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
<span className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||||
形状数量
|
形状数量
|
||||||
@@ -534,6 +730,242 @@ export function SquareHoleResultView({
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section className="platform-subpanel rounded-[1.5rem] p-4 sm:p-5 lg:col-span-2">
|
||||||
|
<div className="mb-3 flex items-center justify-between gap-3">
|
||||||
|
<div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||||
|
形状选项
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={busy}
|
||||||
|
onClick={() =>
|
||||||
|
setEditState((current) => ({
|
||||||
|
...current,
|
||||||
|
shapeOptions: [
|
||||||
|
...current.shapeOptions,
|
||||||
|
createShapeOption(current.shapeOptions.length),
|
||||||
|
],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
className="platform-button platform-button--ghost min-h-0 px-3 py-1.5 text-[11px]"
|
||||||
|
>
|
||||||
|
<Plus className="h-3.5 w-3.5" />
|
||||||
|
新增
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||||
|
{editState.shapeOptions.map((option) => (
|
||||||
|
<div
|
||||||
|
key={option.optionId}
|
||||||
|
className="rounded-[1.1rem] border border-[var(--platform-subpanel-border)] bg-white/58 p-3"
|
||||||
|
>
|
||||||
|
<div className="mb-3 flex items-start gap-3">
|
||||||
|
<label className="relative grid h-20 w-20 shrink-0 cursor-pointer place-items-center overflow-hidden rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/82 text-slate-600">
|
||||||
|
{option.imageSrc ? (
|
||||||
|
<ResolvedAssetImage
|
||||||
|
src={option.imageSrc}
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
className="h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ImagePlus className="h-6 w-6" />
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
className="sr-only"
|
||||||
|
disabled={busy}
|
||||||
|
onChange={(event) => {
|
||||||
|
void handleShapeImageChange(option.optionId, event);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div className="min-w-0 flex-1 space-y-2">
|
||||||
|
<input
|
||||||
|
value={option.label}
|
||||||
|
disabled={busy}
|
||||||
|
onChange={(event) =>
|
||||||
|
setEditState((current) => ({
|
||||||
|
...current,
|
||||||
|
shapeOptions: current.shapeOptions.map((entry) =>
|
||||||
|
entry.optionId === option.optionId
|
||||||
|
? { ...entry, label: event.target.value }
|
||||||
|
: entry,
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
className="w-full rounded-[0.85rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-3 py-2 text-sm font-semibold text-[var(--platform-text-strong)] outline-none"
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
value={option.shapeKind}
|
||||||
|
disabled={busy}
|
||||||
|
onChange={(event) =>
|
||||||
|
setEditState((current) => ({
|
||||||
|
...current,
|
||||||
|
shapeOptions: current.shapeOptions.map((entry) =>
|
||||||
|
entry.optionId === option.optionId
|
||||||
|
? { ...entry, shapeKind: event.target.value }
|
||||||
|
: entry,
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
className="w-full rounded-[0.85rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-3 py-2 text-sm font-semibold text-[var(--platform-text-strong)] outline-none"
|
||||||
|
>
|
||||||
|
{SQUARE_HOLE_SHAPE_KIND_OPTIONS.map((kind) => (
|
||||||
|
<option key={kind} value={kind}>
|
||||||
|
{kind}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={busy || editState.shapeOptions.length <= 1}
|
||||||
|
onClick={() =>
|
||||||
|
setEditState((current) => ({
|
||||||
|
...current,
|
||||||
|
shapeOptions: current.shapeOptions.filter(
|
||||||
|
(entry) => entry.optionId !== option.optionId,
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
className="rounded-full p-2 text-slate-500 hover:bg-white/72 disabled:opacity-40"
|
||||||
|
aria-label="删除形状选项"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
value={option.imagePrompt}
|
||||||
|
disabled={busy}
|
||||||
|
rows={2}
|
||||||
|
onChange={(event) =>
|
||||||
|
setEditState((current) => ({
|
||||||
|
...current,
|
||||||
|
shapeOptions: current.shapeOptions.map((entry) =>
|
||||||
|
entry.optionId === option.optionId
|
||||||
|
? { ...entry, imagePrompt: event.target.value }
|
||||||
|
: entry,
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
className="w-full resize-none rounded-[0.85rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-3 py-2 text-sm leading-5 text-[var(--platform-text-strong)] outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="platform-subpanel rounded-[1.5rem] p-4 sm:p-5 lg:col-span-2">
|
||||||
|
<div className="mb-3 flex items-center justify-between gap-3">
|
||||||
|
<div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||||
|
洞口选项
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={busy}
|
||||||
|
onClick={() =>
|
||||||
|
setEditState((current) => ({
|
||||||
|
...current,
|
||||||
|
holeOptions: [
|
||||||
|
...current.holeOptions,
|
||||||
|
createHoleOption(current.holeOptions.length),
|
||||||
|
],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
className="platform-button platform-button--ghost min-h-0 px-3 py-1.5 text-[11px]"
|
||||||
|
>
|
||||||
|
<Plus className="h-3.5 w-3.5" />
|
||||||
|
新增
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||||
|
{editState.holeOptions.map((option) => (
|
||||||
|
<div
|
||||||
|
key={option.holeId}
|
||||||
|
className="rounded-[1.1rem] border border-[var(--platform-subpanel-border)] bg-white/58 p-3"
|
||||||
|
>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<input
|
||||||
|
value={option.label}
|
||||||
|
disabled={busy}
|
||||||
|
onChange={(event) =>
|
||||||
|
setEditState((current) => ({
|
||||||
|
...current,
|
||||||
|
holeOptions: current.holeOptions.map((entry) =>
|
||||||
|
entry.holeId === option.holeId
|
||||||
|
? { ...entry, label: event.target.value }
|
||||||
|
: entry,
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
className="w-full rounded-[0.85rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-3 py-2 text-sm font-semibold text-[var(--platform-text-strong)] outline-none"
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
value={option.holeKind}
|
||||||
|
disabled={busy}
|
||||||
|
onChange={(event) =>
|
||||||
|
setEditState((current) => ({
|
||||||
|
...current,
|
||||||
|
holeOptions: current.holeOptions.map((entry) =>
|
||||||
|
entry.holeId === option.holeId
|
||||||
|
? { ...entry, holeKind: event.target.value }
|
||||||
|
: entry,
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
className="w-full rounded-[0.85rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-3 py-2 text-sm font-semibold text-[var(--platform-text-strong)] outline-none"
|
||||||
|
>
|
||||||
|
{SQUARE_HOLE_HOLE_KIND_OPTIONS.map((kind) => (
|
||||||
|
<option key={kind} value={kind}>
|
||||||
|
{kind}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<label className="inline-flex min-w-0 items-center gap-2 text-sm font-semibold text-[var(--platform-text-strong)]">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={option.bonus}
|
||||||
|
disabled={busy}
|
||||||
|
onChange={(event) =>
|
||||||
|
setEditState((current) => ({
|
||||||
|
...current,
|
||||||
|
holeOptions: current.holeOptions.map((entry) =>
|
||||||
|
entry.holeId === option.holeId
|
||||||
|
? { ...entry, bonus: event.target.checked }
|
||||||
|
: entry,
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
className="h-4 w-4"
|
||||||
|
/>
|
||||||
|
加分选项
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={busy || editState.holeOptions.length <= 1}
|
||||||
|
onClick={() =>
|
||||||
|
setEditState((current) => ({
|
||||||
|
...current,
|
||||||
|
holeOptions: current.holeOptions.filter(
|
||||||
|
(entry) => entry.holeId !== option.holeId,
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
className="rounded-full p-2 text-slate-500 hover:bg-white/72 disabled:opacity-40"
|
||||||
|
aria-label="删除洞口选项"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import type {
|
|||||||
SquareHoleHoleSnapshot,
|
SquareHoleHoleSnapshot,
|
||||||
SquareHoleRunSnapshot,
|
SquareHoleRunSnapshot,
|
||||||
} from '../../../packages/shared/src/contracts/squareHoleRuntime';
|
} from '../../../packages/shared/src/contracts/squareHoleRuntime';
|
||||||
|
import { ResolvedAssetImage } from '../ResolvedAssetImage';
|
||||||
|
|
||||||
type SquareHoleRuntimeShellProps = {
|
type SquareHoleRuntimeShellProps = {
|
||||||
run: SquareHoleRunSnapshot | null;
|
run: SquareHoleRunSnapshot | null;
|
||||||
@@ -241,7 +242,17 @@ export function SquareHoleRuntimeShell({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="relative flex min-h-dvh w-full justify-center overflow-hidden bg-[#101827] text-white">
|
<main className="relative flex min-h-dvh w-full justify-center overflow-hidden bg-[#101827] text-white">
|
||||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_10%,rgba(125,211,252,0.32),transparent_27%),radial-gradient(circle_at_80%_80%,rgba(248,113,113,0.24),transparent_34%),linear-gradient(180deg,#1f3a5f_0%,#152238_48%,#111827_100%)]" />
|
{run.backgroundImageSrc ? (
|
||||||
|
<ResolvedAssetImage
|
||||||
|
src={run.backgroundImageSrc}
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute inset-0 h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_10%,rgba(125,211,252,0.32),transparent_27%),radial-gradient(circle_at_80%_80%,rgba(248,113,113,0.24),transparent_34%),linear-gradient(180deg,#1f3a5f_0%,#152238_48%,#111827_100%)]" />
|
||||||
|
)}
|
||||||
|
<div className="absolute inset-0 bg-slate-950/42" />
|
||||||
<div
|
<div
|
||||||
className="relative flex min-h-dvh min-w-0 flex-col overflow-hidden px-3 pb-[calc(env(safe-area-inset-bottom,0px)+0.8rem)] pt-[calc(env(safe-area-inset-top,0px)+0.65rem)]"
|
className="relative flex min-h-dvh min-w-0 flex-col overflow-hidden px-3 pb-[calc(env(safe-area-inset-bottom,0px)+0.8rem)] pt-[calc(env(safe-area-inset-top,0px)+0.65rem)]"
|
||||||
style={{
|
style={{
|
||||||
@@ -287,14 +298,14 @@ export function SquareHoleRuntimeShell({
|
|||||||
|
|
||||||
<section className="mt-3 rounded-[1.5rem] border border-white/14 bg-black/18 p-3 shadow-[0_18px_42px_rgba(15,23,42,0.28)] backdrop-blur">
|
<section className="mt-3 rounded-[1.5rem] border border-white/14 bg-black/18 p-3 shadow-[0_18px_42px_rgba(15,23,42,0.28)] backdrop-blur">
|
||||||
<div className="flex items-center justify-between gap-2 text-xs font-bold text-white/68">
|
<div className="flex items-center justify-between gap-2 text-xs font-bold text-white/68">
|
||||||
<span>{run.ruleLabel}</span>
|
<span>当前形状</span>
|
||||||
<span>v{run.snapshotVersion}</span>
|
<span>{progressText}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 flex min-h-[12rem] items-center justify-center rounded-[1.35rem] border border-white/10 bg-white/10">
|
<div className="mt-3 flex min-h-[12rem] items-center justify-center rounded-[1.35rem] border border-white/10 bg-white/10">
|
||||||
{currentShape ? (
|
{currentShape ? (
|
||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<div
|
<div
|
||||||
className={`h-24 w-24 shadow-[0_18px_38px_rgba(15,23,42,0.34)] ${getShapePreviewClass(
|
className={`relative h-24 w-24 overflow-hidden shadow-[0_18px_38px_rgba(15,23,42,0.34)] ${getShapePreviewClass(
|
||||||
currentShape.shapeKind,
|
currentShape.shapeKind,
|
||||||
)}`}
|
)}`}
|
||||||
style={{
|
style={{
|
||||||
@@ -302,7 +313,16 @@ export function SquareHoleRuntimeShell({
|
|||||||
currentShape.color ||
|
currentShape.color ||
|
||||||
'linear-gradient(135deg,#f8fafc,#38bdf8)',
|
'linear-gradient(135deg,#f8fafc,#38bdf8)',
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
{currentShape.imageSrc ? (
|
||||||
|
<ResolvedAssetImage
|
||||||
|
src={currentShape.imageSrc}
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
className="h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
<div className="flex items-center gap-2 text-base font-black">
|
<div className="flex items-center gap-2 text-base font-black">
|
||||||
<Shapes size={18} />
|
<Shapes size={18} />
|
||||||
<span>{currentShape.label}</span>
|
<span>{currentShape.label}</span>
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ import type {
|
|||||||
CustomWorldGenerationProgress,
|
CustomWorldGenerationProgress,
|
||||||
CustomWorldGenerationStep,
|
CustomWorldGenerationStep,
|
||||||
} from '../../packages/shared/src/contracts/runtime';
|
} from '../../packages/shared/src/contracts/runtime';
|
||||||
|
import type { SquareHoleSessionSnapshot } from '../../packages/shared/src/contracts/squareHoleAgent';
|
||||||
import type { CustomWorldStructuredAnchorEntry } from './customWorldAgentGenerationProgress';
|
import type { CustomWorldStructuredAnchorEntry } from './customWorldAgentGenerationProgress';
|
||||||
|
|
||||||
export type MiniGameDraftGenerationKind = 'puzzle' | 'big-fish';
|
export type MiniGameDraftGenerationKind = 'puzzle' | 'big-fish' | 'square-hole';
|
||||||
|
|
||||||
export type MiniGameDraftGenerationPhase =
|
export type MiniGameDraftGenerationPhase =
|
||||||
| 'idle'
|
| 'idle'
|
||||||
@@ -17,6 +18,10 @@ export type MiniGameDraftGenerationPhase =
|
|||||||
| 'big-fish-draft'
|
| 'big-fish-draft'
|
||||||
| 'big-fish-levels'
|
| 'big-fish-levels'
|
||||||
| 'big-fish-runtime'
|
| 'big-fish-runtime'
|
||||||
|
| 'square-hole-draft'
|
||||||
|
| 'square-hole-cover'
|
||||||
|
| 'square-hole-shapes'
|
||||||
|
| 'square-hole-ready'
|
||||||
| 'puzzle-images'
|
| 'puzzle-images'
|
||||||
| 'puzzle-select-image'
|
| 'puzzle-select-image'
|
||||||
| 'ready'
|
| 'ready'
|
||||||
@@ -86,12 +91,39 @@ const BIG_FISH_STEPS = [
|
|||||||
},
|
},
|
||||||
] as const satisfies ReadonlyArray<MiniGameStepDefinition>;
|
] as const satisfies ReadonlyArray<MiniGameStepDefinition>;
|
||||||
|
|
||||||
|
const SQUARE_HOLE_STEPS = [
|
||||||
|
{
|
||||||
|
id: 'square-hole-draft',
|
||||||
|
label: '整理玩法草稿',
|
||||||
|
detail: '收拢题材、形状、洞口与加分选项。',
|
||||||
|
weight: 28,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'square-hole-cover',
|
||||||
|
label: '生成封面与背景',
|
||||||
|
detail: '生成作品封面和运行背景。',
|
||||||
|
weight: 32,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'square-hole-shapes',
|
||||||
|
label: '生成形状贴图',
|
||||||
|
detail: '为每个可投放形状生成贴图。',
|
||||||
|
weight: 40,
|
||||||
|
},
|
||||||
|
] as const satisfies ReadonlyArray<MiniGameStepDefinition>;
|
||||||
|
|
||||||
function clampProgress(value: number) {
|
function clampProgress(value: number) {
|
||||||
return Math.max(0, Math.min(100, Math.round(value)));
|
return Math.max(0, Math.min(100, Math.round(value)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStepDefinitions(kind: MiniGameDraftGenerationKind) {
|
function getStepDefinitions(kind: MiniGameDraftGenerationKind) {
|
||||||
return kind === 'puzzle' ? PUZZLE_STEPS : BIG_FISH_STEPS;
|
if (kind === 'puzzle') {
|
||||||
|
return PUZZLE_STEPS;
|
||||||
|
}
|
||||||
|
if (kind === 'square-hole') {
|
||||||
|
return SQUARE_HOLE_STEPS;
|
||||||
|
}
|
||||||
|
return BIG_FISH_STEPS;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getActiveStepIndex(
|
function getActiveStepIndex(
|
||||||
@@ -132,7 +164,12 @@ export function createMiniGameDraftGenerationState(
|
|||||||
): MiniGameDraftGenerationState {
|
): MiniGameDraftGenerationState {
|
||||||
return {
|
return {
|
||||||
kind,
|
kind,
|
||||||
phase: kind === 'big-fish' ? 'big-fish-draft' : 'compile',
|
phase:
|
||||||
|
kind === 'big-fish'
|
||||||
|
? 'big-fish-draft'
|
||||||
|
: kind === 'square-hole'
|
||||||
|
? 'square-hole-draft'
|
||||||
|
: 'compile',
|
||||||
startedAtMs: Date.now(),
|
startedAtMs: Date.now(),
|
||||||
completedAssetCount: 0,
|
completedAssetCount: 0,
|
||||||
totalAssetCount: 0,
|
totalAssetCount: 0,
|
||||||
@@ -152,6 +189,18 @@ function resolveBigFishPhaseByElapsedMs(
|
|||||||
return 'big-fish-draft';
|
return 'big-fish-draft';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveSquareHolePhaseByElapsedMs(
|
||||||
|
elapsedMs: number,
|
||||||
|
): MiniGameDraftGenerationPhase {
|
||||||
|
if (elapsedMs >= 6_500) {
|
||||||
|
return 'square-hole-shapes';
|
||||||
|
}
|
||||||
|
if (elapsedMs >= 2_400) {
|
||||||
|
return 'square-hole-cover';
|
||||||
|
}
|
||||||
|
return 'square-hole-draft';
|
||||||
|
}
|
||||||
|
|
||||||
export function buildMiniGameDraftGenerationProgress(
|
export function buildMiniGameDraftGenerationProgress(
|
||||||
state: MiniGameDraftGenerationState | null,
|
state: MiniGameDraftGenerationState | null,
|
||||||
nowMs = Date.now(),
|
nowMs = Date.now(),
|
||||||
@@ -169,6 +218,13 @@ export function buildMiniGameDraftGenerationProgress(
|
|||||||
...state,
|
...state,
|
||||||
phase: resolveBigFishPhaseByElapsedMs(elapsedMs),
|
phase: resolveBigFishPhaseByElapsedMs(elapsedMs),
|
||||||
}
|
}
|
||||||
|
: state.kind === 'square-hole' &&
|
||||||
|
state.phase !== 'failed' &&
|
||||||
|
state.phase !== 'ready'
|
||||||
|
? {
|
||||||
|
...state,
|
||||||
|
phase: resolveSquareHolePhaseByElapsedMs(elapsedMs),
|
||||||
|
}
|
||||||
: state;
|
: state;
|
||||||
|
|
||||||
const steps = getStepDefinitions(normalizedState.kind);
|
const steps = getStepDefinitions(normalizedState.kind);
|
||||||
@@ -190,6 +246,8 @@ export function buildMiniGameDraftGenerationProgress(
|
|||||||
? 1
|
? 1
|
||||||
: normalizedState.kind === 'big-fish'
|
: normalizedState.kind === 'big-fish'
|
||||||
? 0.55
|
? 0.55
|
||||||
|
: normalizedState.kind === 'square-hole'
|
||||||
|
? 0.42
|
||||||
: 0;
|
: 0;
|
||||||
const overallProgress =
|
const overallProgress =
|
||||||
normalizedState.phase === 'failed'
|
normalizedState.phase === 'failed'
|
||||||
@@ -223,6 +281,8 @@ export function buildMiniGameDraftGenerationProgress(
|
|||||||
? 0
|
? 0
|
||||||
: normalizedState.kind === 'big-fish'
|
: normalizedState.kind === 'big-fish'
|
||||||
? Math.max(0, 7_000 - elapsedMs)
|
? Math.max(0, 7_000 - elapsedMs)
|
||||||
|
: normalizedState.kind === 'square-hole'
|
||||||
|
? Math.max(0, 12_000 - elapsedMs)
|
||||||
: null,
|
: null,
|
||||||
activeStepIndex,
|
activeStepIndex,
|
||||||
steps: buildMiniGameProgressSteps(steps, activeStepIndex, normalizedState),
|
steps: buildMiniGameProgressSteps(steps, activeStepIndex, normalizedState),
|
||||||
@@ -306,3 +366,46 @@ export function buildBigFishGenerationAnchorEntries(
|
|||||||
}))
|
}))
|
||||||
.filter((entry) => entry.value.trim());
|
.filter((entry) => entry.value.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildSquareHoleGenerationAnchorEntries(
|
||||||
|
session: SquareHoleSessionSnapshot | null | undefined,
|
||||||
|
): CustomWorldStructuredAnchorEntry[] {
|
||||||
|
if (!session) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const draft = session.draft;
|
||||||
|
const shapeCount =
|
||||||
|
draft?.shapeOptions.filter((option) => option.imageSrc?.trim()).length ??
|
||||||
|
session.config.shapeOptions.filter((option) => option.imageSrc?.trim())
|
||||||
|
.length;
|
||||||
|
const totalShapeCount =
|
||||||
|
draft?.shapeOptions.length || session.config.shapeOptions.length;
|
||||||
|
|
||||||
|
const entries: Array<MiniGameAnchorSource | null> = [
|
||||||
|
{
|
||||||
|
key: 'square-hole-title',
|
||||||
|
label: '作品名称',
|
||||||
|
value: draft?.gameName || `${session.config.themeText}方洞挑战`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'square-hole-theme',
|
||||||
|
label: '题材与规则',
|
||||||
|
value: `${session.config.themeText}|${session.config.twistRule}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'square-hole-options',
|
||||||
|
label: '选项资产',
|
||||||
|
value: totalShapeCount > 0 ? `形状贴图 ${shapeCount}/${totalShapeCount}` : '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return entries
|
||||||
|
.filter((entry): entry is MiniGameAnchorSource => Boolean(entry))
|
||||||
|
.map((entry) => ({
|
||||||
|
id: entry.key,
|
||||||
|
label: entry.label,
|
||||||
|
value: entry.value,
|
||||||
|
}))
|
||||||
|
.filter((entry) => entry.value.trim());
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user