Compare commits

...

90 Commits

Author SHA1 Message Date
uberwald 31e2561797 feat: separate skill value/max (transcendence pool vs level)
Release Creation / build (release) Successful in 58s
- skill.value = spendable transcendence pool (editable select)
- skill.max = max level (locked by default, unlock btn)
- computeRollFormula/computeResults/confront use skill.max
- clamp transcendence spend to skill.value
- resetTranscendence button in Bio&Notes
- v14 compat: duplicate→deepClone, mergeObject→spread, socket fixes
- Dialog v1 removed, DragDrop dedup, await fixes
2026-07-24 09:00:26 +02:00
uberwald 8c79e6f0ba PUsh all v14 Stuff
Release Creation / build (release) Successful in 2m11s
2026-07-21 23:58:37 +02:00
uberwald 55a3c694db fix: translate specialisation compendium sub-folders (skills) via Babele 2026-07-21 23:40:05 +02:00
uberwald 85c970869c feat: add Foundry publish step to release workflow 2026-07-21 23:14:20 +02:00
uberwald 97f1f999b1 fix: spleen/ideal name fills available width, remove item-filler spacer 2026-07-21 22:39:26 +02:00
uberwald 06c1beabe0 fix: i18n audit - fix typos, add missing keys, align fr/en 2026-07-21 22:35:26 +02:00
uberwald 3ac589d411 PUsh all v14 Stuff
Release Creation / build (release) Successful in 42s
2026-07-21 22:22:57 +02:00
uberwald f4d7b99497 PUsh all v14 Stuff 2026-07-21 22:22:44 +02:00
uberwald 92b2a5ef5e fix: build CSS from LESS in CI before zipping release
Release Creation / build (release) Successful in 41s
2026-07-21 22:19:19 +02:00
uberwald 3b621d2af7 chore: remove stale migration docs, fix release zip ref to LICENSE.md
Release Creation / build (release) Successful in 31s
2026-07-21 22:02:04 +02:00
uberwald 31b22fad68 fix: replace gitea.com release action with direct API upload 2026-07-21 22:01:32 +02:00
uberwald f08a741708 fix: add https:// prefix to Gitea release action refs
Release Creation / build (release) Failing after 30s
2026-07-21 21:58:55 +02:00
uberwald f49d9e2cf4 PUsh all v14 Stuff
Release Creation / build (release) Failing after 30s
2026-07-21 21:45:06 +02:00
uberwald 6ae99aadb0 Fix v14 migration issues
Release Creation / build (release) Successful in 45s
2026-05-25 23:08:58 +02:00
uberwald c1a9bfbb04 Corrections CSS diverses 2026-05-10 18:37:58 +02:00
uberwald 0fec217528 Foundryv14 migration
Release Creation / build (release) Successful in 46s
2026-04-01 22:25:32 +02:00
uberwald 1d95628a23 Migration vers datamodels
Release Creation / build (release) Successful in 46s
2026-02-26 13:52:52 +01:00
uberwald 4a65bee2dc Migration vers datamodels 2026-02-26 13:52:21 +01:00
uberwald 497c687eb8 Migration vers datamodels 2026-02-26 13:45:24 +01:00
uberwald b0c6b2a3e8 Migration vers datamodels 2026-02-26 13:33:28 +01:00
uberwald 055d853171 Ajout des app v2 et refont du CSS 2026-02-26 11:08:14 +01:00
uberwald f1ab04bf32 Migration vers datamodels 2026-02-25 15:49:55 +01:00
uberwald 64eb40abfb Migration vers datamodels 2026-02-24 18:31:28 +01:00
uberwald b5b1d2ca24 Remove invalid DataModels - keep only template.json types
Removed DataModels that are NOT in template.json types array:
- scar, annency-item, boheme, contact, confrontation

Only valid Item types are: equipment, weapon, trait, specialization, maneuver
Only valid Actor types are: pc, npc, annency

Updated:
- modules/models/_module.js
- modules/ecryme-main.js (CONFIG.Item.dataModels)
- system.json (documentTypes)
- modules/models/README.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-18 18:40:58 +01:00
uberwald 5ce0059f1b Add documentTypes section to system.json for DataModels
Declare all Actor and Item types with their HTML fields.
Required for Foundry VTT v13 with DataModels.

Actor types: pc, npc, annency
Item types: equipment, weapon, trait, specialization, maneuver, scar, annency, boheme, contact, confrontation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-18 11:44:00 +01:00
uberwald 76a5afc79e Fix buildSkillConfig to work with DataModels
Replace game.data.template.Actor reference with hardcoded skill structure.
With DataModels, template.json is no longer used at runtime.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-18 11:35:46 +01:00
uberwald f130f24a23 Migration complète vers DataModels Foundry VTT
- Ajout de 14 DataModels (10 Items + 3 Acteurs)
  * Items: equipment, weapon, trait, specialization, maneuver, scar, annency, boheme, contact, confrontation
  * Acteurs: pc, npc, annency

- Corrections d'initialisation
  * Ordre d'initialisation corrigé (CONFIG.dataModels avant game.system)
  * Import dynamique des DataModels pour éviter timing issues
  * Helper functions pour éviter réutilisation de champs

- Documentation complète
  * AUDIT_DATAMODELS.md: Rapport d'audit complet (85+ champs vérifiés)
  * MIGRATION_DATAMODELS.md: Guide de migration
  * FIX_INIT_ERROR.md: Résolution des erreurs
  * BABELE_ERROR_ANALYSIS.md: Analyse erreur Babele
  * RESUME_MIGRATION.md: Résumé complet
  * modules/models/README.md: Documentation des DataModels

- template.json marqué comme DEPRECATED
- changelog.md mis à jour

Note: Erreur Babele/LibWrapper non résolue (problème de module externe)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-18 11:32:29 +01:00
uberwald e47ad95a38 Fix FR translatrion with babele 2026-02-18 00:08:07 +01:00
uberwald fc386487e8 Fix FR translatrion with babele 2026-02-17 23:38:06 +01:00
uberwald bda88c067e Release with specific message 2025-10-17 22:50:39 +02:00
uberwald 4003e0e42b Release with specific message 2025-10-17 22:50:04 +02:00
uberwald a6d811bcda Sync message 2025-10-17 15:32:44 +02:00
uberwald 94eb637637 Sync message 2025-10-17 15:32:22 +02:00
uberwald 8c58367cdc Sync message 2025-10-17 15:31:53 +02:00
uberwald c439ca978c Update internal system with dynamic message 2025-10-17 15:17:04 +02:00
uberwald ffe1144f2a Update internal system with dynamic message 2025-10-17 15:16:00 +02:00
uberwald 38ef07d17b Update internal system with dynamic message 2025-10-17 15:15:03 +02:00
uberwald a8cc2dce4b Update internal system with dynamic message 2025-10-17 15:14:29 +02:00
uberwald 0fadd0783c Update internal system with dynamic message 2025-10-17 15:12:19 +02:00
uberwald a55a038d32 Update internal system with dynamic message 2025-10-17 15:01:42 +02:00
uberwald d012f78881 Update internal system with dynamic message 2025-10-17 14:59:37 +02:00
uberwald 01e13da234 Correction sur application tokens acteurs 2025-10-17 00:33:24 +02:00
uberwald bc09b5050d Correction sur application tokens acteurs 2025-10-17 00:32:42 +02:00
uberwald d68001b376 Fix token/actor 2025-10-02 22:53:09 +02:00
uberwald 6b22dade9c Foundry v13 migration 2025-05-01 23:27:51 +02:00
uberwald 965fc02eb3 Foundry v13 migration 2025-05-01 23:23:29 +02:00
uberwald 0ef689bf1b Fix specialization again 2025-03-11 23:47:08 +01:00
uberwald c6dcc187d8 Fix babele 2025-03-11 22:29:24 +01:00
uberwald e2f24405af Fix waring about grid 2025-02-14 13:36:52 +01:00
uberwald 0487893f47 Fix v12 issue + remove warnings 2024-04-27 09:33:22 +02:00
uberwald d7b7bccbdf Update v11/v12 et correction sur le niveau de jeu 2024-04-26 19:31:30 +02:00
uberwald dbf4b17afb Update v11/v12 et correction sur le niveau de jeu 2024-04-26 19:28:57 +02:00
uberwald 51e5a409c4 Update v11/v12 et correction sur le niveau de jeu 2024-04-26 19:28:28 +02:00
uberwald 1e4692e850 Ajout des traits dans le header + gestion plus simple des PNJs 2024-03-19 09:16:08 +01:00
uberwald 40e12c1bba Ajout des traits dans le header + gestion plus simple des PNJs 2024-03-19 09:15:30 +01:00
uberwald 2450dce46c Fix traits bonus/malus and pictures in journals 2024-03-18 21:57:15 +01:00
uberwald 67c1066194 Fix traits bonus/malus and pictures in journals 2024-03-18 21:56:10 +01:00
uberwald 65fe498572 Fix background+landing pages 2024-03-15 12:12:02 +01:00
uberwald e1990f88b4 Fix background+landing pages 2024-03-15 12:10:44 +01:00
uberwald f99abb0966 Enhance stats 2024-02-08 13:02:57 +01:00
uberwald ec6e6be231 Fix confrontation with players 2023-10-12 08:28:30 +02:00
uberwald 9d26d7bfad Amélirations des specialisations 2023-10-10 20:04:02 +02:00
uberwald 81848155e5 Amélirations des specialisations 2023-10-10 20:02:13 +02:00
uberwald 6778a9aa0b Release fix 2023-08-26 21:49:32 +02:00
uberwald 63aa49c77f Fix CI/CD 2023-08-26 19:18:07 +02:00
uberwald 0a3265419a Move repo to public area 2023-08-26 09:57:43 +02:00
uberwald f5093980b0 Move repo to public area 2023-08-26 09:53:14 +02:00
uberwald 062a597b70 Various fixes in equipment, biodata 2023-08-25 09:18:45 +02:00
uberwald 1d0d123947 Various fixes in equipment, biodata 2023-08-25 09:09:21 +02:00
uberwald bf94dc30b8 Picture link 2023-08-22 08:35:02 +02:00
uberwald fc3b83cfc2 Picture link 2023-08-22 08:34:12 +02:00
uberwald 19b88c3de9 Add changelog 2023-08-21 15:20:10 +02:00
uberwald d669059403 Corrections diverses 2023-08-17 08:31:22 +02:00
uberwald 45d419115d Needs babele, fix translation, fix margine 2023-08-16 21:17:49 +02:00
uberwald b9e2a77e88 Needs babele, fix translation, fix margine 2023-08-16 21:17:34 +02:00
uberwald 5dca748bf3 v11 ready 2023-08-16 20:52:48 +02:00
uberwald 98f9e1e2a6 v11 ready 2023-08-16 20:49:26 +02:00
uberwald 1a90be13be v11 ready 2023-08-16 20:45:23 +02:00
uberwald e2f97762b9 Add workflow 2023-08-14 17:39:39 +02:00
uberwald de11053a9a Add workflow 2023-08-14 17:02:40 +02:00
uberwald 3f7ce9918e Add workflow 2023-08-14 16:59:00 +02:00
uberwald 919147b4af Add workflow 2023-08-14 16:58:18 +02:00
uberwald 22953744ba Add workflow 2023-08-14 16:55:43 +02:00
uberwald 6aee41b817 Final import 2023-08-14 16:47:12 +02:00
uberwald e7268a1578 Fix NPC sheet 2023-08-09 20:44:25 +02:00
uberwald 0d25bbe764 Ajout annence et NPC 2023-08-09 17:33:11 +02:00
uberwald 554a1af531 Annence/Boheme 2023-08-08 18:05:10 +02:00
uberwald b2219c0f81 Annence/Boheme 2023-08-08 14:54:39 +02:00
uberwald 61a1a4a904 Annence/Boheme 2023-08-08 09:08:11 +02:00
uberwald 06ef98bed9 Manage compendiusm 2023-07-29 09:44:29 +02:00
169 changed files with 13831 additions and 3580 deletions
+71
View File
@@ -0,0 +1,71 @@
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
# 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/${{gitea.repository}}/releases/download/latest/system.json
download: https://www.uberwald.me/gitea/${{gitea.repository}}/releases/download/${{github.event.release.tag_name}}/fvtt-ecryme-${{github.event.release.tag_name}}.zip
# Build CSS from LESS
- uses: https://github.com/actions/setup-node@v4
with:
node-version: "20"
- run: npm install && npm run build
# Create a zip file with all files required by the system to add to the release
- run: |
apt update -y
apt install -y zip jq
- run: zip -r ./fvtt-ecryme-${{github.event.release.tag_name}}.zip system.json README.md LICENSE.md changelog.md css/ fonts/ images/ lang/ modules/ styles/ packs/ templates/ translated/ welcome-message-ecryme.html
- name: Upload release assets
env:
GITEA_TOKEN: ${{ secrets.ALLOW_PUSH_RELEASE }}
TAG_NAME: ${{ github.event.release.tag_name }}
REPO: ${{ gitea.repository }}
run: |
RELEASE_ID=$(curl -s -H "Authorization: token $GITEA_TOKEN" \
"https://www.uberwald.me/gitea/api/v1/repos/$REPO/releases/tags/$TAG_NAME" | jq -r '.id')
curl -s -H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/zip" \
--data-binary @./fvtt-ecryme-$TAG_NAME.zip \
"https://www.uberwald.me/gitea/api/v1/repos/$REPO/releases/$RELEASE_ID/assets?name=fvtt-ecryme-$TAG_NAME.zip"
curl -s -H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
--data-binary @./system.json \
"https://www.uberwald.me/gitea/api/v1/repos/$REPO/releases/$RELEASE_ID/assets?name=system.json"
- name: Publish to Foundry server
uses: https://github.com/djlechuck/foundryvtt-publish-package-action@v1
with:
token: ${{ secrets.FOUNDRYVTT_RELEASE_TOKEN }}
id: "fvtt-ecryme"
version: ${{steps.get_version.outputs.version-without-v}}
manifest: "https://www.uberwald.me/gitea/${{gitea.repository}}/releases/download/latest/system.json"
notes: "https://www.uberwald.me/gitea/${{gitea.repository}}/releases/download/${{github.event.release.tag_name}}/fvtt-ecryme-${{github.event.release.tag_name}}.zip"
compatibility-minimum: "13"
compatibility-verified: "14"
+2
View File
@@ -1 +1,3 @@
.history/
node_modules/
css/ecryme.css
+54
View File
@@ -0,0 +1,54 @@
# Ecryme (FoundryVTT) — agent notes
**This is a FoundryVTT Game System** (not a module), id `fvtt-ecryme`. Compatible with Foundry v13v14. Requires `babele` and `lib-wrapper`.
## Quick commands
| Command | What |
|---------|------|
| `npm run build` | Compile `styles/ecryme.less``css/ecryme.css` via Gulp |
| `npm run watch` | Watch `styles/**/*.less` and rebuild |
No other npm scripts exist. **No tests, no linter, no typechecker, no TypeScript.** Pure JS ES modules, no bundling.
## Project structure
```
modules/ # JS source (loaded directly by Foundry, no build step)
ecryme-main.js # Entry point — registers documents, sheets, hooks, templates
actors/ # Actor document + sheet classes (pc, npc, annency)
items/ # Item document + sheet classes (equipment, weapon, trait, spec, maneuver)
models/ # Foundry DataModels (extends TypeDataModel) — schema definitions
dialogs/ # Roll/confrontation dialog classes
app/ # Combat, commands, hotbar, summary app
common/ # Config constants + utility helpers (798-line utility)
templates/ # Handlebars sheets (actors/, items/, chat/, dialogs/)
styles/ # LESS source files (ecryme.less imports all partials)
lang/ # fr.json, en.json localization
packs/ # LevelDB compendium packs (specialisation, equipment, traits, scenes, maneuvers, help)
translated/fr/ # Babele translation JSON files for compendium content
images/ # Assets, icons, UI
css/ # Compiled CSS output (committed despite being in .gitignore)
```
## Architecture
- **App v1 sheets** (`foundry.appv1.sheets.ActorSheet`/`ItemSheet`), not ApplicationV2
- **DataModels** replace the deprecated `template.json` (kept as `template.json.SAVED`)
- Actor types: `pc`, `npc`, `annency`
- Item types: `equipment`, `weapon`, `trait`, `specialization`, `maneuver`
- ES module imports use relative paths with `.js` extension
- Custom combat class extends `Combat` with its own initiative formula
- Socket enabled (`"socket": true` in `system.json`)
## Conventions (verified from codebase)
- **JS files**: kebab-case (`ecryme-actor.js`)
- **Classes**: PascalCase (`EcrymeActor`)
- **Templates**: kebab-case `.hbs` (`actor-sheet.hbs`)
- **Barrel files**: `_module.js`
- **Hooks**: init → register classes/sheets/templates; ready → welcome msg/scene import; babele.init → register translation dir
## Release
Gitea CI (`.gitea/workflows/release.yaml`) triggers on published release. Zips the source tree directly — JS is used as-is, no build step for JS. CSS must be pre-compiled before release.
+393
View File
@@ -0,0 +1,393 @@
Creative Commons Legal Code
CC BY-NC-SA 4.0
Attribution-NonCommercial-ShareAlike 4.0 International
=======================================================================
Creative Commons Corporation ("Creative Commons") is not a law firm and
does not provide legal services or legal advice. Distribution of
Creative Commons public licenses does not create a lawyer-client or
other relationship. Creative Commons makes its licenses and related
information available on an "as-is" basis. Creative Commons gives no
warranties regarding its licenses, any material licensed under their
terms and conditions, or any related information. Creative Commons
disclaims all liability for damages resulting from their use to the
fullest extent possible.
By exercising the Licensed Rights (defined below), You accept and agree
to be bound by the terms and conditions of this Creative Commons
Attribution-NonCommercial-ShareAlike 4.0 International Public License
("Public License"). To the extent this Public License may be interpreted as
a contract, You are granted the Licensed Rights in consideration of Your
acceptance of these terms and conditions, and the Licensor grants You
such rights in consideration of benefits the Licensor receives from
making the Licensed Material available under these terms and
conditions.
Section 1 -- Definitions.
a. Adapted Material means material subject to Copyright and Similar
Rights that is derived from or based upon the Licensed Material
and in which the Licensed Material is translated, altered,
arranged, transformed, or otherwise modified in a manner requiring
permission under the Copyright and Similar Rights held by the
Licensor. For purposes of this Public License, where the Licensed
Material is a musical work, performance, or sound recording,
Adapted Material is always produced where the Licensed Material is
synched in timed relation with a moving image.
b. Adapter's License means the license You apply to Your Copyright and
Similar Rights in Your contributions to Adapted Material in
accordance with the terms and conditions of this Public License.
c. BY-NC-SA Compatible License means a license listed at
creativecommons.org/compatiblelicenses, approved by Creative
Commons as essentially the equivalent of this Public License.
d. Copyright and Similar Rights means copyright and/or similar rights
closely related to copyright including, without limitation, performance,
broadcast, sound recording, and Sui Generis Database Rights, without
regard to how the rights are labeled or categorized. For purposes of
this Public License, the rights specified in Section 2(b)(1)-(2) are
not Copyright and Similar Rights.
e. Effective Technological Measures means those measures that, in the
absence of proper authority, may not be circumvented under laws
fulfilling obligations under Article 11 of the WIPO Copyright Treaty
adopted on December 20, 1996, and/or similar international agreements.
f. Exceptions and Limitations means fair use, fair dealing, and/or any
other exception or limitation to Copyright and Similar Rights that
apply to Your use of the Licensed Material.
g. Licensed Material means the artistic or literary work, database, or
other material to which the Licensor applied this Public License.
h. Licensed Rights means the rights granted to You subject to the
terms and conditions of this Public License, which have as their
object the Licensed Material and which include all Copyright and
Similar Rights that apply to Your use of the Licensed Material and
that the Licensor has authority to license.
i. Licensor means the individual(s) or entity(ies) granting rights
under this Public License.
j. NonCommercial means not primarily intended for or directed towards
commercial advantage or monetary compensation. For purposes of
this Public License, the exchange of the Licensed Material for a
fee or other remuneration is considered NonCommercial if the fee or
remuneration is solely intended to compensate the Licensor for the
Licensor's time and effort in producing the Licensed Material or to
provide a financial contribution to the Licensor's non-commercial
operations, and the exchange is separate and independent from the
Licensed Material such that the Licensor's commercial operations are
not enhanced by the exchange.
k. Original Work means the work to which the Licensor applied this
Public License.
l. Share means to provide material to the public by any means or
process that requires permission under the Licensed Rights, such as
reproduction, public display, public performance, distribution,
dissemination, communication, or importation, and to make material
available to the public including in ways that members of the public
may access the material from a place and at a time individually chosen
by them.
m. Shared with You means the Licensed Material has been provided to
You by the Licensor, or by a recipient of the Licensed Material from
the Licensor, under this Public License.
n. Sui Generis Database Rights means rights other than copyright
resulting from Directive 96/9/EC of the European Parliament and of the
Council of 11 March 1996 on the legal protection of databases, as
amended and/or succeeded, as well as other essentially equivalent rights
anywhere in the world.
o. This Public License does not apply to Sui Generis Database Rights to
the extent they do not fall under Section 2(a)(5).
p. You means the individual or entity exercising the Licensed Rights
under this Public License. Your has a corresponding meaning in the
context of Your acting as the Licensor.
Section 2 -- Scope.
a. License grant.
1. Subject to the terms and conditions of this Public License, the
Licensor hereby grants You a worldwide, royalty-free, non-sublicensable,
non-exclusive, irrevocable license to exercise the Licensed Rights in
the Licensed Material to:
a. reproduce and Share the Licensed Material, in whole or in part;
b. create and reproduce Adapted Material and Share Adapted Material.
2. Exceptions and Limitations. For the avoidance of doubt, where
Exceptions and Limitations apply to Your use, this Public License
does not apply, and You do not need to comply with its terms and
conditions.
3. Term. The term of this Public License is specified in Section 6(a).
4. Media and formats; technical modifications allowed. The Licensor
authorizes You to exercise the Licensed Rights in all media and
formats whether now known or hereafter created, and to make technical
modifications necessary to do so. The Licensor waives and/or agrees
not to assert any right or authority to forbid You from making
technical modifications necessary to exercise the Licensed Rights,
including technical modifications necessary to circumvent Effective
Technological Measures. For purposes of this Public License, simply as a
consequence of modifying the Licensed Material, You do not become a
Licensor of the Licensed Material, and the Adapted Material You create
is not subject to the terms and conditions of this Public License.
5. Downstream recipients.
a. Offer from the Licensor -- Licensed Material: The Licensor
offers to the recipient of Licensed Material a license to the
Licensed Material on the same terms and conditions as granted
to You under this Public License.
b. Additional offer from the Licensor -- Adapted Material: The
Licensor offers to the recipient of Adapted Material a license
to the Adapted Material on the terms and conditions of the
Adapter's License You apply to Your Adapted Material.
c. No downstream restrictions. You may not offer or impose any
additional or different terms or conditions on, or apply any
Effective Technological Measures to, the Licensed Material if doing
so restricts exercise of the Licensed Rights by any recipient of
the Licensed Material.
b. No endorsement. Nothing in this Public License constitutes or may be
construed as permission to assert or imply that You are, or that Your
use of the Licensed Material is, connected with, or sponsored by, the
Licensor, or any other person.
c. Fair use; other user rights. Nothing in this Public License is intended
to reduce, limit, or restrict any uses free from copyright or rights
arising from limitations or exceptions that are provided for in
connection with the copyright and other similar rights applicable to
the particular use or which cannot be waived.
d. Patent and trademark notice. This Public License does not grant
permission to use the trade name, trademark, service mark, or similar
branding of the Licensor, except as required for reasonable and customary
notice of attribution when You engage in the reproduction or distribution
of the Licensed Material. This Public License does not grant permission to
use the patent rights of the Licensor.
Section 3 -- License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the
following conditions:
a. Attribution.
1. If You Share the Licensed Material (including in modified form), You
must:
a. retain the following if it is supplied by the Licensor with the
Licensed Material:
i. identification of the creator(s) of the Licensed Material and
any others designated to receive attribution, in any reasonable
manner requested by the Licensor (including by pseudonym if
designated);
ii. a copyright notice;
iii. a notice that refers to this Public License;
iv. a notice that refers to the disclaimer of warranties;
v. a URI or hyperlink to the Licensed Material to the extent
reasonably practicable;
b. indicate if You modified the Licensed Material and retain an
indication of any previous modifications; and
c. indicate the Licensed Material is licensed under this Public
License, and include the text of, or the URI or hyperlink to, this
Public License.
2. You may satisfy the conditions in Section 3(a)(1) in any reasonable
manner based on the medium, means, and context in which You Share the
Licensed Material. For example, it may be reasonable to satisfy the
conditions by providing a URI or hyperlink to a resource that includes
the required information.
3. If requested by the Licensor, You must remove any of the information
required by Section 3(a)(1)(A) to the extent reasonably practicable.
b. NonCommercial.
You may not exercise the Licensed Rights for Commercial Purposes.
c. ShareAlike.
In addition to the conditions in Section 3(a) and (b), if You Share
Adapted Material You produce, the following conditions also apply:
1. The Adapter's License You apply must be a BY-NC-SA Compatible License
with terms that are equivalent to this Public License.
2. You must include the text of, or the URI or hyperlink to, the
Adapter's License You apply. You may satisfy this condition in any
reasonable manner based on the medium, means, and context in which You
Share Adapted Material.
3. You may not offer or impose any additional or different terms or
conditions on, or apply any Effective Technological Measures to,
Adapted Material that restrict exercise of the rights granted under
the Adapter's License You apply.
Section 4 -- Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that apply to
Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right to
extract, reuse, reproduce, and Share all or a substantial portion of the
contents of the database for NonCommercial purposes only;
b. if You include all or a substantial portion of the database contents in
Adapted Material, You must comply with the conditions in Section
3(a) and (c) of this Public License.
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE EXTENT
POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS AND AS-AVAILABLE,
AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND, EXPRESS, IMPLIED,
STATUTORY OR OTHERWISE, AND DISCLAIMS ALL RESPONSIBILITY FOR ANY HARM OR
DAMAGE ARISING OUT OF OR IN CONNECTION WITH THE LICENSED MATERIAL OR
THIS PUBLIC LICENSE, INCLUDING WITHOUT LIMITATION DAMAGES FOR LOSS OF
INCOME, PROFITS, BUSINESS INTERRUPTION, OR OTHER PECUNIARY LOSS, OR FOR
ANY INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE, OR EXEMPLARY
DAMAGES, WHETHER ARISING UNDER CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY FROM THE USE OF THE
LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
b. WHERE THE LICENSED RIGHTS INCLUDE SUI GENERIS DATABASE RIGHTS THAT
APPLY TO YOUR USE OF THE LICENSED MATERIAL, THE LICENSOR MAKES NO
REPRESENTATIONS OR WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
AS TO THE DATA INCLUDED IN THE LICENSED MATERIAL; THE LICENSOR MAKES NO
REPRESENTATIONS OR WARRANTIES AS TO THE ACCURACY OR COMPLETENESS OF THE
DATA; THE LICENSOR DISCLAIMS ALL LIABILITY FOR ANY HARM OR DAMAGE ARISING
OUT OF INACCURACIES OR OMITTIONS IN THE DATA; AND THE LICENSOR MAKES NO
COMMITMENT TO CONTINUE TO SUPPLY, MAINTAIN, OR CORRECT THE DATA.
c. OTHER THAN AS EXPRESSLY AGREED TO BY THE PARTIES IN WRITING, THE LICENSOR
PROVIDES THE LICENSED MATERIAL ON AN "AS-IS" AND "AS-AVAILABLE" BASIS AND
WITHOUT CONDITIONS OR WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
INCLUDING, WITHOUT LIMITATION, ANY WARRANTY OF TITLE, MERCHANTABILITY,
OR FITNESS FOR A PARTICULAR PURPOSE. YOU BEAR THE SOLE RISK OF DETERMINING
WHETHER THE LICENSED MATERIAL IS SUITABLE FOR YOUR PURPOSES AND OF THE
CONSEQUENCES OF USING THE LICENSED MATERIAL. IN ALL CIRCUMSTANCES, UNLESS
OTHERWISE AGREED IN WRITING, THE LICENSOR SHALL NOT BE LIABLE TO YOU OR
TO ANY OTHER PARTY FOR ANY DAMAGES, INCLUDING WITHOUT LIMITATION SPECIAL OR
CONSEQUENTIAL DAMAGES. IN NO EVENT SHALL THE LICENSOR BE LIABLE FOR ANY
DIRECT, INDIRECT, PUNITIVE, OR INCIDENTAL DAMAGES ARISING OUT OF THIS
PUBLIC LICENSE OR THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
d. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL THE
LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT
LIMITATION, NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, OR EXEMPLARY DAMAGES, INCLUDING WITHOUT
LIMITATION DAMAGES FOR LOSS OF PROFITS, DATA, OR GOODWILL, EVEN IF THE
LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
Section 6 -- Term and Termination.
a. This Public License applies for the duration of any applicable
copyright and Similar Rights licensed here under. However, if You
violate this Public License, the Licensor's grant of rights to You
under this Public License terminates automatically.
b. Where Your right to use the Licensed Material has terminated under
Section 6(a), it reinstates:
1. automatically as of the date the violation is cured, provided it is
cured within 30 days of Your discovery of the violation; or
2. upon express reinstatement by the Licensor.
For the avoidance of doubt, this Section 6(b) does not affect any right
the Licensor may have to seek remedies for Your violation of this Public
License.
c. For the avoidance of doubt, the Licensor may also terminate this Public
License for all uses of the Licensed Material by You if You fail to
comply with the Licensor's requirement to remove a reference to the
Licensor or attribution information, as requested under Section
3(a)(1)(A)(v), from any cue or companion material to the Licensed
Material.
d. Who may terminate this Public License? Only the Licensor or an
individual or entity authorized to act on behalf of the Licensor.
Section 7 -- Other Rights.
a. Moral rights, such as the right of integrity, are not licensed under
this Public License, nor are publicity, privacy, and/or other similar
personality rights; however, to the extent possible, the Licensor waives
and/or agrees not to assert any such rights held by the Licensor to the
extent necessary to allow You to exercise the Licensed Rights, but not
otherwise.
b. Patent rights. This Public License does not grant permissions to use,
sell, or license patent claims or patented inventions in any work
based on the Licensed Material, and the Licensor reserves all patent and
similar rights in the Licensed Material.
c. Trademark rights. This Public License does not grant permissions to
use the trade name, trademark, service mark, or other similar branding
of the Licensor, except as required for reasonable and customary use in
describing the origin of the Licensed Material and describing the use
of the Licensed Material, including Modified Versions of You.
d. No additional restrictions. You may not impose any technological
restrictions or attempt to impose technological restrictions through legal
or other means that restrict other Users from exercising the Licensed
Rights.
Section 8 -- Interpretation.
a. For the avoidance of doubt, this Public License does not, and shall be
interpreted not to, create or imply any grant of patent or trademark
rights to You.
b. This Public License does not address the permissions and restrictions
that apply to Private Rights or Other Rights, as such terms are defined
under Section 2(a)(4).
c. No waiver. Any failure by the Licensor to comply with a term or
condition of this Public License or to seek remedies for violation of
this Public License does not constitute a waiver of the Licensor's
right to enforce that term, condition, or remedy.
d. Severability. If any provision of this Public License is invalid or
unenforceable under applicable law, it shall not affect the validity or
enforceability of the remainder of the terms and conditions of this
Public License without that provision.
e. No surrender of rights. This Public License does not require You or any
User to surrender any rights, including moral rights or copyright,
they may have in the Licensed Material or Adapted Material.
f. All rights not explicitly granted. Any use of the Licensed Material
not expressly permitted by this Public License is reserved and
prohibited.
+55 -3
View File
@@ -1,6 +1,58 @@
# Système Foundry pour Ecryme v2 (French RPG, Open Sesam Games, Official)
# Ecryme v2 system for FoundryVTT (French RPG, Open Sesam Games, Official)
# Developmement
Uberwald
This is a base game system with functionnal character sheets for the game Ecryme, powered by the Engrenage system.
You can join the kickstarter and obtain the base books here : https://www.kickstarter.com/projects/osg-us/ecryme
# System overview
The game system in Foundry offers the following features :
- PC/NPC sheet
- Skill rolls
- Cephaly rolls (with Anency support)
- Confrontation management, with detailed result in the chat card
- Weapon rolls
- Trait management, with Spleen and Ideal also.
- Compendiums of items for the game
![System Snapshot](https://www.lahiette.com/leratierbretonnien/wp-content/uploads/2023/08/ecryme_snapshot_01.webp "System Snapshot")
# Contributions
- Original code realised by Uberwald (https://www.uberwald.me/)
# English translation
English translation by Conal Longden and Ian McClung
# License
This work is licensed under **Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)**.
You are free to:
- **Share** — copy and redistribute the material in any medium or format
- **Adapt** — remix, transform, and build upon the material
Under the following terms:
- **Attribution** — You must give appropriate credit, provide a link to the license, and indicate if changes were made
- **NonCommercial** — You may not use the material for commercial purposes
- **ShareAlike** — If you remix, transform, or build upon the material, you must distribute your contributions under the same license
For the full legal text, see [LICENSE.md](LICENSE.md).
## Copyright mentions
Copyright 2023 Open Sesame Games
All rights reserved
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Ecryme is a game written by Alexandre Clavel and Samuel Metzener, in a universe created by Mathieu gaborit. All of the aforementionned authors retain there moral rights regarding this work in both print and digital formats.
# Requests or Problems
Please report any requests or problems you have at contact@open-sesame.games
+57
View File
@@ -0,0 +1,57 @@
## [Version à venir] - Migration DataModels
### 🔄 Changements majeurs
- **Migration complète vers DataModels** : Le système n'utilise plus `template.json` pour définir les structures de données
- Tous les types d'acteurs (PC, NPC, Annency) utilisent maintenant des DataModels
- Tous les types d'items (Equipment, Weapon, Trait, Specialization, Maneuver, Scar, Annency, Boheme, Contact, Confrontation) utilisent maintenant des DataModels
### ✨ Améliorations
- Validation automatique des types de données
- Valeurs par défaut cohérentes pour tous les champs
- Meilleure performance grâce aux optimisations internes de Foundry VTT
- Code mieux organisé dans `modules/models/`
### 🔧 Technique
- Ajout du dossier `modules/models/` avec 14 fichiers DataModel
- `template.json` est maintenant marqué comme deprecated mais conservé pour référence
- Compatibilité ascendante : les données existantes sont automatiquement migrées
### 📚 Documentation
- Ajout d'un README dans `modules/models/` expliquant la structure et l'utilisation
- Guide de développement pour ajouter de nouveaux types
---
v12.0.0
- Support Foundry v11/v12
- Correction sur le niveau de jeu par défaut
v11.0.39
- Modification sur la prise en compte des traits en bonus/malus
- Correction sur les images de l'aide intégrée FR
v11.0.38
- Corrections sur les champs background/notes/equipement libre
- Taduction des répertoires
- Ajout de 2 landing pages
v11.0.36
- Enable deletion specialization
- Custome bonus for specializations
- Specialization direct rolls
v11.0.31
Add profession, fix equipment tab and add missing translation
v11.0.30
Snapshot and more detailed README
v11.0.28
Initial release
+26 -20
View File
@@ -1,25 +1,31 @@
var gulp = require('gulp');
const gulp = require('gulp');
const less = require('gulp-less');
var postcss = require('gulp-postcss');
var autoprefixer = require('autoprefixer');
var cssnext = require('cssnext');
var precss = require('precss');
gulp.task('css', function () {
var processors = [
autoprefixer,
cssnext,
precss
];
return gulp.src('./postcss/*.css')
.pipe(postcss(processors))
.pipe(gulp.dest('./styles'));
});
/* ----------------------------------------- */
/* Compile LESS
/* ----------------------------------------- */
function compileLESS() {
return gulp.src("styles/ecryme.less")
.pipe(less()).on('error', console.log.bind(console))
.pipe(gulp.dest("./css"));
}
const css = gulp.series(compileLESS);
/* ----------------------------------------- */
/* Watch Updates
/* ----------------------------------------- */
const LESS_FILES = ["styles/**/*.less"];
function watchUpdates() {
gulp.watch('./postcss/*.css', css);
gulp.watch(LESS_FILES, css);
}
/* ----------------------------------------- */
/* Export Tasks
/* ----------------------------------------- */
exports.default = gulp.series(
gulp.parallel(css),
watchUpdates
);
exports.css = css;
exports.watchUpdates = watchUpdates;
+4
View File
@@ -0,0 +1,4 @@
[Dolphin]
Timestamp=2024,3,15,11,30,15.103
Version=4
VisibleRoles=Details_text,Details_size,Details_modificationtime,Details_creationtime,CustomizedDetails
+4
View File
@@ -0,0 +1,4 @@
[Dolphin]
Timestamp=2024,3,15,11,30,26.235
Version=4
VisibleRoles=Details_text,Details_size,Details_modificationtime,Details_creationtime,CustomizedDetails
Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

+4
View File
@@ -0,0 +1,4 @@
[Dolphin]
Timestamp=2024,3,15,11,30,19.681
Version=4
VisibleRoles=Details_text,Details_size,Details_modificationtime,Details_creationtime,CustomizedDetails
+50 -5
View File
@@ -1,7 +1,9 @@
{
"TYPES": {
"Actor": {
"Personnage": "PC"
"pc": "Player Character",
"npc": "Non-Player Character",
"annency": "Annency"
},
"Item": {
"trait": "Trait",
@@ -16,7 +18,9 @@
"cogs": "Cogs",
"cephaly": "Cephaly",
"boheme": "Boheme",
"amertume": "Amertume"
"amertume": "Amertume",
"gamelevel": "Game level",
"gamelevelhelp": "Choose the game level (Cogs, Cephaly, Boheme, Amertume)"
},
"chat": {
"formula": "Formula",
@@ -30,24 +34,30 @@
"traitbonus": "Bonus trait",
"traitmalus": "Malus trait",
"bonusmalustraits": "Traits Bonus/Malus",
"spectranscend": "Self-Transcend : "
"spectranscend": "Self-Transcend : ",
"confrontselect": "Selected for confrontation",
"sentogm": "Confrontation has been sent to GM"
},
"rule": {
"cephaly-success-2": "Duration : 1 scene - Impact : Superficial - Bonus : 1 - Elegy : 1",
"cephaly-success-4": "Duration : 1 week - Impact : Light - Bonus : 2 - Elegy : 2",
"cephaly-success-6": "Duration : 1 month - Impact : Serious - Bonus : 3 - Elegy : 3",
"cephaly-success-8": "Duration : 1 year - Impact : Major - Bonus : 4 - Elegy : 4",
"cephaly-success-9": "Duration : Permanent - Impact : Major - Bonus : 4 - Elegy : 4",
"cephaly-success-10": "Duration : Permanent - Impact : Dead - Bonus : 5 - Elegy : 5",
"cephaly-failure-2": "Duration : 1 scene - Impact : Superficial - Malus : 1",
"cephaly-failure-4": "Duration : 1 week - Impact : Light - Malus : 2",
"cephaly-failure-6": "Duration : 1 month - Impact : Serious - Malus : 3",
"cephaly-failure-8": "Duration : 1 year - Impact : Major - Malus : 4",
"cephaly-failure-9": "Duration : Permanent - Impact : Major - Malus : 4",
"cephaly-failure-10": "Duration : Permanent - Impact : Death/Madness - Malus : 5"
},
"warn": {
"notenoughdice": "Execution and Preservation must have 2 dices allocated"
"notenoughdice": "Execution and Preservation must have 2 dices allocated",
"confrontalready": "Confrontation already in progress with this roll"
},
"ui": {
"equipmentfree": "Equipments (free input)",
"traitType": "Trait type",
"niveauTrait": "Trait level",
"weight": "Weight",
@@ -63,6 +73,10 @@
"notes": "Notes",
"bio": "Bio",
"bionotes": "Bio&Notes",
"resetTranscendence": "Reset transcendence",
"transcendenceCurrent": "Current value",
"transcendenceMax": "Max value",
"toggleEditMax": "Edit max level",
"skills": "Skills",
"traits": "Traits",
"equipment": "Equipment",
@@ -106,6 +120,7 @@
"applyspleen": "Apply spleen",
"skilltranscendence": "Self Transcendence",
"confrontation": "Confrontation",
"confrontresult": "Confrontation Result",
"rollnormal": "Normal (4d6)",
"rollspleen": "With Spleen (5d6, worst 4 are kept)",
"rollideal": "With Ideal (5d6, best 4 are kept)",
@@ -144,7 +159,37 @@
"mekany": "Mekany",
"psyche": "Psyche",
"scoria": "Scoria",
"cephalydifficulty": "Set Cephaly difficulty"
"cephalydifficulty": "Set Cephaly difficulty",
"maneuvers": "Maneuvers",
"annency": "Annency",
"iscollective": "Collective",
"ismultiple": "Multiple",
"description": "Description",
"location": "Location",
"characters": "Characters",
"enhancements": "Enhancements",
"oniricform": "Oniric shape (Boheme)",
"ideals": "Ideals",
"politic": "Political ideal",
"boheme": "Boheme",
"annencybonus": "Annency bonus",
"bornplace": "Born place",
"residence": "Residence",
"origin": "Origin",
"childhood": "Childhood",
"bonus": "Bonus",
"details": "Details",
"quantity": "Quantity",
"background": "Background",
"gmnotes": "GM Notes",
"age": "Age",
"profession": "Profession",
"level": "Level",
"create": "Create",
"delete": "Delete",
"edit": "Edit",
"spleen": "Spleen",
"ideal": "Ideal"
}
}
}
+55 -10
View File
@@ -1,7 +1,9 @@
{
"TYPES": {
"Actor":{
"Personnage": "PJ"
"pc": "Personnage Joueur",
"npc": "Personnage Non Joueur",
"annency": "Anence"
},
"Item": {
"trait": "Trait",
@@ -16,7 +18,9 @@
"cogs": "Engrenages",
"cephaly": "Céphalie",
"boheme": "Bohême",
"amertume": "Amertume"
"amertume": "Amertume",
"gamelevel": "Niveau de jeu",
"gamelevelhelp": "Choisissez le niveau de jeu (Engrenages, Céphalie, Bohême, Amertume)"
},
"chat": {
"formula": "Formule",
@@ -30,25 +34,31 @@
"traitbonus": "Trait bonus",
"traitmalus": "Trait malus",
"bonusmalustraits": "Bonus/Malus des Traits",
"spectranscend": "Dépassement de soi : "
"spectranscend": "Dépassement de soi : ",
"confrontselect": "Sélectionné pour la confrontation",
"sentogm": "La confrontation a été envoyée au MJ"
},
"rule": {
"cephaly-success-12": "Durée : 1 scène - Impact : Superficiel - Bonus : 1 - Elegie : 1",
"cephaly-success-34": "Durée : 1 semaine - Impact : Léger - Bonus : 2 - Elegie : 2",
"cephaly-success-56": "Durée : 1 mois - Impact : Grave - Bonus : 3 - Elegie : 3",
"cephaly-success-78": "Durée : 1 année - Impact : Majeur - Bonus : 4 - Elegie : 4",
"cephaly-success-910": "Durée : Permanent - Impact : Mort - Bonus : 5 - Elegie : 5",
"cephaly-success-2": "Durée : 1 scène - Impact : Superficiel - Bonus : 1 - Elegie : 1",
"cephaly-success-4": "Durée : 1 semaine - Impact : Léger - Bonus : 2 - Elegie : 2",
"cephaly-success-6": "Durée : 1 mois - Impact : Grave - Bonus : 3 - Elegie : 3",
"cephaly-success-8": "Durée : 1 année - Impact : Majeur - Bonus : 4 - Elegie : 4",
"cephaly-success-9": "Durée : Permanent - Impact : Majeur - Bonus : 4 - Elegie : 4",
"cephaly-success-10": "Durée : Permanent - Impact : Mort - Bonus : 5 - Elegie : 5",
"cephaly-failure-2": "Durée : 1 scène - Impact : Superficiel - Malus : 1 - Symptôme non visible et sans gravité - Altération bégigne difficilement repérable",
"cephaly-failure-4": "Durée : 1 semaine - Impact : Léger - Malus : 2 - Symptôme visible non incapacitant - Altération repérable",
"cephaly-failure-6": "Durée : 1 mois - Impact : Grave - Malus : 3 - Symptôme incapacitant - Altération repérable et fâcheuse",
"cephaly-failure-8": "Durée : 1 année - Impact : Majeur - Malus : 4 - Symptôme très incapacitant - Altération dangereuse",
"cephaly-failure-9": "Durée : Permanent - Impact : Majeur - Malus : 4 - Symptôme très incapacitant - Altération dangereuse",
"cephaly-failure-10": "Durée : Permanent - Impact : Mort/Folie - Malus : 5 - Symptôme spectaculaire et repoussant - Altération dangereuse globalement"
},
"warn": {
"notenoughdice": "L'Accomplissement et la Préservation doivent avoir 2 dés chacun"
"notenoughdice": "L'Accomplissement et la Préservation doivent avoir 2 dés chacun",
"confrontalready": "Confrontation déjà en cours avec ce jet"
},
"ui": {
"equipmentfree": "Equipements (saisie libre)",
"traitType": "Type de trait",
"niveauTrait": "Niveau du trait",
"effect": "Incidence",
@@ -64,6 +74,10 @@
"notes": "Notes",
"bio": "Bio",
"bionotes": "Bio&Notes",
"resetTranscendence": "Réinitialiser la transcendance",
"transcendenceCurrent": "Valeur actuelle",
"transcendenceMax": "Valeur max",
"toggleEditMax": "Modifier le niveau max",
"skills": "Compétences",
"traits": "Traits",
"equipment": "Equipement",
@@ -107,6 +121,7 @@
"applyspleen": "Utiliser le spleen",
"skilltranscendence": "Dépassement de soi",
"confrontation": "Confrontation",
"confrontresult": "Résultat de Confrontation",
"rollnormal": "Normal (4d6)",
"rollspleen": "Avec le Spleen (5d6, 4 plus bas conservés)",
"rollideal": "Avec l'Idéal (5d6, 4 plus haut conservés)",
@@ -145,7 +160,37 @@
"mekany": "Mekanë",
"psyche": "Psyché",
"scoria": "Scorie",
"cephalydifficulty": "Difficulté de la Céphalie"
"cephalydifficulty": "Difficulté de la Céphalie",
"maneuvers": "Manoeuvres",
"annency": "Anence",
"iscollective": "Collective",
"ismultiple": "Multiple",
"description": "Description",
"location": "Lieu",
"characters": "Personnages",
"enhancements": "Améliorations",
"oniricform": "Forme Onorique (Bohême)",
"ideals": "Idéaux",
"politic": "Idéaux politiques",
"boheme": "Bohême",
"annencybonus": "Bonus d'Anence",
"bornplace": "Lieu de naissance",
"residence": "Résidence",
"origin": "Origine",
"childhood": "Enfance",
"bonus": "Bonus",
"details": "Détails",
"quantity": "Quantité",
"background": "Background",
"gmnotes": "Notes GM",
"age": "Âge",
"profession": "Profession",
"level": "Niveau",
"create": "Créer",
"delete": "Supprimer",
"edit": "Éditer",
"spleen": "Spleen",
"ideal": "Idéal"
}
}
}
+51 -27
View File
@@ -6,20 +6,21 @@
import { EcrymeUtility } from "../common/ecryme-utility.js";
/* -------------------------------------------- */
export class EcrymeActorSheet extends ActorSheet {
export class EcrymeActorSheet extends foundry.appv1.sheets.ActorSheet {
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
return {
...super.defaultOptions,
classes: ["fvtt-ecryme", "sheet", "actor"],
template: "systems/fvtt-ecryme/templates/actors/actor-sheet.hbs",
width: 860,
height:680,
tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "skills" }],
dragDrop: [{ dragSelector: ".item-list .item", dropSelector: null }],
editScore: true
});
editScore: false
};
}
/* -------------------------------------------- */
@@ -33,7 +34,7 @@ export class EcrymeActorSheet extends ActorSheet {
name: this.actor.name,
editable: this.isEditable,
cssClass: this.isEditable ? "editable" : "locked",
system: duplicate(this.object.system),
system: foundry.utils.deepClone(this.object.system),
limited: this.object.limited,
skills: this.actor.prepareSkills(),
traits: this.actor.getRollTraits(),
@@ -41,17 +42,21 @@ export class EcrymeActorSheet extends ActorSheet {
ideal: this.actor.getIdeal(),
spleen: this.actor.getSpleen(),
impacts: this.object.getImpacts(),
config: duplicate(game.system.ecryme.config),
config: foundry.utils.deepClone(game.ecryme.config),
weapons: this.actor.getWeapons(),
maneuvers: this.actor.getManeuvers(),
impactsMalus: this.actor.getImpactsMalus(),
archetype: duplicate(this.actor.getArchetype()),
equipements: this.actor.getEquipments(),
archetype: foundry.utils.deepClone(this.actor.getArchetype()),
equipments: this.actor.getEquipments(),
hasCephaly: EcrymeUtility.hasCephaly(),
hasBoheme: EcrymeUtility.hasBoheme(),
hasAmertume: EcrymeUtility.hasAmertume(),
cephalySkills: this.actor.getCephalySkills(),
subActors: duplicate(this.actor.getSubActors()),
description: await TextEditor.enrichHTML(this.object.system.description, { async: true }),
notes: await TextEditor.enrichHTML(this.object.system.notes, { async: true }),
equipementlibre: await TextEditor.enrichHTML(this.object.system.equipementlibre, { async: true }),
subActors: foundry.utils.deepClone(this.actor.getSubActors()),
annency: this.actor.getAnnency(),
description: await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.object.system.biodata.description, { async: true }),
notes: await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.object.system.biodata.notes, { async: true }),
equipementlibre: await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.object.system.equipmentfree, { async: true }),
options: this.options,
owner: this.document.isOwner,
editScore: this.options.editScore,
@@ -59,7 +64,7 @@ export class EcrymeActorSheet extends ActorSheet {
}
this.formData = formData;
console.log("PC : ", formData, this.object);
//console.log("PC : ", formData, this.object);
return formData;
}
@@ -71,15 +76,24 @@ export class EcrymeActorSheet extends ActorSheet {
// Everything below here is only needed if the sheet is editable
if (!this.options.editable) return;
html.bind("keydown", function(e) { // Ignore Enter in actores sheet
if (e.keyCode === 13) return false;
});
});
html.find('.open-annency').click(ev => {
let actorId = $(ev.currentTarget).data("annency-id")
const actor = game.actors.get(actorId)
actor.sheet.render(true)
})
// Update Inventory Item
html.find('.item-edit').click(ev => {
const li = $(ev.currentTarget).parents(".item")
let itemId = li.data("item-id")
if (!itemId) {
itemId = $(ev.currentTarget).data("item-id")
}
const item = this.actor.items.get( itemId );
item.sheet.render(true);
});
@@ -92,14 +106,14 @@ export class EcrymeActorSheet extends ActorSheet {
let dataType = $(ev.currentTarget).data("type")
this.actor.createEmbeddedDocuments('Item', [{ name: "NewItem", type: dataType }], { renderSheet: true })
})
html.find('.subactor-edit').click(ev => {
const li = $(ev.currentTarget).parents(".item");
let actorId = li.data("actor-id");
let actor = game.actors.get( actorId );
actor.sheet.render(true);
});
html.find('.subactor-delete').click(ev => {
const li = $(ev.currentTarget).parents(".item");
let actorId = li.data("actor-id");
@@ -118,7 +132,13 @@ export class EcrymeActorSheet extends ActorSheet {
let categKey = $(event.currentTarget).data("category-key")
let skillKey = $(event.currentTarget).data("skill-key")
this.actor.rollSkill(categKey, skillKey)
});
});
html.find('.roll-spec').click((event) => {
let categKey = $(event.currentTarget).data("category-key")
let skillKey = $(event.currentTarget).data("skill-key")
let specId = $(event.currentTarget).data("spec-id")
this.actor.rollSpec(categKey, skillKey, specId)
});
html.find('.roll-skill-confront').click((event) => {
let categKey = $(event.currentTarget).data("category-key")
let skillKey = $(event.currentTarget).data("skill-key")
@@ -132,36 +152,40 @@ export class EcrymeActorSheet extends ActorSheet {
const li = $(event.currentTarget).parents(".item")
let weaponId = li.data("item-id");
this.actor.rollWeaponConfront(weaponId)
});
});
html.find('.impact-modify').click((event) => {
let impactType = $(event.currentTarget).data("impact-type")
let impactLevel = $(event.currentTarget).data("impact-level")
let modifier = Number($(event.currentTarget).data("impact-modifier"))
this.actor.modifyImpact(impactType, impactLevel, modifier)
});
html.find('.roll-weapon').click((event) => {
const armeId = $(event.currentTarget).data("arme-id")
this.actor.rollArme(armeId)
this.actor.rollWeapon(armeId)
});
html.find('.lock-unlock-sheet').click((event) => {
this.options.editScore = !this.options.editScore;
this.render(true);
});
});
html.find('.item-equip').click(ev => {
const li = $(ev.currentTarget).parents(".item");
this.actor.equipItem( li.data("item-id") );
this.render(true);
this.render(true);
});
html.find('.update-field').change(ev => {
const fieldName = $(ev.currentTarget).data("field-name");
let value = Number(ev.currentTarget.value);
this.actor.update( { [`${fieldName}`]: value } );
});
});
html.find('.reset-transcendence').click(async (ev) => {
await this.actor.resetSkillPool();
this.render(true);
});
}
/* -------------------------------------------- */
/** @override */
setPosition(options = {}) {
+141 -41
View File
@@ -29,9 +29,9 @@ export class EcrymeActor extends Actor {
if (data instanceof Array) {
return super.create(data, options);
}
// If the created actor has items (only applicable to duplicated actors) bypass the new actor creation logic
// If the created actor has items bypass the new actor creation logic
if (data.items) {
let actor = super.create(data, options);
let actor = await super.create(data, options);
return actor;
}
@@ -49,9 +49,21 @@ export class EcrymeActor extends Actor {
}
/* -------------------------------------------- */
_preUpdate(changed, options, user) {
async _preUpdate(changed, options, user) {
super._preUpdate(changed, options, user);
await super._preUpdate(changed, options, user);
}
/* -------------------------------------------- */
async _preCreate(data, options, user) {
await super._preCreate(data, options, user);
// Configure prototype token settings
const prototypeToken = {};
if (this.type === "pc") Object.assign(prototypeToken, {
sight: { enabled: true }, actorLink: true, disposition: CONST.TOKEN_DISPOSITIONS.FRIENDLY
});
this.updateSource({ prototypeToken });
}
/* -------------------------------------------- */
@@ -61,13 +73,39 @@ export class EcrymeActor extends Actor {
return comp;
}
getArchetype() {
let comp = duplicate(this.items.find(item => item.type == 'archetype') || { name: "Pas d'archetype" })
let comp = foundry.utils.deepClone(this.items.find(item => item.type == 'archetype') || { name: "Pas d'archetype" })
if (comp?.system) {
comp.tarot = EcrymeUtility.getTarot(comp.system.lametutelaire)
}
return comp;
}
/* -------------------------------------------- */
buildAnnencyActorList() {
let membersFull = {}
for(let id of this.system.base.characters) {
let actor = game.actors.get(id)
membersFull[id] = { name: actor.name, id: actor.id, img: actor.img }
}
return membersFull
}
/* ----------------------- --------------------- */
addAnnencyActor(actorId) {
let members = foundry.utils.deepClone(this.system.base.characters)
members.push(actorId)
this.update({ 'system.base.characters': members })
}
async removeAnnencyActor(actorId) {
let members = this.system.base.characters.filter(id => id != actorId)
this.update({ 'system.base.characters': members })
}
/* -------------------------------------------- */
getAnnency() {
let annency = game.actors.find(a => a.type == 'annency' && a.system.base.characters.includes(this.id))
return annency || {}
}
/* -------------------------------------------- */
getConfrontations() {
return this.items.filter(it => it.type == "confrontation")
@@ -85,7 +123,7 @@ export class EcrymeActor extends Actor {
/* -------------------------------------------- */
getTrait(id) {
//console.log("TRAITS", this.items, this.items.filter(it => it.type == "trait") )
return this.items.find(it => it.type == "trait" && it._id == id)
return this.items.find(it => it.type == "trait" && it.id === id)
}
/* -------------------------------------------- */
getSpecialization(id) {
@@ -98,7 +136,7 @@ export class EcrymeActor extends Actor {
}
/* -------------------------------------------- */
prepareSkills() {
let skills = duplicate(this.system.skills)
let skills = foundry.utils.deepClone(this.system.skills)
for (let categKey in skills) {
let category = skills[categKey]
for (let skillKey in category.skilllist) {
@@ -110,17 +148,22 @@ export class EcrymeActor extends Actor {
}
/* -------------------------------------------- */
getCephalySkills() {
let skills = duplicate(this.system.cephaly.skilllist)
let skills = foundry.utils.deepClone(this.system.cephaly.skilllist)
return skills
}
/* -------------------------------------------- */
getImpacts() {
let comp = duplicate(this.items.filter(item => item.type == 'impact') || [])
let comp = foundry.utils.deepClone(this.items.filter(item => item.type == 'impact') || [])
return comp;
}
/* -------------------------------------------- */
getWeapons() {
let comp = duplicate(this.items.filter(item => item.type == 'weapon') || [])
let comp = foundry.utils.deepClone(this.items.filter(item => item.type == 'weapon') || [])
EcrymeUtility.sortArrayObjectsByName(comp)
return comp;
}
getManeuvers() {
let comp = foundry.utils.deepClone(this.items.filter(item => item.type == 'maneuver') || [])
EcrymeUtility.sortArrayObjectsByName(comp)
return comp;
}
@@ -128,7 +171,7 @@ export class EcrymeActor extends Actor {
getItemById(id) {
let item = this.items.find(item => item.id == id);
if (item) {
item = duplicate(item)
item = foundry.utils.deepClone(item)
}
return item;
}
@@ -158,18 +201,18 @@ export class EcrymeActor extends Actor {
/* ------------------------------------------- */
getEquipments() {
return this.items.filter(item => item.type == 'equipement')
return this.items.filter(item => item.type == 'equipment')
}
/* ------------------------------------------- */
async buildContainerTree() {
let equipments = duplicate(this.items.filter(item => item.type == "equipment") || [])
let equipments = foundry.utils.deepClone(this.items.filter(item => item.type == "equipment") || [])
for (let equip1 of equipments) {
if (equip1.system.iscontainer) {
equip1.system.contents = []
equip1.system.contentsEnc = 0
for (let equip2 of equipments) {
if (equip1._id != equip2.id && equip2.system.containerid == equip1.id) {
if (equip1.id !== equip2.id && equip2.system.containerid == equip1.id) {
equip1.system.contents.push(equip2)
let q = equip2.system.quantity ?? 1
equip1.system.contentsEnc += q * equip2.system.weight
@@ -245,7 +288,7 @@ export class EcrymeActor extends Actor {
/* -------------------------------------------- */
clearInitiative() {
this.getFlag("world", "initiative", -1)
this.setFlag("world", "initiative", -1)
}
/* -------------------------------------------- */
getInitiativeScore(combatId, combatantId) {
@@ -258,13 +301,13 @@ export class EcrymeActor extends Actor {
getSubActors() {
let subActors = [];
for (let id of this.system.subactors) {
subActors.push(duplicate(game.actors.get(id)))
subActors.push(foundry.utils.deepClone(game.actors.get(id)))
}
return subActors;
}
/* -------------------------------------------- */
async addSubActor(subActorId) {
let subActors = duplicate(this.system.subactors);
let subActors = foundry.utils.deepClone(this.system.subactors);
subActors.push(subActorId);
await this.update({ 'system.subactors': subActors });
}
@@ -306,7 +349,7 @@ export class EcrymeActor extends Actor {
/* -------------------------------------------- */
modifyConfrontBonus( modifier ) {
let newBonus = this.system.internals.confrontbonus + bonus
let newBonus = this.system.internals.confrontbonus + modifier
this.update({'system.internals.confrontbonus': newBonus})
}
@@ -335,9 +378,10 @@ export class EcrymeActor extends Actor {
rollData.actorId = this.id
rollData.img = this.img
rollData.isReroll = false
rollData.traits = duplicate(this.getRollTraits())
rollData.spleen = duplicate(this.getSpleen() || {})
rollData.ideal = duplicate(this.getIdeal() || {})
rollData.config = game.ecryme.config
rollData.traits = this.getRollTraits().map(t => ({ id: t.id, name: t.name, img: t.img, system: { level: t.system.level, traitype: t.system.traitype } }))
rollData.spleen = this.getSpleen() ? foundry.utils.deepClone(this.getSpleen()) : null
rollData.ideal = this.getIdeal() ? foundry.utils.deepClone(this.getIdeal()) : null
rollData.confrontBonus = this.getBonusList()
return rollData
@@ -345,14 +389,30 @@ export class EcrymeActor extends Actor {
/* -------------------------------------------- */
getCommonSkill(categKey, skillKey) {
let skill = this.system.skills[categKey].skilllist[skillKey]
let rollData = this.getCommonRollData()
// Specific NPC case
let skill
if (skillKey == "rawnpc") {
skill = {
name: "ECRY.ui." + categKey,
max: 10,
value: this.system.skills[categKey].pnjvalue,
spec: []
}
} else {
skill = this.system.skills[categKey].skilllist[skillKey]
skill = foundry.utils.deepClone(skill)
skill.spec = this.getSpecializations(skillKey)
}
skill = duplicate(skill)
rollData.skillLevelOptions = [];
for (let i=0; i<=skill.value; i++) {
rollData.skillLevelOptions[i] = `${i}`
}
skill.categKey = categKey
skill.skillKey = skillKey
skill.spec = this.getSpecializations(skillKey)
rollData.skill = skill
rollData.img = skill.img
rollData.impactMalus = this.getImpactMalus(categKey)
@@ -368,16 +428,28 @@ export class EcrymeActor extends Actor {
this.startRoll(rollData).catch("Error on startRoll")
}
/* -------------------------------------------- */
rollSpec(categKey, skillKey, specId) {
let rollData = this.getCommonSkill(categKey, skillKey)
let spec = this.items.find(it => it.type == "specialization" && it.id == specId)
rollData.mode = "skill"
rollData.selectedSpecs = [spec.id]
rollData.forcedSpec = foundry.utils.deepClone(spec)
rollData.title = game.i18n.localize(rollData.skill.name)
this.startRoll(rollData).catch("Error on startRoll")
}
/* -------------------------------------------- */
async rollSkillConfront(categKey, skillKey) {
let rollData = this.getCommonSkill(categKey, skillKey)
rollData.mode = "skill"
rollData.title = game.i18n.localize("ECRY.ui.confrontation") + " : " + game.i18n.localize(rollData.skill.name)
rollData.executionTotal = rollData.skill.value
rollData.preservationTotal = rollData.skill.value
rollData.executionTotal = rollData.skill.max
rollData.preservationTotal = rollData.skill.max
rollData.applyTranscendence = "execution"
rollData.traitsBonus = duplicate(rollData.traits)
rollData.traitsMalus = duplicate(rollData.traits)
rollData.traitsBonus = foundry.utils.deepClone(rollData.traits)
rollData.traitsMalus = foundry.utils.deepClone(rollData.traits)
console.log("ROLLDATA", rollData)
let confrontStartDialog = await EcrymeConfrontStartDialog.create(this, rollData)
confrontStartDialog.render(true)
}
@@ -385,16 +457,16 @@ export class EcrymeActor extends Actor {
async rollCephalySkillConfront(skillKey) {
let rollData = this.getCommonRollData()
rollData.mode = "cephaly"
rollData.skill = duplicate(this.system.cephaly.skilllist[skillKey])
rollData.skill = foundry.utils.deepClone(this.system.cephaly.skilllist[skillKey])
rollData.annency = foundry.utils.deepClone(this.getAnnency())
rollData.img = rollData.skill.img
rollData.skill.categKey = "cephaly"
rollData.skill.skillKey = skillKey
//rollData.impactMalus = this.getImpactMalus(categKey)
rollData.title = game.i18n.localize("ECRY.ui.cephaly") + " : " + game.i18n.localize(rollData.skill.name)
rollData.executionTotal = rollData.skill.value
rollData.preservationTotal = rollData.skill.value
rollData.traitsBonus = duplicate(rollData.traits)
rollData.traitsMalus = duplicate(rollData.traits)
rollData.executionTotal = rollData.skill.max
rollData.preservationTotal = rollData.skill.max
rollData.traitsBonus = foundry.utils.deepClone(rollData.traits)
rollData.traitsMalus = foundry.utils.deepClone(rollData.traits)
rollData.applyTranscendence = "execution"
let confrontStartDialog = await EcrymeConfrontStartDialog.create(this, rollData)
confrontStartDialog.render(true)
@@ -410,25 +482,27 @@ export class EcrymeActor extends Actor {
rollData = this.getCommonSkill("physical", "shooting")
}
rollData.mode = "weapon"
rollData.weapon = duplicate(weapon)
rollData.weapon = foundry.utils.deepClone(weapon)
rollData.title = game.i18n.localize("ECRY.ui.confrontation") + " : " + game.i18n.localize(rollData.skill.name)
rollData.executionTotal = rollData.skill.value
rollData.preservationTotal = rollData.skill.value
rollData.executionTotal = rollData.skill.max
rollData.preservationTotal = rollData.skill.max
rollData.traitsBonus = foundry.utils.deepClone(rollData.traits)
rollData.traitsMalus = foundry.utils.deepClone(rollData.traits)
rollData.applyTranscendence = "execution"
let confrontStartDialog = await EcrymeConfrontStartDialog.create(this, rollData)
confrontStartDialog.render(true)
}
/* -------------------------------------------- */
rollWeapon(weaponId) {
let weapon = this.items.get(weaponId)
if (weapon) {
weapon = duplicate(weapon)
weapon = foundry.utils.deepClone(weapon)
let rollData = this.getCommonRollData()
if (weapon.system.armetype == "mainsnues" || weapon.system.armetype == "epee") {
rollData.attr = { label: "(Physique+Habilité)/2", value: Math.floor((this.getPhysiqueMalus() + this.system.attributs.physique.value + this.system.attributs.habilite.value) / 2) }
} else {
rollData.attr = duplicate(this.system.attributs.habilite)
rollData.attr = foundry.utils.deepClone(this.system.attributs.habilite)
}
rollData.mode = "weapon"
rollData.weapon = weapon
@@ -440,6 +514,32 @@ export class EcrymeActor extends Actor {
}
}
/* -------------------------------------------- */
resetSkillPool() {
const updateData = {}
const skills = this.system.skills
for (const categKey of Object.keys(skills)) {
const skilllist = skills[categKey]?.skilllist
if (!skilllist) continue
for (const skillKey of Object.keys(skilllist)) {
const skill = skilllist[skillKey]
if (skill.max !== undefined) {
updateData[`system.skills.${categKey}.skilllist.${skillKey}.value`] = skill.max
}
}
}
const cephaly = this.system.cephaly?.skilllist
if (cephaly) {
for (const skillKey of Object.keys(cephaly)) {
const skill = cephaly[skillKey]
if (skill.max !== undefined) {
updateData[`system.cephaly.skilllist.${skillKey}.value`] = skill.max
}
}
}
return this.update(updateData)
}
/* -------------------------------------------- */
async startRoll(rollData) {
let rollDialog = await EcrymeRollDialog.create(this, rollData)
+144
View File
@@ -0,0 +1,144 @@
/**
* Extend the basic ActorSheet with some very simple modifications
* @extends {ActorSheet}
*/
import { EcrymeUtility } from "../common/ecryme-utility.js";
/* -------------------------------------------- */
export class EcrymeAnnencySheet extends foundry.appv1.sheets.ActorSheet {
/** @override */
static get defaultOptions() {
return {
...super.defaultOptions,
classes: ["fvtt-ecryme", "sheet", "actor"],
template: "systems/fvtt-ecryme/templates/actors/annency-sheet.hbs",
width: 640,
height: 600,
tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "annency" }],
dragDrop: [{ dragSelector: ".item-list .item", dropSelector: null }],
editScore: true
};
}
/* -------------------------------------------- */
async getData() {
let formData = {
title: this.title,
id: this.actor.id,
type: this.actor.type,
img: this.actor.img,
name: this.actor.name,
editable: this.isEditable,
cssClass: this.isEditable ? "editable" : "locked",
system: foundry.utils.deepClone(this.object.system),
limited: this.object.limited,
config: foundry.utils.deepClone(game.ecryme.config),
hasCephaly: EcrymeUtility.hasCephaly(),
hasBoheme: EcrymeUtility.hasBoheme(),
hasAmertume: EcrymeUtility.hasAmertume(),
characters: this.actor.buildAnnencyActorList(),
options: this.options,
owner: this.document.isOwner,
editScore: this.options.editScore,
isGM: game.user.isGM
}
this.formData = formData;
console.log("Annency : ", formData, this.object);
return formData;
}
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
// Everything below here is only needed if the sheet is editable
if (!this.options.editable) return;
html.bind("keydown", function (e) { // Ignore Enter in actores sheet
if (e.keyCode === 13) return false;
});
html.find('.actor-edit').click(ev => {
const li = $(ev.currentTarget).parents(".item")
let actorId = li.data("actor-id")
const actor = game.actors.get(actorId)
actor.sheet.render(true)
})
html.find('.actor-delete').click(ev => {
const li = $(ev.currentTarget).parents(".item")
let actorId = li.data("actor-id")
this.actor.removeAnnencyActor(actorId)
})
// Update Inventory Item
html.find('.item-edit').click(ev => {
const li = $(ev.currentTarget).parents(".item")
let itemId = li.data("item-id")
const item = this.actor.items.get(itemId);
item.sheet.render(true);
});
// Delete Inventory Item
html.find('.item-delete').click(ev => {
const li = $(ev.currentTarget).parents(".item")
EcrymeUtility.confirmDelete(this, li).catch("Error : No deletion confirmed")
})
html.find('.item-add').click(ev => {
let dataType = $(ev.currentTarget).data("type")
this.actor.createEmbeddedDocuments('Item', [{ name: "NewItem", type: dataType }], { renderSheet: true })
})
html.find('.subactor-edit').click(ev => {
const li = $(ev.currentTarget).parents(".item");
let actorId = li.data("actor-id");
let actor = game.actors.get(actorId);
actor.sheet.render(true);
});
html.find('.subactor-delete').click(ev => {
const li = $(ev.currentTarget).parents(".item");
let actorId = li.data("actor-id");
this.actor.delSubActor(actorId);
});
html.find('.update-field').change(ev => {
const fieldName = $(ev.currentTarget).data("field-name");
let value = Number(ev.currentTarget.value);
this.actor.update({ [`${fieldName}`]: value });
});
}
/* -------------------------------------------- */
async _onDropActor(event, dragData) {
const actor = fromUuidSync(dragData.uuid)
if (actor) {
this.actor.addAnnencyActor(actor.id)
} else {
ui.notifications.warn("Actor not found")
}
super._onDropActor(event, dragData)
}
/* -------------------------------------------- */
/** @override */
setPosition(options = {}) {
const position = super.setPosition(options);
const sheetBody = this.element.find(".sheet-body");
const bodyHeight = position.height - 192;
sheetBody.css("height", bodyHeight);
return position;
}
/* -------------------------------------------- */
/** @override */
_updateObject(event, formData) {
// Update the Actor
return this.object.update(formData);
}
}
+2
View File
@@ -0,0 +1,2 @@
export { default as EcrymeActorSheet } from "./pc-npc-sheet.js"
export { default as EcrymeAnnencySheet } from "./annency-sheet.js"
+127
View File
@@ -0,0 +1,127 @@
import EcrymeBaseActorSheet from "./base-actor-sheet.js"
import { EcrymeUtility } from "../../common/ecryme-utility.js"
/**
* Actor sheet for the Annency type using Application V2.
*/
export default class EcrymeAnnencySheet extends EcrymeBaseActorSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["annency"],
position: { width: 640, height: 600 },
actions: {
actorEdit: EcrymeAnnencySheet.#onActorEdit,
actorDelete: EcrymeAnnencySheet.#onActorDelete,
itemEdit: EcrymeAnnencySheet.#onItemEdit,
itemDelete: EcrymeAnnencySheet.#onItemDelete,
itemCreate: EcrymeAnnencySheet.#onItemCreate,
},
}
/** @override */
static PARTS = {
header: { template: "systems/fvtt-ecryme/templates/actors/partials/actor-header.hbs" },
tabs: { template: "templates/generic/tab-navigation.hbs" },
annency: { template: "systems/fvtt-ecryme/templates/actors/annency-annency.hbs" },
boheme: { template: "systems/fvtt-ecryme/templates/actors/annency-boheme.hbs" },
}
/** @override */
tabGroups = { primary: "annency" }
/** Build tabs conditionally based on active modules */
_getTabs() {
const tabs = {}
if (EcrymeUtility.hasCephaly()) {
tabs.annency = { id: "annency", group: "primary", label: "ECRY.ui.annency" }
}
if (EcrymeUtility.hasBoheme()) {
tabs.boheme = { id: "boheme", group: "primary", label: "ECRY.ui.boheme" }
}
// Ensure initial tab is valid
if (!tabs[this.tabGroups.primary]) {
this.tabGroups.primary = Object.keys(tabs)[0] ?? "annency"
}
for (const tab of Object.values(tabs)) {
tab.active = this.tabGroups[tab.group] === tab.id
tab.cssClass = tab.active ? "active" : ""
}
return tabs
}
/** @override */
async _prepareContext() {
const actor = this.document
return {
actor,
system: actor.system,
source: actor.toObject(),
fields: actor.schema.fields,
systemFields: actor.system.schema.fields,
type: actor.type,
img: actor.img,
name: actor.name,
isEditable: this.isEditable,
config: game.ecryme.config,
hasCephaly: EcrymeUtility.hasCephaly(),
hasBoheme: EcrymeUtility.hasBoheme(),
characters: actor.buildAnnencyActorList(),
owner: this.document.isOwner,
isGM: game.user.isGM,
tabs: this._getTabs(),
}
}
/** @override */
async _preparePartContext(partId, context) {
context = await super._preparePartContext(partId, context)
if (partId === "annency" || partId === "boheme") {
context.tab = context.tabs[partId]
}
return context
}
/** @override */
async _onDrop(event) {
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event)
if (data?.type === "Actor") {
const actor = await fromUuid(data.uuid)
if (actor) {
this.actor.addAnnencyActor(actor.id)
} else {
ui.notifications.warn("Actor not found")
}
}
}
// #region Static Action Handlers
static #onActorEdit(event, target) {
const li = target.closest("[data-actor-id]")
game.actors.get(li?.dataset.actorId)?.sheet.render(true)
}
static async #onActorDelete(event, target) {
const li = target.closest("[data-actor-id]")
this.actor.removeAnnencyActor(li?.dataset.actorId)
}
static #onItemEdit(event, target) {
const li = target.closest("[data-item-id]")
const itemId = li?.dataset.itemId ?? target.dataset.itemId
this.document.items.get(itemId)?.sheet.render(true)
}
static async #onItemDelete(event, target) {
const li = target.closest("[data-item-id]")
EcrymeUtility.confirmDelete(this, $(li)).catch(() => {})
}
static async #onItemCreate(event, target) {
const dataType = target.dataset.type
await this.document.createEmbeddedDocuments("Item", [{ name: "NewItem", type: dataType }], { renderSheet: true })
}
// #endregion
}
+53
View File
@@ -0,0 +1,53 @@
const { HandlebarsApplicationMixin } = foundry.applications.api
/**
* Base actor sheet for Ecryme using Application V2.
* Provides common drag-drop, image editing, and shared structure.
*/
export default class EcrymeBaseActorSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ActorSheetV2) {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["fvtt-ecryme", "sheet", "actor"],
position: {
width: 860,
height: 680,
},
form: {
submitOnChange: true,
},
window: {
resizable: true,
},
dragDrop: [{ dragSelector: ".item-list .item[data-item-id]", dropSelector: null }],
actions: {
editImage: EcrymeBaseActorSheet.#onEditImage,
},
}
_canDragStart(selector) { return this.isEditable }
_canDragDrop(selector) { return this.isEditable && this.document.isOwner }
_onDragStart(event) {}
_onDragOver(event) {}
async _onDrop(event) {}
// #endregion
// #region Actions
static async #onEditImage(event, target) {
const attr = target.dataset.edit
const current = foundry.utils.getProperty(this.document, attr)
const { img } = this.document.constructor.getDefaultArtwork?.(this.document.toObject()) ?? {}
const fp = new FilePicker({
current,
type: "image",
redirectToRoot: img ? [img] : [],
callback: (path) => {
this.document.update({ [attr]: path })
},
top: this.position.top + 40,
left: this.position.left + 10,
})
return fp.browse()
}
// #endregion
}
+263
View File
@@ -0,0 +1,263 @@
import EcrymeBaseActorSheet from "./base-actor-sheet.js"
import { EcrymeUtility } from "../../common/ecryme-utility.js"
/**
* Actor sheet for PC and NPC types using Application V2.
*/
export default class EcrymeActorSheet extends EcrymeBaseActorSheet {
#editScore = false
/** @override */
static DEFAULT_OPTIONS = {
classes: ["pc-npc"],
position: { width: 860, height: 680 },
actions: {
openAnnency: EcrymeActorSheet.#onOpenAnnency,
itemEdit: EcrymeActorSheet.#onItemEdit,
itemDelete: EcrymeActorSheet.#onItemDelete,
itemCreate: EcrymeActorSheet.#onItemCreate,
subactorEdit: EcrymeActorSheet.#onSubactorEdit,
subactorDelete: EcrymeActorSheet.#onSubactorDelete,
rollSkill: EcrymeActorSheet.#onRollSkill,
rollSpec: EcrymeActorSheet.#onRollSpec,
rollSkillConfront: EcrymeActorSheet.#onRollSkillConfront,
rollCephaly: EcrymeActorSheet.#onRollCephaly,
rollWeaponConfront:EcrymeActorSheet.#onRollWeaponConfront,
impactModify: EcrymeActorSheet.#onImpactModify,
rollWeapon: EcrymeActorSheet.#onRollWeapon,
lockUnlock: EcrymeActorSheet.#onLockUnlock,
equipItem: EcrymeActorSheet.#onEquipItem,
quantityMinus: EcrymeActorSheet.#onQuantityMinus,
quantityPlus: EcrymeActorSheet.#onQuantityPlus,
resetTranscendence:EcrymeActorSheet.#onResetTranscendence,
},
}
/** @override */
static PARTS = {
header: { template: "systems/fvtt-ecryme/templates/actors/partials/actor-header.hbs" },
tabs: { template: "templates/generic/tab-navigation.hbs" },
skills: { template: "systems/fvtt-ecryme/templates/actors/actor-skills.hbs" },
traits: { template: "systems/fvtt-ecryme/templates/actors/actor-traits.hbs" },
combat: { template: "systems/fvtt-ecryme/templates/actors/actor-combat.hbs" },
cephaly: { template: "systems/fvtt-ecryme/templates/actors/actor-cephaly.hbs" },
equipements:{ template: "systems/fvtt-ecryme/templates/actors/actor-equipements.hbs" },
biodata: { template: "systems/fvtt-ecryme/templates/actors/actor-biodata.hbs" },
}
/** @override */
tabGroups = { primary: "skills" }
/** Build tabs, conditionally adding cephaly if the module is active */
_getTabs() {
const hasCephaly = EcrymeUtility.hasCephaly()
const tabs = {
skills: { id: "skills", group: "primary", label: "ECRY.ui.skills" },
traits: { id: "traits", group: "primary", label: "ECRY.ui.traits" },
combat: { id: "combat", group: "primary", label: "ECRY.ui.healthcombat" },
equipements:{ id: "equipements", group: "primary", label: "ECRY.ui.equipment" },
biodata: { id: "biodata", group: "primary", label: "ECRY.ui.bionotes" },
}
if (hasCephaly) {
// Insert cephaly after combat, rebuilding the object to preserve insertion order
const ordered = {}
for (const [k, v] of Object.entries(tabs)) {
ordered[k] = v
if (k === "combat") ordered.cephaly = { id: "cephaly", group: "primary", label: "ECRY.ui.cephaly" }
}
for (const tab of Object.values(ordered)) {
tab.active = this.tabGroups[tab.group] === tab.id
tab.cssClass = tab.active ? "active" : ""
}
return ordered
}
for (const tab of Object.values(tabs)) {
tab.active = this.tabGroups[tab.group] === tab.id
tab.cssClass = tab.active ? "active" : ""
}
return tabs
}
/** @override */
async _prepareContext() {
const actor = this.document
return {
actor,
system: actor.system,
source: actor.toObject(),
fields: actor.schema.fields,
systemFields: actor.system.schema.fields,
type: actor.type,
img: actor.img,
name: actor.name,
isEditable: this.isEditable,
config: game.ecryme.config,
hasCephaly: EcrymeUtility.hasCephaly(),
hasBoheme: EcrymeUtility.hasBoheme(),
hasAmertume: EcrymeUtility.hasAmertume(),
skills: actor.prepareSkills(),
traits: actor.getRollTraits(),
ideal: actor.getIdeal(),
spleen: actor.getSpleen(),
weapons: actor.getWeapons(),
maneuvers: actor.getManeuvers(),
impactsMalus: actor.getImpactsMalus(),
equipments: actor.getEquipments(),
cephalySkills:actor.getCephalySkills(),
confrontations: actor.getConfrontations(),
subActors: actor.getSubActors(),
annency: actor.getAnnency(),
owner: this.document.isOwner,
isGM: game.user.isGM,
editScore: this.#editScore,
tabs: this._getTabs(),
}
}
/** @override */
async _preparePartContext(partId, context) {
context = await super._preparePartContext(partId, context)
switch (partId) {
case "skills":
case "traits":
case "combat":
case "cephaly":
case "equipements":
context.tab = context.tabs[partId] ?? { cssClass: "" }
break
case "biodata":
context.tab = context.tabs.biodata
context.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
this.document.system.biodata.description, { async: true }
)
context.enrichedGmnotes = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
this.document.system.biodata.gmnotes, { async: true }
)
break
}
return context
}
// #region Drag and Drop
/** Handle incoming drops: Items from sidebar/compendium, Actors as subactors */
async _onDrop(event) {
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event)
if (!data?.type) return
if (data.type === "Item") {
const item = await fromUuid(data.uuid)
if (!item) return
await this.document.createEmbeddedDocuments("Item", [item.toObject()])
} else if (data.type === "Actor") {
const actor = fromUuidSync(data.uuid)
if (actor) await this.actor.addSubActor(actor.id)
}
}
/** Handle outgoing drag from embedded item rows */
_onDragStart(event) {
const li = event.currentTarget.closest("[data-item-id]")
if (!li) return
const item = this.document.items.get(li.dataset.itemId)
if (!item) return
event.dataTransfer.setData("text/plain", JSON.stringify(item.toDragData()))
}
// #endregion
// #region Static Action Handlers
static #onOpenAnnency(event, target) {
const actorId = target.dataset.annencyId
game.actors.get(actorId)?.sheet.render(true)
}
static #onItemEdit(event, target) {
const li = target.closest("[data-item-id]")
const itemId = li?.dataset.itemId ?? target.dataset.itemId
this.document.items.get(itemId)?.sheet.render(true)
}
static async #onItemDelete(event, target) {
const li = target.closest("[data-item-id]")
EcrymeUtility.confirmDelete(this, $(li)).catch(() => {})
}
static async #onItemCreate(event, target) {
const dataType = target.dataset.type
await this.document.createEmbeddedDocuments("Item", [{ name: "NewItem", type: dataType }], { renderSheet: true })
}
static #onSubactorEdit(event, target) {
const li = target.closest("[data-actor-id]")
game.actors.get(li?.dataset.actorId)?.sheet.render(true)
}
static #onSubactorDelete(event, target) {
const li = target.closest("[data-actor-id]")
this.actor.delSubActor(li?.dataset.actorId)
}
static #onRollSkill(event, target) {
this.actor.rollSkill(target.dataset.categoryKey, target.dataset.skillKey)
}
static #onRollSpec(event, target) {
this.actor.rollSpec(target.dataset.categoryKey, target.dataset.skillKey, target.dataset.specId)
}
static #onRollSkillConfront(event, target) {
this.actor.rollSkillConfront(target.dataset.categoryKey, target.dataset.skillKey)
}
static #onRollCephaly(event, target) {
this.actor.rollCephalySkillConfront(target.dataset.skillKey)
}
static #onRollWeaponConfront(event, target) {
const li = target.closest("[data-item-id]")
this.actor.rollWeaponConfront(li?.dataset.itemId)
}
static #onImpactModify(event, target) {
this.actor.modifyImpact(
target.dataset.impactType,
target.dataset.impactLevel,
Number(target.dataset.impactModifier)
)
}
static #onRollWeapon(event, target) {
this.actor.rollWeapon(target.dataset.armeId)
}
static #onLockUnlock(event, target) {
this.#editScore = !this.#editScore
this.render(true)
}
static #onEquipItem(event, target) {
const li = target.closest("[data-item-id]")
this.actor.equipItem(li?.dataset.itemId)
this.render(true)
}
static #onQuantityMinus(event, target) {
const li = target.closest("[data-item-id]")
this.actor.incDecQuantity(li?.dataset.itemId, -1)
}
static #onQuantityPlus(event, target) {
const li = target.closest("[data-item-id]")
this.actor.incDecQuantity(li?.dataset.itemId, +1)
}
static async #onResetTranscendence(event, target) {
await this.actor.resetSkillPool()
this.render(true)
}
// #endregion
}
+4 -7
View File
@@ -8,7 +8,7 @@ export class EcrymeCombat extends Combat {
ids = typeof ids === "string" ? [ids] : ids;
for (let cId = 0; cId < ids.length; cId++) {
const c = this.combatants.get(ids[cId]);
let id = c._id || c.id;
let id = c.id;
let initBonus = c.actor ? c.actor.getInitiativeScore( this.id, id ) : -1;
await this.updateEmbeddedDocuments("Combatant", [ { _id: id, initiative: initBonus } ]);
}
@@ -17,9 +17,6 @@ export class EcrymeCombat extends Combat {
}
/* -------------------------------------------- */
_onUpdate(changed, options, userId) {
}
/* -------------------------------------------- */
static async checkTurnPosition() {
while (game.combat.turn > 0) {
@@ -28,13 +25,13 @@ export class EcrymeCombat extends Combat {
}
/* -------------------------------------------- */
_onDelete() {
async _onDelete() {
let combatants = this.combatants.contents
for (let c of combatants) {
let actor = game.actors.get(c.actorId)
actor.clearInitiative()
await actor.clearInitiative()
}
super._onDelete()
await super._onDelete()
}
}
+3 -3
View File
@@ -7,10 +7,10 @@ import { EcrymeCharacterSummary } from "../app/ecryme-summary-app.js"
export class EcrymeCommands {
static init() {
if (!game.system.ecryme.commands) {
if (!game.ecryme.commands) {
const commands = new EcrymeCommands();
commands.registerCommand({ path: ["/resume"], func: (content, msg, params) => EcrymeCharacterSummary.displayPCSummary(), descr: "Affiche la liste des PJs!" });
game.system.ecryme.commands = commands;
game.ecryme.commands = commands;
}
}
constructor() {
@@ -88,7 +88,7 @@ export class EcrymeCommands {
if (command && command.func) {
const result = command.func(content, msg, params);
if (result == false) {
CrucibleCommands._chatAnswer(msg, command.descr);
EcrymeCommands._chatAnswer(msg, command.descr);
}
return true;
}
+4 -4
View File
@@ -14,7 +14,7 @@ export class EcrymeHotbar {
if (documentData.type == "Item") {
console.log("Drop done !!!", bar, documentData, slot)
let item = documentData.data
let command = `game.system.ecryme.EcrymeHotbar.rollMacro("${item.name}", "${item.type}");`
let command = `game.ecryme.EcrymeHotbar.rollMacro("${item.name}", "${item.type}");`
let macro = game.macros.contents.find(m => (m.name === item.name) && (m.command === command))
if (!macro) {
macro = await Macro.create({
@@ -33,9 +33,9 @@ export class EcrymeHotbar {
let macro = game.macros.contents.find(m => (m.name === actor.name) && (m.command === command));
if (!macro) {
macro = await Macro.create({
name: actor.data.name,
name: actor.name,
type: "script",
img: actor.data.img,
img: actor.img,
command: command
}, { displaySheet: false })
game.user.assignHotbarMacro(macro, slot);
@@ -48,7 +48,7 @@ export class EcrymeHotbar {
let macro = game.macros.contents.find(m => (m.name === journal.name) && (m.command === command));
if (!macro) {
macro = await Macro.create({
name: journal.data.name,
name: journal.name,
type: "script",
img: "",
command: command
+75 -75
View File
@@ -1,13 +1,37 @@
/* -------------------------------------------- */
import { EcrymeUtility } from "../common/ecryme-utility.js";
const { HandlebarsApplicationMixin } = foundry.applications.api
/* -------------------------------------------- */
export class EcrymeCharacterSummary extends Application {
export class EcrymeCharacterSummary extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
/* -------------------------------------------- */
static DEFAULT_OPTIONS = {
id: "ecryme-character-summary",
classes: ["fvtt-ecryme", "dialog"],
position: { width: 920 },
window: { title: "ECRY.ui.charactersummary", resizable: true },
actions: {
actorOpen: EcrymeCharacterSummary.#onActorOpen,
summaryRoll: EcrymeCharacterSummary.#onSummaryRoll,
actorDelete: EcrymeCharacterSummary.#onActorDelete,
},
}
/* -------------------------------------------- */
static PARTS = {
content: { template: "systems/fvtt-ecryme/templates/dialogs/character-summary.hbs" },
}
/* -------------------------------------------- */
constructor(options = {}) {
super(options)
this.settings = game.settings.get("fvtt-ecryme", "character-summary-data")
}
/* -------------------------------------------- */
static displayPCSummary() {
if (game.user.isGM) {
game.system.ecryme.charSummary.render(true)
game.ecryme.charSummary.render(true)
} else {
ui.notifications.info("Commande /summary réservée au MJ !")
}
@@ -16,119 +40,95 @@ export class EcrymeCharacterSummary extends Application {
/* -------------------------------------------- */
updatePCSummary() {
if (this.rendered) {
this.render(true)
this.render()
}
}
/* -------------------------------------------- */
static createSummaryPos() {
return { top: 200, left: 200 };
}
/* -------------------------------------------- */
static ready() {
if (!game.user.isGM) { // Uniquement si GM
if (!game.user.isGM) {
return
}
let charSummary = new EcrymeCharacterSummary()
game.system.ecryme.charSummary = charSummary
game.ecryme.charSummary = charSummary
}
/* -------------------------------------------- */
constructor() {
super();
//game.settings.set("world", "character-summary-data", {npcList: [], x:0, y:0})
//this.settings = game.settings.get("world", "character-summary-data")
}
/* -------------------------------------------- */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
template: "systems/fvtt-ecryme/templates/dialogs/character-summary.hbs",
popOut: true,
resizable: true,
dragDrop: [{ dragSelector: ".items-list .item", dropSelector: null }],
classes: ["bol", "dialog"], width: 920, height: 'fit-content'
})
}
/* -------------------------------------------- */
getData() {
let formData = super.getData();
formData.pcs = game.actors.filter(ac => ac.type == "personnage" && ac.hasPlayerOwner)
formData.npcs = []
async _prepareContext() {
let pcs = game.actors.filter(ac => ac.type == "pc" && ac.hasPlayerOwner)
let npcs = []
let newList = []
let toUpdate = false
for (let actorId of this.settings.npcList) {
let actor = game.actors.get(actorId)
if (actor) {
formData.npcs.push(actor)
npcs.push(actor)
newList.push(actorId)
} else {
toUpdate = true
}
}
formData.config = game.system.ecryme.config
if (toUpdate) {
this.settings.npcList = newList
//console.log("Going to update ...", this.settings)
game.settings.set("world", "character-summary-data", this.settings)
game.settings.set("fvtt-ecryme", "character-summary-data", this.settings)
}
return {
pcs,
npcs,
config: game.ecryme.config,
}
return formData
}
/* -------------------------------------------- */
updateNPC() {
game.settings.set("world", "character-summary-data", game.system.ecryme.charSummary.settings)
game.system.ecryme.charSummary.close()
setTimeout(function () { game.system.ecryme.charSummary.render(true) }, 500)
game.settings.set("fvtt-ecryme", "character-summary-data", this.settings)
this.close()
setTimeout(() => this.render(true), 500)
}
/* -------------------------------------------- */
async _onDrop(event) {
//console.log("Dragged data are : ", dragData)
let data = event.dataTransfer.getData('text/plain')
let dataItem = JSON.parse(data)
let actor = fromUuidSync(dataItem.uuid)
if (actor) {
game.system.ecryme.charSummary.settings.npcList.push(actor.id)
game.system.ecryme.charSummary.updateNPC()
_onDragOver(event) {
event.preventDefault()
}
/* -------------------------------------------- */
_onDrop(event) {
let data
try { data = JSON.parse(event.dataTransfer.getData('text/plain')) } catch(e) { return }
let actor = fromUuidSync(data.uuid)
if (actor) {
this.settings.npcList.push(actor.id)
this.updateNPC()
} else {
ui.notifications.warn("Pas d'acteur trouvé")
}
}
/* -------------------------------------------- */
/** @override */
async activateListeners(html) {
super.activateListeners(html);
_onRender(context, options) {
super._onRender(context, options)
this.element.addEventListener("dragover", this._onDragOver.bind(this))
this.element.addEventListener("drop", this._onDrop.bind(this))
}
html.find('.actor-open').click((event) => {
const li = $(event.currentTarget).parents(".item")
const actor = game.actors.get(li.data("actor-id"))
actor.sheet.render(true)
})
/* -------------------------------------------- */
static #onActorOpen(event, target) {
const actorId = target.closest("[data-actor-id]").dataset.actorId
game.actors.get(actorId)?.sheet.render(true)
}
html.find('.summary-roll').click((event) => {
const li = $(event.currentTarget).parents(".item")
const actor = game.actors.get(li.data("actor-id"))
let type = $(event.currentTarget).data("type")
let key = $(event.currentTarget).data("key")
actor.rollAttribut(key)
})
html.find('.actor-delete').click(event => {
const li = $(event.currentTarget).parents(".item");
let actorId = li.data("actor-id")
let newList = game.system.ecryme.charSummary.settings.npcList.filter(id => id != actorId)
game.system.ecryme.charSummary.settings.npcList = newList
game.system.ecryme.charSummary.updateNPC()
})
/* -------------------------------------------- */
static #onSummaryRoll(event, target) {
const actorId = target.closest("[data-actor-id]").dataset.actorId
const key = target.dataset.key
game.actors.get(actorId)?.rollAttribut(key)
}
/* -------------------------------------------- */
static #onActorDelete(event, target) {
const actorId = target.closest("[data-actor-id]").dataset.actorId
this.settings.npcList = this.settings.npcList.filter(id => id !== actorId)
this.updateNPC()
}
}
+22 -9
View File
@@ -10,14 +10,14 @@ export const ECRYME_CONFIG = {
"melee": "ECRY.ui.melee",
"ranged": "ECRY.ui.ranged"
},
traitLevel: [
{value: -3, text: "-3"},
{value: -2, text: "-2"},
{value: -1, text: "-1"},
{value: +1, text: "+1"},
{value: +2, text: "+2"},
{value: +3, text: "+3"}
],
traitLevel: {
"-3":{value: "-3", text: "-3"},
"-2":{value: "-2", text: "-2"},
"-1":{value: "-1", text: "-1"},
"+1":{value: "+1", text: "+1"},
"+2":{value: "+2", text: "+2"},
"+3":{value: "+3", text: "+3"}
},
impactTypes: {
physical: "ECRY.ui.physical",
mental: "ECRY.ui.mental",
@@ -30,7 +30,7 @@ export const ECRYME_CONFIG = {
major: "ECRY.ui.major"
},
difficulty: {
"-1": {difficulty: "ECRY.ui.none", frequency: "ECRY.ui.none", value: "-"},
"-1": {difficulty: "ECRY.ui.none", frequency: "ECRY.ui.none", value: "-1"},
"8": { difficulty: "ECRY.ui.troublesome", frequency: "ECRY.ui.occasional", value: 8 },
"10": { difficulty: "ECRY.ui.difficult", frequency: "ECRY.ui.uncommon", value: 10 },
"12": { difficulty: "ECRY.ui.verydifficult", frequency: "ECRY.ui.rare", value: 12 },
@@ -57,6 +57,19 @@ export const ECRYME_CONFIG = {
"lige": {name: "ECRY.ui.lige", value: 100 },
"hurle": {name: "ECRY.ui.hurle", value: 10 },
"coin": {name: "ECRY.ui.coin", value: 1 }
},
transcendanceOptions: {
"execution": "ECRY.ui.execution",
"preservation": "ECRY.ui.preservation"
},
bonusMalusPersoOptions: {
"-3": {value: "-3", label: "-3"},
"-2": {value: "-2", label: "-2"},
"-1": {value: "-1", label: "-1"},
"0": {value: "0", label: "0"},
"+1": {value: "1", label: "+1"},
"+2": {value: "2", label: "+2"},
"+3": {value: "3", label: "+3"}
}
}
+136 -88
View File
@@ -36,7 +36,7 @@ export class EcrymeUtility {
/* -------------------------------------------- */
static async init() {
Hooks.on('renderChatLog', (log, html, data) => EcrymeUtility.chatListeners(html));
Hooks.on("getChatLogEntryContext", (html, options) => EcrymeUtility.chatMenuManager(html, options));
Hooks.on("getChatMessageContextOptions", (html, options) => EcrymeUtility.chatMenuManager(html, options));
this.rollDataStore = {}
this.defenderStore = {}
@@ -97,9 +97,17 @@ export class EcrymeUtility {
"level_b": game.i18n.localize("ECRY.settings.boheme"),
"level_a": game.i18n.localize("ECRY.settings.amertume"),
},
default: "level_a",
restricted: true
})
game.settings.register("fvtt-ecryme", "character-summary-data", {
scope: 'world',
config: false,
type: Object,
default: { npcList: [] }
})
this.buildSkillConfig()
}
@@ -109,16 +117,61 @@ export class EcrymeUtility {
let level = game.settings.get("fvtt-ecryme", "ecryme-game-level")
return level != "level_e"
}
/*-------------------------------------------- */
static hasBoheme() {
let level = game.settings.get("fvtt-ecryme", "ecryme-game-level")
return level == "level_b" || level == "level_a"
}
/*-------------------------------------------- */
static hasAmertume() {
let level = game.settings.get("fvtt-ecryme", "ecryme-game-level")
return level == "level_a"
}
/*-------------------------------------------- */
static buildSkillConfig() {
game.system.ecryme.config.skills = {}
for (let categKey in game.data.template.Actor.templates.core.skills) {
let category = game.data.template.Actor.templates.core.skills[categKey]
// Build skill configuration from DataModel structure
game.ecryme.config.skills = {}
const skillCategories = {
physical: {
name: "ECRY.ui.physical",
skilllist: {
athletics: { key: "athletics", name: "ECRY.ui.athletics", max: 0, value: 0 },
driving: { key: "driving", name: "ECRY.ui.driving", max: 0, value: 0 },
fencing: { key: "fencing", name: "ECRY.ui.fencing", max: 0, value: 0 },
brawling: { key: "brawling", name: "ECRY.ui.brawling", max: 0, value: 0 },
shooting: { key: "shooting", name: "ECRY.ui.shooting", max: 0, value: 0 }
}
},
mental: {
name: "ECRY.ui.mental",
skilllist: {
anthropomecanology: { key: "anthropomecanology", name: "ECRY.ui.anthropomecanology", max: 10, value: 0 },
ecrymology: { key: "ecrymology", name: "ECRY.ui.ecrymology", max: 10, value: 0 },
traumatology: { key: "traumatology", name: "ECRY.ui.traumatology", max: 10, value: 0 },
traversology: { key: "traversology", name: "ECRY.ui.traversology", max: 10, value: 0 },
urbatechnology: { key: "urbatechnology", name: "ECRY.ui.urbatechnology", max: 10, value: 0 }
}
},
social: {
name: "ECRY.ui.social",
skilllist: {
quibbling: { key: "quibbling", name: "ECRY.ui.quibbling", max: 10, value: 0 },
creativity: { key: "creativity", name: "ECRY.ui.creativity", max: 10, value: 0 },
loquacity: { key: "loquacity", name: "ECRY.ui.loquacity", max: 10, value: 0 },
guile: { key: "guile", name: "ECRY.ui.guile", max: 10, value: 0 },
performance: { key: "performance", name: "ECRY.ui.performance", max: 10, value: 0 }
}
}
}
for (let categKey in skillCategories) {
let category = skillCategories[categKey]
for (let skillKey in category.skilllist) {
let skill = duplicate(category.skilllist[skillKey])
skill.categKey = categKey // Auto reference the category
game.system.ecryme.config.skills[skillKey] = skill
let skill = foundry.utils.deepClone(category.skilllist[skillKey])
skill.categKey = categKey
game.ecryme.config.skills[skillKey] = skill
}
}
}
@@ -144,8 +197,8 @@ export class EcrymeUtility {
/* -------------------------------------------- */
static getActorFromRollData(rollData) {
let actor = game.actors.get(rollData.actorId)
if (rollData.tokenId) {
let token = canvas.tokens.placeables.find(t => t.id == rollData.tokenId)
if (rollData.defenderTokenId) {
let token = canvas.tokens.placeables.find(t => t.id == rollData.defenderTokenId)
if (token) {
actor = token.actor
}
@@ -165,6 +218,8 @@ export class EcrymeUtility {
type: "confront-data",
rollData1: this.confrontData1,
rollData2: this.confrontData2,
alias: this.confrontData1.alias,
actorImg: this.confrontData1.actorImg,
}
// Compute margin
confront.marginExecution = this.confrontData1.executionTotal - this.confrontData2.preservationTotal
@@ -174,19 +229,19 @@ export class EcrymeUtility {
let maxMargin // Dummy max
if (confront.marginExecution > 0) { // Successful hit
// Limit with skill+spec
maxMargin = confront.rollData1.skill.value + ((confront.rollData1.spec) ? 2 : 0)
maxMargin = confront.rollData1.skill.max + ((confront.rollData1.spec) ? 2 : 0)
confront.marginExecution = Math.min(confront.marginExecution, maxMargin)
} else { // Failed hit
maxMargin = confront.rollData2.skill.value + ((confront.rollData2.spec) ? 2 : 0)
maxMargin = confront.rollData2.skill.max + ((confront.rollData2.spec) ? 2 : 0)
confront.marginExecution = -Math.min(Math.abs(confront.marginExecution), maxMargin)
}
if (confront.marginPreservation > 0) { // Successful defense
// Limit with skill+spec
maxMargin = confront.rollData1.skill.value + ((confront.rollData1.spec) ? 2 : 0)
maxMargin = confront.rollData1.skill.max + ((confront.rollData1.spec) ? 2 : 0)
confront.marginPreservation = Math.min(confront.marginPreservation, maxMargin)
} else { // Failed defense
maxMargin = confront.rollData2.skill.value + ((confront.rollData2.spec) ? 2 : 0)
maxMargin = confront.rollData2.skill.max + ((confront.rollData2.spec) ? 2 : 0)
confront.marginPreservation = - Math.min(Math.abs(confront.marginPreservation), maxMargin)
}
@@ -205,13 +260,13 @@ export class EcrymeUtility {
confront.impactPreservation = this.getImpactFromEffect(Math.abs(confront.effectPreservation))
}
if (confront.marginPreservation > 0) {
confront.bonus1 = -confront.marginPreservation
confront.bonus1 = confront.marginPreservation
}
let msg = await this.createChatWithRollMode(this.confrontData1.alias, {
content: await renderTemplate(`systems/fvtt-ecryme/templates/chat/chat-confrontation-result.hbs`, confront)
content: await foundry.applications.handlebars.renderTemplate(`systems/fvtt-ecryme/templates/chat/chat-confrontation-result.hbs`, confront)
})
msg.setFlag("world", "ecryme-rolldata", confront)
await msg.setFlag("world", "ecryme-rolldata", confront)
console.log("Confront result", confront)
this.lastConfront = confront
@@ -232,7 +287,7 @@ export class EcrymeUtility {
rollData.marginPreservation = -1
}
let msg = await this.createChatWithRollMode(rollData.alias, {
content: await renderTemplate(`systems/fvtt-ecryme/templates/chat/chat-cephaly-result.hbs`, rollData)
content: await foundry.applications.handlebars.renderTemplate(`systems/fvtt-ecryme/templates/chat/chat-cephaly-result.hbs`, rollData)
})
msg.setFlag("world", "ecryme-rolldata", rollData)
console.log("Cephaly result", rollData)
@@ -262,20 +317,17 @@ export class EcrymeUtility {
let canTranscendRoll = []
for (let i = 1; i <= 10; i++) {
canTranscendRoll[i] = function (li) {
let message = game.messages.get(li.attr("data-message-id"))
let message = game.messages.get($(li).attr("data-message-id"))
let rollData = message.getFlag("world", "rolldata")
//console.log(">>>>>>>>>>>>>>>>>>>>>>>>>> Menu !!!!", rollData)
if (rollData.skill && i <= rollData.skill.value && !rollData.transcendUsed && rollData.spec) {
return true
}
return false
return (rollData?.skill?.value >= i && !rollData.transcendUsed && rollData.spec)
}
options.push({
name: game.i18n.localize("ECRY.chat.spectranscend") + i,
icon: '<i class="fas fa-plus-square"></i>',
condition: canTranscendRoll[i],
callback: li => {
let message = game.messages.get(li.attr("data-message-id"))
let message = game.messages.get($(li).attr("data-message-id"))
let rollData = message.getFlag("world", "rolldata")
EcrymeUtility.transcendFromSpec(rollData, i).catch("Error on Transcend")
}
@@ -286,26 +338,35 @@ export class EcrymeUtility {
/* -------------------------------------------- */
static async chatListeners(html) {
html.on("click", '.button-select-confront', event => {
$(html).on("click", '.button-select-confront', event => {
let messageId = EcrymeUtility.findChatMessageId(event.currentTarget)
let message = game.messages.get(messageId)
let rollData = message.getFlag("world", "ecryme-rolldata")
ui.notifications.info(game.i18n.localize("ECRY.chat.confrontselect"))
EcrymeUtility.manageConfrontation(rollData)
})
html.on("click", '.button-apply-cephaly-difficulty', event => {
$(html).on("click", '.button-apply-cephaly-difficulty', event => {
let messageId = EcrymeUtility.findChatMessageId(event.currentTarget)
let message = game.messages.get(messageId)
let rollData = message.getFlag("world", "ecryme-rolldata")
let difficulty = $("#" + rollData.rollId + "-cephaly-difficulty").val()
EcrymeUtility.manageCephalyDifficulty(rollData, difficulty)
})
html.on("click", '.button-apply-impact', event => {
$(html).on("click", '.button-apply-impact', event => {
let messageId = EcrymeUtility.findChatMessageId(event.currentTarget)
let message = game.messages.get(messageId)
let actor = game.actors.get($(event.currentTarget).data("actor-id"))
let tokenId = $(event.currentTarget).data("token-id")
let actor
if (!tokenId) {
actorId = $(event.currentTarget).data("actor-id")
actor = game.actors.get(actorId)
} else {
let token = canvas.tokens.placeables.find(t => t.id == tokenId)
actor = token?.actor
}
actor.modifyImpact($(event.currentTarget).data("impact-type"), $(event.currentTarget).data("impact"), 1)
})
html.on("click", '.button-apply-bonus', event => {
$(html).on("click", '.button-apply-bonus', event => {
let messageId = EcrymeUtility.findChatMessageId(event.currentTarget)
let message = game.messages.get(messageId)
let actor = game.actors.get($(event.currentTarget).data("actor-id"))
@@ -326,8 +387,11 @@ export class EcrymeUtility {
'systems/fvtt-ecryme/templates/dialogs/partial-confront-dice-area.hbs',
'systems/fvtt-ecryme/templates/dialogs/partial-confront-bonus-area.hbs',
'systems/fvtt-ecryme/templates/actors/partial-impacts.hbs',
'systems/fvtt-ecryme/templates/actors/partials/actor-header.hbs',
'systems/fvtt-ecryme/templates/items/partials/item-header.hbs',
'systems/fvtt-ecryme/templates/items/partials/item-description.hbs',
]
return loadTemplates(templatePaths);
return foundry.applications.handlebars.loadTemplates(templatePaths);
}
/* -------------------------------------------- */
@@ -393,16 +457,21 @@ export class EcrymeUtility {
let id = rollData.rollId
let oldRollData = this.rollDataStore[id] || {}
let newRollData = mergeObject(oldRollData, rollData)
let newRollData = { ...oldRollData, ...rollData }
this.rollDataStore[id] = newRollData
}
/* -------------------------------------------- */
static async onSocketMesssage(msg) {
console.log("SOCKET MESSAGE", msg.name)
if (msg.name == "msg-draw-card") {
if (game.user.isGM && game.system.ecryme.currentTirage) {
game.system.ecryme.currentTirage.addCard(msg.data.msgId)
console.log("SOCKET MESSAGE", msg)
if (msg.name == "msg_gm_chat_message") {
let rollData = msg.data.rollData
if (game.user.isGM) {
let chatMsg = await this.createChatMessage(rollData.alias, "blindroll", {
content: await foundry.applications.handlebars.renderTemplate(msg.data.template, rollData),
whisper: game.user.id
})
chatMsg.setFlag("world", "ecryme-rolldata", rollData)
}
}
}
@@ -411,7 +480,7 @@ export class EcrymeUtility {
static async searchItem(dataItem) {
let item
if (dataItem.pack) {
let id = dataItem.id || dataItem._id
let id = dataItem.id
let items = await this.loadCompendium(dataItem.pack, item => item.id == id)
item = items[0] || undefined
} else {
@@ -430,7 +499,7 @@ export class EcrymeUtility {
if (["gmroll", "blindroll"].includes(chatData.rollMode)) chatData["whisper"] = ChatMessage.getWhisperRecipients("GM").map(u => u.id);
if (chatData.rollMode === "blindroll") chatData["blind"] = true;
else if (chatData.rollMode === "selfroll") chatData["whisper"] = [game.user];
else if (chatData.rollMode === "selfroll") chatData["whisper"] = [game.user.id];
if (forceWhisper) { // Final force !
chatData["speaker"] = ChatMessage.getSpeaker();
@@ -482,7 +551,7 @@ export class EcrymeUtility {
rollData.margin = rollData.total - rollData.difficulty
if (rollData.total > rollData.difficulty) {
rollData.isSuccess = true
let maxMargin = rollData.skill.value + (rollData.spec) ? 2 : 0
let maxMargin = rollData.skill.max + ((rollData.spec) ? 2 : 0)
rollData.margin = Math.min(rollData.margin, maxMargin)
}
}
@@ -498,15 +567,16 @@ export class EcrymeUtility {
diceFormula = (isConfrontation) ? "5d6kl2" : "3d6kl2"
}
if (rollData.skill) {
diceFormula += "+" + rollData.skill.value
diceFormula += "+" + rollData.skill.max
}
if (rollData.skillTranscendence) {
diceFormula += "+" + rollData.skillTranscendence
actor.spentSkillTranscendence(rollData.skill, rollData.skillTranscendence)
let toSpend = Math.min(rollData.skillTranscendence, rollData.skill.value)
diceFormula += "+" + toSpend
actor.spentSkillTranscendence(rollData.skill, toSpend)
}
if (rollData.selectedSpecs && rollData.selectedSpecs.length > 0) {
rollData.spec = actor.getSpecialization(rollData.selectedSpecs[0])
diceFormula += "+2"
diceFormula += "+" + (String(rollData.spec.system?.bonus) || "2")
}
rollData.bonusMalusTraits = 0
if (rollData.traitsBonus && rollData.traitsBonus.length > 0) {
@@ -515,7 +585,7 @@ export class EcrymeUtility {
let trait = actor.getTrait(id)
console.log(trait, id)
rollData.traitsBonusList.push(trait)
rollData.bonusMalusTraits += trait.system.level
rollData.bonusMalusTraits += Math.abs(trait.system.level)
}
}
if (rollData.traitsMalus && rollData.traitsMalus.length > 0) {
@@ -523,12 +593,15 @@ export class EcrymeUtility {
for (let id of rollData.traitsMalus) {
let trait = actor.getTrait(id)
rollData.traitsMalusList.push(trait)
rollData.bonusMalusTraits -= trait.system.level
rollData.bonusMalusTraits -= Math.abs(trait.system.level)
}
}
diceFormula += "+" + rollData.bonusMalusTraits
diceFormula += "+" + rollData.bonusMalusPerso
diceFormula += "+" + rollData.impactMalus
if (rollData.annency) {
diceFormula += "+" + rollData.annencyBonus
}
rollData.diceFormula = diceFormula
return diceFormula
}
@@ -538,7 +611,7 @@ export class EcrymeUtility {
let actor = game.actors.get(rollData.actorId)
// Fix difficulty
if (!rollData.difficulty || rollData.difficulty == "-") {
if (!rollData.difficulty || rollData.difficulty == "-1") {
rollData.difficulty = 0
}
rollData.difficulty = Number(rollData.difficulty)
@@ -546,18 +619,18 @@ export class EcrymeUtility {
let diceFormula = this.computeRollFormula(rollData, actor)
// Performs roll
let myRoll = new Roll(diceFormula).roll({ async: false })
let myRoll = await new Roll(diceFormula).roll()
await this.showDiceSoNice(myRoll, game.settings.get("core", "rollMode"))
rollData.roll = duplicate(myRoll)
rollData.roll = foundry.utils.deepClone(myRoll)
rollData.total = myRoll.total
rollData.diceSum = myRoll.terms[0].total
this.computeResults(rollData)
let msg = await this.createChatWithRollMode(rollData.alias, {
content: await renderTemplate(`systems/fvtt-ecryme/templates/chat/chat-generic-result.hbs`, rollData)
content: await foundry.applications.handlebars.renderTemplate(`systems/fvtt-ecryme/templates/chat/chat-generic-result.hbs`, rollData)
})
msg.setFlag("world", "ecryme-rolldata", rollData)
await msg.setFlag("world", "ecryme-rolldata", rollData)
console.log("Rolldata result", rollData)
}
@@ -572,9 +645,9 @@ export class EcrymeUtility {
actor.spentSkillTranscendence(rollData.skill, value)
let msg = await this.createChatWithRollMode(rollData.alias, {
content: await renderTemplate(`systems/fvtt-ecryme/templates/chat/chat-generic-result.hbs`, rollData)
content: await foundry.applications.handlebars.renderTemplate(`systems/fvtt-ecryme/templates/chat/chat-generic-result.hbs`, rollData)
})
msg.setFlag("world", "ecryme-rolldata", rollData)
await msg.setFlag("world", "ecryme-rolldata", rollData)
}
/* -------------------------------------------- */
@@ -618,12 +691,10 @@ export class EcrymeUtility {
}
/* -------------------------------------------- */
static blindMessageToGM(chatOptions) {
let chatGM = duplicate(chatOptions);
chatGM.whisper = this.getUsers(user => user.isGM);
chatGM.content = "Blinde message of " + game.user.name + "<br>" + chatOptions.content;
console.log("blindMessageToGM", chatGM);
game.socket.emit("system.fvtt-ecryme", { msg: "msg_gm_chat_message", data: chatGM });
static blindMessageToGM(chatData) {
chatData.whisper = this.getUsers(user => user.isGM);
console.log("blindMessageToGM", chatData);
game.socket.emit("system.fvtt-ecryme", { name: "msg_gm_chat_message", data: chatData });
}
@@ -649,12 +720,8 @@ export class EcrymeUtility {
switch (rollMode) {
case "blindroll": // GM only
if (!game.user.isGM) {
this.blindMessageToGM(chatOptions);
chatOptions.whisper = [game.user.id];
chatOptions.content = "Message only to the GM";
}
else {
} else {
chatOptions.whisper = this.getUsers(user => user.isGM);
}
break;
@@ -669,19 +736,20 @@ export class EcrymeUtility {
/* -------------------------------------------- */
static getBasicRollData() {
let rollData = {
rollId: randomID(16),
rollId: foundry.utils.randomID(16),
type: "roll-data",
bonusMalusPerso: 0,
bonusMalusPerso: "0",
bonusMalusSituation: 0,
bonusMalusDef: 0,
annencyBonus: 0,
bonusMalusPortee: 0,
skillTranscendence: 0,
rollMode: game.settings.get("core", "rollMode"),
difficulty: "-",
difficulty: "-1",
useSpleen: false,
useIdeal: false,
impactMalus: 0,
config: duplicate(game.system.ecryme.config)
config: foundry.utils.deepClone(game.ecryme.config)
}
EcrymeUtility.updateWithTarget(rollData)
return rollData
@@ -703,29 +771,9 @@ export class EcrymeUtility {
/* -------------------------------------------- */
static async confirmDelete(actorSheet, li) {
let itemId = li.data("item-id");
let msgTxt = "<p>Are you sure to remove this Item ?";
let buttons = {
delete: {
icon: '<i class="fas fa-check"></i>',
label: "Yes, remove it",
callback: () => {
actorSheet.actor.deleteEmbeddedDocuments("Item", [itemId]);
li.slideUp(200, () => actorSheet.render(false));
}
},
cancel: {
icon: '<i class="fas fa-times"></i>',
label: "Cancel"
}
}
msgTxt += "</p>";
let d = new Dialog({
title: "Confirm removal",
content: msgTxt,
buttons: buttons,
default: "cancel"
});
d.render(true);
if (!confirm("Etes vous certain de souhaiter envoyer cet item dans les limbes ?")) return;
actorSheet.actor.deleteEmbeddedDocuments("Item", [itemId]);
li.slideUp(200, () => actorSheet.render(false));
}
}
+206 -185
View File
@@ -1,251 +1,272 @@
import { EcrymeUtility } from "../common/ecryme-utility.js";
import { EcrymeRollDialog } from "./ecryme-roll-dialog.js";
export class EcrymeConfrontDialog extends Dialog {
const { HandlebarsApplicationMixin } = foundry.applications.api
/**
* Main confrontation dialog — Application V2 version.
* Features drag-and-drop of dice from the pool to execution/preservation slots.
* All event listeners (change + drag-drop) are bound once via _listenersAdded guard.
*/
export class EcrymeConfrontDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
#dragDrop
_listenersAdded = false
/* -------------------------------------------- */
constructor(actor, rollData, options = {}) {
super(options)
this.actor = actor
this.rollData = rollData
this.buttonDisabled = true
this.#dragDrop = this.#createDragDropHandlers()
}
/* -------------------------------------------- */
/** @override */
static DEFAULT_OPTIONS = {
classes: ["fvtt-ecryme", "ecryme-confrontation-dialog"],
position: { width: 640 },
window: { title: "ECRY.ui.confront" },
dragDrop: [{ dragSelector: ".confront-dice-container", dropSelector: null }],
actions: {
launchConfront: EcrymeConfrontDialog.#onLaunchConfront,
cancel: EcrymeConfrontDialog.#onCancel,
},
}
/** @override */
static PARTS = {
content: { template: "systems/fvtt-ecryme/templates/dialogs/confront-dialog.hbs" },
}
/* -------------------------------------------- */
static async create(actor, rollData) {
let options = mergeObject(super.defaultOptions, {
classes: ["fvtt-ecryme ecryme-confrontation-dialog"],
dragDrop: [{ dragSelector: ".confront-dice-container", dropSelector: null }],
width: 620, height: 'fit-content', 'z-index': 99999
});
let html = await renderTemplate('systems/fvtt-ecryme/templates/dialogs/confront-dialog.hbs', rollData);
return new EcrymeConfrontDialog(actor, rollData, html, options);
return new EcrymeConfrontDialog(actor, rollData)
}
/* -------------------------------------------- */
constructor(actor, rollData, html, options, close = undefined) {
let conf = {
title: game.i18n.localize("ECRY.ui.confront"),
content: html,
buttons: {
launchConfront: {
icon: '<i class="fas fa-check"></i>',
label: game.i18n.localize("ECRY.ui.launchconfront"),
callback: () => { this.launchConfront().catch("Error when launching Confrontation") }
},
cancel: {
icon: '<i class="fas fa-times"></i>',
label: game.i18n.localize("ECRY.ui.cancel"),
callback: () => { this.close() }
}
},
close: close
async _prepareContext() {
return {
...this.rollData,
config: game.ecryme.config,
buttonDisabled: this.buttonDisabled,
}
super(conf, options);
this.actor = actor;
this.rollData = rollData;
// Ensure button is disabled
setTimeout(function () { $(".launchConfront").attr("disabled", true) }, 180)
}
/* -------------------------------------------- */
async launchConfront() {
let msg = await EcrymeUtility.createChatMessage(this.rollData.alias, "blindroll", {
content: await renderTemplate(`systems/fvtt-ecryme/templates/chat/chat-confrontation-pending.hbs`, this.rollData)
/** Bind drag-drop and form-change listeners once; re-bind DragDrop on each render. */
_onRender(context, options) {
// DragDrop must be re-bound each render because the DOM is replaced
this.#dragDrop.forEach(d => d.bind(this.element))
// Form-change listener is bound once (event delegation survives DOM re-renders)
if (!this._listenersAdded) {
this._listenersAdded = true
this.element.addEventListener('change', this.#onFormChange.bind(this))
}
}
/* -------------------------------------------- */
#onFormChange(event) {
const target = event.target
switch (target.id) {
case 'bonusMalusPerso':
this.rollData.bonusMalusPerso = Number(target.value)
this.computeTotals()
break
case 'roll-specialization':
this.rollData.selectedSpecs = Array.from(target.selectedOptions).map(o => o.value)
this.computeTotals()
break
case 'roll-trait-bonus':
this.rollData.traitsBonusSelected = Array.from(target.selectedOptions).map(o => o.value)
this.computeTotals()
break
case 'roll-trait-malus':
this.rollData.traitsMalusSelected = Array.from(target.selectedOptions).map(o => o.value)
this.computeTotals()
break
case 'roll-select-transcendence':
this.rollData.skillTranscendence = Number(target.value)
this.computeTotals()
break
case 'roll-apply-transcendence':
this.rollData.applyTranscendence = target.value
this.computeTotals()
break
case 'annency-bonus':
this.rollData.annencyBonus = Number(target.value)
break
}
}
// #region Drag-and-Drop
#createDragDropHandlers() {
return this.options.dragDrop.map(d => {
d.permissions = {
dragstart: () => true,
drop: () => true,
}
d.callbacks = {
dragstart: this._onDragStart.bind(this),
dragover: this._onDragOver.bind(this),
drop: this._onDrop.bind(this),
}
return new foundry.applications.ux.DragDrop.implementation(d)
})
console.log("MSG", this.rollData)
msg.setFlag("world", "ecryme-rolldata", this.rollData)
}
/* -------------------------------------------- */
async refreshDice() {
this.rollData.filter = "execution"
let content = await renderTemplate("systems/fvtt-ecryme/templates/dialogs/partial-confront-dice-area.hbs", this.rollData )
content += await renderTemplate("systems/fvtt-ecryme/templates/dialogs/partial-confront-bonus-area.hbs", this.rollData )
$("#confront-execution").html(content)
this.rollData.filter = "preservation"
content = await renderTemplate("systems/fvtt-ecryme/templates/dialogs/partial-confront-dice-area.hbs", this.rollData )
content += await renderTemplate("systems/fvtt-ecryme/templates/dialogs/partial-confront-bonus-area.hbs", this.rollData )
$("#confront-preservation").html(content)
this.rollData.filter = "mainpool"
content = await renderTemplate("systems/fvtt-ecryme/templates/dialogs/partial-confront-dice-area.hbs", this.rollData )
$("#confront-dice-pool").html(content)
content = await renderTemplate("systems/fvtt-ecryme/templates/dialogs/partial-confront-bonus-area.hbs", this.rollData )
$("#confront-bonus-pool").html(content)
}
/* -------------------------------------------- */
async refreshDialog() {
const content = await renderTemplate("systems/fvtt-ecryme/templates/dialogs/confront-dialog.hbs", this.rollData)
this.data.content = content
this.render(true)
let button = this.buttonDisabled
setTimeout(function () { $(".launchConfront").attr("disabled", button) }, 180)
}
/* ------------------ -------------------------- */
_onDragStart(event) {
super._onDragStart(event)
let dragType = $(event.srcElement).data("drag-type")
let diceData = {}
//console.log("DRAGTYPE", dragType)
if (dragType == "dice") {
const target = event.target
const dragType = target.dataset.dragType
let diceData
if (dragType === "dice") {
diceData = {
dragType: "dice",
diceIndex: $(event.srcElement).data("dice-idx"),
diceValue: $(event.srcElement).data("dice-value"),
dragType: "dice",
diceIndex: target.dataset.diceIdx,
diceValue: target.dataset.diceValue,
}
} else {
diceData = {
dragType: "bonus",
bonusIndex: $(event.srcElement).data("bonus-idx"),
bonusValue: 1
dragType: "bonus",
bonusIndex: target.dataset.bonusIdx,
bonusValue: 1,
}
}
event.dataTransfer.setData("text/plain", JSON.stringify(diceData));
event.dataTransfer.setData("text/plain", JSON.stringify(diceData))
}
/* -------------------------------------------- */
_onDragOver(event) {
event.preventDefault()
}
/* -------------------------------------------- */
_onDrop(event) {
let dataJSON = event.dataTransfer.getData('text/plain')
let data = JSON.parse(dataJSON)
if ( data.dragType == "dice") {
let idx = Number(data.diceIndex)
//console.log("DATA", data, event, event.srcElement.className)
if (event.srcElement.className.includes("execution") &&
this.rollData.availableDices.filter(d => d.location == "execution").length < 2) {
let data
try { data = JSON.parse(event.dataTransfer.getData("text/plain")) }
catch (e) { return }
// Walk up the DOM to find a meaningful drop area
const executionArea = event.target.closest('.confront-execution-area')
const preservationArea = event.target.closest('.confront-preservation-area')
const diceList = event.target.closest('.confrontation-dice-list')
const bonusList = event.target.closest('.confrontation-bonus-list')
if (data.dragType === "dice") {
const idx = Number(data.diceIndex)
if (executionArea && this.rollData.availableDices.filter(d => d.location === "execution").length < 2) {
this.rollData.availableDices[idx].location = "execution"
}
if (event.srcElement.className.includes("preservation") &&
this.rollData.availableDices.filter(d => d.location == "preservation").length < 2) {
} else if (preservationArea && this.rollData.availableDices.filter(d => d.location === "preservation").length < 2) {
this.rollData.availableDices[idx].location = "preservation"
}
if (event.srcElement.className.includes("dice-list")) {
} else if (diceList) {
this.rollData.availableDices[idx].location = "mainpool"
}
if (this.rollData.availableDices.filter(d => d.location == "execution").length == 2 && this.rollData.availableDices.filter(d => d.location == "preservation").length == 2) {
this.buttonDisabled = false
} else {
this.buttonDisabled = true
}
} else {
let idx = Number(data.bonusIndex)
if (event.srcElement.className.includes("execution")) {
this.rollData.confrontBonus[idx].location = "execution"
}
if (event.srcElement.className.includes("preservation")) {
this.rollData.confrontBonus[idx].location = "preservation"
}
if (event.srcElement.className.includes("bonus-list")) {
this.rollData.confrontBonus[idx].location = "mainpool"
}
const execCount = this.rollData.availableDices.filter(d => d.location === "execution").length
const presCount = this.rollData.availableDices.filter(d => d.location === "preservation").length
this.buttonDisabled = !(execCount === 2 && presCount === 2)
} else if (data.dragType === "bonus") {
const idx = Number(data.bonusIndex)
if (executionArea) this.rollData.confrontBonus[idx].location = "execution"
else if (preservationArea) this.rollData.confrontBonus[idx].location = "preservation"
else if (bonusList) this.rollData.confrontBonus[idx].location = "mainpool"
}
// Manage total values
this.computeTotals()
}
// #endregion
/* -------------------------------------------- */
processTranscendence() {
// Apply Transcend if needed
if (this.rollData.skillTranscendence > 0) {
if (this.rollData.applyTranscendence == "execution") {
this.rollData.executionTotal += this.rollData.skillTranscendence
if (this.rollData.applyTranscendence === "execution") {
this.rollData.executionTotal += Number(this.rollData.skillTranscendence)
} else {
this.rollData.preservationTotal += this.rollData.skillTranscendence
this.rollData.preservationTotal += Number(this.rollData.skillTranscendence)
}
}
}
/* -------------------------------------------- */
computeTotals() {
let rollData = this.rollData
let actor = game.actors.get(rollData.actorId)
const rollData = this.rollData
const actor = game.actors.get(rollData.actorId)
rollData.executionTotal = rollData.availableDices.filter(d => d.location == "execution").reduce((previous, current) => {
return previous + current.result
}, rollData.skill.value)
rollData.executionTotal = rollData.confrontBonus.filter(d => d.location == "execution").reduce((previous, current) => {
return previous + 1
}, rollData.executionTotal)
rollData.executionTotal = rollData.availableDices
.filter(d => d.location === "execution")
.reduce((acc, d) => acc + d.result, rollData.skill.max)
rollData.executionTotal = rollData.confrontBonus
.filter(d => d.location === "execution")
.reduce((acc) => acc + 1, rollData.executionTotal)
rollData.preservationTotal = rollData.availableDices.filter(d => d.location == "preservation").reduce((previous, current) => {
return previous + current.result
}, rollData.skill.value)
rollData.preservationTotal = rollData.confrontBonus.filter(d => d.location == "preservation").reduce((previous, current) => {
return previous + 1
}, rollData.preservationTotal)
rollData.preservationTotal = rollData.availableDices
.filter(d => d.location === "preservation")
.reduce((acc, d) => acc + d.result, rollData.skill.max)
rollData.preservationTotal = rollData.confrontBonus
.filter(d => d.location === "preservation")
.reduce((acc) => acc + 1, rollData.preservationTotal)
this.processTranscendence()
if (rollData.selectedSpecs && rollData.selectedSpecs.length > 0) {
rollData.spec = duplicate(actor.getSpecialization(rollData.selectedSpecs[0]))
// Specialization
if (rollData.selectedSpecs?.length > 0) {
rollData.spec = foundry.utils.deepClone(actor.getSpecialization(rollData.selectedSpecs[0]))
rollData.specApplied = true
rollData.executionTotal += 2
rollData.executionTotal += 2
rollData.preservationTotal += 2
}
if ( rollData.specApplied && rollData.selectedSpecs.length == 0) {
rollData.spec = undefined
if (rollData.specApplied && rollData.selectedSpecs?.length === 0) {
rollData.spec = undefined
rollData.specApplied = false
}
// Traits bonus/malus
rollData.bonusMalusTraits = 0
for (let t of rollData.traitsBonus) {
t.activated = false
for (const t of rollData.traitsBonus) t.activated = false
for (const t of rollData.traitsMalus) t.activated = false
for (const id of (rollData.traitsBonusSelected ?? [])) {
const trait = rollData.traitsBonus.find(t => t.id === id)
if (trait) { trait.activated = true; rollData.bonusMalusTraits += Number(trait.system.level) }
}
for (let t of rollData.traitsMalus) {
t.activated = false
}
if (rollData.traitsBonusSelected && rollData.traitsBonusSelected.length > 0) {
for (let id of rollData.traitsBonusSelected) {
let trait = rollData.traitsBonus.find(t => t._id == id)
trait.activated = true
rollData.bonusMalusTraits += trait.system.level
}
}
if (rollData.traitsMalusSelected && rollData.traitsMalusSelected.length > 0) {
for (let id of rollData.traitsMalusSelected) {
let trait = rollData.traitsMalus.find(t => t._id == id)
trait.activated = true
rollData.bonusMalusTraits -= trait.system.level
}
for (const id of (rollData.traitsMalusSelected ?? [])) {
const trait = rollData.traitsMalus.find(t => t.id === id)
if (trait) { trait.activated = true; rollData.bonusMalusTraits -= Number(trait.system.level) }
}
rollData.executionTotal += rollData.bonusMalusTraits + rollData.bonusMalusPerso
rollData.preservationTotal += rollData.bonusMalusTraits + rollData.bonusMalusPerso
rollData.executionTotal += Number(rollData.bonusMalusTraits) + Number(rollData.bonusMalusPerso)
rollData.preservationTotal += Number(rollData.bonusMalusTraits) + Number(rollData.bonusMalusPerso)
this.refreshDialog()
this.render()
}
/* -------------------------------------------- */
activateListeners(html) {
super.activateListeners(html);
html.find('#bonusMalusPerso').change((event) => {
this.rollData.bonusMalusPerso = Number(event.currentTarget.value)
this.computeTotals()
async launchConfront() {
const msg = await EcrymeUtility.createChatMessage(this.rollData.alias, "blindroll", {
content: await foundry.applications.handlebars.renderTemplate(
`systems/fvtt-ecryme/templates/chat/chat-confrontation-pending.hbs`, this.rollData
),
})
html.find('#roll-specialization').change((event) => {
this.rollData.selectedSpecs = $('#roll-specialization').val()
this.computeTotals()
EcrymeUtility.blindMessageToGM({
rollData: this.rollData,
template: "systems/fvtt-ecryme/templates/chat/chat-confrontation-pending.hbs",
})
html.find('#roll-trait-bonus').change((event) => {
this.rollData.traitsBonusSelected = $('#roll-trait-bonus').val()
this.computeTotals()
})
html.find('#roll-trait-malus').change((event) => {
this.rollData.traitsMalusSelected = $('#roll-trait-malus').val()
this.computeTotals()
})
html.find('#roll-select-transcendence').change((event) => {
this.rollData.skillTranscendence = Number($('#roll-select-transcendence').val())
this.computeTotals()
})
html.find('#roll-apply-transcendence').change((event) => {
this.rollData.applyTranscendence = $('#roll-apply-transcendence').val()
this.computeTotals()
})
msg.setFlag("world", "ecryme-rolldata", this.rollData)
}
}
/* -------------------------------------------- */
static async #onLaunchConfront(event, target) {
await this.launchConfront()
this.close()
}
static #onCancel(event, target) {
this.close()
}
}
+60 -54
View File
@@ -1,74 +1,80 @@
import { EcrymeUtility } from "../common/ecryme-utility.js";
import {EcrymeConfrontDialog } from "./ecryme-confront-dialog.js";
import { EcrymeConfrontDialog } from "./ecryme-confront-dialog.js";
export class EcrymeConfrontStartDialog extends Dialog {
const { HandlebarsApplicationMixin } = foundry.applications.api
/**
* Confrontation start dialog — Application V2 version.
* Player picks which dice formula to roll (normal / spleen / ideal),
* the dice are rolled and the main EcrymeConfrontDialog is opened.
*/
export class EcrymeConfrontStartDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["fvtt-ecryme", "ecryme-confront-start-dialog"],
position: { width: 540 },
window: { title: "ECRY.ui.confront" },
actions: {
rollNormal: EcrymeConfrontStartDialog.#onRollNormal,
rollSpleen: EcrymeConfrontStartDialog.#onRollSpleen,
rollIdeal: EcrymeConfrontStartDialog.#onRollIdeal,
cancel: EcrymeConfrontStartDialog.#onCancel,
},
}
/** @override */
static PARTS = {
content: { template: "systems/fvtt-ecryme/templates/dialogs/confront-start-dialog.hbs" },
}
/* -------------------------------------------- */
constructor(actor, rollData, options = {}) {
super(options)
this.actor = actor?.token?.actor ?? actor
this.rollData = rollData
}
/* -------------------------------------------- */
static async create(actor, rollData) {
let options = { classes: ["fvtt-ecryme ecryme-confront-dialog"], width: 540, height: 'fit-content', 'z-index': 99999 }
let html = await renderTemplate('systems/fvtt-ecryme/templates/dialogs/confront-start-dialog.hbs', rollData);
return new EcrymeConfrontStartDialog(actor, rollData, html, options);
if (!actor) throw new Error("Ecryme | No actor provided for confront dialog")
if (!rollData) throw new Error("Ecryme | No roll data provided for confront dialog")
if (actor?.token) rollData.tokenId = actor.token.id
return new EcrymeConfrontStartDialog(actor, rollData)
}
/* -------------------------------------------- */
constructor(actor, rollData, html, options, close = undefined) {
let conf = {
title: game.i18n.localize("ECRY.ui.confront"),
content: html,
buttons: {
rollNormal: {
icon: '<i class="fas fa-check"></i>',
label: game.i18n.localize("ECRY.ui.rollnormal"),
callback: () => { this.rollConfront("4d6") }
},
rollSpleen: {
icon: '<i class="fas fa-check"></i>',
label: game.i18n.localize("ECRY.ui.rollspleen"),
callback: () => { this.rollConfront("5d6kl4") }
},
rollIdeal: {
icon: '<i class="fas fa-check"></i>',
label: game.i18n.localize("ECRY.ui.rollideal"),
callback: () => { this.rollConfront("5d6kh4") }
},
cancel: {
icon: '<i class="fas fa-times"></i>',
label: game.i18n.localize("ECRY.ui.cancel"),
callback: () => { this.close() }
}
},
close: close
async _prepareContext() {
return {
...this.rollData,
config: game.ecryme.config,
}
super(conf, options);
this.actor = actor;
this.rollData = rollData;
}
/* -------------------------------------------- */
async rollConfront( diceFormula ) {
// Do the initial roll
let myRoll = new Roll(diceFormula).roll({async: false})
async #rollConfront(diceFormula) {
const myRoll = await new Roll(diceFormula).roll()
await EcrymeUtility.showDiceSoNice(myRoll, game.settings.get("core", "rollMode"))
// Fill the available dice table
let rollData = this.rollData
rollData.roll = duplicate(myRoll)
const rollData = this.rollData
rollData.roll = foundry.utils.deepClone(myRoll)
rollData.availableDices = []
for (let result of myRoll.terms[0].results) {
if ( !result.discarded) {
let resultDup = duplicate(result)
resultDup.location = "mainpool"
rollData.availableDices.push(resultDup)
for (const result of myRoll.terms[0].results) {
if (!result.discarded) {
const dup = foundry.utils.deepClone(result)
dup.location = "mainpool"
rollData.availableDices.push(dup)
}
}
let confrontDialog = await EcrymeConfrontDialog.create(this.actor, rollData)
const confrontDialog = await EcrymeConfrontDialog.create(this.actor, rollData)
confrontDialog.render(true)
this.close()
}
/* -------------------------------------------- */
activateListeners(html) {
super.activateListeners(html);
}
}
static async #onRollNormal(event, target) { await this.#rollConfront("4d6") }
static async #onRollSpleen(event, target) { await this.#rollConfront("5d6kl4") }
static async #onRollIdeal(event, target) { await this.#rollConfront("5d6kh4") }
static #onCancel(event, target) { this.close() }
}
+61 -69
View File
@@ -1,86 +1,78 @@
import { EcrymeUtility } from "../common/ecryme-utility.js";
export class EcrymeRollDialog extends Dialog {
const { HandlebarsApplicationMixin } = foundry.applications.api
/**
* Roll dialog — Application V2 version.
* Reads all form values at roll time (no live tracking needed).
*/
export class EcrymeRollDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["fvtt-ecryme", "ecryme-roll-dialog"],
position: { width: 540 },
window: { title: "ECRY.ui.rolltitle" },
actions: {
roll: EcrymeRollDialog.#onRoll,
cancel: EcrymeRollDialog.#onCancel,
},
}
/** @override */
static PARTS = {
content: { template: "systems/fvtt-ecryme/templates/dialogs/roll-dialog-generic.hbs" },
}
/* -------------------------------------------- */
constructor(actor, rollData, options = {}) {
super(options)
this.actor = actor
this.rollData = rollData
}
/* -------------------------------------------- */
static async create(actor, rollData) {
let options = { classes: ["ecryme-roll-dialog"], width: 540, height: 'fit-content', 'z-index': 99999 }
let html = await renderTemplate('systems/fvtt-ecryme/templates/dialogs/roll-dialog-generic.hbs', rollData);
return new EcrymeRollDialog(actor, rollData, html, options);
return new EcrymeRollDialog(actor, rollData)
}
/* -------------------------------------------- */
constructor(actor, rollData, html, options, close = undefined) {
let conf = {
title: game.i18n.localize("ECRY.ui.rolltitle"),
content: html,
buttons: {
roll: {
icon: '<i class="fas fa-check"></i>',
label: game.i18n.localize("ECRY.ui.roll"),
callback: () => { this.roll() }
},
cancel: {
icon: '<i class="fas fa-times"></i>',
label: game.i18n.localize("ECRY.ui.cancel"),
callback: () => { this.close() }
}
},
close: close
async _prepareContext() {
return {
...this.rollData,
config: game.ecryme.config,
}
super(conf, options);
this.actor = actor;
this.rollData = rollData;
}
/* -------------------------------------------- */
roll() {
/** Read all form values at roll time, then execute. */
static #onRoll(event, target) {
const el = this.element
const bonusEl = el.querySelector('#bonusMalusPerso')
const diffEl = el.querySelector('#roll-difficulty')
const specEl = el.querySelector('#roll-specialization')
const traitBonusEl = el.querySelector('#roll-trait-bonus')
const traitMalusEl = el.querySelector('#roll-trait-malus')
const transcEl = el.querySelector('#roll-select-transcendence')
const spleenEl = el.querySelector('#roll-use-spleen')
const idealEl = el.querySelector('#roll-use-ideal')
if (bonusEl) this.rollData.bonusMalusPerso = Number(bonusEl.value)
if (diffEl) this.rollData.difficulty = Number(diffEl.value) || 0
if (specEl) this.rollData.selectedSpecs = Array.from(specEl.selectedOptions).map(o => o.value)
if (traitBonusEl) this.rollData.traitsBonus = Array.from(traitBonusEl.selectedOptions).map(o => o.value)
if (traitMalusEl) this.rollData.traitsMalus = Array.from(traitMalusEl.selectedOptions).map(o => o.value)
if (transcEl) this.rollData.skillTranscendence = Number(transcEl.value)
if (spleenEl) this.rollData.useSpleen = spleenEl.checked
if (idealEl) this.rollData.useIdeal = idealEl.checked
EcrymeUtility.rollEcryme(this.rollData)
this.close()
}
/* -------------------------------------------- */
async refreshDialog() {
const content = await renderTemplate("systems/fvtt-ecryme/templates/dialogs/roll-dialog-generic.hbs", this.rollData)
this.data.content = content
this.render(true)
static #onCancel(event, target) {
this.close()
}
/* -------------------------------------------- */
activateListeners(html) {
super.activateListeners(html);
var dialog = this;
function onLoad() {
}
$(function () { onLoad(); });
html.find('#bonusMalusPerso').change((event) => {
this.rollData.bonusMalusPerso = Number(event.currentTarget.value)
})
html.find('#roll-difficulty').change((event) => {
this.rollData.difficulty = Number(event.currentTarget.value) || 0
})
html.find('#roll-specialization').change((event) => {
this.rollData.selectedSpecs = $('#roll-specialization').val()
})
html.find('#roll-trait-bonus').change((event) => {
this.rollData.traitsBonus = $('#roll-trait-bonus').val()
})
html.find('#roll-trait-malus').change((event) => {
this.rollData.traitsMalus = $('#roll-trait-malus').val()
})
html.find('#roll-select-transcendence').change((event) => {
this.rollData.skillTranscendence = Number($('#roll-select-transcendence').val())
})
html.find('#roll-use-spleen').change((event) => {
this.rollData.useSpleen = event.currentTarget.checked
})
html.find('#roll-use-ideal').change((event) => {
this.rollData.useIdeal = event.currentTarget.checked
})
}
}
}
+87 -47
View File
@@ -5,18 +5,30 @@
*/
/* -------------------------------------------- */
const ECRYME_WELCOME_MESSAGE_URL = "https://www.uberwald.me/gitea/public/fvtt-ecryme/raw/branch/master/welcome-message-ecryme.html"
/* -------------------------------------------- */
// Import Modules
import { EcrymeActor } from "./actors/ecryme-actor.js";
import { EcrymeItemSheet } from "./items/ecryme-item-sheet.js";
import { EcrymeActorSheet } from "./actors/ecryme-actor-sheet.js";
// import { EcrymeItemSheet } from "./items/ecryme-item-sheet.js";
import {
EcrymeEquipmentSheet,
EcrymeWeaponSheet,
EcrymeTraitSheet,
EcrymeSpecializationSheet,
EcrymeManeuverSheet
} from "./items/sheets/_module.js";
import {
EcrymeActorSheet,
EcrymeAnnencySheet
} from "./actors/sheets/_module.js";
import { EcrymeUtility } from "./common/ecryme-utility.js";
import { EcrymeCombat } from "./app/ecryme-combat.js";
import { EcrymeItem } from "./items/ecryme-item.js";
import { EcrymeHotbar } from "./app/ecryme-hotbar.js"
import { EcrymeCharacterSummary } from "./app/ecryme-summary-app.js"
import { ECRYME_CONFIG } from "./common/ecryme-config.js"
import * as models from "./models/_module.js"
/* -------------------------------------------- */
/* Foundry VTT Initialization */
@@ -25,19 +37,14 @@ import { ECRYME_CONFIG } from "./common/ecryme-config.js"
/************************************************************************************/
Hooks.once("init", async function () {
console.log(`Initializing Ecryme RPG`);
game.system.ecryme = {
config: ECRYME_CONFIG,
EcrymeHotbar
}
console.log(`Initializing Ecryme RPG System`);
/* -------------------------------------------- */
// preload handlebars templates
EcrymeUtility.preloadHandlebarsTemplates();
/* -------------------------------------------- */
// Set an initiative formula for the system
// Set an initiative formula for the system
CONFIG.Combat.initiative = {
formula: "1d6",
decimals: 1
@@ -52,16 +59,38 @@ Hooks.once("init", async function () {
// Define custom Entity classes
CONFIG.Combat.documentClass = EcrymeCombat
CONFIG.Actor.documentClass = EcrymeActor
CONFIG.Actor.dataModels = {
pc: models.EcrymePCDataModel,
npc: models.EcrymeNPCDataModel,
annency: models.EcrymeAnnencyDataModel
}
CONFIG.Item.documentClass = EcrymeItem
CONFIG.Item.dataModels = {
equipment: models.EcrymeEquipmentDataModel,
weapon: models.EcrymeWeaponDataModel,
trait: models.EcrymeTraitDataModel,
specialization: models.EcrymeSpecializationDataModel,
maneuver: models.EcrymeManeuverDataModel
}
game.ecryme = {
config: ECRYME_CONFIG,
models,
EcrymeHotbar
}
/* -------------------------------------------- */
// Register sheet application classes
Actors.unregisterSheet("core", ActorSheet);
Actors.registerSheet("fvtt-ecryme", EcrymeActorSheet, { types: ["pc"], makeDefault: true });
//Actors.registerSheet("fvtt-ecryme", EcrymeNPCSheet, { types: ["pnj"], makeDefault: false });
foundry.documents.collections.Actors.registerSheet("fvtt-ecryme", EcrymeActorSheet, { types: ["pc"], makeDefault: true });
foundry.documents.collections.Actors.registerSheet("fvtt-ecryme", EcrymeActorSheet, { types: ["npc"], makeDefault: true });
foundry.documents.collections.Actors.registerSheet("fvtt-ecryme", EcrymeAnnencySheet, { types: ["annency"], makeDefault: true });
Items.unregisterSheet("core", ItemSheet);
Items.registerSheet("fvtt-ecryme", EcrymeItemSheet, { makeDefault: true });
foundry.documents.collections.Items.registerSheet("fvtt-ecryme", EcrymeEquipmentSheet, { types: ["equipment"], makeDefault: true });
foundry.documents.collections.Items.registerSheet("fvtt-ecryme", EcrymeWeaponSheet, { types: ["weapon"], makeDefault: true });
foundry.documents.collections.Items.registerSheet("fvtt-ecryme", EcrymeTraitSheet, { types: ["trait"], makeDefault: true });
foundry.documents.collections.Items.registerSheet("fvtt-ecryme", EcrymeSpecializationSheet, { types: ["specialization"], makeDefault: true });
foundry.documents.collections.Items.registerSheet("fvtt-ecryme", EcrymeManeuverSheet, { types: ["maneuver"], makeDefault: true });
EcrymeUtility.init()
@@ -70,38 +99,38 @@ Hooks.once("init", async function () {
/* -------------------------------------------- */
function welcomeMessage() {
if (game.user.isGM) {
ChatMessage.create({
user: game.user.id,
whisper: [game.user.id],
content: `<div id="welcome-message-ecryme"><span class="rdd-roll-part">
<strong>Bienvenu dans Ecryme !</strong>` });
// Try to fetch the welcome message from the github repo "welcome-message-ecryme.html"
fetch(ECRYME_WELCOME_MESSAGE_URL)
.then(response => response.text())
.then(html => {
//console.log("Fetched welcome message:", html);
ChatMessage.create({
user: game.user.id,
whisper: [game.user.id],
content: html
});
})
.catch(error => {
console.error("Error fetching welcome message:", error);
ChatMessage.create({
user: game.user.id,
whisper: [game.user.id],
content: "<b>Bienvenue dans Ecryme RPG !</b><br>Visitez le site officiel pour plus d'informations."
});
});
}
}
/* -------------------------------------------- */
// Register world usage statistics
function registerUsageCount(registerKey) {
if (game.user.isGM) {
game.settings.register(registerKey, "world-key", {
name: "Unique world key",
scope: "world",
config: false,
default: "",
type: String
});
let worldKey = game.settings.get(registerKey, "world-key")
if (worldKey == undefined || worldKey == "") {
worldKey = randomID(32)
game.settings.set(registerKey, "world-key", worldKey)
}
// Simple API counter
let regURL = `https://www.uberwald.me/fvtt_appcount/count.php?name="${registerKey}"&worldKey="${worldKey}"&version="${game.release.generation}.${game.release.build}"&system="${game.system.id}"&systemversion="${game.system.version}"`
//$.ajaxSetup({
//headers: { 'Access-Control-Allow-Origin': '*' }
//})
$.ajax(regURL)
async function importDefaultScene() {
let exists = game.scenes.find(j => j.name == "Landing page 1");
if (!exists) {
const scenes = await EcrymeUtility.loadCompendium("fvtt-ecryme.scenes")
let newDocuments = scenes.filter(i => i.name == "Landing page 1");
await game.scenes.documentClass.create(newDocuments);
game.scenes.find(i => i.name == "Landing page 1").activate();
}
}
/* -------------------------------------------- */
@@ -114,17 +143,29 @@ Hooks.once("ready", function () {
ui.notifications.info("Attention ! Aucun personnage relié au joueur !");
ChatMessage.create({
content: "<b>WARNING</b> Le joueur " + game.user.name + " n'est pas relié à un personnage !",
user: game.user._id
user: game.user.id
});
}
registerUsageCount(game.system.id)
import("https://www.uberwald.me/fvtt_appcount/count-class-ready.js").then(moduleCounter => {
console.log("ClassCounter loaded", moduleCounter)
moduleCounter.ClassCounter.registerUsageCount()
}).catch(err =>
console.log("No stats available, giving up.")
)
welcomeMessage();
EcrymeUtility.ready()
EcrymeCharacterSummary.ready()
EcrymeUtility.ready();
EcrymeCharacterSummary.ready();
importDefaultScene();
})
/* -------------------------------------------- */
Hooks.once("babele.init", (babele) => {
console.log("Initializing Babele translations")
babele.setSystemTranslationsDir("translated");
});
/* -------------------------------------------- */
/* Foundry VTT Initialization */
@@ -133,10 +174,9 @@ Hooks.on("chatMessage", (html, content, msg) => {
if (content[0] == '/') {
let regExp = /(\S+)/g;
let commands = content.match(regExp);
if (game.system.ecryme.commands.processChatCommand(commands, content, msg)) {
if (game.ecryme.commands.processChatCommand(commands, content, msg)) {
return false;
}
}
return true;
});
+13 -26
View File
@@ -4,32 +4,19 @@ import { EcrymeUtility } from "../common/ecryme-utility.js";
* Extend the basic ItemSheet with some very simple modifications
* @extends {ItemSheet}
*/
export class EcrymeItemSheet extends ItemSheet {
export class EcrymeItemSheet extends foundry.appv1.sheets.ItemSheet {
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
return {
...super.defaultOptions,
classes: ["fvtt-ecryme", "sheet", "item"],
template: "systems/fvtt-ecryme/templates/item-sheet.hbs",
dragDrop: [{ dragSelector: null, dropSelector: null }],
width: 620,
height: 580,
tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "description" }]
});
}
/* -------------------------------------------- */
_getHeaderButtons() {
let buttons = super._getHeaderButtons();
// Add "Post to chat" button
// We previously restricted this to GM and editable items only. If you ever find this comment because it broke something: eh, sorry!
buttons.unshift(
{
class: "post",
icon: "fas fa-comment",
onclick: ev => { }
})
return buttons
};
}
/* -------------------------------------------- */
@@ -56,20 +43,20 @@ export class EcrymeItemSheet extends ItemSheet {
name: this.object.name,
editable: this.isEditable,
cssClass: this.isEditable ? "editable" : "locked",
system: duplicate(this.object.system),
config: duplicate(game.system.ecryme.config),
system: foundry.utils.deepClone(this.object.system),
config: foundry.utils.deepClone(game.ecryme.config),
limited: this.object.limited,
options: this.options,
owner: this.document.isOwner,
description: await TextEditor.enrichHTML(this.object.system.description, { async: true }),
notes: await TextEditor.enrichHTML(this.object.system.notes, { async: true }),
description: await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.object.system.description, { async: true }),
notes: await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.object.system.notes, { async: true }),
isGM: game.user.isGM
}
if ( this.object.type == "archetype") {
formData.tarots = EcrymeUtility.getTarots()
}
this.options.editable = !(this.object.origin == "embeddedItem");
console.log("ITEM DATA", formData, this);
return formData;
@@ -89,7 +76,7 @@ export class EcrymeItemSheet extends ItemSheet {
/* -------------------------------------------- */
postItem() {
let chatData = duplicate(this.item)
let chatData = foundry.utils.deepClone(this.item)
if (this.actor) {
chatData.actor = { id: this.actor.id };
}
@@ -104,7 +91,7 @@ export class EcrymeItemSheet extends ItemSheet {
payload: chatData,
});
renderTemplate('systems/Ecryme/templates/post-item.html', chatData).then(html => {
foundry.applications.handlebars.renderTemplate('systems/fvtt-ecryme/templates/post-item.html', chatData).then(html => {
let chatOptions = EcrymeUtility.chatDataSetup(html);
ChatMessage.create(chatOptions)
});
@@ -115,7 +102,7 @@ export class EcrymeItemSheet extends ItemSheet {
let levelIndex = Number($(ev.currentTarget).parents(".item").data("level-index"))
let choiceIndex = Number($(ev.currentTarget).parents(".item").data("choice-index"))
let featureId = $(ev.currentTarget).parents(".item").data("feature-id")
let itemData = this.object.system.levels[levelIndex].choices[choiceIndex].features[featureId]
if (itemData.name != 'None') {
@@ -154,7 +141,7 @@ export class EcrymeItemSheet extends ItemSheet {
// Update Inventory Item
html.find('.item-edit').click(ev => {
const li = $(ev.currentTarget).parents(".item");
const item = this.object.options.actor.getOwnedItem(li.data("item-id"));
const item = this.actor?.items.get(li.data("item-id"));
item.sheet.render(true);
});
+6
View File
@@ -0,0 +1,6 @@
export { default as EcrymeBaseItemSheet } from "./base-item-sheet.js"
export { default as EcrymeEquipmentSheet } from "./equipment-sheet.js"
export { default as EcrymeWeaponSheet } from "./weapon-sheet.js"
export { default as EcrymeTraitSheet } from "./trait-sheet.js"
export { default as EcrymeSpecializationSheet } from "./specialization-sheet.js"
export { default as EcrymeManeuverSheet } from "./maneuver-sheet.js"
+103
View File
@@ -0,0 +1,103 @@
const { HandlebarsApplicationMixin } = foundry.applications.api
/**
* Base item sheet for Ecryme using Application V2.
* Subclasses must define static PARTS including header, tabs, description, and optionally details.
*/
export default class EcrymeBaseItemSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ItemSheetV2) {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["fvtt-ecryme", "item"],
position: {
width: 520,
height: "auto",
},
form: {
submitOnChange: true,
},
window: {
resizable: true,
},
dragDrop: [{ dragSelector: "[data-drag]", dropSelector: null }],
actions: {
editImage: EcrymeBaseItemSheet.#onEditImage,
},
}
/** Active tab group tracking */
tabGroups = {
primary: "description",
}
/**
* Build the tabs definition, adding a "details" tab only if the subclass has a "details" PART.
* @returns {Record<string, object>}
*/
_getTabs() {
const tabs = {
description: { id: "description", group: "primary", label: "ECRY.ui.description" },
}
if (this.constructor.PARTS?.details) {
tabs.details = { id: "details", group: "primary", label: "ECRY.ui.details" }
}
for (const tab of Object.values(tabs)) {
tab.active = this.tabGroups[tab.group] === tab.id
tab.cssClass = tab.active ? "active" : ""
}
return tabs
}
/** @override */
async _prepareContext() {
const context = {
fields: this.document.schema.fields,
systemFields: this.document.system.schema.fields,
item: this.document,
system: this.document.system,
source: this.document.toObject(),
config: game.ecryme.config,
isEditable: this.isEditable,
tabs: this._getTabs(),
}
return context
}
/** @override */
async _preparePartContext(partId, context) {
context = await super._preparePartContext(partId, context)
if (partId === "description") {
context.tab = context.tabs.description
context.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
this.document.system.description, { async: true }
)
}
return context
}
_canDragStart(selector) { return this.isEditable }
_canDragDrop(selector) { return this.isEditable && this.document.isOwner }
_onDragStart(event) {}
_onDragOver(event) {}
async _onDrop(event) {}
// #endregion
// #region Actions
static async #onEditImage(event, target) {
const attr = target.dataset.edit
const current = foundry.utils.getProperty(this.document, attr)
const { img } = this.document.constructor.getDefaultArtwork?.(this.document.toObject()) ?? {}
const fp = new FilePicker({
current,
type: "image",
redirectToRoot: img ? [img] : [],
callback: (path) => {
this.document.update({ [attr]: path })
},
top: this.position.top + 40,
left: this.position.left + 10,
})
return fp.browse()
}
// #endregion
}
+24
View File
@@ -0,0 +1,24 @@
import EcrymeBaseItemSheet from "./base-item-sheet.js"
export default class EcrymeEquipmentSheet extends EcrymeBaseItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["equipment"],
position: { width: 520 },
}
/** @override */
static PARTS = {
header: { template: "systems/fvtt-ecryme/templates/items/partials/item-header.hbs" },
tabs: { template: "templates/generic/tab-navigation.hbs" },
description: { template: "systems/fvtt-ecryme/templates/items/partials/item-description.hbs" },
details: { template: "systems/fvtt-ecryme/templates/items/item-equipment-details.hbs" },
}
/** @override */
async _preparePartContext(partId, context) {
context = await super._preparePartContext(partId, context)
if (partId === "details") context.tab = context.tabs.details
return context
}
}
+24
View File
@@ -0,0 +1,24 @@
import EcrymeBaseItemSheet from "./base-item-sheet.js"
export default class EcrymeManeuverSheet extends EcrymeBaseItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["maneuver"],
position: { width: 520 },
}
/** @override */
static PARTS = {
header: { template: "systems/fvtt-ecryme/templates/items/partials/item-header.hbs" },
tabs: { template: "templates/generic/tab-navigation.hbs" },
description: { template: "systems/fvtt-ecryme/templates/items/partials/item-description.hbs" },
details: { template: "systems/fvtt-ecryme/templates/items/item-maneuver-details.hbs" },
}
/** @override */
async _preparePartContext(partId, context) {
context = await super._preparePartContext(partId, context)
if (partId === "details") context.tab = context.tabs.details
return context
}
}
@@ -0,0 +1,24 @@
import EcrymeBaseItemSheet from "./base-item-sheet.js"
export default class EcrymeSpecializationSheet extends EcrymeBaseItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["specialization"],
position: { width: 520 },
}
/** @override */
static PARTS = {
header: { template: "systems/fvtt-ecryme/templates/items/partials/item-header.hbs" },
tabs: { template: "templates/generic/tab-navigation.hbs" },
description: { template: "systems/fvtt-ecryme/templates/items/partials/item-description.hbs" },
details: { template: "systems/fvtt-ecryme/templates/items/item-specialization-details.hbs" },
}
/** @override */
async _preparePartContext(partId, context) {
context = await super._preparePartContext(partId, context)
if (partId === "details") context.tab = context.tabs.details
return context
}
}
+24
View File
@@ -0,0 +1,24 @@
import EcrymeBaseItemSheet from "./base-item-sheet.js"
export default class EcrymeTraitSheet extends EcrymeBaseItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["trait"],
position: { width: 520 },
}
/** @override */
static PARTS = {
header: { template: "systems/fvtt-ecryme/templates/items/partials/item-header.hbs" },
tabs: { template: "templates/generic/tab-navigation.hbs" },
description: { template: "systems/fvtt-ecryme/templates/items/partials/item-description.hbs" },
details: { template: "systems/fvtt-ecryme/templates/items/item-trait-details.hbs" },
}
/** @override */
async _preparePartContext(partId, context) {
context = await super._preparePartContext(partId, context)
if (partId === "details") context.tab = context.tabs.details
return context
}
}
+24
View File
@@ -0,0 +1,24 @@
import EcrymeBaseItemSheet from "./base-item-sheet.js"
export default class EcrymeWeaponSheet extends EcrymeBaseItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["weapon"],
position: { width: 540 },
}
/** @override */
static PARTS = {
header: { template: "systems/fvtt-ecryme/templates/items/partials/item-header.hbs" },
tabs: { template: "templates/generic/tab-navigation.hbs" },
description: { template: "systems/fvtt-ecryme/templates/items/partials/item-description.hbs" },
details: { template: "systems/fvtt-ecryme/templates/items/item-weapon-details.hbs" },
}
/** @override */
async _preparePartContext(partId, context) {
context = await super._preparePartContext(partId, context)
if (partId === "details") context.tab = context.tabs.details
return context
}
}
+1
View File
@@ -0,0 +1 @@
# This file ensures the models directory is tracked by git
+85
View File
@@ -0,0 +1,85 @@
# DataModels Ecryme
## Vue d'ensemble
Ce dossier contient les DataModels pour le système Ecryme. Les DataModels sont la méthode moderne de Foundry VTT (v10+) pour définir les structures de données des acteurs et des items.
## Migration depuis template.json
Le système Ecryme a été migré de l'ancien système `template.json` vers les DataModels. Le fichier `template.json` est conservé pour référence mais est maintenant marqué comme deprecated.
## Structure des fichiers
### Modèles d'Items
- **equipment.js** - Équipements génériques
- **weapon.js** - Armes (mêlée et distance)
- **trait.js** - Traits de personnage
- **specialization.js** - Spécialisations de compétences
- **maneuver.js** - Manœuvres de combat
### Modèles d'Acteurs
- **pc.js** - Personnages joueurs (PC)
- **npc.js** - Personnages non-joueurs (NPC)
- **annency.js** - Annency (acteurs spéciaux)
### Fichier d'index
- **_module.js** - Centralise tous les exports des DataModels
## Utilisation
Les DataModels sont automatiquement enregistrés dans `CONFIG.Actor.dataModels` et `CONFIG.Item.dataModels` lors de l'initialisation du système dans `ecryme-main.js`.
### Accès aux données
Dans les acteurs et items, les données du système sont accessibles via `actor.system` ou `item.system` :
```javascript
// Exemple avec un PC
const athletics = actor.system.skills.physical.skilllist.athletics.value;
// Exemple avec une arme
const weaponType = item.system.weapontype;
```
## Avantages des DataModels
1. **Validation automatique** - Les types de champs sont vérifiés automatiquement
2. **Valeurs par défaut** - Chaque champ a une valeur initiale définie
3. **Type safety** - Meilleure autocomplete dans les IDEs
4. **Performance** - Optimisation interne de Foundry VTT
5. **Maintenance** - Code plus propre et organisé
## Compatibilité
Les DataModels sont rétrocompatibles avec les données existantes. Les acteurs et items créés avec l'ancien système `template.json` seront automatiquement migrés vers les nouveaux DataModels lors de leur chargement.
## Développement
Pour ajouter un nouveau type d'acteur ou d'item :
1. Créer un nouveau fichier DataModel dans ce dossier
2. Définir le schema avec `static defineSchema()`
3. Exporter le modèle dans `_module.js`
4. Enregistrer le modèle dans `ecryme-main.js` (CONFIG.Actor.dataModels ou CONFIG.Item.dataModels)
### Exemple minimal
```javascript
export default class MyNewItemDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
description: new fields.HTMLField({ initial: "" }),
value: new fields.NumberField({ initial: 0, integer: true, min: 0 })
};
}
}
```
## Documentation Foundry VTT
Pour plus d'informations sur les DataModels :
https://foundryvtt.com/article/system-data-models/
+16
View File
@@ -0,0 +1,16 @@
/**
* Index des DataModels pour Ecryme
* Ce fichier centralise tous les exports des modèles de données
*/
// Modèles d'items (uniquement les types définis dans template.json types array)
export { default as EcrymeEquipmentDataModel } from './equipment.js';
export { default as EcrymeWeaponDataModel } from './weapon.js';
export { default as EcrymeTraitDataModel } from './trait.js';
export { default as EcrymeSpecializationDataModel } from './specialization.js';
export { default as EcrymeManeuverDataModel } from './maneuver.js';
// Modèles d'acteurs
export { default as EcrymePCDataModel } from './pc.js';
export { default as EcrymeNPCDataModel } from './npc.js';
export { default as EcrymeAnnencyDataModel } from './annency.js';
+32
View File
@@ -0,0 +1,32 @@
/**
* Data model pour les Annency (acteurs)
*/
export default class EcrymeAnnencyDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
base: new fields.SchemaField({
iscollective: new fields.BooleanField({ initial: false }),
ismultiple: new fields.BooleanField({ initial: false }),
characters: new fields.ArrayField(new fields.StringField(), { initial: [] }),
location: new fields.SchemaField({
"1": new fields.StringField({ initial: "" }),
"2": new fields.StringField({ initial: "" }),
"3": new fields.StringField({ initial: "" }),
"4": new fields.StringField({ initial: "" }),
"5": new fields.StringField({ initial: "" })
}),
description: new fields.HTMLField({ initial: "" }),
enhancements: new fields.StringField({ initial: "" })
}),
boheme: new fields.SchemaField({
name: new fields.StringField({ initial: "" }),
ideals: new fields.StringField({ initial: "" }),
politic: new fields.StringField({ initial: "" }),
description: new fields.HTMLField({ initial: "" })
})
};
}
}
+15
View File
@@ -0,0 +1,15 @@
/**
* Data model pour les équipements
*/
export default class EcrymeEquipmentDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
description: new fields.HTMLField({ initial: "" }),
weight: new fields.NumberField({ initial: 0, integer: true, min: 0 }),
cost: new fields.NumberField({ initial: 0, integer: true, min: 0 }),
costunit: new fields.StringField({ initial: "" }),
quantity: new fields.NumberField({ initial: 1, integer: true, min: 0 })
};
}
}
+11
View File
@@ -0,0 +1,11 @@
/**
* Data model pour les manœuvres
*/
export default class EcrymeManeuverDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
description: new fields.HTMLField({ initial: "" })
};
}
}
+9
View File
@@ -0,0 +1,9 @@
/**
* Data model pour les PNJs (NPC)
* Utilise la même structure que les PC
*/
import EcrymePCDataModel from './pc.js';
export default class EcrymeNPCDataModel extends EcrymePCDataModel {
// Les NPCs utilisent exactement la même structure que les PCs
}
+129
View File
@@ -0,0 +1,129 @@
/**
* Data model pour les personnages joueurs (PC)
*/
export default class EcrymePCDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
// Template biodata
const biodataSchema = {
age: new fields.StringField({ initial: "" }),
size: new fields.StringField({ initial: "" }),
lieunaissance: new fields.StringField({ initial: "" }),
nationalite: new fields.StringField({ initial: "" }),
profession: new fields.StringField({ initial: "" }),
residence: new fields.StringField({ initial: "" }),
milieusocial: new fields.StringField({ initial: "" }),
poids: new fields.StringField({ initial: "" }),
cheveux: new fields.StringField({ initial: "" }),
sexe: new fields.StringField({ initial: "" }),
yeux: new fields.StringField({ initial: "" }),
enfance: new fields.StringField({ initial: "" }),
description: new fields.HTMLField({ initial: "" }),
gmnotes: new fields.HTMLField({ initial: "" })
};
// Helper function to create a skill schema (creates new instances each time)
const createSkillSchema = (keyValue, nameValue, maxValue = 0) => ({
key: new fields.StringField({ initial: keyValue }),
name: new fields.StringField({ initial: nameValue }),
value: new fields.NumberField({ initial: 0, integer: true, min: 0 }),
max: new fields.NumberField({ initial: maxValue, integer: true, min: 0 })
});
// Skills categories
const physicalSkills = {
athletics: new fields.SchemaField(createSkillSchema("athletics", "ECRY.ui.athletics")),
driving: new fields.SchemaField(createSkillSchema("driving", "ECRY.ui.driving")),
fencing: new fields.SchemaField(createSkillSchema("fencing", "ECRY.ui.fencing")),
brawling: new fields.SchemaField(createSkillSchema("brawling", "ECRY.ui.brawling")),
shooting: new fields.SchemaField(createSkillSchema("shooting", "ECRY.ui.shooting"))
};
const mentalSkills = {
anthropomecanology: new fields.SchemaField(createSkillSchema("anthropomecanology", "ECRY.ui.anthropomecanology")),
ecrymology: new fields.SchemaField(createSkillSchema("ecrymology", "ECRY.ui.ecrymology")),
traumatology: new fields.SchemaField(createSkillSchema("traumatology", "ECRY.ui.traumatology")),
traversology: new fields.SchemaField(createSkillSchema("traversology", "ECRY.ui.traversology")),
urbatechnology: new fields.SchemaField(createSkillSchema("urbatechnology", "ECRY.ui.urbatechnology"))
};
const socialSkills = {
quibbling: new fields.SchemaField(createSkillSchema("quibbling", "ECRY.ui.quibbling")),
creativity: new fields.SchemaField(createSkillSchema("creativity", "ECRY.ui.creativity")),
loquacity: new fields.SchemaField(createSkillSchema("loquacity", "ECRY.ui.loquacity")),
guile: new fields.SchemaField(createSkillSchema("guile", "ECRY.ui.guile")),
performance: new fields.SchemaField(createSkillSchema("performance", "ECRY.ui.performance"))
};
// Helper function to create a cephaly skill schema
const createCephalySkillSchema = (nameValue) => ({
name: new fields.StringField({ initial: nameValue }),
value: new fields.NumberField({ initial: 0, integer: true, min: 0 }),
max: new fields.NumberField({ initial: 0, integer: true })
});
// Cephaly skills
const cephalySkills = {
elegy: new fields.SchemaField(createCephalySkillSchema("ECRY.ui.elegy")),
entelechy: new fields.SchemaField(createCephalySkillSchema("ECRY.ui.entelechy")),
mekany: new fields.SchemaField(createCephalySkillSchema("ECRY.ui.mekany")),
psyche: new fields.SchemaField(createCephalySkillSchema("ECRY.ui.psyche")),
scoria: new fields.SchemaField(createCephalySkillSchema("ECRY.ui.scoria"))
};
// Helper function to create an impact schema
const createImpactSchema = () => ({
superficial: new fields.NumberField({ initial: 0, integer: true, min: 0 }),
light: new fields.NumberField({ initial: 0, integer: true, min: 0 }),
serious: new fields.NumberField({ initial: 0, integer: true, min: 0 }),
major: new fields.NumberField({ initial: 0, integer: true, min: 0 })
});
return {
// Biodata
biodata: new fields.SchemaField(biodataSchema),
// Core data
subactors: new fields.ArrayField(new fields.StringField(), { initial: [] }),
equipmentfree: new fields.StringField({ initial: "" }),
// Skills
skills: new fields.SchemaField({
physical: new fields.SchemaField({
name: new fields.StringField({ initial: "ECRY.ui.physical" }),
pnjvalue: new fields.NumberField({ initial: 0, integer: true }),
skilllist: new fields.SchemaField(physicalSkills)
}),
mental: new fields.SchemaField({
name: new fields.StringField({ initial: "ECRY.ui.mental" }),
pnjvalue: new fields.NumberField({ initial: 0, integer: true }),
skilllist: new fields.SchemaField(mentalSkills)
}),
social: new fields.SchemaField({
name: new fields.StringField({ initial: "ECRY.ui.social" }),
pnjvalue: new fields.NumberField({ initial: 0, integer: true }),
skilllist: new fields.SchemaField(socialSkills)
})
}),
// Impacts
impacts: new fields.SchemaField({
physical: new fields.SchemaField(createImpactSchema()),
mental: new fields.SchemaField(createImpactSchema()),
social: new fields.SchemaField(createImpactSchema())
}),
// Cephaly
cephaly: new fields.SchemaField({
name: new fields.StringField({ initial: "ECRY.ui.cephaly" }),
skilllist: new fields.SchemaField(cephalySkills)
}),
// Internals
internals: new fields.SchemaField({
confrontbonus: new fields.NumberField({ initial: 0, integer: true })
})
};
}
}
+13
View File
@@ -0,0 +1,13 @@
/**
* Data model pour les spécialisations
*/
export default class EcrymeSpecializationDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
description: new fields.HTMLField({ initial: "" }),
bonus: new fields.NumberField({ initial: 2, integer: true }),
skillkey: new fields.StringField({ initial: "" })
};
}
}
+13
View File
@@ -0,0 +1,13 @@
/**
* Data model pour les traits
*/
export default class EcrymeTraitDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
description: new fields.HTMLField({ initial: "" }),
traitype: new fields.StringField({ initial: "normal" }),
level: new fields.NumberField({ initial: 1, integer: true, min: 1 })
};
}
}
+16
View File
@@ -0,0 +1,16 @@
/**
* Data model pour les armes
*/
export default class EcrymeWeaponDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
description: new fields.HTMLField({ initial: "" }),
weight: new fields.NumberField({ initial: 0, integer: true, min: 0 }),
cost: new fields.NumberField({ initial: 0, integer: true, min: 0 }),
costunit: new fields.StringField({ initial: "" }),
weapontype: new fields.StringField({ initial: "melee", choices: { melee: "Mêlée", ranged: "Distance" } }),
effect: new fields.NumberField({ initial: 0, integer: true })
};
}
}
+2167
View File
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
{
"name": "fvtt-ecryme",
"private": true,
"version": "1.0.0",
"description": "Ecryme RPG system for Foundry Virtual TableTop",
"author": "LeRatierBretonnien",
"license": "CC-BY-NC-SA-4.0",
"main": "gulpfile.js",
"devDependencies": {
"gulp": "^5.0.0",
"gulp-less": "^5.0.0",
"less": "^4.1.3"
},
"scripts": {
"build": "gulp css",
"watch": "gulp"
},
"repository": {
"type": "git",
"url": "https://www.uberwald.me/gitea/uberwald/fvtt-ecryme.git"
}
}
Binary file not shown.
View File
+1
View File
@@ -0,0 +1 @@
MANIFEST-000403
View File
+7
View File
@@ -0,0 +1,7 @@
2026/07/24-08:30:57.764367 7fb397fff6c0 Recovering log #401
2026/07/24-08:30:57.820757 7fb397fff6c0 Delete type=3 #399
2026/07/24-08:30:57.820848 7fb397fff6c0 Delete type=0 #401
2026/07/24-08:59:46.956225 7fb394b4f6c0 Level-0 table #406: started
2026/07/24-08:59:46.956258 7fb394b4f6c0 Level-0 table #406: 0 bytes OK
2026/07/24-08:59:46.963004 7fb394b4f6c0 Delete type=0 #404
2026/07/24-08:59:46.977171 7fb394b4f6c0 Manual compaction at level-0 from '!folders!1GrTlI1xWvaxdKRI' @ 72057594037927935 : 1 .. '!items!zs7krgXhDRndtqbl' @ 0 : 0; will stop at (end)
+7
View File
@@ -0,0 +1,7 @@
2026/07/23-23:29:38.540622 7fb3977fe6c0 Recovering log #397
2026/07/23-23:29:38.552087 7fb3977fe6c0 Delete type=3 #395
2026/07/23-23:29:38.552145 7fb3977fe6c0 Delete type=0 #397
2026/07/24-08:14:15.212953 7fb394b4f6c0 Level-0 table #402: started
2026/07/24-08:14:15.212989 7fb394b4f6c0 Level-0 table #402: 0 bytes OK
2026/07/24-08:14:15.263841 7fb394b4f6c0 Delete type=0 #400
2026/07/24-08:14:15.341416 7fb394b4f6c0 Manual compaction at level-0 from '!folders!1GrTlI1xWvaxdKRI' @ 72057594037927935 : 1 .. '!items!zs7krgXhDRndtqbl' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
View File
+1
View File
@@ -0,0 +1 @@
MANIFEST-000336
View File
+8
View File
@@ -0,0 +1,8 @@
2026/07/24-08:30:58.009658 7fb396ffd6c0 Recovering log #334
2026/07/24-08:30:58.067952 7fb396ffd6c0 Delete type=3 #332
2026/07/24-08:30:58.068041 7fb396ffd6c0 Delete type=0 #334
2026/07/24-08:59:46.969755 7fb394b4f6c0 Level-0 table #339: started
2026/07/24-08:59:46.969790 7fb394b4f6c0 Level-0 table #339: 0 bytes OK
2026/07/24-08:59:46.976981 7fb394b4f6c0 Delete type=0 #337
2026/07/24-08:59:46.977429 7fb394b4f6c0 Manual compaction at level-0 from '!journal!wooTFYjEwh83FwgT' @ 72057594037927935 : 1 .. '!journal.pages!wooTFYjEwh83FwgT.xhc7hqoL8kdW6lrD' @ 0 : 0; will stop at (end)
2026/07/24-08:59:46.990444 7fb394b4f6c0 Manual compaction at level-1 from '!journal!wooTFYjEwh83FwgT' @ 72057594037927935 : 1 .. '!journal.pages!wooTFYjEwh83FwgT.xhc7hqoL8kdW6lrD' @ 0 : 0; will stop at (end)
+8
View File
@@ -0,0 +1,8 @@
2026/07/23-23:29:38.605125 7fb397fff6c0 Recovering log #330
2026/07/23-23:29:38.614967 7fb397fff6c0 Delete type=3 #328
2026/07/23-23:29:38.615053 7fb397fff6c0 Delete type=0 #330
2026/07/24-08:14:15.385470 7fb394b4f6c0 Level-0 table #335: started
2026/07/24-08:14:15.385508 7fb394b4f6c0 Level-0 table #335: 0 bytes OK
2026/07/24-08:14:15.417431 7fb394b4f6c0 Delete type=0 #333
2026/07/24-08:14:15.479154 7fb394b4f6c0 Manual compaction at level-0 from '!journal!wooTFYjEwh83FwgT' @ 72057594037927935 : 1 .. '!journal.pages!wooTFYjEwh83FwgT.xhc7hqoL8kdW6lrD' @ 0 : 0; will stop at (end)
2026/07/24-08:14:15.542162 7fb394b4f6c0 Manual compaction at level-1 from '!journal!wooTFYjEwh83FwgT' @ 72057594037927935 : 1 .. '!journal.pages!wooTFYjEwh83FwgT.xhc7hqoL8kdW6lrD' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
View File
+1
View File
@@ -0,0 +1 @@
MANIFEST-000401
View File
+7
View File
@@ -0,0 +1,7 @@
2026/07/24-08:30:57.946392 7fb3977fe6c0 Recovering log #399
2026/07/24-08:30:58.006024 7fb3977fe6c0 Delete type=3 #397
2026/07/24-08:30:58.006152 7fb3977fe6c0 Delete type=0 #399
2026/07/24-08:59:46.984189 7fb394b4f6c0 Level-0 table #404: started
2026/07/24-08:59:46.984218 7fb394b4f6c0 Level-0 table #404: 0 bytes OK
2026/07/24-08:59:46.990285 7fb394b4f6c0 Delete type=0 #402
2026/07/24-08:59:47.001933 7fb394b4f6c0 Manual compaction at level-0 from '!items!13IYF6BPUTivFZzB' @ 72057594037927935 : 1 .. '!items!oSutlbe9wyBZccmf' @ 0 : 0; will stop at (end)
+7
View File
@@ -0,0 +1,7 @@
2026/07/23-23:29:38.590323 7fb3977fe6c0 Recovering log #395
2026/07/23-23:29:38.602055 7fb3977fe6c0 Delete type=3 #393
2026/07/23-23:29:38.602139 7fb3977fe6c0 Delete type=0 #395
2026/07/24-08:14:15.341819 7fb394b4f6c0 Level-0 table #400: started
2026/07/24-08:14:15.341876 7fb394b4f6c0 Level-0 table #400: 0 bytes OK
2026/07/24-08:14:15.385247 7fb394b4f6c0 Delete type=0 #398
2026/07/24-08:14:15.479122 7fb394b4f6c0 Manual compaction at level-0 from '!items!13IYF6BPUTivFZzB' @ 72057594037927935 : 1 .. '!items!oSutlbe9wyBZccmf' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
View File
+1
View File
@@ -0,0 +1 @@
MANIFEST-000289
View File
+8
View File
@@ -0,0 +1,8 @@
2026/07/24-08:30:57.885965 7fb397fff6c0 Recovering log #287
2026/07/24-08:30:57.939362 7fb397fff6c0 Delete type=3 #285
2026/07/24-08:30:57.939470 7fb397fff6c0 Delete type=0 #287
2026/07/24-08:59:46.948977 7fb394b4f6c0 Level-0 table #292: started
2026/07/24-08:59:46.949053 7fb394b4f6c0 Level-0 table #292: 0 bytes OK
2026/07/24-08:59:46.956075 7fb394b4f6c0 Delete type=0 #290
2026/07/24-08:59:46.977151 7fb394b4f6c0 Manual compaction at level-0 from '!scenes!DDibQQLAvyIq9y09' @ 72057594037927935 : 1 .. '!scenes.levels!zvY1RwBhTfwdZIBa.defaultLevel0000' @ 0 : 0; will stop at (end)
2026/07/24-08:59:46.977404 7fb394b4f6c0 Manual compaction at level-1 from '!scenes!DDibQQLAvyIq9y09' @ 72057594037927935 : 1 .. '!scenes.levels!zvY1RwBhTfwdZIBa.defaultLevel0000' @ 0 : 0; will stop at (end)
+8
View File
@@ -0,0 +1,8 @@
2026/07/23-23:29:38.570468 7fb396ffd6c0 Recovering log #283
2026/07/23-23:29:38.581351 7fb396ffd6c0 Delete type=3 #281
2026/07/23-23:29:38.581438 7fb396ffd6c0 Delete type=0 #283
2026/07/24-08:14:15.298614 7fb394b4f6c0 Level-0 table #288: started
2026/07/24-08:14:15.298651 7fb394b4f6c0 Level-0 table #288: 0 bytes OK
2026/07/24-08:14:15.341159 7fb394b4f6c0 Delete type=0 #286
2026/07/24-08:14:15.341595 7fb394b4f6c0 Manual compaction at level-0 from '!scenes!DDibQQLAvyIq9y09' @ 72057594037927935 : 1 .. '!scenes.levels!zvY1RwBhTfwdZIBa.defaultLevel0000' @ 0 : 0; will stop at (end)
2026/07/24-08:14:15.417678 7fb394b4f6c0 Manual compaction at level-1 from '!scenes!DDibQQLAvyIq9y09' @ 72057594037927935 : 1 .. '!scenes.levels!zvY1RwBhTfwdZIBa.defaultLevel0000' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
View File
+1
View File
@@ -0,0 +1 @@
MANIFEST-000403
View File
+7
View File
@@ -0,0 +1,7 @@
2026/07/24-08:30:57.704269 7fb3e4fff6c0 Recovering log #401
2026/07/24-08:30:57.760459 7fb3e4fff6c0 Delete type=3 #399
2026/07/24-08:30:57.760535 7fb3e4fff6c0 Delete type=0 #401
2026/07/24-08:59:46.963178 7fb394b4f6c0 Level-0 table #406: started
2026/07/24-08:59:46.963211 7fb394b4f6c0 Level-0 table #406: 0 bytes OK
2026/07/24-08:59:46.969620 7fb394b4f6c0 Delete type=0 #404
2026/07/24-08:59:46.977188 7fb394b4f6c0 Manual compaction at level-0 from '!folders!00Hn2nNarlL7b0DR' @ 72057594037927935 : 1 .. '!items!yozTUjNuc2rEGjFK' @ 0 : 0; will stop at (end)
+8
View File
@@ -0,0 +1,8 @@
2026/07/23-23:29:38.526675 7fb397fff6c0 Recovering log #398
2026/07/23-23:29:38.536758 7fb397fff6c0 Delete type=0 #398
2026/07/23-23:29:38.536840 7fb397fff6c0 Delete type=3 #397
2026/07/23-23:29:38.536904 7fb397fff6c0 Delete type=0 #393
2026/07/24-08:14:15.178686 7fb394b4f6c0 Level-0 table #402: started
2026/07/24-08:14:15.178752 7fb394b4f6c0 Level-0 table #402: 0 bytes OK
2026/07/24-08:14:15.212738 7fb394b4f6c0 Delete type=0 #400
2026/07/24-08:14:15.341395 7fb394b4f6c0 Manual compaction at level-0 from '!folders!00Hn2nNarlL7b0DR' @ 72057594037927935 : 1 .. '!items!yozTUjNuc2rEGjFK' @ 0 : 0; will stop at (end)
Binary file not shown.
View File
+1
View File
@@ -0,0 +1 @@
MANIFEST-000002
View File
+1
View File
@@ -0,0 +1 @@
2026/05/06-14:41:23.043596 7f0aceffd6c0 Delete type=3 #1
Binary file not shown.
Binary file not shown.

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