Boss sheet

This commit is contained in:
walcutt 2024-12-13 22:31:03 -05:00
parent d1d8bb8516
commit 31f29772f1
6 changed files with 377 additions and 1 deletions

View File

@ -1,4 +1,4 @@
import { HenchDataModel } from "./module/data-models.mjs";
import { BossDataModel, HenchDataModel } from "./module/data-models.mjs";
import { HenchActorSheet } from "./module/sheets/hench-actor-sheet.mjs";
@ -8,6 +8,15 @@ Handlebars.registerHelper('int2checkbox', (size, threshold, options) => {
).reduce((prev, next) => (prev + next), "");
});
Handlebars.registerHelper('partialint2checkbox', (size, threshold, start, end, options) => {
const indexBase = start + 1;
const arrSize = Math.max(end - start, 0);
return Array(arrSize).fill(0).map(
(e, i) => options.fn({ index: i + indexBase, marked: (i + start) < threshold })
).reduce((prev, next) => (prev + next), "");
});
Handlebars.registerHelper('partialList', (list, start, end, options) => {
return list.slice(start, end).map(
(e, i) => options.fn({ item: e, index: (start + i)})
@ -28,6 +37,7 @@ Handlebars.registerHelper('decrement', (value) => (value - 1));
Hooks.once("init", () => {
CONFIG.Actor.dataModels = {
hench: HenchDataModel,
boss: BossDataModel,
};
Actors.unregisterSheet('core', ActorSheet);

142
module/boss.mjs Normal file
View File

@ -0,0 +1,142 @@
export const nullStorylineKey = "FREEFORM";
export const storylineKeys = [
nullStorylineKey,
"BOOTSTRAPPER",
"VENGEANCE",
"DOWNWARD SPIRAL",
"DOMINION",
"CLEANUP CREW"
];
export function getBossMutation() {
return bossData;
}
const bossData = {
details: [
{
question: "What fuels your quest for villainy?",
answer: "",
},
{
question: "How do you dress your henches?",
answer: "",
},
{
question: "How is your lair designed? ",
answer: "",
},
{
question: "Where do you draw the line to what you will do? ",
answer: "",
},
],
moves: [
{
marked: true,
name: "Force of Will",
description: `Whenever you act, do not draw. Simply declare what you do, and what happens after.`,
hasWriteIn: false,
writeIn: null,
},
{
marked: false,
name: "Extra Armorment",
description: `Write in an additional gear type on each of your henches' sheets. They can take it on any future mission.`,
hasWriteIn: false,
writeIn: null,
},
{
marked: false,
name: "Rotating Armorment I",
description: `Erase the write-in gear from any number of your henches' sheets, and write in a new equipment.`,
hasWriteIn: false,
writeIn: null,
},
{
marked: false,
name: "Rotating Armorment II",
description: `Erase the write-in gear from any number of your henches' sheets, and write in a new equipment.`,
hasWriteIn: false,
writeIn: null,
},
{
marked: false,
name: "Rotating Armorment III",
description: `Erase the write-in gear from any number of your henches' sheets, and write in a new equipment.`,
hasWriteIn: false,
writeIn: null,
},
{
marked: false,
name: "Specialized Training",
description: `Write a new inclination below. It applies for all of your henches.`,
hasWriteIn: true,
writeIn: "",
},
{
marked: false,
name: "General Training",
description: `When you take this ability, fill the experience track of each of your henches immediately.`,
hasWriteIn: false,
writeIn: null,
},
{
marked: false,
name: "Exceptional Planning",
description: `At the start of each mission, remove one card from the deck. Any time one of your henches draws, you may replace the card they play with the removed card.`,
hasWriteIn: false,
writeIn: null,
},
{
marked: false,
name: "Limelight Lovers",
description: `If your heat is in Tier III, your henches can take 1 stress to take +1 card to any draw <em>(possibly drawing 4)</em>.`,
hasWriteIn: false,
writeIn: null,
},
{
marked: false,
name: "Plot Armor",
description: `Once per mission, one of your henches can ignore any single instance of harm they would take.`,
hasWriteIn: false,
writeIn: null,
},
{
marked: false,
name: "Play it Off",
description: `Your henches can resist consequences by taking 1 harm "Laughing Stock", instead of marking stress. They can do this even if all of their stress is marked.`,
hasWriteIn: false,
writeIn: null,
},
{
marked: false,
name: "Guild Bigwig",
description: `During fallout, you may draw a second time and use that card instead. Explain who intervenes on your behalf, and how.`,
hasWriteIn: false,
writeIn: null,
},
{
marked: false,
name: "Exit Plan",
description: `When you take this move, pick a hench to pass the mantle on to. You retire to peace, and they take your place. Is there any ceremony to the transfer? Who knows, and who doesn't?`,
hasWriteIn: false,
writeIn: null,
},
],
experienceTriggers: [
{
marked: false,
description: "Your henches followed even your most superfluous orders.",
},
{
marked: false,
description: "You achieved your evil ambitions.",
},
{
marked: false,
description: "News of your deeds spreads far and wide.",
},
]
};

View File

@ -1,5 +1,6 @@
const { HTMLField, SchemaField, NumberField, StringField, BooleanField, FilePathField, ArrayField } = foundry.data.fields;
import { getBossMutation, nullStorylineKey, storylineKeys } from './boss.mjs';
import { nullPlaybookKey, playbookKeys, lookupPlaybook, getPlaybookMutation } from './playbooks.mjs';
const textField = () => new StringField({ required: true, blank: true });
@ -110,4 +111,42 @@ export class HenchDataModel extends foundry.abstract.TypeDataModel {
this.customGear,
];
}
}
export class BossDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
return {
look: textField(),
details: cappedArrayField(promptField(), 4),
storyline: new StringField({ required: true, blank: false, initial: nullStorylineKey, options: storylineKeys}),
heat: new NumberField({ required: true, integer: true, min: 0, initial: 0, max: 18}),
experienceTriggers: cappedArrayField(markableField(), 4),
experience: new NumberField({ required: true, integer: true, min: 0, initial: 0, max: 5 }),
moves: cappedArrayField(moveField(), 13),
};
}
static migrateData(source) {
// no migrations yet
return super.migrateData(source);
}
/** @override */
async _preCreate(data, options, user) {
await super._preCreate(data, options, user);
const initMutation = getBossMutation();
return this.updateSource(initMutation);
}
get tier() {
const divisor = 18 / 3;
return Math.ceil(this.heat / divisor);
}
}

View File

@ -1,5 +1,6 @@
import { playbookKeys, validatePlaybookKey, getPlaybookMutation } from "../playbooks.mjs";
import { updateField } from "../helpers/mutation-helper.mjs";
import { storylineKeys } from "../boss.mjs";
export class HenchActorSheet extends ActorSheet {
/** @override */
@ -13,10 +14,12 @@ export class HenchActorSheet extends ActorSheet {
const context = super.getData();
context.playbookKeys = playbookKeys.map((k) => ({ key: k, selected: k === this.actor.system.playbook}));
context.storylineKeys = storylineKeys.map((k) => ({ key: k, selected: k === this.actor.system.storyline}));
// TODO define system constants for these
context.maxStress = 12;
context.maxExp = 5;
context.maxHeat = 18;
context.minGear = 3;
context.maxGear = 5;
@ -54,6 +57,14 @@ export class HenchActorSheet extends ActorSheet {
updateField(this.actor, path, value);
});
// normal dropdowns
html.find('.hench-hench-sheet-dropdown').on('change', (event) => {
const value = event.target.value;
const path = event.currentTarget.dataset.fieldPath;
updateField(this.actor, path, value);
});
// text fields
html.find('.hench-text-input').on('change', async (event) => {
const element = event.currentTarget;

View File

@ -48,6 +48,11 @@
font-size: 1.5em;
}
.hench-huge {
font-size: 3em;
font-weight: 700;
}
/* Flexbox */
.hench-row {
display: flex;

169
templates/actors/boss.hbs Normal file
View File

@ -0,0 +1,169 @@
<form>
<div class="hench-sheet-container hench-padding-wide hench-gap-wide hench-white">
<!-- ID row -->
<div class="hench-row hench-gap-wide">
<!-- core-->
<div class="hench-box hench-flex-resizeable hench-l-grey hench-padding-wide hench-gap-narrow">
<!-- Name -->
<div class="hench-field hench-row hench-gap-narrow">
<label class="hench-flex-fixed" for="hench-name">Name: </label>
<input type="text" name="hench-name" class="hench-text-input hench-flex-resizeable" value="{{actor.name}}" data-field-path="name" />
</div>
<!-- Look -->
<div class="hench-field hench-row hench-gap-narrow">
<label class="hench-flex-fixed" for="hench-look">Look: </label>
<input type="text" name="hench-look" class="hench-text-input hench-flex-resizeable" value="{{actor.system.look}}" data-field-path="system.look" />
</div>
<!-- Storyline -->
<div class="hench-field hench-row hench-gap-narrow">
<label class="hench-flex-fixed" for="hench-storyline">Storyline: </label>
<select name="hench-storyline" class="hench-hench-sheet-dropdown hench-flex-resizeable" data-field-path="system.storyline">
{{#each storylineKeys}}
<option value="{{this.key}}" {{#if this.selected}}selected{{/if}}>{{this.key}}</option>
{{/each}}
</select>
</div>
</div>
<div class="hench-box hench-d-grey hench-flex-resizeable hench-centered hench-huge">
THE BOSS
</div>
<!-- icon -->
<div class="hench-box hench-l-grey hench-flex-fixed">
<img src="{{actor.img}}" data-edit="img" class="hench-icon" />
</div>
</div>
<!-- Big Box -->
<div class="hench-row hench-gap-wide">
<!-- Column -->
<div class="hench-box hench-box-stretch hench-flex-fixed hench-gap-wide">
<!-- Details -->
<div class="hench-row hench-flex-resizeable">
<div class="hench-box hench-l-grey hench-flex-resizeable hench-padding-wide hench-gap-narrow">
<div class="hench-row hench-flex-resizeable">
<div class="hench-centered hench-title hench-flex-resizeable">
Details
</div>
</div>
{{#each actor.system.details}}
<div class="hench-row">
<label for="hench-detail-{{@index}}">{{this.question}}</label>
</div>
<div class="hench-row">
<input name="hench-detail-{{@index}}" type="text" class="hench-text-input" data-field-path="system.details[{{@index}}].answer" value="{{this.answer}}" />
</div>
{{/each}}
</div>
</div>
<!-- Heat -->
<div class="hench-row hench-flex-resizeable">
<div class="hench-box hench-l-grey hench-flex-resizeable hench-padding-wide hench-gap-narrow">
<div class="hench-row hench-flex-resizeable">
<div class="hench-centered hench-title hench-flex-resizeable">
Heat
</div>
</div>
<div class="hench-row hench-centered hench-flex-resizeable">
<div class="hench-flex-resizeable hench-centered">
<strong>Tier I</strong>
</div>
</div>
<div class="hench-row hench-centered hench-flex-resizeable">
<div class="hench-flex-resizeable hench-centered">
<em>(Nuisance, low-stakes, local cops)</em>
</div>
</div>
<div class="hench-row hench-flex-resizeable hench-row-even hench-gap-narrow">
{{#partialint2checkbox maxHeat actor.system.heat 0 6}}
<input type="checkbox" class="hench-checkbox-int-field" data-field-path="system.heat" data-value="{{index}}" {{#if marked}} checked {{/if}} />
{{/partialint2checkbox}}
</div>
<div class="hench-row hench-centered hench-flex-resizeable">
<div class="hench-flex-resizeable hench-centered">
<strong>Tier II</strong>
</div>
</div>
<div class="hench-row hench-centered hench-flex-resizeable">
<div class="hench-flex-resizeable hench-centered">
<em>(Full-time, classic stakes, local super)</em>
</div>
</div>
<div class="hench-row hench-flex-resizeable hench-row-even hench-gap-narrow">
{{#partialint2checkbox maxHeat actor.system.heat 6 12}}
<input type="checkbox" class="hench-checkbox-int-field" data-field-path="system.heat" data-value="{{index}}" {{#if marked}} checked {{/if}} />
{{/partialint2checkbox}}
</div>
<div class="hench-row hench-centered hench-flex-resizeable">
<div class="hench-flex-resizeable hench-centered">
<strong>Tier III</strong>
</div>
</div>
<div class="hench-row hench-centered hench-flex-resizeable">
<div class="hench-flex-resizeable hench-centered">
<em>(Top-class, high-stakes, world's finest heroes)</em>
</div>
</div>
<div class="hench-row hench-flex-resizeable hench-row-even hench-gap-narrow">
{{#partialint2checkbox maxHeat actor.system.heat 12 18}}
<input type="checkbox" class="hench-checkbox-int-field" data-field-path="system.heat" data-value="{{index}}" {{#if marked}} checked {{/if}} />
{{/partialint2checkbox}}
</div>
<!-- maybe add guidelines down here? -->
</div>
</div>
<!-- Experience -->
<div class="hench-row hench-flex-resizeable">
<div class="hench-box hench-l-grey hench-flex-resizeable hench-padding-narrow hench-gap-narrow">
<div class="hench-row hench-flex-resizeable">
<div class="hench-centered hench-title hench-flex-resizeable">
Experience
</div>
</div>
<div class="hench-row hench-row-even hench-gap-narrow hench-flex-resizeable">
{{#int2checkbox maxExp actor.system.experience}}
<input type="checkbox" class="hench-checkbox-int-field" data-field-path="system.experience" data-value="{{index}}" {{#if marked}} checked {{/if}} />
{{/int2checkbox}}
</div>
{{#each actor.system.experienceTriggers}}
<div class="hench-row hench-m-grey hench-flex-resizeable hench-padding-narrow hench-gap-narrow">
<div class="hench-box hench-flex-fixed">
<input type="checkbox" class="hench-checkbox hench-checkbox-toggle-field" data-field-path="system.experienceTriggers[{{@index}}].marked" {{#if this.marked}} checked {{/if}} />
</div>
<div class="hench-box hench-flex-resizeable">
{{this.description}}
</div>
</div>
{{/each}}
</div>
</div>
</div>
<!-- Moves -->
<div class="hench-box hench-flex-resizeable hench-m-grey hench-padding-narrow hench-gap-narrow">
<div class="hench-row">
<div class="hench-centered hench-title hench-flex-resizeable">
Abilities
</div>
</div>
{{#each actor.system.moves}}
<div class="hench-row hench-l-grey hench-flex-resizeable hench-gap-narrow">
<div class="hench-box hench-flex-fixed">
<input type="checkbox" name="hench-move-checkbox-{{@index}}" class="hench-checkbox-toggle-field" data-field-path="system.moves[{{@index}}].marked" {{#if this.marked}}checked{{/if}} />
</div>
<div class="hench-box hench-flex-resizeable hench-gap-narrow hench-padding-narrow">
<div>
<label for="hench-move-checkbox-{{@index}}"><em><strong>{{this.name}}</strong></em></label>
</div>
<div>
{{{this.description}}}
</div>
{{#if this.hasWriteIn}}
<div>
<input type="text" name="hench-move-writein-{{$index}}" class="hench-text-field" data-field-path="system.moves[{{@index}}].writein" value="{{this.writein}}" />
</div>
{{/if}}
</div>
</div>
{{/each}}
</div>
</div>
</div>
</form>