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}")
|
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.middleware.cors import CORSMiddleware
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
# 确保可以导入 src 模块
|
# 确保可以导入 src 模块
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
|
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.sect import sects_by_id
|
||||||
from src.classes.color import serialize_hover_lines
|
from src.classes.color import serialize_hover_lines
|
||||||
from src.classes.event import Event
|
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
|
import random
|
||||||
|
|
||||||
# 全局游戏实例
|
# 全局游戏实例
|
||||||
@@ -71,7 +73,6 @@ def serialize_events_for_client(events: List[Event]) -> List[dict]:
|
|||||||
year = None
|
year = None
|
||||||
try:
|
try:
|
||||||
month_obj = month_stamp.get_month()
|
month_obj = month_stamp.get_month()
|
||||||
month = int(getattr(month_obj, "value", month_obj))
|
|
||||||
except Exception:
|
except Exception:
|
||||||
month = None
|
month = None
|
||||||
|
|
||||||
@@ -374,6 +375,42 @@ def get_hover_info(
|
|||||||
"lines": serialize_hover_lines([str(line) for line in lines]),
|
"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():
|
def start():
|
||||||
"""启动服务的入口函数"""
|
"""启动服务的入口函数"""
|
||||||
# 改为 8002 端口
|
# 改为 8002 端口
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
|
|
||||||
要求与约束:
|
要求与约束:
|
||||||
- thought从侧面体现出角色特质、宗门信息等
|
- thought从侧面体现出角色特质、宗门信息等
|
||||||
|
- 长期目标是非常重要的一个参数,其权重最高
|
||||||
- 执行动作只能从给定的全部动作中选,且需满足对应条件,见动作的requirements文本
|
- 执行动作只能从给定的全部动作中选,且需满足对应条件,见动作的requirements文本
|
||||||
- 一些动作需要先移动满足某些条件才可执行,可以适当规划。
|
- 一些动作需要先移动满足某些条件才可执行,可以适当规划。
|
||||||
- 和另一个角色交互的动作,必须在对应角色附近。执行前可以先MoveToAvatar
|
- 和另一个角色交互的动作,必须在对应角色附近。执行前可以先MoveToAvatar
|
||||||
@@ -6,11 +6,16 @@ const store = useGameStore()
|
|||||||
const panelRef = ref<HTMLElement | null>(null)
|
const panelRef = ref<HTMLElement | null>(null)
|
||||||
let lastOpenAt = 0
|
let lastOpenAt = 0
|
||||||
|
|
||||||
|
const showObjectiveModal = ref(false)
|
||||||
|
const objectiveContent = ref('')
|
||||||
|
|
||||||
const title = computed(() => store.selectedTarget?.name ?? '')
|
const title = computed(() => store.selectedTarget?.name ?? '')
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => store.selectedTarget,
|
() => store.selectedTarget,
|
||||||
(target) => {
|
(target) => {
|
||||||
|
showObjectiveModal.value = false
|
||||||
|
objectiveContent.value = ''
|
||||||
if (target) {
|
if (target) {
|
||||||
lastOpenAt = performance.now()
|
lastOpenAt = performance.now()
|
||||||
}
|
}
|
||||||
@@ -29,6 +34,20 @@ function handleDocumentPointerDown(event: PointerEvent) {
|
|||||||
store.closeInfoPanel()
|
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(() => {
|
onMounted(() => {
|
||||||
document.addEventListener('pointerdown', handleDocumentPointerDown)
|
document.addEventListener('pointerdown', handleDocumentPointerDown)
|
||||||
})
|
})
|
||||||
@@ -44,6 +63,11 @@ onUnmounted(() => {
|
|||||||
class="info-panel"
|
class="info-panel"
|
||||||
ref="panelRef"
|
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-header">
|
||||||
<div class="info-title">{{ title || '详情' }}</div>
|
<div class="info-title">{{ title || '详情' }}</div>
|
||||||
<button class="close-btn" type="button" @click="store.closeInfoPanel()">×</button>
|
<button class="close-btn" type="button" @click="store.closeInfoPanel()">×</button>
|
||||||
@@ -70,6 +94,20 @@ onUnmounted(() => {
|
|||||||
</ul>
|
</ul>
|
||||||
<div v-else class="placeholder">暂无信息</div>
|
<div v-else class="placeholder">暂无信息</div>
|
||||||
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -88,7 +126,51 @@ onUnmounted(() => {
|
|||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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 {
|
.info-header {
|
||||||
@@ -123,6 +205,9 @@ onUnmounted(() => {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
|
/* 确保body内容滚动时圆角 */
|
||||||
|
border-bottom-left-radius: 8px;
|
||||||
|
border-bottom-right-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-list {
|
.info-list {
|
||||||
@@ -149,5 +234,86 @@ onUnmounted(() => {
|
|||||||
.placeholder.error {
|
.placeholder.error {
|
||||||
color: #ff7875;
|
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)
|
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,
|
InitialStateResponse,
|
||||||
MapResponse
|
MapResponse
|
||||||
} from '../types/game'
|
} from '../types/game'
|
||||||
import { apiGet } from './apiClient'
|
import { apiGet, apiPost } from './apiClient'
|
||||||
|
|
||||||
function buildHoverQuery(target: HoverTarget) {
|
function buildHoverQuery(target: HoverTarget) {
|
||||||
const query = new URLSearchParams({
|
const query = new URLSearchParams({
|
||||||
@@ -25,6 +25,18 @@ export const gameApi = {
|
|||||||
|
|
||||||
getMap() {
|
getMap() {
|
||||||
return apiGet<MapResponse>('/api/map')
|
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 key = cacheKey(target)
|
||||||
const cached = hoverCache.get(key)
|
if (!forceRefresh) {
|
||||||
if (cached) {
|
const cached = hoverCache.get(key)
|
||||||
if (selectedTarget.value && cacheKey(selectedTarget.value) === key) {
|
if (cached) {
|
||||||
hoverInfo.value = 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
|
infoLoading.value = true
|
||||||
infoError.value = null
|
infoError.value = null
|
||||||
hoverInfo.value = []
|
if (!forceRefresh) hoverInfo.value = []
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await gameApi.getHoverInfo(target)
|
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() {
|
function connect() {
|
||||||
gateway.connect()
|
gateway.connect()
|
||||||
}
|
}
|
||||||
@@ -181,6 +199,8 @@ export const useGameStore = defineStore('game', () => {
|
|||||||
disconnect,
|
disconnect,
|
||||||
fetchInitialState,
|
fetchInitialState,
|
||||||
openInfoPanel,
|
openInfoPanel,
|
||||||
closeInfoPanel
|
closeInfoPanel,
|
||||||
|
setLongTermObjective,
|
||||||
|
clearLongTermObjective
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user