Merge pull request #402 from deltagreen-foundryvtt/sobrandm/xxx/extravaganza

Sobrandm/xxx/extravaganza
This commit is contained in:
Aaron Lutomski
2026-06-22 11:03:02 -07:00
committed by GitHub
232 changed files with 18545 additions and 7191 deletions
+11
View File
@@ -29,6 +29,17 @@ jobs:
- name: Check Prettier formatting
run: npm run format:check
sass:
name: Compile Sass
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Node Setup
uses: ./.github/actions/setup-node
- name: Compile Sass
run: npm run build:css
defaults:
run:
shell: bash
@@ -54,6 +54,12 @@ jobs:
- name: Build Packs
uses: ./.github/actions/build-packs
- name: Node Setup
uses: ./.github/actions/setup-node
- name: Build CSS
run: npm run build:css
- name: Substitute Download Link
uses: TomaszKandula/variable-substitution@v1.0.1
with:
@@ -24,6 +24,12 @@ jobs:
- name: Build Packs
uses: ./.github/actions/build-packs
- name: Node Setup
uses: ./.github/actions/setup-node
- name: Build CSS
run: npm run build:css
- name: Get Version
id: get_version
uses: dhkatz/get-version-action@v3.0.0
+4
View File
@@ -15,3 +15,7 @@ deltagreen.lock
# Ignore npm modules
node_modules
# Sass build output (source lives in scss/)
css/*.css
css/*.css.map
+4
View File
@@ -13,3 +13,7 @@ packs/**
# Ignore html/handlebars files as a different formatter is used.
*.html
*.hbs
# Generated from scss/ via npm run build:css
css/*.css
css/*.css.map
+2 -1
View File
@@ -5,7 +5,8 @@
"esbenp.prettier-vscode",
"monosans.djlint",
"DavidAnson.vscode-markdownlint",
"EditorConfig.EditorConfig"
"EditorConfig.EditorConfig",
"syler.sass-indented"
],
"unwantedRecommendations": []
}
+21
View File
@@ -11,6 +11,27 @@
- [#296](https://github.com/deltagreen-foundryvtt/delta-green-foundry-vtt-system/issues/296) - Remove adaptation checks if not adapted.
- [#281](https://github.com/deltagreen-foundryvtt/delta-green-foundry-vtt-system/issues/281) - Add tooltips to checkboxes in adaptation area.
- [#211](https://github.com/deltagreen-foundryvtt/delta-green-foundry-vtt-system/issues/211) - Move macros to System macros folder in compendium.
- [#283](https://github.com/deltagreen-foundryvtt/delta-green-foundry-vtt-system/issues/283) - Added new actor sheet with left hand Sidebar RFC
- [#291](https://github.com/deltagreen-foundryvtt/delta-green-foundry-vtt-system/issues/291) - Moving Luck button from three dots to left hand panel
- [#297](https://github.com/deltagreen-foundryvtt/delta-green-foundry-vtt-system/issues/297) - Move adaption ticks to left hand panel
- [#374](https://github.com/deltagreen-foundryvtt/delta-green-foundry-vtt-system/issues/374) - Moving Age to Bio tab
- [#393](https://github.com/deltagreen-foundryvtt/delta-green-foundry-vtt-system/issues/393) - Updated to support Foundry v14
- [#223](https://github.com/deltagreen-foundryvtt/delta-green-foundry-vtt-system/issues/223) - Migrated item sheets to Foundry ApplicationV2 (`ItemSheetV2`)
- [#123](https://github.com/deltagreen-foundryvtt/delta-green-foundry-vtt-system/issues/123) - Added Active Effect support
- [#346](https://github.com/deltagreen-foundryvtt/delta-green-foundry-vtt-system/issues/346) - Maximum resources modifiable through AE
- [#365](https://github.com/deltagreen-foundryvtt/delta-green-foundry-vtt-system/issues/365) - Added buttons to trigger exhaustion, rest, and use stimulants
- [#209](https://github.com/deltagreen-foundryvtt/delta-green-foundry-vtt-system/issues/209) - Added **Profession** item type with partial chargen automation
### **Bug Fixes:**
- [#392](https://github.com/deltagreen-foundryvtt/delta-green-foundry-vtt-system/issues/392) - Fixed sheet rolls
- [#394](https://github.com/deltagreen-foundryvtt/delta-green-foundry-vtt-system/issues/394) - Unified actor and item description fields on schema with shared ProseMirror preparation
- [#396](https://github.com/deltagreen-foundryvtt/delta-green-foundry-vtt-system/issues/396) - Fixed typed skill dialog when editing existing skill
### **Under the Hood:**
- [#323](https://github.com/deltagreen-foundryvtt/delta-green-foundry-vtt-system/issues/323) - Refactored roll logic and split into util functions
- [#233](https://github.com/deltagreen-foundryvtt/delta-green-foundry-vtt-system/issues/233) - Refactored CSS to Sass
## Version 1.6.6 - 2026-04-15
+27 -5
View File
@@ -12,7 +12,7 @@
### Formatters
- JavaScript, JSON, CSS: [Prettier](https://prettier.io/docs/install)
- JavaScript, JSON, SCSS: [Prettier](https://prettier.io/docs/install)
- Handlebars: [DJLint](https://djlint.com/docs/getting-started/)
### Linters
@@ -20,12 +20,13 @@
- JavaScript: [Eslint](https://eslint.org/docs/latest/use/getting-started)
- Markdown: [Markdownlint](https://github.com/markdownlint/markdownlint)
- Handlebars: DJLint (see above)
- CSS: VSCode Built-in
- SCSS: VSCode Built-in
### Misc
- [EditorConfig](https://editorconfig.org/) - Used to standardize file configurations like tabs vs. spaces, end-of-line characters, etc.
- [FVTT CLI](https://github.com/foundryvtt/foundryvtt-cli) - Used to package document JSON files into LevelDB packs, and vice-versa. In simpler terms, this lets us convert documents in compendia into JSON files, OR turn those JSON files back into documents in the compendia.
- [Sass](https://sass-lang.com/) (via npm) - Compiles `scss/` source into the `css/` files Foundry loads at runtime.
### VSCode Extensions
@@ -81,7 +82,25 @@ npm -v
2. Install `djlint` (doesn't come through NPM). This requires `Python` and `pip`; instructions for installing these are specific to your operating system, and beyond the scope of this guide.
### 4. Set Up Your Code Editor
### 4. Compile Stylesheets
Styles live in [`scss/`](scss/). Foundry loads compiled output from [`css/`](css/), which is **not** committed to git — you must build it locally.
1. Run a one-off compile (required after a fresh clone, or before testing if you are not using watch mode):
```bash
npm run build:css
```
2. While actively editing styles, run Sass in watch mode in a **separate terminal** (leave it running):
```bash
npm run watch:css
```
Edit files under `scss/`; output is written to `css/`. Foundry's hot reload (configured in `system.json`) picks up changes to the compiled CSS when watch mode is running.
### 5. Set Up Your Code Editor
We recommend [Visual Studio Code](https://code.visualstudio.com/) for working on this project, but you're welcome to use any editor. The following extensions are **auto-recommended** by VSCode, but here they are again for special instructions, manual installation, or reference.
@@ -106,7 +125,7 @@ Once these extensions are installed, your editor should automatically highlight
Your pull request will automatically be checked for linting and formatting via GitHub Actions. If your changes don't pass, they can't be merged (see [Branching and Pull Requests](#branching-commits-and-pull-requests)).
### 5. Set Up a Local Foundry VTT Test Instance
### 6. Set Up a Local Foundry VTT Test Instance
1. We **highly** recommend setting up a separate instance of Foundry from your main installation. This way you are not at risk of breaking things in live worlds.
@@ -454,6 +473,8 @@ Make sure your code is clean and ready:
- ✅ You’ve manually tested your change in a local Foundry world.
- ✅ `npm run lint` shows no errors
- ✅ `npm run format:check` passes
- ✅ `npm run build:css` succeeds (required if you changed anything under `scss/`)
- ✅ If you changed styles, you tested with `npm run watch:css` running and confirmed the UI looks correct
- ✅ Your editor doesn’t show linting/formatting warnings.
We use a GitHub Action to enforce code quality. If your code doesn’t meet the linting and formatting standards, your PR cannot be merged until those checks pass.
@@ -473,8 +494,9 @@ It is difficult to create a one-size-fits-all procedure for testing, because eac
1. Prepare your environment
- **Styles:** On a fresh clone, run `npm run build:css` (or keep `npm run watch:css` running) so Foundry has compiled CSS to load.
- Test in a safe environment (**HIGHLY RECOMMENDED**):
- We highly recommend [setting up an installation of Foundry separate from your main instance](#5-set-up-a-local-foundry-vtt-test-instance).
- We highly recommend [setting up an installation of Foundry separate from your main instance](#6-set-up-a-local-foundry-vtt-test-instance).
- This way, you are never at risk of breaking anything in your main worlds.
- If you do not follow the above advice, be aware that opening your main world while you have an in-progress (non-release) version of the system installed **will almost certainly break things in a way that cannot be fixed**. To go back to the release version of the system, uninstall the test build, and reinstall from the in-Foundry system browser.
- Update to the target build:
+1 -1
View File
@@ -35,4 +35,4 @@ If there is a need to manually install the system, go to _Game Systems_ and clic
## System Documentation
You may find documentation for the system in [this folder](https://github.com/TheLastScrub/delta-green-foundry-vtt-system/blob/master/documentation/home.md).
You may find documentation for the system in [this folder](https://github.com/deltagreen-foundryvtt/delta-green-foundry-vtt-system/blob/master/documentation/home.md).
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

View File
-1470
View File
File diff suppressed because it is too large Load Diff
-37
View File
@@ -1,37 +0,0 @@
@font-face {
font-family: "SpecialElite";
src: url("../assets/font/special_elite/SpecialElite-Regular.ttf")
format("truetype"); /* Chrome 4+, Firefox 3.5, Opera 10+, Safari 3—5 */
}
.special-elite-font {
font-family: "SpecialElite", sans-serif !important;
}
.editor-content {
font-family: "SpecialElite", sans-serif !important;
}
.tox .tox-editor-container {
font-family: "SpecialElite", sans-serif !important;
}
.tox .tox-edit-area {
font-family: "SpecialElite", sans-serif !important;
}
.editor-host .editor {
font-family: "SpecialElite", sans-serif !important;
}
.editor-host .tox {
font-family: "SpecialElite", sans-serif !important;
}
.editor-host .tox-tinymce {
font-family: "SpecialElite", sans-serif !important;
}
.editor-host .tox .tox-edit-area {
font-family: "SpecialElite", sans-serif !important;
}
-37
View File
@@ -3,9 +3,7 @@
"DG.AgentNameFieldTooltip": "COGNOM, NOM (ALIES O NOM EN CLAU EN CAS DE SER NECESSARI)",
"DG.RollLuck": "Tira sort",
"DG.Luck": "Sort",
"DG.FallbackText.ProfessionAndOrRank": "Professió (Rang si aplica)",
"DG.Skills.accounting": "Comptabilitat",
"DG.Skills.alertness": "Alerta",
"DG.Skills.anthropology": "Antropologia",
@@ -44,14 +42,12 @@
"DG.Skills.unarmed_combat": "Combat sense armes",
"DG.Skills.unnatural": "Sobrenatural",
"DG.Skills.flight": "Flight",
"DG.Skills.AddTypedOrCustomSkill": "Afegir tipus o habilitat personalitzada",
"DG.Skills.AddSkill": "Afegir habilitat",
"DG.Skills.EditTypedOrCustomSkill": "Edit Typed or Custom Skill",
"DG.Skills.EditSkill": "Edit Skill",
"DG.Skills.SkillGroup": "Habilitat grupal",
"DG.Skills.SkillName": "Nom de l'habilitat",
"DG.TypeSkills.Art": "Art",
"DG.TypeSkills.Craft": "Ofici",
"DG.TypeSkills.ForeignLanguage": "Idioma",
@@ -59,7 +55,6 @@
"DG.TypeSkills.Pilot": "Pilotar",
"DG.TypeSkills.Science": "Ciència",
"DG.TypeSkills.Other": "Altre",
"DG.Attributes.HP": "PV",
"DG.Attributes.WP": "PVOL",
"DG.Attributes.SAN": "SENY",
@@ -69,21 +64,17 @@
"DG.Attributes.int": "INT",
"DG.Attributes.pow": "POD",
"DG.Attributes.cha": "CAR",
"DG.Navigation.Skills": "Habilitats",
"DG.Navigation.Physical": "Físic",
"DG.Navigation.Mental": "Mental",
"DG.Navigation.Gear": "Equip",
"DG.Navigation.CV": "C.V.",
"DG.Navigation.Contacts": "Contactes",
"DG.About.OnlineHelp": "Documentació del sistema oberta en (EN LÍNEA)",
"DG.About.OnlineHelpTooltip": "Inicia el navegador amb ajuda en línea de Github",
"DG.About.OnlineBugTracker": "Preguntes, informes d'errors o sol·licituts de funcions (EN LÍNEA)",
"DG.About.OnlineBugTrackerTooltip": "Inicia el navegador el rastrejador d'errors de Github. ",
"DG.Roll.Rolling": "Tirant",
"DG.Roll.Target": "Objectiu: ",
"DG.Roll.Success": "Èxit",
"DG.Roll.Failure": "Fallada",
"DG.Roll.Critical": "Crític",
@@ -94,14 +85,11 @@
"DG.Roll.RollingDamageFor": "Tirant dany per",
"DG.Roll.Or": "O",
"DG.Roll.Roll": "Tirar",
"DG.Physical.Wounds": "Ferides",
"DG.Physical.FirstAidLabel": "Primers auxilis ja aplicats: ",
"DG.Physical.Exhausted": "Exhausted:",
"DG.Physical.ExhaustionPenalty": "Penalty:",
"DG.Generic.AddButtonLabel": "Afegir",
"DG.Mental.BreakingPoint": "Punt de trencament: ",
"DG.Mental.Reset": "Esborrar",
"DG.Mental.SanityLossIncidentsLabel": "Incidents o pèrdua de SENY sense embogir",
@@ -111,7 +99,6 @@
"DG.Mental.AdaptedToHelplessness": "Adaptat a la impotència",
"DG.Mental.Motivations": "Motivacions",
"DG.Mental.Disorders": "Desordres",
"DG.Gear.Weapons": "ARMES",
"DG.Gear.Armor": "ARMADURA",
"DG.Gear.OtherGear": "ALTRE EQUIP",
@@ -120,7 +107,6 @@
"DG.Gear.ArmorPiercing": "Perforació d'armadura",
"DG.Gear.ArmorRating": "Classificació d'armadura",
"DG.Gear.Equipped": "Equipat",
"DG.CV.Sex": "Sexe",
"DG.CV.SexPlaceholder": "Sexe",
"DG.CV.AgeAndDoB": "Edat y D.D.N.",
@@ -131,12 +117,10 @@
"DG.CV.NationalityPlaceholder": "Nacionalitat",
"DG.CV.Education": "Educació",
"DG.CV.EducationPlaceholder": "Educació o entrenament",
"DG.Bonds.Bond": "Vincle",
"DG.Bonds.Name": "Nom",
"DG.Bonds.Score": "Puntuació",
"DG.Bonds.NewBond": "Nou vincle",
"DG.Attributes.Tooltip.HP": "Punts de vida, un personatge cau inconscient ales 2 PV i mor amb 0 PV.",
"DG.Tooltip.MaximumHP": "La salut màxima és el promig de FOR + CON, arrodonint avall.",
"DG.Attributes.Tooltip.WP": "Els punts de voluntat són el combustible mental. Els necessiten per tirar endavant tot i l'esgotament o le situacions mentals greus. Consulta la pàgina 47 del manual de l'agent per més detalls.",
@@ -155,17 +139,14 @@
"DG.Tooltip.AdaptedToHelplessness": "Estar adaptat a la impotència significa que l'impuls personal de l'agent s'allunya del món que el rodeja. Perd permanentement 1D6 POD. ",
"DG.Tooltip.ToggleEquippedState": "Fes click per alternar si aquest item està equipat o no. ",
"DG.Tooltip.Exhaustion": "An Agent who works too long or faces extreme danger and injury without resting becomes exhausted. When this comes into play is up to the Handler, but a good rule of thumb is that going a night without sleep or refusing to rest after losing SAN or Hit Points leads to exhaustion.",
"DG.Gear.Expense.NA": "N/A",
"DG.Gear.Expense.Incidental": "Accidental",
"DG.Gear.Expense.Standard": "Estàndar",
"DG.Gear.Expense.Unusual": "Inusual",
"DG.Gear.Expense.Major": "Destacat",
"DG.Gear.Expense.Extreme": "Extrem",
"DG.ItemWindow.Bonds.Description": "Descripció / Desenvolupament ",
"DG.ItemWindow.Bonds.ScoreTooltip": "Una puntuació més alta representa una relació més forta entre l'agente i el vincle. ",
"DG.ItemWindow.Motivations.Attributes": "Atributs",
"DG.ItemWindow.Motivations.Motivation": "Motivació",
"DG.ItemWindow.Motivations.Description": "Descripció / Notes",
@@ -176,19 +157,16 @@
"DG.ItemWindow.Motivations.IsDisorderTooltip": "Marca aquesta opció si afageix un trastorn o si ha perdut una motivació.",
"DG.ItemWindow.Motivations.DisorderHasBeenCured": "El trastorn s'ha curat.",
"DG.ItemWindow.Motivations.DisorderHasBeenCuredTooltip": "Un èxito crític en una tirada de sort terapèutica pot curar un trastorn. Tot i així, pot tornar la pròxima vegada que s'arribi a un punt de ruptura. ",
"DG.ItemWindow.Gear.Attributes": "Atributs",
"DG.ItemWindow.Gear.Description": "Descripció",
"DG.ItemWindow.Gear.NamePlaceholder": "Nom",
"DG.ItemWindow.Gear.Expense": "Despesa",
"DG.ItemWindow.Gear.Equipped": "Equipat",
"DG.ItemWindow.Armor.Description": "Descripció",
"DG.ItemWindow.Armor.NamePlaceholder": "Nom",
"DG.ItemWindow.Armor.ArmorRating": "Nivell armadura",
"DG.ItemWindow.Armor.Expense": "Despesa",
"DG.ItemWindow.Armor.Equipped": "Equipat",
"DG.ItemWindow.Weapons.Description": "Descripció",
"DG.ItemWindow.Weapons.NamePlaceholder": "Nom",
"DG.ItemWindow.Weapons.Skill": "Habilitat",
@@ -202,27 +180,21 @@
"DG.ItemWindow.Weapons.Ammo": "Munició (Quantitat/Tipus)",
"DG.ItemWindow.Weapons.Range": "Abast",
"DG.ItemWindow.Weapons.KillRadius": "Radi de mort",
"DG.ModifySkillRollDialogue.Title": "Modificar tirada",
"DG.ModifySkillRollDialogue.Test": "Test:",
"DG.ModifySkillRollDialogue.OriginalTarget": "Objectiu original:",
"DG.ModifySkillRollDialogue.TargetModifier": "Modificar objectiu:",
"DG.ModifySkillRollDialogue.ModifierTooltip": "Donar un modificador positiu o negatiu aquí per sumar o restar al objectiu en la tirada d'habilitats.",
"DG.ModifyDamageRollDialogue.Weapon": "Arma:",
"DG.ModifyDamageRollDialogue.OuterModifierTooltip": "Utilitza aquest camp per modificar la fòrmula, com duplicar el resultat.",
"DG.ModifyDamageRollDialogue.InnerModifierTooltip": "Utilitza aquest camp per afegir un modificar com +1 o un altre terma com +1d4.",
"DG.Roll.Inhuman": "Inhumà",
"DG.Tooltip.BreakPoint": "El punt de trencament actual, actualitzar quan el seny actual sigui igual a aquest valor. El punt de trencament inciial és SENY - POD.",
"DG.Tooltip.BreakPointReset": "Actualitzar el punt de trencament al valor actual de SENY - PVOL.",
"DG.Skills.ritual": "Ritual",
"DG.RollMode": "Modo rollo",
"DG.Bonds.Relationship": "Relació",
"DG.TypeSkills.Subskills.Painting": "Pintar",
"DG.Sheet.BlockHeaders.Attributes": "ATTRIBUTES",
"DG.Sheet.BlockHeaders.Statistics": "STATISTICS",
"DG.Sheet.BlockHeaders.PhysicalDescriptionNotes": "Descripció física/notes",
@@ -232,47 +204,38 @@
"DG.Sheet.BlockHeaders.MotivationsAndDisorders": "MOTIVATIONS AND DISORDERS",
"DG.Sheet.BlockHeaders.PersonalInfoCV": "PERSONAL INFORMATION / CV",
"DG.Sheet.BlockHeaders.BondsRelationshipsContacts": "BONDS / RELATIONSHIPS / CONTACTS",
"DG.UnnaturalSheet.HideUntrainedSkillsLabel": "Amagar habilitats no entrenades",
"DG.UnnaturalSheet.NotesLabel": "Notes",
"DG.UnnaturalSheet.ShortDescriptionFallbackText": "Descripció de la criatura o tipus de criatura",
"DG.UnnaturalSheet.Tabs.NotesTabLabel": "Habilitats i notes",
"DG.UnnaturalSheet.Tabs.ItemsTabLabel": "Atacs i equipament",
"DG.NPCSheet.BreakPointAbbreviation": "PT",
"DG.ImpossibleLanscapes.IHaveSeenThe": "I HAVE SEEN THE",
"DG.ImpossibleLanscapes.YellowSign": "YELLOW SIGN",
"DG.ImpossibleLanscapes.CorruptionFirstHalf": "Corrup",
"DG.ImpossibleLanscapes.CorruptionSecondHalf": "tion",
"DG.ImpossibleLanscapes.Gift": "Gift",
"DG.ImpossibleLanscapes.Insight": "Insight",
"DG.VehicleSheet.NotesTabName": "Notes",
"DG.VehicleSheet.GearTabName": "Gear",
"DG.VehicleSheet.NotesLabel": "NOTES",
"DG.VehicleSheet.DescriptionPlaceHolder": "Vehicle Description",
"DG.Generic.CheckDamagedBondsBoxUntilNextHomeScene": "Check a damaged Bond's box until the next Home Scene ends.",
"DG.Generic.ClearAll": "Clear All",
"DG.Generic.SanDamage": "SAN Damage",
"DG.ItemWindow.Weapons.CustomSkillTarget": "Target",
"DG.ItemWindow.Weapons.SkillModifier": "Skill Modifier",
"DG.ItemWindow.Custom": "Custom",
"TYPES.Item.weapon": "Arma",
"TYPES.Item.armor": "Armadura",
"TYPES.Item.motivation": "Motivació",
"TYPES.Item.bond": "Vincle",
"TYPES.Item.gear": "Equip",
"DG.ItemWindow.Ritual.ActivationSanityDamage": "Activation SAN Damage",
"DG.ItemWindow.Ritual.ActivationTime": "Activation Time",
"DG.ItemWindow.Ritual.ActivationCosts": "Activation Costs",
"DG.ItemWindow.Ritual.StudyTime": "Study Time",
"DG.ItemWindow.Ritual.LearnedSanityDamage": "Learn SAN Damage",
"DG.Messages.Skillsmark.Marked": "Aquesta habilitat està marcada",
"DG.Messages.Skillsmark.Tooltip": "Vegeu la p. 29 del Manual de l'agent"
}
-41
View File
@@ -3,9 +3,7 @@
"DG.AgentNameFieldTooltip": "姓氏, 名字 (如适用则包括别名)",
"DG.RollLuck": "幸运检定",
"DG.Luck": "幸运",
"DG.FallbackText.ProfessionAndOrRank": "职业(如适用则包括职位)",
"DG.Skills.accounting": "会计",
"DG.Skills.alertness": "警觉",
"DG.Skills.anthropology": "人类学",
@@ -45,7 +43,6 @@
"DG.Skills.unnatural": "非自然知识",
"DG.Skills.flight": "飞机",
"DG.TypeSkills.Subskills.Painting": "绘画",
"DG.Skills.AddTypedOrCustomSkill": "Add Typed/Custom Skill",
"DG.Skills.AddSkill": "Add Skill",
"DG.Skills.Apply": "Apply",
@@ -58,7 +55,6 @@
"DG.Skills.EditSkill": "编辑技能",
"DG.Skills.SkillGroup": "技能组别",
"DG.Skills.SkillName": "技能名称",
"DG.TypeSkills.Art": "艺术",
"DG.TypeSkills.Craft": "工艺",
"DG.TypeSkills.ForeignLanguage": "外语",
@@ -66,7 +62,6 @@
"DG.TypeSkills.Pilot": "特种载具",
"DG.TypeSkills.Science": "科学",
"DG.TypeSkills.Other": "其他",
"DG.SpecialTraining.Dialog.CreateSpecialTraining": "添加特训",
"DG.SpecialTraining.Dialog.DropDown.CustomSkills": "Custom自定义/类别 技能",
"DG.SpecialTraining.Dialog.DropDown.Skills": "技能",
@@ -75,7 +70,6 @@
"DG.SpecialTraining.Dialog.NameLabel": "名称",
"DG.SpecialTraining.Dialog.SkillLabel": "技能货属性",
"DG.SpecialTraining.Dialog.Title": "创建/编辑特训",
"DG.Attributes.HP": "HP",
"DG.Attributes.WP": "WP",
"DG.Attributes.SAN": "理智",
@@ -85,21 +79,17 @@
"DG.Attributes.int": "智力",
"DG.Attributes.pow": "意志",
"DG.Attributes.cha": "魅力",
"DG.Navigation.Skills": "技能",
"DG.Navigation.Physical": "健康",
"DG.Navigation.Mental": "精神",
"DG.Navigation.Gear": "装备",
"DG.Navigation.CV": "个人情况",
"DG.Navigation.Contacts": "重要关系",
"DG.About.OnlineHelp": "System Documentation on (ONLINE)",
"DG.About.OnlineHelpTooltip": "Launch browser with online GitHub help.",
"DG.About.OnlineBugTracker": "Questions, Bug Reports or Feature Requests (ONLINE)",
"DG.About.OnlineBugTrackerTooltip": "Launch browser with GitHub issue tracker.",
"DG.Roll.Rolling": "正在掷骰",
"DG.Roll.Target": "需求: ",
"DG.Roll.Success": "成功",
"DG.Roll.Failure": "失败",
"DG.Roll.Critical": "大成功",
@@ -110,14 +100,11 @@
"DG.Roll.RollingDamageFor": "正在为以下目标掷伤害骰",
"DG.Roll.Or": "或",
"DG.Roll.Roll": "掷骰",
"DG.Physical.Wounds": "伤口",
"DG.Physical.FirstAidLabel": "已尝试急救: ",
"DG.Physical.Exhausted": "精疲力尽:",
"DG.Physical.ExhaustionPenalty": "惩罚:",
"DG.Generic.AddButtonLabel": "添加",
"DG.Mental.BreakingPoint": "崩溃点: ",
"DG.Mental.Reset": "重置",
"DG.Mental.SanityLossIncidentsLabel": "未陷入疯狂的理智损失事件",
@@ -127,7 +114,6 @@
"DG.Mental.AdaptedToHelplessness": "适应无力感",
"DG.Mental.Motivations": "人生动力",
"DG.Mental.Disorders": "Disorder",
"DG.Gear.Weapons": "武器",
"DG.Gear.Armor": "护甲",
"DG.Gear.OtherGear": "其他装备",
@@ -136,7 +122,6 @@
"DG.Gear.ArmorPiercing": "护甲开销",
"DG.Gear.ArmorRating": "护甲等级",
"DG.Gear.Equipped": "已装备",
"DG.CV.Sex": "性别",
"DG.CV.SexPlaceholder": "性别",
"DG.CV.AgeAndDoB": "年龄和出生日期.",
@@ -147,12 +132,10 @@
"DG.CV.NationalityPlaceholder": "国籍",
"DG.CV.Education": "教育",
"DG.CV.EducationPlaceholder": "教育或培训",
"DG.Bonds.Bond": "重要关系",
"DG.Bonds.Name": "名称",
"DG.Bonds.Score": "关系值",
"DG.Bonds.NewBond": "新关系",
"DG.Attributes.Tooltip.HP": "体力,体力降到2点时,你的特工会陷入昏迷。体力降到0点时,你的特工会死亡。",
"DG.Tooltip.MaximumHP": "体力等于力量加上体质,除以二并向上取整。",
"DG.Attributes.Tooltip.WP": "毅力(WP):毅力代表精神耐受度。特工需要毅力来克服疲劳或精神消耗的情况。(详见47页的毅力一节)",
@@ -171,17 +154,14 @@
"DG.Tooltip.AdaptedToHelplessness": "适应无力感意味着你的特工做事的动力受到了损伤。这导致意志永久损失1D6点。",
"DG.Tooltip.ToggleEquippedState": "点击切换此物品是否已装备。",
"DG.Tooltip.Exhaustion": "一名特工在工作太长时间后,或是面临极端的危险或受伤却没有得到休息后,将变得精疲力尽。其适用的时机取决于掌局者,但彻夜不眠,或是在损失理智或体力后拒绝休息,应当导致精疲力尽。",
"DG.Gear.Expense.NA": "N/A or None",
"DG.Gear.Expense.Incidental": "细微开销",
"DG.Gear.Expense.Standard": "常规开销",
"DG.Gear.Expense.Unusual": "显著开销",
"DG.Gear.Expense.Major": "重大开销",
"DG.Gear.Expense.Extreme": "极端开销",
"DG.ItemWindow.Bonds.Description": "描述/发展",
"DG.ItemWindow.Bonds.ScoreTooltip": "A更高的关系值代表特工与重要关系之间的联系更强。",
"DG.ItemWindow.Motivations.Attributes": "属性",
"DG.ItemWindow.Motivations.Motivation": "人生动机",
"DG.ItemWindow.Motivations.Description": "描述/笔记",
@@ -192,19 +172,16 @@
"DG.ItemWindow.Motivations.IsDisorderTooltip": "如果添加精神疾病,或现有动机因获得精神障碍而失去,请勾选此项。",
"DG.ItemWindow.Motivations.DisorderHasBeenCured": "精神疾病已被 \"治愈\"",
"DG.ItemWindow.Motivations.DisorderHasBeenCuredTooltip": "心理治疗幸运骰的大成功可以 '治愈' 精神疾病。但它可能在下次达到崩溃点时再次出现。参见《特工手册》第78-79页的心理治疗部分",
"DG.ItemWindow.Gear.Attributes": "属性",
"DG.ItemWindow.Gear.Description": "描述",
"DG.ItemWindow.Gear.NamePlaceholder": "名称",
"DG.ItemWindow.Gear.Expense": "开销",
"DG.ItemWindow.Gear.Equipped": "已装备",
"DG.ItemWindow.Armor.Description": "描述",
"DG.ItemWindow.Armor.NamePlaceholder": "名称",
"DG.ItemWindow.Armor.ArmorRating": "护甲等级",
"DG.ItemWindow.Armor.Expense": "开销",
"DG.ItemWindow.Armor.Equipped": "已装备",
"DG.ItemWindow.Weapons.Description": "描述",
"DG.ItemWindow.Weapons.NamePlaceholder": "名称",
"DG.ItemWindow.Weapons.Skill": "技能",
@@ -218,25 +195,20 @@
"DG.ItemWindow.Weapons.Ammo": "弹药 (数量/类别)",
"DG.ItemWindow.Weapons.Range": "射程",
"DG.ItemWindow.Weapons.KillRadius": "击杀半径",
"DG.ModifySkillRollDialogue.Title": "修改掷骰",
"DG.ModifySkillRollDialogue.Test": "测试:",
"DG.ModifySkillRollDialogue.OriginalTarget": "初始目标:",
"DG.ModifySkillRollDialogue.TargetModifier": "修改目标:",
"DG.ModifySkillRollDialogue.ModifierTooltip": "在这里给出正面或负面修正,以增加/减少技能检定的目标值。",
"DG.ModifyDamageRollDialogue.Weapon": "武器:",
"DG.ModifyDamageRollDialogue.OuterModifierTooltip": "使用此字段修改公式的外部部分,例如将结果加倍。",
"DG.ModifyDamageRollDialogue.InnerModifierTooltip": "使用此字段添加固定修正值,如'+1'或另一个骰子项,如'+1d4'。",
"DG.Roll.Inhuman": "非人类",
"DG.Tooltip.BreakPoint": "当前崩溃点,当当前理智降至或低于此值时重置。初始崩溃点等于初始理智减去意志。",
"DG.Tooltip.BreakPointReset": "将崩溃点重置为新的理智减去意志。",
"DG.Skills.ritual": "仪式",
"DG.RollMode": "掷骰模式",
"DG.Bonds.Relationship": "关系",
"DG.Sheet.BlockHeaders.Attributes": "属性",
"DG.Sheet.BlockHeaders.Statistics": "统计",
"DG.Sheet.BlockHeaders.PhysicalDescriptionNotes": "外貌描述 / 笔记",
@@ -246,7 +218,6 @@
"DG.Sheet.BlockHeaders.MotivationsAndDisorders": "动机和精神疾病",
"DG.Sheet.BlockHeaders.PersonalInfoCV": "个人信息 / 简历",
"DG.Sheet.BlockHeaders.BondsRelationshipsContacts": "重要关系 / 关系 / 联系人",
"DG.UnnaturalSheet.HideUntrainedSkillsLabel": "隐藏未训练技能",
"DG.UnnaturalSheet.NotesLabel": "笔记",
"DG.UnnaturalSheet.ShortDescriptionFallbackText": "生物或生物类型的描述",
@@ -254,33 +225,26 @@
"DG.UnnaturalSheet.Tabs.NotesTabLabel": "技能 & 笔记",
"DG.UnnaturalSheet.Tabs.ItemsTabLabel": "攻击 & 能力",
"DG.NPCSheet.BreakPointAbbreviation": "BP",
"DG.ImpossibleLanscapes.IHaveSeenThe": "我看见祂了",
"DG.ImpossibleLanscapes.YellowSign": "黄印",
"DG.ImpossibleLanscapes.CorruptionFirstHalf": "腐化",
"DG.ImpossibleLanscapes.CorruptionSecondHalf": "tion",
"DG.ImpossibleLanscapes.Gift": "天赋",
"DG.ImpossibleLanscapes.Insight": "洞察",
"DG.VehicleSheet.NotesTabName": "笔记",
"DG.VehicleSheet.GearTabName": "装备",
"DG.VehicleSheet.NotesLabel": "笔记",
"DG.VehicleSheet.DescriptionPlaceHolder": "载具描述",
"DG.Generic.CheckDamagedBondsBoxUntilNextHomeScene": "Check a damaged Bond's box until the next Home Scene ends.",
"DG.Generic.ClearAll": "Clear Checks",
"DG.Generic.SanDamage": "理智损失",
"DG.ItemWindow.Weapons.CustomSkillTarget": "目标",
"DG.ItemWindow.Weapons.SkillModifier": "技能修正",
"DG.ItemWindow.Custom": "自定义",
"TYPES.Actor.agent": "特工",
"TYPES.Actor.npc": "NPC",
"TYPES.Actor.unnatural": "非自然",
"TYPES.Actor.vehicle": "载具",
"TYPES.Item.armor": "护甲",
"TYPES.Item.bond": "重要关系",
"TYPES.Item.gear": "装备",
@@ -288,13 +252,10 @@
"TYPES.Item.weapon": "武器",
"TYPES.Item.tome": "典籍",
"TYPES.Item.ritual": "仪式",
"DG.Gear.Tomes": "典籍",
"DG.Gear.Rituals": "仪式",
"DG.Tomes.Name": "名称",
"DG.Ritual.Name": "名称",
"DG.Tome.Name": "名称",
"DG.ItemWindow.Tome.Description": "描述",
"DG.ItemWindow.Tome.LanguagePlaceholder": "语言",
@@ -304,13 +265,11 @@
"DG.ItemWindow.Tome.StudySanityDamage": "学习的理智损失",
"DG.ItemWindow.Tome.HandlerOnlyTabHeader": "仅掌局者可见",
"DG.ItemWindow.Tome.HandlerNotes": "掌局者笔记",
"DG.ItemWindow.Ritual.ActivationSanityDamage": "激活造成的理智损失",
"DG.ItemWindow.Ritual.ActivationTime": "激活耗时",
"DG.ItemWindow.Ritual.ActivationCosts": "激活代价",
"DG.ItemWindow.Ritual.StudyTime": "学习时长",
"DG.ItemWindow.Ritual.LearnedSanityDamage": "学习造成的理智损失",
"DG.Messages.Skillsmark.Marked": "此技能被标记",
"DG.Messages.Skillsmark.Tooltip": "参见第 29 页《代理人手册》"
}
-40
View File
@@ -3,9 +3,7 @@
"DG.AgentNameFieldTooltip": "NACHNAME, VORNAME (UND GGF. ALIAS)",
"DG.RollLuck": "Glücksprobe ausführen",
"DG.Luck": "Glück",
"DG.FallbackText.ProfessionAndOrRank": "BERUF (RANG, FALLS ZUTREFFEND)",
"DG.Skills.accounting": "Buchführung",
"DG.Skills.alertness": "Wachsamkeit",
"DG.Skills.anthropology": "Anthropologie",
@@ -44,7 +42,6 @@
"DG.Skills.unarmed_combat": "Waffenloser Kampf",
"DG.Skills.unnatural": "Mythos",
"DG.Skills.flight": "Flight",
"DG.Skills.AddTypedOrCustomSkill": "Eigene Fertigkeit hinzufügen",
"DG.Skills.AddSkill": "Fertigkeit hinzufügen",
"DG.Skills.Apply": "Anwenden",
@@ -57,7 +54,6 @@
"DG.Skills.EditSkill": "Fertigkeit bearbeiten",
"DG.Skills.SkillGroup": "Fertigkeiten-Gruppe",
"DG.Skills.SkillName": "Fertigkeit Name",
"DG.TypeSkills.Art": "Kunst",
"DG.TypeSkills.Craft": "Handwerk",
"DG.TypeSkills.ForeignLanguage": "Fremdsprache",
@@ -65,7 +61,6 @@
"DG.TypeSkills.Pilot": "Pilot",
"DG.TypeSkills.Science": "Wissenschaft",
"DG.TypeSkills.Other": "Andere",
"DG.SpecialTraining.Dialog.CreateSpecialTraining": "Besondere Ausbildung hinzufügen",
"DG.SpecialTraining.Dialog.DropDown.CustomSkills": "Eigene Fertigkeiten",
"DG.SpecialTraining.Dialog.DropDown.Skills": "Fertigkeiten",
@@ -74,7 +69,6 @@
"DG.SpecialTraining.Dialog.NameLabel": "Name",
"DG.SpecialTraining.Dialog.SkillLabel": "Fertigkeit oder Attribut",
"DG.SpecialTraining.Dialog.Title": "Spezialausbildung erstellen/ bearbeiten",
"DG.Attributes.HP": "Gesundheit",
"DG.Attributes.WP": "Wille",
"DG.Attributes.SAN": "Stabilität",
@@ -84,21 +78,17 @@
"DG.Attributes.int": "INT",
"DG.Attributes.pow": "MAC",
"DG.Attributes.cha": "CHA",
"DG.Navigation.Skills": "Fertigkeiten",
"DG.Navigation.Physical": "Körperlich",
"DG.Navigation.Mental": "Geistig",
"DG.Navigation.Gear": "Ausrüstung",
"DG.Navigation.CV": "Persönlich",
"DG.Navigation.Contacts": "Bindungen",
"DG.About.OnlineHelp": "Systemdokumentation auf (ONLINE)",
"DG.About.OnlineHelpTooltip": "Starten des Browsers mit der Online-Hilfe von GitHub.",
"DG.About.OnlineBugTracker": "Fragen, Fehlerberichte oder Funktionsanfragen (ONLINE)",
"DG.About.OnlineBugTrackerTooltip": "Browser mit GitHub-Problem-Tracker starten.",
"DG.Roll.Rolling": "Probe",
"DG.Roll.Target": "Ziel: ",
"DG.Roll.Success": "Erfolg",
"DG.Roll.Failure": "Fehlschlag",
"DG.Roll.Critical": "Kritisch",
@@ -109,14 +99,11 @@
"DG.Roll.RollingDamageFor": "SCHADEN würfeln für",
"DG.Roll.Or": "Oder",
"DG.Roll.Roll": "Probe",
"DG.Physical.Wounds": "Verletzungen",
"DG.Physical.FirstAidLabel": "Erste Hilfe bereits geleistet: ",
"DG.Physical.Exhausted": "Exhausted:",
"DG.Physical.ExhaustionPenalty": "Penalty:",
"DG.Generic.AddButtonLabel": "Hinzufügen",
"DG.Mental.BreakingPoint": "Schwelle: ",
"DG.Mental.Reset": "Reset",
"DG.Mental.SanityLossIncidentsLabel": "Vorfälle von Stabilitäts-Verlust, ohne verrückt zu werden",
@@ -126,7 +113,6 @@
"DG.Mental.AdaptedToHelplessness": "An Hilflosigkeit gewöhnt",
"DG.Mental.Motivations": "Motivation",
"DG.Mental.Disorders": "Störung",
"DG.Gear.Weapons": "WAFFEN",
"DG.Gear.Armor": "RÜSTUNG",
"DG.Gear.OtherGear": "ANDERE AUSRÜSTUNG",
@@ -135,7 +121,6 @@
"DG.Gear.ArmorPiercing": "Rüstungsdurchdringung",
"DG.Gear.ArmorRating": "Rüstungsschutz",
"DG.Gear.Equipped": "Ausgerüstet",
"DG.CV.Sex": "Geschlecht",
"DG.CV.SexPlaceholder": "Geschlecht",
"DG.CV.AgeAndDoB": "Alter und Geb.",
@@ -146,19 +131,15 @@
"DG.CV.NationalityPlaceholder": "Nationalität",
"DG.CV.Education": "Bildung",
"DG.CV.EducationPlaceholder": "Bildung oder Training",
"DG.Bonds.Bond": "Bindung",
"DG.Bonds.Name": "Name",
"DG.Bonds.Score": "Wert",
"DG.Bonds.NewBond": "Neue Bindung",
"DG.Attributes.Tooltip.HP": "Gesundheit. Ein Charakter wird bei 2 Gesundheit bewusstlos und stirbt bei 0 Gesundheit.",
"DG.Tooltip.MaximumHP": "Die maximale Gesundheit ist der Durchschnitt von STR + KON, aufgerundet.",
"DG.Attributes.Tooltip.WP": "Die Punkte in WILLE sind mentaler Treibstoff. Agenten brauchen WILLE, um trotz Erschöpfung oder geistig anstrengender Situationen weiterzumachen. Auf Seite 47 des Agentenhandbuchs findest du weitere Informationen ('Willpower').",
"DG.Tooltip.MaximumWP": "Der maximale WILLE ist gleich MAC.",
"DG.Attributes.Tooltip.SAN": "Klicken, um eine Stabilitäts-Probe durchzuführen...",
"DG.Tooltip.CurrentSanityPartOne": "Die aktuelle Grenze der Stabilität. Die aktuelle Belastungsschwelle ist ",
"DG.Tooltip.CurrentSanityPartTwo": ", Klicke oben auf STABILITÄT, um eine Stabilitäts-Probe durchzuführen.",
"DG.Tooltip.MaximumSanity": "Die maximale Stabilität ist gleich 99 abzüglich der Fähigkeit 'Mythos'.",
@@ -172,17 +153,14 @@
"DG.Tooltip.AdaptedToHelplessness": "Die 'Gewöhnung an Hilflosigkeit' ('Adapted to Helplessness') bedeutet, dass der persönliche Antrieb deines Agenten leidet, da er sich von der Welt um ihn herum zurückzieht. Sobald dein Agent an die Hilflosigkeit gewöhnt ist, verliert er dauerhaft 1D6 MAC.",
"DG.Tooltip.ToggleEquippedState": "Klicke hier, um umzuschalten, ob dieser Gegenstand ausgerüstet ist oder nicht.",
"DG.Tooltip.Exhaustion": "An Agent who works too long or faces extreme danger and injury without resting becomes exhausted. When this comes into play is up to the Handler, but a good rule of thumb is that going a night without sleep or refusing to rest after losing SAN or Hit Points leads to exhaustion.",
"DG.Gear.Expense.NA": "N/A bzw Keine",
"DG.Gear.Expense.Incidental": "Zufällig",
"DG.Gear.Expense.Standard": "Standard",
"DG.Gear.Expense.Unusual": "Ungewöhnlich",
"DG.Gear.Expense.Major": "Schwierig",
"DG.Gear.Expense.Extreme": "Extrem",
"DG.ItemWindow.Bonds.Description": "Beschreibung/ Entwicklungen",
"DG.ItemWindow.Bonds.ScoreTooltip": "Eine höhere Punktzahl steht für eine stärkere Beziehung zwischen dem Agenten und seiner Bindung.",
"DG.ItemWindow.Motivations.Attributes": "Attribute",
"DG.ItemWindow.Motivations.Motivation": "Motivation",
"DG.ItemWindow.Motivations.Description": "Beschreibung/ Notizen",
@@ -193,19 +171,16 @@
"DG.ItemWindow.Motivations.IsDisorderTooltip": "Prüfe dies, wenn du eine Störung hinzufügst oder wenn eine bestehende Motivation durch den Erwerb einer Störung verloren gegangen ist.",
"DG.ItemWindow.Motivations.DisorderHasBeenCured": "Störung wurde \"Geheilt\"",
"DG.ItemWindow.Motivations.DisorderHasBeenCuredTooltip": "Ein kritischer Erfolg bei einer Therapie-Glücksprobe kann eine Störung 'heilen'. Die Störung kann jedoch beim nächsten Mal zurückkehren, wenn eine Belastungsschwelle erreicht wird. Siehe den Abschnitt Therapie auf den Seiten 78-79 des Agentenhandbuchs. (siehe 'Therapy')",
"DG.ItemWindow.Gear.Attributes": "Attribute",
"DG.ItemWindow.Gear.Description": "Beschreibung",
"DG.ItemWindow.Gear.NamePlaceholder": "Name",
"DG.ItemWindow.Gear.Expense": "Ausgaben",
"DG.ItemWindow.Gear.Equipped": "Ausgerüstet",
"DG.ItemWindow.Armor.Description": "Beschreibung",
"DG.ItemWindow.Armor.NamePlaceholder": "Name",
"DG.ItemWindow.Armor.ArmorRating": "Rüstungsschutz",
"DG.ItemWindow.Armor.Expense": "Ausgaben",
"DG.ItemWindow.Armor.Equipped": "Ausgerüstet",
"DG.ItemWindow.Weapons.Description": "Beschreibung",
"DG.ItemWindow.Weapons.NamePlaceholder": "Name",
"DG.ItemWindow.Weapons.Skill": "Fertigkeit",
@@ -219,27 +194,21 @@
"DG.ItemWindow.Weapons.Ammo": "Munition (Menge/Typ)",
"DG.ItemWindow.Weapons.Range": "Reichweite",
"DG.ItemWindow.Weapons.KillRadius": "Tötungsradius",
"DG.ModifySkillRollDialogue.Title": "Probe modifizieren",
"DG.ModifySkillRollDialogue.Test": "Probe:",
"DG.ModifySkillRollDialogue.OriginalTarget": "Ursprüngliches Ziel:",
"DG.ModifySkillRollDialogue.TargetModifier": "Ziel modifizieren (+/-):",
"DG.ModifySkillRollDialogue.ModifierTooltip": "Gib hier einen positiven oder negativen Modifikator an, der zum Ziel für die Fertigkeitsprobe addiert/ abgezogen wird.",
"DG.ModifyDamageRollDialogue.Weapon": "Waffe:",
"DG.ModifyDamageRollDialogue.OuterModifierTooltip": "Verwende dieses Feld, um den äußeren Teil der Formel zu ändern, z. B. um das Ergebnis zu verdoppeln.",
"DG.ModifyDamageRollDialogue.InnerModifierTooltip": "Verwende dieses Feld, um einen Modifikator wie '+1' oder einen anderen Würfelbegriff wie '+1d4' hinzuzufügen.",
"DG.Roll.Inhuman": "Nicht-Menschlich",
"DG.Tooltip.BreakPoint": "Die aktuelle Belastungsschwelle, die zurückgesetzt wird, wenn die aktuelle Stabilität auf oder unter diesen Wert fällt. Die Belastungsschwelle zu Beginn ist STABILITÄT abzüglich MAC.",
"DG.Tooltip.BreakPointReset": "Belastungsschwelle zurücksetzen auf aktuelle MAC abzüglich WILLE.",
"DG.Skills.ritual": "Ritual",
"DG.RollMode": "Probe-Modus",
"DG.Bonds.Relationship": "Beziehung",
"DG.TypeSkills.Subskills.Painting": "Malerei",
"DG.Sheet.BlockHeaders.Attributes": "ATTRIBUTE",
"DG.Sheet.BlockHeaders.Statistics": "STATISTIK",
"DG.Sheet.BlockHeaders.PhysicalDescriptionNotes": "PHYSISCHE BESCHREIBUNG / ANMERKUNGEN",
@@ -249,7 +218,6 @@
"DG.Sheet.BlockHeaders.MotivationsAndDisorders": "MOTIVATIONEN UND STÖRUNGEN",
"DG.Sheet.BlockHeaders.PersonalInfoCV": "PERSÖNLICHE INFORMATIONEN / LEBENSLAUF",
"DG.Sheet.BlockHeaders.BondsRelationshipsContacts": "BINDUNGEN / BEZIEHUNGEN / KONTAKTE",
"DG.UnnaturalSheet.HideUntrainedSkillsLabel": "Untrainierte ausblenden",
"DG.UnnaturalSheet.NotesLabel": "Notizen",
"DG.UnnaturalSheet.ShortDescriptionFallbackText": "Beschreibung der Kreatur oder Kreaturenart",
@@ -257,40 +225,32 @@
"DG.UnnaturalSheet.Tabs.NotesTabLabel": "Fertigkeiten & Notizen",
"DG.UnnaturalSheet.Tabs.ItemsTabLabel": "Angriffe & Fähigkeiten",
"DG.NPCSheet.BreakPointAbbreviation": "BS",
"DG.ImpossibleLanscapes.IHaveSeenThe": "ICH HABE GESEHEN:",
"DG.ImpossibleLanscapes.YellowSign": "DAS GELBE ZEICHEN",
"DG.ImpossibleLanscapes.CorruptionFirstHalf": "Korrum",
"DG.ImpossibleLanscapes.CorruptionSecondHalf": "piert",
"DG.ImpossibleLanscapes.Gift": "Gabe",
"DG.ImpossibleLanscapes.Insight": "Erkenntnis",
"DG.VehicleSheet.NotesTabName": "Notizen",
"DG.VehicleSheet.GearTabName": "Ausrüstung",
"DG.VehicleSheet.NotesLabel": "NOTIZEN",
"DG.VehicleSheet.DescriptionPlaceHolder": "Fahrzeugbeschreibung",
"DG.Generic.CheckDamagedBondsBoxUntilNextHomeScene": "Prüfe das Kästchen einer beschädigten Bindung, bis die nächste Heimszene endet.",
"DG.Generic.ClearAll": "Proben löschen",
"DG.Generic.SanDamage": "Stabilitäts-Verlust",
"DG.ItemWindow.Weapons.CustomSkillTarget": "Ziel",
"DG.ItemWindow.Weapons.SkillModifier": "Fertigkeitsmodifikator",
"DG.ItemWindow.Custom": "Benutzerdefiniert",
"TYPES.Item.weapon": "Waffe",
"TYPES.Item.armor": "Rüstung",
"TYPES.Item.motivation": "Motivation",
"TYPES.Item.bond": "Bindung",
"TYPES.Item.gear": "Ausrüstung",
"DG.ItemWindow.Ritual.ActivationSanityDamage": "Activation SAN Damage",
"DG.ItemWindow.Ritual.ActivationTime": "Activation Time",
"DG.ItemWindow.Ritual.ActivationCosts": "Activation Costs",
"DG.ItemWindow.Ritual.StudyTime": "Study Time",
"DG.ItemWindow.Ritual.LearnedSanityDamage": "Learn SAN Damage",
"DG.Messages.Skillsmark.Marked": "Diese Fähigkeit ist gekennzeichnet.",
"DG.Messages.Skillsmark.Tooltip": "Siehe S. 29 des Agentenhandbuchs"
}
+192 -77
View File
@@ -1,11 +1,8 @@
{
"DG.SystemName": "Delta Green",
"DG.AgentNameFieldTooltip": "LAST NAME, FIRST NAME (AND ALIAS IF APPLICABLE)",
"DG.RollLuck": "Roll Luck",
"DG.Luck": "Luck",
"DG.FallbackText.newItem": "New {type}",
"DG.Skills.accounting": "Accounting",
"DG.Skills.alertness": "Alertness",
"DG.Skills.anthropology": "Anthropology",
@@ -44,7 +41,8 @@
"DG.Skills.unarmed_combat": "Unarmed Combat",
"DG.Skills.unnatural": "Unnatural",
"DG.Skills.flight": "Flight",
"DG.Skills.Tooltip.flight": "Fly or maneuver in air using wings or unnatural means.",
"DG.Skills.Tooltip.ritual": "Perform occult rituals; each ritual is a separate skill.",
"DG.Skills.Tooltip.accounting": "Spot financial red flags like hidden assets, shell companies, or embezzlement.",
"DG.Skills.Tooltip.alertness": "Notice hidden dangers, subtle movements, or quiet sounds before they become threats.",
"DG.Skills.Tooltip.anthropology": "Understand the beliefs, behaviors, and taboos of living cultures and societies.",
@@ -82,8 +80,9 @@
"DG.Skills.Tooltip.swim": "Survive in water under duress—rescue others, escape, or endure hazards.",
"DG.Skills.Tooltip.unarmed_combat": "Subdue or injure opponents using fists, grapples, kicks, and other hand-to-hand techniques.",
"DG.Skills.Tooltip.unnatural": "Recognize, research, or recall truths about entities and forces beyond human understanding.",
"DG.Skills.AddTypedOrCustomSkill": "Add Typed/Custom Skill",
"DG.Skills.ToggleEditDeleteSkills": "Edit/Delete Skills",
"DG.Skills.EditDeleteTypedSkills": "Edit/Delete Typed Skills",
"DG.Skills.AddSkill": "Add Skill",
"DG.Skills.Apply": "Apply",
"DG.Skills.ApplySkillImprovements.Title": "Apply Skill Improvements",
@@ -95,7 +94,6 @@
"DG.Skills.EditSkill": "Edit Skill",
"DG.Skills.SkillGroup": "Skill Group",
"DG.Skills.SkillName": "Skill Name",
"DG.TypeSkills.Art": "Art",
"DG.TypeSkills.Craft": "Craft",
"DG.TypeSkills.ForeignLanguage": "Foreign Language",
@@ -103,7 +101,6 @@
"DG.TypeSkills.Pilot": "Pilot",
"DG.TypeSkills.Science": "Science",
"DG.TypeSkills.Other": "Other",
"DG.TypeSkills.Tooltip.Art": "Create or appraise artistic works, and spot forgeries in a given field.",
"DG.TypeSkills.Tooltip.Craft": "Build, fix, or sabotage specialized tools or systems like engines, locks, or wiring.",
"DG.TypeSkills.Tooltip.ForeignLanguage": "Speak, read, and understand a specific language.",
@@ -111,7 +108,6 @@
"DG.TypeSkills.Tooltip.Pilot": "Control and navigate air, sea, or space vehicles in critical situations or conditions.",
"DG.TypeSkills.Tooltip.Science": "Apply specialized scientific knowledge to interpret or investigate the physical world.",
"DG.TypeSkills.Tooltip.Other": "Other",
"DG.SpecialTraining.Dialog.CreateSpecialTraining": "Add Special Training",
"DG.SpecialTraining.Dialog.DropDown.CustomSkills": "Custom/Typed Skills",
"DG.SpecialTraining.Dialog.DropDown.Skills": "Skills",
@@ -120,7 +116,6 @@
"DG.SpecialTraining.Dialog.NameLabel": "Name",
"DG.SpecialTraining.Dialog.SkillLabel": "Skill or Stat",
"DG.SpecialTraining.Dialog.Title": "Create/Edit Special Training",
"DG.Attributes.HP": "HP",
"DG.Attributes.WP": "WP",
"DG.Attributes.SAN": "SAN",
@@ -130,7 +125,6 @@
"DG.Attributes.int": "INT",
"DG.Attributes.pow": "POW",
"DG.Attributes.cha": "CHA",
"DG.Attributes.Tooltip.HP": "Hit Points (HP) indicate physical health, a character becomes unconsious at 2 HP, and dies at 0 HP.",
"DG.Attributes.Tooltip.WP": "Willpower Points (WP) indicate mental fortitude and drive. Agents require Willpower Points to keep going despite exhaustion or mentally taxing situations. See Page 47 of the Agent's Handbook for more detail.",
"DG.Attributes.Tooltip.SAN": "Sanity Points (SAN) indicate a connection with humanity and reality as most people perceive it. Click to roll a SAN test",
@@ -140,18 +134,22 @@
"DG.Attributes.Tooltip.int": "See Page 19 of the Agent's Handbook - Intelligence",
"DG.Attributes.Tooltip.pow": "See Page 19 of the Agent's Handbook - Power",
"DG.Attributes.Tooltip.cha": "See Page 19 of the Agent's Handbook - Charisma",
"DG.Navigation.Agent.skills": "Skills",
"DG.Navigation.Agent.combat": "Combat",
"DG.Navigation.Agent.physical": "Statistics",
"DG.Navigation.Agent.motivations": "Psychological",
"DG.Navigation.Agent.gear": "Equipment",
"DG.Navigation.Agent.personal": "Personal",
"DG.Navigation.Agent.effects": "Effects",
"DG.Navigation.Agent.LeftBar.rest": "Rest",
"DG.Navigation.Agent.LeftBar.sanity": "Sanity",
"DG.Navigation.Item.effects": "Effects",
"DG.Navigation.Agent.bio": "Bio",
"DG.Navigation.Agent.bonds": "Bonds",
"DG.Navigation.NPC.skills": "Skills & Training",
"DG.Navigation.NPC.gear": "Attacks & Abilities",
"DG.Navigation.Vehicle.gear": "Gear",
"DG.Navigation.Vehicle.notes": "Notes",
"DG.About.Changelog": "CHANGELOG (ONLINE)",
"DG.About.Copyright": "Published by Arc Dream Publishing by arrangement with the Delta Green Partnership. The intellectual property known as Delta Green is a trademark and copyright owned by the Delta Green Partnership, who has licensed its use here.",
"DG.About.License": "The contents of this system are licensed per the terms of the included <a href=\"systems/deltagreen/LICENSE.txt\">license</a> file, excepting those elements that are components of the Delta Green intellectual property.",
@@ -159,32 +157,74 @@
"DG.About.OnlineHelpTooltip": "Launch browser with online GitHub help.",
"DG.About.OnlineBugTracker": "Questions, Bug Reports or Feature Requests (ONLINE)",
"DG.About.OnlineBugTrackerTooltip": "Launch browser with GitHub issue tracker.",
"DG.Roll.Rolling": "Rolling",
"DG.Roll.Target": "Target: ",
"DG.About.ChangelogTooltip": "Launch browser with the online changelog.",
"DG.Roll.Success": "Success",
"DG.Roll.Failure": "Failure",
"DG.Roll.CriticalSuccess": "Critical Success!",
"DG.Roll.CriticalFailure": "Critical Failure!",
"DG.Roll.Lethality": "Lethality",
"DG.Roll.Lethal": "Lethal",
"DG.Roll.For": "For",
"DG.Roll.Rolling": "Rolling",
"DG.Roll.For": "for",
"DG.Roll.Damage": "Damage",
"DG.Roll.RollingDamageFor": "Rolling DAMAGE for",
"DG.Roll.Or": "Or",
"DG.Roll.Roll": "Roll",
"DG.Roll.CannotRollStat": "A statistic of 0 cannot be rolled.",
"DG.Physical.Wounds": "Wounds",
"DG.Physical.FirstAidLabel": "First Aid Has Already Been Attempted: ",
"DG.Physical.Exhausted": "Exhausted:",
"DG.Physical.ExhaustionPenalty": "Penalty:",
"DG.Physical.Exhaust": "Trigger Exhaustion",
"DG.Physical.Rest": "Rest",
"DG.Physical.Stimulants": "Stimulants",
"DG.Physical.SuppressExhaustion": "Exhaustion Suppressed",
"DG.Physical.ExhaustionEffectName": "Exhaustion",
"DG.Physical.StimulantsEffectName": "Stimulants",
"DG.Physical.ExhaustDialogTitle": "Exhaust Agent",
"DG.Physical.RollWillpowerLoss": "Roll Willpower Loss",
"DG.Physical.StimulantsDialogTitle": "Take Stimulants",
"DG.Physical.StimulantsRegular": "Regular Stimulants",
"DG.Physical.StimulantsRegularExplanation": "Chain-smoking, coffee, or other mild stimulants suppress exhaustion for 1D6 hours.",
"DG.Physical.StimulantsHard": "Hard Drugs",
"DG.Physical.StimulantsHardExplanation": "Hard drugs, while illegal, suppress exhaustion for 2D6 hours.",
"DG.Physical.Tooltip.SuppressExhaustion": "While checked, exhaustion penalties do not apply to rolls.",
"DG.Physical.StimulantsNotExhaustedWarning": "{name} is not exhausted. Taking stimulants would have no effect!",
"DG.Physical.Chat.ExhaustedRollLabel": "<b>Exhausted.</b><br>Lost {willpowerChange}.",
"DG.Physical.Chat.RestedRollLabel": "<b>Rested.</b><br>Regained {willpowerChange}.",
"DG.Physical.Chat.RestedRollLabelExhausted": "<b>Rested.</b><br>Regained {willpowerChange}.<br><b>No longer exhausted.</b>",
"DG.Physical.Chat.Willpower": "Willpower",
"DG.Physical.Chat.WillpowerAmount": "{amount} {willpower}",
"DG.Physical.Chat.WillpowerTooltip": "<b>Willpower:</b> {current}/{max}",
"DG.Physical.Chat.StimulantsRegularRollLabel": "<b>Regular Stimulants.</b><br>Exhaustion suppressed for <b>{hours}</b> hours.<br>Cannot sleep during this time.",
"DG.Physical.Chat.StimulantsHardRollLabel": "<b>Hard Drugs.</b><br>Exhaustion suppressed for <b>{hours}</b> hours.<br><b>Cannot sleep</b> during this time.",
"DG.Generic.AddButtonLabel": "Add",
"DG.ActiveEffects.SheetLabel": "Delta Green Active Effect",
"DG.ActiveEffects.DefaultName": "Active Effect",
"DG.ActiveEffects.Add": "Add Active Effect",
"DG.ActiveEffects.Temporary": "Temporary Effects",
"DG.ActiveEffects.Permanent": "Permanent Effects",
"DG.ActiveEffects.Source": "Source",
"DG.ActiveEffects.None": "None",
"DG.ActiveEffects.Toggle": "Toggle Active Effect",
"DG.ActiveEffects.Edit": "Edit Active Effect",
"DG.ActiveEffects.Delete": "Delete Active Effect",
"DG.ActiveEffects.SelectAttribute": "Select attribute…",
"DG.ActiveEffects.TransferWhenEquipped": "Transferred effects apply to the agent only while this item is equipped.",
"DG.ActiveEffects.TransferWhenAcuteEpisode": "Transferred effects apply to the agent only during an acute episode.",
"DG.ActiveEffects.SuppressedNotEquipped": "Inactive — item is not equipped",
"DG.ActiveEffects.SuppressedAcuteEpisode": "Inactive — acute episode is off",
"DG.ActiveEffects.SuppressedUnsupportedItem": "Inactive — not supported for this item type",
"DG.ActiveEffects.Groups.Statistics": "Statistics",
"DG.ActiveEffects.Groups.Resources": "Resources",
"DG.ActiveEffects.Groups.RollTargets": "Roll Targets",
"DG.ActiveEffects.Fields.AllSkills": "All Skills",
"DG.ActiveEffects.Fields.SanityRolls": "Sanity Rolls",
"DG.ActiveEffects.Fields.StatisticRolls": "Statistic Rolls",
"DG.ActiveEffects.Fields.MaxHP": "Maximum HP",
"DG.ActiveEffects.Fields.MaxWP": "Maximum Willpower",
"DG.ActiveEffects.Fields.MaxSanity": "Maximum Sanity",
"DG.Mental.BreakingPoint": "Breaking Point: ",
"DG.Mental.Tooltip.BreakingPoint": "When SAN falls to or below this number, Agent gains a long-term mental disorder; set a new Breaking Point to current SAN - POW. Disorders remain even if SAN later rises above a previous Breaking Point.",
"DG.Mental.Reset": "Reset",
"DG.Mental.SanityLossIncidentsLabel": "Incidents of SAN Loss Without Going Insane",
"DG.Mental.Violence": "Violence",
"DG.Mental.Tooltip.Violence": "Adapting to Violence: After losing SAN to violence three times without going insane, you become hardened to it. You automatically succeed SAN rolls against violence, but permanently lose 1D6 CHA and that much from each Bond. Agents Handbook Page 73.",
"DG.Mental.Tooltip.Checkbox.Violence": "When you lose SAN due to violence and without going temporarily insane or hitting your breaking point, increase until adapted. Agents Handbook Page 73.",
@@ -195,18 +235,17 @@
"DG.Mental.AdaptedToHelplessness": "Adapted to Helplessness",
"DG.Mental.Motivations": "Motivation",
"DG.Mental.Disorders": "Disorder",
"DG.Gear.SelectCompendium": "Select Compendium",
"DG.Gear.WeaponTypes.Firearms": "Firearms",
"DG.Gear.WeaponTypes.Melee": "Melee",
"DG.Gear.Weapons": "WEAPONS",
"DG.Gear.Armor": "ARMOR",
"DG.Gear.OtherGear": "OTHER GEAR",
"DG.Gear.Name": "Name",
"DG.Gear.DamageOrLethality": "Damage/Lethality",
"DG.Gear.DamageOrLethality": "Damage/ Lethality",
"DG.Gear.ArmorPiercing": "Armor Piercing",
"DG.Gear.ArmorRating": "Armor Rating",
"DG.Gear.Equipped": "Equipped",
"DG.CV.Sex": "Sex",
"DG.CV.SexPlaceholder": "Sex",
"DG.CV.AgeAndDoB": "Age and D.O.B.",
@@ -217,12 +256,8 @@
"DG.CV.NationalityPlaceholder": "Nationality",
"DG.CV.Education": "Education",
"DG.CV.EducationPlaceholder": "Education or Training",
"DG.Bonds.Bond": "Bond",
"DG.Bonds.Name": "Name",
"DG.Bonds.Score": "Score",
"DG.Bonds.NewBond": "New Bond",
"DG.Tooltip.MaximumHP": "Maximum health is the average of STR + CON, rounded UP.",
"DG.Tooltip.MaximumWP": "Maximum WP is equal to POW.",
"DG.Tooltip.CurrentSanityPartOne": "Current sanity, current breaking point is ",
@@ -233,23 +268,23 @@
"DG.Tooltip.SkillFailCheckbox": "After failing a test on this skill (if proficiency is at least 1%), check this box. At the end of the session, add 1D4 percentage points to the skill and uncheck the box. Does not apply to the Unnatural or Ritual skill. See Page 29 of the Agent's Handbook.",
"DG.Tooltip.DistinguishingFeature": "Distinguishing Feature (fill out if score is 13 or higher, or 8 or lower).",
"DG.Tooltip.x5AttributeLabel": "Click to roll a x5 check.",
"DG.Tooltip.RollTarget.Target": "Target: {value}%",
"DG.Tooltip.RollTarget.BaseTarget": "Base Target",
"DG.Tooltip.RollTarget.FinalTarget": "Final Target",
"DG.Tooltip.RollTarget.RollModifier": "Roll Modifier",
"DG.Tooltip.FirstAidAttempted": "Once an agent receives first aid, they cannot benefit from it again until they receive further damage from a different source.",
"DG.Tooltip.AdaptedToViolence": "Being adapted to violence means your Agent’s empathy suffers as they come to view violence as nothing but another tool to be wielded. Agent permanently loses 1D6 CHA and the same amount from each of their Bonds.",
"DG.Tooltip.AdaptedToHelplessness": "Being adapted to helplessness means your Agent’s personal drive suffers as they pull back from the world around them. As soon as your Agent becomes adapted to helplessness, agent permanently loses 1D6 POW.",
"DG.Tooltip.ToggleEquippedState": "Click to toggle whether this item is equipped or not.",
"DG.Tooltip.Exhaustion": "An Agent who works too long or faces extreme danger and injury without resting becomes exhausted. When this comes into play is up to the Handler, but a good rule of thumb is that going a night without sleep or refusing to rest after losing SAN or Hit Points leads to exhaustion.",
"DG.Gear.Expense.NA": "N/A or None",
"DG.Gear.Expense.Incidental": "Incidental",
"DG.Gear.Expense.Standard": "Standard",
"DG.Gear.Expense.Unusual": "Unusual",
"DG.Gear.Expense.Major": "Major",
"DG.Gear.Expense.Extreme": "Extreme",
"DG.ItemWindow.Bonds.Description": "Description/Developments",
"DG.ItemWindow.Bonds.ScoreTooltip": "A higher score represents a stronger relationship between the agent and the bond.",
"DG.ItemWindow.Motivations.Attributes": "Attributes",
"DG.ItemWindow.Motivations.Motivation": "Motivation",
"DG.ItemWindow.Motivations.Description": "Description/Notes",
"DG.ItemWindow.Motivations.Disorder": "Disorder",
@@ -259,19 +294,17 @@
"DG.ItemWindow.Motivations.IsDisorderTooltip": "Check this if adding a disorder, or if an existing motivation has been lost due to gaining a disorder.",
"DG.ItemWindow.Motivations.DisorderHasBeenCured": "Disorder Has Been \"Cured\"",
"DG.ItemWindow.Motivations.DisorderHasBeenCuredTooltip": "A critical success on a therapy luck roll can 'cure' a disorder. It may return the next time a breaking point is hit however. See Pages 78-79 of the Agent's Handbook - Therapy.",
"DG.ItemWindow.Gear.Attributes": "Attributes",
"DG.ItemWindow.Motivations.AcuteEpisode": "Acute Episode",
"DG.ItemWindow.Motivations.AcuteEpisodeTooltip": "While checked, transferred active effects on this motivation apply to the agent.",
"DG.ItemWindow.Gear.Description": "Description",
"DG.ItemWindow.Gear.NamePlaceholder": "Name",
"DG.ItemWindow.Gear.Expense": "Expense",
"DG.ItemWindow.Gear.Equipped": "Equipped",
"DG.ItemWindow.Armor.Description": "Description",
"DG.ItemWindow.Armor.NamePlaceholder": "Name",
"DG.ItemWindow.Armor.ArmorRating": "Armor Rating",
"DG.ItemWindow.Armor.Expense": "Expense",
"DG.ItemWindow.Armor.Equipped": "Equipped",
"DG.ItemWindow.Weapons.Description": "Description",
"DG.ItemWindow.Weapons.NamePlaceholder": "Name",
"DG.ItemWindow.Weapons.Skill": "Skill",
@@ -282,82 +315,76 @@
"DG.ItemWindow.Weapons.LethalityTooltip": "A lethal weapon has a chance to instantly kill a target. Roll a D100 instead of damage die. If it is not lethal, damage is dealt instead as if the result was a 2d10 roll, with a '00' on the first d10 equaling a 10.",
"DG.ItemWindow.Weapons.IsLethalityTooltip": "Check this if the weapon has a lethality % associated with it.",
"DG.ItemWindow.Weapons.ArmorPiercing": "Armor Piercing",
"DG.ItemWindow.Weapons.Ammo": "Ammo (Amount/Type)",
"DG.ItemWindow.Weapons.Ammo": "Ammo",
"DG.ItemWindow.Weapons.Range": "Range",
"DG.ItemWindow.Weapons.KillRadius": "Kill Radius",
"DG.ModifySkillRollDialogue.Title": "Modify Roll",
"DG.ModifySkillRollDialogue.Test": "Test:",
"DG.ModifySkillRollDialogue.OriginalTarget": "Original Target:",
"DG.ModifySkillRollDialogue.TargetModifier": "Modify Target:",
"DG.ModifySkillRollDialogue.ModifierTooltip": "Give a positive or negative modifier here to add/subtract from the target for the skill check.",
"DG.ModifySkillRollDialogue.ModifierTooltip": "Enter a positive value to increase the target, or a negative value to decrease it.",
"DG.ModifySkillRollDialogue.QuickModifiersHeader": "Quick Modifiers",
"DG.ModifySkillRollDialogue.QuickModifiersHint": "Click one of the following to apply a preset adjustment and roll immediately.",
"DG.ModifySkillRollDialogue.ModifierMuchHarder": "Makes the roll much harder",
"DG.ModifySkillRollDialogue.ModifierHarder": "Makes the roll harder",
"DG.ModifySkillRollDialogue.ModifierEasier": "Makes the roll easier",
"DG.ModifySkillRollDialogue.ModifierMuchEasier": "Makes the roll much easier",
"DG.ModifySkillRollDialogue.CustomModifierHeader": "Custom Modifier",
"DG.ModifySkillRollDialogue.CustomModifierHint": "For custom adjustments to the roll target, enter a positive or negative value and click the Roll button.",
"DG.ModifySkillRollDialogue.InvalidModifier": "Enter a valid modifier to roll.",
"DG.ModifyDamageRollDialogue.Weapon": "Weapon:",
"DG.ModifyDamageRollDialogue.OuterModifierTooltip": "Use this field to modify the outer part of the formula, such as to double the result.",
"DG.ModifyDamageRollDialogue.InnerModifierTooltip": "Use this field to add a flat modifier such as '+1' or another dice term such as '+1d4'.",
"DG.DamageOrLethalityRoll.Title": "Roll Damage or Lethality",
"DG.DamageOrLethalityRoll.Intro": "This weapon has both damage and lethality. Choose which to roll.",
"DG.DamageOrLethalityRoll.RollDamage": "Roll Damage",
"DG.DamageOrLethalityRoll.RollLethality": "Roll Lethality",
"DG.EditStats.Title": "Edit Statistics",
"DG.EditStats.Button": "Edit Statistics",
"DG.EditStats.DistinguishingFeature": "Distinguishing Feature",
"DG.Roll.Inhuman": "Inhuman",
"DG.Tooltip.BreakPoint": "The current breaking point, reset when current sanity falls to or below this value. The starting breaking point is SAN - POW.",
"DG.Tooltip.BreakPointReset": "Reset breaking point to be current SAN - WP.",
"DG.Skills.ritual": "Ritual",
"DG.RollMode": "Roll Mode",
"DG.Bonds.Relationship": "Relationship",
"DG.TypeSkills.Subskills.Painting": "Painting",
"DG.Sheet.BlockHeaders.Attributes": "ATTRIBUTES",
"DG.Sheet.BlockHeaders.Statistics": "STATISTICS",
"DG.Sheet.BlockHeaders.PhysicalDescriptionNotes": "PHYSICAL DESCRIPTION / NOTES",
"DG.Sheet.BlockHeaders.SkillsAndTraining": "SKILLS AND TRAINING",
"DG.Sheet.BlockHeaders.SpecialTraining": "SPECIAL TRAINING",
"DG.Sheet.BlockHeaders.Injuries": "INJURIES",
"DG.Sheet.BlockHeaders.BreakingPointAndAdaptations": "BREAKING POINT AND ADAPTATIONS",
"DG.Sheet.BlockHeaders.Adaptations": "Adaptations",
"DG.Sheet.BlockHeaders.MotivationsAndDisorders": "MOTIVATIONS AND DISORDERS",
"DG.Sheet.BlockHeaders.PersonalInfoCV": "PERSONAL INFORMATION / CV",
"DG.Sheet.BlockHeaders.ScenarioInformation": "SCENARIO INFORMATION",
"DG.Sheet.BlockHeaders.BondsRelationshipsContacts": "BONDS / RELATIONSHIPS / CONTACTS",
"DG.AgentSheet.ProfessionPlaceholder": "PROFESSION (RANK IF APPLICABLE)",
"DG.AgentSheet.ProfessionPlaceholder": "PROFESSION (RANK)",
"DG.AgentSheet.RenameProfession.Title": "Rename Profession",
"DG.AgentSheet.RenameProfession.Tooltip": "Click to rename profession",
"DG.UnnaturalSheet.HideUntrainedSkillsLabel": "Hide Untrained",
"DG.UnnaturalSheet.NotesLabel": "Notes",
"DG.UnnaturalSheet.ShortDescriptionPlaceholder": "Description of creature or creature type",
"DG.UnnaturalSheet.ShowUntrainedSkillsLabel": "Show Untrained",
"DG.UnnaturalSheet.Tabs.NotesTabLabel": "Skills & Notes",
"DG.UnnaturalSheet.Tabs.ItemsTabLabel": "Attacks & Abilities",
"DG.NPCSheet.BreakPointAbbreviation": "BP",
"DG.ImpossibleLanscapes.IHaveSeenThe": "I HAVE SEEN THE",
"DG.ImpossibleLanscapes.YellowSign": "YELLOW SIGN",
"DG.ImpossibleLanscapes.CorruptionFirstHalf": "Corrup",
"DG.ImpossibleLanscapes.CorruptionSecondHalf": "tion",
"DG.ImpossibleLanscapes.Gift": "Gift",
"DG.ImpossibleLanscapes.Insight": "Insight",
"DG.VehicleSheet.NotesTabName": "Notes",
"DG.VehicleSheet.GearTabName": "Gear",
"DG.VehicleSheet.NotesLabel": "NOTES",
"DG.VehicleSheet.DescriptionPlaceHolder": "Vehicle Description",
"DG.Generic.CheckDamagedBondsBoxUntilNextHomeScene": "Check a damaged Bond's box until the next Home Scene ends.",
"DG.Generic.ClearAll": "Clear Checks",
"DG.Generic.SanDamage": "SAN Damage",
"DG.ItemWindow.Weapons.CustomSkillTarget": "Target",
"DG.ItemWindow.Weapons.SkillModifier": "Skill Modifier",
"DG.ItemWindow.Custom": "Custom",
"DG.Sheet.class.item": "Item Sheet",
"DG.Sheet.class.agent": "Agent Sheet",
"DG.Sheet.class.npc": "NPC Sheet",
"DG.Sheet.class.unnatural": "Unnatural Sheet",
"DG.Sheet.class.vehicle": "Vehicle Sheet",
"TYPES.Actor.agent": "Agent",
"TYPES.Actor.npc": "NPC",
"TYPES.Actor.unnatural": "Unnatural",
"TYPES.Actor.vehicle": "Vehicle",
"TYPES.Item.armor": "Armor",
"TYPES.Item.bond": "Bond",
"TYPES.Item.gear": "Gear",
@@ -365,16 +392,104 @@
"TYPES.Item.weapon": "Weapon",
"TYPES.Item.tome": "Tome",
"TYPES.Item.ritual": "Ritual",
"TYPES.Item.profession": "Profession",
"DG.Profession.AlreadyAssigned": "This agent already has a profession. Another cannot be assigned.",
"DG.Profession.Dialog.Title": "Character Creation",
"DG.Profession.Dialog.Tab.SkillOptions": "Skill Selections",
"DG.Profession.Dialog.Tab.BonusSkills": "Bonus Skills",
"DG.Profession.Dialog.Tab.Bonds": "Bonds",
"DG.Profession.Dialog.Skill": "Skill",
"DG.Profession.Dialog.Value": "Value",
"DG.Profession.Dialog.SkillTotalsHeader": "Skill Totals",
"DG.Profession.Dialog.SkillTotalsHint": "Skills in green have been increased by your profession. Hover over a skill to see what it does or consult the Agent's Handbook for the full description.",
"DG.Profession.Dialog.SkillsOptionsHeader": "Skill Selections ({picks} picks)",
"DG.Profession.Dialog.BonusSkillsIntro": "Select skills from the dropdowns to add 20% to their total. A skill may be selected multiple times. Unnatural cannot be selected, nor can any skill be increased above 80% at character creation.",
"DG.Profession.Dialog.BonusSkillWasteTooHigh": "{skill} cannot be increased above 80. Select a different bonus skill.",
"DG.Profession.Dialog.BondsIntro": "Bonds are the important relationships in your agent's life, such as family, friends, and other loved ones. They can provide a buffer against SAN loss, at the cost of that relationship. Initial relationship score is equal to CHA.",
"DG.Profession.Dialog.BondFieldLabel": "Bond{number}",
"DG.Profession.Dialog.SelectSkill": "Select a skill…",
"DG.Profession.Dialog.SkillTypePlaceholder": "Type or specialization",
"DG.Profession.Dialog.Submit": "Submit",
"DG.Profession.Dialog.CapWarning": "{skill} is being raised to {attempted} but is capped at 80 during agent creation, wasting {waste} skill points.",
"DG.Profession.Dialog.DuplicateTypedName": "Duplicate {type} ({name}) detected. Please choose unique names.",
"DG.Profession.ChooseOnePlaceholder": "choose one",
"DG.Profession.Dialog.ChooseOneNamesHeader": "Name Skill Specializations",
"DG.Profession.Dialog.ChooseOneNameLabel": "{group} — specialization",
"DG.DamagedVeterans.Dialog.Title": "Damaged Veterans",
"DG.DamagedVeterans.Dialog.Intro": "Decide if your character is a fresh recruit or an experienced Delta Green Agent. If experienced, choose one of the other options to apply further modifications before starting play.",
"DG.DamagedVeterans.Dialog.FreshRecruit.Label": "Fresh Recruit:",
"DG.DamagedVeterans.Dialog.FreshRecruit.Body": "Your character is not yet part of Delta Green or has just joined. Start play with no modifications.",
"DG.DamagedVeterans.Dialog.ExtremeViolence.Label": "Extreme Violence:",
"DG.DamagedVeterans.Dialog.ExtremeViolence.Body": "+10 Occult, -5 SAN, -3 CHA, -3 to all Bonds. Start play Adapted to Violence.",
"DG.DamagedVeterans.Dialog.Captivity.Label": "Captivity or Imprisonment:",
"DG.DamagedVeterans.Dialog.Captivity.Body": "+10 Occult, -5 SAN, -3 POW. Start play Adapted to Helplessness.",
"DG.DamagedVeterans.Dialog.HardExperience.Label": "Hard Experience:",
"DG.DamagedVeterans.Dialog.HardExperience.Body": "+10 Occult, -5 SAN, remove one Bond. Gain +10 to any five skills other than Unnatural, to a maximum of 90%.",
"DG.DamagedVeterans.Dialog.ThingsMan.Label": "Things Man Was Not Meant to Know:",
"DG.DamagedVeterans.Dialog.ThingsMan.Body": "+10 Unnatural, +20 Occult, -SAN equal to POW. Start play with a Disorder caused by the Unnatural. Reset Breaking Point.",
"DG.DamagedVeterans.Dialog.SelectionRequired": "Select a damaged veteran option before submitting.",
"DG.DamagedVeterans.ThingsMan.Dialog.Title": "Things Man Was Not Meant to Know",
"DG.DamagedVeterans.ThingsMan.Dialog.Intro": "Choose one disorder with which to begin play. You can find the descriptions for these disorders on pages 71-72 of the Agent's Handbook.",
"DG.DamagedVeterans.ThingsMan.Disorder.Amnesia": "Amnesia",
"DG.DamagedVeterans.ThingsMan.Disorder.DepersonalizationDisorder": "Depersonalization Disorder",
"DG.DamagedVeterans.ThingsMan.Disorder.Depression": "Depression",
"DG.DamagedVeterans.ThingsMan.Disorder.DissociativeIdentityDisorder": "Dissociative Identity Disorder",
"DG.DamagedVeterans.ThingsMan.Disorder.Fugues": "Fugues",
"DG.DamagedVeterans.ThingsMan.Disorder.Megalomania": "Megalomania",
"DG.DamagedVeterans.ThingsMan.Disorder.Paranoia": "Paranoia",
"DG.DamagedVeterans.ThingsMan.Disorder.SleepDisorder": "Sleep Disorder",
"DG.DamagedVeterans.ThingsMan.Dialog.SelectionRequired": "Select a disorder before submitting.",
"DG.DamagedVeterans.ThingsMan.MotivationName": "Describe Motivation",
"DG.HardExperience.Dialog.Title": "Hard Experience",
"DG.HardExperience.Dialog.BonusIntro": "Select skills from the dropdowns to add 10% to their total. A skill may be selected multiple times. Unnatural cannot be selected, nor can any skill be increased above 90%.",
"DG.HardExperience.Dialog.BondRemovalHeader": "Remove One Bond",
"DG.HardExperience.Dialog.BondRemovalIntro": "Choose one bond to remove from your agent.",
"DG.HardExperience.Dialog.BondRemovalRequired": "Select a bond to remove before submitting.",
"DG.HardExperience.Dialog.BonusSkillRequired": "Select a skill in each of the five bonus skill dropdowns.",
"DG.HardExperience.Dialog.BonusSkillWasteTooHigh": "{skill} cannot be increased above 90. Select a different bonus skill.",
"DG.ProfessionSetup.PickStatistics.Title": "Pick Statistics",
"DG.ProfessionSetup.GoBack": "Go Back",
"DG.ProfessionSetup.PickStatistics.Intro": "Choose how to set this agent's statistics before picking skills.",
"DG.ProfessionSetup.PickStatistics.RollStats": "Roll Stats",
"DG.ProfessionSetup.PickStatistics.RollStatsTooltip": "Roll 4D6, drop lowest, then assign as desired.",
"DG.ProfessionSetup.PickStatistics.AssignStats": "Assign Stats",
"DG.ProfessionSetup.PickStatistics.AssignStatsTooltip": "Divide 72 points between stats as desired.",
"DG.ProfessionSetup.PickStatistics.LeaveStatsAlone": "Leave my stats alone!",
"DG.ProfessionSetup.RollStats.Title": "Roll Stats",
"DG.ProfessionSetup.RollStats.PoolHeader": "Drag each rolled value onto a stat.",
"DG.ProfessionSetup.RollStats.Submit": "Submit",
"DG.ProfessionSetup.RollStats.ChatLabel": "Stat Rolls (4d6, drop lowest)",
"DG.ProfessionSetup.AssignStats.Title": "Assign Stats",
"DG.ProfessionSetup.AssignStats.PointsRemaining": "Points remaining: {remaining}",
"DG.ProfessionSetup.AssignStats.Submit": "Submit",
"DG.ItemWindow.Profession.Description": "Description",
"DG.ItemWindow.Profession.SkillsTab": "Skills",
"DG.ItemWindow.Profession.Name": "Profession Name",
"DG.ItemWindow.Profession.Bonds": "Bonds",
"DG.ItemWindow.Profession.BondsTooltip": "Number of bond slots granted when this profession is assigned to an agent (1 – 5).",
"DG.ItemWindow.Profession.AutomaticSkills": "Automatic Skills",
"DG.ItemWindow.Profession.OptionSkills": "Skill Options",
"DG.ItemWindow.Profession.OptionPicks": "Option Picks",
"DG.ItemWindow.Profession.AddSkill": "Add Skill",
"DG.ItemWindow.Profession.AddSkillTitle": "Add Skill to Profession",
"DG.ItemWindow.Profession.RemoveSkill": "Remove Skill",
"DG.ItemWindow.Profession.Skill": "Skill",
"DG.ItemWindow.Profession.SelectSkill": "Select a skill…",
"DG.ItemWindow.Profession.Rating": "Rating (%)",
"DG.ItemWindow.Profession.SkillType": "Skill Type",
"DG.ItemWindow.Profession.SkillTypePlaceholder": "e.g. German, Painting",
"DG.ItemWindow.Profession.TypedSkillRequired": "Enter a skill type before adding this skill.",
"DG.ItemWindow.Profession.ChooseOne": "Choose One",
"DG.ItemWindow.Profession.ChooseOneHint": "The player will name this skill when the profession is applied to an agent.",
"DG.ItemWindow.Profession.TypedSkillNameConflict": "Another automatic or option skill in this category already uses the name \"{name}\".",
"DG.Gear.Tomes": "TOMES",
"DG.Gear.Rituals": "RITUALS",
"DG.Tomes.Name": "Name",
"DG.Ritual.Name": "Name",
"DG.Tome.Name": "Name",
"DG.Tome.Language": "Language",
"DG.Tome.StudyTime": "Study Time",
"DG.ItemWindow.Tome.NamePlaceholder": "Name",
"DG.ItemWindow.Tome.Description": "Description",
"DG.ItemWindow.Tome.LanguagePlaceholder": "Language",
"DG.ItemWindow.Tome.StudyTime": "Study Time",
@@ -383,15 +498,12 @@
"DG.ItemWindow.Tome.StudySanityDamage": "Study Sanity Damage",
"DG.ItemWindow.Tome.HandlerOnlyTabHeader": "Handler Only",
"DG.ItemWindow.Tome.HandlerNotes": "Handler Notes",
"DG.ItemWindow.Ritual.ActivationSanityDamage": "Activation SAN Damage",
"DG.ItemWindow.Ritual.ActivationTime": "Activation Time",
"DG.ItemWindow.Ritual.ActivationCosts": "Activation Costs",
"DG.ItemWindow.Ritual.StudyTime": "Study Time",
"DG.ItemWindow.Ritual.LearnedSanityDamage": "Learn SAN Damage",
"DG.MissionPaused": "Mission Paused",
"DG.Settings.charactersheet.name": "Character Sheet Style",
"DG.Settings.charactersheet.hint": "Choose how actor sheets should be styled. 'Program' is a more modern government style, where 'Cowboy/Outlaw' is an older, grittier looking typewriter style.",
"DG.Settings.charactersheet.cowboys": "The Cowboys",
@@ -419,15 +531,18 @@
"DG.Settings.skillFailure.name": "Auto-mark Skill Failure",
"DG.Settings.skillFailure.hint": "Activate this setting to always mark a failure checkbox upon failing a skill. The Handler can undo the action from the chat message for the roll.",
"DG.Settings.Saved": "The current settings have been saved.",
"DG.SettingsMenu.automation.name": "Automation Menu",
"DG.SettingsMenu.automation.label": "Automation Menu",
"DG.SettingsMenu.automation.title": "Automation Settings",
"DG.SettingsMenu.handler.name": "Handler Settings",
"DG.SettingsMenu.handler.label": "Handler Settings",
"DG.SettingsMenu.handler.title": "Handler-only Settings",
"DG.Messages.Skillsmark.Marked": "This skill is marked",
"DG.Messages.Skillsmark.Tooltip": "See p. 29 Agent's Handbook"
"DG.Messages.Skillsmark.Tooltip": "See p. 29 Agent's Handbook",
"DG.Profession.Dialog.OptionPicksRequired": "Select exactly {picks} skill option(s).",
"DG.Profession.Dialog.BondNameRequired": "Enter a name for each bond.",
"DG.Profession.Dialog.BondRelationshipRequired": "Enter a relationship for each bond.",
"DG.Profession.Dialog.BonusSkillRequired": "Select all {count} bonus skills.",
"DG.Profession.Dialog.BonusSkillTypeRequired": "Enter a specialization for each typed bonus skill.",
"DG.Profession.Dialog.TypedNameRequired": "Enter a name for each {type} skill."
}
-51
View File
@@ -3,9 +3,7 @@
"DG.AgentNameFieldTooltip": "Apellido, nombre, alias o nombre en código",
"DG.RollLuck": "Tira Suerte",
"DG.Luck": "Suerte",
"DG.FallbackText.newItem": "Nuevo {type}",
"DG.Skills.accounting": "Contabilidad",
"DG.Skills.alertness": "Alerta",
"DG.Skills.anthropology": "Antropología",
@@ -44,7 +42,6 @@
"DG.Skills.unarmed_combat": "Combate sin armas",
"DG.Skills.unnatural": "Antinatural",
"DG.Skills.flight": "Volar",
"DG.Skills.Tooltip.accounting": "El estudio de las finanzas y los negocios. Úsala para localizar anomalías en informes financieros como, por ejemplo, una cuenta bancaria oculta o blanqueo de dinero.",
"DG.Skills.Tooltip.alertness": "La Alerta detecta el peligro. Úsala para escuchar cómo se quita el seguro de un arma, para entender los balbuceos al otro lado de una pared, para detectar el bulto de una pistola debajo de una chaqueta o para atrapar a alguien que está intentando escabullirse mediante Sigilo.",
"DG.Skills.Tooltip.anthropology": "El estudio de las culturas humanas vivas. Úsala para entender valores morales, creencias religiosas y costumbres, y para identificar (pero no traducir) idiomas desconocidos. Mientras que la Historia trata sobre el pasado y la Arqueología estudia los restos físicos, la Antropología trata sobre el comportamiento de las culturas vivas, cómo se relacionan con el resto y su punto de vista acerca de las otras y del pasado.",
@@ -82,7 +79,6 @@
"DG.Skills.Tooltip.swim": "La mayoría de los Agentes saben nadar como pasatiempo. Usa la habilidad Natación durante una crisis peligrosa: recorrer una larga distancia en aguas movidas, evitar que alguien se ahogue o subirte a un barco antes de que la cosa con tentáculos de ahí debajo te agarre.",
"DG.Skills.Tooltip.unarmed_combat": "Autodefensa. Una pelea entre combatientes no entrenados a menudo implica más empujones y gritos que violencia real. Usa el Combate sin armas para lastimar o matar a un oponente con las manos de tu Agente (o los pies, codos, dientes o cabeza).",
"DG.Skills.Tooltip.unnatural": "Conocimiento acerca de los secretos más fundamentales y desgarradores del universo. Úsala para recordar, reconocer o investigar hechos sobre todo aquello que los humanos consideran antinatural. Esto va mucho más allá del ocultismo, porque la habilidad Antinatural representa cosas que existen de verdad. Utilízala para poder filtrar información de los confines más oscuros de los mitos y el folclore y reconocer qué partes son ciertas. El valor de COR de tu Agente nunca puede ser superior a 99 menos su valor de habilidad Antinatural.",
"DG.Skills.AddTypedOrCustomSkill": "Añadir tipo o habilidad personalizada",
"DG.Skills.AddSkill": "Añadir habilidad",
"DG.Skills.Apply": "Aplicar",
@@ -95,7 +91,6 @@
"DG.Skills.EditSkill": "Editar habilidad",
"DG.Skills.SkillGroup": "Habilidad de grupo",
"DG.Skills.SkillName": "Nombre de la habilidad",
"DG.TypeSkills.Art": "Arte",
"DG.TypeSkills.Craft": "Oficio",
"DG.TypeSkills.ForeignLanguage": "Idioma extranjero",
@@ -103,7 +98,6 @@
"DG.TypeSkills.Pilot": "Pilotar",
"DG.TypeSkills.Science": "Ciencia",
"DG.TypeSkills.Other": "Otra",
"DG.TypeSkills.Tooltip.Art": "Pericia en la creación o representación de una obra que genera emoción y opinión. También incluye el conocimiento de técnicas y tendencias en tu campo, y la capacidad de distinguir las piezas reales de las falsas de un creador concreto. Cualquiera puede dibujar un boceto, pero la habilidad Arte refleja conocimiento, práctica y talento. Cada tipo de Arte es una habilidad independiente: Actuación, Canto, Danza, Escritura creativa, Escritura de guiones, Escultura, Falsificación, Flauta, Guitarra, Pintura, Poesía, Violín, etc.",
"DG.TypeSkills.Tooltip.Craft": "Creación y reparación de herramientas y estructuras sofisticadas. Un trabajo que la mayoría de gente podría averiguar cómo se hace no requiere la habilidad Oficio, solo una prueba de INT o DES. Usa Oficio para el trabajo especializado que requiere formación: Oficio (Electricista) para cambiar el cableado a una casa, hacer el puente a un vehículo con rapidez, intervenir un teléfono o una línea de datos o identificar señales de sabotaje eléctrico; Oficio (Mecánica) para ensamblar apresuradamente una máquina o hacer que un motor averiado vuelva a funcionar… o para sabotear uno de manera que no se pueda volver a reparar; Oficio (Cerrajería) para abrir una cerradura sin la llave; Oficio (Armería) para reparar un arma de fuego estropeada. El Supervisor decide si una tarea concreta requiere un Oficio o no. Cada tipo de Oficio es una habilidad independiente: Armería, Arquitectura, Carpintería, Cerrajería, Electricista, Fontanería, Mecánica, Microelectrónica, etc.",
"DG.TypeSkills.Tooltip.ForeignLanguage": "Soltura con otro idioma. Cada idioma extranjero es una habilidad independiente. Tener un 20% permite comunicar ideas sencillas; al llegar a 50%, tu Agente habla y lee como un nativo. Cuanto mayor es el valor de tu habilidad, mayor es la complejidad de información que tu Agente comprende y menor el tiempo que necesita para ello. No necesitas tirar los dados a no ser que el Supervisor diga que la situación es excepcionalmente difícil. A discreción del Supervisor, tener formación especial podría permitir el uso de la misma habilidad con un idioma de la misma familia.",
@@ -111,7 +105,6 @@
"DG.TypeSkills.Tooltip.Pilot": "Pilotar, manejar y capitanear vehículos marítimos, aéreos o espaciales. Úsala para mantener una nave a salvo en una crisis como, por ejemplo, una tormenta o una persecución peligrosa. Cada tipo de nave es una habilidad independiente: Avión, Dron, Helicóptero, Barca, Barco, Lanzadera espacial, etc. A discreción del Supervisor, la habilidad con un tipo de nave podría permitir pilotar otro tipo de nave relacionado.",
"DG.TypeSkills.Tooltip.Science": "El estudio profundo de los procesos que rigen nuestro mundo. Esta habilidad implica más que la educación común del colegio: cualquier persona puede hacer una prueba de INT para recordar algo de una clase del instituto. La Ciencia se usa para dar con un conocimiento clave sobre el modo en que funciona el universo… o, al menos, el modo en que debería funcionar. Cada Ciencia es una habilidad independiente: Astronomía, Biología, Botánica, Ciencias Planetarias, Física, Genética, Geología, Ingeniería, Matemáticas, Meteorología, Química, Zoología, etc.",
"DG.TypeSkills.Tooltip.Other": "Otra",
"DG.SpecialTraining.Dialog.CreateSpecialTraining": "Añadir entrenamiento especial",
"DG.SpecialTraining.Dialog.DropDown.CustomSkills": "Habilidades personalizadas/de tipo",
"DG.SpecialTraining.Dialog.DropDown.Skills": "Habilidades",
@@ -120,7 +113,6 @@
"DG.SpecialTraining.Dialog.NameLabel": "Nombre",
"DG.SpecialTraining.Dialog.SkillLabel": "Habilidad o característica",
"DG.SpecialTraining.Dialog.Title": "Crear/editar entrenamiento especial",
"DG.Attributes.HP": "Puntos de salud (PS)",
"DG.Attributes.WP": "Puntos de voluntad (PV)",
"DG.Attributes.SAN": "Puntos de Cordura (COR)",
@@ -130,7 +122,6 @@
"DG.Attributes.int": "INT",
"DG.Attributes.pow": "POD",
"DG.Attributes.cha": "CAR",
"DG.Attributes.Tooltip.HP": "Los puntos de Salud representan la cantidad de daño que tu Agente puede soportar. Cuando resulte herido, resta el daño de sus PS. Cuando tu Agente llegue a 2 PS, queda inconsciente. Cuando tu Agente llegue a 0 PS, muere. Los puntos de Salud se pueden recuperar mediante reposo y atención médica.",
"DG.Attributes.Tooltip.WP": "Los puntos de Voluntad representan la fortaleza mental, y disminuyen cuando tu Agente trata de suprimir los síntomas de las enfermedades mentales (consulta RESISTIRSE A LA DEMENCIA en la página 73), cuando acaba agotado (consulta AGOTAMIENTO en la página 47), cuando intenta resistir contra la persuasión (consulta INTERROGATORIO en la página 168), cuando acaba quemado a nivel emocional (consulta PIFIA en la página 44) o cuando trata de avivar fenómenos antinaturales. Cuando tu Agente llegue a 2 PV, sufre un colapso emocional transitorio. Cuando tu Agente llegue a 0 PV, queda inconsciente. Los puntos de Voluntad se pueden recuperar mediante el reposo (consulta PUNTOS DE VOLUNTAD en la página 47).",
"DG.Attributes.Tooltip.SAN": "Los puntos de Cordura representan la salud mental o, dicho de otra manera, la cantidad de traumas psicológicos o exposición a lo Antinatural que tu Agente puede soportar antes de caer en la demencia. En los momentos de trauma psicológico, tu Agente debe hacer una prueba de Cordura buscando un resultado igual o inferior a su COR con 1D100. Si la tirada falla, tu Agente pierde COR (consulta CORDURA en la página 64). Los puntos de Cordura se pueden recuperar mediante terapia psiquiátrica o la neutralización de amenazas antinaturales. Cuando tu Agente baje hasta los 0 puntos de Cordura, entra en un estado de demencia irreversible y se convierte en un PNJ de manera permanente bajo el control del Supervisor.",
@@ -140,7 +131,6 @@
"DG.Attributes.Tooltip.int": "La capacidad de un Agente para recordar, conectar o darse cuenta de las cosas. Junto a su profesión, indica su educación y brillantez en general.",
"DG.Attributes.Tooltip.pow": "Fuerza de personalidad, motivación y resiliencia física.",
"DG.Attributes.Tooltip.cha": "Encanto, liderazgo y atractivo personal. Podría indicar atractivo físico.",
"DG.Navigation.Agent.skills": "Habilidades",
"DG.Navigation.Agent.physical": "Características",
"DG.Navigation.Agent.motivations": "Salud mental",
@@ -151,7 +141,6 @@
"DG.Navigation.NPC.gear": "Ataques y atributos",
"DG.Navigation.Vehicle.gear": "Equipo",
"DG.Navigation.Vehicle.notes": "Notas",
"DG.About.Changelog": "Registro de cambios (en línea)",
"DG.About.Copyright": "Publicado por Arc Dream Publishing de acuerdo a la Delta Green Partnership. La propiedad intelectual conocida como Delta Green es una marca comercial y derechos de autor de la Delta Green Partnership, que ha licenciado su uso en este sistema.",
"DG.About.License": "Los contenidos de este sistema están licenciados según los términos incluidos en este <a href=\"systems/deltagreen/LICENSE.txt\">documento</a>, excepto aquellos elementos que son parte de la propiedad intelectual de Delta Green.",
@@ -159,9 +148,7 @@
"DG.About.OnlineHelpTooltip": "Abre la página de GitHub en el navegador con la página de ayuda.",
"DG.About.OnlineBugTracker": "Preguntas, errores y petición de nuevas funcionalidades.",
"DG.About.OnlineBugTrackerTooltip": "Abre la página de GitHub en el navegador con la página de notificación de errores.",
"DG.Roll.Rolling": "Tirando",
"DG.Roll.Target": "Objetivo: ",
"DG.Roll.Success": "Éxito",
"DG.Roll.Failure": "Fallo",
"DG.Roll.CriticalSuccess": "¡Crítico!",
@@ -173,14 +160,11 @@
"DG.Roll.RollingDamageFor": "Tirando daño para",
"DG.Roll.Or": "O",
"DG.Roll.Roll": "Tirar",
"DG.Physical.Wounds": "Heridas",
"DG.Physical.FirstAidLabel": "Ya se han aplicado primeros auxilios: ",
"DG.Physical.Exhausted": "Exhausto:",
"DG.Physical.ExhaustionPenalty": "Penalizador:",
"DG.Generic.AddButtonLabel": "Añadir",
"DG.Mental.BreakingPoint": "Punto de ruptura: ",
"DG.Mental.Tooltip.BreakingPoint": "Cuando se falla una tirada de COR y la tirada es igual o está por debajo de este valor, el Agente gana un nuevo trastorno mental; ajusta el nuevo punto de ruptura a la COR actual - POD. Los trastornos se mantienen incluso si la COR vuelve a subir por encima del punto de ruptura anterior.",
"DG.Mental.Reset": "Borrar",
@@ -193,7 +177,6 @@
"DG.Mental.AdaptedToHelplessness": "Adaptado a la indefensión",
"DG.Mental.Motivations": "Motivaciones",
"DG.Mental.Disorders": "Desórdenes",
"DG.Gear.WeaponTypes.Firearms": "Armas de fuego",
"DG.Gear.WeaponTypes.Melee": "Cuerpo a cuerpo",
"DG.Gear.Weapons": "ARMAS",
@@ -204,7 +187,6 @@
"DG.Gear.ArmorPiercing": "Perforación de armadura",
"DG.Gear.ArmorRating": "Clasificación de armadura",
"DG.Gear.Equipped": "Equipado",
"DG.CV.Sex": "Género",
"DG.CV.SexPlaceholder": "Género",
"DG.CV.AgeAndDoB": "Edad y f. de nac.",
@@ -215,12 +197,10 @@
"DG.CV.NationalityPlaceholder": "Nacionalidad",
"DG.CV.Education": "Educación",
"DG.CV.EducationPlaceholder": "Educación y entrenamiento",
"DG.Bonds.Bond": "Lazo",
"DG.Bonds.Name": "Nombre",
"DG.Bonds.Score": "Valor",
"DG.Bonds.NewBond": "Nuevo lazo",
"DG.Tooltip.MaximumHP": "La salud máxima es el promedio de FUE + CON, redondeado hacia arriba.",
"DG.Tooltip.MaximumWP": "Los puntos de voluntad máximos son iguales al POD.",
"DG.Tooltip.CurrentSanityPartOne": "Cordura actual, el punto de ruptura actual es ",
@@ -236,17 +216,14 @@
"DG.Tooltip.AdaptedToHelplessness": "Estar adaptado a la indefensión significa que el impulso personal de su Agente sufre cuando se aleja del mundo que lo rodea. Tan pronto como su Agente se adapte a la indefensión, perderá permanentemente 1D6 POD.",
"DG.Tooltip.ToggleEquippedState": "Haga clic para alternar si este artículo está equipado o no.",
"DG.Tooltip.Exhaustion": "Un Agente que trabaja demasiado o se enfrenta a peligros extremos sin descansar queda exhausto. El que ocurra o no queda a criterio del Controlador, pero como guía, cualquiera que permanezca despierto toda la noche sin dormir o no descanse tras haber perdido COR o puntos de vida quedará exhausto.",
"DG.Gear.Expense.NA": "N/A o ninguno",
"DG.Gear.Expense.Incidental": "Imprevisto",
"DG.Gear.Expense.Standard": "Corriente",
"DG.Gear.Expense.Unusual": "Extraordinario",
"DG.Gear.Expense.Major": "Considerable",
"DG.Gear.Expense.Extreme": "Extremo",
"DG.ItemWindow.Bonds.Description": "Descripción/desarrollo",
"DG.ItemWindow.Bonds.ScoreTooltip": "Una puntuación más alta representa una relación más fuerte entre el agente y el lazo.",
"DG.ItemWindow.Motivations.Attributes": "Atributos",
"DG.ItemWindow.Motivations.Motivation": "Motivación",
"DG.ItemWindow.Motivations.Description": "Descripción/notas",
@@ -257,19 +234,16 @@
"DG.ItemWindow.Motivations.IsDisorderTooltip": "Marque esta opción si agrega un trastorno o si se ha perdido una motivación existente debido a la adquisición de un trastorno.",
"DG.ItemWindow.Motivations.DisorderHasBeenCured": "El trastorno ha sido curado.",
"DG.ItemWindow.Motivations.DisorderHasBeenCuredTooltip": "Un éxito crítico en una tirada de suerte terapéutica puede 'curar' un trastorno. Sin embargo, puede regresar la próxima vez que se alcance un punto de ruptura. Consulte la sección Ir a terapia en las páginas 78-79 del Manual del Agente.",
"DG.ItemWindow.Gear.Attributes": "Atributos",
"DG.ItemWindow.Gear.Description": "Descripción",
"DG.ItemWindow.Gear.NamePlaceholder": "Nombre",
"DG.ItemWindow.Gear.Expense": "Gasto",
"DG.ItemWindow.Gear.Equipped": "Equipado",
"DG.ItemWindow.Armor.Description": "Descripción",
"DG.ItemWindow.Armor.NamePlaceholder": "Nombre",
"DG.ItemWindow.Armor.ArmorRating": "Armadura",
"DG.ItemWindow.Armor.Expense": "Gasto",
"DG.ItemWindow.Armor.Equipped": "Equipado",
"DG.ItemWindow.Weapons.Description": "Descripción",
"DG.ItemWindow.Weapons.NamePlaceholder": "Nombre",
"DG.ItemWindow.Weapons.Skill": "Habilidad",
@@ -283,27 +257,21 @@
"DG.ItemWindow.Weapons.Ammo": "Munición (cantidad/tipo)",
"DG.ItemWindow.Weapons.Range": "Alcance",
"DG.ItemWindow.Weapons.KillRadius": "Radio muerte",
"DG.ModifySkillRollDialogue.Title": "Modificar tirada",
"DG.ModifySkillRollDialogue.Test": "Tirada",
"DG.ModifySkillRollDialogue.OriginalTarget": "Valor original",
"DG.ModifySkillRollDialogue.TargetModifier": "Modificador",
"DG.ModifySkillRollDialogue.ModifierTooltip": "Dar un modificador positivo o negativo aquí para sumar o restar al objetivo en la chequeo de habilidades.",
"DG.ModifyDamageRollDialogue.Weapon": "Arma:",
"DG.ModifyDamageRollDialogue.OuterModifierTooltip": "Utilice este campo para modificar la fórmula, como duplicar el resultado.",
"DG.ModifyDamageRollDialogue.InnerModifierTooltip": "Utilice este campo para añadir un modificador como '+1' u otro término como '+1d4'.",
"DG.Roll.Inhuman": "Inhumano",
"DG.Tooltip.BreakPoint": "El punto de ruptura actual, actualizar cuando la Cordura actual sea igual o inferior a este valor. El punto de ruptura inicial es COR - POD.",
"DG.Tooltip.BreakPointReset": "Actualizar el Punto de Ruptura al valor actual de COR - PVol.",
"DG.Skills.ritual": "Ritual",
"DG.RollMode": "Modo de tirada",
"DG.Bonds.Relationship": "Relación",
"DG.TypeSkills.Subskills.Painting": "Pintar",
"DG.Sheet.BlockHeaders.Attributes": "ATRIBUTOS",
"DG.Sheet.BlockHeaders.Statistics": "CARACTERÍSTICAS",
"DG.Sheet.BlockHeaders.PhysicalDescriptionNotes": "DESCRIPCIÓN FÍSICA Y NOTAS",
@@ -313,9 +281,7 @@
"DG.Sheet.BlockHeaders.MotivationsAndDisorders": "MOTIVACIONES Y TRANSTORNOS MENTALES",
"DG.Sheet.BlockHeaders.PersonalInfoCV": "INFORMACIÓN PERSONAL Y CURRÍCULO",
"DG.Sheet.BlockHeaders.BondsRelationshipsContacts": "LAZOS, RELACIONES Y CONTACTOS",
"DG.AgentSheet.ProfessionPlaceholder": "Profesión (rango, si es necesario)",
"DG.UnnaturalSheet.HideUntrainedSkillsLabel": "Ocultar habilidades no entrenadas",
"DG.UnnaturalSheet.NotesLabel": "Notas",
"DG.UnnaturalSheet.ShortDescriptionPlaceholder": "Descripción de la criatura o tipo",
@@ -323,39 +289,31 @@
"DG.UnnaturalSheet.Tabs.NotesTabLabel": "Habilidades & notas",
"DG.UnnaturalSheet.Tabs.ItemsTabLabel": "Ataques & habilidades",
"DG.NPCSheet.BreakPointAbbreviation": "PR",
"DG.ImpossibleLanscapes.IHaveSeenThe": "He visto el ",
"DG.ImpossibleLanscapes.YellowSign": "Signo Amarillo",
"DG.ImpossibleLanscapes.CorruptionFirstHalf": "Corrup",
"DG.ImpossibleLanscapes.CorruptionSecondHalf": "ción",
"DG.ImpossibleLanscapes.Gift": "Don",
"DG.ImpossibleLanscapes.Insight": "Visión",
"DG.VehicleSheet.NotesTabName": "Notas",
"DG.VehicleSheet.GearTabName": "Equipo",
"DG.VehicleSheet.NotesLabel": "Notas",
"DG.VehicleSheet.DescriptionPlaceHolder": "Descripción del vehículo",
"DG.Generic.CheckDamagedBondsBoxUntilNextHomeScene": "Marca el cuadro de comprobación de lazo dañado hasta que termine la próxima escena de hogar.",
"DG.Generic.ClearAll": "Borrar todo",
"DG.Generic.SanDamage": "Daño COR",
"DG.ItemWindow.Weapons.CustomSkillTarget": "Objetivo",
"DG.ItemWindow.Weapons.SkillModifier": "Mod. habilidad",
"DG.ItemWindow.Custom": "Personalizado",
"DG.Sheet.class.item": "Hoja de objeto",
"DG.Sheet.class.agent": "Hoja de agente",
"DG.Sheet.class.npc": "Hoja de PNJ",
"DG.Sheet.class.unnatural": "Hoja de antinatural",
"DG.Sheet.class.vehicle": "Hoja de vehículo",
"TYPES.Actor.agent": "Agente",
"TYPES.Actor.npc": "PNJ",
"TYPES.Actor.unnatural": "Antinatural",
"TYPES.Actor.vehicle": "Vehículo",
"TYPES.Item.armor": "Armadura",
"TYPES.Item.bond": "Lazo",
"TYPES.Item.gear": "Equipo",
@@ -363,13 +321,10 @@
"TYPES.Item.weapon": "Arma",
"TYPES.Item.tome": "Tomo",
"TYPES.Item.ritual": "Ritual",
"DG.Gear.Tomes": "TOMOS",
"DG.Gear.Rituals": "RITUALES",
"DG.Tomes.Name": "Nombre",
"DG.Ritual.Name": "Nombre",
"DG.Tome.Name": "Nombre",
"DG.Tome.Language": "Idioma",
"DG.Tome.StudyTime": "Tiempo de estudio",
@@ -381,15 +336,12 @@
"DG.ItemWindow.Tome.StudySanityDamage": "Pérdida de Cordura al estudiarlo",
"DG.ItemWindow.Tome.HandlerOnlyTabHeader": "Datos | Controlador",
"DG.ItemWindow.Tome.HandlerNotes": "Notas | Controlador",
"DG.ItemWindow.Ritual.ActivationSanityDamage": "COR activación",
"DG.ItemWindow.Ritual.ActivationTime": "Tiempo de activación",
"DG.ItemWindow.Ritual.ActivationCosts": "Coste de activación",
"DG.ItemWindow.Ritual.StudyTime": "Tiempo de estudio",
"DG.ItemWindow.Ritual.LearnedSanityDamage": "COR aprendizaje",
"DG.MissionPaused": "Misión en pausa",
"DG.Settings.charactersheet.name": "Estilo de hoja de personaje",
"DG.Settings.charactersheet.hint": "Elegir el estilo de las hojas de personaje. 'Programa' se ajusta a una agencia gubernamental moderna, mientras que 'Vaquero/Forajido' es antiguo, crudo, estilo vieja máquina de escribir.",
"DG.Settings.charactersheet.cowboys": "Los vaqueros",
@@ -417,15 +369,12 @@
"DG.Settings.skillFailure.name": "Marcar automáticamente fallo en habilidad",
"DG.Settings.skillFailure.hint": "Active este ajuste para activar la casilla al fallar una habilidad. El Supervisor puede deshacer esta acción desde la tarjeta del chat de la tirada.",
"DG.Settings.Saved": "Los ajustes han sido guardados.",
"DG.SettingsMenu.automation.name": "Menú de automatización",
"DG.SettingsMenu.automation.label": "Menú de automatización",
"DG.SettingsMenu.automation.title": "Menú de ajustes",
"DG.SettingsMenu.handler.name": "Ajustes del Supervisor",
"DG.SettingsMenu.handler.label": "Ajustes del Supervisor",
"DG.SettingsMenu.handler.title": "Ajustes solo para el Supervisor",
"DG.Messages.Skillsmark.Marked": "Habilidad marcada",
"DG.Messages.Skillsmark.Tooltip": "Consulte la página 29 del Manual del Agente"
}
-51
View File
@@ -3,9 +3,7 @@
"DG.AgentNameFieldTooltip": "NOM, PRÉNOM, INITIALES",
"DG.RollLuck": "Jet de Chance",
"DG.Luck": "Chance",
"DG.FallbackText.newItem": "Nouveau/el/elle {type}",
"DG.Skills.accounting": "Comptabilité",
"DG.Skills.alertness": "Vigilance",
"DG.Skills.anthropology": "Anthropologie",
@@ -44,7 +42,6 @@
"DG.Skills.unarmed_combat": "Cbt à mains nues",
"DG.Skills.unnatural": "Inconcevable",
"DG.Skills.flight": "Voler",
"DG.Skills.Tooltip.accounting": "Repérez les signaux d'alerte financiers tels que les actifs cachés, les sociétés écrans ou les détournements de fonds.",
"DG.Skills.Tooltip.alertness": "Remarquez les dangers cachés, les mouvements subtils ou les sons discrets avant qu'ils ne deviennent des menaces.",
"DG.Skills.Tooltip.anthropology": "Comprendre la moralité, les croyances religieuses, les coutumes et les moeurs, ainsi qu’à identifier (mais pas à traduire) les langues obscures",
@@ -82,7 +79,6 @@
"DG.Skills.Tooltip.swim": "Pour la plupart des Agents, la natation est un loisir. Servez-vous de la compétence Natation lors des situations de crise.",
"DG.Skills.Tooltip.unarmed_combat": "Autodéfense. Servez-vous de Combat à mains nues pour blesser ou tuer un adversaire avec vos seuls poings (ainsi que vos pieds, vos coudes, vos dents et votre tête).",
"DG.Skills.Tooltip.unnatural": "Connaissance des secrets fondamentaux de l’univers, propres à provoquer la folie.",
"DG.Skills.AddTypedOrCustomSkill": "Ajouter une compétence personnalisée",
"DG.Skills.AddSkill": "Ajout Compétence",
"DG.Skills.Apply": "Appliquer",
@@ -95,7 +91,6 @@
"DG.Skills.EditSkill": "Éditer compétence",
"DG.Skills.SkillGroup": "Groupe de compétence",
"DG.Skills.SkillName": "Nom de la compétence",
"DG.TypeSkills.Art": "Art",
"DG.TypeSkills.Craft": "Artisanat",
"DG.TypeSkills.ForeignLanguage": "Langues étrangères",
@@ -103,7 +98,6 @@
"DG.TypeSkills.Pilot": "Pilotage",
"DG.TypeSkills.Science": "Science",
"DG.TypeSkills.Other": "Autre",
"DG.TypeSkills.Tooltip.Art": "Maîtrise de la création ou de l’exécution d’une oeuvre capable d’altérer les émotions et les opinions.",
"DG.TypeSkills.Tooltip.Craft": "Fabriquer et réparer des outils et structures sophistiqués.",
"DG.TypeSkills.Tooltip.ForeignLanguage": "Parler couramment une autre langue. Chaque langue étrangère représente une compétence distincte.",
@@ -111,7 +105,6 @@
"DG.TypeSkills.Tooltip.Pilot": "Piloter, diriger et naviguer avec des véhicules aquatiques, aériens ou aérospatiaux.",
"DG.TypeSkills.Tooltip.Science": "Étude approfondie des phénomènes qui nous entourent.",
"DG.TypeSkills.Tooltip.Other": "Autre",
"DG.SpecialTraining.Dialog.CreateSpecialTraining": "Ajouter un Entraînement Spécial",
"DG.SpecialTraining.Dialog.DropDown.CustomSkills": "Compétences Personnalisées/Typées",
"DG.SpecialTraining.Dialog.DropDown.Skills": "Compétences",
@@ -120,7 +113,6 @@
"DG.SpecialTraining.Dialog.NameLabel": "Nom",
"DG.SpecialTraining.Dialog.SkillLabel": "Compétence ou Statistique",
"DG.SpecialTraining.Dialog.Title": "Créer/Éditer un Entraînement Spécial",
"DG.Attributes.HP": "PV",
"DG.Attributes.WP": "VOL",
"DG.Attributes.SAN": "SAN",
@@ -130,7 +122,6 @@
"DG.Attributes.int": "INT",
"DG.Attributes.pow": "POU",
"DG.Attributes.cha": "CHA",
"DG.Attributes.Tooltip.HP": "Points de vie, un personnage devient inconscient à 2PV et meurt à 0PV",
"DG.Attributes.Tooltip.WP": "Les Points de Volonté (VOL) sont le carburant psychologique. Les Agents ont besoin de Points de Volonté pour continuer à travailler malgré l'épuisement ou les situations mentalement éprouvantes. Voir la page 47 du Manuel de l'agent pour plus de détails",
"DG.Attributes.Tooltip.SAN": "Les points de santé mentale (SAN) indiquent un lien avec l'humanité et la réalité telles que la plupart des gens les perçoivent. Cliquer pour effectuer un jet de SAN.",
@@ -140,7 +131,6 @@
"DG.Attributes.Tooltip.int": "Voir Page 19 du Manuel de l'Agent - Intelligence",
"DG.Attributes.Tooltip.pow": "Voir Page 19 du Manuel de l'Agent - Pouvoir",
"DG.Attributes.Tooltip.cha": "Voir Page 19 du Manuel de l'Agent - Charisme",
"DG.Navigation.Agent.skills": "Compétences",
"DG.Navigation.Agent.physical": "Statistiques",
"DG.Navigation.Agent.motivations": "Psychologie",
@@ -151,7 +141,6 @@
"DG.Navigation.NPC.gear": "Attaques & Capacités",
"DG.Navigation.Vehicle.gear": "Équipement",
"DG.Navigation.Vehicle.notes": "Notes",
"DG.About.Changelog": "CHANGELOG (ONLINE)",
"DG.About.Copyright": "Publié par Arc Dream Publishing en accord avec Delta Green Partnership. La propriété intellectuelle connue sous le nom de Delta Green est une marque déposée et protégée par les droits d'auteur appartenant à Delta Green Partnership, qui en a autorisé l'utilisation ici.",
"DG.About.License": "Le contenu de ce système est concédé sous licence conformément aux conditions générales incluses dans le fichier <a href=\"systems/deltagreen/LICENSE.txt\">licence</a>, à l'exception des éléments qui font partie de la propriété intellectuelle de Delta Green.",
@@ -159,9 +148,7 @@
"DG.About.OnlineHelpTooltip": "Lancer le navigateur avec l'aide GitHub en ligne.",
"DG.About.OnlineBugTracker": "Questions, rapports de bug ou demandes de fonctionnalités (ONLINE)",
"DG.About.OnlineBugTrackerTooltip": "Lancer le navigateur avec le suivi des problèmes GitHub.",
"DG.Roll.Rolling": "Jet de dés",
"DG.Roll.Target": "Cible: ",
"DG.Roll.Success": "Succés",
"DG.Roll.Failure": "Échec",
"DG.Roll.CriticalSuccess": "Succés critique!",
@@ -173,14 +160,11 @@
"DG.Roll.RollingDamageFor": "Jets de dégâts pour",
"DG.Roll.Or": "Ou",
"DG.Roll.Roll": "Lancer",
"DG.Physical.Wounds": "Blessures",
"DG.Physical.FirstAidLabel": "Jet de Premiers Secours depuis sa dernière blessure: ",
"DG.Physical.Exhausted": "Épuisé:",
"DG.Physical.ExhaustionPenalty": "Pénalité:",
"DG.Generic.AddButtonLabel": "Ajouter",
"DG.Mental.BreakingPoint": "Seuil de Rupture: ",
"DG.Mental.Tooltip.BreakingPoint": "le Seuil de Rupture est le point à partir duquel la perte de SAN déclenche des troubles psychiques à long terme. Dès que la SAN de l’Agent atteint le Seuil de Rupture, il reçoit un trouble. Réinitialisez le Seuil de Rupture : il est désormais égal à la nouvelle valeur de SAN moins POU",
"DG.Mental.Reset": "Réinit.",
@@ -193,7 +177,6 @@
"DG.Mental.AdaptedToHelplessness": "Accoutumée à l'impuissance",
"DG.Mental.Motivations": "Motivations",
"DG.Mental.Disorders": "Troubles psychiques",
"DG.Gear.WeaponTypes.Firearms": "Armes à feu",
"DG.Gear.WeaponTypes.Melee": "Mêlée",
"DG.Gear.Weapons": "ARMES",
@@ -204,7 +187,6 @@
"DG.Gear.ArmorPiercing": "Perforant",
"DG.Gear.ArmorRating": "Niveau d'Armure",
"DG.Gear.Equipped": "Équipé",
"DG.CV.Sex": "Sexe",
"DG.CV.SexPlaceholder": "Sexe",
"DG.CV.AgeAndDoB": "Âge et Date de Naissance",
@@ -215,12 +197,10 @@
"DG.CV.NationalityPlaceholder": "Nationalité",
"DG.CV.Education": "Études et Carrière",
"DG.CV.EducationPlaceholder": "Études et Carrière",
"DG.Bonds.Bond": "Attaches",
"DG.Bonds.Name": "Nom",
"DG.Bonds.Score": "Score",
"DG.Bonds.NewBond": "Nouvelle Attache",
"DG.Tooltip.MaximumHP": "La Santé maximum est la moyenne de FOR+CON, arrondi au supérieur",
"DG.Tooltip.MaximumWP": "La Volonté maximum est égale au POUVOIR",
"DG.Tooltip.CurrentSanityPartOne": "Santé Mentale actuelle, le seuil de rupture actuel est ",
@@ -236,17 +216,14 @@
"DG.Tooltip.AdaptedToHelplessness": "Être adapté à l'impuissance signifie que la motivation personnelle de votre agent souffre lorsqu'il se retire du monde qui l'entoure. Dès que votre agent s'adapte à l'impuissance, il ou elle perd définitivement 1D6 POU",
"DG.Tooltip.ToggleEquippedState": "Cliquez pour indiquer si cet élément est équipé ou non",
"DG.Tooltip.Exhaustion": "Un agent qui travaille trop longtemps, qui fait face à un danger extrême ou des blessures sans repos devient épuisé",
"DG.Gear.Expense.NA": "N/A ou Aucun",
"DG.Gear.Expense.Incidental": "Courante",
"DG.Gear.Expense.Standard": "Standard",
"DG.Gear.Expense.Unusual": "Importante",
"DG.Gear.Expense.Major": "Substantielle",
"DG.Gear.Expense.Extreme": "Considérable",
"DG.ItemWindow.Bonds.Description": "Description/développements",
"DG.ItemWindow.Bonds.ScoreTooltip": "Un score plus élevé représente une relation plus forte entre l'Agent et l'Attache",
"DG.ItemWindow.Motivations.Attributes": "Caractéristiques",
"DG.ItemWindow.Motivations.Motivation": "Motivation",
"DG.ItemWindow.Motivations.Description": "Description/Notes",
@@ -257,19 +234,16 @@
"DG.ItemWindow.Motivations.IsDisorderTooltip": "Cochez cette case si vous ajoutez un trouble psychique, ou si une motivation existante a été perdue en raison de l'acquisition d'un trouble",
"DG.ItemWindow.Motivations.DisorderHasBeenCured": "Le trouble psychique a été \"Soigné\"",
"DG.ItemWindow.Motivations.DisorderHasBeenCuredTooltip": "Un succès critique sur un jet de chance peut guérir un trouble psychique. Il peut cependant réapparaître la prochaine fois qu'un seuil de rupture est atteint",
"DG.ItemWindow.Gear.Attributes": "Caractéristiques",
"DG.ItemWindow.Gear.Description": "Description",
"DG.ItemWindow.Gear.NamePlaceholder": "Nom",
"DG.ItemWindow.Gear.Expense": "Dépense",
"DG.ItemWindow.Gear.Equipped": "Équipé",
"DG.ItemWindow.Armor.Description": "Description",
"DG.ItemWindow.Armor.NamePlaceholder": "Nom",
"DG.ItemWindow.Armor.ArmorRating": "Valeur d'Armure",
"DG.ItemWindow.Armor.Expense": "Dépense",
"DG.ItemWindow.Armor.Equipped": "Équipé",
"DG.ItemWindow.Weapons.Description": "Description",
"DG.ItemWindow.Weapons.NamePlaceholder": "Nom",
"DG.ItemWindow.Weapons.Skill": "Compétence",
@@ -283,27 +257,21 @@
"DG.ItemWindow.Weapons.Ammo": "Munitions (Nombre/Type)",
"DG.ItemWindow.Weapons.Range": "Portée",
"DG.ItemWindow.Weapons.KillRadius": "Rayon de mortalité",
"DG.ModifySkillRollDialogue.Title": "Modifier le Lancer",
"DG.ModifySkillRollDialogue.Test": "Test de:",
"DG.ModifySkillRollDialogue.OriginalTarget": "Niveau actuel:",
"DG.ModifySkillRollDialogue.TargetModifier": "Modificateur (+/-):",
"DG.ModifySkillRollDialogue.ModifierTooltip": "Donnez ici un modificateur positif ou négatif à ajouter/soustraire à la cible pour le test de compétence.",
"DG.ModifyDamageRollDialogue.Weapon": "Arme:",
"DG.ModifyDamageRollDialogue.OuterModifierTooltip": "Utilisez ce champ pour modifier la partie extérieure de la formule, par exemple pour doubler le résultat.",
"DG.ModifyDamageRollDialogue.InnerModifierTooltip": "Utilisez ce champ pour ajouter un modificateur fixe tel que '+1' ou un autre terme de dé tel que '+1d4'.",
"DG.Roll.Inhuman": "Non humain",
"DG.Tooltip.BreakPoint": "Le seuil de rupture actuel, réinitialisé lorsque la santé mentale actuelle tombe à cette valeur ou en dessous. Le seuil de rupture de départ est SAN - POU",
"DG.Tooltip.BreakPointReset": "Le seuil de rupture est réinitialisé à la valeur actuelle SAN - POU",
"DG.Skills.ritual": "Rituel",
"DG.RollMode": "Mode de Jet",
"DG.Bonds.Relationship": "Type d'Attache",
"DG.TypeSkills.Subskills.Painting": "Peinture",
"DG.Sheet.BlockHeaders.Attributes": "CARACTÉRISTIQUES",
"DG.Sheet.BlockHeaders.Statistics": "STATISTIQUES",
"DG.Sheet.BlockHeaders.PhysicalDescriptionNotes": "DESCRIPTION PHYSIQUE / NOTES",
@@ -313,9 +281,7 @@
"DG.Sheet.BlockHeaders.MotivationsAndDisorders": "MOTIVATIONS ET TROUBLES PSYCHIQUES",
"DG.Sheet.BlockHeaders.PersonalInfoCV": "DÉTAILS PERSONNELS ET NOTES",
"DG.Sheet.BlockHeaders.BondsRelationshipsContacts": "ATTACHES, RELATIONS ET CONTACTS",
"DG.AgentSheet.ProfessionPlaceholder": "PROFESSION (RANG LE CAS ÉCHÉANT)",
"DG.UnnaturalSheet.HideUntrainedSkillsLabel": "Cacher compétences inconnues",
"DG.UnnaturalSheet.NotesLabel": "Notes",
"DG.UnnaturalSheet.ShortDescriptionFallbackText": "Description de créature ou du type de créature",
@@ -323,39 +289,31 @@
"DG.UnnaturalSheet.Tabs.NotesTabLabel": "Compétences & Notes",
"DG.UnnaturalSheet.Tabs.ItemsTabLabel": "Attaques & Capacités",
"DG.NPCSheet.BreakPointAbbreviation": "SR",
"DG.ImpossibleLanscapes.IHaveSeenThe": "J'AI VU LE",
"DG.ImpossibleLanscapes.YellowSign": "SIGNE JAUNE",
"DG.ImpossibleLanscapes.CorruptionFirstHalf": "Corrup",
"DG.ImpossibleLanscapes.CorruptionSecondHalf": "tion",
"DG.ImpossibleLanscapes.Gift": "Don",
"DG.ImpossibleLanscapes.Insight": "Intuition",
"DG.VehicleSheet.NotesTabName": "Notes",
"DG.VehicleSheet.GearTabName": "Équipement",
"DG.VehicleSheet.NotesLabel": "NOTES",
"DG.VehicleSheet.DescriptionPlaceHolder": "Description du Véhicule",
"DG.Generic.CheckDamagedBondsBoxUntilNextHomeScene": "Quand une Attache est endommagée, cochez sa case jusqu'à la fin de la prochaine scène privée.",
"DG.Generic.ClearAll": "Supprimer les coches",
"DG.Generic.SanDamage": "Perte de SAN",
"DG.ItemWindow.Weapons.CustomSkillTarget": "Cible",
"DG.ItemWindow.Weapons.SkillModifier": "Modificateur de compétence",
"DG.ItemWindow.Custom": "Personnalisé",
"DG.Sheet.class.item": "Feuille d'Équipement",
"DG.Sheet.class.agent": "Feuille d'Agent",
"DG.Sheet.class.npc": "Feuille de PNJ",
"DG.Sheet.class.unnatural": "Feuille d'Inconcevable",
"DG.Sheet.class.vehicle": "Feuille de Véhicule",
"TYPES.Actor.agent": "Agent",
"TYPES.Actor.npc": "PNJ",
"TYPES.Actor.unnatural": "Inconcevable",
"TYPES.Actor.vehicle": "Véhicule",
"TYPES.Item.armor": "Armure",
"TYPES.Item.bond": "Lien",
"TYPES.Item.gear": "Équipement",
@@ -363,13 +321,10 @@
"TYPES.Item.weapon": "Arme",
"TYPES.Item.tome": "Ouvrage",
"TYPES.Item.ritual": "Rituel",
"DG.Gear.Tomes": "OUVRAGES",
"DG.Gear.Rituals": "RITUELS",
"DG.Tomes.Name": "Titre",
"DG.Ritual.Name": "Nom",
"DG.Tome.Name": "Titre",
"DG.Tome.Language": "Langage",
"DG.Tome.StudyTime": "Temps d'Étude",
@@ -381,15 +336,12 @@
"DG.ItemWindow.Tome.StudySanityDamage": "Perte de SAN dûe à l'étude",
"DG.ItemWindow.Tome.HandlerOnlyTabHeader": "Officier Uniquement",
"DG.ItemWindow.Tome.HandlerNotes": "Notes de l'Officier",
"DG.ItemWindow.Ritual.ActivationSanityDamage": "Dégâts SAN Activation",
"DG.ItemWindow.Ritual.ActivationTime": "Temp d'Activation",
"DG.ItemWindow.Ritual.ActivationCosts": "Coût d'Activation",
"DG.ItemWindow.Ritual.StudyTime": "Temps d'étude",
"DG.ItemWindow.Ritual.LearnedSanityDamage": "Dégâts SAN Étude",
"DG.MissionPaused": "Mission en pause",
"DG.Settings.charactersheet.name": "Style de Feuille de Personnage",
"DG.Settings.charactersheet.hint": "Choisissez le style des fiches de Personnage. Le Programme est un style gouvernemental plus moderne, tandis que Les Cowboys et les Hors-la-loi ont un style machine à écrire plus ancien et plus brut.",
"DG.Settings.charactersheet.cowboys": "Les Cowboys",
@@ -417,15 +369,12 @@
"DG.Settings.skillFailure.name": "Marquer automatiquement l'échec d'une compétence",
"DG.Settings.skillFailure.hint": "Activez ce paramètre pour toujours cocher la case « Échec » en cas d'échec d'une compétence. Le MJ peut annuler l'action à partir du message du jet dans le chat.",
"DG.Settings.Saved": "Les paramètres actuels ont été enregistrés.",
"DG.SettingsMenu.automation.name": "Menu Automatisaton",
"DG.SettingsMenu.automation.label": "Menu Automatisaton",
"DG.SettingsMenu.automation.title": "Config. Automatisation",
"DG.SettingsMenu.handler.name": "Config. Officier Traitant",
"DG.SettingsMenu.handler.label": "Config. Officier Traitant",
"DG.SettingsMenu.handler.title": "Config. Officier Traitant seulement",
"DG.Messages.Skillsmark.Marked": "Cette compétence est marquée.",
"DG.Messages.Skillsmark.Tooltip": "Voir p. 29 du Manuel de l'agent."
}
-43
View File
@@ -3,10 +3,8 @@
"DG.AgentNameFieldTooltip": "姓, 名 (必要に応じ通称)",
"DG.RollLuck": "幸運ロール",
"DG.Luck": "幸運",
"DG.FallbackText.ProfessionAndOrRank": "職業 (必要に応じ役職)",
"DG.FallbackText.newItem": "新規{type}",
"DG.Skills.accounting": "経理",
"DG.Skills.alertness": "警戒",
"DG.Skills.anthropology": "人類学",
@@ -45,7 +43,6 @@
"DG.Skills.unarmed_combat": "徒手格闘",
"DG.Skills.unnatural": "非自然",
"DG.Skills.flight": "飛行",
"DG.Skills.AddTypedOrCustomSkill": "類型/カスタム技能を追加",
"DG.Skills.AddSkill": "技能を追加",
"DG.Skills.Apply": "適用",
@@ -58,7 +55,6 @@
"DG.Skills.EditSkill": "技能を編集",
"DG.Skills.SkillGroup": "技能グループ",
"DG.Skills.SkillName": "技能の名前",
"DG.TypeSkills.Art": "芸術",
"DG.TypeSkills.Craft": "製作",
"DG.TypeSkills.ForeignLanguage": "ほかの言語",
@@ -66,7 +62,6 @@
"DG.TypeSkills.Pilot": "操縦",
"DG.TypeSkills.Science": "科学",
"DG.TypeSkills.Other": "その他",
"DG.SpecialTraining.Dialog.CreateSpecialTraining": "特殊訓練を追加",
"DG.SpecialTraining.Dialog.DropDown.CustomSkills": "カスタム/類型技能",
"DG.SpecialTraining.Dialog.DropDown.Skills": "技能",
@@ -75,7 +70,6 @@
"DG.SpecialTraining.Dialog.NameLabel": "名前",
"DG.SpecialTraining.Dialog.SkillLabel": "技能/能力値",
"DG.SpecialTraining.Dialog.Title": "特殊訓練の作成/編集",
"DG.Attributes.HP": "HP",
"DG.Attributes.WP": "WP",
"DG.Attributes.SAN": "SAN",
@@ -85,21 +79,17 @@
"DG.Attributes.int": "INT",
"DG.Attributes.pow": "POW",
"DG.Attributes.cha": "CHA",
"DG.Navigation.Skills": "技能",
"DG.Navigation.Physical": "能力値",
"DG.Navigation.Mental": "精神",
"DG.Navigation.Gear": "装備",
"DG.Navigation.CV": "経歴",
"DG.Navigation.Contacts": "絆",
"DG.About.OnlineHelp": "システムドキュメンテーション(オンライン)",
"DG.About.OnlineHelpTooltip": "オンラインGitHubヘルプをブラウザで開く。",
"DG.About.OnlineBugTracker": "質問、バグ報告、機能リクエスト(オンライン)",
"DG.About.OnlineBugTrackerTooltip": "オンラインGitHubのissueトラッカーをブラウザで開く。",
"DG.Roll.Rolling": "ローリング",
"DG.Roll.Target": "目標値: ",
"DG.Roll.Success": "成功",
"DG.Roll.Failure": "失敗",
"DG.Roll.Critical": "クリティカル",
@@ -110,14 +100,11 @@
"DG.Roll.RollingDamageFor": "ダメージロール: ",
"DG.Roll.Or": " / ",
"DG.Roll.Roll": "ロール",
"DG.Physical.Wounds": "傷跡",
"DG.Physical.FirstAidLabel": "応急手当済: ",
"DG.Physical.Exhausted": "疲労:",
"DG.Physical.ExhaustionPenalty": "ペナルティ:",
"DG.Generic.AddButtonLabel": "追加",
"DG.Mental.BreakingPoint": "限界点: ",
"DG.Mental.Reset": "リセット",
"DG.Mental.SanityLossIncidentsLabel": "SANを失ったが正気を保った出来事",
@@ -127,7 +114,6 @@
"DG.Mental.AdaptedToHelplessness": "無力への慣れ",
"DG.Mental.Motivations": "モチベーション",
"DG.Mental.Disorders": "精神障害",
"DG.Gear.Weapons": "武器",
"DG.Gear.Armor": "防具",
"DG.Gear.OtherGear": "他の装備",
@@ -136,7 +122,6 @@
"DG.Gear.ArmorPiercing": "装甲貫通",
"DG.Gear.ArmorRating": "装甲値",
"DG.Gear.Equipped": "装備",
"DG.CV.Sex": "性別",
"DG.CV.SexPlaceholder": "性別",
"DG.CV.AgeAndDoB": "年齢・誕生日",
@@ -147,12 +132,10 @@
"DG.CV.NationalityPlaceholder": "国籍",
"DG.CV.Education": "教育",
"DG.CV.EducationPlaceholder": "教育と訓練",
"DG.Bonds.Bond": "絆",
"DG.Bonds.Name": "名前",
"DG.Bonds.Score": "スコア",
"DG.Bonds.NewBond": "新規絆",
"DG.Attributes.Tooltip.HP": "ヒットポイント、HPが2になるとエージェントは意識を失い、HPが0で死亡する。",
"DG.Tooltip.MaximumHP": "最大HPはSTRとCONの平均(切り上げ)。",
"DG.Attributes.Tooltip.WP": "意志力ポイント(WP)は精神的な燃料。エージェントは、疲労や精神的に負担のかかる状況下でも活動を続けるために、WPを必要とする。詳細はAgent's Handbook 47ページ参照。",
@@ -171,17 +154,14 @@
"DG.Tooltip.AdaptedToHelplessness": "無力に慣れるということは、彼らが周囲の世界から身を引くことによって、エージェントの個人的な意欲が損なわれることを意味する。無力に慣れるとすぐに、エージェントは1D6のPOWを永久的に失う。",
"DG.Tooltip.ToggleEquippedState": "クリックでこのアイテムの装備の有無を切り替え。",
"DG.Tooltip.Exhaustion": "長時間働きすぎたり、休息を取らずに極度の危険や負傷に直面したりすると、エージェントは疲労する。 これが起こるのはハンドラー次第だが、目安として、一晩眠らずに過ごしたり、SANやHPを失った後に休息を拒否したりすると、疲労につながる。",
"DG.Gear.Expense.NA": "N/A",
"DG.Gear.Expense.Incidental": "付帯費用",
"DG.Gear.Expense.Standard": "標準",
"DG.Gear.Expense.Unusual": "特別",
"DG.Gear.Expense.Major": "高額",
"DG.Gear.Expense.Extreme": "極高額",
"DG.ItemWindow.Bonds.Description": "説明/進展",
"DG.ItemWindow.Bonds.ScoreTooltip": "スコアが高いほど、エージェントと絆の関係性が強いことを示す。",
"DG.ItemWindow.Motivations.Attributes": "能力値",
"DG.ItemWindow.Motivations.Motivation": "モチベーション",
"DG.ItemWindow.Motivations.Description": "説明とノート",
@@ -192,19 +172,16 @@
"DG.ItemWindow.Motivations.IsDisorderTooltip": "精神障害である場合このボックスをチェックする",
"DG.ItemWindow.Motivations.DisorderHasBeenCured": "精神障害は\"治癒\"した",
"DG.ItemWindow.Motivations.DisorderHasBeenCuredTooltip": "セラピーでの幸運ロールでクリティカル成功をすると、精神障害を「治療」することができる。しかし、限界点に達した場合それが次回に再発する可能性がある。Agent's Handbook 78~79ページの「Go To Therapy」を参照。",
"DG.ItemWindow.Gear.Attributes": "能力値",
"DG.ItemWindow.Gear.Description": "説明",
"DG.ItemWindow.Gear.NamePlaceholder": "名前",
"DG.ItemWindow.Gear.Expense": "費用",
"DG.ItemWindow.Gear.Equipped": "装備",
"DG.ItemWindow.Armor.Description": "説明",
"DG.ItemWindow.Armor.NamePlaceholder": "名前",
"DG.ItemWindow.Armor.ArmorRating": "装甲値",
"DG.ItemWindow.Armor.Expense": "費用",
"DG.ItemWindow.Armor.Equipped": "装備",
"DG.ItemWindow.Weapons.Description": "説明",
"DG.ItemWindow.Weapons.NamePlaceholder": "名前",
"DG.ItemWindow.Weapons.Skill": "技能",
@@ -218,27 +195,21 @@
"DG.ItemWindow.Weapons.Ammo": "弾 (数量/種類)",
"DG.ItemWindow.Weapons.Range": "射程",
"DG.ItemWindow.Weapons.KillRadius": "致命半径",
"DG.ModifySkillRollDialogue.Title": "ロールの修正",
"DG.ModifySkillRollDialogue.Test": "テスト:",
"DG.ModifySkillRollDialogue.OriginalTarget": "元の目標値:",
"DG.ModifySkillRollDialogue.TargetModifier": "目標値修正:",
"DG.ModifySkillRollDialogue.ModifierTooltip": "技能チェックの目標値にプラスまたはマイナスの修正を加える。",
"DG.ModifyDamageRollDialogue.Weapon": "武器:",
"DG.ModifyDamageRollDialogue.OuterModifierTooltip": "ここでダイスの式の外を変えられる、結果を2倍にするために使われる。",
"DG.ModifyDamageRollDialogue.InnerModifierTooltip": "ここでダイスの式の内側を変えられる、結果を「+1」とか、「+1d4」とかにするために使われる。",
"DG.Roll.Inhuman": "人ならざるもの",
"DG.Tooltip.BreakPoint": "現在の限界点、現在SANがこの値以下に低下した場合にリセットする。初期値はSAN - POW。",
"DG.Tooltip.BreakPointReset": "限界点を現在SAN - WPにリセットする。",
"DG.Skills.ritual": "儀式",
"DG.RollMode": "ロールモード",
"DG.Bonds.Relationship": "関係",
"DG.TypeSkills.Subskills.Painting": "絵画",
"DG.Sheet.BlockHeaders.Attributes": "能力値",
"DG.Sheet.BlockHeaders.Statistics": "能力値",
"DG.Sheet.BlockHeaders.PhysicalDescriptionNotes": "身体的特徴とメモ",
@@ -248,7 +219,6 @@
"DG.Sheet.BlockHeaders.MotivationsAndDisorders": "モチベーションと精神障害",
"DG.Sheet.BlockHeaders.PersonalInfoCV": "個人情報 / 経歴",
"DG.Sheet.BlockHeaders.BondsRelationshipsContacts": "絆 / 関係 / 協力者",
"DG.UnnaturalSheet.HideUntrainedSkillsLabel": "初期値の技能を非表示",
"DG.UnnaturalSheet.NotesLabel": "ノート",
"DG.UnnaturalSheet.ShortDescriptionFallbackText": "怪物や種類の説明",
@@ -256,33 +226,26 @@
"DG.UnnaturalSheet.Tabs.NotesTabLabel": "技能・ノート",
"DG.UnnaturalSheet.Tabs.ItemsTabLabel": "攻撃・能力",
"DG.NPCSheet.BreakPointAbbreviation": "限界点",
"DG.ImpossibleLanscapes.IHaveSeenThe": "目撃: ",
"DG.ImpossibleLanscapes.YellowSign": "黄色の印",
"DG.ImpossibleLanscapes.CorruptionFirstHalf": "腐",
"DG.ImpossibleLanscapes.CorruptionSecondHalf": "敗",
"DG.ImpossibleLanscapes.Gift": "ギフト",
"DG.ImpossibleLanscapes.Insight": "ひらめき",
"DG.VehicleSheet.NotesTabName": "ノート",
"DG.VehicleSheet.GearTabName": "装備",
"DG.VehicleSheet.NotesLabel": "ノート",
"DG.VehicleSheet.DescriptionPlaceHolder": "車両の説明",
"DG.Generic.CheckDamagedBondsBoxUntilNextHomeScene": "次のホームシーンが終了するまで、ダメージを負った絆のボックスにチェックを入れる。",
"DG.Generic.ClearAll": "全てのチェックを消す",
"DG.Generic.SanDamage": "SANダメージ",
"DG.ItemWindow.Weapons.CustomSkillTarget": "目標値",
"DG.ItemWindow.Weapons.SkillModifier": "技能修正",
"DG.ItemWindow.Custom": "カスタム",
"TYPES.Actor.agent": "エージェント",
"TYPES.Actor.npc": "NPC",
"TYPES.Actor.unnatural": "非自然",
"TYPES.Actor.vehicle": "車両",
"TYPES.Item.armor": "防具",
"TYPES.Item.bond": "絆",
"TYPES.Item.gear": "装備",
@@ -290,13 +253,10 @@
"TYPES.Item.weapon": "武器",
"TYPES.Item.tome": "魔道書",
"TYPES.Item.ritual": "儀式",
"DG.Gear.Tomes": "魔道書",
"DG.Gear.Rituals": "儀式",
"DG.Tomes.Name": "名前",
"DG.Ritual.Name": "名前",
"DG.Tome.Name": "名前",
"DG.Tome.Language": "言語",
"DG.Tome.StudyTime": "研究期間",
@@ -308,13 +268,11 @@
"DG.ItemWindow.Tome.StudySanityDamage": "研究時SANダメージ",
"DG.ItemWindow.Tome.HandlerOnlyTabHeader": "ハンドラー限定",
"DG.ItemWindow.Tome.HandlerNotes": "ハンドラーノート",
"DG.ItemWindow.Ritual.ActivationSanityDamage": "実行時SANダメージ",
"DG.ItemWindow.Ritual.ActivationTime": "実行時間",
"DG.ItemWindow.Ritual.ActivationCosts": "実行コスト",
"DG.ItemWindow.Ritual.StudyTime": "研究期間",
"DG.ItemWindow.Ritual.LearnedSanityDamage": "習得時SANダメージ",
"DG.Skills.ruby.accounting": "けいり",
"DG.Skills.ruby.alertness": "けいかい",
"DG.Skills.ruby.anthropology": "じんるいがく",
@@ -353,7 +311,6 @@
"DG.Skills.ruby.unarmed_combat": "としゅかくとう",
"DG.Skills.ruby.unnatural": "ひしぜん",
"DG.Skills.ruby.flight": "ひこう",
"DG.Messages.Skillsmark.Marked": "このスキルはマークされています",
"DG.Messages.Skillsmark.Tooltip": "エージェントハンドブック29ページ参照"
}
-51
View File
@@ -3,9 +3,7 @@
"DG.AgentNameFieldTooltip": "NAZWISKO, IMIE (PSEUDONIM/KRYPTONIM, JESLI DOTYCZY)",
"DG.RollLuck": "Test Szczęścia",
"DG.Luck": "Szczęście",
"DG.FallbackText.newItem": "Nowy {type}",
"DG.Skills.accounting": "Księgowość",
"DG.Skills.alertness": "Czujność",
"DG.Skills.anthropology": "Antropologia",
@@ -44,7 +42,6 @@
"DG.Skills.unarmed_combat": "Walka Bez Broni",
"DG.Skills.unnatural": "Zjawiska Nadnaturalne",
"DG.Skills.flight": "Latanie",
"DG.Skills.Tooltip.accounting": "Wykrywanie finansowych sygnałów ostrzegawczych, takich jak ukryte aktywa, firmy fasadowe lub defraudacje.",
"DG.Skills.Tooltip.alertness": "Dostrzegaj ukryte zagrożenia, subtelne ruchy lub ciche dźwięki, zanim staną się one niebezpieczeństwem.",
"DG.Skills.Tooltip.anthropology": "Zrozumienie przekonań, zachowań i tematów tabu w współczesnych kulturach i społeczeństwach.",
@@ -82,7 +79,6 @@
"DG.Skills.Tooltip.swim": "Przetrwanie w wodzie pod przymusem - ratowanie innych, ucieczka lub znoszenie niebezpieczeństw.",
"DG.Skills.Tooltip.unarmed_combat": "Poskromienie lub zranienie przeciwników przy użyciu pięści, chwytów, kopnięć i innych technik walki wręcz.",
"DG.Skills.Tooltip.unnatural": "Rozpoznawanie, badanie lub przypominanie sobie prawd o bytach i siłach wykraczających poza ludzkie zrozumienie.",
"DG.Skills.AddTypedOrCustomSkill": "Dodaj Typową/Niestandardową Umiejętność",
"DG.Skills.AddSkill": "Dodaj Umiejętność",
"DG.Skills.Apply": "Zastosuj",
@@ -95,7 +91,6 @@
"DG.Skills.EditSkill": "Edytuj Umiejętność",
"DG.Skills.SkillGroup": "Grupa Umiejętności",
"DG.Skills.SkillName": "Nazwa Umiejętności",
"DG.TypeSkills.Art": "Sztuka",
"DG.TypeSkills.Craft": "Rzemiosło",
"DG.TypeSkills.ForeignLanguage": "Język Obcy",
@@ -103,7 +98,6 @@
"DG.TypeSkills.Pilot": "Pilotowanie",
"DG.TypeSkills.Science": "Nauka",
"DG.TypeSkills.Other": "Inne",
"DG.TypeSkills.Tooltip.Art": "Twórz lub oceniaj dzieła artystyczne i wykrywaj fałszerstwa w danej dziedzinie.",
"DG.TypeSkills.Tooltip.Craft": "Budowanie, naprawianie lub sabotowanie specjalistycznych narzędzi lub systemów, takich jak silniki, zamki lub okablowanie.",
"DG.TypeSkills.Tooltip.ForeignLanguage": "Mówisz, czytasz i rozumiesz dany język.",
@@ -111,7 +105,6 @@
"DG.TypeSkills.Tooltip.Pilot": "Kontrolowanie i nawigowanie pojazdami powietrznymi, morskimi lub kosmicznymi w krytycznych sytuacjach lub warunkach.",
"DG.TypeSkills.Tooltip.Science": "Zastosowanie specjalistycznej wiedzy naukowej do interpretacji lub badania świata fizycznego.",
"DG.TypeSkills.Tooltip.Other": "Inne",
"DG.SpecialTraining.Dialog.CreateSpecialTraining": "Dodaj Dodatkowe Szkolenie",
"DG.SpecialTraining.Dialog.DropDown.CustomSkills": "Niestandardowe Umiejętności",
"DG.SpecialTraining.Dialog.DropDown.Skills": "Umiejętności",
@@ -120,7 +113,6 @@
"DG.SpecialTraining.Dialog.NameLabel": "Nazwa",
"DG.SpecialTraining.Dialog.SkillLabel": "Umiejętność lub Statystyka",
"DG.SpecialTraining.Dialog.Title": "Stwórz/Edytuj Dodatkowe Szkolenie",
"DG.Attributes.HP": "PW",
"DG.Attributes.WP": "PSW",
"DG.Attributes.SAN": "PP",
@@ -130,7 +122,6 @@
"DG.Attributes.int": "INT",
"DG.Attributes.pow": "MOC",
"DG.Attributes.cha": "CHA",
"DG.Attributes.Tooltip.HP": "Punkty Wytrzymałości, postać traci przytomność przy 2 PW i umiera przy 0 PW.",
"DG.Attributes.Tooltip.WP": "Punkty Siły Woli to paliwo mentalne. Agenci potrzebują Punktów Siły Woli, aby kontynuować mimo wyczerpania lub mentalnych trudności. Zobacz stronę 47 w Podręczniku Agenta po więcej szczegółów.",
"DG.Attributes.Tooltip.SAN": "Kliknij, aby wykonać test na Poczytalność...",
@@ -140,7 +131,6 @@
"DG.Attributes.Tooltip.int": "Zobacz stronę 19 Podręcznika Agenta – Inteligencja",
"DG.Attributes.Tooltip.pow": "Zobacz stronę 19 Podręcznika Agenta – Siła Woli",
"DG.Attributes.Tooltip.cha": "Zobacz stronę 19 Podręcznika Agenta – Charyzma",
"DG.Navigation.Agent.skills": "Umiejętności",
"DG.Navigation.Agent.physical": "Statystyki",
"DG.Navigation.Agent.motivations": "Psychika",
@@ -151,7 +141,6 @@
"DG.Navigation.NPC.gear": "Ataki i Zdolności",
"DG.Navigation.Vehicle.gear": "Wyposażenie",
"DG.Navigation.Vehicle.notes": "Uwagi",
"DG.About.Changelog": "DZIENNIK ZMIAN (ONLINE)",
"DG.About.Copyright": "Opublikowano przez Arc Dream Publishing na mocy umowy z Delta Green Partnership. Własność intelektualna znana jako Delta Green jest znakiem towarowym i prawem autorskim należącym do Delta Green Partnership, które udzieliło tutaj licencji na jej wykorzystanie.",
"DG.About.License": "Zawartość tego systemu jest licencjonowana zgodnie z warunkami dołączonego pliku <a href=\"systems/deltagreen/LICENSE.txt\">licencyjnego</a>, z wyjątkiem tych elementów, które są składnikami własności intelektualnej Delta Green.",
@@ -159,9 +148,7 @@
"DG.About.OnlineHelpTooltip": "Uruchom przeglądarkę z dokumentacją online na GitHubie.",
"DG.About.OnlineBugTracker": "Pytania, zgłoszenia błędów lub prośby o funkcje (ONLINE)",
"DG.About.OnlineBugTrackerTooltip": "Uruchom przeglądarkę z trackerem błędów na GitHubie.",
"DG.Roll.Rolling": "Rzut na",
"DG.Roll.Target": "Cel: ",
"DG.Roll.Success": "Sukces",
"DG.Roll.Failure": "Porażka",
"DG.Roll.CriticalSuccess": "Krytyczny sukces!",
@@ -173,14 +160,11 @@
"DG.Roll.RollingDamageFor": "Rzut na Obrażenia",
"DG.Roll.Or": "Lub",
"DG.Roll.Roll": "Rzuć",
"DG.Physical.Wounds": "Rany",
"DG.Physical.FirstAidLabel": "Pierwsza pomoc została już podjęta: ",
"DG.Physical.Exhausted": "Wyczerpany:",
"DG.Physical.ExhaustionPenalty": "Kara:",
"DG.Generic.AddButtonLabel": "Dodaj",
"DG.Mental.BreakingPoint": "Punkt Załamania: ",
"DG.Mental.Tooltip.BreakingPoint": "Punkt Załamania to dokładna wartość Poczytalności, przy której Twój Agent został wystarczająco wyczerpany traumą, by rozwinąć nową, długotrwałą chorobę psychiczną. Jeśli Poczytalność osiągnie ten punkt lub spadnie poniżej niego, odejmij Siłę Woli Agenta od nowej Poczytalności. To będzie nowy Punkt Załamania Twojego Agenta.",
"DG.Mental.Reset": "Reset",
@@ -191,7 +175,6 @@
"DG.Mental.AdaptedToHelplessness": "Adaptacja do bezsilności",
"DG.Mental.Motivations": "Motywacje",
"DG.Mental.Disorders": "Zaburzenia",
"DG.Gear.WeaponTypes.Firearms": "Broń Palna",
"DG.Gear.WeaponTypes.Melee": "Walka wręcz",
"DG.Gear.Weapons": "Broń",
@@ -202,7 +185,6 @@
"DG.Gear.ArmorPiercing": "Przebicie Pancerza",
"DG.Gear.ArmorRating": "Siła Pancerza",
"DG.Gear.Equipped": "Wyposażony",
"DG.CV.Sex": "Płeć",
"DG.CV.SexPlaceholder": "Płeć",
"DG.CV.AgeAndDoB": "Wiek i Data Ur.",
@@ -213,12 +195,10 @@
"DG.CV.NationalityPlaceholder": "Narodowość",
"DG.CV.Education": "Edukacja",
"DG.CV.EducationPlaceholder": "Edukacja lub Szkolenie",
"DG.Bonds.Bond": "Więź",
"DG.Bonds.Name": "Nazwa",
"DG.Bonds.Score": "Wynik",
"DG.Bonds.NewBond": "Nowa Więź",
"DG.Tooltip.MaximumHP": "Maksymalne Punkty Wytrzymałości to średnia STR + CON, zaokrąglona w górę.",
"DG.Tooltip.MaximumWP": "Maksymalne PSW to wartość MOC.",
"DG.Tooltip.CurrentSanityPartOne": "Aktualne PP, aktualny punkt załamania to ",
@@ -234,17 +214,14 @@
"DG.Tooltip.AdaptedToHelplessness": "Zaadaptowanie do bezsilności oznacza, że motywacja agenta cierpi, gdy wycofuje się z otaczającego go świata. Gdy tylko agent dostosuje się do bezsilności, trwale traci 1D6 MOC.",
"DG.Tooltip.ToggleEquippedState": "Kliknij, aby przełączyć stan wyposażenia.",
"DG.Tooltip.Exhaustion": "Agent, który pracuje zbyt długo lub stawia czoła ekstremalnemu niebezpieczeństwu i obrażeniom bez odpoczynku, staje się wyczerpany. Kiedy to nastąpi, zależy od Oficera Prowadzącego, ale dobrą zasadą jest, że nieprzespana noc lub odmowa odpoczynku po utracie PP lub Punktów Wytrzymałości prowadzi do wyczerpania.",
"DG.Gear.Expense.NA": "N/A albo Brak",
"DG.Gear.Expense.Incidental": "Drobny",
"DG.Gear.Expense.Standard": "Standardowy",
"DG.Gear.Expense.Unusual": "Niezwykły",
"DG.Gear.Expense.Major": "Poważny",
"DG.Gear.Expense.Extreme": "Ekstremalny",
"DG.ItemWindow.Bonds.Description": "Opis/Rozwój",
"DG.ItemWindow.Bonds.ScoreTooltip": "Wyższy wynik oznacza silniejszą relację między agentem a więzią.",
"DG.ItemWindow.Motivations.Attributes": "Atrybuty",
"DG.ItemWindow.Motivations.Motivation": "Motywacja",
"DG.ItemWindow.Motivations.Description": "Opis/Uwagi",
@@ -255,19 +232,16 @@
"DG.ItemWindow.Motivations.IsDisorderTooltip": "Zaznacz to, jeśli dodajesz zaburzenie, lub jeśli istniejąca motywacja została utracona na skutek zyskania zaburzenia.",
"DG.ItemWindow.Motivations.DisorderHasBeenCured": "Zaburzenie zostało \"wyleczone\"",
"DG.ItemWindow.Motivations.DisorderHasBeenCuredTooltip": "Krytyczny sukces w teście terapii może 'wyleczyć' zaburzenie. Może ono powrócić przy następnym osiągnięciu punktu złamania. Zobacz sekcję Terapia na stronach 78-79 Podręcznika Agenta.",
"DG.ItemWindow.Gear.Attributes": "Atrybuty",
"DG.ItemWindow.Gear.Description": "Opis",
"DG.ItemWindow.Gear.NamePlaceholder": "Nazwa",
"DG.ItemWindow.Gear.Expense": "Koszt",
"DG.ItemWindow.Gear.Equipped": "Wyposażony",
"DG.ItemWindow.Armor.Description": "Opis",
"DG.ItemWindow.Armor.NamePlaceholder": "Nazwa",
"DG.ItemWindow.Armor.ArmorRating": "Siła Pancerza",
"DG.ItemWindow.Armor.Expense": "Koszt",
"DG.ItemWindow.Armor.Equipped": "Wyposażony",
"DG.ItemWindow.Weapons.Description": "Opis",
"DG.ItemWindow.Weapons.NamePlaceholder": "Nazwa",
"DG.ItemWindow.Weapons.Skill": "Umiejętność",
@@ -281,27 +255,21 @@
"DG.ItemWindow.Weapons.Ammo": "Amunicja (Ilość/Typ)",
"DG.ItemWindow.Weapons.Range": "Zasięg",
"DG.ItemWindow.Weapons.KillRadius": "Zasięg Śmierci",
"DG.ModifySkillRollDialogue.Title": "Modyfikuj Rzut",
"DG.ModifySkillRollDialogue.Test": "Test:",
"DG.ModifySkillRollDialogue.OriginalTarget": "Oryginalny Cel:",
"DG.ModifySkillRollDialogue.TargetModifier": "Modyfikator Celu:",
"DG.ModifySkillRollDialogue.ModifierTooltip": "Podaj dodatni lub ujemny modyfikator, aby dodać/odjąć od celu dla testu umiejętności.",
"DG.ModifyDamageRollDialogue.Weapon": "Broń:",
"DG.ModifyDamageRollDialogue.OuterModifierTooltip": "Użyj tego pola, aby zmodyfikować zewnętrzną część formuły, na przykład aby podwoić wynik.",
"DG.ModifyDamageRollDialogue.InnerModifierTooltip": "Użyj tego pola, aby dodać modyfikator stały, np. '+1' lub inną jednostkową kostkę, np. '+1d4'.",
"DG.Roll.Inhuman": "Nieludzki",
"DG.Tooltip.BreakPoint": "Aktualny Punkt Załamania, resetowany, gdy aktualne PP spada do tego punktu lub poniżej. Początkowy punkt złamania to PP - MOC.",
"DG.Tooltip.BreakPointReset": "Zresetuj Punkt Załamania, aby wynosił aktualne PP - PSW.",
"DG.Skills.ritual": "Rytuał",
"DG.RollMode": "Tryb Rzutu",
"DG.Bonds.Relationship": "Relacja",
"DG.TypeSkills.Subskills.Painting": "Malarstwo",
"DG.Sheet.BlockHeaders.Attributes": "ATRYBUTY",
"DG.Sheet.BlockHeaders.Statistics": "STATYSTYKI",
"DG.Sheet.BlockHeaders.PhysicalDescriptionNotes": "OPIS FIZYCZNY / UWAGI",
@@ -311,9 +279,7 @@
"DG.Sheet.BlockHeaders.MotivationsAndDisorders": "MOTYWACJE I ZABURZENIA",
"DG.Sheet.BlockHeaders.PersonalInfoCV": "DANE OSOBOWE / CV",
"DG.Sheet.BlockHeaders.BondsRelationshipsContacts": "WIĘZI / RELACJE / KONTAKTY",
"DG.AgentSheet.ProfessionPlaceholder": "ZAWÓD (RANGA, JEŚLI DOTYCZY)",
"DG.UnnaturalSheet.HideUntrainedSkillsLabel": "Ukryj Niewyuczone",
"DG.UnnaturalSheet.NotesLabel": "Uwagi",
"DG.UnnaturalSheet.ShortDescriptionPlaceholder": "Opis stworzenia lub typu stworzenia",
@@ -321,39 +287,31 @@
"DG.UnnaturalSheet.Tabs.NotesTabLabel": "Umiejętności i Uwagi",
"DG.UnnaturalSheet.Tabs.ItemsTabLabel": "Ataki i Zdolności",
"DG.NPCSheet.BreakPointAbbreviation": "BP",
"DG.ImpossibleLanscapes.IHaveSeenThe": "WIDZIAŁEM",
"DG.ImpossibleLanscapes.YellowSign": "ŻÓŁTY ZNAK",
"DG.ImpossibleLanscapes.CorruptionFirstHalf": "Zepsu",
"DG.ImpossibleLanscapes.CorruptionSecondHalf": "cie",
"DG.ImpossibleLanscapes.Gift": "Dar",
"DG.ImpossibleLanscapes.Insight": "Wgląd",
"DG.VehicleSheet.NotesTabName": "Uwagi",
"DG.VehicleSheet.GearTabName": "Wyposażenie",
"DG.VehicleSheet.NotesLabel": "UWAGI",
"DG.VehicleSheet.DescriptionPlaceHolder": "Opis pojazdu",
"DG.Generic.CheckDamagedBondsBoxUntilNextHomeScene": "Zaznacz pole uszkodzonej więzi, dopóki kolejna scena w domu się nie skończy.",
"DG.Generic.ClearAll": "Wyczyść zaznaczenia",
"DG.Generic.SanDamage": "Utrata PP",
"DG.ItemWindow.Weapons.CustomSkillTarget": "Cel",
"DG.ItemWindow.Weapons.SkillModifier": "Modyfikator Umiejętności",
"DG.ItemWindow.Custom": "Niestandardowy",
"DG.Sheet.class.item": "Karta Przedmiotu",
"DG.Sheet.class.agent": "Karta Agenta",
"DG.Sheet.class.npc": "Karta NPC",
"DG.Sheet.class.unnatural": "Karta Nadnaturalnego",
"DG.Sheet.class.vehicle": "Karta Pojazdu",
"TYPES.Actor.agent": "Agent",
"TYPES.Actor.npc": "NPC",
"TYPES.Actor.unnatural": "Nadnaturalne",
"TYPES.Actor.vehicle": "Pojazd",
"TYPES.Item.armor": "Pancerz",
"TYPES.Item.bond": "Więź",
"TYPES.Item.gear": "Wyposażenie",
@@ -361,13 +319,10 @@
"TYPES.Item.weapon": "Broń",
"TYPES.Item.tome": "Księga",
"TYPES.Item.ritual": "Rytuał",
"DG.Gear.Tomes": "KSIĘGI",
"DG.Gear.Rituals": "RYTUAŁY",
"DG.Tomes.Name": "Nazwa",
"DG.Ritual.Name": "Nazwa",
"DG.Tome.Name": "Nazwa",
"DG.Tome.Language": "Język",
"DG.Tome.StudyTime": "Czas Nauki",
@@ -379,15 +334,12 @@
"DG.ItemWindow.Tome.StudySanityDamage": "Uszkodzenie PP po Studiowaniu",
"DG.ItemWindow.Tome.HandlerOnlyTabHeader": "Tylko dla Oficera Prowadzącego",
"DG.ItemWindow.Tome.HandlerNotes": "Uwagi dla Oficera Prowadzącego",
"DG.ItemWindow.Ritual.ActivationSanityDamage": "Utrata PP przy Aktywacji",
"DG.ItemWindow.Ritual.ActivationTime": "Czas Aktywacji",
"DG.ItemWindow.Ritual.ActivationCosts": "Koszt Aktywacji",
"DG.ItemWindow.Ritual.StudyTime": "Czas Nauki",
"DG.ItemWindow.Ritual.LearnedSanityDamage": "Utrata PP po Nauce",
"DG.MissionPaused": "Misja wstrzymana",
"DG.Settings.charactersheet.name": "Styl karty postaci",
"DG.Settings.charactersheet.hint": "Wybierz styl kart postaci. „Program” to nowocześniejszy, rządowy styl, natomiast „Kowboj/Banita” to starszy, bardziej surowy styl przypominający maszynę do pisania.",
"DG.Settings.charactersheet.cowboys": "Kowboje",
@@ -412,15 +364,12 @@
"DG.Settings.skillFailure.name": "Automatyczne zaznaczanie niepowodzenia w umiejętności",
"DG.Settings.skillFailure.hint": "Aktywuj tę opcję, aby zawsze zaznaczać pole niepowodzenia po nieudanym teście umiejętności. Oficer prowadzący może cofnąć tę akcję z wiadomości na czacie dotyczącej rzutu.",
"DG.Settings.Saved": "Bieżące ustawienia zostały zapisane.",
"DG.SettingsMenu.automation.name": "Menu automatyzacji",
"DG.SettingsMenu.automation.label": "Menu automatyzacji",
"DG.SettingsMenu.automation.title": "Ustawienia automatyzacji",
"DG.SettingsMenu.handler.name": "Ustawienia Oficera Prowadzącego",
"DG.SettingsMenu.handler.label": "Ustawienia Oficera Prowadzącego",
"DG.SettingsMenu.handler.title": "Ustawienia tylko dla Oficera prowadzącego",
"DG.Messages.Skillsmark.Marked": "Ta umiejętność jest oznaczona.",
"DG.Messages.Skillsmark.Tooltip": "Zobacz str. 29 Podręcznika Agenta."
}
-60
View File
@@ -3,9 +3,7 @@
"DG.AgentNameFieldTooltip": "SOBRENOME, NOME (OU PSEUDÔNIMO)",
"DG.RollLuck": "Rolar Sorte",
"DG.Luck": "Sorte",
"DG.FallbackText.newItem": "Novo {type}",
"DG.Skills.accounting": "Contabilidade",
"DG.Skills.alertness": "Prontidão",
"DG.Skills.anthropology": "Antropologia",
@@ -44,7 +42,6 @@
"DG.Skills.unarmed_combat": "Combate Desarmado",
"DG.Skills.unnatural": "Antinatural",
"DG.Skills.flight": "Pilotagem",
"DG.Skills.Tooltip.accounting": "Identificar irregularidades financeiras como ativos ocultos, empresas de fachada ou desvio de recursos.",
"DG.Skills.Tooltip.alertness": "Perceber perigos ocultos, movimentos sutis ou sons discretos antes que se tornem ameaças.",
"DG.Skills.Tooltip.anthropology": "Compreender crenças, comportamentos e tabus de culturas e sociedades vivas.",
@@ -82,7 +79,6 @@
"DG.Skills.Tooltip.swim": "Sobreviver na água sob condições adversas — resgatar outros, escapar ou suportar riscos.",
"DG.Skills.Tooltip.unarmed_combat": "Imobilizar ou ferir oponentes usando socos, agarrões, chutes e outras técnicas de combate corporal.",
"DG.Skills.Tooltip.unnatural": "Reconhecer, pesquisar ou recordar verdades sobre entidades e forças além da compreensão humana.",
"DG.Skills.AddTypedOrCustomSkill": "Adicionar Habilidade Personalizada",
"DG.Skills.AddSkill": "Adicionar Habilidade",
"DG.Skills.Apply": "Aplicar",
@@ -95,7 +91,6 @@
"DG.Skills.EditSkill": "Editar Habilidade",
"DG.Skills.SkillGroup": "Grupo de Habilidade",
"DG.Skills.SkillName": "Nome da Habilidade",
"DG.TypeSkills.Art": "Arte",
"DG.TypeSkills.Craft": "Ofício",
"DG.TypeSkills.ForeignLanguage": "Idioma Estrangeiro",
@@ -103,7 +98,6 @@
"DG.TypeSkills.Pilot": "Pilotar",
"DG.TypeSkills.Science": "Ciência",
"DG.TypeSkills.Other": "Outros",
"DG.TypeSkills.Tooltip.Art": "Criar ou avaliar obras artísticas e identificar falsificações em uma área específica.",
"DG.TypeSkills.Tooltip.Craft": "Construir, consertar ou sabotar ferramentas ou sistemas especializados, como motores, fechaduras ou fiação.",
"DG.TypeSkills.Tooltip.ForeignLanguage": "Falar, ler e compreender um idioma específico.",
@@ -111,7 +105,6 @@
"DG.TypeSkills.Tooltip.Pilot": "Controlar e navegar veículos aéreos, marítimos ou espaciais em situações ou condições críticas.",
"DG.TypeSkills.Tooltip.Science": "Aplicar conhecimento científico especializado para interpretar ou investigar o mundo físico.",
"DG.TypeSkills.Tooltip.Other": "Outros.",
"DG.SpecialTraining.Dialog.CreateSpecialTraining": "Adicionar Treinamento Especial",
"DG.SpecialTraining.Dialog.DropDown.CustomSkills": "Habilidades Personalizadas",
"DG.SpecialTraining.Dialog.DropDown.Skills": "Habilidades",
@@ -120,7 +113,6 @@
"DG.SpecialTraining.Dialog.NameLabel": "Nome",
"DG.SpecialTraining.Dialog.SkillLabel": "Habilidade ou Estatística",
"DG.SpecialTraining.Dialog.Title": "Criar/Editar Treinamento Especial",
"DG.Attributes.HP": "PV",
"DG.Attributes.WP": "FV",
"DG.Attributes.SAN": "SAN",
@@ -130,7 +122,6 @@
"DG.Attributes.int": "INT",
"DG.Attributes.pow": "POD",
"DG.Attributes.cha": "CAR",
"DG.Attributes.Tooltip.HP": "Pontos de Vida (PV) indicam a saúde física. Um personagem fica inconsciente com 2 PV e morre ao chegar a 0 PV.",
"DG.Attributes.Tooltip.WP": "Pontos de Força de Vontade (FV) indicam a resistência mental e a determinação. Agentes precisam de FV para continuar apesar do cansaço ou de situações mentalmente extenuantes. Veja a página 47 do Manual do Agente para mais detalhes.",
"DG.Attributes.Tooltip.SAN": "Pontos de Sanidade (SAN) indicam a conexão com a humanidade e com a realidade como a maioria das pessoas a percebe. Clique para rolar um teste de SAN.",
@@ -140,7 +131,6 @@
"DG.Attributes.Tooltip.int": "Veja a página 19 do Manual do Agente — Inteligência.",
"DG.Attributes.Tooltip.pow": "Veja a página 19 do Manual do Agente — Poder.",
"DG.Attributes.Tooltip.cha": "Veja a página 19 do Manual do Agente — Carisma.",
"DG.Navigation.Agent.skills": "Habilidades",
"DG.Navigation.Agent.physical": "Estatísticas",
"DG.Navigation.Agent.motivations": "Mental",
@@ -151,7 +141,6 @@
"DG.Navigation.NPC.gear": "Ataques & Habilidades",
"DG.Navigation.Vehicle.gear": "Equipamento",
"DG.Navigation.Vehicle.notes": "Notas",
"DG.About.Changelog": "CHANGELOG (ONLINE)",
"DG.About.Copyright": "Publicado pela Arc Dream Publishing por meio de acordo com a Delta Green Partnership. A propriedade intelectual conhecida como Delta Green é uma marca registrada e protegida por direitos autorais pertencente à Delta Green Partnership, que licenciou seu uso neste material.",
"DG.About.License": "O conteúdo deste sistema é licenciado de acordo com os termos do arquivo de <a href=\"systems/deltagreen/LICENSE.txt\">licença</a> incluído, com exceção dos elementos que fazem parte da propriedade intelectual de Delta Green.",
@@ -159,9 +148,7 @@
"DG.About.OnlineHelpTooltip": "Abrir navegador com ajuda online GitHub",
"DG.About.OnlineBugTracker": "Dúvidas, Relatos de Erros ou Solicitações de Funcionalides (ONLINE)",
"DG.About.OnlineBugTrackerTooltip": "Abrir navegador com rastreador de erros GitHub",
"DG.Roll.Rolling": "Rolando",
"DG.Roll.Target": "Alvo: ",
"DG.Roll.Success": "Sucesso",
"DG.Roll.Failure": "Falha",
"DG.Roll.CriticalSuccess": "Sucesso Crítico!",
@@ -173,14 +160,11 @@
"DG.Roll.RollingDamageFor": "Rolando DANO para",
"DG.Roll.Or": "Ou",
"DG.Roll.Roll": "Rolar",
"DG.Physical.Wounds": "Ferimentos",
"DG.Physical.FirstAidLabel": "Já houve tentativa de Primeiro Socorros: ",
"DG.Physical.Exhausted": "Exausto(a):",
"DG.Physical.ExhaustionPenalty": "Penalidade:",
"DG.Generic.AddButtonLabel": "Adicionar",
"DG.Mental.BreakingPoint": "Ponto de Ruptura: ",
"DG.Mental.Tooltip.BreakingPoint": "O Ponto de Ruptura é o valor exato de SAN no qual seu Agente foi suficientemente desgastado pelo trauma a ponto de desenvolver um novo distúrbio mental de longo prazo. Se a SAN atingir esse valor ou menos, subtraia o POD do Agente da nova SAN. Esse resultado passa a ser o novo Ponto de Ruptura do Agente.",
"DG.Mental.Reset": "Resetar",
@@ -193,7 +177,6 @@
"DG.Mental.AdaptedToHelplessness": "Adaptado ao Desamparo",
"DG.Mental.Motivations": "Motivações",
"DG.Mental.Disorders": "Distúrbios",
"DG.Gear.Weapons": "Armas",
"DG.Gear.WeaponTypes.Firearms": "Armas de Fogo",
"DG.Gear.WeaponTypes.Melee": "Armas Corpo a Corpo",
@@ -204,7 +187,6 @@
"DG.Gear.ArmorPiercing": "Perfuração de Armadura",
"DG.Gear.ArmorRating": "Categoria de Armadura",
"DG.Gear.Equipped": "Equipado",
"DG.CV.Sex": "Sexo",
"DG.CV.SexPlaceholder": "Sexo",
"DG.CV.AgeAndDoB": "Idade e Data de Nascimento",
@@ -215,12 +197,10 @@
"DG.CV.NationalityPlaceholder": "Nacionalidade",
"DG.CV.Education": "Educação",
"DG.CV.EducationPlaceholder": "Educação ou Treinamento",
"DG.Bonds.Bond": "Laço",
"DG.Bonds.Name": "Nome",
"DG.Bonds.Score": "Valor",
"DG.Bonds.NewBond": "Novo Laço",
"DG.Tooltip.MaximumHP": "A Vida máxima é a média de FOR + CON, arredondado para CIMA.",
"DG.Tooltip.MaximumWP": "FV máximo é igual a POD.",
"DG.Tooltip.CurrentSanityPartOne": "Sanidade atual, atual ponto de ruptura é ",
@@ -236,17 +216,14 @@
"DG.Tooltip.AdaptedToHelplessness": "Estar adaptado ao desamparo significa que a motivação pessoal do seu agente sofre à medida que ele se afasta do mundo ao seu redor. Assim que seu Agente se adaptar ao desamparo, o agente perde permanentemente 1D6 de POD.",
"DG.Tooltip.ToggleEquippedState": "Clique para alternar se este item está equipado ou não.",
"DG.Tooltip.Exhaustion": "Um agente que trabalha por muito tempo ou enfrenta perigo extremo e ferimentos sem descansar fica exausto. Quando isso entra em jogo depende do Handler, mas uma boa regra é que passar uma noite sem dormir ou se recusar a descansar após perder SAN ou PV leva à exaustão.",
"DG.Gear.Expense.NA": "N/A",
"DG.Gear.Expense.Incidental": "Incidental",
"DG.Gear.Expense.Standard": "Padrão",
"DG.Gear.Expense.Unusual": "Incomum",
"DG.Gear.Expense.Major": "Maior",
"DG.Gear.Expense.Extreme": "Extremo",
"DG.ItemWindow.Bonds.Description": "Descrição/Desenvolvimentos",
"DG.ItemWindow.Bonds.ScoreTooltip": "Uma pontuação mais alta representa uma relação mais forte entre o agente e o vínculo.",
"DG.ItemWindow.Motivations.Attributes": "Atributos",
"DG.ItemWindow.Motivations.Motivation": "Motivação",
"DG.ItemWindow.Motivations.Description": "Descrição/Notas",
@@ -257,19 +234,16 @@
"DG.ItemWindow.Motivations.IsDisorderTooltip": "Marque isso se adicionar um distúrbio ou se uma motivação existente foi perdida devido ao ganho de um distúrbio.",
"DG.ItemWindow.Motivations.DisorderHasBeenCured": "Distúrbio foi \"Curado\"",
"DG.ItemWindow.Motivations.DisorderHasBeenCuredTooltip": "Um sucesso crítico em um teste de sorte na terapia pode 'curar' um distúrbio. No entanto, pode retornar na próxima vez que um ponto de ruptura for atingido. Consulte a seção Terapia nas páginas 78-79 do Manual do Agente.",
"DG.ItemWindow.Gear.Attributes": "Atributos",
"DG.ItemWindow.Gear.Description": "Descrição",
"DG.ItemWindow.Gear.NamePlaceholder": "Nome",
"DG.ItemWindow.Gear.Expense": "Custo",
"DG.ItemWindow.Gear.Equipped": "Equipado",
"DG.ItemWindow.Armor.Description": "Descrição",
"DG.ItemWindow.Armor.NamePlaceholder": "Nome",
"DG.ItemWindow.Armor.ArmorRating": "Categoria de Armadura",
"DG.ItemWindow.Armor.Expense": "Custo",
"DG.ItemWindow.Armor.Equipped": "Equipado",
"DG.ItemWindow.Weapons.Description": "Descrição",
"DG.ItemWindow.Weapons.NamePlaceholder": "Nome",
"DG.ItemWindow.Weapons.Skill": "Habilidade",
@@ -283,27 +257,21 @@
"DG.ItemWindow.Weapons.Ammo": "Munição (quantidade/tipo)",
"DG.ItemWindow.Weapons.Range": "Alcance",
"DG.ItemWindow.Weapons.KillRadius": "Raio de Morte",
"DG.ModifySkillRollDialogue.Title": "Modificar Rolagem",
"DG.ModifySkillRollDialogue.Test": "Teste:",
"DG.ModifySkillRollDialogue.OriginalTarget": "Alvo Original:",
"DG.ModifySkillRollDialogue.TargetModifier": "Modificar Alvo (+/-):",
"DG.ModifySkillRollDialogue.ModifierTooltip": "Dê um modificador positivo ou negativo aqui para adicionar/subtrair do alvo para o teste de perícia.",
"DG.ModifyDamageRollDialogue.Weapon": "Arma:",
"DG.ModifyDamageRollDialogue.OuterModifierTooltip": "Use este campo para modificar a parte externa da fórmula, como dobrar o resultado.",
"DG.ModifyDamageRollDialogue.InnerModifierTooltip": "Use este campo para adicionar um modificador simples como '+1' ou outro termo de dados como '+1d4'.",
"DG.Roll.Inhuman": "Desumano",
"DG.Tooltip.BreakPoint": "O ponto de ruptura atual, redefinido quando a sanidade atual cair para ou abaixo desse valor. O ponto de ruptura inicial é SAN - POW.",
"DG.Tooltip.BreakPointReset": "Redefina o ponto de ruptura para ser SAN - FV.",
"DG.Skills.ritual": "Ritual",
"DG.RollMode": "Modo de Rolagem",
"DG.Bonds.Relationship": "Relacionamento",
"DG.TypeSkills.Subskills.Painting": "Pintura",
"DG.Sheet.BlockHeaders.Attributes": "ATRIBUTOS",
"DG.Sheet.BlockHeaders.Statistics": "ESTATÍSTICAS",
"DG.Sheet.BlockHeaders.PhysicalDescriptionNotes": "Descrição Física/Notas",
@@ -313,50 +281,39 @@
"DG.Sheet.BlockHeaders.MotivationsAndDisorders": "MOTIVAÇÕES E DISTÚRBIOS",
"DG.Sheet.BlockHeaders.PersonalInfoCV": "INFORMAÇÕES PESSOAIS / C.V.",
"DG.Sheet.BlockHeaders.BondsRelationshipsContacts": "LAÇOS / RELACIONAMENTOS / CONTATOS",
"DG.AgentSheet.ProfessionPlaceholder": "PROFISSÂO",
"DG.UnnaturalSheet.HideUntrainedSkillsLabel": "Esconder Habilidades não Treinadas",
"DG.UnnaturalSheet.NotesLabel": "Notas",
"DG.UnnaturalSheet.ShortDescriptionPlaceholder": "Descrição de Criatura ou Tipo de Criatura",
"DG.UnnaturalSheet.ShowUntrainedSkillsLabel": "Mostrar Habilidades não Treinadas",
"DG.UnnaturalSheet.Tabs.NotesTabLabel": "Habilidades & Notas",
"DG.UnnaturalSheet.Tabs.ItemsTabLabel": "Ataques & Habilidades",
"DG.NPCSheet.BreakPointAbbreviation": "PR",
"DG.ImpossibleLanscapes.IHaveSeenThe": "EU JÁ VI O",
"DG.ImpossibleLanscapes.YellowSign": "SÍMBOLO AMARELO",
"DG.ImpossibleLanscapes.CorruptionFirstHalf": "Corrup",
"DG.ImpossibleLanscapes.CorruptionSecondHalf": "ção",
"DG.ImpossibleLanscapes.Gift": "Dom",
"DG.ImpossibleLanscapes.Insight": "Intuição",
"DG.VehicleSheet.NotesTabName": "Notas",
"DG.VehicleSheet.GearTabName": "Equipamentos",
"DG.VehicleSheet.NotesLabel": "NOTAS",
"DG.VehicleSheet.DescriptionPlaceHolder": "Descrição do Veículo",
"DG.Generic.CheckDamagedBondsBoxUntilNextHomeScene": "Marcar Vínculos Danificados até a Próxima Cena em Casa",
"DG.Generic.ClearAll": "Limpar Tudo",
"DG.Generic.SanDamage": "Dano de SAN",
"DG.ItemWindow.Weapons.CustomSkillTarget": "Alvo",
"DG.ItemWindow.Weapons.SkillModifier": "Modificador de Habilidade",
"DG.ItemWindow.Custom": "Personalizado",
"DG.Sheet.class.item": "Ficha de item",
"DG.Sheet.class.agent": "Ficha de Agente",
"DG.Sheet.class.npc": "Ficha de PNJ",
"DG.Sheet.class.unnatural": "Ficha de Antinatural",
"DG.Sheet.class.vehicle": "Ficha de Veículo",
"TYPES.Actor.agent": "Agente",
"TYPES.Actor.npc": "PNJ",
"TYPES.Actor.unnatural": "Antinatural",
"TYPES.Actor.vehicle": "Veículo",
"TYPES.Item.armor": "Armadura",
"TYPES.Item.bond": "Vinculo",
"TYPES.Item.gear": "Equipamento",
@@ -364,13 +321,10 @@
"TYPES.Item.weapon": "Arma",
"TYPES.Item.tome": "Tomo",
"TYPES.Item.ritual": "Ritual",
"DG.Gear.Tomes": "TOMOS",
"DG.Gear.Rituals": "RITUAIS",
"DG.Tomes.Name": "Nome",
"DG.Ritual.Name": "Nome",
"DG.Tome.Name": "Nome",
"DG.Tome.Language": "Idioma",
"DG.Tome.StudyTime": "Tempo de Estudo",
@@ -382,59 +336,45 @@
"DG.ItemWindow.Tome.StudySanityDamage": "Dano de SAN ao Estudar",
"DG.ItemWindow.Tome.HandlerOnlyTabHeader": "Chefe Somente",
"DG.ItemWindow.Tome.HandlerNotes": "Notas do Chefe",
"DG.ItemWindow.Ritual.ActivationSanityDamage": "Dano de SAN na Ativação",
"DG.ItemWindow.Ritual.ActivationTime": "Tempo de Ativação",
"DG.ItemWindow.Ritual.ActivationCosts": "Custos de Ativação",
"DG.ItemWindow.Ritual.StudyTime": "Tempo de Estudo",
"DG.ItemWindow.Ritual.LearnedSanityDamage": "Dano de SAN ao Aprender",
"DG.MissionPaused": "Operação Pausada",
"DG.Settings.charactersheet.name": "Estilo da Ficha de Personagem",
"DG.Settings.charactersheet.hint": "Escolha como as fichas devem ser estilizadas. Programa tem um visual governamental moderno, enquanto Caubois/Fora-da-lei remete a um estilo mais antigo e áspero, como datilografado.",
"DG.Settings.charactersheet.cowboys": "Os Caubois",
"DG.Settings.charactersheet.outlaws": "Os Fora-da-lei",
"DG.Settings.charactersheet.program": "O Programa",
"DG.Settings.sortskills.name": "Ordenar Perícias por Coluna?",
"DG.Settings.sortskills.hint": "Marcado: ordena por coluna. Desmarcado: ordena por linha.",
"DG.Settings.skillTooltipDisplay.name": "Exibir dicas (tooltips)",
"DG.Settings.skillTooltipDisplay.hint": "Escolha quando exibir as dicas das perícias nas fichas dos Agentes.",
"DG.Settings.skillTooltipDisplay.hover": "Ao passar o mouse (padrão)",
"DG.Settings.skillTooltipDisplay.never": "Nunca",
"DG.Settings.skillTooltipDisplay.hoverShift": "Ao passar o mouse + Shift",
"DG.Settings.keepSanityPrivate.name": "Manter Sanidade Privada",
"DG.Settings.keepSanityPrivate.hint": "Oculta a Sanidade dos jogadores tanto nas fichas quanto nas rolagens.",
"DG.Settings.skillImprovementFormula.name": "Rolagem Padrão de Aprimoramento de Perícia",
"DG.Settings.skillImprovementFormula.hint": "Padrão: 1d4. Houve múltiplas erratas sobre isso; escolha a opção que melhor se adequa ao ritmo do seu jogo.",
"DG.Settings.skillImprovementFormula.1": "+1% Fixo",
"DG.Settings.skillImprovementFormula.2": "+1D3%",
"DG.Settings.skillImprovementFormula.3": "+1D4%",
"DG.Settings.skillImprovementFormula.4": "+1D4-1%",
"DG.Settings.alwaysShowHypergeometrySectionForPlayers.name": "Exibir Seção de Hipergeometria",
"DG.Settings.alwaysShowHypergeometrySectionForPlayers.hint": "Sempre exibe a seção de itens de Hipergeometria (ex.: Rituais e Tomos) nas fichas dos Agentes, mesmo que o Agente não possua itens desse tipo.",
"DG.Settings.showImpossibleLandscapesContent.name": "Exibir Conteúdo de Impossible Landscapes",
"DG.Settings.showImpossibleLandscapesContent.hint": "Exibe campos específicos de Impossible Landscapes nas fichas dos Agentes. Esses campos são sempre visíveis apenas para o Chefe, mesmo com esta opção ativada.",
"DG.Settings.skillFailure.name": "Marcar Falha de Perícia Automaticamente",
"DG.Settings.skillFailure.hint": "Ative esta opção para marcar automaticamente a caixa de falha ao falhar em uma perícia. O Chefe pode desfazer a ação a partir da mensagem de chat da rolagem.",
"DG.Settings.Saved": "As configurações atuais foram salvas.",
"DG.SettingsMenu.automation.name": "Menu de Automação",
"DG.SettingsMenu.automation.label": "Menu de Automação",
"DG.SettingsMenu.automation.title": "Configurações de Automação",
"DG.SettingsMenu.handler.name": "Configurações do Chefe",
"DG.SettingsMenu.handler.label": "Configurações do Chefe",
"DG.SettingsMenu.handler.title": "Configurações Exclusivas do Chefe",
"DG.Messages.Skillsmark.Marked": "Esta perícia é marcada",
"DG.Messages.Skillsmark.Tooltip": "Veja pág. 29 do Manual do Agente"
}
-42
View File
@@ -3,10 +3,8 @@
"DG.AgentNameFieldTooltip": "EFTERNAMN, FÖRNAMN (OCH ALIAS OM RELEVANT)",
"DG.RollLuck": "Turkast",
"DG.Luck": "Tur",
"DG.FallbackText.ProfessionAndOrRank": "YRKE",
"DG.FallbackText.newItem": "Ny {type}",
"DG.Skills.accounting": "Bokföring",
"DG.Skills.alertness": "Vaksamhet",
"DG.Skills.anthropology": "Antropologi",
@@ -45,7 +43,6 @@
"DG.Skills.unarmed_combat": "Slagsmål",
"DG.Skills.unnatural": "Onaturligt",
"DG.Skills.flight": "Flyg",
"DG.Skills.AddTypedOrCustomSkill": "Lägg till anpassad färdighet",
"DG.Skills.AddSkill": "Lägg till färdighet",
"DG.Skills.Apply": "Tillämpa",
@@ -58,7 +55,6 @@
"DG.Skills.EditSkill": "Redigera färdighet",
"DG.Skills.SkillGroup": "Färdighetsgrupp",
"DG.Skills.SkillName": "Färdighetsnamn",
"DG.TypeSkills.Art": "Konst",
"DG.TypeSkills.Craft": "Hantverk",
"DG.TypeSkills.ForeignLanguage": "Främmande språk",
@@ -66,7 +62,6 @@
"DG.TypeSkills.Pilot": "Pilot",
"DG.TypeSkills.Science": "Vetenskap",
"DG.TypeSkills.Other": "Annat",
"DG.SpecialTraining.Dialog.CreateSpecialTraining": "Lägg till specialträning",
"DG.SpecialTraining.Dialog.DropDown.CustomSkills": "Anpassade färdigheter",
"DG.SpecialTraining.Dialog.DropDown.Skills": "Färdigheter",
@@ -75,7 +70,6 @@
"DG.SpecialTraining.Dialog.NameLabel": "Namn",
"DG.SpecialTraining.Dialog.SkillLabel": "Färdighet eller statistik",
"DG.SpecialTraining.Dialog.Title": "Skapa/Redigera specialträning",
"DG.Attributes.HP": "HP",
"DG.Attributes.WP": "VP",
"DG.Attributes.SAN": "PSY",
@@ -85,21 +79,17 @@
"DG.Attributes.int": "INT",
"DG.Attributes.pow": "VIL",
"DG.Attributes.cha": "KAR",
"DG.Navigation.Skills": "Färdigheter",
"DG.Navigation.Physical": "Statistik",
"DG.Navigation.Mental": "Psykologi",
"DG.Navigation.Gear": "Utrustning",
"DG.Navigation.CV": "Personligt",
"DG.Navigation.Contacts": "Kontakter",
"DG.About.OnlineHelp": "Systemdokumentation på (ONLINE)",
"DG.About.OnlineHelpTooltip": "Starta webbläsare med online GitHub-hjälp.",
"DG.About.OnlineBugTracker": "Frågor, buggrapporter eller funktionsförfrågningar (ONLINE)",
"DG.About.OnlineBugTrackerTooltip": "Starta webbläsare med GitHub-issue tracker.",
"DG.Roll.Rolling": "Slår för",
"DG.Roll.Target": "Mål: ",
"DG.Roll.Success": "Träff",
"DG.Roll.Failure": "Miss",
"DG.Roll.Critical": "Kritisk",
@@ -110,14 +100,11 @@
"DG.Roll.RollingDamageFor": "Slår SKADA för",
"DG.Roll.Or": "Eller",
"DG.Roll.Roll": "Slå",
"DG.Physical.Wounds": "Sår",
"DG.Physical.FirstAidLabel": "Första hjälpen har redan försökts: ",
"DG.Physical.Exhausted": "Utmattad:",
"DG.Physical.ExhaustionPenalty": "Nedsättning:",
"DG.Generic.AddButtonLabel": "Lägg till",
"DG.Mental.BreakingPoint": "Brytpunkt: ",
"DG.Mental.Reset": "Reset",
"DG.Mental.SanityLossIncidentsLabel": "Episoder av PSY-förlust utan att bli galen",
@@ -127,7 +114,6 @@
"DG.Mental.AdaptedToHelplessness": "Anpassad till hjälplöshet",
"DG.Mental.Motivations": "Motivation",
"DG.Mental.Disorders": "Störningar",
"DG.Gear.Weapons": "Vapen",
"DG.Gear.Armor": "Kroppskydd",
"DG.Gear.OtherGear": "Annan utrustning",
@@ -136,7 +122,6 @@
"DG.Gear.ArmorPiercing": "Genomträngande",
"DG.Gear.ArmorRating": "Skyddsnivå",
"DG.Gear.Equipped": "Utrustad",
"DG.CV.Sex": "Kön",
"DG.CV.SexPlaceholder": "Kön",
"DG.CV.AgeAndDoB": "Ålder och födelsedatum",
@@ -147,12 +132,10 @@
"DG.CV.NationalityPlaceholder": "Nationalitet",
"DG.CV.Education": "Utbildning",
"DG.CV.EducationPlaceholder": "Utbildning eller skola",
"DG.Bonds.Bond": "Kamrat",
"DG.Bonds.Name": "Namn",
"DG.Bonds.Score": "Poäng",
"DG.Bonds.NewBond": "Ny Kamrat",
"DG.Attributes.Tooltip.HP": "Hälsopoäng, en karaktär blir medvetslös vid 2 HP och dör vid 0 HP.",
"DG.Tooltip.MaximumHP": "Maximala hälsopoäng är hälften av STY + FYS, avrundat UPP.",
"DG.Attributes.Tooltip.WP": "Viljepoäng är mentalt bränsle. Agenter behöver viljepoäng för att fortsätta trots utmattning eller mentalt påfrestande situationer. Se sidan 47 i Agenthandboken för mer detaljer.",
@@ -171,17 +154,14 @@
"DG.Tooltip.AdaptedToHelplessness": "Att vara anpassad till hjälplöshet innebär att din agent förlorat sin personliga drivkraft och drar sig tillbaka från världen omkring dem. När din agent är anpassad till hjälplöshet förlorar agenten permanent 1D6 VIL.",
"DG.Tooltip.ToggleEquippedState": "Klicka för att välja om din agent har saken med sig eller ej.",
"DG.Tooltip.Exhaustion": "En agent som arbetat för länge eller varit under press länge utan sömn blir utmattad.",
"DG.Gear.Expense.NA": "Ingen",
"DG.Gear.Expense.Incidental": "Småsak",
"DG.Gear.Expense.Standard": "Standard",
"DG.Gear.Expense.Unusual": "Ovanlig",
"DG.Gear.Expense.Major": "Stor",
"DG.Gear.Expense.Extreme": "Extrem",
"DG.ItemWindow.Bonds.Description": "Beskrivning/Utvecklingar",
"DG.ItemWindow.Bonds.ScoreTooltip": "Högre poäng representerar ett starkare förhållande mellan agenten och Kamraten.",
"DG.ItemWindow.Motivations.Attributes": "Egenskaper",
"DG.ItemWindow.Motivations.Motivation": "Motivation",
"DG.ItemWindow.Motivations.Description": "Beskrivning/Anteckningar",
@@ -192,19 +172,16 @@
"DG.ItemWindow.Motivations.IsDisorderTooltip": "Kryssa i detta om du lägger till en störning, eller om en befintlig motivation har förlorats på grund av att du fått en störning.",
"DG.ItemWindow.Motivations.DisorderHasBeenCured": "Störning har blivit \"botad\"",
"DG.ItemWindow.Motivations.DisorderHasBeenCuredTooltip": "En kritisk framgång på ett terapi-turkast kan 'bota' en störning. Den kan dock återkomma nästa gång en brytpunkt nås. Se Terapisektionen på sidorna 78-79 i Agenthandboken.",
"DG.ItemWindow.Gear.Attributes": "Egenskaper",
"DG.ItemWindow.Gear.Description": "Beskrivning",
"DG.ItemWindow.Gear.NamePlaceholder": "Namn",
"DG.ItemWindow.Gear.Expense": "Kostnad",
"DG.ItemWindow.Gear.Equipped": "Utrustad",
"DG.ItemWindow.Armor.Description": "Beskrivning",
"DG.ItemWindow.Armor.NamePlaceholder": "Namn",
"DG.ItemWindow.Armor.ArmorRating": "Skyddsnivå",
"DG.ItemWindow.Armor.Expense": "Kostnad",
"DG.ItemWindow.Armor.Equipped": "Utrustad",
"DG.ItemWindow.Weapons.Description": "Beskrivning",
"DG.ItemWindow.Weapons.NamePlaceholder": "Namn",
"DG.ItemWindow.Weapons.Skill": "Färdighet",
@@ -218,27 +195,21 @@
"DG.ItemWindow.Weapons.Ammo": "Ammo (Mängd/Typ)",
"DG.ItemWindow.Weapons.Range": "Räckvidd",
"DG.ItemWindow.Weapons.KillRadius": "Dödsradie",
"DG.ModifySkillRollDialogue.Title": "Modifiera kast",
"DG.ModifySkillRollDialogue.Test": "Slag:",
"DG.ModifySkillRollDialogue.OriginalTarget": "Ursprungligt mål:",
"DG.ModifySkillRollDialogue.TargetModifier": "Modifiera mål:",
"DG.ModifySkillRollDialogue.ModifierTooltip": "Ange en positiv eller negativ modifiering här.",
"DG.ModifyDamageRollDialogue.Weapon": "Vapen:",
"DG.ModifyDamageRollDialogue.OuterModifierTooltip": "Använd detta fält för att modifiera den yttre delen av formeln, som att dubbla resultatet.",
"DG.ModifyDamageRollDialogue.InnerModifierTooltip": "Använd detta fält för att lägga till en platt modifierare såsom '+1' eller en annan tärningsterm såsom '+1d4'.",
"DG.Roll.Inhuman": "Omänsklig",
"DG.Tooltip.BreakPoint": "Den nuvarande brytpunkten, återställs när nuvarande Stabilitet faller till eller under detta värde. Startvärdet på brytpunkten är PSY minus VIL.",
"DG.Tooltip.BreakPointReset": "Återställ brytpunkten till att vara nuvarande PSY minus VP.",
"DG.Skills.ritual": "Ritual",
"DG.RollMode": "Tärningskast-läge",
"DG.Bonds.Relationship": "Relation",
"DG.TypeSkills.Subskills.Painting": "Målning",
"DG.Sheet.BlockHeaders.Attributes": "EGENSKAPER",
"DG.Sheet.BlockHeaders.Statistics": "STATISTIK",
"DG.Sheet.BlockHeaders.PhysicalDescriptionNotes": "ANTECKNINGAR",
@@ -248,7 +219,6 @@
"DG.Sheet.BlockHeaders.MotivationsAndDisorders": "MOTIVATIONER OCH STÖRNINGAR",
"DG.Sheet.BlockHeaders.PersonalInfoCV": "PERSONLIG INFORMATION",
"DG.Sheet.BlockHeaders.BondsRelationshipsContacts": "KAMRATER OCH RELATIONER",
"DG.UnnaturalSheet.HideUntrainedSkillsLabel": "Dölj otränade färdigheter",
"DG.UnnaturalSheet.NotesLabel": "Anteckningar",
"DG.UnnaturalSheet.ShortDescriptionFallbackText": "Beskrivning av varelse",
@@ -256,33 +226,26 @@
"DG.UnnaturalSheet.Tabs.NotesTabLabel": "Färdigheter & Anteckningar",
"DG.UnnaturalSheet.Tabs.ItemsTabLabel": "Attacker & Förmågor",
"DG.NPCSheet.BreakPointAbbreviation": "BP",
"DG.ImpossibleLanscapes.IHaveSeenThe": "JAG HAR SETT",
"DG.ImpossibleLanscapes.YellowSign": "DET GULA MÄRKET",
"DG.ImpossibleLanscapes.CorruptionFirstHalf": "Korrup",
"DG.ImpossibleLanscapes.CorruptionSecondHalf": "tion",
"DG.ImpossibleLanscapes.Gift": "Gåva",
"DG.ImpossibleLanscapes.Insight": "Insikt",
"DG.VehicleSheet.NotesTabName": "Anteckningar",
"DG.VehicleSheet.GearTabName": "Utrustning",
"DG.VehicleSheet.NotesLabel": "ANTECKNINGAR",
"DG.VehicleSheet.DescriptionPlaceHolder": "Beskrivning av fordon",
"DG.Generic.CheckDamagedBondsBoxUntilNextHomeScene": "Bocka i rutan om ett förhållande är skadat för att komma ihåg det under Hem-scenerna.",
"DG.Generic.ClearAll": "Rensa alla",
"DG.Generic.SanDamage": "PSY-Skada",
"DG.ItemWindow.Weapons.CustomSkillTarget": "Mål",
"DG.ItemWindow.Weapons.SkillModifier": "Färdighetsmod",
"DG.ItemWindow.Custom": "Anpassad",
"TYPES.Actor.agent": "Agent",
"TYPES.Actor.npc": "NPC",
"TYPES.Actor.unnatural": "Onaturligt",
"TYPES.Actor.vehicle": "Fordon",
"TYPES.Item.armor": "Kroppskydd",
"TYPES.Item.bond": "Kamrat",
"TYPES.Item.gear": "Utrustning",
@@ -290,13 +253,10 @@
"TYPES.Item.weapon": "Vapen",
"TYPES.Item.tome": "Trollbok",
"TYPES.Item.ritual": "Ritual",
"DG.Gear.Tomes": "Trollböcker",
"DG.Gear.Rituals": "Ritualer",
"DG.Tomes.Name": "Namn",
"DG.Ritual.Name": "Namn",
"DG.Tome.Name": "Namn",
"DG.ItemWindow.Tome.Description": "Beskrivning",
"DG.ItemWindow.Tome.LanguagePlaceholder": "Språk",
@@ -306,13 +266,11 @@
"DG.ItemWindow.Tome.StudySanityDamage": "Skada på Stabilitet att läsa",
"DG.ItemWindow.Tome.HandlerOnlyTabHeader": "Endast Handledaren",
"DG.ItemWindow.Tome.HandlerNotes": "Handledarens anteckningar",
"DG.ItemWindow.Ritual.ActivationSanityDamage": "PSY-Skada för aktivering",
"DG.ItemWindow.Ritual.ActivationTime": "Aktiveringstid",
"DG.ItemWindow.Ritual.ActivationCosts": "Aktiveringskostnader",
"DG.ItemWindow.Ritual.StudyTime": "Studietid",
"DG.ItemWindow.Ritual.LearnedSanityDamage": "Learn SAN Damage",
"DG.Messages.Skillsmark.Marked": "Denna färdighet är markerad",
"DG.Messages.Skillsmark.Tooltip": "Se s. 29 Agenthandbok"
}
-51
View File
@@ -3,9 +3,7 @@
"DG.AgentNameFieldTooltip": "ПРІЗВИЩЕ, ІМ’Я (ТА ПСЕВДОНІМ, ЯКЩО Є)",
"DG.RollLuck": "Кинути Удачу",
"DG.Luck": "Удача",
"DG.FallbackText.newItem": "Новий(а) {type}",
"DG.Skills.accounting": "Бухгалтерія",
"DG.Skills.alertness": "Пильність",
"DG.Skills.anthropology": "Антропологія",
@@ -44,7 +42,6 @@
"DG.Skills.unarmed_combat": "Рукопашний бій",
"DG.Skills.unnatural": "Протиприродне",
"DG.Skills.flight": "Пілотування",
"DG.Skills.Tooltip.accounting": "Помічати фінансові «червоні прапорці»: приховані активи, фірми-«прокладки» або розкрадання.",
"DG.Skills.Tooltip.alertness": "Помічати приховані загрози, ледь помітні рухи або тихі звуки, доки вони не стали небезпекою.",
"DG.Skills.Tooltip.anthropology": "Розуміти вірування, поведінку та табу живих культур і суспільств.",
@@ -82,7 +79,6 @@
"DG.Skills.Tooltip.swim": "Виживати у воді під тиском: рятувати, тікати або витримувати загрози.",
"DG.Skills.Tooltip.unarmed_combat": "Знешкоджувати або травмувати супротивника ударами, захватами та прийомами рукопашного бою.",
"DG.Skills.Tooltip.unnatural": "Розпізнавати, досліджувати або пригадувати істини про сили, що виходять за межі людського розуміння.",
"DG.Skills.AddTypedOrCustomSkill": "Додати типізовану/власну навичку",
"DG.Skills.AddSkill": "Додати навичку",
"DG.Skills.Apply": "Застосувати",
@@ -95,7 +91,6 @@
"DG.Skills.EditSkill": "Редагувати навичку",
"DG.Skills.SkillGroup": "Група навичок",
"DG.Skills.SkillName": "Назва навички",
"DG.TypeSkills.Art": "Мистецтво",
"DG.TypeSkills.Craft": "Ремесло",
"DG.TypeSkills.ForeignLanguage": "Іноземна мова",
@@ -103,7 +98,6 @@
"DG.TypeSkills.Pilot": "Пілотування",
"DG.TypeSkills.Science": "Наука",
"DG.TypeSkills.Other": "Інше",
"DG.TypeSkills.Tooltip.Art": "Створювати або оцінювати мистецькі роботи та помічати підробки у вибраній сфері.",
"DG.TypeSkills.Tooltip.Craft": "Будувати, лагодити або саботувати спеціалізовані інструменти чи системи (двигуни, замки, проводка).",
"DG.TypeSkills.Tooltip.ForeignLanguage": "Говорити, читати й розуміти конкретну мову.",
@@ -111,7 +105,6 @@
"DG.TypeSkills.Tooltip.Pilot": "Керувати та навігувати повітряні/морські/космічні апарати в критичних умовах.",
"DG.TypeSkills.Tooltip.Science": "Застосовувати спеціалізовані наукові знання для тлумачення або дослідження фізичного світу.",
"DG.TypeSkills.Tooltip.Other": "Інше",
"DG.SpecialTraining.Dialog.CreateSpecialTraining": "Додати спеціальну підготовку",
"DG.SpecialTraining.Dialog.DropDown.CustomSkills": "Власні/типізовані навички",
"DG.SpecialTraining.Dialog.DropDown.Skills": "Навички",
@@ -120,7 +113,6 @@
"DG.SpecialTraining.Dialog.NameLabel": "Назва",
"DG.SpecialTraining.Dialog.SkillLabel": "Навичка або атрибут",
"DG.SpecialTraining.Dialog.Title": "Створити/Редагувати спеціальну підготовку",
"DG.Attributes.HP": "ПЗ",
"DG.Attributes.WP": "ПВ",
"DG.Attributes.SAN": "ГЗД",
@@ -130,7 +122,6 @@
"DG.Attributes.int": "ІНТ",
"DG.Attributes.pow": "НЕП",
"DG.Attributes.cha": "ХАР",
"DG.Attributes.Tooltip.HP": "Пункти Здоров’я (ПЗ) показують фізичний стан. Персонаж непритомніє при 2 ПЗ і помирає при 0 ПЗ.",
"DG.Attributes.Tooltip.WP": "Пункти Волі (ПВ) показують психічну витримку та внутрішній ресурс. Агентам потрібні ПВ, щоб діяти попри виснаження або ментально важкі ситуації. Див. стор. 47 в Agent's Handbook.",
"DG.Attributes.Tooltip.SAN": "Пункти Глузду (ГЗД) показують зв’язок із людяністю та реальністю, як її сприймає більшість людей. Натисни, щоб кинути тест ГЗД.",
@@ -140,7 +131,6 @@
"DG.Attributes.Tooltip.int": "Див. стор. 19 в Agent's Handbook — Intelligence (Інтелект)",
"DG.Attributes.Tooltip.pow": "Див. стор. 19 в Agent's Handbook — Power (Непохитність)",
"DG.Attributes.Tooltip.cha": "Див. стор. 19 в Agent's Handbook — Charisma (Харизма)",
"DG.Navigation.Agent.skills": "Навички",
"DG.Navigation.Agent.physical": "Статистика",
"DG.Navigation.Agent.motivations": "Психологія",
@@ -151,7 +141,6 @@
"DG.Navigation.NPC.gear": "Атаки та здібності",
"DG.Navigation.Vehicle.gear": "Спорядження",
"DG.Navigation.Vehicle.notes": "Нотатки",
"DG.About.Changelog": "СПИСОК ЗМІН (ОНЛАЙН)",
"DG.About.Copyright": "Опубліковано Arc Dream Publishing за домовленістю з Delta Green Partnership. Інтелектуальна власність під назвою Delta Green є торговельною маркою та об’єктом авторського права Delta Green Partnership, яка ліцензувала її використання тут.",
"DG.About.License": "Вміст цієї системи ліцензовано згідно з умовами файлу <a href=\"systems/deltagreen/LICENSE.txt\">license</a>, за винятком елементів, які належать до інтелектуальної власності Delta Green.",
@@ -159,9 +148,7 @@
"DG.About.OnlineHelpTooltip": "Відкрити браузер з онлайн-довідкою GitHub.",
"DG.About.OnlineBugTracker": "Запитання, баг-репорти або пропозиції (ОНЛАЙН)",
"DG.About.OnlineBugTrackerTooltip": "Відкрити браузер з трекером задач GitHub.",
"DG.Roll.Rolling": "Кидаємо...",
"DG.Roll.Target": "Ціль: ",
"DG.Roll.Success": "Успіх",
"DG.Roll.Failure": "Провал",
"DG.Roll.CriticalSuccess": "Критичний успіх!",
@@ -173,14 +160,11 @@
"DG.Roll.RollingDamageFor": "Кидок ШКОДИ для",
"DG.Roll.Or": "Або",
"DG.Roll.Roll": "Кинути",
"DG.Physical.Wounds": "Поранення",
"DG.Physical.FirstAidLabel": "Перша допомога вже була застосована: ",
"DG.Physical.Exhausted": "Виснаження:",
"DG.Physical.ExhaustionPenalty": "Штраф:",
"DG.Generic.AddButtonLabel": "Додати",
"DG.Mental.BreakingPoint": "Точка зламу (ТЗ): ",
"DG.Mental.Tooltip.BreakingPoint": "Точка зламу — це конкретне значення ГЗД, на якому Агент настільки виснажений травмою, що отримує новий довготривалий психічний розлад. Якщо ГЗД падає до цього значення або нижче, відніми НЕП (POW) від нового ГЗД. Це і є нова Точка зламу.",
"DG.Mental.Reset": "Скинути",
@@ -193,7 +177,6 @@
"DG.Mental.AdaptedToHelplessness": "Адаптований до безпорадності",
"DG.Mental.Motivations": "Мотивації",
"DG.Mental.Disorders": "Розлади",
"DG.Gear.WeaponTypes.Firearms": "Вогнепальна",
"DG.Gear.WeaponTypes.Melee": "Холодна",
"DG.Gear.Weapons": "ЗБРОЯ",
@@ -204,7 +187,6 @@
"DG.Gear.ArmorPiercing": "Бронепробиття",
"DG.Gear.ArmorRating": "Рейтинг броні",
"DG.Gear.Equipped": "Екіпіровано",
"DG.CV.Sex": "Стать",
"DG.CV.SexPlaceholder": "Стать",
"DG.CV.AgeAndDoB": "Вік і дата народження",
@@ -215,12 +197,10 @@
"DG.CV.NationalityPlaceholder": "Національність",
"DG.CV.Education": "Освіта",
"DG.CV.EducationPlaceholder": "Освіта або підготовка",
"DG.Bonds.Bond": "Зв’язок",
"DG.Bonds.Name": "Ім’я",
"DG.Bonds.Score": "Рівень",
"DG.Bonds.NewBond": "Новий зв’язок",
"DG.Tooltip.MaximumHP": "Максимальні ПЗ — це середнє значення (СИЛ + СТА), округлене ВГОРУ.",
"DG.Tooltip.MaximumWP": "Максимальні ПВ дорівнюють НЕП.",
"DG.Tooltip.CurrentSanityPartOne": "Поточний ГЗД, поточна Точка зламу: ",
@@ -236,17 +216,14 @@
"DG.Tooltip.AdaptedToHelplessness": "Адаптація до безпорадності означає, що внутрішній драйв Агента згасає. Щойно Агент адаптується, він назавжди втрачає 1D6 НЕП.",
"DG.Tooltip.ToggleEquippedState": "Натисни, щоб перемкнути стан «Екіпіровано».",
"DG.Tooltip.Exhaustion": "Агент, який працює надто довго або стикається з крайнім ризиком і травмами без відпочинку, виснажується. Коли це настає — вирішує Handler, але хороше правило: ніч без сну або відмова від відпочинку після втрати ГЗД чи ПЗ веде до виснаження.",
"DG.Gear.Expense.NA": "Н/Д або немає",
"DG.Gear.Expense.Incidental": "Дрібне",
"DG.Gear.Expense.Standard": "Стандартне",
"DG.Gear.Expense.Unusual": "Незвичне",
"DG.Gear.Expense.Major": "Значне",
"DG.Gear.Expense.Extreme": "Екстремальне",
"DG.ItemWindow.Bonds.Description": "Опис/розвиток",
"DG.ItemWindow.Bonds.ScoreTooltip": "Вищий рівень означає сильніший зв’язок між Агентом і цією людиною.",
"DG.ItemWindow.Motivations.Attributes": "Атрибути",
"DG.ItemWindow.Motivations.Motivation": "Мотивація",
"DG.ItemWindow.Motivations.Description": "Опис/нотатки",
@@ -257,19 +234,16 @@
"DG.ItemWindow.Motivations.IsDisorderTooltip": "Познач, якщо додаєш розлад або якщо мотивацію втрачено через отримання розладу.",
"DG.ItemWindow.Motivations.DisorderHasBeenCured": "Розлад було «вилікувано»",
"DG.ItemWindow.Motivations.DisorderHasBeenCuredTooltip": "Критичний успіх на терапевтичному кидку Удачі може «вилікувати» розлад. Втім, він може повернутися наступного разу, коли буде досягнуто точки зламу. Див. стор. 78–79 в Agent's Handbook — Therapy.",
"DG.ItemWindow.Gear.Attributes": "Атрибути",
"DG.ItemWindow.Gear.Description": "Опис",
"DG.ItemWindow.Gear.NamePlaceholder": "Назва",
"DG.ItemWindow.Gear.Expense": "Вартість",
"DG.ItemWindow.Gear.Equipped": "Екіпіровано",
"DG.ItemWindow.Armor.Description": "Опис",
"DG.ItemWindow.Armor.NamePlaceholder": "Назва",
"DG.ItemWindow.Armor.ArmorRating": "Рейтинг броні",
"DG.ItemWindow.Armor.Expense": "Вартість",
"DG.ItemWindow.Armor.Equipped": "Екіпіровано",
"DG.ItemWindow.Weapons.Description": "Опис",
"DG.ItemWindow.Weapons.NamePlaceholder": "Назва",
"DG.ItemWindow.Weapons.Skill": "Навичка",
@@ -283,27 +257,21 @@
"DG.ItemWindow.Weapons.Ammo": "Набої (кількість/тип)",
"DG.ItemWindow.Weapons.Range": "Дальність",
"DG.ItemWindow.Weapons.KillRadius": "Радіус ураження",
"DG.ModifySkillRollDialogue.Title": "Змінити кидок",
"DG.ModifySkillRollDialogue.Test": "Тест:",
"DG.ModifySkillRollDialogue.OriginalTarget": "Початкова ціль:",
"DG.ModifySkillRollDialogue.TargetModifier": "Змінити ціль:",
"DG.ModifySkillRollDialogue.ModifierTooltip": "Вкажи позитивний або негативний модифікатор, щоб додати/відняти від цілі для перевірки навички.",
"DG.ModifyDamageRollDialogue.Weapon": "Зброя:",
"DG.ModifyDamageRollDialogue.OuterModifierTooltip": "Це поле змінює зовнішню частину формули, наприклад, щоб подвоїти результат.",
"DG.ModifyDamageRollDialogue.InnerModifierTooltip": "Це поле додає фіксований модифікатор (наприклад, «+1») або інший кістковий термін (наприклад, «+1d4»).",
"DG.Roll.Inhuman": "Нелюдське",
"DG.Tooltip.BreakPoint": "Поточна точка зламу. Скидається, коли поточний ГЗД падає до цього значення або нижче. Початкова точка зламу: ГЗД − НЕП.",
"DG.Tooltip.BreakPointReset": "Скинути точку зламу до «поточний ГЗД − ПВ».",
"DG.Skills.ritual": "Ритуал",
"DG.RollMode": "Режим кидка",
"DG.Bonds.Relationship": "Стосунки",
"DG.TypeSkills.Subskills.Painting": "Живопис",
"DG.Sheet.BlockHeaders.Attributes": "АТРИБУТИ",
"DG.Sheet.BlockHeaders.Statistics": "СТАТИСТИКА",
"DG.Sheet.BlockHeaders.PhysicalDescriptionNotes": "ФІЗИЧНИЙ ОПИС / НОТАТКИ",
@@ -313,9 +281,7 @@
"DG.Sheet.BlockHeaders.MotivationsAndDisorders": "МОТИВАЦІЇ ТА РОЗЛАДИ",
"DG.Sheet.BlockHeaders.PersonalInfoCV": "ОСОБИСТІ ДАНІ / РЕЗЮМЕ",
"DG.Sheet.BlockHeaders.BondsRelationshipsContacts": "ЗВ’ЯЗКИ / СТОСУНКИ / КОНТАКТИ",
"DG.AgentSheet.ProfessionPlaceholder": "ПРОФЕСІЯ (ЗВАННЯ, ЯКЩО Є)",
"DG.UnnaturalSheet.HideUntrainedSkillsLabel": "Приховати нетреновані",
"DG.UnnaturalSheet.NotesLabel": "Нотатки",
"DG.UnnaturalSheet.ShortDescriptionPlaceholder": "Опис істоти або типу істоти",
@@ -323,39 +289,31 @@
"DG.UnnaturalSheet.Tabs.NotesTabLabel": "Навички та нотатки",
"DG.UnnaturalSheet.Tabs.ItemsTabLabel": "Атаки та здібності",
"DG.NPCSheet.BreakPointAbbreviation": "ТЗ",
"DG.ImpossibleLanscapes.IHaveSeenThe": "Я БАЧИВ",
"DG.ImpossibleLanscapes.YellowSign": "ЖОВТИЙ ЗНАК",
"DG.ImpossibleLanscapes.CorruptionFirstHalf": "Розк",
"DG.ImpossibleLanscapes.CorruptionSecondHalf": "лад",
"DG.ImpossibleLanscapes.Gift": "Дар",
"DG.ImpossibleLanscapes.Insight": "Осяяння",
"DG.VehicleSheet.NotesTabName": "Нотатки",
"DG.VehicleSheet.GearTabName": "Спорядження",
"DG.VehicleSheet.NotesLabel": "НОТАТКИ",
"DG.VehicleSheet.DescriptionPlaceHolder": "Опис транспорту",
"DG.Generic.CheckDamagedBondsBoxUntilNextHomeScene": "Познач пошкоджений Зв’язок до завершення наступної Домашньої сцени.",
"DG.Generic.ClearAll": "Очистити позначки",
"DG.Generic.SanDamage": "Втрата ГЗД",
"DG.ItemWindow.Weapons.CustomSkillTarget": "Ціль",
"DG.ItemWindow.Weapons.SkillModifier": "Модифікатор навички",
"DG.ItemWindow.Custom": "Власне",
"DG.Sheet.class.item": "Аркуш предмета",
"DG.Sheet.class.agent": "Аркуш агента",
"DG.Sheet.class.npc": "Аркуш NPC",
"DG.Sheet.class.unnatural": "Аркуш протиприродного",
"DG.Sheet.class.vehicle": "Аркуш транспорту",
"TYPES.Actor.agent": "Агент",
"TYPES.Actor.npc": "NPC",
"TYPES.Actor.unnatural": "Протиприродне",
"TYPES.Actor.vehicle": "Транспорт",
"TYPES.Item.armor": "Броня",
"TYPES.Item.bond": "Зв’язок",
"TYPES.Item.gear": "Спорядження",
@@ -363,13 +321,10 @@
"TYPES.Item.weapon": "Зброя",
"TYPES.Item.tome": "Том",
"TYPES.Item.ritual": "Ритуал",
"DG.Gear.Tomes": "ТОМИ",
"DG.Gear.Rituals": "РИТУАЛИ",
"DG.Tomes.Name": "Назва",
"DG.Ritual.Name": "Назва",
"DG.Tome.Name": "Назва",
"DG.Tome.Language": "Мова",
"DG.Tome.StudyTime": "Час вивчення",
@@ -381,15 +336,12 @@
"DG.ItemWindow.Tome.StudySanityDamage": "Втрата ГЗД від вивчення",
"DG.ItemWindow.Tome.HandlerOnlyTabHeader": "Лише для Handler",
"DG.ItemWindow.Tome.HandlerNotes": "Нотатки для Handler",
"DG.ItemWindow.Ritual.ActivationSanityDamage": "Втрата ГЗД при активації",
"DG.ItemWindow.Ritual.ActivationTime": "Час активації",
"DG.ItemWindow.Ritual.ActivationCosts": "Вартість активації",
"DG.ItemWindow.Ritual.StudyTime": "Час вивчення",
"DG.ItemWindow.Ritual.LearnedSanityDamage": "Втрата ГЗД при вивченні",
"DG.MissionPaused": "Місію призупинено",
"DG.Settings.charactersheet.name": "Стиль аркуша персонажа",
"DG.Settings.charactersheet.hint": "Обери стиль відображення аркушів. «Програма» — більш сучасний «урядовий» стиль. «Ковбої/Поза законом» — старіший, грубіший «друкарська машинка».",
"DG.Settings.charactersheet.cowboys": "Ковбої",
@@ -417,15 +369,12 @@
"DG.Settings.skillFailure.name": "Авто-позначка «Провал» для навички",
"DG.Settings.skillFailure.hint": "Автоматично ставить позначку провалу при невдалій перевірці навички. Handler може скасувати дію з повідомлення кидка в чаті.",
"DG.Settings.Saved": "Налаштування збережено.",
"DG.SettingsMenu.automation.name": "Меню автоматизації",
"DG.SettingsMenu.automation.label": "Меню автоматизації",
"DG.SettingsMenu.automation.title": "Налаштування автоматизації",
"DG.SettingsMenu.handler.name": "Налаштування Handler",
"DG.SettingsMenu.handler.label": "Налаштування Handler",
"DG.SettingsMenu.handler.title": "Налаштування лише для Handler",
"DG.Messages.Skillsmark.Marked": "Цю навичку позначено",
"DG.Messages.Skillsmark.Tooltip": "Див. стор. 29 Agent's Handbook"
}
+38
View File
@@ -0,0 +1,38 @@
# Delta Green system — `module/` layout
Entry point: [`deltagreen.js`](deltagreen.js) (registered in `system.json`).
## Folder responsibilities
| Path | Purpose |
| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`config/`](config/) | System constants (`DG`, skill list, template paths) |
| [`data/`](data/) | TypeDataModels (`actor/`, `item/`, `derived/`) |
| [`active-effect/`](active-effect/) | Active effect metadata, data model, document class, runtime sync |
| [`profession/`](profession/) | Profession catalog, validation, character-creation math, deferred commit ([`commit-character-creation.js`](profession/commit-character-creation.js)), Damaged Veteran modifiers ([`damaged-veteran-modifiers.js`](profession/damaged-veteran-modifiers.js)), disorder IDs ([`disorders.js`](profession/disorders.js)) |
| [`roll/`](roll/) | Dice roll subclasses, roll pipeline, and roll UX. [`roll/roll.js`](roll/roll.js) is the public API. Internal Roll subclasses live in [`roll/classes/`](roll/classes/). |
| [`sheets/`](sheets/) | ApplicationV2 sheets and mixins |
| [`applications/`](applications/) | Standalone dialogs, chargen orchestrator ([`profession-setup-flow.js`](applications/profession-setup-flow.js)), shared dialog UI ([`character-creation-ui.js`](applications/character-creation-ui.js)), and dialog factory ([`dg-dialog.js`](applications/dg-dialog.js)) |
| [`actor/`](actor/), [`item/`](item/) | Document subclasses |
| [`chat/`](chat/) | Chat cards ([`dg-chat-card.js`](chat/dg-chat-card.js)) and inline actions |
| [`integrations/`](integrations/) | Optional module hooks (Dice So Nice) |
| [`utils/`](utils/) | Generic helpers only (Handlebars, migration, skill layout/tooltips) |
Root-level modules: [`settings.js`](settings.js), [`templates.js`](templates.js).
## Import conventions
- **Roll types and pipeline:** import `DGRoll`, `DGPercentileRoll`, `createDGRollFromDataset`, `processDGRoll`, etc. from [`roll/roll.js`](roll/roll.js) only — not from [`roll/classes/`](roll/classes/).
- **Roll modifier dialogs:** import from [`roll/roll-dialogs.js`](roll/roll-dialogs.js) only when calling dialogs directly (today only used from roll classes).
- **Skill improvement rolls:** import from [`roll/skill-improvement-roll.js`](roll/skill-improvement-roll.js) (used by agent sheet).
- **Profession logic:** import from [`profession/index.js`](profession/index.js) (includes `commitProfessionSetup`, `getDisorderLabel`, etc.).
- **Profession setup flow:** import `assignProfessionToAgent` from [`applications/profession-setup-flow.js`](applications/profession-setup-flow.js).
- **Dialog factory:** feature dialogs use [`showDgDialog`](applications/dg-dialog.js) — not raw `DialogV2`.
- **Chat cards:** rolls via [`createDGRollChatMessage`](chat/dg-chat-card.js); non-roll cards via [`createDGChatMessage`](chat/dg-chat-card.js).
- **Active effect runtime:** import exhaustion/stimulant sync from [`active-effect/runtime/`](active-effect/runtime/).
- **Derived actor math:** import from [`data/derived/actor-derived.js`](data/derived/actor-derived.js).
- **Agent skill defaults:** single source in [`data/actor/base/agent-skill-defaults.js`](data/actor/base/agent-skill-defaults.js) (used by human actor schema and profession code).
## Profession setup flow
[`profession-setup-flow.js`](applications/profession-setup-flow.js) orchestrates: Pick Statistics → Roll/Assign Stats → Character Creation → Damaged Veterans → (optional follow-ups) → `commitProfessionSetup`. Dialogs return `{ outcome: 'submitted' | 'back', ... }` or `null` (window close aborts). Character Creation alone uses **Cancel** to abort the entire assignment.
@@ -0,0 +1,56 @@
const { ArrayField, SchemaField, StringField, NumberField, AnyField } =
foundry.data.fields;
/**
* Delta Green Active Effect system data — matches core change schema with `final` as the default phase.
* @extends {foundry.data.ActiveEffectTypeDataModel}
*/
export default class DGActiveEffectTypeDataModel extends foundry.data
.ActiveEffectTypeDataModel {
/** @override */
static defineSchema() {
return {
changes: new ArrayField(
new SchemaField({
key: new StringField({ required: true }),
type: new StringField({
required: true,
blank: false,
initial: "add",
validate: DGActiveEffectTypeDataModel.#validateType,
}),
value: new AnyField({
required: true,
nullable: true,
serializable: true,
initial: "",
}),
phase: new StringField({
required: true,
blank: false,
initial: "final",
}),
priority: new NumberField(),
}),
),
};
}
/**
* @param {string} type
* @returns {true}
*/
static #validateType(type) {
if (type.length < 3)
throw new Error("must be at least three characters long");
if (
!/^custom\.-?\d+$/.test(type) &&
!type.split(".").every((s) => /^[a-z0-9]+$/i.test(s))
) {
throw new Error(
'A change type must either be a sequence of dot-delimited, alpha-numeric substrings or of the form "custom.{number}"',
);
}
return true;
}
}
@@ -0,0 +1,84 @@
/**
* @param {ActiveEffect} effect
* @returns {Item|undefined}
*/
function resolveTransferItem(effect) {
const item = effect.parent;
if (item?.documentName !== "Item") return undefined;
const { actor } = item;
if (actor?.documentName === "Actor") {
return actor.items.get(item.id) ?? item;
}
return item;
}
/**
* @param {Item} item
* @returns {{ inactive: boolean, reasonKey: string|null }}
*/
function getTransferSuppressionState(item) {
switch (item.type) {
case "weapon":
case "armor":
case "gear":
if (item.system.equipped !== true) {
return {
inactive: true,
reasonKey: "DG.ActiveEffects.SuppressedNotEquipped",
};
}
return { inactive: false, reasonKey: null };
case "motivation":
if (item.system.acuteEpisode !== true) {
return {
inactive: true,
reasonKey: "DG.ActiveEffects.SuppressedAcuteEpisode",
};
}
return { inactive: false, reasonKey: null };
case "tome":
case "ritual":
return {
inactive: true,
reasonKey: "DG.ActiveEffects.SuppressedUnsupportedItem",
};
case "bond":
return { inactive: false, reasonKey: null };
default:
return { inactive: false, reasonKey: null };
}
}
/**
* @param {Item} item
* @returns {boolean} True when a transfer effect on this item should not apply.
*/
function isTransferEffectInactive(item) {
return getTransferSuppressionState(item).inactive;
}
/**
* @param {ActiveEffect} effect
* @returns {string|null} Localization key for why a transfer effect is inactive.
*/
export function getTransferSuppressionReasonKey(effect) {
const item = resolveTransferItem(effect);
if (!item || !effect.transfer) return null;
return getTransferSuppressionState(item).reasonKey;
}
/** @extends {foundry.documents.ActiveEffect} */
export default class DGActiveEffect extends ActiveEffect {
/** @override */
get isSuppressed() {
if (this.disabled || this.duration?.expired) return true;
if (this.system?.isSuppressed) return true;
if (!this.transfer) return false;
const item = resolveTransferItem(this);
if (!item) return false;
return isTransferEffectInactive(item);
}
}
+77
View File
@@ -0,0 +1,77 @@
/** @type {Record<string, { label: string, fields: Record<string, string> }>} */
export const EFFECT_FIELD_GROUPS = {
statistics: {
label: "DG.ActiveEffects.Groups.Statistics",
fields: {
"system.statistics.str.modifier": "DG.Attributes.str",
"system.statistics.con.modifier": "DG.Attributes.con",
"system.statistics.dex.modifier": "DG.Attributes.dex",
"system.statistics.int.modifier": "DG.Attributes.int",
"system.statistics.pow.modifier": "DG.Attributes.pow",
"system.statistics.cha.modifier": "DG.Attributes.cha",
},
},
resources: {
label: "DG.ActiveEffects.Groups.Resources",
fields: {
"system.health.maxBonus": "DG.ActiveEffects.Fields.MaxHP",
"system.wp.maxBonus": "DG.ActiveEffects.Fields.MaxWP",
"system.sanity.maxBonus": "DG.ActiveEffects.Fields.MaxSanity",
},
},
rollTargets: {
label: "DG.ActiveEffects.Groups.RollTargets",
fields: {
"system.rollTarget.allSkills": "DG.ActiveEffects.Fields.AllSkills",
"system.rollTarget.sanity": "DG.ActiveEffects.Fields.SanityRolls",
"system.rollTarget.statistics": "DG.ActiveEffects.Fields.StatisticRolls",
},
},
};
export const ROLL_TARGET_FIELD_KEYS = Object.keys(
EFFECT_FIELD_GROUPS.rollTargets.fields,
);
export const SUPPORTED_ITEM_TYPES = new Set([
"weapon",
"armor",
"gear",
"bond",
"motivation",
]);
/**
* @param {ActiveEffect} effect
* @returns {Record<string, { label: string, fields: Record<string, string> }>}
*/
export function getEffectFieldGroupsForDocument(effect) {
const { parent } = effect;
if (parent?.documentName === "Actor") {
if (parent.type !== "agent") return {};
return EFFECT_FIELD_GROUPS;
}
if (parent?.documentName === "Item") {
if (!SUPPORTED_ITEM_TYPES.has(parent.type)) return {};
return EFFECT_FIELD_GROUPS;
}
return {};
}
/**
* @param {Record<string, { label: string, fields: Record<string, string> }>} groups
* @returns {Array<{ group: string, groupLabel: string, options: Array<{ value: string, label: string }> }>}
*/
export function buildEffectFieldSelectOptions(groups) {
return Object.entries(groups).map(([groupId, group]) => ({
group: groupId,
groupLabel: game.i18n.localize(group.label),
options: Object.entries(group.fields).map(([value, labelKey]) => ({
value,
label: game.i18n.localize(labelKey),
})),
}));
}
@@ -0,0 +1,25 @@
import DG from "../../config/index.js";
/**
* @param {Actor} actor
* @returns {boolean}
*/
function hasActiveStimulantEffect(actor) {
return (
actor.effects?.some(
(effect) => effect.getFlag(DG.ID, "stimulant") && !effect.isSuppressed,
) ?? false
);
}
/**
* Persisted manual suppress or an active stimulant AE.
*
* @param {Actor} actor
* @returns {boolean}
*/
export default function getEffectiveSuppressExhaustion(actor) {
const persisted =
actor._source?.system?.physical?.suppressExhaustion ?? false;
return Boolean(persisted) || hasActiveStimulantEffect(actor);
}
+181
View File
@@ -0,0 +1,181 @@
import {
applyAgentResourceMaxBonuses,
calculateHealthMax,
setSkillTargetProficiencies,
calculateMeleeDamageBonusFormula,
clampSanityRitualValue,
prepareAgentSkillFlags,
prepareBreakingPointHit,
prepareStatisticsX5,
} from "../../data/derived/actor-derived.js";
/**
* @param {number} current
* @param {number} base
* @returns {string}
*/
export function getActiveEffectModificationClass(current, base) {
if (current == null || base == null || current === base) return "";
return current > base ? "ae-mod-increase" : "ae-mod-decrease";
}
/**
* Sheet color for roll-target modifiers (uses modifier sign, not clamped effective vs base).
* @param {number} modifier
* @returns {string}
*/
export function getRollTargetDisplayClassFromModifier(modifier) {
const numericModifier = Number(modifier) || 0;
if (!numericModifier) return "";
return numericModifier > 0 ? "ae-mod-increase" : "ae-mod-decrease";
}
/**
* Apply a roll-target Active Effect modifier with the same clamp rules as percentile rolls.
* @param {number} base
* @param {number} modifier
* @param {{ allowOver99?: boolean }} [options]
* @returns {number}
*/
export function clampPercentileRollTarget(
base,
modifier,
{ allowOver99 = false } = {},
) {
const numericBase = Number(base);
const numericModifier = Number(modifier) || 0;
if (!numericModifier || !Number.isFinite(numericBase)) return numericBase;
let target = Math.round(numericBase + numericModifier);
target = Math.max(1, target);
if (!allowOver99) {
target = Math.min(target, 99);
}
return target;
}
/**
* @param {object} entry
* @param {number} modifier
*/
function attachRollTargetDisplay(entry, modifier) {
const numericModifier = Number(modifier) || 0;
if (!numericModifier) {
entry.rollTargetDisplayClass = "";
return;
}
entry.rollTargetDisplayClass =
getRollTargetDisplayClassFromModifier(numericModifier);
}
/**
* Sheet display for agent roll-target Active Effects (skills, stats x5, SAN).
* @param {Actor} actor
* @returns {void}
*/
export function prepareAgentRollTargetDisplay(actor) {
if (actor.type !== "agent") return;
const { system } = actor;
const { rollTarget } = system;
if (!rollTarget) return;
const allSkillsMod = Number(rollTarget.allSkills) || 0;
const sanityMod = Number(rollTarget.sanity) || 0;
const statisticsMod = Number(rollTarget.statistics) || 0;
for (const skill of Object.values(system.skills ?? {})) {
attachRollTargetDisplay(skill, allSkillsMod);
}
for (const skill of Object.values(system.typedSkills ?? {})) {
attachRollTargetDisplay(skill, allSkillsMod);
}
attachRollTargetDisplay(system.sanity, sanityMod);
for (const stat of Object.values(system.statistics ?? {})) {
attachRollTargetDisplay(stat, statisticsMod);
}
}
/**
* Attach sheet display classes comparing prepared data to persisted source (pre–AE).
* @param {Actor} actor
* @returns {void}
*/
export function prepareAgentActiveEffectDisplay(actor) {
if (actor.type !== "agent") return;
const { system } = actor;
const source = actor._source?.system;
if (!source) return;
for (const [key, stat] of Object.entries(system.statistics ?? {})) {
const baseValue = source.statistics?.[key]?.value;
const effective = stat.effectiveValue ?? stat.value;
stat.valueDisplayClass = getActiveEffectModificationClass(
effective,
baseValue,
);
stat.statisticAeModifier = stat.valueDisplayClass
? effective - (baseValue ?? 0)
: 0;
}
const persistedStats = source.statistics ?? system.statistics;
const healthFormulaMax = calculateHealthMax(persistedStats, persistedStats);
const wpFormulaMax = persistedStats.pow?.value ?? 0;
const persistedUnnatural =
source.skills?.unnatural?.proficiency ??
system.skills?.unnatural?.proficiency ??
0;
const sanityFormulaMax = 99 - persistedUnnatural;
const healthBaseMax = healthFormulaMax + (source.health?.maxBonus ?? 0);
system.health.maxDisplayClass = getActiveEffectModificationClass(
system.health.max,
healthBaseMax,
);
const wpBaseMax = wpFormulaMax + (source.wp?.maxBonus ?? 0);
system.wp.maxDisplayClass = getActiveEffectModificationClass(
system.wp.max,
wpBaseMax,
);
const sanityBaseMax = sanityFormulaMax + (source.sanity?.maxBonus ?? 0);
system.sanity.maxDisplayClass = getActiveEffectModificationClass(
system.sanity.max,
sanityBaseMax,
);
}
/**
* Recompute agent fields that depend on Active Effects (final phase) and refresh sheet display.
* Single post-AE pass for agents — do not mirror this logic in AgentData.prepareDerivedData.
* @param {Actor} actor
* @returns {void}
*/
export function refreshDerivedAfterActiveEffects(actor) {
if (actor.type !== "agent") return;
const { system } = actor;
const sourceStatistics = actor._source?.system?.statistics;
prepareStatisticsX5(system.statistics, sourceStatistics);
prepareAgentRollTargetDisplay(actor);
applyAgentResourceMaxBonuses(system, sourceStatistics);
setSkillTargetProficiencies(system.skills);
setSkillTargetProficiencies(system.typedSkills);
prepareAgentSkillFlags(system.skills);
clampSanityRitualValue(system.sanity);
prepareBreakingPointHit(system.sanity);
system.statistics.str.meleeDamageBonusFormula =
calculateMeleeDamageBonusFormula(
system.statistics.str.effectiveValue ?? system.statistics.str.value,
);
prepareAgentActiveEffectDisplay(actor);
}
@@ -0,0 +1,152 @@
import DG from "../../config/index.js";
import { ROLL_TARGET_FIELD_KEYS } from "../effect-fields.js";
import getEffectiveSuppressExhaustion from "./agent-condition-sync.js";
/** @type {WeakMap<Actor, Promise<void>>} */
const syncLocks = new WeakMap();
/**
* @param {Actor} actor
* @returns {ActiveEffect[]}
*/
function getExhaustionEffects(actor) {
return (
actor.effects?.filter((effect) => effect.getFlag(DG.ID, "exhaustion")) ?? []
);
}
/**
* @param {Actor} actor
* @returns {ActiveEffect|undefined}
*/
function getExhaustionEffect(actor) {
return getExhaustionEffects(actor)[0];
}
/**
* @param {number} penalty
* @returns {object[]}
*/
function buildExhaustionChanges(penalty) {
const value = String(penalty);
return ROLL_TARGET_FIELD_KEYS.map((key) => ({
key,
type: "add",
value,
phase: "final",
}));
}
/**
* @param {ActiveEffect} effect
* @param {number} penalty
* @param {boolean} suppressExhaustion
* @returns {boolean}
*/
function effectMatchesState(effect, penalty, suppressExhaustion) {
if (Boolean(effect.disabled) !== Boolean(suppressExhaustion)) return false;
const changes = effect.system?.changes ?? [];
if (changes.length !== ROLL_TARGET_FIELD_KEYS.length) return false;
return changes.every(
(change) =>
ROLL_TARGET_FIELD_KEYS.includes(change.key) &&
change.type === "add" &&
String(change.value) === String(penalty),
);
}
/**
* @param {Actor} actor
* @returns {Promise<void>}
*/
async function pruneDuplicateExhaustionEffects(actor) {
const effects = getExhaustionEffects(actor);
if (effects.length <= 1) return;
for (const effect of effects.slice(1)) {
await effect.delete();
}
}
/**
* @param {Actor} actor
* @returns {Promise<void>}
*/
async function syncExhaustionEffectInner(actor) {
if (actor.type !== "agent") return;
await pruneDuplicateExhaustionEffects(actor);
const { exhausted, exhaustedPenalty } = actor.system.physical;
const suppressExhaustion = getEffectiveSuppressExhaustion(actor);
const effect = getExhaustionEffect(actor);
const penalty = exhausted ? -1 * Math.abs(Number(exhaustedPenalty) || 0) : 0;
if (!exhausted) {
if (effect) await effect.delete();
return;
}
const changes = buildExhaustionChanges(penalty);
const disabled = Boolean(suppressExhaustion);
if (effect) {
if (effectMatchesState(effect, penalty, suppressExhaustion)) return;
await effect.update({ changes, disabled });
return;
}
const documentClass = foundry.utils.getDocumentClass("ActiveEffect");
await documentClass.create(
{
name: game.i18n.localize("DG.Physical.ExhaustionEffectName"),
img: "systems/deltagreen/assets/icons/magic-shield.svg",
transfer: false,
disabled,
changes,
flags: { [DG.ID]: { exhaustion: true } },
},
{ parent: actor },
);
}
/**
* Keeps the embedded exhaustion Active Effect aligned with physical exhaustion data.
*
* @param {Actor} actor
* @returns {Promise<void>}
*/
export async function syncExhaustionEffect(actor) {
const pending = syncLocks.get(actor);
if (pending) return pending;
const promise = syncExhaustionEffectInner(actor).finally(() => {
if (syncLocks.get(actor) === promise) syncLocks.delete(actor);
});
syncLocks.set(actor, promise);
return promise;
}
/**
* @param {object} data Actor update payload.
* @returns {boolean}
*/
export function updateTouchesExhaustionPhysical(data) {
if (!data) return false;
const physical = foundry.utils.getProperty(data, "system.physical");
if (physical) {
if (
"exhausted" in physical ||
"exhaustedPenalty" in physical ||
"suppressExhaustion" in physical
) {
return true;
}
}
return [
"system.physical.exhausted",
"system.physical.exhaustedPenalty",
"system.physical.suppressExhaustion",
].some((key) => foundry.utils.hasProperty(data, key));
}
@@ -0,0 +1,158 @@
import DG from "../../config/index.js";
import { syncExhaustionEffect } from "./exhaustion-effect.js";
/**
* @param {Actor} actor
* @returns {ActiveEffect[]}
*/
function getStimulantEffects(actor) {
return (
actor.effects?.filter((effect) => effect.getFlag(DG.ID, "stimulant")) ?? []
);
}
/**
* @param {Actor} actor
* @returns {ActiveEffect|undefined}
*/
function getStimulantEffect(actor) {
return getStimulantEffects(actor).find((effect) => !effect.isSuppressed);
}
/**
* @param {Actor} actor
* @returns {boolean}
*/
export function hasActiveStimulantEffect(actor) {
return getStimulantEffects(actor).some((effect) => !effect.isSuppressed);
}
/**
* @param {ActiveEffect} effect
* @returns {number}
*/
function getStimulantRemainingHours(effect) {
effect.updateDuration();
const { remaining, secondsRemaining, units } = effect.duration ?? {};
if (Number.isFinite(remaining) && remaining > 0 && units === "hours") {
return Math.ceil(remaining);
}
if (Number.isFinite(secondsRemaining) && secondsRemaining > 0) {
const { calendar } = game.time;
const components = calendar.difference(
game.time.worldTime + secondsRemaining,
game.time.worldTime,
);
const hours = calendar.componentsToUnit(components, "hour", {
roundFn: "ceil",
});
return Math.max(0, Math.ceil(Math.abs(hours)));
}
return 0;
}
export { default as getEffectiveSuppressExhaustion } from "./agent-condition-sync.js";
/**
* @param {number} hours
* @returns {object}
*/
function buildStimulantEffectData(hours) {
return {
name: game.i18n.localize("DG.Physical.StimulantsEffectName"),
img: "systems/deltagreen/assets/icons/tablet.svg",
transfer: false,
disabled: false,
start: { time: game.time.worldTime },
duration: {
value: hours,
units: "hours",
},
changes: [],
flags: { [DG.ID]: { stimulant: true } },
};
}
/**
* Create or refresh the stimulant AE; duration is max(remaining, newHours).
* @param {Actor} actor
* @param {number} newHours
* @returns {Promise<number>} Hours applied on the stimulant AE.
*/
export async function applyStimulantEffect(actor, newHours) {
if (actor.type !== "agent") return 0;
const rolledHours = Math.max(1, Math.ceil(Number(newHours) || 0));
const existing = getStimulantEffect(actor);
const remainingHours = existing ? getStimulantRemainingHours(existing) : 0;
const hours = Math.max(remainingHours, rolledHours);
const data = buildStimulantEffectData(hours);
if (existing) {
await existing.update({
start: data.start,
duration: data.duration,
changes: data.changes,
disabled: false,
});
return hours;
}
const documentClass = foundry.utils.getDocumentClass("ActiveEffect");
await documentClass.create(data, { parent: actor });
return hours;
}
/**
* @param {Actor} actor
* @returns {ActiveEffect[]}
*/
function getExpiredStimulantEffects(actor) {
return getStimulantEffects(actor).filter(
(effect) => effect.duration?.expired,
);
}
/**
* Remove stimulant AEs that have expired.
* @param {Actor} actor
* @returns {Promise<void>}
*/
export async function pruneExpiredStimulantEffects(actor) {
if (actor.type !== "agent") return;
const expired = getExpiredStimulantEffects(actor);
if (!expired.length) return;
await actor.deleteEmbeddedDocuments(
"ActiveEffect",
expired.map((effect) => effect.id),
);
}
/**
* Delete all stimulant AEs on the actor (e.g. after a full rest).
* @param {Actor} actor
* @returns {Promise<void>}
*/
export async function clearStimulantEffects(actor) {
if (actor.type !== "agent") return;
const effects = getStimulantEffects(actor);
if (!effects.length) return;
await actor.deleteEmbeddedDocuments(
"ActiveEffect",
effects.map((effect) => effect.id),
);
}
/**
* @returns {Promise<void>}
*/
export async function pruneAllAgentsExpiredStimulants() {
if (!game.user.isActiveGM) return;
for (const actor of game.actors) {
if (actor.type === "agent") {
await pruneExpiredStimulantEffects(actor);
await syncExhaustionEffect(actor);
}
}
}
+49
View File
@@ -0,0 +1,49 @@
import DG from "../../config/index.js";
import { getStatisticEffectiveValue } from "../../data/derived/actor-derived.js";
/**
* @param {Actor} actor
* @param {object} data
* @param {object} source
*/
function sanitizeStatisticValues(actor, data, source) {
for (const key of DG.statistics) {
const submitted = foundry.utils.getProperty(
data,
`system.statistics.${key}.value`,
);
if (submitted !== undefined) {
const sourceStat = source.statistics?.[key];
const preparedStat = actor.system.statistics?.[key];
if (sourceStat && preparedStat) {
const base = sourceStat.value;
const effective = getStatisticEffectiveValue(preparedStat, sourceStat);
if (submitted === effective && submitted !== base) {
foundry.utils.setProperty(
data,
`system.statistics.${key}.value`,
base,
);
}
}
}
}
}
/**
* Strip form values that match post–AE prepared numbers so persisted data stays at base values.
* @param {Actor} actor
* @param {object} data Update payload (mutated in place).
* @returns {object}
*/
export default function sanitizeActiveEffectBackedUpdateData(actor, data) {
if (!data?.system) return data;
const source = actor._source?.system;
if (!source) return data;
sanitizeStatisticValues(actor, data, source);
return data;
}
+31 -283
View File
@@ -1,296 +1,35 @@
import { refreshDerivedAfterActiveEffects } from "../active-effect/runtime/derived.js";
import { prepareAgentStatSanityTooltips } from "../utils/roll-target-tooltip.js";
import {
syncExhaustionEffect,
updateTouchesExhaustionPhysical,
} from "../active-effect/runtime/exhaustion-effect.js";
import { pruneExpiredStimulantEffects } from "../active-effect/runtime/stimulant-effect.js";
import sanitizeActiveEffectBackedUpdateData from "../active-effect/runtime/submit.js";
/**
* Extend the base Actor entity by defining a custom roll data structure which is ideal for the Simple system.
* @extends {Actor}
*/
export default class DeltaGreenActor extends Actor {
/**
* Augment the basic actor data with additional dynamic data.
*/
/** @override */
prepareData() {
super.prepareData();
const actorData = this;
// console.log('actor.js prepareData');
// console.log(this);
// Make separate methods for each Actor type (character, npc, etc.) to keep
// things organized.
if (actorData.type === "agent") {
this._prepareAgentData(this);
} else if (actorData.type === "unnatural") {
this._prepareUnnaturalData(this);
} else if (actorData.type === "npc") {
this._prepareNpcData(this);
} else if (actorData.type === "vehicle") {
this._prepareVehicleData(this);
}
refreshDerivedAfterActiveEffects(this);
if (this.type === "agent") prepareAgentStatSanityTooltips(this);
}
/**
*
* @param {*} agent
*/
_prepareVehicleData(actor) {
const actorData = actor;
// calculate total armor rating
let protection = 0;
for (const i of actor.items) {
if (i.type === "armor") {
if (i.system.equipped === true) {
protection += i.system.protection;
}
}
}
actorData.system.health.protection = protection;
// console.log(actor);
}
_prepareNpcData(actor) {
const { system } = actor;
// Loop through ability scores, and add their modifiers to our sheet output.
for (const statistic of Object.values(system.statistics)) {
// the x5 is just whatever the raw statistic is x 5 to turn it into a d100 percentile
statistic.x5 = statistic.value * 5;
}
// initialize sanity, don't set these afterwards, as they need to be manually edited
if (system.sanity.value >= 100) {
system.sanity.value = system.statistics.pow.x5;
system.sanity.currentBreakingPoint =
system.sanity.value - system.statistics.pow.value;
}
system.sanity.max = 99 - system.skills.unnatural.proficiency;
system.wp.max = system.statistics.pow.value;
try {
system.health.max = Math.ceil(
(system.statistics.con.value + system.statistics.str.value) / 2,
);
} catch {
system.health.max = 10;
}
const strength = system.statistics.str;
system.statistics.str.meleeDamageBonusFormula =
this._calculateMeleeDamageBonusFormula(strength.value);
try {
delete system.skills.ritual; // try to remove legacy skill for ritual if it exists
} catch {
// do nothing
}
system.sanity.ritual = 99 - system.sanity.value;
if (system.sanity.ritual > 99) {
system.sanity.ritual = 99;
} else if (system.sanity.ritual < 1) {
system.sanity.ritual = 1;
}
// calculate total armor rating
let protection = 0;
for (const i of actor.items) {
if (i.type === "armor") {
if (i.system.equipped === true) {
protection += i.system.protection;
}
}
}
system.health.protection = protection;
for (const skill of Object.values(system.skills)) {
skill.targetProficiency = skill.proficiency;
}
}
/**
*
* @param {*} agent
*/
_prepareUnnaturalData(actor) {
const { system } = actor;
// Loop through ability scores, and add their modifiers to our sheet output.
for (const statistic of Object.values(system.statistics)) {
// the x5 is just whatever the raw statistic is x 5 to turn it into a d100 percentile
statistic.x5 = statistic.value * 5;
}
if (system.wp.maxNeedsUpdate) {
system.wp.max = system.statistics.pow.value;
}
if (system.health.maxNeedsUpdate) {
try {
system.health.max = Math.ceil(
(system.statistics.con.value + system.statistics.str.value) / 2,
);
} catch {
system.health.max = 10;
}
}
// calculate total armor rating
let protection = 0;
for (const i of actor.items) {
if (i.type === "armor") {
if (i.system.equipped === true) {
protection += i.system.protection;
}
}
}
system.health.protection = protection;
for (const skill of Object.values(system.skills)) {
skill.targetProficiency = skill.proficiency;
}
}
/**
* Prepare Agent type specific data
*/
_prepareAgentData(agent) {
const { system } = agent;
// Make modifications to data here. For example:
// Loop through ability scores, and add their modifiers to our sheet output.
for (const statistic of Object.values(system.statistics)) {
// the x5 is just whatever the raw statistic is x 5 to turn it into a d100 percentile
statistic.x5 = statistic.value * 5;
}
try {
delete system.skills.ritual; // try to remove legacy skill for ritual if it exists
} catch {
// do nothing
}
system.sanity.ritual = 99 - system.sanity.value;
if (system.sanity.ritual > 99) {
system.sanity.ritual = 99;
} else if (system.sanity.ritual < 1) {
system.sanity.ritual = 1;
}
// The unnatural skill is sort of special
// It cannot be improved via failure, so add in a special property to reflect this
// Mostly to make it easy to deactivate the failure checkbox in the GUI
for (const [key, skill] of Object.entries(system.skills)) {
if (key === "unnatural") {
skill.cannotBeImprovedByFailure = true;
} else if (key === "luck") {
skill.cannotBeImprovedByFailure = true;
} else if (key === "ritual") {
skill.cannotBeImprovedByFailure = true;
} else {
skill.cannotBeImprovedByFailure = false;
}
// For ritual skill, it's calculated, so add some logic to turn off changing that entirely.
if (key === "ritual") {
skill.isCalculatedValue = true;
} else {
skill.isCalculatedValue = false;
}
// Add field for calculation
skill.targetProficiency = skill.proficiency;
}
system.wp.max = system.statistics.pow.value;
system.health.max = Math.ceil(
(system.statistics.con.value + system.statistics.str.value) / 2,
/** @override */
async update(data, options = {}) {
const sanitized = sanitizeActiveEffectBackedUpdateData(
this,
foundry.utils.deepClone(data),
);
// initialize sanity, don't set these afterwards, as they need to be manually edited
if (system.sanity.value >= 100) {
system.sanity.value = system.statistics.pow.x5;
system.sanity.currentBreakingPoint =
system.sanity.value - system.statistics.pow.value;
}
system.sanity.max = 99 - system.skills.unnatural.proficiency;
// Sanity Loss Adaptations Logic
const { adaptations } = system.sanity;
if (
adaptations.violence.incident1 &&
adaptations.violence.incident2 &&
adaptations.violence.incident3
) {
system.sanity.adaptations.violence.isAdapted = true;
} else {
system.sanity.adaptations.violence.isAdapted = false;
}
if (
adaptations.helplessness.incident1 &&
adaptations.helplessness.incident2 &&
adaptations.helplessness.incident3
) {
system.sanity.adaptations.helplessness.isAdapted = true;
} else {
system.sanity.adaptations.helplessness.isAdapted = false;
}
if (system.sanity.value <= system.sanity.currentBreakingPoint) {
system.sanity.breakingPointHit = true;
} else {
system.sanity.breakingPointHit = false;
}
// calculate total armor rating
let protection = 0;
for (const i of agent.items) {
if (i.type === "armor") {
if (i.system.equipped === true) {
protection += i.system.protection;
}
}
}
system.health.protection = protection;
const strength = system.statistics.str;
system.statistics.str.meleeDamageBonusFormula =
this._calculateMeleeDamageBonusFormula(strength.value);
if (system.physical.exhaustedPenalty > 0) {
system.physical.exhaustedPenalty =
-1 * Math.abs(system.physical.exhaustedPenalty);
}
}
// Returns Bonus From Strength in Hand-to-hand Combat (melee/unarmed)
_calculateMeleeDamageBonusFormula(strengthValue) {
if (strengthValue < 5) {
return "-2";
}
if (strengthValue < 9) {
return "-1";
}
if (strengthValue > 12 && strengthValue < 17) {
return "+1";
}
if (strengthValue > 16) {
return "+2";
}
return "";
const shouldSyncExhaustion =
this.type === "agent" && updateTouchesExhaustionPhysical(sanitized);
const result = await super.update(sanitized, options);
if (shouldSyncExhaustion) await syncExhaustionEffect(this);
return result;
}
/** @override */
@@ -450,4 +189,13 @@ export default class DeltaGreenActor extends Actor {
await this.createEmbeddedDocuments("Item", [weaponData]);
}
/** @override */
async onUpdateEffectDurations(effects, event, context) {
await super.onUpdateEffectDurations(effects, event, context);
if (!game.user.isActiveGM || this.type !== "agent") return;
if (!effects.some((effect) => effect.parent === this)) return;
await pruneExpiredStimulantEffects(this);
await syncExhaustionEffect(this);
}
}
@@ -0,0 +1,537 @@
import { BASE_TEMPLATE_PATH } from "../config/index.js";
import {
bindDialogTabs,
getDialogContentRoot,
showDgDialog,
} from "./dg-dialog.js";
import {
buildSkillDisplayRows,
renderValidationMessagesHtml,
updateSkillGridBody,
} from "./character-creation-ui.js";
import {
BONUS_SKILL_COUNT,
buildBonusSkillCatalog,
buildCharacterCreationPayload,
computeSkillValues,
formatProfessionValidationMessages,
getTypedGroupDisplayName,
parseProfessionSkillKey,
prepareProfessionSkillRows,
splitProfessionSkillMap,
} from "../profession/index.js";
import { splitIntoColumns } from "../utils/skill-layout.js";
import { applySkillTooltipDisplayMode } from "../utils/skill-tooltip.js";
const CHARACTER_CREATION_OPTION_COLUMNS = 2;
const { renderTemplate } = foundry.applications.handlebars;
/**
* @typedef {object} CharacterCreationDraft
* @property {string[]} checkedOptionKeys
* @property {Record<string, string>} chooseOneLabels
* @property {string[]} bonusCatalogIds
* @property {string[]} bonusTypedLabels
* @property {string[]} bondNames
* @property {string[]} bondRelationships
*/
/**
* @param {Item} professionItem
* @param {Actor} actor
* @param {object} [options]
* @param {CharacterCreationDraft} [options.draft]
* @returns {Promise<{ outcome: 'submitted', payload: import("../profession/commit-character-creation.js").CharacterCreationPayload, draft: CharacterCreationDraft } | null>}
*/
export default async function showPickSkillsDialog(
professionItem,
actor,
{ draft } = {},
) {
// eslint-disable-next-line no-use-before-define -- class defined below in this module
const controller = new AddProfessionDialogController(professionItem, actor, {
draft,
});
return controller.run();
}
class AddProfessionDialogController {
/**
* @param {Item} professionItem
* @param {Actor} actor
* @param {object} [options]
* @param {CharacterCreationDraft} [options.draft]
*/
constructor(professionItem, actor, { draft } = {}) {
this.professionItem = professionItem;
this.actor = actor;
const { optionPicks, skills: optionSkillMap } = splitProfessionSkillMap(
professionItem.system.optionSkills ?? {},
);
this.automaticSkills = { ...(professionItem.system.automaticSkills ?? {}) };
this.automaticSkillMeta = {
...(professionItem.system.automaticSkillMeta ?? {}),
};
this.optionSkills = { ...optionSkillMap };
this.optionSkillMeta = { ...(professionItem.system.optionSkillMeta ?? {}) };
this.optionPicks = optionPicks;
/** @type {Set<string>} */
this.checkedOptionKeys = new Set(draft?.checkedOptionKeys ?? []);
/** @type {Record<string, string>} */
this.chooseOneLabels = { ...(draft?.chooseOneLabels ?? {}) };
/** @type {string[]} */
this.bonusCatalogIds = draft?.bonusCatalogIds
? [...draft.bonusCatalogIds]
: Array(BONUS_SKILL_COUNT).fill("");
/** @type {string[]} */
this.bonusTypedLabels = draft?.bonusTypedLabels
? [...draft.bonusTypedLabels]
: Array(BONUS_SKILL_COUNT).fill("");
/** @type {string[]} */
this.bondNames = draft?.bondNames ? [...draft.bondNames] : [];
/** @type {string[]} */
this.bondRelationships = draft?.bondRelationships
? [...draft.bondRelationships]
: [];
/** @type {import("foundry.applications.api.DialogV2").default | null} */
this.dialog = null;
/** @type {{ outcome: 'submitted', payload: import("../profession/commit-character-creation.js").CharacterCreationPayload, draft: CharacterCreationDraft } | null} */
this.result = null;
}
async run() {
const content = await this.#buildTemplate();
this.result = null;
return showDgDialog({
modifier: "character-creation",
content,
window: {
title: game.i18n.localize("DG.Profession.Dialog.Title"),
},
position: { width: 1080, height: 780 },
form: { closeOnSubmit: false },
onRender: async (dialog) => {
this.dialog = dialog;
const root = this.#contentRoot();
if (root) {
bindDialogTabs(root);
applySkillTooltipDisplayMode(root);
}
this.#bindListeners();
this.#restoreFormSelections();
await this.#refreshUi();
},
close: () => this.result,
buttons: [
{
action: "cancel",
label: game.i18n.localize("Cancel"),
callback: async (_event, _button, dialog) => {
this.result = null;
await dialog.close();
return false;
},
},
{
action: "submit",
label: game.i18n.localize("DG.Profession.Dialog.Submit"),
default: true,
disabled: true,
callback: async () => {
await this.#onSubmit();
return false;
},
},
],
});
}
#getFormState() {
this.#syncFormStateFromDom();
return {
checkedOptionKeys: this.checkedOptionKeys,
chooseOneLabels: { ...this.chooseOneLabels },
bonusCatalogIds: [...this.bonusCatalogIds],
bonusTypedLabels: [...this.bonusTypedLabels],
bondNames: [...this.bondNames],
bondRelationships: [...this.bondRelationships],
};
}
/** @returns {CharacterCreationDraft} */
#getDraft() {
const formState = this.#getFormState();
return {
checkedOptionKeys: [...formState.checkedOptionKeys],
chooseOneLabels: formState.chooseOneLabels,
bonusCatalogIds: formState.bonusCatalogIds,
bonusTypedLabels: formState.bonusTypedLabels,
bondNames: formState.bondNames,
bondRelationships: formState.bondRelationships,
};
}
#bondCount() {
return Math.max(
1,
Math.trunc(Number(this.professionItem.system.bonds) || 1),
);
}
#computeContext() {
return {
automaticMeta: this.automaticSkillMeta,
optionMeta: this.optionSkillMeta,
bondCount: this.#bondCount(),
};
}
#compute() {
return computeSkillValues(
this.automaticSkills,
this.optionSkills,
this.optionPicks,
this.#getFormState(),
this.#computeContext(),
);
}
#buildAutomaticChooseOneRows() {
return Object.entries(this.automaticSkills)
.map(([key]) => {
if (!this.automaticSkillMeta[key]?.chooseOne) return null;
const ref = parseProfessionSkillKey(key);
if (!ref || ref.kind !== "typed") return null;
return {
key,
groupLabel: getTypedGroupDisplayName(ref.group),
};
})
.filter(Boolean);
}
async #buildTemplate() {
const computed = this.#compute();
const chaScore =
this.actor.system.statistics.cha.effectiveValue ??
this.actor.system.statistics.cha.value;
const skillRows = buildSkillDisplayRows(computed);
const optionRows = prepareProfessionSkillRows(
this.optionSkills,
this.optionSkillMeta,
).map((row) => ({
...row,
isChooseOne: Boolean(this.optionSkillMeta[row.key]?.chooseOne),
}));
const optionColumns = splitIntoColumns(
optionRows,
CHARACTER_CREATION_OPTION_COLUMNS,
);
const automaticChooseOneRows = this.#buildAutomaticChooseOneRows();
const bondCount = this.#bondCount();
const bondRows = Array.from({ length: bondCount }, (_, index) => ({
index,
number: index + 1,
chaScore,
}));
const bonusSlots = Array.from(
{ length: BONUS_SKILL_COUNT },
(_, index) => ({
index,
}),
);
const showSkillOptionsTab = this.optionPicks > 0;
return renderTemplate(`${BASE_TEMPLATE_PATH}/dialog/add-profession.html`, {
skillRows,
showSkillOptionsTab,
defaultTabSkillOptions: showSkillOptionsTab,
defaultTabBonus: !showSkillOptionsTab,
optionPicks: this.optionPicks,
optionColumns,
automaticChooseOneRows,
catalog: buildBonusSkillCatalog(),
bonusSlots,
...this.#getDisplayMessages(computed),
bondRows,
});
}
#contentRoot() {
return this.dialog ? getDialogContentRoot(this.dialog) : null;
}
#bindListeners() {
const root = this.#contentRoot();
if (!root) return;
root.querySelectorAll('[name="optionSkillChecked"]').forEach((el) => {
el.addEventListener("change", () => this.#onOptionCheckChange(el));
});
root.querySelectorAll("[data-choose-one-key]").forEach((el) => {
el.addEventListener("input", () => this.#refreshUi());
});
root.querySelectorAll("select[data-bonus-index]").forEach((el) => {
el.addEventListener("change", () => this.#onBonusChange(el));
});
root.querySelectorAll("[data-bonus-typed-index]").forEach((el) => {
el.addEventListener("input", () => this.#refreshUi());
});
for (let i = 0; i < this.#bondCount(); i++) {
root
.querySelector(`[name="bondName-${i}"]`)
?.addEventListener("input", () => this.#refreshUi());
root
.querySelector(`[name="bondRelationship-${i}"]`)
?.addEventListener("input", () => this.#refreshUi());
}
}
/**
* @param {HTMLInputElement} checkbox
*/
#onOptionCheckChange(checkbox) {
const key = checkbox.value;
const isChooseOne = checkbox.dataset.isChooseOne === "true";
if (checkbox.checked) {
if (this.checkedOptionKeys.size >= this.optionPicks) {
checkbox.checked = false;
return;
}
this.checkedOptionKeys.add(key);
if (isChooseOne) {
const nameInput = this.#getOptionChooseOneInput(key);
if (nameInput) nameInput.disabled = false;
}
} else {
this.checkedOptionKeys.delete(key);
if (isChooseOne) {
const nameInput = this.#getOptionChooseOneInput(key);
if (nameInput) {
nameInput.disabled = true;
nameInput.value = "";
}
delete this.chooseOneLabels[key];
}
}
this.#refreshUi();
}
/**
* @param {string} key
* @returns {HTMLInputElement | null}
*/
#getOptionChooseOneInput(key) {
const root = this.#contentRoot();
if (!root) return null;
return root.querySelector(
`[data-option-choose-one="true"][data-choose-one-key="${CSS.escape(
key,
)}"]`,
);
}
/**
* @param {HTMLSelectElement} select
*/
#onBonusChange(select) {
const index = Number(select.dataset.bonusIndex);
this.bonusCatalogIds[index] = select.value;
const root = this.#contentRoot();
const typedInput = root?.querySelector(`[name="bonusTypedLabel-${index}"]`);
const isTyped = select.value.startsWith("typed:");
if (typedInput) {
typedInput.classList.toggle("hidden", !isTyped);
if (!isTyped) {
typedInput.value = "";
this.bonusTypedLabels[index] = "";
}
}
this.#syncFormStateFromDom();
this.#refreshUi();
}
#syncFormStateFromDom() {
const root = this.#contentRoot();
if (!root) return;
root.querySelectorAll("[data-choose-one-key]").forEach((input) => {
const key = input.dataset.chooseOneKey;
if (key) this.chooseOneLabels[key] = input.value;
});
for (let i = 0; i < BONUS_SKILL_COUNT; i++) {
const select = root.querySelector(`[name="bonusSkill-${i}"]`);
this.bonusCatalogIds[i] = select?.value ?? "";
const typedInput = root.querySelector(`[name="bonusTypedLabel-${i}"]`);
this.bonusTypedLabels[i] = typedInput?.value ?? "";
}
const bondCount = this.#bondCount();
this.bondNames.length = bondCount;
this.bondRelationships.length = bondCount;
for (let i = 0; i < bondCount; i++) {
this.bondNames[i] =
root.querySelector(`[name="bondName-${i}"]`)?.value ?? "";
this.bondRelationships[i] =
root.querySelector(`[name="bondRelationship-${i}"]`)?.value ?? "";
}
}
#restoreFormSelections() {
const root = this.#contentRoot();
if (!root) return;
for (const key of this.checkedOptionKeys) {
const cb = root.querySelector(
`[name="optionSkillChecked"][value="${CSS.escape(key)}"]`,
);
if (cb) cb.checked = true;
if (this.optionSkillMeta[key]?.chooseOne) {
const nameInput = this.#getOptionChooseOneInput(key);
if (nameInput) {
nameInput.disabled = false;
nameInput.value = this.chooseOneLabels[key] ?? "";
}
}
}
for (const row of this.#buildAutomaticChooseOneRows()) {
const nameInput = root.querySelector(
`.character-creation-choose-one-section [data-choose-one-key="${CSS.escape(
row.key,
)}"]`,
);
if (nameInput) nameInput.value = this.chooseOneLabels[row.key] ?? "";
}
for (let i = 0; i < BONUS_SKILL_COUNT; i++) {
const select = root.querySelector(`[name="bonusSkill-${i}"]`);
if (select) select.value = this.bonusCatalogIds[i] ?? "";
const typedInput = root.querySelector(`[name="bonusTypedLabel-${i}"]`);
const isTyped = (this.bonusCatalogIds[i] ?? "").startsWith("typed:");
if (typedInput) {
typedInput.classList.toggle("hidden", !isTyped);
typedInput.value = this.bonusTypedLabels[i] ?? "";
}
}
for (let i = 0; i < this.#bondCount(); i++) {
const nameInput = root.querySelector(`[name="bondName-${i}"]`);
if (nameInput) nameInput.value = this.bondNames[i] ?? "";
const relInput = root.querySelector(`[name="bondRelationship-${i}"]`);
if (relInput) relInput.value = this.bondRelationships[i] ?? "";
}
}
/**
* @param {ReturnType<computeSkillValues>} computed
*/
#getDisplayMessages(computed) {
const wastePrefix = "bonusWaste:";
const mainErrors = computed.validationErrors.filter(
(code) => !code.startsWith(wastePrefix),
);
const wasteErrors = computed.validationErrors.filter((code) =>
code.startsWith(wastePrefix),
);
return {
validationMessages: formatProfessionValidationMessages(mainErrors, {
optionPicks: this.optionPicks,
}),
capWarnings: computed.capWarnings.map((w) => ({
warning: game.i18n.format("DG.Profession.Dialog.CapWarning", {
skill: w.label,
attempted: w.attempted,
waste: w.waste,
}),
})),
wasteMessages: formatProfessionValidationMessages(wasteErrors, {
optionPicks: this.optionPicks,
}),
};
}
/**
* @param {ReturnType<computeSkillValues>} computed
* @returns {string}
*/
#buildMessagesHtml(computed) {
const { validationMessages, capWarnings, wasteMessages } =
this.#getDisplayMessages(computed);
const capHtml = capWarnings
.map(
(w) =>
`<p class="dg-dialog__message--warning">${foundry.utils.escapeHTML(
w.warning,
)}</p>`,
)
.join("");
return (
renderValidationMessagesHtml(validationMessages) +
capHtml +
renderValidationMessagesHtml(wasteMessages)
);
}
async #refreshUi() {
const root = this.#contentRoot();
if (!root) return;
this.#syncFormStateFromDom();
const computed = this.#compute();
const rows = buildSkillDisplayRows(computed);
updateSkillGridBody(
root.querySelector("[data-profession-skills-body]"),
rows,
);
const messagesEl = root.querySelector("[data-profession-messages]");
if (messagesEl) {
messagesEl.innerHTML = this.#buildMessagesHtml(computed);
}
const submitBtn = this.dialog?.element?.querySelector(
'button[data-action="submit"]',
);
if (submitBtn) submitBtn.disabled = !computed.isValid;
}
async #onSubmit() {
this.#syncFormStateFromDom();
const computed = this.#compute();
if (!computed.isValid) return;
const draft = this.#getDraft();
this.result = {
outcome: "submitted",
payload: buildCharacterCreationPayload(computed, this.professionItem, {
bondNames: this.bondNames,
bondRelationships: this.bondRelationships,
}),
draft,
};
await this.dialog?.close();
}
}
+147
View File
@@ -0,0 +1,147 @@
import { BASE_TEMPLATE_PATH } from "../config/index.js";
import {
STAT_KEYS,
STAT_MAX,
STAT_MIN,
applyAgentStatistics,
buildStatisticRows,
getDefaultPointBuyValues,
validatePointBuyValues,
} from "../profession/stat-setup.js";
import { getDialogContentRoot, showDgDialog } from "./dg-dialog.js";
const { renderTemplate } = foundry.applications.handlebars;
/** @typedef {'submitted' | 'back'} AssignStatsOutcome */
/**
* @param {DialogV2} dialog
* @param {Record<string, number>} values
*/
function syncValuesFromDom(dialog, values) {
const root = getDialogContentRoot(dialog);
if (!root) return;
for (const key of STAT_KEYS) {
const input = root.querySelector(`[data-stat-key="${key}"]`);
if (input) {
let value = Number(input.value);
if (!Number.isFinite(value)) value = STAT_MIN;
values[key] = Math.clamp(Math.trunc(value), STAT_MIN, STAT_MAX);
}
}
}
/**
* @param {DialogV2} dialog
* @param {Record<string, number>} values
*/
function refreshAssignStatsUi(dialog, values) {
syncValuesFromDom(dialog, values);
const { isValid, remaining } = validatePointBuyValues(values);
const root = getDialogContentRoot(dialog);
const remainingEl = root?.querySelector("[data-points-remaining]");
if (remainingEl) {
remainingEl.textContent = game.i18n.format(
"DG.ProfessionSetup.AssignStats.PointsRemaining",
{ remaining },
);
remainingEl.classList.toggle("is-invalid", remaining !== 0);
}
const submitBtn = dialog.element?.querySelector(
'button[data-action="submit"]',
);
if (submitBtn) submitBtn.disabled = !isValid;
}
/**
* @param {DialogV2} dialog
* @param {Record<string, number>} values
*/
function bindAssignStatsListeners(dialog, values) {
const root = getDialogContentRoot(dialog);
if (!root) return;
root.querySelectorAll("[data-stat-key]").forEach((input) => {
input.addEventListener("input", () => {
const key = input.dataset.statKey;
if (!key) return;
let value = Number(input.value);
if (!Number.isFinite(value)) value = STAT_MIN;
value = Math.clamp(Math.trunc(value), STAT_MIN, STAT_MAX);
input.value = String(value);
values[key] = value;
refreshAssignStatsUi(dialog, values);
});
input.addEventListener("change", () =>
refreshAssignStatsUi(dialog, values),
);
});
}
/**
* @param {Actor} actor
* @returns {Promise<{ outcome: 'submitted' } | { outcome: 'back' } | null>}
*/
export default async function showAssignStatsDialog(actor) {
/** @type {Record<string, number>} */
const values = getDefaultPointBuyValues();
const { remaining } = validatePointBuyValues(values);
/** @type {{ outcome: 'submitted' } | { outcome: 'back' } | null} */
let result = null;
const content = await renderTemplate(
`${BASE_TEMPLATE_PATH}/dialog/assign-stats.html`,
{
stats: buildStatisticRows(),
values,
statMin: STAT_MIN,
statMax: STAT_MAX,
remaining,
},
);
return showDgDialog({
modifier: "assign-stats",
content,
window: {
title: game.i18n.localize("DG.ProfessionSetup.AssignStats.Title"),
},
position: { width: 420 },
form: { closeOnSubmit: false },
onRender: (dialog) => {
bindAssignStatsListeners(dialog, values);
refreshAssignStatsUi(dialog, values);
},
close: () => result,
buttons: [
{
action: "back",
label: game.i18n.localize("DG.ProfessionSetup.GoBack"),
callback: async (_event, _button, dialog) => {
result = { outcome: "back" };
await dialog.close();
},
},
{
action: "submit",
label: game.i18n.localize("DG.ProfessionSetup.AssignStats.Submit"),
default: true,
disabled: true,
callback: async (_event, _button, dialog) => {
syncValuesFromDom(dialog, values);
const { isValid } = validatePointBuyValues(values);
if (!isValid) return false;
await applyAgentStatistics(actor, { ...values });
result = { outcome: "submitted" };
await dialog.close();
return false;
},
},
],
});
}
@@ -0,0 +1,170 @@
import {
buildSortedFixedSkillRows,
buildSortedTypedSkillRows,
getBonusTrackLabel,
} from "../profession/index.js";
import { reorderForColumnSorting } from "../utils/skill-layout.js";
import {
applySkillTooltipDisplayMode,
buildSkillTooltip,
} from "../utils/skill-tooltip.js";
export const CHARACTER_CREATION_SKILL_COLUMNS = 2;
/**
* @param {{ sortLabel: string, rowId: string, label: string, value: number, isModified: boolean, tooltip?: string }[]} rows
* @param {number} [columns]
* @returns {{ rowId: string, label: string, value: number, isModified: boolean, tooltip: string }[]}
*/
export function orderSkillRowsForDisplay(
rows,
columns = CHARACTER_CREATION_SKILL_COLUMNS,
) {
const sorted = [...rows].sort((a, b) =>
a.sortLabel.localeCompare(b.sortLabel, game.i18n.lang),
);
const ordered = game.settings.get("deltagreen", "sortSkills")
? reorderForColumnSorting(sorted, columns)
: sorted;
return ordered.map(({ rowId, label, value, isModified, tooltip }) => ({
rowId,
label,
value,
isModified: Boolean(isModified),
tooltip: tooltip ?? "",
}));
}
/**
* @param {object} computed
* @param {Record<string, { group: string, label: string, value: number }>} [typedValuesSource]
* @returns {{ rowId: string, label: string, value: number, isModified: boolean, tooltip: string, sortLabel: string }[]}
*/
export function buildSkillDisplayRows(computed, typedValuesSource) {
const typedValues = typedValuesSource ?? computed.typedValues ?? {};
const modifiedFixed = new Set(computed.modifiedFixedKeys ?? []);
const modifiedTyped = new Set(computed.modifiedTypedKeys ?? []);
const fixedRows = buildSortedFixedSkillRows(computed.fixedValues);
const typedRows = buildSortedTypedSkillRows(computed.typedValues);
return orderSkillRowsForDisplay([
...fixedRows.map((r) => ({
rowId: r.key,
label: r.label,
value: r.value,
sortLabel: r.sortLabel,
isModified: modifiedFixed.has(r.key),
tooltip: buildSkillTooltip("dialog", null, {
kind: "fixed",
key: r.key,
}),
})),
...typedRows.map((r) => {
const data = typedValues[r.storageKey];
return {
rowId: r.storageKey,
label: r.label,
value: r.value,
sortLabel: r.sortLabel,
isModified: modifiedTyped.has(r.storageKey),
tooltip: buildSkillTooltip("dialog", null, {
kind: "typed",
group: data?.group ?? "",
label: r.label,
}),
};
}),
]);
}
/**
* @param {{ rowId: string, label: string, value: number, isModified: boolean, tooltip: string }[]} rows
* @returns {string}
*/
export function renderSkillGridHtml(rows) {
return rows
.map((r) => {
const modifiedClass = r.isModified ? " is-modified" : "";
const tooltipAttr = r.tooltip
? ` data-tooltip="${foundry.utils.escapeHTML(r.tooltip)}"`
: "";
return `<div class="dg-dialog__skill-entry${modifiedClass}" data-skill-row="${foundry.utils.escapeHTML(
r.rowId,
)}"><span class="dg-dialog__skill-label"${tooltipAttr}>${foundry.utils.escapeHTML(
r.label,
)}</span><span class="dg-dialog__skill-value" data-skill-value>${
r.value
}%</span></div>`;
})
.join("");
}
/**
* @param {HTMLElement} body
* @param {{ rowId: string, label: string, value: number, isModified: boolean, tooltip: string }[]} rows
*/
export function updateSkillGridBody(body, rows) {
if (!body) return;
body.innerHTML = renderSkillGridHtml(rows);
applySkillTooltipDisplayMode(body);
}
/**
* @param {string[]} validationErrors
* @param {object} [options]
* @param {string} [options.bonusSkillRequiredKey]
* @param {string} [options.bonusSkillWasteTooHighKey]
* @returns {string[]}
*/
export function formatBonusPickValidationMessages(
validationErrors,
{
bonusSkillRequiredKey = "DG.Profession.Dialog.BonusSkillRequired",
bonusSkillWasteTooHighKey = "DG.Profession.Dialog.BonusSkillWasteTooHigh",
} = {},
) {
const messages = [];
if (validationErrors.some((code) => /^bonus\d+$/.test(code))) {
messages.push(game.i18n.localize(bonusSkillRequiredKey));
}
if (validationErrors.some((code) => code.startsWith("bonusType"))) {
messages.push(
game.i18n.localize("DG.Profession.Dialog.BonusSkillTypeRequired"),
);
}
/** @type {Set<string>} */
const bonusWasteTrackKeys = new Set();
for (const code of validationErrors) {
const match = code.match(/^bonusWaste:\d+\|(\d+)\|(.+)$/);
if (match) bonusWasteTrackKeys.add(match[2]);
}
for (const trackKey of bonusWasteTrackKeys) {
messages.push(
game.i18n.format(bonusSkillWasteTooHighKey, {
skill: getBonusTrackLabel(trackKey),
}),
);
}
return messages;
}
/**
* @param {string[]} messages
* @returns {string}
*/
export function renderValidationMessagesHtml(messages) {
return messages
.map(
(message) =>
`<p class="dg-dialog__message--error">${foundry.utils.escapeHTML(
message,
)}</p>`,
)
.join("");
}
@@ -0,0 +1,120 @@
import { BASE_TEMPLATE_PATH } from "../config/index.js";
import { getDialogContentRoot, showDgDialog } from "./dg-dialog.js";
const { renderTemplate } = foundry.applications.handlebars;
/** @typedef {'freshRecruit' | 'extremeViolence' | 'captivity' | 'hardExperience' | 'thingsMan'} DamagedVeteranPath */
const VETERAN_OPTIONS = /** @type {const} */ ([
{
value: "freshRecruit",
labelKey: "DG.DamagedVeterans.Dialog.FreshRecruit.Label",
bodyKey: "DG.DamagedVeterans.Dialog.FreshRecruit.Body",
},
{
value: "extremeViolence",
labelKey: "DG.DamagedVeterans.Dialog.ExtremeViolence.Label",
bodyKey: "DG.DamagedVeterans.Dialog.ExtremeViolence.Body",
},
{
value: "captivity",
labelKey: "DG.DamagedVeterans.Dialog.Captivity.Label",
bodyKey: "DG.DamagedVeterans.Dialog.Captivity.Body",
},
{
value: "hardExperience",
labelKey: "DG.DamagedVeterans.Dialog.HardExperience.Label",
bodyKey: "DG.DamagedVeterans.Dialog.HardExperience.Body",
},
{
value: "thingsMan",
labelKey: "DG.DamagedVeterans.Dialog.ThingsMan.Label",
bodyKey: "DG.DamagedVeterans.Dialog.ThingsMan.Body",
},
]);
/**
* @param {import("../profession/commit-character-creation.js").CharacterCreationPayload} payload
* @param {object} [options]
* @param {DamagedVeteranPath} [options.selectedPath]
* @returns {Promise<{ outcome: 'submitted', path: DamagedVeteranPath } | { outcome: 'back' } | null>}
*/
export default async function showDamagedVeteransDialog(
payload,
{ selectedPath } = {},
) {
const defaultPath = selectedPath ?? "freshRecruit";
const content = await renderTemplate(
`${BASE_TEMPLATE_PATH}/dialog/damaged-veterans.html`,
{
options: VETERAN_OPTIONS.map((option) => ({
...option,
checked: option.value === defaultPath,
})),
},
);
/** @type {{ outcome: 'submitted', path: DamagedVeteranPath } | { outcome: 'back' } | null} */
let result = null;
await showDgDialog({
modifier: "damaged-veterans",
content,
window: {
title: game.i18n.localize("DG.DamagedVeterans.Dialog.Title"),
},
position: { width: 720, height: "auto" },
form: { closeOnSubmit: false },
close: () => result,
onRender: (dialog) => {
const root = getDialogContentRoot(dialog);
if (!root) return;
const submitBtn = dialog.element?.querySelector(
'button[data-action="submit"]',
);
const updateSubmitState = () => {
const selected = root.querySelector(
'input[name="damagedVeteranPath"]:checked',
);
if (submitBtn) submitBtn.disabled = !selected;
};
root
.querySelectorAll('input[name="damagedVeteranPath"]')
.forEach((input) => {
input.addEventListener("change", updateSubmitState);
});
updateSubmitState();
},
buttons: [
{
action: "back",
label: game.i18n.localize("DG.ProfessionSetup.GoBack"),
callback: async (_event, _button, dialog) => {
result = { outcome: "back" };
await dialog.close();
return false;
},
},
{
action: "submit",
label: game.i18n.localize("DG.Profession.Dialog.Submit"),
default: true,
disabled: false,
callback: async (_event, _button, dialog) => {
const root = getDialogContentRoot(dialog);
const selected = root?.querySelector(
'input[name="damagedVeteranPath"]:checked',
);
if (!selected) return false;
const path = /** @type {DamagedVeteranPath} */ (selected.value);
result = { outcome: "submitted", path };
await dialog.close();
return false;
},
},
],
});
return result;
}
@@ -0,0 +1,75 @@
import DG from "../config/index.js";
import {
buildEffectFieldSelectOptions,
getEffectFieldGroupsForDocument,
} from "../active-effect/effect-fields.js";
const { renderTemplate } = foundry.applications.handlebars;
const { ActiveEffectConfig } = foundry.applications.sheets;
const DG_CHANGE_TYPES = ["add", "subtract", "override"];
/** @extends {foundry.applications.sheets.ActiveEffectConfig} */
export default class DGActiveEffectConfig extends ActiveEffectConfig {
/** @type {Record<string, string>|null} */
#dgChangeTypes = null;
/** @type {Array<object>|null} */
#fieldGroups = null;
/** @returns {Record<string, string>} */
#getDgChangeTypes() {
if (!this.#dgChangeTypes) {
this.#dgChangeTypes = DG_CHANGE_TYPES.reduce((types, type) => {
const label = ActiveEffect.CHANGE_TYPES[type]?.label;
types[type] = label ? game.i18n.localize(label) : type;
return types;
}, {});
}
return this.#dgChangeTypes;
}
/** @returns {Array<object>} */
#getFieldGroups() {
if (!this.#fieldGroups) {
const groups = getEffectFieldGroupsForDocument(this.document);
this.#fieldGroups = buildEffectFieldSelectOptions(groups);
}
return this.#fieldGroups;
}
/** @override */
_processChangeSubmission(change, index) {
super._processChangeSubmission(change, index);
if (!change.phase) change.phase = "final";
}
/** @override */
async _renderChange(context) {
const { change, index } = context;
if (typeof change.value !== "string") {
change.value = JSON.stringify(change.value);
}
if (!change.phase) change.phase = "final";
Object.assign(
change,
["key", "type", "value", "phase", "priority"].reduce(
(paths, fieldName) => {
paths[`${fieldName}Path`] = `system.changes.${index}.${fieldName}`;
return paths;
},
{},
),
);
return renderTemplate(
`systems/${DG.ID}/templates/active-effect/change-row.html`,
{
...context,
fieldGroups: this.#getFieldGroups(),
dgChangeTypes: this.#getDgChangeTypes(),
},
);
}
}
+103
View File
@@ -0,0 +1,103 @@
import DG from "../config/index.js";
const { DialogV2 } = foundry.applications.api;
/** @type {readonly string[]} */
export const DG_DIALOG_APP_CLASSES = ["deltagreen", "dg-dialog-app"];
/**
* @returns {"program-style" | "cowboy-style" | "outlaw-style"}
*/
export function getCharacterSheetThemeClass() {
const theme = game.settings.get(DG.ID, "characterSheetStyle");
return `${theme}-style`;
}
/**
* @param {DialogV2} dialog
* @returns {HTMLElement | null}
*/
export function getDialogContentRoot(dialog) {
return (
dialog.element?.querySelector(".dialog-content") ??
dialog.element?.querySelector(".dialog-form") ??
null
);
}
/**
* @param {DialogV2} dialog
* @returns {void}
*/
export function applyDialogTheme(dialog) {
const windowContent = dialog.element?.querySelector("section.window-content");
if (!windowContent) return;
windowContent.classList.add(getCharacterSheetThemeClass());
}
/**
* @param {string} [modifier]
* @returns {string[]}
*/
export function buildDialogAppClasses(modifier) {
const classes = [...DG_DIALOG_APP_CLASSES];
if (modifier) classes.push(`dg-dialog-app--${modifier}`);
return classes;
}
/**
* @param {HTMLElement} root
* @param {string} tabId
* @returns {void}
*/
export function activateDialogTab(root, tabId) {
root.querySelectorAll(".tabs [data-action='tab']").forEach((el) => {
el.classList.toggle("active", el.dataset.tab === tabId);
});
root.querySelectorAll(".tab[data-tab]").forEach((el) => {
el.classList.toggle("active", el.dataset.tab === tabId);
});
}
/**
* @param {HTMLElement} root
* @returns {void}
*/
export function bindDialogTabs(root) {
root.querySelectorAll(".tabs [data-action='tab']").forEach((tab) => {
tab.addEventListener("click", (event) => {
event.preventDefault();
const tabId = tab.dataset.tab;
if (tabId) activateDialogTab(root, tabId);
});
});
}
/**
* @param {object} options — DialogV2.wait options plus DG extensions
* @param {string} [options.modifier] — app class modifier: dg-dialog-app--{modifier}
* @param {(dialog: DialogV2, event: Event) => void | Promise<void>} [options.onRender]
* @param {string[]} [options.classes] — extra app classes merged after base
* @returns {Promise<unknown>}
*/
export function showDgDialog(options) {
const {
modifier,
onRender,
classes: extraClasses = [],
render: userRender,
...dialogOptions
} = options;
const classes = [...buildDialogAppClasses(modifier), ...extraClasses];
return DialogV2.wait({
...dialogOptions,
classes,
render: async (event, dialog) => {
applyDialogTheme(dialog);
if (userRender) await userRender(event, dialog);
if (onRender) await onRender(dialog, event);
},
});
}
@@ -1,4 +1,5 @@
import DG, { BASE_TEMPLATE_PATH } from "../config.js";
import DG, { BASE_TEMPLATE_PATH } from "../config/index.js";
import { applyDialogTheme } from "./dg-dialog.js";
export default class ActorEditStatForm extends foundry.applications.api.HandlebarsApplicationMixin(
foundry.applications.api.ApplicationV2,
@@ -11,9 +12,13 @@ export default class ActorEditStatForm extends foundry.applications.api.Handleba
/** @override */
static DEFAULT_OPTIONS = {
tag: "form",
classes: [DG.ID, "edit-stat-form"],
window: { title: "Edit statistics form", resizable: true },
position: { width: 400, height: 200 },
classes: [DG.ID, "edit-stat-form", "dg-dialog-app"],
window: {
title: "DG.EditStats.Title",
resizable: true,
contentClasses: ["standard-form"],
},
position: { width: 600, height: "auto" },
actions: {},
form: {
handler: this.formHandler,
@@ -28,11 +33,20 @@ export default class ActorEditStatForm extends foundry.applications.api.Handleba
},
});
/** @override */
async _onRender(context, options) {
await super._onRender(context, options);
applyDialogTheme(this);
}
/** @override */
async _prepareContext(options) {
return {
...(await super._prepareContext(options)),
actor: this.actor,
statKeys: DG.statistics,
sourceStatistics:
this.actor._source?.system?.statistics ?? this.actor.system.statistics,
};
}
@@ -0,0 +1,241 @@
import { BASE_TEMPLATE_PATH } from "../config/index.js";
import {
HARD_EXPERIENCE_BONUS_COUNT,
buildBonusSkillCatalog,
computeHardExperienceSkillValues,
} from "../profession/index.js";
import {
buildSkillDisplayRows,
formatBonusPickValidationMessages,
renderValidationMessagesHtml,
updateSkillGridBody,
} from "./character-creation-ui.js";
import { getDialogContentRoot, showDgDialog } from "./dg-dialog.js";
const { renderTemplate } = foundry.applications.handlebars;
/**
* @typedef {object} HardExperienceDialogState
* @property {string[]} [bonusCatalogIds]
* @property {string[]} [bonusTypedLabels]
* @property {number|null} [removedBondIndex]
*/
/**
* @param {import("../profession/commit-character-creation.js").CharacterCreationPayload} payload
* @param {HardExperienceDialogState} [state]
* @returns {Promise<{ outcome: 'submitted', path: 'hardExperience', bonusCatalogIds: string[], bonusTypedLabels: string[], removedBondIndex: number } | { outcome: 'back' } | null>}
*/
export default async function showHardExperienceDialog(payload, state = {}) {
const bonusCatalogIds = state.bonusCatalogIds
? [...state.bonusCatalogIds]
: Array(HARD_EXPERIENCE_BONUS_COUNT).fill("");
const bonusTypedLabels = state.bonusTypedLabels
? [...state.bonusTypedLabels]
: Array(HARD_EXPERIENCE_BONUS_COUNT).fill("");
let removedBondIndex =
state.removedBondIndex ?? (payload.bonds.length > 0 ? 0 : null);
/** @type {{ outcome: 'submitted', path: 'hardExperience', bonusCatalogIds: string[], bonusTypedLabels: string[], removedBondIndex: number } | { outcome: 'back' } | null} */
let result = null;
/** @type {import("foundry.applications.api.DialogV2").default | null} */
let dialog = null;
const bondRows = payload.bonds.map((bond, index) => ({
index,
name: bond.name,
relationship: bond.relationship,
checked: index === removedBondIndex,
}));
const bonusSlots = Array.from(
{ length: HARD_EXPERIENCE_BONUS_COUNT },
(_, index) => ({ index }),
);
const syncFormStateFromDom = () => {
const root = dialog ? getDialogContentRoot(dialog) : null;
if (!root) return;
for (let i = 0; i < HARD_EXPERIENCE_BONUS_COUNT; i++) {
const select = root.querySelector(`[name="bonusSkill-${i}"]`);
bonusCatalogIds[i] = select?.value ?? "";
const typedInput = root.querySelector(`[name="bonusTypedLabel-${i}"]`);
bonusTypedLabels[i] = typedInput?.value ?? "";
}
const bondSelected = root.querySelector(
'input[name="removedBondIndex"]:checked',
);
removedBondIndex = bondSelected ? Number(bondSelected.value) : null;
};
const compute = () =>
computeHardExperienceSkillValues(payload, {
bonusCatalogIds,
bonusTypedLabels,
});
const buildTemplateData = () => {
const computed = compute();
return {
skillRows: buildSkillDisplayRows(computed),
catalog: buildBonusSkillCatalog(),
bonusSlots,
bondRows,
validationMessages: formatBonusPickValidationMessages(
computed.validationErrors,
{
bonusSkillRequiredKey: "DG.HardExperience.Dialog.BonusSkillRequired",
bonusSkillWasteTooHighKey:
"DG.HardExperience.Dialog.BonusSkillWasteTooHigh",
},
),
isValid: computed.isValid && removedBondIndex !== null,
};
};
const content = await renderTemplate(
`${BASE_TEMPLATE_PATH}/dialog/hard-experience.html`,
buildTemplateData(),
);
const refreshUi = async () => {
syncFormStateFromDom();
const root = dialog ? getDialogContentRoot(dialog) : null;
if (!root) return;
const templateData = buildTemplateData();
updateSkillGridBody(
root.querySelector("[data-hard-experience-skills-body]"),
templateData.skillRows,
);
const messagesEl = root.querySelector("[data-hard-experience-messages]");
if (messagesEl) {
let messagesHtml = renderValidationMessagesHtml(
templateData.validationMessages,
);
if (removedBondIndex === null) {
messagesHtml += renderValidationMessagesHtml([
game.i18n.localize("DG.HardExperience.Dialog.BondRemovalRequired"),
]);
}
messagesEl.innerHTML = messagesHtml;
}
const submitBtn = dialog?.element?.querySelector(
'button[data-action="submit"]',
);
if (submitBtn) {
submitBtn.disabled = !(templateData.isValid && removedBondIndex !== null);
}
for (let i = 0; i < HARD_EXPERIENCE_BONUS_COUNT; i++) {
const select = root.querySelector(`[name="bonusSkill-${i}"]`);
if (select) select.value = bonusCatalogIds[i] ?? "";
const typedInput = root.querySelector(`[name="bonusTypedLabel-${i}"]`);
const isTyped = (bonusCatalogIds[i] ?? "").startsWith("typed:");
if (typedInput) {
typedInput.classList.toggle("hidden", !isTyped);
typedInput.value = bonusTypedLabels[i] ?? "";
}
}
if (removedBondIndex !== null) {
const bondInput = root.querySelector(
`input[name="removedBondIndex"][value="${removedBondIndex}"]`,
);
if (bondInput) bondInput.checked = true;
}
};
const bindListeners = () => {
const root = dialog ? getDialogContentRoot(dialog) : null;
if (!root) return;
root.querySelectorAll("select[data-bonus-index]").forEach((select) => {
select.addEventListener("change", () => {
const index = Number(select.dataset.bonusIndex);
bonusCatalogIds[index] = select.value;
const typedInput = root.querySelector(
`[name="bonusTypedLabel-${index}"]`,
);
const isTyped = select.value.startsWith("typed:");
if (typedInput) {
typedInput.classList.toggle("hidden", !isTyped);
if (!isTyped) {
typedInput.value = "";
bonusTypedLabels[index] = "";
}
}
refreshUi();
});
});
root.querySelectorAll("[data-bonus-typed-index]").forEach((input) => {
input.addEventListener("input", () => refreshUi());
});
root.querySelectorAll('input[name="removedBondIndex"]').forEach((input) => {
input.addEventListener("change", () => refreshUi());
});
};
await showDgDialog({
modifier: "hard-experience",
content,
window: {
title: game.i18n.localize("DG.HardExperience.Dialog.Title"),
},
position: { width: 1080, height: 780 },
form: { closeOnSubmit: false },
close: () => result,
onRender: async (instance) => {
dialog = instance;
bindListeners();
await refreshUi();
},
buttons: [
{
action: "back",
label: game.i18n.localize("DG.ProfessionSetup.GoBack"),
callback: async (_event, _button, instance) => {
syncFormStateFromDom();
result = {
outcome: "back",
bonusCatalogIds: [...bonusCatalogIds],
bonusTypedLabels: [...bonusTypedLabels],
removedBondIndex,
};
await instance.close();
return false;
},
},
{
action: "submit",
label: game.i18n.localize("DG.Profession.Dialog.Submit"),
default: true,
disabled: true,
callback: async () => {
syncFormStateFromDom();
const computed = compute();
if (!computed.isValid || removedBondIndex === null) return false;
result = {
outcome: "submitted",
path: "hardExperience",
bonusCatalogIds: [...bonusCatalogIds],
bonusTypedLabels: [...bonusTypedLabels],
removedBondIndex,
};
await dialog?.close();
return false;
},
},
],
});
return result;
}
@@ -0,0 +1,55 @@
import { BASE_TEMPLATE_PATH } from "../config/index.js";
import { showDgDialog } from "./dg-dialog.js";
const { renderTemplate } = foundry.applications.handlebars;
/** @typedef {'roll' | 'assign' | 'skip'} PickStatisticsChoice */
/**
* @returns {Promise<PickStatisticsChoice | null>}
*/
export default async function showPickStatisticsDialog() {
const content = await renderTemplate(
`${BASE_TEMPLATE_PATH}/dialog/pick-statistics.html`,
{},
);
return showDgDialog({
modifier: "pick-statistics",
content,
window: {
title: game.i18n.localize("DG.ProfessionSetup.PickStatistics.Title"),
},
position: { width: 480 },
close: () => null,
buttons: [
{
action: "roll",
label: game.i18n.localize(
"DG.ProfessionSetup.PickStatistics.RollStats",
),
tooltip: game.i18n.localize(
"DG.ProfessionSetup.PickStatistics.RollStatsTooltip",
),
callback: () => "roll",
},
{
action: "assign",
label: game.i18n.localize(
"DG.ProfessionSetup.PickStatistics.AssignStats",
),
tooltip: game.i18n.localize(
"DG.ProfessionSetup.PickStatistics.AssignStatsTooltip",
),
callback: () => "assign",
},
{
action: "skip",
label: game.i18n.localize(
"DG.ProfessionSetup.PickStatistics.LeaveStatsAlone",
),
callback: () => "skip",
},
],
});
}
@@ -0,0 +1,186 @@
import showPickSkillsDialog from "./add-profession-dialog.js";
import showAssignStatsDialog from "./assign-stats-dialog.js";
import showDamagedVeteransDialog from "./damaged-veterans-dialog.js";
import showHardExperienceDialog from "./hard-experience-dialog.js";
import showPickStatisticsDialog from "./pick-statistics-dialog.js";
import showRollStatsDialog from "./roll-stats-dialog.js";
import showThingsDisorderDialog from "./things-disorder-dialog.js";
import { commitProfessionSetup } from "../profession/index.js";
/**
* @param {Actor} actor
* @param {object} itemData
* @returns {Item}
*/
function createProfessionStub(actor, itemData) {
const data = foundry.utils.duplicate(itemData);
delete data._id;
const ItemDocument = foundry.utils.getDocumentClass("Item");
return new ItemDocument(data, { parent: actor });
}
/**
* @param {Actor} actor
* @param {object} [options]
* @param {TokenDocument|null} [options.token]
* @returns {Promise<boolean>}
*/
async function runStatsPhase(actor, { token = null } = {}) {
const choice = await showPickStatisticsDialog();
if (choice === null) return false;
if (choice === "skip") return true;
if (choice === "roll") {
const result = await showRollStatsDialog(actor, { token });
if (result === null) return false;
if (result.outcome === "back") return runStatsPhase(actor, { token });
return true;
}
if (choice === "assign") {
const result = await showAssignStatsDialog(actor);
if (result === null) return false;
if (result.outcome === "back") return runStatsPhase(actor, { token });
return true;
}
return false;
}
/**
* @param {import("../profession/commit-character-creation.js").CharacterCreationPayload} payload
* @param {object} [options]
* @param {string} [options.selectedPath]
* @param {import("./hard-experience-dialog.js").HardExperienceDialogState} [options.heState]
* @param {import("./things-disorder-dialog.js").ThingsDisorderDialogState} [options.tdState]
* @returns {Promise<object|null>}
*/
async function resolveVeteranResult(
payload,
{ selectedPath, heState, tdState } = {},
) {
const dvResult = await showDamagedVeteransDialog(payload, { selectedPath });
if (dvResult === null) return null;
if (dvResult.outcome === "back") return { outcome: "back" };
const { path } = dvResult;
if (path === "hardExperience") {
const heResult = await showHardExperienceDialog(payload, heState);
if (heResult === null) return null;
if (heResult.outcome === "back") {
return resolveVeteranResult(payload, {
selectedPath: "hardExperience",
heState: {
bonusCatalogIds: heResult.bonusCatalogIds,
bonusTypedLabels: heResult.bonusTypedLabels,
removedBondIndex: heResult.removedBondIndex,
},
});
}
return {
path: "hardExperience",
bonusCatalogIds: heResult.bonusCatalogIds,
bonusTypedLabels: heResult.bonusTypedLabels,
removedBondIndex: heResult.removedBondIndex,
};
}
if (path === "thingsMan") {
const tdResult = await showThingsDisorderDialog(tdState);
if (tdResult === null) return null;
if (tdResult.outcome === "back") {
return resolveVeteranResult(payload, {
selectedPath: "thingsMan",
tdState: { disorder: tdResult.disorder },
});
}
return {
path: "thingsMan",
disorder: tdResult.disorder,
};
}
return { path };
}
/**
* @param {Item} professionItem
* @param {Actor} actor
* @param {import("./add-profession-dialog.js").CharacterCreationDraft} [characterDraft]
* @returns {Promise<boolean>}
*/
async function runCharacterCreationPhase(
professionItem,
actor,
characterDraft,
) {
const ccResult = await showPickSkillsDialog(professionItem, actor, {
draft: characterDraft,
});
if (ccResult === null) return false;
const veteranResult = await resolveVeteranResult(ccResult.payload);
if (veteranResult === null) return false;
if (veteranResult.outcome === "back") {
return runCharacterCreationPhase(professionItem, actor, ccResult.draft);
}
await commitProfessionSetup(
actor,
professionItem,
ccResult.payload,
veteranResult,
);
return true;
}
/**
* Run the full profession setup flow: Pick Statistics → optional stat setup → Character Creation → Damaged Veterans.
*
* @param {Item} professionItem Profession item or unsaved stub on the actor
* @param {Actor} actor
* @param {object} [options]
* @param {TokenDocument|null} [options.token]
* @returns {Promise<boolean>} True if the full setup flow completed successfully.
*/
async function runProfessionSetupFlow(
professionItem,
actor,
{ token = null } = {},
) {
const statsComplete = await runStatsPhase(actor, { token });
if (!statsComplete) return false;
return runCharacterCreationPhase(professionItem, actor);
}
/**
* Assign a profession to an agent: full setup flow, then embed the item only on success.
*
* @param {Actor} actor
* @param {object} itemData Item source data (e.g. from drop `toObject()` or create template)
* @param {object} [options]
* @param {TokenDocument|null} [options.token]
* @returns {Promise<Item|null>}
*/
export default async function assignProfessionToAgent(
actor,
itemData,
{ token = null } = {},
) {
if (actor.items.some((i) => i.type === "profession")) {
ui.notifications.info(game.i18n.localize("DG.Profession.AlreadyAssigned"), {
localize: false,
});
return null;
}
const stub = createProfessionStub(actor, itemData);
const submitted = await runProfessionSetupFlow(stub, actor, { token });
if (!submitted) return null;
return actor.items.find((item) => item.type === "profession") ?? null;
}
@@ -0,0 +1,45 @@
import { BASE_TEMPLATE_PATH } from "../config/index.js";
import { getDialogContentRoot, showDgDialog } from "./dg-dialog.js";
const { renderTemplate } = foundry.applications.handlebars;
/**
* @param {Item} professionItem
* @returns {Promise<boolean>} True if the name was updated.
*/
export default async function showRenameProfessionDialog(professionItem) {
if (professionItem.type !== "profession") return false;
const content = await renderTemplate(
`${BASE_TEMPLATE_PATH}/dialog/rename-profession.html`,
{ name: professionItem.name },
);
const result = await showDgDialog({
modifier: "rename-profession",
content,
position: { width: 360 },
window: {
title: game.i18n.localize("DG.AgentSheet.RenameProfession.Title"),
},
default: "submit",
buttons: [
{
default: true,
action: "submit",
label: game.i18n.localize("DG.Profession.Dialog.Submit"),
callback: (_event, _button, dialog) => {
const name = getDialogContentRoot(dialog)
?.querySelector("[name='profession-rename']")
?.value?.trim();
return name || null;
},
},
],
});
if (!result || result === professionItem.name) return false;
await professionItem.update({ name: result });
return true;
}
+271
View File
@@ -0,0 +1,271 @@
import { BASE_TEMPLATE_PATH } from "../config/index.js";
import { createDGChatMessage } from "../chat/dg-chat-card.js";
import {
STAT_KEYS,
applyAgentStatistics,
buildRollStatsChatContent,
buildStatisticRows,
rollStatisticScores,
} from "../profession/stat-setup.js";
import { getDialogContentRoot, showDgDialog } from "./dg-dialog.js";
const { renderTemplate } = foundry.applications.handlebars;
/** @typedef {'submitted' | 'back'} RollStatsOutcome */
/**
* @param {object} state
* @param {DialogV2} dialog
* @param {number|null} rollIndex
* @returns {string|null}
*/
function findStatKeyForRollIndex(state, rollIndex) {
for (const key of STAT_KEYS) {
if (state.assignments[key] === rollIndex) return key;
}
return null;
}
/**
* @param {object} state
* @param {DialogV2} dialog
*/
function syncDomFromAssignments(state, dialog) {
const root = getDialogContentRoot(dialog);
if (!root) return;
const pool = root.querySelector("[data-roll-stats-pool]");
if (!pool) return;
root.querySelectorAll(".roll-stat-token").forEach((token) => {
pool.appendChild(token);
});
for (const key of STAT_KEYS) {
const rollIndex = state.assignments[key];
if (rollIndex !== null) {
const slot = root.querySelector(
`[data-stat-slot][data-stat-key="${key}"]`,
);
const token = pool.querySelector(`[data-roll-index="${rollIndex}"]`);
if (slot && token) {
slot.appendChild(token);
}
}
}
}
/**
* @param {object} state
* @param {DialogV2} dialog
*/
function refreshRollStatsUi(state, dialog) {
const submitBtn = dialog.element?.querySelector(
'button[data-action="submit"]',
);
if (submitBtn) {
submitBtn.disabled = !STAT_KEYS.every(
(key) => state.assignments[key] !== null,
);
}
}
/**
* @param {object} state
* @param {DialogV2} dialog
*/
function bindRollStatsListeners(state, dialog) {
const root = getDialogContentRoot(dialog);
if (!root) return;
if (root.dataset.rollStatsListenersBound === "true") return;
root.dataset.rollStatsListenersBound = "true";
root.querySelectorAll(".roll-stat-token").forEach((token) => {
token.addEventListener("dragstart", (event) => {
const rollIndex = Number(token.dataset.rollIndex);
state.dragRollIndex = rollIndex;
token.classList.add("is-dragging");
event.dataTransfer?.setData("text/plain", String(rollIndex));
if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
});
token.addEventListener("dragend", () => {
token.classList.remove("is-dragging");
state.dragRollIndex = null;
root
.querySelectorAll(".drag-over")
.forEach((el) => el.classList.remove("drag-over"));
});
});
const readDragRollIndex = (event) => {
const fromData = event.dataTransfer?.getData("text/plain");
if (fromData !== undefined && fromData !== "") {
const parsed = Number(fromData);
if (Number.isInteger(parsed)) return parsed;
}
if (state.dragRollIndex !== null) return state.dragRollIndex;
return null;
};
root.querySelectorAll("[data-stat-slot]").forEach((slot) => {
slot.addEventListener("dragover", (event) => {
event.preventDefault();
if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
slot.classList.add("drag-over");
});
slot.addEventListener("dragleave", () =>
slot.classList.remove("drag-over"),
);
slot.addEventListener("drop", (event) => {
event.preventDefault();
slot.classList.remove("drag-over");
const rollIndex = readDragRollIndex(event);
if (rollIndex === null) return;
const { statKey } = slot.dataset;
if (!statKey) return;
const sourceStatKey = findStatKeyForRollIndex(state, rollIndex);
const displacedIndex = state.assignments[statKey];
if (sourceStatKey === statKey) return;
if (sourceStatKey) {
state.assignments[sourceStatKey] = null;
}
if (displacedIndex !== null && displacedIndex !== rollIndex) {
if (sourceStatKey) {
state.assignments[sourceStatKey] = displacedIndex;
}
}
state.assignments[statKey] = rollIndex;
syncDomFromAssignments(state, dialog);
refreshRollStatsUi(state, dialog);
});
});
const pool = root.querySelector("[data-roll-stats-pool]");
if (!pool) return;
pool.addEventListener("dragover", (event) => {
event.preventDefault();
if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
pool.classList.add("drag-over");
});
pool.addEventListener("dragleave", (event) => {
if (
event.currentTarget === event.target ||
!event.currentTarget.contains(event.relatedTarget)
) {
pool.classList.remove("drag-over");
}
});
pool.addEventListener("drop", (event) => {
event.preventDefault();
pool.classList.remove("drag-over");
const rollIndex = readDragRollIndex(event);
if (rollIndex === null) return;
const sourceStatKey = findStatKeyForRollIndex(state, rollIndex);
if (sourceStatKey) {
state.assignments[sourceStatKey] = null;
syncDomFromAssignments(state, dialog);
refreshRollStatsUi(state, dialog);
}
});
}
/**
* @param {Actor} actor
* @param {object} [options]
* @param {TokenDocument|null} [options.token]
* @returns {Promise<{ outcome: 'submitted' } | { outcome: 'back' } | null>}
*/
export default async function showRollStatsDialog(
actor,
{ token = null } = {},
) {
const rolled = await rollStatisticScores({ actor, token });
const rolls = rolled.map((entry, index) => ({
index,
total: entry.total,
}));
await createDGChatMessage({
actor,
token,
title: game.i18n.localize("DG.ProfessionSetup.RollStats.ChatLabel"),
content: buildRollStatsChatContent(rolls),
});
const state = {
rolls,
assignments: Object.fromEntries(STAT_KEYS.map((key) => [key, null])),
dragRollIndex: null,
};
/** @type {{ outcome: 'submitted' } | { outcome: 'back' } | null} */
let result = null;
const content = await renderTemplate(
`${BASE_TEMPLATE_PATH}/dialog/roll-stats.html`,
{
rolls: state.rolls,
stats: buildStatisticRows(),
},
);
return showDgDialog({
modifier: "roll-stats",
content,
window: {
title: game.i18n.localize("DG.ProfessionSetup.RollStats.Title"),
},
position: { width: 520 },
form: { closeOnSubmit: false },
onRender: (dialog) => {
bindRollStatsListeners(state, dialog);
refreshRollStatsUi(state, dialog);
},
close: () => result,
buttons: [
{
action: "back",
label: game.i18n.localize("DG.ProfessionSetup.GoBack"),
callback: async (_event, _button, dialog) => {
result = { outcome: "back" };
await dialog.close();
},
},
{
action: "submit",
label: game.i18n.localize("DG.ProfessionSetup.RollStats.Submit"),
default: true,
disabled: true,
callback: async (_event, _button, dialog) => {
const isComplete = STAT_KEYS.every(
(key) => state.assignments[key] !== null,
);
if (!isComplete) return false;
/** @type {Record<string, number>} */
const valuesByKey = {};
for (const key of STAT_KEYS) {
const rollIndex = state.assignments[key];
const roll = state.rolls.find((entry) => entry.index === rollIndex);
if (!roll) return false;
valuesByKey[key] = roll.total;
}
await applyAgentStatistics(actor, valuesByKey);
result = { outcome: "submitted" };
await dialog.close();
return false;
},
},
],
});
}
@@ -0,0 +1,100 @@
import { BASE_TEMPLATE_PATH } from "../config/index.js";
import { DISORDER_OPTIONS } from "../profession/disorders.js";
import { getDialogContentRoot, showDgDialog } from "./dg-dialog.js";
const { renderTemplate } = foundry.applications.handlebars;
/**
* @typedef {object} ThingsDisorderDialogState
* @property {string} [disorder]
*/
/**
* @param {ThingsDisorderDialogState} [state]
* @returns {Promise<{ outcome: 'submitted', path: 'thingsMan', disorder: string } | { outcome: 'back' } | null>}
*/
export default async function showThingsDisorderDialog(state = {}) {
const defaultDisorder = state.disorder ?? "amnesia";
const content = await renderTemplate(
`${BASE_TEMPLATE_PATH}/dialog/things-disorder.html`,
{
disorders: DISORDER_OPTIONS.map(({ id, labelKey }) => ({
value: id,
labelKey,
checked: id === defaultDisorder,
})),
},
);
/** @type {{ outcome: 'submitted', path: 'thingsMan', disorder: string } | { outcome: 'back' } | null} */
let result = null;
await showDgDialog({
modifier: "things-disorder",
content,
window: {
title: game.i18n.localize("DG.DamagedVeterans.ThingsMan.Dialog.Title"),
},
position: { width: 560, height: "auto" },
form: { closeOnSubmit: false },
close: () => result,
onRender: (dialog) => {
const root = getDialogContentRoot(dialog);
if (!root) return;
const submitBtn = dialog.element?.querySelector(
'button[data-action="submit"]',
);
const updateSubmitState = () => {
const selected = root.querySelector(
'input[name="thingsDisorder"]:checked',
);
if (submitBtn) submitBtn.disabled = !selected;
};
root.querySelectorAll('input[name="thingsDisorder"]').forEach((input) => {
input.addEventListener("change", updateSubmitState);
});
updateSubmitState();
},
buttons: [
{
action: "back",
label: game.i18n.localize("DG.ProfessionSetup.GoBack"),
callback: async (_event, _button, dialog) => {
const root = getDialogContentRoot(dialog);
const selected = root?.querySelector(
'input[name="thingsDisorder"]:checked',
);
result = {
outcome: "back",
disorder: selected?.value,
};
await dialog.close();
return false;
},
},
{
action: "submit",
label: game.i18n.localize("DG.Profession.Dialog.Submit"),
default: true,
disabled: false,
callback: async (_event, _button, dialog) => {
const root = getDialogContentRoot(dialog);
const selected = root?.querySelector(
'input[name="thingsDisorder"]:checked',
);
if (!selected) return false;
result = {
outcome: "submitted",
path: "thingsMan",
disorder: selected.value,
};
await dialog.close();
return false;
},
},
],
});
return result;
}
+496
View File
@@ -0,0 +1,496 @@
/* global TokenDocument */
import DG from "../config/index.js";
import { getCharacterSheetThemeClass } from "../applications/dg-dialog.js";
const CHAT_CARD_TEMPLATE = "systems/deltagreen/templates/chat/dg-chat-card.hbs";
const { renderTemplate } = foundry.applications.handlebars;
/**
* Resolve a TokenDocument from sheet/macro context.
* @param {TokenDocument|object|string|null} token
* @param {Scene|SceneDocument|string|null} [scene]
* @returns {TokenDocument|null}
*/
export function resolveDGTokenDocument(token, scene = null) {
if (!token) return null;
if (token.documentName === "Token" || token instanceof TokenDocument) {
return token;
}
if (token.document?.documentName === "Token") {
return token.document;
}
const tokenId = typeof token === "string" ? token : token.id ?? token._id;
if (!tokenId) return null;
const sceneId =
typeof scene === "string"
? scene
: scene?.id ?? token.parent?.id ?? canvas.scene?.id;
const sceneDoc = sceneId ? game.scenes.get(sceneId) : canvas.scene;
return (
sceneDoc?.tokens.get(tokenId) ??
canvas.tokens.get(tokenId)?.document ??
null
);
}
/**
* Speaker for Delta Green chat; uses token name for unlinked tokens.
* @param {object} params
* @param {Actor|null} [params.actor]
* @param {TokenDocument|object|string|null} [params.token]
* @param {Scene|SceneDocument|string|null} [params.scene]
* @returns {ChatMessage.SpeakerData}
*/
export function getDGSpeaker({
actor = null,
token = null,
scene = null,
} = {}) {
const tokenDoc = resolveDGTokenDocument(token, scene);
const sceneDoc = scene ?? tokenDoc?.parent ?? canvas.scene ?? null;
const alias = tokenDoc?.name ?? actor?.name;
return ChatMessage.getSpeaker({
actor,
token: tokenDoc,
scene: sceneDoc,
alias,
});
}
/**
* Token document from an existing chat speaker payload.
* @param {ChatMessage.SpeakerData|null} speaker
* @returns {TokenDocument|null}
*/
export function getTokenFromChatSpeaker(speaker) {
if (!speaker?.token) return null;
return resolveDGTokenDocument(speaker.token, speaker.scene);
}
/**
* Best token for a roll from sheet or macro context.
* @param {Actor|null} actor
* @param {TokenDocument|object|string|null} [sheetToken]
* @returns {TokenDocument|null}
*/
export function getDGRollToken(actor, sheetToken = null) {
const fromSheet = resolveDGTokenDocument(sheetToken);
if (fromSheet) return fromSheet;
if (!actor) return null;
const controlled = canvas.tokens?.controlled?.find((t) => t.actor === actor);
if (controlled?.document) return controlled.document;
const active = actor.getActiveTokens?.(true, true)?.[0];
return active ?? null;
}
/**
* @param {ChatMessage.SpeakerData|null} speaker
* @returns {string}
*/
export function getDGChatSpeakerName(speaker) {
if (!speaker) {
return game.i18n.localize("CHAT.UnknownUser");
}
return speaker.alias ?? game.i18n.localize("CHAT.UnknownUser");
}
/**
* Portrait image for a Delta Green chat card.
* @param {object} params
* @param {Actor|null} [params.actor]
* @param {TokenDocument|object|string|null} [params.token]
* @param {ChatMessage.SpeakerData|null} [params.speaker]
* @returns {string}
*/
export function getDGChatPortraitSrc({
actor = null,
token = null,
speaker = null,
} = {}) {
const tokenDoc =
resolveDGTokenDocument(token) ?? getTokenFromChatSpeaker(speaker);
const actorDoc =
actor ?? (speaker ? ChatMessage.getSpeakerActor(speaker) : null);
return tokenDoc?.texture?.src ?? actorDoc?.img ?? "icons/svg/mystery-man.svg";
}
/**
* @param {object} params
* @param {string} [params.title]
* @param {string} [params.subtitle]
* @param {string} [params.label] Backward-compat fallback treated as title
* @param {string} [params.flavor] Foundry flavor fallback treated as title
* @param {string} [params.rollLabel] Explicit skill/target line below the card header
* @returns {{ rollLabel: string }}
*/
function normalizeChatCardHeader({
title = "",
subtitle = "",
label = "",
flavor = "",
rollLabel = "",
} = {}) {
if (rollLabel) return { rollLabel };
const resolvedTitle = title || label || flavor;
if (resolvedTitle && subtitle) {
return { rollLabel: `${resolvedTitle}: ${subtitle}` };
}
return { rollLabel: resolvedTitle };
}
/**
* @param {object} params
* @param {string} [params.title]
* @param {string} [params.subtitle]
* @param {string} [params.label]
* @param {string} [params.flavor]
* @param {string} [params.rollLabel]
* @returns {boolean}
*/
function shouldUseChatCard({
title = "",
subtitle = "",
label = "",
flavor = "",
rollLabel = "",
} = {}) {
return Boolean(
normalizeChatCardHeader({ title, subtitle, label, flavor, rollLabel })
.rollLabel,
);
}
/**
* @param {ChatMessage} message
* @param {HTMLElement} element
*/
export function enrichDGChatCardMessage(message, element) {
const card = element.querySelector(".dg-chat-card");
if (!card) return;
card.classList.remove("program-style", "cowboy-style", "outlaw-style");
card.classList.add(getCharacterSheetThemeClass());
const foundryHeader = element.querySelector(".message-header");
const header = card.querySelector(".dg-chat-card__header");
let portrait = card.querySelector(".dg-chat-card__portrait");
if (!portrait) {
portrait = card.querySelector(".dg-chat-card__icon");
if (portrait) portrait.classList.add("dg-chat-card__portrait");
}
if (portrait) {
portrait.src = getDGChatPortraitSrc({ speaker: message.speaker });
}
const speakerEl = card.querySelector(".dg-chat-card__speaker");
const authorEl = card.querySelector(".dg-chat-card__author");
const speakerName = message.alias ?? "";
const authorName = message.author?.name ?? "";
if (speakerEl) speakerEl.textContent = speakerName;
if (authorEl) {
const whisperEl = foundryHeader?.querySelector(".whisper-to");
if (whisperEl) {
authorEl.textContent = whisperEl.textContent.trim();
authorEl.hidden = false;
whisperEl.remove();
} else if (speakerName !== authorName && authorName) {
authorEl.textContent = authorName;
authorEl.hidden = false;
} else {
authorEl.hidden = true;
}
}
let metadataSlot = card.querySelector(".dg-chat-card__metadata");
if (!metadataSlot && header) {
metadataSlot = document.createElement("span");
metadataSlot.className = "dg-chat-card__metadata";
header.appendChild(metadataSlot);
}
const foundryMetadata = foundryHeader?.querySelector(".message-metadata");
if (metadataSlot && foundryMetadata) {
metadataSlot.replaceWith(foundryMetadata);
foundryMetadata.classList.add("dg-chat-card__metadata");
}
foundryHeader?.querySelector(".flavor-text")?.remove();
}
/**
* @param {ChatMessage.SpeakerData} speaker
* @param {string} [authorId]
* @returns {{ speakerName: string, authorName: string, showAuthor: boolean }}
*/
function getDGChatCardIdentity(speaker, authorId = game.user.id) {
const speakerName = getDGChatSpeakerName(speaker);
const authorName = game.users.get(authorId)?.name ?? "";
return {
speakerName,
authorName,
showAuthor: Boolean(authorName) && speakerName !== authorName,
};
}
/**
* @param {object} params
* @param {string} params.portraitSrc
* @param {string} [params.rollLabel]
* @param {string} [params.speakerName]
* @param {string} [params.authorName]
* @param {boolean} [params.showAuthor]
* @param {string} params.content
* @returns {Promise<string>}
*/
export async function renderDGChatCard({
portraitSrc,
rollLabel = "",
speakerName = "",
authorName = "",
showAuthor = false,
content,
}) {
return renderTemplate(CHAT_CARD_TEMPLATE, {
portraitSrc,
rollLabel,
speakerName,
authorName,
showAuthor,
content,
});
}
/**
* Whether HTML already includes an inline dice roll block.
* @param {string} content
* @returns {boolean}
*/
function contentIncludesRollDisplay(content) {
return typeof content === "string" && content.includes("dice-roll");
}
/**
* Render roll HTML for embedding in a chat card body.
* @param {Roll[]} rolls
* @returns {Promise<string>}
*/
async function renderRollsHTML(rolls) {
let html = "";
for (const roll of rolls) {
if (!roll._evaluated) await roll.evaluate();
html += await roll.render();
}
return html;
}
/**
* Wrap message content in the Delta Green chat card shell.
* @param {object} messageData
* @param {object} [options]
* @param {string} [options.title]
* @param {string} [options.subtitle]
* @param {string} [options.label] Backward-compat fallback treated as title
* @param {Actor|null} [options.actor]
* @param {TokenDocument|null} [options.token]
* @returns {Promise<object>}
*/
export async function wrapDGChatMessageData(
messageData,
{ title, subtitle, label, rollLabel, actor = null, token = null } = {},
) {
const header = normalizeChatCardHeader({
title,
subtitle,
label: label ?? messageData.flavor,
rollLabel,
});
if (!header.rollLabel) return messageData;
const portraitSrc = getDGChatPortraitSrc({
actor,
token,
speaker: messageData.speaker,
});
const identity = getDGChatCardIdentity(
messageData.speaker,
messageData.author,
);
const wrappedContent = await renderDGChatCard({
portraitSrc,
...header,
...identity,
content: messageData.content ?? "",
});
const data = foundry.utils.deepClone(messageData);
data.content = wrappedContent;
data.flavor = "";
foundry.utils.setProperty(data, `flags.${DG.ID}.chatCard`, true);
return data;
}
/**
* Build chat message data for a roll with optional Delta Green card wrapping.
*
* @param {object} params
* @param {Roll} params.roll
* @param {Actor|null} [params.actor]
* @param {TokenDocument|null} [params.token]
* @param {Scene|SceneDocument|null} [params.scene]
* @param {string} [params.title]
* @param {string} [params.subtitle]
* @param {string} [params.label]
* @param {string} [params.content]
* @param {string} [params.messageMode]
* @param {object} [params.flags]
* @returns {Promise<{ messageData: object, mappedMode: string }>}
*/
export async function prepareDGRollChatMessageData({
roll,
actor = null,
token = null,
scene = null,
title = "",
subtitle = "",
label = "",
rollLabel = "",
content,
messageMode,
flags = {},
}) {
const mode = messageMode ?? game.settings.get("core", "messageMode");
const mappedMode = foundry.dice.Roll._mapLegacyRollMode(mode);
const resolvedActor = actor ?? roll.actor ?? null;
const resolvedToken = token ?? roll.options?.token ?? null;
const useCard = shouldUseChatCard({ title, subtitle, label, rollLabel });
if (!roll._evaluated) {
await roll.evaluate({ allowInteractive: mappedMode !== "blind" });
}
const speaker = getDGSpeaker({
actor: resolvedActor,
token: resolvedToken,
scene,
});
let bodyContent = content ?? String(roll.total);
// Foundry only auto-injects roll HTML when content has no child elements.
// DG chat cards always wrap content in a <section>, so embed rolls in the body.
if (useCard && !contentIncludesRollDisplay(bodyContent)) {
bodyContent = `${bodyContent}${await renderRollsHTML([roll])}`;
}
let messageData = {
author: game.user.id,
speaker,
content: bodyContent,
sound: CONFIG.sounds.dice,
rolls: [roll],
flags: foundry.utils.mergeObject({ [DG.ID]: { chatCard: useCard } }, flags),
};
if (useCard) {
messageData = await wrapDGChatMessageData(messageData, {
title,
subtitle,
label,
rollLabel,
actor: resolvedActor,
token: resolvedToken,
});
}
return { messageData, mappedMode };
}
/**
* Create a roll chat message with the Delta Green card shell and an explicit rolls array.
* Matches the Dolmenwood/DSN pattern: styled content in `content`, Roll objects in `rolls`.
*
* @see https://gitlab.com/riccisi/foundryvtt-dice-so-nice/-/wikis/Integration
* @param {object} params
* @param {Roll} params.roll Evaluated or unevaluated roll to attach to the message.
* @param {Actor|null} [params.actor]
* @param {TokenDocument|null} [params.token]
* @param {Scene|SceneDocument|null} [params.scene]
* @param {string} [params.title]
* @param {string} [params.subtitle]
* @param {string} [params.label]
* @param {string} [params.content] Card body HTML
* @param {string} [params.messageMode]
* @param {object} [params.flags]
* @returns {Promise<ChatMessage>}
*/
export async function createDGRollChatMessage(params) {
const { messageData, mappedMode } =
await prepareDGRollChatMessageData(params);
const ChatMessageDocument = foundry.utils.getDocumentClass("ChatMessage");
const msg = new ChatMessageDocument(messageData);
msg.applyMode(mappedMode);
return ChatMessageDocument.create(msg);
}
/**
* @param {object} params
* @param {Actor} params.actor
* @param {TokenDocument|null} [params.token]
* @param {Scene|SceneDocument|null} [params.scene]
* @param {string} [params.title]
* @param {string} [params.subtitle]
* @param {string} [params.label] Backward-compat fallback treated as title
* @param {string} params.content Card body HTML
* @param {string} [params.messageMode]
* @param {object} [params.flags]
* @returns {Promise<ChatMessage>}
*/
export async function createDGChatMessage({
actor,
token = null,
scene = null,
title = "",
subtitle = "",
label = "",
rollLabel = "",
content,
messageMode,
flags = {},
}) {
const header = normalizeChatCardHeader({ title, subtitle, label, rollLabel });
if (!header.rollLabel) {
return ChatMessage.create({
speaker: getDGSpeaker({ actor, token, scene }),
content,
messageMode: messageMode ?? game.settings.get("core", "messageMode"),
flags,
});
}
const speaker = getDGSpeaker({ actor, token, scene });
const portraitSrc = getDGChatPortraitSrc({ actor, token, speaker });
const identity = getDGChatCardIdentity(speaker);
const wrappedContent = await renderDGChatCard({
portraitSrc,
...header,
...identity,
content,
});
return ChatMessage.create({
speaker,
content: wrappedContent,
messageMode: messageMode ?? game.settings.get("core", "messageMode"),
flags: foundry.utils.mergeObject({ [DG.ID]: { chatCard: true } }, flags),
});
}
+72
View File
@@ -0,0 +1,72 @@
import { createDGRollChatMessage } from "./dg-chat-card.js";
/**
* Underlined "{amount} Willpower" span with a current/max tooltip for chat roll labels.
*
* @param {object} params
* @param {number} params.amount Willpower lost or regained.
* @param {number} params.current
* @param {number} params.max
* @returns {string}
*/
export function buildWillpowerChangeSpan({ amount, current, max }) {
const willpower = game.i18n.localize("DG.Physical.Chat.Willpower");
const tooltip = game.i18n.format("DG.Physical.Chat.WillpowerTooltip", {
current,
max,
});
const label = game.i18n.format("DG.Physical.Chat.WillpowerAmount", {
amount,
willpower,
});
return `<span class="dg-chat-card__wp-change" data-tooltip-html="${foundry.utils.escapeHTML(
tooltip,
)}"><u>${foundry.utils.escapeHTML(label)}</u></span>`;
}
/**
* @param {object} params
* @param {Actor} params.actor
* @param {TokenDocument|null} [params.token]
* @param {string} [params.contentKey] i18n key for message body.
* @param {string} [params.labelKey] i18n key for the roll-label row when rollLabelKey is omitted.
* @param {string} [params.rollLabelKey] i18n key for a formatted roll-label row (HTML).
* @param {Record<string, string|number>} params.i18nData
* @param {Roll} params.roll Roll attached to the chat message for display and Dice So Nice.
* @param {string|null} [params.extraContentKey] Optional i18n key appended on a new line after the main body.
* @returns {Promise<ChatMessage>}
*/
export default async function createAgentResourceChatMessage({
actor,
token = null,
contentKey = null,
labelKey = "",
rollLabelKey = null,
i18nData,
roll,
extraContentKey = null,
}) {
let content = contentKey ? game.i18n.format(contentKey, i18nData) : "";
if (extraContentKey) {
const extra = game.i18n.format(extraContentKey, i18nData);
content = content ? `${content}<br>${extra}` : extra;
}
const rollLabel = rollLabelKey
? game.i18n.format(rollLabelKey, i18nData)
: labelKey
? game.i18n.localize(labelKey)
: "";
const messageMode = game.settings.get("core", "messageMode");
return createDGRollChatMessage({
actor,
token,
roll,
rollLabel,
content,
messageMode,
});
}
@@ -47,7 +47,6 @@ const DG = /** @type {const} */ ({
"swim",
"unarmed_combat",
"unnatural",
"ritual",
],
skillImprovementFormulas: {
+61
View File
@@ -0,0 +1,61 @@
/**
* Rich-text field specs keyed by actor sheet part id.
* @type {Record<string, Record<string, { path: string, key: string }[]>>}
*/
const ACTOR_RICH_TEXT_BY_PART = {
agent: {
rightBar: [{ path: "physical.description", key: "description" }],
},
npc: {
skills: [{ path: "notes", key: "description" }],
},
unnatural: {
skills: [{ path: "notes", key: "description" }],
},
vehicle: {
notes: [{ path: "description", key: "description" }],
},
};
/**
* Rich-text field specs keyed by item sheet part id.
* @type {Record<string, { path: string, key: string, types?: string[] }[]>}
*/
const ITEM_RICH_TEXT_BY_PART = {
description: [{ path: "description", key: "description" }],
handler: [
{
path: "handlerNotes",
key: "handlerNotes",
types: ["tome", "ritual"],
},
],
};
/**
* @param {"Actor"|"Item"} documentName
* @param {string} partId
* @param {object} [options]
* @param {string} [options.actorType]
* @param {string} [options.itemType]
* @param {boolean} [options.showNotesInSkills]
* @returns {{ path: string, key: string }[]}
*/
export default function getRichTextFieldsForPart(
documentName,
partId,
{ actorType, itemType, showNotesInSkills = true } = {},
) {
if (documentName === "Actor") {
if (partId === "skills" && !showNotesInSkills) return [];
return ACTOR_RICH_TEXT_BY_PART[actorType]?.[partId] ?? [];
}
if (documentName === "Item") {
return (ITEM_RICH_TEXT_BY_PART[partId] ?? []).filter(
(spec) => !spec.types || spec.types.includes(itemType),
);
}
return [];
}
+56 -10
View File
@@ -1,20 +1,35 @@
import HumanSkillsActorData from "./base/human-skills.js";
import CharacterData from "./base/character.js";
import DGHTMLField from "../fields/html-content-field.js";
import { agentResourceField } from "./base/general.js";
import {
computeEquippedArmorProtection,
initializeSanityIfUnset,
prepareSanityAdaptations,
prepareStatisticsX5,
removeLegacyRitualSkill,
} from "../derived/actor-derived.js";
const { SchemaField, NumberField, StringField, BooleanField, HTMLField } =
const { SchemaField, NumberField, StringField, BooleanField } =
foundry.data.fields;
export default class AgentData extends CharacterData {
static defineSchema() {
const superData = super.defineSchema();
const { health: _health, wp: _wp, ...restSuper } = superData;
return {
...superData,
...restSuper,
health: agentResourceField(10, 10),
wp: agentResourceField(10, 10),
...HumanSkillsActorData.defineSchema(),
schemaVersion: new NumberField({ initial: 1.0 }),
schemaVersion: new NumberField({ initial: 2.0 }),
sanity: new SchemaField({
value: new NumberField({ initial: 100 }),
currentBreakingPoint: new NumberField({ initial: 101 }),
value: new NumberField({ initial: 100, min: 0, integer: true }),
max: new NumberField({ initial: 99, min: 0, integer: true }),
currentBreakingPoint: new NumberField({ initial: 101, integer: true }),
maxBonus: new NumberField({ initial: 0, integer: true }),
adaptations: new SchemaField({
violence: new SchemaField({
incident1: new BooleanField({ initial: false }),
@@ -28,16 +43,18 @@ export default class AgentData extends CharacterData {
}),
}),
}),
rollTarget: new SchemaField({
allSkills: new NumberField({ initial: 0, integer: true }),
sanity: new NumberField({ initial: 0, integer: true }),
statistics: new NumberField({ initial: 0, integer: true }),
}),
physical: new SchemaField({
description: new HTMLField({
initial: "",
blank: true,
textSearch: true,
}),
description: DGHTMLField(),
wounds: new StringField({ initial: "" }),
firstAidAttempted: new BooleanField({ initial: false }),
exhausted: new BooleanField({ initial: false }),
exhaustedPenalty: new NumberField({ initial: -20 }),
suppressExhaustion: new BooleanField({ initial: false }),
}),
biography: new SchemaField({
profession: new StringField({ initial: "" }),
@@ -55,4 +72,33 @@ export default class AgentData extends CharacterData {
}),
};
}
/** @inheritdoc */
prepareBaseData() {
// rollTarget is modified by add-type Active Effects each prepare cycle.
this.rollTarget.allSkills = 0;
this.rollTarget.sanity = 0;
this.rollTarget.statistics = 0;
}
/** @inheritdoc */
prepareDerivedData() {
// Pre–final-AE setup only. Post-AE derived fields (max, x5 refresh,
// breaking point, etc.) are applied in DeltaGreenActor.prepareData via
// refreshDerivedAfterActiveEffects after the final Active Effect phase.
prepareStatisticsX5(
this.statistics,
this.parent?._source?.system?.statistics,
);
removeLegacyRitualSkill(this);
initializeSanityIfUnset(this.sanity, this.statistics);
prepareSanityAdaptations(this.sanity.adaptations);
this.health.protection = computeEquippedArmorProtection(this.parent.items);
if (this.physical.exhaustedPenalty > 0) {
this.physical.exhaustedPenalty =
-1 * Math.abs(this.physical.exhaustedPenalty);
}
}
}
@@ -0,0 +1,41 @@
/** Default proficiency for fixed agent skills (single source of truth for schema initials). */
const AGENT_SKILL_DEFAULTS = /** @type {Record<string, number>} */ ({
accounting: 10,
alertness: 20,
anthropology: 0,
archeology: 0,
artillery: 0,
athletics: 30,
bureaucracy: 10,
computer_science: 0,
criminology: 10,
demolitions: 0,
disguise: 10,
dodge: 30,
drive: 20,
firearms: 20,
first_aid: 10,
forensics: 0,
heavy_machiner: 10,
heavy_weapons: 0,
history: 10,
humint: 10,
law: 0,
medicine: 0,
melee_weapons: 30,
navigate: 10,
occult: 10,
persuade: 20,
pharmacy: 0,
psychotherapy: 10,
ride: 10,
search: 20,
sigint: 0,
stealth: 10,
surgery: 0,
survival: 10,
swim: 20,
unarmed_combat: 40,
});
export default AGENT_SKILL_DEFAULTS;
+21 -5
View File
@@ -3,7 +3,12 @@ const { SchemaField, NumberField, StringField, BooleanField } =
export function skillField(initialValue, label) {
return new SchemaField({
proficiency: new NumberField({ initial: initialValue }),
proficiency: new NumberField({
initial: initialValue,
min: 0,
max: 99,
integer: true,
}),
label: new StringField({ initial: label }),
failure: new BooleanField({ initial: false }),
});
@@ -11,15 +16,26 @@ export function skillField(initialValue, label) {
export function resourceField(initialValue, initialMax) {
return new SchemaField({
min: new NumberField({ initial: 0 }),
value: new NumberField({ initial: initialValue }),
max: new NumberField({ initial: initialMax }),
min: new NumberField({ initial: 0, min: 0, integer: true }),
value: new NumberField({ initial: initialValue, min: 0, integer: true }),
max: new NumberField({ initial: initialMax, min: 0, integer: true }),
});
}
/** Agent resources that support Active Effect maximum bonuses. */
export function agentResourceField(initialValue, initialMax) {
return new SchemaField({
min: new NumberField({ initial: 0, min: 0, integer: true }),
value: new NumberField({ initial: initialValue, min: 0, integer: true }),
max: new NumberField({ initial: initialMax, min: 0, integer: true }),
maxBonus: new NumberField({ initial: 0, integer: true }),
});
}
export function statisticsField(initialValue) {
return new SchemaField({
value: new NumberField({ initial: initialValue }),
value: new NumberField({ initial: initialValue, min: 0, integer: true }),
modifier: new NumberField({ initial: 0, integer: true }),
distinguishing_feature: new StringField({ initial: "" }),
});
}
+44 -37
View File
@@ -1,3 +1,4 @@
import AGENT_SKILL_DEFAULTS from "./agent-skill-defaults.js";
import { skillField } from "./general.js";
const { SchemaField, NumberField, StringField, ArrayField, ObjectField } =
@@ -6,46 +7,52 @@ const { TypeDataModel } = foundry.abstract;
export default class HumanSkillsActorData extends TypeDataModel {
static defineSchema() {
const d = AGENT_SKILL_DEFAULTS;
return {
skills: new SchemaField({
accounting: skillField(10, "Accounting"),
alertness: skillField(20, "Alertness"),
anthropology: skillField(0, "Anthropology"),
archeology: skillField(0, "Archeology"),
artillery: skillField(0, "Artillery"),
athletics: skillField(30, "Athletics"),
bureaucracy: skillField(10, "Bureaucracy"),
computer_science: skillField(0, "Computer Science"),
criminology: skillField(10, "Criminology"),
demolitions: skillField(0, "Demolitions"),
disguise: skillField(10, "Disguise"),
dodge: skillField(30, "Dodge"),
drive: skillField(20, "Drive"),
firearms: skillField(20, "Firearms"),
first_aid: skillField(10, "First Aid"),
forensics: skillField(0, "Forensics"),
heavy_machiner: skillField(10, "Heavy Machinery"),
heavy_weapons: skillField(0, "Heavy Weapons"),
history: skillField(10, "History"),
humint: skillField(10, "HUMINT"),
law: skillField(0, "Law"),
medicine: skillField(0, "Medicine"),
melee_weapons: skillField(30, "Melee Weapons"),
navigate: skillField(10, "Navigate"),
occult: skillField(10, "Occult"),
persuade: skillField(20, "Persuade"),
pharmacy: skillField(0, "Pharmacy"),
psychotherapy: skillField(10, "Psychotherapy"),
ride: skillField(10, "Ride"),
search: skillField(20, "Search"),
sigint: skillField(0, "SIGINT"),
stealth: skillField(10, "Stealth"),
surgery: skillField(0, "Surgery"),
survival: skillField(10, "Survival"),
swim: skillField(20, "Swim"),
unarmed_combat: skillField(40, "Unarmed Combat"),
accounting: skillField(d.accounting, "Accounting"),
alertness: skillField(d.alertness, "Alertness"),
anthropology: skillField(d.anthropology, "Anthropology"),
archeology: skillField(d.archeology, "Archeology"),
artillery: skillField(d.artillery, "Artillery"),
athletics: skillField(d.athletics, "Athletics"),
bureaucracy: skillField(d.bureaucracy, "Bureaucracy"),
computer_science: skillField(d.computer_science, "Computer Science"),
criminology: skillField(d.criminology, "Criminology"),
demolitions: skillField(d.demolitions, "Demolitions"),
disguise: skillField(d.disguise, "Disguise"),
dodge: skillField(d.dodge, "Dodge"),
drive: skillField(d.drive, "Drive"),
firearms: skillField(d.firearms, "Firearms"),
first_aid: skillField(d.first_aid, "First Aid"),
forensics: skillField(d.forensics, "Forensics"),
heavy_machiner: skillField(d.heavy_machiner, "Heavy Machinery"),
heavy_weapons: skillField(d.heavy_weapons, "Heavy Weapons"),
history: skillField(d.history, "History"),
humint: skillField(d.humint, "HUMINT"),
law: skillField(d.law, "Law"),
medicine: skillField(d.medicine, "Medicine"),
melee_weapons: skillField(d.melee_weapons, "Melee Weapons"),
navigate: skillField(d.navigate, "Navigate"),
occult: skillField(d.occult, "Occult"),
persuade: skillField(d.persuade, "Persuade"),
pharmacy: skillField(d.pharmacy, "Pharmacy"),
psychotherapy: skillField(d.psychotherapy, "Psychotherapy"),
ride: skillField(d.ride, "Ride"),
search: skillField(d.search, "Search"),
sigint: skillField(d.sigint, "SIGINT"),
stealth: skillField(d.stealth, "Stealth"),
surgery: skillField(d.surgery, "Surgery"),
survival: skillField(d.survival, "Survival"),
swim: skillField(d.swim, "Swim"),
unarmed_combat: skillField(d.unarmed_combat, "Unarmed Combat"),
unnatural: new SchemaField({
proficiency: new NumberField({ initial: 0 }),
proficiency: new NumberField({
initial: 0,
min: 0,
max: 99,
integer: true,
}),
label: new StringField({ initial: "Unnatural" }),
}),
}),
+48 -5
View File
@@ -1,7 +1,19 @@
import HumanSkillsActorData from "./base/human-skills.js";
import CharacterData from "./base/character.js";
import DGHTMLField from "../fields/html-content-field.js";
import {
calculateHealthMax,
calculateMeleeDamageBonusFormula,
clampSanityRitualValue,
cleanDerivedNumber,
computeEquippedArmorProtection,
initializeSanityIfUnset,
prepareStatisticsX5,
removeLegacyRitualSkill,
setSkillTargetProficiencies,
} from "../derived/actor-derived.js";
const { SchemaField, NumberField, StringField, BooleanField, HTMLField } =
const { SchemaField, NumberField, StringField, BooleanField } =
foundry.data.fields;
export default class NPCData extends CharacterData {
@@ -11,17 +23,48 @@ export default class NPCData extends CharacterData {
return {
...superData,
...HumanSkillsActorData.defineSchema(),
schemaVersion: new NumberField({ initial: 1.0 }),
schemaVersion: new NumberField({ initial: 2.0 }),
sanity: new SchemaField({
value: new NumberField({ initial: 100 }),
currentBreakingPoint: new NumberField({ initial: 101 }),
value: new NumberField({ initial: 100, min: 0, integer: true }),
max: new NumberField({ initial: 99, min: 0, integer: true }),
currentBreakingPoint: new NumberField({ initial: 101, integer: true }),
}),
biography: new SchemaField({
profession: new StringField({ initial: "" }),
}),
notes: new HTMLField({ initial: "", blank: true, textSearch: true }),
notes: DGHTMLField(),
shortDescription: new StringField({ initial: "" }),
showUntrainedSkills: new BooleanField({ initial: true }),
};
}
/** @inheritdoc */
prepareDerivedData() {
const sourceStatistics = this.parent?._source?.system?.statistics;
prepareStatisticsX5(this.statistics, sourceStatistics);
initializeSanityIfUnset(this.sanity, this.statistics);
this.sanity.max = cleanDerivedNumber(
this,
"sanity.max",
99 - this.skills.unnatural.proficiency,
);
this.wp.max = cleanDerivedNumber(
this,
"wp.max",
this.statistics.pow.effectiveValue ?? this.statistics.pow.value,
);
this.health.max = cleanDerivedNumber(
this,
"health.max",
calculateHealthMax(this.statistics, sourceStatistics),
);
this.statistics.str.meleeDamageBonusFormula =
calculateMeleeDamageBonusFormula(
this.statistics.str.effectiveValue ?? this.statistics.str.value,
);
removeLegacyRitualSkill(this);
clampSanityRitualValue(this.sanity);
this.health.protection = computeEquippedArmorProtection(this.parent.items);
setSkillTargetProficiencies(this.skills);
}
}
+38 -8
View File
@@ -1,7 +1,16 @@
import UnnaturalSkillsActorData from "./base/unnatural-skills.js";
import CharacterData from "./base/character.js";
import DGHTMLField from "../fields/html-content-field.js";
import {
calculateHealthMax,
cleanDerivedNumber,
computeEquippedArmorProtection,
prepareStatisticsX5,
removeLegacyRitualSkill,
setSkillTargetProficiencies,
} from "../derived/actor-derived.js";
const { SchemaField, NumberField, StringField, BooleanField, HTMLField } =
const { SchemaField, NumberField, StringField, BooleanField } =
foundry.data.fields;
export default class UnnaturalData extends CharacterData {
@@ -16,15 +25,36 @@ export default class UnnaturalData extends CharacterData {
failedLoss: new StringField({ initial: "1D4" }),
successLoss: new StringField({ initial: "1" }),
}),
schemaVersion: new NumberField({ initial: 1.0 }),
notes: new HTMLField({
initial: "",
blank: true,
textSearch: true,
allowColor: true,
}),
schemaVersion: new NumberField({ initial: 2.0 }),
notes: DGHTMLField(),
shortDescription: new StringField({ initial: "" }),
showUntrainedSkills: new BooleanField({ initial: true }),
};
}
/** @inheritdoc */
prepareDerivedData() {
const sourceStatistics = this.parent?._source?.system?.statistics;
prepareStatisticsX5(this.statistics, sourceStatistics);
if (this.wp.maxNeedsUpdate) {
this.wp.max = cleanDerivedNumber(
this,
"wp.max",
this.statistics.pow.effectiveValue ?? this.statistics.pow.value,
);
}
if (this.health.maxNeedsUpdate) {
this.health.max = cleanDerivedNumber(
this,
"health.max",
calculateHealthMax(this.statistics, sourceStatistics),
);
}
removeLegacyRitualSkill(this);
this.health.protection = computeEquippedArmorProtection(this.parent.items);
setSkillTargetProficiencies(this.skills);
}
}
+9 -6
View File
@@ -1,6 +1,8 @@
import SheetSettingsActorData from "./base/sheet-settings.js";
import DGHTMLField from "../fields/html-content-field.js";
import { computeEquippedArmorProtection } from "../derived/actor-derived.js";
const { SchemaField, NumberField, StringField, ArrayField, HTMLField } =
const { SchemaField, NumberField, StringField, ArrayField } =
foundry.data.fields;
export default class VehicleData extends foundry.abstract.TypeDataModel {
@@ -8,11 +10,7 @@ export default class VehicleData extends foundry.abstract.TypeDataModel {
return {
...SheetSettingsActorData.defineSchema(),
name: new StringField({ initial: "" }),
description: new HTMLField({
initial: "",
blank: true,
textSearch: true,
}),
description: DGHTMLField(),
shortDescription: new StringField({ initial: "" }),
health: new SchemaField({
value: new NumberField({ initial: 10 }),
@@ -24,4 +22,9 @@ export default class VehicleData extends foundry.abstract.TypeDataModel {
passengers: new ArrayField(new StringField()),
};
}
/** @inheritdoc */
prepareDerivedData() {
this.health.protection = computeEquippedArmorProtection(this.parent.items);
}
}
+297
View File
@@ -0,0 +1,297 @@
/**
* Shared derived-data helpers for actor TypeDataModels.
*
* Active Effect validation layers (do not duplicate clamp logic elsewhere):
* - Schema `NumberField` min/max: enforced when values pass through `field.clean()`
* (document updates, Active Effect `applyChange` on registered paths).
* - `cleanDerivedNumber`: use when assigning derived fields in code (e.g. `health.max`,
* `sanity.max`); plain assignment bypasses schema cleaners unless the path has a NumberField.
* - `clampStatisticValue`: effective ratings after statistic modifier AEs (min 0).
* - `applyAgentResourceMaxBonuses`: recompute resource max after AE;
* agents call these only from `refreshDerivedAfterActiveEffects` (post–final phase).
*/
/**
* @param {Item[]} items
* @returns {number}
*/
export function computeEquippedArmorProtection(items) {
let protection = 0;
for (const item of items) {
if (item.type === "armor" && item.system.equipped === true) {
protection += item.system.protection;
}
}
return protection;
}
/**
* @param {number} strengthValue
* @returns {string}
*/
export function calculateMeleeDamageBonusFormula(strengthValue) {
if (strengthValue < 5) return "-2";
if (strengthValue < 9) return "-1";
if (strengthValue > 12 && strengthValue < 17) return "+1";
if (strengthValue > 16) return "+2";
return "";
}
/**
* @param {number} value
* @returns {number}
*/
function clampStatisticValue(value) {
const numeric = Number(value);
if (!Number.isFinite(numeric)) return 0;
return Math.max(0, Math.round(numeric));
}
/**
* Effective statistic rating: persisted value plus AE modifier (minimum 0).
* @param {object} stat Prepared statistic.
* @param {object} [sourceStat] Persisted statistic from `_source`.
* @returns {number}
*/
export function getStatisticEffectiveValue(stat, sourceStat) {
const base = clampStatisticValue(sourceStat?.value ?? stat?.value);
const modifier = Number(stat?.modifier) || 0;
return clampStatisticValue(base + modifier);
}
/**
* @param {object} statistics
* @param {object} [sourceStatistics] Persisted statistics from `_source`.
* @returns {void}
*/
export function prepareStatisticsX5(statistics, sourceStatistics) {
for (const [key, statistic] of Object.entries(statistics ?? {})) {
statistic.effectiveValue = getStatisticEffectiveValue(
statistic,
sourceStatistics?.[key],
);
statistic.x5 = statistic.effectiveValue * 5;
}
}
/**
* Clamp a derived numeric value through the TypeDataModel field definition when available.
* @param {foundry.abstract.TypeDataModel} model
* @param {string} path Dot-delimited path (e.g. "health.max")
* @param {number} raw
* @returns {number}
*/
export function cleanDerivedNumber(model, path, raw) {
const field = model.getFieldForProperty?.(path);
const numeric = Number(raw);
if (!field) {
return Number.isFinite(numeric) ? Math.max(0, Math.round(numeric)) : 0;
}
return field.clean(numeric, { persisted: false });
}
/**
* @param {object} statistics
* @returns {number}
*/
export function calculateHealthMax(statistics, sourceStatistics) {
try {
return Math.ceil(
(getStatisticEffectiveValue(statistics.con, sourceStatistics?.con) +
getStatisticEffectiveValue(statistics.str, sourceStatistics?.str)) /
2,
);
} catch {
return 10;
}
}
/**
* @param {foundry.abstract.TypeDataModel} model Agent system TypeDataModel.
* @returns {void}
*/
export function applyAgentResourceMaxBonuses(model, sourceStatistics) {
const sourceStats = sourceStatistics ?? model._source?.statistics;
model.health.max = cleanDerivedNumber(
model,
"health.max",
calculateHealthMax(model.statistics, sourceStats) +
(model.health.maxBonus ?? 0),
);
model.wp.max = cleanDerivedNumber(
model,
"wp.max",
getStatisticEffectiveValue(model.statistics.pow, sourceStats?.pow) +
(model.wp.maxBonus ?? 0),
);
model.sanity.max = cleanDerivedNumber(
model,
"sanity.max",
99 - model.skills.unnatural.proficiency + (model.sanity.maxBonus ?? 0),
);
}
/**
* Compute agent HP/WP maximums from current statistics (including AE bonuses on max).
* @param {Actor} actor
* @returns {{ healthMax: number, wpMax: number }}
*/
export function computeAgentResourceMaximums(actor) {
const { system } = actor;
const sourceStatistics = actor._source?.system?.statistics;
const healthMax = cleanDerivedNumber(
system,
"health.max",
calculateHealthMax(system.statistics, sourceStatistics) +
(system.health.maxBonus ?? 0),
);
const wpMax = cleanDerivedNumber(
system,
"wp.max",
getStatisticEffectiveValue(system.statistics.pow, sourceStatistics?.pow) +
(system.wp.maxBonus ?? 0),
);
return { healthMax, wpMax };
}
/**
* @param {Actor} actor
* @param {string} veteranPath
* @returns {number}
*/
export function computeAgentBreakingPoint(actor, veteranPath) {
const sourceStatistics = actor._source?.system?.statistics;
const pow = getStatisticEffectiveValue(
actor.system.statistics.pow,
sourceStatistics?.pow,
);
if (veteranPath === "thingsMan") {
const sanity = Number(actor.system.sanity.value) || 0;
return Math.max(0, sanity - pow);
}
const fullStartingSan = pow * 5;
return Math.max(0, fullStartingSan - pow);
}
/**
* Persist agent HP/WP max from current statistics, fill both resources to max,
* initialize SAN for fresh recruits, and set breaking point after chargen.
* @param {Actor} actor
* @param {string} [veteranPath]
* @returns {Promise<void>}
*/
export async function finalizeAgentStartingResources(
actor,
veteranPath = "freshRecruit",
) {
if (actor.type !== "agent") return;
const { healthMax, wpMax } = computeAgentResourceMaximums(actor);
const breakingPoint = computeAgentBreakingPoint(actor, veteranPath);
/** @type {Record<string, unknown>} */
const updateData = {
"system.health.max": healthMax,
"system.health.value": healthMax,
"system.wp.max": wpMax,
"system.wp.value": wpMax,
"system.sanity.currentBreakingPoint": breakingPoint,
};
const persistedSanity = Number(actor._source?.system?.sanity?.value) ?? 0;
if (veteranPath === "freshRecruit" && persistedSanity >= 100) {
const sourceStatistics = actor._source?.system?.statistics;
const pow = getStatisticEffectiveValue(
actor.system.statistics.pow,
sourceStatistics?.pow,
);
updateData["system.sanity.value"] = pow * 5;
}
await actor.update(updateData);
}
export function setSkillTargetProficiencies(skills) {
for (const skill of Object.values(skills)) {
skill.targetProficiency = skill.proficiency;
}
}
/**
* @param {object} sanity
* @param {object} statistics
* @returns {void}
*/
export function initializeSanityIfUnset(sanity, statistics) {
if (sanity.value >= 100) {
sanity.value = statistics.pow.x5;
sanity.currentBreakingPoint =
sanity.value - (statistics.pow.effectiveValue ?? statistics.pow.value);
}
}
/**
* @param {object} sanity
* @returns {void}
*/
export function clampSanityRitualValue(sanity) {
sanity.ritual = 99 - sanity.value;
if (sanity.ritual > 99) {
sanity.ritual = 99;
} else if (sanity.ritual < 1) {
sanity.ritual = 1;
}
}
/**
* @param {object} adaptations
* @returns {void}
*/
export function prepareSanityAdaptations(adaptations) {
adaptations.violence.isAdapted =
adaptations.violence.incident1 &&
adaptations.violence.incident2 &&
adaptations.violence.incident3;
adaptations.helplessness.isAdapted =
adaptations.helplessness.incident1 &&
adaptations.helplessness.incident2 &&
adaptations.helplessness.incident3;
}
/**
* @param {object} sanity
* @returns {void}
*/
export function prepareBreakingPointHit(sanity) {
sanity.breakingPointHit = sanity.value <= sanity.currentBreakingPoint;
}
/**
* @param {object} skills
* @returns {void}
*/
export function prepareAgentSkillFlags(skills) {
for (const [key, skill] of Object.entries(skills)) {
skill.cannotBeImprovedByFailure =
key === "unnatural" || key === "luck" || key === "ritual";
skill.isCalculatedValue = key === "ritual";
}
}
/**
* @param {object} system
* @returns {void}
*/
export function removeLegacyRitualSkill(system) {
try {
delete system.skills.ritual;
} catch {
// Legacy field may be absent or non-configurable.
}
}
+15
View File
@@ -0,0 +1,15 @@
const { HTMLField } = foundry.data.fields;
/**
* Standard Delta Green HTML / ProseMirror field defaults.
* @param {Partial<foundry.data.fields.StringFieldOptions>} [options]
* @returns {foundry.data.fields.HTMLField}
*/
export default function DGHTMLField(options = {}) {
return new HTMLField({
required: false,
blank: true,
initial: "",
...options,
});
}
+2 -2
View File
@@ -1,4 +1,4 @@
const { HTMLField } = foundry.data.fields;
import DGHTMLField from "../fields/html-content-field.js";
/**
* Shared system fields for all Delta Green item types (from legacy template base).
@@ -6,6 +6,6 @@ const { HTMLField } = foundry.data.fields;
*/
export default function defineBaseItemSystemFields() {
return {
description: new HTMLField({ initial: "", blank: true, textSearch: true }),
description: DGHTMLField(),
};
}
+1
View File
@@ -9,6 +9,7 @@ export default class MotivationItemData extends foundry.abstract.TypeDataModel {
disorder: new StringField({ initial: "" }),
crossedOut: new BooleanField({ initial: false }),
disorderCured: new BooleanField({ initial: false }),
acuteEpisode: new BooleanField({ initial: false }),
};
}
}
+32
View File
@@ -0,0 +1,32 @@
import defineBaseItemSystemFields from "./base-fields.js";
const { NumberField, ObjectField } = foundry.data.fields;
/** Reserved key inside {@link ProfessionItemData.optionSkills}. */
export const PROFESSION_OPTION_PICKS_KEY = "optionPicks";
export default class ProfessionItemData extends foundry.abstract.TypeDataModel {
/** @inheritdoc */
static migrateData(source, options, _state) {
if ("bonds" in source) {
const bonds = Number(source.bonds);
if (!Number.isFinite(bonds) || bonds < 1) source.bonds = 1;
}
return super.migrateData(source, options, _state);
}
static defineSchema() {
return {
...defineBaseItemSystemFields(),
bonds: new NumberField({ initial: 1, min: 1, max: 5, integer: true }),
automaticSkills: new ObjectField({ initial: {} }),
automaticSkillMeta: new ObjectField({ initial: {} }),
optionSkills: new ObjectField({
initial: { [PROFESSION_OPTION_PICKS_KEY]: 0 },
}),
optionSkillMeta: new ObjectField({ initial: {} }),
};
}
}
/** @typedef {{ chooseOne?: boolean }} ProfessionSkillMeta */
+3 -6
View File
@@ -1,6 +1,7 @@
import defineBaseItemSystemFields from "./base-fields.js";
import DGHTMLField from "../fields/html-content-field.js";
const { NumberField, StringField, BooleanField, SchemaField, HTMLField } =
const { NumberField, StringField, BooleanField, SchemaField } =
foundry.data.fields;
export default class RitualItemData extends foundry.abstract.TypeDataModel {
@@ -22,11 +23,7 @@ export default class RitualItemData extends foundry.abstract.TypeDataModel {
activationCosts: new StringField({ initial: "" }),
activationTime: new StringField({ initial: "" }),
complexity: new StringField({ initial: "Simple" }),
handlerNotes: new HTMLField({
initial: "",
blank: true,
textSearch: true,
}),
handlerNotes: DGHTMLField(),
revealed: new BooleanField({ initial: false }),
};
}
+3 -6
View File
@@ -1,6 +1,7 @@
import defineBaseItemSystemFields from "./base-fields.js";
import DGHTMLField from "../fields/html-content-field.js";
const { NumberField, StringField, BooleanField, SchemaField, HTMLField } =
const { NumberField, StringField, BooleanField, SchemaField } =
foundry.data.fields;
export default class TomeItemData extends foundry.abstract.TypeDataModel {
@@ -16,11 +17,7 @@ export default class TomeItemData extends foundry.abstract.TypeDataModel {
failedLoss: new StringField({ initial: "1D6" }),
successLoss: new StringField({ initial: "1D4" }),
}),
handlerNotes: new HTMLField({
initial: "",
blank: true,
textSearch: true,
}),
handlerNotes: DGHTMLField(),
revealed: new BooleanField({ initial: false }),
};
}
+136 -35
View File
@@ -1,26 +1,33 @@
// Import Modules
import DG from "./config.js";
import DG from "./config/index.js";
import DeltaGreenActor from "./actor/actor.js";
import DGAgentSheet from "./sheets/agent-sheet.js";
import DeltaGreenItem from "./item/item.js";
import DeltaGreenItemSheet from "./item/item-sheet.js";
import * as DGRolls from "./roll/roll.js";
import DGItemSheet from "./sheets/base-item-sheet.js";
import {
DGRoll,
DGPercentileRoll,
DGLethalityRoll,
DGDamageRoll,
DGSanityDamageRoll,
} from "./roll/roll.js";
import registerSystemSettings from "./settings.js";
import preloadHandlebarsTemplates from "./templates.js";
import registerHandlebarsHelpers from "./other/register-helpers.js";
import ParseDeltaGreenStatBlock from "./other/stat-parser-macro.js";
import registerHandlebarsHelpers from "./utils/register-helpers.js";
import ParseDeltaGreenStatBlock from "./macros/stat-parser-macro.js";
import {
createDeltaGreenMacro,
rollItemMacro,
rollItemSkillCheckMacro,
rollSkillMacro,
rollSkillTestAndDamageForOwnedItem,
} from "./other/macro-functions.js";
import { handleInlineActions } from "./other/inline.js";
} from "./macros/macro-functions.js";
import { handleInlineActions } from "./chat/inline.js";
import { enrichDGChatCardMessage } from "./chat/dg-chat-card.js";
import runWorldMigration from "./utils/world-migration.js";
import DGNPCSheet from "./sheets/npc-sheet.js";
import DGUnnaturalSheet from "./sheets/unnatural-sheet.js";
import DGVehicleSheet from "./sheets/vehicle-sheet.js";
import DGAgentSheetV2 from "./sheets/agent-sheet-v2.js";
import AgentData from "./data/actor/agent.js";
import UnnaturalData from "./data/actor/unnatural.js";
import NPCData from "./data/actor/npc.js";
@@ -32,8 +39,18 @@ import BondItemData from "./data/item/bond.js";
import GearItemData from "./data/item/gear.js";
import TomeItemData from "./data/item/tome.js";
import RitualItemData from "./data/item/ritual.js";
import ProfessionItemData from "./data/item/profession.js";
import DGActiveEffect from "./active-effect/documents/dg-active-effect.js";
import DGActiveEffectConfig from "./applications/dg-active-effect-config.js";
import DGActiveEffectTypeDataModel from "./active-effect/data/dg-active-effect-data.js";
import { syncExhaustionEffect } from "./active-effect/runtime/exhaustion-effect.js";
import {
pruneAllAgentsExpiredStimulants,
pruneExpiredStimulantEffects,
} from "./active-effect/runtime/stimulant-effect.js";
const { Actors, Items } = foundry.documents.collections;
const { DocumentSheetConfig } = foundry.applications.apps;
Hooks.once("init", async () => {
Object.assign(CONFIG.Actor.dataModels, {
@@ -52,11 +69,26 @@ Hooks.once("init", async () => {
gear: GearItemData,
tome: TomeItemData,
ritual: RitualItemData,
profession: ProfessionItemData,
});
CONFIG.ActiveEffect.documentClass = DGActiveEffect;
CONFIG.ActiveEffect.dataModels.base = DGActiveEffectTypeDataModel;
DocumentSheetConfig.registerSheet(
DGActiveEffect,
DG.ID,
DGActiveEffectConfig,
{
makeDefault: true,
label: "DG.ActiveEffects.SheetLabel",
},
);
game.deltagreen = {
DeltaGreenActor,
DeltaGreenItem,
DGActiveEffect,
rollItemMacro,
rollItemSkillCheckMacro,
rollSkillMacro,
@@ -74,7 +106,13 @@ Hooks.once("init", async () => {
};
// Register custom dice rolls
Object.values(DGRolls).forEach((cls) => CONFIG.Dice.rolls.push(cls));
[
DGRoll,
DGPercentileRoll,
DGLethalityRoll,
DGDamageRoll,
DGSanityDamageRoll,
].forEach((cls) => CONFIG.Dice.rolls.push(cls));
// Register System Settings
registerSystemSettings();
@@ -103,16 +141,8 @@ Hooks.once("init", async () => {
});
});
// Don't register new sheet yet.
// Actors.registerSheet(DG.ID, DGAgentSheetV2, {
// makeDefault: false,
// themes: null,
// label: `Agent Sheet V2`,
// types: ["agent"],
// });
// Register item sheet.
Items.registerSheet(DG.ID, DeltaGreenItemSheet, {
Items.registerSheet(DG.ID, DGItemSheet, {
makeDefault: true,
label: "DG.Sheet.class.item",
themes: null,
@@ -125,7 +155,79 @@ Hooks.once("init", async () => {
registerHandlebarsHelpers();
});
/**
* @param {ActiveEffect} effect
* @returns {Actor|undefined}
*/
function getAgentFromEffect(effect) {
const { parent } = effect;
if (parent?.documentName === "Actor" && parent.type === "agent") {
return parent;
}
if (parent?.documentName === "Item") {
const { actor } = parent;
if (actor?.type === "agent") return actor;
}
return undefined;
}
/**
* @param {ActiveEffect} effect
* @returns {Promise<void>}
*/
async function onAgentActiveEffectChange(effect) {
const actor = getAgentFromEffect(effect);
if (!actor) return;
actor.reset();
if (game.user.isActiveGM) await pruneExpiredStimulantEffects(actor);
await syncExhaustionEffect(actor);
}
Hooks.on("createActiveEffect", (effect) => {
onAgentActiveEffectChange(effect);
});
Hooks.on("updateActiveEffect", (effect) => {
onAgentActiveEffectChange(effect);
});
Hooks.on("deleteActiveEffect", (effect) => {
onAgentActiveEffectChange(effect);
});
Hooks.on("updateWorldTime", () => {
if (!game.user.isActiveGM) return;
pruneAllAgentsExpiredStimulants();
});
Hooks.on("updateItem", (item, changes) => {
const flat = foundry.utils.flattenObject(changes);
const transferStateKeys = [
"system.equipped",
"system.acuteEpisode",
"system.crossedOut",
"system.disorderCured",
];
if (transferStateKeys.some((key) => key in flat)) {
const { actor } = item;
if (actor) {
actor.reset();
if (actor.sheet?.rendered) actor.sheet.render();
}
}
if (
item.type === "profession" &&
"name" in flat &&
item.actor?.type === "agent"
) {
item.actor.update({ "system.biography.profession": item.name });
}
});
Hooks.once("ready", async () => {
await runWorldMigration();
// Wait to register hotbar drop hook on ready so that modules could register earlier if they want to
// TODO: Fix eslint issue on next line
// eslint-disable-next-line consistent-return
@@ -164,32 +266,25 @@ Hooks.on("preCreateItem", (item) => {
});
// Hook into the render call for the Actors Directory to add an extra button
Hooks.on("renderActorDirectory", (app, html, data) => {
if (!game.user.isGM) {
return;
}
Hooks.on("renderActorDirectory", (app, element) => {
if (!game.user.isGM) return;
if (html.querySelector("#statParserButton")) {
return;
}
const footer = element?.querySelector?.(".directory-footer");
if (!footer || footer.querySelector("#statParserButton")) return;
const button = document.createElement("button");
button.id = "statParserButton";
button.type = "button";
const icon = document.createElement("i");
icon.classList.add("fas", "fa-file-import");
button.appendChild(icon);
button.append("Delta Green Stat Block Parser");
html.querySelector(".directory-footer").appendChild(button);
if (button) {
button.addEventListener("click", function (event) {
ParseDeltaGreenStatBlock();
});
}
footer.appendChild(button);
button.addEventListener("click", () => {
ParseDeltaGreenStatBlock();
});
});
// Note - this event is fired on ALL connected clients...
@@ -244,8 +339,14 @@ Hooks.on("renderChatLog", async (app, element) => {
addEventListenerToChatMessage(element);
});
Hooks.on("renderChatMessageHTML", async (app, element, context) => {
Hooks.on("renderChatMessageHTML", async (message, element, context) => {
if (message.getFlag(DG.ID, "chatCard")) {
element.classList.add("dg-chat-card-message");
enrichDGChatCardMessage(message, element);
}
// ignore non chat card notifications
if (!context.canClose) return;
addEventListenerToChatMessage(element);
});
+86
View File
@@ -0,0 +1,86 @@
import { getDGSpeaker } from "../chat/dg-chat-card.js";
/**
* Whether Dice So Nice is active and its Roll API is available.
* @returns {boolean}
*/
export function isDiceSoNiceAvailable() {
return Boolean(
game.modules.get("dice-so-nice")?.active &&
typeof game.dice3d?.showForRoll === "function",
);
}
/**
* Show a 3D dice animation for a Foundry Roll via the DSN Roll API.
* Used for rolls that are not posted as chat messages (e.g. stat generation).
* Chat-based rolls should rely on DSN's createChatMessage hook instead.
*
* @see https://gitlab.com/riccisi/foundryvtt-dice-so-nice/-/wikis/Integration
* @see https://gitlab.com/riccisi/foundryvtt-dice-so-nice/-/wikis/API/Roll
* @param {Roll} roll
* @param {object} [options]
* @param {boolean} [options.synchronize=true] Broadcast animation to other clients.
* @param {ChatMessage.SpeakerData|null} [options.speaker] Chat speaker for NPC roll hiding.
* @param {string[]|null} [options.whisper] User IDs who can see the roll; null for everyone.
* @param {boolean} [options.blind=false] Whether the roll is blind for the rolling user.
* @param {string|null} [options.chatMessageId] Chat message to reveal when the animation ends.
* @returns {Promise<boolean>} True if animation was displayed.
*/
export async function showDiceSoNiceRoll(
roll,
{
synchronize = true,
speaker = null,
whisper = null,
blind = false,
chatMessageId = null,
} = {},
) {
if (!isDiceSoNiceAvailable()) return false;
return game.dice3d.showForRoll(
roll,
game.user,
synchronize,
whisper,
blind,
chatMessageId,
speaker ?? undefined,
{ ghost: false, secret: false },
);
}
/**
* Merge evaluated rolls into a pool and show one 3D animation for all of them.
*
* @see https://gitlab.com/riccisi/foundryvtt-dice-so-nice/-/wikis/Integration#step-4-select-roll-order-for-multiple-rolls
* @param {Roll[]} rolls
* @param {object} [options]
* @param {Actor|null} [options.actor]
* @param {TokenDocument|null} [options.token]
* @returns {Promise<void>}
*/
export async function showDiceSoNicePooledRolls(
rolls,
{ actor = null, token = null } = {},
) {
if (!isDiceSoNiceAvailable() || !rolls?.length) return;
const pool = foundry.dice.terms.PoolTerm.fromRolls(rolls);
const pooledRoll = Roll.fromTerms([pool]);
const speaker = actor ? getDGSpeaker({ actor, token }) : null;
await showDiceSoNiceRoll(pooledRoll, { speaker });
}
/**
* Wait for the 3D animation tied to a chat message, if DSN is available.
*
* @param {string} messageId
* @returns {Promise<void>}
*/
export async function waitForDiceSoNiceMessageAnimation(messageId) {
if (!isDiceSoNiceAvailable()) return;
if (typeof game.dice3d.waitFor3DAnimationByMessageID !== "function") return;
await game.dice3d.waitFor3DAnimationByMessageID(messageId);
}
-196
View File
@@ -1,196 +0,0 @@
import {
DGPercentileRoll,
DGLethalityRoll,
DGDamageRoll,
DGSanityDamageRoll,
} from "../roll/roll.js";
/**
* Extend the basic ItemSheet with some very simple modifications
* @extends {ItemSheet}
*/
export default class DeltaGreenItemSheet extends foundry.appv1.sheets
.ItemSheet {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["deltagreen", "sheet", "item"],
width: 520,
height: 600,
tabs: [
{
navSelector: ".sheet-tabs",
contentSelector: ".sheet-body",
initial: "attributes",
},
],
});
}
/** @override */
get template() {
const path = "systems/deltagreen/templates/item";
// unique item sheet by type, like `weapon-sheet.html`.
return `${path}/item-${this.item.type}-sheet.html`;
}
/* -------------------------------------------- */
/** @override */
async getData() {
const data = super.getData();
data.enrichedDescription =
await foundry.applications.ux.TextEditor.implementation.enrichHTML(
this.object.system.description,
{ async: true },
);
if (data.item.type === "tome" || data.item.type === "ritual") {
data.enrichedHandlerNotes =
await foundry.applications.ux.TextEditor.implementation.enrichHTML(
this.object.system.handlerNotes,
{ async: true },
);
}
return data;
}
/* -------------------------------------------- */
/** @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 */
activateListeners(html) {
super.activateListeners(html);
// Everything below here is only needed if the sheet is editable
// eslint-disable-next-line no-useless-return
if (!this.options.editable) return;
// Rollable abilities - bind to everything with the 'Rollable' class
html.find(".rollable").click(this._onRoll.bind(this));
html.find(".rollable").contextmenu(this._onRoll.bind(this)); // this is for right-click, which triggers the roll modifier dialogue for most rolls
// toggle whether information is shown to a player for a tome or not
html.find(".tome-show-hide-info").click((event) => {
event.preventDefault();
this.item.system.revealed = !this.item.system.revealed;
this.item.update({ "system.revealed": this.item.system.revealed });
});
}
/**
* Handle clickable rolls.
*
* @param {Event} event The originating click event
* @async
* @private
*/
async _onRoll(event) {
event.preventDefault();
const element = event.currentTarget;
const { dataset } = element;
if (event && event.which === 2) {
// probably don't want rolls to trigger from a middle mouse click so just kill it here
return;
}
// const item = this.actor.items.get(dataset.iid);
const rollOptions = {
rollType: dataset.rolltype,
key: dataset.key,
// actor: this,
specialTrainingName: dataset?.name || null, // Only applies to Special Training Rolls
item: this.item,
};
// Create a default 1d100 roll just in case.
let roll = new Roll("1d100", {});
switch (dataset.rolltype) {
case "stat":
case "skill":
case "sanity":
case "special-training":
case "weapon":
roll = new DGPercentileRoll("1D100", {}, rollOptions);
break;
case "lethality":
roll = new DGLethalityRoll("1D100", {}, rollOptions);
break;
case "damage": {
let diceFormula = this.item.system.damage;
const { skill } = this.item.system;
if (
(this.actor.type === "agent" || this.actor.type === "npc") &&
(skill === "unarmed_combat" || skill === "melee_weapons")
) {
diceFormula +=
this.actor.system.statistics.str.meleeDamageBonusFormula;
}
roll = new DGDamageRoll(diceFormula, {}, rollOptions);
break;
}
case "sanity-damage": {
// Applies only to rituals, as far as Jalen can tell. Tomes have "study damage", but it just uses `sanity`.
const isLearntDamage = element.hasAttribute("data-san-on-learn");
const { successLoss, failedLoss } = isLearntDamage
? this.item.system.learnedSanity
: this.item.system.sanity;
const combinedFormula = `{${successLoss}, ${failedLoss}}`;
roll = new DGSanityDamageRoll(combinedFormula, {}, rollOptions);
break;
}
default:
break;
}
this.processRoll(event, roll, rollOptions);
}
/**
* Show a dialog for the roll and then send to chat.
* Broke this logic out from `_onRoll()` so that other files can call it,
* namely the macro logic.
*
* TODO: Move this logic to the roll.js.
*
* @param {Event} event The originating click event
* @param {Event} roll The roll to show a dialog for and then send to chat.
* @async
*/
async processRoll(event, roll) {
// Open dialog if user requests it (no dialog for Sanity Damage rolls)
if (
(event.shiftKey || event.which === 3) &&
!(roll instanceof DGSanityDamageRoll)
) {
const dialogData = await roll.showDialog();
if (!dialogData) return;
if (dialogData.newFormula) {
roll = new DGDamageRoll(dialogData.newFormula, {}, roll.options);
}
roll.modifier += dialogData.targetModifier;
roll.options.rollMode = dialogData.rollMode;
}
// Evaluate the roll.
await roll.evaluate();
// Send the roll to chat.
roll.toChat();
}
}
+19 -22
View File
@@ -1,3 +1,5 @@
import { getDGRollToken } from "../chat/dg-chat-card.js";
import appendMeleeDamageBonus from "../roll/melee-damage.js";
import { DGDamageRoll, DGLethalityRoll } from "../roll/roll.js";
/**
@@ -5,18 +7,6 @@ import { DGDamageRoll, DGLethalityRoll } from "../roll/roll.js";
* @extends {Item}
*/
export default class DeltaGreenItem extends Item {
/**
* Augment the basic Item data model with additional dynamic data.
*/
prepareData() {
super.prepareData();
// Get the Item's data
const itemData = this;
const actorData = this.actor || {};
const { system } = itemData;
}
/**
* Handle clickable rolls.
* @param {Event} event The originating click event
@@ -26,23 +16,25 @@ export default class DeltaGreenItem extends Item {
// Basic template rendering data
const item = this;
const { actor } = this;
const actorSystemData = this.actor.system || {};
let roll;
if (item.system.isLethal) {
roll = new DGLethalityRoll(
"1D100",
{},
{ rollType: "lethality", actor, item },
{
rollType: "lethality",
actor,
item,
token: getDGRollToken(actor, actor.sheet?.token),
},
);
} else {
// regular damage roll
let diceFormula = item.system.damage;
const skillType = item.system.skill;
if (skillType === "unarmed_combat" || skillType === "melee_weapons") {
diceFormula += actorSystemData.statistics.str.meleeDamageBonusFormula;
}
let diceFormula = appendMeleeDamageBonus(
item.system.damage,
actor,
item.system.skill,
);
if (isCrit) {
diceFormula = `2*(${diceFormula})`;
@@ -51,7 +43,12 @@ export default class DeltaGreenItem extends Item {
roll = new DGDamageRoll(
diceFormula,
{},
{ rollType: "damage", actor, item },
{
rollType: "damage",
actor,
item,
token: getDGRollToken(actor, actor.sheet?.token),
},
);
}
return actor.sheet.processRoll({}, roll);
@@ -1,3 +1,4 @@
import { getTokenFromChatSpeaker } from "../chat/dg-chat-card.js";
import * as DGRolls from "../roll/roll.js";
const { fromUuid } = foundry.utils;
@@ -108,11 +109,11 @@ export function rollItemSkillCheckMacro(itemId) {
);
const skillName = item.system.skill.toString();
const token = getTokenFromChatSpeaker(speaker);
const roll = new DGRolls.DGPercentileRoll(
"1D100",
{},
{ rollType: "weapon", key: skillName, actor, item },
{ rollType: "weapon", key: skillName, actor, item, token },
);
return actor.sheet.processRoll({}, roll);
}
@@ -130,13 +131,13 @@ export function rollSkillMacro(skillName) {
if (!skill) return ui.notifications.warn("Bad skill name passed to macro.");
const token = getTokenFromChatSpeaker(speaker);
const roll = new DGRolls.DGPercentileRoll(
"1D100",
{},
{ rollType: "skill", key: skillName, actor },
{ rollType: "skill", key: skillName, actor, token },
);
return actor.sheet.processRoll({}, roll);
// sendPercentileTestToChat(actor, translatedSkillLabel, skill.proficiency);
}
/**
@@ -167,12 +168,15 @@ export async function rollSkillTestAndDamageForOwnedItem(
return ui.notifications.warn("Invalid item targeted in macro.");
}
const speaker = ChatMessage.getSpeaker();
const token = getTokenFromChatSpeaker(speaker);
const rollOptions = {
rollType: "weapon",
key: item.system.skill,
actor: item.parent,
specialTrainingName: null, // Only applies to Special Training Rolls
item,
token,
};
const roll = new DGRolls.DGPercentileRoll("1D100", {}, rollOptions);
@@ -1,3 +1,5 @@
import { showDgDialog } from "../applications/dg-dialog.js";
function GetAttacksFromInput(inputText) {
const attacks = [];
try {
@@ -111,9 +113,11 @@ function GetAttacksFromInput(inputText) {
}
}
// look for armor piercing
if (attackLine.indexOf("ARMOR PIERCING") >= 0) {
// TODO: This should do something.
const apMatch = attackLine.match(/ARMOR\s+PIERCING\s*(\d+)/i);
const armorPiercing = apMatch?.[1] ? parseInt(apMatch[1]) : 5;
weaponData.armorPiercing = armorPiercing;
weaponData.system.armorPiercing = armorPiercing;
}
}
@@ -657,14 +661,16 @@ async function GetUserInput() {
</div>
</form>`;
new foundry.applications.api.DialogV2({
window: { title: "Stat Block Parser" },
await showDgDialog({
modifier: "stat-parser",
content,
window: { title: "Stat Block Parser" },
buttons: [
{
label: "PARSE",
action: "roll",
callback: (event, button, dialog) => {
default: true,
callback: (_event, _button, dialog) => {
const textInput =
dialog.element.querySelector("[name=parse-input]")?.value;
@@ -675,9 +681,9 @@ async function GetUserInput() {
},
},
],
}).render(true);
});
}
export default async function ParseDeltaGreenStatBlock() {
GetUserInput();
await GetUserInput();
}
-257
View File
@@ -1,257 +0,0 @@
import DGUtils from "./utility-functions.js";
export default function registerHandlebarsHelpers() {
// Add Handlebars helpers
Handlebars.registerHelper("localizeWithFallback", (value, fallbackValue) => {
return DGUtils.localizeWithFallback(value, fallbackValue);
});
Handlebars.registerHelper("concat", (...args) => {
let outStr = "";
for (const arg in args) {
if (typeof args[arg] !== "object") {
outStr += args[arg];
}
}
return outStr;
});
Handlebars.registerHelper("toLowerCase", (str) => {
try {
return str.toLowerCase();
} catch (error) {
return "";
}
});
Handlebars.registerHelper("toUpperCase", (str) => {
try {
return str.toUpperCase();
} catch (error) {
return "";
}
});
Handlebars.registerHelper("if_eq", (a, b, opts) => {
if (a === b) {
return opts.fn(this);
}
return opts.inverse(this);
});
Handlebars.registerHelper("if_not_eq", (a, b, opts) => {
if (a !== b) {
return opts.fn(this);
}
return opts.inverse(this);
});
Handlebars.registerHelper("if_gt", (a, b, trueVal, falseVal) => {
if (a > b) {
return trueVal;
}
return falseVal;
});
Handlebars.registerHelper("cite_ahb", (page) => {
return `See page ${page} of the Agent's Handbook.`;
});
Handlebars.registerHelper("formatLethality", (lethality) => {
if (lethality > 0) {
return `${lethality.toString()}%`;
}
return "";
});
// Is this used anywhere?
Handlebars.registerHelper(
"getActorSkillProp",
(actorData, skillName, prop) => {
try {
if (skillName !== "" && prop !== "") {
const { skills } = actorData.data;
const skill = skills[skillName];
const propVal = skill[prop];
return propVal;
}
return "";
} catch (ex) {
console.log(ex);
return "";
}
},
);
Handlebars.registerHelper("getAvailableRollModes", () => {
try {
return CONFIG.Dice.rollModes;
} catch (error) {
return console.log(error);
}
});
Handlebars.registerHelper("getDefaultRollMode", () => {
try {
return game.settings.get("core", "rollMode");
} catch (error) {
return console.log(error);
}
});
Handlebars.registerHelper(
"calculateHandToHandCombatDamageFormulaBonus",
(strength) => {
try {
let bonus = "";
if (strength.value < 5) {
bonus = " - 2";
} else if (strength.value < 9) {
bonus = " - 1";
} else if (strength.value > 12 && strength < 17) {
bonus = " + 1";
} else if (strength.value > 16) {
bonus = " + 2";
}
return bonus;
} catch (error) {
return console.log(error);
}
},
);
Handlebars.registerHelper("localizeWeaponSkill", (skill) => {
let label = skill;
try {
if (skill === "dex") {
label = game.i18n.localize("DG.Attributes.dex");
}
if (skill === "DG.Skills.custom") {
label = game.i18n.localize("DG.ItemWindow.Custom");
} else {
label = game.i18n.localize(`DG.Skills.${skill}`);
}
} catch (error) {
console.log(error);
}
return label;
});
Handlebars.registerHelper(
"hideSkillBasedOnProficiencyAndUserChoice",
(hideUntrainedSkills, proficiency) => {
let showValue = true;
try {
if (hideUntrainedSkills === false) {
showValue = true;
} else if (proficiency > 0) {
showValue = true;
} else {
showValue = false;
}
} catch (error) {
console.log(error);
}
return showValue;
},
);
// looks at system setting for what font to use and returns the class that is then used in the handlebars template that
// generates the character sheet.
Handlebars.registerHelper("getFontFamilySystemSettingClass", () => {
const setting = game.settings.get("deltagreen", "characterSheetFont");
const characterSheetStyle = game.settings.get(
"deltagreen",
"characterSheetStyle",
);
/*
if(setting === "TypeWriterCondensed"){
return "typewriter-condensed-font";
}
else if(setting === "Martel"){
return "martel-font";
}
else if(setting === "Signika"){
return "signika-font";
}
else if(setting === "atwriter"){
return "atwriter-font";
}
else if(setting === "SpecialElite"){
return "special-elite-font";
}
else if(setting === "PublicSans"){
return "public-sans-font";
}
else{
return "martel-font";
}
*/
/*
if(characterSheetStyle === "cowboy"){
return "special-elite-font";
}
else{
return "public-sans-font";
}
*/
});
Handlebars.registerHelper("getCharacterSheetStyle", () => {
const characterSheetStyle = game.settings.get(
"deltagreen",
"characterSheetStyle",
);
if (characterSheetStyle === "cowboy") {
return "cowboy-style";
}
if (characterSheetStyle === "outlaw") {
return "outlaw-style";
}
return "program-style";
});
Handlebars.registerHelper("keepSanityPrivate", () => {
let setting = false;
try {
setting = game.settings.get("deltagreen", "keepSanityPrivate");
if (game.user.isGM) {
setting = false;
}
} catch (ex) {
setting = false;
}
return setting;
});
Handlebars.registerHelper("playerHasGamemasterPrivileges", () => {
return game.user.isGM;
});
Handlebars.registerHelper("showImpossibleLandscapesContent", () => {
let result = false;
const setting = game.settings.get(
"deltagreen",
"showImpossibleLandscapesContent",
);
if (game.user.isGM === true && setting === true) {
result = true;
}
return result;
});
}
+349
View File
@@ -0,0 +1,349 @@
import DG from "../config/index.js";
import {
BONUS_SKILL_CATALOG_EXCLUDED_FIXED,
BONUS_SKILL_COUNT,
BONUS_SKILL_INCREMENT,
MAX_ALLOWED_BONUS_WASTE,
SKILL_CAP,
TYPED_SKILL_TEMPLATE_GROUPS,
} from "./constants.js";
import {
formatProfessionSkillKey,
formatProfessionSkillLabel,
getAgentSkillDefaults,
getTypedGroupDisplayName,
parseProfessionSkillKey,
} from "./keys.js";
/**
* @typedef {object} SkillCatalogEntry
* @property {string} id - Select value (`fixed:accounting` or `typed:Craft`)
* @property {ProfessionSkillRef} ref
* @property {string} label
* @property {string} sortLabel
*/
/**
* @returns {SkillCatalogEntry[]}
*/
export function buildSkillCatalog() {
/** @type {SkillCatalogEntry[]} */
const entries = [];
for (const key of DG.skills) {
const ref = /** @type {FixedSkillRef} */ ({ kind: "fixed", key });
const label = formatProfessionSkillLabel(ref);
entries.push({
id: `fixed:${key}`,
ref,
label,
sortLabel: label,
});
}
for (const group of TYPED_SKILL_TEMPLATE_GROUPS) {
const ref = /** @type {TypedSkillRef} */ ({
kind: "typed",
group,
label: "",
});
const label = getTypedGroupDisplayName(group);
entries.push({
id: `typed:${group}`,
ref,
label,
sortLabel: label,
});
}
entries.sort((a, b) =>
a.sortLabel.localeCompare(b.sortLabel, game.i18n.lang),
);
return entries;
}
/**
* Skill catalog for character-creation bonus picks (excludes Unnatural).
*
* @returns {SkillCatalogEntry[]}
*/
export function buildBonusSkillCatalog() {
return buildSkillCatalog().filter(
(entry) =>
entry.ref.kind !== "fixed" ||
!BONUS_SKILL_CATALOG_EXCLUDED_FIXED.has(entry.ref.key),
);
}
/**
* @param {string} catalogId
* @param {string} [typedLabel]
* @param {{ allowEmptyTypedLabel?: boolean }} [options]
* @returns {ProfessionSkillRef | null}
*/
export function catalogIdToSkillRef(
catalogId,
typedLabel = "",
{ allowEmptyTypedLabel = false } = {},
) {
if (!catalogId) return null;
if (catalogId.startsWith("fixed:")) {
const key = catalogId.slice(6);
if (!DG.skills.includes(key)) return null;
return { kind: "fixed", key };
}
if (catalogId.startsWith("typed:")) {
const group = catalogId.slice(6);
if (!TYPED_SKILL_TEMPLATE_GROUPS.includes(group)) return null;
const label = typedLabel.trim();
if (!label && !allowEmptyTypedLabel) return null;
return { kind: "typed", group, label };
}
return null;
}
/**
* @param {ProfessionSkillRef} ref
* @returns {string}
*/
function getBonusTrackKey(ref) {
if (ref.kind === "fixed") return `fixed:${ref.key}`;
return formatProfessionSkillKey(ref);
}
/**
* @param {string} trackKey
* @returns {string}
*/
export function getBonusTrackLabel(trackKey) {
if (trackKey.startsWith("fixed:")) {
const key = trackKey.slice(6);
return formatProfessionSkillLabel(
/** @type {FixedSkillRef} */ ({ kind: "fixed", key }),
);
}
const ref = parseProfessionSkillKey(trackKey);
if (ref) return formatProfessionSkillLabel(ref);
return trackKey;
}
/**
* @param {string} trackKey
* @param {Record<string, number>} baseFixed
* @param {Record<string, { group: string, label: string, value: number }>} baseTyped
* @param {Record<string, number>} defaults
* @returns {number}
*/
function getBonusTrackBaseValue(trackKey, baseFixed, baseTyped, defaults) {
if (trackKey.startsWith("fixed:")) {
const key = trackKey.slice(6);
return baseFixed[key] ?? defaults[key] ?? 0;
}
return baseTyped[trackKey]?.value ?? 0;
}
/**
* @typedef {object} BonusPickOptions
* @property {number} [count]
* @property {number} [increment]
* @property {number} [cap]
* @property {number} [maxWaste]
*/
/**
* Bonus skill slots that would waste points over the creation cap.
*
* @param {Record<string, number>} baseFixed
* @param {Record<string, { group: string, label: string, value: number }>} baseTyped
* @param {string[]} bonusCatalogIds
* @param {string[]} bonusTypedLabels
* @param {BonusPickOptions} [options]
* @returns {string[]}
*/
export function collectBonusCapValidationErrors(
baseFixed,
baseTyped,
bonusCatalogIds,
bonusTypedLabels,
{
count = BONUS_SKILL_COUNT,
increment = BONUS_SKILL_INCREMENT,
cap = SKILL_CAP,
maxWaste = MAX_ALLOWED_BONUS_WASTE,
} = {},
) {
const defaults = getAgentSkillDefaults();
/** @type {Record<string, number>} */
const counts = {};
/** @type {(string | null)[]} */
const slotTrackKeys = [];
for (let i = 0; i < count; i++) {
const catalogId = bonusCatalogIds?.[i];
if (!catalogId) {
slotTrackKeys.push(null);
} else {
const ref = catalogIdToSkillRef(catalogId, bonusTypedLabels?.[i] ?? "");
if (!ref) {
slotTrackKeys.push(null);
} else {
const trackKey = getBonusTrackKey(ref);
slotTrackKeys.push(trackKey);
counts[trackKey] = (counts[trackKey] ?? 0) + 1;
}
}
}
/** @type {Set<string>} */
const violatingTrackKeys = new Set();
for (const [trackKey, countForTrack] of Object.entries(counts)) {
const base = getBonusTrackBaseValue(
trackKey,
baseFixed,
baseTyped,
defaults,
);
const final = base + countForTrack * increment;
const waste = Math.max(0, final - cap);
if (base < cap && waste >= maxWaste) {
violatingTrackKeys.add(trackKey);
}
}
/** @type {string[]} */
const errors = [];
for (let i = 0; i < count; i++) {
const trackKey = slotTrackKeys[i];
if (trackKey && violatingTrackKeys.has(trackKey)) {
const base = getBonusTrackBaseValue(
trackKey,
baseFixed,
baseTyped,
defaults,
);
const countForTrack = counts[trackKey] ?? 0;
const final = base + countForTrack * increment;
const waste = Math.max(0, final - cap);
errors.push(`bonusWaste:${i}|${waste}|${trackKey}`);
}
}
return errors;
}
/**
* @param {Record<string, number>} baseFixed
* @param {Record<string, { group: string, label: string, value: number }>} baseTyped
* @param {{ bonusCatalogIds?: string[], bonusTypedLabels?: string[] }} formState
* @param {BonusPickOptions} [options]
* @returns {{
* fixedValues: Record<string, number>,
* typedValues: Record<string, { group: string, label: string, value: number }>,
* modifiedFixedKeys: string[],
* modifiedTypedKeys: string[],
* validationErrors: string[],
* isValid: boolean,
* }}
*/
export function applyBonusSkillPicks(
baseFixed,
baseTyped,
formState,
{
count = BONUS_SKILL_COUNT,
increment = BONUS_SKILL_INCREMENT,
cap = SKILL_CAP,
maxWaste = MAX_ALLOWED_BONUS_WASTE,
} = {},
) {
const defaults = getAgentSkillDefaults();
const fixedValues = { ...baseFixed };
const typedValues = foundry.utils.deepClone(baseTyped);
/** @type {Set<string>} */
const modifiedFixedKeys = new Set();
/** @type {Set<string>} */
const modifiedTypedKeys = new Set();
/** @type {Record<string, number>} */
const bonusCounts = {};
const bonusIds = formState.bonusCatalogIds ?? [];
const bonusLabels = formState.bonusTypedLabels ?? [];
for (let i = 0; i < count; i++) {
const catalogId = bonusIds[i];
if (catalogId) {
const typedLabel = bonusLabels[i] ?? "";
const ref = catalogIdToSkillRef(catalogId, typedLabel);
if (ref) {
if (ref.kind === "fixed") modifiedFixedKeys.add(ref.key);
else modifiedTypedKeys.add(formatProfessionSkillKey(ref));
const trackKey = getBonusTrackKey(ref);
bonusCounts[trackKey] = (bonusCounts[trackKey] ?? 0) + 1;
}
}
}
for (const [trackKey, pickCount] of Object.entries(bonusCounts)) {
const bonus = pickCount * increment;
if (trackKey.startsWith("fixed:")) {
const key = trackKey.slice(6);
fixedValues[key] = (fixedValues[key] ?? defaults[key] ?? 0) + bonus;
} else {
const ref = parseProfessionSkillKey(trackKey);
if (ref?.kind === "typed") {
const base = typedValues[trackKey]?.value ?? 0;
typedValues[trackKey] = {
group: ref.group,
label: ref.label,
value: base + bonus,
};
}
}
}
for (const key of Object.keys(fixedValues)) {
if (fixedValues[key] > cap) fixedValues[key] = cap;
}
for (const data of Object.values(typedValues)) {
if (data.value > cap) data.value = cap;
}
/** @type {string[]} */
const validationErrors = [];
for (let i = 0; i < count; i++) {
const catalogId = bonusIds[i];
if (!catalogId) {
validationErrors.push(`bonus${i}`);
} else {
const ref = catalogIdToSkillRef(catalogId, bonusLabels[i] ?? "");
if (!ref) {
validationErrors.push(`bonus${i}`);
} else if (ref.kind === "typed" && !bonusLabels[i]?.trim()) {
validationErrors.push(`bonusType${i}`);
}
}
}
validationErrors.push(
...collectBonusCapValidationErrors(
baseFixed,
baseTyped,
bonusIds,
bonusLabels,
{ count, increment, cap, maxWaste },
),
);
return {
fixedValues,
typedValues,
modifiedFixedKeys: [...modifiedFixedKeys],
modifiedTypedKeys: [...modifiedTypedKeys],
validationErrors,
isValid: validationErrors.length === 0,
};
}
@@ -0,0 +1,134 @@
import { applyDamagedVeteranModifications } from "./damaged-veteran-modifiers.js";
import { finalizeAgentStartingResources } from "../data/derived/actor-derived.js";
/**
* @typedef {object} CharacterCreationBond
* @property {string} name
* @property {string} relationship
*/
/**
* @typedef {object} CharacterCreationPayload
* @property {Record<string, number>} fixedValues
* @property {Record<string, { group: string, label: string, value: number }>} typedValues
* @property {string} professionName
* @property {CharacterCreationBond[]} bonds
*/
/**
* @param {ReturnType<import("./compute.js").default>} computed
* @param {Item} professionItem
* @param {{ bondNames: string[], bondRelationships: string[] }} formState
* @returns {CharacterCreationPayload}
*/
export function buildCharacterCreationPayload(
computed,
professionItem,
formState,
) {
const bonds = [];
const bondCount = Math.max(
1,
Math.trunc(Number(professionItem.system.bonds) || 1),
);
for (let i = 0; i < bondCount; i++) {
bonds.push({
name: formState.bondNames?.[i]?.trim() ?? "",
relationship: formState.bondRelationships?.[i]?.trim() ?? "",
});
}
return {
fixedValues: { ...computed.fixedValues },
typedValues: foundry.utils.deepClone(computed.typedValues),
professionName: professionItem.name,
bonds,
};
}
/**
* @param {Actor} actor
* @param {CharacterCreationPayload} payload
* @param {{ skipBondIndices?: number[] }} [options]
*/
export async function applyCharacterCreationPayload(
actor,
payload,
{ skipBondIndices = [] } = {},
) {
const skipSet = new Set(skipBondIndices);
const updateData = {};
for (const [key, value] of Object.entries(payload.fixedValues)) {
updateData[`system.skills.${key}.proficiency`] = value;
updateData[`system.skills.${key}.failure`] = false;
}
const typedSkills = { ...(actor.system.typedSkills ?? {}) };
for (const [, data] of Object.entries(payload.typedValues)) {
const id = foundry.utils.randomID();
typedSkills[id] = {
label: data.label,
group: data.group,
proficiency: data.value,
failure: false,
};
}
updateData["system.typedSkills"] = typedSkills;
updateData["system.biography.profession"] = payload.professionName;
await actor.update(updateData);
const chaScore =
actor.system.statistics.cha.effectiveValue ??
actor.system.statistics.cha.value;
/** @type {object[]} */
const bondDocs = [];
payload.bonds.forEach((bond, index) => {
if (skipSet.has(index)) return;
bondDocs.push({
name: bond.name,
type: "bond",
system: {
score: chaScore,
relationship: bond.relationship,
},
});
});
if (bondDocs.length) {
await actor.createEmbeddedDocuments("Item", bondDocs);
}
}
/**
* @param {Item} professionStub
* @param {Actor} actor
* @param {CharacterCreationPayload} payload
* @param {object} veteranResult
*/
export async function commitProfessionSetup(
actor,
professionStub,
payload,
veteranResult,
) {
const skipBondIndices =
veteranResult.path === "hardExperience" &&
Number.isInteger(veteranResult.removedBondIndex)
? [veteranResult.removedBondIndex]
: [];
await applyCharacterCreationPayload(actor, payload, { skipBondIndices });
await applyDamagedVeteranModifications(actor, veteranResult, payload);
await finalizeAgentStartingResources(actor, veteranResult.path);
const createData = professionStub.toObject();
delete createData._id;
const created = await actor.createEmbeddedDocuments("Item", [createData]);
return created[0] ?? null;
}
+207
View File
@@ -0,0 +1,207 @@
import { SKILL_CAP } from "./constants.js";
import {
applyBonusSkillPicks,
collectBonusCapValidationErrors,
} from "./catalog.js";
import {
formatProfessionSkillKey,
formatProfessionSkillLabel,
getAgentSkillDefaults,
parseProfessionSkillKey,
} from "./keys.js";
import { validateProfessionFormState } from "./validation.js";
/**
* @param {Record<string, number>} automaticSkills
* @param {Record<string, number>} optionSkills
* @param {number} optionPicks
* @param {object} formState
* @param {Set<string>} formState.checkedOptionKeys
* @param {Record<string, string>} formState.chooseOneLabels - keyed by profession map key
* @param {string[]} formState.bonusCatalogIds - length 8
* @param {string[]} formState.bonusTypedLabels - length 8
* @param {string[]} [formState.bondNames]
* @param {string[]} [formState.bondRelationships]
* @param {object} [context]
* @param {Record<string, ProfessionSkillMeta>} [context.automaticMeta]
* @param {Record<string, ProfessionSkillMeta>} [context.optionMeta]
* @param {number} [context.bondCount]
* @returns {{
* fixedValues: Record<string, number>,
* typedValues: Record<string, { group: string, label: string, value: number }>,
* capWarnings: { label: string, attempted: number, waste: number }[],
* isValid: boolean,
* validationErrors: string[],
* modifiedFixedKeys: string[],
* modifiedTypedKeys: string[],
* }}
*/
export default function computeSkillValues(
automaticSkills,
optionSkills,
optionPicks,
formState,
{ automaticMeta = {}, optionMeta = {}, bondCount = 0 } = {},
) {
const defaults = getAgentSkillDefaults();
/** @type {Record<string, number>} */
const fixedValues = { ...defaults };
/** @type {Record<string, { group: string, label: string, value: number }>} */
const typedValues = {};
/** @type {Set<string>} */
const modifiedFixedKeys = new Set();
/** @type {Set<string>} */
const modifiedTypedKeys = new Set();
/**
* @param {TypedSkillRef} ref
* @param {string} label
*/
const markTypedModified = (ref, label) => {
modifiedTypedKeys.add(
formatProfessionSkillKey({ kind: "typed", group: ref.group, label }),
);
};
/**
* @param {string} mapKey
* @param {number} rating
* @param {{ labelOverride?: string }} [options]
* @returns {string | null} Merged typed storage key, if applied.
*/
const applyOverride = (mapKey, rating, options = {}) => {
const ref = parseProfessionSkillKey(mapKey);
if (!ref) return null;
const value = Number(rating);
if (ref.kind === "fixed") {
fixedValues[ref.key] = value;
return null;
}
const label = options.labelOverride?.trim() ?? ref.label?.trim() ?? "";
if (!label) return null;
const storageKey = formatProfessionSkillKey({
kind: "typed",
group: ref.group,
label,
});
typedValues[storageKey] = {
group: ref.group,
label,
value,
};
return storageKey;
};
for (const [key, rating] of Object.entries(automaticSkills ?? {})) {
const ref = parseProfessionSkillKey(key);
if (ref) {
let labelOverride;
let skip = false;
if (ref.kind === "typed" && automaticMeta[key]?.chooseOne) {
labelOverride = formState.chooseOneLabels?.[key]?.trim();
if (!labelOverride) skip = true;
}
if (!skip) {
const storageKey = applyOverride(key, rating, { labelOverride });
if (ref.kind === "fixed") modifiedFixedKeys.add(ref.key);
else if (storageKey) {
markTypedModified(ref, labelOverride ?? ref.label);
}
}
}
}
for (const key of formState.checkedOptionKeys ?? []) {
if (optionSkills[key] || optionSkills[key] === 0) {
const ref = parseProfessionSkillKey(key);
if (ref) {
let labelOverride;
let skip = false;
if (ref.kind === "typed" && optionMeta[key]?.chooseOne) {
labelOverride = formState.chooseOneLabels?.[key]?.trim();
if (!labelOverride) skip = true;
} else if (ref.kind === "typed") {
labelOverride = ref.label?.trim();
if (!labelOverride) skip = true;
}
if (!skip) {
const storageKey = applyOverride(key, optionSkills[key], {
labelOverride,
});
if (ref.kind === "fixed") modifiedFixedKeys.add(ref.key);
else if (storageKey) {
markTypedModified(ref, labelOverride ?? ref.label);
}
}
}
}
}
/** Pre-bonus base for cap warnings */
const baseFixed = { ...fixedValues };
const baseTyped = foundry.utils.deepClone(typedValues);
const bonusResult = applyBonusSkillPicks(baseFixed, baseTyped, formState);
/** @type {{ label: string, attempted: number, waste: number }[]} */
const capWarnings = [];
const checkCap = (label, base, final) => {
if (final > SKILL_CAP) {
capWarnings.push({
label,
attempted: final,
waste: final - SKILL_CAP,
});
}
};
for (const [key, final] of Object.entries(bonusResult.fixedValues)) {
const base = baseFixed[key] ?? defaults[key] ?? 0;
const ref = /** @type {FixedSkillRef} */ ({ kind: "fixed", key });
checkCap(formatProfessionSkillLabel(ref), base, final);
}
for (const [storageKey, data] of Object.entries(bonusResult.typedValues)) {
const base = baseTyped[storageKey]?.value ?? 0;
const ref = /** @type {TypedSkillRef} */ ({
kind: "typed",
group: data.group,
label: data.label,
});
checkCap(formatProfessionSkillLabel(ref), base, data.value);
}
const validationErrors = validateProfessionFormState(optionPicks, formState, {
automaticSkills,
optionSkills,
automaticMeta,
optionMeta,
bondCount,
});
validationErrors.push(
...collectBonusCapValidationErrors(
baseFixed,
baseTyped,
formState.bonusCatalogIds ?? [],
formState.bonusTypedLabels ?? [],
),
);
return {
fixedValues: bonusResult.fixedValues,
typedValues: bonusResult.typedValues,
capWarnings,
isValid: validationErrors.length === 0,
validationErrors,
modifiedFixedKeys: [
...new Set([...modifiedFixedKeys, ...bonusResult.modifiedFixedKeys]),
],
modifiedTypedKeys: [
...new Set([...modifiedTypedKeys, ...bonusResult.modifiedTypedKeys]),
],
};
}
+46
View File
@@ -0,0 +1,46 @@
/** @typedef {"fixed" | "typed"} ProfessionSkillKind */
/** @typedef {{ kind: "fixed", key: string }} FixedSkillRef */
/** @typedef {{ kind: "typed", group: string, label: string }} TypedSkillRef */
/** @typedef {FixedSkillRef | TypedSkillRef} ProfessionSkillRef */
/** @typedef {{ chooseOne?: boolean }} ProfessionSkillMeta */
/** Typed skill template groups (camelCase storage keys). */
export const TYPED_SKILL_TEMPLATE_GROUPS = /** @type {const} */ ([
"Art",
"Craft",
"ForeignLanguage",
"MilitaryScience",
"Pilot",
"Science",
"Other",
]);
export const TYPED_GROUP_I18N = {
Art: "DG.TypeSkills.Art",
Craft: "DG.TypeSkills.Craft",
ForeignLanguage: "DG.TypeSkills.ForeignLanguage",
MilitaryScience: "DG.TypeSkills.MilitaryScience",
Pilot: "DG.TypeSkills.Pilot",
Science: "DG.TypeSkills.Science",
Other: "DG.TypeSkills.Other",
};
export const BONUS_SKILL_COUNT = 8;
export const BONUS_SKILL_INCREMENT = 20;
export const HARD_EXPERIENCE_BONUS_COUNT = 5;
export const HARD_EXPERIENCE_BONUS_INCREMENT = 10;
export const HARD_EXPERIENCE_SKILL_CAP = 90;
/** @type {ReadonlySet<string>} */
export const BONUS_SKILL_CATALOG_EXCLUDED_FIXED = new Set(["unnatural"]);
export const SKILL_CAP = 80;
/** Bonus picks that would waste this many points or more are rejected (e.g. 80 + 20% at cap). */
export const MAX_ALLOWED_BONUS_WASTE = 20;
export const TYPED_KEY_PATTERN =
/^(Art|Craft|Foreign Language|ForeignLanguage|Military Science|MilitaryScience|Pilot|Science|Other)\s*\(([^)]+)\)\s*$/i;
/** Legacy internal keys from earlier builds (`typed:ForeignLanguage:abc12345`). */
export const INTERNAL_TYPED_KEY_PATTERN =
/^typed:([A-Za-z]+):([A-Za-z0-9]{8,16})(?::(.+))?$/;
@@ -0,0 +1,222 @@
import {
HARD_EXPERIENCE_BONUS_COUNT,
HARD_EXPERIENCE_BONUS_INCREMENT,
HARD_EXPERIENCE_SKILL_CAP,
} from "./constants.js";
import { applyBonusSkillPicks } from "./catalog.js";
import { formatProfessionSkillKey } from "./keys.js";
function getPowRating(actor) {
return (
actor.system.statistics.pow.effectiveValue ??
actor.system.statistics.pow.value
);
}
function getChaRating(actor) {
return (
actor.system.statistics.cha.effectiveValue ??
actor.system.statistics.cha.value
);
}
const DEFAULT_SANITY_POW_MULTIPLIER = 5;
const THINGS_MAN_SANITY_POW_MULTIPLIER = 4;
/**
* @param {Actor} actor
* @param {number} [powMultiplier]
* @returns {number}
*/
function getSanityFromPow(
actor,
powMultiplier = DEFAULT_SANITY_POW_MULTIPLIER,
) {
const pow = getPowRating(actor);
return Math.max(0, pow * powMultiplier);
}
function buildOccultSanityStatUpdate(actor, options) {
const occult = Number(actor.system.skills.occult?.proficiency) || 0;
const sanity = Math.max(
0,
getSanityFromPow(actor, DEFAULT_SANITY_POW_MULTIPLIER) +
(options.sanityDelta ?? 0),
);
/** @type {Record<string, unknown>} */
const updateData = {
"system.skills.occult.proficiency": occult + (options.occultBonus ?? 0),
"system.sanity.value": sanity,
};
if (options.chaDelta) {
updateData["system.statistics.cha.value"] =
getChaRating(actor) + options.chaDelta;
}
if (options.powDelta) {
updateData["system.statistics.pow.value"] =
getPowRating(actor) + options.powDelta;
}
return updateData;
}
async function adjustAllBondScores(actor, delta) {
const bonds = actor.items.filter((item) => item.type === "bond");
if (!bonds.length) return;
const updates = bonds.map((bond) => ({
_id: bond.id,
system: {
score: Math.max(0, Number(bond.system.score) + delta),
},
}));
await actor.updateEmbeddedDocuments("Item", updates);
}
async function applyExtremeViolence(actor) {
const updateData = buildOccultSanityStatUpdate(actor, {
occultBonus: 10,
sanityDelta: -5,
chaDelta: -3,
});
updateData["system.sanity.adaptations.violence.incident1"] = true;
updateData["system.sanity.adaptations.violence.incident2"] = true;
updateData["system.sanity.adaptations.violence.incident3"] = true;
await actor.update(updateData);
await adjustAllBondScores(actor, -3);
}
async function applyCaptivity(actor) {
const updateData = buildOccultSanityStatUpdate(actor, {
occultBonus: 10,
sanityDelta: -5,
powDelta: -3,
});
updateData["system.sanity.adaptations.helplessness.incident1"] = true;
updateData["system.sanity.adaptations.helplessness.incident2"] = true;
updateData["system.sanity.adaptations.helplessness.incident3"] = true;
await actor.update(updateData);
}
async function applyHardExperience(actor, veteranResult, payload) {
const occultSanityUpdate = buildOccultSanityStatUpdate(actor, {
occultBonus: 10,
sanityDelta: -5,
});
const computed = applyBonusSkillPicks(
payload.fixedValues,
payload.typedValues,
{
bonusCatalogIds: veteranResult.bonusCatalogIds,
bonusTypedLabels: veteranResult.bonusTypedLabels,
},
{
count: HARD_EXPERIENCE_BONUS_COUNT,
increment: HARD_EXPERIENCE_BONUS_INCREMENT,
cap: HARD_EXPERIENCE_SKILL_CAP,
maxWaste: HARD_EXPERIENCE_BONUS_INCREMENT,
},
);
const updateData = { ...occultSanityUpdate };
for (const [key, value] of Object.entries(computed.fixedValues)) {
updateData[`system.skills.${key}.proficiency`] = value;
}
const typedSkills = { ...(actor.system.typedSkills ?? {}) };
const existingByKey = new Map();
for (const [id, skill] of Object.entries(typedSkills)) {
const storageKey = formatProfessionSkillKey({
kind: "typed",
group: skill.group,
label: skill.label,
});
existingByKey.set(storageKey, id);
}
for (const [storageKey, data] of Object.entries(computed.typedValues)) {
const existingId = existingByKey.get(storageKey);
if (existingId) {
typedSkills[existingId].proficiency = data.value;
} else {
const id = foundry.utils.randomID();
typedSkills[id] = {
label: data.label,
group: data.group,
proficiency: data.value,
failure: false,
};
}
}
updateData["system.typedSkills"] = typedSkills;
await actor.update(updateData);
}
async function applyThingsMan(actor, veteranResult) {
const sanity = getSanityFromPow(actor, THINGS_MAN_SANITY_POW_MULTIPLIER);
const occult = Number(actor.system.skills.occult?.proficiency) || 0;
const unnatural = Number(actor.system.skills.unnatural?.proficiency) || 0;
await actor.update({
"system.skills.unnatural.proficiency": unnatural + 10,
"system.skills.occult.proficiency": occult + 20,
"system.sanity.value": sanity,
});
await actor.createEmbeddedDocuments("Item", [
{
name: game.i18n.localize("DG.DamagedVeterans.ThingsMan.MotivationName"),
type: "motivation",
system: {
crossedOut: true,
disorder: veteranResult.disorder,
},
},
]);
}
export async function applyDamagedVeteranModifications(
actor,
veteranResult,
payload,
) {
if (veteranResult.path === "freshRecruit") {
return;
}
if (veteranResult.path === "extremeViolence") {
await applyExtremeViolence(actor);
return;
}
if (veteranResult.path === "captivity") {
await applyCaptivity(actor);
return;
}
if (veteranResult.path === "hardExperience") {
await applyHardExperience(actor, veteranResult, payload);
return;
}
if (veteranResult.path === "thingsMan") {
await applyThingsMan(actor, veteranResult);
}
}
export function computeHardExperienceSkillValues(payload, formState) {
return applyBonusSkillPicks(
payload.fixedValues,
payload.typedValues,
formState,
{
count: HARD_EXPERIENCE_BONUS_COUNT,
increment: HARD_EXPERIENCE_BONUS_INCREMENT,
cap: HARD_EXPERIENCE_SKILL_CAP,
maxWaste: HARD_EXPERIENCE_BONUS_INCREMENT,
},
);
}
+89
View File
@@ -0,0 +1,89 @@
/** @typedef {typeof DISORDER_OPTIONS[number]["id"]} DisorderId */
export const DISORDER_OPTIONS = /** @type {const} */ ([
{
id: "amnesia",
labelKey: "DG.DamagedVeterans.ThingsMan.Disorder.Amnesia",
},
{
id: "depersonalizationDisorder",
labelKey: "DG.DamagedVeterans.ThingsMan.Disorder.DepersonalizationDisorder",
},
{
id: "depression",
labelKey: "DG.DamagedVeterans.ThingsMan.Disorder.Depression",
},
{
id: "dissociativeIdentityDisorder",
labelKey:
"DG.DamagedVeterans.ThingsMan.Disorder.DissociativeIdentityDisorder",
},
{
id: "fugues",
labelKey: "DG.DamagedVeterans.ThingsMan.Disorder.Fugues",
},
{
id: "megalomania",
labelKey: "DG.DamagedVeterans.ThingsMan.Disorder.Megalomania",
},
{
id: "paranoia",
labelKey: "DG.DamagedVeterans.ThingsMan.Disorder.Paranoia",
},
{
id: "sleepDisorder",
labelKey: "DG.DamagedVeterans.ThingsMan.Disorder.SleepDisorder",
},
]);
/** @type {Map<string, string>} */
const LABEL_KEY_BY_ID = new Map(
DISORDER_OPTIONS.map((entry) => [entry.id, entry.labelKey]),
);
/** Legacy English display strings from earlier builds. */
/** @type {Map<string, string>} */
const LEGACY_LABEL_TO_ID = new Map([
["Amnesia", "amnesia"],
["Depersonalization Disorder", "depersonalizationDisorder"],
["Depression", "depression"],
["Dissociative Identity Disorder", "dissociativeIdentityDisorder"],
["Fugues", "fugues"],
["Megalomania", "megalomania"],
["Paranoia", "paranoia"],
["Sleep Disorder", "sleepDisorder"],
]);
/**
* @param {string} value
* @returns {string|null}
*/
export function resolveDisorderId(value) {
const trimmed = value?.trim();
if (!trimmed) return null;
if (LABEL_KEY_BY_ID.has(trimmed)) return trimmed;
return LEGACY_LABEL_TO_ID.get(trimmed) ?? null;
}
/**
* @param {string} value
* @returns {boolean}
*/
export function isKnownDisorderId(value) {
return resolveDisorderId(value) !== null;
}
/**
* @param {string} value Stored disorder id or legacy display string.
* @returns {string}
*/
export function getDisorderLabel(value) {
const trimmed = value?.trim();
if (!trimmed) return "";
const id = resolveDisorderId(trimmed) ?? trimmed;
const labelKey = LABEL_KEY_BY_ID.get(id);
if (labelKey) return game.i18n.localize(labelKey);
return trimmed;
}
+70
View File
@@ -0,0 +1,70 @@
import {
formatProfessionSkillKeyLabel,
formatProfessionSkillLabel,
} from "./keys.js";
/**
* Sorted fixed skill rows for display.
* @param {Record<string, number>} fixedValues
* @returns {{ key: string, label: string, value: number }[]}
*/
export function buildSortedFixedSkillRows(fixedValues) {
const rows = Object.entries(fixedValues).map(([key, value]) => {
const ref = /** @type {FixedSkillRef} */ ({ kind: "fixed", key });
return {
key,
label: formatProfessionSkillLabel(ref),
value,
sortLabel: formatProfessionSkillLabel(ref),
};
});
rows.sort((a, b) => a.sortLabel.localeCompare(b.sortLabel, game.i18n.lang));
return rows;
}
/**
* Sorted typed skill rows for display.
* @param {Record<string, { group: string, label: string, value: number }>} typedValues
* @returns {{ storageKey: string, label: string, value: number, sortLabel: string }[]}
*/
export function buildSortedTypedSkillRows(typedValues) {
const rows = Object.entries(typedValues).map(([storageKey, data]) => {
const ref = {
kind: "typed",
group: data.group,
label: data.label,
};
const label = formatProfessionSkillLabel(ref);
return {
storageKey,
label,
value: data.value,
sortLabel: label,
};
});
rows.sort((a, b) => a.sortLabel.localeCompare(b.sortLabel, game.i18n.lang));
return rows;
}
/**
* Prepare sorted skill rows from profession item maps.
* @param {Record<string, number>} skillMap
* @param {Record<string, ProfessionSkillMeta>} [skillMeta]
* @returns {{ key: string, label: string, rating: number }[]}
*/
export function prepareProfessionSkillRows(skillMap, skillMeta = {}) {
const rows = Object.entries(skillMap ?? {}).map(([key, rating]) => {
const label = formatProfessionSkillKeyLabel(key, {
automaticMeta: skillMeta,
optionMeta: skillMeta,
});
return {
key,
label,
rating: Number(rating),
sortLabel: label,
};
});
rows.sort((a, b) => a.sortLabel.localeCompare(b.sortLabel, game.i18n.lang));
return rows;
}
+54
View File
@@ -0,0 +1,54 @@
export {
TYPED_SKILL_TEMPLATE_GROUPS,
BONUS_SKILL_COUNT,
HARD_EXPERIENCE_BONUS_COUNT,
} from "./constants.js";
export {
buildCharacterCreationPayload,
applyCharacterCreationPayload,
commitProfessionSetup,
} from "./commit-character-creation.js";
export {
applyDamagedVeteranModifications,
computeHardExperienceSkillValues,
} from "./damaged-veteran-modifiers.js";
export {
DISORDER_OPTIONS,
getDisorderLabel,
isKnownDisorderId,
resolveDisorderId,
} from "./disorders.js";
export {
getChooseOnePlaceholderLabel,
normalizeTypedSkillName,
isChooseOnePlaceholderLabel,
isChooseOneProfessionSkillKey,
findTypedSkillNameConflict,
getAgentSkillDefaults,
normalizeTypedGroup,
getTypedGroupDisplayName,
formatProfessionSkillKey,
allocateProfessionSkillStorageKey,
parseProfessionSkillKey,
formatProfessionSkillLabel,
formatProfessionSkillKeyLabel,
splitProfessionSkillMap,
} from "./keys.js";
export {
buildSkillCatalog,
buildBonusSkillCatalog,
catalogIdToSkillRef,
collectBonusCapValidationErrors,
applyBonusSkillPicks,
getBonusTrackLabel,
} from "./catalog.js";
export { default as computeSkillValues } from "./compute.js";
export {
validateProfessionFormState,
formatProfessionValidationMessages,
} from "./validation.js";
export {
buildSortedFixedSkillRows,
buildSortedTypedSkillRows,
prepareProfessionSkillRows,
} from "./display.js";
+272
View File
@@ -0,0 +1,272 @@
import DG from "../config/index.js";
import AGENT_SKILL_DEFAULTS from "../data/actor/base/agent-skill-defaults.js";
import { PROFESSION_OPTION_PICKS_KEY } from "../data/item/profession.js";
import {
INTERNAL_TYPED_KEY_PATTERN,
TYPED_KEY_PATTERN,
TYPED_GROUP_I18N,
} from "./constants.js";
/**
* @returns {string}
*/
export function getChooseOnePlaceholderLabel() {
return game.i18n.localize("DG.Profession.ChooseOnePlaceholder");
}
/**
* @param {string} label
* @returns {string}
*/
export function normalizeTypedSkillName(label) {
return label.trim().toLocaleLowerCase(game.i18n.lang);
}
/**
* @param {string} label
* @returns {boolean}
*/
export function isChooseOnePlaceholderLabel(label) {
return (
normalizeTypedSkillName(label) ===
normalizeTypedSkillName(getChooseOnePlaceholderLabel())
);
}
/**
* @param {string} mapKey
* @param {Record<string, ProfessionSkillMeta>} automaticMeta
* @param {Record<string, ProfessionSkillMeta>} optionMeta
* @returns {boolean}
*/
export function isChooseOneProfessionSkillKey(
mapKey,
automaticMeta = {},
optionMeta = {},
) {
return Boolean(
automaticMeta[mapKey]?.chooseOne ?? optionMeta[mapKey]?.chooseOne,
);
}
/**
* @returns {Record<string, number>}
*/
export function getAgentSkillDefaults() {
return { ...AGENT_SKILL_DEFAULTS };
}
/**
* @param {string} group
* @returns {string}
*/
export function normalizeTypedGroup(group) {
if (group == null) return "";
const trimmed = String(group).trim();
const map = {
"Foreign Language": "ForeignLanguage",
"Military Science": "MilitaryScience",
};
return map[trimmed] ?? trimmed.replace(/\s+/g, "");
}
/**
* @param {string} key
* @returns {ProfessionSkillRef | null}
*/
export function parseProfessionSkillKey(key) {
if (!key || key === PROFESSION_OPTION_PICKS_KEY) return null;
if (DG.skills.includes(key)) return { kind: "fixed", key };
const internalMatch = key.match(INTERNAL_TYPED_KEY_PATTERN);
if (internalMatch) {
let label = "";
if (internalMatch[3] !== undefined) {
try {
label = decodeURIComponent(internalMatch[3]);
} catch {
const [, , , fallbackLabel] = internalMatch;
label = fallbackLabel;
}
}
return {
kind: "typed",
group: internalMatch[1],
label,
};
}
const match = key.match(TYPED_KEY_PATTERN);
if (match) {
return {
kind: "typed",
group: normalizeTypedGroup(match[1]),
label: match[2].trim(),
};
}
return null;
}
/**
* @param {object} params
* @param {string} params.group
* @param {string} params.label
* @param {boolean} params.chooseOne
* @param {Record<string, number>} params.automaticSkills
* @param {Record<string, number>} params.optionSkills
* @param {Record<string, ProfessionSkillMeta>} [params.automaticMeta]
* @param {Record<string, ProfessionSkillMeta>} [params.optionMeta]
* @param {string} [params.excludeKey]
* @returns {string | null} Conflicting map key, if any.
*/
export function findTypedSkillNameConflict({
group,
label,
chooseOne,
automaticSkills,
optionSkills,
automaticMeta = {},
optionMeta = {},
excludeKey = null,
}) {
// Multiple choose-one slots per category are allowed on the profession item.
if (chooseOne) return null;
if (!group) return null;
const normalizedGroup = normalizeTypedGroup(group);
const normalizedNew = normalizeTypedSkillName(label);
if (!normalizedNew) return "__empty__";
const maps = [
[automaticSkills, automaticMeta],
[optionSkills, optionMeta],
];
for (const [skillMap, metaMap] of maps) {
for (const mapKey of Object.keys(skillMap ?? {})) {
if (mapKey !== excludeKey && mapKey !== PROFESSION_OPTION_PICKS_KEY) {
const ref = parseProfessionSkillKey(mapKey);
if (
ref &&
ref.kind === "typed" &&
normalizeTypedGroup(ref.group) === normalizedGroup &&
!metaMap[mapKey]?.chooseOne &&
normalizeTypedSkillName(ref.label) === normalizedNew
) {
return mapKey;
}
}
}
}
return null;
}
/**
* @param {string} group
* @returns {string}
*/
export function getTypedGroupDisplayName(group) {
const key = normalizeTypedGroup(group);
const i18nKey = TYPED_GROUP_I18N[key];
if (i18nKey) return game.i18n.localize(i18nKey);
return group;
}
/**
* @param {ProfessionSkillRef} ref
* @returns {string}
*/
export function formatProfessionSkillKey(ref) {
if (ref.kind === "fixed") return ref.key;
const groupLabel = getTypedGroupDisplayName(ref.group);
return `${groupLabel} (${ref.label.trim()})`;
}
/**
* Storage key for a profession skill map entry.
*
* @param {ProfessionSkillRef} ref
* @param {{ chooseOne?: boolean }} [options]
* @returns {string}
*/
export function allocateProfessionSkillStorageKey(
ref,
{ chooseOne = false } = {},
) {
if (ref.kind === "fixed") return ref.key;
if (chooseOne) {
return `typed:${normalizeTypedGroup(ref.group)}:${foundry.utils.randomID(
8,
)}`;
}
const label = ref.label?.trim() ?? "";
return formatProfessionSkillKey({
kind: "typed",
group: ref.group,
label,
});
}
/**
* @param {ProfessionSkillRef} ref
* @returns {string}
*/
export function formatProfessionSkillLabel(ref) {
if (ref.kind === "fixed") {
if (game.i18n.lang === "ja") {
const ja = game.i18n.localize(`DG.Skills.ruby.${ref.key}`);
if (ja && ja !== `DG.Skills.ruby.${ref.key}`) return ja;
}
const label = game.i18n.localize(`DG.Skills.${ref.key}`);
if (label && label !== `DG.Skills.${ref.key}`) return label;
return ref.key;
}
if (!ref.label?.trim()) return getTypedGroupDisplayName(ref.group);
return formatProfessionSkillKey(ref);
}
/**
* @param {string} key
* @param {object} [metaContext]
* @param {Record<string, ProfessionSkillMeta>} [metaContext.automaticMeta]
* @param {Record<string, ProfessionSkillMeta>} [metaContext.optionMeta]
* @returns {string}
*/
export function formatProfessionSkillKeyLabel(
key,
{ automaticMeta = {}, optionMeta = {} } = {},
) {
const ref = parseProfessionSkillKey(key);
if (!ref) return key;
if (
ref.kind === "typed" &&
isChooseOneProfessionSkillKey(key, automaticMeta, optionMeta)
) {
return formatProfessionSkillKey({
kind: "typed",
group: ref.group,
label: getChooseOnePlaceholderLabel(),
});
}
if (ref.kind === "typed" && !ref.label?.trim()) {
return getTypedGroupDisplayName(ref.group);
}
return formatProfessionSkillLabel(ref);
}
/**
* @param {Record<string, number>} obj
* @returns {{ optionPicks: number, skills: Record<string, number> }}
*/
export function splitProfessionSkillMap(obj) {
const source = obj ?? {};
const optionPicks = Number(source[PROFESSION_OPTION_PICKS_KEY]) || 0;
/** @type {Record<string, number>} */
const skills = {};
for (const [key, value] of Object.entries(source)) {
if (key !== PROFESSION_OPTION_PICKS_KEY) {
skills[key] = Number(value);
}
}
return { optionPicks, skills };
}
+112
View File
@@ -0,0 +1,112 @@
import DG from "../config/index.js";
import { showDiceSoNicePooledRolls } from "../integrations/dice-so-nice.js";
export const STAT_KEYS = DG.statistics;
export const POINT_BUY_TOTAL = 72;
export const STAT_MIN = 3;
export const STAT_MAX = 18;
export const ROLL_STAT_FORMULA = "4d6dl";
/**
* @param {string} key
* @returns {string}
*/
export function getStatisticLabel(key) {
return game.i18n.localize(`DG.Attributes.${key}`);
}
/**
* @returns {{ key: string, label: string }[]}
*/
export function buildStatisticRows() {
return STAT_KEYS.map((key) => ({
key,
label: getStatisticLabel(key),
}));
}
/**
* @param {Actor} actor
* @param {Record<string, number>} valuesByKey
* @returns {Promise<void>}
*/
export async function applyAgentStatistics(actor, valuesByKey) {
const updateData = {};
for (const key of STAT_KEYS) {
updateData[`system.statistics.${key}.value`] = valuesByKey[key];
}
await actor.update(updateData);
}
/**
* @param {Record<string, number>} values
* @returns {{ isValid: boolean, remaining: number }}
*/
export function validatePointBuyValues(values) {
let sum = 0;
for (const key of STAT_KEYS) {
const value = Number(values[key]);
if (!Number.isInteger(value) || value < STAT_MIN || value > STAT_MAX) {
return { isValid: false, remaining: POINT_BUY_TOTAL - sum };
}
sum += value;
}
const remaining = POINT_BUY_TOTAL - sum;
return { isValid: remaining === 0, remaining };
}
/**
* @param {object} [options]
* @param {Actor|null} [options.actor]
* @param {TokenDocument|null} [options.token]
* @param {boolean} [options.animate=true]
* @returns {Promise<{ total: number, roll: Roll }[]>}
*/
export async function rollStatisticScores({
actor = null,
token = null,
animate = true,
} = {}) {
const rollPromises = [];
for (let i = 0; i < STAT_KEYS.length; i++) {
rollPromises.push(new Roll(ROLL_STAT_FORMULA).evaluate());
}
const rolls = await Promise.all(rollPromises);
if (animate) {
await showDiceSoNicePooledRolls(rolls, { actor, token });
}
return rolls.map((roll, index) => ({
index,
total: roll.total,
roll,
}));
}
/**
* @param {{ total: number }[]} rolls
* @returns {string}
*/
export function buildRollStatsChatContent(rolls) {
const cells = rolls.map(
(entry, index) =>
`<td class="roll-stats-chat-value roll-stats-chat-value--${
index % 2 === 0 ? "a" : "b"
}">${foundry.utils.escapeHTML(String(entry.total))}</td>`,
);
const rows = [];
for (let i = 0; i < cells.length; i += 3) {
rows.push(`<tr>${cells.slice(i, i + 3).join("")}</tr>`);
}
return `<table class="roll-stats-chat-table"><tbody>${rows.join(
"",
)}</tbody></table>`;
}
/**
* @returns {Record<string, number>}
*/
export function getDefaultPointBuyValues() {
return Object.fromEntries(STAT_KEYS.map((key) => [key, STAT_MIN]));
}
+217
View File
@@ -0,0 +1,217 @@
import { BONUS_SKILL_COUNT } from "./constants.js";
import { catalogIdToSkillRef, getBonusTrackLabel } from "./catalog.js";
import {
getTypedGroupDisplayName,
normalizeTypedGroup,
normalizeTypedSkillName,
parseProfessionSkillKey,
} from "./keys.js";
/**
* @param {number} optionPicks
* @param {object} formState
* @param {object} [context]
* @param {Record<string, number>} [context.automaticSkills]
* @param {Record<string, number>} [context.optionSkills]
* @param {Record<string, ProfessionSkillMeta>} [context.automaticMeta]
* @param {Record<string, ProfessionSkillMeta>} [context.optionMeta]
* @param {number} [context.bondCount]
* @returns {string[]}
*/
export function validateProfessionFormState(
optionPicks,
formState,
context = {},
) {
const errors = [];
const checked = formState.checkedOptionKeys ?? new Set();
const picks = Number(optionPicks) || 0;
const {
automaticSkills = {},
automaticMeta = {},
optionMeta = {},
bondCount = 0,
} = context;
if (checked.size !== picks) {
errors.push("optionPicks");
}
for (let i = 0; i < bondCount; i++) {
if (!formState.bondNames?.[i]?.trim()) errors.push(`bondName${i}`);
if (!formState.bondRelationships?.[i]?.trim()) {
errors.push(`bondRelationship${i}`);
}
}
for (let i = 0; i < BONUS_SKILL_COUNT; i++) {
const catalogId = formState.bonusCatalogIds?.[i];
if (!catalogId) {
errors.push(`bonus${i}`);
} else {
const ref = catalogIdToSkillRef(
catalogId,
formState.bonusTypedLabels?.[i] ?? "",
);
if (!ref) {
errors.push(`bonus${i}`);
} else if (
ref.kind === "typed" &&
!formState.bonusTypedLabels?.[i]?.trim()
) {
errors.push(`bonusType${i}`);
}
}
}
/** @type {Map<string, Set<string>>} */
const resolvedByGroup = new Map();
const registerResolvedName = (ref, rawLabel) => {
const label = rawLabel?.trim();
if (!label) {
errors.push(`typedNameRequired:${ref.group}`);
return;
}
const group = normalizeTypedGroup(ref.group);
const normalized = normalizeTypedSkillName(label);
if (!resolvedByGroup.has(group)) resolvedByGroup.set(group, new Set());
const names = resolvedByGroup.get(group);
if (names.has(normalized)) {
errors.push(`typedNameConflict:${group}:${label}`);
return;
}
names.add(normalized);
};
for (const [key] of Object.entries(automaticSkills)) {
const ref = parseProfessionSkillKey(key);
if (ref && ref.kind === "typed") {
if (automaticMeta[key]?.chooseOne) {
registerResolvedName(ref, formState.chooseOneLabels?.[key]);
} else {
registerResolvedName(ref, ref.label);
}
}
}
for (const key of checked) {
const ref = parseProfessionSkillKey(key);
if (ref && ref.kind === "typed") {
if (optionMeta[key]?.chooseOne) {
registerResolvedName(ref, formState.chooseOneLabels?.[key]);
} else {
registerResolvedName(ref, ref.label);
}
}
}
return errors;
}
/**
* User-facing messages for Add Profession validation error codes.
* @param {string[]} errors
* @param {object} [context]
* @param {number} [context.optionPicks]
* @returns {string[]}
*/
export function formatProfessionValidationMessages(
errors,
{ optionPicks = 0, bonusCount = BONUS_SKILL_COUNT, skillCap = 80 } = {},
) {
const messages = [];
const seen = new Set();
const push = (msg) => {
if (seen.has(msg)) return;
seen.add(msg);
messages.push(msg);
};
let needsOptionPicks = false;
let needsBondName = false;
let needsBondRelationship = false;
let needsBonusSkill = false;
let needsBonusSkillType = false;
/** @type {Set<string>} */
const typedNameRequiredGroups = new Set();
/** @type {Set<string>} */
const bonusWasteTrackKeys = new Set();
for (const code of errors) {
if (code === "optionPicks") {
needsOptionPicks = true;
} else if (code.startsWith("bondName")) {
needsBondName = true;
} else if (code.startsWith("bondRelationship")) {
needsBondRelationship = true;
} else {
const bonusWaste = code.match(/^bonusWaste:\d+\|(\d+)\|(.+)$/);
if (bonusWaste) {
bonusWasteTrackKeys.add(bonusWaste[2]);
} else if (code.startsWith("bonusType")) {
needsBonusSkillType = true;
} else if (/^bonus\d+$/.test(code)) {
needsBonusSkill = true;
} else {
const typedNameRequired = code.match(/^typedNameRequired:(.+)$/);
if (typedNameRequired) {
typedNameRequiredGroups.add(typedNameRequired[1]);
} else {
const conflict = code.match(/^typedNameConflict:([^:]+):(.+)$/);
if (conflict) {
const [, group, name] = conflict;
push(
game.i18n.format("DG.Profession.Dialog.DuplicateTypedName", {
type: getTypedGroupDisplayName(group),
name,
}),
);
}
}
}
}
}
if (needsOptionPicks) {
push(
game.i18n.format("DG.Profession.Dialog.OptionPicksRequired", {
picks: optionPicks,
}),
);
}
if (needsBonusSkill) {
push(
game.i18n.format("DG.Profession.Dialog.BonusSkillRequired", {
count: bonusCount,
}),
);
}
if (needsBonusSkillType) {
push(game.i18n.localize("DG.Profession.Dialog.BonusSkillTypeRequired"));
}
for (const group of typedNameRequiredGroups) {
push(
game.i18n.format("DG.Profession.Dialog.TypedNameRequired", {
type: getTypedGroupDisplayName(group),
}),
);
}
if (needsBondName) {
push(game.i18n.localize("DG.Profession.Dialog.BondNameRequired"));
}
if (needsBondRelationship) {
push(game.i18n.localize("DG.Profession.Dialog.BondRelationshipRequired"));
}
for (const trackKey of bonusWasteTrackKeys) {
push(
game.i18n.format("DG.Profession.Dialog.BonusSkillWasteTooHigh", {
skill: getBonusTrackLabel(trackKey),
}),
);
}
return messages;
}
+65
View File
@@ -0,0 +1,65 @@
/** @internal Import roll subclasses only from ../roll.js. */
/* eslint-disable import/prefer-default-export */
import { showDamageRollModifyDialog } from "../roll-dialogs.js";
import { DGRoll } from "./dg-roll.js";
const { renderTemplate } = foundry.applications.handlebars;
export class DGDamageRoll extends DGRoll {
/**
* @returns {{ rollLabel: string }}
*/
createChatHeader() {
try {
const armorPiercingTooltip = game.i18n.localize("DG.Gear.ArmorPiercing");
const subtitle = `<span class="dg-chat-card__armor-piercing" data-tooltip-text="${foundry.utils.escapeHTML(
armorPiercingTooltip,
)}">(<b>${
this.item.system.armorPiercing
}</b><img class="armor-piercing-chat-card-img" src="systems/deltagreen/assets/icons/supersonic-bullet.svg" alt=""/>)</span>`;
const title = `${game.i18n.localize("DG.Roll.Rolling")} <b>${game.i18n
.localize("DG.Roll.Damage")
.toUpperCase()}</b> ${game.i18n.localize("DG.Roll.For")} <b>${
this.item.name
}</b>`;
return {
rollLabel: `${title}: ${subtitle}`,
};
} catch (ex) {
return {
rollLabel: `${game.i18n.localize(
"DG.Roll.RollingDamageFor",
)} <b>${this.formula.toUpperCase()}</b>`,
};
}
}
/**
* Prepares data for a chat message and then passes that data
* to a method that actually creates a ChatMessage.
*
* @returns {Promise<ChatMessage>} - the created chat message.
* @override
*/
async toChat() {
const { rollLabel } = this.createChatHeader();
const html = await renderTemplate(
"systems/deltagreen/templates/roll/damage-roll.hbs",
{
formula: this.formula,
total: this.total,
},
);
return this.toMessage({ content: html, rollLabel });
}
async showDialog() {
return showDamageRollModifyDialog({
itemName: this.item?.name,
formula: this.formula,
});
}
}
+132
View File
@@ -0,0 +1,132 @@
/** @internal Import roll subclasses only from ../roll.js. */
/* eslint-disable import/prefer-default-export */
import { buildRollTargetDisplayHtml } from "../../utils/roll-target-tooltip.js";
import { DGPercentileRoll } from "./dg-percentile-roll.js";
const { renderTemplate } = foundry.applications.handlebars;
export class DGLethalityRoll extends DGPercentileRoll {
/**
* See constructor for DGPercentileRoll. This theoretically could be done in the parent class'
* constructor, but since Lethality rolls needs its own class for custom methods anyway,
* we will set the target and localized key here.
*
* @param {String} formula
* @param {Object} data
* @param {Object} options
*/
constructor(formula, data, options) {
super(formula, data, options);
this.target = options.item.system.lethality;
this.localizedKey = game.i18n.localize("DG.ItemWindow.Weapons.Lethality");
}
/**
* Prepares data for a chat message and then passes that data
* to a method that actually creates a ChatMessage.
*
* Lays out and styles message based on outcome of the roll.
*
* Overrides `DGPercentileRoll.toChat()`
*
* @returns {Promise<ChatMessage>} - the created chat message.
* @override
*/
async toChat() {
let resultString = "";
let styleOverride = "";
if (this.total <= this.target) {
resultString = `${game.i18n.localize("DG.Roll.Lethal").toUpperCase()}`;
styleOverride = "color: red";
} else {
resultString = `${game.i18n.localize("DG.Roll.Failure")}`;
}
const { nonLethalDamage } = this;
const { rollLabel } = this.createChatHeader();
const html = await renderTemplate(
"systems/deltagreen/templates/roll/lethality-roll.hbs",
{
styleOverride,
resultString,
total: this.total,
die1: nonLethalDamage.die1,
die2: nonLethalDamage.die2,
nonLethalTotal: nonLethalDamage.total,
damageLabel: game.i18n.localize("DG.Roll.Damage"),
},
);
return this.toMessage({ content: html, rollLabel });
}
/**
* @returns {{ rollLabel: string }}
*/
createChatHeader() {
const base = Number(this.target) || 0;
const manualModifier = Number(this.modifier) || 0;
const effectiveTarget = base + manualModifier;
const targetLine = buildRollTargetDisplayHtml({
targetValue: `${effectiveTarget}%`,
base,
manualModifier,
finalTarget: effectiveTarget,
});
const title = `${game.i18n.localize("DG.Roll.Rolling")} <b>${game.i18n
.localize("DG.Roll.Lethality")
.toUpperCase()}</b> ${game.i18n.localize(
"DG.Roll.For",
)} <b>${this.item.name.toUpperCase()}</b>`;
return {
rollLabel: `${title}: ${targetLine}`,
};
}
/**
* Calculates the damage for when a lethality roll fails.
* If roll has not been evaluated, return null.
*
* See full rules on page 57 of agent's handbook.
*
* Note, this getter does not actually care if the roll has failed.
*
* @returns {null|Object} - return data about the non-lethal damage.
*/
get nonLethalDamage() {
if (!this.total) {
return null;
}
// Try to determine what the d100 result would be as if it was two d10's being rolled.
const totalString = this.total.toString();
const digits = totalString.length;
let die1;
let die2;
switch (digits) {
case 1:
// If one digit in the result, one die is a 10, and the other is the result.
[die1, die2] = [10, this.total];
break;
case 2:
// If two digits in the result, each die is the value of one of the digits. If one of those digits is 0, make it 10.
[die1, die2] = totalString
.split("")
.map((digit) => parseInt(digit))
.map((digit) => digit || 10);
break;
case 3:
// If three digits in the result (aka result === 100), each die is a 10.
[die1, die2] = [10, 10];
break;
default:
break;
}
const total = die1 + die2;
return { die1, die2, total };
}
}
+437
View File
@@ -0,0 +1,437 @@
/** @internal Import roll subclasses only from ../roll.js. */
/* eslint-disable import/prefer-default-export */
import DG from "../../config/index.js";
import { showPercentileRollModifyDialog } from "../roll-dialogs.js";
import {
isDiceSoNiceAvailable,
waitForDiceSoNiceMessageAnimation,
} from "../../integrations/dice-so-nice.js";
import {
clampPercentileRollTarget,
getRollTargetDisplayClassFromModifier,
} from "../../active-effect/runtime/derived.js";
import { formatProfessionSkillLabel } from "../../profession/index.js";
import { buildRollTargetDisplayHtml } from "../../utils/roll-target-tooltip.js";
import { DGRoll } from "./dg-roll.js";
const { renderTemplate } = foundry.applications.handlebars;
export class DGPercentileRoll extends DGRoll {
/**
* Creates D100 rolls, the base die of the system.
*
* This constructor embeds the following info into the roll:
* 1. Target number that the roll needs to beat.
* 2. Localized name for the roll.
*
* Note: In order for all of our custom data to persist, our constructor must use the same parameters as its parent class.
* So, even though percentile rolls will always have a formula of "1d100" and we don't use the `data` object,
* we still have to keep them as parameters.
*
* @param {string} formula Unused - The string formula to parse (from Foundry) - Always "1d100" for percentile rolls.
* @param {Object} data Unused - The data object against which to parse attributes within the formula
* @param {Object} [options] Additional data which is preserved in the database
* @param {Number} [options.rollType] The type of roll (stat, skill, sanity, etc).
* @param {String} [options.key] The key of the skill, stat, etc. to use as a basis for this roll.
* @param {DeltaGreenActor} [options.actor] The actor that this roll originates from.
* @param {DeltaGreenItem} [options.item] Optional - The item from which the roll originates.
* @param {DeltaGreenItem} [options.specialTrainingName] Optional - Special training rolls have names that are different from the roll key.
*/
// eslint-disable-next-line default-param-last, no-unused-vars
constructor(formula = "1D100", data = {}, options) {
super("1D100", {}, options);
// Set roll info for Skill, Stat, Typed Skill, and non-custom Weapon Percentile rolls.
const { target, localizedKey, skillPath } = this.getRollInfoFromKey();
this.target = target;
this.localizedKey = localizedKey;
this.skillPath = skillPath;
// Set roll info for other Percentile rolls
switch (this.type) {
case "special-training":
this.specialTrainingName = options.specialTrainingName;
this.localizedKey = `${this.specialTrainingName} - (${this.localizedKey})`;
break;
case "weapon":
// If this weapon uses a custom target for rolls, we set that explicitly.
if (this.key === "custom") {
this.target = this.item.system.customSkillTarget;
this.localizedKey = game.i18n.localize("DG.ItemWindow.Custom");
}
// Add a the weapon's internal modifier.
this.modifier += this.item.system.skillModifier;
break;
case "sanity":
this.target = this.actor.system.sanity.value;
this.localizedKey = game.i18n.localize("DG.Attributes.SAN");
break;
case "luck":
this.target = 50;
this.localizedKey = game.i18n.localize("DG.Luck");
break;
default:
break;
}
}
/**
* Shows a dialog that can modify the roll.
*
* @returns {Promise<Object|void>} - the results of the dialog.
*/
async showDialog() {
const privateSanSetting = game.settings.get(
"deltagreen",
"keepSanityPrivate",
);
let hideSanTarget = false;
if (
privateSanSetting &&
(this.type === "sanity" || this.key === "ritual") &&
!game.user.isGM
) {
hideSanTarget = true;
}
let customModifierTarget = 20;
if (this.actor != null) {
try {
customModifierTarget = parseInt(
this.actor.system.settings.rolling.defaultPercentileModifier,
);
} catch {
// do nothing
}
}
const { rollTargetModifier } = this;
const targetDisplayClass =
getRollTargetDisplayClassFromModifier(rollTargetModifier);
return showPercentileRollModifyDialog({
label: this.localizedKey,
target: this.target,
targetDisplayClass,
hideTarget: hideSanTarget,
defaultModifier: customModifierTarget,
actor: this.actor,
});
}
/**
* Prepares data for a chat message and then passes that data
* to a method that actually creates a ChatMessage.
*
* Lays out and styles message based on outcome of the roll.
*
* @returns {Promise<ChatMessage>} - the created chat message.
*/
async toChat() {
// if using private san rolls, must hide any SAN roll unless user is a GM
const privateSanSetting = game.settings.get(
"deltagreen",
"keepSanityPrivate",
);
if (
privateSanSetting &&
(this.type === "sanity" || this.key === "ritual") &&
!game.user.isGM
) {
this.options.messageMode = "blind";
}
const { rollLabel } = this.createChatHeader();
let resultString = "";
let styleOverride = "";
if (this.isSuccess) {
if (this.isCritical) {
resultString = `${game.i18n.localize("DG.Roll.CriticalSuccess")}`;
resultString = `${resultString.toUpperCase()}`;
styleOverride = "color: green";
} else {
resultString = `${game.i18n.localize("DG.Roll.Success")}`;
}
} else if (this.isCritical) {
resultString = `${game.i18n.localize("DG.Roll.CriticalFailure")}`;
resultString = `${resultString.toUpperCase()}`;
styleOverride = "color: red";
} else {
resultString = `${game.i18n.localize("DG.Roll.Failure")}`;
}
const failureMark =
this.actor?.type === "agent" &&
!this.isSuccess &&
this.skillPath &&
this.key !== "unnatural" &&
!foundry.utils.getProperty(this.actor, `${this.skillPath}.failure`) &&
game.settings.get(DG.ID, "skillFailure");
const html = await renderTemplate(
"systems/deltagreen/templates/roll/percentile-roll.hbs",
{
styleOverride,
resultString,
formula: this.formula,
total: this.total,
failureMark,
},
);
// TODO: add setting for it?
if (failureMark) {
const keyForUpdate = `${this.skillPath}.failure`;
const diceSoNice = isDiceSoNiceAvailable();
const message = await this.toMessage({
flags: {
deltagreen: {
rollbacks: {
[keyForUpdate]: false,
},
},
},
content: html,
rollLabel,
});
if (diceSoNice) {
await waitForDiceSoNiceMessageAnimation(message.id);
}
// TODO: auto-update actor or post icon with manual apply
await this.actor.update({
[keyForUpdate]: true,
});
return message;
}
return this.toMessage({ content: html, rollLabel });
}
/**
* Resolve roll target, localized label, and skill path from `this.key` and `this.actor`.
*
* Used for Stat, Skill, Typed Skill, Weapon, and Special Training Rolls.
*
* @returns {{ target: number|null, localizedKey: string|null, skillPath: string|null }}
*/
getRollInfoFromKey() {
const actorData = this.actor.system;
const skillKeys = Object.keys(actorData.skills);
const typedSkillKeys = Object.keys(actorData.typedSkills);
const statKeys = Object.keys(actorData.statistics);
let target = null;
let localizedKey = null;
let skillPath = null; // For optimization of failure checks
if (statKeys.includes(this.key)) {
target = actorData.statistics[this.key].x5;
localizedKey = game.i18n.localize(`DG.Attributes.${this.key}`);
}
if (skillKeys.includes(this.key)) {
// use calculated target proficiency (effects and etc like aim + 20%)
target =
actorData.skills[this.key].targetProficiency ||
actorData.skills[this.key].proficiency;
localizedKey = game.i18n.localize(`DG.Skills.${this.key}`);
skillPath = `system.skills.${this.key}`;
}
if (typedSkillKeys.includes(this.key)) {
const skill = actorData.typedSkills[this.key];
target = skill.targetProficiency ?? skill.proficiency;
localizedKey = formatProfessionSkillLabel({
kind: "typed",
group: skill.group,
label: skill.label,
});
skillPath = `system.typedSkills.${this.key}`;
}
if (this.key === "ritual") {
target = actorData.sanity.ritual;
localizedKey = game.i18n.localize(`DG.Skills.ritual`);
}
return { target, localizedKey, skillPath };
}
/**
* i18n key for a user-facing warning when this roll must not be made, or null if allowed.
* @returns {string|null}
*/
get blockedRollMessage() {
if (this.type !== "stat") return null;
const x5 = Number(this.target);
if (!Number.isFinite(x5) || x5 < 1) {
return "DG.Roll.CannotRollStat";
}
return null;
}
/**
* Active-effect roll target field for this roll type (excludes luck).
* @returns {"system.rollTarget.allSkills"|"system.rollTarget.sanity"|"system.rollTarget.statistics"|null}
*/
get rollTargetFieldKey() {
if (this.type === "luck") return null;
if (this.type === "sanity") return "system.rollTarget.sanity";
if (this.type === "stat") return "system.rollTarget.statistics";
return "system.rollTarget.allSkills";
}
/**
* Create the roll label shown below the card header.
*
* @returns {{ rollLabel: string }}
*/
createChatHeader() {
let displayKey = this.localizedKey;
if (
this.type === "weapon" &&
this.item?.name &&
this.key !== "unarmed_combat"
) {
displayKey = `${this.localizedKey} (${this.item.name})`;
}
const title = this.isInhuman
? `<b>${displayKey} [${game.i18n
.localize("DG.Roll.Inhuman")
.toUpperCase()}]</b>`
: `<b>${displayKey}</b>`;
const targetValue = this.isInhuman
? `${this.effectiveTarget}`
: `${this.effectiveTarget}%`;
const base = Number(this.target) || 0;
const targetLine = buildRollTargetDisplayHtml({
targetValue,
actor: this.actor,
rollTargetFieldKey: this.rollTargetFieldKey,
base,
manualModifier: this.modifier,
finalTarget: this.effectiveTarget,
allowOver99: this.target > 99 && this.type === "stat",
});
return { rollLabel: `${title}: ${targetLine}` };
}
/**
* Active Effect roll-target modifier for this roll type (excludes luck).
* @returns {number}
*/
get rollTargetModifier() {
if (this.type === "luck") return 0;
try {
const { rollTarget } = this.actor.system;
if (!rollTarget) return 0;
if (this.type === "sanity") return Number(rollTarget.sanity) || 0;
if (this.type === "stat") return Number(rollTarget.statistics) || 0;
return Number(rollTarget.allSkills) || 0;
} catch {
return 0;
}
}
/**
* "Inhuman" stat being rolled, logic is different per page 188 of the Handler's Guide.
* Note - originally implemented by Uriele, but my attempt at merging conficts went poorly, so re-implementing.
* For an inhuman check, the roll succeeds except on a roll of 100 which fails AND fumbles.
* If the roll is a matching digit roll, it is a critical as normal.
* Also, if the roll is below the regular (non-x5) value of the stat, it is a critical. E.g. a CON of 25, a d100 roll of 21 would be a critical.
*
* @returns {Boolean}
*/
get isInhuman() {
/*
Changing this to only consider the base x5 stat target for whether something is 'inhuman'
because I do not think the intent was an Agent with a high strength getting a +40% bonus to be considered 'inhuman'
and therefore benefit from the increased crit threshold, although could be wrong about this.
*/
if (this.target > 99 && this.type === "stat") {
return true;
}
return false;
}
/**
* Determines if a roll result is critical.
* If roll has not been evaluated, return null.
*
* @returns {null|Boolean}
*/
get isCritical() {
// If roll isn't evaluated, return null.
if (!this.total) {
return null;
}
let isCritical = false;
// 1, 100, or any matching dice are a crit, i.e. 11, 22, 33...99.
if (this.total === 1 || this.total === 100 || this.total % 11 === 0) {
// really good, or reeaaaally bad
isCritical = true;
}
// If inhuman and the roll is below the regular (non-x5) value of the stat, it is a critical.
// E.g. a CON of 25, a d100 roll of 21 would be a critical.
if (this.isInhuman && this.total <= this.target / 5) {
isCritical = true;
}
return isCritical;
}
/**
* Determines if a roll succeeded.
* If roll has not been evaluated, return null.
*
* @returns {null|Boolean}
*/
get isSuccess() {
// If roll isn't evaluated, return null.
if (!this.total) {
return null;
}
// A roll of 100 always (critically) fails, even for inhuman rolls.
if (this.total === 100) return false;
return this.total <= this.effectiveTarget;
}
/**
* Actual target for the roll accounting for Active Effects and manual modifiers.
* All modifiers are summed on the base target, then clamped to 1–99 (or above 99 for inhuman stats).
*
* @returns {null|integer}
*/
get effectiveTarget() {
if (this.target == null || Number.isNaN(this.target)) {
return null;
}
const base = parseInt(this.target);
const rollTargetModifier = Number(this.rollTargetModifier) || 0;
const manualModifier =
this.modifier && !Number.isNaN(this.modifier)
? parseInt(this.modifier)
: 0;
const totalModifier = rollTargetModifier + manualModifier;
if (!totalModifier) return base;
return clampPercentileRollTarget(base, totalModifier, {
allowOver99: this.target > 99 && this.type === "stat",
});
}
}
+83
View File
@@ -0,0 +1,83 @@
/** @internal Import roll subclasses only from ../roll.js. */
/* eslint-disable import/prefer-default-export */
import {
createDGRollChatMessage,
prepareDGRollChatMessageData,
} from "../../chat/dg-chat-card.js";
export class DGRoll extends Roll {
/**
* NOTE: This class will rarely be called on its own. It should generally be extended. Look to DGPercentileRoll as an example.
*
* Customize our roll with some useful information, passed in the `options` Object.
*
* @param {string} formula Unused - The string formula to parse (from Foundry)
* @param {Object} data Unused - The data object against which to parse attributes within the formula
* @param {Object} [options] Additional data which is preserved in the database
* @param {Number} [options.rollType] The type of roll (stat, skill, sanity, damage, etc).
* @param {String} [options.key] The key of the skill, stat, etc. to use as a basis for this roll.
* @param {DeltaGreenActor} [options.actor] The actor that this roll originates from.
* @param {DeltaGreenItem} [options.item] Optional - The item from which the roll originates.
*/
constructor(formula, data = {}, options = {}) {
super(formula, data, options);
const { rollType, key, actor, item } = options;
this.type = rollType;
this.key = key;
this.actor = actor;
this.item = item;
this.modifier = 0;
}
/**
* Posts a roll to chat with the Delta Green card shell and an explicit `rolls`
* array for Dice So Nice (listen-path compatible).
*
* @override
* The following `@param` descriptions comes from the Foundry VTT code.
* @param {object} messageData The data object to use when creating the message
* @param {options} [options] Additional options which modify the created message.
* @param {string} [options.messageMode] A key of CONFIG.ChatMessage.modes
* @param {boolean} [options.create=true] Whether to automatically create the chat message, or only return the
* prepared chatData object.
* @returns {Promise<ChatMessage|object>} A promise which resolves to the created ChatMessage document if create is
* true, or the Object of prepared chatData otherwise.
*/
async toMessage(messageData = {}, { messageMode, create = true } = {}) {
const { title, subtitle, rollLabel, label, flavor } = messageData;
const resolvedLabel = label ?? flavor;
delete messageData.title;
delete messageData.subtitle;
delete messageData.rollLabel;
delete messageData.label;
const mode =
messageMode ?? this.options.messageMode ?? this.options.rollMode;
const chatParams = {
roll: this,
actor: this.actor,
token: this.options.token,
title,
subtitle,
rollLabel,
label: resolvedLabel,
content: messageData.content,
messageMode: mode,
flags: messageData.flags ?? {},
};
if (create) {
return createDGRollChatMessage(chatParams);
}
const { messageData: prepared, mappedMode } =
await prepareDGRollChatMessageData(chatParams);
const cls = foundry.utils.getDocumentClass("ChatMessage");
// eslint-disable-next-line new-cap -- Foundry document class resolved at runtime
const msg = new cls(prepared);
msg.applyMode(mappedMode);
return msg.toObject();
}
}
@@ -0,0 +1,60 @@
/** @internal Import roll subclasses only from ../roll.js. */
/* eslint-disable import/prefer-default-export */
import DGUtils from "../../utils/utility-functions.js";
import { DGRoll } from "./dg-roll.js";
const { renderTemplate } = foundry.applications.handlebars;
export class DGSanityDamageRoll extends DGRoll {
/**
* Prepares data for a chat message and then passes that data
* to a method that actually creates a ChatMessage.
*
* @returns {Promise<ChatMessage>} - the created chat message.
* @override
*/
async toChat() {
const [lowDie, highDie] = this.terms[0].terms.map((formula) => {
return Roll.parse(formula)[0] || { faces: parseInt(formula), number: 1 };
});
const [lowResult, highResult] = this.damageResults;
const rollLabel = `${game.i18n.localize(
"DG.Roll.Rolling",
)} <b>${DGUtils.localizeWithFallback(
"DG.Generic.SanDamage",
"SAN DAMAGE",
)}</b>: ${game.i18n.localize("DG.Roll.For")} <b>${lowDie.formula} / ${
highDie.formula
}</b>`;
const html = await renderTemplate(
"systems/deltagreen/templates/roll/sanity-damage-roll.hbs",
{
lowFormula: lowDie.formula,
highFormula: highDie.formula,
lowFaces: lowDie.faces,
highFaces: highDie.faces,
lowResult,
highResult,
},
);
return this.toMessage({ content: html, rollLabel });
}
/**
* Returns the two results for a sanity damage roll.
*
* Returns null if the roll has not been evaluated.
*
* @returns {null|Array<Number>} - Array of result numbers
*/
get damageResults() {
if (!this.total) return null;
const [lowResult, highResult] = this.terms[0].results;
return [lowResult?.result, highResult?.result];
}
}
+21
View File
@@ -0,0 +1,21 @@
/**
* Append STR melee damage bonus to a weapon damage formula when applicable.
*
* @param {string} diceFormula
* @param {Actor|null} actor
* @param {string|null|undefined} skill Weapon skill key from item system
* @returns {string}
*/
export default function appendMeleeDamageBonus(diceFormula, actor, skill) {
if (
!actor ||
(actor.type !== "agent" && actor.type !== "npc") ||
(skill !== "unarmed_combat" && skill !== "melee_weapons")
) {
return diceFormula;
}
const bonus = actor.system?.statistics?.str?.meleeDamageBonusFormula;
if (!bonus) return diceFormula;
return `${diceFormula}${bonus}`;
}
+257
View File
@@ -0,0 +1,257 @@
import { showDgDialog } from "../applications/dg-dialog.js";
import DGUtils from "../utils/utility-functions.js";
const { renderTemplate } = foundry.applications.handlebars;
const PERCENTILE_ROLL_DIALOG_TEMPLATE =
"systems/deltagreen/templates/dialog/modify-percentile-roll.html";
const DAMAGE_ROLL_DIALOG_TEMPLATE =
"systems/deltagreen/templates/dialog/modify-damage-roll.html";
const DAMAGE_OR_LETHALITY_ROLL_DIALOG_TEMPLATE =
"systems/deltagreen/templates/dialog/damage-or-lethality-roll.html";
const QUICK_MODIFIER_PRESETS = [
{
action: "modMinus40",
mod: -40,
label: "−40",
explanationKey: "DG.ModifySkillRollDialogue.ModifierMuchHarder",
},
{
action: "modMinus20",
mod: -20,
label: "−20",
explanationKey: "DG.ModifySkillRollDialogue.ModifierHarder",
},
{
action: "modPlus20",
mod: 20,
label: "+20",
explanationKey: "DG.ModifySkillRollDialogue.ModifierEasier",
},
{
action: "modPlus40",
mod: 40,
label: "+40",
explanationKey: "DG.ModifySkillRollDialogue.ModifierMuchEasier",
},
];
/**
* @param {DialogV2} dialog
* @returns {string|undefined}
*/
function readMessageMode(dialog) {
return dialog.element.querySelector("[name='messageMode']")?.value;
}
/**
* @param {object} options
* @param {string} options.label
* @param {number} options.target
* @param {boolean} [options.hideTarget]
* @param {number} [options.defaultModifier]
* @param {Actor|null} [options.actor]
* @returns {Promise<{ targetModifier: number, messageMode?: string }|void>}
*/
export async function showPercentileRollModifyDialog({
label,
target,
targetDisplayClass = "",
hideTarget = false,
defaultModifier = 20,
actor = null,
}) {
const quickModifiers = QUICK_MODIFIER_PRESETS.map((preset) => ({
...preset,
explanation: game.i18n.localize(preset.explanationKey),
}));
const content = await renderTemplate(PERCENTILE_ROLL_DIALOG_TEMPLATE, {
data: {
label,
originalTarget: target,
targetDisplayClass,
targetModifier: defaultModifier,
hideTarget,
},
quickModifiers,
});
/** @type {{ targetModifier: number, messageMode?: string } | null} */
let dialogResult = null;
/**
* @param {DialogV2} dialog
* @param {{ targetModifier: number, messageMode?: string }} result
*/
const resolveAndClose = async (dialog, result) => {
dialogResult = result;
await dialog.close();
return result;
};
// Footer buttons are callback-only; visible controls live in modify-percentile-roll.html.
const hiddenFooterButton = { class: "hidden" };
const presetButtons = QUICK_MODIFIER_PRESETS.map(({ action, mod }) => ({
action,
label: "",
...hiddenFooterButton,
callback: async (_event, _button, dialog) =>
resolveAndClose(dialog, {
targetModifier: mod,
messageMode: readMessageMode(dialog),
}),
}));
return showDgDialog({
modifier: "modify-roll",
content,
form: { closeOnSubmit: false },
position: { width: 420 },
window: {
title: DGUtils.localizeWithFallback(
"DG.ModifySkillRollDialogue.Title",
"Modify Roll",
),
},
close: () => dialogResult,
buttons: [
{
default: true,
action: "roll",
label: "",
...hiddenFooterButton,
callback: async (_event, _button, dialog) => {
const raw =
dialog.element
.querySelector("[name='targetModifier']")
?.value?.trim() ?? "";
const targetModifier = Number(raw);
if (raw === "" || Number.isNaN(targetModifier)) {
ui.notifications.warn(
DGUtils.localizeWithFallback(
"DG.ModifySkillRollDialogue.InvalidModifier",
"Enter a valid modifier to roll.",
),
);
return false;
}
if (actor != null) {
await actor.update({
"system.settings.rolling.defaultPercentileModifier":
targetModifier,
});
}
return resolveAndClose(dialog, {
targetModifier,
messageMode: readMessageMode(dialog),
});
},
},
...presetButtons,
],
});
}
/**
* @param {object} options
* @param {string} [options.itemName]
* @param {string} options.formula
* @returns {Promise<{ newFormula: string, messageMode?: string }|void>}
*/
export async function showDamageRollModifyDialog({ itemName, formula }) {
const content = await renderTemplate(DAMAGE_ROLL_DIALOG_TEMPLATE, {
data: {
label: itemName,
originalFormula: formula,
outerModifier: "2 * ",
innerModifier: "+ 0",
},
});
return showDgDialog({
modifier: "modify-damage-roll",
content,
window: {
title: game.i18n.localize("DG.ModifySkillRollDialogue.Title"),
},
close: () => null,
buttons: [
{
default: true,
label: game.i18n.translations.DG.Roll.Roll,
action: "roll",
callback: (_event, _button, dialog) => {
const outerModifier = dialog.element.querySelector(
"[name='outerModifier']",
)?.value;
let innerModifier = dialog.element.querySelector(
"[name='innerModifier']",
)?.value;
const modifiedBaseRoll = dialog.element.querySelector(
"[name='originalFormula']",
)?.value;
const messageMode = dialog.element.querySelector(
"[name='targetRollMode']",
)?.value;
if (innerModifier.replace(" ", "") === "+0") {
innerModifier = "";
}
let newFormula = "";
if (outerModifier.trim() !== "") {
newFormula += `${outerModifier}(${modifiedBaseRoll}${innerModifier.trim()})`;
} else {
newFormula += modifiedBaseRoll + innerModifier.trim();
}
return { newFormula, messageMode };
},
},
],
});
}
/**
* @param {object} options
* @param {string} options.itemName
* @returns {Promise<"damage"|"lethality"|void>}
*/
export async function showDamageOrLethalityChoiceDialog({ itemName }) {
const content = await renderTemplate(
DAMAGE_OR_LETHALITY_ROLL_DIALOG_TEMPLATE,
{
itemName,
},
);
return showDgDialog({
modifier: "damage-or-lethality",
content,
window: {
title: game.i18n.localize("DG.DamageOrLethalityRoll.Title"),
},
close: () => null,
buttons: [
{
default: true,
label: game.i18n.localize("DG.DamageOrLethalityRoll.RollDamage"),
action: "damage",
icon: "fas fa-dice",
callback: () => "damage",
},
{
label: game.i18n.localize("DG.DamageOrLethalityRoll.RollLethality"),
action: "lethality",
icon: "fas fa-skull-crossbones",
callback: () => "lethality",
},
],
});
}
+108 -832
View File
@@ -1,847 +1,123 @@
/* eslint-disable max-classes-per-file */
import DGUtils from "../other/utility-functions.js";
import DG from "../config.js";
import { DGRoll } from "./classes/dg-roll.js";
import { DGPercentileRoll } from "./classes/dg-percentile-roll.js";
import { DGLethalityRoll } from "./classes/dg-lethality-roll.js";
import { DGDamageRoll } from "./classes/dg-damage-roll.js";
import { DGSanityDamageRoll } from "./classes/dg-sanity-damage-roll.js";
import appendMeleeDamageBonus from "./melee-damage.js";
const { renderTemplate } = foundry.applications.handlebars;
const { DialogV2 } = foundry.applications.api;
export {
DGRoll,
DGPercentileRoll,
DGLethalityRoll,
DGDamageRoll,
DGSanityDamageRoll,
};
export class DGRoll extends Roll {
/**
* NOTE: This class will rarely be called on its own. It should generally be extended. Look to DGPercentileRoll as an example.
*
* Customize our roll with some useful information, passed in the `options` Object.
*
* @param {string} formula Unused - The string formula to parse (from Foundry)
* @param {Object} data Unused - The data object against which to parse attributes within the formula
* @param {Object} [options] Additional data which is preserved in the database
* @param {Number} [options.rollType] The type of roll (stat, skill, sanity, damage, etc).
* @param {String} [options.key] The key of the skill, stat, etc. to use as a basis for this roll.
* @param {DeltaGreenActor} [options.actor] The actor that this roll originates from.
* @param {DeltaGreenItem} [options.item] Optional - The item from which the roll originates.
*/
constructor(formula, data = {}, options = {}) {
super(formula, data, options);
const { rollType, key, actor, item } = options;
this.type = rollType;
this.key = key;
this.actor = actor;
this.item = item;
this.modifier = 0;
}
/**
* Build a Delta Green roll from a click target's dataset.
*
* @param {DOMStringMap} dataset
* @param {object} params
* @param {Actor|null} [params.actor]
* @param {Item|null} [params.item]
* @param {HTMLElement} [params.element]
* @param {"actor"|"item"} [params.sanityDamageSource]
* @returns {Roll}
*/
export function createDGRollFromDataset(
dataset,
{
actor = null,
item = null,
element = null,
sanityDamageSource = "actor",
token = null,
} = {},
) {
const rollOptions = {
rollType: dataset.rolltype,
key: dataset.key,
actor,
specialTrainingName: dataset?.name || null,
item,
token,
};
/**
* Simple function that actually creates the message and sends it to chat.
* We override this to have a little more control over certain aspects of the message,
* right now, its `speaker` and `rollMode`.
*
* @override
* The following `@param` descriptions comes from the Foundry VTT code.
* @param {object} messageData The data object to use when creating the message
* @param {options} [options] Additional options which modify the created message.
* @param {string} [options.rollMode] The template roll mode to use for the message from CONFIG.Dice.rollModes
* @param {boolean} [options.create=true] Whether to automatically create the chat message, or only return the
* prepared chatData object.
* @returns {Promise<ChatMessage|object>} A promise which resolves to the created ChatMessage document if create is
* true, or the Object of prepared chatData otherwise.
*/
async toMessage(messageData = {}, { rollMode, create = true } = {}) {
// eslint-disable-next-line no-param-reassign
messageData.speaker = ChatMessage.getSpeaker({ actor: this.actor });
return super.toMessage(messageData, {
rollMode: this.options.rollMode || rollMode,
create,
});
}
}
export class DGPercentileRoll extends DGRoll {
/**
* Creates D100 rolls, the base die of the system.
*
* This constructor embeds the following info into the roll:
* 1. Target number that the roll needs to beat.
* 2. Localized name for the roll.
*
* Note: In order for all of our custom data to persist, our constructor must use the same parameters as its parent class.
* So, even though percentile rolls will always have a formula of "1d100" and we don't use the `data` object,
* we still have to keep them as parameters.
*
* @param {string} formula Unused - The string formula to parse (from Foundry) - Always "1d100" for percentile rolls.
* @param {Object} data Unused - The data object against which to parse attributes within the formula
* @param {Object} [options] Additional data which is preserved in the database
* @param {Number} [options.rollType] The type of roll (stat, skill, sanity, etc).
* @param {String} [options.key] The key of the skill, stat, etc. to use as a basis for this roll.
* @param {DeltaGreenActor} [options.actor] The actor that this roll originates from.
* @param {DeltaGreenItem} [options.item] Optional - The item from which the roll originates.
* @param {DeltaGreenItem} [options.specialTrainingName] Optional - Special training rolls have names that are different from the roll key.
*/
// eslint-disable-next-line default-param-last, no-unused-vars
constructor(formula = "1D100", data = {}, options) {
super("1D100", {}, options);
// Set roll info for Skill, Stat, Typed Skill, and non-custom Weapon Percentile rolls.
const { target, localizedKey, skillPath } = this.getRollInfoFromKey(
this.key,
this.actor.system,
);
this.target = target;
this.localizedKey = localizedKey;
this.skillPath = skillPath;
// Set roll info for other Percentile rolls
switch (this.type) {
case "special-training":
this.specialTrainingName = options.specialTrainingName;
this.localizedKey = `${this.specialTrainingName} - (${this.localizedKey})`;
break;
case "weapon":
// If this weapon uses a custom target for rolls, we set that explicitly.
if (this.key === "custom") {
this.target = this.item.system.customSkillTarget;
this.localizedKey = game.i18n.localize("DG.ItemWindow.Custom");
}
// Add a the weapon's internal modifier.
this.modifier += this.item.system.skillModifier;
break;
case "sanity":
this.target = this.actor.system.sanity.value;
this.localizedKey = game.i18n.localize("DG.Attributes.SAN");
break;
case "luck":
this.target = 50;
this.localizedKey = game.i18n.localize("DG.Luck");
break;
default:
break;
let roll = new Roll("1d100", {});
switch (dataset.rolltype) {
case "stat":
case "skill":
case "sanity":
case "special-training":
case "weapon":
case "luck":
roll = new DGPercentileRoll("1D100", {}, rollOptions);
break;
case "lethality":
roll = new DGLethalityRoll("1D100", {}, rollOptions);
break;
case "damage": {
const diceFormula = appendMeleeDamageBonus(
item.system.damage,
actor,
item.system.skill,
);
roll = new DGDamageRoll(diceFormula, {}, rollOptions);
break;
}
}
/**
* Shows a dialog that can modify the roll.
*
* @returns {Promise<Object|void>} - the results of the dialog.
*/
async showDialog() {
const privateSanSetting = game.settings.get(
"deltagreen",
"keepSanityPrivate",
);
let hideSanTarget = false;
if (
privateSanSetting &&
(this.type === "sanity" || this.key === "ritual") &&
!game.user.isGM
) {
hideSanTarget = true;
}
let customModifierTarget = 20;
if (this.actor != null) {
try {
customModifierTarget = parseInt(
this.actor.system.settings.rolling.defaultPercentileModifier,
);
} catch {
// do nothing
}
}
const backingData = {
data: {
label: this.localizedKey,
originalTarget: this.target,
targetModifier: customModifierTarget,
hideTarget: hideSanTarget,
},
};
const template =
"systems/deltagreen/templates/dialog/modify-percentile-roll.html";
const content = await renderTemplate(template, backingData);
return new Promise((resolve, reject) => {
const modButtons = [-40, -20, 20, 40].map((mod) => {
const sign = mod > 0 ? "Positive" : "Negative";
return {
action: `roll${Math.abs(mod)}${sign}`,
label: String(mod),
callback: (event, button, dialog) => {
try {
const rollMode =
dialog.element.querySelector("[name='rollMode']")?.value;
resolve({ targetModifier: mod, rollMode });
} catch (ex) {
reject(console.log(ex));
}
},
};
});
new DialogV2({
content,
window: {
title: DGUtils.localizeWithFallback(
"DG.ModifySkillRollDialogue.Title",
"Modify Roll",
),
},
buttons: [
{
default: true,
action: "roll",
label: DGUtils.localizeWithFallback("DG.Roll.Roll", "Roll"),
callback: (event, button, dialog) => {
try {
let targetModifier = dialog.element.querySelector(
"[name='targetModifier']",
)?.value; // this is text as a heads up
const rollMode =
dialog.element.querySelector("[name='rollMode']")?.value;
const plusMinus = dialog.element.querySelector(
"[name='plusOrMinus']",
)?.value;
if (
targetModifier.trim() !== "" &&
!Number.isNaN(targetModifier)
) {
targetModifier = Math.abs(parseInt(targetModifier));
if (plusMinus === "-") {
targetModifier *= -1;
}
// update the custom target modifier so it 'persists' between dialogs
if (this.actor != null) {
this.actor.update({
"system.settings.rolling.defaultPercentileModifier":
targetModifier,
});
}
}
resolve({ targetModifier, rollMode });
} catch (ex) {
reject(console.log(ex));
}
},
},
...modButtons,
],
}).render(true);
});
}
/**
* Prepares data for a chat message and then passes that data
* to a method that actually creates a ChatMessage.
*
* Lays out and styles message based on outcome of the roll.
*
* @returns {Promise<ChatMessage>} - the created chat message.
*/
async toChat() {
// if using private san rolls, must hide any SAN roll unless user is a GM
const privateSanSetting = game.settings.get(
"deltagreen",
"keepSanityPrivate",
);
if (
privateSanSetting &&
(this.type === "sanity" || this.key === "ritual") &&
!game.user.isGM
) {
this.options.rollMode = "blindroll";
}
const diceSoNice =
game.modules.has("dice-so-nice") &&
game.modules.get("dice-so-nice").active;
const label = this.createLabel();
let resultString = "";
let styleOverride = "";
if (this.isSuccess) {
if (this.isCritical) {
resultString = `${game.i18n.localize("DG.Roll.CriticalSuccess")}`;
resultString = `${resultString.toUpperCase()}`;
styleOverride = "color: green";
case "sanity-damage": {
let successLoss;
let failedLoss;
if (sanityDamageSource === "item" && item) {
const isLearntDamage = element?.hasAttribute("data-san-on-learn");
const sanityData = isLearntDamage
? item.system.learnedSanity
: item.system.sanity;
({ successLoss, failedLoss } = sanityData);
} else if (actor) {
({ successLoss, failedLoss } = actor.system.sanity);
} else {
resultString = `${game.i18n.localize("DG.Roll.Success")}`;
break;
}
} else if (this.isCritical) {
resultString = `${game.i18n.localize("DG.Roll.CriticalFailure")}`;
resultString = `${resultString.toUpperCase()}`;
styleOverride = "color: red";
} else {
resultString = `${game.i18n.localize("DG.Roll.Failure")}`;
const combinedFormula = `{${successLoss}, ${failedLoss}}`;
roll = new DGSanityDamageRoll(combinedFormula, {}, rollOptions);
break;
}
const failureMark =
this.actor?.type === "agent" &&
!this.isSuccess &&
this.skillPath &&
this.key !== "unnatural" &&
!foundry.utils.getProperty(this.actor, `${this.skillPath}.failure`) &&
game.settings.get(DG.ID, "skillFailure");
const html = await renderTemplate(
"systems/deltagreen/templates/roll/percentile-roll.hbs",
{
styleOverride,
resultString,
formula: this.formula,
total: this.total,
failureMark,
},
);
// TODO: add setting for it?
if (failureMark) {
const keyForUpdate = `${this.skillPath}.failure`;
const message = await this.toMessage({
flags: {
deltagreen: {
rollbacks: {
[keyForUpdate]: false,
},
},
},
content: html,
flavor: label,
});
if (diceSoNice) {
await game.dice3d.waitFor3DAnimationByMessageID(message.id);
}
// TODO: auto-update actor or post icon with manual apply
await this.actor.update({
[keyForUpdate]: true,
});
return message;
}
return this.toMessage({ content: html, flavor: label });
}
/**
* Utility function, called in the DGPercentileRoll constructor.
* If this roll key corresponds to a stat, skill,
* or typedSkill, get pertinent info.
*
* This is used for Stat, Skill, Typed Skill, Weapon, and Special Training Rolls.
*
* @returns {Object} - Contains the roll target and localized version of the key.
*/
getRollInfoFromKey() {
const actorData = this.actor.system;
const skillKeys = Object.keys(actorData.skills);
const typedSkillKeys = Object.keys(actorData.typedSkills);
const statKeys = Object.keys(actorData.statistics);
let target = null;
let localizedKey = null;
let skillPath = null; // For optimization of failure checks
if (statKeys.includes(this.key)) {
target = actorData.statistics[this.key].x5;
localizedKey = game.i18n.localize(`DG.Attributes.${this.key}`);
}
if (skillKeys.includes(this.key)) {
// use calculated target proficiency (effects and etc like aim + 20%)
target =
actorData.skills[this.key].targetProficiency ||
actorData.skills[this.key].proficiency;
localizedKey = game.i18n.localize(`DG.Skills.${this.key}`);
skillPath = `system.skills.${this.key}`;
}
if (typedSkillKeys.includes(this.key)) {
const skill = actorData.typedSkills[this.key];
target = skill.proficiency;
localizedKey = `${skill.group} (${skill.label})`;
skillPath = `system.typedSkills.${this.key}`;
}
if (this.key === "ritual") {
target = actorData.sanity.ritual;
localizedKey = game.i18n.localize(`DG.Skills.ritual`);
}
return { target, localizedKey, skillPath };
}
/**
* Create label based on result of roll
*
* todo: do we want make isInhuman more similar to base label?
*
* @returns {string}
*/
createLabel() {
const startOfLabel = `${game.i18n.localize("DG.Roll.Rolling")} <b>${
this.localizedKey
}`;
const endOfLabel = `${game.i18n.localize("DG.Roll.Target")} ${
this.effectiveTarget
}`;
let label = this.isInhuman
? // "Inhuman" stat being rolled. See function for details.
`${startOfLabel} [${game.i18n
.localize("DG.Roll.Inhuman")
.toUpperCase()}]</b> ${endOfLabel}`
: `${startOfLabel}</b><br> ${endOfLabel}%`;
const { isExhausted, exhaustedCheckPenalty } = this.exhausted;
if (this.modifier || isExhausted) {
label += ` (${this.target}%`;
if (this.modifier) {
label += `${DGUtils.formatStringWithLeadingPlus(this.modifier)}%`;
}
if (isExhausted) {
label += `${DGUtils.formatStringWithLeadingPlus(
exhaustedCheckPenalty,
)}%`;
}
label += `)`;
}
return label;
}
get exhausted() {
let isExhausted = false;
let exhaustedCheckPenalty = -20;
try {
// I suspect (but am not entirely certain) that being tired doesn't make you less lucky)
if (this.type !== "luck") {
isExhausted = this.actor.system.physical.exhausted;
exhaustedCheckPenalty = this.actor.system.physical.exhaustedPenalty;
exhaustedCheckPenalty = -1 * Math.abs(exhaustedCheckPenalty);
}
} catch {
isExhausted = false;
exhaustedCheckPenalty = -20;
}
return { isExhausted, exhaustedCheckPenalty };
}
/**
* "Inhuman" stat being rolled, logic is different per page 188 of the Handler's Guide.
* Note - originally implemented by Uriele, but my attempt at merging conficts went poorly, so re-implementing.
* For an inhuman check, the roll succeeds except on a roll of 100 which fails AND fumbles.
* If the roll is a matching digit roll, it is a critical as normal.
* Also, if the roll is below the regular (non-x5) value of the stat, it is a critical. E.g. a CON of 25, a d100 roll of 21 would be a critical.
*
* @returns {Boolean}
*/
get isInhuman() {
/*
Changing this to only consider the base x5 stat target for whether something is 'inhuman'
because I do not think the intent was an Agent with a high strength getting a +40% bonus to be considered 'inhuman'
and therefore benefit from the increased crit threshold, although could be wrong about this.
*/
if (this.target > 99 && this.type === "stat") {
return true;
}
return false;
}
/**
* Determines if a roll result is critical.
* If roll has not been evaluated, return null.
*
* @returns {null|Boolean}
*/
get isCritical() {
// If roll isn't evaluated, return null.
if (!this.total) {
return null;
}
let isCritical = false;
// 1, 100, or any matching dice are a crit, i.e. 11, 22, 33...99.
if (this.total === 1 || this.total === 100 || this.total % 11 === 0) {
// really good, or reeaaaally bad
isCritical = true;
}
// If inhuman and the roll is below the regular (non-x5) value of the stat, it is a critical.
// E.g. a CON of 25, a d100 roll of 21 would be a critical.
if (this.isInhuman && this.total <= this.target / 5) {
isCritical = true;
}
return isCritical;
}
/**
* Determines if a roll succeeded.
* If roll has not been evaluated, return null.
*
* @returns {null|Boolean}
*/
get isSuccess() {
// If roll isn't evaluated, return null.
if (!this.total) {
return null;
}
// A roll of 100 always (critically) fails, even for inhuman rolls.
if (this.total === 100) return false;
return this.total <= this.effectiveTarget;
}
/**
* Actual target for the roll accounting for modifier if present.
* Floored to 1 if a negative modifier would bring it below 1.
* Capped at 99 unless it is an inhuman stat test.
* Also worth noting, per page 47 of the Agent's Handbook, Exhaustion penalties
* affect not only skill and stat tests, but SAN tests as well...
*
* @returns {null|integer}
*/
get effectiveTarget() {
let target = 1;
const { isExhausted, exhaustedCheckPenalty } = this.exhausted;
if (!this.target || Number.isNaN(this.target)) {
return null;
}
target = parseInt(this.target);
if (isExhausted) {
target += exhaustedCheckPenalty;
}
if (this.modifier && !Number.isNaN(this.modifier)) {
const modifier = parseInt(this.modifier);
target += modifier;
// per agent's handbook (pg.43), a negative modifier can't lower a target below 1%
target = Math.max(target, 1);
}
// an 'inhuman' stat test can exceed 99% as a target, but skill tests otherwise cannot (agents handbook pg.43)
if (!this.isInhuman) {
target = Math.min(target, 99);
}
return target;
default:
break;
}
return roll;
}
export class DGLethalityRoll extends DGPercentileRoll {
/**
* See constructor for DGPercentileRoll. This theoretically could be done in the parent class'
* constructor, but since Lethality rolls needs its own class for custom methods anyway,
* we will set the target and localized key here.
*
* @param {String} formula
* @param {Object} data
* @param {Object} options
*/
constructor(formula, data, options) {
super(formula, data, options);
this.target = options.item.system.lethality;
this.localizedKey = game.i18n.localize("DG.ItemWindow.Weapons.Lethality");
/**
* Show an optional modifier dialog, evaluate, and send a roll to chat.
*
* @param {Event|object} event
* @param {Roll} roll
* @returns {Promise<void>}
*/
export async function processDGRoll(event, roll) {
const shiftKey = event?.shiftKey ?? false;
const which = event?.which ?? 0;
if (roll instanceof DGPercentileRoll && roll.blockedRollMessage) {
ui.notifications.warn(roll.blockedRollMessage, { localize: true });
return;
}
/**
* Prepares data for a chat message and then passes that data
* to a method that actually creates a ChatMessage.
*
* Lays out and styles message based on outcome of the roll.
*
* Overrides `DGPercentileRoll.toChat()`
*
* @returns {Promise<ChatMessage>} - the created chat message.
* @override
*/
async toChat() {
let resultString = "";
let styleOverride = "";
if (this.total <= this.target) {
resultString = `${game.i18n.localize("DG.Roll.Lethal").toUpperCase()}`;
styleOverride = "color: red";
} else {
resultString = `${game.i18n.localize("DG.Roll.Failure")}`;
if (shiftKey || which === 3) {
if (!(roll instanceof DGSanityDamageRoll)) {
const dialogData = await roll.showDialog();
if (!dialogData) return;
if (dialogData.newFormula) {
roll = new DGDamageRoll(dialogData.newFormula, {}, roll.options);
}
roll.modifier += dialogData.targetModifier;
if (dialogData.messageMode) {
roll.options.messageMode = dialogData.messageMode;
}
}
const { nonLethalDamage } = this;
let label = `${game.i18n.localize("DG.Roll.Rolling")} <b>${game.i18n
.localize("DG.Roll.Lethality")
.toUpperCase()}</b> ${game.i18n.localize(
"DG.Roll.For",
)} <b>${this.item.name.toUpperCase()}</b> ${game.i18n.localize(
"DG.Roll.Target",
)} ${this.target + this.modifier}`;
if (this.modifier) {
label += ` (${DGUtils.formatStringWithLeadingPlus(this.modifier)}%)`;
}
let html = "";
html += `<div class="dice-roll" data-action="expandRoll">`;
html += ` <div class="dice-result">`;
html += ` <div style="${styleOverride}" class="dice-formula">${resultString}</div>`;
html += ` <div class="dice-tooltip">`;
html += ` <div class="wrapper">`;
html += ` <section class="tooltip-part">`;
html += ` <div class="dice">`;
html += ` <header class="part-header flexrow">`;
html += ` <span class="part-formula">`;
html += ` d100`;
html += ` </span>`;
html += ` <span class="part-total">`;
html += ` ${this.total}`;
html += ` </span>`;
html += ` </header>`;
html += ` <ol class="dice-rolls">`;
html += ` <li class="roll die d100">${this.total}`;
html += ` </ol>`;
html += ` <hr>`;
html += ` <header class="part-header flexrow">`;
html += ` <span class="part-formula">`;
html += ` 2d10 (d10 + d10)`;
html += ` </span>`;
html += ` <span class="part-total">`;
html += ` ${nonLethalDamage.total}`;
html += ` </span>`;
html += ` </header>`;
html += ` <ol class="dice-rolls">`;
html += ` <li class="roll die d10">${nonLethalDamage.die1}</li>`;
html += ` <li class="roll die d10">${nonLethalDamage.die2}</li>`;
html += ` </ol>`;
html += ` </div>`;
html += ` </section>`;
html += ` </div>`;
html += ` </div>`;
html += ` <h4 class="dice-total">${this.total} (${
nonLethalDamage.total
} ${game.i18n.localize("DG.Roll.Damage")})</h4>`;
html += ` </div>`;
html += `</div>`;
return this.toMessage({ content: html, flavor: label });
}
/**
* Calculates the damage for when a lethality roll fails.
* If roll has not been evaluated, return null.
*
* See full rules on page 57 of agent's handbook.
*
* Note, this getter does not actually care if the roll has failed.
*
* @returns {null|Object} - return data about the non-lethal damage.
*/
get nonLethalDamage() {
if (!this.total) {
return null;
}
// Try to determine what the d100 result would be as if it was two d10's being rolled.
const totalString = this.total.toString();
const digits = totalString.length;
let die1;
let die2;
switch (digits) {
case 1:
// If one digit in the result, one die is a 10, and the other is the result.
[die1, die2] = [10, this.total];
break;
case 2:
// If two digits in the result, each die is the value of one of the digits. If one of those digits is 0, make it 10.
[die1, die2] = totalString
.split("")
.map((digit) => parseInt(digit))
.map((digit) => digit || 10);
break;
case 3:
// If three digits in the result (aka result === 100), each die is a 10.
[die1, die2] = [10, 10];
break;
default:
break;
}
const total = die1 + die2;
return { die1, die2, total };
}
}
export class DGDamageRoll extends DGRoll {
/**
* Prepares data for a chat message and then passes that data
* to a method that actually creates a ChatMessage.
*
* @returns {Promise<ChatMessage>} - the created chat message.
* @override
*/
async toChat() {
let label = this.formula;
try {
label = `${game.i18n.localize("DG.Roll.Rolling")} <b>${game.i18n
.localize("DG.Roll.Damage")
.toUpperCase()}</b> ${game.i18n.localize("DG.Roll.For")} ${
this.item.name
} (<b>${
this.item.system.armorPiercing
} </b><img class="armor-piercing-chat-card-img" src="systems/deltagreen/assets/icons/supersonic-bullet.svg" alt="armor penetration"/>)`;
} catch (ex) {
// console.log(ex);
label = `Rolling <b>DAMAGE</b> for <b>${label.toUpperCase()}</b>`;
}
return this.toMessage({ content: this.total, flavor: label });
}
async showDialog() {
const template =
"systems/deltagreen/templates/dialog/modify-damage-roll.html";
const backingData = {
data: {
label: this.item?.name,
originalFormula: this.formula,
outerModifier: "2 * ",
innerModifier: "+ 0",
},
};
const content = await renderTemplate(template, backingData);
return new Promise((resolve, reject) => {
new DialogV2({
content,
window: {
title: game.i18n.localize("DG.ModifySkillRollDialogue.Title"),
},
buttons: [
{
default: true,
label: game.i18n.translations.DG.Roll.Roll,
action: "roll",
callback: (event, button, dialog) => {
try {
const outerModifier = dialog.element.querySelector(
"[name='outerModifier']",
)?.value; // this is text as a heads up
let innerModifier = dialog.element.querySelector(
"[name='innerModifier']",
)?.value; // this is text as a heads up
const modifiedBaseRoll = dialog.element.querySelector(
"[name='originalFormula']",
)?.value; // this is text as a heads up
const rollMode = dialog.element.querySelector(
"[name='targetRollMode']",
)?.value;
if (innerModifier.replace(" ", "") === "+0") {
innerModifier = "";
}
let newFormula = "";
if (outerModifier.trim() !== "") {
newFormula += `${outerModifier}(${modifiedBaseRoll}${innerModifier.trim()})`;
} else {
newFormula += modifiedBaseRoll + innerModifier.trim();
}
resolve({ newFormula, rollMode });
} catch (ex) {
reject(console.log(ex));
}
},
},
],
}).render(true);
});
}
}
export class DGSanityDamageRoll extends DGRoll {
/**
* Prepares data for a chat message and then passes that data
* to a method that actually creates a ChatMessage.
*
* @returns {Promise<ChatMessage>} - the created chat message.
* @override
*/
async toChat() {
const [lowDie, highDie] = this.terms[0].terms.map((formula) => {
return Roll.parse(formula)[0] || { faces: parseInt(formula), number: 1 };
});
const [lowResult, highResult] = this.damageResults;
const flavor = `Rolling <b>${DGUtils.localizeWithFallback(
"DG.Generic.SanDamage",
"SAN DAMAGE",
)}</b> For <b>${lowDie.formula} / ${highDie.formula}</b>`;
let html = "";
html += `<div class="dice-roll" data-action="expandRoll">`;
html += ` <div class="dice-result">`;
html += ` <div class="dice-formula">${lowDie.formula} / ${highDie.formula}</div>`;
html += ` <div class="dice-tooltip">`;
html += ` <div class="wrapper">`;
html += ` <section class="tooltip-part">`;
html += ` <div class="dice">`;
html += ` <header class="part-header flexrow">`;
html += ` <span class="part-formula">`;
html += ` ${lowDie.formula}`;
html += ` </span>`;
html += ` <span class="part-total">`;
html += ` ${lowResult}`;
html += ` </span>`;
html += ` </header>`;
html += ` <ol class="dice-rolls">`;
html += ` <li class="roll die d${lowDie.faces}">${lowResult}`;
html += ` </ol>`;
html += ` <hr>`;
html += ` <header class="part-header flexrow">`;
html += ` <span class="part-formula">`;
html += ` ${highDie.formula}`;
html += ` </span>`;
html += ` <span class="part-total">`;
html += ` ${highResult}`;
html += ` </span>`;
html += ` </header>`;
html += ` <ol class="dice-rolls">`;
html += ` <li class="roll die d${highDie.faces}">${highResult}</li>`;
html += ` </ol>`;
html += ` </div>`;
html += ` </section>`;
html += ` </div>`;
html += ` </div>`;
html += ` <h4 class="dice-total">${lowResult} / ${highResult}</h4>`;
html += ` </div>`;
html += `</div>`;
return this.toMessage({ content: html, flavor });
}
/**
* Returns the two results for a sanity damage roll.
*
* Returns null if the roll has not been evaluated.
*
* @returns {null|Array<Number>} - Array of result numbers
*/
get damageResults() {
if (!this.total) return null;
const [lowResult, highResult] = this.terms[0].results;
return [lowResult?.result, highResult?.result];
}
await roll.evaluate();
await roll.toChat();
}
+109
View File
@@ -0,0 +1,109 @@
import { createDGChatMessage } from "../chat/dg-chat-card.js";
import DG from "../config/index.js";
import { formatProfessionSkillLabel } from "../profession/index.js";
/**
* @param {"1"|"d3"|"d4"|"d4-1"} baseFormula
* @returns {string}
*/
export function getSkillImprovementFormulaAsPercent(baseFormula) {
const flat = baseFormula === "1";
const formula = flat ? "1" : `1${baseFormula}`;
return `${formula}%`.toUpperCase();
}
/**
* Generates and evaluates skill improvement rolls for failed skills.
*
* @param {Actor} actor
* @param {string} baseFormula
* @param {object[]} failedSkills
* @param {object[]} failedTypedSkills
* @returns {Promise<Record<string, number>>}
*/
export async function evaluateSkillImprovementRolls(
actor,
baseFormula,
failedSkills,
failedTypedSkills,
) {
const totalFailures = failedSkills.length + failedTypedSkills.length;
if (!Object.keys(DG.skillImprovementFormulas).includes(baseFormula)) {
throw new Error(`Unknown roll formula: ${baseFormula}`);
}
const resultObj = {};
if (baseFormula === "1") {
return resultObj;
}
const rollPromises = [];
for (let i = 0; i < totalFailures; i++) {
rollPromises.push(new Roll(baseFormula, actor.system).evaluate());
}
const rolls = await Promise.all(rollPromises);
[...failedSkills, ...failedTypedSkills].forEach((skill, index) => {
resultObj[skill.key] = rolls[index].total;
});
return resultObj;
}
/**
* @param {object} params
* @param {Actor} params.actor
* @param {TokenDocument|null} [params.token]
* @param {string} params.baseFormula
* @param {object[]} params.failedSkills
* @param {object[]} params.failedTypedSkills
* @param {Record<string, number>} params.resultObj
* @returns {Promise<ChatMessage>}
*/
export async function createSkillImprovementChatMessage({
actor,
token = null,
baseFormula,
failedSkills,
failedTypedSkills,
resultObj,
}) {
const localizeFailedSkills = (skillsArray, kind) => {
return skillsArray.map((skill) => {
const increment = resultObj[skill.key] ?? 1;
const label =
kind === "typed"
? formatProfessionSkillLabel({
kind: "typed",
group: skill.group,
label: skill.label,
})
: formatProfessionSkillLabel({ kind: "fixed", key: skill.key });
return `${label}: <b>+${increment}%</b>`;
});
};
const failedSkillNames = localizeFailedSkills(failedSkills, "fixed");
const failedTypedSkillNames = localizeFailedSkills(
failedTypedSkills,
"typed",
);
const content = [...failedSkillNames, ...failedTypedSkillNames].join(", ");
const title = game.i18n.format(
"DG.Skills.ApplySkillImprovements.ChatFlavor",
{
formula: getSkillImprovementFormulaAsPercent(baseFormula),
},
);
return createDGChatMessage({
actor,
token,
title,
content,
messageMode: game.settings.get("core", "messageMode"),
});
}
+11 -46
View File
@@ -1,6 +1,7 @@
/* eslint-disable max-classes-per-file */
import DG, { BASE_TEMPLATE_PATH } from "./config.js";
import DG, { BASE_TEMPLATE_PATH } from "./config/index.js";
import DGActorSheet from "./sheets/base-actor-sheet.js";
import DGItemSheet from "./sheets/base-item-sheet.js";
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
@@ -87,7 +88,7 @@ const SettingForm = class extends HandlebarsApplicationMixin(ApplicationV2) {
}
// Once all promises resolve, show a notification.
Promise.allSettled(settingsPromises).then((values) => {
Promise.allSettled(settingsPromises).then(() => {
ui.notifications.info(game.i18n.localize("DG.Settings.Saved"));
});
}
@@ -297,7 +298,7 @@ export default function registerSystemSettings() {
program: game.i18n.localize("DG.Settings.charactersheet.program"),
},
default: "program", // The default value for the setting
onChange: (value) => {
onChange: () => {
// A callback function which triggers when the setting is changed
// console.log(value)
},
@@ -329,54 +330,18 @@ export default function registerSystemSettings() {
default: "hover",
onChange: () => {
foundry.applications.instances.forEach((app) => {
if (app instanceof DGActorSheet) {
if (app instanceof DGActorSheet || app instanceof DGItemSheet) {
app.render();
}
});
},
});
// obsolete - will be removed at some point
game.settings.register("deltagreen", "characterSheetFont", {
name: "World Font Choice",
hint: "Choose font style for use throughout this world.",
scope: "world", // This specifies a world-stored setting
config: false, // This specifies that the setting appears in the configuration view
type: String,
choices: {
// If choices are defined, the resulting setting will be a select menu
SpecialElite: "Special Elite (Classic Typewriters Font)",
Martel: "Martel (Clean Modern Font)",
Signika: "Signika (Foundry Default Font)",
TypeWriterCondensed:
"Condensed Typewriter (Modern, Small Typewriter Font)",
PublicSans: "Public Sans (US Government-style sans serif font)",
// "atwriter": "Another Typewriter (Alternate Old-style Typewriter Font)"
},
default: "SpecialElite", // The default value for the setting
onChange: (value) => {
// A callback function which triggers when the setting is changed
// console.log(value)
},
});
// obsolete - will be removed at some point
game.settings.register("deltagreen", "characterSheetBackgroundImageSetting", {
name: "World Sheet Background Image",
hint: "Choose background image for use throughout this world. (Refresh page to see change.)",
scope: "world", // This specifies a world-stored setting
config: false, // This specifies that the setting appears in the configuration view
type: String,
choices: {
// If choices are defined, the resulting setting will be a select menu
OldPaper1: "Old Dirty Paper (Good with Special Elite Font)",
IvoryPaper: "Ivory White Paper (Good with Martel Font)",
DefaultParchment: "Default Parchment (Good with Signika Font)",
},
default: "OldPaper1", // The default value for the setting
onChange: (value) => {
// A callback function which triggers when the setting is changed
// console.log(value)
},
game.settings.register(DG.ID, "schemaMigrationVersion", {
name: "Schema Migration Version",
scope: "world",
config: false,
type: Number,
default: 0,
});
}
-43
View File
@@ -1,43 +0,0 @@
import DGAgentSheet from "./agent-sheet.js";
import ActorEditStatForm from "./edit-stats.js";
/** @extends {DGAgentSheet} */
export default class DGAgentSheetV2 extends DGAgentSheet {
static DEFAULT_OPTIONS = /** @type {const} */ ({
classes: ["actor-sheet-v2"],
position: { width: 950, height: 665 },
actions: {
openStatsEdit: this._openStatsEdit,
},
});
/** @override */
static PARTS = /** @type {const} */ ({
leftBar: {
template: `${this.TEMPLATE_PATH}/parts/left-bar.html`,
templates: [`${this.TEMPLATE_PATH}/partials/sanity-partial.html`],
},
rightBar: {
template: `${this.TEMPLATE_PATH}/parts/right-bar.html`,
templates: [
`${this.TEMPLATE_PATH}/parts/skills-tab.html`,
`${this.TEMPLATE_PATH}/parts/physical-tab-v2.html`,
`${this.TEMPLATE_PATH}/parts/motivations-tab-v2.html`,
`${this.TEMPLATE_PATH}/parts/gear-tab.html`,
`${this.TEMPLATE_PATH}/parts/bio-tab-v2.html`,
`${this.TEMPLATE_PATH}/parts/bonds-tab.html`,
`${this.TEMPLATE_PATH}/parts/about-tab.html`,
`${this.TEMPLATE_PATH}/partials/custom-skills-partial.html`,
`${this.TEMPLATE_PATH}/partials/notes-partial.html`,
],
},
tabs: this.BASE_PARTS.tabs,
});
static _openStatsEdit() {
const form = new ActorEditStatForm({
actor: this.actor,
});
form.render(true);
}
}
+546 -149
View File
@@ -1,21 +1,69 @@
import DG, { BASE_TEMPLATE_PATH } from "../config.js";
import DG, { BASE_TEMPLATE_PATH } from "../config/index.js";
import { prepareAgentSkillColumns } from "../utils/skill-layout.js";
import { formatProfessionSkillLabel } from "../profession/index.js";
import { buildSkillTooltip } from "../utils/skill-tooltip.js";
import createAgentResourceChatMessage, {
buildWillpowerChangeSpan,
} from "../chat/resource-chat.js";
import {
createSkillImprovementChatMessage,
evaluateSkillImprovementRolls,
getSkillImprovementFormulaAsPercent,
} from "../roll/skill-improvement-roll.js";
import DGActorSheet from "./base-actor-sheet.js";
import ActorEditStatForm from "../applications/edit-stats.js";
import EffectsTabMixin from "./mixins/effects-tab-mixin.js";
import {
applyStimulantEffect,
clearStimulantEffects,
getEffectiveSuppressExhaustion,
hasActiveStimulantEffect,
} from "../active-effect/runtime/stimulant-effect.js";
import assignProfessionToAgent from "../applications/profession-setup-flow.js";
import { showDgDialog } from "../applications/dg-dialog.js";
import showRenameProfessionDialog from "../applications/rename-profession-dialog.js";
import { PROFESSION_OPTION_PICKS_KEY } from "../data/item/profession.js";
const { renderTemplate } = foundry.applications.handlebars;
/** @extends {DGActorSheet} */
export default class DGAgentSheet extends DGActorSheet {
const AgentSheetBase = EffectsTabMixin(DGActorSheet);
/** @extends {AgentSheetBase} */
export default class DGAgentSheet extends AgentSheetBase {
/** @override */
static DEFAULT_OPTIONS = /** @type {const} */ ({
classes: ["agent-sheet"],
position: { width: 978, height: 720 },
actions: {
// Resets.
clearBondDamage: DGAgentSheet._clearBondDamage,
resetBreakingPoint: DGAgentSheet._resetBreakingPoint,
// Other actions.
applySkillImprovements: DGAgentSheet._processSkillImprovements,
openStatsEdit: DGAgentSheet._openStatsEdit,
toggleCustomSkillsEdit: function toggleCustomSkillsEdit(event) {
event.preventDefault();
event.stopPropagation();
this._customSkillsEditMode = !this._customSkillsEditMode;
this._syncCustomSkillsEditModeUi();
return this.render();
},
createEffect: AgentSheetBase.createEffect,
openEffect: AgentSheetBase.openEffect,
deleteEffect: AgentSheetBase.deleteEffect,
toggleEffect: AgentSheetBase.toggleEffect,
openEffectOrigin: AgentSheetBase.openEffectOrigin,
toggleAcuteEpisode: DGAgentSheet._toggleAcuteEpisode,
exhaustAgent: DGAgentSheet._exhaustAgent,
restAgent: DGAgentSheet._restAgent,
takeStimulants: DGAgentSheet._takeStimulants,
openProfessionItem: DGAgentSheet._openProfessionItem,
},
});
/** @type {boolean} */
_customSkillsEditMode = false;
/** @override */
static TABS = /** @type {const} */ ({
primary: {
@@ -23,45 +71,244 @@ export default class DGAgentSheet extends DGActorSheet {
labelPrefix: "DG.Navigation.Agent",
tabs: [
{ id: "skills" },
{ id: "physical" },
{ id: "motivations" },
{ id: "combat" },
{ id: "gear" },
{ id: "bio" },
{ id: "bonds" },
{ id: "motivations" },
{ id: "personal" },
{ id: "effects" },
{ id: "about", icon: "fas fa-question-circle", label: "" },
],
},
leftBarRestSanity: {
initial: "sanity",
labelPrefix: "DG.Navigation.Agent.LeftBar",
tabs: [{ id: "sanity" }, { id: "rest" }],
},
});
/** @override */
static PARTS = /** @type {const} */ ({
header: this.BASE_PARTS.header,
tabs: this.BASE_PARTS.tabs,
skills: this.BASE_PARTS.skills,
physical: {
template: `${this.TEMPLATE_PATH}/parts/physical-tab.html`,
leftBar: {
template: `${this.TEMPLATE_PATH}/parts/left-bar.html`,
templates: [
`${this.TEMPLATE_PATH}/partials/attributes-grid-partial.html`,
`${this.TEMPLATE_PATH}/partials/sanity-partial.html`,
`${this.TEMPLATE_PATH}/partials/left-bar-exhaustion-section.html`,
`${this.TEMPLATE_PATH}/partials/left-bar-rest-tab.html`,
`${this.TEMPLATE_PATH}/partials/left-bar-sanity-tab.html`,
`${this.TEMPLATE_PATH}/partials/profession-subname.html`,
],
scrollable: [""],
},
motivations: {
template: `${this.TEMPLATE_PATH}/parts/motivations-tab.html`,
tabs: this.BASE_PARTS.tabs,
rightBar: {
template: `${this.TEMPLATE_PATH}/parts/right-bar.html`,
templates: [
`${this.TEMPLATE_PATH}/parts/skills-tab-agent.html`,
`${this.TEMPLATE_PATH}/parts/combat-tab-agent.html`,
`${this.TEMPLATE_PATH}/parts/gear-tab-agent.html`,
`${this.TEMPLATE_PATH}/parts/motivations-tab-agent.html`,
`${this.TEMPLATE_PATH}/parts/personal-tab-agent.html`,
`${this.TEMPLATE_PATH}/parts/effects-tab.html`,
`${this.TEMPLATE_PATH}/parts/about-tab.html`,
`${this.TEMPLATE_PATH}/partials/agent-skill-row-partial.html`,
`${this.TEMPLATE_PATH}/partials/agent-special-training-row-partial.html`,
`${this.TEMPLATE_PATH}/partials/custom-skills-partial-agent.html`,
`${this.TEMPLATE_PATH}/partials/bonds-section-partial.html`,
...this.GEAR_SECTION_PARTIALS,
`${this.TEMPLATE_PATH}/partials/injuries-section-partial.html`,
`${this.TEMPLATE_PATH}/partials/notes-partial.html`,
],
},
gear: this.BASE_PARTS.gear,
bio: {
template: `${this.TEMPLATE_PATH}/parts/bio-tab.html`,
templates: [`${this.TEMPLATE_PATH}/partials/notes-partial.html`],
scrollable: [""],
},
bonds: {
template: `${this.TEMPLATE_PATH}/parts/bonds-tab.html`,
scrollable: [""],
},
about: this.BASE_PARTS.about,
});
/* -------------------------------------------- */
/** @override */
_configureRenderOptions(options) {
super._configureRenderOptions(options);
if (!this.actor.limited) return;
options.parts = ["leftBar", "tabs", "rightBar"];
}
/** @override */
_prepareTabs(group) {
const tabs = super._prepareTabs(group);
if (
!this.actor.limited ||
this.actor.type !== "agent" ||
group !== "primary"
) {
return tabs;
}
return {
personal: { ...tabs.personal, active: true, cssClass: "active" },
};
}
/** @override */
async _prepareContext(options) {
const context = await super._prepareContext(options);
context.tabs = this._prepareTabs("primary");
context.leftBarRestSanityTabs = this._prepareTabs("leftBarRestSanity");
if (this.actor.type !== "agent") return context;
const sortSkillsByColumn = game.settings.get("deltagreen", "sortSkills");
context.sortSkillsByColumn = sortSkillsByColumn;
context.skillColumns = prepareAgentSkillColumns(
this.actor.system.sortedSkills,
sortSkillsByColumn,
);
context.specialTrainingRows = this._prepareSpecialTrainingBlocks();
context.showSpecialTrainingSection = context.specialTrainingRows.length > 0;
context.specialTrainingColumns = prepareAgentSkillColumns(
context.specialTrainingRows,
sortSkillsByColumn,
);
context.customSkillsEditMode = this._customSkillsEditMode;
context.physicalUi = this._preparePhysicalUi();
context.professionItem = this.actor.items.find(
(i) => i.type === "profession",
);
return context;
}
/** @returns {object} */
_preparePhysicalUi() {
const { physical } = this.actor.system;
const isExhausted = DGAgentSheet._isActorExhausted(this.actor);
const stimulantActive = hasActiveStimulantEffect(this.actor);
const suppressExhaustion = getEffectiveSuppressExhaustion(this.actor);
return {
isExhausted,
displayPenalty: isExhausted ? physical.exhaustedPenalty : 0,
suppressExhaustionInactive: !isExhausted,
suppressExhaustionInputDisabled: !isExhausted || stimulantActive,
suppressExhaustionChecked: isExhausted && suppressExhaustion,
};
}
/** @override */
_sortSkills() {
const sortedSkills = [];
for (const [key, skill] of Object.entries(this.actor.system.skills)) {
let sortLabel;
if (game.i18n.lang === "ja") {
sortLabel = game.i18n.localize(`DG.Skills.ruby.${key}`);
} else {
sortLabel = game.i18n.localize(`DG.Skills.${key}`);
}
if (sortLabel === "" || sortLabel === `DG.Skills.${key}`) {
sortLabel = key;
}
sortedSkills.push({
...skill,
skillKind: "fixed",
key,
sortLabel,
});
}
for (const [key, skill] of Object.entries(this.actor.system.typedSkills)) {
const displayLabel = formatProfessionSkillLabel({
kind: "typed",
group: skill.group,
label: skill.label,
});
sortedSkills.push({
...skill,
skillKind: "typed",
key,
displayLabel,
sortLabel: displayLabel,
actorType: this.actor.type,
});
}
sortedSkills.sort((a, b) =>
a.sortLabel.localeCompare(b.sortLabel, game.i18n.lang),
);
this.actor.system.sortedSkills = sortedSkills;
}
/** @override */
_prepareSkillTooltips() {
for (const skill of this.actor.system.sortedSkills) {
if (skill.skillKind === "typed") {
skill.tooltip = buildSkillTooltip(
"sheet",
this.actor,
{ kind: "typed", group: skill.group, label: skill.label },
{ proficiency: Number(skill.proficiency) || 0 },
);
} else {
skill.tooltip = buildSkillTooltip(
"sheet",
this.actor,
{ kind: "fixed", key: skill.key },
{ proficiency: Number(skill.proficiency) || 0 },
);
}
}
}
/** @override */
async _onRender(context, options) {
await super._onRender(context, options);
this._syncCustomSkillsEditModeUi();
}
/** @returns {void} */
_syncCustomSkillsEditModeUi() {
this.element?.classList.toggle(
"custom-skills-edit-mode",
Boolean(this._customSkillsEditMode),
);
}
/**
* @param {PointerEvent} event
* @param {HTMLElement} target
*/
static async _toggleAcuteEpisode(event, target) {
event.preventDefault();
const itemId =
target.dataset.itemId ??
target.closest("[data-item-id]")?.dataset?.itemId;
const item = this.actor.items.get(itemId);
if (!item || item.type !== "motivation") return;
await item.update({
"system.acuteEpisode": !item.system.acuteEpisode,
});
}
/**
* @param {PointerEvent} _event
* @param {HTMLElement} target
*/
static async _openProfessionItem(_event, target) {
const itemId =
target.dataset.itemId ??
target.closest("[data-item-id]")?.dataset?.itemId;
const item = this.actor.items.get(itemId);
if (item?.type !== "profession") return;
await showRenameProfessionDialog(item);
}
static _clearBondDamage() {
for (const bond of this.actor.itemTypes.bond) {
if (bond.system.hasBeenDamagedSinceLastHomeScene) {
bond.update({ "system.hasBeenDamagedSinceLastHomeScene": false });
}
}
}
/**
* Resets the actor's current breaking point by recalculating it as the difference
@@ -71,8 +318,11 @@ export default class DGAgentSheet extends DGActorSheet {
* @returns {void}
*/
static _resetBreakingPoint() {
const pow =
this.actor.system.statistics.pow.effectiveValue ??
this.actor.system.statistics.pow.value;
const currentBreakingPoint = Math.max(
this.actor.system.sanity.value - this.actor.system.statistics.pow.value,
this.actor.system.sanity.value - pow,
0,
);
@@ -93,6 +343,218 @@ export default class DGAgentSheet extends DGActorSheet {
this.actor.update(dataToUpdate);
}
/**
* Sorted special-training blocks for the skills tab.
*
* @returns {object[]}
*/
_prepareSpecialTrainingBlocks() {
const trainings = this._prepareSpecialTrainings().map((training) => ({
...training,
sortLabel: training.name.toUpperCase(),
actorType: this.actor.type,
}));
trainings.sort((a, b) =>
a.sortLabel.localeCompare(b.sortLabel, game.i18n.lang),
);
return trainings;
}
static _openStatsEdit() {
const form = new ActorEditStatForm({
actor: this.actor,
});
form.render(true);
}
/**
* @param {number} raw
* @returns {number}
*/
static _normalizeExhaustionPenalty(raw) {
const value = Number(raw);
if (Number.isNaN(value) || value === 0) return -20;
return value > 0 ? -1 * Math.abs(value) : value;
}
static async _exhaustAgent() {
const { actor } = this;
const content = await renderTemplate(
`${BASE_TEMPLATE_PATH}/dialog/exhaust-agent.html`,
{
penalty: actor.system.physical.exhaustedPenalty ?? -20,
},
);
let penalty = null;
const confirmed = await showDgDialog({
modifier: "exhaust-agent",
content,
window: {
title: game.i18n.localize("DG.Physical.ExhaustDialogTitle"),
},
buttons: [
{
action: "rollWpLoss",
label: game.i18n.localize("DG.Physical.RollWillpowerLoss"),
default: true,
icon: "fas fa-dice",
callback: (_event, _button, dialog) => {
const input = dialog.element.querySelector(
'[name="exhaustionPenalty"]',
);
penalty = DGAgentSheet._normalizeExhaustionPenalty(input?.value);
return true;
},
},
],
});
if (!confirmed || penalty === null) return;
const wpRoll = await new Roll("1d6").evaluate();
const loss = wpRoll.total;
const currentWp = Number(actor.system.wp.value) || 0;
const maxWp = Number(actor.system.wp.max) || 0;
const newWp = Math.max(0, currentWp - loss);
await actor.update({
"system.physical.exhausted": true,
"system.physical.exhaustedPenalty": penalty,
"system.wp.value": newWp,
});
await createAgentResourceChatMessage({
actor,
token: this.token,
roll: wpRoll,
rollLabelKey: "DG.Physical.Chat.ExhaustedRollLabel",
i18nData: {
willpowerChange: buildWillpowerChangeSpan({
amount: loss,
current: newWp,
max: maxWp,
}),
},
});
}
static async _restAgent() {
const { actor } = this;
const wasExhausted = DGAgentSheet._isActorExhausted(actor);
const wpRoll = await new Roll("1d6").evaluate();
const gain = wpRoll.total;
const currentWp = Number(actor.system.wp.value) || 0;
const maxWp = Number(actor.system.wp.max) || 0;
const newWp = Math.min(maxWp, currentWp + gain);
await clearStimulantEffects(actor);
await actor.update({
"system.physical.exhausted": false,
"system.physical.suppressExhaustion": false,
"system.wp.value": newWp,
});
await createAgentResourceChatMessage({
actor,
token: this.token,
roll: wpRoll,
rollLabelKey: wasExhausted
? "DG.Physical.Chat.RestedRollLabelExhausted"
: "DG.Physical.Chat.RestedRollLabel",
i18nData: {
willpowerChange: buildWillpowerChangeSpan({
amount: gain,
current: newWp,
max: maxWp,
}),
},
});
}
/**
* @param {Actor} actor
* @returns {boolean}
*/
static _isActorExhausted(actor) {
return Boolean(actor._source?.system?.physical?.exhausted);
}
static async _takeStimulants() {
const { actor } = this;
if (!DGAgentSheet._isActorExhausted(actor)) {
ui.notifications.info(
game.i18n.format("DG.Physical.StimulantsNotExhaustedWarning", {
name: actor.name,
}),
);
return;
}
const content = await renderTemplate(
`${BASE_TEMPLATE_PATH}/dialog/take-stimulants.html`,
{},
);
const choice = await showDgDialog({
modifier: "stimulants",
content,
window: {
title: game.i18n.localize("DG.Physical.StimulantsDialogTitle"),
},
buttons: [
{
action: "regular",
label: game.i18n.localize("DG.Physical.StimulantsRegular"),
default: true,
callback: () => "regular",
},
{
action: "hard",
label: game.i18n.localize("DG.Physical.StimulantsHard"),
callback: () => "hard",
},
],
});
if (!choice) return;
const formula = choice === "hard" ? "2d6" : "1d6";
const hoursRoll = await new Roll(formula).evaluate();
const hours = hoursRoll.total;
const rollLabelKey =
choice === "hard"
? "DG.Physical.Chat.StimulantsHardRollLabel"
: "DG.Physical.Chat.StimulantsRegularRollLabel";
const appliedHours = await applyStimulantEffect(actor, hours);
await createAgentResourceChatMessage({
actor,
token: this.token,
roll: hoursRoll,
rollLabelKey,
i18nData: {
hours: appliedHours,
},
});
}
/**
* @param {Record<string, object>} skillMap
* @returns {object[]}
*/
static _collectFailedSkills(skillMap) {
return Object.entries(skillMap)
.filter(([, skill]) => skill.failure && !skill.cannotBeImprovedByFailure)
.map(([key, skill]) => ({ ...skill, key }));
}
/**
* Runs through the whole process of improving skills,
* i.e., prompting the user, rolling, and creating the chat card.
@@ -102,12 +564,8 @@ export default class DGAgentSheet extends DGActorSheet {
static async _processSkillImprovements() {
const { skills, typedSkills } = this.actor.system;
const failedSkills = Object.values(skills).filter(
(skill) => skill.failure && !skill.cannotBeImprovedByFailure,
);
const failedTypedSkills = Object.values(typedSkills).filter(
(skill) => skill.failure && !skill.cannotBeImprovedByFailure,
);
const failedSkills = DGAgentSheet._collectFailedSkills(skills);
const failedTypedSkills = DGAgentSheet._collectFailedSkills(typedSkills);
if (failedSkills.length + failedTypedSkills.length === 0) {
ui.notifications.warn("DG.Skills.ApplySkillImprovements.Warning", {
@@ -126,18 +584,21 @@ export default class DGAgentSheet extends DGActorSheet {
if (!prompt) return null;
const resultObj = await this._createSkillImprovementRolls(
const resultObj = await evaluateSkillImprovementRolls(
this.actor,
baseRollFormula,
failedSkills,
failedTypedSkills,
);
await this._createSkillImprovementChatCard(
baseRollFormula,
await createSkillImprovementChatMessage({
actor: this.actor,
token: this.token,
baseFormula: baseRollFormula,
failedSkills,
failedTypedSkills,
resultObj,
);
});
return this._applySkillImprovements(
failedSkills,
@@ -180,14 +641,16 @@ export default class DGAgentSheet extends DGActorSheet {
failedTypedSkills,
) {
const localizedFailedSkills = failedSkills.map((skill) =>
game.i18n.localize(`DG.Skills.${skill.key}`),
formatProfessionSkillLabel({ kind: "fixed", key: skill.key }),
);
const localizedFailedTypedSkills = failedTypedSkills.map((skill) => {
const groupKey = `DG.TypeSkills.${skill.group.replace(/\s+/g, "")}`;
const groupLabel = game.i18n.localize(groupKey);
return `${groupLabel} (${skill.label})`;
});
const localizedFailedTypedSkills = failedTypedSkills.map((skill) =>
formatProfessionSkillLabel({
kind: "typed",
group: skill.group,
label: skill.label,
}),
);
const failedSkillNames = [
...localizedFailedSkills,
@@ -198,12 +661,12 @@ export default class DGAgentSheet extends DGActorSheet {
`${BASE_TEMPLATE_PATH}/dialog/apply-skill-improvements.html`,
{
failedSkillNames,
baseFormula:
DGAgentSheet._getSkillImprovementFormulaAsPercent(baseFormula),
baseFormula: getSkillImprovementFormulaAsPercent(baseFormula),
},
);
return foundry.applications.api.DialogV2.wait({
return showDgDialog({
modifier: "apply-skill-improvements",
content,
window: {
title: game.i18n.localize("DG.Skills.ApplySkillImprovements.Title"),
@@ -220,109 +683,56 @@ export default class DGAgentSheet extends DGActorSheet {
}
/**
* Generates and evaluates the rolls for skill improvements based on failed skills.
*
* @param {SkillImprovementFormula} baseFormula - The formula used to calculate skill improvements.
* @param {FailedSkill[]} failedSkills - An array of failed skills.
* @param {FailedTypedSkill[]} failedTypedSkills - An array of failed typed skills.
*
* @returns {Promise<{roll: Roll|undefined, resultObj: ResultObj}>} - An object containing the roll result and a map of skill keys to improvement values.
*
* @throws {Error} - Throws an error if the baseFormula is unknown.
* @param {DragEvent} event
* @param {Item} item
* @returns {Promise<Item|null>}
*/
async _createSkillImprovementRolls(
baseFormula,
failedSkills,
failedTypedSkills,
) {
const totalFailures = failedSkills.length + failedTypedSkills.length;
if (!Object.keys(DG.skillImprovementFormulas).includes(baseFormula)) {
throw new Error(`Unknown roll formula: ${baseFormula}`);
async _onDropItem(event, item) {
const dropType = item.type ?? item.system?.type;
if (dropType !== "profession") {
return super._onDropItem(event, item);
}
const rollPromises = [];
const resultObj = {};
if (baseFormula !== "1") {
for (let i = 0; i < totalFailures; i++) {
rollPromises.push(new Roll(baseFormula, this.actor.system).evaluate());
}
const rolls = await Promise.all(rollPromises);
const itemData =
typeof item.toObject === "function"
? item.toObject()
: foundry.utils.duplicate(item);
[...failedSkills, ...failedTypedSkills].forEach((skill, index) => {
resultObj[skill.key] = rolls[index].total;
});
}
return resultObj;
return assignProfessionToAgent(this.actor, itemData, {
token: this.token,
});
}
/**
* Create a chat card to record skill improvements.
*
* @param {FailedSkill[]} failedSkills - array of failed skills
* @param {FailedTypedSkill[]} failedTypedSkills - array of failed typed skills
* @param {Roll|undefined} roll - the improvement roll
* @param {ResultObj} resultObj
*
* @returns {Promise<ChatMessage>}
* @param {string} type
* @returns {Promise<Item|Item[]>}
*/
_createSkillImprovementChatCard(
baseFormula,
failedSkills,
failedTypedSkills,
resultObj,
) {
const localizeFailedSkills = (skillsArray) => {
return skillsArray.map((skill) => {
const increment = resultObj[skill.key] ?? 1;
const label =
skill.label ?? game.i18n.localize(`DG.Skills.${skill.key}`); // fallback for regular skills
const groupLabel = skill.group
? `${game.i18n.localize(
`DG.TypeSkills.${skill.group.replace(/\s+/g, "")}`,
)} (${label})`
: label;
_onItemCreate(type) {
if (type !== "profession") {
return super._onItemCreate(type);
}
return `${groupLabel}: <b>+${increment}%</b>`;
});
};
const name = game.i18n.format("DG.FallbackText.newItem", {
type: game.i18n.localize(`TYPES.Item.${type}`),
});
const failedSkillNames = localizeFailedSkills(failedSkills);
const failedTypedSkillNames = localizeFailedSkills(failedTypedSkills);
// Prepare chat data
const content = [...failedSkillNames, ...failedTypedSkillNames].join(", ");
const flavor = game.i18n.format(
"DG.Skills.ApplySkillImprovements.ChatFlavor",
{
formula: DGAgentSheet._getSkillImprovementFormulaAsPercent(baseFormula),
const itemData = {
name,
type: "profession",
system: {
bonds: 0,
automaticSkills: {},
automaticSkillMeta: {},
optionSkills: { [PROFESSION_OPTION_PICKS_KEY]: 0 },
optionSkillMeta: {},
},
);
const chatData = {
speaker: ChatMessage.getSpeaker({
actor: this.actor,
token: this.token,
alias: this.actor.name,
}),
content,
flavor,
rollMode: game.settings.get("core", "rollMode"),
};
return ChatMessage.create(chatData);
return assignProfessionToAgent(this.actor, itemData, {
token: this.token,
}).then((created) => (created ? [created] : []));
}
/**
* Updates the actor's skills / typed skills with the improvements,
* persisting the changes to the database.
*
* @param {FailedSkill[]} failedSkills - array of failed skills that need to be updated
* @param {FailedTypedSkill[]} failedTypedSkills - array of failed skills that need to be updated
* @param {ResultObj} resultObj
* @returns {Promise<DeltaGreenActor>} - the update promise
*/
_applySkillImprovements(failedSkills, failedTypedSkills, resultObj) {
const updateSkills = (skillsArray, updatedTarget) => {
skillsArray.forEach((skill) => {
@@ -344,17 +754,4 @@ export default class DGAgentSheet extends DGActorSheet {
system: { skills: updatedSkills, typedSkills: updatedTypedSkills },
});
}
/**
* Takes a base formula and returns a string representing the skill improvement formula as a percentage.
* TODO: Move to the roll class itself, probably even a "SkillImprovementRoll".
*
* @param {SkillImprovementFormula} baseFormula - the base formula to convert
* @returns {string} the skill improvement formula as a percentage (e.g. "1D3%" or "1%")
*/
static _getSkillImprovementFormulaAsPercent(baseFormula) {
const flat = baseFormula === "1";
const formula = flat ? "1" : `1${baseFormula}`;
return `${formula}%`.toUpperCase();
}
}
File diff suppressed because it is too large Load Diff
+311
View File
@@ -0,0 +1,311 @@
import { BASE_TEMPLATE_PATH } from "../config/index.js";
import {
getDisorderLabel,
isKnownDisorderId,
} from "../profession/disorders.js";
import DGSheetMixin from "./base-sheet.js";
import EffectsTabMixin from "./mixins/effects-tab-mixin.js";
import ProfessionItemMixin from "./mixins/profession-item-mixin.js";
import { getDGRollToken } from "../chat/dg-chat-card.js";
import { createDGRollFromDataset, processDGRoll } from "../roll/roll.js";
const { ItemSheetV2 } = foundry.applications.sheets;
const ITEM_PARTS_PATH = `${BASE_TEMPLATE_PATH}/item/parts`;
const TAB_NAV_TEMPLATE = "templates/generic/tab-navigation.hbs";
/** Tab group id used by item sheet templates (`data-group="primary"`). */
const ITEM_TAB_GROUP = "primary";
const EFFECTS_TAB = /** @type {const} */ ({
id: "effects",
label: "DG.Navigation.Item.effects",
});
const EFFECTS_PART = /** @type {const} */ ({
effects: {
template: `${ITEM_PARTS_PATH}/effects-tab.html`,
scrollable: [""],
},
});
const TABS_PART = /** @type {const} */ ({
tabs: { template: TAB_NAV_TEMPLATE },
});
/**
* @param {object} config
* @param {string} config.header
* @param {string} config.description
* @param {string} config.descriptionLabel
* @returns {object}
*/
function activeEffectItemLayout({ header, description, descriptionLabel }) {
return {
tabs: {
initial: "description",
tabs: [{ id: "description", label: descriptionLabel }, EFFECTS_TAB],
},
parts: {
header: { template: header },
...TABS_PART,
description: { template: description, scrollable: [""] },
...EFFECTS_PART,
},
};
}
/** Tab and part layout per item type. */
const ITEM_SHEET_LAYOUT = /** @type {const} */ ({
weapon: activeEffectItemLayout({
header: `${ITEM_PARTS_PATH}/weapon-header.html`,
description: `${ITEM_PARTS_PATH}/weapon-description.html`,
descriptionLabel: "DG.ItemWindow.Weapons.Description",
}),
gear: activeEffectItemLayout({
header: `${ITEM_PARTS_PATH}/gear-header.html`,
description: `${ITEM_PARTS_PATH}/gear-description.html`,
descriptionLabel: "DG.ItemWindow.Gear.Description",
}),
armor: activeEffectItemLayout({
header: `${ITEM_PARTS_PATH}/armor-header.html`,
description: `${ITEM_PARTS_PATH}/armor-description.html`,
descriptionLabel: "DG.ItemWindow.Armor.Description",
}),
bond: activeEffectItemLayout({
header: `${ITEM_PARTS_PATH}/bond-header.html`,
description: `${ITEM_PARTS_PATH}/bond-description.html`,
descriptionLabel: "DG.ItemWindow.Bonds.Description",
}),
motivation: activeEffectItemLayout({
header: `${ITEM_PARTS_PATH}/motivation-header.html`,
description: `${ITEM_PARTS_PATH}/motivation-description.html`,
descriptionLabel: "DG.ItemWindow.Motivations.Description",
}),
tome: {
tabs: {
initial: "description",
tabs: [
{ id: "description", label: "DG.ItemWindow.Tome.Description" },
{
id: "handler",
label: "DG.ItemWindow.Tome.HandlerOnlyTabHeader",
gmOnly: true,
},
],
},
parts: {
header: { template: `${ITEM_PARTS_PATH}/tome-header.html` },
...TABS_PART,
description: {
template: `${ITEM_PARTS_PATH}/tome-description.html`,
scrollable: [""],
},
handler: {
template: `${ITEM_PARTS_PATH}/tome-handler.html`,
scrollable: [""],
},
},
},
ritual: {
tabs: {
initial: "description",
tabs: [
{ id: "description", label: "DG.ItemWindow.Tome.Description" },
{
id: "handler",
label: "DG.ItemWindow.Tome.HandlerOnlyTabHeader",
gmOnly: true,
},
],
},
parts: {
header: { template: `${ITEM_PARTS_PATH}/ritual-header.html` },
...TABS_PART,
description: {
template: `${ITEM_PARTS_PATH}/ritual-description.html`,
scrollable: [""],
},
handler: {
template: `${ITEM_PARTS_PATH}/ritual-handler.html`,
scrollable: [""],
},
},
},
profession: {
tabs: {
initial: "description",
tabs: [
{ id: "description", label: "DG.ItemWindow.Profession.Description" },
{ id: "skills", label: "DG.ItemWindow.Profession.SkillsTab" },
],
},
parts: {
header: { template: `${ITEM_PARTS_PATH}/profession-header.html` },
...TABS_PART,
description: {
template: `${ITEM_PARTS_PATH}/profession-description.html`,
scrollable: [""],
},
skills: {
template: `${ITEM_PARTS_PATH}/profession-skills-tab.html`,
scrollable: [".profession-skills-scroll"],
},
},
},
});
const ItemSheetBase = ProfessionItemMixin(
EffectsTabMixin(DGSheetMixin(ItemSheetV2)),
);
/**
* @extends {ItemSheetBase}
*/
export default class DGItemSheet extends ItemSheetBase {
/** @override */
static DEFAULT_OPTIONS = /** @type {const} */ ({
classes: ["item"],
position: { width: 520, height: 600 },
actions: {
roll: DGItemSheet._onRoll,
toggleTomeRevealed: DGItemSheet._toggleTomeRevealed,
addAutomaticSkill: DGItemSheet.addAutomaticSkill,
addOptionSkill: DGItemSheet.addOptionSkill,
removeAutomaticSkill: DGItemSheet.removeAutomaticSkill,
removeOptionSkill: DGItemSheet.removeOptionSkill,
createEffect: ItemSheetBase.createEffect,
openEffect: ItemSheetBase.openEffect,
deleteEffect: ItemSheetBase.deleteEffect,
toggleEffect: ItemSheetBase.toggleEffect,
},
});
/** @inheritdoc */
get item() {
return this.document;
}
/** @inheritdoc */
_configureRenderParts(options) {
const layout = ITEM_SHEET_LAYOUT[this.item.type];
if (!layout) return super._configureRenderParts(options);
const parts = foundry.utils.deepClone(layout.parts);
if (layout.tabs) {
const tabIds = layout.tabs.tabs
.filter((tab) => !tab.gmOnly || game.user.isGM)
.map((tab) => tab.id);
for (const partId of Object.keys(parts)) {
if (
partId !== "tabs" &&
partId !== "header" &&
!tabIds.includes(partId)
) {
delete parts[partId];
}
}
}
Object.values(parts).forEach((p) => {
p.templates ??= [];
});
return parts;
}
/** @inheritdoc */
_getTabsConfig(group) {
const layout = ITEM_SHEET_LAYOUT[this.item.type];
if (!layout?.tabs || group !== ITEM_TAB_GROUP) {
return super._getTabsConfig(group);
}
const tabs = layout.tabs.tabs.filter(
(tab) => !tab.gmOnly || game.user.isGM,
);
return { ...layout.tabs, tabs };
}
/** @inheritdoc */
async _preparePartContext(partId, context, options) {
const partContext = await super._preparePartContext(
partId,
context,
options,
);
if (partId === "tabs") partContext.tabClasses = "sheet-tabs";
return partContext;
}
/** @inheritdoc */
async _prepareContext(options) {
const context = await super._prepareContext(options);
const layout = ITEM_SHEET_LAYOUT[this.item.type];
if (layout?.tabs) {
const { initial } = layout.tabs;
const tabIds = layout.tabs.tabs
.filter((tab) => !tab.gmOnly || game.user.isGM)
.map((tab) => tab.id);
if (!tabIds.includes(this.tabGroups[ITEM_TAB_GROUP])) {
this.tabGroups[ITEM_TAB_GROUP] = initial;
}
context.tabs = this._prepareTabs(ITEM_TAB_GROUP);
}
context.owner = this.document.isOwner;
if (this.item.type === "motivation" && this.item.system.disorder) {
context.disorderUsesKnownId = isKnownDisorderId(
this.item.system.disorder,
);
context.disorderLabel = getDisorderLabel(this.item.system.disorder);
}
return context;
}
/** @inheritdoc */
async _onFirstRender(context, options) {
await super._onFirstRender(context, options);
this._setRightClickListeners();
}
/**
* Right-click on roll controls opens the modifier dialog.
* @returns {void}
*/
_setRightClickListeners() {
this.element.addEventListener("contextmenu", (event) => {
const target = event.target.closest("[data-action='roll']");
if (!target) return;
event.preventDefault();
DGItemSheet._onRoll.call(this, event, target);
});
}
/**
* @param {PointerEvent} event
* @param {HTMLElement} target
*/
static async _onRoll(event, target) {
if (event.which === 2) return;
const roll = createDGRollFromDataset(target.dataset, {
actor: this.actor,
item: this.item,
element: target,
sanityDamageSource: "item",
token: getDGRollToken(this.actor, this.token),
});
await processDGRoll(event, roll);
}
/**
* Toggle whether tome/ritual details are visible to players.
* @param {PointerEvent} event
*/
static _toggleTomeRevealed(event) {
event.preventDefault();
const revealed = !this.item.system.revealed;
this.item.update({ "system.revealed": revealed });
}
}
+27 -1
View File
@@ -1,4 +1,6 @@
import DG, { BASE_TEMPLATE_PATH } from "../config.js";
import DG, { BASE_TEMPLATE_PATH } from "../config/index.js";
import getRichTextFieldsForPart from "../config/rich-text-fields.js";
import { prepareRichTextContext } from "../utils/rich-text.js";
const HbsAppMixin = foundry.applications.api.HandlebarsApplicationMixin;
@@ -29,8 +31,32 @@ const DGSheetMixin = (Base) => {
// Add the document to the context under a descriptive name (i.e. "actor", "item")
context[docName] = this.document;
context.richText = {};
return context;
}
/** @inheritdoc */
async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options);
const { documentName } = this.document;
const fieldSpecs = getRichTextFieldsForPart(documentName, partId, {
actorType: this.document.type,
itemType: this.document.type,
showNotesInSkills: context.showNotesInSkills,
});
if (fieldSpecs.length) {
Object.assign(
context.richText,
await prepareRichTextContext(this.document, fieldSpecs),
);
}
return context;
}
/** @inheritdoc */
async _onFirstRender(context, options) {
+61
View File
@@ -0,0 +1,61 @@
/** Copy typographic layout from the real input so the overlay matches except color. */
function syncAeDisplayTypography(input, display) {
const cs = getComputedStyle(input);
display.style.inset = "auto";
display.style.top = cs.borderTopWidth;
display.style.left = cs.borderLeftWidth;
display.style.right = cs.borderRightWidth;
display.style.bottom = cs.borderBottomWidth;
display.style.font = cs.font;
display.style.lineHeight = cs.lineHeight;
display.style.padding = cs.padding;
display.style.textAlign = cs.textAlign;
display.style.letterSpacing = cs.letterSpacing;
display.style.boxSizing = cs.boxSizing;
}
/** @type {WeakMap<HTMLElement, ResizeObserver>} */
const hostObservers = new WeakMap();
/** @param {typeof foundry.applications.api.ApplicationV2} Base */
export default function AeInputMixin(Base) {
return class AeInputHost extends Base {
/** @param {HTMLElement} root */
_bindAeBackedInputs(root) {
for (const host of root.querySelectorAll(".ae-input-host")) {
hostObservers.get(host)?.disconnect();
const input = host.querySelector("input");
const display = host.querySelector(".ae-input-display");
if (input && display) {
const modifier = Number(display.dataset.aeModifier) || 0;
const syncTypography = () => syncAeDisplayTypography(input, display);
const syncDisplay = () => {
if (host.matches(":focus-within")) return;
const base = Number(input.value);
if (Number.isFinite(base)) {
const effective = Math.max(0, Math.round(base + modifier));
display.textContent = String(effective);
}
};
input.addEventListener("focus", () => input.select());
input.addEventListener("blur", () => {
syncTypography();
syncDisplay();
});
syncTypography();
requestAnimationFrame(syncTypography);
syncDisplay();
const observer = new ResizeObserver(syncTypography);
observer.observe(input);
hostObservers.set(host, observer);
}
}
}
};
}

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