v0.1.2: 宗族气韵(职事/悟道/谱系/碑录/庆典/飞升/音效)
- 职事任命:长老(+修炼)/供奉(+战力)/执事(+坊市)/掌教(+闭关) 每人加成叠加,上限管理 - 功法悟道:修习积悟性点,小成/大成逐级+战力,大境界突破传道于直系晚辈 - 宗族谱系页:世代分带 + 夫妻同卷 + 子孙连枝,点节点开详情,先人簿可查 - 功德碑:宗主辞世即勒石,宗祠页碑录永存 - 百年庆典 + 飞升之路事件链(留世坐镇/仙风入体 或 云游飞升/宗族蒙荫) - 合成音效(WebAudio):鼓点/钟鸣/纸韵/磬响,设置页开关 - 单测 29→38;typecheck/build 通过;GUI 冒烟受限于 WSLg 掉线(SMOKE-TIMEOUT 优雅退出)
This commit is contained in:
@@ -52,6 +52,11 @@ export function MemberCard({ c }: { c: Character }) {
|
||||
)
|
||||
}
|
||||
|
||||
function postLabel(c: Character): string {
|
||||
const map: Record<string, string> = { head: '家主', elder: '长老', guardian: '供奉', steward: '执事', master: '掌教' }
|
||||
return map[c.post ?? ''] ?? ''
|
||||
}
|
||||
|
||||
const C_STATE: Record<string, string> = {
|
||||
idle: '',
|
||||
meditation: 'good',
|
||||
|
||||
@@ -7,6 +7,7 @@ import { TECHNIQUES } from '../../game/data/techniques'
|
||||
import { ITEMS, ARTIFACT_POWER } from '../../game/data/items'
|
||||
import { combatPowerOf } from '../../game/engine/systems/combat'
|
||||
import { lifespanOf } from '../../game/engine/systems/lifecycle'
|
||||
import { POSTS, POST_ORDER } from '../../game/data/posts'
|
||||
|
||||
function equipableItems(member: Character, s: GameState): string[] {
|
||||
const inv = Object.keys(ARTIFACT_POWER).filter((ai) => (s.family.inventory[ai] ?? 0) > 0)
|
||||
@@ -185,6 +186,42 @@ export function MemberModal({ member }: { member: Character }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{member.alive && (
|
||||
<div className="mm-sec">
|
||||
<div className="dim" style={{ marginBottom: 6 }}>职事(族中分工)</div>
|
||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
||||
<button
|
||||
className={`btn btn-sm ${!member.post ? 'btn-primary' : ''}`}
|
||||
onClick={() => {
|
||||
w.assignPost(member.id, undefined)
|
||||
bump()
|
||||
}}
|
||||
>
|
||||
闲居
|
||||
</button>
|
||||
{POST_ORDER.filter((pid) => pid !== 'head').map((pid) => {
|
||||
const def = POSTS[pid]
|
||||
const maxed = w.postCount(pid) >= def.max && member.post !== pid
|
||||
return (
|
||||
<button
|
||||
key={pid}
|
||||
className={`btn btn-sm ${member.post === pid ? 'btn-primary' : ''}`}
|
||||
disabled={maxed && !!member.post}
|
||||
style={maxed && !member.post ? { opacity: 0.45 } : undefined}
|
||||
title={`${def.desc}${w.postCount(pid)}/${def.max} 在任`}
|
||||
onClick={() => {
|
||||
w.assignPost(member.id, pid)
|
||||
bump()
|
||||
}}
|
||||
>
|
||||
{def.name}({w.postCount(pid)}/{def.max})
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{member.alive && w.canMarry(member.id) && (
|
||||
<div className="mm-sec">
|
||||
<div className="dim" style={{ marginBottom: 6 }}>指婚(寻配族内共居者,远亲无碍)</div>
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { useGameStore } from '../store'
|
||||
import { computeGenealogy } from '../../game/core/genealogy'
|
||||
import { describeRealm } from '../../game/data/realms'
|
||||
import { Character, FamilyState } from '../../game/types/domain'
|
||||
import { useState } from 'react'
|
||||
import { MemberModal } from '../components/MemberModal'
|
||||
|
||||
export default function GenealogyPanel() {
|
||||
const world = useGameStore((s) => s.world)
|
||||
const revision = useGameStore((s) => s.revision)
|
||||
const selectedMemberId = useGameStore((s) => s.selectedMemberId)
|
||||
const setSelected = useGameStore((s) => s.setSelected)
|
||||
const [deceasedTab, setDeceasedTab] = useState(false)
|
||||
void revision
|
||||
|
||||
if (!world) return null
|
||||
const w = world
|
||||
const s = w.state
|
||||
const rows = computeGenealogy(w)
|
||||
const deceased = !deceasedTab
|
||||
? []
|
||||
: Object.values(s.members).filter((c) => !c.alive).sort((a, b) => (a.deathYear ?? 0) - (b.deathYear ?? 0))
|
||||
const selected = selectedMemberId ? s.members[selectedMemberId] as Character | undefined : undefined
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="help-text">
|
||||
宗族谱系:世代分带,夫妻同卷,子孙列后。点任一名字可查详情;染灰者已下世,功绩铭记。
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 6, marginBottom: 10 }}>
|
||||
<button
|
||||
className={`btn btn-sm ${!deceasedTab ? 'btn-primary' : ''}`}
|
||||
onClick={() => setDeceasedTab(false)}
|
||||
>
|
||||
在世谱系
|
||||
</button>
|
||||
<button className={`btn btn-sm ${deceasedTab ? 'btn-primary' : ''}`} onClick={() => setDeceasedTab(true)}>
|
||||
先人簿({Object.values(s.members).filter((c) => !c.alive).length}位)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{!deceasedTab &&
|
||||
rows.map((row) => (
|
||||
<div key={row.gen} className="genealogy-row">
|
||||
<div className="genealogy-gen">第{row.gen}代</div>
|
||||
<div className="genealogy-body">
|
||||
{row.units.map((unit, i) => (
|
||||
<div key={i} className="genealogy-unit">
|
||||
<div className="genealogy-parents">
|
||||
{unit.parents[0] && <GeneNode c={unit.parents[0]} w={w} />}
|
||||
<div className="genealogy-matron">×</div>
|
||||
{unit.parents[1] && <GeneNode c={unit.parents[1]} w={w} />}
|
||||
</div>
|
||||
{unit.children.length > 0 && (
|
||||
<>
|
||||
<div className="genealogy-drop" />
|
||||
<div className="genealogy-children">
|
||||
{unit.children.map((ch) => (
|
||||
<GeneNode key={ch.id} c={ch} w={w} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{row.singles.length > 0 && (
|
||||
<div className="genealogy-singles">
|
||||
{row.singles.map((c) => (
|
||||
<GeneNode key={c.id} c={c} w={w} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{row.units.length + row.singles.length === 0 && <div className="dim2">此代凋零</div>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{deceasedTab &&
|
||||
deceased.map((c) => (
|
||||
<div key={c.id} className="set-row" style={{ maxWidth: 760 }}>
|
||||
<span>
|
||||
<span className="ch-year" style={{ marginRight: 10 }}>殁于{c.deathYear}年</span>
|
||||
{c.name} · {c.gender === 'male' ? '男' : '女'} · 第{c.generation}代
|
||||
<span className="dim2" style={{ marginLeft: 8 }}>{c.deathCause ?? '寿终'}</span>
|
||||
</span>
|
||||
<button className="btn btn-sm" onClick={() => setSelected(c.id)}>阅</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{selected && <MemberModal member={selected} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function GeneNode({ c, w }: { c: Character; w: NonNullable<ReturnType<typeof useGameStore.getState>['world']> }) {
|
||||
const setSelected = useGameStore((s) => s.setSelected)
|
||||
const isHead = w.state.family.headId === c.id
|
||||
const rank = c.techniqueRank ?? 0
|
||||
const post = POST_NAME[c.post ?? ''] ?? ''
|
||||
return (
|
||||
<div
|
||||
className={`gene-node ${c.alive ? '' : 'gene-dead'} ${isHead ? 'gene-head' : ''}`}
|
||||
onClick={() => setSelected(c.id)}
|
||||
title={`${c.name} · ${describeRealm(c.realm)} · ${w.ageOf(c)}岁`}
|
||||
>
|
||||
<div className="gene-name">
|
||||
{c.name}
|
||||
{isHead && <span className="tag head-t" style={{ marginLeft: 4 }}>主</span>}
|
||||
{post && <span className="tag" style={{ marginLeft: 4 }}>{post}</span>}
|
||||
{!c.alive && <span className="tag" style={{ marginLeft: 4 }}>殁</span>}
|
||||
</div>
|
||||
<div className="gene-sub">
|
||||
{c.alive ? `${describeRealm(c.realm)} · ${w.ageOf(c)}岁` : `${c.deathCause ?? '寿终'}${c.deathYear ? `(${c.deathYear}年)` : ''}`}
|
||||
{rank > 0 && <span className="tag gold-t" style={{ marginLeft: 4 }}>悟{rank}</span>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const POST_NAME: Record<string, string> = {
|
||||
head: '家主',
|
||||
elder: '长老',
|
||||
guardian: '供奉',
|
||||
steward: '执事',
|
||||
master: '掌教'
|
||||
}
|
||||
|
||||
export type { FamilyState }
|
||||
@@ -13,6 +13,8 @@ export default function SettingsPanel() {
|
||||
const snapshots = useGameStore((s) => s.snapshots)
|
||||
const go = useGameStore((s) => s.go)
|
||||
const setSpeed = useGameStore((s) => s.setSpeed)
|
||||
const soundOn = useGameStore((s) => s.soundOn)
|
||||
const toggleSound = useGameStore((s) => s.toggleSound)
|
||||
if (!world) return null
|
||||
|
||||
return (
|
||||
@@ -63,6 +65,12 @@ export default function SettingsPanel() {
|
||||
<span>刷新存档槽</span>
|
||||
<button className="btn btn-sm" onClick={() => void refreshSlots()}>刷新</button>
|
||||
</div>
|
||||
<div className="set-row">
|
||||
<span>音效(合成音:鼓点·钟鸣·纸韵)</span>
|
||||
<button className="btn btn-sm" onClick={toggleSound}>
|
||||
{soundOn ? '开(点击关)' : '关(点击开)'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="set-row">
|
||||
<span>暂停时间</span>
|
||||
<button className="btn btn-sm" onClick={() => setSpeed(0)}>停止流转</button>
|
||||
|
||||
@@ -104,6 +104,26 @@ export default function TerritoryPanel() {
|
||||
)
|
||||
}
|
||||
|
||||
function MonumentList() {
|
||||
const world = useGameStore((s) => s.world)
|
||||
const revision = useGameStore((s) => s.revision)
|
||||
void revision
|
||||
if (!world) return null
|
||||
const monuments = world.state.chronicle.filter((e) => e.text.startsWith('功德碑')).slice().reverse()
|
||||
if (monuments.length === 0) return null
|
||||
return (
|
||||
<div className="monument-strip">
|
||||
<div className="card-title" style={{ fontSize: '0.98rem' }}>宗祠碑录</div>
|
||||
{monuments.slice(0, 4).map((m) => (
|
||||
<div key={m.id} className="dim" style={{ lineHeight: 1.7 }}>
|
||||
<span className="ch-year" style={{ marginRight: 8 }}>{m.year}年</span>
|
||||
{m.text.slice(3)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function itemLabel(k: string): string {
|
||||
const map: Record<string, string> = {
|
||||
stone: '灵石',
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useGameStore } from '../store'
|
||||
import { PanelId } from '../store'
|
||||
import { RES_INFO } from '../components'
|
||||
import FamilyPanel from '../panels/FamilyPanel'
|
||||
import GenealogyPanel from '../panels/GenealogyPanel'
|
||||
import TerritoryPanel from '../panels/TerritoryPanel'
|
||||
import MarketPanel from '../panels/MarketPanel'
|
||||
import DiplomacyPanel from '../panels/DiplomacyPanel'
|
||||
@@ -12,6 +13,7 @@ import { LogFeed } from '../components/LogFeed'
|
||||
|
||||
const TABS: { id: PanelId; label: string }[] = [
|
||||
{ id: 'family', label: '宗族' },
|
||||
{ id: 'genealogy', label: '谱系' },
|
||||
{ id: 'territory', label: '领地' },
|
||||
{ id: 'market', label: '坊市' },
|
||||
{ id: 'diplomacy', label: '外交' },
|
||||
@@ -78,6 +80,7 @@ export default function GameScreen() {
|
||||
<div className="main">
|
||||
<div className="main-left">
|
||||
{panel === 'family' && <FamilyPanel />}
|
||||
{panel === 'genealogy' && <GenealogyPanel />}
|
||||
{panel === 'territory' && <TerritoryPanel />}
|
||||
{panel === 'market' && <MarketPanel />}
|
||||
{panel === 'diplomacy' && <DiplomacyPanel />}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
// 合成音效:鼓点·钟鸣·纸韵(Web Audio,零素材依赖)
|
||||
let ctx: AudioContext | null = null
|
||||
let enabled = true
|
||||
|
||||
export function setSoundEnabled(on: boolean): void {
|
||||
enabled = on
|
||||
}
|
||||
|
||||
export function isSoundEnabled(): boolean {
|
||||
return enabled
|
||||
}
|
||||
|
||||
function ac(): AudioContext | null {
|
||||
if (!enabled) return null
|
||||
try {
|
||||
if (!ctx) {
|
||||
const AC = window.AudioContext ?? (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext
|
||||
ctx = new AC()
|
||||
}
|
||||
if (ctx.state === 'suspended') void ctx.resume()
|
||||
return ctx
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function tone(freq: number, dur: number, gain: number, type: OscillatorType = 'sine', when = 0, slideTo?: number): void {
|
||||
const a = ac()
|
||||
if (!a) return
|
||||
const t0 = a.currentTime + when
|
||||
const o = a.createOscillator()
|
||||
const g = a.createGain()
|
||||
o.type = type
|
||||
o.frequency.setValueAtTime(freq, t0)
|
||||
if (slideTo) o.frequency.exponentialRampToValueAtTime(slideTo, t0 + dur)
|
||||
g.gain.setValueAtTime(0, t0)
|
||||
g.gain.linearRampToValueAtTime(gain, t0 + 0.012)
|
||||
g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur)
|
||||
o.connect(g)
|
||||
g.connect(a.destination)
|
||||
o.start(t0)
|
||||
o.stop(t0 + dur + 0.05)
|
||||
}
|
||||
|
||||
function noise(dur: number, gain: number, when = 0, freq = 3200): void {
|
||||
const a = ac()
|
||||
if (!a) return
|
||||
const t0 = a.currentTime + when
|
||||
const len = Math.floor(a.sampleRate * dur)
|
||||
const buf = a.createBuffer(1, len, a.sampleRate)
|
||||
const data = buf.getChannelData(0)
|
||||
for (let i = 0; i < len; i++) data[i] = (Math.random() * 2 - 1) * (1 - i / len)
|
||||
const src = a.createBufferSource()
|
||||
src.buffer = buf
|
||||
const f = a.createBiquadFilter()
|
||||
f.type = 'highpass'
|
||||
f.frequency.value = freq
|
||||
const g = a.createGain()
|
||||
g.gain.setValueAtTime(gain, t0)
|
||||
g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur)
|
||||
src.connect(f)
|
||||
f.connect(g)
|
||||
g.connect(a.destination)
|
||||
src.start(t0)
|
||||
}
|
||||
|
||||
/** 月度推进:极轻的硖墨声 */
|
||||
export function sTick(): void {
|
||||
noise(0.05, 0.015, 0, 5000)
|
||||
}
|
||||
|
||||
/** 开卷 / 纸笺 */
|
||||
export function sPaper(): void {
|
||||
noise(0.12, 0.03, 0, 2400)
|
||||
tone(620, 0.1, 0.015, 'sine', 0.02)
|
||||
}
|
||||
|
||||
/** 好消息:铜钟一声 */
|
||||
export function sGood(): void {
|
||||
tone(660, 0.5, 0.06, 'sine')
|
||||
tone(990, 0.42, 0.03, 'sine', 0.05)
|
||||
tone(1320, 0.3, 0.018, 'sine', 0.09)
|
||||
}
|
||||
|
||||
/** 坏消息:折磬低响 */
|
||||
export function sBad(): void {
|
||||
tone(196, 0.4, 0.05, 'triangle', 0, 130)
|
||||
tone(131, 0.5, 0.04, 'triangle', 0.04, 82)
|
||||
}
|
||||
|
||||
/** 战报:鼓点急雨 */
|
||||
export function sWar(): void {
|
||||
tone(120, 0.16, 0.07, 'sine', 0, 60)
|
||||
tone(120, 0.16, 0.06, 'sine', 0.16, 60)
|
||||
tone(160, 0.3, 0.06, 'sine', 0.32, 80)
|
||||
noise(0.28, 0.02, 0.36, 1500)
|
||||
}
|
||||
|
||||
/** 钟鸣:大事件(碑立/飞升) */
|
||||
export function sBell(): void {
|
||||
tone(392, 1.4, 0.06, 'sine')
|
||||
tone(494, 1.2, 0.035, 'sine', 0.08)
|
||||
tone(587, 1.0, 0.028, 'sine', 0.16)
|
||||
}
|
||||
|
||||
/** 突破:磬缶清越 */
|
||||
export function sGong(): void {
|
||||
tone(440, 0.9, 0.05, 'sine')
|
||||
tone(660, 0.7, 0.03, 'sine', 0.06)
|
||||
}
|
||||
|
||||
/** 按钮:纸面轻叩 */
|
||||
export function sClick(): void {
|
||||
noise(0.045, 0.03, 0, 4200)
|
||||
}
|
||||
@@ -5,9 +5,10 @@ import { findEvent, applyEventChoice } from '../game/engine/systems/events'
|
||||
import { BattleLog, ChronicleEntry, GameState, LogItem, SaveMeta, YearlyReport, SnapshotMeta } from '../game/types/domain'
|
||||
import { getSlotManager, getSaveSlot } from '../game/storage/db'
|
||||
import { metaFromState, updateSlotMeta } from './storeHelper'
|
||||
import { setSoundEnabled, sPaper, sGood, sBad, sWar, sBell, sGong, sClick, sTick } from './sound'
|
||||
|
||||
export type Screen = 'boot' | 'newgame' | 'game'
|
||||
export type PanelId = 'family' | 'territory' | 'market' | 'diplomacy' | 'expedition' | 'chronicle' | 'settings'
|
||||
export type PanelId = 'family' | 'genealogy' | 'territory' | 'market' | 'diplomacy' | 'expedition' | 'chronicle' | 'settings'
|
||||
|
||||
export interface GameStore {
|
||||
screen: Screen
|
||||
@@ -28,6 +29,7 @@ export interface GameStore {
|
||||
gameOverReason?: string
|
||||
paperReport?: YearlyReport
|
||||
snapshots: SnapshotMeta[]
|
||||
soundOn: boolean
|
||||
|
||||
init: () => Promise<void>
|
||||
go: (screen: Screen) => void
|
||||
@@ -54,6 +56,7 @@ export interface GameStore {
|
||||
refreshSnapshots: () => Promise<void>
|
||||
onYearPaper: (report: YearlyReport) => void
|
||||
closePaper: () => void
|
||||
toggleSound: () => void
|
||||
}
|
||||
|
||||
const LOG_CAP = 260
|
||||
@@ -78,6 +81,7 @@ export const useGameStore = create<GameStore>((set, get) => ({
|
||||
gameOverReason: undefined,
|
||||
paperReport: undefined,
|
||||
snapshots: [],
|
||||
soundOn: true,
|
||||
|
||||
init: async () => {
|
||||
const manager = getSlotManager()
|
||||
@@ -145,6 +149,7 @@ export const useGameStore = create<GameStore>((set, get) => ({
|
||||
try {
|
||||
w.advanceMonth()
|
||||
w.syncRng()
|
||||
sTick()
|
||||
set((s) => ({ revision: s.revision + 1 }))
|
||||
await Stash.save(st, '自动')
|
||||
} catch (e) {
|
||||
@@ -176,12 +181,19 @@ export const useGameStore = create<GameStore>((set, get) => ({
|
||||
|
||||
onYearPaper: (report) => {
|
||||
if (report.year >= 2) {
|
||||
sPaper()
|
||||
set({ paperReport: report, speed: 0 })
|
||||
}
|
||||
},
|
||||
|
||||
closePaper: () => set({ paperReport: undefined }),
|
||||
|
||||
toggleSound: () => {
|
||||
const on = !get().soundOn
|
||||
set({ soundOn: on })
|
||||
setSoundEnabled(on)
|
||||
},
|
||||
|
||||
restoreSnapshot: async (snapshotId: string) => {
|
||||
const st = get()
|
||||
const file = await getSaveSlot(st.slot)
|
||||
@@ -299,6 +311,10 @@ function openState(state: GameState, slot: number): void {
|
||||
function makeBus(st: GameStore): WorldEventBus {
|
||||
return {
|
||||
onLog: (kind, text) => {
|
||||
if (kind === 'good') sGood()
|
||||
else if (kind === 'bad') sBad()
|
||||
else if (kind === 'war') sWar()
|
||||
else if (kind === 'chronicle') sBell()
|
||||
const w = st.world
|
||||
st.addLog({ id: logSeq++, kind, text, year: w?.state.year ?? 0, month: w?.state.month ?? 0 })
|
||||
},
|
||||
|
||||
@@ -1229,6 +1229,105 @@ body {
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
/* ---------- 谱系 ---------- */
|
||||
.genealogy-row {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
margin-bottom: 14px;
|
||||
background: linear-gradient(180deg, rgba(242, 233, 212, 0.035), rgba(242, 233, 212, 0.015));
|
||||
border: 1px solid rgba(140, 120, 80, 0.22);
|
||||
border-radius: 3px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
.genealogy-gen {
|
||||
writing-mode: vertical-rl;
|
||||
color: #cdaa63;
|
||||
letter-spacing: 6px;
|
||||
font-size: 0.95rem;
|
||||
border-right: 1px solid rgba(140, 120, 80, 0.25);
|
||||
padding-right: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.genealogy-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.genealogy-unit {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
border-left: 3px solid rgba(150, 128, 88, 0.35);
|
||||
padding-left: 10px;
|
||||
}
|
||||
.genealogy-parents {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.genealogy-matron {
|
||||
color: #a09374;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.genealogy-drop {
|
||||
height: 12px;
|
||||
border-left: 2px solid rgba(150, 128, 88, 0.4);
|
||||
margin-left: 30px;
|
||||
}
|
||||
.genealogy-children,
|
||||
.genealogy-singles {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.gene-node {
|
||||
background: linear-gradient(180deg, #f4ecd9, #e7d6b4);
|
||||
color: #2e2415;
|
||||
border: 1px solid #b3a075;
|
||||
border-radius: 3px;
|
||||
padding: 5px 10px;
|
||||
min-width: 108px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 252, 240, 0.6);
|
||||
transition: all 0.14s;
|
||||
}
|
||||
.gene-node:hover {
|
||||
border-color: var(--gold);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.gene-dead {
|
||||
filter: saturate(0.4);
|
||||
opacity: 0.55;
|
||||
background: linear-gradient(180deg, #d9cfb8, #c8bb9e);
|
||||
}
|
||||
.gene-head {
|
||||
border-color: var(--vermilion);
|
||||
box-shadow: 0 0 0 1px rgba(169, 59, 46, 0.25), 0 2px 6px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.gene-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.94rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.gene-sub {
|
||||
font-size: 0.75rem;
|
||||
color: #6d5b3e;
|
||||
margin-top: 1px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.gene-node .tag {
|
||||
font-size: 0.62rem;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.genealogy-gen + .genealogy-body:has(.genealogy-singles:empty) .genealogy-singles {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ---------- 媒人提示 / 年度族簿 ---------- */
|
||||
.matchmaker-strip {
|
||||
background: linear-gradient(180deg, rgba(240, 214, 160, 0.12), rgba(240, 214, 160, 0.05));
|
||||
@@ -1307,3 +1406,15 @@ body {
|
||||
margin-bottom: 16px;
|
||||
text-shadow: 0 4px 18px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
/* ---------- 宗祠碑录 ---------- */
|
||||
.monument-strip {
|
||||
margin-bottom: 14px;
|
||||
background: rgba(20, 15, 9, 0.72);
|
||||
border: 1px solid #33291a;
|
||||
border-radius: 3px;
|
||||
padding: 10px 16px;
|
||||
}
|
||||
.monument-strip .card-title {
|
||||
color: #cdaa63;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user