Compare commits

..

5 Commits

Author SHA1 Message Date
hypercross 028074e4f1 fix: columns 2026-03-23 22:22:01 +08:00
hypercross 6f8be557b4 fix: jest 2026-03-23 20:24:25 +08:00
hypercross 30ddcfc32d refactor: layout 2026-03-23 19:01:42 +08:00
hypercross f658fd3380 refactor: convert xdx tables by default 2026-03-23 18:57:55 +08:00
hypercross 8f6a6b96e2 feat: mothership character store 2026-03-23 01:13:19 +08:00
9 changed files with 567 additions and 27 deletions

View File

@ -29,22 +29,24 @@ const SidebarContent: Component<SidebarContentProps> = (props) => {
});
return (
<div class="p-4">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-bold text-gray-900"></h2>
<Show when={!props.isDesktop}>
<button
onClick={props.onClose}
class="text-gray-500 hover:text-gray-700"
title="关闭"
>
</button>
</Show>
<div class="flex flex-col h-full">
<div class="p-4 border-b">
<div class="flex items-center justify-between">
<h2 class="text-lg font-bold text-gray-900"></h2>
<Show when={!props.isDesktop}>
<button
onClick={props.onClose}
class="text-gray-500 hover:text-gray-700"
title="关闭"
>
</button>
</Show>
</div>
</div>
{/* 文件树 */}
<div class="mb-4">
{/* 文件树滚动区域 */}
<div class="flex-1 overflow-y-auto p-4 min-h-0">
<h3 class="text-xs font-semibold text-gray-500 uppercase mb-2 px-2">
</h3>
@ -59,9 +61,9 @@ const SidebarContent: Component<SidebarContentProps> = (props) => {
))}
</div>
{/* 当前文件标题 */}
{/* 当前文件标题滚动区域 */}
<Show when={currentFileHeadings().length > 0}>
<div class="xl:fixed xl:top-20 xl:right-4 xl:bottom-4 xl:overflow-y-auto xl:max-w-30 2xl:max-w-60">
<div class="flex-1 border-t overflow-y-auto p-4 min-h-0">
<h3 class="text-xs font-semibold text-gray-500 uppercase mb-2 px-2">
</h3>
@ -104,7 +106,7 @@ export const MobileSidebar: Component<SidebarProps> = (props) => {
/>
{/* 侧边栏 */}
<aside
class={`fixed top-0 left-0 h-full w-64 bg-white shadow-lg z-50 overflow-y-auto transform transition-transform duration-300 ease-in-out md:hidden ${
class={`fixed top-0 left-0 h-full w-64 bg-white shadow-lg z-50 transform transition-transform duration-300 ease-in-out md:hidden ${
props.isOpen ? "translate-x-0" : "-translate-x-full"
}`}
>
@ -135,7 +137,7 @@ export const DesktopSidebar: Component<{}> = () => {
});
return (
<aside class="hidden md:block fixed top-0 left-0 h-full w-64 bg-white shadow-lg z-30 overflow-y-auto pt-16">
<aside class="hidden md:block fixed top-0 left-0 h-full w-64 bg-white shadow-lg z-30 overflow-hidden pt-16">
<SidebarContent
fileTree={fileTree()}
pathHeadings={pathHeadings()}

View File

@ -0,0 +1,334 @@
import { createStore } from "solid-js/store";
import type {
CharacterStats,
CharacterSaves,
InventoryItem,
MothershipCharacter,
MothershipStoreState,
VitalValue,
StressValue,
} from "./types";
/**
*
*/
export function createDefaultCharacter(): MothershipCharacter {
return {
stats: {
strength: 50,
agility: 50,
combat: 50,
intellect: 50,
},
saves: {
fear: 50,
sanity: 50,
body: 50,
},
skills: [],
inventory: [],
status: [],
hp: { current: 0, max: 0 },
stress: { current: 0, min: 0 },
wounds: { current: 0, max: 0 },
};
}
/**
* 0-99
*/
function clampStat(value: number): number {
return Math.max(0, Math.min(99, value));
}
/**
*
*/
function clampValue(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value));
}
const [store, setStore] = createStore<MothershipStoreState>({
character: createDefaultCharacter(),
});
/**
*
*/
export function setStat<K extends keyof CharacterStats>(
key: K,
value: number
): void {
setStore("character", "stats", key, clampStat(value));
}
/**
*
*/
export function setStats(stats: Partial<CharacterStats>): void {
setStore("character", "stats", (prev) => ({
...prev,
...Object.fromEntries(
Object.entries(stats).map(([key, value]) => [key, clampStat(value)])
),
}));
}
/**
*
*/
export function setSave<K extends keyof CharacterSaves>(
key: K,
value: number
): void {
setStore("character", "saves", key, clampStat(value));
}
/**
*
*/
export function setSaves(saves: Partial<CharacterSaves>): void {
setStore("character", "saves", (prev) => ({
...prev,
...Object.fromEntries(
Object.entries(saves).map(([key, value]) => [key, clampStat(value)])
),
}));
}
/**
*
*/
export function addSkill(skill: string): void {
setStore("character", "skills", (prev) => [...prev, skill]);
}
/**
*
*/
export function removeSkill(skill: string): void {
setStore("character", "skills", (prev) =>
prev.filter((s) => s !== skill)
);
}
/**
*
*/
export function setSkills(skills: string[]): void {
setStore("character", "skills", [...skills]);
}
/**
*
*/
export function addInventoryItem(
name: string,
quantity: number = 1,
attributes?: Record<string, any>
): void {
setStore("character", "inventory", (prev) => [
...prev,
{ name, quantity: Math.max(1, quantity), attributes },
]);
}
/**
*
*/
export function removeInventoryItem(name: string): void {
setStore("character", "inventory", (prev) =>
prev.filter((item) => item.name !== name)
);
}
/**
*
*/
export function updateInventoryItemQuantity(
name: string,
quantity: number
): void {
setStore("character", "inventory", (prev) =>
prev.map((item) =>
item.name === name
? { ...item, quantity: Math.max(0, quantity) }
: item
).filter((item) => item.quantity > 0)
);
}
/**
*
*/
export function updateInventoryItemAttributes(
name: string,
attributes: Record<string, any>
): void {
setStore("character", "inventory", (prev) =>
prev.map((item) =>
item.name === name
? { ...item, attributes }
: item
)
);
}
/**
*
*/
export function addStatus(
name: string,
quantity: number = 1,
attributes?: Record<string, any>
): void {
setStore("character", "status", (prev) => [
...prev,
{ name, quantity: Math.max(1, quantity), attributes },
]);
}
/**
*
*/
export function removeStatus(name: string): void {
setStore("character", "status", (prev) =>
prev.filter((item) => item.name !== name)
);
}
/**
* HP
*/
export function setHP(value: Partial<VitalValue>): void {
setStore("character", "hp", (prev) => ({
...prev,
...value,
current: value.current !== undefined
? clampValue(value.current, 0, value.max ?? prev.max)
: prev.current,
max: value.max !== undefined
? Math.max(0, value.max)
: prev.max,
}));
}
/**
*
*/
export function takeDamage(amount: number): void {
setStore("character", "hp", (prev) => ({
...prev,
current: Math.max(0, prev.current - amount),
}));
}
/**
* HP
*/
export function healHP(amount: number): void {
setStore("character", "hp", (prev) => ({
...prev,
current: Math.min(prev.max, prev.current + amount),
}));
}
/**
*
*/
export function setStress(value: Partial<StressValue>): void {
setStore("character", "stress", (prev) => ({
...prev,
...value,
current: value.current !== undefined
? clampValue(value.current, value.min ?? prev.min, Number.MAX_SAFE_INTEGER)
: prev.current,
min: value.min !== undefined ? Math.max(0, value.min) : prev.min,
}));
}
/**
*
*/
export function addStress(amount: number): void {
setStore("character", "stress", (prev) => ({
...prev,
current: prev.current + amount,
}));
}
/**
*
*/
export function reduceStress(amount: number): void {
setStore("character", "stress", (prev) => ({
...prev,
current: Math.max(prev.min, prev.current - amount),
}));
}
/**
*
*/
export function setWounds(value: Partial<VitalValue>): void {
setStore("character", "wounds", (prev) => ({
...prev,
...value,
current: value.current !== undefined
? clampValue(value.current, 0, value.max ?? prev.max)
: prev.current,
max: value.max !== undefined
? Math.max(0, value.max)
: prev.max,
}));
}
/**
*
*/
export function addWound(amount: number = 1): void {
setStore("character", "wounds", (prev) => ({
...prev,
current: Math.min(prev.max, prev.current + amount),
}));
}
/**
*
*/
export function healWound(amount: number = 1): void {
setStore("character", "wounds", (prev) => ({
...prev,
current: Math.max(0, prev.current - amount),
}));
}
/**
*
*/
export function resetCharacter(): void {
setStore("character", createDefaultCharacter());
}
/**
*
*/
export function setCharacter(character: MothershipCharacter): void {
setStore("character", character);
}
/**
*
*/
export function getCharacter(): MothershipCharacter {
return store.character;
}
/**
* Store SolidJS
*/
export function useCharacterStore() {
return store;
}
export { store, setStore };

View File

@ -0,0 +1,66 @@
/**
* Mothership TRPG Store
*
* @module journals/mothership
*/
export type {
CharacterStats,
CharacterSaves,
InventoryItem,
VitalValue,
StressValue,
MothershipCharacter,
MothershipStoreState,
} from "./types";
export {
// Store 核心
store,
setStore,
useCharacterStore,
getCharacter,
// 初始化
createDefaultCharacter,
resetCharacter,
setCharacter,
// Stats 操作
setStat,
setStats,
// Saves 操作
setSave,
setSaves,
// Skills 操作
addSkill,
removeSkill,
setSkills,
// Inventory 操作
addInventoryItem,
removeInventoryItem,
updateInventoryItemQuantity,
updateInventoryItemAttributes,
// Status 操作
addStatus,
removeStatus,
// HP 操作
setHP,
takeDamage,
healHP,
// Stress 操作
setStress,
addStress,
reduceStress,
// Wounds 操作
setWounds,
addWound,
healWound,
} from "./characterStore";

View File

@ -0,0 +1,76 @@
/**
* Mothership TRPG
* 0-99
*/
export interface CharacterStats {
/** 力量 */
strength: number;
/** 敏捷 */
agility: number;
/** 战斗 */
combat: number;
/** 智力 */
intellect: number;
}
/**
* Mothership TRPG
* 0-99
*/
export interface CharacterSaves {
/** 恐惧豁免 */
fear: number;
/** 理智豁免 */
sanity: number;
/** 体质豁免 */
body: number;
}
/**
*
*/
export interface InventoryItem {
/** 物品名称 */
name: string;
/** 数量 */
quantity: number;
/** 自定义属性,如护甲值 { ap: 3 } */
attributes?: Record<string, any>;
}
/**
* /
*/
export interface VitalValue {
current: number;
max: number;
}
/**
*
*/
export interface StressValue {
current: number;
min: number;
}
/**
* Mothership
*/
export interface MothershipCharacter {
stats: CharacterStats;
saves: CharacterSaves;
skills: string[];
inventory: InventoryItem[];
status: InventoryItem[];
hp: VitalValue;
stress: StressValue;
wounds: VitalValue;
}
/**
* Store
*/
export type MothershipStoreState = {
character: MothershipCharacter;
};

64
src/markdown/columns.ts Normal file
View File

@ -0,0 +1,64 @@
import {TokenizerAndRendererExtension} from "marked";
export default function markedColumns(): TokenizerAndRendererExtension[] {
return [{
name: 'col-divider',
level: 'block',
start(src: string) {
return src.match(/^-\|-+ *\n/)?.index;
},
tokenizer(src){
const match = src.match(/^-\|(-+) *\n/);
if(!match) return;
return {
type: 'col-divider',
raw: match[0],
tokens: []
};
},
renderer(token){
const extra = token.raw.match(/^-\|(-+) *\n/)?.[1].length || 0;
const sfx = extra > 1 ? '-' + (extra) : '';
return `</div><div class="col${sfx}">`;
}
},{
name: 'col-start',
level: 'block',
start(src: string) {
return src.match(/^\|--+ *\n/)?.index;
},
tokenizer(src){
const match = src.match(/^\|-(-+) *\n/);
if(!match) return;
return {
type: 'col-start',
raw: match[0],
tokens: []
};
},
renderer(token){
const extra = token.raw.match(/^\|-(-+) *\n/)?.[1].length || 0;
const sfx = extra > 1 ? '-' + (extra) : '';
return `<div class="cols"><div class="col${sfx}">`;
}
},{
name: 'col-end',
level: 'block',
start(src: string) {
return src.match(/^--\| *\n/)?.index;
},
tokenizer(src){
const match = src.match(/^--\| *\n/);
if(!match) return;
return {
type: 'col-end',
raw: match[0],
tokens: []
};
},
renderer(token){
return `</div></div>`;
}
}
];
}

View File

@ -5,6 +5,7 @@ import markedAlert from "marked-alert";
import markedMermaid from "./mermaid";
import markedTable from "./table";
import {gfmHeadingId} from "marked-gfm-heading-id";
import markedColumns from "./columns";
let globalIconPrefix: string | undefined = undefined;
function overrideIconPrefix(path?: string){
@ -45,8 +46,10 @@ const marked = new Marked()
}
},
]), {
extensions: [
...markedColumns(),
{
// 自定义代码块渲染器,支持 yaml/tag 格式
extensions: [{
name: 'code-block-yaml-tag',
level: 'block',
start(src: string) {

View File

@ -29,14 +29,14 @@ export default function markedTable(): MarkedExtension {
const header = token.header;
let roll = '';
const labelIndex = header.findIndex(cell => {
if(cell.text === 'md-roll-label'){
if(cell.text === 'md-roll-label' || cell.text.match(/(\d+)?d\d+/)){
roll = ' roll=true';
return true;
}else if(cell.text === 'md-remix-label'){
roll = ' roll=true remix=true';
return true;
}
return cell.text === 'md-table-label';
return cell.text === 'md-table-label' || cell.text === 'label';
});
// 默认表格渲染 - 使用 marked 默认行为

View File

@ -1,10 +1,5 @@
import { getRoundedPolygonPoints, getTangentCircleCenter, getProjectedPoint } from './rounded';
/* eslint-disable @typescript-eslint/no-explicit-any */
declare const describe: any;
declare const test: any;
declare const expect: any;
describe('getProjectedPoint', () => {
test('should project point onto line segment', () => {
// 点 (2, 2) 投影到线段 (0, 0) -> (4, 0)

View File

@ -15,7 +15,7 @@
"outDir": "./dist",
"rootDir": "./src",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["node"]
"types": ["node", "jest"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]