Compare commits

..

16 Commits

Author SHA1 Message Date
uberwald bc49286f91 Correction compendiums 2026-04-27 21:30:33 +02:00
uberwald 1e252ff6f2 Correction compendiums 2026-04-27 17:49:00 +02:00
uberwald d12a7debdf Fix release process
Release Creation / build (release) Successful in 1m19s
2026-04-23 20:46:22 +02:00
uberwald 30d9e4e4df Fix release process 2026-04-23 20:46:13 +02:00
uberwald 606b38e022 fix: Track package-lock.json to enable npm ci in CI/CD
Release Creation / build (release) Successful in 1m20s
Retire package-lock.json du .gitignore afin que npm ci puisse
fonctionner dans le workflow de release Gitea.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-01 09:19:35 +02:00
uberwald 4d9fa45ab6 AJout workflow
Release Creation / build (release) Failing after 2m18s
2026-04-01 09:05:20 +02:00
uberwald 8a4e0ba0e4 ci: Make pack compilation explicit in release workflow
Remplace npm run build:full par 3 étapes séparées et nommées :
- Install dependencies (npm ci)
- Compile compendium packs (npm run pack:compile)
- Build CSS and JavaScript (npm run build)

Les compendiums sont désormais générés à la volée depuis packs-src/
à chaque release, indépendamment de l'état commité de packs/.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-01 08:51:39 +02:00
uberwald 93df69a43e ci: Add Gitea release workflow
Adapté depuis fvtt-te-deum, avec :
- Étape npm ci + npm run build:full (compilation CSS et packs)
- Zip incluant css/ fonts/ images/ lang/ packs/ src/ templates/
- Substitution version/manifest/download dans system.json
- Publication sur le serveur FoundryVTT (compatibility 13)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-01 08:46:49 +02:00
uberwald 6a5819b905 Ajout des compendiums et cleanup des images 2026-04-01 08:41:40 +02:00
uberwald 389d4b8008 feat: Add per-type NPC icons + update compendium icons
6 new SVG icons in images/icons/, one per NPC type:
- icon-npc-esprit-animal.svg (amber) — paw print + yin-yang
- icon-npc-fantome.svg (cyan) — ghost silhouette + 鬼 character
- icon-npc-demon.svg (red) — horned demon face
- icon-npc-jiugwaai.svg (emerald) — hybrid creature + 怪 character
- icon-npc-dieu.svg (gold) — divine halo + lotus
- icon-npc-mortel.svg (steel blue) — human silhouette + talisman

All 49 NPC JSON files updated with type-specific icon paths.
Pack cde-npcs recompiled.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-31 23:12:27 +02:00
uberwald ce1ed17ce1 Ajout des compendiums et cleanup des images 2026-03-31 23:04:06 +02:00
uberwald ac4bcb4217 chore: Remove accidental temp/debug files from repo
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-31 22:58:44 +02:00
uberwald 01861340ad feat: Add NPC compendium with 49 PNJs from the rulebook
- 9 esprits-animaux nommés (James Kam, Karen Sen, Susan Chow, Hyun Ci, Michelle Chun, Scott Zang, Pui Gan, Bui Hok, Sonia Tsui)
- 8 fantômes (Phil Mok, Tony Meng + 4 archétypes génériques)
- 13 démons (Je Maan, Good Boy, Charlie Fei, Zoeng, Ban Daan + 8 archétypes génériques)
- 4 jiugwaai (Juk Lyun + archétypes gaaujan, gwat jyun)
- 2 divinités (Ruby Jin Jin / tinneoi, Jing Tin)
- 3 mortels initiés nommés (Laureen Zoeng, Julian Po, John Wa)
- 8 PNJ du scénario L'Année du Lion (Alexander Weng, Maximilian Pang, Ginny Ching, Ken Luan, Jeffrey Chiu, Paul Fei, Leonie Fei, Carrie Suet)
- 3 archétypes mortels génériques (Fat si sorcier, Intermédiaire, Consultant ésotérique)
- Suppression du fichier modèle npc_Modele_Creature.json
- Recompilation du pack cde-npcs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-31 22:58:35 +02:00
uberwald 6091d4bf58 feat(compendiums): add 54 ingredient items from rulebook (all disciplines)
- 27 general esoteric items (school=all)
- 6 Cinabre interne items
- 7 Alchimie items
- 7 Exorcisme items
- 7 Géomancie items
- No specific Maîtrise de la Voie items (not listed in rulebook)
- Template file removed

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-31 21:11:20 +02:00
uberwald bc9f4d9867 feat(compendiums): add 5 sanhei and 16 supernatural capacity items
- Add 5 pre-constructed sanhei from rulebook (p.216-218):
  Lance du Croc d'Argent, Gantelet aux Écailles irisées,
  Pinceau écarlate, Rouleau de la Monture céleste,
  Sceau du Dragon-gardien
- Each sanhei has 3 filled properties with name/heiCost/heiType/description
- Add 16 supernatural capacities from bestiary chapter (p.349-353):
  absorption_yang, arme, don_inne, immortalite, intangibilite,
  metamorphe (x5 sub-types), peur, possession, protection,
  regeneration, vol, venin
- Delete template placeholder files for both packs
- Recompile cde-sanhei and cde-supernaturals packs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-31 20:49:01 +02:00
uberwald 2756d95ea5 feat(compendiums): add 25 spells and 20 martial arts from rulebook
- Extract all 25 magical speciality items from PDF (all 5 disciplines)
- Extract all 20 martial arts style items from PDF
- Fix Wing Chun technique 'La terre tourne autour du soleil'
- Fix La Paume des Huit Trigrammes technique 2 'Contourner l'obstacle'
- Fix Les Poignards volants with all 3 techniques from rulebook
- Delete template placeholder files
- Recompile all packs (cde-spells: 25, cde-kungfus: 20)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-31 20:32:36 +02:00
808 changed files with 52200 additions and 23 deletions
+78
View File
@@ -0,0 +1,78 @@
name: Release Creation
on:
release:
types: [published]
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo "💡 The ${{ gitea.repository }} repository will be cloned to the runner."
- uses: https://github.com/RouxAntoine/checkout@v3.5.4
# Get part of the tag after the `v`
- name: Extract tag version number
id: get_version
uses: https://github.com/battila7/get-version-action@v2
- name: Setup Node.js
uses: https://github.com/actions/setup-node@v3
with:
node-version: "20"
- name: Install dependencies
run: npm ci
# Génération à la volée des compendiums (packs-src/ → packs/ LevelDB)
- name: Compile compendium packs
run: npm run pack:compile
# Compilation CSS (LESS → css/) et bundle JavaScript (src/ → dist/)
- name: Build CSS and JavaScript
run: npm run build
# Substitute the Manifest and Download URLs in the system.json
- name: Substitute Manifest and Download Links For Versioned Ones
id: sub_manifest_link_version
uses: https://github.com/microsoft/variable-substitution@v1
with:
files: "system.json"
env:
version: ${{steps.get_version.outputs.version-without-v}}
url: https://www.uberwald.me/gitea/${{gitea.repository}}
manifest: https://www.uberwald.me/gitea/uberwald/fvtt-chroniques-de-l-etrange/releases/download/latest/system.json
download: https://www.uberwald.me/gitea/uberwald/fvtt-chroniques-de-l-etrange/releases/download/${{github.event.release.tag_name}}/fvtt-chroniques-de-l-etrange.zip
# Create a zip file with all files required by the system
- run: |
apt update -y
apt install -y zip
- run: zip -r ./fvtt-chroniques-de-l-etrange.zip system.json README.md CHANGELOG.md LICENSE.txt css/ fonts/ images/ lang/ packs/ src/ templates/
- name: Setup Go
uses: https://github.com/actions/setup-go@v4
with:
go-version: ">=1.20.1"
- name: Upload release assets
id: use-go-action
uses: https://gitea.com/actions/release-action@main
with:
files: |-
./fvtt-chroniques-de-l-etrange.zip
system.json
api_key: "${{secrets.ALLOW_PUSH_RELEASE}}"
#- name: Publish to Foundry server
# uses: https://github.com/djlechuck/foundryvtt-publish-package-action@v1
# with:
# #token: ${{ secrets.FOUNDRYVTT_RELEASE_TOKEN }}
# id: 'fvtt-chroniques-de-l-etrange'
# version: ${{github.event.release.tag_name}}
# manifest: 'https://www.uberwald.me/gitea/public/fvtt-chroniques-de-l-etrange/releases/download/latest/system.json'
# notes: 'https://www.uberwald.me/gitea/${{gitea.repository}}/releases/download/${{github.event.release.tag_name}}/fvtt-chroniques-de-l-etrange.zip'
# compatibility-minimum: '13'
# compatibility-verified: '13'
-1
View File
@@ -5,7 +5,6 @@
# Node Modules # Node Modules
node_modules/ node_modules/
package-lock.json
chroniquesdeletrange.lock chroniquesdeletrange.lock
*.pdf *.pdf
+330
View File
@@ -0,0 +1,330 @@
#!/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
@@ -0,0 +1,213 @@
#!/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
@@ -0,0 +1,317 @@
#!/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
@@ -0,0 +1,306 @@
#!/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
+389 -1
View File
@@ -177,6 +177,11 @@
flex: 1 1 110px; flex: 1 1 110px;
max-width: 200px; max-width: 200px;
} }
.cde-stat-cell--wide {
flex: 1 1 100%;
max-width: 100%;
min-width: 0;
}
.cde-stat-label { .cde-stat-label {
font-size: 10px; font-size: 10px;
font-family: "Averia", sans-serif; font-family: "Averia", sans-serif;
@@ -205,6 +210,23 @@
.cde-stat-cell input:focus { .cde-stat-cell input:focus {
border-bottom-color: #00d4d4; border-bottom-color: #00d4d4;
} }
.cde-stat-cell textarea {
width: 100%;
background: transparent;
border: 1px solid #1a2436;
border-radius: 2px;
color: #e2e8f4;
font-size: 13px;
font-family: inherit;
padding: 4px 6px;
outline: none;
resize: vertical;
transition: border-color 0.15s;
line-height: 1.5;
}
.cde-stat-cell textarea:focus {
border-color: #ff3d5a;
}
.cde-stat-cell select { .cde-stat-cell select {
width: 100%; width: 100%;
border: none; border: none;
@@ -385,7 +407,6 @@ section.npc .cde-neon-tabs .item.active {
display: none; display: none;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
min-height: 100%;
} }
.cde-tab-body .tab.active { .cde-tab-body .tab.active {
display: flex; display: flex;
@@ -1254,6 +1275,12 @@ section.npc .cde-neon-tabs .item.active {
width: auto; width: auto;
height: auto; height: auto;
opacity: 0.6; opacity: 0.6;
cursor: zoom-in;
transition: opacity 0.2s ease, box-shadow 0.2s ease;
}
.cde-loksyu-standalone .cde-loksyu-visual-row .cde-lok-visual:hover {
opacity: 0.9;
box-shadow: 0 0 10px #263853;
} }
.cde-loksyu-standalone .cde-lok-footer { .cde-loksyu-standalone .cde-lok-footer {
display: flex; display: flex;
@@ -3080,6 +3107,12 @@ strong.ellipsis {
max-height: 180px; max-height: 180px;
opacity: 0.45; opacity: 0.45;
border-radius: 4px; border-radius: 4px;
cursor: zoom-in;
transition: opacity 0.2s ease, box-shadow 0.2s ease;
}
.cde-nghang-diagram img:hover {
opacity: 0.85;
box-shadow: 0 0 12px #263853;
} }
.img-die-sm { .img-die-sm {
width: 27px !important; width: 27px !important;
@@ -3800,3 +3833,358 @@ ol.item-list li.item .item-controls a.item-control:hover {
color: #ff3d5a; color: #ff3d5a;
text-shadow: 0 0 5px rgba(255, 61, 90, 0.4); text-shadow: 0 0 5px rgba(255, 61, 90, 0.4);
} }
/* ============================================================
ROUE D'INITIATIVE — CDEWheelApp
============================================================ */
.cde-wheel-app {
color: #e2e8f4;
background: #080c14;
font-family: "Averia", "Averia Regular", sans-serif;
}
.cde-wheel-app .window-content {
padding: 0;
overflow: hidden;
}
/* Two-column layout: SVG wheel left, panel right */
.cde-wheel-layout {
display: flex;
height: 100%;
min-height: 520px;
}
/* ---- Left: SVG wheel ---- */
.cde-wheel-svg-container {
flex: 0 0 480px;
width: 480px;
padding: 12px 12px 6px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #0d1520;
border-right: 1px solid #1a2436;
}
.cde-wheel-svg-container svg {
flex: 1 1 auto;
width: 100%;
max-width: 456px;
max-height: 456px;
overflow: visible;
}
.cde-wheel-svg-container .cde-wheel-legend {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0 2px;
font-size: 11px;
color: rgba(255, 255, 255, 0.55);
flex-shrink: 0;
}
.cde-wheel-svg-container .cde-wheel-legend-dot {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
.cde-wheel-svg-container .cde-wheel-segment {
stroke: #080c14;
stroke-width: 1.5;
transition: opacity 0.2s;
}
.cde-wheel-svg-container .cde-wheel-cran-label {
font-family: "Averia Regular", sans-serif;
font-size: 12px;
fill: rgba(255, 255, 255, 0.55);
text-anchor: middle;
dominant-baseline: central;
pointer-events: none;
user-select: none;
}
.cde-wheel-svg-container .cde-wheel-fighter-circle {
stroke-width: 2;
cursor: pointer;
transition: r 0.2s, stroke-width 0.2s;
}
.cde-wheel-svg-container .cde-wheel-fighter-circle:hover {
stroke-width: 3;
}
.cde-wheel-svg-container .cde-wheel-fighter-circle.is-active {
r: 18;
stroke-width: 3;
filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.7));
}
.cde-wheel-svg-container .cde-wheel-fighter-circle.is-turn {
stroke-dasharray: 3 2;
animation: cde-spin 4s linear infinite;
}
.cde-wheel-svg-container .cde-wheel-fighter-initial {
font-size: 13px;
font-weight: 700;
fill: #fff;
text-anchor: middle;
dominant-baseline: central;
pointer-events: none;
}
/* ---- Right: panel ---- */
.cde-wheel-panel {
flex: 1 1 auto;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 0;
min-width: 0;
}
.cde-wheel-section-title {
padding: 7px 12px 6px;
border-bottom: 1px solid #1a2436;
background: #0d1520;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.12em;
color: #7d94b8;
border-left: 3px solid #4a9eff;
flex-shrink: 0;
}
.cde-wheel-section-title em {
font-style: normal;
color: #e2e8f4;
text-transform: none;
letter-spacing: 0;
font-size: 11px;
}
/* Combatant list */
.cde-wheel-combatants {
flex: 0 0 auto;
max-height: 130px;
overflow-y: auto;
border-bottom: 1px solid #1a2436;
}
.cde-wheel-combatant {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px 6px 9px;
cursor: pointer;
border-bottom: 1px solid rgba(26, 36, 54, 0.5);
border-left: 3px solid transparent;
transition: background 0.15s, border-color 0.15s;
}
.cde-wheel-combatant:last-child {
border-bottom: none;
}
.cde-wheel-combatant:hover {
background: #101622;
}
.cde-wheel-combatant.cde-wheel-combatant--selected {
background: rgba(74, 158, 255, 0.1);
border-left-color: #4a9eff;
}
.cde-wheel-combatant.cde-wheel-combatant--selected .cde-wheel-combatant-name {
color: #e2e8f4;
font-weight: 600;
}
.cde-wheel-combatant.cde-wheel-combatant--active .cde-wheel-active-marker {
color: #f0c040;
filter: drop-shadow(0 0 3px #f0c040);
}
.cde-wheel-combatant-img {
width: 30px;
height: 30px;
border-radius: 50%;
object-fit: cover;
border: 1.5px solid #263853;
flex-shrink: 0;
}
.cde-wheel-combatant-name {
flex: 1 1 auto;
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #7d94b8;
}
.cde-wheel-combatant-cran {
font-size: 11px;
font-weight: 700;
font-variant-numeric: tabular-nums;
flex-shrink: 0;
min-width: 22px;
text-align: center;
padding: 2px 6px;
border-radius: 12px;
line-height: 1.3;
}
.cde-wheel-active-marker {
font-size: 10px;
color: #7d94b8;
flex-shrink: 0;
}
/* Action area */
.cde-wheel-actions {
flex: 1 1 auto;
display: flex;
flex-direction: column;
overflow-y: auto;
padding: 8px 10px 10px;
gap: 6px;
}
.cde-wheel-actions.cde-wheel-actions--hint {
justify-content: center;
align-items: center;
}
.cde-wheel-hint {
color: #7d94b8;
font-size: 12px;
text-align: center;
font-style: italic;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.cde-wheel-hint i {
font-size: 22px;
opacity: 0.4;
}
.cde-wheel-action-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 5px;
}
.cde-wheel-action-btn {
display: flex;
align-items: center;
justify-content: space-between;
gap: 4px;
background: #101622;
border: 1px solid #263853;
border-radius: 5px;
color: #7d94b8;
font-size: 11px;
padding: 5px 7px;
cursor: pointer;
text-align: left;
transition: background 0.15s, border-color 0.15s, color 0.15s, box-shadow 0.15s;
font-family: inherit;
}
.cde-wheel-action-btn:hover {
color: #e2e8f4;
background: #1a2436;
border-color: #263853;
}
.cde-wheel-action-btn .cde-wheel-action-name {
flex: 1 1 auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cde-wheel-action-btn .cde-wheel-action-cost {
flex-shrink: 0;
font-weight: 700;
font-size: 10px;
padding: 1px 5px;
border-radius: 10px;
background: #263853;
color: #7d94b8;
line-height: 1.4;
}
.cde-wheel-action-btn[data-cost="1"]:hover {
box-shadow: 0 0 6px rgba(74, 158, 255, 0.22);
}
.cde-wheel-action-btn[data-cost="1"] .cde-wheel-action-cost {
background: #1a3d6a;
color: #6aadff;
}
.cde-wheel-action-btn[data-cost="2"]:hover {
box-shadow: 0 0 6px rgba(212, 160, 80, 0.25);
}
.cde-wheel-action-btn[data-cost="2"] .cde-wheel-action-cost {
background: #4a3200;
color: #d4a050;
}
.cde-wheel-action-btn[data-cost="3"]:hover {
box-shadow: 0 0 6px rgba(224, 96, 48, 0.28);
}
.cde-wheel-action-btn[data-cost="3"] .cde-wheel-action-cost {
background: #4a1800;
color: #e07840;
}
.cde-wheel-action-btn[data-cost="6"] {
border-color: rgba(204, 32, 64, 0.4);
}
.cde-wheel-action-btn[data-cost="6"]:hover {
box-shadow: 0 0 6px rgba(204, 32, 64, 0.35);
}
.cde-wheel-action-btn[data-cost="6"] .cde-wheel-action-cost {
background: #4a0814;
color: #e03050;
}
/* Special action buttons */
.cde-wheel-special-actions {
display: flex;
gap: 6px;
margin-top: 2px;
}
.cde-wheel-btn-roll,
.cde-wheel-btn-surprise {
flex: 1 1 0;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
border-radius: 5px;
font-size: 11px;
font-weight: 600;
padding: 7px 8px;
cursor: pointer;
font-family: inherit;
transition: background 0.15s, box-shadow 0.15s;
border: 1px solid;
}
.cde-wheel-btn-roll {
background: rgba(192, 138, 0, 0.14);
border-color: rgba(192, 138, 0, 0.5);
color: #e0b030;
}
.cde-wheel-btn-roll:hover {
background: rgba(192, 138, 0, 0.26);
box-shadow: 0 0 8px rgba(192, 138, 0, 0.4);
}
.cde-wheel-btn-surprise {
background: rgba(255, 61, 90, 0.12);
border-color: rgba(255, 61, 90, 0.45);
color: #ff3d5a;
}
.cde-wheel-btn-surprise:hover {
background: rgba(255, 61, 90, 0.24);
box-shadow: 0 0 8px rgba(255, 61, 90, 0.35);
}
/* No-combat empty state */
.cde-wheel-no-combat {
flex: 1 1 auto;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #7d94b8;
gap: 8px;
padding: 20px;
text-align: center;
font-size: 12px;
}
.cde-wheel-no-combat i {
font-size: 28px;
opacity: 0.4;
}
/* Spin animation for active-turn token */
@keyframes cde-spin {
from {
transform-origin: var(--fx) var(--fy);
transform: rotate(0deg);
}
to {
transform-origin: var(--fx) var(--fy);
transform: rotate(360deg);
}
}
+430 -1
View File
@@ -208,6 +208,12 @@
min-width: 110px; min-width: 110px;
flex: 1 1 110px; flex: 1 1 110px;
max-width: 200px; max-width: 200px;
&--wide {
flex: 1 1 100%;
max-width: 100%;
min-width: 0;
}
} }
.cde-stat-label { .cde-stat-label {
@@ -240,6 +246,23 @@
&:focus { border-bottom-color: @cde-item; } &:focus { border-bottom-color: @cde-item; }
} }
.cde-stat-cell textarea {
width: 100%;
background: transparent;
border: 1px solid @cde-border;
border-radius: 2px;
color: @cde-text;
font-size: 13px;
font-family: inherit;
padding: 4px 6px;
outline: none;
resize: vertical;
transition: border-color 0.15s;
line-height: 1.5;
&:focus { border-color: @cde-kungfu; }
}
.cde-stat-cell select { .cde-stat-cell select {
width: 100%; width: 100%;
border: none; border: none;
@@ -369,7 +392,6 @@ section.npc .cde-neon-tabs .item.active { color: @cde-supernatural; borde
display: none; display: none;
flex-direction: column; flex-direction: column;
gap: @cde-gap; gap: @cde-gap;
min-height: 100%;
} }
.tab.active { .tab.active {
@@ -1289,6 +1311,13 @@ section.npc .cde-neon-tabs .item.active { color: @cde-supernatural; borde
width: auto; width: auto;
height: auto; height: auto;
opacity: 0.6; opacity: 0.6;
cursor: zoom-in;
transition: opacity 0.2s ease, box-shadow 0.2s ease;
&:hover {
opacity: 0.9;
box-shadow: 0 0 10px @cde-border-hi;
}
} }
} }
@@ -3131,6 +3160,13 @@ strong.ellipsis {
max-height: 180px; max-height: 180px;
opacity: 0.45; opacity: 0.45;
border-radius: 4px; border-radius: 4px;
cursor: zoom-in;
transition: opacity 0.2s ease, box-shadow 0.2s ease;
&:hover {
opacity: 0.85;
box-shadow: 0 0 12px @cde-border-hi;
}
} }
} }
@@ -3915,3 +3951,396 @@ ol.item-list {
text-shadow: 0 0 5px fade(@cde-kungfu, 40%); text-shadow: 0 0 5px fade(@cde-kungfu, 40%);
} }
} }
/* ============================================================
ROUE D'INITIATIVE — CDEWheelApp
============================================================ */
// Wu Xing segment colours (match JS constants)
@wu-metal: #b8c4cc;
@wu-water: #3a7bd5;
@wu-earth: #c8a84b;
@wu-fire: #d94f3d;
@wu-wood: #4a9b5a;
@wu-reference: #2c1f6b;
.cde-wheel-app {
color: @cde-text;
background: @cde-bg;
font-family: "Averia", "Averia Regular", sans-serif;
.window-content {
padding: 0;
overflow: hidden;
}
}
/* Two-column layout: SVG wheel left, panel right */
.cde-wheel-layout {
display: flex;
height: 100%;
min-height: 520px;
}
/* ---- Left: SVG wheel ---- */
.cde-wheel-svg-container {
flex: 0 0 480px;
width: 480px;
padding: 12px 12px 6px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: @cde-surface2;
border-right: 1px solid @cde-border;
svg {
flex: 1 1 auto;
width: 100%;
max-width: 456px;
max-height: 456px;
overflow: visible;
}
.cde-wheel-legend {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0 2px;
font-size: 11px;
color: rgba(255,255,255,0.55);
flex-shrink: 0;
}
.cde-wheel-legend-dot {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
.cde-wheel-segment {
stroke: @cde-bg;
stroke-width: 1.5;
transition: opacity 0.2s;
}
.cde-wheel-cran-label {
font-family: "Averia Regular", sans-serif;
font-size: 12px;
fill: rgba(255,255,255,0.55);
text-anchor: middle;
dominant-baseline: central;
pointer-events: none;
user-select: none;
}
// Combatant token circle on the wheel
.cde-wheel-fighter-circle {
stroke-width: 2;
cursor: pointer;
transition: r 0.2s, stroke-width 0.2s;
&:hover {
stroke-width: 3;
}
&.is-active {
r: 18;
stroke-width: 3;
filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.7));
}
&.is-turn {
stroke-dasharray: 3 2;
animation: cde-spin 4s linear infinite;
}
}
.cde-wheel-fighter-initial {
font-size: 13px;
font-weight: 700;
fill: #fff;
text-anchor: middle;
dominant-baseline: central;
pointer-events: none;
}
}
/* ---- Right: panel ---- */
.cde-wheel-panel {
flex: 1 1 auto;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 0;
min-width: 0;
}
.cde-wheel-section-title {
padding: 7px 12px 6px;
border-bottom: 1px solid @cde-border;
background: @cde-surface2;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.12em;
color: @cde-muted;
border-left: 3px solid @cde-spell;
flex-shrink: 0;
em {
font-style: normal;
color: @cde-text;
text-transform: none;
letter-spacing: 0;
font-size: 11px;
}
}
/* Combatant list */
.cde-wheel-combatants {
flex: 0 0 auto;
max-height: 130px;
overflow-y: auto;
border-bottom: 1px solid @cde-border;
}
.cde-wheel-combatant {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px 6px 9px;
cursor: pointer;
border-bottom: 1px solid fade(@cde-border, 50%);
border-left: 3px solid transparent;
transition: background 0.15s, border-color 0.15s;
&:last-child { border-bottom: none; }
&:hover {
background: @cde-surface;
}
&.cde-wheel-combatant--selected {
background: fade(@cde-spell, 10%);
border-left-color: @cde-spell;
.cde-wheel-combatant-name {
color: @cde-text;
font-weight: 600;
}
}
&.cde-wheel-combatant--active {
.cde-wheel-active-marker {
color: #f0c040;
filter: drop-shadow(0 0 3px #f0c040);
}
}
}
.cde-wheel-combatant-img {
width: 30px;
height: 30px;
border-radius: 50%;
object-fit: cover;
border: 1.5px solid @cde-border-hi;
flex-shrink: 0;
}
.cde-wheel-combatant-name {
flex: 1 1 auto;
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: @cde-muted;
}
.cde-wheel-combatant-cran {
font-size: 11px;
font-weight: 700;
font-variant-numeric: tabular-nums;
flex-shrink: 0;
min-width: 22px;
text-align: center;
padding: 2px 6px;
border-radius: 12px;
line-height: 1.3;
}
.cde-wheel-active-marker {
font-size: 10px;
color: @cde-muted;
flex-shrink: 0;
}
/* Action area */
.cde-wheel-actions {
flex: 1 1 auto;
display: flex;
flex-direction: column;
overflow-y: auto;
padding: 8px 10px 10px;
gap: 6px;
&.cde-wheel-actions--hint {
justify-content: center;
align-items: center;
}
}
.cde-wheel-hint {
color: @cde-muted;
font-size: 12px;
text-align: center;
font-style: italic;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
i {
font-size: 22px;
opacity: 0.4;
}
}
.cde-wheel-action-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 5px;
}
.cde-wheel-action-btn {
display: flex;
align-items: center;
justify-content: space-between;
gap: 4px;
background: @cde-surface;
border: 1px solid @cde-border-hi;
border-radius: @cde-radius-sm;
color: @cde-muted;
font-size: 11px;
padding: 5px 7px;
cursor: pointer;
text-align: left;
transition: background 0.15s, border-color 0.15s, color 0.15s, box-shadow 0.15s;
font-family: inherit;
&:hover {
color: @cde-text;
background: @cde-border;
border-color: @cde-border-hi;
}
.cde-wheel-action-name {
flex: 1 1 auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cde-wheel-action-cost {
flex-shrink: 0;
font-weight: 700;
font-size: 10px;
padding: 1px 5px;
border-radius: 10px;
background: @cde-border-hi;
color: @cde-muted;
line-height: 1.4;
}
&[data-cost="1"] {
&:hover { box-shadow: 0 0 6px fade(@cde-spell, 22%); }
.cde-wheel-action-cost { background: #1a3d6a; color: #6aadff; }
}
&[data-cost="2"] {
&:hover { box-shadow: 0 0 6px fade(#d4a050, 25%); }
.cde-wheel-action-cost { background: #4a3200; color: #d4a050; }
}
&[data-cost="3"] {
&:hover { box-shadow: 0 0 6px fade(#e06030, 28%); }
.cde-wheel-action-cost { background: #4a1800; color: #e07840; }
}
&[data-cost="6"] {
border-color: fade(#cc2040, 40%);
&:hover { box-shadow: 0 0 6px fade(#cc2040, 35%); }
.cde-wheel-action-cost { background: #4a0814; color: #e03050; }
}
}
/* Special action buttons */
.cde-wheel-special-actions {
display: flex;
gap: 6px;
margin-top: 2px;
}
.cde-wheel-btn-roll,
.cde-wheel-btn-surprise {
flex: 1 1 0;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
border-radius: @cde-radius-sm;
font-size: 11px;
font-weight: 600;
padding: 7px 8px;
cursor: pointer;
font-family: inherit;
transition: background 0.15s, box-shadow 0.15s;
border: 1px solid;
}
.cde-wheel-btn-roll {
background: fade(#c08a00, 14%);
border-color: fade(#c08a00, 50%);
color: #e0b030;
&:hover {
background: fade(#c08a00, 26%);
box-shadow: 0 0 8px fade(#c08a00, 40%);
}
}
.cde-wheel-btn-surprise {
background: fade(@cde-kungfu, 12%);
border-color: fade(@cde-kungfu, 45%);
color: @cde-kungfu;
&:hover {
background: fade(@cde-kungfu, 24%);
box-shadow: 0 0 8px fade(@cde-kungfu, 35%);
}
}
/* No-combat empty state */
.cde-wheel-no-combat {
flex: 1 1 auto;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: @cde-muted;
gap: 8px;
padding: 20px;
text-align: center;
font-size: 12px;
i {
font-size: 28px;
opacity: 0.4;
}
}
/* Spin animation for active-turn token */
@keyframes cde-spin {
from { transform-origin: var(--fx) var(--fy); transform: rotate(0deg); }
to { transform-origin: var(--fx) var(--fy); transform: rotate(360deg); }
}
+284 -5
View File
@@ -130,7 +130,8 @@ var TEMPLATE_PARTIALS = [
"systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-npc-kungfus.html", "systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-npc-kungfus.html",
"systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-npc-items.html", "systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-npc-items.html",
"systems/fvtt-chroniques-de-l-etrange/templates/apps/cde-loksyu-app.html", "systems/fvtt-chroniques-de-l-etrange/templates/apps/cde-loksyu-app.html",
"systems/fvtt-chroniques-de-l-etrange/templates/apps/cde-tinji-app.html" "systems/fvtt-chroniques-de-l-etrange/templates/apps/cde-tinji-app.html",
"systems/fvtt-chroniques-de-l-etrange/templates/apps/cde-wheel-app.html"
]; ];
// src/config/settings.js // src/config/settings.js
@@ -648,7 +649,25 @@ var CDEActor = class extends Actor {
}; };
// src/documents/item.js // src/documents/item.js
var DEFAULT_ICONS = {
kungfu: "systems/fvtt-chroniques-de-l-etrange/images/icons/icon-kungfu.svg",
spell: "systems/fvtt-chroniques-de-l-etrange/images/icons/icon-spell.svg",
supernatural: "systems/fvtt-chroniques-de-l-etrange/images/icons/icon-supernatural.svg",
weapon: "systems/fvtt-chroniques-de-l-etrange/images/icons/icon-weapon.svg",
armor: "systems/fvtt-chroniques-de-l-etrange/images/icons/icon-armor.svg",
sanhei: "systems/fvtt-chroniques-de-l-etrange/images/icons/icon-sanhei.svg",
ingredient: "systems/fvtt-chroniques-de-l-etrange/images/icons/icon-ingredient.svg",
item: "systems/fvtt-chroniques-de-l-etrange/images/icons/icon-item.svg"
};
var CDEItem = class extends Item { var CDEItem = class extends Item {
/** @override */
async _preCreate(data, options, userId) {
await super._preCreate(data, options, userId);
const defaultIcon = DEFAULT_ICONS[this.type];
if (defaultIcon && (!data.img || data.img === Item.DEFAULT_ICON)) {
this.updateSource({ img: defaultIcon });
}
}
}; };
// src/ui/dice.js // src/ui/dice.js
@@ -789,6 +808,20 @@ function registerHandlebarsHelpers() {
}; };
return game.i18n.localize(keys[activation] ?? "CDE.Activation"); return game.i18n.localize(keys[activation] ?? "CDE.Activation");
}); });
Handlebars.registerHelper("cranPosition", function(cran, cx, cy, r) {
const angleDeg = 90 + cran * 15;
const angleRad = angleDeg * Math.PI / 180;
const x = Math.round(cx + r * Math.cos(angleRad));
const y = Math.round(cy - r * Math.sin(angleRad));
return { x, y };
});
Handlebars.registerHelper("fighterX", function(cx, index, total) {
const offset = total > 1 ? (index - (total - 1) / 2) * 34 : 0;
return Math.round(cx - 15 + offset);
});
Handlebars.registerHelper("fighterY", function(cy, index, total) {
return Math.round(cy - 50);
});
} }
// src/ui/templates.js // src/ui/templates.js
@@ -1622,6 +1655,7 @@ var CDECharacterSheet = class extends CDEBaseActorSheet {
this.#bindPrefs(); this.#bindPrefs();
this.#bindRollButtons(); this.#bindRollButtons();
this.#bindComponentRandomize(); this.#bindComponentRandomize();
this.#bindDiagramZoom();
} }
#bindInitiativeControls() { #bindInitiativeControls() {
const buttons = this.element?.querySelectorAll(".click-initiative"); const buttons = this.element?.querySelectorAll(".click-initiative");
@@ -1700,6 +1734,16 @@ var CDECharacterSheet = class extends CDEBaseActorSheet {
}); });
}); });
} }
#bindDiagramZoom() {
const img = this.element?.querySelector("[data-action='zoom-diagram']");
if (!img) return;
img.addEventListener("click", () => {
new ImagePopout(img.src, {
title: game.i18n.localize("CDE.NghangDiagramTitle"),
shareable: false
}).render(true);
});
}
#bindComponentRandomize() { #bindComponentRandomize() {
const btn = this.element?.querySelector("[data-action='randomize-component']"); const btn = this.element?.querySelector("[data-action='randomize-component']");
if (!btn) return; if (!btn) return;
@@ -1992,7 +2036,8 @@ var CDELoksyuApp = class _CDELoksyuApp extends foundry.applications.api.Handleba
position: { width: 520, height: "auto" }, position: { width: 520, height: "auto" },
actions: { actions: {
resetElement: _CDELoksyuApp.#onResetElement, resetElement: _CDELoksyuApp.#onResetElement,
resetAll: _CDELoksyuApp.#onResetAll resetAll: _CDELoksyuApp.#onResetAll,
zoomVisual: _CDELoksyuApp.#onZoomVisual
} }
}; };
static PARTS = { static PARTS = {
@@ -2075,6 +2120,12 @@ var CDELoksyuApp = class _CDELoksyuApp extends foundry.applications.api.Handleba
for (const k of KEYS) data[k] = { yin: 0, yang: 0 }; for (const k of KEYS) data[k] = { yin: 0, yang: 0 };
await setLoksyuData(data); await setLoksyuData(data);
} }
static #onZoomVisual(_event, target) {
new ImagePopout(target.src, {
title: game.i18n.localize("CDE.LoksyuDiagramTitle"),
shareable: false
}).render(true);
}
}; };
// src/ui/apps/tinji-app.js // src/ui/apps/tinji-app.js
@@ -2090,7 +2141,7 @@ var CDETinjiApp = class _CDETinjiApp extends foundry.applications.api.Handlebars
resizable: false resizable: false
}, },
classes: ["cde-app", "cde-tinji-standalone"], classes: ["cde-app", "cde-tinji-standalone"],
position: { width: 320, height: "auto" }, position: { width: 380, height: "auto" },
actions: { actions: {
increment: _CDETinjiApp.#onIncrement, increment: _CDETinjiApp.#onIncrement,
decrement: _CDETinjiApp.#onDecrement, decrement: _CDETinjiApp.#onDecrement,
@@ -2173,6 +2224,205 @@ var CDETinjiApp = class _CDETinjiApp extends foundry.applications.api.Handlebars
} }
}; };
// src/documents/combat.js
var CDECombat = class extends Combat {
/**
* Override rollInitiative to open the PC or NPC initiative dialog
* for each selected combatant, then sync the result to the Combatant document.
*/
async rollInitiative(ids, options = {}) {
const combatantIds = typeof ids === "string" ? [ids] : ids;
for (const id of combatantIds) {
const combatant = this.combatants.get(id);
if (!combatant) continue;
const actor = combatant.actor;
if (!actor) continue;
if (actor.type === ACTOR_TYPES.character) {
await rollInitiativePC(actor);
} else {
await rollInitiativeNPC(actor);
}
}
return this;
}
/**
* Sort combatants: highest initiative first (furthest counter-clockwise = acts first).
* Ties: PCs before NPCs; among PCs, by name; among NPCs, by name.
* Calls super.setupTurns() first to ensure this.current is properly initialized.
*/
setupTurns() {
super.setupTurns();
this.turns = this.turns.slice().sort((a, b) => {
const ia = a.initiative ?? 0;
const ib = b.initiative ?? 0;
if (ia !== ib) return ib - ia;
const aIsPC = a.actor?.type === ACTOR_TYPES.character ? 1 : 0;
const bIsPC = b.actor?.type === ACTOR_TYPES.character ? 1 : 0;
if (aIsPC !== bIsPC) return bIsPC - aIsPC;
return (a.name ?? "").localeCompare(b.name ?? "");
});
return this.turns;
}
};
async function advanceCombatantPosition(combatant, cranCost) {
const current = combatant.initiative ?? combatant.actor?.system?.initiative ?? 1;
const newValue = (current - cranCost - 1 + 48) % 24 + 1;
await combatant.update({ initiative: newValue });
}
// src/ui/apps/wheel-app.js
var WHEEL_TEMPLATE = "systems/fvtt-chroniques-de-l-etrange/templates/apps/cde-wheel-app.html";
var ACTION_COSTS = [
{ key: "draw", label: "CDE.ActionCostDraw", cost: 1 },
{ key: "changestyle", label: "CDE.ActionCostChangeStyle", cost: 1 },
{ key: "defense", label: "CDE.ActionCostDefense", cost: 1 },
{ key: "aim", label: "CDE.ActionCostAim", cost: 2 },
{ key: "help", label: "CDE.ActionCostHelp", cost: 2 },
{ key: "defally", label: "CDE.ActionCostDefendAlly", cost: 2 },
{ key: "move", label: "CDE.ActionCostMove", cost: 2 },
{ key: "attack", label: "CDE.ActionCostAttack", cost: 3 },
{ key: "delay", label: "CDE.ActionCostDelay", cost: 6 }
];
var WHEEL_SEGMENTS = [
{ label: "M\xE9tal", color: "#b8c4cc", textColor: "#1a1a1a", crans: [1, 2, 3, 4] },
{ label: "Eau", color: "#3a7bd5", textColor: "#ffffff", crans: [5, 6, 7, 8] },
{ label: "Terre", color: "#c8a84b", textColor: "#1a1a1a", crans: [9, 10, 11, 12] },
{ label: "Feu", color: "#d94f3d", textColor: "#ffffff", crans: [13, 14, 15, 16] },
{ label: "Bois", color: "#4a9b5a", textColor: "#ffffff", crans: [17, 18, 19, 20] },
{ label: "Rep\xE8re", color: "#1a1a2e", textColor: "#aaaaaa", crans: [21, 22, 23, 24] }
];
function segmentForCran(cran) {
return WHEEL_SEGMENTS.find((s) => s.crans.includes(cran)) ?? WHEEL_SEGMENTS[0];
}
var CDEWheelApp = class _CDEWheelApp extends foundry.applications.api.ApplicationV2 {
static DEFAULT_OPTIONS = {
id: "cde-wheel-app",
classes: ["cde-wheel-app"],
tag: "div",
window: {
title: "CDE.InitiativeWheel",
icon: "fas fa-circle-notch",
resizable: true
},
position: { width: 820, height: 620 },
actions: {
advanceCran: _CDEWheelApp.#advanceCran,
setSurprised: _CDEWheelApp.#setSurprised,
rollInitiative: _CDEWheelApp.#rollInitiative
}
};
/** @type {CDEWheelApp|null} */
static #instance = null;
/** Open (or bring to front) the singleton instance. */
static open() {
if (!_CDEWheelApp.#instance || _CDEWheelApp.#instance.rendered === false) {
_CDEWheelApp.#instance = new _CDEWheelApp();
_CDEWheelApp.#instance.render(true);
} else {
_CDEWheelApp.#instance.bringToFront();
}
return _CDEWheelApp.#instance;
}
/** Currently selected combatant id (for action panel). */
#selectedId = null;
async _prepareContext(options) {
const combat = game.combat;
const combatants = combat ? [...combat.combatants.values()] : [];
const sorted = [...combatants].sort((a, b) => (b.initiative ?? 0) - (a.initiative ?? 0));
const cranData = this.#buildCranData(combatants);
const selected = this.#selectedId ? combatants.find((c) => c.id === this.#selectedId) : null;
const actionCosts = ACTION_COSTS.map((a) => ({
...a,
label: game.i18n.localize(a.label)
}));
return {
hasCombat: !!combat,
combatants: sorted.map((c) => ({
id: c.id,
name: c.name,
img: c.token?.texture?.src ?? c.actor?.img ?? "icons/svg/mystery-man.svg",
initiative: c.initiative ?? "\u2014",
segment: segmentForCran(c.initiative ?? 1),
isActive: combat?.current?.combatantId === c.id,
isSelected: c.id === this.#selectedId,
hasInitiative: c.initiative != null
})),
cranData,
selected,
selectedName: selected?.name ?? null,
actionCosts
};
}
async _renderHTML(context, options) {
return foundry.applications.handlebars.renderTemplate(WHEEL_TEMPLATE, context);
}
_replaceHTML(result, content, options) {
content.innerHTML = result;
this.#bindEvents(content);
}
/** Build per-cran data for the SVG wheel. */
#buildCranData(combatants) {
const data = [];
for (let cran = 1; cran <= 24; cran++) {
const segment = segmentForCran(cran);
const fighters = combatants.filter((c) => Math.round(c.initiative) === cran);
data.push({ cran, segment, fighters });
}
return data;
}
/** Bind click events for combatant selection. */
#bindEvents(content) {
content.querySelectorAll("[data-select-combatant]").forEach((el) => {
el.addEventListener("click", () => {
this.#selectedId = el.dataset.selectCombatant;
this.render();
});
});
}
/** Action: advance selected combatant by given cran cost. */
static async #advanceCran(event, element) {
const app = _CDEWheelApp.#instance;
if (!app?.#selectedId) return;
const cost = parseInt(element.dataset.cost, 10);
if (!cost || isNaN(cost)) return;
const combatant = game.combat?.combatants.get(app.#selectedId);
if (!combatant) return;
await advanceCombatantPosition(combatant, cost);
}
/** Action: set selected combatant to surprised (position 1 = reference). */
static async #setSurprised(event, element) {
const app = _CDEWheelApp.#instance;
if (!app?.#selectedId) return;
const combatant = game.combat?.combatants.get(app.#selectedId);
if (!combatant) return;
await combatant.update({ initiative: 1 });
}
/** Action: open the initiative dialog for the selected combatant. */
static async #rollInitiative(event, element) {
const app = _CDEWheelApp.#instance;
if (!app?.#selectedId) return;
const combatant = game.combat?.combatants.get(app.#selectedId);
if (!combatant) return;
await game.combat.rollInitiative([app.#selectedId]);
}
/** Re-render when combat state changes. */
static registerHooks() {
const refresh = () => {
if (_CDEWheelApp.#instance?.rendered) _CDEWheelApp.#instance.render();
};
Hooks.on("updateCombat", refresh);
Hooks.on("updateCombatant", refresh);
Hooks.on("createCombatant", refresh);
Hooks.on("deleteCombatant", refresh);
Hooks.on("updateActor", (_actor, diff) => {
if (foundry.utils.hasProperty(diff, "system.initiative")) refresh();
});
Hooks.on("deleteCombat", () => {
if (_CDEWheelApp.#instance?.rendered) _CDEWheelApp.#instance.render();
});
}
};
// src/ui/roll-actions.js // src/ui/roll-actions.js
var RESULT_TEMPLATE3 = "systems/fvtt-chroniques-de-l-etrange/templates/form/cde-dice-result.html"; var RESULT_TEMPLATE3 = "systems/fvtt-chroniques-de-l-etrange/templates/form/cde-dice-result.html";
function injectRollActions(message, html) { function injectRollActions(message, html) {
@@ -2326,7 +2576,8 @@ Hooks.once("init", async () => {
console.info(`CHRONIQUESDELETRANGE | Initializing ${SYSTEM_ID}`); console.info(`CHRONIQUESDELETRANGE | Initializing ${SYSTEM_ID}`);
registerSettings(); registerSettings();
game.system.CONST = { MAGICS, SUBTYPES }; game.system.CONST = { MAGICS, SUBTYPES };
game.cde = { CDELoksyuApp, CDETinjiApp }; game.cde = { CDELoksyuApp, CDETinjiApp, CDEWheelApp };
CONFIG.Combat.documentClass = CDECombat;
CONFIG.Actor.dataModels = { CONFIG.Actor.dataModels = {
[ACTOR_TYPES.character]: CharacterDataModel, [ACTOR_TYPES.character]: CharacterDataModel,
[ACTOR_TYPES.npc]: NpcDataModel [ACTOR_TYPES.npc]: NpcDataModel
@@ -2404,6 +2655,7 @@ Hooks.once("init", async () => {
}); });
Hooks.once("ready", async () => { Hooks.once("ready", async () => {
await migrateIfNeeded(); await migrateIfNeeded();
CDEWheelApp.registerHooks();
}); });
Hooks.on("renderChatLog", (_app, html) => { Hooks.on("renderChatLog", (_app, html) => {
const el = html instanceof HTMLElement ? html : html[0] ?? html; const el = html instanceof HTMLElement ? html : html[0] ?? html;
@@ -2418,10 +2670,14 @@ Hooks.on("renderChatLog", (_app, html) => {
<button type="button" class="cde-chat-btn cde-chat-btn--tinji"> <button type="button" class="cde-chat-btn cde-chat-btn--tinji">
<i class="fas fa-star"></i> ${game.i18n.localize("CDE.TinJi2")} <i class="fas fa-star"></i> ${game.i18n.localize("CDE.TinJi2")}
</button> </button>
<button type="button" class="cde-chat-btn cde-chat-btn--wheel">
<i class="fas fa-circle-notch"></i> ${game.i18n.localize("CDE.InitiativeWheel")}
</button>
`; `;
wrapper.addEventListener("click", (ev) => { wrapper.addEventListener("click", (ev) => {
if (ev.target.closest(".cde-chat-btn--loksyu")) CDELoksyuApp.open(); if (ev.target.closest(".cde-chat-btn--loksyu")) CDELoksyuApp.open();
if (ev.target.closest(".cde-chat-btn--tinji")) CDETinjiApp.open(); if (ev.target.closest(".cde-chat-btn--tinji")) CDETinjiApp.open();
if (ev.target.closest(".cde-chat-btn--wheel")) CDEWheelApp.open();
}); });
const anchor = el.querySelector(".chat-form") ?? el.querySelector(".chat-message-form") ?? el.querySelector("form"); const anchor = el.querySelector(".chat-form") ?? el.querySelector(".chat-message-form") ?? el.querySelector("form");
if (anchor) anchor.parentElement.insertBefore(wrapper, anchor); if (anchor) anchor.parentElement.insertBefore(wrapper, anchor);
@@ -2436,12 +2692,35 @@ Hooks.on("updateSetting", (setting) => {
refreshAllRollActions(); refreshAllRollActions();
} }
}); });
Hooks.on("updateActor", (actor, diff) => {
if (!foundry.utils.hasProperty(diff, "system.initiative")) return;
if (!game.combat) return;
const initiative = actor.system.initiative;
const combatant = game.combat.combatants.find((c) => c.actor?.id === actor.id);
if (combatant && combatant.initiative !== initiative) {
combatant.update({ initiative }).catch(() => {
});
}
});
Hooks.on("updateCombatant", (combatant, diff) => {
if (!("initiative" in diff)) return;
const initiative = combatant.initiative;
if (initiative == null) return;
setTimeout(() => {
const actor = combatant.actor;
if (actor && actor.system?.initiative !== initiative) {
actor.update({ "system.initiative": initiative }).catch(() => {
});
}
}, 0);
});
/** /**
* Chroniques de l'Étrange — Système FoundryVTT * Chroniques de l'Étrange — Système FoundryVTT
* *
* Chroniques de l'Étrange est un jeu de rôle édité par Antre-Monde Éditions. * Chroniques de l'Étrange est un jeu de rôle édité par Antre-Monde Éditions.
* Ce système FoundryVTT est une implémentation indépendante et n'est pas * Ce système FoundryVTT est une implémentation indépendante et n'est pas
* affilié à Antre-Monde Éditions. * affilié à Antre-Monde Éditions,
* mais a été réalisé avec l'autorisation d'Antre-Monde Éditions.
* *
* @author LeRatierBretonnien * @author LeRatierBretonnien
* @copyright 20242026 LeRatierBretonnien * @copyright 20242026 LeRatierBretonnien
+3 -3
View File
File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

+58
View File
@@ -0,0 +1,58 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<defs>
<radialGradient id="ar-glow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#4ecdc4" stop-opacity="0.3"/>
<stop offset="100%" stop-color="#4ecdc4" stop-opacity="0"/>
</radialGradient>
</defs>
<!-- Fond -->
<circle cx="50" cy="50" r="48" fill="#101622" stroke="#1a2436" stroke-width="2"/>
<circle cx="50" cy="50" r="48" fill="url(#ar-glow)"/>
<!-- Bouclier circulaire extérieur -->
<circle cx="50" cy="50" r="36" fill="#4ecdc4" fill-opacity="0.08" stroke="#4ecdc4" stroke-width="2.5"/>
<!-- Anneau décoratif intérieur -->
<circle cx="50" cy="50" r="30" fill="none" stroke="#4ecdc4" stroke-width="1" opacity="0.5"/>
<!-- 8 rivets décoratifs sur le bord extérieur du bouclier -->
<circle cx="50" cy="15" r="2" fill="#4ecdc4" opacity="0.8"/>
<circle cx="75" cy="25" r="2" fill="#4ecdc4" opacity="0.8"/>
<circle cx="85" cy="50" r="2" fill="#4ecdc4" opacity="0.8"/>
<circle cx="75" cy="75" r="2" fill="#4ecdc4" opacity="0.8"/>
<circle cx="50" cy="85" r="2" fill="#4ecdc4" opacity="0.8"/>
<circle cx="25" cy="75" r="2" fill="#4ecdc4" opacity="0.8"/>
<circle cx="15" cy="50" r="2" fill="#4ecdc4" opacity="0.8"/>
<circle cx="25" cy="25" r="2" fill="#4ecdc4" opacity="0.8"/>
<!-- Tête de tigre stylisée -->
<!-- Front / dessus de la tête -->
<path d="M34 40 Q50 30 66 40 Q68 48 64 54 Q58 62 50 64 Q42 62 36 54 Q32 48 34 40 Z"
fill="#4ecdc4" fill-opacity="0.15" stroke="#4ecdc4" stroke-width="2"/>
<!-- Oreilles de tigre -->
<path d="M34 40 Q28 30 32 24 Q36 32 40 38" fill="#4ecdc4" fill-opacity="0.2" stroke="#4ecdc4" stroke-width="1.5"/>
<path d="M66 40 Q72 30 68 24 Q64 32 60 38" fill="#4ecdc4" fill-opacity="0.2" stroke="#4ecdc4" stroke-width="1.5"/>
<!-- Yeux du tigre -->
<ellipse cx="43" cy="46" rx="5" ry="4" fill="none" stroke="#4ecdc4" stroke-width="1.5"/>
<ellipse cx="57" cy="46" rx="5" ry="4" fill="none" stroke="#4ecdc4" stroke-width="1.5"/>
<ellipse cx="43" cy="46" rx="2" ry="3" fill="#4ecdc4" fill-opacity="0.5"/>
<ellipse cx="57" cy="46" rx="2" ry="3" fill="#4ecdc4" fill-opacity="0.5"/>
<!-- Nez du tigre -->
<path d="M47 52 Q50 50 53 52 Q50 55 47 52 Z" fill="#4ecdc4" fill-opacity="0.6" stroke="#4ecdc4" stroke-width="1"/>
<!-- Moustaches -->
<line x1="32" y1="53" x2="45" y2="53" stroke="#4ecdc4" stroke-width="1" opacity="0.7"/>
<line x1="55" y1="53" x2="68" y2="53" stroke="#4ecdc4" stroke-width="1" opacity="0.7"/>
<line x1="33" y1="57" x2="45" y2="55" stroke="#4ecdc4" stroke-width="1" opacity="0.7"/>
<line x1="55" y1="55" x2="67" y2="57" stroke="#4ecdc4" stroke-width="1" opacity="0.7"/>
<!-- Rayures de tigre sur le front -->
<path d="M46 36 Q48 40 47 44" fill="none" stroke="#4ecdc4" stroke-width="1.5" stroke-linecap="round" opacity="0.6"/>
<path d="M50 34 Q50 38 50 42" fill="none" stroke="#4ecdc4" stroke-width="1.5" stroke-linecap="round" opacity="0.6"/>
<path d="M54 36 Q52 40 53 44" fill="none" stroke="#4ecdc4" stroke-width="1.5" stroke-linecap="round" opacity="0.6"/>
<!-- Crocs en bas -->
<path d="M46 60 L44 68 L46 64 L48 70 L50 63 L52 70 L54 64 L56 68 L54 60"
fill="none" stroke="#4ecdc4" stroke-width="1.5" stroke-linejoin="round" opacity="0.8"/>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

+53
View File
@@ -0,0 +1,53 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<defs>
<radialGradient id="ig-glow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#98c379" stop-opacity="0.3"/>
<stop offset="100%" stop-color="#98c379" stop-opacity="0"/>
</radialGradient>
</defs>
<!-- Fond -->
<circle cx="50" cy="50" r="48" fill="#101622" stroke="#1a2436" stroke-width="2"/>
<circle cx="50" cy="50" r="48" fill="url(#ig-glow)"/>
<!-- Pilon (pestle) — en haut à droite -->
<line x1="62" y1="20" x2="52" y2="40" stroke="#98c379" stroke-width="3.5" stroke-linecap="round"/>
<!-- Tête du pilon (plus épaisse) -->
<ellipse cx="63" cy="19" rx="4" ry="3" fill="#98c379" fill-opacity="0.4" stroke="#98c379" stroke-width="1.5" transform="rotate(-30 63 19)"/>
<!-- Mortier (bol) -->
<!-- Corps extérieur du mortier -->
<path d="M22 54 Q22 76 50 80 Q78 76 78 54 Z"
fill="#98c379" fill-opacity="0.12" stroke="#98c379" stroke-width="2.5"/>
<!-- Bord supérieur du mortier (ellipse) -->
<ellipse cx="50" cy="54" rx="28" ry="8" fill="#98c379" fill-opacity="0.1" stroke="#98c379" stroke-width="2"/>
<!-- Contenu / intérieur sombre -->
<ellipse cx="50" cy="56" rx="22" ry="6" fill="#101622" fill-opacity="0.6"/>
<!-- Surface du contenu (herbes) -->
<ellipse cx="50" cy="55" rx="18" ry="4" fill="#98c379" fill-opacity="0.2" stroke="#98c379" stroke-width="0.8" opacity="0.6"/>
<!-- Ornements du mortier (gravures) -->
<!-- Motif vague (eau) sur le corps du mortier -->
<path d="M26 64 Q32 60 38 64 Q44 68 50 64 Q56 60 62 64 Q68 68 74 64"
fill="none" stroke="#98c379" stroke-width="1" opacity="0.5"/>
<!-- Fleur de lotus au-dessus du mortier -->
<!-- Pétales (6) -->
<path d="M50 44 Q46 36 42 30 Q46 34 50 32 Q54 34 58 30 Q54 36 50 44"
fill="#98c379" fill-opacity="0.2" stroke="#98c379" stroke-width="1.5"/>
<path d="M38 48 Q30 44 26 38 Q32 40 36 36 Q40 32 40 38 Q40 44 38 48"
fill="#98c379" fill-opacity="0.2" stroke="#98c379" stroke-width="1.5"/>
<path d="M62 48 Q70 44 74 38 Q68 40 64 36 Q60 32 60 38 Q60 44 62 48"
fill="#98c379" fill-opacity="0.2" stroke="#98c379" stroke-width="1.5"/>
<!-- Coeur du lotus -->
<circle cx="50" cy="46" r="4" fill="#98c379" fill-opacity="0.4" stroke="#98c379" stroke-width="1.5"/>
<circle cx="50" cy="46" r="1.5" fill="#98c379"/>
<!-- Vapeurs alchimiques -->
<path d="M36 52 Q32 46 36 40 Q34 44 38 46 Q36 48 38 52"
fill="none" stroke="#98c379" stroke-width="1" stroke-linecap="round" opacity="0.5"/>
<path d="M50 52 Q48 44 52 38 Q50 44 54 46 Q52 48 52 52"
fill="none" stroke="#98c379" stroke-width="1" stroke-linecap="round" opacity="0.5"/>
<path d="M64 52 Q68 46 64 40 Q66 44 62 46 Q64 48 62 52"
fill="none" stroke="#98c379" stroke-width="1" stroke-linecap="round" opacity="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

+59
View File
@@ -0,0 +1,59 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<defs>
<radialGradient id="it-glow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#00d4d4" stop-opacity="0.3"/>
<stop offset="100%" stop-color="#00d4d4" stop-opacity="0"/>
</radialGradient>
</defs>
<!-- Fond -->
<circle cx="50" cy="50" r="48" fill="#101622" stroke="#1a2436" stroke-width="2"/>
<circle cx="50" cy="50" r="48" fill="url(#it-glow)"/>
<!-- Coffret laqué (corps) -->
<!-- Couvercle -->
<rect x="20" y="26" width="60" height="20" rx="3" fill="#00d4d4" fill-opacity="0.12" stroke="#00d4d4" stroke-width="2.5"/>
<!-- Corps du coffret -->
<rect x="18" y="46" width="64" height="32" rx="3" fill="#00d4d4" fill-opacity="0.08" stroke="#00d4d4" stroke-width="2.5"/>
<!-- Ligne de séparation couvercle/corps -->
<line x1="18" y1="46" x2="82" y2="46" stroke="#00d4d4" stroke-width="1.5" opacity="0.7"/>
<!-- Ornements du couvercle -->
<!-- Motif de losanges (style laque chinoise) -->
<path d="M36 36 L50 28 L64 36 L50 44 Z" fill="none" stroke="#00d4d4" stroke-width="1" opacity="0.5"/>
<!-- Lignes décoratives côtés couvercle -->
<line x1="22" y1="30" x2="22" y2="42" stroke="#00d4d4" stroke-width="1" opacity="0.4"/>
<line x1="78" y1="30" x2="78" y2="42" stroke="#00d4d4" stroke-width="1" opacity="0.4"/>
<!-- Serrure yin-yang au centre du corps du coffret -->
<!-- Cercle de la serrure -->
<circle cx="50" cy="60" r="9" fill="#101622" stroke="#00d4d4" stroke-width="2"/>
<!-- Yin-yang simplifié dans la serrure -->
<!-- Moitié yang (gauche, claire) -->
<path d="M50 51 A9 9 0 0 0 50 69 A4.5 4.5 0 0 0 50 60 A4.5 4.5 0 0 1 50 51"
fill="#00d4d4" fill-opacity="0.4"/>
<!-- Petits cercles du yin-yang -->
<circle cx="50" cy="55.5" r="2" fill="#101622"/>
<circle cx="50" cy="64.5" r="2" fill="#00d4d4" fill-opacity="0.6"/>
<!-- Anneau de la serrure (trou de clé) -->
<rect x="48" y="66" width="4" height="5" rx="1" fill="#00d4d4" fill-opacity="0.5"/>
<!-- Ferrures du coffret (coins) -->
<!-- Coins du corps -->
<path d="M18 46 L18 54 L24 54" fill="none" stroke="#00d4d4" stroke-width="1.5" stroke-linecap="round"/>
<path d="M82 46 L82 54 L76 54" fill="none" stroke="#00d4d4" stroke-width="1.5" stroke-linecap="round"/>
<path d="M18 78 L18 70 L24 70" fill="none" stroke="#00d4d4" stroke-width="1.5" stroke-linecap="round"/>
<path d="M82 78 L82 70 L76 70" fill="none" stroke="#00d4d4" stroke-width="1.5" stroke-linecap="round"/>
<!-- Charnières du coffret (sur le côté droit) -->
<rect x="76" y="40" width="8" height="5" rx="2" fill="#00d4d4" fill-opacity="0.3" stroke="#00d4d4" stroke-width="1"/>
<rect x="76" y="50" width="8" height="5" rx="2" fill="#00d4d4" fill-opacity="0.3" stroke="#00d4d4" stroke-width="1"/>
<!-- Motif décoratif sur le bas du coffret -->
<path d="M26 72 Q50 68 74 72" fill="none" stroke="#00d4d4" stroke-width="1" stroke-dasharray="3 2" opacity="0.4"/>
<!-- Petits ornements latéraux sur le corps -->
<line x1="22" y1="56" x2="22" y2="68" stroke="#00d4d4" stroke-width="1" stroke-dasharray="2 2" opacity="0.4"/>
<line x1="78" y1="56" x2="78" y2="68" stroke="#00d4d4" stroke-width="1" stroke-dasharray="2 2" opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

+38
View File
@@ -0,0 +1,38 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<!-- Fond circulaire -->
<circle cx="50" cy="50" r="48" fill="#101622" stroke="#1a2436" stroke-width="2"/>
<circle cx="50" cy="50" r="48" fill="url(#kg-glow)" fill-opacity="0.12"/>
<defs>
<radialGradient id="kg-glow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#ff3d5a" stop-opacity="0.4"/>
<stop offset="100%" stop-color="#ff3d5a" stop-opacity="0"/>
</radialGradient>
</defs>
<!-- Poing stylisé (vue de face, doigts repliés) -->
<!-- Paume / base -->
<rect x="32" y="52" width="36" height="20" rx="4" fill="#ff3d5a" fill-opacity="0.18" stroke="#ff3d5a" stroke-width="2"/>
<!-- Doigts repliés — 4 rangées -->
<rect x="33" y="38" width="8" height="16" rx="3" fill="#ff3d5a" fill-opacity="0.18" stroke="#ff3d5a" stroke-width="2"/>
<rect x="43" y="35" width="8" height="19" rx="3" fill="#ff3d5a" fill-opacity="0.18" stroke="#ff3d5a" stroke-width="2"/>
<rect x="53" y="36" width="8" height="18" rx="3" fill="#ff3d5a" fill-opacity="0.18" stroke="#ff3d5a" stroke-width="2"/>
<rect x="63" y="40" width="6" height="14" rx="3" fill="#ff3d5a" fill-opacity="0.18" stroke="#ff3d5a" stroke-width="2"/>
<!-- Pouce -->
<path d="M32 62 Q24 60 25 54 Q26 50 32 52" fill="#ff3d5a" fill-opacity="0.18" stroke="#ff3d5a" stroke-width="2" stroke-linejoin="round"/>
<!-- Éclairs de qi (3 rayons) -->
<!-- Éclair gauche-haut -->
<polyline points="24,28 19,20 25,22 20,13" fill="none" stroke="#ff3d5a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" opacity="0.9"/>
<!-- Éclair centre-haut -->
<polyline points="50,30 47,20 52,23 49,13" fill="none" stroke="#ff3d5a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" opacity="0.9"/>
<!-- Éclair droite-haut -->
<polyline points="74,32 80,22 74,24 79,14" fill="none" stroke="#ff3d5a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" opacity="0.9"/>
<!-- Petites étincelles -->
<circle cx="22" cy="35" r="1.5" fill="#ff3d5a" opacity="0.8"/>
<circle cx="78" cy="37" r="1.5" fill="#ff3d5a" opacity="0.8"/>
<circle cx="50" cy="26" r="1.5" fill="#ff3d5a" opacity="0.8"/>
<!-- Ligne de force sous le poing -->
<path d="M28 73 Q50 80 72 73" fill="none" stroke="#ff3d5a" stroke-width="1.5" stroke-dasharray="3 2" opacity="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

+43
View File
@@ -0,0 +1,43 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<defs>
<radialGradient id="dm-glow" cx="50%" cy="45%" r="50%">
<stop offset="0%" stop-color="#cc2222" stop-opacity="0.5"/>
<stop offset="100%" stop-color="#cc2222" stop-opacity="0"/>
</radialGradient>
</defs>
<!-- Fond -->
<circle cx="50" cy="50" r="48" fill="#101622" stroke="#1a2436" stroke-width="2"/>
<circle cx="50" cy="50" r="48" fill="url(#dm-glow)"/>
<!-- Flammes infernales en arrière-plan -->
<path d="M30 78 Q28 65 34 56 Q30 62 32 52 Q36 42 42 48 Q38 36 46 28 Q48 42 44 50 Q50 38 54 30 Q56 44 50 54 Q56 46 62 40 Q62 54 56 62 Q62 56 68 52 Q66 64 62 72 Q56 66 54 72 Q50 80 48 72 Q44 64 40 72 Q36 80 30 78 Z"
fill="#cc2222" fill-opacity="0.18" stroke="#cc2222" stroke-width="1" stroke-linejoin="round" opacity="0.7"/>
<!-- Visage de démon - contour de tête -->
<ellipse cx="50" cy="52" rx="20" ry="22" fill="#cc2222" fill-opacity="0.12" stroke="#cc2222" stroke-width="2"/>
<!-- Deux cornes -->
<path d="M36 35 Q32 20 38 16 Q40 24 38 30" fill="#cc2222" fill-opacity="0.7" stroke="#cc2222" stroke-width="1.5" stroke-linejoin="round"/>
<path d="M64 35 Q68 20 62 16 Q60 24 62 30" fill="#cc2222" fill-opacity="0.7" stroke="#cc2222" stroke-width="1.5" stroke-linejoin="round"/>
<!-- Yeux (forme de flamme) -->
<path d="M40 50 Q44 44 48 50 Q44 56 40 50 Z" fill="#cc2222" opacity="0.9"/>
<path d="M52 50 Q56 44 60 50 Q56 56 52 50 Z" fill="#cc2222" opacity="0.9"/>
<!-- Pupilles -->
<ellipse cx="44" cy="50" rx="1.5" ry="2.5" fill="#101622"/>
<ellipse cx="56" cy="50" rx="1.5" ry="2.5" fill="#101622"/>
<!-- Nez aplati (démon) -->
<path d="M47 58 Q50 60 53 58" fill="none" stroke="#cc2222" stroke-width="2" stroke-linecap="round" opacity="0.7"/>
<!-- Bouche avec crocs -->
<path d="M38 66 Q50 74 62 66" fill="none" stroke="#cc2222" stroke-width="2" stroke-linecap="round" opacity="0.8"/>
<!-- Crocs -->
<path d="M43 67 L42 73 L45 68" fill="#cc2222" opacity="0.7"/>
<path d="M57 67 L58 73 L55 68" fill="#cc2222" opacity="0.7"/>
<!-- Sourcils menaçants -->
<path d="M38 44 Q44 40 48 43" fill="none" stroke="#cc2222" stroke-width="2.5" stroke-linecap="round" opacity="0.8"/>
<path d="M52 43 Q56 40 62 44" fill="none" stroke="#cc2222" stroke-width="2.5" stroke-linecap="round" opacity="0.8"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

+44
View File
@@ -0,0 +1,44 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<defs>
<radialGradient id="dv-glow" cx="50%" cy="40%" r="55%">
<stop offset="0%" stop-color="#ddaa00" stop-opacity="0.5"/>
<stop offset="100%" stop-color="#ddaa00" stop-opacity="0"/>
</radialGradient>
</defs>
<!-- Fond -->
<circle cx="50" cy="50" r="48" fill="#101622" stroke="#1a2436" stroke-width="2"/>
<circle cx="50" cy="50" r="48" fill="url(#dv-glow)"/>
<!-- Rayons célestes (halo) -->
<line x1="50" y1="10" x2="50" y2="18" stroke="#ddaa00" stroke-width="2" opacity="0.6"/>
<line x1="68" y1="15" x2="64" y2="22" stroke="#ddaa00" stroke-width="2" opacity="0.6"/>
<line x1="80" y1="28" x2="74" y2="32" stroke="#ddaa00" stroke-width="2" opacity="0.6"/>
<line x1="32" y1="15" x2="36" y2="22" stroke="#ddaa00" stroke-width="2" opacity="0.6"/>
<line x1="20" y1="28" x2="26" y2="32" stroke="#ddaa00" stroke-width="2" opacity="0.6"/>
<line x1="86" y1="44" x2="80" y2="46" stroke="#ddaa00" stroke-width="1.5" opacity="0.5"/>
<line x1="14" y1="44" x2="20" y2="46" stroke="#ddaa00" stroke-width="1.5" opacity="0.5"/>
<!-- Halo circulaire doré -->
<circle cx="50" cy="36" r="14" fill="none" stroke="#ddaa00" stroke-width="2.5" opacity="0.8"/>
<circle cx="50" cy="36" r="16" fill="none" stroke="#ddaa00" stroke-width="0.8" opacity="0.4" stroke-dasharray="3,4"/>
<!-- Lotus (5 pétales) -->
<ellipse cx="50" cy="72" rx="8" ry="4" fill="#ddaa00" fill-opacity="0.6" transform="rotate(0,50,72)"/>
<ellipse cx="50" cy="72" rx="8" ry="4" fill="#ddaa00" fill-opacity="0.4" transform="rotate(36,50,72)"/>
<ellipse cx="50" cy="72" rx="8" ry="4" fill="#ddaa00" fill-opacity="0.4" transform="rotate(72,50,72)"/>
<ellipse cx="50" cy="72" rx="8" ry="4" fill="#ddaa00" fill-opacity="0.4" transform="rotate(108,50,72)"/>
<ellipse cx="50" cy="72" rx="8" ry="4" fill="#ddaa00" fill-opacity="0.4" transform="rotate(144,50,72)"/>
<!-- Cœur du lotus -->
<circle cx="50" cy="72" r="4" fill="#ddaa00" fill-opacity="0.8"/>
<!-- Silhouette divine — corps lumineux -->
<ellipse cx="50" cy="36" rx="8" ry="10" fill="#ddaa00" fill-opacity="0.25"/>
<!-- Corps stylisé (robe longue) -->
<path d="M44 44 Q40 58 42 72 Q50 68 58 72 Q60 58 56 44 Z"
fill="#ddaa00" fill-opacity="0.18" stroke="#ddaa00" stroke-width="1.5"/>
<!-- Mains en prière -->
<path d="M44 54 Q38 52 36 56 Q38 60 44 58" fill="#ddaa00" fill-opacity="0.3" stroke="#ddaa00" stroke-width="1.2"/>
<path d="M56 54 Q62 52 64 56 Q62 60 56 58" fill="#ddaa00" fill-opacity="0.3" stroke="#ddaa00" stroke-width="1.2"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

+41
View File
@@ -0,0 +1,41 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<defs>
<radialGradient id="ea-glow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#e8a030" stop-opacity="0.4"/>
<stop offset="100%" stop-color="#e8a030" stop-opacity="0"/>
</radialGradient>
</defs>
<!-- Fond -->
<circle cx="50" cy="50" r="48" fill="#101622" stroke="#1a2436" stroke-width="2"/>
<circle cx="50" cy="50" r="48" fill="url(#ea-glow)"/>
<!-- Anneau extérieur -->
<circle cx="50" cy="50" r="42" fill="none" stroke="#e8a030" stroke-width="1.2" opacity="0.4"/>
<!-- Yin-Yang stylisé (transformation) -->
<!-- Demi-cercle yang (clair) -->
<path d="M50 18 A32 32 0 0 1 50 82 A16 16 0 0 0 50 50 A16 16 0 0 1 50 18 Z"
fill="#e8a030" fill-opacity="0.25" stroke="#e8a030" stroke-width="1.5"/>
<!-- Demi-cercle yin (sombre) -->
<path d="M50 18 A32 32 0 0 0 50 82 A16 16 0 0 1 50 50 A16 16 0 0 0 50 18 Z"
fill="#e8a030" fill-opacity="0.05" stroke="#e8a030" stroke-width="1.5"/>
<!-- Petits cercles yin-yang -->
<circle cx="50" cy="34" r="6" fill="#e8a030" fill-opacity="0.6"/>
<circle cx="50" cy="66" r="6" fill="#e8a030" fill-opacity="0.15" stroke="#e8a030" stroke-width="1.2"/>
<!-- Patte d'animal (5 coussinets) -->
<!-- Coussinet principal (paume) -->
<ellipse cx="50" cy="55" rx="9" ry="7" fill="#e8a030" fill-opacity="0.7"/>
<!-- 4 orteils -->
<ellipse cx="40" cy="44" rx="4" ry="3.5" fill="#e8a030" fill-opacity="0.7" transform="rotate(-15,40,44)"/>
<ellipse cx="45" cy="41" rx="4" ry="3.5" fill="#e8a030" fill-opacity="0.7" transform="rotate(-5,45,41)"/>
<ellipse cx="55" cy="41" rx="4" ry="3.5" fill="#e8a030" fill-opacity="0.7" transform="rotate(5,55,41)"/>
<ellipse cx="60" cy="44" rx="4" ry="3.5" fill="#e8a030" fill-opacity="0.7" transform="rotate(15,60,44)"/>
<!-- Griffes stylisées -->
<path d="M38 42 Q35 38 34 34" fill="none" stroke="#e8a030" stroke-width="1.8" stroke-linecap="round" opacity="0.8"/>
<path d="M43 39 Q42 35 42 31" fill="none" stroke="#e8a030" stroke-width="1.8" stroke-linecap="round" opacity="0.8"/>
<path d="M57 39 Q58 35 58 31" fill="none" stroke="#e8a030" stroke-width="1.8" stroke-linecap="round" opacity="0.8"/>
<path d="M62 42 Q65 38 66 34" fill="none" stroke="#e8a030" stroke-width="1.8" stroke-linecap="round" opacity="0.8"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

+41
View File
@@ -0,0 +1,41 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<defs>
<radialGradient id="gh-glow" cx="50%" cy="40%" r="55%">
<stop offset="0%" stop-color="#88ccee" stop-opacity="0.45"/>
<stop offset="100%" stop-color="#88ccee" stop-opacity="0"/>
</radialGradient>
</defs>
<!-- Fond -->
<circle cx="50" cy="50" r="48" fill="#101622" stroke="#1a2436" stroke-width="2"/>
<circle cx="50" cy="50" r="48" fill="url(#gh-glow)"/>
<!-- Silhouette fantôme — tête arrondie -->
<path d="M34 44 Q34 24 50 22 Q66 24 66 44 L66 68 Q62 64 58 68 Q55 72 52 68 Q49 64 50 68 Q47 72 44 68 Q40 64 36 68 Q34 64 34 68 Z"
fill="#88ccee" fill-opacity="0.15" stroke="#88ccee" stroke-width="2" stroke-linejoin="round"/>
<!-- Yeux fantôme — vides, inquiétants -->
<ellipse cx="43" cy="42" rx="4" ry="5" fill="#101622" stroke="#88ccee" stroke-width="1.5" opacity="0.9"/>
<ellipse cx="57" cy="42" rx="4" ry="5" fill="#101622" stroke="#88ccee" stroke-width="1.5" opacity="0.9"/>
<!-- Lueurs dans les yeux -->
<ellipse cx="43" cy="42" rx="1.5" ry="2" fill="#88ccee" opacity="0.5"/>
<ellipse cx="57" cy="42" rx="1.5" ry="2" fill="#88ccee" opacity="0.5"/>
<!-- Kanji 鬼 stylisé en filigrane (simplifié) -->
<!-- Traits horizontaux et verticaux évoquant le caractère -->
<line x1="40" y1="53" x2="60" y2="53" stroke="#88ccee" stroke-width="1.5" opacity="0.5"/>
<line x1="50" y1="53" x2="50" y2="62" stroke="#88ccee" stroke-width="1.5" opacity="0.5"/>
<path d="M42 58 Q50 55 58 58" fill="none" stroke="#88ccee" stroke-width="1.5" opacity="0.5"/>
<!-- Traînée vaporeuse en bas -->
<path d="M38 68 Q36 75 38 82 Q40 88 44 84 Q46 78 48 84 Q50 88 52 84 Q54 78 56 84 Q58 88 62 82 Q64 75 62 68"
fill="#88ccee" fill-opacity="0.08" stroke="#88ccee" stroke-width="1.2" stroke-dasharray="3,3" opacity="0.6"/>
<!-- Chaînes (lien au monde des vivants) — deux petits maillons -->
<circle cx="32" cy="52" r="3" fill="none" stroke="#88ccee" stroke-width="1.5" opacity="0.5"/>
<circle cx="32" cy="58" r="3" fill="none" stroke="#88ccee" stroke-width="1.5" opacity="0.5"/>
<line x1="32" y1="49" x2="32" y2="55" stroke="#88ccee" stroke-width="1.5" opacity="0.5"/>
<circle cx="68" cy="52" r="3" fill="none" stroke="#88ccee" stroke-width="1.5" opacity="0.5"/>
<circle cx="68" cy="58" r="3" fill="none" stroke="#88ccee" stroke-width="1.5" opacity="0.5"/>
<line x1="68" y1="49" x2="68" y2="55" stroke="#88ccee" stroke-width="1.5" opacity="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

+47
View File
@@ -0,0 +1,47 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<defs>
<radialGradient id="jg-glow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#22cc88" stop-opacity="0.4"/>
<stop offset="100%" stop-color="#22cc88" stop-opacity="0"/>
</radialGradient>
</defs>
<!-- Fond -->
<circle cx="50" cy="50" r="48" fill="#101622" stroke="#1a2436" stroke-width="2"/>
<circle cx="50" cy="50" r="48" fill="url(#jg-glow)"/>
<!-- Cercle de transformation mystique -->
<circle cx="50" cy="50" r="38" fill="none" stroke="#22cc88" stroke-width="1.2" opacity="0.35" stroke-dasharray="4,3"/>
<!-- Silhouette multi-forme (créature hybride) -->
<!-- Corps central — humanoïde -->
<ellipse cx="50" cy="52" rx="12" ry="15" fill="#22cc88" fill-opacity="0.15" stroke="#22cc88" stroke-width="1.8"/>
<!-- Tête avec cornes/oreilles animales -->
<circle cx="50" cy="35" r="9" fill="#22cc88" fill-opacity="0.15" stroke="#22cc88" stroke-width="1.8"/>
<!-- Oreilles pointues (animal) -->
<path d="M41 30 L38 20 L45 28" fill="#22cc88" fill-opacity="0.5" stroke="#22cc88" stroke-width="1.5" stroke-linejoin="round"/>
<path d="M59 30 L62 20 L55 28" fill="#22cc88" fill-opacity="0.5" stroke="#22cc88" stroke-width="1.5" stroke-linejoin="round"/>
<!-- Yeux (regard surnaturel) -->
<ellipse cx="46" cy="35" rx="3" ry="2" fill="#22cc88" opacity="0.8"/>
<ellipse cx="54" cy="35" rx="3" ry="2" fill="#22cc88" opacity="0.8"/>
<!-- Queue spiralée -->
<path d="M62 60 Q72 58 74 65 Q76 72 68 74 Q62 74 62 68"
fill="none" stroke="#22cc88" stroke-width="2" stroke-linecap="round" opacity="0.7"/>
<!-- Tentacule / membre supplémentaire gauche -->
<path d="M38 55 Q26 52 22 60 Q20 68 28 66"
fill="none" stroke="#22cc88" stroke-width="2" stroke-linecap="round" opacity="0.6"/>
<!-- Écailles / motif sur le corps -->
<path d="M44 46 Q50 42 56 46 Q52 50 50 48 Q48 50 44 46 Z"
fill="#22cc88" fill-opacity="0.3" stroke="#22cc88" stroke-width="1"/>
<path d="M44 52 Q50 48 56 52 Q52 56 50 54 Q48 56 44 52 Z"
fill="#22cc88" fill-opacity="0.3" stroke="#22cc88" stroke-width="1"/>
<!-- Kanji 怪 (étrange) stylisé — simplifié -->
<text x="50" y="81" text-anchor="middle" font-size="11" font-family="serif"
fill="#22cc88" opacity="0.55"></text>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

+41
View File
@@ -0,0 +1,41 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<defs>
<radialGradient id="mt-glow" cx="50%" cy="40%" r="50%">
<stop offset="0%" stop-color="#6688aa" stop-opacity="0.35"/>
<stop offset="100%" stop-color="#6688aa" stop-opacity="0"/>
</radialGradient>
</defs>
<!-- Fond -->
<circle cx="50" cy="50" r="48" fill="#101622" stroke="#1a2436" stroke-width="2"/>
<circle cx="50" cy="50" r="48" fill="url(#mt-glow)"/>
<!-- Anneau ésotérique (cercle de protection) -->
<circle cx="50" cy="50" r="40" fill="none" stroke="#6688aa" stroke-width="1.2" opacity="0.4"/>
<circle cx="50" cy="50" r="38" fill="none" stroke="#6688aa" stroke-width="0.6" opacity="0.25" stroke-dasharray="5,4"/>
<!-- Silhouette humaine -->
<!-- Tête -->
<circle cx="50" cy="30" r="9" fill="#6688aa" fill-opacity="0.25" stroke="#6688aa" stroke-width="1.8"/>
<!-- Corps -->
<path d="M40 42 L36 66 L42 66 L45 56 L50 60 L55 56 L58 66 L64 66 L60 42 Z"
fill="#6688aa" fill-opacity="0.2" stroke="#6688aa" stroke-width="1.8" stroke-linejoin="round"/>
<!-- Bras gauche -->
<path d="M40 44 Q30 52 28 60" fill="none" stroke="#6688aa" stroke-width="2" stroke-linecap="round"/>
<!-- Bras droit (levé, tenant un talisman) -->
<path d="M60 44 Q70 48 72 42" fill="none" stroke="#6688aa" stroke-width="2" stroke-linecap="round"/>
<!-- Talisman / ofuda (papier de prière) -->
<rect x="68" y="32" width="10" height="14" rx="1"
fill="#6688aa" fill-opacity="0.2" stroke="#6688aa" stroke-width="1.5"/>
<!-- Lignes du talisman -->
<line x1="70" y1="36" x2="76" y2="36" stroke="#6688aa" stroke-width="1" opacity="0.7"/>
<line x1="70" y1="39" x2="76" y2="39" stroke="#6688aa" stroke-width="1" opacity="0.7"/>
<line x1="70" y1="42" x2="76" y2="42" stroke="#6688aa" stroke-width="1" opacity="0.7"/>
<!-- Symbole occulte sur la poitrine (trigramme Pa Kua simplifié) -->
<line x1="45" y1="46" x2="55" y2="46" stroke="#6688aa" stroke-width="1.5" opacity="0.6"/>
<line x1="45" y1="49" x2="55" y2="49" stroke="#6688aa" stroke-width="1.5" opacity="0.6"/>
<line x1="45" y1="52" x2="50" y2="52" stroke="#6688aa" stroke-width="1.5" opacity="0.6"/>
<line x1="52" y1="52" x2="55" y2="52" stroke="#6688aa" stroke-width="1.5" opacity="0.6"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

+58
View File
@@ -0,0 +1,58 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<defs>
<radialGradient id="sh-glow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#cc44ff" stop-opacity="0.35"/>
<stop offset="100%" stop-color="#cc44ff" stop-opacity="0"/>
</radialGradient>
</defs>
<!-- Fond -->
<circle cx="50" cy="50" r="48" fill="#101622" stroke="#1a2436" stroke-width="2"/>
<circle cx="50" cy="50" r="48" fill="url(#sh-glow)"/>
<!-- Disque Bi (玉璧) — anneau de jade -->
<!-- Anneau extérieur -->
<circle cx="50" cy="50" r="32" fill="#cc44ff" fill-opacity="0.12" stroke="#cc44ff" stroke-width="2.5"/>
<!-- Trou central du bi -->
<circle cx="50" cy="50" r="14" fill="#101622" stroke="#cc44ff" stroke-width="2"/>
<!-- Gravures décoratives sur le disque (surface du bi) -->
<!-- Spirales de grain (yun wen) — motif traditionnel des bi -->
<circle cx="50" cy="50" r="23" fill="none" stroke="#cc44ff" stroke-width="0.8" stroke-dasharray="2.5 2" opacity="0.5"/>
<circle cx="50" cy="50" r="19" fill="none" stroke="#cc44ff" stroke-width="0.8" stroke-dasharray="1.5 3" opacity="0.4"/>
<!-- Petits motifs en spirale sur la surface du bi (8 grains) -->
<path d="M50 22 Q52 20 54 22 Q52 24 50 22" fill="none" stroke="#cc44ff" stroke-width="1" opacity="0.7"/>
<path d="M68 32 Q70 30 72 32 Q70 34 68 32" fill="none" stroke="#cc44ff" stroke-width="1" opacity="0.7"/>
<path d="M78 50 Q80 48 82 50 Q80 52 78 50" fill="none" stroke="#cc44ff" stroke-width="1" opacity="0.7"/>
<path d="M68 68 Q70 66 72 68 Q70 70 68 68" fill="none" stroke="#cc44ff" stroke-width="1" opacity="0.7"/>
<path d="M50 78 Q52 76 54 78 Q52 80 50 78" fill="none" stroke="#cc44ff" stroke-width="1" opacity="0.7"/>
<path d="M28 68 Q30 66 32 68 Q30 70 28 68" fill="none" stroke="#cc44ff" stroke-width="1" opacity="0.7"/>
<path d="M18 50 Q20 48 22 50 Q20 52 18 50" fill="none" stroke="#cc44ff" stroke-width="1" opacity="0.7"/>
<path d="M28 32 Q30 30 32 32 Q30 34 28 32" fill="none" stroke="#cc44ff" stroke-width="1" opacity="0.7"/>
<!-- 3 Perles (San = 3 en cantonais) flottant autour du bi -->
<!-- Perle 1 — haut -->
<circle cx="50" cy="10" r="5" fill="#cc44ff" fill-opacity="0.25" stroke="#cc44ff" stroke-width="1.8"/>
<circle cx="48" cy="8" r="1.5" fill="#cc44ff" fill-opacity="0.6"/>
<!-- Fil de perle haut -->
<line x1="50" y1="15" x2="50" y2="18" stroke="#cc44ff" stroke-width="1" stroke-dasharray="2 1" opacity="0.6"/>
<!-- Perle 2 — bas-gauche -->
<circle cx="26" cy="84" r="5" fill="#cc44ff" fill-opacity="0.25" stroke="#cc44ff" stroke-width="1.8"/>
<circle cx="24" cy="82" r="1.5" fill="#cc44ff" fill-opacity="0.6"/>
<!-- Fil perle bas-gauche -->
<line x1="30" y1="80" x2="34" y2="76" stroke="#cc44ff" stroke-width="1" stroke-dasharray="2 1" opacity="0.6"/>
<!-- Perle 3 — bas-droit -->
<circle cx="74" cy="84" r="5" fill="#cc44ff" fill-opacity="0.25" stroke="#cc44ff" stroke-width="1.8"/>
<circle cx="72" cy="82" r="1.5" fill="#cc44ff" fill-opacity="0.6"/>
<!-- Fil perle bas-droit -->
<line x1="70" y1="80" x2="66" y2="76" stroke="#cc44ff" stroke-width="1" stroke-dasharray="2 1" opacity="0.6"/>
<!-- Runes dans le trou central -->
<!-- Symbole 三 (3 traits) -->
<line x1="44" y1="46" x2="56" y2="46" stroke="#cc44ff" stroke-width="1.5" opacity="0.8"/>
<line x1="43" y1="50" x2="57" y2="50" stroke="#cc44ff" stroke-width="1.5" opacity="0.8"/>
<line x1="44" y1="54" x2="56" y2="54" stroke="#cc44ff" stroke-width="1.5" opacity="0.8"/>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

+62
View File
@@ -0,0 +1,62 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<defs>
<radialGradient id="sp-glow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#4a9eff" stop-opacity="0.35"/>
<stop offset="100%" stop-color="#4a9eff" stop-opacity="0"/>
</radialGradient>
</defs>
<!-- Fond -->
<circle cx="50" cy="50" r="48" fill="#101622" stroke="#1a2436" stroke-width="2"/>
<circle cx="50" cy="50" r="48" fill="url(#sp-glow)"/>
<!-- Octogone extérieur (bagua) -->
<polygon points="50,18 68,24 79,40 79,60 68,76 50,82 32,76 21,60 21,40 32,24"
fill="none" stroke="#4a9eff" stroke-width="1.8" opacity="0.7"/>
<!-- 8 trigrammes sur les côtés (petites lignes stylisées) -->
<!-- Chaque trigramme = 3 lignes, positionnées sur les 8 côtés de l'octogone -->
<!-- Nord -->
<line x1="46" y1="13" x2="54" y2="13" stroke="#4a9eff" stroke-width="1.5"/>
<line x1="46" y1="16" x2="54" y2="16" stroke="#4a9eff" stroke-width="1.5"/>
<line x1="46" y1="19" x2="54" y2="19" stroke="#4a9eff" stroke-width="1.5"/>
<!-- Nord-Est -->
<line x1="66" y1="18" x2="72" y2="22" stroke="#4a9eff" stroke-width="1.5"/>
<line x1="64" y1="21" x2="70" y2="25" stroke="#4a9eff" stroke-width="1.5"/>
<line x1="62" y1="24" x2="66" y2="27" stroke="#4a9eff" stroke-width="1.5"/>
<!-- Est -->
<line x1="81" y1="46" x2="87" y2="46" stroke="#4a9eff" stroke-width="1.5"/>
<line x1="81" y1="50" x2="87" y2="50" stroke="#4a9eff" stroke-width="1.5"/>
<line x1="83" y1="54" x2="87" y2="54" stroke="#4a9eff" stroke-width="1.5"/>
<!-- Sud-Est -->
<line x1="66" y1="78" x2="72" y2="74" stroke="#4a9eff" stroke-width="1.5"/>
<line x1="64" y1="75" x2="70" y2="71" stroke="#4a9eff" stroke-width="1.5"/>
<line x1="62" y1="72" x2="68" y2="69" stroke="#4a9eff" stroke-width="1.5"/>
<!-- Sud -->
<line x1="46" y1="87" x2="54" y2="87" stroke="#4a9eff" stroke-width="1.5"/>
<line x1="46" y1="84" x2="54" y2="84" stroke="#4a9eff" stroke-width="1.5"/>
<line x1="46" y1="81" x2="54" y2="81" stroke="#4a9eff" stroke-width="1.5"/>
<!-- Sud-Ouest -->
<line x1="34" y1="78" x2="28" y2="74" stroke="#4a9eff" stroke-width="1.5"/>
<line x1="36" y1="75" x2="30" y2="71" stroke="#4a9eff" stroke-width="1.5"/>
<line x1="38" y1="72" x2="32" y2="69" stroke="#4a9eff" stroke-width="1.5"/>
<!-- Ouest -->
<line x1="19" y1="46" x2="13" y2="46" stroke="#4a9eff" stroke-width="1.5"/>
<line x1="19" y1="50" x2="13" y2="50" stroke="#4a9eff" stroke-width="1.5"/>
<line x1="17" y1="54" x2="13" y2="54" stroke="#4a9eff" stroke-width="1.5"/>
<!-- Nord-Ouest -->
<line x1="34" y1="18" x2="28" y2="22" stroke="#4a9eff" stroke-width="1.5"/>
<line x1="36" y1="21" x2="30" y2="25" stroke="#4a9eff" stroke-width="1.5"/>
<line x1="38" y1="24" x2="34" y2="27" stroke="#4a9eff" stroke-width="1.5"/>
<!-- Cercle intérieur -->
<circle cx="50" cy="50" r="22" fill="none" stroke="#4a9eff" stroke-width="1.5" opacity="0.6"/>
<!-- Spirale de cinnabre (qi) — double spirale -->
<path d="M50 50 Q54 42 50 38 Q44 34 40 40 Q36 48 42 54 Q50 62 60 56 Q68 48 62 38 Q54 28 42 32"
fill="none" stroke="#4a9eff" stroke-width="2" stroke-linecap="round" opacity="0.9"/>
<!-- Point central (cinnabre) -->
<circle cx="50" cy="50" r="4" fill="#4a9eff" fill-opacity="0.5" stroke="#4a9eff" stroke-width="1.5"/>
<circle cx="50" cy="50" r="1.5" fill="#4a9eff"/>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

+49
View File
@@ -0,0 +1,49 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<defs>
<radialGradient id="sn-glow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#cc44ff" stop-opacity="0.4"/>
<stop offset="100%" stop-color="#cc44ff" stop-opacity="0"/>
</radialGradient>
</defs>
<!-- Fond -->
<circle cx="50" cy="50" r="48" fill="#101622" stroke="#1a2436" stroke-width="2"/>
<circle cx="50" cy="50" r="48" fill="url(#sn-glow)"/>
<!-- Flammes surnaturelles (gauche) -->
<path d="M22 72 Q20 60 26 52 Q22 56 24 46 Q28 38 32 42 Q28 32 36 26 Q38 38 34 44 Q40 36 42 28 Q46 40 40 50 Q46 44 48 36 Q52 48 46 56 Q50 50 54 44 Q56 56 50 64 Q54 58 58 52 Q60 62 56 70 Q52 68 50 72"
fill="#cc44ff" fill-opacity="0.2" stroke="#cc44ff" stroke-width="1.5" stroke-linejoin="round" opacity="0.8"/>
<!-- Contour de l'œil (grand) -->
<path d="M20 50 Q35 30 50 30 Q65 30 80 50 Q65 70 50 70 Q35 70 20 50 Z"
fill="none" stroke="#cc44ff" stroke-width="2" opacity="0.9"/>
<!-- Remplissage doux de l'œil -->
<path d="M25 50 Q38 36 50 36 Q62 36 75 50 Q62 64 50 64 Q38 64 25 50 Z"
fill="#cc44ff" fill-opacity="0.08"/>
<!-- Iris de l'œil -->
<circle cx="50" cy="50" r="10" fill="none" stroke="#cc44ff" stroke-width="2" opacity="0.8"/>
<!-- Pupille verticale (reptilienne / surnaturelle) -->
<ellipse cx="50" cy="50" rx="3" ry="8" fill="#cc44ff" fill-opacity="0.6"/>
<!-- Reflet -->
<circle cx="47" cy="46" r="2" fill="#cc44ff" fill-opacity="0.5"/>
<!-- Flammes supérieures (au-dessus de l'œil) -->
<path d="M38 30 Q36 22 42 18 Q40 26 46 22 Q44 28 50 24 Q48 30 54 26 Q52 32 58 28 Q56 34 62 30"
fill="none" stroke="#cc44ff" stroke-width="1.8" stroke-linecap="round" opacity="0.9"/>
<!-- Lignes de rayonnement autour de l'œil -->
<line x1="50" y1="24" x2="50" y2="18" stroke="#cc44ff" stroke-width="1.5" opacity="0.6"/>
<line x1="50" y1="76" x2="50" y2="82" stroke="#cc44ff" stroke-width="1.5" opacity="0.6"/>
<line x1="14" y1="50" x2="8" y2="50" stroke="#cc44ff" stroke-width="1.5" opacity="0.6"/>
<line x1="86" y1="50" x2="92" y2="50" stroke="#cc44ff" stroke-width="1.5" opacity="0.6"/>
<!-- Diagonaux -->
<line x1="27" y1="27" x2="22" y2="22" stroke="#cc44ff" stroke-width="1.2" opacity="0.5"/>
<line x1="73" y1="27" x2="78" y2="22" stroke="#cc44ff" stroke-width="1.2" opacity="0.5"/>
<line x1="27" y1="73" x2="22" y2="78" stroke="#cc44ff" stroke-width="1.2" opacity="0.5"/>
<line x1="73" y1="73" x2="78" y2="78" stroke="#cc44ff" stroke-width="1.2" opacity="0.5"/>
<!-- Spirales surnaturelles aux coins -->
<path d="M16 22 Q20 16 26 20 Q22 24 18 22" fill="none" stroke="#cc44ff" stroke-width="1.2" opacity="0.6"/>
<path d="M84 22 Q80 16 74 20 Q78 24 82 22" fill="none" stroke="#cc44ff" stroke-width="1.2" opacity="0.6"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

+46
View File
@@ -0,0 +1,46 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<defs>
<radialGradient id="wp-glow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#ff6b35" stop-opacity="0.3"/>
<stop offset="100%" stop-color="#ff6b35" stop-opacity="0"/>
</radialGradient>
</defs>
<!-- Fond -->
<circle cx="50" cy="50" r="48" fill="#101622" stroke="#1a2436" stroke-width="2"/>
<circle cx="50" cy="50" r="48" fill="url(#wp-glow)"/>
<!-- Dao (sabre courbé) — de bas-gauche à haut-droit -->
<!-- Lame du dao (légèrement courbée) -->
<path d="M22 78 Q30 60 45 45 Q58 32 72 22"
fill="none" stroke="#ff6b35" stroke-width="3.5" stroke-linecap="round"/>
<!-- Dos de la lame (trait intérieur plus fin) -->
<path d="M25 76 Q33 58 47 44 Q59 33 72 24"
fill="none" stroke="#ff6b35" stroke-width="1" stroke-linecap="round" opacity="0.5"/>
<!-- Brillance de la lame -->
<path d="M28 73 Q38 56 52 42"
fill="none" stroke="#ff6b35" stroke-width="0.8" stroke-linecap="round" opacity="0.8" stroke-dasharray="2 3"/>
<!-- Garde du dao (petite croix ornementée) -->
<path d="M36 66 Q38 60 46 56 Q50 54 56 58 Q50 62 44 68 Z"
fill="#ff6b35" fill-opacity="0.3" stroke="#ff6b35" stroke-width="1.5"/>
<!-- Poignée du dao -->
<line x1="22" y1="78" x2="15" y2="86" stroke="#ff6b35" stroke-width="4" stroke-linecap="round" opacity="0.7"/>
<rect x="13" y="84" width="6" height="4" rx="2" fill="#ff6b35" fill-opacity="0.5" stroke="#ff6b35" stroke-width="1" transform="rotate(-45 16 86)"/>
<!-- Lance — de bas-droit à haut-gauche -->
<!-- Hampe de la lance -->
<line x1="78" y1="78" x2="22" y2="22" stroke="#ff6b35" stroke-width="2" stroke-linecap="round" opacity="0.7"/>
<!-- Pointe de la lance (haut-gauche) -->
<path d="M22 22 L16 14 L20 20 L14 18 Z" fill="#ff6b35" stroke="#ff6b35" stroke-width="1" stroke-linejoin="round"/>
<!-- Ornement milieu de hampe -->
<path d="M44 44 Q48 40 52 44 Q48 48 44 44 Z" fill="#ff6b35" fill-opacity="0.4" stroke="#ff6b35" stroke-width="1"/>
<!-- Embout de la lance (bas-droit) -->
<path d="M78 78 L84 85 L80 80 L86 82 Z" fill="#ff6b35" stroke="#ff6b35" stroke-width="1" stroke-linejoin="round" opacity="0.7"/>
<!-- Petites étincelles au croisement -->
<circle cx="50" cy="50" r="3" fill="#ff6b35" fill-opacity="0.3" stroke="#ff6b35" stroke-width="1.5"/>
<line x1="50" y1="44" x2="50" y2="40" stroke="#ff6b35" stroke-width="1" opacity="0.7"/>
<line x1="56" y1="50" x2="60" y2="50" stroke="#ff6b35" stroke-width="1" opacity="0.7"/>
<line x1="44" y1="50" x2="40" y2="50" stroke="#ff6b35" stroke-width="1" opacity="0.7"/>
<line x1="50" y1="56" x2="50" y2="60" stroke="#ff6b35" stroke-width="1" opacity="0.7"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

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