Files
tvbox_collect/recha18/03.json
T
2026-07-12 17:13:55 +00:00

4984 lines
202 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
```
<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. 裝備欄網格容器 - 14 */
.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>
```