Благодарим за выбор нашего сервиса!
Тестовое сообщение
Сообщений 1 страница 9 из 9
Поделиться22026-09-02 03:24:15
This is a quote
Content of collapsible section
Поделиться32026-09-07 03:12:17
[html]
<article class="lore-article chronicle-page" id="chronicle-app">
<span class="lore-badge">Общая хронология</span>
<h1>Хроники Материка</h1>
<p class="lore-tagline">От исхода пришлых до нынешнего безмолвного противостояния</p>
<p>Единого летоисчисления материк не знал до основания престола — годы до него провинции считали каждая по-своему, и здесь они даны условно, «от основания» (И.Э. — Имперская эра). Эта хроника сводит две вещи, которые обычно рассказывают порознь: то, что происходило со всем материком разом, и то, что происходило в землях, которые сегодня зовутся Латериумом, Альсаром и Тянься.</p>
<p>Выберите эпоху ниже — каждая карточка открывает свою хронологию. Фильтр под эпохами общий для всех них: он остаётся в силе, даже если вы переключаетесь между эпохами.</p>
<div class="chronicle-filter">
<span class="chronicle-filter-label">Показать:</span>
<div class="chronicle-filter-chips" id="chronicle-filter-chips" role="group" aria-label="Фильтр по стране, Империи в целом или общим событиям"></div>
</div>
<!-- инлайновый скрипт ниже вставляет сюда, внутрь #chronicle-app: N
радио.chronicle-era-input, nav.lore-tabs.chronicle-eras и
div.lore-panels.chronicle-panels — построено из JSON, а не руками,
чтобы контент и разметка не расходились -->
</article>
<script>
// Данные хроники — отдельный JSON-файл (articles/timeline-data.json,
// залитый на forumstatic.ru тем же способом, что и другие внешние ассеты
// форума), а не разметка в этом файле — так фильтр, карточки эпох и сам
// таймлайн строятся из одного источника и правятся в одном месте.
const CHRONICLE_DATA_URL = 'https://forumstatic.ru/files/001c/c7/56/26482.json';
let chronicleData = null;
const scopeLabel = (scopeId) =>
chronicleData.scopes.find((scope) => scope.id === scopeId)?.label ?? scopeId;
const renderChip = ({ id, label }) => `
<input class="chronicle-filter-input" type="checkbox" id="filter-${id}" data-scope="${id}">
<label class="chronicle-chip" data-scope="${id}" for="filter-${id}">${label}</label>
`;
// мета-строка теперь год+заголовок (год несёт акцентный цвет вместе с
// меткой на спине), а страна/Империя — отдельная безрамочная подпись
// ниже, а не значок-пилюля — меньше повторов одного акцента на карточку
const renderEvent = ({ year, scope, title, text }) => `
<li class="chronicle-item" data-scope="${scope}">
<div class="chronicle-item-meta">
<span class="chronicle-year">${year}</span>
<h4>${title}</h4>
</div>
<span class="chronicle-scope-tag">${scopeLabel(scope)}</span>
<p>${text}</p>
</li>
`;
const renderEraInput = ({ id }, index) => `
<input class="lore-tab-input chronicle-era-input" type="radio" name="chronicle-era" id="era-${id}" ${index === 0 ? 'checked' : ''}>
`;
const renderEraCard = ({ id, name, range, oneLiner }) => `
<label class="lore-tab chronicle-era-card" for="era-${id}">
<span class="chronicle-era-name">${name}</span>
<span class="chronicle-era-range">${range}</span>
<span class="chronicle-era-oneliner">${oneLiner}</span>
</label>
`;
const renderEraPanel = ({ id, range, intro, events }) => `
<section class="lore-panel chronicle-detail" data-panel="${id}">
<p class="chronicle-era-range">${range}</p>
<p>${intro}</p>
<ul class="chronicle-timeline">${events.map(renderEvent).join('')}</ul>
<p class="chronicle-empty" hidden>Ни одно событие этой эпохи не подходит под выбранный фильтр.</p>
</section>
`;
const renderEras = () => {
const { periods } = chronicleData;
const app = document.getElementById('chronicle-app');
app.insertAdjacentHTML('beforeend', `
${periods.map(renderEraInput).join('')}
<nav class="lore-tabs chronicle-eras" aria-label="Эпохи материка">${periods.map(renderEraCard).join('')}</nav>
<div class="lore-panels chronicle-panels">${periods.map(renderEraPanel).join('')}</div>
`);
};
const renderFilterChips = () => {
const chips = document.getElementById('chronicle-filter-chips');
chips.insertAdjacentHTML('beforeend', `
<input class="chronicle-filter-input" type="checkbox" id="filter-all" checked>
<label class="chronicle-chip" for="filter-all">Всё</label>
${chronicleData.scopes.map(renderChip).join('')}
`);
};
const scopeCheckboxes = () => Array.from(document.querySelectorAll('.chronicle-filter-input[data-scope]'));
// пустой список = фильтр не сужает ничего, показываем все события
const activeScopes = () => {
const checked = scopeCheckboxes().filter((input) => input.checked).map((input) => input.dataset.scope);
return checked.length ? checked : null;
};
const applyFilter = () => {
const active = activeScopes();
document.querySelectorAll('.chronicle-detail').forEach((panel) => {
let visibleCount = 0;
panel.querySelectorAll('.chronicle-item').forEach((item) => {
const show = !active || active.includes(item.dataset.scope);
item.hidden = !show;
if (show) visibleCount += 1;
});
panel.querySelector('.chronicle-empty').hidden = visibleCount > 0;
});
};
const onFilterChange = (event) => {
const target = event.target;
if (!target.matches('.chronicle-filter-input')) return;
const allCheckbox = document.getElementById('filter-all');
if (target.id === 'filter-all') {
if (target.checked) scopeCheckboxes().forEach((input) => { input.checked = false; });
} else {
if (target.checked) allCheckbox.checked = false;
if (!scopeCheckboxes().some((input) => input.checked)) allCheckbox.checked = true;
}
applyFilter();
};
const initChronicle = () => {
renderEras();
renderFilterChips();
document.getElementById('chronicle-filter-chips').addEventListener('change', onFilterChange);
};
fetch(CHRONICLE_DATA_URL)
.then((response) => response.json())
.then((data) => {
chronicleData = data;
initChronicle();
});
</script>
[/html][hideprofile]
Поделиться42026-09-08 03:46:02
[html]
<article class="lore-article roster-page" id="roster-app">
<span class="lore-badge">Двор и знать Империи</span>
<h1>Кто есть кто в Империи</h1>
<p class="lore-tagline">Наместники, их дворы и великие дома — по территориям и землям внутри них</p>
<p class="roster-intro">Список организован по территориям Империи (крупным наделам под властью Наместника или, как в случае Латериума, центральных сановников). Разверните территорию, чтобы увидеть её двор, затем выберите землю внутри неё — там перечислены дома, держащие эту землю, и их люди.</p>
<div id="roster-territories"><!-- сюда скрипт вставляет .roster-territory по одному на territories[] --></div>
</article>
<script>
// Данные ростера — отдельный JSON (см. комментарий вверху файла про заливку на форум)
const CHARACTERS_DATA_URL = 'https://forumstatic.ru/files/001c/c7/56/54183.json';
// русское склонение количества — форум читает по-русски, дефолтный "N штук" не подходит
const pluralize = (n, one, few, many) => {
const mod10 = n % 10;
const mod100 = n % 100;
if (mod10 === 1 && mod100 !== 11) return one;
if ([2, 3, 4].includes(mod10) && ![12, 13, 14].includes(mod100)) return few;
return many;
};
const countTerritory = (territory) => {
const landFamilies = territory.subterritories.flatMap((sub) => [
...(sub.families || []),
...(sub.districts || []).flatMap((district) => district.families),
]);
const families = territory.rulingFamily ? [territory.rulingFamily, ...landFamilies] : landFamilies;
const members = families.flatMap((family) => family.members);
return {
lands: territory.subterritories.length,
families: families.length,
members: members.length,
};
};
// имя персонажа — просто текст, если у него нет ссылки (профиль на форуме
// есть не у всех), или ссылка на неё, если есть; и то и другое остаётся
// жирным через общий .strong снаружи, а не задаётся заново на <a>
const renderName = ({ name, url }) => (url ? `<a href="${url}">${name}</a>` : name);
// одна строка — одно имя: жирное имя, тире, роль (+ note отдельным
// предложением, если есть) — единственный способ показать персонажа на
// всей странице, будь то двор, местный правитель или член дома
const renderRow = (person) => `
<p class="roster-row">
<strong>${renderName(person)}</strong>${person.role ? ` — ${person.role}${person.note ? `. ${person.note}` : ''}` : ''}
</p>
`;
// одна и та же секция карточки для двух разных вещей — правящего дома (если
// он у территории есть отдельно от Наместника, как в Альсаре) и двора —
// так на странице только один визуальный приём для "ещё одной группы людей
// внутри карточки", а не два похожих
const renderHeroSection = (label, rows, note) => `
<div class="roster-hero-section">
<p class="roster-eyebrow roster-hero-section-label">${label}</p>
${note ? `<p class="roster-note">${note}</p>` : ''}
<div class="roster-hero-rows">${rows.map(renderRow).join('')}</div>
</div>
`;
// одна "голова" карточки — эмблема, эйброу-титул, опционально имя и
// заметка, опционально список персонажей: и Наместник, и правящий дом
// рендерятся через одно и то же, чтобы стоять рядом как равные, а не как
// главная фигура и подпись под ней
const renderHeroHead = ({ icon, eyebrow, name, nameUrl, note, rows }) => `
<div class="roster-hero-head">
<p class="roster-eyebrow roster-hero-title">${eyebrow}</p>
${name ? `<p class="roster-hero-lede"><strong>${renderName({ name, url: nameUrl })}</strong></p>` : ''}
${note ? `<p class="roster-note">${note}</p>` : ''}
${rows ? `<div class="roster-hero-rows roster-hero-rows--stack">${rows.map(renderRow).join('')}</div>` : ''}
</div>
`;
// без Наместника (как в Латериуме) голова остаётся без имени — просто
// титул и note, объясняющий отсутствие, вместо имени-плейсхолдера
const renderHero = ({ capitalMage, note, staff, rulingFamily }) => {
const overseerHead = renderHeroHead({
icon: 'fa-crown',
eyebrow: capitalMage ? capitalMage.title : 'Наместник',
name: capitalMage ? capitalMage.name : null,
nameUrl: capitalMage ? capitalMage.url : null,
note,
});
const rulingFamilyHead = rulingFamily ? renderHeroHead({
icon: 'fa-shield-halved',
eyebrow: rulingFamily.name,
note: rulingFamily.note,
rows: rulingFamily.members,
}) : '';
return `
<div class="roster-hero">
<div class="roster-hero-heads${rulingFamily ? ' roster-hero-heads--split' : ''}">
${overseerHead}${rulingFamilyHead}
</div>
${staff.length ? renderHeroSection('Двор', staff) : ''}
</div>
`;
};
const renderHouse = ({ name, members }) => `
<div class="roster-house">
<p class="roster-house-label">${name}</p>
${members.map(renderRow).join('')}
</div>
`;
// деление земли на более мелкие территории (уезды, кварталы и т.п.) —
// не у каждой земли оно есть, families прямо на самой земле остаются
// способом для домов, не привязанных ни к одному из них
const renderDistrict = ({ name, note, families }) => `
<div class="roster-district">
<p class="roster-district-header">${name}</p>
${note ? `<p class="roster-note">${note}</p>` : ''}
${families.map(renderHouse).join('')}
</div>
`;
const renderLandCard = ({ name, note, localRuler, districts = [], families = [] }) => `
<div class="roster-land-card">
<p class="roster-land-card-header">${name}</p>
${note ? `<p class="roster-note">${note}</p>` : ''}
${localRuler ? `
<div class="roster-land-ruler">
<p class="roster-eyebrow">${localRuler.title}</p>
<p class="roster-row"><strong>${renderName(localRuler)}</strong></p>
</div>
` : ''}
${districts.map(renderDistrict).join('')}
${families.map(renderHouse).join('')}
</div>
`;
const renderTerritory = (territory, index) => {
const counts = countTerritory(territory);
return `
<details class="roster-territory" ${index === 0 ? 'open' : ''}>
<summary class="roster-territory-summary">
<div class="roster-territory-heading">
<h2 class="roster-territory-name">${territory.name}</h2>
<span class="roster-territory-count">${counts.lands} ${pluralize(counts.lands, 'земля', 'земли', 'земель')} · ${counts.families} ${pluralize(counts.families, 'дом', 'дома', 'домов')} · ${counts.members} ${pluralize(counts.members, 'персонаж', 'персонажа', 'персонажей')}</span>
</div>
<span class="roster-territory-toggle"></span>
</summary>
<div class="roster-territory-body">
${renderHero(territory)}
<div class="roster-lands">${territory.subterritories.map(renderLandCard).join('')}</div>
</div>
</details>
`;
};
fetch(CHARACTERS_DATA_URL)
.then((response) => response.json())
.then((data) => {
document.getElementById('roster-territories').innerHTML = data.territories.map(renderTerritory).join('');
});
</script>
[/html][hideprofile]
Поделиться52026-09-10 03:10:40
Поделиться62026-09-10 03:12:14
BigDamnHero написал(а):
- шефчшуиечфиушечф
-wefjwefewf
Поделиться72026-09-10 04:06:01
[dice=1d20:Просто так!]
Поделиться82026-09-10 04:06:35
[dice=10d20+1:Прост]
Поделиться92026-09-10 04:18:38
[dice=1d20:Хммммм]
