Compare commits

..

6 Commits

Author SHA1 Message Date
uberwald 9617005a5c Finalisation du système
Release Creation / build (release) Successful in 1m24s
2026-05-06 22:37:40 +02:00
uberwald 73a3381d2a Import des persos du système précédent 2026-05-06 21:31:03 +02:00
uberwald fbfb265570 Sync compendiums 2026-05-06 20:27:40 +02:00
uberwald eda9b77f46 Avec initiative
Release Creation / build (release) Successful in 1m10s
2026-04-29 22:21:41 +02:00
uberwald 64ab54daf3 Avec initiative 2026-04-29 22:20:47 +02:00
uberwald 0e1594773b Actualiser README.md 2026-04-27 21:43:53 +02:00
138 changed files with 4083 additions and 4622 deletions
+11 -2
View File
@@ -1,3 +1,12 @@
# Les Chroniques de l'étrange pour FoundryVTT
# Chroniques de l'étrange — Système FoundryVTT
Système [Foundry VTT](https://foundryvtt.com) pour **Chroniques de l'Etrange**, le jeu de rôle d'[Antre Monde Éditions](https://antremonde.fr).
Copyright 2025-2026 Antre Monde Editions All rights reserved
Chroniques de l'ETrange is a game written by Romain d'Huissier and Cédric Lameire. The authors retain their moral rights to this work in both print and digital formats.
This system for FoundryVTT has been approved and authorized by Antre-Monde Edition
---
Implémentation du JDR Les Chroniques de l'Etrange de Antre-Monde éditions.
-330
View File
@@ -1,330 +0,0 @@
#!/usr/bin/env python3
"""Analyze all JSON files in packs-src/ for text quality issues."""
import json
import os
import re
import sys
from pathlib import Path
from html.parser import HTMLParser
BASE = Path("/home/morr/work/uberwald/fvtt-chroniques-de-l-etrange")
PACKS = BASE / "packs-src"
REGLES = BASE / "regles.txt"
# Load PDF text
pdf_lines = REGLES.read_text(encoding="utf-8").splitlines()
pdf_text = REGLES.read_text(encoding="utf-8")
issues = []
# ---------- helpers ----------
def strip_html(html):
"""Remove HTML tags and return plain text."""
return re.sub(r'<[^>]+>', '', html or '')
def check_unclosed_tags(html):
"""Returns list of unclosed/mismatched tags."""
open_tags = re.findall(r'<([a-zA-Z][a-zA-Z0-9]*)[^>]*>', html)
close_tags = re.findall(r'</([a-zA-Z][a-zA-Z0-9]*)>', html)
issues_found = []
# basic: count opens vs closes for block-level tags
for tag in ['ul', 'ol', 'li', 'p', 'div', 'strong', 'em', 'b', 'i']:
opens = open_tags.count(tag)
closes = close_tags.count(tag)
if opens != closes:
issues_found.append(f"<{tag}>: {opens} open, {closes} close")
return issues_found
def has_bad_newlines(html):
"""Check for literal \\n inside HTML strings that would render as bad breaks."""
# In JSON, \n is a newline. In HTML strings, raw newlines can be bad.
return '\n' in html
def looks_truncated(text):
"""Heuristics for truncation - text ends without proper punctuation."""
if not text:
return False
plain = strip_html(text).strip()
if not plain:
return False
# ends without sentence-ending punctuation
if plain and plain[-1] not in '.!?»)':
return True
return False
def looks_truncated_strict(text):
"""Stricter: ends mid-word or mid-sentence."""
if not text:
return False
plain = strip_html(text).strip()
if not plain:
return False
# ends mid-word (no space before end, no punctuation)
last_char = plain[-1] if plain else ''
if last_char.isalpha() or last_char in ',;:-(':
return True
return False
def get_field(data, path):
"""Get nested field value by dot-path."""
parts = path.split('.')
cur = data
for p in parts:
if isinstance(cur, dict):
cur = cur.get(p)
else:
return None
if cur is None:
return None
return cur
def search_pdf(keyword, context=300):
"""Search PDF text for a keyword and return surrounding context."""
# clean keyword for searching
kw = re.sub(r'<[^>]+>', '', keyword).strip()
if len(kw) < 10:
return None
# take last 30 chars of plain text as search key
search_key = kw[-30:].strip()
# normalize whitespace
search_key_norm = re.sub(r'\s+', ' ', search_key)
# Try to find in PDF
idx = pdf_text.find(search_key_norm)
if idx == -1:
# try shorter
search_key_norm = re.sub(r'\s+', ' ', kw[-20:].strip())
idx = pdf_text.find(search_key_norm)
if idx == -1:
# try even shorter
search_key_norm = re.sub(r'\s+', ' ', kw[-15:].strip())
idx = pdf_text.find(search_key_norm)
if idx == -1:
return None
start = max(0, idx - 50)
end = min(len(pdf_text), idx + len(search_key_norm) + context)
return pdf_text[start:end].replace('\n', ' ')
def get_all_html_fields(data, prefix=""):
"""Recursively yield (field_path, value) for all string fields containing HTML."""
if isinstance(data, dict):
for k, v in data.items():
path = f"{prefix}.{k}" if prefix else k
if isinstance(v, str) and ('<' in v or len(v) > 50):
yield path, v
elif isinstance(v, (dict, list)):
yield from get_all_html_fields(v, path)
elif isinstance(data, list):
for i, v in enumerate(data):
yield from get_all_html_fields(v, f"{prefix}[{i}]")
# ---------- fields to check ----------
IMPORTANT_FIELDS = [
"system.description",
"system.effects",
"system.examples",
"system.components",
"system.notes",
"system.style",
"system.techniques.technique1.technique",
"system.techniques.technique2.technique",
"system.techniques.technique3.technique",
]
# ---------- main scan ----------
json_files = sorted(PACKS.rglob("*.json"))
print(f"Scanning {len(json_files)} JSON files...", flush=True)
for jf in json_files:
rel = str(jf.relative_to(PACKS))
try:
data = json.loads(jf.read_text(encoding="utf-8"))
except json.JSONDecodeError as e:
issues.append({
"file": rel,
"field": "(file)",
"issue": "json_parse_error",
"current_text": str(e),
"correct_continuation": None,
})
continue
item_name = data.get("name", "(unnamed)")
# Check all relevant fields
for field in IMPORTANT_FIELDS:
val = get_field(data, field)
if not val or not isinstance(val, str):
continue
plain = strip_html(val).strip()
# 1. Check truncation (strict)
if looks_truncated_strict(val):
pdf_context = search_pdf(val)
issues.append({
"file": rel,
"field": field,
"issue": "truncated",
"item_name": item_name,
"current_end": f"...{plain[-100:]}",
"current_full_preview": f"{plain[:200]}",
"correct_continuation": pdf_context,
})
# 2. Check bad newlines in HTML strings
if has_bad_newlines(val):
issues.append({
"file": rel,
"field": field,
"issue": "unwanted_newlines",
"item_name": item_name,
"current_text": val[:300],
"correct_continuation": None,
})
# 3. Check malformed HTML
html_errors = check_unclosed_tags(val)
if html_errors:
issues.append({
"file": rel,
"field": field,
"issue": "malformed_html",
"item_name": item_name,
"html_errors": html_errors,
"current_text": val[:300],
"correct_continuation": None,
})
# 4. Check system.style (plain text field, can also be truncated)
style_val = get_field(data, "system.style")
if style_val and isinstance(style_val, str):
plain_style = style_val.strip()
if plain_style and plain_style[-1] not in '.!?»)':
pdf_context = search_pdf(plain_style)
issues.append({
"file": rel,
"field": "system.style",
"issue": "truncated",
"item_name": item_name,
"current_end": f"...{plain_style[-100:]}",
"current_full_preview": f"{plain_style[:200]}",
"correct_continuation": pdf_context,
})
# 5. Bleeding content: look for HTML tags in non-HTML fields
for field in ["system.style", "system.reference", "system.speciality"]:
val = get_field(data, field)
if val and isinstance(val, str) and '<' in val:
issues.append({
"file": rel,
"field": field,
"issue": "html_in_plain_field",
"item_name": item_name,
"current_text": val[:300],
"correct_continuation": None,
})
# 6. Check for text outside HTML tags in description-like fields (bleeding)
for field in ["system.description", "system.effects", "system.examples", "system.components", "system.notes"]:
val = get_field(data, field)
if not val or not isinstance(val, str):
continue
# Strip all HTML and check if leading text is outside tags
# e.g., "<p>foo</p> some leaked text <p>bar</p>"
# Check if there's text before the first tag
stripped = val.strip()
if stripped and not stripped.startswith('<'):
issues.append({
"file": rel,
"field": field,
"issue": "text_outside_html_tags",
"item_name": item_name,
"current_text": val[:300],
"correct_continuation": None,
})
# 7. Check technique fields for bleeding (multiple paragraphs that shouldn't be there)
for tkey in ["technique1", "technique2", "technique3"]:
tech = get_field(data, f"system.techniques.{tkey}")
if not tech:
continue
tech_text = tech.get("technique", "")
if tech_text:
plain = strip_html(tech_text).strip()
# Check for suspiciously long techniques that might have bled content
# Techniques with multiple <p> blocks may be fine, but flag very long ones
p_count = tech_text.count('</p>')
if p_count > 3:
issues.append({
"file": rel,
"field": f"system.techniques.{tkey}.technique",
"issue": "possible_bleeding_content",
"item_name": item_name,
"paragraph_count": p_count,
"current_text": tech_text[:400],
"correct_continuation": None,
})
print(f"Found {len(issues)} potential issues.", flush=True)
# ---------- output ----------
out_json = BASE / "compendium-issues.json"
out_txt = BASE / "compendium-issues.txt"
with open(out_json, 'w', encoding='utf-8') as f:
json.dump(issues, f, ensure_ascii=False, indent=2)
# Group by issue type for summary
from collections import defaultdict
by_type = defaultdict(list)
by_file = defaultdict(list)
for issue in issues:
by_type[issue['issue']].append(issue)
by_file[issue['file']].append(issue)
with open(out_txt, 'w', encoding='utf-8') as f:
f.write("=" * 80 + "\n")
f.write("COMPENDIUM TEXT QUALITY REPORT\n")
f.write("Les Chroniques de l'Étrange — FoundryVTT\n")
f.write("=" * 80 + "\n\n")
f.write(f"Total files scanned: {len(json_files)}\n")
f.write(f"Total issues found: {len(issues)}\n\n")
f.write("SUMMARY BY ISSUE TYPE:\n")
for itype, ilist in sorted(by_type.items()):
f.write(f" {itype}: {len(ilist)}\n")
f.write("\n")
f.write("=" * 80 + "\n")
f.write("DETAILED ISSUES BY FILE\n")
f.write("=" * 80 + "\n\n")
for fpath in sorted(by_file.keys()):
f.write(f"\n--- {fpath} ---\n")
for issue in by_file[fpath]:
f.write(f" FIELD: {issue['field']}\n")
f.write(f" ISSUE: {issue['issue']}\n")
if issue.get('item_name'):
f.write(f" ITEM: {issue['item_name']}\n")
if issue.get('current_end'):
f.write(f" END: {issue['current_end']}\n")
if issue.get('current_full_preview'):
f.write(f" TEXT: {issue['current_full_preview'][:200]}\n")
if issue.get('current_text'):
f.write(f" TEXT: {issue['current_text'][:200]}\n")
if issue.get('html_errors'):
f.write(f" HTML ERRORS: {issue['html_errors']}\n")
if issue.get('correct_continuation'):
f.write(f" PDF: {issue['correct_continuation'][:300]}\n")
f.write("\n")
print(f"Reports written to:\n {out_json}\n {out_txt}", flush=True)
-213
View File
@@ -1,213 +0,0 @@
#!/usr/bin/env python3
import json, re
from pathlib import Path
from collections import defaultdict
BASE = Path("/home/morr/work/uberwald/fvtt-chroniques-de-l-etrange")
PACKS = BASE / "packs-src"
pdf_text = (BASE / "regles.txt").read_text(encoding="utf-8")
WATERMARK_RE = re.compile(
r's\s*c\s*r\s*a\s*l\s*e\s*l|les\s+chroniqu|de\s+l.etrange|chr.niqu|hr\s+ng',
re.IGNORECASE)
def strip_html(html):
return re.sub(r'<[^>]+>', '', html or '').strip()
def has_watermark_bleed(text):
plain = strip_html(text)
return bool(WATERMARK_RE.search(plain))
def has_bad_newlines(text):
lines = text.split('\n')
if len(lines) <= 1:
return False
for line in lines:
s = line.strip()
if s and not re.match(r'^<[/a-zA-Z]', s) and not s.endswith('>') and len(s) > 3:
return True
return False
def looks_truncated(text):
if not text:
return False
plain = strip_html(text).strip()
plain_clean = re.sub(r'\s+[a-z]\s+[a-z]\s+[a-z]{1,2}\s+.*$', '', plain).strip()
if not plain_clean:
plain_clean = plain
last = plain_clean[-1] if plain_clean else ''
return last.isalpha() or last in ',;:-('
def get_field(data, path):
parts = path.split('.')
cur = data
for p in parts:
if isinstance(cur, dict):
cur = cur.get(p)
else:
return None
if cur is None:
return None
return cur
def pdf_search(keyword_text, context=500):
plain = strip_html(keyword_text)
plain = re.sub(r'\s+[a-z]\s+[a-z]\s+[a-z]{1,2}\s+.*$', '', plain).strip()
if len(plain) < 15:
return None
for suffix_len in [40, 30, 20, 15]:
suffix = re.sub(r'\s+', ' ', plain[-suffix_len:]).strip()
if len(suffix) < 10:
continue
idx = pdf_text.find(suffix)
if idx != -1:
snippet = pdf_text[idx:min(len(pdf_text), idx + len(suffix) + context)]
snippet = re.sub(r'\n+', ' ', snippet)
snippet = re.sub(r'\s{3,}', ' ', snippet)
return snippet[:600]
return None
issues = []
all_files = sorted(PACKS.rglob("*.json"))
print(f"Scanning {len(all_files)} files...", flush=True)
HTML_FIELDS = [
"system.description",
"system.effects",
"system.examples",
"system.components",
"system.notes",
"system.techniques.technique1.technique",
"system.techniques.technique2.technique",
"system.techniques.technique3.technique",
]
PLAIN_FIELDS = ["system.style"]
for jf in sorted(all_files):
rel = str(jf.relative_to(PACKS))
try:
data = json.loads(jf.read_text(encoding="utf-8"))
except Exception as e:
issues.append({"file": rel, "field": "(file)", "issue": "json_error",
"item_name": "?", "current_text": str(e)})
continue
name = data.get("name", "?")
def add_issue(field, issue_type, **kwargs):
issues.append({"file": rel, "field": field, "issue": issue_type,
"item_name": name, **kwargs})
for field in HTML_FIELDS + PLAIN_FIELDS:
val = get_field(data, field)
if not val or not isinstance(val, str) or not val.strip():
continue
plain = strip_html(val).strip()
if has_watermark_bleed(val):
pdf_ctx = pdf_search(val)
add_issue(field, "bleeding_watermark",
current_text=val[:400],
plain_text=plain[:300],
pdf_context=pdf_ctx)
elif looks_truncated(val):
is_ingredient = 'cde-ingredients' in rel
if is_ingredient and len(plain) < 30:
add_issue(field, "truncated_or_short",
current_text=plain,
note="May be legitimate (ingredient quantity)",
pdf_context=pdf_search(plain))
else:
pdf_ctx = pdf_search(val)
add_issue(field, "truncated",
current_end=plain[-120:],
current_preview=plain[:200],
pdf_context=pdf_ctx)
if has_bad_newlines(val):
add_issue(field, "unwanted_newlines",
current_text=val[:400],
plain_text=plain[:300])
for tkey in ['technique1', 'technique2', 'technique3']:
tech = get_field(data, f"system.techniques.{tkey}")
if not tech:
continue
t_text = tech.get("technique", "")
if not t_text:
continue
plain_t = strip_html(t_text)
activation_count = plain_t.count("Activation :")
if activation_count > 1:
add_issue(f"system.techniques.{tkey}.technique",
"bleeding_multiple_techniques",
activation_count=activation_count,
current_text=t_text[:500],
note=f"{activation_count} 'Activation :' markers found")
if ("Style" in plain_t or "Orientation :" in plain_t) and len(plain_t) > 300:
add_issue(f"system.techniques.{tkey}.technique",
"bleeding_style_or_orientation",
current_text=t_text[:500],
note="Contains 'Style' or 'Orientation' markers inside technique text")
print(f"Found {len(issues)} issues.", flush=True)
out_json = BASE / "compendium-issues.json"
out_txt = BASE / "compendium-issues.txt"
with open(out_json, 'w', encoding='utf-8') as f:
json.dump(issues, f, ensure_ascii=False, indent=2)
by_type = defaultdict(list)
by_file = defaultdict(list)
for iss in issues:
by_type[iss['issue']].append(iss)
by_file[iss['file']].append(iss)
with open(out_txt, 'w', encoding='utf-8') as f:
w = f.write
w("=" * 80 + "\n")
w("COMPENDIUM TEXT QUALITY REPORT\n")
w("Les Chroniques de l'Etrange — FoundryVTT\n")
w("=" * 80 + "\n\n")
w(f"Files scanned: {len(all_files)}\n")
w(f"Files with issues: {len(by_file)}\n")
w(f"Total issues: {len(issues)}\n\n")
w("SUMMARY BY ISSUE TYPE:\n")
for itype, ilist in sorted(by_type.items(), key=lambda x: -len(x[1])):
w(f" {itype:50s} {len(ilist):3d}\n")
w("\nFILES WITH ISSUES:\n")
for fpath in sorted(by_file.keys()):
types = sorted(set(i['issue'] for i in by_file[fpath]))
w(f" {fpath} [{', '.join(types)}]\n")
w("\n")
w("=" * 80 + "\n")
w("DETAILED ISSUES\n")
w("=" * 80 + "\n")
for itype in ['bleeding_watermark', 'bleeding_multiple_techniques',
'bleeding_style_or_orientation', 'truncated',
'unwanted_newlines', 'truncated_or_short']:
ilist = by_type.get(itype, [])
if not ilist:
continue
w(f"\n{''*80}\n")
w(f"ISSUE TYPE: {itype} ({len(ilist)} occurrences)\n")
w(f"{''*80}\n")
for iss in ilist:
w(f"\n File: {iss['file']}\n")
w(f" Item: {iss.get('item_name','?')}\n")
w(f" Field: {iss['field']}\n")
if iss.get('note'):
w(f" Note: {iss['note']}\n")
if iss.get('current_end'):
w(f" Ends: ...{iss['current_end']}\n")
if iss.get('current_preview'):
w(f" Text: {iss['current_preview'][:200]}\n")
if iss.get('current_text'):
ct = iss['current_text']
w(f" Text: {ct[:300]}\n")
if iss.get('pdf_context'):
w(f" PDF>>: {iss['pdf_context'][:400]}\n")
print(f"Written: {out_json}\n {out_txt}", flush=True)
-317
View File
@@ -1,317 +0,0 @@
#!/usr/bin/env python3
"""Final comprehensive analysis including missing beginnings and garbled content."""
import json, re
from pathlib import Path
from collections import defaultdict
BASE = Path("/home/morr/work/uberwald/fvtt-chroniques-de-l-etrange")
PACKS = BASE / "packs-src"
pdf_text = (BASE / "regles.txt").read_text(encoding="utf-8")
WATERMARK_RE = re.compile(
r's\s*c\s*r\s*a\s*l\s*e\s*l|les\s+chroniqu|de\s+l.etrange|chr.niqu|hr\s+ng',
re.IGNORECASE)
def strip_html(html):
return re.sub(r'<[^>]+>', '', html or '').strip()
def has_watermark_bleed(text):
plain = strip_html(text)
return bool(WATERMARK_RE.search(plain))
def has_bad_newlines(text):
lines = text.split('\n')
if len(lines) <= 1:
return False
for line in lines:
s = line.strip()
if s and not re.match(r'^<[/a-zA-Z]', s) and not s.endswith('>') and len(s) > 3:
return True
return False
def looks_truncated(text):
"""Text appears cut off at the end."""
if not text:
return False
plain = strip_html(text).strip()
plain_clean = re.sub(r'\s+[a-z]\s+[a-z]\s+[a-z]{1,2}\s+.*$', '', plain).strip()
if not plain_clean:
plain_clean = plain
last = plain_clean[-1] if plain_clean else ''
return last.isalpha() or last in ',;:-('
def looks_missing_beginning(text):
"""Text starts mid-sentence (lowercase, or starts with punctuation)."""
if not text:
return False
plain = strip_html(text).strip()
if not plain:
return False
first_char = plain[0]
# Starts with lowercase letter (unlikely to be intentional)
if first_char.islower():
return True
# Starts with a bullet/list item that makes no sense
if re.match(r'^(et|ou|de|du|des|les|la|le|un|une|à|au|aux|mais|car|si|que)\s', plain, re.IGNORECASE):
return True
return False
def is_garbled_page_layout(text):
"""Detects when text is broken into single-letter paragraphs (PDF artifact)."""
# Pattern: multiple single-letter <p> tags = garbled page layout
single_p = re.findall(r'<p>([a-zA-Z0-9])</p>', text)
if len(single_p) >= 5:
return True
return False
def get_field(data, path):
parts = path.split('.')
cur = data
for p in parts:
if isinstance(cur, dict):
cur = cur.get(p)
else:
return None
if cur is None:
return None
return cur
def pdf_search(keyword_text, context=500):
"""Search PDF text after the given keyword."""
plain = strip_html(keyword_text)
plain = re.sub(r'\s+[a-z]\s+[a-z]\s+[a-z]{1,2}\s+.*$', '', plain).strip()
if len(plain) < 15:
return None
for suffix_len in [40, 30, 20, 15]:
suffix = re.sub(r'\s+', ' ', plain[-suffix_len:]).strip()
if len(suffix) < 10:
continue
idx = pdf_text.find(suffix)
if idx != -1:
snippet = pdf_text[idx:min(len(pdf_text), idx + len(suffix) + context)]
snippet = re.sub(r'\n+', ' ', snippet)
snippet = re.sub(r'\s{3,}', ' ', snippet)
return snippet[:600]
return None
def pdf_search_forward(keyword_text, context=500):
"""Search PDF text BEFORE the start of the given text (find what precedes it)."""
plain = strip_html(keyword_text)
plain = re.sub(r'\s+', ' ', plain[:60]).strip()
if len(plain) < 15:
return None
# Search for the prefix
for prefix_len in [50, 40, 30, 20]:
prefix = re.sub(r'\s+', ' ', plain[:prefix_len]).strip()
if len(prefix) < 10:
continue
idx = pdf_text.find(prefix)
if idx != -1:
# Get text before this position
start = max(0, idx - context)
snippet = pdf_text[start:idx + len(prefix)]
snippet = re.sub(r'\n+', ' ', snippet)
snippet = re.sub(r'\s{3,}', ' ', snippet)
return snippet[-400:]
return None
issues = []
all_files = sorted(PACKS.rglob("*.json"))
print(f"Scanning {len(all_files)} files...", flush=True)
HTML_FIELDS = [
"system.description",
"system.effects",
"system.examples",
"system.components",
"system.notes",
"system.techniques.technique1.technique",
"system.techniques.technique2.technique",
"system.techniques.technique3.technique",
]
PLAIN_FIELDS = ["system.style"]
for jf in sorted(all_files):
rel = str(jf.relative_to(PACKS))
try:
data = json.loads(jf.read_text(encoding="utf-8"))
except Exception as e:
issues.append({"file": rel, "field": "(file)", "issue": "json_error",
"item_name": "?", "current_text": str(e)})
continue
name = data.get("name", "?")
def add_issue(field, issue_type, **kwargs):
issues.append({"file": rel, "field": field, "issue": issue_type,
"item_name": name, **kwargs})
for field in HTML_FIELDS + PLAIN_FIELDS:
val = get_field(data, field)
if not val or not isinstance(val, str) or not val.strip():
continue
plain = strip_html(val).strip()
# --- Garbled page layout ---
if is_garbled_page_layout(val):
add_issue(field, "garbled_page_layout",
current_text=val[:400],
note="Text broken into single-character <p> tags — PDF layout artifact")
continue # other checks not useful
# --- Watermark bleeding ---
if has_watermark_bleed(val):
pdf_ctx = pdf_search(val)
add_issue(field, "bleeding_watermark",
current_text=val[:400],
plain_text=plain[:300],
pdf_context=pdf_ctx)
# --- Missing beginning ---
if looks_missing_beginning(val):
pdf_ctx = pdf_search_forward(val)
add_issue(field, "missing_beginning",
current_start=plain[:150],
pdf_context_before=pdf_ctx)
# --- Truncation ---
if looks_truncated(val):
is_ingredient = 'cde-ingredients' in rel
if is_ingredient and len(plain) < 30:
add_issue(field, "truncated_or_short",
current_text=plain,
note="May be legitimate (ingredient quantity/name)")
else:
pdf_ctx = pdf_search(val)
add_issue(field, "truncated",
current_end=plain[-120:],
current_preview=plain[:200],
pdf_context=pdf_ctx)
# --- Unwanted newlines ---
if has_bad_newlines(val):
add_issue(field, "unwanted_newlines",
current_text=val[:400],
plain_text=plain[:300])
# --- Technique-level checks ---
for tkey in ['technique1', 'technique2', 'technique3']:
tech = get_field(data, f"system.techniques.{tkey}")
if not tech:
continue
t_text = tech.get("technique", "")
if not t_text:
continue
plain_t = strip_html(t_text)
activation_count = plain_t.count("Activation :")
if activation_count > 1:
add_issue(f"system.techniques.{tkey}.technique",
"bleeding_multiple_techniques",
activation_count=activation_count,
current_text=t_text[:500],
note=f"{activation_count} 'Activation :' markers — multiple techniques merged")
if ("Style" in plain_t or "Orientation :" in plain_t) and len(plain_t) > 300:
add_issue(f"system.techniques.{tkey}.technique",
"bleeding_style_or_orientation",
current_text=t_text[:500],
note="Contains 'Style' or 'Orientation' markers — extra text from page layout")
print(f"Found {len(issues)} issues.", flush=True)
# Deduplicate (same file+field+issue_type)
seen = set()
deduped = []
for iss in issues:
key = (iss['file'], iss['field'], iss['issue'])
if key not in seen:
seen.add(key)
deduped.append(iss)
issues = deduped
print(f"After dedup: {len(issues)} issues.", flush=True)
out_json = BASE / "compendium-issues.json"
out_txt = BASE / "compendium-issues.txt"
with open(out_json, 'w', encoding='utf-8') as f:
json.dump(issues, f, ensure_ascii=False, indent=2)
by_type = defaultdict(list)
by_file = defaultdict(list)
for iss in issues:
by_type[iss['issue']].append(iss)
by_file[iss['file']].append(iss)
PRIORITY_ORDER = [
'garbled_page_layout',
'missing_beginning',
'bleeding_watermark',
'bleeding_multiple_techniques',
'bleeding_style_or_orientation',
'truncated',
'unwanted_newlines',
'truncated_or_short',
]
with open(out_txt, 'w', encoding='utf-8') as f:
w = f.write
w("=" * 80 + "\n")
w("COMPENDIUM TEXT QUALITY REPORT\n")
w("Les Chroniques de l'Etrange — FoundryVTT\n")
w("=" * 80 + "\n\n")
w(f"Files scanned: {len(all_files)}\n")
w(f"Files with issues: {len(by_file)}\n")
w(f"Total issues: {len(issues)}\n\n")
w("SUMMARY BY ISSUE TYPE:\n")
for itype in PRIORITY_ORDER:
ilist = by_type.get(itype, [])
if ilist:
w(f" {itype:50s} {len(ilist):3d}\n")
other_types = set(by_type.keys()) - set(PRIORITY_ORDER)
for itype in sorted(other_types):
ilist = by_type.get(itype, [])
if ilist:
w(f" {itype:50s} {len(ilist):3d}\n")
w("\n")
w("FILES WITH ISSUES:\n")
for fpath in sorted(by_file.keys()):
types = sorted(set(i['issue'] for i in by_file[fpath]))
w(f" {fpath}\n [{', '.join(types)}]\n")
w("\n")
w("=" * 80 + "\n")
w("DETAILED ISSUES (by priority)\n")
w("=" * 80 + "\n")
for itype in PRIORITY_ORDER + sorted(set(by_type.keys()) - set(PRIORITY_ORDER)):
ilist = by_type.get(itype, [])
if not ilist:
continue
w(f"\n{''*80}\n")
w(f"ISSUE TYPE: {itype} ({len(ilist)} occurrences)\n")
w(f"{''*80}\n")
for iss in ilist:
w(f"\n File: {iss['file']}\n")
w(f" Item: {iss.get('item_name','?')}\n")
w(f" Field: {iss['field']}\n")
if iss.get('note'):
w(f" Note: {iss['note']}\n")
if iss.get('current_start'):
w(f" Starts: {iss['current_start'][:150]}\n")
if iss.get('current_end'):
w(f" Ends: ...{iss['current_end']}\n")
if iss.get('current_preview'):
w(f" Text: {iss['current_preview'][:200]}\n")
if iss.get('current_text'):
ct = iss['current_text']
w(f" Text: {ct[:300]}\n")
if iss.get('plain_text'):
w(f" Plain: {iss['plain_text'][:200]}\n")
if iss.get('pdf_context'):
w(f" PDF>>: {iss['pdf_context'][:400]}\n")
if iss.get('pdf_context_before'):
w(f" <<PDF: {iss['pdf_context_before'][:400]}\n")
print(f"Written: {out_json}\n {out_txt}", flush=True)
-306
View File
@@ -1,306 +0,0 @@
#!/usr/bin/env python3
"""Final comprehensive analysis — clean version with bug fixes."""
import json, re
from pathlib import Path
from collections import defaultdict
BASE = Path("/home/morr/work/uberwald/fvtt-chroniques-de-l-etrange")
PACKS = BASE / "packs-src"
pdf_text = (BASE / "regles.txt").read_text(encoding="utf-8")
WATERMARK_RE = re.compile(
r's\s*c\s*r\s*a\s*l\s*e\s*l|les\s+chroniqu|de\s+l.etrange|chr.niqu|hr\s+ng',
re.IGNORECASE)
def strip_html(html):
return re.sub(r'<[^>]+>', '', html or '').strip()
def has_watermark_bleed(text):
plain = strip_html(text)
return bool(WATERMARK_RE.search(plain))
def has_bad_newlines(text):
lines = text.split('\n')
if len(lines) <= 1:
return False
for line in lines:
s = line.strip()
if s and not re.match(r'^<[/a-zA-Z]', s) and not s.endswith('>') and len(s) > 3:
return True
return False
def looks_truncated(text):
"""Text appears cut off at the end."""
if not text:
return False
plain = strip_html(text).strip()
if not plain:
return False
# Remove watermark garbage from end before checking
plain_clean = re.sub(r'\s+[a-z]\s+[a-z]\s+[a-z]{1,2}\s+.*$', '', plain).strip()
if not plain_clean:
return False # FIX: was using original empty plain, now correctly returns False
last = plain_clean[-1]
return last.isalpha() or last in ',;:-('
def looks_missing_beginning(text):
"""Text starts mid-sentence (truly lowercase first char only)."""
if not text:
return False
plain = strip_html(text).strip()
if not plain:
return False
# Only flag if truly starts lowercase (not French articles/prepositions)
first_char = plain[0]
return first_char.islower()
def is_garbled_page_layout(text):
"""Multiple single-letter <p> tags = garbled PDF artifact."""
single_p = re.findall(r'<p>([a-zA-Z0-9])</p>', text)
return len(single_p) >= 5
def get_field(data, path):
parts = path.split('.')
cur = data
for p in parts:
if isinstance(cur, dict):
cur = cur.get(p)
else:
return None
if cur is None:
return None
return cur
def pdf_search_after(keyword_text, context=500):
"""Return PDF text after the given keyword."""
plain = strip_html(keyword_text)
plain = re.sub(r'\s+[a-z]\s+[a-z]\s+[a-z]{1,2}\s+.*$', '', plain).strip()
if len(plain) < 15:
return None
for suffix_len in [40, 30, 20, 15]:
suffix = re.sub(r'\s+', ' ', plain[-suffix_len:]).strip()
if len(suffix) < 10:
continue
idx = pdf_text.find(suffix)
if idx != -1:
snippet = pdf_text[idx:min(len(pdf_text), idx + len(suffix) + context)]
snippet = re.sub(r'\n+', ' ', snippet)
snippet = re.sub(r'\s{3,}', ' ', snippet)
return snippet[:600]
return None
def pdf_search_before(keyword_text, context=400):
"""Return PDF text before the given keyword."""
plain = strip_html(keyword_text)
plain_start = re.sub(r'\s+', ' ', plain[:60]).strip()
if len(plain_start) < 15:
return None
for prefix_len in [50, 40, 30, 20]:
prefix = re.sub(r'\s+', ' ', plain_start[:prefix_len]).strip()
if len(prefix) < 10:
continue
idx = pdf_text.find(prefix)
if idx != -1:
start = max(0, idx - context)
snippet = pdf_text[start:idx + len(prefix)]
snippet = re.sub(r'\n+', ' ', snippet)
snippet = re.sub(r'\s{3,}', ' ', snippet)
return snippet[-400:]
return None
issues = []
all_files = sorted(PACKS.rglob("*.json"))
print(f"Scanning {len(all_files)} files...", flush=True)
HTML_FIELDS = [
"system.description",
"system.effects",
"system.examples",
"system.components",
"system.notes",
"system.techniques.technique1.technique",
"system.techniques.technique2.technique",
"system.techniques.technique3.technique",
]
PLAIN_FIELDS = ["system.style"]
for jf in sorted(all_files):
rel = str(jf.relative_to(PACKS))
try:
data = json.loads(jf.read_text(encoding="utf-8"))
except Exception as e:
issues.append({"file": rel, "field": "(file)", "issue": "json_error",
"item_name": "?", "current_text": str(e)})
continue
name = data.get("name", "?")
def add(field, issue_type, **kwargs):
issues.append({"file": rel, "field": field, "issue": issue_type,
"item_name": name, **kwargs})
for field in HTML_FIELDS + PLAIN_FIELDS:
val = get_field(data, field)
if not val or not isinstance(val, str) or not val.strip():
continue
plain = strip_html(val).strip()
# Garbled page layout (skip other checks)
if is_garbled_page_layout(val):
add(field, "garbled_page_layout",
current_text=val[:400],
note="Text broken into single-character <p> tags — PDF layout artifact")
continue
# Watermark bleeding
if has_watermark_bleed(val):
add(field, "bleeding_watermark",
current_text=val[:400],
plain_text=plain[:300],
pdf_context=pdf_search_after(val))
# Missing beginning (only truly lowercase-starting)
if looks_missing_beginning(val):
add(field, "missing_beginning",
current_start=plain[:150],
pdf_context_before=pdf_search_before(val))
# Truncation
if looks_truncated(val):
# Skip empty ingredient placeholders
is_ingredient = 'cde-ingredients' in rel
if is_ingredient:
# Only flag if there's actually short meaningful text
if plain and len(plain) < 30:
add(field, "empty_or_short_ingredient",
current_text=plain,
note="Short ingredient description — check if intentional")
else:
add(field, "truncated",
current_end=plain[-120:],
current_preview=plain[:200],
pdf_context=pdf_search_after(val))
# Unwanted newlines
if has_bad_newlines(val):
add(field, "unwanted_newlines",
current_text=val[:400],
plain_text=plain[:200])
# Technique cross-checks
for tkey in ['technique1', 'technique2', 'technique3']:
tech = get_field(data, f"system.techniques.{tkey}")
if not tech:
continue
t_text = tech.get("technique", "")
if not t_text:
continue
plain_t = strip_html(t_text)
activation_count = plain_t.count("Activation :")
if activation_count > 1:
add(f"system.techniques.{tkey}.technique",
"bleeding_multiple_techniques",
activation_count=activation_count,
current_text=t_text[:500],
note=f"{activation_count} 'Activation :' markers — multiple techniques merged")
if ("Style" in plain_t or "Orientation :" in plain_t) and len(plain_t) > 300:
already = any(i['file'] == rel and i['field'] == f"system.techniques.{tkey}.technique"
and i['issue'] == 'bleeding_style_or_orientation' for i in issues)
if not already:
add(f"system.techniques.{tkey}.technique",
"bleeding_style_or_orientation",
current_text=t_text[:500],
note="Contains 'Style' or 'Orientation' — extra text from PDF page layout")
print(f"Found {len(issues)} issues.", flush=True)
out_json = BASE / "compendium-issues.json"
out_txt = BASE / "compendium-issues.txt"
with open(out_json, 'w', encoding='utf-8') as f:
json.dump(issues, f, ensure_ascii=False, indent=2)
by_type = defaultdict(list)
by_file = defaultdict(list)
for iss in issues:
by_type[iss['issue']].append(iss)
by_file[iss['file']].append(iss)
PRIORITY_ORDER = [
'garbled_page_layout',
'missing_beginning',
'bleeding_watermark',
'bleeding_multiple_techniques',
'bleeding_style_or_orientation',
'truncated',
'unwanted_newlines',
'empty_or_short_ingredient',
]
with open(out_txt, 'w', encoding='utf-8') as f:
w = f.write
w("=" * 80 + "\n")
w("COMPENDIUM TEXT QUALITY REPORT\n")
w("Les Chroniques de l'Etrange — FoundryVTT\n")
w("=" * 80 + "\n\n")
w(f"Files scanned: {len(all_files)}\n")
w(f"Files with issues: {len(by_file)}\n")
w(f"Total issues: {len(issues)}\n\n")
w("SUMMARY BY ISSUE TYPE:\n")
for itype in PRIORITY_ORDER:
ilist = by_type.get(itype, [])
if ilist:
desc = {
'garbled_page_layout': 'text broken into single-char HTML tags (PDF artifact)',
'missing_beginning': 'field starts mid-word (lowercase start = truncated at front)',
'bleeding_watermark': '"Les Chroniques de l\'Étrange" watermark fragments in text',
'bleeding_multiple_techniques': 'multiple techniques merged into one field',
'bleeding_style_or_orientation': 'Style/Orientation text bled into technique field',
'truncated': 'field ends mid-sentence without proper punctuation',
'unwanted_newlines': 'raw newlines inside HTML string values',
'empty_or_short_ingredient': 'ingredient has empty or very short description',
}.get(itype, '')
w(f" {itype:45s} {len(ilist):3d}{desc}\n")
w("\nFILES WITH ISSUES:\n")
for fpath in sorted(by_file.keys()):
types = sorted(set(i['issue'] for i in by_file[fpath]))
w(f" {fpath}\n [{', '.join(types)}]\n")
w("\n")
w("=" * 80 + "\n")
w("DETAILED ISSUES (by priority)\n")
w("=" * 80 + "\n")
for itype in PRIORITY_ORDER:
ilist = by_type.get(itype, [])
if not ilist:
continue
w(f"\n{''*80}\n")
w(f"ISSUE TYPE: {itype} ({len(ilist)} occurrences)\n")
w(f"{''*80}\n")
for iss in ilist:
w(f"\n File: {iss['file']}\n")
w(f" Item: {iss.get('item_name','?')}\n")
w(f" Field: {iss['field']}\n")
if iss.get('note'):
w(f" Note: {iss['note']}\n")
if iss.get('current_start'):
w(f" Starts: {iss['current_start'][:160]}\n")
if iss.get('current_end'):
w(f" Ends: ...{iss['current_end']}\n")
if iss.get('current_preview'):
w(f" Text: {iss['current_preview'][:200]}\n")
if iss.get('current_text'):
ct = iss['current_text']
w(f" Text: {ct[:300]}\n")
if iss.get('plain_text'):
w(f" Plain: {iss['plain_text'][:200]}\n")
if iss.get('pdf_context'):
w(f" PDF>>: {iss['pdf_context'][:400]}\n")
if iss.get('pdf_context_before'):
w(f" <<PDF: {iss['pdf_context_before'][:400]}\n")
print(f"Written: {out_json}\n {out_txt}", flush=True)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+268
View File
@@ -4188,3 +4188,271 @@ ol.item-list li.item .item-controls a.item-control:hover {
transform: rotate(360deg);
}
}
/* ===================================================================
Migration App
=================================================================== */
.cde-migration-app .window-content {
padding: 0;
overflow-y: auto;
}
.cde-migration-body {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px;
}
/* Drop zone */
.cde-migration-drop-zone {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 28px 20px;
border: 2px dashed #1a2436;
border-radius: 8px;
background: rgba(13, 21, 32, 0.6);
text-align: center;
transition: border-color 0.15s, background 0.15s;
cursor: pointer;
}
.cde-migration-drop-zone.is-dragover {
border-color: #4a9eff;
background: rgba(74, 158, 255, 0.15);
}
.cde-migration-drop-icon {
font-size: 36px;
color: #4a9eff;
opacity: 0.7;
}
.cde-migration-drop-hint {
margin: 0;
font-size: 12px;
color: #7d94b8;
}
.cde-migration-file-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border: 1px solid #4a9eff;
border-radius: 4px;
color: #4a9eff;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: background 0.15s;
}
.cde-migration-file-btn:hover {
background: rgba(74, 158, 255, 0.2);
}
/* Preview section */
.cde-migration-preview {
border: 1px solid #1a2436;
border-radius: 6px;
overflow: hidden;
}
.cde-migration-preview-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: rgba(13, 21, 32, 0.8);
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #7d94b8;
}
.cde-migration-clear-btn {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border: 1px solid #1a2436;
border-radius: 4px;
font-size: 11px;
color: #7d94b8;
background: none;
cursor: pointer;
transition: color 0.15s, border-color 0.15s;
}
.cde-migration-clear-btn:hover {
color: #e04444;
border-color: #e04444;
}
/* Preview table */
.cde-migration-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
.cde-migration-table th {
padding: 5px 8px;
background: rgba(13, 21, 32, 0.9);
color: #7d94b8;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
text-align: left;
border-bottom: 1px solid #1a2436;
}
.cde-migration-table td {
padding: 5px 8px;
border-bottom: 1px solid rgba(26, 36, 54, 0.4);
vertical-align: middle;
}
.cde-migration-table tr:last-child td {
border-bottom: none;
}
.cde-migration-thumb {
width: 28px;
height: 28px;
border-radius: 3px;
object-fit: cover;
}
.cde-migration-name {
font-weight: 600;
color: #e2e8f4;
}
.cde-migration-items-count {
text-align: center;
color: #7d94b8;
}
.cde-migration-srcfile {
font-size: 10px;
color: #7d94b8;
max-width: 130px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Type badge */
.cde-migration-type-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 10px;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
}
.cde-migration-type-badge.cde-migration-type-character {
background: rgba(74, 158, 255, 0.2);
color: #4a9eff;
border: 1px solid rgba(74, 158, 255, 0.4);
}
.cde-migration-type-badge.cde-migration-type-npc {
background: rgba(156, 77, 204, 0.2);
color: #c97ae0;
border: 1px solid rgba(156, 77, 204, 0.4);
}
/* Errors */
.cde-migration-errors {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.cde-migration-errors li {
display: flex;
align-items: flex-start;
gap: 6px;
padding: 6px 10px;
border: 1px solid rgba(224, 68, 68, 0.6);
border-radius: 4px;
background: rgba(224, 68, 68, 0.1);
color: #e07070;
font-size: 11px;
}
.cde-migration-errors li i {
margin-top: 2px;
flex-shrink: 0;
}
/* Bottom action bar */
.cde-migration-actions {
display: flex;
justify-content: center;
padding-top: 4px;
}
.cde-migration-import-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 9px 24px;
border: none;
border-radius: 6px;
background: #4a9eff;
color: #fff;
font-size: 13px;
font-weight: 700;
cursor: pointer;
transition: filter 0.15s;
}
.cde-migration-import-btn:hover {
filter: brightness(1.15);
}
.cde-migration-hint {
margin: 0;
font-size: 12px;
color: #7d94b8;
text-align: center;
}
.cde-welcome-message {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: #101622;
border: 1px solid #263853;
border-radius: 8px;
text-align: center;
}
.cde-welcome-logo {
width: 120px;
height: auto;
object-fit: contain;
filter: drop-shadow(0 0 8px rgba(74, 158, 255, 0.4));
}
.cde-welcome-title {
margin: 0;
font-size: 15px;
font-weight: 700;
color: #4a9eff;
text-shadow: 0 0 8px rgba(74, 158, 255, 0.5);
}
.cde-welcome-links {
margin: 0;
font-size: 12px;
color: #7d94b8;
}
.cde-welcome-links a {
color: #00d4d4;
text-decoration: none;
border-bottom: 1px solid rgba(0, 212, 212, 0.4);
}
.cde-welcome-links a:hover {
color: #fff;
border-bottom-color: #fff;
}
.cde-welcome-help-btn {
display: inline-flex;
align-items: center;
gap: 6px;
margin-top: 4px;
padding: 7px 18px;
background: #4a9eff;
border: none;
border-radius: 6px;
color: #fff;
font-size: 12px;
font-weight: 700;
cursor: pointer;
transition: filter 0.15s;
}
.cde-welcome-help-btn:hover {
filter: brightness(1.2);
}
+310
View File
@@ -4344,3 +4344,313 @@ ol.item-list {
from { transform-origin: var(--fx) var(--fy); transform: rotate(0deg); }
to { transform-origin: var(--fx) var(--fy); transform: rotate(360deg); }
}
/* ===================================================================
Migration App
=================================================================== */
.cde-migration-app {
.window-content {
padding: 0;
overflow-y: auto;
}
}
.cde-migration-body {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px;
}
/* Drop zone */
.cde-migration-drop-zone {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 28px 20px;
border: 2px dashed @cde-border;
border-radius: 8px;
background: fadeout(@cde-surface2, 40%);
text-align: center;
transition: border-color 0.15s, background 0.15s;
cursor: pointer;
&.is-dragover {
border-color: @cde-spell;
background: fadeout(@cde-spell, 85%);
}
}
.cde-migration-drop-icon {
font-size: 36px;
color: @cde-spell;
opacity: 0.7;
}
.cde-migration-drop-hint {
margin: 0;
font-size: 12px;
color: @cde-muted;
}
.cde-migration-file-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border: 1px solid @cde-spell;
border-radius: 4px;
color: @cde-spell;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: background 0.15s;
&:hover {
background: fadeout(@cde-spell, 80%);
}
}
/* Preview section */
.cde-migration-preview {
border: 1px solid @cde-border;
border-radius: 6px;
overflow: hidden;
}
.cde-migration-preview-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: fadeout(@cde-surface2, 20%);
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: @cde-muted;
}
.cde-migration-clear-btn {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border: 1px solid @cde-border;
border-radius: 4px;
font-size: 11px;
color: @cde-muted;
background: none;
cursor: pointer;
transition: color 0.15s, border-color 0.15s;
&:hover {
color: #e04444;
border-color: #e04444;
}
}
/* Preview table */
.cde-migration-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
th {
padding: 5px 8px;
background: fadeout(@cde-surface2, 10%);
color: @cde-muted;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
text-align: left;
border-bottom: 1px solid @cde-border;
}
td {
padding: 5px 8px;
border-bottom: 1px solid fadeout(@cde-border, 60%);
vertical-align: middle;
}
tr:last-child td {
border-bottom: none;
}
}
.cde-migration-thumb {
width: 28px;
height: 28px;
border-radius: 3px;
object-fit: cover;
}
.cde-migration-name {
font-weight: 600;
color: @cde-text;
}
.cde-migration-items-count {
text-align: center;
color: @cde-muted;
}
.cde-migration-srcfile {
font-size: 10px;
color: @cde-muted;
max-width: 130px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Type badge */
.cde-migration-type-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 10px;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
&.cde-migration-type-character {
background: fadeout(@cde-spell, 80%);
color: @cde-spell;
border: 1px solid fadeout(@cde-spell, 60%);
}
&.cde-migration-type-npc {
background: fadeout(#9c4dcc, 80%);
color: #c97ae0;
border: 1px solid fadeout(#9c4dcc, 60%);
}
}
/* Errors */
.cde-migration-errors {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 4px;
li {
display: flex;
align-items: flex-start;
gap: 6px;
padding: 6px 10px;
border: 1px solid fadeout(#e04444, 40%);
border-radius: 4px;
background: fadeout(#e04444, 90%);
color: #e07070;
font-size: 11px;
i { margin-top: 2px; flex-shrink: 0; }
}
}
/* Bottom action bar */
.cde-migration-actions {
display: flex;
justify-content: center;
padding-top: 4px;
}
.cde-migration-import-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 9px 24px;
border: none;
border-radius: 6px;
background: @cde-spell;
color: #fff;
font-size: 13px;
font-weight: 700;
cursor: pointer;
transition: filter 0.15s;
&:hover {
filter: brightness(1.15);
}
}
.cde-migration-hint {
margin: 0;
font-size: 12px;
color: @cde-muted;
text-align: center;
}
// ============================================================
// Welcome message
// ============================================================
.cde-welcome-message {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: @cde-surface;
border: 1px solid @cde-border-hi;
border-radius: 8px;
text-align: center;
}
.cde-welcome-logo {
width: 120px;
height: auto;
object-fit: contain;
filter: drop-shadow(0 0 8px fade(@cde-spell, 40%));
}
.cde-welcome-title {
margin: 0;
font-size: 15px;
font-weight: 700;
color: @cde-spell;
text-shadow: 0 0 8px fade(@cde-spell, 50%);
}
.cde-welcome-links {
margin: 0;
font-size: 12px;
color: @cde-muted;
a {
color: @cde-item;
text-decoration: none;
border-bottom: 1px solid fade(@cde-item, 40%);
&:hover {
color: #fff;
border-bottom-color: #fff;
}
}
}
.cde-welcome-help-btn {
display: inline-flex;
align-items: center;
gap: 6px;
margin-top: 4px;
padding: 7px 18px;
background: @cde-spell;
border: none;
border-radius: 6px;
color: #fff;
font-size: 12px;
font-weight: 700;
cursor: pointer;
transition: filter 0.15s;
&:hover {
filter: brightness(1.2);
}
}
+519
View File
@@ -134,8 +134,456 @@ var TEMPLATE_PARTIALS = [
"systems/fvtt-chroniques-de-l-etrange/templates/apps/cde-wheel-app.html"
];
// src/migration/migrator.js
var ELEMENT_LABEL_TO_KEY = {
"m\xE9tal": "metal",
"metal": "metal",
"eau": "eau",
"terre": "terre",
"feu": "feu",
"bois": "bois"
};
function elementKey(label = "") {
return ELEMENT_LABEL_TO_KEY[label.toLowerCase().trim()] ?? "metal";
}
function heiKey(label = "") {
const l = label.toLowerCase().trim();
if (l === "yin/yang" || l === "yinyang") return "yinyang";
if (l === "yang") return "yang";
return "yin";
}
var SPECIALITY_TO_DISCIPLINE = {
// internalcinnabar
"essence": "internalcinnabar",
"esprit": "internalcinnabar",
"mind": "internalcinnabar",
"purification": "internalcinnabar",
"manipulation": "internalcinnabar",
"aura": "internalcinnabar",
// alchemy
"acupuncture": "alchemy",
"\xE9lixirs": "alchemy",
"elixirs": "alchemy",
"poisons": "alchemy",
"arsenal": "alchemy",
"potions": "alchemy",
// masteryoftheway
"mal\xE9diction": "masteryoftheway",
"malediction": "masteryoftheway",
"transfiguration": "masteryoftheway",
"n\xE9cromancie": "masteryoftheway",
"necromancie": "masteryoftheway",
"contr\xF4le climatique": "masteryoftheway",
"controle climatique": "masteryoftheway",
"magie d'or": "masteryoftheway",
"magie dor": "masteryoftheway",
// exorcism
"invocation": "exorcism",
"pistage": "exorcism",
"tra\xE7age": "exorcism",
"tracage": "exorcism",
"protection": "exorcism",
"ch\xE2timent": "exorcism",
"chatiment": "exorcism",
"domination": "exorcism",
// geomancy
"neutralisation": "geomancy",
"divination": "geomancy",
"pri\xE8re terrestre": "geomancy",
"priere terrestre": "geomancy",
"pri\xE8re c\xE9leste": "geomancy",
"priere celeste": "geomancy",
"g\xE9omancie": "geomancy",
"geomancie": "geomancy",
"feng shui": "geomancy",
"fungseoi": "geomancy"
};
function inferDiscipline(specialityName = "", itemName = "") {
const key = specialityName.toLowerCase().trim();
if (SPECIALITY_TO_DISCIPLINE[key]) return SPECIALITY_TO_DISCIPLINE[key];
const name = itemName.toLowerCase();
if (name.includes("exorcis")) return "exorcism";
if (name.includes("g\xE9omanci") || name.includes("geomanci")) return "geomancy";
if (name.includes("alchimi")) return "alchemy";
if (name.includes("cinnabre") || name.includes("interne")) return "internalcinnabar";
if (name.includes("ma\xEEtrise") || name.includes("maitrise") || name.includes("tao")) return "masteryoftheway";
return "internalcinnabar";
}
function mapActivation(oldActivation = "") {
const s = oldActivation.toLowerCase();
if (s.includes("inflig\xE9s") || s.includes("infliges")) return "damage-inflicted";
if (s.includes("re\xE7us") || s.includes("recus")) return "damage-received";
if (s.includes("r\xE9action") || s.includes("reaction")) return "reaction";
if (s.includes("d\xE9s-fastes") || s.includes("des-fastes") || s.includes("fastes")) return "dice";
if (s.includes("aide")) return "action-aid";
if (s.includes("attaque") && s.includes("d\xE9fense")) return "action-attack-defense";
if (s.includes("attaque") && s.includes("defense")) return "action-attack-defense";
if (s.includes("attaque")) return "action-attack";
if (s.includes("d\xE9fense") || s.includes("defense")) return "action-defense";
return "action-attack";
}
var DEFAULT_ACTOR_IMG = "icons/svg/mystery-man.svg";
var DEFAULT_ITEM_IMG = "icons/svg/item-bag.svg";
function migrateEquipmentItem(oldItem) {
const s = oldItem.system ?? {};
return {
name: oldItem.name,
type: "item",
img: oldItem.img || DEFAULT_ITEM_IMG,
system: {
reference: s.reference ?? "",
description: s.description ?? "",
quantity: Number(s.quantity ?? 1),
weight: Number(s.weight ?? 0),
notes: s.notes ?? ""
}
};
}
function migrateKungfuItem(oldItem) {
const s = oldItem.system ?? {};
const techs = s.techniques ?? {};
const migratedTechs = {};
for (const key of ["technique1", "technique2", "technique3"]) {
const t = techs[key] ?? {};
migratedTechs[key] = {
check: Boolean(t.check),
name: t.name ?? "",
activation: mapActivation(t.activation ?? ""),
technique: t.technique ?? ""
};
}
return {
name: oldItem.name,
type: "kungfu",
img: oldItem.img || DEFAULT_ITEM_IMG,
system: {
reference: s.reference ?? "",
description: s.description ?? "",
orientation: s.orientation || "yin",
aspect: s.aspect || "metal",
skill: s.skill || "kungfu",
speciality: s.speciality ?? "",
style: s.style ?? "",
techniques: migratedTechs,
notes: s.notes ?? ""
}
};
}
function migrateSpellItem(oldItem) {
const s = oldItem.system ?? {};
return {
name: oldItem.name,
type: "spell",
img: oldItem.img || DEFAULT_ITEM_IMG,
system: {
reference: s.reference ?? "",
description: s.description ?? "",
specialityname: s.specialityname ?? "",
associatedelement: elementKey(s.associatedelement ?? ""),
heiType: heiKey(s.hei ?? ""),
heiCost: Number(s.heiCost ?? 0),
difficulty: Number(s.difficulty ?? 0),
realizationtimeritual: s.realizationtimeritual ?? "",
realizationtimeaccelerated: s.realizationtimeaccelerated ?? "",
flashback: s.flashback ?? "",
components: s.components ?? "",
effects: s.effects ?? "",
examples: s.examples ?? "",
notes: s.notes ?? "",
discipline: inferDiscipline(s.specialityname ?? "", oldItem.name ?? "")
}
};
}
function migrateSupernaturalItem(oldItem) {
const s = oldItem.system ?? {};
const nestedRef = s.supernatural?.reference ?? "";
return {
name: oldItem.name,
type: "supernatural",
img: oldItem.img || DEFAULT_ITEM_IMG,
system: {
reference: s.reference || nestedRef,
description: s.description ?? "",
notes: s.notes ?? "",
heiType: "yin",
heiCost: 0,
trigger: "",
effects: ""
}
};
}
function migrateItem(oldItem) {
switch (oldItem.type) {
case "item":
return migrateEquipmentItem(oldItem);
case "kungfu":
return migrateKungfuItem(oldItem);
case "spell":
return migrateSpellItem(oldItem);
case "supernatural":
return migrateSupernaturalItem(oldItem);
default:
return migrateEquipmentItem({ ...oldItem, type: "item" });
}
}
function migrateCharacter(old) {
const s = old.system ?? {};
const aspect = {};
for (const [k, v] of Object.entries(s.aspect ?? {})) {
aspect[k] = { chinese: v.chinese ?? "", label: v.label ?? "", value: Number(v.value ?? 0) };
}
const skills = {};
for (const [k, v] of Object.entries(s.skills ?? {})) {
skills[k] = { label: v.label ?? "", specialities: v.specialities ?? "", value: Number(v.value ?? 0) };
}
const resources = {};
for (const [k, v] of Object.entries(s.resources ?? {})) {
resources[k] = { label: v.label ?? "", specialities: v.specialities ?? "", value: Number(v.value ?? 0), debt: Boolean(v.debt) };
}
const component = {};
for (const [k, v] of Object.entries(s.component ?? {})) {
component[k] = { value: v.value ?? "" };
}
const MAGIC_SPECIALITIES = {
internalcinnabar: ["essence", "mind", "purification", "manipulation", "aura"],
alchemy: ["acupuncture", "elixirs", "poisons", "arsenal", "potions"],
masteryoftheway: ["curse", "transfiguration", "necromancy", "climatecontrol", "goldenmagic"],
exorcism: ["invocation", "tracking", "protection", "punishment", "domination"],
geomancy: ["neutralization", "divination", "earthlyprayer", "heavenlyprayer", "fungseoi"]
};
const magics = {};
for (const [school, specs] of Object.entries(MAGIC_SPECIALITIES)) {
const om = s.magics?.[school] ?? {};
const speciality = {};
for (const spec of specs) {
speciality[spec] = { check: Boolean(om.speciality?.[spec]?.check) };
}
magics[school] = { visible: Boolean(om.visible), value: Number(om.value ?? 0), speciality };
}
const tt = s.threetreasures ?? {};
const threetreasures = {
heiyang: { value: Number(tt.heiyang?.value ?? 0), max: Number(tt.heiyang?.max ?? 0) },
heiyin: { value: Number(tt.heiyin?.value ?? 0), max: Number(tt.heiyin?.max ?? 0) },
dicelevel: {
level0d: {
san: { value: Number(tt.dicelevel?.level0d?.san?.value ?? 0), max: Number(tt.dicelevel?.level0d?.san?.max ?? 0) },
zing: { value: Number(tt.dicelevel?.level0d?.zing?.value ?? 0), max: Number(tt.dicelevel?.level0d?.zing?.max ?? 0) }
},
level1d: {
san: { value: Number(tt.dicelevel?.level1d?.san?.value ?? 0), max: Number(tt.dicelevel?.level1d?.san?.max ?? 0) },
zing: { value: Number(tt.dicelevel?.level1d?.zing?.value ?? 0), max: Number(tt.dicelevel?.level1d?.zing?.max ?? 0) }
},
level2d: {
san: { value: Number(tt.dicelevel?.level2d?.san?.value ?? 0), max: Number(tt.dicelevel?.level2d?.san?.max ?? 0) },
zing: { value: Number(tt.dicelevel?.level2d?.zing?.value ?? 0), max: Number(tt.dicelevel?.level2d?.zing?.max ?? 0) }
}
}
};
const description = s.description || s.biography || "";
return {
name: old.name,
type: "character",
img: old.img || DEFAULT_ACTOR_IMG,
system: {
concept: s.concept ?? "",
guardian: parseInt(s.guardian ?? "0") || 0,
initiative: Number(s.initiative ?? 1),
anti_initiative: Number(s.anti_initiative ?? 24),
description,
aspect,
skills,
resources,
component,
magics,
threetreasures,
experience: {
value: Number(s.experience?.value ?? 0),
max: Number(s.experience?.max ?? 0),
min: Number(s.experience?.min ?? 0)
}
},
items: (old.items ?? []).map(migrateItem)
};
}
function migrateNpc(old) {
const s = old.system ?? {};
const aptitudes = {};
for (const [k, v] of Object.entries(s.aptitudes ?? {})) {
aptitudes[k] = { value: Number(v.value ?? 0), speciality: v.speciality ?? "" };
}
return {
name: old.name,
type: "npc",
img: old.img || DEFAULT_ACTOR_IMG,
system: {
type: s.type ?? "",
// Old system had separate `levelofthreat`/`powerofnuisance` as numbers
// and string copies `threat`/`nuisance` — use the numeric fields
threat: Number(s.levelofthreat ?? s.threat ?? 0),
nuisance: Number(s.powerofnuisance ?? s.nuisance ?? 0),
initiative: Number(s.initiative ?? 1),
anti_initiative: Number(s.anti_initiative ?? 24),
aptitudes,
vitality: {
value: Number(s.vitality?.value ?? 0),
calcul: Number(s.vitality?.calcul ?? 0),
note: s.vitality?.note ?? ""
},
hei: {
value: Number(s.hei?.value ?? 0),
calcul: Number(s.hei?.calcul ?? 0),
note: s.hei?.note ?? ""
},
description: s.description ?? ""
},
items: (old.items ?? []).map(migrateItem)
};
}
function migrateActor(oldJson) {
switch (oldJson.type) {
case "character":
return migrateCharacter(oldJson);
case "npc":
return migrateNpc(oldJson);
default:
throw new Error(`Unknown actor type "${oldJson.type}" in "${oldJson.name}"`);
}
}
function parseLegacyJson(jsonText) {
const parsed = JSON.parse(jsonText);
if (typeof parsed !== "object" || parsed === null) {
throw new Error("Le fichier JSON doit contenir un objet acteur ou un tableau d'acteurs");
}
const actors = Array.isArray(parsed) ? parsed : [parsed];
return actors.map(migrateActor);
}
// src/ui/apps/migration-app.js
var MIGRATION_TEMPLATE = "systems/fvtt-chroniques-de-l-etrange/templates/apps/cde-migration-app.html";
var CDEMigrationApp = class _CDEMigrationApp extends foundry.applications.api.HandlebarsApplicationMixin(
foundry.applications.api.ApplicationV2
) {
static DEFAULT_OPTIONS = {
id: "cde-migration-app",
classes: ["cde-migration-app"],
tag: "div",
window: {
title: "CDE.MigrationTitle",
icon: "fas fa-file-import",
resizable: false
},
position: { width: 560, height: "auto" },
actions: {
clearFiles: _CDEMigrationApp.#clearFiles,
doImport: _CDEMigrationApp.#doImport
}
};
static PARTS = {
form: { template: MIGRATION_TEMPLATE }
};
/** @type {Array<{name: string, type: string, img: string, system: object, items: object[], _srcFile: string}>} */
#pending = [];
/** @type {string[]} - error messages per file */
#errors = [];
async _prepareContext(options) {
return {
pending: this.#pending,
errors: this.#errors,
hasPending: this.#pending.length > 0,
hasErrors: this.#errors.length > 0,
count: this.#pending.length
};
}
/** After render, wire up the file input. */
_onRender(context, options) {
super._onRender(context, options);
const input = this.element.querySelector(".cde-migration-file-input");
input?.addEventListener("change", this.#onFileChange.bind(this));
const dropZone = this.element.querySelector(".cde-migration-drop-zone");
if (dropZone) {
dropZone.addEventListener("dragover", (e) => {
e.preventDefault();
dropZone.classList.add("is-dragover");
});
dropZone.addEventListener("dragleave", () => dropZone.classList.remove("is-dragover"));
dropZone.addEventListener("drop", (e) => {
e.preventDefault();
dropZone.classList.remove("is-dragover");
this.#processFiles(Array.from(e.dataTransfer.files));
});
}
}
async #onFileChange(event) {
const files = Array.from(event.target.files ?? []);
event.target.value = "";
await this.#processFiles(files);
}
async #processFiles(files) {
for (const file of files) {
if (!file.name.endsWith(".json")) {
this.#errors.push(game.i18n.format("CDE.MigrationErrorNotJson", { file: file.name }));
continue;
}
try {
const text = await file.text();
const actors = parseLegacyJson(text);
for (const actor of actors) {
actor._srcFile = file.name;
if (!this.#pending.some((p) => p.name === actor.name)) {
this.#pending.push(actor);
}
}
} catch (err) {
this.#errors.push(game.i18n.format("CDE.MigrationErrorParse", { file: file.name, error: err.message }));
}
}
this.render();
}
static async #clearFiles() {
this.#pending = [];
this.#errors = [];
this.render();
}
static async #doImport() {
if (!this.#pending.length) return;
const created = [];
const failed = [];
for (const data of this.#pending) {
try {
const { _srcFile, ...actorData } = data;
const actor = await Actor.create(actorData);
created.push(actor.name);
} catch (err) {
failed.push(`${data.name}: ${err.message}`);
console.error(`CHRONIQUESDELETRANGE | Migration failed for "${data.name}":`, err);
}
}
this.#pending = [];
this.#errors = failed;
this.render();
if (created.length) {
ui.notifications.info(
game.i18n.format("CDE.MigrationSuccess", { count: created.length, names: created.join(", ") })
);
}
if (failed.length) {
ui.notifications.warn(
game.i18n.format("CDE.MigrationPartialError", { count: failed.length })
);
}
}
};
// src/config/settings.js
function registerSettings() {
game.settings.registerMenu(SYSTEM_ID, "migrationTool", {
name: "CDE.MigrationTitle",
label: "CDE.MigrationMenuLabel",
hint: "CDE.MigrationMenuHint",
icon: "fas fa-file-import",
type: CDEMigrationApp,
restricted: true
});
game.settings.register(SYSTEM_ID, "loksyuData", {
scope: "world",
config: false,
@@ -154,9 +602,36 @@ function registerSettings() {
type: Number,
default: 0
});
game.settings.register(SYSTEM_ID, "welcomeSceneLoaded", {
scope: "world",
config: false,
type: Boolean,
default: false
});
}
async function migrateIfNeeded() {
}
async function loadWelcomeSceneIfNeeded() {
if (!game.user.isGM) return;
if (game.settings.get(SYSTEM_ID, "welcomeSceneLoaded")) return;
try {
const pack = game.packs.get(`${SYSTEM_ID}.cde-scenes`);
if (!pack) return;
const index = await pack.getIndex();
const entry = index.find((e) => e.name === "Accueil");
if (!entry) return;
const existing = game.scenes.find((s) => s.name === "Accueil");
let scene = existing;
if (!scene) {
const doc = await pack.getDocument(entry._id);
[scene] = await Scene.createDocuments([doc.toObject()]);
}
await game.settings.set(SYSTEM_ID, "welcomeSceneLoaded", true);
await scene.activate();
} catch (err) {
console.error("CHRONIQUESDELETRANGE | loadWelcomeSceneIfNeeded failed:", err);
}
}
// src/config/localize.js
function preLocalizeConfig() {
@@ -2570,6 +3045,47 @@ function refreshAllRollActions() {
});
}
// src/ui/apps/welcome.js
var HELP_JOURNAL_UUID = `Compendium.${SYSTEM_ID}.cde-help.JournalEntry.CDEGuideMain0001`;
async function showWelcomeMessage() {
const logo = `systems/${SYSTEM_ID}/images/logo_jeu.webp`;
const content = `
<div class="cde-welcome-message">
<img class="cde-welcome-logo" src="${logo}" alt="Les Chroniques de l'\xC9trange" />
<h2 class="cde-welcome-title">Les Chroniques de l'\xC9trange</h2>
<p class="cde-welcome-links">
Un jeu de r\xF4le \xE9dit\xE9 par
<a href="https://antre-monde.com/les-chroniques-de-letrengae/" target="_blank" rel="noopener">Antre-Monde \xC9ditions</a>
</p>
<p class="cde-welcome-links">
Syst\xE8me FoundryVTT r\xE9alis\xE9 par
<a href="https://www.uberwald.me" target="_blank" rel="noopener">LeRatierBretonnien</a>
</p>
<button type="button" class="cde-welcome-help-btn" data-action="open-cde-help">
<i class="fas fa-book-open"></i>
${game.i18n.localize("CDE.WelcomeOpenHelp")}
</button>
</div>`;
await ChatMessage.create({
content,
speaker: { alias: "Les Chroniques de l'\xC9trange" },
flags: { [SYSTEM_ID]: { welcome: true } }
});
}
function injectWelcomeActions(_message, html) {
const el = html instanceof HTMLElement ? html : html[0] ?? html;
const btn = el?.querySelector?.("[data-action='open-cde-help']");
if (!btn) return;
btn.addEventListener("click", async () => {
try {
const doc = await fromUuid(HELP_JOURNAL_UUID);
doc?.sheet?.render(true);
} catch {
game.packs.get(`${SYSTEM_ID}.cde-help`)?.render(true);
}
});
}
// src/system.js
Hooks.once("i18nInit", preLocalizeConfig);
Hooks.once("init", async () => {
@@ -2655,7 +3171,9 @@ Hooks.once("init", async () => {
});
Hooks.once("ready", async () => {
await migrateIfNeeded();
await loadWelcomeSceneIfNeeded();
CDEWheelApp.registerHooks();
if (game.user.isGM) showWelcomeMessage();
});
Hooks.on("renderChatLog", (_app, html) => {
const el = html instanceof HTMLElement ? html : html[0] ?? html;
@@ -2685,6 +3203,7 @@ Hooks.on("renderChatLog", (_app, html) => {
});
Hooks.on("renderChatMessageHTML", (message, html) => {
injectRollActions(message, html);
if (message.flags?.[SYSTEM_ID]?.welcome) injectWelcomeActions(message, html);
});
Hooks.on("updateSetting", (setting) => {
if (!setting.key) return;
+3 -3
View File
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1010 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1024 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 918 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 962 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 968 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 986 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 902 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 939 KiB

+19 -1
View File
@@ -131,6 +131,23 @@
"CDE.InitiativeNPCSpeciality": "Première action (Aptitude) que vous escomptez effectuer",
"CDE.InitiativeRoll": "Jet d'initiative",
"CDE.InitiativeSpeciality": "Première action (Compétence) que vous escomptez effectuer",
"CDE.MigrationTitle": "Migration depuis l'ancien système",
"CDE.MigrationMenuLabel": "Importer des personnages",
"CDE.MigrationMenuHint": "Importer des fiches de personnage depuis l'ancien système CDE",
"CDE.MigrationHint": "Glissez-déposez des fichiers JSON ou cliquez pour les sélectionner.",
"CDE.MigrationDropHint": "Déposez vos fichiers JSON ici",
"CDE.MigrationChooseFiles": "Choisir des fichiers",
"CDE.MigrationPreviewTitle": "Personnages à importer",
"CDE.MigrationClear": "Vider",
"CDE.MigrationColName": "Nom",
"CDE.MigrationColType": "Type",
"CDE.MigrationColItems": "Objets",
"CDE.MigrationColFile": "Fichier source",
"CDE.MigrationImport": "Importer",
"CDE.MigrationSuccess": "{count} personnage(s) importé(s) : {names}",
"CDE.MigrationPartialError": "{count} personnage(s) n'ont pas pu être importés.",
"CDE.MigrationErrorNotJson": "Le fichier « {file} » n'est pas un fichier JSON.",
"CDE.MigrationErrorParse": "Erreur lors de la lecture de « {file} » : {error}",
"CDE.InitiativeWheel": "Roue d'Initiative",
"CDE.InitiativeWheelOpen": "Ouvrir la Roue d'Initiative",
"CDE.InitiativeWheelHint": "Roue d'initiative Les Chroniques de l'Étrange",
@@ -415,5 +432,6 @@
"CDE.TotalDamage": "Dommages",
"CDE.WeaponRoll": "Jet d'arme",
"CDE.RangePenalty": "Pénalité de portée",
"CDE.SuccessTimesDamage": "succès × dégâts de base"
"CDE.SuccessTimesDamage": "succès × dégâts de base",
"CDE.WelcomeOpenHelp": "Ouvrir l'aide en ligne"
}
+293
View File
@@ -0,0 +1,293 @@
{
"_id": "CDEGuideMain0001",
"_key": "!journal!CDEGuideMain0001",
"name": "Guide du Système CDE",
"pages": [
{
"_id": "CDEHelpP01Intro",
"_key": "!journal.pages!CDEGuideMain0001.CDEHelpP01Intro",
"name": "Bienvenue dans CDE",
"type": "text",
"text": {
"content": "\n<h1>Bienvenue dans les Chroniques de l'Étrange</h1>\n<p>Ce guide vous présente l'interface du système FoundryVTT pour <em>Chroniques de l'Étrange</em> (CDE), jeu de rôle d'enquête et d'action surnaturelle dans le Hong Kong contemporain, édité par Antre-Monde Éditions.</p>\n<h2>Structure du système</h2>\n<ul>\n<li><strong>Fiches de personnage (Fat Si)</strong> — Héros joueurs avec cinq aspects Wu Xing, compétences, Trois Trésors et équipement.</li>\n<li><strong>Fiches de PNJ</strong> — Créatures, dieux, fantômes et humains à l'usage du MJ.</li>\n<li><strong>Compendiums</strong> — Arts martiaux, sortilèges, équipements, PNJs, capacités surnaturelles, ingrédients, San Hei.</li>\n<li><strong>Outils de MJ</strong> — Roue d'initiative, compteurs Loksyu/Tin Ji, outil de migration.</li>\n</ul>\n<p>Naviguez dans ce journal via les onglets de page pour découvrir chaque aspect du système.</p>\n",
"format": 1,
"markdown": ""
},
"sort": 100000,
"title": {
"show": true,
"level": 1
},
"image": {
"caption": ""
},
"video": {
"controls": true,
"volume": 0.5
},
"src": null,
"flags": {},
"ownership": {
"default": -1
}
},
{
"_id": "CDEHelpP02WuXin",
"_key": "!journal.pages!CDEGuideMain0001.CDEHelpP02WuXin",
"name": "Le Cycle Wu Xing",
"type": "text",
"text": {
"content": "\n<h1>Le Cycle Wu Xing (五行)</h1>\n<p>Le Wu Xing est le cœur du système de résolution. Cinq aspects — <strong>Métal (㊎)</strong>, <strong>Eau (㊌)</strong>, <strong>Terre (㊏)</strong>, <strong>Feu (㊋)</strong> et <strong>Bois (㊍)</strong> — définissent les capacités d'un personnage.</p>\n<h2>Faces des d10 et résultats</h2>\n<p>Chaque aspect est associé à deux faces du d10. Lors d'un jet, vous déclarez l'aspect utilisé avant de lancer vos dés. Chaque dé donne un résultat :</p>\n<ul>\n<li><strong>Succès</strong> — avancez vers votre objectif.</li>\n<li><strong>Dés-fastes (吉)</strong> — résultats favorables supplémentaires.</li>\n<li><strong>Dés-néfastes (凶)</strong> — complications.</li>\n<li><strong>Loksyu (落穗)</strong> — alimentent le compteur mondial de chance collective.</li>\n<li><strong>Tin Ji (天機)</strong> — alimentent le compteur de destin.</li>\n</ul>\n<h2>Correspondances</h2>\n<table>\n<thead><tr><th>Aspect</th><th>Faces d10</th><th>Caractère</th></tr></thead>\n<tbody>\n<tr><td>㊎ Métal</td><td>1 &amp; 6</td><td>Agressif, passionné, combatif</td></tr>\n<tr><td>㊌ Eau</td><td>2 &amp; 7</td><td>Souple, appliqué, adaptable</td></tr>\n<tr><td>㊏ Terre</td><td>3 &amp; 8</td><td>Obstiné, résilient, endurant</td></tr>\n<tr><td>㊋ Feu</td><td>4 &amp; 9</td><td>Chaleureux, créatif, empathique</td></tr>\n<tr><td>㊍ Bois</td><td>5 &amp; 10</td><td>Intuitif, observateur, instinctif</td></tr>\n</tbody>\n</table>\n<p><em>La valeur d'un aspect (de 1 à 5) détermine le nombre de dés que vous lancez.</em></p>\n",
"format": 1,
"markdown": ""
},
"sort": 200000,
"title": {
"show": true,
"level": 1
},
"image": {
"caption": ""
},
"video": {
"controls": true,
"volume": 0.5
},
"src": null,
"flags": {},
"ownership": {
"default": -1
}
},
{
"_id": "CDEHelpP03Sheet",
"_key": "!journal.pages!CDEGuideMain0001.CDEHelpP03Sheet",
"name": "La Fiche de Personnage",
"type": "text",
"text": {
"content": "\n<h1>La Fiche de Personnage (Fat Si)</h1>\n<figure><img src=\"systems/fvtt-chroniques-de-l-etrange/images/ui/character-sheet-nghang.png\" alt=\"Fiche de personnage — onglet Ng Hang\"/><figcaption>Onglet Ng Hang : les cinq aspects Wu Xing</figcaption></figure>\n<p>La fiche de personnage se décompose en sept onglets :</p>\n<ol>\n<li><strong>Description</strong> — Biographie, concept, gardien céleste.</li>\n<li><strong>Ng Hang</strong> — Les cinq aspects Wu Xing (valeur 15). Cliquez sur l'image du dé pour lancer.</li>\n<li><strong>Compétences</strong> — Les compétences générales et ressources.</li>\n<li><strong>Trois Trésors</strong> — Hei-Yang, Hei-Yin et les niveaux de dés.</li>\n<li><strong>Magies</strong> — Les cinq écoles de magie et leurs sortilèges.</li>\n<li><strong>Kung Fu</strong> — Arts martiaux possédés.</li>\n<li><strong>Équipement</strong> — Objets portés.</li>\n</ol>\n<h2>En-tête de fiche</h2>\n<p>En haut de la fiche se trouvent le <strong>concept du personnage</strong>, son <strong>gardien céleste</strong> (aspect dominant) et la zone d'<strong>initiative</strong> avec les boutons ±.</p>\n<figure><img src=\"systems/fvtt-chroniques-de-l-etrange/images/ui/character-sheet-skills.png\" alt=\"Fiche de personnage — onglet Compétences\"/><figcaption>Onglet Compétences : compétences et ressources</figcaption></figure>\n",
"format": 1,
"markdown": ""
},
"sort": 300000,
"title": {
"show": true,
"level": 1
},
"image": {
"caption": ""
},
"video": {
"controls": true,
"volume": 0.5
},
"src": null,
"flags": {},
"ownership": {
"default": -1
}
},
{
"_id": "CDEHelpP04Treas",
"_key": "!journal.pages!CDEGuideMain0001.CDEHelpP04Treas",
"name": "Les Trois Trésors",
"type": "text",
"text": {
"content": "\n<h1>Les Trois Trésors (三寶)</h1>\n<figure><img src=\"systems/fvtt-chroniques-de-l-etrange/images/ui/character-sheet-treasures.png\" alt=\"Fiche de personnage — onglet Trois Trésors\"/><figcaption>Onglet Trois Trésors : Hei-Yang, Hei-Yin et niveaux de dés</figcaption></figure>\n<p>Les Trois Trésors représentent les réserves d'énergie vitale du personnage :</p>\n<h2>Hei-Yang (陽氣) et Hei-Yin (陰氣)</h2>\n<p>Ce sont les deux jauges de vitalité. Le Hei-Yang représente l'énergie active, le Hei-Yin l'énergie passive. Ensemble, ils forment le <strong>token attribute</strong> visible sur la carte.</p>\n<h2>Niveaux de dés</h2>\n<p>Les niveaux de dés (d4 → d6 → d8 → d10 → d12) reflètent la progression du personnage dans un aspect. Chaque niveau de dé confère un bonus ou un avantage supplémentaire.</p>\n<h2>Blessures</h2>\n<p>Les blessures s'accumulent et imposent des malus croissants :</p>\n<ul>\n<li>Blessé : 1 dé à tous les jets</li>\n<li>Gravement blessé : 2 dés</li>\n<li>État critique : 3 dés</li>\n</ul>\n",
"format": 1,
"markdown": ""
},
"sort": 400000,
"title": {
"show": true,
"level": 1
},
"image": {
"caption": ""
},
"video": {
"controls": true,
"volume": 0.5
},
"src": null,
"flags": {},
"ownership": {
"default": -1
}
},
{
"_id": "CDEHelpP05Magic",
"_key": "!journal.pages!CDEGuideMain0001.CDEHelpP05Magic",
"name": "Magie",
"type": "text",
"text": {
"content": "\n<h1>Magie</h1>\n<figure><img src=\"systems/fvtt-chroniques-de-l-etrange/images/ui/character-sheet-magics.png\" alt=\"Fiche de personnage — onglet Magies\"/><figcaption>Onglet Magies : les cinq écoles et leurs sortilèges</figcaption></figure>\n<p>CDE dispose de cinq écoles de magie, chacune divisée en cinq spécialités :</p>\n<ul>\n<li><strong>Cinabre Interne</strong> (内丹) — magie du souffle et du corps.</li>\n<li><strong>Alchimie</strong> (外丹) — préparations, potions et talismans matériels.</li>\n<li><strong>Maîtrise du Tao</strong> (道術) — maîtrise des principes cosmiques.</li>\n<li><strong>Exorcisme</strong> (驅魔) — combat contre les entités surnaturelles.</li>\n<li><strong>Géomancie</strong> (風水) — magie des lieux et de l'environnement.</li>\n</ul>\n<h2>Utiliser un sortilège</h2>\n<ol>\n<li>Cliquez sur l'icône dé du sort dans l'onglet Magies.</li>\n<li>Un dialog apparaît avec l'aspect associé, le coût en Hei et le nombre de dés.</li>\n<li>Validez pour effectuer le jet.</li>\n</ol>\n<p>Les sortilèges sont importés depuis le compendium <em>Sortilèges</em> et glissés sur la fiche.</p>\n",
"format": 1,
"markdown": ""
},
"sort": 500000,
"title": {
"show": true,
"level": 1
},
"image": {
"caption": ""
},
"video": {
"controls": true,
"volume": 0.5
},
"src": null,
"flags": {},
"ownership": {
"default": -1
}
},
{
"_id": "CDEHelpP06KungF",
"_key": "!journal.pages!CDEGuideMain0001.CDEHelpP06KungF",
"name": "Arts Martiaux",
"type": "text",
"text": {
"content": "\n<h1>Arts Martiaux (武術)</h1>\n<figure><img src=\"systems/fvtt-chroniques-de-l-etrange/images/ui/character-sheet-kungfu.png\" alt=\"Fiche de personnage — onglet Kung Fu\"/><figcaption>Onglet Kung Fu : arts martiaux possédés</figcaption></figure>\n<p>Les arts martiaux représentent les techniques de combat du personnage. Chaque art martial possède :</p>\n<ul>\n<li><strong>Un mode d'activation</strong> : passif, action d'attaque, réaction, etc.</li>\n<li><strong>Une description</strong> des effets en jeu.</li>\n</ul>\n<h2>Importer un art martial</h2>\n<p>Ouvrez le compendium <em>Arts Martiaux</em> et faites glisser une technique sur la fiche du personnage. Elle apparaît alors dans l'onglet Kung Fu.</p>\n<h2>Types d'activation</h2>\n<table>\n<thead><tr><th>Type</th><th>Déclencheur</th></tr></thead>\n<tbody>\n<tr><td>Passif (dés)</td><td>Toujours actif</td></tr>\n<tr><td>Action d'attaque</td><td>Lors d'une attaque</td></tr>\n<tr><td>Action de défense</td><td>Lors d'une défense</td></tr>\n<tr><td>Réaction</td><td>En réponse à un événement</td></tr>\n<tr><td>Dégâts infligés</td><td>Quand vous blessez</td></tr>\n</tbody>\n</table>\n",
"format": 1,
"markdown": ""
},
"sort": 600000,
"title": {
"show": true,
"level": 1
},
"image": {
"caption": ""
},
"video": {
"controls": true,
"volume": 0.5
},
"src": null,
"flags": {},
"ownership": {
"default": -1
}
},
{
"_id": "CDEHelpP07Items",
"_key": "!journal.pages!CDEGuideMain0001.CDEHelpP07Items",
"name": "Équipement & Inventaire",
"type": "text",
"text": {
"content": "\n<h1>Équipement &amp; Inventaire</h1>\n<figure><img src=\"systems/fvtt-chroniques-de-l-etrange/images/ui/character-sheet-items.png\" alt=\"Fiche de personnage — onglet Équipement\"/><figcaption>Onglet Équipement : objets portés</figcaption></figure>\n<p>L'onglet Équipement liste tout ce que porte le personnage. Les objets sont classés en plusieurs catégories :</p>\n<ul>\n<li><strong>Armes</strong> — avec dégâts, distance et type.</li>\n<li><strong>Protections</strong> — armures et protections spirituelles.</li>\n<li><strong>San Hei (三氣)</strong> — objets magiques à charges.</li>\n<li><strong>Ingrédients</strong> — matériaux pour l'alchimie.</li>\n<li><strong>Équipement générique</strong> — tout autre objet.</li>\n</ul>\n<h2>Ajouter un objet</h2>\n<p>Faites glisser un objet depuis un compendium (Armes, Protections, San Hei, Ingrédients, Équipements) ou créez-en un avec le bouton <em>Créer</em> correspondant.</p>\n<p>Cliquez sur l'image d'un objet pour ouvrir sa fiche détaillée.</p>\n",
"format": 1,
"markdown": ""
},
"sort": 700000,
"title": {
"show": true,
"level": 1
},
"image": {
"caption": ""
},
"video": {
"controls": true,
"volume": 0.5
},
"src": null,
"flags": {},
"ownership": {
"default": -1
}
},
{
"_id": "CDEHelpP08NPCSH",
"_key": "!journal.pages!CDEGuideMain0001.CDEHelpP08NPCSH",
"name": "Les PNJ",
"type": "text",
"text": {
"content": "\n<h1>Les Personnages Non-Joueurs (PNJ)</h1>\n<figure><img src=\"systems/fvtt-chroniques-de-l-etrange/images/ui/npc-sheet.png\" alt=\"Fiche de PNJ\"/><figcaption>Fiche PNJ : type, nuisance, menace et aptitudes</figcaption></figure>\n<p>Les PNJ ont une fiche simplifiée par rapport aux personnages joueurs.</p>\n<h2>Caractéristiques</h2>\n<ul>\n<li><strong>Type de créature</strong> : Mortel, Démon, Esprit, Esprit animal, Fantôme, Jiugwaai, Dieu/Divinité.</li>\n<li><strong>Capacité de nuisance</strong> : Figurant, Sbire, Adversaire, Allié, Boss, Divinité.</li>\n<li><strong>Niveau de menace</strong> : Profane → Apprenti → Initié → Accompli → Renommé.</li>\n</ul>\n<h2>Aptitudes</h2>\n<p>Les PNJ ont quatre aptitudes (Physique, Martiale, Mentale, Sociale) avec une spécialité optionnelle chacune.</p>\n<h2>Capacités surnaturelles</h2>\n<p>Les PNJ peuvent avoir des capacités importées depuis le compendium <em>Capacités Surnaturelles</em>.</p>\n",
"format": 1,
"markdown": ""
},
"sort": 800000,
"title": {
"show": true,
"level": 1
},
"image": {
"caption": ""
},
"video": {
"controls": true,
"volume": 0.5
},
"src": null,
"flags": {},
"ownership": {
"default": -1
}
},
{
"_id": "CDEHelpP09Initi",
"_key": "!journal.pages!CDEGuideMain0001.CDEHelpP09Initi",
"name": "Initiative & Combat",
"type": "text",
"text": {
"content": "\n<h1>Initiative &amp; Combat</h1>\n<figure><img src=\"systems/fvtt-chroniques-de-l-etrange/images/ui/initiative-wheel.png\" alt=\"Roue d'initiative\"/><figcaption>La Roue d'Initiative : 24 crans, couleurs Wu Xing</figcaption></figure>\n<h2>La Roue d'Initiative</h2>\n<p>La roue est un cercle de <strong>24 crans</strong> numérotés. L'initiative de chaque personnage est calculée comme suit :</p>\n<ul>\n<li><strong>Personnage joueur</strong> : Prouesse + valeur de compétence de la première action.</li>\n<li><strong>PNJ</strong> : Aptitude physique + aptitude de la première action.</li>\n</ul>\n<p>Les crans sont colorés selon le cycle Wu Xing (4 crans par couleur, 6 couleurs). <strong>Un effet qui dure 6 crans</strong> court jusqu'au prochain cran de la même couleur.</p>\n<h2>Ordre d'action</h2>\n<p>Les personnages agissent <strong>du numéro le plus élevé au plus bas</strong>. Après chaque action, le jeton avance dans le sens horaire du <strong>coût de l'action</strong> :</p>\n<table>\n<thead><tr><th>Action</th><th>Coût (crans)</th></tr></thead>\n<tbody>\n<tr><td>Défense</td><td>1</td></tr>\n<tr><td>Déplacement</td><td>2</td></tr>\n<tr><td>Attaque</td><td>3</td></tr>\n<tr><td>Retarder</td><td>6</td></tr>\n</tbody>\n</table>\n<h2>Accès à la roue</h2>\n<p>Ouvrez la roue depuis la barre latérale du chat (icône roue) ou via la console : <code>game.cde.CDEWheelApp.open()</code>.</p>\n",
"format": 1,
"markdown": ""
},
"sort": 900000,
"title": {
"show": true,
"level": 1
},
"image": {
"caption": ""
},
"video": {
"controls": true,
"volume": 0.5
},
"src": null,
"flags": {},
"ownership": {
"default": -1
}
},
{
"_id": "CDEHelpP10Extra",
"_key": "!journal.pages!CDEGuideMain0001.CDEHelpP10Extra",
"name": "Loksyu, Tin Ji & Migration",
"type": "text",
"text": {
"content": "\n<h1>Loksyu, Tin Ji &amp; Outils de MJ</h1>\n<h2>Loksyu (落穗) et Tin Ji (天機)</h2>\n<figure><img src=\"systems/fvtt-chroniques-de-l-etrange/images/ui/loksyu-app.png\" alt=\"Application Loksyu\"/><figcaption>Application Loksyu : compteurs Yin/Yang et Tin Ji</figcaption></figure>\n<p>Ces deux compteurs sont <strong>partagés entre tous les joueurs</strong> et le MJ :</p>\n<ul>\n<li><strong>Loksyu</strong> — Se divise en Yin et Yang. Les jets de dés alimentent ces compteurs selon les résultats. Les joueurs peuvent puiser dans le Loksyu pour améliorer leurs jets.</li>\n<li><strong>Tin Ji</strong> — Le compteur de destin. Peut être dépensé pour des effets exceptionnels.</li>\n</ul>\n<p>Accédez via la barre du chat ou : <code>game.cde.CDELoksyuApp.open()</code></p>\n<h2>Migration de l'ancien système</h2>\n<figure><img src=\"systems/fvtt-chroniques-de-l-etrange/images/ui/migration-dialog.png\" alt=\"Outil de migration\"/><figcaption>Outil de migration : importation depuis l'ancien système</figcaption></figure>\n<p>Si vous possédez des fiches de personnage créées dans l'ancien système CDE (non maintenu), l'outil de migration les convertit automatiquement :</p>\n<ol>\n<li>Ouvrez <strong>Paramètres de la partie → Paramètres du système → Importer des personnages</strong>.</li>\n<li>Glissez les fichiers JSON des anciens personnages dans la zone de dépôt.</li>\n<li>Vérifiez l'aperçu et cliquez <strong>Importer</strong>.</li>\n</ol>\n<p>Les personnages migrés apparaissent dans la liste des Acteurs.</p>\n",
"format": 1,
"markdown": ""
},
"sort": 1000000,
"title": {
"show": true,
"level": 1
},
"image": {
"caption": ""
},
"video": {
"controls": true,
"volume": 0.5
},
"src": null,
"flags": {},
"ownership": {
"default": -1
}
}
],
"sort": 0,
"ownership": {
"default": 0
},
"flags": {},
"folder": null
}
Binary file not shown.
Binary file not shown.
View File
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000006
MANIFEST-000035
+7 -3
View File
@@ -1,3 +1,7 @@
2026/04/27-20:01:11.390845 7fed927fc6c0 Recovering log #4
2026/04/27-20:01:11.400505 7fed927fc6c0 Delete type=3 #2
2026/04/27-20:01:11.400599 7fed927fc6c0 Delete type=0 #4
2026/05/06-22:32:13.366106 7fe44efef6c0 Recovering log #33
2026/05/06-22:32:13.376874 7fe44efef6c0 Delete type=3 #31
2026/05/06-22:32:13.376974 7fe44efef6c0 Delete type=0 #33
2026/05/06-22:37:23.617629 7fe44cfeb6c0 Level-0 table #38: started
2026/05/06-22:37:23.617668 7fe44cfeb6c0 Level-0 table #38: 0 bytes OK
2026/05/06-22:37:23.624147 7fe44cfeb6c0 Delete type=0 #36
2026/05/06-22:37:23.643567 7fe44cfeb6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
-5
View File
@@ -1,5 +0,0 @@
2026/04/27-17:47:13.055628 7f2779bff6c0 Delete type=3 #1
2026/04/27-17:47:13.058468 7f272b7fe6c0 Level-0 table #5: started
2026/04/27-17:47:13.061813 7f272b7fe6c0 Level-0 table #5: 1330 bytes OK
2026/04/27-17:47:13.067956 7f272b7fe6c0 Delete type=0 #3
2026/04/27-17:47:13.068111 7f272b7fe6c0 Manual compaction at level-0 from '!items!3aig6MWvZCRoWXPW' @ 72057594037927935 : 1 .. '!items!cXaQG1TBE0jzrbNt' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
View File
Binary file not shown.
View File
+1
View File
@@ -0,0 +1 @@
MANIFEST-000023
View File
+8
View File
@@ -0,0 +1,8 @@
2026/05/06-22:32:13.427495 7fe44efef6c0 Recovering log #21
2026/05/06-22:32:13.437731 7fe44efef6c0 Delete type=3 #19
2026/05/06-22:32:13.437796 7fe44efef6c0 Delete type=0 #21
2026/05/06-22:37:23.643733 7fe44cfeb6c0 Level-0 table #26: started
2026/05/06-22:37:23.643771 7fe44cfeb6c0 Level-0 table #26: 0 bytes OK
2026/05/06-22:37:23.650510 7fe44cfeb6c0 Delete type=0 #24
2026/05/06-22:37:23.669664 7fe44cfeb6c0 Manual compaction at level-0 from '!journal!CDEGuideMain0001' @ 72057594037927935 : 1 .. '!journal.pages!CDEGuideMain0001.x83SZpLrbEi96PVQ' @ 0 : 0; will stop at (end)
2026/05/06-22:37:23.669706 7fe44cfeb6c0 Manual compaction at level-1 from '!journal!CDEGuideMain0001' @ 72057594037927935 : 1 .. '!journal.pages!CDEGuideMain0001.x83SZpLrbEi96PVQ' @ 0 : 0; will stop at (end)
+8
View File
@@ -0,0 +1,8 @@
2026/05/06-22:24:45.849795 7fe44efef6c0 Recovering log #17
2026/05/06-22:24:45.867206 7fe44efef6c0 Delete type=3 #15
2026/05/06-22:24:45.867306 7fe44efef6c0 Delete type=0 #17
2026/05/06-22:30:22.773890 7fe44cfeb6c0 Level-0 table #22: started
2026/05/06-22:30:22.773918 7fe44cfeb6c0 Level-0 table #22: 0 bytes OK
2026/05/06-22:30:22.780047 7fe44cfeb6c0 Delete type=0 #20
2026/05/06-22:30:22.798899 7fe44cfeb6c0 Manual compaction at level-0 from '!journal!CDEGuideMain0001' @ 72057594037927935 : 1 .. '!journal.pages!CDEGuideMain0001.x83SZpLrbEi96PVQ' @ 0 : 0; will stop at (end)
2026/05/06-22:30:22.798944 7fe44cfeb6c0 Manual compaction at level-1 from '!journal!CDEGuideMain0001' @ 72057594037927935 : 1 .. '!journal.pages!CDEGuideMain0001.x83SZpLrbEi96PVQ' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
Binary file not shown.
View File
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000006
MANIFEST-000035
+7 -3
View File
@@ -1,3 +1,7 @@
2026/04/27-20:01:11.418750 7fed93fff6c0 Recovering log #4
2026/04/27-20:01:11.428738 7fed93fff6c0 Delete type=3 #2
2026/04/27-20:01:11.428793 7fed93fff6c0 Delete type=0 #4
2026/05/06-22:32:13.390543 7fe44efef6c0 Recovering log #33
2026/05/06-22:32:13.400800 7fe44efef6c0 Delete type=3 #31
2026/05/06-22:32:13.400881 7fe44efef6c0 Delete type=0 #33
2026/05/06-22:37:23.636590 7fe44cfeb6c0 Level-0 table #38: started
2026/05/06-22:37:23.636613 7fe44cfeb6c0 Level-0 table #38: 0 bytes OK
2026/05/06-22:37:23.643393 7fe44cfeb6c0 Delete type=0 #36
2026/05/06-22:37:23.643628 7fe44cfeb6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
+7 -5
View File
@@ -1,5 +1,7 @@
2026/04/27-17:47:13.085519 7f27793fe6c0 Delete type=3 #1
2026/04/27-17:47:13.087023 7f272b7fe6c0 Level-0 table #5: started
2026/04/27-17:47:13.091030 7f272b7fe6c0 Level-0 table #5: 5923 bytes OK
2026/04/27-17:47:13.097545 7f272b7fe6c0 Delete type=0 #3
2026/04/27-17:47:13.097759 7f272b7fe6c0 Manual compaction at level-0 from '!items!0NDBw1YB54q3hLH0' @ 72057594037927935 : 1 .. '!items!ykekdZlirabRobEF' @ 0 : 0; will stop at (end)
2026/05/06-22:24:45.793005 7fe44efef6c0 Recovering log #29
2026/05/06-22:24:45.808875 7fe44efef6c0 Delete type=3 #27
2026/05/06-22:24:45.808947 7fe44efef6c0 Delete type=0 #29
2026/05/06-22:30:22.754997 7fe44cfeb6c0 Level-0 table #34: started
2026/05/06-22:30:22.755020 7fe44cfeb6c0 Level-0 table #34: 0 bytes OK
2026/05/06-22:30:22.760743 7fe44cfeb6c0 Delete type=0 #32
2026/05/06-22:30:22.773745 7fe44cfeb6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
View File
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000006
MANIFEST-000035
+7 -3
View File
@@ -1,3 +1,7 @@
2026/04/27-20:01:11.433002 7fed92ffd6c0 Recovering log #4
2026/04/27-20:01:11.442974 7fed92ffd6c0 Delete type=3 #2
2026/04/27-20:01:11.443041 7fed92ffd6c0 Delete type=0 #4
2026/05/06-22:32:13.402637 7fe44e7ee6c0 Recovering log #33
2026/05/06-22:32:13.413187 7fe44e7ee6c0 Delete type=3 #31
2026/05/06-22:32:13.413261 7fe44e7ee6c0 Delete type=0 #33
2026/05/06-22:37:23.630418 7fe44cfeb6c0 Level-0 table #38: started
2026/05/06-22:37:23.630444 7fe44cfeb6c0 Level-0 table #38: 0 bytes OK
2026/05/06-22:37:23.636504 7fe44cfeb6c0 Delete type=0 #36
2026/05/06-22:37:23.643610 7fe44cfeb6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
+7 -5
View File
@@ -1,5 +1,7 @@
2026/04/27-17:47:13.116591 7f272bfff6c0 Delete type=3 #1
2026/04/27-17:47:13.117666 7f272b7fe6c0 Level-0 table #5: started
2026/04/27-17:47:13.121072 7f272b7fe6c0 Level-0 table #5: 559 bytes OK
2026/04/27-17:47:13.127453 7f272b7fe6c0 Delete type=0 #3
2026/04/27-17:47:13.127641 7f272b7fe6c0 Manual compaction at level-0 from '!items!HKq5ANSGiBIdcnki' @ 72057594037927935 : 1 .. '!items!HKq5ANSGiBIdcnki' @ 0 : 0; will stop at (end)
2026/05/06-22:24:45.812009 7fe44dfed6c0 Recovering log #29
2026/05/06-22:24:45.827425 7fe44dfed6c0 Delete type=3 #27
2026/05/06-22:24:45.827509 7fe44dfed6c0 Delete type=0 #29
2026/05/06-22:30:22.760828 7fe44cfeb6c0 Level-0 table #34: started
2026/05/06-22:30:22.760852 7fe44cfeb6c0 Level-0 table #34: 0 bytes OK
2026/05/06-22:30:22.766828 7fe44cfeb6c0 Delete type=0 #32
2026/05/06-22:30:22.773753 7fe44cfeb6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
View File
Binary file not shown.
Binary file not shown.
View File
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000006
MANIFEST-000035
+7 -3
View File
@@ -1,3 +1,7 @@
2026/04/27-20:01:11.327150 7fed93fff6c0 Recovering log #4
2026/04/27-20:01:11.338223 7fed93fff6c0 Delete type=3 #2
2026/04/27-20:01:11.338311 7fed93fff6c0 Delete type=0 #4
2026/05/06-22:32:13.317725 7fe44efef6c0 Recovering log #33
2026/05/06-22:32:13.328138 7fe44efef6c0 Delete type=3 #31
2026/05/06-22:32:13.328231 7fe44efef6c0 Delete type=0 #33
2026/05/06-22:37:23.590888 7fe44cfeb6c0 Level-0 table #38: started
2026/05/06-22:37:23.590952 7fe44cfeb6c0 Level-0 table #38: 0 bytes OK
2026/05/06-22:37:23.597751 7fe44cfeb6c0 Delete type=0 #36
2026/05/06-22:37:23.617391 7fe44cfeb6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
+7 -5
View File
@@ -1,5 +1,7 @@
2026/04/27-17:47:13.145290 7f2778bfd6c0 Delete type=3 #1
2026/04/27-17:47:13.146592 7f272b7fe6c0 Level-0 table #5: started
2026/04/27-17:47:13.150681 7f272b7fe6c0 Level-0 table #5: 32988 bytes OK
2026/04/27-17:47:13.157088 7f272b7fe6c0 Delete type=0 #3
2026/04/27-17:47:13.157210 7f272b7fe6c0 Manual compaction at level-0 from '!items!2nKXEHLG0fXtSOdy' @ 72057594037927935 : 1 .. '!items!tlIc1bmIAbQeUwj7' @ 0 : 0; will stop at (end)
2026/05/06-22:24:45.678133 7fe44d7ec6c0 Recovering log #29
2026/05/06-22:24:45.699614 7fe44d7ec6c0 Delete type=3 #27
2026/05/06-22:24:45.699668 7fe44d7ec6c0 Delete type=0 #29
2026/05/06-22:30:22.729240 7fe44cfeb6c0 Level-0 table #34: started
2026/05/06-22:30:22.729273 7fe44cfeb6c0 Level-0 table #34: 0 bytes OK
2026/05/06-22:30:22.735324 7fe44cfeb6c0 Delete type=0 #32
2026/05/06-22:30:22.748747 7fe44cfeb6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
View File
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000006
MANIFEST-000038
+7 -3
View File
@@ -1,3 +1,7 @@
2026/04/27-20:01:11.445769 7fed937fe6c0 Recovering log #4
2026/04/27-20:01:11.456194 7fed937fe6c0 Delete type=3 #2
2026/04/27-20:01:11.456260 7fed937fe6c0 Delete type=0 #4
2026/05/06-22:32:13.415167 7fe44efef6c0 Recovering log #36
2026/05/06-22:32:13.424997 7fe44efef6c0 Delete type=3 #34
2026/05/06-22:32:13.425065 7fe44efef6c0 Delete type=0 #36
2026/05/06-22:37:23.794167 7fe44cfeb6c0 Level-0 table #41: started
2026/05/06-22:37:23.794211 7fe44cfeb6c0 Level-0 table #41: 0 bytes OK
2026/05/06-22:37:23.800534 7fe44cfeb6c0 Delete type=0 #39
2026/05/06-22:37:23.800731 7fe44cfeb6c0 Manual compaction at level-0 from '!actors!4ZjFZ1HoJV9mJStt' @ 72057594037927935 : 1 .. '!actors!zVpmacwoWEG8YTCQ' @ 0 : 0; will stop at (end)
+7 -5
View File
@@ -1,5 +1,7 @@
2026/04/27-17:47:13.175381 7f2778bfd6c0 Delete type=3 #1
2026/04/27-17:47:13.176524 7f272b7fe6c0 Level-0 table #5: started
2026/04/27-17:47:13.180270 7f272b7fe6c0 Level-0 table #5: 21686 bytes OK
2026/04/27-17:47:13.186334 7f272b7fe6c0 Delete type=0 #3
2026/04/27-17:47:13.186548 7f272b7fe6c0 Manual compaction at level-0 from '!actors!4ZjFZ1HoJV9mJStt' @ 72057594037927935 : 1 .. '!actors!zVpmacwoWEG8YTCQ' @ 0 : 0; will stop at (end)
2026/05/06-22:24:45.830634 7fe44efef6c0 Recovering log #32
2026/05/06-22:24:45.846170 7fe44efef6c0 Delete type=3 #30
2026/05/06-22:24:45.846232 7fe44efef6c0 Delete type=0 #32
2026/05/06-22:30:22.780170 7fe44cfeb6c0 Level-0 table #37: started
2026/05/06-22:30:22.780198 7fe44cfeb6c0 Level-0 table #37: 0 bytes OK
2026/05/06-22:30:22.786085 7fe44cfeb6c0 Delete type=0 #35
2026/05/06-22:30:22.798917 7fe44cfeb6c0 Manual compaction at level-0 from '!actors!4ZjFZ1HoJV9mJStt' @ 72057594037927935 : 1 .. '!actors!zVpmacwoWEG8YTCQ' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
View File
Binary file not shown.
Binary file not shown.
View File
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000006
MANIFEST-000035
+7 -3
View File
@@ -1,3 +1,7 @@
2026/04/27-20:01:11.404989 7fed937fe6c0 Recovering log #4
2026/04/27-20:01:11.415714 7fed937fe6c0 Delete type=3 #2
2026/04/27-20:01:11.415769 7fed937fe6c0 Delete type=0 #4
2026/05/06-22:32:13.378942 7fe44dfed6c0 Recovering log #33
2026/05/06-22:32:13.388592 7fe44dfed6c0 Delete type=3 #31
2026/05/06-22:32:13.388647 7fe44dfed6c0 Delete type=0 #33
2026/05/06-22:37:23.624309 7fe44cfeb6c0 Level-0 table #38: started
2026/05/06-22:37:23.624349 7fe44cfeb6c0 Level-0 table #38: 0 bytes OK
2026/05/06-22:37:23.630330 7fe44cfeb6c0 Delete type=0 #36
2026/05/06-22:37:23.643593 7fe44cfeb6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
+7 -5
View File
@@ -1,5 +1,7 @@
2026/04/27-17:47:13.202702 7f27793fe6c0 Delete type=3 #1
2026/04/27-17:47:13.203697 7f272b7fe6c0 Level-0 table #5: started
2026/04/27-17:47:13.207185 7f272b7fe6c0 Level-0 table #5: 4830 bytes OK
2026/04/27-17:47:13.213948 7f272b7fe6c0 Delete type=0 #3
2026/04/27-17:47:13.214169 7f272b7fe6c0 Manual compaction at level-0 from '!items!DC2kimCi9sWxqhXG' @ 72057594037927935 : 1 .. '!items!qzfAEhmvVxEMzm0k' @ 0 : 0; will stop at (end)
2026/05/06-22:24:45.774814 7fe44e7ee6c0 Recovering log #29
2026/05/06-22:24:45.790576 7fe44e7ee6c0 Delete type=3 #27
2026/05/06-22:24:45.790660 7fe44e7ee6c0 Delete type=0 #29
2026/05/06-22:30:22.748887 7fe44cfeb6c0 Level-0 table #34: started
2026/05/06-22:30:22.748914 7fe44cfeb6c0 Level-0 table #34: 0 bytes OK
2026/05/06-22:30:22.754926 7fe44cfeb6c0 Delete type=0 #32
2026/05/06-22:30:22.773735 7fe44cfeb6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
View File
Binary file not shown.
View File
+1
View File
@@ -0,0 +1 @@
MANIFEST-000010
View File
+8
View File
@@ -0,0 +1,8 @@
2026/05/06-22:32:13.440388 7fe44dfed6c0 Recovering log #8
2026/05/06-22:32:13.451363 7fe44dfed6c0 Delete type=3 #6
2026/05/06-22:32:13.451431 7fe44dfed6c0 Delete type=0 #8
2026/05/06-22:37:23.650643 7fe44cfeb6c0 Level-0 table #13: started
2026/05/06-22:37:23.650683 7fe44cfeb6c0 Level-0 table #13: 0 bytes OK
2026/05/06-22:37:23.656717 7fe44cfeb6c0 Delete type=0 #11
2026/05/06-22:37:23.669680 7fe44cfeb6c0 Manual compaction at level-0 from '!scenes!2C6gyZpvPxWlsVZi' @ 72057594037927935 : 1 .. '!scenes.levels!olYe9bhuXwRWQ8j7.defaultLevel0000' @ 0 : 0; will stop at (end)
2026/05/06-22:37:23.669713 7fe44cfeb6c0 Manual compaction at level-1 from '!scenes!2C6gyZpvPxWlsVZi' @ 72057594037927935 : 1 .. '!scenes.levels!olYe9bhuXwRWQ8j7.defaultLevel0000' @ 0 : 0; will stop at (end)
View File
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More