Compare commits

..

14 Commits
master ... dev

Author SHA1 Message Date
Joscha Maier
8470800ee3
further dev 2024-11-24 18:59:43 +01:00
Joscha Maier
dd42e5487c
a 2024-11-04 00:47:13 +01:00
Joscha Maier
bc71b3d2cc
Giving up again 2024-10-19 23:02:28 +02:00
Joscha Maier
54798bd300
I hate this software and documentation 2024-10-07 14:12:05 +02:00
Joscha Maier
8da6565aae
chore: more model stuff 2024-10-02 14:45:44 +02:00
Joscha Maier
ada2dc40c5
feat: continuing to add data models 2024-10-02 00:14:03 +02:00
Joscha Maier
ebed4c3622
The documentation for this shit is non existent holy fuck how are you supposed to know how to do this? they do not know themselves I bet 2024-09-30 16:09:02 +02:00
Joscha Maier
bcf1d8827d
stuff 2024-09-29 14:58:03 +02:00
WinterMyst
c9acf8b9ff Replace Kids_on_Brooms_Cover.jpg 2024-09-23 20:44:01 +00:00
WinterMyst
34c4f8ed5c Replace Kids_on_Brooms_Cover.jpg 2024-09-23 20:43:37 +00:00
WinterMyst
78ad7fc3ac Upload New File 2024-09-23 20:42:29 +00:00
Winter_Myst
9f6e16e741 feat: ci/cd 2024-09-22 01:44:24 +02:00
Winter_Myst
05626878d3 gitignore 2024-09-22 01:15:17 +02:00
Joscha Maier
84067f07dc
feat: updated to github version 2024-09-22 01:11:29 +02:00
51 changed files with 3205 additions and 2391 deletions

401
.gitignore vendored
View File

@ -1,7 +1,398 @@
# IDE ## Ignore Visual Studio temporary files, build results, and
.idea/ ## files generated by popular Visual Studio add-ons.
.vs/ ##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# Node Modules # User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/ node_modules/
package-lock.json
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml

View File

@ -1,84 +1,94 @@
image: ubuntu:latest
stages: stages:
- build - compile
- release - release
variables: # Compile Job (runs on every commit)
MANIFEST: "system.json" compile:
ZIPFILE: "kidsonbrooms.zip" stage: compile
PACKAGE_REGISTRY_URL: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/${CI_PROJECT_NAME}/${CI_COMMIT_TAG}" image: node:14
MANIFEST_RELEASE_URL: "${PACKAGE_REGISTRY_URL}/${MANIFEST}"
ZIPFILE_RELEASE_URL: "${PACKAGE_REGISTRY_URL}/${ZIPFILE}"
MANIFEST_PERMALINK_URL: "https://gitlab.com/${CI_PROJECT_NAMESPACE}/${CI_PROJECT_NAME}/-/releases/${CI_COMMIT_TAG}/downloads/${MANIFEST}"
ZIPFILE_PERMALINK_URL: "https://gitlab.com/${CI_PROJECT_NAMESPACE}/${CI_PROJECT_NAME}/-/releases/${CI_COMMIT_TAG}/downloads/${ZIPFILE}"
dry_run: true
# Build job
build:
stage: build
before_script:
# Install Node.js v21.x manually
- apt-get update && apt-get install -y curl
- curl -fsSL https://deb.nodesource.com/setup_21.x | bash -
- apt-get install -y nodejs
- node -v # Verify the correct Node.js version
# Install Gulp globally
- npm install --global gulp-cli
- gulp --version # Verify Gulp is installed
script: script:
- npm install --global gulp-cli
- npm install - npm install
- gulp build - gulp compile
artifacts:
paths:
- kidsonbrooms.zip
- system.json
- packs/
only: only:
- branches - branches
# Release job # Release Job (manually triggered with version)
release: release:
stage: release stage: release
rules: image: node:14
- if: $CI_COMMIT_TAG
variables:
dry_run: "false"
before_script: before_script:
# Install Node.js v21.x manually - apt-get update && apt-get install -y curl jq
- apt-get update && apt-get install -y curl
- curl -fsSL https://deb.nodesource.com/setup_21.x | bash -
- apt-get install -y nodejs
- node -v # Verify the correct Node.js version
# Install Gulp globally
- npm install --global gulp-cli - npm install --global gulp-cli
- gulp --version # Verify Gulp is installed
script:
- npm install - npm install
# Set up SSH agent and add private key for pushing to protected branch
- 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- ssh-keyscan gitlab.com >> ~/.ssh/known_hosts
script:
# Check if VERSION is provided
- if [ -z "$VERSION" ]; then echo "Error: VERSION variable is required." && exit 1; fi
# Run Gulp release task (includes zipping)
- gulp release - gulp release
# Create GitLab release # Create a release on GitLab
create-release: - export RELEASE_RESPONSE=$(curl --request POST \
stage: release --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
image: registry.gitlab.com/gitlab-org/release-cli:latest --header "Content-Type: application/json" \
needs: --data '{
- job: release "name": "Release v'$VERSION'",
rules: "tag_name": "v'$VERSION'",
- if: $CI_COMMIT_TAG "description": "Release v'$VERSION'",
script: "ref": "master",
- echo "Creating GitLab release for $CI_COMMIT_TAG" "assets": {
release: "links": [
name: "$CI_COMMIT_TAG" {
tag_name: "$CI_COMMIT_TAG" "name": "Download kids-on-brooms.zip",
description: "Release $CI_COMMIT_TAG of $CI_PROJECT_NAME." "url": "https://gitlab.com/YOUR_NAMESPACE/YOUR_PROJECT/-/jobs/$CI_JOB_ID/artifacts/download"
assets: }
links: ]
- name: "$MANIFEST" }
url: "${MANIFEST_RELEASE_URL}" }' "https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/releases")
filepath: "/${MANIFEST}"
- name: "$ZIPFILE"
url: "${ZIPFILE_RELEASE_URL}"
filepath: "/${ZIPFILE}"
# Get the release URL from the response
- export RELEASE_URL=$(echo $RELEASE_RESPONSE | jq -r '.assets.links[0].url')
# Update the system.json file with the release URL
- sed -i "s|\"download\":.*|\"download\": \"$RELEASE_URL\",|" system.json
# Commit the updated system.json and push it to master
- git config --global user.name "GitLab CI"
- git config --global user.email "ci@gitlab.com"
- git add system.json
- git commit -m "Update system.json with release URL"
- git push origin master
# Publish the release to the Foundry API
- curl -X POST https://api.foundryvtt.com/_api/packages/release_version/ \
-H "Authorization: $FOUNDRY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"id": "Your-Package-ID",
"release": {
"version": "'$VERSION'",
"manifest": "https://gitlab.com/wintermyst/kids-on-brooms/-/raw/master/system.json",
"notes": "https://gitlab.com/wintermyst/kids-on-brooms/releases/tag/v'$VERSION'",
"compatibility": {
"minimum": "12.331",
"verified": "12.331",
"maximum": ""
}
}
}'
only:
- master
when: manual
allow_failure: false
artifacts:
paths:
- kids-on-brooms.zip
expire_in: never

View File

@ -1,10 +0,0 @@
# IDE
.idea/
.vs/
# Node Modules
node_modules/
npm-debug.log
# Foundry
*.lock

BIN
Kids_on_Brooms_Cover.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 915 KiB

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Joscha Maier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

2
README.md Normal file
View File

@ -0,0 +1,2 @@
# KidsOnBroomsFoundryVTT
The Kids on Brooms System for Foundry VTT

View File

@ -1,548 +0,0 @@
@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap");
/* Global styles */
@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap");
.window-app {
font-family: "Roboto", sans-serif;
}
.window-app .window-content > * {
flex:0;
}
.rollable:hover, .rollable:focus {
color: #000;
text-shadow: 0 0 10px rgb(146, 0, 225);
cursor: pointer;
}
.grid {
display: grid;
gap: 10px;
margin: 10px 0;
padding: 0;
}
.grid-start-2 {
grid-column-start: 2;
}
.grid-span-2 {
grid-column-end: span 2;
}
.grid-2col {
grid-column: span 2/span 2;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.grid-start-3 {
grid-column-start: 3;
}
.grid-span-3 {
grid-column-end: span 3;
}
.grid-3col {
grid-column: span 3/span 3;
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.grid-start-4 {
grid-column-start: 4;
}
.grid-span-4 {
grid-column-end: span 4;
}
.grid-4col {
grid-column: span 4/span 4;
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.grid-start-5 {
grid-column-start: 5;
}
.grid-span-5 {
grid-column-end: span 5;
}
.grid-5col {
grid-column: span 5/span 5;
grid-template-columns: repeat(5, minmax(0, 1fr));
}
.grid-start-6 {
grid-column-start: 6;
}
.grid-span-6 {
grid-column-end: span 6;
}
.grid-6col {
grid-column: span 6/span 6;
grid-template-columns: repeat(6, minmax(0, 1fr));
}
.grid-start-7 {
grid-column-start: 7;
}
.grid-span-7 {
grid-column-end: span 7;
}
.grid-7col {
grid-column: span 7/span 7;
grid-template-columns: repeat(7, minmax(0, 1fr));
}
.grid-start-8 {
grid-column-start: 8;
}
.grid-span-8 {
grid-column-end: span 8;
}
.grid-8col {
grid-column: span 8/span 8;
grid-template-columns: repeat(8, minmax(0, 1fr));
}
.grid-start-9 {
grid-column-start: 9;
}
.grid-span-9 {
grid-column-end: span 9;
}
.grid-9col {
grid-column: span 9/span 9;
grid-template-columns: repeat(9, minmax(0, 1fr));
}
.grid-start-10 {
grid-column-start: 10;
}
.grid-span-10 {
grid-column-end: span 10;
}
.grid-10col {
grid-column: span 10/span 10;
grid-template-columns: repeat(10, minmax(0, 1fr));
}
.grid-start-11 {
grid-column-start: 11;
}
.grid-span-11 {
grid-column-end: span 11;
}
.grid-11col {
grid-column: span 11/span 11;
grid-template-columns: repeat(11, minmax(0, 1fr));
}
.grid-start-12 {
grid-column-start: 12;
}
.grid-span-12 {
grid-column-end: span 12;
}
.grid-12col {
grid-column: span 12/span 12;
grid-template-columns: repeat(12, minmax(0, 1fr));
}
.flex-group-center {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: center;
flex-direction: center;
-ms-flex-wrap: center;
flex-wrap: center;
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: flex-start;
-webkit-box-align: stretch;
-ms-flex-align: stretch;
align-items: stretch;
text-align: center;
}
.flex-group-left {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: flex-start;
flex-direction: flex-start;
-ms-flex-wrap: center;
flex-wrap: center;
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: flex-start;
-webkit-box-align: stretch;
-ms-flex-align: stretch;
align-items: stretch;
text-align: left;
}
.flex-group-right {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: flex-end;
flex-direction: flex-end;
-ms-flex-wrap: center;
flex-wrap: center;
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: flex-start;
-webkit-box-align: stretch;
-ms-flex-align: stretch;
align-items: stretch;
text-align: right;
}
.flexshrink {
-webkit-box-flex: 0;
-ms-flex: 0;
flex: 0;
}
.flex-between {
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
}
.flexlarge {
-webkit-box-flex: 2;
-ms-flex: 2;
flex: 2;
}
.align-left {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: flex-start;
flex-direction: flex-start;
-ms-flex-wrap: center;
flex-wrap: center;
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: flex-start;
-webkit-box-align: stretch;
-ms-flex-align: stretch;
align-items: stretch;
text-align: left;
}
.align-right {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: flex-end;
flex-direction: flex-end;
-ms-flex-wrap: center;
flex-wrap: center;
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: flex-start;
-webkit-box-align: stretch;
-ms-flex-align: stretch;
align-items: stretch;
text-align: right;
}
.align-center {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: center;
flex-direction: center;
-ms-flex-wrap: center;
flex-wrap: center;
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: flex-start;
-webkit-box-align: stretch;
-ms-flex-align: stretch;
align-items: stretch;
text-align: center;
}
.right-align-input {
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
margin-left: auto;
max-width: 260px;
}
.window-app {
font-family: "Roboto", sans-serif;
}
.rollable:hover, .rollable:focus {
color: #000;
text-shadow: 0 0 10px rgb(179, 7, 217);
cursor: pointer;
}
.editor-container {
min-height: 200px; /* Adjust this value as needed */
}
/* Styles limited to kidsonbrooms sheets */
.fvtt-never-stop-blowing-up .item-form {
font-family: "Roboto", sans-serif;
}
.fvtt-never-stop-blowing-up .sheet-header {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
-webkit-box-align: start;
-ms-flex-align: start;
align-items: flex-start;
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: flex-start;
-webkit-box-flex: 0;
-ms-flex: 0 1 auto;
flex: 0 1 auto;
overflow: hidden;
margin-bottom: 10px;
height: 110px;
}
.fvtt-never-stop-blowing-up .sheet-header .profile-img {
-webkit-box-flex: 0;
-ms-flex: 0 0 100px;
flex: 0 0 100px;
height: 100px;
margin-right: 10px;
}
.fvtt-never-stop-blowing-up .sheet-header .header-fields {
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
}
.fvtt-never-stop-blowing-up .sheet-header h1.charname {
height: 50px;
padding: 0;
margin: 5px 0;
border-bottom: 0;
}
.fvtt-never-stop-blowing-up .sheet-header h1.charname input {
width: 100%;
height: 100%;
margin: 0;
}
.fvtt-never-stop-blowing-up div.editor-border {
border: 2px solid rgb(81, 81, 81);
border-radius: 10px;
}
.fvtt-never-stop-blowing-up .sheet-tabs {
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
}
.fvtt-never-stop-blowing-up .sheet-body,
.fvtt-never-stop-blowing-up .sheet-body .tab,
.fvtt-never-stop-blowing-up .sheet-body .tab .editor {
height: 100%;
}
.fvtt-never-stop-blowing-up .tox .tox-editor-container {
background: #fff;
}
.fvtt-never-stop-blowing-up .tox .tox-edit-area {
padding: 0 8px;
}
.fvtt-never-stop-blowing-up .selection-row {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
margin-bottom: 10px;
}
.fvtt-never-stop-blowing-up .resource-label {
font-weight: bold;
}
.fvtt-never-stop-blowing-up .items-header {
height: 28px;
margin: 2px 0;
padding: 0;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
background: rgba(0, 0, 0, 0.05);
border: 2px groove #eeede0;
font-weight: bold;
}
.fvtt-never-stop-blowing-up .items-header > * {
font-size: 14px;
text-align: center;
}
.fvtt-never-stop-blowing-up .items-header .item-name {
font-weight: bold;
padding-left: 5px;
text-align: left;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
}
.fvtt-never-stop-blowing-up .items-list {
list-style: none;
margin: 0;
padding: 0;
overflow-y: auto;
scrollbar-width: thin;
color: #444;
}
.fvtt-never-stop-blowing-up .items-list .item-list {
list-style: none;
margin: 0;
padding: 0;
}
.fvtt-never-stop-blowing-up .items-list .item {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
padding: 0 2px;
border-bottom: 1px solid #c9c7b8;
}
.fvtt-never-stop-blowing-up .items-list .item:last-child {
border-bottom: none;
}
.fvtt-never-stop-blowing-up .items-list .item .item-name {
-webkit-box-flex: 2;
-ms-flex: 2;
flex: 2;
margin: 0;
overflow: hidden;
font-size: 13px;
text-align: left;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
color: #191813;
}
.fvtt-never-stop-blowing-up .items-list .item .item-name h3, .fvtt-never-stop-blowing-up .items-list .item .item-name h4 {
margin: 0;
white-space: nowrap;
overflow-x: hidden;
}
.fvtt-never-stop-blowing-up .items-list .item .item-name .item-image {
-webkit-box-flex: 0;
-ms-flex: 0 0 30px;
flex: 0 0 30px;
height: 30px;
background-size: 30px;
border: none;
margin-right: 5px;
}
.fvtt-never-stop-blowing-up .items-list .item-controls {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-flex: 0;
-ms-flex: 0 0 100px;
flex: 0 0 100px;
-webkit-box-pack: end;
-ms-flex-pack: end;
justify-content: flex-end;
}
.fvtt-never-stop-blowing-up .items-list .item-controls a {
font-size: 12px;
text-align: center;
margin: 0 6px;
}
.fvtt-never-stop-blowing-up .items-list .item-prop {
text-align: center;
border-left: 1px solid #c9c7b8;
border-right: 1px solid #c9c7b8;
font-size: 12px;
}
.fvtt-never-stop-blowing-up .items-list .items-header {
height: 28px;
margin: 2px 0;
padding: 0;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
background: rgba(0, 0, 0, 0.05);
border: 2px groove #eeede0;
font-weight: bold;
}
.fvtt-never-stop-blowing-up .items-list .items-header > * {
font-size: 12px;
text-align: center;
}
.fvtt-never-stop-blowing-up .items-list .items-header .item-name {
padding-left: 5px;
text-align: left;
}
.fvtt-never-stop-blowing-up .item-formula {
-webkit-box-flex: 0;
-ms-flex: 0 0 200px;
flex: 0 0 200px;
padding: 0 8px;
}
.fvtt-never-stop-blowing-up .effects .item .effect-source,
.fvtt-never-stop-blowing-up .effects .item .effect-duration,
.fvtt-never-stop-blowing-up .effects .item .effect-controls {
text-align: center;
border-left: 1px solid #c9c7b8;
border-right: 1px solid #c9c7b8;
font-size: 12px;
}
.fvtt-never-stop-blowing-up .effects .item .effect-controls {
border: none;
}
.fvtt-never-stop-blowing-up .fvtt-never-stop-blowing-up input:focus,
.fvtt-never-stop-blowing-up .fvtt-never-stop-blowing-up textarea:focus,
.fvtt-never-stop-blowing-up .fvtt-never-stop-blowing-up select:focus {
outline: none;
border-color: #8102dd;
}

View File

@ -3,41 +3,7 @@ const prefix = require('gulp-autoprefixer');
const sourcemaps = require('gulp-sourcemaps'); const sourcemaps = require('gulp-sourcemaps');
const sass = require('gulp-sass')(require('sass')); const sass = require('gulp-sass')(require('sass'));
const zip = require('gulp-zip'); const zip = require('gulp-zip');
const fs = require('fs'); const { compile } = require('sass');
const fetch = require('node-fetch');
const replace = require('gulp-replace');
const FormData = require('form-data');
/* ----------------------------------------- */
/* Export Tasks
/* ----------------------------------------- */
exports.default = gulp.series(
compileScss,
watchUpdates
);
exports.build = gulp.series(
compileScss,
checkVersion,
ensureOutputDirExists,
packageCompendiums,
updateSystemJson,
zipRelease
);
exports.compile = gulp.series(
compileScss,
ensureOutputDirExists,
packageCompendiums,
);
exports.release = gulp.series(
exports.build,
uploadToPackageRegistry,
publishToFoundry
);
/* ----------------------------------------- */ /* ----------------------------------------- */
/* Compile Sass /* Compile Sass
@ -65,15 +31,28 @@ function compileScss() {
})) }))
.pipe(gulp.dest("./css")) .pipe(gulp.dest("./css"))
} }
const css = gulp.series(compileScss);
/* ----------------------------------------- */ /* ----------------------------------------- */
/* Watch Updates /* Watch Updates
/* ----------------------------------------- */ /* ----------------------------------------- */
function watchUpdates() { function watchUpdates() {
gulp.watch(SYSTEM_SCSS, compileScss); gulp.watch(SYSTEM_SCSS, compileScss());
} }
/* ----------------------------------------- */
/* Export Tasks
/* ----------------------------------------- */
exports.default = gulp.series(
compileScss,
watchUpdates
);
exports.build = gulp.series(
compileScss
);
exports.css = css;
/* ----------------------------------------- */ /* ----------------------------------------- */
/* Zip Release /* Zip Release
@ -90,249 +69,12 @@ function zipRelease() {
'!./package.json', '!./package.json',
'!./scss/**/*', '!./scss/**/*',
'!./.github/**/*', '!./.github/**/*',
'!./.gitlab-ci.yml',
'!./README.md',
'!./compendiums/**/*',
'!./*.zip'
], { base: '.' }) ], { base: '.' })
.pipe(zip('kidsonbrooms.zip')) .pipe(zip('kids-on-brooms.zip'))
.pipe(gulp.dest('.')); .pipe(gulp.dest('.'));
} }
/* ----------------------------------------- */ exports.release = gulp.series(
/* Version Check compileScss,
/* ----------------------------------------- */ zipRelease
);
function checkVersion(done) {
const Manifest = JSON.parse(fs.readFileSync('system.json'));
const manifestVersion = Manifest.version;
const gitTag = process.env.CI_COMMIT_TAG;
if (gitTag && manifestVersion !== gitTag) {
console.error(`Version mismatch between tag (${gitTag}) and manifest (${manifestVersion})!`);
process.exit(1);
} else {
console.log(`Version check passed: ${manifestVersion}`);
done();
}
}
/* ----------------------------------------- */
/* Bundle Compendium
/* ----------------------------------------- */
const { exec } = require('child_process');
function packageCompendiums(done) {
const packsDir = './compendiums'; // Adjust to your compendium source directory
const outputDir = './packs';
const moduleId = 'kidsonbrooms'; // Replace with your actual module ID
// Read all subdirectories in the packsDir
if (!fs.existsSync(packsDir)) {
console.log(`Compendium directory ${packsDir} does not exist. Skipping packaging.`);
done();
return;
}
// Read all files and directories in the packsDir
fs.readdir(packsDir, (err, files) => {
if (err) {
console.error(`Error reading directory ${packsDir}: ${err}`);
process.exit(1);
}
// Filter to get only directories
const folders = files.filter(file => {
const fullPath = path.join(packsDir, file);
return fs.statSync(fullPath).isDirectory();
});
if (folders.length === 0) {
console.log(`No compendium folders found in ${packsDir}. Skipping packaging.`);
done();
return;
}
let completed = 0;
folders.forEach(folder => {
const packName = folder; // Use the folder name as the pack name
const inputPath = path.join(packsDir, folder);
const command = `npx fvtt package pack --type System --id ${moduleId} -n "${packName}" --in "${inputPath}" --out "${outputDir}" --yaml`;
exec(command, (err, stdout, stderr) => {
if (err) {
console.error(`Error packaging compendium ${packName}:\n${stderr}`);
process.exit(1);
} else {
console.log(`Compendium ${packName} packaged successfully.`);
console.log(stdout);
completed++;
// When all compendiums have been processed, call done()
if (completed === folders.length) {
done();
}
}
});
});
});
}
/* ----------------------------------------- */
/* Ensure Output Directory Exists
/* ----------------------------------------- */
function ensureOutputDirExists() {
const outputDir = './packs';
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir);
}
return Promise.resolve();
}
/* ----------------------------------------- */
/* Upload to Package Registry
/* ----------------------------------------- */
async function uploadToPackageRegistry(done) {
const manifestFile = 'system.json';
const zipFile = process.env.ZIPFILE || 'kidsonbrooms.zip';
const packageRegistryUrl = process.env.PACKAGE_REGISTRY_URL;
const ciJobToken = process.env.CI_JOB_TOKEN;
if (!packageRegistryUrl || !ciJobToken) {
console.error('PACKAGE_REGISTRY_URL or CI_JOB_TOKEN is not defined.');
process.exit(1);
}
try {
// Upload manifest file
const manifestUploadUrl = `${packageRegistryUrl}/${manifestFile}`;
console.log(`Uploading ${manifestFile} to ${manifestUploadUrl}`);
let response = await fetch(manifestUploadUrl, {
method: 'PUT',
headers: {
'JOB-TOKEN': ciJobToken,
'Content-Type': 'application/octet-stream',
},
body: fs.createReadStream(manifestFile),
});
if (response.ok) {
console.log(`Uploaded ${manifestFile} successfully.`);
} else {
console.error(`Failed to upload ${manifestFile}: ${response.statusText}`);
process.exit(1);
}
// Upload zip file
const zipUploadUrl = `${packageRegistryUrl}/${zipFile}`;
console.log(`Uploading ${zipFile} to ${zipUploadUrl}`);
response = await fetch(zipUploadUrl, {
method: 'PUT',
headers: {
'JOB-TOKEN': ciJobToken,
'Content-Type': 'application/octet-stream',
},
body: fs.createReadStream(zipFile),
});
if (response.ok) {
console.log(`Uploaded ${zipFile} successfully.`);
} else {
console.error(`Failed to upload ${zipFile}: ${response.statusText}`);
process.exit(1);
}
done();
} catch (error) {
console.error(`Error uploading files: ${error.message}`);
process.exit(1);
}
}
/* ----------------------------------------- */
/* Publish to FoundryVTT
/* ----------------------------------------- */
async function publishToFoundry(done) {
const moduleManifestPath = 'system.json';
const moduleManifest = JSON.parse(fs.readFileSync(moduleManifestPath));
const id = moduleManifest.name;
const version = moduleManifest.version;
const compMin = moduleManifest.compatibility.minimum;
const compVer = moduleManifest.compatibility.verified;
const compMax = moduleManifest.compatibility.maximum;
const manifest = process.env.MANIFEST_PERMALINK_URL || `https://gitlab.com/${process.env.CI_PROJECT_NAMESPACE}/${process.env.CI_PROJECT_NAME}/-/releases/${process.env.CI_COMMIT_TAG}/downloads/${moduleManifestPath}`;
const notes = `https://gitlab.com/${process.env.CI_PROJECT_NAMESPACE}/${process.env.CI_PROJECT_NAME}/-/releases/${process.env.CI_COMMIT_TAG}`;
const dryRun = process.env.dry_run === 'true';
const authToken = process.env.FOUNDRY_API_KEY;
if (!authToken) {
console.error('Foundry VTT API authentication token (FOUNDRY_API_KEY) is not defined.');
process.exit(1);
}
// Construct the payload
const payload = {
id: "kidsonbrooms",
release: {
version: version,
manifest: manifest,
notes: notes,
compatibility: {
minimum: compMin,
verified: compVer,
maximum: compMax,
},
},
};
if (dryRun) {
payload['dry-run'] = true;
}
// Send the POST request to Foundry VTT API
const response = await fetch('https://api.foundryvtt.com/_api/packages/release_version', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: authToken,
},
body: JSON.stringify(payload),
});
const responseData = await response.text();
if (responseData.includes('success')) {
console.log('Successfully published to Foundry VTT:');
console.log(JSON.stringify(responseData, null, 2));
done();
} else {
console.error('Failed to publish to Foundry VTT:');
console.error(JSON.stringify(responseData, null, 2));
process.exit(1);
}
}
/* ----------------------------------------- */
/* Update systen.json with Download URL
/* ----------------------------------------- */
function updateSystemJson(done) {
const ManifestPath = 'system.json';
const Manifest = JSON.parse(fs.readFileSync(ManifestPath));
const zipUrl = process.env.ZIPFILE_RELEASE_URL || 'https://gitlab.com/wintermyst/kidsonbrooms/-/raw/master/kidsonbrooms.zip?inline=false';
Manifest.download = zipUrl;
fs.writeFileSync(ManifestPath, JSON.stringify(Manifest, null, 2));
console.log(`Updated module.json with download URL: ${zipUrl}`);
done();
}

23
kidsonbrooms.mjs Normal file
View File

@ -0,0 +1,23 @@
import * as models from "./modules/dataModel/_system.mjs";
import * as sheets from "./modules/sheets/_system.mjs";
/* -------------------------------------------- */
/* Foundry VTT Initialization */
/* -------------------------------------------- */
const SYSTEM = {
id: "kidsonbrooms",
}
Hooks.once("init", async function() {
console.log("Initialising Kids on Brooms system");
globalThis.kidsonbrooms = game.system;
game.system.CONST = SYSTEM;
Actors.unregisterSheet("core", ActorSheet);
Actors.registerSheet(SYSTEM.id, "{{sheets.BaseActorSheet}}", {types: ["PlayerCharacter"], makeDefault: true});
sheets.BaseActorSheet.getTemplate();
CONFIG.Actor.dataModels = models.playerCharacterModel;
})

View File

@ -1,9 +1,30 @@
{ {
"BaseActor": {
"FIELDS": {
"age": "Age",
"pronouns": "Pronouns",
"fear": "Fear",
"grade": "Grade",
"stats": {
"fight": "Fight",
"brains": "Brains",
"charm": "Charm",
"flight": "Flight",
"brawn": "Brawn",
"grit": "Grit"
}
}
},
"NEVERSTOPBLOWINGUP.EffectCreate": "Create Effect", "PlayerCharacter": {
"NEVERSTOPBLOWINGUP.EffectToggle": "Toggle Effect", "FIELDS": {
"NEVERSTOPBLOWINGUP.EffectEdit": "Edit Effect", "description": "Description",
"NEVERSTOPBLOWINGUP.EffectDelete": "Delete Effect", "broom": "Broom",
"wand": "Wand",
"NEVERSTOPBLOWINGUP.Add": "Add" "animalFamiliar": "Animal Familiar",
"schoolbag": "Schoolbag",
"strengths": "Strengths",
"adversityTokens": "Adversity Tokens"
}
}
} }

View File

@ -1,65 +0,0 @@
/**
* Extend the base Actor document by defining a custom roll data structure which is ideal for the Simple system.
* @extends {Actor}
*/
export class NeverStopBlowingUpActor extends Actor {
/**
* Override getRollData() that's supplied to rolls.
*/
getRollDataPC() {
let data = { ...this.system };
// Wand bonuses
data.wandBonus = {
wood: this._getWandBonus(this.system.wand.wood),
core: this._getWandBonus(this.system.wand.core)
};
return data;
}
getRollDataNPC() {
let data = { ...this.system};
return data;
}
_getWandBonus(type) {
const bonuses = {
"Wisteria": { stat: "brains", bonus: 1 },
"Hawthorn": { stat: "brains", bonus: 1 },
"Pine": { stat: "brawn", bonus: 1 },
"Oak": { stat: "brawn", bonus: 1 },
"Crabapple": { stat: "fight", bonus: 1 },
"Dogwood": { stat: "fight", bonus: 1 },
"Birch": { stat: "flight", bonus: 1 },
"Bamboo": { stat: "flight", bonus: 1 },
"Ironwood": { stat: "grit", bonus: 1 },
"Maple": { stat: "grit", bonus: 1 },
"Lilac": { stat: "charm", bonus: 1 },
"Cherry": { stat: "charm", bonus: 1 },
"Parchment": { stat: "brains", bonus: 1 },
"Phoenix Feather": { stat: "brains", bonus: 1 },
"Owl Feather": { stat: "brains", bonus: 1 },
"Gorilla Fur": { stat: "brawn", bonus: 1 },
"Ogres Fingernail": { stat: "brawn", bonus: 1 },
"Hippos Tooth": { stat: "brawn", bonus: 1 },
"Dragons Heartstring": { stat: "fight", bonus: 1 },
"Wolfs Tooth": { stat: "fight", bonus: 1 },
"Elks Antler": { stat: "fight", bonus: 1 },
"Hawks Feather": { stat: "flight", bonus: 1 },
"Bats Bone": { stat: "flight", bonus: 1 },
"Changelings Hair": { stat: "charm", bonus: 1 },
"Gold": { stat: "charm", bonus: 1 },
"Mirror": { stat: "charm", bonus: 1 },
"Steel": { stat: "grit", bonus: 1 },
"Diamond": { stat: "grit", bonus: 1 },
"Lions Mane": { stat: "grit", bonus: 1 }
};
return bonuses[type] || { stat: "", bonus: 0 };
}
}

View File

@ -1,7 +0,0 @@
export const NEVERSTOPBLOWINGUP = {};
// Define constants here, such as:
NEVERSTOPBLOWINGUP.foobar = {
'bas': 'NEVERSTOPBLOWINGUP.bas',
'bar': 'NEVERSTOPBLOWINGUP.bar'
};

View File

@ -1,15 +0,0 @@
/**
* Define a set of template paths to pre-load
* Pre-loaded templates are compiled and cached for fast access when rendering
* @return {Promise}
*/
export const preloadHandlebarsTemplates = async function() {
return loadTemplates([
// Actor partials.
"systems/fvtt-never-stop-blowing-up/templates/actor/parts/actor-features.html",
"systems/fvtt-never-stop-blowing-up/templates/actor/parts/actor-adversity.html",
"systems/fvtt-never-stop-blowing-up/templates/actor/parts/actor-stats.html",
"systems/fvtt-never-stop-blowing-up/templates/actor/parts/actor-npc-stats.html",
]);
};

View File

@ -1,332 +0,0 @@
// Import document classes.
import { NeverStopBlowingUpActor } from "./documents/actor.mjs";
// Import sheet classes.
import { NeverStopBlowingUpActorSheet } from "./sheets/actor-sheet.mjs";
// Import helper/utility classes and constants.
import { preloadHandlebarsTemplates } from "./helpers/templates.mjs";
import { NEVERSTOPBLOWINGUP } from "./helpers/config.mjs";
/* -------------------------------------------- */
/* Init Hook */
/* -------------------------------------------- */
Hooks.once('init', async function() {
// Register the helper
Handlebars.registerHelper('capitalizeFirst', function(string) {
if (typeof string === 'string') {
return string.charAt(0).toUpperCase() + string.slice(1);
}
return '';
});
// Add utility classes and functions to the global game object so that they're more easily
// accessible in global contexts.
game.kidsonbrooms = {
NeverStopBlowingUpActor,
_onTakeAdversityToken: _onTakeAdversityToken, // Add the function to the global object
_onSpendAdversityTokens: _onSpendAdversityTokens // Add the function to the global object
};
// Add custom constants for configuration.
CONFIG.NEVERSTOPBLOWINGUP = NEVERSTOPBLOWINGUP;
/**
* Set an initiative formula for the system
* @type {String}
*/
CONFIG.Combat.initiative = {
formula: "1d20",
decimals: 2
};
// Define custom Document classes
CONFIG.Actor.documentClass = NeverStopBlowingUpActor;
// Register sheet application classes
Actors.unregisterSheet("core", ActorSheet);
Actors.registerSheet("fvtt-never-stop-blowing-up", NeverStopBlowingUpActorSheet, { makeDefault: true });
//If there is a new chat message that is a roll we add the adversity token controls
Hooks.on("renderChatMessage", (message, html, messageData) => {
const adversityControls = html.find('.adversity-controls');
if (adversityControls.length > 0) {
const messageToEdit = adversityControls.data("roll-id");
// Bind event listeners for the controls
adversityControls.find(".take-adversity").off("click").click((event) => {
const actorId = event.currentTarget.dataset.actorId;
const actor = game.actors.get(actorId);
// Check if the current user owns the actor - They can not claim if they are not
if (!actor.testUserPermission(game.user, "owner")) {
ui.notifications.warn("You don't own this character and cannot take adversity tokens.");
return;
}
// Check if the token has already been claimed -- Contigency if the button somehow activates again
if (message.getFlag("kidsonbrooms", "tokenClaimed")) {
ui.notifications.warn("This adversity token has already been claimed.");
return;
}
_onTakeAdversityToken(event, actor);
if (game.user.isGM) {
let tokenControls = game.messages.get(message.id);
console.log(tokenControls);
// Update the chat message content with the button disabled and text changed
const updatedContent = tokenControls.content.replace(
`<button class="take-adversity" data-actor-id="${actor.id}">Take Adversity Token</button>`,
`<button class="take-adversity" data-actor-id="${actor.id}" disabled>Token claimed</button>`
);
console.log("Removing Button");
// Update the message content
tokenControls.update({ content: updatedContent });
// Set the flag on the chat message to indicate that the token has been claimed
tokenControls.setFlag("fvtt-never-stop-blowing-up", "tokenClaimed", true);
} else {
// Emit a socket request to update the message to show that the token has been claimed
game.socket.emit('system.fvtt-never-stop-blowing-up', {
action: "takeToken",
messageID: message.id,
actorID: actor.id,
});
}
console.log("Send socket message for taking a token");
});
adversityControls.find(".spend-adversity").off("click").click((event) => {
//This entails a lot more, so I offloaded it to a new function
_onSpendAdversityTokens(event, messageToEdit);
});
}
});
// Preload Handlebars templates.
return preloadHandlebarsTemplates();
});
/***
* This handles the incoming socket requests.
* If a player wants to spend tokens on another players roll the gm has to approve first
* if a player wants to claim a token we will update the message since they do not have the permissions
*/
Hooks.once('ready', function() {
game.socket.on('system.fvtt-never-stop-blowing-up', async (data) => {
console.log("Socket data received:", data);
if (data.action === "spendTokens") {
console.log(`Request to spend tokens: ${data.tokensToSpend} tokens for ${data.rollActorId} by ${data.spendingActorId}`);
// Only handle the request if the GM is logged in
if (!game.user.isGM) {
console.log("Not GM, ignoring the token spend request.");
return;
}
// The actor who made the roll
const rollActor = game.actors.get(data.rollActorId);
// The actor who is spending tokens
const spendingActor = game.actors.get(data.spendingActorId);
//If these for some reason do not exist
if (!rollActor || !spendingActor) {
console.warn("Actor not found:", data.rollActorId, data.spendingActorId);
return;
}
// Create a confirmation dialog for the GM
new Dialog({
title: "Approve Adversity Token Spending?",
content: `<p>${spendingActor.name} wants to spend ${data.tokenCost} adversity tokens on ${rollActor.name}'s roll to increase it by ${data.tokensToSpend}. Approve?</p>`,
buttons: {
yes: {
label: "Yes",
callback: async () => {
const currentTokens = spendingActor.system.adversityTokens || 0;
// Update the spending actor's adversity token count
await spendingActor.update({ "system.adversityTokens": currentTokens - data.tokenCost });
// Modify the roll message with the new total
await _updateRollMessage(data.rollMessageId, data.tokensToSpend, false);
console.log(`${spendingActor.name} spent ${data.tokensToSpend} tokens, updated roll total to ${roll.cumulativeTotal}`);
ui.notifications.info(`${spendingActor.name} successfully spent ${data.tokensToSpend} tokens.`);
}
},
no: {
label: "No",
callback: () => {
ui.notifications.info(`The GM denied ${spendingActor.name}'s request to spend tokens.`);
}
}
},
default: "yes"
}).render(true);
} else if (data.action === "takeToken") {
// Only handle the request if the GM is logged in
if (!game.user.isGM) {
console.log("Not GM, ignoring the token spend request.");
return;
}
let tokenControls = game.messages.get(data.messageID);
console.log(tokenControls);
// Update the chat message content with the button disabled and text changed
const updatedContent = tokenControls.content.replace(
`<button class="take-adversity" data-actor-id="${data.actorID}">Take Adversity Token</button>`,
`<button class="take-adversity" data-actor-id="${data.actorID}" disabled>Token claimed</button>`
);
console.log("Removing Button");
// Update the message content
tokenControls.update({ content: updatedContent });
// Set the flag on the chat message to indicate that the token has been claimed
tokenControls.setFlag("fvtt-never-stop-blowing-up", "tokenClaimed", true);
}
});
});
/***
* This function adds the adversity token to the actor that made the roll and logs it
*
* @param {Event} e - The button click event
* @param {Actor} actor - The actor object that made the roll
*/
async function _onTakeAdversityToken(e, actor) {
e.preventDefault();
// Get the chat message ID (assuming it's stored in the dataset)
const messageId = e.currentTarget.closest('.message').dataset.messageId;
const message = game.messages.get(messageId);
// Add an adversity token to the actor
const currentTokens = actor.system.adversityTokens || 0;
await actor.update({ "system.adversityTokens": currentTokens + 1 });
// Notify the user
ui.notifications.info(`You gained 1 adversity token.`);
console.log(`Gave one adversity token to ${actor.id}`)
}
/***
* This function allows players to spend tokens to change a roll. This will automatically be calculated in their sheet
*
*/
async function _onSpendAdversityTokens(e, rollMessageId) {
e.preventDefault();
// The actor who made the roll
const rollActorId = e.currentTarget.dataset.actorId;
const rollActor = game.actors.get(rollActorId); //technically redundant since it is also done in the main hook, but perfomance is good enuff
// Get the actor of the player who is spending tokens
const spendingPlayerActor = game.actors.get(game.user.character?.id || game.actors.filter(actor => actor.testUserPermission(game.user, "owner"))[0]?.id);
if (!spendingPlayerActor) {
ui.notifications.warn("You don't control any actors.");
return;
}
//Get the tokens to be spend from the input field
const tokenInput = $(e.currentTarget).closest('.adversity-controls').find('.token-input').val();
const tokensToSpend = parseInt(tokenInput, 10);
if (isNaN(tokensToSpend) || tokensToSpend <= 0) {
ui.notifications.warn("Please enter a valid number of tokens.");
return;
}
let tokenCost = tokensToSpend;
// If the player spending tokens is not the owner of the actor who rolled, they spend double
//(note, this is a rule of mine, I have disabled it by default)
if ((!spendingPlayerActor.testUserPermission(game.user, "owner") || spendingPlayerActor.id !== rollActorId) && false) {
tokenCost = tokensToSpend * 2;
}
// Ensure the spending actor has enough adversity tokens
if (spendingPlayerActor.system.adversityTokens < tokenCost) {
ui.notifications.warn(`You do not have enough adversity tokens.`);
return;
}
// Check if the player owns the actor who made the roll
if (spendingPlayerActor.id === rollActorId) {
// The player owns the actor, so they can spend tokens directly without GM approval
const currentTokens = spendingPlayerActor.system.adversityTokens || 0;
// Deduct the tokens from the player
await spendingPlayerActor.update({ "system.adversityTokens": currentTokens - tokenCost });
// Modify the roll message with the new total
await _updateRollMessage(rollMessageId, tokensToSpend, true);
} else {
// The player does not own the actor, so request GM approval to spend the tokens
console.log(`Requesting to spend ${tokensToSpend} tokens for ${rollActor.name} by ${spendingPlayerActor.name} (cost: ${tokenCost})`);
// Emit a socket request to spend tokens
game.socket.emit('system.fvtt-never-stop-blowing-up', {
action: "spendTokens",
rollActorId: rollActorId,
spendingActorId: spendingPlayerActor.id, // Send the player's actor who is spending the tokens
tokensToSpend: tokensToSpend,
tokenCost: tokenCost,
rollMessageId: rollMessageId // Pass message ID to update the roll result
});
ui.notifications.info(`Requested to spend ${tokenCost} tokens for ${rollActor.name}`);
}
}
// Helper function to send a new message with the updated roll result
async function _updateRollMessage(rollMessageId, tokensToSpend, isPlayerOfActor) {
const message = game.messages.get(rollMessageId);
if (!message) {
console.error("Message not found with ID:", rollMessageId);
return;
}
// Retrieve current tokens spent from flags, or initialize to 0 if not found
let cumulativeTokensSpent = message.getFlag("fvtt-never-stop-blowing-up", "tokensSpent") || 0;
let newTotal = message.getFlag("fvtt-never-stop-blowing-up", "newRollTotal") || message.rolls[0].total;
/*if(isPlayerOfActor)
{
// Add the new tokens to the cumulative total
cumulativeTokensSpent += tokensToSpend;
} else {
cumulativeTokensSpent += 2*tokensToSpend;
}*/
cumulativeTokensSpent += tokensToSpend;
newTotal += tokensToSpend;
await message.setFlag("fvtt-never-stop-blowing-up", "newRollTotal", newTotal);
// Update the message's flags to store the cumulative tokens spent
await message.setFlag("fvtt-never-stop-blowing-up", "tokensSpent", cumulativeTokensSpent);
let newContent = "";
if(cumulativeTokensSpent === 1)
{
newContent = `You have now spent ${cumulativeTokensSpent} token. The new roll total is ${newTotal}.`;
} else {
newContent = `You have now spent ${cumulativeTokensSpent} tokens. The new roll total is ${newTotal}.`;
}
// Create a new chat message to display the updated total
await ChatMessage.create({
speaker: ChatMessage.getSpeaker({ actor: message.speaker.actor }),
content: newContent,
type: CONST.CHAT_MESSAGE_STYLES.OTHER,
});
}

View File

@ -1,156 +0,0 @@
/**
* Extend the basic ActorSheet with some very simple modifications
* @extends {ActorSheet}
*/
export class NeverStopBlowingUpActorSheet extends ActorSheet {
/** @override */
static get defaultOptions()
{
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["fvtt-never-stop-blowing-up", "sheet", "actor"],
width: 800,
height: 800,
tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "features" }]
});
}
/** @override */
get template()
{
console.log("template", this.actor)
return `systems/fvtt-never-stop-blowing-up/templates/actor/actor-${this.actor.type}-sheet.html`;
}
/* -------------------------------------------- */
/** @override */
/** @override */
async getData()
{
// Retrieve the data structure from the base sheet.
const context = super.getData();
// Use a safe clone of the actor data for further operations.
const actorData = this.document.toObject(false);
// Add the actor's data to context.data for easier access, as well as flags.
context.system = actorData.system;
context.flags = actorData.flags;
// Add roll data for TinyMCE editors.
context.rollData = context.actor.getRollData();
console.log(context);
return context;
}
/* -------------------------------------------- */
/** @override */
activateListeners(html)
{
super.activateListeners(html);
// -------------------------------------------------------------
// Everything below here is only needed if the sheet is editable
if (!this.isEditable) return;
// Rollable abilities.
html.find('.rollable').click(this._onRoll.bind(this));
//If the user changes their wand material save that
html.find('select[name="system.wand.wood"]').change(event => {
const value = event.target.value;
this.actor.update({ "system.wand.wood": value });
});
html.find('select[name="system.wand.core"]').change(event => {
const value = event.target.value;
this.actor.update({ "system.wand.core": value });
});
}
/**
* Handle clickable rolls.
* @param {Event} event The originating click event
* @private
*/
async _onRoll(e) {
e.preventDefault();
const element = e.currentTarget;
const dataset = element.dataset;
// Handle rolls that supply the formula directly
if (dataset.roll) {
let label = dataset.label ? `${dataset.label}` : '';
// Get the roll data and include wand bonuses
let rollData;
if(this.actor.type == "character") {
rollData = this.actor.getRollDataPC();
} else if (this.actor.type == "npc") {
rollData = this.actor.getRollDataNPC();
} else {
console.log("ERROR: UNKNOWN AUTHOR TYPE");
return;
}
let totalBonus = 0;
console.log(dataset.roll);
// Apply wood bonus if it matches the stat being rolled for
if (rollData.wandBonus.wood.stat === dataset.key) {
totalBonus += rollData.wandBonus.wood.bonus;
}
// Apply core bonus if it matches the stat being rolled for AND it's different from the wood bonus
if (rollData.wandBonus.core.stat === dataset.key && rollData.wandBonus.core.stat !== rollData.wandBonus.wood.stat) {
totalBonus += rollData.wandBonus.core.bonus;
}
let rollFormula = dataset.roll + `+${totalBonus}`;
let roll = new Roll(rollFormula, rollData);
console.log(rollFormula);
console.log(rollData);
// Send the roll message to chat
const rollMessage = await roll.toMessage({
speaker: ChatMessage.getSpeaker({ actor: this.actor }),
flavor: label,
rollMode: game.settings.get('core', 'rollMode'),
})
// Now send the follow-up message with the adversity controls
await this._sendAdversityControlsMessage(this.actor.id, rollMessage.id);
return roll;
}
}
//This just sends the buttons for the adversity token system
async _sendAdversityControlsMessage(actorId, rollMessageId) {
// Create the content for the adversity controls
const adversityControlsHtml = this._createAdversityControls(actorId, rollMessageId);
// Send the adversity controls as a follow-up message
const controlMessage = await ChatMessage.create({
speaker: ChatMessage.getSpeaker({ actor: this.actor }),
content: adversityControlsHtml,
});
return controlMessage;
}
// Create HTML content for adversity controls
_createAdversityControls(actorId, rollMessageId) {
return `
<div class="adversity-controls" data-roll-id="${rollMessageId}">
<button class="take-adversity" data-actor-id="${actorId}">Take Adversity Token</button>
<input type="number" class="token-input" value="1" min="1" />
<button class="spend-adversity" data-actor-id="${actorId}">Spend Adversity Tokens</button>
</div>
`;
}
}

View File

@ -0,0 +1,4 @@
export * from "./itemModel.mjs";
export * from "./dataModel.mjs";
export * from "./playerCharacterModel.mjs";
export * from "./baseActorModel.mjs"

View File

@ -0,0 +1,31 @@
import Stat from "./dataModel.mjs"
/* -------------------------------------------- */
/* Actor base Model */
/* -------------------------------------------- */
const fields = foundry.data.fields;
export default class ActorGeneral extends foundry.abstract.TypeDataModel
{
static defineSchema(){
return {
age: new fields.StringField({required: false}),
pronouns: new fields.StringField({required: false}),
fear: new fields.StringField({required: false}),
grade: new fields.StringField({required: false}),
stats: new fields.SchemaField({
fight: new fields.EmbeddedDataField(Stat, {required: true, nullable: false, default: new Stat()}),
brains: new fields.EmbeddedDataField(Stat, {required: true, nullable: false, default: new Stat()}),
charm: new fields.EmbeddedDataField(Stat, {required: true, nullable: false, default: new Stat()}),
flight: new fields.EmbeddedDataField(Stat, {required: true, nullable: false, default: new Stat()}),
brawn: new fields.EmbeddedDataField(Stat, {required: true, nullable: false, default: new Stat()}),
grit: new fields.EmbeddedDataField(Stat, {required: true, nullable: false, default: new Stat()}),
}),
}
}
static LOCALISATION_PREFIXES = ["BaseActor"];
prepareDerivedData() {
super.prepareDerivedData();
}
}

View File

@ -0,0 +1,71 @@
/* -------------------------------------------- */
/* Base Models */
/* -------------------------------------------- */
const fields = foundry.data.fields;
export default class Stat extends foundry.abstract.DataModel
{
static defineSchema() {
return {
id: new fields.StringField({ required: true, initial: "statID"}),
name: new fields.StringField({ required: true, intial: "Stat"}),
die: new fields.NumberField({ required: true, nullable: false, initial: "d4"}),
modifiers: new fields.ArrayField({ required: true, type: Modifier, default: []}),
modifier: new fields.NumberField({required: true, integer: true, initial: 0})
};
}
}
export class Modifier extends foundry.abstract.DataModel
{
static defineSchema() {
return {
statID: new fields.StringField({ required: true, initial: "statID"}),
name: new fields.StringField({ required: true, initial: "Modifier"}),
value: new fields.NumberField({ required: true, integer: true, initial: 0}),
};
}
Modifier(statID,name,value) {
this.statID = statID;
this.name = name;
this.value = value;
};
}
/* -------------------------------------------- */
/* Effect Models */
/* -------------------------------------------- */
export class Effect extends foundry.abstract.DataModel
{
static defineSchema() {
return {
description: new fields.StringField({ required: true, initial: "A EffectDescription" }),
modifier: new fields.EmbeddedDataField(Modifier, { required: true, nullable: true, default: null}),
};
}
}
export class Flaw extends foundry.abstract.DataModel
{
static defineSchema() {
return {
name: new fields.StringField({ required: true, initial: "Flaw"}),
description: new fields.StringField({ required: true, initial: "A FlawDescription" })
};
}
}
export class Strength extends Effect
{
static defineSchema() {
return {
...super.defineSchema(),
name: new fields.StringField({ required: true, initial: "Strength"})
};
}
}

View File

@ -0,0 +1,45 @@
import Effect from "./dataModel.mjs";
/* -------------------------------------------- */
/* Item Models */
/* -------------------------------------------- */
/* How this will work is when we first load a sheet we load all the items we have and take their effects and apply them to our stats. */
const fields = foundry.data.fields;
export default class KidsOnBroomsItem extends foundry.abstract.TypeDataModel
{
static defineSchema() {
return {
description: new fields.StringField({ required: true, initial: "An KidsOnBroomsItemDescription" }),
effects: new fields.ArrayField({ required: true, type: Effect, default: []}),
quantity: new fields.NumberField({required: true, nullable: false, integer: true, initial: 1, min: 0}),
price: new fields.NumberField({required: true, nullable: false, integer: true, initial: 0, min: 0}),
};
}
KidsOnBroomsItem(name,description,effects) {
this.name = name;
this.description = description;
this.effects = effects;
}
}
export class Wand extends KidsOnBroomsItem
{
static defineSchema() {
return {
wood: new fields.EmbeddedDataField(KidsOnBroomsItem, { required: true, nullable: true, default: null}), //These are just KidsOnBroomsItems!
core: new fields.EmbeddedDataField(KidsOnBroomsItem, { required: true, nullable: true, default: null}),
};
}
}
export class Broom extends KidsOnBroomsItem
{
static defineSchema(){
return {
look: new fields.StringField({required: true, initial: "A broom"}),
mechanicalBenefit: new fields.EmbeddedDataField(Effect, {required: false})
}
}
}

View File

@ -0,0 +1,48 @@
import ActorGeneral from "./baseActorModel.mjs";
import {Wand, Broom} from "./itemModel.mjs"
import KidsOnBroomsItem from "./itemModel.mjs"
/* -------------------------------------------- */
/* PC Model */
/* -------------------------------------------- */
const fields = foundry.data.fields;
export default class PlayerCharacter extends ActorGeneral
{
static defineSchema(){
return {
...super.defineSchema(),
description: new fields.StringField({required: false, intial: "Enter your characters description here."}),
broom: new fields.EmbeddedDataField(Broom, {nullable: true}),
wand: new fields.EmbeddedDataField(Wand, {nullable: true}),
animalFamiliar: new fields.StringField({required: false, initial: "Describe your companion!"}),
schoolbag: new fields.ArrayField({type: KidsOnBroomsItem, default: []}),
strengths: new fields.ArrayField({type: Strength, default: []}),
adversityTokens: new fields.NumberField({required: true, nullable: false, integer: true, initial: 3, min: 0})
}
}
static LOCALISATION_PREFIXES = ["PlayerCharacter"];
prepareBaseData() {
super.prepareBaseData();
let effectsToApply = this.gatherEffects();
effectsToApply.forEach(element => {
console.log(element);
})
}
gatherEffects() {
let effectsToApply = new [];
this.schoolbag.array.forEach(element => {
if(element.effects != []) {
element.effects.forEach(effect => {
effectsToApply.push(effect);
})
}
});
return effectsToApply;
}
}

View File

@ -0,0 +1,7 @@
export default class KidsOnBroomsActor extends Actor {
constructor(data, context) {
super(data, context)
}
}

View File

@ -0,0 +1 @@
export * from "./actor-sheet.mjs"

View File

@ -0,0 +1,31 @@
const {api, sheets} = foundry.applications;
export default class BaseActorSheet extends api.HandlebarsApplicationMixin(sheets.ActorSheetV2) {
/** @override */
static get defaultOptions()
{
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["kidsonbrooms", "sheet", "actor"],
width: 800,
height: 800,
tabs: [{}]
});
}
/** @override */
get template()
{
console.log("template", this.actor)
return `systems/kidsonbrooms/templates/actor/actor-sheet-{$this.actor.type}.html`;
}
async getData()
{
const context = super.getData();
console.log(context);
return context;
}
}

2332
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,31 +1,20 @@
{ {
"name": "kidsonbrooms", "name": "kidsonbroomsfoundryvtt",
"version": "1.1.5", "version": "0.1.0",
"description": "CSS compiler for the Kids On Brooms system", "description": "The Kids on Brooms System for Foundry VTT",
"main": "kidsonbroomsfoundryvtt.js",
"scripts": { "scripts": {
"build": "gulp build", "sass": "sass --watch scss/kidsOnBrooms.scss css/kidsOnBrooms.css"
"compile": "gulp css",
"watch": "gulp",
"gulp": "gulp"
},
"browserslist": [
"last 5 versions"
],
"author": "Joscha Maier",
"license": "MIT",
"private": true,
"dependencies": {
"form-data": "^4.0.0",
"gulp": "^5",
"gulp-autoprefixer": "^8",
"gulp-replace": "^1.1.4",
"gulp-sass": "^5",
"gulp-sourcemaps": "^2.6.5",
"gulp-zip": "^5.0.1",
"kidsonbrooms": "file:",
"node-fetch": "^2.7.0"
}, },
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": { "devDependencies": {
"sass": "^1.79.1" "gulp": "^5.0.0",
"mathjs": "^13.1.1",
"sass": "^1.79.3"
},
"dependencies": {
"kidsonbroomsfoundryvtt": "file:"
} }
} }

View File

@ -1,2 +0,0 @@
The Never Stop Blowing Up System Implemented in FoundryVTT

View File

@ -1,22 +0,0 @@
.effects .item {
.effect-source,
.effect-duration,
.effect-controls {
text-align: center;
border-left: 1px solid #c9c7b8;
border-right: 1px solid #c9c7b8;
font-size: 12px;
}
.effect-controls {
border: none;
}
}
// _effects.scss
.kids-on-brooms input:focus,
.kids-on-brooms textarea:focus,
.kids-on-brooms select:focus {
outline: none;
border-color: $focus-border-color;
}

View File

@ -1,71 +0,0 @@
.item-form {
font-family: $font-primary;
}
.sheet-header {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: flex-start;
justify-content: flex-start; // Use a mixin for flexbox
flex: 0 1 auto;
overflow: hidden;
margin-bottom: 10px;
height: 110px;
.profile-img {
flex: 0 0 100px;
height: 100px;
margin-right: 10px;
}
.header-fields {
flex: 1;
}
h1.charname {
height: 50px;
padding: 0;
margin: 5px 0;
border-bottom: 0;
input {
width: 100%;
height: 100%;
margin: 0;
}
}
}
div.editor-border {
border: 2px solid $primary-border-color; // Replace the hardcoded color with a variable
border-radius: 10px;
}
.sheet-tabs {
flex: 1;
}
.sheet-body,
.sheet-body .tab,
.sheet-body .tab .editor {
height: 100%;
}
.tox {
.tox-editor-container {
background: $c-white;
}
.tox-edit-area {
padding: 0 8px;
}
}
// Flexbox container for each row
.selection-row {
display: flex;
justify-content: space-between; // Ensures label stays left and input moves to the right
align-items: center; // Vertically center the label and input
margin-bottom: 10px; // Optional: Add some space between rows
}

View File

@ -1,124 +0,0 @@
// Section Header
.items-header {
height: 28px;
margin: 2px 0;
padding: 0;
align-items: center;
background: rgba(0, 0, 0, 0.05);
border: $border-groove;
font-weight: bold;
> * {
font-size: 14px;
text-align: center;
}
.item-name {
font-weight: bold;
padding-left: 5px;
text-align: left;
display: flex;
}
}
// Items Lists
.items-list {
list-style: none;
margin: 0;
padding: 0;
overflow-y: auto;
scrollbar-width: thin;
color: $c-tan;
// Child lists
.item-list {
list-style: none;
margin: 0;
padding: 0;
}
// Individual Item
.item {
display: flex;
align-items: center;
padding: 0 2px; // Align with the header border
border-bottom: 1px solid $c-faint;
&:last-child {
border-bottom: none;
}
// Item name and image
.item-name {
flex: 2;
margin: 0;
overflow: hidden;
font-size: 13px;
text-align: left;
display: flex;
color: $c-dark;
h3, h4 {
margin: 0;
white-space: nowrap;
overflow-x: hidden;
}
.item-image {
flex: 0 0 30px;
height: 30px;
background-size: 30px;
border: none;
margin-right: 5px;
}
}
}
// Control Buttons
.item-controls {
display: flex;
flex: 0 0 100px;
justify-content: flex-end;
a {
font-size: 12px;
text-align: center;
margin: 0 6px;
}
}
// Item Properties (like stats or details)
.item-prop {
text-align: center;
border-left: 1px solid $c-faint;
border-right: 1px solid $c-faint;
font-size: 12px;
}
}
// Section Header inside Items List
.items-list .items-header {
height: 28px;
margin: 2px 0;
padding: 0;
align-items: center;
background: rgba(0, 0, 0, 0.05);
border: $border-groove;
font-weight: bold;
> * {
font-size: 12px;
text-align: center;
}
.item-name {
padding-left: 5px;
text-align: left;
}
}
// Optional item formula block
.item-formula {
flex: 0 0 200px;
padding: 0 8px;
}

View File

@ -1,3 +0,0 @@
.resource-label {
font-weight: bold;
}

View File

@ -1,15 +0,0 @@
// _base.scss
@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap");
.window-app {
font-family: $font-stack;
}
.rollable {
&:hover,
&:focus {
color: #000;
text-shadow: $hover-text-shadow;
cursor: pointer;
}
}

View File

@ -1,50 +0,0 @@
// Flexbox Utility Classes
.flex-group-center {
@include flexbox(center, center);
text-align: center;
}
.flex-group-left {
@include flexbox(flex-start, center);
text-align: left;
}
.flex-group-right {
@include flexbox(flex-end, center);
text-align: right;
}
.flexshrink {
flex: 0;
}
.flex-between {
justify-content: space-between;
}
.flexlarge {
flex: 2;
}
// Alignment Utility Classes
.align-left {
@include flexbox(flex-start, center);
text-align: left;
}
.align-right {
@include flexbox(flex-end, center);
text-align: right;
}
.align-center {
@include flexbox(center, center);
text-align: center;
}
// Only apply the right alignment to specific inputs with this class
.right-align-input {
flex: 1;
margin-left: auto; // Push the input to the far right
max-width: 260px; // Optional: Control the width of the input field
}

View File

@ -1,25 +0,0 @@
// _grid.scss
.grid {
display: grid;
gap: 10px;
margin: 10px 0;
padding: 0;
}
@for $i from 2 through 12 {
// Create grid-start-* classes for offsets
.grid-start-#{$i} {
grid-column-start: #{$i};
}
// Create grid-span-* classes for column spans
.grid-span-#{$i} {
grid-column-end: span #{$i};
}
// Create grid-*col classes for grid columns
.grid-#{$i}col {
grid-column: span #{$i} / span #{$i};
grid-template-columns: repeat(#{$i}, minmax(0, 1fr));
}
}

View File

@ -1,12 +0,0 @@
.window-app {
font-family: $font-primary;
}
.rollable {
&:hover,
&:focus {
color: #000;
text-shadow: 0 0 10px rgb(146, 0, 225);
cursor: pointer;
}
}

0
scss/kidsOnBrooms.scss Normal file
View File

View File

@ -1,27 +0,0 @@
// Add custom fonts by visiting and search https://fonts.google.com
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap');
// This is the font used for the book, will not buy it but for refrence https://www.myfonts.com/collections/dreadful-font-aiyari?queryId=undefined&index=universal_search_data&objectIDs=5368854002
// This is the font used for text https://www.myfonts.com/products/lapidary-333-lapidary-333-434881?queryId=undefined&index=universal_search_data&objectIDs=5468003002
// Import utilities.
@import 'utils/variables';
@import 'utils/typography';
@import 'utils/colors';
@import 'utils/mixins';
/* Global styles */
@import 'global/window';
@import 'global/grid';
@import 'global/flex';
@import 'global/base';
.editor-container {
min-height: 200px; /* Adjust this value as needed */
}
/* Styles limited to kidsonbrooms sheets */
.kids-on-brooms {
@import 'components/forms';
@import 'components/resource';
@import 'components/items';
@import 'components/effects';
}

View File

@ -1,13 +0,0 @@
$c-white: #fff;
$c-black: #000;
$c-dark: #191813;
$c-faint: #c9c7b8;
$c-beige: #b5b3a4;
$c-tan: #444;
$primary-border-color: rgb(81, 81, 81);
$focus-border-color: #8102dd;
$hover-text-shadow: 0 0 10px rgb(179, 7, 217);
$border-color: #ccc;

View File

@ -1,25 +0,0 @@
@mixin element-invisible {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
border: 0;
padding: 0;
clip: rect(0 0 0 0);
overflow: hidden;
}
@mixin hide {
display: none;
}
// Update the mixin to accept 3 arguments: direction, wrap, and justify
@mixin flexbox($direction, $wrap: nowrap, $justify: flex-start, $align: stretch) {
display: flex;
flex-direction: $direction;
flex-wrap: $wrap;
justify-content: $justify;
align-items: $align;
}

View File

@ -1,2 +0,0 @@
$font-primary: 'Roboto', sans-serif;
$font-secondary: 'Roboto', sans-serif;

View File

@ -1,10 +0,0 @@
$padding-sm: 5px;
$padding-md: 10px;
$padding-lg: 20px;
$border-groove: 2px groove #eeede0;
$font-stack: "Roboto", sans-serif;
$padding: 10px;
$border-radius: 5px;
$flex-align: center;

View File

@ -1,26 +1,51 @@
{ {
"id": "fvtt-never-stop-blowing-up", "name": "kidsonbrooms",
"title": "Never Stop Blowing Up", "title": "Kids on Brooms",
"description": "The Never Stop Blowing Up system for FoundryVTT!", "description": "This is a implementation of the Kids on Brooms system in FoundryVTT.",
"version": "12.0.0", "version": "0.1.0",
"compatibility": { "compatibility": {
"minimum": 12, "minimum": 12,
"verified": 12 "verified": 12
}, },
"esmodules": ["kidsonbrooms.mjs"],
"authors": [{ "authors": [{
"name": "Joscha Maier" "name": "Joscha Maier",
},{ "url": "https://gitlab.com/wintermyst"
"name": "LeRatierBretonnien"
}], }],
"esmodules": ["module/never-stop-blowing-up.mjs"], "documentTypes": {
"styles": ["css/never-stop-blowing-up.css"], "Actor": {
"playerCharacter": {
}
}
},
"languages": [{
"lang": "en",
"name": "English",
"path": "lang/en.json"
}],
"packs": [],
"packFolders": [],
"socket": true, "socket": true,
"grid": { "url": "https://gitlab.com/wintermyst/kidsonbrooms",
"manifest": "https://your/hosted/system/repo/system.json",
"download": "https://your/packaged/download/archive.zip",
"grid:": {
"type": 1,
"distance": 5, "distance": 5,
"units": "ft" "units": "ft"
}, },
"primaryTokenAttribute": "system.adversityTokens", "relationships": {
"url": "https://www.uberwald.me/gitea/uberwald/fvtt-never-stop-blowing-up", "requires": [
"manifest": "https://www.uberwald.me/gitea/uberwald/fvtt-never-stop-blowing-up/raw/branch/master/system.json", {
"download": "https://www.uberwald.me/gitea/uberwald/fvtt-never-stop-blowing-up/archive/12.0.0.zip" "id": "lib-wrapper",
"type": "module",
"compatibility": {
"minimum": "1.0.0.0",
"verified": "1.12.6.0"
}
}
]
},
"primaryTokenAttribute": "system.adversityTokens"
} }

View File

@ -1,103 +0,0 @@
{
"Actor": {
"types": ["character", "npc"],
"templates": {
"base": {
"stats": {
"stat1": {
"name": "fight",
"value": "d4",
"stat": 0,
"magic": 0
},
"stat2": {
"name": "flight",
"value": "d4",
"stat": 0,
"magic": 0
},
"stat3": {
"name": "brains",
"value": "d4",
"stat": 0,
"magic": 0
},
"stat4": {
"name": "brawn",
"value": "d4",
"stat": 0,
"magic": 0
},
"stat5": {
"name": "charm",
"value": "d4",
"stat": 0,
"magic": 0
},
"stat6": {
"name": "grit",
"value": "d4",
"stat": 0,
"magic": 0
},
"stat7": {
"name": "N/A",
"value": "d4",
"stat": 0,
"magic": 0
},
"stat8": {
"name": "N/A",
"value": "d4",
"stat": 0,
"magic": 0
},
"stat9": {
"name": "N/A",
"value": "d4",
"stat": 0,
"magic": 0
},
"stat10": {
"name": "N/A",
"value": "d4",
"stat": 0,
"magic": 0
}
},
"description": ""
}
},
"character": {
"templates": ["base"],
"wounds": {
"minor": {"m1": false, "m2": false, "m3": false},
"moderate": {"m1": false, "m2": false},
"mortal": {"m1": false}
},
"trope": "",
"age": "",
"pronouns": "",
"fear": "",
"motivation": "",
"grade":"",
"broom": {
"name": "",
"look": "",
"mechanicalbenifit": ""
},
"wand": {
"wood": "",
"core": ""
},
"animalfamiliar":"",
"schoolbag": "",
"adversityTokens": 0,
"tropequestions": "",
"strengths": ""
},
"npc": {
"templates": ["base"]
}
}
}

View File

@ -1,66 +0,0 @@
<form class="{{cssClass}} {{actor.type}} flexcol" autocomplete="off">
{{!-- Sheet Header --}}
<header class="sheet-header">
<img class="profile-img" src="{{actor.img}}" data-edit="img" title="{{actor.name}}" height="100" width="100"/>
<div class="header-fields">
<h1 class="charname"><input name="name" type="text" value="{{actor.name}}" placeholder="Name"/></h1>
<div class="resources grid">
<div class="resource flex-group-center">
<label for="system.trope" class="resource-label">Class</label>
<div class="resource-content flexrow flex-center flex-between">
<input type="text" name="system.trope" value="{{system.trope}}" data-dtype="String"/>
</div>
</div>
</div>
</div>
</header>
{{!-- Sheet Tab Navigation --}}
<nav class="sheet-tabs tabs" data-group="primary">
{{!-- Default tab is specified in actor-sheet.mjs --}}
<a class="item" data-tab="features">Features</a>
<a class="item" data-tab="schoolbag">Inventory</a>
<a class="item" data-tab="strengths">Abilities</a>
<!-- <a class="item" data-tab="trope">Trope Questions</a> -->
</nav>
{{!-- Sheet Body --}}
<section class="sheet-body">
{{!-- Owned Features Tab --}}
<div class="tab features" data-group="primary" data-tab="features">
<section class="grid grid-3col">
<section class="main grid-span-2">
{{> "systems/fvtt-never-stop-blowing-up/templates/actor/parts/actor-features.html"}}
{{> "systems/fvtt-never-stop-blowing-up/templates/actor/parts/actor-adversity.html"}}
</section>
<aside class="sidebar">
{{> "systems/fvtt-never-stop-blowing-up/templates/actor/parts/actor-stats.html"}}
</aside>
</section>
</div>
<div class="tab schoolbag" data-group="primary" data-tab="schoolbag">
{{!-- Schoolbag Tab --}}
<div class="tab features editor-border" data-group="primary" data-tab="schoolbag">
{{editor schoolbag target="system.schoolbag" engine="prosemirror" button=false collaborate=false editable=true}}
</div>
</div>
{{!-- Strengths Tab --}}
<div class="tab features editor-border" data-group="primary" data-tab="strengths">
{{editor strengths target="system.strengths" engine="prosemirror" button=false collaborate=false editable=true}}
</div>
{{!-- Trope Questions Tab --}}
<div class="tab features editor-border" data-group="primary" data-tab="trope">
{{editor tropequestions target="system.tropequestions" engine="prosemirror" button=false collaborate=false editable=true}}
</div>
</section>
</form>

View File

@ -1,5 +1,4 @@
<form class="{{cssClass}} {{actor.type}} flexcol" autocomplete="off"> <form class="{{cssClass}} {{actor.type}} flexcol" autocomplete="off">
{{!-- Sheet Header --}} {{!-- Sheet Header --}}
<header class="sheet-header"> <header class="sheet-header">
<img class="profile-img" src="{{actor.img}}" data-edit="img" title="{{actor.name}}" height="100" width="100"/> <img class="profile-img" src="{{actor.img}}" data-edit="img" title="{{actor.name}}" height="100" width="100"/>
@ -17,19 +16,5 @@
</div> </div>
</header> </header>
{{!-- Sheet Tab Navigation --}}
<nav class="sheet-tabs tabs" data-group="primary">
{{!-- Default tab is specified in actor-sheet.mjs --}}
<a class="item" data-tab="features">Features</a>
</nav>
{{!-- Sheet Body --}}
<section class="sheet-body">
{{!-- Owned Features Tab --}}
<div class="tab features" data-group="primary" data-tab="features">
{{> "systems/fvtt-never-stop-blowing-up/templates/actor/parts/actor-npc-stats.html"}}
</div>
</section>
</form> </form>

View File

@ -1,11 +0,0 @@
<fieldset>
<legend>Adversity Tokens</legend>
<div class="resource flexcol" >
<label for="system.adversity" class="resource-label">
Begin the game with 3
adversity tokens. Add 1
each time you fail a roll.
</label>
<input type="number" name="system.adversityTokens" value="{{system.adversityTokens}}" data-dtype="Number"/>
</div>
</fieldset>

View File

@ -1,183 +0,0 @@
<section class="grid grid-3col">
<fieldset class="resource grid-span-3 flexcol">
<div class="resource grid-span-3 flexrow">
<label for="system.grade" class="resource-label">Species</label>
<input type="text" name="system.grade" value="{{system.grade}}" data-dtype="String" />
</div>
<div class="resource flexrow">
<label for="system.age" class="resource-label">Age</label>
<input type="text" name="system.age" value="{{system.age}}" data-dtype="String" />
</div>
<div class="resource grid-span-2 flexrow">
<label for="system.pronouns" class="resource-label">Pronouns</label>
<input type="text" name="system.pronouns" value="{{system.pronouns}}" data-dtype="String" />
</div>
<div class="resource grid-span-3 flexrow">
<label for="system.fear" class="resource-label">Fear</label>
<input type="text" name="system.fear" value="{{system.fear}}" data-dtype="String" />
</div>
<div class="resource grid-span-3 flexrow">
<label for="system.motivation" class="resource-label">Motivation</label>
<input type="text" name="system.motivation" value="{{system.motivation}}" data-dtype="String" />
</div>
<div class="resource grid-span-3 flexrow">
<label for="system.description" class="resource-label">Description</label>
<input type="text" name="system.description" value="{{system.description}}" data-dtype="String" />
</div>
</fieldset>
<!-- <fieldset class="resource grid-span-3 flexcol">
<legend>Your Broom</legend> -->
<!-- Broom Name Input with Dropdown -->
<!--
<div class="resource flexrow">
<label for="broom-name" class="resource-label">Name</label>
<input list="broomOptions" id="broom-name" name="system.broom.name" value="{{system.broom.name}}"
data-dtype="String" placeholder="Select or Enter Broom Name" oninput="updateBroomDetails()"
onblur="updateBroomDetails()">
<datalist id="broomOptions">
<option value="The Blocker's Broom" data-look="Defensive" data-mechanical="Gain the Guardian Strength"></option>
<option value="Bolting 4000" data-look="Fast" data-mechanical="+1 to Flight checks"></option>
<option value="The Bruiser" data-look="Intense" data-mechanical="+1 to Fight checks"></option>
<option value="Cunning Captains Cruiser" data-look="Natural Leader"
data-mechanical="Treat Snap Decisions as Planned Actions unless facing fear"></option>
<option value="Daredevils Duster" data-look="Flashy"
data-mechanical="+3 to Charm checks when performing a stunt"></option>
<option value="The Daring Dodger 3000" data-look="Ambitious"
data-mechanical="Each Adversity Token adds +2 to your roll instead of +1"></option>
<option value="Heartwoods Helper" data-look="Outgoing"
data-mechanical="Each successful check grants an ally one Adversity Token"></option>
<option value="Mapmakers Friend" data-look="Level-Headed"
data-mechanical="Cannot get lost if you know the area"></option>
<option value="The Masterminds Sweeper" data-look="Confident" data-mechanical="+1 to Brains checks"></option>
<option value="The Strong Sweep 2500" data-look="Strong" data-mechanical="+1 to Brawn checks"></option>
<option value="The Suave Sweeper" data-look="Trustworthy" data-mechanical="+1 to Charm checks"></option>
<option value="The Tough Break" data-look="Tough" data-mechanical="+1 to Grit checks"></option>
<option value="Valiance 2400" data-look="Brave" data-mechanical="May ignore your fears"></option>
<option value="Weasels Whisk" data-look="Sneaky" data-mechanical="Gain the Unassuming Strength"></option>
</datalist>
</div>
-->
<!--
<div class="resource flexrow">
<label for="broom-look" class="resource-label">Look</label>
<input type="text" id="broom-look" name="system.broom.look" value="{{system.broom.look}}" data-dtype="String" />
</div>
<div class="resource flexrow">
<label for="broom-mechanical" class="resource-label">Mechanical Benefit</label>
<textarea id="broom-mechanical" name="system.broom.mechanicalbenefit" data-dtype="String" rows="3"
style="resize:none;"></textarea>
</div>
</fieldset> -->
<script>
function updateBroomDetails() {
// Use a short delay to allow browser to properly handle the datalist input
setTimeout(function () {
const broomNameInput = document.getElementById("broom-name").value.trim();
const broomOptions = document.querySelectorAll("#broomOptions option");
let selectedLook = "";
let selectedMechanical = "";
// Loop through the datalist options to find a matching broom name
broomOptions.forEach(option => {
if (option.value.toLowerCase() === broomNameInput.toLowerCase()) {
selectedLook = option.getAttribute("data-look");
selectedMechanical = option.getAttribute("data-mechanical");
}
});
// Update the look and mechanical benefit fields if a predefined broom is selected
document.getElementById("broom-look").value = selectedLook || "";
document.getElementById("broom-mechanical").value = selectedMechanical || "";
}, 100); // Delay of 100 milliseconds
}
</script>
<!--
<fieldset class="resource grid-span-3 flexcol">
<legend>Wand Selection</legend>
<div class="resource-flexrow">
<label for="system.wand.wood" class="resource-label">Wood Type</label>
<input list="WoodOptions" id="wandWoodChoice" name="system.wand.wood" value="{{system.wand.wood}}"
placeholder="Select Wood type" oninput="updateWandWoodDetails()" onblur="updateWandWoodDetails()">
<datalist id="WoodOptions">
<option value="">Select Wood</option>
<option value="Wisteria">(Brains)</option>
<option value="Hawthorn">(Brains)</option>
<option value="Pine">(Brawn)</option>
<option value="Oak">(Brawn)</option>
<option value="Crabapple">(Fight)</option>
<option value="Dogwood">(Fight)</option>
<option value="Birch">(Flight)</option>
<option value="Bamboo">(Flight)</option>
<option value="Ironwood">(Grit)</option>
<option value="Maple">(Grit)</option>
<option value="Lilac">(Charm)</option>
<option value="Cherry">(Charm)</option>
</datalist>
</div>
<div class="resource-flexrow">
<label for="system.wand.core" class="resource-label">Core Type</label>
<input list="CoreOptions" id="wandCoreChoice" name="system.wand.core" value="{{system.wand.core}}"
placeholder="Select Core type" oninput="updateWandCoreDetails()" onblur="updateWandCoreDetails()">
<datalist id="CoreOptions">
<option value="">Select Core</option>
<option value="Parchment">(Brains)</option>
<option value="Phoenix Feather">(Brains)</option>
<option value="Owl Feather">(Brains)</option>
<option value="Gorilla Fur">(Brawn)</option>
<option value="Ogres Fingernail">(Brawn)</option>
<option value="Hippos Tooth">(Brawn)</option>
<option value="Dragons Heartstring">(Fight)</option>
<option value="Wolfs Tooth">(Fight)</option>
<option value="Elks Antler">(Fight)</option>
<option value="Hawks Feather">(Flight)</option>
<option value="Bats Bone">(Flight)</option>
<option value="Changelings Hair">(Charm)</option>
<option value="Gold">(Charm)</option>
<option value="Mirror">(Charm)</option>
<option value="Steel">(Grit)</option>
<option value="Diamond">(Grit)</option>
<option value="Lions Mane">(Grit)</option>
</datalist>
</div>
</fieldset>
<fieldset class="resource grid-span-3 flexcol">
<legend>Animal Familiar</legend>
<div class="resource grid-span-3 flexrow">
<label for="system.animalfamiliar" class="resource-label">Animal Familiar</label>
<input type="text" name="system.animalfamiliar" value="{{system.animalfamiliar}}" data-dtype="String" />
</div>
</fieldset>
-->
<fieldset class="resource grid-span-3 flexcol">
<legend>Wounds</legend>
<div class="flexrow">
<span>Minor</span>
{{#each system.wounds.minor as |minorWound key|}}
<input type="checkbox" name="system.wounds.minor.{{key}}" {{checked minorWound}} />
{{/each}}
</div>
<div class="flexrow">
<span>Moderate</span>
{{#each system.wounds.moderate as |minorWound key|}}
<input type="checkbox" name="system.wounds.moderate.{{key}}" {{checked minorWound}} />
{{/each}}
</div>
<div class="flexrow">
<span>Mortal</span>
{{#each system.wounds.mortal as |minorWound key|}}
<input type="checkbox" name="system.wounds.mortal.{{key}}" {{checked minorWound}} />
{{/each}}
</div>
</fieldset>
</section>

View File

@ -1,31 +0,0 @@
<section class="flexcol">
{{#each system.stats as |stat key|}}
<Fieldset class="grid grid-5col">
<legend>{{stat.name}}</legend>
<select name="system.stats.{{key}}.value">
{{#select stat.value}}
<option value="d20">d20</option>
<option value="d12">d12</option>
<option value="d10">d10</option>
<option value="d8">d8</option>
<option value="d6">d6</option>
<option value="d4">d4</option>
{{/select}}
</select>
<div class="flexrow grid-span-4">
<Fieldset class="flexrow">
<legend>Stat</legend>
<span class="ability-mod rollable" data-roll="{{stat.value}}+{{stat.stat}}" data-label="Stat Roll for {{key}}"><i class="fas fa-dice-d20"></i></span>
<input type="text" name="system.stats.{{key}}.stat" value="{{stat.stat}}" data-dtype="String"/>
</Fieldset>
<!--
<Fieldset class="flexrow">
<legend>Magic</legend>
<span class="ability-mod rollable" data-roll="{{stat.value}}+{{stat.magic}}" data-label="Magic Roll for {{key}}"><i class="fas fa-dice-d20"></i></span>
<input type="text" name="system.stats.{{key}}.magic" value="{{stat.magic}}" data-dtype="String"/>
</Fieldset>
-->
</div>
</Fieldset>
{{/each}}
</section>

View File

@ -1,37 +0,0 @@
<section class="flexcol">
{{#each system.stats as |stat key|}}
<Fieldset class="flexrow">
<legend><input type="text" value="{{capitalizeFirst stat.name}}" name="system.stats.{{key}}.name"></legend>
<div class="flexrow flex-group-center">
<!-- Die type dropdown -->
<select name="system.stats.{{key}}.value">
<option value="d20" {{#if (eq stat.value 'd20')}}selected{{/if}}>d20</option>
<option value="d12" {{#if (eq stat.value 'd12')}}selected{{/if}}>d12</option>
<option value="d10" {{#if (eq stat.value 'd10')}}selected{{/if}}>d10</option>
<option value="d8" {{#if (eq stat.value 'd8')}}selected{{/if}}>d8</option>
<option value="d6" {{#if (eq stat.value 'd6')}}selected{{/if}}>d6</option>
<option value="d4" {{#if (eq stat.value 'd4')}}selected{{/if}}>d4</option>
</select>
<!-- Stat rolling and input -->
<Fieldset class="flexrow flex-group-center">
<legend>Stat</legend>
<span class="ability-mod rollable" data-roll="1{{stat.value}}x+{{stat.stat}}" data-label="Stat Roll for {{key}}" data-key="{{key}}">
<i class="fas fa-dice-d20"></i>
</span>
<input type="number" name="system.stats.{{key}}.stat" value="{{stat.stat}}" data-dtype="Number"/>
</Fieldset>
<!-- Magic rolling and input
<Fieldset class="flexrow flex-group-center">
<legend>Magic</legend>
<span class="ability-mod rollable" data-roll="1{{stat.value}}x+1d4x+{{stat.stat}}" data-label="Magic Roll for {{key}}" data-key="{{key}}">
<i class="fas fa-dice-d20"></i>
</span>
</Fieldset>-->
</div>
</Fieldset>
{{/each}}
</section>

View File