4984 lines
202 KiB
JSON
4984 lines
202 KiB
JSON
```
|
||
<html><head><style>
|
||
/* --- Modern Theme Overrides --- */
|
||
:root {
|
||
--survival-primary: #111828;
|
||
--survival-secondary: rgba(32, 45, 68, 0.75);
|
||
--survival-surface: rgba(20, 34, 54, 0.78);
|
||
--survival-surface-strong: rgba(24, 36, 58, 0.92);
|
||
--survival-glass-border: rgba(148, 197, 255, 0.18);
|
||
--survival-accent: #5ea1ff;
|
||
--survival-accent-strong: #8fd6ff;
|
||
--survival-warning: #ffb86c;
|
||
--survival-success: #58f0a7;
|
||
--survival-danger: #ff7b9c;
|
||
--survival-text: #eff6ff;
|
||
--survival-muted: #a0aec0;
|
||
--survival-border: rgba(148, 197, 255, 0.25);
|
||
--survival-shadow: 0 24px 60px rgba(6, 12, 25, 0.55);
|
||
--survival-glow: 0 0 30px rgba(94, 161, 255, 0.35);
|
||
}
|
||
|
||
body {
|
||
background: radial-gradient(circle at 20% 20%, rgba(94, 161, 255, 0.12), transparent 55%),
|
||
radial-gradient(circle at 80% 10%, rgba(255, 120, 180, 0.1), transparent 50%),
|
||
linear-gradient(160deg, #050a16, #101b2f 55%, #070b18);
|
||
font-family: "Inter", "Microsoft YaHei", "Segoe UI", Arial, sans-serif;
|
||
color: var(--survival-text);
|
||
min-height: 100vh;
|
||
margin: 0;
|
||
padding: 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background-attachment: fixed;
|
||
}
|
||
|
||
.survival-ui {
|
||
width: clamp(320px, 92vw, 1180px);
|
||
margin: 0 auto;
|
||
padding: 18px clamp(18px, 3vw, 28px) 28px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 28px;
|
||
border-radius: 28px;
|
||
background: var(--survival-surface);
|
||
border: 1px solid var(--survival-glass-border);
|
||
box-shadow: var(--survival-shadow);
|
||
backdrop-filter: blur(18px) saturate(160%);
|
||
position: relative;
|
||
overflow: hidden;
|
||
font-weight: 600;
|
||
font-family:"Microsoft YaHei","Segoe UI",Arial,sans-serif;color:var(--survival-text);gap:8px}
|
||
|
||
.survival-ui::before { /* 這是那個漂亮的左上角光暈 */
|
||
content: "";
|
||
position: absolute;
|
||
inset: -60% 35% 45% -40%;
|
||
background: radial-gradient(circle, rgba(94, 161, 255, 0.16), transparent 60%);
|
||
opacity: 0.6;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.survival-ui::-webkit-scrollbar{width:6px}.survival-ui::-webkit-scrollbar-track{background:rgba(0,0,0,.2);border-radius:3px}.survival-ui::-webkit-scrollbar-thumb{background:var(--survival-accent);border-radius:3px}
|
||
|
||
.section__title {
|
||
font-size: 15px;
|
||
letter-spacing: 0.18em;
|
||
text-transform: uppercase;
|
||
color: var(--survival-muted);
|
||
margin: 0 0 12px;
|
||
}
|
||
|
||
.section__list{list-style:none;margin:0;padding-left:12px;font-size:14px;line-height:1.35}
|
||
.item{display:flex;justify-content:space-between;gap:4px}
|
||
.item__key{white-space:nowrap}
|
||
.item__value{font-weight:600}
|
||
|
||
/* --- 裝備欄樣式 - 參考攜帶物品的對齊方式 --- */
|
||
|
||
/* 1. 裝備欄網格容器 - 1行4個 */
|
||
.survival-ui .inventory-grid.equipment-grid {
|
||
display: grid !important;
|
||
grid-template-columns: repeat(4, 1fr) !important; /* 一行4個 */
|
||
grid-template-rows: repeat(2, auto) !important; /* 兩行 */
|
||
gap: 15px !important;
|
||
margin-top: 20px !important;
|
||
margin-bottom: 20px !important;
|
||
}
|
||
|
||
/* 2. 裝備欄位基本樣式 */
|
||
.survival-ui .equipment-slot {
|
||
background: var(--survival-surface-strong) !important;
|
||
border: 1px solid var(--survival-glass-border) !important;
|
||
border-radius: 12px !important;
|
||
padding: 16px !important;
|
||
transition: all 0.3s ease !important;
|
||
min-height: 140px !important;
|
||
display: flex !important;
|
||
flex-direction: column !important;
|
||
align-items: center !important;
|
||
justify-content: flex-start !important;
|
||
text-align: center !important;
|
||
position: relative !important;
|
||
overflow: hidden !important;
|
||
}
|
||
|
||
/* 3. 裝備欄位懸停效果 */
|
||
.survival-ui .equipment-slot:hover {
|
||
transform: translateY(-3px) !important;
|
||
box-shadow:
|
||
0 8px 25px rgba(0, 0, 0, 0.3),
|
||
0 0 20px rgba(94, 161, 255, 0.15) !important;
|
||
background: rgba(40, 55, 85, 0.9) !important;
|
||
border-color: var(--survival-accent) !important;
|
||
}
|
||
|
||
/* 4. 裝備部位標題 - 20px字體 */
|
||
.survival-ui .equipment-slot .slot-title {
|
||
color: #d17d2e !important;
|
||
font-weight: 700 !important;
|
||
font-size: 20px !important;
|
||
text-shadow: none !important;
|
||
letter-spacing: 0.05em !important;
|
||
text-transform: uppercase !important;
|
||
margin-bottom: 12px !important;
|
||
padding-bottom: 8px !important;
|
||
border-bottom: 1px solid rgba(148, 197, 255, 0.2) !important;
|
||
line-height: 1.2 !important;
|
||
width: 100% !important;
|
||
text-align: center !important;
|
||
}
|
||
|
||
/* 5. 裝備名稱 - 16px字體,簡單樣式 */
|
||
.survival-ui .equipment-slot .equipment-item-name {
|
||
font-size: 16px !important;
|
||
font-weight: 600 !important;
|
||
color: var(--survival-accent-strong) !important;
|
||
margin-bottom: 10px !important;
|
||
padding: 8px 10px !important;
|
||
background: rgba(94, 161, 255, 0.1) !important;
|
||
border: 1px solid rgba(94, 161, 255, 0.2) !important;
|
||
border-radius: 6px !important;
|
||
width: 100% !important;
|
||
text-align: center !important;
|
||
word-wrap: break-word !important;
|
||
}
|
||
|
||
/* 6. 空裝備欄顯示 - 居中,20px字體 */
|
||
.survival-ui .equipment-slot .equipment-empty {
|
||
display: flex !important;
|
||
align-items: center !important;
|
||
justify-content: center !important;
|
||
height: 100% !important;
|
||
min-height: 80px !important;
|
||
font-size: 20px !important;
|
||
font-weight: 600 !important;
|
||
color: var(--survival-muted) !important;
|
||
width: 100% !important;
|
||
text-align: center !important;
|
||
position: absolute !important;
|
||
top: 50% !important;
|
||
left: 50% !important;
|
||
transform: translate(-50%, -50%) !important;
|
||
}
|
||
|
||
/* 7. 裝備屬性容器 */
|
||
.survival-ui .equipment-slot .equipment-item-props {
|
||
font-size: 12px !important;
|
||
color: var(--survival-text) !important;
|
||
text-align: left !important;
|
||
line-height: 1.4 !important;
|
||
margin-top: 10px !important;
|
||
padding: 8px !important;
|
||
background: rgba(20, 30, 50, 0.5) !important;
|
||
border-radius: 6px !important;
|
||
border: 1px solid rgba(94, 161, 255, 0.1) !important;
|
||
width: 100% !important;
|
||
}
|
||
|
||
/* 8. 裝備屬性行 */
|
||
.survival-ui .equipment-slot .equipment-prop {
|
||
display: flex !important;
|
||
align-items: flex-start !important;
|
||
margin-bottom: 4px !important;
|
||
font-size: 12px !important;
|
||
line-height: 1.4 !important;
|
||
}
|
||
|
||
.survival-ui .equipment-slot .equipment-prop .prop-key {
|
||
color: var(--survival-muted) !important;
|
||
white-space: nowrap !important;
|
||
flex-shrink: 0 !important;
|
||
min-width: 50px !important;
|
||
font-size: 12px !important;
|
||
}
|
||
|
||
.survival-ui .equipment-slot .equipment-prop .prop-sep {
|
||
flex-shrink: 0 !important;
|
||
white-space: nowrap !important;
|
||
margin-right: 4px !important;
|
||
font-size: 12px !important;
|
||
}
|
||
|
||
.survival-ui .equipment-slot .equipment-prop .prop-val {
|
||
word-break: break-word !important;
|
||
min-width: 0 !important;
|
||
font-weight: 500 !important;
|
||
font-size: 12px !important;
|
||
color: var(--survival-text) !important;
|
||
line-height: 1.4 !important;
|
||
}
|
||
|
||
/* 9. 響應式設計 */
|
||
@media (max-width: 1024px) {
|
||
.survival-ui .inventory-grid.equipment-grid {
|
||
grid-template-columns: repeat(2, 1fr) !important; /* 中等屏幕變為2列 */
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.survival-ui .inventory-grid.equipment-grid {
|
||
grid-template-columns: 1fr !important; /* 小屏幕變為單列 */
|
||
gap: 12px !important;
|
||
}
|
||
}
|
||
|
||
/* 10. 裝備欄位順序(10個裝備排成2行) */
|
||
/* 第一行:頭部、胸部、背部、左手 */
|
||
.survival-ui .equipment-slot[data-slot="頭部"] { order: 1; }
|
||
.survival-ui .equipment-slot[data-slot="頸部"] { order: 2; }
|
||
.survival-ui .equipment-slot[data-slot="胸部"] { order: 3; }
|
||
.survival-ui .equipment-slot[data-slot="背部"] { order: 4; }
|
||
.survival-ui .equipment-slot[data-slot="腰部"] { order: 5; }
|
||
/* 第二行:右手、腰部、腿部、腳部 */
|
||
.survival-ui .equipment-slot[data-slot="左手"] { order: 6; }
|
||
.survival-ui .equipment-slot[data-slot="右手"] { order: 7; }
|
||
.survival-ui .equipment-slot[data-slot="腿部"] { order: 8; }
|
||
.survival-ui .equipment-slot[data-slot="腳部"] { order: 9; }
|
||
.survival-ui .equipment-slot[data-slot="其他"] { order: 10; }
|
||
|
||
/* --- 同伴裝備欄樣式 --- */
|
||
.companion-equipment-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||
gap: 10px;
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.companion-equipment-slot {
|
||
background: rgba(30, 45, 70, 0.7);
|
||
border: 1px solid rgba(148, 197, 255, 0.2);
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
transition: all 0.2s ease;
|
||
min-height: 100px;
|
||
}
|
||
|
||
.companion-equipment-slot:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||
border-color: var(--survival-accent);
|
||
}
|
||
|
||
.companion-equipment-slot .slot-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #ffb86c;
|
||
margin-bottom: 8px;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.companion-equipment-slot .item-name {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: var(--survival-accent-strong);
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
/* --- 營地信息特殊樣式 v5.0 · 終 · By 果農 --- */
|
||
/* 步驟一:先讓所有孩子都乖乖站好,別自己亂加距離 */
|
||
.camp-info-item {
|
||
font-size: 16px !important;
|
||
font-weight: 700 !important;
|
||
padding: 0; /* 內邊距清零,讓它更緊湊 */
|
||
line-height: 1.6;
|
||
transition: all 0.2s ease;
|
||
/* 把個體的margin-bottom去掉,由容器統一管理 */
|
||
margin-bottom: 0 !important;
|
||
}
|
||
|
||
/* 步驟二:容器來發號施令,精準控制每個孩子之間的距離 */
|
||
.camp-info-container {
|
||
display: flex; /* 啟用flex佈局,這是現代佈局的魔法核心!*/
|
||
flex-direction: column; /* 讓孩子們垂直排隊 */
|
||
gap: 8px; /* 每個孩子之間自動產生8px的間距!不多不少,剛剛好!*/
|
||
margin-bottom: 28px; /* 容器和下面的物品列表保持24px的社交距離 */
|
||
/* 關鍵!把和上面標題的距離交給父級的padding,不再自己加margin-top */
|
||
}
|
||
|
||
|
||
/* 顏色和懸停效果還是原來那套,因為它們是無辜的 */
|
||
.camp-info-item .prop-key,
|
||
.camp-info-item .prop-sep {
|
||
color: #ffb86c !important;
|
||
}
|
||
|
||
.camp-info-item .prop-val {
|
||
color: var(--survival-text) !important;
|
||
}
|
||
|
||
.camp-info-item:hover .prop-key,
|
||
.camp-info-item:hover .prop-sep {
|
||
color: #ffd19a !important;
|
||
text-shadow: 0 0 8px rgba(255, 184, 108, 0.6);
|
||
}
|
||
|
||
.camp-info-item:hover .prop-val {
|
||
color: #ffffff !important;
|
||
}
|
||
|
||
.top-info {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 2fr) minmax(0, 3fr) minmax(0, 2fr);
|
||
gap: 18px;
|
||
padding: 22px clamp(18px, 3vw, 24px);
|
||
border-radius: 22px;
|
||
background: var(--survival-surface-strong);
|
||
border: 1px solid var(--survival-glass-border);
|
||
box-shadow: var(--survival-glow);
|
||
align-items: center;
|
||
}
|
||
|
||
@media (max-width: 960px) {
|
||
.top-info {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
.top-info .location-display {
|
||
grid-column: 1 / -1;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 720px) {
|
||
.top-info {
|
||
grid-template-columns: 1fr;
|
||
align-items: flex-start;
|
||
}
|
||
}
|
||
|
||
.top-info .time-display {
|
||
font-size: clamp(44px, 7vw, 66px);
|
||
font-weight: 700;
|
||
line-height: 1;
|
||
letter-spacing: -0.04em;
|
||
color: var(--survival-accent-strong);
|
||
text-shadow: 0 0 32px rgba(94, 161, 255, 0.45);
|
||
}
|
||
|
||
.top-info .info-stack {
|
||
display: grid;
|
||
gap: 12px;
|
||
}
|
||
|
||
.top-info .info-row {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12px 18px;
|
||
color: var(--survival-text); /* High contrast */
|
||
font-size: 15px;
|
||
margin-bottom: 4px; /* 確保行間距 */
|
||
}
|
||
.row-item { white-space: nowrap; }
|
||
|
||
.top-info .info-row .row-item:nth-child(3) {
|
||
color: #8fd6ff; /* 給月相特殊的顏色 */
|
||
font-weight: 600;
|
||
}
|
||
|
||
.top-info .location-display {
|
||
justify-self: end;
|
||
font-size: clamp(20px, 3.2vw, 30px);
|
||
font-weight: 600;
|
||
color: var(--survival-text);
|
||
}
|
||
@media (max-width: 720px) {
|
||
.top-info .location-display {
|
||
justify-self: start;
|
||
margin-top: 8px;
|
||
width: 100%;
|
||
font-size: 24px;
|
||
}
|
||
}
|
||
|
||
.core-stats-wrapper{display:flex;gap:12px;margin-top:10px}.core-stats-wrapper .core-stat-item{flex:1}
|
||
.progress-wrapper{margin-bottom:16px}
|
||
.mini-progress-wrapper{display:flex;align-items:center;gap:8px;flex:1;min-width:0}
|
||
.mini-progress-wrapper .mini-progress__text{font-size:12px;white-space:nowrap;min-width:40px;text-align:right}
|
||
|
||
@media(max-width: 480px){.mini-progress-wrapper{flex:1;width:100%}}
|
||
|
||
/* --- 【終極修正方案 v4.0 - 全局統一】 --- */
|
||
.collapsible {
|
||
margin-bottom: 24px;
|
||
border: 1px solid var(--survival-border);
|
||
border-radius: 6px;
|
||
overflow: hidden;
|
||
}
|
||
.collapsible__header {
|
||
/* 核心修改:啟用flex佈局來對齊標題和標籤 */
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
|
||
background: rgba(40, 58, 88, 0.85); /* 2. 底色:一個更有對比度的深藍色,且帶點透明感 */
|
||
padding: 16px 20px; /* 3. 高度:增加上下內邊距,自然就把高度撐起來了 */
|
||
font-size: 16px; /* 讓標題字大一點 */
|
||
color: #FFFFFF;/* 4. 字體顏色:使用你定義的亮藍色,更醒目 */
|
||
font-weight: 700; /* 加粗標題字 */
|
||
border-left: 5px solid var(--survival-accent); /* 左邊的指示條也加粗一點 */
|
||
transition: background 0.3s ease; /* 加個懸停過渡效果,更有質感 */
|
||
cursor: pointer;
|
||
}
|
||
|
||
/* 給鼠標懸停時加個交互效果 */
|
||
.collapsible__header:hover {
|
||
background: rgba(55, 78, 115, 0.95);
|
||
}
|
||
|
||
/* 2. 針對整個折疊欄容器 .collapsible */
|
||
.collapsible {
|
||
border-radius: 14px; /* 5. 圓角加大,從 6px 增加到 14px,看起來更圓潤 */
|
||
border: 1px solid var(--survival-glass-border); /* 邊框顏色用你定義的玻璃質感邊框,更統一 */
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); /* 加點陰影,讓它浮起來 */
|
||
margin-bottom: 16px; /* 增加一點和下面元素的間距 */
|
||
}
|
||
|
||
/* 3. 修改折疊後的內容區,讓它風格也統一 */
|
||
.collapsible__body {
|
||
background: rgba(10, 20, 35, 0.6); /* 內容區背景也深一點 */
|
||
padding: 20px; /* 內容區的內邊距也加大 */
|
||
border-top: 1px solid var(--survival-glass-border); /* 在頭部和內容之間加一條細線分割 */
|
||
display: none; /* 這一行至關重要!保證內容區默認隱藏 */
|
||
}
|
||
|
||
.collapsible__bars-container{display:flex;flex-wrap:wrap;gap:8px;flex:1;min-width:0}
|
||
@media(max-width: 480px){.collapsible__bars-container{flex-direction:column;width:100%}}
|
||
.collapsible.active .collapsible__header:after{transform:rotate(0)}
|
||
.collapsible.active .collapsible__header--with-progress .collapsible__indicator{transform:rotate(0)}
|
||
|
||
.temp-range-wrapper { margin:10px 0 15px; padding:10px; background:rgba(0,0,0,.15); border-radius:6px; font-size:14px; }
|
||
.nutrition-hydration-wrapper { display:flex; flex-direction:column; gap:10px; background:rgba(0,0,0,.15); padding:10px; border-radius:4px; }
|
||
.phys-mental-wrapper { display:flex;gap:32px;flex-wrap:wrap; }
|
||
.phys-mental-wrapper .phys-col,
|
||
.phys-mental-wrapper .mental-col { flex:1 1 300px; min-width:260px; background:rgba(0,0,0,.15); padding:10px; border-radius:4px; }
|
||
.attr-skill-wrapper .attr-col,
|
||
.attr-skill-wrapper .skill-col { flex:1 1 300px; min-width:260px; background:rgba(0,0,0,.15); padding:10px; border-radius:4px; }
|
||
.task-bar { margin-bottom:12px; background:var(--survival-surface-strong); border:1px solid var(--survival-glass-border); border-radius:20px; padding:18px clamp(18px, 3vw, 24px); }
|
||
|
||
/* --- 從一號狀態欄移植的狀態效果CSS --- */
|
||
.status-effects-container {
|
||
margin-top: 12px; /* 與上方的溫度條保持間距 */
|
||
margin-bottom: 4px; /* 與下方的核心狀態條保持間距 */
|
||
padding: 10px;
|
||
background: rgba(0,0,0,.15);
|
||
border-radius: 6px;
|
||
}
|
||
.status-effects-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-top: 8px;
|
||
}
|
||
.status-effect-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
.status-effect-name {
|
||
padding: 4px 10px;
|
||
background: var(--survival-bg-light);
|
||
border: 1px solid var(--survival-border);
|
||
border-radius: 4px;
|
||
font-size: 13px;
|
||
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
|
||
white-space: nowrap; /* 防止標籤內文字換行 */
|
||
transition: all 0.2s ease;
|
||
cursor: default;
|
||
min-height: 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
/* 有效果的狀態名 - 可點選 */
|
||
.status-effect-name.has-effect {
|
||
cursor: pointer;
|
||
position: relative;
|
||
padding-right: 20px; /* 為箭頭留出空間 */
|
||
}
|
||
|
||
/* 有效果的狀態名 - 新增箭頭指示器 */
|
||
.status-effect-name.has-effect::after {
|
||
content: '▼';
|
||
font-size: 10px;
|
||
margin-left: 5px;
|
||
transition: transform 0.3s ease;
|
||
position: absolute;
|
||
right: 8px;
|
||
}
|
||
|
||
/* 展開狀態 */
|
||
.status-effect-name.has-effect.expanded::after {
|
||
transform: rotate(180deg);
|
||
}
|
||
|
||
.status-effect-name:hover {
|
||
transform: translateY(-2px);
|
||
background: var(--survival-secondary);
|
||
border-color: var(--survival-accent);
|
||
}
|
||
|
||
/* 效果詳情區域 */
|
||
.status-effect-detail {
|
||
background: rgba(0, 0, 0, 0.25);
|
||
border: 1px solid var(--survival-border);
|
||
border-radius: 4px;
|
||
padding: 8px 10px;
|
||
margin-top: 4px;
|
||
font-size: 12px;
|
||
line-height: 1.4;
|
||
color: var(--survival-text);
|
||
white-space: normal; /* 允許換行 */
|
||
max-width: 250px;
|
||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||
animation: fadeIn 0.2s ease;
|
||
}
|
||
|
||
/* 效果詳情的淡入動畫 */
|
||
@keyframes fadeIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(-5px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
/* 移動端適配 */
|
||
@media (max-width: 768px) {
|
||
.status-effects-list {
|
||
gap: 6px;
|
||
}
|
||
|
||
.status-effect-name {
|
||
padding: 4px 8px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.status-effect-detail {
|
||
max-width: 200px;
|
||
font-size: 11px;
|
||
padding: 6px 8px;
|
||
}
|
||
|
||
.status-effect-name.has-effect {
|
||
padding-right: 25px; /* 移動端箭頭更大一點 */
|
||
}
|
||
|
||
.status-effect-name.has-effect::after {
|
||
right: 6px;
|
||
font-size: 11px;
|
||
}
|
||
}
|
||
|
||
/* 小螢幕適配 */
|
||
@media (max-width: 480px) {
|
||
.status-effects-container {
|
||
padding: 8px;
|
||
}
|
||
|
||
.status-effects-list {
|
||
justify-content: center;
|
||
}
|
||
|
||
.status-effect-detail {
|
||
max-width: 180px;
|
||
}
|
||
}
|
||
/* --- 狀態效果CSS結束 --- */
|
||
|
||
/* 加值欄位特殊樣式 */
|
||
.bonus-prop {
|
||
background: rgba(94, 161, 255, 0.08) !important;
|
||
border-left: 3px solid var(--survival-accent) !important;
|
||
padding: 6px 8px 6px 10px !important;
|
||
border-radius: 4px !important;
|
||
margin: 4px 0 !important;
|
||
}
|
||
|
||
.bonus-prop .prop-key {
|
||
color: var(--survival-accent-strong) !important;
|
||
font-weight: 700 !important;
|
||
}
|
||
|
||
.bonus-prop .prop-val {
|
||
color: #8fd6ff !important;
|
||
font-weight: 600 !important;
|
||
}
|
||
|
||
/* 加值分隔線 */
|
||
.bonus-separator {
|
||
animation: separatorGlow 3s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes separatorGlow {
|
||
0%, 100% {
|
||
background: linear-gradient(90deg, transparent, rgba(148, 197, 255, 0.4), transparent);
|
||
}
|
||
50% {
|
||
background: linear-gradient(90deg, transparent, rgba(94, 161, 255, 0.6), transparent);
|
||
}
|
||
}
|
||
|
||
/* 天賦加值樣式 */
|
||
.talent-bonus-line {
|
||
background: rgba(94, 161, 255, 0.08) !important;
|
||
border-left: 3px solid var(--survival-accent) !important;
|
||
padding: 6px 8px 6px 10px !important;
|
||
border-radius: 4px !important;
|
||
margin: 8px 0 !important;
|
||
color: #8fd6ff !important;
|
||
font-weight: 600 !important;
|
||
}
|
||
|
||
/* 天賦名稱中的加值提示 */
|
||
.talent-has-bonus .skill-name::after {
|
||
content: "✨";
|
||
margin-left: 8px;
|
||
font-size: 12px;
|
||
color: var(--survival-accent);
|
||
animation: sparkle 2s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes sparkle {
|
||
0%, 100% { opacity: 0.6; }
|
||
50% { opacity: 1; }
|
||
}
|
||
|
||
.temp-range-bar,
|
||
.nitrogen-gradient,
|
||
.immune-bar,
|
||
.progress,
|
||
.mini-progress {
|
||
height: 12px;
|
||
background: linear-gradient(135deg, rgba(18, 30, 48, 0.65), rgba(11, 20, 31, 0.65));
|
||
border-radius: 999px;
|
||
border: 1px solid rgba(148, 197, 255, 0.14);
|
||
position: relative;
|
||
overflow: visible;
|
||
}
|
||
/* --- 體溫與免疫條 視覺修正 --- */
|
||
|
||
/* 這是控制體溫條漸變背景的本體 */
|
||
.temp-range-bar .temp-gradient {
|
||
/* 我們在這裡明確指定背景圖像,覆蓋掉任何其他可能的通用樣式 */
|
||
background-image: linear-gradient(90deg, #3498db 0%, #2ecc71 50%, #e74c3c 100%) !important;
|
||
width: 100%; /* 確保它填滿整個條 */
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
height: 100%;
|
||
border-radius: inherit;
|
||
}
|
||
|
||
.mini-progress { height: 8px; }
|
||
|
||
.progress__bar,
|
||
.mini-progress__bar,
|
||
.immune-fill,
|
||
.temp-gradient {
|
||
position:absolute; top:0; left:0; height:100%;
|
||
border-radius: inherit;
|
||
background-image: linear-gradient(90deg, rgba(94, 161, 255, 0.9), rgba(96, 225, 183, 0.9));
|
||
box-shadow: 0 0 18px rgba(94, 161, 255, 0.35);
|
||
}
|
||
|
||
.hp-progress .progress__bar { background-image: linear-gradient(90deg, #ff8fb1, #58f0a7); }
|
||
.mp-progress .progress__bar { background-image: linear-gradient(90deg, #8c8bff, #58f0a7); }
|
||
.hunger-progress .progress__bar, .hunger-progress.mini-progress .mini-progress__bar { background-image: linear-gradient(90deg, #ffdc84, #ff9f6c); }
|
||
.water-progress .progress__bar, .water-progress.mini-progress .mini-progress__bar { background-image: linear-gradient(90deg, #7fd8ff, #5ea1ff); }
|
||
|
||
.carbs-progress .progress__bar{background-image:linear-gradient(90deg, #f39c12, #d35400)}
|
||
.fat-progress .progress__bar{background-image:linear-gradient(90deg, #f1c40f, #f39c12)}
|
||
.protein-progress .progress__bar{background-image:linear-gradient(90deg, #27ae60, #16a085)}
|
||
.vitamin-progress .progress__bar{background-image:linear-gradient(90deg, #2ecc71, #27ae60)}
|
||
|
||
.progress-wrapper > div:first-child, .mini-progress__text, .progress-number {
|
||
/* --- 字體修正點 --- */
|
||
font-family: "IBM Plex Mono", Consolas, monospace;
|
||
}
|
||
.mini-progress__text {
|
||
font-family: "IBM Plex Mono", Consolas, monospace;
|
||
font-size: 13px;
|
||
letter-spacing: 0.06em;
|
||
color: var(--survival-text); /* High Contrast */
|
||
}
|
||
|
||
.button-container{
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
justify-content: flex-end;
|
||
margin-top:auto;
|
||
}
|
||
|
||
@media (max-width: 600px) {
|
||
.button-container {
|
||
justify-content: stretch;
|
||
}
|
||
|
||
.action-btn { /* Renamed for clarity */
|
||
flex: 1 1 100%;
|
||
text-align: center;
|
||
}
|
||
}
|
||
|
||
.action-btn { /* Renamed and unified */
|
||
position: relative;
|
||
padding: 12px 18px;
|
||
border-radius: 999px;
|
||
border: 1px solid rgba(148, 197, 255, 0.25);
|
||
background: linear-gradient(135deg, rgba(17, 31, 49, 0.9), rgba(29, 45, 68, 0.85));
|
||
color: var(--survival-text);
|
||
font-family: "Inter", "Microsoft YaHei", sans-serif;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.04em;
|
||
text-transform: uppercase;
|
||
overflow: hidden;
|
||
cursor: pointer;
|
||
transition: transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease;
|
||
}
|
||
|
||
.action-btn::before {
|
||
content: "";
|
||
position: absolute;
|
||
inset: 0;
|
||
background: linear-gradient(120deg, transparent 25%, rgba(94, 161, 255, 0.2), transparent 75%);
|
||
opacity: 0;
|
||
transition: opacity 0.3s ease;
|
||
}
|
||
|
||
.action-btn:hover {
|
||
transform: translateY(-3px);
|
||
border-color: rgba(148, 197, 255, 0.55);
|
||
box-shadow: 0 12px 28px rgba(8, 18, 34, 0.35);
|
||
}
|
||
|
||
.action-btn:hover::before {
|
||
opacity: 1;
|
||
}
|
||
|
||
.action-btn:active {transform:translateY(0)}
|
||
|
||
/* Breathing light effect for draw buttons */
|
||
.draw-btn.survival {
|
||
animation: glow-survival 3s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes glow-survival {
|
||
0%, 100% {
|
||
box-shadow: 0 0 5px rgba(94, 161, 255, 0.2), 0 0 10px rgba(94, 161, 255, 0.2);
|
||
border-color: rgba(94, 161, 255, 0.4);
|
||
}
|
||
50% {
|
||
box-shadow: 0 0 15px rgba(94, 161, 255, 0.5), 0 0 25px rgba(94, 161, 255, 0.5);
|
||
border-color: rgba(148, 197, 255, 0.8);
|
||
}
|
||
}
|
||
|
||
@keyframes glow-miracle {
|
||
0%, 100% {
|
||
box-shadow: 0 0 5px rgba(243, 156, 18, 0.2), 0 0 10px rgba(243, 156, 18, 0.2);
|
||
border-color: rgba(243, 156, 18, 0.4);
|
||
}
|
||
50% {
|
||
box-shadow: 0 0 15px rgba(243, 156, 18, 0.5), 0 0 25px rgba(243, 156, 18, 0.5);
|
||
border-color: rgba(255, 184, 108, 0.8);
|
||
}
|
||
}
|
||
|
||
.inventory-modal{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.8);backdrop-filter:blur(8px);z-index:1000;display:flex;justify-content:center;align-items:center}
|
||
/* --- 【全景物品欄升級補丁】 --- */
|
||
.inventory-modal__content {
|
||
position: relative;
|
||
/* 讓寬度在90%到1400px之間浮動,既能滿版又不至於在超寬螢幕上過於分散 */
|
||
width: clamp(90%, 95vw, 1400px);
|
||
height: clamp(80%, 90vh, 800px); /* 同時也讓高度更合理 */
|
||
padding: 20px;
|
||
overflow-y:auto;display:flex;flex-direction:row;gap:20px;
|
||
border-radius: 26px;
|
||
border: 1px solid rgba(148, 197, 255, 0.18);
|
||
background: linear-gradient(155deg, rgba(17, 28, 45, 0.94), rgba(24, 36, 58, 0.94));
|
||
box-shadow: var(--survival-shadow);
|
||
backdrop-filter: blur(20px) saturate(160%);
|
||
animation:modalFadeIn .3s ease-out
|
||
}
|
||
|
||
.inventory-modal__close{position:absolute;top:10px;right:15px;font-size:24px;cursor:pointer;color:var(--survival-text);transition:all .2s ease;width:30px;height:30px;display:flex;align-items:center;justify-content:center;border-radius:50%;background: rgba(0,0,0,0.6)}
|
||
.inventory-modal__close:hover{color:var(--survival-danger);background:rgba(231,76,60,.2);transform:scale(1.1)}
|
||
@keyframes modalFadeIn{from{opacity:0;transform:translateY(-20px)}to{opacity:1;transform:translateY(0)}}
|
||
.attr-skill-wrapper{display:flex;gap:24px;flex-wrap:wrap}
|
||
.attr-skill-wrapper .attr-col,.attr-skill-wrapper .skill-col{flex:1 1 300px;min-width:260px;background:rgba(0,0,0,.15);padding:18px;border-radius:16px;}
|
||
.inventory-grid {
|
||
display: grid;
|
||
/* 核心改動:將每個格子的最小寬度從 320px!
|
||
這樣一來,在大多數常見的螢幕寬度下,一行最多只能放下3-4個格子。 */
|
||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||
gap: 6px; /* 格子之間的空隙也再拉開一點,顯得更大氣 */
|
||
}
|
||
.inventory-item {
|
||
background: var(--survival-surface-strong);
|
||
border: 1px solid var(--survival-border);
|
||
border-radius: 10px; /* 圓角稍微大一點,更有設計感 */
|
||
padding: 16px; /* 內邊距加大,讓文字和邊框的距離更遠 */
|
||
transition: all .25s ease-out;
|
||
display: flex;
|
||
flex-direction: column;
|
||
position: relative;
|
||
overflow: hidden;
|
||
align-items: flex-start;
|
||
text-align: left;
|
||
min-height: 100px; /* 最小高度也稍微增加 */
|
||
box-shadow: var(--survival-glow); /* 加上一點點輝光效果 */
|
||
}
|
||
.inventory-item:hover{transform:translateY(-3px);box-shadow:0 5px 15px rgba(0,0,0,.3);background:rgba(52,73,94,.9);border-color:var(--survival-accent)}
|
||
.inventory-item .item-name {
|
||
font-weight: 600; /* 字重降低一點,顯得更優雅 */
|
||
margin-bottom: 12px;
|
||
font-size: 18px; /* 標題字號:16px -> 18px,醒目! */
|
||
color: var(--survival-accent-strong); /* 使用更亮眼的強調色 */
|
||
letter-spacing: 0.05em; /* 增加一點點字間距 */
|
||
}
|
||
/* --- 裝備列表網格佈局 --- */
|
||
.inventory-list {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr); /* 一行3個 */
|
||
gap: 12px; /* 格子間距 */
|
||
margin-top: 10px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
/* 裝備項目的樣式調整 */
|
||
.inventory-list-item {
|
||
background: var(--survival-surface-strong);
|
||
border: 1px solid var(--survival-border);
|
||
border-radius: 10px;
|
||
padding: 16px;
|
||
transition: all .25s ease-out;
|
||
display: flex;
|
||
flex-direction: column;
|
||
position: relative;
|
||
overflow: hidden;
|
||
align-items: flex-start;
|
||
text-align: left;
|
||
min-height: 120px; /* 稍微增加最小高度 */
|
||
box-shadow: var(--survival-glow);
|
||
}
|
||
|
||
.inventory-list-item:hover {
|
||
transform: translateY(-3px);
|
||
box-shadow: 0 5px 15px rgba(0,0,0,.3);
|
||
background: rgba(52,73,94,.9);
|
||
border-color: var(--survival-accent);
|
||
}
|
||
|
||
.inventory-list-item .item-name {
|
||
font-weight: 600;
|
||
margin-bottom: 12px;
|
||
font-size: 18px;
|
||
color: var(--survival-accent-strong);
|
||
letter-spacing: 0.05em;
|
||
}
|
||
|
||
/* 響應式設計 */
|
||
@media (max-width: 960px) {
|
||
.inventory-list {
|
||
grid-template-columns: repeat(2, 1fr); /* 中等屏幕一行2個 */
|
||
}
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
.inventory-list {
|
||
grid-template-columns: 1fr; /* 小屏幕一行1個 */
|
||
}
|
||
}
|
||
/* --- 【最终修正补丁 v5.0 - 标签防断裂】 --- */
|
||
|
||
/* 步骤一:确保整个属性条目可以灵活换行 */
|
||
.item-prop {
|
||
display: flex; /* 啟用Flexbox佈局,這是所有魔法的基礎 */
|
||
align-items: flex-start; /* 頂部對齊,防止長短不一時錯位 */
|
||
gap: 4px; /* 元素之間留點空隙 */
|
||
font-size: 14px;
|
||
line-height: 1.6;
|
||
color: #ffffff;
|
||
}
|
||
|
||
/* 步驟二:給家長下死命令 - 標題和冒號,你們倆給爺鎖死! */
|
||
/* 為了美觀,單獨調整一下標題的顏色 */
|
||
.item-prop .prop-key {
|
||
color: var(--survival-muted);
|
||
white-space: nowrap; /* <-- 就是加上這一行! */
|
||
|
||
}
|
||
|
||
.item-prop .prop-sep {
|
||
flex-shrink: 0; /* 空間再小,也不許壓縮你們的寬度! */
|
||
white-space: nowrap; /* 你們本身內部也絕對不許換行! */
|
||
}
|
||
|
||
.item-prop .prop-sep {
|
||
white-space: nowrap; /* <-- 核心:强制这两个部分永不换行 */
|
||
flex-shrink: 0; /* <-- 辅助:告诉flex布局,不要压缩我! */
|
||
}
|
||
|
||
/* 步驟三:讓孩子承擔所有 - 描述內容,所有換行的壓力都是你的! */
|
||
.item-prop .prop-val {
|
||
/* 這裡不需要特別的 flex 屬性,它會自動佔據剩餘空間 */
|
||
word-break: break-word; /* 讓它智能地斷詞換行 */
|
||
min-width: 0; /* 防止溢出的保險措施 */
|
||
font-weight: 500;
|
||
}
|
||
.inventory-list-item{background:var(--survival-bg-light);border:1px solid var(--survival-border);border-radius:6px;padding:12px;transition:all .2s ease-out;display:flex;flex-direction:column;position:relative;overflow:hidden}
|
||
.inventory-list-item:hover{transform:translateY(-2px);box-shadow:0 3px 10px rgba(0,0,0,.3);background:rgba(52,73,94,.9);border-color:var(--survival-accent)}
|
||
.inventory-list-item .item-name{font-weight:bold;margin-bottom:6px;font-size:16px;color:#fff}
|
||
|
||
/*.nitrogen-balance:before{content:"";position:absolute;top:0;bottom:0;left:50%;width:2px;background:hsla(0,0%,100%,.7);z-index:1}*/
|
||
.vertical-progress{width:30px;height:200px;background:rgba(0,0,0,.3);border-radius:15px;position:relative;margin:20px auto;box-shadow:inset 0 0 10px rgba(0,0,0,.5)}
|
||
.vertical-progress__bar{position:absolute;bottom:0;left:0;width:100%;background:linear-gradient(to top, var(--survival-warning), var(--survival-accent));border-radius:15px;transition:height .5s ease-out;box-shadow:0 0 10px rgba(52,152,219,.5)}
|
||
.vertical-progress__label{position:absolute;bottom:-30px;left:50%;transform:translateX(-50%);white-space:nowrap;text-align:center;width:100px;font-weight:bold;color:var(--survival-text);text-shadow:0 1px 2px rgba(0,0,0,.5)}
|
||
.vertical-progress__value{position:absolute;top:-30px;left:50%;transform:translateX(-50%);white-space:nowrap;text-align:center;width:100px;font-weight:bold;color:var(--survival-accent);text-shadow:0 1px 2px rgba(0,0,0,.5)}
|
||
.weight-container{display:flex;flex-direction:column;align-items:center;min-width:100px;padding-right:20px;border-right:1px solid var(--survival-border);justify-content:center}
|
||
.inventory-content{flex:1;display:flex;flex-direction:column;overflow-y:auto}
|
||
.inventory-content h3{margin-top:0;margin-bottom:15px;color:var(--survival-accent);font-size:18px;border-bottom:1px solid hsla(0,0%,100%,.1);padding-bottom:8px;position:relative}
|
||
.inventory-content h3:after{content:"";position:absolute;bottom:-1px;left:0;width:60px;height:2px;background:var(--survival-accent)}
|
||
.temp-range-wrapper{font-size:14px}
|
||
.temp-range-bar{height:16px;margin-top:8px;overflow:visible}
|
||
.comfort-zone{position:absolute;height:100%;background:hsla(0,0%,100%,.2);border:1px dashed hsla(0,0%,100%,.7);border-radius:8px;z-index:1}
|
||
.current-temp{position:absolute;width:4px;height:24px;background:#fff;border-radius:2px;top:-4px;transform:translateX(-2px);z-index:2;box-shadow:0 0 5px hsla(0,0%,100%,.8)}
|
||
|
||
.nitrogen-gradient{height:12px;}
|
||
.nitrogen-marker{position:absolute;top:-6px;bottom:-6px;width:4px;background:#fff;border-radius:2px;box-shadow:0 0 5px hsla(0,0%,100%,.8)}
|
||
.temp-gradient{height:12px; background-image: linear-gradient(90deg, #3498db 0%, #2ecc71 50%, #e74c3c 100%);}
|
||
.temp-marker{position:absolute;top:-6px;bottom:-6px;width:4px;background:#fff;border-radius:2px;box-shadow:0 0 5px hsla(0,0%,100%,.8)}
|
||
.temp-normal-zone{position:absolute;height:100%;background:hsla(0,0%,100%,.2);border:1px dashed hsla(0,0%,100%,.7);border-radius:4px;z-index:1}
|
||
.temp-gradient{height:12px; background-image: linear-gradient(90deg, #3498db 0%, #2ecc71 50%, #e74c3c 100%);}
|
||
.temp-marker{position:absolute;top:-6px;bottom:-6px;width:4px;background:#fff;border-radius:2px;box-shadow:0 0 5px hsla(0,0%,100%,.8)}
|
||
.temp-normal-zone{position:absolute;height:100%;background:hsla(0,0%,100%,.2);border:1px dashed hsla(0,0%,100%,.7);border-radius:4px;z-index:1}
|
||
.immune-bar{height:12px;}
|
||
.immune-fill{background-image:linear-gradient(90deg, #2ecc71 0%, #f39c12 50%, #e74c3c 100%);transition:width .3s ease}
|
||
.immune-marker{position:absolute;top:-6px;bottom:-6px;width:4px;background:#fff;border-radius:2px;box-shadow:0 0 5px hsla(0,0%,100%,.8);z-index:2}
|
||
.skill-item{margin-bottom:2px;background:rgba(0,0,0,.1);padding:2px 8px;border-radius:4px}
|
||
.skill-item .skill-name{font-weight:bold;margin-bottom:5px}
|
||
.skill-desc { font-size: .85em; color: var(--survival-muted); opacity: 0.8; margin-top: 4px; }
|
||
|
||
/* 終極任務(世界任務)樣式 - 金色傳說 */
|
||
.task-strip.world-task {
|
||
background: linear-gradient(135deg,
|
||
rgba(255, 215, 0, 0.25),
|
||
rgba(255, 165, 0, 0.15),
|
||
rgba(255, 215, 0, 0.08));
|
||
border-color: rgba(255, 215, 0, 0.6);
|
||
color: #ffd966;
|
||
text-shadow: 0 0 8px rgba(255, 215, 0, 0.5);
|
||
border-width: 2px;
|
||
box-shadow: 0 0 15px rgba(255, 215, 0, 0.3);
|
||
animation: worldTaskGlow 3s ease-in-out infinite;
|
||
}
|
||
|
||
.task-strip.world-task:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 0 25px rgba(255, 215, 0, 0.5);
|
||
border-color: rgba(255, 215, 0, 0.9);
|
||
}
|
||
|
||
/* 終極任務的呼吸光效 */
|
||
@keyframes worldTaskGlow {
|
||
0%, 100% {
|
||
box-shadow: 0 0 8px rgba(255, 215, 0, 0.3);
|
||
border-color: rgba(255, 215, 0, 0.5);
|
||
}
|
||
50% {
|
||
box-shadow: 0 0 20px rgba(255, 215, 0, 0.6);
|
||
border-color: rgba(255, 215, 0, 0.9);
|
||
}
|
||
}
|
||
|
||
/* 終極任務展開時的內容區特殊背景 */
|
||
.task-detail.world-detail {
|
||
background: linear-gradient(135deg,
|
||
rgba(255, 215, 0, 0.08),
|
||
rgba(0, 0, 0, 0.3));
|
||
border: 1px solid rgba(255, 215, 0, 0.3);
|
||
}
|
||
|
||
/* 任務系統樣式 */
|
||
.task-strip {
|
||
padding: 12px 16px;
|
||
margin: 8px 0;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
font-weight: 600;
|
||
text-align: center;
|
||
border: 1px solid transparent;
|
||
}
|
||
|
||
.task-strip:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
.task-strip.main-task {
|
||
background: linear-gradient(135deg, rgba(52, 152, 219, 0.2), rgba(52, 152, 219, 0.1));
|
||
border-color: rgba(52, 152, 219, 0.3);
|
||
color: #3498db;
|
||
}
|
||
|
||
.task-strip.side-task {
|
||
background: linear-gradient(135deg, rgba(243, 156, 18, 0.2), rgba(243, 156, 18, 0.1));
|
||
border-color: rgba(243, 156, 18, 0.3);
|
||
color: #f39c12;
|
||
}
|
||
|
||
.task-strip.emergency-task {
|
||
background: linear-gradient(135deg, rgba(231, 76, 60, 0.2), rgba(231, 76, 60, 0.1));
|
||
border-color: rgba(231, 76, 60, 0.3);
|
||
color: #e74c3c;
|
||
animation: pulse 2s infinite;
|
||
}
|
||
/* 日常任務樣式 */
|
||
.task-strip.daily-task {
|
||
background: linear-gradient(135deg, rgba(46, 204, 113, 0.2), rgba(46, 204, 113, 0.1));
|
||
border-color: rgba(46, 204, 113, 0.3);
|
||
color: #2ecc71;
|
||
}
|
||
|
||
/* 羈絆任務樣式 */
|
||
.task-strip.bond-task {
|
||
background: linear-gradient(135deg, rgba(189, 147, 249, 0.25), rgba(189, 147, 249, 0.15)); /* 紫色調得更淺更亮 */
|
||
border-color: rgba(142, 68, 173, 0.4); /* 邊框顏色可以稍微保留一點深度,增加輪廓感 */
|
||
color: #f0e6ff; /* 字體顏色直接換成非常淺的淡紫色,接近白色,保證對比度 */
|
||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3); /* 給文字加一點點陰影,讓它更有立體感,更容易閱讀 */
|
||
}
|
||
|
||
.task-detail {
|
||
background: rgba(0, 0, 0, 0.15);
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
margin: 8px 0;
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.task-desc {
|
||
text-align: left;
|
||
margin-bottom: 16px;
|
||
line-height: 1.4;
|
||
color: var(--survival-text);
|
||
white-space: pre-line;
|
||
word-wrap: break-word;
|
||
/* 這裏可以添加字體大小,例如: */
|
||
font-size: 15px; /* 添加這一行來設置任務描述字體大小 */
|
||
}
|
||
|
||
.target-section {
|
||
margin: 16px 0;
|
||
}
|
||
|
||
.section-title {
|
||
font-weight: 600;
|
||
margin-bottom: 8px;
|
||
color: var(--survival-accent);
|
||
/* 這裏可以添加字體大小,例如: */
|
||
font-size: 15px; /* 設置"任務要求"、"進階目標"等標題的字體大小 */
|
||
}
|
||
|
||
.target-desc {
|
||
margin-bottom: 8px;
|
||
line-height: 1.4;
|
||
color: var(--survival-text);
|
||
white-space: pre-line;
|
||
word-wrap: break-word;
|
||
/* 這裏可以添加字體大小,例如: */
|
||
font-size: 15px; /* 添加這一行來設置任務目標描述字體大小 */
|
||
}
|
||
|
||
.reward-info {
|
||
color: var(--survival-text);
|
||
font-weight: 600;
|
||
margin-top: 13px;
|
||
/* 這裏可以添加字體大小,例如: */
|
||
font-size: 15px; /* 設置獎勵信息的字體大小 */
|
||
}
|
||
|
||
.reward-info .points {
|
||
color: var(--survival-warning);
|
||
}
|
||
|
||
.penalty-info {
|
||
/* 1. 核心佈局與對齊 */
|
||
display: flex; /* 啟用flex佈局,讓圖標和文字能肩並肩站好 */
|
||
align-items: center; /* 垂直居中對齊,看起來更舒服 */
|
||
text-align: left; /* 改成你想要的左對齊 */
|
||
|
||
/* 2. 背景與邊框,打造"警報卡片"感 */
|
||
background: linear-gradient(135deg, rgba(231, 76, 60, 0.15), rgba(231, 76, 60, 0.05));
|
||
border-left: 4px solid var(--survival-danger); /* 左側加一條醒目的紅色指示條 */
|
||
border-radius: 6px; /* 加個小圓角,不那麼生硬 */
|
||
|
||
/* 3. 內邊距與外邊距,讓它呼吸起來 */
|
||
padding: 10px 14px;
|
||
margin-top: 12px;
|
||
|
||
/* 4. 字體與文字效果 */
|
||
font-family: "Microsoft YaHei", "Segoe UI", Arial, sans-serif; /* 確保字體統一 */
|
||
font-size: 18px; /* 字號稍微調整一下 */
|
||
color: #ffdde5; /* 文字用柔和的粉紅色,而不是刺眼的鮮紅 */
|
||
font-weight: 600;
|
||
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.5); /* 加點文字陰影,更有立體感 */
|
||
}
|
||
|
||
/* 5. 畫龍點睛的圖標! */
|
||
.penalty-info::before {
|
||
content: '⚠️'; /* 你也可以換成 '🛡️💥💀' */
|
||
margin-right: 8px; /* 讓圖標和文字之間有點距離 */
|
||
font-size: 18px; /* 圖標稍微大一點點 */
|
||
animation: pulse-danger 2s infinite; /* 給圖標加個呼吸動畫 */
|
||
}
|
||
|
||
/* 6. 配套的呼吸動畫,讓警告"活"起來 */
|
||
@keyframes pulse-danger {
|
||
0% { transform: scale(1); opacity: 1; }
|
||
50% { transform: scale(1.1); opacity: 0.8; }
|
||
100% { transform: scale(1); opacity: 1; }
|
||
}
|
||
|
||
.task-divider {
|
||
height: 1px;
|
||
background: linear-gradient(90deg, transparent, var(--survival-border), transparent);
|
||
margin: 16px 0;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0% { opacity: 1; }
|
||
50% { opacity: 0.7; }
|
||
100% { opacity: 1; }
|
||
}
|
||
|
||
/* 抽卡系統樣式 - 與任務系統完全一致 */
|
||
.draw-main-task {
|
||
background: linear-gradient(135deg, rgba(52, 152, 219, 0.2), rgba(52, 152, 219, 0.1));
|
||
border-color: rgba(52, 152, 219, 0.3);
|
||
color: #3498db;
|
||
}
|
||
|
||
.draw-side-task {
|
||
background: linear-gradient(135deg, rgba(243, 156, 18, 0.2), rgba(243, 156, 18, 0.1));
|
||
border-color: rgba(243, 156, 18, 0.3);
|
||
color: #f39c12;
|
||
}
|
||
|
||
.draw-container {
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
/* 抽卡選項的特定樣式 */
|
||
.requirement-item.draw-ready .req-desc {
|
||
color: var(--survival-success);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.requirement-item.draw-disabled .req-desc {
|
||
color: var(--survival-text);
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.requirement-item {
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
padding: 8px 12px;
|
||
border-radius: 4px;
|
||
margin: 4px 0;
|
||
}
|
||
|
||
.requirement-item:hover {
|
||
background: rgba(148, 197, 255, 0.1);
|
||
}
|
||
|
||
.requirement-item.draw-ready:hover {
|
||
background: rgba(46, 204, 113, 0.1);
|
||
}
|
||
|
||
.requirement-item.draw-disabled {
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.requirement-item.draw-disabled:hover {
|
||
background: transparent;
|
||
}
|
||
|
||
/* 任務完成樣式 - 整個描述(包括進度數字)都變灰劃掉 */
|
||
.target-desc.completed {
|
||
color: #999 !important;
|
||
text-decoration: line-through;
|
||
opacity: 0.7;
|
||
}
|
||
|
||
/* 進度數字的黃色樣式(和生存點數一樣的黃色) */
|
||
.progress-number {
|
||
color: var(--survival-warning) !important;
|
||
font-weight: bold;
|
||
}
|
||
|
||
/* 獎勵點數樣式 */
|
||
.reward-info .points {
|
||
color: var(--survival-warning) !important;
|
||
font-weight: bold;
|
||
}
|
||
|
||
/* 緊急任務時間信息樣式 */
|
||
.time-info {
|
||
font-size: 15px;
|
||
line-height: 1.5;
|
||
color: var(--survival-text);
|
||
margin-top: 8px;
|
||
text-align: left;
|
||
}
|
||
|
||
.time-info div {
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
/* 失敗懲罰樣式 */
|
||
.penalty-info {
|
||
color: var(--survival-danger) !important;
|
||
font-weight: bold;
|
||
text-align: center;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
/* 物品狀態樣式 */
|
||
.item-state {
|
||
font-size: .8em;
|
||
color: var(--survival-success);
|
||
margin-top: 4px;
|
||
}
|
||
|
||
/* 修復描述文本換行 */
|
||
.item-props {
|
||
width: 100%;
|
||
}
|
||
|
||
.item-prop .prop-val {
|
||
word-wrap: break-word;
|
||
white-space: normal;
|
||
line-height: 1.3;
|
||
overflow-wrap: break-word;
|
||
}
|
||
|
||
/* 任務系統樣式 - 簡化狀態指示器 */
|
||
.task-strip {
|
||
position: relative;
|
||
padding: 12px 16px;
|
||
margin: 8px 0;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
font-weight: 600;
|
||
text-align: center;
|
||
border: 1px solid transparent;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
min-height: 44px;
|
||
}
|
||
|
||
.task-strip:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
.task-strip.main-task {
|
||
background: linear-gradient(135deg, rgba(52, 152, 219, 0.2), rgba(52, 152, 219, 0.1));
|
||
border-color: rgba(52, 152, 219, 0.3);
|
||
color: #3498db;
|
||
}
|
||
|
||
.task-strip.side-task {
|
||
background: linear-gradient(135deg, rgba(243, 156, 18, 0.2), rgba(243, 156, 18, 0.1));
|
||
border-color: rgba(243, 156, 18, 0.3);
|
||
color: #f39c12;
|
||
}
|
||
|
||
.task-strip.daily-task {
|
||
background: linear-gradient(135deg, rgba(46, 204, 113, 0.2), rgba(46, 204, 113, 0.1));
|
||
border-color: rgba(46, 204, 113, 0.3);
|
||
color: #2ecc71;
|
||
}
|
||
|
||
.task-strip.emergency-task {
|
||
background: linear-gradient(135deg, rgba(231, 76, 60, 0.2), rgba(231, 76, 60, 0.1));
|
||
border-color: rgba(231, 76, 60, 0.3);
|
||
color: #e74c3c;
|
||
animation: pulse 2s infinite;
|
||
}
|
||
|
||
/* 任務狀態指示器 - 簡化版 */
|
||
.task-status-indicator {
|
||
position: absolute;
|
||
right: 13px; /* 從16px減小到12px,讓位置更緊湊 */
|
||
top: 50%;
|
||
transform: translateY(-50%) scale(0.9); /* 添加整體縮放90% */
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.status-text {
|
||
font-size: 10px; /* 從12px減小到10px */
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.8px; /* 稍微減小字間距 */
|
||
transform: rotate(-15deg);
|
||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||
padding: 1px 4px; /* 從2px 6px減小到1px 4px */
|
||
border-radius: 2px;
|
||
}
|
||
|
||
/* 不同狀態的顏色 */
|
||
.status-ongoing {
|
||
color: #3498db;
|
||
background: rgba(52, 152, 219, 0.15);
|
||
}
|
||
|
||
.status-pending {
|
||
color: #f39c12;
|
||
background: rgba(243, 156, 18, 0.15);
|
||
}
|
||
|
||
.status-completed {
|
||
color: #2ecc71;
|
||
background: rgba(46, 204, 113, 0.15);
|
||
}
|
||
|
||
.status-failed {
|
||
color: #e74c3c;
|
||
background: rgba(231, 76, 60, 0.15);
|
||
}
|
||
|
||
.status-abandoned {
|
||
color: #95a5a6;
|
||
background: rgba(149, 165, 166, 0.15);
|
||
text-decoration: line-through;
|
||
}
|
||
|
||
/* 修改 .task-content 爲抽卡專用樣式 */
|
||
.task-content {
|
||
text-align: center;
|
||
line-height: 1.4;
|
||
font-family: "Microsoft YaHei","Segoe UI",Arial,sans-serif;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: inherit;
|
||
width: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
/* 其他現有樣式保持不變 */
|
||
.task-detail {
|
||
background: rgba(0, 0, 0, 0.15);
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
margin: 8px 0;
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.task-desc {
|
||
text-align: left;
|
||
margin-bottom: 16px;
|
||
line-height: 1.4;
|
||
color: var(--survival-text);
|
||
white-space: pre-line;
|
||
word-wrap: break-word;
|
||
}
|
||
|
||
.target-section {
|
||
margin: 16px 0;
|
||
}
|
||
|
||
.section-title {
|
||
font-weight: 600;
|
||
margin-bottom: 8px;
|
||
color: var(--survival-accent);
|
||
}
|
||
|
||
.target-desc {
|
||
margin-bottom: 8px;
|
||
line-height: 1.4;
|
||
color: var(--survival-text);
|
||
white-space: pre-line;
|
||
word-wrap: break-word;
|
||
}
|
||
|
||
.reward-info {
|
||
color: var(--survival-text);
|
||
font-weight: 600;
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.reward-info .points {
|
||
color: var(--survival-warning);
|
||
}
|
||
|
||
.penalty-info {
|
||
color: var(--survival-danger);
|
||
font-weight: 600;
|
||
text-align: center;
|
||
}
|
||
|
||
.task-divider {
|
||
height: 1px;
|
||
background: linear-gradient(90deg, transparent, var(--survival-border), transparent);
|
||
margin: 16px 0;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0% { opacity: 1; }
|
||
50% { opacity: 0.7; }
|
||
100% { opacity: 1; }
|
||
}
|
||
|
||
/* --- MAP CSS START --- */
|
||
.map-modal {
|
||
position: fixed; inset: 0; display: flex; justify-content: center; align-items: center; color: #ffffff;
|
||
background: rgba(0,0,0,0.8); backdrop-filter: blur(8px); z-index: 1000;
|
||
}
|
||
.map-modal__content {
|
||
position: relative; width: 95vw; height: 95vh; padding: 0; overflow: hidden; display: flex; flex-direction: column;
|
||
border-radius: 26px; border: 1px solid rgba(148, 197, 255, 0.18);
|
||
background: linear-gradient(155deg, rgba(17, 28, 45, 0.94), rgba(24, 36, 58, 0.94));
|
||
box-shadow: var(--survival-shadow); backdrop-filter: blur(20px) saturate(160%);
|
||
animation: modalFadeIn .3s ease-out;
|
||
}
|
||
.map-modal__close {
|
||
position:absolute; top:10px; right:15px; font-size:24px; cursor:pointer;
|
||
color:rgba(239, 246, 255, 0.7); transition:all .2s ease;
|
||
width:30px; height:30px; display:flex; align-items:center; justify-content:center; border-radius:50%; z-index: 1100;
|
||
}
|
||
.map-modal__close:hover{ color:var(--survival-danger); background:rgba(231,76,60,.2); transform:scale(1.1) }
|
||
|
||
.map-modal .map-container {
|
||
flex: 1; position: relative; overflow: hidden; touch-action: none; background: #2a2a2a;
|
||
cursor: grab;
|
||
}
|
||
.map-modal .map-container.linking { cursor: crosshair; }
|
||
.map-modal .map-container.dragging { cursor: grabbing; }
|
||
|
||
.map-modal .info-panel, .map-modal .zoom-controls button {
|
||
border-color: rgba(148, 197, 255, 0.2);
|
||
background: rgba(24, 36, 58, 0.9);
|
||
box-shadow: 0 12px 28px rgba(6, 12, 25, 0.45);
|
||
}
|
||
.map-modal .zoom-btn:hover {
|
||
background: rgba(94, 161, 255, 0.25);
|
||
transform: translateY(-3px) scale(1.05);
|
||
}
|
||
/* --- MAP CSS END --- */
|
||
|
||
/* --- Draw Confirmation Modal --- */
|
||
.draw-confirm-modal { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; padding: 24px; background: radial-gradient(circle at center, rgba(41, 128, 185, 0.38), rgba(10, 14, 22, 0.92)); backdrop-filter: blur(10px); z-index: 10000; animation: fadeInModal 0.25s ease; }
|
||
.draw-confirm-modal__content {
|
||
position: relative; width: min(420px, 90vw); overflow: hidden; text-align: center;
|
||
border-radius: 26px; border: 1px solid rgba(148, 197, 255, 0.18);
|
||
background: linear-gradient(155deg, rgba(17, 28, 45, 0.94), rgba(24, 36, 58, 0.94));
|
||
box-shadow: var(--survival-shadow); backdrop-filter: blur(20px) saturate(160%);
|
||
padding: 28px 26px 24px;
|
||
}
|
||
.draw-confirm-modal__glow { position: absolute; inset: -45%; background: radial-gradient(circle, rgba(77, 208, 255, 0.35) 0%, transparent 65%); filter: blur(60px); opacity: 0.65; animation: modalGlow 6s ease-in-out infinite; pointer-events: none; }
|
||
.draw-confirm-modal__title { position: relative; z-index: 1; font-size: 20px; font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; color: #e9f5ff; margin: 0 0 12px; }
|
||
.draw-confirm-modal__desc { position: relative; z-index: 1; font-size: 15px; color: var(--survival-text); margin: 0 0 18px; }
|
||
.draw-confirm-modal__points { position: relative; z-index: 1; font-size: 14px; color: var(--survival-muted); letter-spacing: 0.05em; margin-bottom: 24px; }
|
||
.draw-confirm-modal__points span { display: inline-block; margin-top: 6px; font-size: 30px; font-weight: 700; color: #4dd0ff; text-shadow: 0 0 18px rgba(77, 208, 255, 0.55); }
|
||
.draw-confirm-modal__actions { position: relative; z-index: 1; display: flex; gap: 12px; justify-content: center; margin-top: 10px; }
|
||
.draw-confirm-modal__button { flex: 1; padding: 12px 16px; border-radius: 999px; border: none; font-size: 15px; font-weight: 600; cursor: pointer; transition: transform 0.18s ease, box-shadow 0.18s ease, background 0.18s ease; font-family: "Inter", "Microsoft YaHei", sans-serif;}
|
||
.draw-confirm-modal__button:active { transform: scale(0.96); }
|
||
.draw-confirm-modal__confirm { background: linear-gradient(135deg, #26c6da, #7c4dff); color: #fff; box-shadow: 0 12px 34px rgba(124, 77, 255, 0.35); }
|
||
.draw-confirm-modal__confirm:hover:not(.is-disabled) { transform: translateY(-1px); box-shadow: 0 16px 42px rgba(124, 77, 255, 0.48); }
|
||
.draw-confirm-modal__cancel { background: rgba(37, 49, 68, 0.9); color: var(--survival-text); box-shadow: 0 12px 28px rgba(0, 0, 0, 0.35); }
|
||
.draw-confirm-modal__cancel:hover { transform: translateY(-1px); box-shadow: 0 14px 32px rgba(0, 0, 0, 0.4); }
|
||
.draw-confirm-modal__confirm.is-disabled { background: rgba(90, 96, 112, 0.7); color: var(--survival-text); cursor: not-allowed; box-shadow: none; }
|
||
.draw-confirm-modal__confirm.is-loading { color: transparent; position: relative; }
|
||
.draw-confirm-modal__confirm.is-loading::after { content: ''; position: absolute; top: 50%; left: 50%; width: 22px; height: 22px; margin: -11px 0 0 -11px; border: 2px solid rgba(255, 255, 255, 0.32); border-top-color: #ffffff; border-radius: 50%; animation: spin 0.7s linear infinite; }
|
||
.draw-confirm-modal__status { position: relative; z-index: 1; margin-top: 20px; min-height: 18px; font-size: 13px; color: var(--survival-text); letter-spacing: 0.03em; }
|
||
|
||
@keyframes fadeInModal { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }
|
||
@keyframes modalGlow { 0% { transform: scale(0.8); opacity: 0.5; } 50% { transform: scale(1.1); opacity: 0.7; } 100% { transform: scale(0.8); opacity: 0.5; } }
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
|
||
/* --- Image Viewer Modal --- */
|
||
.image-viewer-modal { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; padding: 30px; background: radial-gradient(circle at 20% 20%, rgba(46, 134, 222, 0.35), transparent 55%), rgba(8, 11, 19, 0.92); backdrop-filter: blur(12px); z-index: 9990; animation: fadeInModal 0.25s ease; }
|
||
.image-viewer-modal__content {
|
||
position: relative; width: min(960px, 94vw); height: min(82vh, 700px); display: flex; flex-direction: column; overflow: hidden;
|
||
border-radius: 26px; border: 1px solid rgba(148, 197, 255, 0.18);
|
||
background: linear-gradient(155deg, rgba(17, 28, 45, 0.94), rgba(24, 36, 58, 0.94));
|
||
box-shadow: var(--survival-shadow); backdrop-filter: blur(20px) saturate(160%);
|
||
}
|
||
.image-viewer__header { display: flex; align-items: center; justify-content: space-between; padding: 18px 24px 12px; color: #e9f5ff; }
|
||
.image-viewer__title { font-size: 18px; text-transform: uppercase; letter-spacing: 0.08em; font-weight: 600; }
|
||
.image-viewer__close { background: none; border: none; font-size: 22px; cursor: pointer; transition: transform 0.2s ease, color 0.2s ease; color: var(--survival-muted); }
|
||
.image-viewer__close:hover { color: #ffffff; transform: rotate(90deg); }
|
||
|
||
.image-viewer__canvas {
|
||
position: relative; flex: 1; margin: 0 24px 24px; overflow: hidden; touch-action: none;
|
||
background: radial-gradient(circle at 35% 30%, rgba(94, 161, 255, 0.14), rgba(9, 13, 22, 0.92));
|
||
border: 1px solid rgba(148, 197, 255, 0.16);
|
||
border-radius: 14px; cursor: grab;
|
||
}
|
||
.image-viewer__canvas.is-grabbing { cursor: grabbing; }
|
||
.image-viewer__image { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(1); transform-origin: center center; max-width: none; max-height: none; will-change: transform; pointer-events: none; user-select: none; }
|
||
.image-viewer__placeholder { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 70%; max-width: 360px; text-align: center; font-size: 14px; line-height: 1.6; pointer-events: none; color: var(--survival-text); }
|
||
.image-viewer__status { position: absolute; left: 24px; bottom: 18px; font-size: 13px; letter-spacing: 0.02em; color: var(--survival-text); }
|
||
.image-viewer__toolbar { display: flex; align-items: center; gap: 12px; padding: 0 24px 16px; flex-wrap: wrap; }
|
||
.image-viewer__controls { display: flex; align-items: center; gap: 8px; margin-left: auto; flex-shrink: 0; }
|
||
.image-viewer__hint { flex: 1 1 auto; font-size: 13px; line-height: 1.5; min-width: 200px; color: var(--survival-text);}
|
||
.image-viewer__hint strong { color: #fff;}
|
||
.image-viewer__control-button {
|
||
width: 38px; height: 38px; border-radius: 50%; border: none; font-size: 16px; cursor: pointer;
|
||
background: rgba(32, 43, 60, 0.85); color: var(--survival-text);
|
||
transition: transform 0.18s ease, background 0.18s ease, box-shadow 0.18s ease;
|
||
font-family: "Inter", "Microsoft YaHei", sans-serif;
|
||
}
|
||
.image-viewer__control-button:hover { transform: translateY(-1px); background: rgba(77, 208, 255, 0.2); box-shadow: 0 12px 24px rgba(0, 0, 0, 0.35); }
|
||
|
||
.temp-fill { position: absolute; top: 0; left: 0; height: 100%; border-radius: 8px; background-image: linear-gradient(90deg, var(--survival-accent), var(--survival-success)); opacity: .55; }
|
||
|
||
@media (max-width: 720px) {
|
||
.image-viewer-modal { padding: 18px; }
|
||
.image-viewer-modal__content { height: 88vh; }
|
||
.image-viewer__toolbar { flex-direction: column; align-items: stretch; }
|
||
.image-viewer__hint { margin-bottom: 8px; }
|
||
.image-viewer__controls { margin-left: 0; }
|
||
.image-viewer__canvas { margin: 0 18px 18px; }
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.draw-confirm-modal__content { padding: 24px 18px 20px; }
|
||
.draw-confirm-modal__actions { flex-direction: column; }
|
||
.image-viewer__header { padding: 16px 18px 10px; }
|
||
.image-viewer-modal__content { border-radius: 16px; }
|
||
}
|
||
|
||
.other-group { margin-top: 8px; }
|
||
.other-card { background: rgba(0,0,0,.15); padding: 8px 10px; border-radius: 6px; border: 1px solid var(--survival-border); margin-bottom: 6px; }
|
||
.other-card__title { font-weight: 700; margin-bottom: 4px; }
|
||
.empty-text { opacity: .75; font-size: 13px; }
|
||
|
||
/* --- 同伴狀態條新配色方案 --- */
|
||
.love-progress .progress__bar {
|
||
background-image: linear-gradient(90deg, #FF8FB1, #FFCAD4); /* 好感度: 粉紅色漸層 */
|
||
box-shadow: 0 0 18px rgba(255, 143, 177, 0.55);
|
||
}
|
||
.desire-progress .progress__bar {
|
||
background-image: linear-gradient(90deg, #E94057, #F27121); /* 性慾: 激情玫瑰紅到柑橘橙的漸變 */
|
||
box-shadow: 0 0 18px rgba(233, 64, 87, 0.55);
|
||
}
|
||
.dev-progress .progress__bar {
|
||
background-image: linear-gradient(90deg, #C39BD3, #8E44AD); /* 開發度: 紫色漸層 */
|
||
box-shadow: 0 0 18px rgba(142, 68, 173, 0.55);
|
||
}
|
||
.crevice-progress .progress__bar {
|
||
background-image: linear-gradient(90deg, #485461, #28313b); /* 墮落值: 從深灰到黑色的漸層,代表裂縫 */
|
||
box-shadow: 0 0 18px rgba(40, 49, 59, 0.55);
|
||
}
|
||
.health-progress .progress__bar {
|
||
background-image: linear-gradient(90deg, #58f0a7, #1abc9c); /* 健康 - 生命的翠綠色 */
|
||
box-shadow: 0 0 18px rgba(46, 204, 113, 0.45);
|
||
}
|
||
.satiety-progress .progress__bar {
|
||
background-image: linear-gradient(90deg, #ffdc84, #ff9f6c); /* 飽腹 - 豐收的南瓜色 */
|
||
box-shadow: 0 0 18px rgba(243, 156, 18, 0.45);
|
||
}
|
||
.fatigue-progress .progress__bar {
|
||
background-image: linear-gradient(90deg, #95a5a6, #7f8c8d); /* 疲勞 - 沉靜的石板灰 */
|
||
box-shadow: 0 0 18px rgba(127, 140, 141, 0.45);
|
||
}
|
||
.water-progress .progress__bar {
|
||
background-image: linear-gradient(90deg, #7fd8ff, #5ea1ff); /* 水分 - 清澈的水藍色 */
|
||
box-shadow: 0 0 18px rgba(94, 161, 255, 0.45);
|
||
}
|
||
|
||
/* --- 新增:同伴物品欄專用網格樣式 --- */
|
||
.companion-inventory-grid {
|
||
display: grid;
|
||
/* 核心指令:固定為4列,每列平分剩餘空間 */
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 12px; /* 格子之間的間距 */
|
||
margin-top: 20px; /* 和上面的技能區塊拉開一點距離 */
|
||
}
|
||
|
||
/* 為了在小螢幕上體驗更好,我們加個媒體查詢 */
|
||
@media (max-width: 960px) {
|
||
.companion-inventory-grid {
|
||
grid-template-columns: repeat(3, 1fr); /* 螢幕稍小時,一行3個 */
|
||
}
|
||
}
|
||
@media (max-width: 640px) {
|
||
.companion-inventory-grid {
|
||
grid-template-columns: repeat(2, 1fr); /* 螢幕再小時,一行2個 */
|
||
}
|
||
}
|
||
|
||
/* ---【最終修復補丁 v4.0】--- */
|
||
/* 規則1: 無論如何,只要是任務卡片的內容區,就先給它隱藏起來! */
|
||
.task-card > .task-body.collapsible__body {
|
||
display: none;
|
||
}
|
||
|
||
/* 規則2: 只有當任務卡片被點擊,擁有了 'active' 狀態後才讓它的內容區顯示出來 */
|
||
.task-card.active > .task-body.collapsible__body {
|
||
display: block;
|
||
}
|
||
/* 🔴 紅色 - 警告(存糧 ≤ 3 天) */
|
||
.crop-status-danger {
|
||
color: #ff4757;
|
||
background: rgba(255, 71, 87, 0.15);
|
||
}
|
||
|
||
/* 🟢 綠色 - 正常(存糧 > 3 天) */
|
||
.crop-status-ready {
|
||
color: #58f0a7;
|
||
background: rgba(88, 240, 167, 0.15);
|
||
}
|
||
/* ============== 新增:作物狀態標籤樣式 ============== */
|
||
.crop-status-indicator {
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.8px;
|
||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
margin-left: 12px;
|
||
}
|
||
.crop-status-ready {
|
||
color: #58f0a7;
|
||
background: rgba(88, 240, 167, 0.15);
|
||
}
|
||
.crop-status-growing {
|
||
color: #a0aec0;
|
||
background: rgba(160, 174, 192, 0.15);
|
||
}
|
||
.item-name .crop-status-indicator { /* 讓二級目錄裡的標籤小一點 */
|
||
transform: scale(0.9);
|
||
}
|
||
|
||
/* ============== 新增:畜牧狀態標籤樣式 ============== */
|
||
.livestock-status-indicator {
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.8px;
|
||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
margin-left: 12px;
|
||
}
|
||
.livestock-status-harvest {
|
||
color: #58f0a7; /* 可收成 - 綠色 */
|
||
background: rgba(88, 240, 167, 0.15);
|
||
}
|
||
.livestock-status-breed {
|
||
color: #3498db; /* 可繁殖 - 藍色 */
|
||
background: rgba(52, 152, 219, 0.15);
|
||
}
|
||
.livestock-status-both {
|
||
color: #f39c12; /* 可收成且可繁殖 - 橙色 */
|
||
background: rgba(243, 156, 18, 0.15);
|
||
}
|
||
.livestock-status-none {
|
||
color: #a0aec0; /* 無特殊狀態 - 灰色 */
|
||
background: rgba(160, 174, 192, 0.15);
|
||
}
|
||
.item-name .livestock-status-indicator { /* 讓二級目錄裡的標籤小一點 */
|
||
transform: scale(0.9);
|
||
}
|
||
|
||
/* ============== 新增:作物加速效果區塊樣式 ============== */
|
||
.crop-acceleration-info {
|
||
padding: 16px;
|
||
margin-bottom: 20px;
|
||
border-radius: 12px;
|
||
background: rgba(30, 45, 70, 0.8);
|
||
border: 1px solid var(--survival-glass-border);
|
||
box-shadow: inset 0 1px 2px rgba(0,0,0,0.4), var(--survival-glow);
|
||
}
|
||
.crop-acceleration-info__title {
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
color: var(--survival-accent-strong);
|
||
margin-bottom: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.crop-acceleration-info__item {
|
||
font-size: 14px;
|
||
padding-left: 28px;
|
||
margin-bottom: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
.crop-acceleration-info__total {
|
||
margin-top: 12px;
|
||
padding-top: 12px;
|
||
border-top: 1px solid var(--survival-border);
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
color: var(--survival-success);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
/* ============== 新增:寵物狀態條樣式 ============== */
|
||
.loyalty-progress .progress__bar {
|
||
background-image: linear-gradient(90deg, #FFD700, #FFA500); /* 忠誠度: 金色漸層 */
|
||
box-shadow: 0 0 18px rgba(255, 215, 0, 0.45);
|
||
}
|
||
.pet-health-progress .progress__bar {
|
||
background-image: linear-gradient(90deg, #27ae60, #16a085); /* 寵物健康: 翠綠色漸層 */
|
||
box-shadow: 0 0 18px rgba(46, 204, 113, 0.45);
|
||
}
|
||
.pet-desire-progress .progress__bar {
|
||
background-image: linear-gradient(90deg, #E94057, #F27121); /* 性慾: 與同伴相同 */
|
||
box-shadow: 0 0 18px rgba(233, 64, 87, 0.45);
|
||
}
|
||
/* 项目/研究/知识库 卡片样式 */
|
||
.task-strip {
|
||
position: relative;
|
||
padding: 12px 16px;
|
||
margin: 8px 0;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
font-weight: 600;
|
||
text-align: center;
|
||
border: 1px solid transparent;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
min-height: 44px;
|
||
}
|
||
.task-strip:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||
}
|
||
.task-strip.main-task {
|
||
background: linear-gradient(135deg, rgba(52, 152, 219, 0.2), rgba(52, 152, 219, 0.1));
|
||
border-color: rgba(52, 152, 219, 0.3);
|
||
color: #3498db;
|
||
}
|
||
.task-strip.side-task {
|
||
background: linear-gradient(135deg, rgba(243, 156, 18, 0.2), rgba(243, 156, 18, 0.1));
|
||
border-color: rgba(243, 156, 18, 0.3);
|
||
color: #f39c12;
|
||
}
|
||
.task-strip.bond-task {
|
||
background: linear-gradient(135deg, rgba(189, 147, 249, 0.25), rgba(189, 147, 249, 0.15));
|
||
border-color: rgba(142, 68, 173, 0.4);
|
||
color: #f0e6ff;
|
||
}
|
||
.task-content {
|
||
text-align: center;
|
||
line-height: 1.4;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
width: 100%;
|
||
}
|
||
.task-detail {
|
||
background: rgba(0, 0, 0, 0.15);
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
margin: 8px 0;
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
.collapse-button {
|
||
width: 100%;
|
||
padding: 8px 12px;
|
||
margin-top: 15px;
|
||
background: rgba(0,0,0,.2);
|
||
border: 1px solid var(--survival-border);
|
||
border-radius: 8px;
|
||
color: var(--survival-warning);
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
.collapse-button:hover {
|
||
background: rgba(52,73,94,.6);
|
||
border-color: var(--survival-warning);
|
||
}
|
||
/* 進度與知識模塊 - 字體大小統一與任務一致 */
|
||
.progress-knowledge-card {
|
||
font-size: 14px;
|
||
}
|
||
.progress-knowledge-card .progress-wrapper > div:first-child,
|
||
.progress-knowledge-card .mini-progress__text {
|
||
font-size: 14px !important;
|
||
}
|
||
.progress-knowledge-card .progress__bar,
|
||
.progress-knowledge-card .mini-progress__bar {
|
||
height: 10px; /* 稍微加高進度條,更易讀 */
|
||
}
|
||
.progress-knowledge-card .item-prop {
|
||
font-size: 14px !important;
|
||
}
|
||
.progress-knowledge-card .prop-key,
|
||
.progress-knowledge-card .prop-val {
|
||
font-size: 14px !important;
|
||
}
|
||
.progress-knowledge-card .skill-desc,
|
||
.progress-knowledge-card .task-desc {
|
||
font-size: 14px !important;
|
||
}
|
||
.progress-knowledge-card .collapse-button {
|
||
font-size: 14px !important;
|
||
padding: 10px 12px;
|
||
}
|
||
/* 卡片內標題 */
|
||
.progress-knowledge-card .task-strip .task-content {
|
||
font-size: 16px !important;
|
||
}
|
||
/* 强制覆盖進度與知識模塊内的所有内联字体大小 */
|
||
.progress-knowledge-card div[style*="font-size"],
|
||
.progress-knowledge-card span[style*="font-size"] {
|
||
font-size: 14px !important;
|
||
}
|
||
/* 進度與知識模塊 - 強制內部所有文字與任務模塊一致 */
|
||
.progress-knowledge-card {
|
||
font-size: 15px !important;
|
||
}
|
||
.progress-knowledge-card * {
|
||
font-size: 15px !important;
|
||
}
|
||
/* 進度條上方文字(例如“進度:12/100”)中的數字部分高亮 */
|
||
.progress-knowledge-card .progress-wrapper > div:first-child,
|
||
.progress-knowledge-card .progress-wrapper > div {
|
||
font-size: 15px !important;
|
||
color: #ffffff !important;
|
||
}
|
||
.progress-knowledge-card .progress-wrapper .progress-number {
|
||
color: var(--survival-warning) !important;
|
||
font-weight: bold !important;
|
||
}
|
||
/* 強制所有帶有內聯樣式的元素統一字體大小和顏色 */
|
||
.progress-knowledge-card div[style*="font-size"],
|
||
.progress-knowledge-card span[style*="font-size"],
|
||
.progress-knowledge-card div[style*="font-size"] *,
|
||
.progress-knowledge-card span[style*="font-size"] * {
|
||
font-size: 15px !important;
|
||
}
|
||
/* 卡片內的描述文字、材料行等 */
|
||
.progress-knowledge-card .skill-desc,
|
||
.progress-knowledge-card .task-desc,
|
||
.progress-knowledge-card .item-prop,
|
||
.progress-knowledge-card .item-prop .prop-key,
|
||
.progress-knowledge-card .item-prop .prop-val,
|
||
.progress-knowledge-card .detail-line,
|
||
.progress-knowledge-card .sub-line {
|
||
font-size: 15px !important;
|
||
color: #cccccc !important;
|
||
}
|
||
/* 材料需求中的數字(已投入/所需)高亮為黃色 */
|
||
.progress-knowledge-card .material-value,
|
||
.progress-knowledge-card .detail-line span:last-child,
|
||
.progress-knowledge-card .sub-line span:last-child {
|
||
color: var(--survival-warning) !important;
|
||
font-weight: bold !important;
|
||
}
|
||
/* 已完成項的灰色和刪除線 */
|
||
.progress-knowledge-card .completed,
|
||
.progress-knowledge-card .detail-line.completed,
|
||
.progress-knowledge-card .sub-line.completed {
|
||
color: #888888 !important;
|
||
text-decoration: line-through !important;
|
||
}
|
||
/* 卡片內的標題(如“材料”、“需求”等) */
|
||
.progress-knowledge-card .field-title {
|
||
font-size: 15px !important;
|
||
font-weight: bold !important;
|
||
color: var(--survival-accent) !important;
|
||
margin-top: 8px !important;
|
||
margin-bottom: 4px !important;
|
||
}
|
||
/* 進度條高度稍微增加 */
|
||
.progress-knowledge-card .progress,
|
||
.progress-knowledge-card .mini-progress {
|
||
height: 10px !important;
|
||
}
|
||
.progress-knowledge-card .progress__bar,
|
||
.progress-knowledge-card .mini-progress__bar {
|
||
height: 10px !important;
|
||
}
|
||
/* 收起按鈕 */
|
||
.progress-knowledge-card .collapse-button {
|
||
font-size: 15px !important;
|
||
padding: 10px 12px;
|
||
}
|
||
/* 項目名稱 / 研究名稱 */
|
||
.progress-knowledge-card .task-content {
|
||
font-size: 16px !important;
|
||
}
|
||
</style></head><body><div id="survival-ui" class="survival-ui" tabindex="0"></div><script>
|
||
|
||
function formatBonusValue(value) {
|
||
if (!value || typeof value !== 'object') {
|
||
return textify(value, '');
|
||
}
|
||
|
||
const bonusEntries = [];
|
||
Object.entries(value).forEach(([bonusKey, bonusValue]) => {
|
||
bonusEntries.push(`${bonusKey}+【${bonusValue}】`);
|
||
});
|
||
return bonusEntries.join('、');
|
||
}
|
||
|
||
function parseTimeToMinutes(timeString) {
|
||
const dayMatch = timeString.match(/第(\d+)天/);
|
||
const timeMatch = timeString.match(/(\d{2}):(\d{2})/);
|
||
|
||
if (!dayMatch || !timeMatch) {
|
||
return null; // 格式不對就直接放棄
|
||
}
|
||
|
||
const days = parseInt(dayMatch[1], 10);
|
||
const hours = parseInt(timeMatch[1], 10);
|
||
const minutes = parseInt(timeMatch[2], 10);
|
||
|
||
return (days * 24 * 60) + (hours * 60) + minutes;
|
||
}
|
||
|
||
// 2. 化妝師:把剩餘的總分鐘數,變成 "X天X小時X分"
|
||
function formatRemainingTime(totalMinutes) {
|
||
if (totalMinutes <= 0) {
|
||
return "已截止";
|
||
}
|
||
|
||
const days = Math.floor(totalMinutes / (24 * 60));
|
||
const hours = Math.floor((totalMinutes % (24 * 60)) / 60);
|
||
const minutes = totalMinutes % 60;
|
||
|
||
let result = "";
|
||
if (days > 0) result += `${days}天`;
|
||
if (hours > 0) result += `${hours}小時`;
|
||
if (minutes > 0) result += `${minutes}分`;
|
||
|
||
return result.trim();
|
||
}
|
||
|
||
// --- 新增的狀態描述函數 ---
|
||
function getFavorabilityStatus(value) {
|
||
if (value < 0) return "仇人";
|
||
if (value >= 0 && value <= 20) return "陌生人";
|
||
if (value > 20 && value <= 40) return "疏離淡漠";
|
||
if (value > 40 && value <= 50) return "平淡如水";
|
||
if (value > 50 && value <= 60) return "親近信賴";
|
||
if (value > 60 && value <= 70) return "依賴眷戀";
|
||
if (value > 70 && value <= 99) return "熱戀痴迷";
|
||
if (value >= 100) return "摯愛不渝";
|
||
return "";
|
||
}
|
||
|
||
function getDesireStatus(value) {
|
||
if (value >= 0 && value <= 20) return "沒感覺";
|
||
if (value > 20 && value <= 40) return "心動";
|
||
if (value > 40 && value <= 60) return "期待愛撫";
|
||
if (value > 60 && value <= 80) return "渴望性愛";
|
||
if (value > 80 && value < 100) return "強烈慾望";
|
||
if (value >= 100) return "失控狂熱";
|
||
return "";
|
||
}
|
||
|
||
function getHealthStatus(value) {
|
||
if (value === 0) return "死亡";
|
||
if (value > 0 && value <= 10) return "重傷昏迷";
|
||
if (value > 10 && value <= 30) return "生病";
|
||
if (value > 30 && value <= 60) return "虛弱";
|
||
if (value > 60 && value <= 100) return "健康";
|
||
if (value > 100) return "超人";
|
||
return "狀態未知";
|
||
}
|
||
|
||
function getCreviceStatus(value) {
|
||
if (value >= 0 && value <= 30) return "萌芽期";
|
||
if (value > 30 && value <= 60) return "依賴期";
|
||
if (value > 60 && value < 100) return "渴望期";
|
||
if (value >= 100) return "崩塌期";
|
||
return "";
|
||
}
|
||
|
||
// --- 寵物狀態描述函數 ---
|
||
function getLoyaltyStatus(value) {
|
||
if (value < 0) return "敵對";
|
||
if (value >= 0 && value <= 20) return "警惕";
|
||
if (value > 20 && value <= 40) return "中立";
|
||
if (value > 40 && value <= 60) return "友善";
|
||
if (value > 60 && value <= 80) return "信賴";
|
||
if (value > 80 && value < 100) return "忠誠";
|
||
if (value >= 100) return "絕對忠誠";
|
||
return "";
|
||
}
|
||
|
||
function getPetHealthStatus(value) {
|
||
if (value === 0) return "死亡";
|
||
if (value > 0 && value <= 20) return "瀕危";
|
||
if (value > 20 && value <= 40) return "重傷";
|
||
if (value > 40 && value <= 60) return "受傷";
|
||
if (value > 60 && value <= 80) return "健康";
|
||
if (value > 80 && value <= 100) return "充滿活力";
|
||
if (value > 100) return "超凡";
|
||
return "狀態未知";
|
||
}
|
||
// --- 狀態描述函數結束 ---
|
||
|
||
function normalizeValue(value) {
|
||
if (Array.isArray(value)) {
|
||
const flattened = [];
|
||
value.forEach((item) => {
|
||
if (Array.isArray(item)) {
|
||
const inner = normalizeValue(item);
|
||
if (Array.isArray(inner)) {
|
||
flattened.push(...inner);
|
||
} else if (inner !== '' && inner !== undefined && inner !== null) {
|
||
flattened.push(inner);
|
||
}
|
||
} else if (item !== undefined && item !== null && item !== '') {
|
||
flattened.push(item);
|
||
}
|
||
});
|
||
if (flattened.length === 0) {
|
||
return '';
|
||
}
|
||
if (flattened.length === 1) {
|
||
return normalizeValue(flattened[0]);
|
||
}
|
||
return flattened;
|
||
}
|
||
return value;
|
||
}
|
||
|
||
function e(value, parse = true) {
|
||
if (value == null) {
|
||
return '';
|
||
}
|
||
const normalized = normalizeValue(value);
|
||
if (Array.isArray(normalized)) {
|
||
const processed = normalized
|
||
.map((item) => e(item, parse))
|
||
.filter((item) => item !== '' && item !== undefined && item !== null);
|
||
if (processed.length === 0) {
|
||
return parse ? '' : [];
|
||
}
|
||
return parse ? processed.join(' / ') : processed;
|
||
}
|
||
if (typeof normalized === 'string' && normalized.includes('->')) {
|
||
const match = normalized.match(/(.+)->(.+)\((.+)\)/);
|
||
if (match) {
|
||
const label = match[2].trim();
|
||
return parse ? a(label) : label;
|
||
}
|
||
}
|
||
return parse ? a(normalized) : normalized;
|
||
}
|
||
|
||
function a(value) {
|
||
if (typeof value === 'number') {
|
||
return value;
|
||
}
|
||
if (typeof value === 'string') {
|
||
const cleaned = value.replace(/,/g, '');
|
||
const parsed = parseFloat(cleaned);
|
||
if (!Number.isNaN(parsed)) {
|
||
return parsed;
|
||
}
|
||
}
|
||
return value;
|
||
}
|
||
|
||
function isMetaOnlyObject(value) {
|
||
return (
|
||
value &&
|
||
typeof value === 'object' &&
|
||
!Array.isArray(value) &&
|
||
'$meta' in value &&
|
||
Object.keys(value).length === 1
|
||
);
|
||
}
|
||
|
||
function textify(value, fallback = '') {
|
||
const raw = e(value, false);
|
||
if (Array.isArray(raw)) {
|
||
const list = raw.map((item) => textify(item, '')).filter(Boolean);
|
||
return list.length > 0 ? list.join(' / ') : fallback;
|
||
}
|
||
if (raw == null) {
|
||
return fallback;
|
||
}
|
||
const str = String(raw).trim();
|
||
return str || fallback;
|
||
}
|
||
|
||
function toEntries(source) {
|
||
if (source == null) {
|
||
return [];
|
||
}
|
||
const raw = e(source, false);
|
||
if (Array.isArray(raw)) {
|
||
const result = [];
|
||
raw.forEach((item, index) => {
|
||
if (item == null) {
|
||
return;
|
||
}
|
||
if (Array.isArray(item)) {
|
||
item.forEach((nested, nestedIndex) => {
|
||
if (nested == null || isMetaOnlyObject(nested)) {
|
||
return;
|
||
}
|
||
result.push([`${index}-${nestedIndex}`, nested]);
|
||
});
|
||
return;
|
||
}
|
||
if (isMetaOnlyObject(item)) {
|
||
return;
|
||
}
|
||
result.push([String(index), item]);
|
||
});
|
||
return result;
|
||
}
|
||
if (typeof raw === 'object') {
|
||
return Object.entries(raw).filter(([key, value]) => {
|
||
if (key === '$meta') {
|
||
return false;
|
||
}
|
||
if (isMetaOnlyObject(value)) {
|
||
return false;
|
||
}
|
||
return value != null;
|
||
});
|
||
}
|
||
return [['0', raw]];
|
||
}
|
||
|
||
function firstMeaningfulObject(source) {
|
||
if (source && typeof source === 'object' && !Array.isArray(source)) {
|
||
const clone = { ...source };
|
||
delete clone.$meta;
|
||
if (Object.keys(clone).length > 0) {
|
||
return clone;
|
||
}
|
||
}
|
||
const entries = toEntries(source);
|
||
for (const [, value] of entries) {
|
||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||
const clone = { ...value };
|
||
delete clone.$meta;
|
||
return clone;
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
|
||
function ensureValueArray(value) {
|
||
return toEntries(value).map(([, item]) => item);
|
||
}
|
||
const s = {
|
||
碳水: 2000,
|
||
脂肪: 100000,
|
||
氮平衡: 200,
|
||
蛋白質: 2400,
|
||
維生素: 100,
|
||
水分: 100,
|
||
飽腹感: 100,
|
||
體溫: 42,
|
||
免疫負荷: 100,
|
||
壓力: 100,
|
||
無聊: 100,
|
||
孤獨: 100,
|
||
睡眠: 100,
|
||
疲勞: 100,
|
||
性慾: 100,
|
||
墮落值: 100,
|
||
};
|
||
|
||
function t(label, current, max, className) {
|
||
const percent = Math.max(0, Math.min(100, max === 0 ? 0 : (current / max) * 100));
|
||
const wrapper = $('<div>').addClass('progress-wrapper');
|
||
const displayText = label.includes('/') ? label : (label ? `${label}: ${current}/${max}` : `${current}/${max}`);
|
||
$('<div>').text(displayText).appendTo(wrapper);
|
||
const bar = $('<div>').addClass('progress');
|
||
if (className) {
|
||
bar.addClass(className);
|
||
}
|
||
$('<div>').addClass('progress__bar').css('width', `${percent}%`).appendTo(bar);
|
||
wrapper.append(bar);
|
||
return wrapper;
|
||
}
|
||
|
||
function d(title, expanded = false) {
|
||
const section = $('<section>').addClass('collapsible');
|
||
if (expanded) {
|
||
section.addClass('active');
|
||
}
|
||
const header = $('<div>').addClass('collapsible__header'); // 我們會在後面添加內容
|
||
const body = $('<div>').addClass('collapsible__body');
|
||
|
||
// 將標題本身放在一個 span 裡,方便和標籤並存
|
||
$('<span>').text(title).appendTo(header);
|
||
|
||
if (expanded) {
|
||
body.css('display', 'block');
|
||
}
|
||
header.on('click', () => {
|
||
section.toggleClass('active');
|
||
body.slideToggle(200);
|
||
});
|
||
section.append(header, body);
|
||
return { $section: section, $header: header, $body: body };
|
||
}
|
||
|
||
function n(title, items, expanded = false) {
|
||
const section = $('<section>').addClass('collapsible');
|
||
if (expanded) {
|
||
section.addClass('active');
|
||
}
|
||
const header = $('<div>').addClass('collapsible__header collapsible__header--with-progress');
|
||
$('<span>').addClass('collapsible__title').text(title).appendTo(header);
|
||
const bars = $('<div>').addClass('collapsible__bars-container');
|
||
items.forEach((item) => {
|
||
const row = $('<div>').addClass('progress-item');
|
||
$('<span>').addClass('progress-label').text(`${item.label}:`).appendTo(row);
|
||
const percent = Math.max(0, Math.min(100, item.max === 0 ? 0 : (item.current / item.max) * 100));
|
||
const miniWrapper = $('<div>').addClass('mini-progress-wrapper');
|
||
const miniBar = $('<div>').addClass('mini-progress');
|
||
if (item.className) {
|
||
miniBar.addClass(item.className);
|
||
}
|
||
$('<div>').addClass('mini-progress__bar').css('width', `${percent}%`).appendTo(miniBar);
|
||
$('<div>').addClass('mini-progress__text').text(`${item.current}/${item.max}`).appendTo(miniWrapper);
|
||
miniWrapper.append(miniBar);
|
||
row.append(miniWrapper);
|
||
bars.append(row);
|
||
});
|
||
header.append(bars);
|
||
$('<span>').addClass('collapsible__indicator').appendTo(header);
|
||
const body = $('<div>').addClass('collapsible__body');
|
||
if (expanded) {
|
||
body.css('display', 'block');
|
||
}
|
||
header.on('click', () => {
|
||
section.toggleClass('active');
|
||
body.slideToggle(200);
|
||
});
|
||
section.append(header, body);
|
||
return { $section: section, $body: body };
|
||
}
|
||
|
||
function i(equipmentSource, itemsSource, campSource, currentWeight, weightLimit, showCamp = true) {
|
||
const modal = $('<div>').addClass('inventory-modal');
|
||
const content = $('<div>').addClass('inventory-modal__content');
|
||
const close = $('<span>').addClass('inventory-modal__close').html('×').on('click', () => modal.remove());
|
||
|
||
const weightContainer = $('<div>').addClass('weight-container');
|
||
const percent = Math.max(0, Math.min(100, weightLimit === 0 ? 0 : (currentWeight / weightLimit) * 100));
|
||
const weightBar = $('<div>').addClass('vertical-progress');
|
||
$('<div>').addClass('vertical-progress__bar').css('height', `${percent}%`).appendTo(weightBar);
|
||
$('<div>').addClass('vertical-progress__label').text('負重').appendTo(weightBar);
|
||
$('<div>').addClass('vertical-progress__value').text(`${currentWeight.toFixed(1)}/${weightLimit}kg`).appendTo(weightBar);
|
||
weightContainer.append(weightBar);
|
||
|
||
const body = $('<div>').addClass('inventory-content');
|
||
|
||
function renderItemCollection(title, source, isListLayout = false) {
|
||
body.append($('<h3>').text(title));
|
||
|
||
// --- 特殊處理:裝備欄(顯示為單一裝備欄)---
|
||
if (title === '裝備') {
|
||
// 創建裝備欄容器
|
||
const equipmentGrid = $('<div>').addClass('inventory-grid equipment-grid').css({
|
||
'grid-template-columns': 'repeat(5, 1fr)', // 一行5個
|
||
'gap': '15px'
|
||
});
|
||
|
||
// 定義裝備部位順序
|
||
const equipmentSlots = [
|
||
'頭部', '頸部', '胸部', '背部', '腰部',
|
||
'右手', '左手', '腿部', '腳部', '其他'
|
||
];
|
||
|
||
equipmentSlots.forEach((slot, index) => {
|
||
const slotContainer = $('<div>').addClass('equipment-slot')
|
||
.attr('data-slot', slot)
|
||
.css({
|
||
'background': 'var(--survival-surface-strong)',
|
||
'border': '1px solid var(--survival-glass-border)',
|
||
'border-radius': '12px',
|
||
'padding': '16px',
|
||
'text-align': 'center',
|
||
'transition': 'all 0.25s ease-out',
|
||
'min-height': '140px', // 稍微增高
|
||
'display': 'flex',
|
||
'flex-direction': 'column',
|
||
'justify-content': 'flex-start' // 從頂部開始
|
||
});
|
||
|
||
// 添加排序
|
||
slotContainer.css('order', index + 1);
|
||
|
||
// 裝備部位標題
|
||
const slotTitle = $('<div>').addClass('slot-title').css({
|
||
'font-size': '20px',
|
||
'font-weight': '700',
|
||
'color': '#d17d2e',
|
||
'letter-spacing': '0.05em',
|
||
'text-transform': 'uppercase',
|
||
'margin-bottom': '12px',
|
||
'padding-bottom': '8px',
|
||
'border-bottom': '1px solid rgba(148, 197, 255, 0.2)',
|
||
'line-height': '1.2'
|
||
}).text(slot);
|
||
|
||
slotContainer.append(slotTitle);
|
||
|
||
// 檢查該部位是否有裝備
|
||
const equipmentItem = source[slot];
|
||
|
||
if (equipmentItem && typeof equipmentItem === 'object' && Object.keys(equipmentItem).length > 0) {
|
||
// 獲取裝備名稱(第一個鍵名)
|
||
const itemKey = Object.keys(equipmentItem)[0];
|
||
const itemData = equipmentItem[itemKey];
|
||
|
||
if (itemKey && itemData) {
|
||
const itemNameDiv = $('<div>').addClass('equipment-item-name').css({
|
||
'font-size': '18px',
|
||
'font-weight': '600',
|
||
'color': 'var(--survival-accent-strong)',
|
||
'margin-bottom': '8px',
|
||
'word-wrap': 'break-word',
|
||
'line-height': '1.3'
|
||
}).text(itemKey);
|
||
|
||
itemNameDiv.append(
|
||
$('<span>').addClass('corner-decor corner-tl'),
|
||
$('<span>').addClass('corner-decor corner-tr'),
|
||
$('<span>').addClass('corner-decor corner-bl'),
|
||
$('<span>').addClass('corner-decor corner-br')
|
||
);
|
||
|
||
slotContainer.append(itemNameDiv);
|
||
|
||
// 顯示裝備屬性
|
||
const itemProps = $('<div>').addClass('equipment-item-props').css({
|
||
'font-size': '12px',
|
||
'color': 'var(--survival-text)',
|
||
'opacity': '0.8',
|
||
'text-align': 'left',
|
||
'line-height': '1.4'
|
||
});
|
||
|
||
// 完整的屬性顯示邏輯
|
||
const orderedProps = ['數量', '位置', '重量', '耐久度'];
|
||
const attributes = new Map();
|
||
|
||
// 先按照順序處理
|
||
orderedProps.forEach(k => {
|
||
if (itemData.hasOwnProperty(k)) {
|
||
attributes.set(k, itemData[k]);
|
||
}
|
||
});
|
||
|
||
// 處理狀態類型和狀態
|
||
const stateType = textify(itemData['狀態類型'] ?? '', '');
|
||
if (stateType && itemData.hasOwnProperty('狀態')) {
|
||
// 如果狀態類型不是'耐久度',或者還沒有設置耐久度,則添加
|
||
if (stateType.toLowerCase() !== '耐久度' || !attributes.has('耐久度')) {
|
||
attributes.set(stateType, itemData['狀態']);
|
||
}
|
||
}
|
||
|
||
// 處理其他屬性
|
||
Object.entries(itemData).forEach(([k, v]) => {
|
||
if (!attributes.has(k) && k !== '$meta' && k !== '狀態類型' && k !== '狀態') {
|
||
if (k === '加值' && v && typeof v === 'object') {
|
||
// 創建加值描述文字
|
||
const bonusEntries = [];
|
||
Object.entries(v).forEach(([bonusKey, bonusValue]) => {
|
||
bonusEntries.push(`${bonusKey}+【${bonusValue}】`);
|
||
});
|
||
attributes.set(k, bonusEntries.join('、'));
|
||
} else {
|
||
attributes.set(k, v);
|
||
}
|
||
}
|
||
});
|
||
|
||
// 顯示所有屬性
|
||
attributes.forEach((v, k) => {
|
||
if (v !== null && v !== '' && v !== undefined) {
|
||
const line = $('<div>').addClass('item-prop equipment-prop');
|
||
line.attr('data-prop', k);
|
||
|
||
// 如果是加值欄位,添加特殊類別
|
||
if (k === '加值') {
|
||
line.addClass('bonus-prop');
|
||
// 在加值前面添加分隔線
|
||
const separator = $('<div>').addClass('bonus-separator').css({
|
||
'height': '1px',
|
||
'background': 'linear-gradient(90deg, transparent, rgba(148, 197, 255, 0.4), transparent)',
|
||
'margin': '10px 0 8px 0',
|
||
'width': '100%'
|
||
});
|
||
itemProps.append(separator);
|
||
}
|
||
|
||
let displayValue = textify(v, '');
|
||
if (k === '重量' && typeof v === 'number') {
|
||
displayValue += 'kg';
|
||
}
|
||
|
||
if (k === '數量') {
|
||
displayValue = String(displayValue);
|
||
}
|
||
|
||
if (k === '描述' && displayValue.length > 80) {
|
||
displayValue = displayValue.substring(0, 77) + '...';
|
||
}
|
||
|
||
$('<span>').addClass('prop-key').text(String(k)).appendTo(line);
|
||
$('<span>').addClass('prop-sep').text(': ').appendTo(line);
|
||
$('<span>').addClass('prop-val').text(displayValue).appendTo(line);
|
||
|
||
itemProps.append(line);
|
||
|
||
// 如果是加值欄位,在後面也添加一個小分隔線
|
||
if (k === '加值') {
|
||
const bottomSeparator = $('<div>').css({
|
||
'height': '1px',
|
||
'background': 'rgba(148, 197, 255, 0.1)',
|
||
'margin': '8px 0 4px 0',
|
||
'width': '100%'
|
||
});
|
||
itemProps.append(bottomSeparator);
|
||
}
|
||
}
|
||
});
|
||
|
||
if (attributes.size > 0) {
|
||
slotContainer.append(itemProps);
|
||
}
|
||
|
||
// 懸停效果
|
||
slotContainer.hover(
|
||
function() {
|
||
$(this).css({
|
||
'transform': 'translateY(-3px)',
|
||
'box-shadow': '0 5px 15px rgba(0,0,0,.3)',
|
||
'background': 'rgba(52,73,94,.9)',
|
||
'border-color': 'var(--survival-accent)'
|
||
});
|
||
},
|
||
function() {
|
||
$(this).css({
|
||
'transform': 'translateY(0)',
|
||
'box-shadow': 'none',
|
||
'background': 'var(--survival-surface-strong)',
|
||
'border-color': 'var(--survival-glass-border)'
|
||
});
|
||
}
|
||
);
|
||
}
|
||
} else {
|
||
// 該部位沒有裝備
|
||
const emptyText = $('<div>').addClass('equipment-empty').css({
|
||
'font-size': '20px'
|
||
}).text('未裝備');
|
||
|
||
slotContainer.append(emptyText);
|
||
}
|
||
|
||
equipmentGrid.append(slotContainer);
|
||
});
|
||
|
||
body.append(equipmentGrid);
|
||
return;
|
||
}
|
||
// --- 裝備欄特殊處理結束 ---
|
||
|
||
const container = isListLayout ? $('<div>').addClass('inventory-list') : $('<div>').addClass('inventory-grid');
|
||
const entries = toEntries(source);
|
||
|
||
if (entries.length === 0) {
|
||
container.append(isListLayout ? $('<p>').addClass('empty-text').text('無') : $('<div>').addClass('inventory-item empty-text').text('空'));
|
||
} else {
|
||
entries.forEach(([key, value]) => {
|
||
if (value && typeof value === 'object') {
|
||
const itemCard = isListLayout ? $('<div>').addClass('inventory-list-item') : $('<div>').addClass('inventory-item');
|
||
$('<div>').addClass('item-name').text(String(key)).appendTo(itemCard);
|
||
const props = $('<div>').addClass('item-props');
|
||
|
||
// --- 這是新手把手教你的邏輯 ---
|
||
const orderedProps = [
|
||
'數量', '位置', '重量', '耐久度'
|
||
];
|
||
|
||
// 處理狀態類型和狀態,把 '耐久度' 這種情況也包含進來
|
||
const stateType = textify(value['狀態類型'] ?? '', '');
|
||
if (stateType && !orderedProps.includes(stateType)) {
|
||
orderedProps.push(stateType);
|
||
}
|
||
|
||
orderedProps.forEach(key => {
|
||
let val = value[key];
|
||
// 特殊處理狀態
|
||
if (key === stateType) {
|
||
val = value['狀態'];
|
||
}
|
||
|
||
if (val !== null && val !== undefined && val !== '') {
|
||
const line = $('<div>').addClass('item-prop');
|
||
$('<span>').addClass('prop-key').text(String(key)).appendTo(line);
|
||
$('<span>').addClass('prop-sep').text(': ').appendTo(line);
|
||
let displayValue = textify(val, '');
|
||
if (key === '重量' && typeof val === 'number') {
|
||
displayValue += 'kg';
|
||
}
|
||
$('<span>').addClass('prop-val').text(displayValue).appendTo(line);
|
||
props.append(line);
|
||
}
|
||
});
|
||
|
||
// 處理剩下的屬性,比如'描述'
|
||
Object.entries(value).forEach(([key, val]) => {
|
||
// 檢查這個屬性是不是我們已經手動處理過的
|
||
if (!orderedProps.includes(key) && key !== '$meta' && key !== '狀態類型' && key !== '狀態' && key !== stateType) {
|
||
if (val !== null && val !== undefined && val !== '') {
|
||
const line = $('<div>').addClass('item-prop');
|
||
|
||
// 如果是加值欄位,添加特殊類別
|
||
if (key === '加值') {
|
||
line.addClass('bonus-prop');
|
||
// 在加值前面添加分隔線
|
||
const separator = $('<div>').addClass('bonus-separator').css({
|
||
'height': '1px',
|
||
'background': 'linear-gradient(90deg, transparent, rgba(148, 197, 255, 0.4), transparent)',
|
||
'margin': '10px 0 8px 0',
|
||
'width': '100%'
|
||
});
|
||
props.append(separator);
|
||
}
|
||
|
||
$('<span>').addClass('prop-key').text(String(key)).appendTo(line);
|
||
$('<span>').addClass('prop-sep').text(': ').appendTo(line);
|
||
|
||
let displayValue = '';
|
||
if (key === '加值' && val && typeof val === 'object') {
|
||
// 處理加值物件
|
||
const bonusEntries = [];
|
||
Object.entries(val).forEach(([bonusKey, bonusValue]) => {
|
||
bonusEntries.push(`${bonusKey}+【${bonusValue}】`);
|
||
});
|
||
displayValue = bonusEntries.join('、');
|
||
} else {
|
||
displayValue = textify(val, '');
|
||
if (key === '重量' && typeof val === 'number') {
|
||
displayValue += 'kg';
|
||
}
|
||
}
|
||
|
||
$('<span>').addClass('prop-val').text(displayValue).appendTo(line);
|
||
props.append(line);
|
||
|
||
// 如果是加值欄位,在後面也添加一個小分隔線
|
||
if (key === '加值') {
|
||
const bottomSeparator = $('<div>').css({
|
||
'height': '1px',
|
||
'background': 'rgba(148, 197, 255, 0.1)',
|
||
'margin': '8px 0 4px 0',
|
||
'width': '100%'
|
||
});
|
||
props.append(bottomSeparator);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
itemCard.append(props);
|
||
container.append(itemCard);
|
||
}
|
||
});
|
||
}
|
||
body.append(container);
|
||
}
|
||
|
||
|
||
renderItemCollection('裝備', equipmentSource, true);
|
||
renderItemCollection('攜帶物品', itemsSource);
|
||
|
||
if (showCamp && campSource && Object.keys(campSource).length > 0) {
|
||
Object.entries(campSource).forEach(([campName, campData]) => {
|
||
if (campData && typeof campData === 'object') {
|
||
Object.entries(campData).forEach(([categoryKey, categoryData]) => {
|
||
if(categoryData && typeof categoryData === 'object' && Object.values(categoryData).some(item => typeof item === 'object')) {
|
||
renderItemCollection(`${campName} - ${categoryKey}`, categoryData);
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
content.append(weightContainer, body, close);
|
||
modal.append(content);
|
||
$('body').append(modal);
|
||
}
|
||
|
||
function createStatusEffects(container, data) {
|
||
// 使用新的變數路徑 '狀態.生效效果'
|
||
const source = data?.['狀態']?.['生效效果'];
|
||
if (source == null) {
|
||
return;
|
||
}
|
||
|
||
// 新的資料結構:物件,鍵是狀態型別,值包含"狀態"和"效果"
|
||
const collected = [];
|
||
|
||
// 遍歷狀態物件
|
||
if (typeof source === 'object' && !Array.isArray(source)) {
|
||
Object.values(source).forEach((statusObj) => {
|
||
if (statusObj && typeof statusObj === 'object') {
|
||
// 提取狀態名稱
|
||
const stateName = statusObj['狀態'];
|
||
if (stateName) {
|
||
// 過濾掉"狀態正常"
|
||
if (stateName.trim() === '狀態正常') {
|
||
return;
|
||
}
|
||
|
||
// ✅ 直接使用腳本已經生成的效果描述
|
||
const effect = statusObj['效果'] || '';
|
||
|
||
collected.push({
|
||
name: stateName,
|
||
effect: effect
|
||
});
|
||
}
|
||
}
|
||
});
|
||
} else {
|
||
// 處理其他可能的資料結構(備用邏輯)
|
||
const collect = (value) => {
|
||
if (value == null) return;
|
||
if (Array.isArray(value)) {
|
||
value.forEach(collect);
|
||
return;
|
||
}
|
||
if (typeof value === 'object') {
|
||
Object.values(value).forEach(collect);
|
||
return;
|
||
}
|
||
const text = String(value);
|
||
text.split(/[,,、\s]+/)
|
||
.map((item) => item.trim())
|
||
.filter(Boolean)
|
||
.forEach((item) => {
|
||
if (item.trim() === '狀態正常') return;
|
||
collected.push({ name: item, effect: '' });
|
||
});
|
||
};
|
||
collect(e(source, false));
|
||
}
|
||
|
||
if (collected.length === 0) {
|
||
const wrapper = $('<div>').addClass('status-effects-container').css('display', 'none');
|
||
container.append(wrapper);
|
||
return;
|
||
}
|
||
|
||
const wrapper = $('<div>').addClass('status-effects-container');
|
||
$('<div>').addClass('section__title').text('當前狀態').appendTo(wrapper);
|
||
|
||
const list = $('<div>').addClass('status-effects-list');
|
||
|
||
collected.forEach((status) => {
|
||
const statusContainer = $('<div>').addClass('status-effect-container');
|
||
const statusName = $('<div>').addClass('status-effect-name')
|
||
.text(status.name)
|
||
.attr('title', '點選檢視效果');
|
||
|
||
if (status.effect) {
|
||
statusName.addClass('has-effect');
|
||
statusName.on('click', function() {
|
||
$(this).toggleClass('expanded');
|
||
$(this).next('.status-effect-detail').slideToggle(200);
|
||
});
|
||
const effectDetail = $('<div>').addClass('status-effect-detail')
|
||
.hide()
|
||
.text(status.effect);
|
||
statusContainer.append(statusName, effectDetail);
|
||
} else {
|
||
statusContainer.append(statusName);
|
||
}
|
||
list.append(statusContainer);
|
||
});
|
||
|
||
wrapper.append(list);
|
||
container.append(wrapper);
|
||
}
|
||
|
||
const IMAGE_VIEWER_STORAGE_KEY = 'survivalImageViewerState';
|
||
const IMAGE_VIEWER_FALLBACK_URL = 'https://cdn.phototourl.com/free/2026-05-23-8b4cc932-bb3f-4107-9a72-9160fb9e2335.png';
|
||
|
||
function loadSavedImageViewerState() {
|
||
try {
|
||
const raw = localStorage.getItem(IMAGE_VIEWER_STORAGE_KEY);
|
||
if (!raw) return null;
|
||
return JSON.parse(raw);
|
||
} catch (error) {
|
||
console.error('讀取地圖查看器狀態失敗:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function persistImageViewerState(state) {
|
||
try {
|
||
if (!state) localStorage.removeItem(IMAGE_VIEWER_STORAGE_KEY);
|
||
else localStorage.setItem(IMAGE_VIEWER_STORAGE_KEY, JSON.stringify(state));
|
||
} catch (error) {
|
||
console.error('保存地圖查看器狀態失敗:', error);
|
||
}
|
||
}
|
||
|
||
let savedImageViewerState = loadSavedImageViewerState();
|
||
|
||
function resolveMapImageUrl(statData, displayData) {
|
||
const candidates = [
|
||
statData?.['地圖']?.['圖片鏈接'], statData?.['地圖']?.['鏈接'], statData?.['地圖鏈接'],
|
||
statData?.['系統']?.['地圖鏈接'], statData?.['系統']?.['地圖']?.['鏈接'],
|
||
displayData?.['地圖']?.['圖片鏈接'], displayData?.['地圖']?.['鏈接'], displayData?.['地圖鏈接'],
|
||
];
|
||
for (const candidate of candidates) {
|
||
const url = textify(candidate, '').trim();
|
||
if (url) return url;
|
||
}
|
||
return IMAGE_VIEWER_FALLBACK_URL || '';
|
||
}
|
||
|
||
class ImageViewer {
|
||
constructor(root, initialState) {
|
||
this.root = root;
|
||
this.canvas = root.find('.image-viewer__canvas');
|
||
this.image = root.find('.image-viewer__image');
|
||
this.placeholder = root.find('.image-viewer__placeholder');
|
||
this.status = root.find('.image-viewer__status');
|
||
this.controlButtons = root.find('.image-viewer__control-button');
|
||
this.scale = initialState?.scale ?? 1;
|
||
this.translateX = initialState?.translateX ?? 0;
|
||
this.translateY = initialState?.translateY ?? 0;
|
||
this.imageUrl = initialState?.url ?? '';
|
||
this.imageLoaded = false;
|
||
this.activePointers = new Map();
|
||
this.initialPinchDistance = 0;
|
||
this.minScale = 0.4;
|
||
this.maxScale = 4;
|
||
this.bindEvents();
|
||
if (this.imageUrl) this.loadImage(this.imageUrl, false);
|
||
else this.updatePlaceholder(true);
|
||
}
|
||
|
||
bindEvents() {
|
||
this.controlButtons.each((_, button) => {
|
||
const $button = $(button);
|
||
const action = $button.data('action');
|
||
if (action === 'zoom-in') $button.on('click.imageViewer', () => this.zoomTo(this.scale * 1.2));
|
||
else if (action === 'zoom-out') $button.on('click.imageViewer', () => this.zoomTo(this.scale * 0.8));
|
||
else if (action === 'reset') $button.on('click.imageViewer', () => this.resetTransform());
|
||
});
|
||
|
||
const canvasElement = this.canvas[0];
|
||
if (canvasElement) canvasElement.addEventListener('wheel', (event) => this.handleWheel(event), { passive: false });
|
||
|
||
this.canvas
|
||
.on('pointerdown.imageViewer', (event) => this.handlePointerDown(event))
|
||
.on('pointermove.imageViewer', (event) => this.handlePointerMove(event))
|
||
.on('pointerup.imageViewer pointercancel.imageViewer pointerleave.imageViewer', (event) => this.handlePointerUp(event));
|
||
}
|
||
|
||
loadImage(url, resetTransform = true) {
|
||
this.setStatus('正在加載圖片...');
|
||
this.placeholder.hide();
|
||
this.imageLoaded = false;
|
||
this.image.off('.imageViewer')
|
||
.on('load.imageViewer', () => {
|
||
this.imageLoaded = true;
|
||
this.imageUrl = url;
|
||
this.setStatus('加載完成');
|
||
if (resetTransform) this.resetTransform();
|
||
else this.applyTransform();
|
||
this.canvas.addClass('has-image');
|
||
this.placeholder.hide();
|
||
setTimeout(() => this.setStatus(url), 800);
|
||
})
|
||
.on('error.imageViewer', () => {
|
||
this.imageLoaded = false;
|
||
this.setStatus('圖片加載失敗。');
|
||
this.updatePlaceholder(false, '圖片加載失敗。');
|
||
});
|
||
this.image.attr('src', url);
|
||
}
|
||
|
||
resetTransform() {
|
||
this.scale = 1;
|
||
this.translateX = 0;
|
||
this.translateY = 0;
|
||
this.applyTransform();
|
||
}
|
||
|
||
applyTransform() {
|
||
const transform = `translate(-50%, -50%) translate(${this.translateX}px, ${this.translateY}px) scale(${this.scale})`;
|
||
this.image.css('transform', transform);
|
||
}
|
||
|
||
zoomTo(newScale, center = null) {
|
||
if (!this.imageLoaded) return;
|
||
const previousScale = this.scale;
|
||
this.scale = Math.min(this.maxScale, Math.max(this.minScale, newScale));
|
||
if (center) {
|
||
const rect = this.canvas[0]?.getBoundingClientRect();
|
||
if (rect) {
|
||
const offsetX = center.x - rect.left;
|
||
const offsetY = center.y - rect.top;
|
||
const scaleRatio = this.scale / previousScale;
|
||
this.translateX = (this.translateX + offsetX) - offsetX * scaleRatio;
|
||
this.translateY = (this.translateY + offsetY) - offsetY * scaleRatio;
|
||
}
|
||
}
|
||
this.applyTransform();
|
||
}
|
||
|
||
handleWheel(event) {
|
||
if (!this.imageLoaded) return;
|
||
event.preventDefault();
|
||
const delta = event.deltaY > 0 ? 0.9 : 1.1;
|
||
this.zoomTo(this.scale * delta, { x: event.clientX, y: event.clientY });
|
||
}
|
||
|
||
handlePointerDown(event) {
|
||
if (!this.imageLoaded) return;
|
||
this.canvas.addClass('is-grabbing');
|
||
this.canvas[0]?.setPointerCapture?.(event.pointerId);
|
||
this.activePointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
||
if (this.activePointers.size === 2) this.initialPinchDistance = this.computePinchDistance();
|
||
event.preventDefault();
|
||
}
|
||
|
||
handlePointerMove(event) {
|
||
if (!this.activePointers.has(event.pointerId)) return;
|
||
const previous = this.activePointers.get(event.pointerId);
|
||
const current = { x: event.clientX, y: event.clientY };
|
||
this.activePointers.set(event.pointerId, current);
|
||
|
||
if (this.activePointers.size === 1) {
|
||
this.translateX += current.x - previous.x;
|
||
this.translateY += current.y - previous.y;
|
||
this.applyTransform();
|
||
return;
|
||
}
|
||
|
||
if (this.activePointers.size >= 2) {
|
||
const newDistance = this.computePinchDistance();
|
||
if (this.initialPinchDistance === 0) {
|
||
this.initialPinchDistance = newDistance;
|
||
return;
|
||
}
|
||
const factor = newDistance / this.initialPinchDistance;
|
||
this.zoomTo(this.scale * factor, this.computePinchCenter());
|
||
this.initialPinchDistance = newDistance;
|
||
}
|
||
}
|
||
|
||
handlePointerUp(event) {
|
||
this.activePointers.delete(event.pointerId);
|
||
if (this.activePointers.size <= 1) this.initialPinchDistance = 0;
|
||
if (this.activePointers.size === 0) this.canvas.removeClass('is-grabbing');
|
||
}
|
||
|
||
computePinchDistance() {
|
||
const pointers = Array.from(this.activePointers.values());
|
||
if (pointers.length < 2) return 0;
|
||
return Math.hypot(pointers[0].x - pointers[1].x, pointers[0].y - pointers[1].y) || 0.0001;
|
||
}
|
||
|
||
computePinchCenter() {
|
||
const pointers = Array.from(this.activePointers.values());
|
||
if (pointers.length < 2) return null;
|
||
return { x: (pointers[0].x + pointers[1].x) / 2, y: (pointers[0].y + pointers[1].y) / 2 };
|
||
}
|
||
|
||
updatePlaceholder(show, message = '') {
|
||
this.placeholder.toggle(show).text(message || '地圖圖片將自動讀取...');
|
||
}
|
||
|
||
setStatus(message) {
|
||
this.status.text(message || '');
|
||
}
|
||
|
||
getState() {
|
||
if (!this.imageUrl) return null;
|
||
return { url: this.imageUrl, scale: this.scale, translateX: this.translateX, translateY: this.translateY };
|
||
}
|
||
|
||
destroy() {
|
||
this.canvas.off('.imageViewer');
|
||
this.image.off('.imageViewer');
|
||
this.controlButtons.off('.imageViewer');
|
||
this.activePointers.clear();
|
||
}
|
||
}
|
||
|
||
function openImageViewerModal(statData, displayData) {
|
||
$('.image-viewer-modal').remove();
|
||
const modal = $('<div>').addClass('image-viewer-modal');
|
||
const content = $('<div>').addClass('image-viewer-modal__content');
|
||
const header = $('<div>').addClass('image-viewer__header').append(
|
||
$('<div>').addClass('image-viewer__title').text('地圖查看器'),
|
||
$('<button>').addClass('image-viewer__close').html('×')
|
||
);
|
||
const toolbar = $('<div>').addClass('image-viewer__toolbar').append(
|
||
$('<div>').addClass('image-viewer__hint').text('支持滾輪與觸控縮放'),
|
||
$('<div>').addClass('image-viewer__controls').append(
|
||
$('<button>').addClass('image-viewer__control-button').data('action', 'zoom-out').text('-'),
|
||
$('<button>').addClass('image-viewer__control-button').data('action', 'reset').text('⟳'),
|
||
$('<button>').addClass('image-viewer__control-button').data('action', 'zoom-in').text('+')
|
||
)
|
||
);
|
||
const canvas = $('<div>').addClass('image-viewer__canvas').append(
|
||
$('<img>').addClass('image-viewer__image').attr({ draggable: 'false', alt: '地圖' }),
|
||
$('<div>').addClass('image-viewer__placeholder'),
|
||
$('<div>').addClass('image-viewer__status')
|
||
);
|
||
content.append(header, toolbar, canvas);
|
||
modal.append(content);
|
||
$('body').append(modal);
|
||
|
||
const initialState = { ...savedImageViewerState };
|
||
const autoUrl = resolveMapImageUrl(statData, displayData);
|
||
if (autoUrl) initialState.url = autoUrl;
|
||
else if (!initialState.url && IMAGE_VIEWER_FALLBACK_URL) initialState.url = IMAGE_VIEWER_FALLBACK_URL;
|
||
|
||
const viewer = new ImageViewer(content, initialState);
|
||
const closeModal = () => {
|
||
viewer.destroy();
|
||
savedImageViewerState = viewer.getState();
|
||
persistImageViewerState(savedImageViewerState);
|
||
modal.remove();
|
||
$(document).off('keydown.imageViewer');
|
||
};
|
||
content.find('.image-viewer__close').on('click', closeModal);
|
||
modal.on('click', (e) => e.target === modal[0] && closeModal());
|
||
$(document).on('keydown.imageViewer', (e) => e.key === 'Escape' && closeModal());
|
||
}
|
||
|
||
function showDrawConfirmModal(drawType, survivalPoints) {
|
||
$('.draw-confirm-modal').remove();
|
||
const pointName = '求生點數';
|
||
const safePoints = Number.isFinite(survivalPoints) ? survivalPoints : 0;
|
||
|
||
const modal = $('<div>').addClass('draw-confirm-modal');
|
||
const content = $('<div>').addClass('draw-confirm-modal__content').append(
|
||
$('<div>').addClass('draw-confirm-modal__glow'),
|
||
$('<div>').addClass('draw-confirm-modal__title').text('求生抽卡確認'),
|
||
$('<div>').addClass('draw-confirm-modal__desc').text(`是否消耗 100 ${pointName}進行抽卡?`),
|
||
$('<div>').addClass('draw-confirm-modal__points').html(`當前${pointName} <span>${safePoints}</span>`),
|
||
$('<div>').addClass('draw-confirm-modal__actions'),
|
||
$('<div>').addClass('draw-confirm-modal__status')
|
||
);
|
||
const confirmBtn = $('<button>').addClass('draw-confirm-modal__button draw-confirm-modal__confirm').text('確認抽卡');
|
||
const cancelBtn = $('<button>').addClass('draw-confirm-modal__button draw-confirm-modal__cancel').text('取消');
|
||
content.find('.draw-confirm-modal__actions').append(confirmBtn, cancelBtn);
|
||
modal.append(content);
|
||
$('body').append(modal);
|
||
|
||
const closeModal = () => modal.remove();
|
||
cancelBtn.on('click', closeModal);
|
||
modal.on('click', (e) => e.target === modal[0] && closeModal());
|
||
$(document).one('keydown', (e) => e.key === 'Escape' && closeModal());
|
||
|
||
if (safePoints < 100) {
|
||
confirmBtn.addClass('is-disabled').prop('disabled', true);
|
||
content.find('.draw-confirm-modal__status').text(`${pointName}不足。`);
|
||
return;
|
||
}
|
||
confirmBtn.on('click', async () => {
|
||
if (confirmBtn.hasClass('is-loading')) return;
|
||
confirmBtn.addClass('is-loading').prop('disabled', true);
|
||
try {
|
||
const msg = `阿彬按下了求生抽卡按鈕,消耗100${pointName}`;
|
||
await triggerSlash(`/send ${msg}`);
|
||
content.find('.draw-confirm-modal__status').text('請求已發送!');
|
||
setTimeout(closeModal, 900);
|
||
} catch (error) {
|
||
console.error('抽卡請求失敗:', error);
|
||
content.find('.draw-confirm-modal__status').text('發送失敗。');
|
||
confirmBtn.removeClass('is-loading').prop('disabled', false);
|
||
}
|
||
});
|
||
}
|
||
|
||
function renderNutritionDetails(target, statData) {
|
||
const wrapper = $('<div>').addClass('nutrition-hydration-wrapper');
|
||
const phys = statData['生理'] ?? {};
|
||
const carb = Number(e(phys['碳水'] ?? 0)), fat = Number(e(phys['脂肪'] ?? 0)),
|
||
protein = Number(e(phys['蛋白質'] ?? 0)), vitamin = Number(e(phys['維生素'] ?? 0)),
|
||
nitrogen = Number(e(phys['氮平衡'] ?? 0));
|
||
wrapper.append(t('碳水', carb, s['碳水'], 'carbs-progress'));
|
||
wrapper.append(t('脂肪', fat, s['脂肪'], 'fat-progress'));
|
||
const nitrogenWrapper = $('<div>').addClass('progress-wrapper');
|
||
$('<div>').text(`氮平衡: ${nitrogen}`).appendTo(nitrogenWrapper);
|
||
const gradient = $('<div>').addClass('nitrogen-gradient');
|
||
$('<div>').addClass('nitrogen-marker').css('left', `${((Math.max(-100, Math.min(100, nitrogen)) + 100) / 200) * 100}%`).appendTo(gradient);
|
||
wrapper.append(nitrogenWrapper.append(gradient));
|
||
wrapper.append(t('蛋白質', protein, s['蛋白質'], 'protein-progress'));
|
||
wrapper.append(t('維生素', vitamin, s['維生素'], 'vitamin-progress'));
|
||
target.append(wrapper);
|
||
}
|
||
|
||
function renderPhysMental(target, physData, mentalData, fullData) {
|
||
const wrapper = $('<div>').addClass('phys-mental-wrapper');
|
||
const physCol = $('<div>').addClass('phys-col').append($('<div>').addClass('section__title').text('生理'));
|
||
const mentalCol = $('<div>').addClass('mental-col').append($('<div>').addClass('section__title').text('精神'));
|
||
const temperature = Number(e(physData['體溫'] ?? 37));
|
||
const tempWrapper = $('<div>').addClass('progress-wrapper').append($('<div>').text(`體溫: ${temperature}°C (36.5-37.5)`));
|
||
const tempGradient = $('<div>').addClass('temp-gradient');
|
||
$('<div>').addClass('temp-marker').css('left', `${Math.min(Math.max(((temperature - 35) / 5) * 100, 0), 100)}%`).appendTo(tempGradient);
|
||
$('<div>').addClass('temp-normal-zone').css({ left: '30%', width: '20%' }).appendTo(tempGradient);
|
||
physCol.append(tempWrapper.append(tempGradient));
|
||
physCol.append(t('疲勞', Number(e(fullData['狀態']?.['疲勞'] ?? 0)), (Number(e(fullData['屬性']?.['耐力']?.['等級'] ?? 25)) || 25) * 4, 'fatigue-progress'));
|
||
const immuneLoad = Number(e(physData['免疫負荷'] ?? 0)), immunePower = Number(e(fullData['次生屬性']?.['免疫力'] ?? 0));
|
||
const immuneWrapper = $('<div>').addClass('progress-wrapper').append($('<div>').text(`免疫: ${immuneLoad} / ${immunePower}`));
|
||
const immuneBar = $('<div>').addClass('immune-bar');
|
||
$('<div>').addClass('immune-fill').css('width', `${Math.min(immuneLoad, 100)}%`).appendTo(immuneBar);
|
||
$('<div>').addClass('immune-marker').css('left', `${Math.min(immunePower, 100)}%`).appendTo(immuneBar);
|
||
physCol.append(immuneWrapper.append(immuneBar));
|
||
|
||
Object.entries(mentalData).forEach(([key, value]) => mentalCol.append(t(key, Number(e(value)), s[key] ?? 100)));
|
||
wrapper.append(physCol, mentalCol);
|
||
target.append(wrapper);
|
||
}
|
||
|
||
function renderAttributesAndSkills(target, attributesSource, skillsSource, talentsSource, companionDetails = null) {
|
||
// ============== 參數保護 ==============
|
||
attributesSource = attributesSource || {};
|
||
skillsSource = skillsSource || {};
|
||
talentsSource = talentsSource || {};
|
||
|
||
// 確保 companionDetails 是物件(如果不是就設為空物件)
|
||
if (companionDetails === null || typeof companionDetails !== 'object') {
|
||
companionDetails = {};
|
||
}
|
||
|
||
const wrapper = $('<div>').addClass('attr-skill-wrapper');
|
||
const leftCol = $('<div>').addClass('attr-col');
|
||
|
||
// ============== 屬性渲染 ==============
|
||
$('<div>').addClass('section__title').text('屬性').appendTo(leftCol);
|
||
|
||
// 安全地獲取屬性物件
|
||
const attributes = firstMeaningfulObject(attributesSource);
|
||
if (Object.keys(attributes).length > 0) {
|
||
Object.entries(attributes).forEach(([name, detail]) => {
|
||
if (detail && typeof detail === 'object') {
|
||
const level = Number(e(detail['等級'] ?? 0));
|
||
const exp = Number(e(detail['當前經驗'] ?? 0));
|
||
const required = Number(e(detail['升級所需經驗'] ?? 1)) || 1;
|
||
leftCol.append($('<div>').text(`${name} Lv.${level}`), t('', exp, required));
|
||
}
|
||
});
|
||
} else {
|
||
leftCol.append($('<p>').addClass('empty-text').text('暫無屬性'));
|
||
}
|
||
|
||
// ============== 天賦渲染 ==============
|
||
const talentEntries = toEntries(talentsSource);
|
||
if (talentEntries.length > 0) {
|
||
leftCol.append($('<div>').addClass('section__title').css('margin-top', '20px').text('天賦/特性'));
|
||
|
||
talentEntries.forEach(([name, value]) => {
|
||
const item = $('<div>').addClass('skill-item');
|
||
|
||
// 天賦名稱(安全處理)
|
||
let talentName = name;
|
||
if (value && typeof value === 'object' && value.name) {
|
||
talentName = value.name;
|
||
}
|
||
$('<div>').addClass('skill-name').text(talentName).appendTo(item);
|
||
|
||
if (value && typeof value === 'object') {
|
||
// 先顯示描述
|
||
if (value['描述']) {
|
||
$('<div>').addClass('skill-desc').text(textify(value['描述'], '')).appendTo(item);
|
||
}
|
||
|
||
// 顯示其他非描述、非加值的內容
|
||
Object.entries(value).forEach(([key, val]) => {
|
||
if (key !== '$meta' && key !== 'name' && key !== '描述' && key !== '加值') {
|
||
$('<div>').addClass('skill-desc').text(`【${key}】${textify(val, '')}`).appendTo(item);
|
||
}
|
||
});
|
||
|
||
// 顯示加值(如果有)
|
||
if (value['加值'] && typeof value['加值'] === 'object') {
|
||
const bonusEntries = [];
|
||
Object.entries(value['加值']).forEach(([bonusKey, bonusValue]) => {
|
||
bonusEntries.push(`${bonusKey}+【${bonusValue}】`);
|
||
});
|
||
|
||
// 如果有其他內容,添加分隔線
|
||
const hasOtherContent = value['描述'] || Object.keys(value).some(k =>
|
||
k !== '$meta' && k !== 'name' && k !== '描述' && k !== '加值'
|
||
);
|
||
|
||
if (hasOtherContent) {
|
||
const separator = $('<div>').css({
|
||
'height': '1px',
|
||
'background': 'linear-gradient(90deg, transparent, rgba(148, 197, 255, 0.4), transparent)',
|
||
'margin': '8px 0',
|
||
'width': '100%'
|
||
});
|
||
item.append(separator);
|
||
}
|
||
|
||
// 加值顯示
|
||
const bonusDiv = $('<div>').addClass('skill-desc talent-bonus').css({
|
||
'background': 'rgba(94, 161, 255, 0.08)',
|
||
'border-left': '3px solid var(--survival-accent)',
|
||
'padding': '6px 10px',
|
||
'border-radius': '4px',
|
||
'margin': '4px 0'
|
||
}).text(`加值效果:${bonusEntries.join('、')}`);
|
||
|
||
item.append(bonusDiv);
|
||
}
|
||
} else if (typeof value === 'string') {
|
||
$('<div>').addClass('skill-desc').text(value).appendTo(item);
|
||
}
|
||
leftCol.append(item);
|
||
});
|
||
}
|
||
|
||
// ============== 專屬特色渲染(新增安全檢查) ==============
|
||
if (companionDetails && typeof companionDetails === 'object') {
|
||
// 安全地獲取專屬特色
|
||
const exclusiveTraits = companionDetails['專屬特色'];
|
||
|
||
if (exclusiveTraits && typeof exclusiveTraits === 'object' && Object.keys(exclusiveTraits).length > 0) {
|
||
leftCol.append($('<div>').addClass('section__title').css('margin-top', '20px').text('專屬特色'));
|
||
|
||
Object.entries(exclusiveTraits).forEach(([name, desc]) => {
|
||
if (name && desc) { // 確保名稱和描述都存在
|
||
const item = $('<div>').addClass('skill-item');
|
||
$('<div>').addClass('skill-name').text(name).appendTo(item);
|
||
|
||
// 處理描述可能是物件或字串
|
||
let description = '';
|
||
if (typeof desc === 'object' && desc['描述']) {
|
||
description = desc['描述'];
|
||
} else if (typeof desc === 'string') {
|
||
description = desc;
|
||
} else {
|
||
description = textify(desc, '');
|
||
}
|
||
|
||
$('<div>').addClass('skill-desc').text(description).appendTo(item);
|
||
leftCol.append(item);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// ============== 右側技能欄 ==============
|
||
const rightCol = $('<div>').addClass('skill-col');
|
||
$('<div>').addClass('section__title').text('技能').appendTo(rightCol);
|
||
|
||
const skillEntries = toEntries(skillsSource);
|
||
if (skillEntries.length === 0) {
|
||
rightCol.append($('<p>').addClass('empty-text').text('暫無'));
|
||
} else {
|
||
skillEntries.forEach(([name, value]) => {
|
||
if (value && typeof value === 'object') {
|
||
const level = Number(e(value['等級'] ?? 0));
|
||
const exp = Number(e(value['當前經驗'] ?? 0));
|
||
const required = Number(e(value['升級所需經驗'] ?? 1)) || 1;
|
||
|
||
const item = $('<div>').addClass('skill-item');
|
||
$('<div>').addClass('skill-name').text(`${name} Lv.${level}`).appendTo(item);
|
||
item.append(t('', exp, required));
|
||
rightCol.append(item);
|
||
} else if (typeof value === 'string' || typeof value === 'number') {
|
||
// 處理簡單類型的技能
|
||
const item = $('<div>').addClass('skill-item');
|
||
$('<div>').addClass('skill-name').text(name).appendTo(item);
|
||
$('<div>').addClass('skill-desc').text(textify(value, '')).appendTo(item);
|
||
rightCol.append(item);
|
||
}
|
||
});
|
||
}
|
||
|
||
wrapper.append(leftCol, rightCol);
|
||
target.append(wrapper);
|
||
}
|
||
|
||
function renderUnifiedStatus(target, statData) {
|
||
const wrapper = $('<div>').addClass('phys-mental-wrapper');
|
||
const leftCol = $('<div>').addClass('phys-col');
|
||
const rightCol = $('<div>').addClass('mental-col');
|
||
const phys = statData['生理'] ?? {};
|
||
const mental = statData['精神'] ?? {};
|
||
const state = statData['狀態'] ?? {};
|
||
|
||
$('<div>').addClass('section__title').text('負重及營養').appendTo(leftCol);
|
||
const currentWeight = Number(e(state['當前負重'] ?? 0)), maxWeight = Number(e(state['負重上限'] ?? 0));
|
||
leftCol.append(t(`負重: ${currentWeight.toFixed(1)}/${maxWeight.toFixed(1)} kg`, currentWeight, maxWeight, 'weight-progress'));
|
||
|
||
// ✅ 修改這裡:從 phys 讀取飽腹感
|
||
leftCol.append(t('飽腹感', Number(e(phys['飽腹感'] ?? 0)), 100, 'hunger-progress'));
|
||
|
||
leftCol.append(t('水分', Number(e(phys['水分'] ?? 0)), 100, 'water-progress'));
|
||
leftCol.append(t('蛋白質', Number(e(phys['蛋白質'] ?? 0)), s['蛋白質'], 'protein-progress'));
|
||
leftCol.append(t('維生素', Number(e(phys['維生素'] ?? 0)), 100, 'vitamin-progress'));
|
||
leftCol.append(t('碳水', Number(e(phys['碳水'] ?? 0)), s['碳水'], 'carbs-progress'));
|
||
leftCol.append(t('脂肪', Number(e(phys['脂肪'] ?? 0)), s['脂肪'], 'fat-progress'));
|
||
|
||
const immuneLoad = Number(e(phys['免疫負荷'] ?? 0)), immunePower = Number(e(state['免疫力'] ?? 0));
|
||
const immuneWrapper = $('<div>').addClass('progress-wrapper').append($('<div>').text(`免疫: ${immuneLoad} / ${immunePower}`));
|
||
const immuneBar = $('<div>').addClass('immune-bar');
|
||
$('<div>').addClass('immune-fill').css('width', `${Math.min(immuneLoad, 100)}%`).appendTo(immuneBar);
|
||
$('<div>').addClass('immune-marker').css('left', `${Math.min(immunePower, 100)}%`).appendTo(immuneBar);
|
||
leftCol.append(immuneWrapper.append(immuneBar));
|
||
|
||
const nitrogen = Number(e(phys['氮平衡'] ?? 0));
|
||
const nitrogenWrapper = $('<div>').addClass('progress-wrapper nitrogen-balance').append($('<div>').text(`氮平衡: ${nitrogen}`));
|
||
const gradient = $('<div>').addClass('nitrogen-gradient');
|
||
$('<div>').addClass('nitrogen-marker').css('left', `${((Math.max(-100, Math.min(100, nitrogen)) + 100) / 200) * 100}%`).appendTo(gradient);
|
||
leftCol.append(nitrogenWrapper.append(gradient));
|
||
|
||
$('<div>').addClass('section__title').text('精神及狀態').appendTo(rightCol);
|
||
rightCol.append(t('壓力', Number(e(mental['壓力'] ?? 0)), 100));
|
||
rightCol.append(t('性慾', Number(e(mental['性慾'] ?? 0)), 100));
|
||
rightCol.append(t('無聊', Number(e(mental['無聊'] ?? 0)), 100));
|
||
rightCol.append(t('孤獨', Number(e(mental['孤獨'] ?? 0)), 100));
|
||
rightCol.append(t('睡眠', Number(e(mental['睡眠'] ?? 0)), 100));
|
||
rightCol.append(t('表皮溼度', Number(e(state['表皮溼度'] ?? 0)), 100));
|
||
rightCol.append(t('疲勞', Number(e(state['疲勞'] ?? 0)), 100));
|
||
rightCol.append(t('清潔', Number(e(state['清潔'] ?? 0)), 100));
|
||
rightCol.append(t('感染', Number(e(state['感染'] ?? 0)), 100, 'danger-progress'));
|
||
rightCol.append(t('疼痛', Number(e(state['疼痛'] ?? 0)), 100, 'warning-progress'));
|
||
|
||
function renderKV(target, data, title) {
|
||
if (!data || Object.keys(data).length === 0) return;
|
||
if (title) $('<div>').addClass('section__title').text(title).appendTo(target);
|
||
const list = $('<ul>').addClass('section__list');
|
||
Object.entries(data).forEach(([key, value]) => {
|
||
$('<li>').addClass('item').append($('<span>').addClass('item__key').text(key), $('<span>').addClass('item__value').text(textify(value, '--'))).appendTo(list);
|
||
});
|
||
target.append(list);
|
||
}
|
||
renderKV(rightCol, {'受傷': textify(state['受傷'], '無'), '流血': textify(state['流血'], '無'), '疾病': textify(state['疾病'], '無')});
|
||
wrapper.append(leftCol, rightCol);
|
||
target.append(wrapper);
|
||
}
|
||
|
||
function renderTemperatures(container, data) { const environment = data['環境'] ?? {}, phys = data['生理'] ?? {};
|
||
const tempWrapper = $('<div>').addClass('temp-range-wrapper');
|
||
|
||
const coreTemp = Number(e(phys['體溫'] ?? 37)), minNormal = 36.5, maxNormal = 37.5;
|
||
const coreTempSubWrapper = $('<div>').css('margin-bottom', '8px').append($('<div>').text(`核心體溫: ${coreTemp.toFixed(1)}°C (正常: ${minNormal}-${maxNormal})`));
|
||
const coreTempBar = $('<div>').addClass('temp-range-bar');
|
||
const coreRangeMin = 35, coreRangeSpan = 7;
|
||
const coreCurrentPercent = Math.max(0, Math.min(100, ((coreTemp - coreRangeMin) / coreRangeSpan) * 100));
|
||
const coreNormalStart = ((minNormal - coreRangeMin) / coreRangeSpan) * 100;
|
||
const coreNormalWidth = ((maxNormal - minNormal) / coreRangeSpan) * 100;
|
||
coreTempBar.append(
|
||
$('<div>').addClass('temp-gradient'),
|
||
$('<div>').addClass('comfort-zone').css({ left: `${coreNormalStart}%`, width: `${coreNormalWidth}%` }),
|
||
$('<div>').addClass('current-temp').css('left', `${coreCurrentPercent}%`)
|
||
);
|
||
tempWrapper.append(coreTempSubWrapper.append(coreTempBar));
|
||
|
||
const feltTemp = Number(e(environment['體感溫度'] ?? 20)), minComfort = Number(e(data['狀態']?.['最低舒適溫度'] ?? 15)), maxComfort = Number(e(data['狀態']?.['最高舒適溫度'] ?? 25));
|
||
const feltTempSubWrapper = $('<div>').append($('<div>').text(`體感溫度: ${feltTemp}°C (舒適: ${minComfort}-${maxComfort})`));
|
||
const feltTempBar = $('<div>').addClass('temp-range-bar');
|
||
const feltRangeMin = 0, feltRangeSpan = 50;
|
||
const feltCurrentPercent = Math.max(0, Math.min(100, ((feltTemp - feltRangeMin) / feltRangeSpan) * 100));
|
||
const feltComfortStart = ((minComfort - feltRangeMin) / feltRangeSpan) * 100;
|
||
const feltComfortWidth = ((maxComfort - minComfort) / feltRangeSpan) * 100;
|
||
feltTempBar.append(
|
||
$('<div>').addClass('temp-gradient'),
|
||
$('<div>').addClass('comfort-zone').css({ left: `${feltComfortStart}%`, width: `${feltComfortWidth}%` }),
|
||
$('<div>').addClass('current-temp').css('left', `${feltCurrentPercent}%`)
|
||
);
|
||
tempWrapper.append(feltTempSubWrapper.append(feltTempBar));
|
||
container.append(tempWrapper);
|
||
}
|
||
|
||
function r(container, data) {
|
||
const environment = data['環境'] ?? {}, system = data['系統'] ?? {};
|
||
const phys = data['生理'] ?? {};
|
||
const mental = data['精神'] ?? {};
|
||
const state = data['狀態'] ?? {};
|
||
|
||
const top = $('<div>').addClass('top-info');
|
||
top.append($('<div>').addClass('time-display').text(textify(system['當前時間'], '--:--')));
|
||
|
||
const infoStack = $('<div>').addClass('info-stack');
|
||
infoStack.append(
|
||
$('<div>').addClass('info-row').append(
|
||
$('<div>').addClass('row-item').text(`Day ${e(system['經過天數'] ?? 0)}`),
|
||
$('<div>').addClass('row-item').text(textify(environment['季節'], '')),
|
||
$('<div>').addClass('row-item').text(textify(environment['月相'], ''))
|
||
),
|
||
$('<div>').addClass('info-row').append(
|
||
$('<div>').addClass('row-item').text(textify(environment['天氣'], '')),
|
||
$('<div>').addClass('row-item').text(`風速 ${textify(environment['風速'], '--')}`),
|
||
$('<div>').addClass('row-item').text(`舒適度 ${textify(environment['舒適度'], '--')}`)
|
||
),
|
||
$('<div>').addClass('info-row').append(
|
||
$('<div>').addClass('row-item').text(`腳本狀態:${textify(system['腳本修正狀態'], '🔄 未執行')}`)
|
||
)
|
||
);
|
||
|
||
top.append(infoStack, $('<div>').addClass('location-display').text(`位置:${textify(environment['當前位置'], '未知')}`));
|
||
container.append(top);
|
||
renderTemperatures(container, data);
|
||
|
||
const hpCurrent = Number(e(phys['當前生命值'] ?? 0)), hpMax = Number(e(phys['生命值上限'] ?? 0));
|
||
const mpCurrent = Number(e(mental['當前精神值'] ?? 0)), mpMax = Number(e(mental['精神值上限'] ?? 0));
|
||
const coreWrapper = $('<div>').addClass('core-stats-wrapper');
|
||
coreWrapper.append($('<div>').addClass('core-stat-item').append(t('生命值', hpCurrent, hpMax, 'hp-progress')));
|
||
coreWrapper.append($('<div>').addClass('core-stat-item').append(t('精神值', mpCurrent, mpMax, 'mp-progress')));
|
||
container.append(coreWrapper);
|
||
}
|
||
|
||
function renderKeyValueTree(target, data) {
|
||
const entries = toEntries(data);
|
||
if (!entries || entries.length === 0) return;
|
||
const list = $('<ul>').addClass('section__list');
|
||
entries.forEach(([key, value]) => {
|
||
const li = $('<li>');
|
||
if (value && typeof value === 'object') {
|
||
$('<div>').addClass('item').append($('<span>').addClass('item__key').text(String(key))).appendTo(li);
|
||
renderKeyValueTree(li, value);
|
||
} else {
|
||
$('<div>').addClass('item').append($('<span>').addClass('item__key').text(String(key)), $('<span>').addClass('item__value').text(textify(value, ''))).appendTo(li);
|
||
}
|
||
list.append(li);
|
||
});
|
||
target.append(list);
|
||
}
|
||
|
||
function renderTaskSystem(container, data) {
|
||
const tasksData = data['任務'] ?? {};
|
||
const { $section, $body } = d('任務', true);
|
||
|
||
const getStatusClass = (status) => ({'進行中':'status-ongoing', '待結算':'status-pending', '已完成':'status-completed', '已失敗':'status-failed', '已放棄':'status-abandoned'}[status] || 'status-ongoing');
|
||
const getStatusText = (status) => ({'進行中':'進行中', '待結算':'待結算', '已完成':'已完成', '已失敗':'已失敗', '已放棄':'已放棄'}[status] || '進行中');
|
||
|
||
const renderRewards = (rewardsData) => {
|
||
if (!rewardsData) return '';
|
||
if (typeof rewardsData === 'string') {
|
||
return '<span class="points">' + rewardsData + '</span>';
|
||
}
|
||
if (typeof rewardsData === 'object') {
|
||
const parts = [];
|
||
Object.entries(rewardsData).forEach(([key, value]) => {
|
||
if (key === '劇情演出') {
|
||
parts.push('<span class="points">特殊劇情:' + value + '</span>');
|
||
} else if (key === '特殊物品') {
|
||
parts.push('<span class="points">特殊物品:' + value.replace('獲得製作「', '獲得製作「').replace('」的配方', '」的配方') + '</span>');
|
||
} else if (typeof value === 'object') {
|
||
const subParts = Object.entries(value).map(function(sub) {
|
||
return sub[0] + ' +' + sub[1] + ' exp';
|
||
});
|
||
parts.push('<span class="points">' + key + ':' + subParts.join(', ') + '</span>');
|
||
} else {
|
||
parts.push('<span class="points">' + key + ' +' + value + '</span>');
|
||
}
|
||
});
|
||
return parts.join(', ');
|
||
}
|
||
return '';
|
||
};
|
||
|
||
const createTaskDetail = (task, type, data) => {
|
||
const detail = $('<div>').addClass('task-detail').hide();
|
||
const colorMap = {'主線任務':'#3498db', '支線任務':'#f39c12', '日常任務':'#2ecc71', '緊急任務':'#e74c3c', '羈絆任務':'#8E44AD'};
|
||
if (task['標題']) detail.append($('<div>').css({'font-size':'18px', 'font-weight':'bold', 'text-align':'center', 'margin-bottom':'8px', 'padding':'4px 0', 'color':colorMap[type] || '#fff'}).text(task['標題']));
|
||
detail.append($('<div>').addClass('task-divider'));
|
||
|
||
// 任務描述
|
||
if (task['描述']) detail.append($('<div>').addClass('task-desc').text(task['描述'] || ''));
|
||
if (task['區域限制']) detail.append($('<div>').addClass('target-desc').html(`區域: <span style="color:var(--survival-warning); font-weight:bold;">${task['區域限制']}</span>`));
|
||
|
||
// 任務時間顯示
|
||
if (task['結束時間']) {
|
||
const currentDay = Number(e(data?.['系統']?.['經過天數'] ?? 0));
|
||
const currentTime = textify(data?.['系統']?.['當前時間'] ?? '00:00');
|
||
const currentTimeString = `第${currentDay}天 ${currentTime}`;
|
||
const totalCurrentMinutes = parseTimeToMinutes(currentTimeString);
|
||
const totalEndMinutes = parseTimeToMinutes(task['結束時間']);
|
||
if (totalCurrentMinutes !== null && totalEndMinutes !== null) {
|
||
const remainingMinutes = totalEndMinutes - totalCurrentMinutes;
|
||
const remainingTimeText = formatRemainingTime(remainingMinutes);
|
||
let timeColor = 'var(--survival-text)';
|
||
if (remainingMinutes <= 0) timeColor = 'var(--survival-danger)';
|
||
else if (remainingMinutes < 6 * 60) timeColor = 'var(--survival-warning)';
|
||
const timeContainer = $('<div>').addClass('target-desc').css('display', 'flex', 'align-items', 'center', 'gap', '8px');
|
||
timeContainer.append($('<span>').css('color', timeColor).text('剩餘時間:'));
|
||
timeContainer.append($('<span>').css('color', 'var(--survival-warning)').css('font-weight', 'bold').text(remainingTimeText));
|
||
detail.append(timeContainer);
|
||
} else {
|
||
detail.append($('<div>').addClass('target-desc').html(`結束時間: <span style="color:var(--survival-warning); font-weight:bold;">${task['結束時間']}</span>`));
|
||
}
|
||
}
|
||
|
||
// 緊急任務特殊處理
|
||
if (type === '緊急任務') {
|
||
const emergencyInfo = $('<div>').addClass('time-info').css('text-align', 'left');
|
||
if (task['結束時間']) emergencyInfo.append($('<div>').html(`截止時間: <span style="color:var(--survival-warning); font-weight:bold;">${task['結束時間']}</span>`));
|
||
if (task['失敗懲罰']) emergencyInfo.append($('<div>').addClass('penalty-info').html(`失敗懲罰: ${task['失敗懲罰']}`));
|
||
detail.append(emergencyInfo);
|
||
}
|
||
|
||
detail.append($('<div>').addClass('task-divider'));
|
||
|
||
// ========== 主要要求(基礎要求) ==========
|
||
if (task['任務要求'] && typeof task['任務要求'] === 'object') {
|
||
const requirementSection = $('<div>').addClass('target-section').append($('<div>').addClass('section-title').text('任務要求'));
|
||
|
||
// 處理基礎要求(可能是陣列或物件)
|
||
const requirements = task['任務要求'];
|
||
Object.entries(requirements).forEach(([reqKey, reqData]) => {
|
||
// 跳過進階目標(因為進階目標會另外處理)
|
||
if (reqKey === '進階目標') return;
|
||
|
||
if (reqData && typeof reqData === 'object') {
|
||
const desc = $('<div>').addClass('target-desc');
|
||
const current = Number(e(reqData['進度'])) || 0;
|
||
const target = Number(e(reqData['目標'])) || 1;
|
||
const reqType = reqData['類型'] || '數值';
|
||
|
||
// 獲取描述文字
|
||
let descriptionText = reqData['描述'] || '';
|
||
if (!descriptionText && reqKey) {
|
||
descriptionText = reqKey.replace(/^基礎要求\d*/, '目標');
|
||
}
|
||
|
||
desc.append($('<span>').text(descriptionText));
|
||
if (reqType === '數值') {
|
||
desc.append($('<span>').addClass('progress-number').text(` (${current}/${target})`));
|
||
}
|
||
if (current >= target) desc.addClass('completed');
|
||
requirementSection.append(desc);
|
||
}
|
||
});
|
||
|
||
if (task['基礎獎勵']) {
|
||
requirementSection.append($('<div>').addClass('reward-info').html(`基礎獎勵: ${renderRewards(task['基礎獎勵'])}`));
|
||
}
|
||
detail.append(requirementSection);
|
||
}
|
||
|
||
// ========== 進階目標(修正版:正確顯示描述) ==========
|
||
if (type !== '日常任務' && task['進階目標'] && Object.keys(task['進階目標']).length > 0) {
|
||
detail.append($('<div>').addClass('task-divider'));
|
||
const bonusSection = $('<div>').addClass('target-section').append($('<div>').addClass('section-title').text('進階要求'));
|
||
|
||
// 正確遍歷進階目標
|
||
Object.entries(task['進階目標']).forEach(([goalKey, goalData]) => {
|
||
if (goalData && typeof goalData === 'object') {
|
||
const desc = $('<div>').addClass('target-desc');
|
||
const current = Number(e(goalData['進度'])) || 0;
|
||
const target = Number(e(goalData['目標'])) || 1;
|
||
const goalType = goalData['類型'] || '數值';
|
||
|
||
// 獲取描述文字(優先使用 goalData['描述'])
|
||
let descriptionText = '';
|
||
if (goalData['描述']) {
|
||
descriptionText = goalData['描述'];
|
||
} else if (goalKey) {
|
||
// 如果沒有描述,使用 key 名稱
|
||
descriptionText = goalKey.replace(/^進階要求\d*/, '目標');
|
||
}
|
||
|
||
desc.append($('<span>').text(descriptionText));
|
||
if (goalType === '數值') {
|
||
desc.append($('<span>').addClass('progress-number').text(` (${current}/${target})`));
|
||
}
|
||
if (current >= target) desc.addClass('completed');
|
||
bonusSection.append(desc);
|
||
}
|
||
});
|
||
|
||
if (task['進階獎勵']) {
|
||
bonusSection.append($('<div>').addClass('reward-info').html(`進階獎勵: ${renderRewards(task['進階獎勵'])}`));
|
||
}
|
||
detail.append(bonusSection);
|
||
}
|
||
|
||
return detail;
|
||
};
|
||
|
||
let hasTasks = false;
|
||
|
||
// ========== 世界任務(終極任務,特殊顯示) ==========
|
||
const worldTask = tasksData['世界'];
|
||
if (worldTask && typeof worldTask === 'object' && worldTask['標題']) {
|
||
hasTasks = true;
|
||
|
||
// 終極任務專屬樣式 - 金色發光邊框 + 特殊動畫
|
||
const strip = $('<div>').addClass('task-strip world-task');
|
||
strip.append($('<div>').addClass('task-content').html('🌍 世界任務 🌍'));
|
||
|
||
// 狀態指示器
|
||
const status = worldTask['狀態'] || '已揭示';
|
||
const indicator = $('<div>').addClass('task-status-indicator');
|
||
indicator.append($('<div>').addClass('status-text').addClass(getStatusClass(status)).text(getStatusText(status)));
|
||
strip.append(indicator);
|
||
|
||
// 任務詳情
|
||
const detail = createTaskDetail(worldTask, '世界任務', data);
|
||
|
||
// 點擊展開/收合
|
||
strip.on('click', () => {
|
||
$('.task-detail').not(detail).slideUp();
|
||
detail.slideToggle();
|
||
});
|
||
|
||
$body.append(strip, detail);
|
||
}
|
||
|
||
['主線', '支線', '日常', '緊急', '背德'].forEach(key => {
|
||
const task = tasksData[key];
|
||
if (task && typeof task === 'object' && task['標題']) {
|
||
hasTasks = true;
|
||
const keyNameMapping = {'主線': '主線任務', '支線': '支線任務', '日常': '日常任務', '緊急': '緊急任務', '背德': '背德任務'};
|
||
const classNameMapping = {'主線': 'main-task', '支線': 'side-task', '日常': 'daily-task', '緊急': 'emergency-task', '背德': 'bond-task'};
|
||
const status = task['狀態'] || '進行中';
|
||
const strip = $('<div>').addClass('task-strip').addClass(classNameMapping[key]);
|
||
strip.append($('<div>').addClass('task-content').text(keyNameMapping[key]));
|
||
const indicator = $('<div>').addClass('task-status-indicator');
|
||
indicator.append($('<div>').addClass('status-text').addClass(getStatusClass(status)).text(getStatusText(status)));
|
||
strip.append(indicator);
|
||
const detail = createTaskDetail(task, keyNameMapping[key], data);
|
||
strip.on('click', () => { $('.task-detail').not(detail).slideUp(); detail.slideToggle(); });
|
||
$body.append(strip, detail);
|
||
}
|
||
});
|
||
|
||
const bondTasks = tasksData['羈絆'];
|
||
if (bondTasks && typeof bondTasks === 'object' && Object.keys(bondTasks).length > 0) {
|
||
hasTasks = true;
|
||
Object.entries(bondTasks).forEach(([companionName, task]) => {
|
||
if (task && typeof task === 'object' && task['標題']) {
|
||
const status = task['狀態'] || '進行中';
|
||
const strip = $('<div>').addClass('task-strip bond-task');
|
||
strip.append($('<div>').addClass('task-content').text(`羈絆 - ${companionName}`));
|
||
const indicator = $('<div>').addClass('task-status-indicator');
|
||
indicator.append($('<div>').addClass('status-text').addClass(getStatusClass(status)).text(getStatusText(status)));
|
||
strip.append(indicator);
|
||
const detail = createTaskDetail(task, '羈絆任務', data);
|
||
strip.on('click', () => { $('.task-detail').not(detail).slideUp(); detail.slideToggle(); });
|
||
$body.append(strip, detail);
|
||
}
|
||
});
|
||
}
|
||
|
||
if (!hasTasks) {
|
||
$body.append($('<div>').addClass('empty-text').text('暫無任務'));
|
||
}
|
||
container.append($section);
|
||
}
|
||
|
||
function renderCompanions(container, companionsSource) {
|
||
if (companionsSource == null || Object.keys(companionsSource).length === 0) return;
|
||
const { $section, $body } = d('同伴', false);
|
||
|
||
const companionEntries = toEntries(companionsSource);
|
||
|
||
if (companionEntries.length === 0) {
|
||
$body.append($('<p>').addClass('empty-text').text('暫無同伴'));
|
||
} else {
|
||
companionEntries.forEach(([name, details]) => {
|
||
if (details && typeof details === 'object') {
|
||
const card = $('<div>').addClass('collapsible').css('margin-bottom', '10px');
|
||
const cardHeader = $('<div>').addClass('collapsible__header').text(name);
|
||
const cardBody = $('<div>').addClass('collapsible__body');
|
||
|
||
// 🔧 修改的地方:安全获取当前状态(支持新的数据结构)
|
||
const currentState = details?.['狀態']?.['當前狀態'] ?? details?.['狀態'];
|
||
const healthStatus = textify(currentState, '狀態未知');
|
||
|
||
const healthValue = Number(e(details['健康度'] ?? 100));
|
||
let statusColor = '#2ecc71';
|
||
if (healthValue <= 0) statusColor = '#7f8c8d';
|
||
else if (healthValue < 10) statusColor = '#8e44ad';
|
||
else if (healthValue < 30) statusColor = '#e74c3c';
|
||
else if (healthValue < 50) statusColor = '#e67e22';
|
||
else if (healthValue < 70) statusColor = '#f39c12';
|
||
|
||
const statusRow = $('<div>').css({'font-size': '16px', 'font-weight': 'bold', 'color': 'var(--survival-warning)', 'margin-top': '14px', 'margin-bottom': '10px', 'text-align': 'left'});
|
||
$('<span>').text('狀態: ').appendTo(statusRow);
|
||
$('<span>').css('color', statusColor).text(healthStatus).appendTo(statusRow);
|
||
cardBody.append(statusRow);
|
||
|
||
const statusWrapper = $('<div>').addClass('phys-mental-wrapper').css('margin-bottom', '20px');
|
||
const physCol = $('<div>').addClass('phys-col');
|
||
const mentalCol = $('<div>').addClass('mental-col');
|
||
|
||
$('<div>').addClass('section__title').text('生理').appendTo(physCol);
|
||
const health = Math.min(Number(e(details['健康度'] ?? 0)), 100);
|
||
const water = Math.min(Number(e(details['水分值'] ?? 0)), 100);
|
||
const satiety = Math.min(Number(e(details['飽腹感'] ?? 0)), 100);
|
||
const fatigue = Math.min(Number(e(details['疲勞度'] ?? 0)), 100);
|
||
physCol.append(t(`健康度: ${health}/100 (${getHealthStatus(health)})`, health, 100, 'health-progress'));
|
||
physCol.append(t(`水分值: ${water}/100`, water, 100, 'water-progress'));
|
||
physCol.append(t(`飽腹感: ${satiety}/100`, satiety, 100, 'satiety-progress'));
|
||
physCol.append(t(`疲勞度: ${fatigue}/100`, fatigue, 100, 'fatigue-progress'));
|
||
|
||
/*$('<div>').addClass('section__title').text('心理').appendTo(mentalCol);
|
||
const love = Math.min(Number(e(details['好感度'] ?? 0)), 120);
|
||
const desire = Math.min(Number(e(details['性慾值'] ?? 0)), 100);
|
||
const dev = Math.min(Number(e(details['開發度'] ?? 0)), 100);
|
||
const crevice = Math.min(Number(e(details['墮落值'] ?? 0)), 100);
|
||
mentalCol.append(t(`好感度: ${love.toFixed(1)}/100 (${getFavorabilityStatus(love)})`, love, 100, 'love-progress'));
|
||
mentalCol.append(t(`性慾值: ${desire.toFixed(1)}/100 (${getDesireStatus(desire)})`, desire, 100, 'desire-progress'));
|
||
mentalCol.append(t(`開發度: ${dev.toFixed(1)}/100`, dev, 100, 'dev-progress'));
|
||
mentalCol.append(t(`墮落值: ${crevice.toFixed(1)}/100 (${getCreviceStatus(crevice)})`, crevice, 100, 'crevice-progress'));
|
||
|
||
statusWrapper.append(physCol, mentalCol);
|
||
cardBody.append(statusWrapper);*/
|
||
|
||
// ================ 移動:先渲染屬性與技能 ================
|
||
renderAttributesAndSkills(cardBody, details['屬性'], details['技能'], details['天賦特性'], details);
|
||
|
||
// ================ 移動:裝備顯示在攜帶物品上面 ================
|
||
const equipment = details['裝備'] ?? {};
|
||
|
||
// 檢查是否有任何裝備
|
||
let hasAnyEquipment = false;
|
||
const equipmentSlots = [
|
||
'頭部', '頸部', '胸部', '背部', '腰部',
|
||
'右手', '左手', '腿部', '腳部', '其他'
|
||
];
|
||
|
||
// 先檢查哪些部位有裝備
|
||
const occupiedSlots = equipmentSlots.filter(slot => {
|
||
const equipmentItem = equipment[slot];
|
||
return equipmentItem && typeof equipmentItem === 'object' && Object.keys(equipmentItem).length > 0;
|
||
});
|
||
|
||
// 如果有任何裝備才顯示裝備欄位
|
||
if (occupiedSlots.length > 0) {
|
||
// 注意:這裡沒有 margin-top,因為它緊接在屬性技能後面
|
||
cardBody.append($('<div>').addClass('section__title').text('裝備'));
|
||
|
||
// 使用跟玩家完全一樣的裝備網格
|
||
const equipmentGrid = $('<div>').addClass('inventory-grid equipment-grid').css({
|
||
'grid-template-columns': 'repeat(5, 1fr)', // 一行5個
|
||
'gap': '15px'
|
||
});
|
||
|
||
// 只顯示有裝備的部位
|
||
occupiedSlots.forEach((slot, index) => {
|
||
const slotContainer = $('<div>').addClass('equipment-slot')
|
||
.attr('data-slot', slot)
|
||
.css({
|
||
'background': 'var(--survival-surface-strong)',
|
||
'border': '1px solid var(--survival-glass-border)',
|
||
'border-radius': '12px',
|
||
'padding': '16px',
|
||
'text-align': 'center',
|
||
'transition': 'all 0.25s ease-out',
|
||
'min-height': '140px',
|
||
'display': 'flex',
|
||
'flex-direction': 'column',
|
||
'justify-content': 'flex-start'
|
||
});
|
||
|
||
// 裝備部位標題(跟玩家一樣)
|
||
const slotTitle = $('<div>').addClass('slot-title').css({
|
||
'font-size': '20px',
|
||
'font-weight': '700',
|
||
'color': '#d17d2e',
|
||
'letter-spacing': '0.05em',
|
||
'text-transform': 'uppercase',
|
||
'margin-bottom': '12px',
|
||
'padding-bottom': '8px',
|
||
'border-bottom': '1px solid rgba(148, 197, 255, 0.2)',
|
||
'line-height': '1.2'
|
||
}).text(slot);
|
||
|
||
slotContainer.append(slotTitle);
|
||
|
||
// 獲取該部位的裝備
|
||
const equipmentItem = equipment[slot];
|
||
|
||
// 獲取裝備名稱(第一個鍵名)
|
||
const itemKey = Object.keys(equipmentItem)[0];
|
||
const itemData = equipmentItem[itemKey];
|
||
|
||
if (itemKey && itemData) {
|
||
const itemNameDiv = $('<div>').addClass('equipment-item-name').css({
|
||
'font-size': '18px',
|
||
'font-weight': '600',
|
||
'color': 'var(--survival-accent-strong)',
|
||
'margin-bottom': '8px',
|
||
'word-wrap': 'break-word',
|
||
'line-height': '1.3',
|
||
'padding': '8px 10px',
|
||
'background': 'rgba(94, 161, 255, 0.1)',
|
||
'border': '1px solid rgba(94, 161, 255, 0.2)',
|
||
'border-radius': '6px',
|
||
'width': '100%',
|
||
'text-align': 'center'
|
||
}).text(itemKey);
|
||
|
||
slotContainer.append(itemNameDiv);
|
||
|
||
// 顯示裝備屬性(跟玩家完全一樣的邏輯)
|
||
const itemProps = $('<div>').addClass('equipment-item-props').css({
|
||
'font-size': '12px',
|
||
'color': 'var(--survival-text)',
|
||
'opacity': '0.8',
|
||
'text-align': 'left',
|
||
'line-height': '1.4',
|
||
'margin-top': '10px',
|
||
'padding': '8px',
|
||
'background': 'rgba(20, 30, 50, 0.5)',
|
||
'border-radius': '6px',
|
||
'border': '1px solid rgba(94, 161, 255, 0.1)',
|
||
'width': '100%'
|
||
});
|
||
|
||
// 完整的屬性顯示邏輯(跟玩家一樣)
|
||
const orderedProps = ['數量', '位置', '重量', '耐久度'];
|
||
const attributes = new Map();
|
||
|
||
// 先按照順序處理
|
||
orderedProps.forEach(k => {
|
||
if (itemData.hasOwnProperty(k)) {
|
||
attributes.set(k, itemData[k]);
|
||
}
|
||
});
|
||
|
||
// 處理狀態類型和狀態
|
||
const stateType = textify(itemData['狀態類型'] ?? '', '');
|
||
if (stateType && itemData.hasOwnProperty('狀態')) {
|
||
// 如果狀態類型不是'耐久度',或者還沒有設置耐久度,則添加
|
||
if (stateType.toLowerCase() !== '耐久度' || !attributes.has('耐久度')) {
|
||
attributes.set(stateType, itemData['狀態']);
|
||
}
|
||
}
|
||
|
||
// 處理其他屬性(跟玩家一樣顯示所有屬性)
|
||
// 處理其他屬性(跟玩家一樣顯示所有屬性)
|
||
Object.entries(itemData).forEach(([k, v]) => {
|
||
if (!attributes.has(k) && k !== '$meta' && k !== '狀態類型' && k !== '狀態') {
|
||
// 特別處理加值欄位
|
||
if (k === '加值' && v && typeof v === 'object') {
|
||
const bonusEntries = [];
|
||
Object.entries(v).forEach(([bonusKey, bonusValue]) => {
|
||
bonusEntries.push(`${bonusKey}+【${bonusValue}】`);
|
||
});
|
||
attributes.set(k, bonusEntries.join('、'));
|
||
} else {
|
||
attributes.set(k, v);
|
||
}
|
||
}
|
||
});
|
||
|
||
// 顯示所有屬性(使用跟玩家一樣的樣式)
|
||
attributes.forEach((v, k) => {
|
||
if (v !== null && v !== '' && v !== undefined) {
|
||
const line = $('<div>').addClass('equipment-prop');
|
||
line.attr('data-prop', k);
|
||
|
||
// 如果是加值欄位,添加特殊類別
|
||
if (k === '加值') {
|
||
line.addClass('bonus-prop');
|
||
// 在加值前面添加分隔線
|
||
const separator = $('<div>').addClass('bonus-separator').css({
|
||
'height': '1px',
|
||
'background': 'linear-gradient(90deg, transparent, rgba(148, 197, 255, 0.4), transparent)',
|
||
'margin': '10px 0 8px 0',
|
||
'width': '100%'
|
||
});
|
||
itemProps.append(separator);
|
||
}
|
||
|
||
let displayValue = textify(v, '');
|
||
if (k === '重量' && typeof v === 'number') {
|
||
displayValue += 'kg';
|
||
}
|
||
|
||
// 如果是數量,確保顯示
|
||
if (k === '數量') {
|
||
displayValue = String(displayValue);
|
||
}
|
||
|
||
// 限制描述長度
|
||
if (k === '描述' && displayValue.length > 80) {
|
||
displayValue = displayValue.substring(0, 77) + '...';
|
||
}
|
||
|
||
$('<span>').addClass('prop-key').text(String(k)).appendTo(line);
|
||
$('<span>').addClass('prop-sep').text(': ').appendTo(line);
|
||
$('<span>').addClass('prop-val').text(displayValue).appendTo(line);
|
||
|
||
itemProps.append(line);
|
||
|
||
// 如果是加值欄位,在後面也添加一個小分隔線
|
||
if (k === '加值') {
|
||
const bottomSeparator = $('<div>').css({
|
||
'height': '1px',
|
||
'background': 'rgba(148, 197, 255, 0.1)',
|
||
'margin': '8px 0 4px 0',
|
||
'width': '100%'
|
||
});
|
||
itemProps.append(bottomSeparator);
|
||
}
|
||
}
|
||
});
|
||
|
||
if (attributes.size > 0) {
|
||
slotContainer.append(itemProps);
|
||
}
|
||
|
||
// 懸停效果(跟玩家一樣)
|
||
slotContainer.hover(
|
||
function() {
|
||
$(this).css({
|
||
'transform': 'translateY(-3px)',
|
||
'box-shadow': '0 8px 25px rgba(0,0,0,0.3), 0 0 20px rgba(94, 161, 255, 0.15)',
|
||
'background': 'rgba(40, 55, 85, 0.9)',
|
||
'border-color': 'var(--survival-accent)'
|
||
});
|
||
},
|
||
function() {
|
||
$(this).css({
|
||
'transform': 'translateY(0)',
|
||
'box-shadow': 'none',
|
||
'background': 'var(--survival-surface-strong)',
|
||
'border-color': 'var(--survival-glass-border)'
|
||
});
|
||
}
|
||
);
|
||
}
|
||
|
||
equipmentGrid.append(slotContainer);
|
||
});
|
||
|
||
// 根據有裝備的部位數量調整網格布局
|
||
const slotCount = occupiedSlots.length;
|
||
if (slotCount <= 5) {
|
||
equipmentGrid.css('grid-template-columns', `repeat(${slotCount}, 1fr)`);
|
||
} else if (slotCount <= 10) {
|
||
equipmentGrid.css('grid-template-columns', 'repeat(5, 1fr)');
|
||
}
|
||
|
||
cardBody.append(equipmentGrid);
|
||
}
|
||
// ================ 完整的同伴裝備欄位結束 ================
|
||
|
||
// ================ 移動:攜帶物品顯示在最下面 ================
|
||
cardBody.append($('<div>').addClass('section__title').css('margin-top', '20px').text('攜帶物品'));
|
||
const inventoryContainer = $('<div>').addClass('companion-inventory-grid');
|
||
|
||
const items = details['攜帶物品'] ? firstMeaningfulObject(details['攜帶物品']) : {};
|
||
const itemEntries = Object.entries(items);
|
||
|
||
if (itemEntries.length === 0) {
|
||
inventoryContainer.append($('<p>').addClass('empty-text').css('grid-column', '1 / -1').text('無'));
|
||
} else {
|
||
itemEntries.forEach(([itemName, itemDetails]) => {
|
||
const itemCard = $('<div>').addClass('inventory-item');
|
||
$('<div>').addClass('item-name').text(itemName).appendTo(itemCard);
|
||
if (itemDetails && typeof itemDetails === 'object') {
|
||
const props = $('<div>').addClass('item-props');
|
||
const attributes = new Map();
|
||
|
||
const definedOrder = ['數量', '位置', '重量', '耐久度'];
|
||
definedOrder.forEach(k => {
|
||
if (itemDetails.hasOwnProperty(k)) {
|
||
attributes.set(k, itemDetails[k]);
|
||
}
|
||
});
|
||
|
||
const stateType = textify(itemDetails['狀態類型'] ?? '');
|
||
if (stateType && itemDetails.hasOwnProperty('狀態')) {
|
||
if (stateType.toLowerCase() !== '耐久度' || !attributes.has('耐久度')) {
|
||
attributes.set(stateType, itemDetails['狀態']);
|
||
}
|
||
}
|
||
|
||
// 處理其他屬性,包括加值
|
||
Object.entries(itemDetails).forEach(([k, v]) => {
|
||
if (!attributes.has(k) && k !== '$meta' && k !== '狀態類型' && k !== '狀態') {
|
||
// 特別處理加值欄位
|
||
if (k === '加值' && v && typeof v === 'object') {
|
||
const bonusEntries = [];
|
||
Object.entries(v).forEach(([bonusKey, bonusValue]) => {
|
||
bonusEntries.push(`${bonusKey}+【${bonusValue}】`);
|
||
});
|
||
attributes.set(k, bonusEntries.join('、'));
|
||
} else {
|
||
attributes.set(k, v);
|
||
}
|
||
}
|
||
});
|
||
|
||
const postStateOrder = ['描述', '功能', '功能1', '功能2', '功能3', '負重加成'];
|
||
postStateOrder.forEach(k => {
|
||
if (itemDetails.hasOwnProperty(k)) {
|
||
attributes.set(k, itemDetails[k]);
|
||
}
|
||
});
|
||
|
||
attributes.forEach((v, k) => {
|
||
if (v !== null && v !== '' && v !== undefined) {
|
||
const line = $('<div>').addClass('item-prop');
|
||
|
||
// 如果是加值欄位,添加特殊類別
|
||
if (k === '加值') {
|
||
line.addClass('bonus-prop');
|
||
// 在加值前面添加分隔線
|
||
const separator = $('<div>').addClass('bonus-separator').css({
|
||
'height': '1px',
|
||
'background': 'linear-gradient(90deg, transparent, rgba(148, 197, 255, 0.4), transparent)',
|
||
'margin': '10px 0 8px 0',
|
||
'width': '100%'
|
||
});
|
||
props.append(separator);
|
||
}
|
||
|
||
$('<span>').addClass('prop-key').text(String(k)).appendTo(line);
|
||
$('<span>').addClass('prop-sep').text(': ').appendTo(line);
|
||
let displayValue = textify(v, '');
|
||
if (k === '重量' && typeof v === 'number') {
|
||
displayValue += 'kg';
|
||
}
|
||
$('<span>').addClass('prop-val').text(displayValue).appendTo(line);
|
||
props.append(line);
|
||
|
||
// 如果是加值欄位,在後面也添加一個小分隔線
|
||
if (k === '加值') {
|
||
const bottomSeparator = $('<div>').css({
|
||
'height': '1px',
|
||
'background': 'rgba(148, 197, 255, 0.1)',
|
||
'margin': '8px 0 4px 0',
|
||
'width': '100%'
|
||
});
|
||
props.append(bottomSeparator);
|
||
}
|
||
}
|
||
});
|
||
|
||
itemCard.append(props);
|
||
}
|
||
inventoryContainer.append(itemCard);
|
||
});
|
||
}
|
||
cardBody.append(inventoryContainer);
|
||
|
||
card.append(cardHeader, cardBody);
|
||
cardHeader.on('click', () => {
|
||
card.toggleClass('active');
|
||
cardBody.slideToggle(200);
|
||
});
|
||
$body.append(card);
|
||
}
|
||
});
|
||
}
|
||
container.append($section);
|
||
}
|
||
|
||
|
||
// ============== 新增:寵物渲染函數 ==============
|
||
function renderPets(container, petsSource) {
|
||
if (petsSource == null || Object.keys(petsSource).length === 0) return;
|
||
const { $section, $body } = d('寵物', false);
|
||
|
||
const petEntries = toEntries(petsSource);
|
||
|
||
if (petEntries.length === 0) {
|
||
$body.append($('<p>').addClass('empty-text').text('暫無寵物'));
|
||
} else {
|
||
petEntries.forEach(([name, details]) => {
|
||
if (details && typeof details === 'object') {
|
||
const card = $('<div>').addClass('collapsible').css('margin-bottom', '10px');
|
||
const cardHeader = $('<div>').addClass('collapsible__header').text(name);
|
||
const cardBody = $('<div>').addClass('collapsible__body');
|
||
|
||
// 左側:狀態、關係、屬性
|
||
const leftCol = $('<div>').addClass('attr-col');
|
||
|
||
// 狀態(單獨一行)
|
||
const status = textify(details['狀態'], '狀態未知');
|
||
const statusRow = $('<div>').css({
|
||
'font-size': '16px',
|
||
'font-weight': 'bold',
|
||
'color': '#3498db',
|
||
'margin-bottom': '8px',
|
||
'text-align': 'left'
|
||
}).text(`狀態: ${status}`);
|
||
leftCol.append(statusRow);
|
||
|
||
// 關係(單獨一行)
|
||
const relationship = textify(details['關系'] || details['關係'], '未知');
|
||
const relationshipRow = $('<div>').css({
|
||
'font-size': '16px',
|
||
'font-weight': 'bold',
|
||
'color': '#2ecc71',
|
||
'margin-bottom': '15px',
|
||
'text-align': 'left'
|
||
}).text(`關係: ${relationship}`);
|
||
leftCol.append(relationshipRow);
|
||
|
||
// 忠誠度(不顯示標題)
|
||
/* const loyalty = Math.min(Number(e(details['忠誠度'] ?? 0)), 100);
|
||
leftCol.append(t(`忠誠度: ${loyalty}/100 (${getLoyaltyStatus(loyalty)})`, loyalty, 100, 'loyalty-progress'));
|
||
|
||
// 性慾值(不顯示標題)
|
||
const desire = Math.min(Number(e(details['性慾值'] ?? 0)), 100);
|
||
leftCol.append(t(`性慾值: ${desire}/100`, desire, 100, 'pet-desire-progress')); */
|
||
|
||
// 屬性
|
||
const attributes = details['屬性'] ?? {};
|
||
leftCol.append($('<div>').addClass('section__title').css('margin-top', '20px').text('屬性'));
|
||
Object.entries(firstMeaningfulObject(attributes)).forEach(([attrName, attrDetails]) => {
|
||
if (attrDetails && typeof attrDetails === 'object') {
|
||
const level = Number(e(attrDetails['等級'] ?? 0));
|
||
const exp = Number(e(attrDetails['當前經驗'] ?? 0));
|
||
const required = Number(e(attrDetails['升級所需經驗'] ?? 1)) || 1;
|
||
leftCol.append($('<div>').text(`${attrName} Lv.${level}`), t('', exp, required));
|
||
}
|
||
});
|
||
|
||
// 右側:天賦特性(位置調整到前面)
|
||
const rightCol = $('<div>').addClass('skill-col');
|
||
|
||
// 先顯示天賦特性
|
||
const talents = details['天賦特性'] ?? {};
|
||
const talentEntries = toEntries(talents);
|
||
|
||
if (talentEntries.length > 0) {
|
||
rightCol.append($('<div>').addClass('section__title').text('天賦特性'));
|
||
talentEntries.forEach(([talentName, talentDetails]) => {
|
||
const item = $('<div>').addClass('skill-item');
|
||
|
||
// 天賦名稱
|
||
let displayName = talentName;
|
||
if (talentDetails && typeof talentDetails === 'object' && talentDetails.name) {
|
||
displayName = talentDetails.name;
|
||
}
|
||
$('<div>').addClass('skill-name').text(displayName).appendTo(item);
|
||
|
||
if (talentDetails && typeof talentDetails === 'object') {
|
||
// 先顯示描述
|
||
if (talentDetails['描述']) {
|
||
$('<div>').addClass('skill-desc').text(textify(talentDetails['描述'], '')).appendTo(item);
|
||
}
|
||
|
||
// 🔴 關鍵修改:分離處理「加值」和其他屬性
|
||
Object.entries(talentDetails).forEach(([key, value]) => {
|
||
if (key === '$meta' || key === 'name' || key === '描述') return;
|
||
|
||
if (key === '加值' && value && typeof value === 'object') {
|
||
// 加值特殊處理(跟主角/同伴一樣)
|
||
const bonusEntries = [];
|
||
Object.entries(value).forEach(([bonusKey, bonusValue]) => {
|
||
bonusEntries.push(`${bonusKey}+【${bonusValue}】`);
|
||
});
|
||
|
||
// 添加分隔線(如果有其他內容)
|
||
const hasOtherContent = talentDetails['描述'] || Object.keys(talentDetails).some(k =>
|
||
k !== '$meta' && k !== 'name' && k !== '描述' && k !== '加值'
|
||
);
|
||
|
||
if (hasOtherContent) {
|
||
const separator = $('<div>').css({
|
||
'height': '1px',
|
||
'background': 'linear-gradient(90deg, transparent, rgba(148, 197, 255, 0.4), transparent)',
|
||
'margin': '8px 0',
|
||
'width': '100%'
|
||
});
|
||
item.append(separator);
|
||
}
|
||
|
||
const bonusDiv = $('<div>').addClass('skill-desc').css({
|
||
'background': 'rgba(94, 161, 255, 0.08)',
|
||
'border-left': '3px solid var(--survival-accent)',
|
||
'padding': '6px 10px',
|
||
'border-radius': '4px',
|
||
'margin': '4px 0'
|
||
}).text(`加值效果:${bonusEntries.join('、')}`);
|
||
|
||
item.append(bonusDiv);
|
||
} else {
|
||
// 其他屬性正常顯示
|
||
$('<div>').addClass('skill-desc').text(`【${key}】${textify(value, '')}`).appendTo(item);
|
||
}
|
||
});
|
||
} else if (typeof talentDetails === 'string') {
|
||
$('<div>').addClass('skill-desc').text(talentDetails).appendTo(item);
|
||
}
|
||
|
||
rightCol.append(item);
|
||
});
|
||
}
|
||
|
||
// 然後顯示技能
|
||
const skills = details['技能'] ?? {};
|
||
const skillEntries = toEntries(skills);
|
||
|
||
rightCol.append($('<div>').addClass('section__title').css('margin-top', '20px').text('技能'));
|
||
|
||
if (skillEntries.length === 0) {
|
||
rightCol.append($('<p>').addClass('empty-text').text('暫無技能'));
|
||
} else {
|
||
skillEntries.forEach(([skillName, skillDetails]) => {
|
||
if (skillDetails && typeof skillDetails === 'object') {
|
||
const level = Number(e(skillDetails['等級'] ?? 0));
|
||
const exp = Number(e(skillDetails['當前經驗'] ?? 0));
|
||
const required = Number(e(skillDetails['升級所需經驗'] ?? 1)) || 1;
|
||
rightCol.append($('<div>').addClass('skill-item').append(
|
||
$('<div>').addClass('skill-name').text(`${skillName} Lv.${level}`),
|
||
t('', exp, required)
|
||
));
|
||
}
|
||
});
|
||
}
|
||
|
||
// 將左右兩欄組合
|
||
const wrapper = $('<div>').addClass('attr-skill-wrapper');
|
||
wrapper.append(leftCol, rightCol);
|
||
cardBody.append(wrapper);
|
||
|
||
card.append(cardHeader, cardBody);
|
||
cardHeader.on('click', () => {
|
||
card.toggleClass('active');
|
||
cardBody.slideToggle(200);
|
||
});
|
||
$body.append(card);
|
||
}
|
||
});
|
||
}
|
||
container.append($section);
|
||
}
|
||
|
||
// 分类颜色
|
||
function getCategoryColor(category) {
|
||
const cat = (category || '').toLowerCase();
|
||
if (cat.includes('建造') || cat.includes('建築')) return '#e67e22';
|
||
if (cat.includes('製作') || cat.includes('大型')) return '#f39c12';
|
||
if (cat.includes('採集') || cat.includes('開採')) return '#27ae60';
|
||
if (cat.includes('馴養') || cat.includes('馴服')) return '#8e44ad';
|
||
if (cat.includes('拆解') || cat.includes('拆除')) return '#c0392b';
|
||
if (cat.includes('教學') || cat.includes('教授')) return '#2980b9';
|
||
return '#ffa502';
|
||
}
|
||
|
||
// 渲染扩展字段(材料、需求等)
|
||
function renderExtendedFields(container, data, skipKeys) {
|
||
if (!data || typeof data !== 'object') return;
|
||
const entries = Object.entries(data).filter(([k]) => !skipKeys.has(k) && !k.startsWith('$'));
|
||
if (entries.length === 0) return;
|
||
|
||
entries.forEach(([key, value]) => {
|
||
const fieldTitle = $('<div>').css({
|
||
'font-size': '12px',
|
||
'font-weight': 'bold',
|
||
'color': 'var(--survival-accent)',
|
||
'margin-top': '6px',
|
||
'margin-bottom': '4px'
|
||
}).text(key);
|
||
container.append(fieldTitle);
|
||
|
||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||
const isMaterialStruct = Object.values(value).some(v => v && typeof v === 'object' && ('所需' in v || '已投入' in v));
|
||
Object.entries(value).forEach(([subKey, subValue]) => {
|
||
if (isMaterialStruct && subValue && typeof subValue === 'object') {
|
||
const needed = Number(subValue['所需'] ?? 0);
|
||
const invested = Number(subValue['已消耗'] ?? 0);
|
||
const isCompleted = needed > 0 && invested >= needed;
|
||
const matLine = $('<div>').css({
|
||
'margin-left': '6px', 'line-height': '1.6', 'font-size': '11px',
|
||
'color': isCompleted ? '#888888' : '#cccccc',
|
||
'text-decoration': isCompleted ? 'line-through' : 'none'
|
||
});
|
||
$('<span>').css('color', isCompleted ? '#888888' : '#cccccc').text(`${subKey}:`).appendTo(matLine);
|
||
$('<span>').css('color', isCompleted ? '#888888' : '#ffa502').text(`${invested}/${needed}`).appendTo(matLine);
|
||
if (isCompleted) $('<span>').css({ 'color': '#2ecc71', 'margin-left': '6px', 'font-size': '10px' }).text('✓').appendTo(matLine);
|
||
container.append(matLine);
|
||
} else {
|
||
const subLine = $('<div>').css({ 'margin-left': '6px', 'line-height': '1.6', 'font-size': '11px', 'color': '#cccccc' });
|
||
$('<span>').css('color', '#aaaaaa').text(`${subKey}:`).appendTo(subLine);
|
||
$('<span>').css('color', '#ffa502').text(textify(subValue, '')).appendTo(subLine);
|
||
container.append(subLine);
|
||
}
|
||
});
|
||
} else if (Array.isArray(value)) {
|
||
const line = $('<div>').css({ 'margin-left': '6px', 'line-height': '1.6', 'font-size': '11px', 'color': '#cccccc' });
|
||
$('<span>').css('color', '#aaaaaa').text(`${key}:`).appendTo(line);
|
||
$('<span>').css('color', '#ffa502').text(value.map(v => textify(v, '')).join('、')).appendTo(line);
|
||
container.append(line);
|
||
} else {
|
||
const line = $('<div>').css({ 'margin-left': '6px', 'line-height': '1.6', 'font-size': '11px', 'color': '#cccccc' });
|
||
$('<span>').css('color', '#aaaaaa').text(`${key}:`).appendTo(line);
|
||
$('<span>').css('color', '#ffa502').text(textify(value, '')).appendTo(line);
|
||
container.append(line);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 核心:渲染项目、研究、知识库
|
||
function renderProgressAndKnowledge(container, statData) {
|
||
const projects = statData['項目'];
|
||
const research = statData['研究'];
|
||
const knowledge = statData['知識庫'];
|
||
|
||
let projectCount = 0, researchCount = 0, knowledgeCount = 0;
|
||
if (projects && typeof projects === 'object') projectCount = Object.keys(projects).filter(k => !k.startsWith('$')).length;
|
||
if (research && typeof research === 'object') {
|
||
for (const cat in research) {
|
||
if (cat.startsWith('$')) continue;
|
||
const ongoing = research[cat]?.['進行中'];
|
||
if (ongoing && typeof ongoing === 'object') researchCount += Object.keys(ongoing).filter(k => !k.startsWith('$')).length;
|
||
}
|
||
}
|
||
if (knowledge && typeof knowledge === 'object') {
|
||
for (const cat in knowledge) {
|
||
if (cat.startsWith('$')) continue;
|
||
knowledgeCount += Object.keys(knowledge[cat] || {}).filter(k => !k.startsWith('$')).length;
|
||
}
|
||
}
|
||
if (projectCount === 0 && researchCount === 0 && knowledgeCount === 0) return;
|
||
|
||
const { $section: parentSection, $body: parentBody } = d('進度與知識', false);
|
||
const skipProjectKeys = new Set(['位置', '類別', '當前進度', '總進度', '描述', '項目產出']);
|
||
const skipResearchKeys = new Set(['當前進度', '總進度', '描述']);
|
||
|
||
// 项目
|
||
if (projectCount > 0) {
|
||
const projStrip = $('<div>').addClass('task-strip side-task').css({ 'padding': '7px 16px', 'min-height': '31px', 'margin-top': '8px' });
|
||
$('<div>').addClass('task-content').css('color', '#f39c12').text('建造項目').appendTo(projStrip);
|
||
const projDetail = $('<div>').addClass('task-detail').hide();
|
||
const projContainer = $('<div>').addClass('progress-knowledge-card').css({ 'display': 'flex', 'flex-direction': 'column', 'gap': '6px', 'padding-top': '6px' });
|
||
for (const [name, proj] of Object.entries(projects || {})) {
|
||
if (name.startsWith('$') || !proj || typeof proj !== 'object') continue;
|
||
const card = $('<div>').css({ 'background': 'rgba(0,0,0,0.1)', 'border': '1px solid rgba(255,152,0,0.15)', 'border-radius': '6px', 'padding': '8px 10px' });
|
||
const titleRow = $('<div>').css({ 'display': 'flex', 'align-items': 'center', 'justify-content': 'center', 'gap': '6px', 'margin-bottom': '4px', 'flex-wrap': 'wrap' });
|
||
$('<div>').css({ 'font-size': '14px', 'font-weight': 'bold', 'color': '#ffa502', 'text-align': 'center' }).text(name).appendTo(titleRow);
|
||
const cat = textify(proj['類別'], '');
|
||
if (cat) {
|
||
const catColor = getCategoryColor(cat);
|
||
$('<div>').css({ 'display': 'inline-block', 'font-size': '8px', 'color': '#ffffff', 'background': catColor, 'padding': '0px 4px', 'border-radius': '6px', 'line-height': '1.4' }).text(cat).appendTo(titleRow);
|
||
}
|
||
card.append(titleRow);
|
||
const cur = Number(proj['當前進度'] ?? 0);
|
||
const max = Number(proj['總進度'] ?? 100);
|
||
const progressWrapper = $('<div>').addClass('progress-wrapper');
|
||
$('<div>').css({ 'font-size': '12px', 'color': 'var(--survival-accent)' }).text('進度:').append($('<span>').css('color', '#ffffff').text(`${cur}/${max}`)).appendTo(progressWrapper);
|
||
const bar = $('<div>').addClass('progress');
|
||
$('<div>').addClass('progress__bar').css('width', `${Math.max(0, Math.min(100, max === 0 ? 0 : (cur / max) * 100))}%`).appendTo(bar);
|
||
progressWrapper.append(bar);
|
||
card.append(progressWrapper);
|
||
const desc = textify(proj['描述'], '');
|
||
if (desc) $('<div>').css({ 'font-size': '11px', 'color': '#aaaaaa', 'line-height': '1.5', 'margin-top': '4px' }).text(desc).appendTo(card);
|
||
renderExtendedFields(card, proj, skipProjectKeys);
|
||
projContainer.append(card);
|
||
}
|
||
const projCollapseBtn = $('<div>').addClass('collapse-button').css({ 'padding': '4px 12px', 'line-height': '1.2' }).text('點擊收起');
|
||
projCollapseBtn.on('click', (e) => { e.stopPropagation(); projDetail.slideUp(300); });
|
||
projDetail.append(projContainer, projCollapseBtn);
|
||
projStrip.on('click', () => projDetail.slideToggle());
|
||
parentBody.append(projStrip, projDetail);
|
||
}
|
||
|
||
// 研究
|
||
if (researchCount > 0) {
|
||
const resStrip = $('<div>').addClass('task-strip main-task').css({ 'padding': '7px 16px', 'min-height': '31px', 'margin-top': '8px' });
|
||
$('<div>').addClass('task-content').css('color', '#3498db').text('研究').appendTo(resStrip);
|
||
const resDetail = $('<div>').addClass('task-detail').hide();
|
||
const resContainer = $('<div>').addClass('progress-knowledge-card').css({ 'display': 'flex', 'flex-direction': 'column', 'gap': '6px', 'padding-top': '6px' });
|
||
for (const [catName, catData] of Object.entries(research || {})) {
|
||
if (catName.startsWith('$') || !catData || typeof catData !== 'object') continue;
|
||
const ongoing = catData['進行中'];
|
||
if (!ongoing || typeof ongoing !== 'object') continue;
|
||
for (const [resName, resItem] of Object.entries(ongoing)) {
|
||
if (resName.startsWith('$') || !resItem || typeof resItem !== 'object') continue;
|
||
const card = $('<div>').css({ 'background': 'rgba(0,0,0,0.1)', 'border': '1px solid rgba(52,152,219,0.15)', 'border-radius': '6px', 'padding': '8px 10px' });
|
||
const titleRow = $('<div>').css({ 'display': 'flex', 'align-items': 'center', 'justify-content': 'center', 'gap': '6px', 'margin-bottom': '4px', 'flex-wrap': 'wrap' });
|
||
$('<div>').css({ 'font-size': '14px', 'font-weight': 'bold', 'color': '#ffa502', 'text-align': 'center' }).text(resName).appendTo(titleRow);
|
||
const catColor = getCategoryColor(catName);
|
||
$('<div>').css({ 'display': 'inline-block', 'font-size': '8px', 'color': '#ffffff', 'background': catColor, 'padding': '0px 4px', 'border-radius': '6px', 'line-height': '1.4' }).text(catName).appendTo(titleRow);
|
||
card.append(titleRow);
|
||
const cur = Number(resItem['當前進度'] ?? 0);
|
||
const max = Number(resItem['總進度'] ?? 100);
|
||
const progressWrapper = $('<div>').addClass('progress-wrapper');
|
||
$('<div>').css({ 'font-size': '12px', 'color': 'var(--survival-accent)' }).text('進度:').append($('<span>').css('color', '#ffffff').text(`${cur}/${max}`)).appendTo(progressWrapper);
|
||
const bar = $('<div>').addClass('progress');
|
||
$('<div>').addClass('progress__bar').css('width', `${Math.max(0, Math.min(100, max === 0 ? 0 : (cur / max) * 100))}%`).appendTo(bar);
|
||
progressWrapper.append(bar);
|
||
card.append(progressWrapper);
|
||
const desc = textify(resItem['描述'], '');
|
||
if (desc) $('<div>').css({ 'font-size': '11px', 'color': '#aaaaaa', 'line-height': '1.5', 'margin-top': '4px' }).text(desc).appendTo(card);
|
||
renderExtendedFields(card, resItem, skipResearchKeys);
|
||
resContainer.append(card);
|
||
}
|
||
}
|
||
const resCollapseBtn = $('<div>').addClass('collapse-button').css({ 'padding': '4px 12px', 'line-height': '1.2' }).text('點擊收起');
|
||
resCollapseBtn.on('click', (e) => { e.stopPropagation(); resDetail.slideUp(300); });
|
||
resDetail.append(resContainer, resCollapseBtn);
|
||
resStrip.on('click', () => resDetail.slideToggle());
|
||
parentBody.append(resStrip, resDetail);
|
||
}
|
||
|
||
// 知识库
|
||
if (knowledgeCount > 0) {
|
||
const knowStrip = $('<div>').addClass('task-strip bond-task').css({ 'padding': '7px 16px', 'min-height': '31px', 'margin-top': '8px' });
|
||
$('<div>').addClass('task-content').css('color', '#00cec9').text('知識庫').appendTo(knowStrip);
|
||
const knowDetail = $('<div>').addClass('task-detail').hide();
|
||
const knowContainer = $('<div>').addClass('progress-knowledge-card').css({ 'display': 'flex', 'flex-direction': 'column', 'gap': '4px', 'padding-top': '6px' });
|
||
for (const [catName, catItems] of Object.entries(knowledge || {})) {
|
||
if (catName.startsWith('$') || !catItems || typeof catItems !== 'object') continue;
|
||
const itemEntries = Object.entries(catItems).filter(([k]) => !k.startsWith('$'));
|
||
if (itemEntries.length === 0) continue;
|
||
$('<div>').css({ 'font-size': '12px', 'font-weight': 'bold', 'color': '#00cec9', 'border-bottom': '1px solid rgba(0,206,201,0.3)', 'padding-bottom': '4px', 'margin-top': '4px' }).text(catName).appendTo(knowContainer);
|
||
itemEntries.forEach(([itemName, itemData]) => {
|
||
const itemRow = $('<div>').css({ 'padding': '3px 0 3px 6px', 'font-size': '12px', 'line-height': '1.4' });
|
||
$('<span>').css({ 'color': '#ffa502', 'font-weight': 'bold' }).text(itemName).appendTo(itemRow);
|
||
const desc = textify(itemData?.['描述'] ?? '', '');
|
||
if (desc) $('<span>').css('color', '#cccccc').text(`:${desc}`).appendTo(itemRow);
|
||
knowContainer.append(itemRow);
|
||
});
|
||
}
|
||
const knowCollapseBtn = $('<div>').addClass('collapse-button').css({ 'padding': '4px 12px', 'line-height': '1.2' }).text('點擊收起');
|
||
knowCollapseBtn.on('click', (e) => { e.stopPropagation(); knowDetail.slideUp(300); });
|
||
knowDetail.append(knowContainer, knowCollapseBtn);
|
||
knowStrip.on('click', () => knowDetail.slideToggle());
|
||
parentBody.append(knowStrip, knowDetail);
|
||
}
|
||
container.append(parentSection);
|
||
}
|
||
|
||
// ============== 修改版 renderGenericSection 函數(包含畜牧邏輯) ==============
|
||
function renderGenericSection(container, title, source, statData) {
|
||
if (source == null) return;
|
||
const entries = toEntries(source);
|
||
if (!entries || entries.length === 0) return;
|
||
|
||
// --- 特殊處理:基本資料(顯示為卡片式) ---
|
||
if (title === '基本資料') {
|
||
const { $section, $body } = d('基本資訊', false);
|
||
|
||
const infoGrid = $('<div>').css({
|
||
'display': 'grid',
|
||
'grid-template-columns': 'repeat(auto-fit, minmax(200px, 1fr))',
|
||
'gap': '12px',
|
||
'margin-top': '10px'
|
||
});
|
||
|
||
entries.forEach(([key, value]) => {
|
||
const infoCard = $('<div>').css({
|
||
'background': 'rgba(30, 45, 70, 0.6)',
|
||
'border': '1px solid var(--survival-border)',
|
||
'border-radius': '8px',
|
||
'padding': '12px',
|
||
'text-align': 'center'
|
||
});
|
||
|
||
$('<div>').css({
|
||
'font-size': '14px',
|
||
'color': '#ffb86c',
|
||
'margin-bottom': '4px',
|
||
'font-weight': 'bold'
|
||
}).text(key).appendTo(infoCard);
|
||
|
||
$('<div>').css({
|
||
'font-size': '16px',
|
||
'color': 'var(--survival-text)'
|
||
}).text(textify(value, '--')).appendTo(infoCard);
|
||
|
||
infoGrid.append(infoCard);
|
||
});
|
||
|
||
$body.append(infoGrid);
|
||
container.append($section);
|
||
return;
|
||
}
|
||
|
||
// --- 特殊處理:如果標題是「捷徑或大型裝置」,使用獨立網格顯示 ---
|
||
if (title === '捷徑或大型裝置') {
|
||
const { $section, $body } = d(title, false);
|
||
|
||
const deviceGrid = $('<div>').addClass('inventory-grid');
|
||
|
||
entries.forEach(([name, data]) => {
|
||
if (data && typeof data === 'object') {
|
||
const cell = $('<div>').addClass('inventory-item');
|
||
$('<div>').addClass('item-name').text(name).appendTo(cell);
|
||
|
||
const props = $('<div>').addClass('item-props');
|
||
|
||
const objectProperties = {};
|
||
const simpleProperties = {};
|
||
|
||
Object.entries(data).forEach(([key, value]) => {
|
||
if (key === '$meta') return;
|
||
|
||
if (value !== null && value !== undefined && value !== '') {
|
||
if (typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length > 0) {
|
||
objectProperties[key] = value;
|
||
} else {
|
||
simpleProperties[key] = value;
|
||
}
|
||
}
|
||
});
|
||
|
||
Object.entries(simpleProperties).forEach(([key, value]) => {
|
||
const line = $('<div>').addClass('item-prop');
|
||
$('<span>').addClass('prop-key').text(String(key)).appendTo(line);
|
||
$('<span>').addClass('prop-sep').text(': ').appendTo(line);
|
||
$('<span>').addClass('prop-val').text(textify(value, '')).appendTo(line);
|
||
props.append(line);
|
||
});
|
||
|
||
Object.entries(objectProperties).forEach(([propertyName, propertyData]) => {
|
||
const objectLine = $('<div>').addClass('item-prop');
|
||
$('<span>').addClass('prop-key').text(propertyName).appendTo(objectLine);
|
||
$('<span>').addClass('prop-sep').text(': ').appendTo(objectLine);
|
||
|
||
const objectContent = $('<div>').addClass('object-content').css({
|
||
'display': 'flex',
|
||
'flex-direction': 'column',
|
||
'gap': '4px',
|
||
'margin-top': '4px'
|
||
});
|
||
|
||
Object.entries(propertyData).forEach(([itemName, itemData]) => {
|
||
if (itemName !== '$meta' && itemData !== null && itemData !== '') {
|
||
const itemLine = $('<div>').addClass('object-item').css({
|
||
'text-align': 'left',
|
||
'color': 'var(--survival-text)'
|
||
});
|
||
|
||
let quantity = '';
|
||
if (itemData && typeof itemData === 'object') {
|
||
if (itemData['數量'] !== undefined) {
|
||
quantity = `(數量:${itemData['數量']})`;
|
||
}
|
||
} else if (typeof itemData === 'number') {
|
||
quantity = `(數量:${itemData})`;
|
||
}
|
||
|
||
itemLine.text(`${itemName}${quantity}`);
|
||
objectContent.append(itemLine);
|
||
}
|
||
});
|
||
|
||
$('<span>').addClass('prop-val').append(objectContent).appendTo(objectLine);
|
||
props.append(objectLine);
|
||
});
|
||
|
||
cell.append(props);
|
||
deviceGrid.append(cell);
|
||
}
|
||
});
|
||
|
||
$body.append(deviceGrid);
|
||
container.append($section);
|
||
return;
|
||
}
|
||
|
||
// --- 以下是作物、畜牧、營地相關邏輯 ---
|
||
let overallCropStatusTag = '';
|
||
let overallLivestockStatusTag = '';
|
||
let overallCampTag = '';
|
||
let accelerationEffects = [];
|
||
let totalAcceleration = 0;
|
||
|
||
// 作物狀態標籤邏輯
|
||
if (title === '作物') {
|
||
const hasReadyCrop = entries.some(([, value]) => textify(value?.['生長階段'], '').includes('可收割'));
|
||
overallCropStatusTag = `<span class="crop-status-indicator ${hasReadyCrop ? 'crop-status-ready' : 'crop-status-growing'}">${hasReadyCrop ? '可收割' : '未熟成'}</span>`;
|
||
|
||
const currentSeason = textify(statData?.['環境']?.['季節'], '');
|
||
const seasonRules = {
|
||
'春季': { effect: 15, icon: '🌸' },
|
||
'夏季': { effect: 20, icon: '☀️' },
|
||
'秋季': { effect: 0, icon: '🍁' },
|
||
'冬季': { effect: -50, icon: '❄️' }
|
||
};
|
||
if (seasonRules[currentSeason]) {
|
||
const seasonalEffect = seasonRules[currentSeason];
|
||
accelerationEffects.push({
|
||
name: `${currentSeason}季節`,
|
||
effect: seasonalEffect.effect,
|
||
icon: seasonalEffect.icon,
|
||
source: '季節',
|
||
order: 1
|
||
});
|
||
totalAcceleration += seasonalEffect.effect;
|
||
}
|
||
|
||
const equipment = statData?.['裝備'] ?? {};
|
||
Object.entries(equipment).forEach(([itemName, itemData]) => {
|
||
if (itemData && typeof itemData === 'object') {
|
||
const cropBonus = Number(e(itemData['作物加成'] ?? 0));
|
||
if (cropBonus !== 0) {
|
||
accelerationEffects.push({
|
||
name: `${itemName}`,
|
||
effect: cropBonus,
|
||
icon: '🛡️',
|
||
source: '裝備',
|
||
order: 2
|
||
});
|
||
totalAcceleration += cropBonus;
|
||
}
|
||
}
|
||
});
|
||
|
||
const carriedItems = statData?.['攜帶物品'] ?? {};
|
||
Object.entries(carriedItems).forEach(([itemName, itemData]) => {
|
||
if (itemData && typeof itemData === 'object') {
|
||
const cropBonus = Number(e(itemData['作物加成'] ?? 0));
|
||
if (cropBonus !== 0) {
|
||
accelerationEffects.push({
|
||
name: `${itemName}`,
|
||
effect: cropBonus,
|
||
icon: '🧺',
|
||
source: '攜帶物品',
|
||
order: 3
|
||
});
|
||
totalAcceleration += cropBonus;
|
||
}
|
||
}
|
||
});
|
||
|
||
entries.forEach(([name, data]) => {
|
||
const growthStage = textify(data['生長階段'], '');
|
||
const cropBonus = Number(e(data['作物加成'] ?? 0));
|
||
|
||
if (growthStage.includes('特殊作物') || cropBonus !== 0) {
|
||
const existing = accelerationEffects.find(eff => eff.name === name);
|
||
if (!existing) {
|
||
accelerationEffects.push({
|
||
name: `${name}`,
|
||
effect: cropBonus,
|
||
icon: '🌿',
|
||
source: '特殊作物',
|
||
order: 4
|
||
});
|
||
totalAcceleration += cropBonus;
|
||
}
|
||
}
|
||
});
|
||
|
||
const companions = statData?.['同伴'] ?? {};
|
||
Object.values(companions).forEach(companion => {
|
||
if (companion && typeof companion === 'object') {
|
||
const companionItems = companion['攜帶物品'] ?? {};
|
||
Object.entries(companionItems).forEach(([itemName, itemData]) => {
|
||
if (itemData && typeof itemData === 'object') {
|
||
const cropBonus = Number(e(itemData['作物加成'] ?? 0));
|
||
if (cropBonus !== 0) {
|
||
accelerationEffects.push({
|
||
name: `${itemName}`,
|
||
effect: cropBonus,
|
||
icon: '👥',
|
||
source: '同伴物品',
|
||
order: 5
|
||
});
|
||
totalAcceleration += cropBonus;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
// 畜牧狀態標籤邏輯
|
||
if (title === '畜牧') {
|
||
const currentDay = Number(e(statData?.['系統']?.['經過天數'] ?? 0));
|
||
let hasHarvestable = false;
|
||
|
||
entries.forEach(([, data]) => {
|
||
if (data && typeof data === 'object') {
|
||
const output = data['產出物'];
|
||
if (output && output['名稱']) {
|
||
const nextOutputDay = Number(e(output['下次產出日'] ?? 0));
|
||
if (nextOutputDay <= currentDay) {
|
||
hasHarvestable = true;
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
if (hasHarvestable) {
|
||
overallLivestockStatusTag = `<span class="livestock-status-indicator livestock-status-harvest">可收成</span>`;
|
||
} else {
|
||
overallLivestockStatusTag = `<span class="livestock-status-indicator livestock-status-none">飼養中</span>`;
|
||
}
|
||
}
|
||
|
||
// 🔴 營地存糧標籤邏輯(獨立放在這裡,與作物/畜牧同級)
|
||
if (title === '營地') {
|
||
const foodSupply = statData?.['系統']?.['當前存糧天數'];
|
||
if (foodSupply !== undefined && foodSupply !== null) {
|
||
const days = Number(foodSupply);
|
||
let displayText = '';
|
||
let colorClass = '';
|
||
|
||
if (days <= 3) {
|
||
displayText = `存糧 ${days.toFixed(1)} 天`; // 顯示小數點
|
||
colorClass = 'crop-status-danger'; // 🔴 紅色(警告)
|
||
} else {
|
||
displayText = `存糧 ${days.toFixed(0)} 天`; // 顯示整數
|
||
colorClass = 'crop-status-ready'; // 🟢 綠色(正常)
|
||
}
|
||
|
||
overallCampTag = `<span class="crop-status-indicator ${colorClass}">${displayText}</span>`;
|
||
}
|
||
}
|
||
|
||
// --- 創建 section ---
|
||
const { $section, $header, $body } = d(title, false);
|
||
|
||
// 🔴 添加所有標籤到標題(順序:營地存糧 → 作物 → 畜牧)
|
||
if (title === '營地' && overallCampTag) {
|
||
$header.append(overallCampTag);
|
||
}
|
||
if (overallCropStatusTag) $header.append(overallCropStatusTag);
|
||
if (overallLivestockStatusTag) $header.append(overallLivestockStatusTag);
|
||
|
||
// 作物加速效果顯示
|
||
if (title === '作物' && accelerationEffects.length > 0) {
|
||
const infoBox = $('<div>').addClass('crop-acceleration-info');
|
||
infoBox.append($('<div>').addClass('crop-acceleration-info__title').html('🏕️ 作物加速效果'));
|
||
|
||
accelerationEffects.sort((a, b) => {
|
||
if (a.order !== b.order) {
|
||
return a.order - b.order;
|
||
}
|
||
return b.effect - a.effect;
|
||
});
|
||
|
||
accelerationEffects.forEach(eff => {
|
||
const effectLine = $('<div>').addClass('crop-acceleration-info__item');
|
||
const effectText = eff.effect > 0 ? `加速 ${eff.effect} %` : `減速 ${Math.abs(eff.effect)} %`;
|
||
effectLine.html(`${eff.icon} ${eff.name}:${effectText}`);
|
||
infoBox.append(effectLine);
|
||
});
|
||
|
||
const totalLine = $('<div>').addClass('crop-acceleration-info__total');
|
||
const totalActionText = totalAcceleration > 0 ? '總加速' : '總減速';
|
||
totalLine.html(`⚡ ${totalActionText}:${Math.abs(totalAcceleration)} %`);
|
||
infoBox.append(totalLine);
|
||
|
||
$body.append(infoBox);
|
||
}
|
||
|
||
const firstValue = entries[0]?.[1];
|
||
let isItemCollection = false;
|
||
if (firstValue && typeof firstValue === 'object') {
|
||
const keys = Object.keys(firstValue);
|
||
const itemKeywords = ['數量', '重量', '耐久度', '狀態', '功能', '成長週期', '飼養數量', '年齡', '生長階段'];
|
||
if (keys.some(k => itemKeywords.includes(k))) isItemCollection = true;
|
||
}
|
||
|
||
if (isItemCollection) {
|
||
const grid = $('<div>').addClass('inventory-grid');
|
||
entries.forEach(([name, data]) => {
|
||
if (title === '作物') {
|
||
const cycle = Number(e(data['成長週期'] ?? 0));
|
||
if (cycle < 0) return;
|
||
}
|
||
|
||
if (data && typeof data === 'object') {
|
||
const cell = $('<div>').addClass('inventory-item');
|
||
const itemNameDiv = $('<div>').addClass('item-name').text(data['名稱'] || String(name));
|
||
|
||
if (title === '作物') {
|
||
const growthStage = textify(data['生長階段'], '');
|
||
if (growthStage) {
|
||
const isReady = growthStage.includes('可收割');
|
||
const statusTag = $('<span>')
|
||
.addClass('crop-status-indicator')
|
||
.addClass(isReady ? 'crop-status-ready' : 'crop-status-growing')
|
||
.text(growthStage);
|
||
itemNameDiv.append(statusTag);
|
||
}
|
||
}
|
||
|
||
if (title === '畜牧') {
|
||
const currentDay = Number(e(statData?.['系統']?.['經過天數'] ?? 0));
|
||
let isHarvestable = false;
|
||
const output = data['產出物'];
|
||
if (output && output['名稱']) {
|
||
const nextOutputDay = Number(e(output['下次產出日'] ?? 0));
|
||
if (nextOutputDay <= currentDay) {
|
||
isHarvestable = true;
|
||
}
|
||
}
|
||
|
||
if (isHarvestable) {
|
||
const statusTag = $('<span>')
|
||
.addClass('livestock-status-indicator')
|
||
.addClass('livestock-status-harvest')
|
||
.text('可收成');
|
||
itemNameDiv.append(statusTag);
|
||
}
|
||
}
|
||
|
||
cell.append(itemNameDiv);
|
||
|
||
const props = $('<div>').addClass('item-props');
|
||
const attributes = new Map();
|
||
|
||
let definedOrder = [];
|
||
if (title === '作物') {
|
||
definedOrder = ['數量', '位置', '重量', '耐久度', '成長週期', '生長進度'];
|
||
} else if (title === '畜牧') {
|
||
definedOrder = ['年齡', '飼養數量', '生長階段', '產出物'];
|
||
} else {
|
||
definedOrder = ['數量', '位置', '重量', '耐久度'];
|
||
}
|
||
|
||
definedOrder.forEach(k => {
|
||
if (data.hasOwnProperty(k)) attributes.set(k, data[k]);
|
||
});
|
||
|
||
const stateType = textify(data['狀態類型'] ?? '');
|
||
if (stateType && data.hasOwnProperty('狀態')) {
|
||
if (stateType.toLowerCase() !== '耐久度' || !attributes.has('耐久度')) {
|
||
attributes.set(stateType, data['狀態']);
|
||
}
|
||
}
|
||
|
||
if (title === '畜牧') {
|
||
if (data.hasOwnProperty('產出物')) {
|
||
attributes.set('產出物', data['產出物']);
|
||
}
|
||
} else {
|
||
const middleOrder = ['描述', '功能', '功能1', '功能2', '功能3'];
|
||
middleOrder.forEach(k => {
|
||
if (data.hasOwnProperty(k)) attributes.set(k, data[k]);
|
||
});
|
||
}
|
||
|
||
Object.entries(data).forEach(([k, v]) => {
|
||
if (!attributes.has(k) && k !== '$meta' && k !== '狀態類型' && k !== '狀態' && k !== '生長階段') {
|
||
attributes.set(k, v);
|
||
}
|
||
});
|
||
|
||
attributes.forEach((v, k) => {
|
||
if (v !== null && v !== '' && v !== undefined) {
|
||
if (k === 'harvestAction') return;
|
||
if (k === '繁殖') return;
|
||
|
||
const line = $('<div>').addClass('item-prop');
|
||
|
||
if (k === '加值') {
|
||
line.addClass('bonus-prop');
|
||
const separator = $('<div>').addClass('bonus-separator').css({
|
||
'height': '1px',
|
||
'background': 'linear-gradient(90deg, transparent, rgba(148, 197, 255, 0.4), transparent)',
|
||
'margin': '10px 0 8px 0',
|
||
'width': '100%'
|
||
});
|
||
props.append(separator);
|
||
}
|
||
|
||
$('<span>').addClass('prop-key').text(String(k)).appendTo(line);
|
||
$('<span>').addClass('prop-sep').text(': ').appendTo(line);
|
||
|
||
let displayValue = '';
|
||
if (typeof v === 'object') {
|
||
if (k === '產出物') {
|
||
const objLines = [];
|
||
Object.entries(v).forEach(([subKey, subVal]) => {
|
||
if (subVal !== null && subVal !== '') {
|
||
objLines.push(`${subKey}: ${textify(subVal, '')}`);
|
||
}
|
||
});
|
||
displayValue = objLines.join(' | ');
|
||
} else if (k === '飼養數量') {
|
||
const male = v['雄性'] || 0;
|
||
const female = v['雌性'] || 0;
|
||
const parts = [];
|
||
if (male > 0) parts.push(`雄性 x ${male}`);
|
||
if (female > 0) parts.push(`雌性 x ${female}`);
|
||
displayValue = parts.join('、');
|
||
} else if (k === '加值') {
|
||
const bonusEntries = [];
|
||
Object.entries(v).forEach(([bonusKey, bonusValue]) => {
|
||
bonusEntries.push(`${bonusKey}+【${bonusValue}】`);
|
||
});
|
||
displayValue = bonusEntries.join('、');
|
||
} else {
|
||
displayValue = '[...]';
|
||
}
|
||
} else {
|
||
displayValue = textify(v, '');
|
||
}
|
||
|
||
if (k === '重量' && typeof v === 'number') displayValue += 'kg';
|
||
if (k === '成長週期' && typeof v === 'number' && v > 0) displayValue += '天';
|
||
if (k === '生長進度' && typeof v === 'number') displayValue += '%';
|
||
if (k === '年齡' && typeof v === 'number') displayValue += '月';
|
||
|
||
$('<span>').addClass('prop-val').text(displayValue).appendTo(line);
|
||
props.append(line);
|
||
|
||
if (k === '加值') {
|
||
const bottomSeparator = $('<div>').css({
|
||
'height': '1px',
|
||
'background': 'rgba(148, 197, 255, 0.1)',
|
||
'margin': '8px 0 4px 0',
|
||
'width': '100%'
|
||
});
|
||
props.append(bottomSeparator);
|
||
}
|
||
}
|
||
});
|
||
cell.append(props);
|
||
grid.append(cell);
|
||
}
|
||
});
|
||
$body.append(grid);
|
||
} else {
|
||
const campInfoContainer = $('<div>').addClass('camp-info-container');
|
||
|
||
if (title === '楓葉潭營地') {
|
||
const basicInfoEntry = entries.find(([key]) => key === '基本資料');
|
||
|
||
if (basicInfoEntry) {
|
||
const [keyName, value] = basicInfoEntry;
|
||
renderGenericSection(campInfoContainer, keyName, value, statData);
|
||
}
|
||
|
||
entries.forEach(([key, value]) => {
|
||
if (key === '基本資料') return;
|
||
|
||
if (value && typeof value === 'object') {
|
||
renderGenericSection(campInfoContainer, key, value, statData);
|
||
} else {
|
||
const infoLine = $('<div>').addClass('camp-info-item');
|
||
infoLine.append(
|
||
$('<span>').addClass('prop-key').text(String(key)),
|
||
$('<span>').addClass('prop-sep').text(': '),
|
||
$('<span>').addClass('prop-val').text(textify(value, '--'))
|
||
);
|
||
campInfoContainer.append(infoLine);
|
||
}
|
||
});
|
||
} else {
|
||
entries.forEach(([key, value]) => {
|
||
if (value && typeof value === 'object') {
|
||
renderGenericSection(campInfoContainer, key, value, statData);
|
||
} else {
|
||
const infoLine = $('<div>').addClass('camp-info-item');
|
||
infoLine.append(
|
||
$('<span>').addClass('prop-key').text(String(key)),
|
||
$('<span>').addClass('prop-sep').text(': '),
|
||
$('<span>').addClass('prop-val').text(textify(value, '--'))
|
||
);
|
||
campInfoContainer.append(infoLine);
|
||
}
|
||
});
|
||
}
|
||
|
||
$body.append(campInfoContainer);
|
||
}
|
||
container.append($section);
|
||
}
|
||
|
||
async function p() {
|
||
try {
|
||
let messageId;
|
||
try {
|
||
messageId = typeof getCurrentMessageId === 'function' ? getCurrentMessageId() : getChatMessages('last')[0]?.message_id;
|
||
} catch(e) {
|
||
messageId = getChatMessages('last')[0]?.message_id;
|
||
}
|
||
|
||
// 關鍵修改:無法獲取消息ID時靜默返回,不報錯
|
||
if (!messageId) {
|
||
console.log('等待消息加載...');
|
||
return;
|
||
}
|
||
|
||
const message = getChatMessages(messageId)[0];
|
||
if (!message) {
|
||
return;
|
||
}
|
||
|
||
const data = message?.data ?? {}, statData = data.stat_data ?? {}, displayData = data.display_data ?? {};
|
||
if (Object.keys(statData).length === 0 && Object.keys(displayData).length === 0) return;
|
||
|
||
const $root = $('#survival-ui').empty();
|
||
const get = (key) => statData?.[key] ?? displayData?.[key];
|
||
|
||
r($root, statData);
|
||
createStatusEffects($root, statData);
|
||
|
||
const { $section: statusSection, $body: statusBody } = d('角色狀態', false);
|
||
renderUnifiedStatus(statusBody, statData);
|
||
$root.append(statusSection);
|
||
|
||
const { $section: skillSection, $body: skillBody } = d('屬性 & 技能', false);
|
||
renderAttributesAndSkills(skillBody, get('屬性'), get('技能'), get('天賦特性'));
|
||
$root.append(skillSection);
|
||
|
||
renderCompanions($root, get('同伴'));
|
||
renderPets($root, get('寵物'));
|
||
|
||
// 這裡新增畜牧的渲染 - 在作物和捷徑之間
|
||
['營地', '作物', '畜牧', '捷徑或大型裝置'].forEach(title => renderGenericSection($root, title, get(title), statData));
|
||
|
||
renderTaskSystem($root, statData);
|
||
renderProgressAndKnowledge($root, statData);
|
||
|
||
const survivalPoints = Number(e(statData['系統']?.['求生點數'])) || 0; // 改成求生點數
|
||
const buttonBar = $('<div>').addClass('button-container');
|
||
buttonBar.append(
|
||
$('<button>').addClass('action-btn draw-btn survival').text(`求生抽卡 (${survivalPoints})`).on('click', () => showDrawConfirmModal('survival', survivalPoints)), // 只傳一個參數
|
||
$('<button>').addClass('action-btn').text('地圖').on('click', () => openImageViewerModal(statData, displayData)),
|
||
$('<button>').addClass('action-btn').text('物品欄').on('click', () => i(get('裝備'), get('攜帶物品'), get('營地'), Number(e(statData['狀態']?.['當前負重'] ?? 0)), Number(e(statData['狀態']?.['負重上限'] ?? 50)), false))
|
||
);
|
||
$root.append(buttonBar);
|
||
|
||
} catch (error) {
|
||
console.error('渲染UI時出錯:', error);
|
||
}
|
||
}
|
||
|
||
$(() => {
|
||
try {
|
||
p();
|
||
const rerender = () => p();
|
||
if (typeof eventOn === 'function') eventOn('mag_variable_update_ended', rerender);
|
||
$(window).on('unload', () => typeof eventRemoveListener === 'function' && eventRemoveListener('mag_variable_update_ended', rerender));
|
||
} catch (error) {
|
||
console.error('初始化UI失敗:', error);
|
||
}
|
||
});
|
||
</script></body></html>
|
||
``` |