Refonte complète du système Anomalies

- DataModel : renommage value→level (1-4), ajout usesRemaining (0-4), suppression scores/notes
- Config : ajout ANOMALY_DEFINITIONS avec compétences applicables par type (8 anomalies)
- Fiche item anomalie : header avec level/uses visuels (dots), barre de compétences applicables,
  2 onglets Description + Technique/Narratif (suppression onglet Scores)
- Fiche PJ onglet Domaines : bloc anomalie proéminent unique avec:
  - Nom + sous-type + icône
  - Dots niveau (●●○○)
  - Dots usages + bouton Utiliser + bouton Réinitialiser
  - Chips des domaines applicables
- Actions : useAnomaly (décrémente usesRemaining), resetAnomalyUses (reset au niveau)
- Contrainte : max 1 anomalie par personnage (drop + createAnomaly)
- Helpers HBS : lte, gte, lt ajoutés
- i18n : nouvelles clés Anomaly.* (level, usesRemaining, use, resetUses, etc.)
- CSS : .anomaly-block sur fiche PJ, dots animés, .anomaly-uses-row sur fiche item

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-03-28 18:15:06 +01:00
parent cff700bd3d
commit f9ddcdf9da
11 changed files with 474 additions and 41 deletions

View File

@@ -55,7 +55,7 @@ Hooks.once("init", () => {
CONFIG.Actor.trackableAttributes = {
character: {
bar: ["blessures.lvl"],
value: ["initiative", "anomaly.value"],
value: ["initiative", "anomaly.level"],
},
npc: {
bar: ["blessures.lvl"],
@@ -132,6 +132,15 @@ function _registerHandlebarsHelpers() {
// Helper : greater than
Handlebars.registerHelper("gt", (a, b) => a > b)
// Helper : less than or equal
Handlebars.registerHelper("lte", (a, b) => a <= b)
// Helper : greater than or equal
Handlebars.registerHelper("gte", (a, b) => a >= b)
// Helper : less than
Handlebars.registerHelper("lt", (a, b) => a < b)
// Helper : logical OR
Handlebars.registerHelper("or", (...args) => args.slice(0, -1).some(Boolean))

View File

@@ -47,6 +47,15 @@
},
"Anomaly": {
"type": "Type d'anomalie",
"level": "Niveau",
"usesRemaining": "Utilisations restantes",
"use": "Utiliser l'anomalie",
"resetUses": "Réinitialiser les utilisations (nouveau scénario)",
"noAnomaly": "Aucune anomalie",
"noUsesLeft": "Plus d'utilisations disponibles pour ce scénario",
"maxAnomaly": "Un personnage ne peut avoir qu'une seule anomalie",
"applicableSkills": "Domaines applicables",
"moonDie": "Dé de lune",
"none": "Aucune",
"entropie": "Entropie",
"communicationaveclesmorts": "Communication avec les morts",
@@ -143,6 +152,7 @@
"extreme": "Extrême"
},
"Item": {
"anomaly": "Anomalie",
"anomalies": "Anomalies",
"aspects": "Aspects",
"attributes": "Attributs",

View File

@@ -108,6 +108,10 @@ export default class CelestopolActorSheet extends HandlebarsApplicationMixin(fou
}
async _onDropItem(item) {
if (item.type === "anomaly" && this.document.itemTypes.anomaly.length > 0) {
ui.notifications.warn(game.i18n.localize("CELESTOPOL.Anomaly.maxAnomaly"))
return
}
await this.document.createEmbeddedDocuments("Item", [item.toObject()], { renderSheet: false })
}

View File

@@ -8,10 +8,12 @@ export default class CelestopolCharacterSheet extends CelestopolActorSheet {
position: { width: 920, height: 660 },
window: { contentClasses: ["character-content"] },
actions: {
createAnomaly: CelestopolCharacterSheet.#onCreateAnomaly,
createAspect: CelestopolCharacterSheet.#onCreateAspect,
createAttribute: CelestopolCharacterSheet.#onCreateAttribute,
createEquipment: CelestopolCharacterSheet.#onCreateEquipment,
createAnomaly: CelestopolCharacterSheet.#onCreateAnomaly,
createAspect: CelestopolCharacterSheet.#onCreateAspect,
createAttribute: CelestopolCharacterSheet.#onCreateAttribute,
createEquipment: CelestopolCharacterSheet.#onCreateEquipment,
useAnomaly: CelestopolCharacterSheet.#onUseAnomaly,
resetAnomalyUses: CelestopolCharacterSheet.#onResetAnomalyUses,
},
}
@@ -64,9 +66,21 @@ export default class CelestopolCharacterSheet extends CelestopolActorSheet {
case "competences":
context.tab = context.tabs.competences
context.anomalies = doc.itemTypes.anomaly
context.anomaly = doc.itemTypes.anomaly[0] ?? null
context.aspects = doc.itemTypes.aspect
context.attributes = doc.itemTypes.attribute
if (context.anomaly) {
const def = SYSTEM.ANOMALY_DEFINITIONS[context.anomaly.system.subtype] ?? SYSTEM.ANOMALY_DEFINITIONS.none
context.anomalySkillLabels = def.technicalSkills.map(key => {
if (key === "lune") return game.i18n.localize("CELESTOPOL.Anomaly.moonDie")
for (const skills of Object.values(SYSTEM.SKILLS)) {
if (skills[key]) return game.i18n.localize(skills[key].label)
}
return key
})
} else {
context.anomalySkillLabels = []
}
break
case "blessures":
@@ -91,6 +105,10 @@ export default class CelestopolCharacterSheet extends CelestopolActorSheet {
}
static #onCreateAnomaly() {
if (this.document.itemTypes.anomaly.length > 0) {
ui.notifications.warn(game.i18n.localize("CELESTOPOL.Anomaly.maxAnomaly"))
return
}
this.document.createEmbeddedDocuments("Item", [{
name: game.i18n.localize("CELESTOPOL.Item.newAnomaly"), type: "anomaly",
}])
@@ -113,4 +131,23 @@ export default class CelestopolCharacterSheet extends CelestopolActorSheet {
name: game.i18n.localize("CELESTOPOL.Item.newEquipment"), type: "equipment",
}])
}
static async #onUseAnomaly(event, target) {
const itemId = target.dataset.itemId
const anomaly = this.document.items.get(itemId)
if (!anomaly) return
const current = anomaly.system.usesRemaining
if (current <= 0) {
ui.notifications.warn(game.i18n.localize("CELESTOPOL.Anomaly.noUsesLeft"))
return
}
await anomaly.update({ "system.usesRemaining": current - 1 })
}
static async #onResetAnomalyUses(event, target) {
const itemId = target.dataset.itemId
const anomaly = this.document.items.get(itemId)
if (!anomaly) return
await anomaly.update({ "system.usesRemaining": anomaly.system.level })
}
}

View File

@@ -4,7 +4,7 @@ import { SYSTEM } from "../../config/system.mjs"
export class CelestopolAnomalySheet extends CelestopolItemSheet {
static DEFAULT_OPTIONS = {
classes: ["anomaly"],
position: { width: 620, height: 560 },
position: { width: 560, height: 460 },
}
static PARTS = {
main: { template: "systems/fvtt-celestopol/templates/anomaly.hbs" },
@@ -12,7 +12,16 @@ export class CelestopolAnomalySheet extends CelestopolItemSheet {
async _prepareContext() {
const ctx = await super._prepareContext()
ctx.anomalyTypes = SYSTEM.ANOMALY_TYPES
ctx.skills = SYSTEM.SKILLS
const def = SYSTEM.ANOMALY_DEFINITIONS[ctx.system.subtype] ?? SYSTEM.ANOMALY_DEFINITIONS.none
ctx.applicableSkillLabels = def.technicalSkills.map(key => {
if (key === "lune") return game.i18n.localize("CELESTOPOL.Anomaly.moonDie")
for (const skills of Object.values(SYSTEM.SKILLS)) {
if (skills[key]) return game.i18n.localize(skills[key].label)
}
return key
})
ctx.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
this.document.system.description, { async: true })
ctx.enrichedTechnique = await foundry.applications.ux.TextEditor.implementation.enrichHTML(

View File

@@ -59,6 +59,23 @@ export const ANOMALY_TYPES = {
voyageastral: { id: "voyageastral", label: "CELESTOPOL.Anomaly.voyageastral" },
}
/**
* Définitions des anomalies : compétences applicables pour l'usage Technique.
* "lune" est une clé spéciale désignant le dé de lune (Entropie).
* Les autres clés correspondent aux identifiants de domaine dans SKILLS.
*/
export const ANOMALY_DEFINITIONS = {
none: { technicalSkills: [] },
entropie: { technicalSkills: ["lune"] },
communicationaveclesmorts:{ technicalSkills: ["instruction", "mtechnologique", "raisonnement", "traitement"] },
telekinesie: { technicalSkills: ["echauffouree", "effacement", "mobilite", "prouesse"] },
telepathie: { technicalSkills: ["appreciation", "attraction", "echauffouree", "faveur"] },
tarotdivinatoire: { technicalSkills: ["appreciation", "arts", "inspiration", "traque"] },
illusion: { technicalSkills: ["coercition", "echauffouree", "effacement", "traque"] },
suggestion: { technicalSkills: ["artifice", "attraction", "coercition", "faveur"] },
voyageastral: { technicalSkills: ["appreciation", "mtechnologique", "traitement", "traque"] },
}
/** Factions du monde de Célestopol. */
export const FACTIONS = {
pinkerton: { id: "pinkerton", label: "CELESTOPOL.Faction.pinkerton" },
@@ -122,6 +139,7 @@ export const SYSTEM = {
SKILLS,
ALL_SKILLS,
ANOMALY_TYPES,
ANOMALY_DEFINITIONS,
FACTIONS,
WOUND_LEVELS,
DIFFICULTY_CHOICES,

View File

@@ -31,15 +31,14 @@ export class CelestopolAnomaly extends foundry.abstract.TypeDataModel {
const fields = foundry.data.fields
const reqInt = { required: true, nullable: false, integer: true }
return {
subtype: new fields.StringField({ required: true, nullable: false, initial: "none",
choices: Object.keys(SYSTEM.ANOMALY_TYPES) }),
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }),
reference: new fields.StringField({ required: true, nullable: false, initial: "" }),
scores: skillScoresSchema(),
description: new fields.HTMLField({ required: true, textSearch: true }),
technique: new fields.HTMLField({ required: true, textSearch: true }),
narratif: new fields.HTMLField({ required: true, textSearch: true }),
notes: new fields.HTMLField({ required: true, textSearch: true }),
subtype: new fields.StringField({ required: true, nullable: false, initial: "none",
choices: Object.keys(SYSTEM.ANOMALY_TYPES) }),
level: new fields.NumberField({ ...reqInt, initial: 2, min: 1, max: 4 }),
usesRemaining: new fields.NumberField({ ...reqInt, initial: 2, min: 0, max: 4 }),
reference: new fields.StringField({ required: true, nullable: false, initial: "" }),
description: new fields.HTMLField({ required: true, textSearch: true }),
technique: new fields.HTMLField({ required: true, textSearch: true }),
narratif: new fields.HTMLField({ required: true, textSearch: true }),
}
}
}

View File

@@ -250,4 +250,187 @@
.section-header { .cel-section-header(); }
.enriched-html { font-size: 0.9em; line-height: 1.6; }
}
// ── Bloc Anomalie sur l'onglet Domaines ──────────────────────────────────
.anomaly-block {
border: 1px solid rgba(196,154,26,0.5);
border-radius: 4px;
margin-bottom: 12px;
overflow: hidden;
.anomaly-block-header {
background: var(--cel-green);
background-image: url("../assets/ui/fond_cadrille.jpg");
background-blend-mode: soft-light;
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px 10px;
border-bottom: 2px solid var(--cel-orange);
.anomaly-block-title {
font-family: var(--cel-font-title);
font-size: 0.85em;
text-transform: uppercase;
letter-spacing: 0.07em;
color: var(--cel-orange);
}
a { color: var(--cel-orange-light); &:hover { color: var(--cel-orange); } }
}
.anomaly-empty {
padding: 12px;
text-align: center;
color: var(--cel-border);
font-style: italic;
font-size: 0.85em;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
i { font-size: 1.1em; }
}
.anomaly-content {
padding: 8px 10px;
background: rgba(240,232,212,0.06);
.anomaly-info-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 7px;
.anomaly-icon {
width: 40px;
height: 40px;
object-fit: cover;
border: 1px solid var(--cel-orange);
border-radius: 3px;
}
.anomaly-details {
flex: 1;
.anomaly-name {
font-family: var(--cel-font-title);
font-size: 1em;
color: var(--cel-orange);
font-weight: bold;
}
.anomaly-subtype {
font-size: 0.75em;
color: var(--cel-orange-light);
text-transform: uppercase;
letter-spacing: 0.05em;
}
}
.anomaly-controls {
display: flex;
gap: 6px;
a { color: var(--cel-border); font-size: 0.9em; &:hover { color: var(--cel-orange); } }
}
}
.anomaly-level-row {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 6px;
.anomaly-level-label {
font-size: 0.72em;
text-transform: uppercase;
color: var(--cel-orange-light);
white-space: nowrap;
}
.anomaly-level-dots {
display: flex;
gap: 4px;
.anomaly-level-dot {
width: 10px;
height: 10px;
border-radius: 50%;
border: 2px solid var(--cel-orange);
&.active { background: var(--cel-orange); }
&.inactive { background: transparent; border-color: rgba(196,154,26,0.25); }
}
}
}
.anomaly-uses-row {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
.anomaly-uses-label {
font-size: 0.72em;
text-transform: uppercase;
color: var(--cel-orange-light);
white-space: nowrap;
}
.anomaly-uses-dots {
display: flex;
gap: 4px;
.anomaly-dot {
width: 12px;
height: 12px;
border-radius: 50%;
border: 2px solid var(--cel-orange);
&.available { background: var(--cel-orange); }
&.spent { background: transparent; }
&.inactive { background: transparent; border-color: rgba(196,154,26,0.25); }
}
}
.anomaly-use-btn {
background: var(--cel-green);
border: 1px solid var(--cel-orange);
color: var(--cel-orange);
font-size: 0.72em;
padding: 2px 8px;
cursor: pointer;
font-family: var(--cel-font-title);
text-transform: uppercase;
letter-spacing: 0.04em;
border-radius: 2px;
transition: background 0.15s;
&:hover:not(:disabled) { background: var(--cel-green-light); }
&:disabled { opacity: 0.4; cursor: default; }
}
.anomaly-reset-btn {
color: var(--cel-border);
font-size: 0.9em;
&:hover { color: var(--cel-orange); }
}
}
.anomaly-skills {
margin-top: 6px;
display: flex;
flex-wrap: wrap;
gap: 4px;
align-items: center;
.anomaly-skills-label {
font-size: 0.7em;
color: var(--cel-border);
text-transform: uppercase;
}
.anomaly-skill-chip {
background: rgba(196,154,26,0.1);
border: 1px solid rgba(196,154,26,0.3);
border-radius: 3px;
padding: 1px 5px;
font-size: 0.7em;
color: var(--cel-orange-light);
}
}
}
}
}

View File

@@ -165,6 +165,90 @@
}
}
// ── Anomaly-specific styles ───────────────────────────────────────────────
&.anomaly {
.anomaly-level-field {
display: flex;
align-items: center;
gap: 4px;
label { color: var(--cel-orange-light); font-size: 0.75em; text-transform: uppercase; }
.level-input, .anomaly-level-value {
width: 38px;
background: transparent;
border: 1px solid var(--cel-orange-light);
color: var(--cel-orange);
text-align: center;
font-size: 1.1em;
font-weight: bold;
}
}
.anomaly-uses-row {
display: flex;
align-items: center;
gap: 6px;
margin-top: 5px;
padding-top: 4px;
border-top: 1px solid rgba(196,154,26,0.2);
.anomaly-uses-label {
color: var(--cel-orange-light);
font-size: 0.7em;
text-transform: uppercase;
white-space: nowrap;
}
.anomaly-uses-dots { display: flex; gap: 4px; }
.uses-number-input {
width: 34px;
background: transparent;
border: 1px solid rgba(196,154,26,0.4);
color: var(--cel-orange);
text-align: center;
font-size: 0.85em;
}
}
.anomaly-dot {
width: 12px;
height: 12px;
border-radius: 50%;
border: 2px solid var(--cel-orange);
display: inline-block;
&.available { background: var(--cel-orange); }
&.spent { background: transparent; }
&.inactive { background: transparent; border-color: rgba(196,154,26,0.25); }
}
.anomaly-skills-bar {
background: rgba(0,0,0,0.18);
border-bottom: 1px solid rgba(196,154,26,0.2);
padding: 5px 10px;
display: flex;
flex-wrap: wrap;
gap: 5px;
align-items: center;
.anomaly-skills-label {
color: var(--cel-orange-light);
font-size: 0.72em;
text-transform: uppercase;
margin-right: 2px;
}
.anomaly-skill-chip {
background: rgba(196,154,26,0.12);
border: 1px solid rgba(196,154,26,0.35);
border-radius: 3px;
padding: 1px 6px;
font-size: 0.72em;
color: var(--cel-orange-light);
}
}
}
// Equipment-specific
&.equipment {
.equipment-stats {

View File

@@ -11,19 +11,51 @@
<option value="{{key}}" {{#if (eq key ../system.subtype)}}selected{{/if}}>{{localize atype.label}}</option>
{{/each}}
</select>
<div class="item-value-field">
<label>{{localize "CELESTOPOL.Item.value"}}</label>
<input type="number" name="system.value" value="{{system.value}}" min="0" max="8"
{{#unless isEditable}}disabled{{/unless}}>
<div class="anomaly-level-field">
<label>{{localize "CELESTOPOL.Anomaly.level"}}</label>
{{#if isEditable}}
<input type="number" name="system.level" value="{{system.level}}" min="1" max="4" class="level-input">
{{else}}
<span class="anomaly-level-value">{{system.level}}</span>
{{/if}}
</div>
</div>
{{!-- Usages restants : dots visuels --}}
<div class="anomaly-uses-row">
<span class="anomaly-uses-label">{{localize "CELESTOPOL.Anomaly.usesRemaining"}} :</span>
<div class="anomaly-uses-dots">
{{#each (array 1 2 3 4) as |n|}}
{{#if (lte n ../system.usesRemaining)}}
<span class="anomaly-dot available"></span>
{{else}}
{{#if (lte n ../system.level)}}
<span class="anomaly-dot spent"></span>
{{else}}
<span class="anomaly-dot inactive"></span>
{{/if}}
{{/if}}
{{/each}}
</div>
{{#if isEditable}}
<input type="number" name="system.usesRemaining" value="{{system.usesRemaining}}" min="0" max="4" class="uses-number-input">
{{/if}}
</div>
</div>
</header>
{{!-- Domaines applicables --}}
{{#if applicableSkillLabels.length}}
<div class="anomaly-skills-bar">
<span class="anomaly-skills-label">{{localize "CELESTOPOL.Anomaly.applicableSkills"}} :</span>
{{#each applicableSkillLabels as |label|}}
<span class="anomaly-skill-chip">{{label}}</span>
{{/each}}
</div>
{{/if}}
<nav class="item-tabs sheet-tabs tabs" data-group="item-tabs">
<a class="item active" data-group="item-tabs" data-tab="description">{{localize "CELESTOPOL.Tab.description"}}</a>
<a class="item" data-group="item-tabs" data-tab="technique">{{localize "CELESTOPOL.Tab.technique"}}</a>
<a class="item" data-group="item-tabs" data-tab="scores">{{localize "CELESTOPOL.Item.scores"}}</a>
</nav>
<section class="tab active" data-group="item-tabs" data-tab="description">
@@ -47,8 +79,4 @@
{{editor system.narratif target="system.narratif" button=true editable=isEditable}}
</div>
</section>
<section class="tab" data-group="item-tabs" data-tab="scores">
{{> "systems/fvtt-celestopol/templates/partials/item-scores.hbs" skills=skills stats=stats system=system}}
</section>
</div>

View File

@@ -58,27 +58,79 @@
{{/each}}
</div>
{{!-- Items : Anomalies, Aspects, Attributs --}}
{{!-- Items : Anomalie (unique), Aspects, Attributs --}}
<div class="items-section">
{{!-- Anomalies --}}
<div class="items-group">
<div class="items-header">
<span>{{localize "CELESTOPOL.Item.anomalies"}}</span>
{{!-- Anomalie : bloc proéminent unique --}}
<div class="anomaly-block">
<div class="anomaly-block-header">
<span class="anomaly-block-title">{{localize "CELESTOPOL.Item.anomaly"}}</span>
{{#if isEditMode}}
<a data-action="createAnomaly" title="{{localize 'CELESTOPOL.Item.newAnomaly'}}"><i class="fas fa-plus"></i></a>
{{#unless anomaly}}
<a data-action="createAnomaly" title="{{localize 'CELESTOPOL.Item.newAnomaly'}}"><i class="fas fa-plus"></i></a>
{{/unless}}
{{/if}}
</div>
{{#each anomalies as |item|}}
<div class="item-row" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}" data-drag="true">
<img src="{{item.img}}" class="item-icon" alt="{{item.name}}">
<span class="item-name">{{item.name}}</span>
<span class="item-value">{{item.system.value}}</span>
<div class="item-controls">
<a data-action="edit" data-item-uuid="{{item.uuid}}"><i class="fas fa-edit"></i></a>
{{#if ../isEditMode}}<a data-action="delete" data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a>{{/if}}
{{#if anomaly}}
<div class="anomaly-content" data-item-id="{{anomaly.id}}" data-item-uuid="{{anomaly.uuid}}" data-drag="true">
<div class="anomaly-info-row">
<img src="{{anomaly.img}}" class="anomaly-icon" alt="{{anomaly.name}}">
<div class="anomaly-details">
<div class="anomaly-name">{{anomaly.name}}</div>
<div class="anomaly-subtype">{{localize (lookup (lookup anomalyTypes anomaly.system.subtype) 'label')}}</div>
</div>
<div class="anomaly-controls">
<a data-action="edit" data-item-uuid="{{anomaly.uuid}}"><i class="fas fa-edit"></i></a>
{{#if isEditMode}}<a data-action="delete" data-item-uuid="{{anomaly.uuid}}"><i class="fas fa-trash"></i></a>{{/if}}
</div>
</div>
<div class="anomaly-level-row">
<span class="anomaly-level-label">{{localize "CELESTOPOL.Anomaly.level"}} :</span>
<div class="anomaly-level-dots">
{{#each (array 1 2 3 4) as |n|}}
<span class="anomaly-level-dot {{#if (lte n ../anomaly.system.level)}}active{{else}}inactive{{/if}}"></span>
{{/each}}
</div>
</div>
<div class="anomaly-uses-row">
<span class="anomaly-uses-label">{{localize "CELESTOPOL.Anomaly.usesRemaining"}} :</span>
<div class="anomaly-uses-dots">
{{#each (array 1 2 3 4) as |n|}}
{{#if (lte n ../anomaly.system.usesRemaining)}}
<span class="anomaly-dot available"></span>
{{else}}
{{#if (lte n ../anomaly.system.level)}}
<span class="anomaly-dot spent"></span>
{{else}}
<span class="anomaly-dot inactive"></span>
{{/if}}
{{/if}}
{{/each}}
</div>
<button class="anomaly-use-btn" data-action="useAnomaly" data-item-id="{{anomaly.id}}"
title="{{localize 'CELESTOPOL.Anomaly.use'}}"
{{#unless (gt anomaly.system.usesRemaining 0)}}disabled{{/unless}}>
<i class="fas fa-bolt"></i> {{localize "CELESTOPOL.Anomaly.use"}}
</button>
<a class="anomaly-reset-btn" data-action="resetAnomalyUses" data-item-id="{{anomaly.id}}"
title="{{localize 'CELESTOPOL.Anomaly.resetUses'}}">
<i class="fas fa-rotate-right"></i>
</a>
</div>
{{#if anomalySkillLabels.length}}
<div class="anomaly-skills">
<span class="anomaly-skills-label">{{localize "CELESTOPOL.Anomaly.applicableSkills"}} :</span>
{{#each anomalySkillLabels as |label|}}
<span class="anomaly-skill-chip">{{label}}</span>
{{/each}}
</div>
{{/if}}
</div>
{{/each}}
{{else}}
<div class="anomaly-empty">
<i class="fas fa-ghost"></i>
<span>{{localize "CELESTOPOL.Anomaly.noAnomaly"}}</span>
</div>
{{/if}}
</div>
{{!-- Aspects --}}