add long objective setting func
This commit is contained in:
@@ -177,3 +177,15 @@ def set_user_long_term_objective(avatar: "Avatar", objective_content: str) -> No
|
||||
)
|
||||
logger.info(f"玩家为角色 {avatar.name} 设定长期目标:{objective_content}")
|
||||
|
||||
|
||||
def clear_user_long_term_objective(avatar: "Avatar") -> bool:
|
||||
"""
|
||||
清空玩家设定的长期目标
|
||||
如果当前目标是 system/llm 生成的,则不清除并返回 False
|
||||
如果是 user 生成的,清除并返回 True
|
||||
"""
|
||||
if avatar.long_term_objective and avatar.long_term_objective.origin == "user":
|
||||
avatar.long_term_objective = None
|
||||
logger.info(f"玩家清空了角色 {avatar.name} 的长期目标")
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -7,6 +7,7 @@ from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException, Quer
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
import uvicorn
|
||||
from pydantic import BaseModel
|
||||
|
||||
# 确保可以导入 src 模块
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||
@@ -20,6 +21,7 @@ from src.utils.config import CONFIG
|
||||
from src.classes.sect import sects_by_id
|
||||
from src.classes.color import serialize_hover_lines
|
||||
from src.classes.event import Event
|
||||
from src.classes.long_term_objective import set_user_long_term_objective, clear_user_long_term_objective
|
||||
import random
|
||||
|
||||
# 全局游戏实例
|
||||
@@ -71,7 +73,6 @@ def serialize_events_for_client(events: List[Event]) -> List[dict]:
|
||||
year = None
|
||||
try:
|
||||
month_obj = month_stamp.get_month()
|
||||
month = int(getattr(month_obj, "value", month_obj))
|
||||
except Exception:
|
||||
month = None
|
||||
|
||||
@@ -374,6 +375,42 @@ def get_hover_info(
|
||||
"lines": serialize_hover_lines([str(line) for line in lines]),
|
||||
}
|
||||
|
||||
class SetObjectiveRequest(BaseModel):
|
||||
avatar_id: str
|
||||
content: str
|
||||
|
||||
class ClearObjectiveRequest(BaseModel):
|
||||
avatar_id: str
|
||||
|
||||
@app.post("/api/action/set_long_term_objective")
|
||||
def set_long_term_objective(req: SetObjectiveRequest):
|
||||
world = game_instance.get("world")
|
||||
if not world:
|
||||
raise HTTPException(status_code=503, detail="World not initialized")
|
||||
|
||||
avatar = world.avatar_manager.avatars.get(req.avatar_id)
|
||||
if not avatar:
|
||||
raise HTTPException(status_code=404, detail="Avatar not found")
|
||||
|
||||
set_user_long_term_objective(avatar, req.content)
|
||||
return {"status": "ok", "message": "Objective set"}
|
||||
|
||||
@app.post("/api/action/clear_long_term_objective")
|
||||
def clear_long_term_objective(req: ClearObjectiveRequest):
|
||||
world = game_instance.get("world")
|
||||
if not world:
|
||||
raise HTTPException(status_code=503, detail="World not initialized")
|
||||
|
||||
avatar = world.avatar_manager.avatars.get(req.avatar_id)
|
||||
if not avatar:
|
||||
raise HTTPException(status_code=404, detail="Avatar not found")
|
||||
|
||||
cleared = clear_user_long_term_objective(avatar)
|
||||
return {
|
||||
"status": "ok",
|
||||
"message": "Objective cleared" if cleared else "No user objective to clear"
|
||||
}
|
||||
|
||||
def start():
|
||||
"""启动服务的入口函数"""
|
||||
# 改为 8002 端口
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
要求与约束:
|
||||
- thought从侧面体现出角色特质、宗门信息等
|
||||
- 长期目标是非常重要的一个参数,其权重最高
|
||||
- 执行动作只能从给定的全部动作中选,且需满足对应条件,见动作的requirements文本
|
||||
- 一些动作需要先移动满足某些条件才可执行,可以适当规划。
|
||||
- 和另一个角色交互的动作,必须在对应角色附近。执行前可以先MoveToAvatar
|
||||
@@ -6,11 +6,16 @@ const store = useGameStore()
|
||||
const panelRef = ref<HTMLElement | null>(null)
|
||||
let lastOpenAt = 0
|
||||
|
||||
const showObjectiveModal = ref(false)
|
||||
const objectiveContent = ref('')
|
||||
|
||||
const title = computed(() => store.selectedTarget?.name ?? '')
|
||||
|
||||
watch(
|
||||
() => store.selectedTarget,
|
||||
(target) => {
|
||||
showObjectiveModal.value = false
|
||||
objectiveContent.value = ''
|
||||
if (target) {
|
||||
lastOpenAt = performance.now()
|
||||
}
|
||||
@@ -29,6 +34,20 @@ function handleDocumentPointerDown(event: PointerEvent) {
|
||||
store.closeInfoPanel()
|
||||
}
|
||||
|
||||
async function handleSetObjective() {
|
||||
if (!store.selectedTarget || !objectiveContent.value.trim()) return
|
||||
await store.setLongTermObjective(store.selectedTarget.id, objectiveContent.value)
|
||||
showObjectiveModal.value = false
|
||||
objectiveContent.value = ''
|
||||
}
|
||||
|
||||
async function handleClearObjective() {
|
||||
if (!store.selectedTarget) return
|
||||
if (confirm('确定要清空该角色的长期目标吗?(系统将在之后自动重新生成)')) {
|
||||
await store.clearLongTermObjective(store.selectedTarget.id)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('pointerdown', handleDocumentPointerDown)
|
||||
})
|
||||
@@ -44,6 +63,11 @@ onUnmounted(() => {
|
||||
class="info-panel"
|
||||
ref="panelRef"
|
||||
>
|
||||
<div class="panel-actions" v-if="store.selectedTarget.type === 'avatar'">
|
||||
<button class="action-btn primary" @click="showObjectiveModal = true">设定长期目标</button>
|
||||
<button class="action-btn secondary" @click="handleClearObjective">清空长期目标</button>
|
||||
</div>
|
||||
|
||||
<div class="info-header">
|
||||
<div class="info-title">{{ title || '详情' }}</div>
|
||||
<button class="close-btn" type="button" @click="store.closeInfoPanel()">×</button>
|
||||
@@ -70,6 +94,20 @@ onUnmounted(() => {
|
||||
</ul>
|
||||
<div v-else class="placeholder">暂无信息</div>
|
||||
</div>
|
||||
|
||||
<!-- 长期目标设定弹窗 -->
|
||||
<div v-if="showObjectiveModal" class="objective-modal">
|
||||
<div class="modal-title">设定长期目标</div>
|
||||
<textarea
|
||||
v-model="objectiveContent"
|
||||
placeholder="请输入该角色未来3-5年的长期目标..."
|
||||
class="objective-input"
|
||||
></textarea>
|
||||
<div class="modal-actions">
|
||||
<button class="modal-btn confirm" @click="handleSetObjective" :disabled="!objectiveContent.trim()">确认</button>
|
||||
<button class="modal-btn cancel" @click="showObjectiveModal = false">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -88,7 +126,51 @@ onUnmounted(() => {
|
||||
pointer-events: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
overflow: visible; /* Allow modal to show outside */
|
||||
}
|
||||
|
||||
.panel-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px 14px 4px;
|
||||
gap: 6px;
|
||||
background: rgba(38, 38, 38, 0.95);
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: #177ddc;
|
||||
color: white;
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.action-btn.primary:hover {
|
||||
background: #1890ff;
|
||||
}
|
||||
|
||||
.action-btn.secondary {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
color: #aaa;
|
||||
padding: 5px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.action-btn.secondary:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-color: rgba(255, 255, 255, 0.25);
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.info-header {
|
||||
@@ -123,6 +205,9 @@ onUnmounted(() => {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px 16px;
|
||||
/* 确保body内容滚动时圆角 */
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
}
|
||||
|
||||
.info-list {
|
||||
@@ -149,5 +234,86 @@ onUnmounted(() => {
|
||||
.placeholder.error {
|
||||
color: #ff7875;
|
||||
}
|
||||
</style>
|
||||
|
||||
/* Modal Styles */
|
||||
.objective-modal {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 100%; /* Position to the left of the panel */
|
||||
margin-right: 12px;
|
||||
width: 280px;
|
||||
background: rgba(32, 32, 32, 0.98);
|
||||
border: 1px solid #444;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.objective-input {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
background: #1f1f1f;
|
||||
border: 1px solid #444;
|
||||
border-radius: 4px;
|
||||
color: #eee;
|
||||
padding: 8px;
|
||||
resize: none;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.objective-input:focus {
|
||||
border-color: #177ddc;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.modal-btn {
|
||||
flex: 1;
|
||||
padding: 6px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.modal-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.modal-btn.confirm {
|
||||
background: #177ddc;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.modal-btn.confirm:hover:not(:disabled) {
|
||||
background: #1890ff;
|
||||
}
|
||||
|
||||
.modal-btn.cancel {
|
||||
background: #444;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.modal-btn.cancel:hover {
|
||||
background: #555;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -19,3 +19,28 @@ export async function apiGet<T>(url: string, options: ApiRequestOptions = {}): P
|
||||
window.clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
|
||||
export async function apiPost<T>(url: string, body: any, options: ApiRequestOptions = {}): Promise<T> {
|
||||
const { timeout = DEFAULT_TIMEOUT, headers, ...init } = options
|
||||
const controller = new AbortController()
|
||||
const timer = window.setTimeout(() => controller.abort(), timeout)
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...headers
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: controller.signal,
|
||||
...init
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求失败:${response.status}`)
|
||||
}
|
||||
return (await response.json()) as T
|
||||
} finally {
|
||||
window.clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import type {
|
||||
InitialStateResponse,
|
||||
MapResponse
|
||||
} from '../types/game'
|
||||
import { apiGet } from './apiClient'
|
||||
import { apiGet, apiPost } from './apiClient'
|
||||
|
||||
function buildHoverQuery(target: HoverTarget) {
|
||||
const query = new URLSearchParams({
|
||||
@@ -25,6 +25,18 @@ export const gameApi = {
|
||||
|
||||
getMap() {
|
||||
return apiGet<MapResponse>('/api/map')
|
||||
},
|
||||
|
||||
setLongTermObjective(avatarId: string, content: string) {
|
||||
return apiPost<{ status: string; message: string }>('/api/action/set_long_term_objective', {
|
||||
avatar_id: avatarId,
|
||||
content
|
||||
})
|
||||
},
|
||||
|
||||
clearLongTermObjective(avatarId: string) {
|
||||
return apiPost<{ status: string; message: string }>('/api/action/clear_long_term_objective', {
|
||||
avatar_id: avatarId
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -111,21 +111,23 @@ export const useGameStore = defineStore('game', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchHoverInfo(target: HoverTarget) {
|
||||
async function fetchHoverInfo(target: HoverTarget, forceRefresh = false) {
|
||||
const key = cacheKey(target)
|
||||
const cached = hoverCache.get(key)
|
||||
if (cached) {
|
||||
if (selectedTarget.value && cacheKey(selectedTarget.value) === key) {
|
||||
hoverInfo.value = cached
|
||||
if (!forceRefresh) {
|
||||
const cached = hoverCache.get(key)
|
||||
if (cached) {
|
||||
if (selectedTarget.value && cacheKey(selectedTarget.value) === key) {
|
||||
hoverInfo.value = cached
|
||||
}
|
||||
infoLoading.value = false
|
||||
infoError.value = null
|
||||
return
|
||||
}
|
||||
infoLoading.value = false
|
||||
infoError.value = null
|
||||
return
|
||||
}
|
||||
|
||||
infoLoading.value = true
|
||||
infoError.value = null
|
||||
hoverInfo.value = []
|
||||
if (!forceRefresh) hoverInfo.value = []
|
||||
|
||||
try {
|
||||
const data = await gameApi.getHoverInfo(target)
|
||||
@@ -146,6 +148,22 @@ export const useGameStore = defineStore('game', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function setLongTermObjective(avatarId: string, content: string) {
|
||||
await gameApi.setLongTermObjective(avatarId, content)
|
||||
// 成功后刷新 info panel
|
||||
if (selectedTarget.value && selectedTarget.value.id === avatarId && selectedTarget.value.type === 'avatar') {
|
||||
await fetchHoverInfo(selectedTarget.value, true)
|
||||
}
|
||||
}
|
||||
|
||||
async function clearLongTermObjective(avatarId: string) {
|
||||
await gameApi.clearLongTermObjective(avatarId)
|
||||
// 成功后刷新 info panel
|
||||
if (selectedTarget.value && selectedTarget.value.id === avatarId && selectedTarget.value.type === 'avatar') {
|
||||
await fetchHoverInfo(selectedTarget.value, true)
|
||||
}
|
||||
}
|
||||
|
||||
function connect() {
|
||||
gateway.connect()
|
||||
}
|
||||
@@ -181,6 +199,8 @@ export const useGameStore = defineStore('game', () => {
|
||||
disconnect,
|
||||
fetchInitialState,
|
||||
openInfoPanel,
|
||||
closeInfoPanel
|
||||
closeInfoPanel,
|
||||
setLongTermObjective,
|
||||
clearLongTermObjective
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user