Compare commits
11 Commits
ec3e65c0f0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23c3bd92d0 | ||
|
|
518dd77788 | ||
|
|
dd0f881d9e | ||
|
|
c446824c2d | ||
|
|
717ea95814 | ||
|
|
767969c201 | ||
|
|
6f1c8b7c88 | ||
|
|
9bba7c82f6 | ||
|
|
6de6353fab | ||
|
|
e20df693d7 | ||
|
|
a48f058583 |
@@ -1,21 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# PostToolUse Hook for Write - Logs file writes and can trigger actions
|
|
||||||
|
|
||||||
# Extract file path from parameters
|
|
||||||
FILE_PATH="${CLAUDE_TOOL_PARAMETERS:-Unknown file}"
|
|
||||||
|
|
||||||
# Log the write operation
|
|
||||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] File written: $FILE_PATH" >> .claude/logs/writes.log
|
|
||||||
|
|
||||||
# Optional: Auto-format specific file types
|
|
||||||
if [[ "$FILE_PATH" =~ \.(js|ts|jsx|tsx)$ ]]; then
|
|
||||||
# Uncomment to enable auto-formatting with prettier
|
|
||||||
# npx prettier --write "$FILE_PATH" 2>/dev/null || true
|
|
||||||
echo " -> JavaScript/TypeScript file detected" >> .claude/logs/writes.log
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$FILE_PATH" =~ \.(py)$ ]]; then
|
|
||||||
# Uncomment to enable auto-formatting with black
|
|
||||||
# black "$FILE_PATH" 2>/dev/null || true
|
|
||||||
echo " -> Python file detected" >> .claude/logs/writes.log
|
|
||||||
fi
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# PreToolUse Hook for Bash - Logs bash commands before execution
|
|
||||||
|
|
||||||
# Extract the bash command from CLAUDE_TOOL_PARAMETERS if available
|
|
||||||
COMMAND="${CLAUDE_TOOL_PARAMETERS:-Unknown command}"
|
|
||||||
|
|
||||||
# Log the command
|
|
||||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Executing: $COMMAND" >> .claude/logs/bash.log
|
|
||||||
|
|
||||||
# Optional: Add safety checks
|
|
||||||
# Example: Block dangerous commands
|
|
||||||
if echo "$COMMAND" | grep -qE "rm -rf /|mkfs|dd if="; then
|
|
||||||
echo "WARNING: Potentially dangerous command blocked!" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# SessionEnd Hook - Runs when a Claude Code session ends
|
|
||||||
|
|
||||||
# Log session end with timestamp
|
|
||||||
echo "Session Ended: $(date '+%Y-%m-%d %H:%M:%S')" >> .claude/logs/session.log
|
|
||||||
echo "" >> .claude/logs/session.log
|
|
||||||
|
|
||||||
# Optional: Clean up temporary files
|
|
||||||
# rm -f .claude/tmp/*
|
|
||||||
|
|
||||||
echo "Session ended. Logs saved to .claude/logs/session.log"
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# SessionStart Hook - Runs when a new Claude Code session starts
|
|
||||||
|
|
||||||
# Create log directory if it doesn't exist
|
|
||||||
mkdir -p .claude/logs
|
|
||||||
|
|
||||||
# Log session start with timestamp
|
|
||||||
echo "========================================" >> .claude/logs/session.log
|
|
||||||
echo "Session Started: $(date '+%Y-%m-%d %H:%M:%S')" >> .claude/logs/session.log
|
|
||||||
echo "Working Directory: $(pwd)" >> .claude/logs/session.log
|
|
||||||
echo "User: $(whoami)" >> .claude/logs/session.log
|
|
||||||
echo "========================================" >> .claude/logs/session.log
|
|
||||||
|
|
||||||
# Output session initialization message to Claude
|
|
||||||
cat << 'EOF'
|
|
||||||
🚀 **New Session Initialized - Foundry VTT Development Environment**
|
|
||||||
|
|
||||||
📋 **MANDATORY REMINDERS FOR THIS SESSION**:
|
|
||||||
|
|
||||||
1. ✅ **CLAUDE.md** has been loaded with project instructions
|
|
||||||
2. ✅ **8 MCP Servers** are available: serena, sequential-thinking, context7, memory, fetch, windows-mcp, playwright, database-server
|
|
||||||
3. ✅ **Specialized Agents** available: Explore, test-engineer, code-reviewer, refactoring-specialist, debugger, architect, documentation-writer, security-analyst
|
|
||||||
|
|
||||||
⚠️ **CRITICAL REQUIREMENTS** - You MUST follow these for EVERY task:
|
|
||||||
|
|
||||||
**At the START of EVERY task, provide a Tooling Strategy Decision:**
|
|
||||||
- **Agents**: State if using (which one) or not using (with reason)
|
|
||||||
- **Slash Commands**: State if using (which one) or not using (with reason)
|
|
||||||
- **MCP Servers**: State if using (which ones) or not using (with reason)
|
|
||||||
- **Approach**: Brief strategy overview
|
|
||||||
|
|
||||||
**At the END of EVERY task, provide a Task Completion Summary:**
|
|
||||||
- What was done
|
|
||||||
- Which features were used (Agents, Slash Commands, MCP Servers, Core Tools)
|
|
||||||
- Files modified
|
|
||||||
- Efficiency notes
|
|
||||||
|
|
||||||
📖 **See documentation**:
|
|
||||||
- **CLAUDE.md**: Full project documentation (automatically loaded)
|
|
||||||
- **.claude/SESSION_INSTRUCTIONS.md**: Quick reference for mandatory policies
|
|
||||||
- "Mandatory Tooling Usage Policy" (CLAUDE.md lines 545-610)
|
|
||||||
- "Task Initiation Requirements" (CLAUDE.md lines 905-920)
|
|
||||||
- "Task Completion Status Messages" (CLAUDE.md lines 925-945)
|
|
||||||
|
|
||||||
🎯 **This Session's Focus**: Foundry VTT v11.315 + PF1e v10.8 macro development and debugging
|
|
||||||
|
|
||||||
💡 **Tip**: You can read .claude/SESSION_INSTRUCTIONS.md anytime for a quick reminder of mandatory policies.
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Session initialized successfully
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Stop hook - Executed when Claude Code finishes responding
|
|
||||||
# Purpose: Log completion of tasks
|
|
||||||
|
|
||||||
# Create logs directory if it doesn't exist
|
|
||||||
mkdir -p .claude/logs
|
|
||||||
|
|
||||||
# Log the stop event
|
|
||||||
echo "[$(date)] Claude finished responding" >> .claude/logs/session.log
|
|
||||||
|
|
||||||
# Note: The actual summary generation is done by Claude in the response
|
|
||||||
# This hook just logs the event for tracking purposes
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# UserPromptSubmit Hook - Runs when user submits a prompt
|
|
||||||
|
|
||||||
# Log prompt submission (without actual content for privacy)
|
|
||||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] User prompt submitted" >> .claude/logs/session.log
|
|
||||||
|
|
||||||
# Optional: Show notification (requires notify-send on Linux or similar)
|
|
||||||
# notify-send "Claude Code" "Processing your request..." 2>/dev/null || true
|
|
||||||
|
|
||||||
# Optional: Track usage statistics
|
|
||||||
PROMPT_COUNT_FILE=".claude/logs/prompt_count.txt"
|
|
||||||
if [ -f "$PROMPT_COUNT_FILE" ]; then
|
|
||||||
COUNT=$(cat "$PROMPT_COUNT_FILE")
|
|
||||||
COUNT=$((COUNT + 1))
|
|
||||||
else
|
|
||||||
COUNT=1
|
|
||||||
fi
|
|
||||||
echo "$COUNT" > "$PROMPT_COUNT_FILE"
|
|
||||||
5
.gitattributes
vendored
Normal file
5
.gitattributes
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
*.ldb filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.mp3 filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.webm filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.7z filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.zip filter=lfs diff=lfs merge=lfs -text
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
# .gitignore Configuration for JPD Portal
|
|
||||||
|
|
||||||
This document explains the .gitignore configuration for the JPD Portal project.
|
|
||||||
|
|
||||||
## Critical Files Protected
|
|
||||||
|
|
||||||
The following sensitive files are **NEVER** committed to version control:
|
|
||||||
|
|
||||||
### 🔐 Security & Credentials
|
|
||||||
- `src/JPD.env` - Contains API keys (Jira, Confluence, Claude API)
|
|
||||||
- `*.env` files - All environment variable files
|
|
||||||
- `secrets.json` - ASP.NET Core user secrets
|
|
||||||
|
|
||||||
### 💾 Databases & Data
|
|
||||||
- `src/JpdPortal/data/vectors.db` - SQLite vector embeddings database
|
|
||||||
- `src/JpdPortal/data/uploads/*` - User-uploaded requirements files (except .gitkeep)
|
|
||||||
- All `.db`, `.sqlite`, `.sqlite3` files
|
|
||||||
|
|
||||||
### 🏗️ Build Artifacts
|
|
||||||
- `bin/` and `obj/` directories - .NET build outputs
|
|
||||||
- `Debug/` and `Release/` - Build configurations
|
|
||||||
- `*.dll`, `*.exe`, `*.pdb` - Compiled binaries
|
|
||||||
|
|
||||||
## Directory Structure Preservation
|
|
||||||
|
|
||||||
Some directories need to exist but their contents should not be committed:
|
|
||||||
|
|
||||||
```
|
|
||||||
src/JpdPortal/data/
|
|
||||||
├── uploads/
|
|
||||||
│ └── .gitkeep ← Tracked to preserve directory
|
|
||||||
│ └── *.txt ← Ignored (user uploads)
|
|
||||||
└── vectors.db ← Ignored (SQLite database)
|
|
||||||
```
|
|
||||||
|
|
||||||
The `.gitkeep` file ensures the `uploads/` directory structure is preserved in git.
|
|
||||||
|
|
||||||
## Pattern Categories
|
|
||||||
|
|
||||||
The .gitignore is organized into these sections:
|
|
||||||
|
|
||||||
1. **Claude Code** - IDE-specific files
|
|
||||||
2. **Project-Specific Sensitive Files** - JPD.env, databases, uploads
|
|
||||||
3. **.NET Core / ASP.NET Core** - Build artifacts, Visual Studio files
|
|
||||||
4. **AI/ML Specific** - ONNX models, training artifacts, vector databases
|
|
||||||
5. **Blazor Specific** - WebAssembly cache, generated assets
|
|
||||||
6. **Testing** - Test results and coverage reports
|
|
||||||
7. **Environment Variables & Secrets** - All .env files
|
|
||||||
8. **Database Files** - SQLite and other database files
|
|
||||||
9. **Logs** - Application log files
|
|
||||||
10. **OS Files** - Windows, macOS, Linux system files
|
|
||||||
11. **Backup Files** - .bak, .old, .tmp files
|
|
||||||
12. **Node.js** - npm packages (if used for frontend)
|
|
||||||
13. **Python** - __pycache__, venv (if used for ML scripts)
|
|
||||||
|
|
||||||
## Already Tracked Files
|
|
||||||
|
|
||||||
If you see modified files in `bin/` or `obj/` directories that should be ignored, they were tracked before the .gitignore was updated. To remove them:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Remove build artifacts from git tracking
|
|
||||||
git rm -r --cached src/JpdPortal/bin/
|
|
||||||
git rm -r --cached src/JpdPortal/obj/
|
|
||||||
|
|
||||||
# Commit the changes
|
|
||||||
git commit -m "chore: remove build artifacts from git tracking"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note**: The `--cached` flag removes files from git tracking but keeps them on your local filesystem.
|
|
||||||
|
|
||||||
## Verifying Ignore Patterns
|
|
||||||
|
|
||||||
To check if a file will be ignored:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check specific files
|
|
||||||
git check-ignore -v src/JPD.env
|
|
||||||
git check-ignore -v src/JpdPortal/data/vectors.db
|
|
||||||
|
|
||||||
# Check entire directory
|
|
||||||
git check-ignore -v src/JpdPortal/bin/*
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected output for ignored files:
|
|
||||||
```
|
|
||||||
.gitignore:199:*.env src/JPD.env
|
|
||||||
```
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
### ✅ DO:
|
|
||||||
- Always verify `src/JPD.env` is NOT staged before committing
|
|
||||||
- Check `git status` before commits to ensure no sensitive data
|
|
||||||
- Use `git check-ignore` to verify patterns
|
|
||||||
- Keep the .gitkeep file in the uploads directory
|
|
||||||
|
|
||||||
### ❌ DON'T:
|
|
||||||
- Never commit API keys or credentials
|
|
||||||
- Never force-add ignored files with `git add -f`
|
|
||||||
- Don't commit build artifacts (bin/, obj/)
|
|
||||||
- Don't commit database files with user data
|
|
||||||
- Don't commit user-uploaded files
|
|
||||||
|
|
||||||
## Emergency: Removing Sensitive Data
|
|
||||||
|
|
||||||
If you accidentally committed sensitive data (like JPD.env), you need to remove it from git history:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Remove file from git history (WARNING: rewrites history)
|
|
||||||
git filter-branch --force --index-filter \
|
|
||||||
"git rm --cached --ignore-unmatch src/JPD.env" \
|
|
||||||
--prune-empty --tag-name-filter cat -- --all
|
|
||||||
|
|
||||||
# Force push (if already pushed to remote)
|
|
||||||
git push origin --force --all
|
|
||||||
```
|
|
||||||
|
|
||||||
**Better approach**: Use GitHub's BFG Repo-Cleaner or contact repository admin.
|
|
||||||
|
|
||||||
## Environment Setup for New Developers
|
|
||||||
|
|
||||||
When cloning this repository:
|
|
||||||
|
|
||||||
1. Copy the environment template:
|
|
||||||
```bash
|
|
||||||
cp src/JPD.env.example src/JPD.env
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Edit `src/JPD.env` with your credentials
|
|
||||||
|
|
||||||
3. Verify it's ignored:
|
|
||||||
```bash
|
|
||||||
git status
|
|
||||||
# Should NOT show JPD.env as untracked
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Create necessary directories:
|
|
||||||
```bash
|
|
||||||
mkdir -p src/JpdPortal/data/uploads
|
|
||||||
```
|
|
||||||
|
|
||||||
## Maintenance
|
|
||||||
|
|
||||||
Review and update this .gitignore when:
|
|
||||||
- Adding new services with credentials
|
|
||||||
- Adding new ML models or data storage
|
|
||||||
- Changing build output directories
|
|
||||||
- Adding new tools or frameworks
|
|
||||||
- Team reports accidental commits of ignored files
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Last Updated**: 2025-10-20
|
|
||||||
**Project**: JPD Portal v1.0.0
|
|
||||||
@@ -1,9 +1,3 @@
|
|||||||
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
|
|
||||||
# * For C, use cpp
|
|
||||||
# * For JavaScript, use typescript
|
|
||||||
# Special requirements:
|
|
||||||
# * csharp: Requires the presence of a .sln file in the project folder.
|
|
||||||
language: bash
|
|
||||||
|
|
||||||
# the encoding used by text files in the project
|
# the encoding used by text files in the project
|
||||||
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
||||||
@@ -67,8 +61,57 @@ excluded_tools: []
|
|||||||
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||||
# (contrary to the memories, which are loaded on demand).
|
# (contrary to the memories, which are loaded on demand).
|
||||||
initial_prompt: ""
|
initial_prompt: ""
|
||||||
|
# the name by which the project can be referenced within Serena
|
||||||
project_name: "Claude Code Setup"
|
project_name: "Claude Code Setup"
|
||||||
|
|
||||||
# Enable tool usage statistics collection for the web dashboard
|
# Enable tool usage statistics collection for the web dashboard
|
||||||
record_tool_usage_stats: true
|
record_tool_usage_stats: true
|
||||||
|
|
||||||
|
# list of mode names to that are always to be included in the set of active modes
|
||||||
|
# The full set of modes to be activated is base_modes + default_modes.
|
||||||
|
# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply.
|
||||||
|
# Otherwise, this setting overrides the global configuration.
|
||||||
|
# Set this to [] to disable base modes for this project.
|
||||||
|
# Set this to a list of mode names to always include the respective modes for this project.
|
||||||
|
base_modes:
|
||||||
|
|
||||||
|
# list of mode names that are to be activated by default.
|
||||||
|
# The full set of modes to be activated is base_modes + default_modes.
|
||||||
|
# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply.
|
||||||
|
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
|
||||||
|
# This setting can, in turn, be overridden by CLI parameters (--mode).
|
||||||
|
default_modes:
|
||||||
|
|
||||||
|
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default)
|
||||||
|
included_optional_tools: []
|
||||||
|
|
||||||
|
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
|
||||||
|
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
|
||||||
|
fixed_tools: []
|
||||||
|
|
||||||
|
|
||||||
|
# list of languages for which language servers are started; choose from:
|
||||||
|
# al bash clojure cpp csharp
|
||||||
|
# csharp_omnisharp dart elixir elm erlang
|
||||||
|
# fortran fsharp go groovy haskell
|
||||||
|
# java julia kotlin lua markdown
|
||||||
|
# matlab nix pascal perl php
|
||||||
|
# powershell python python_jedi r rego
|
||||||
|
# ruby ruby_solargraph rust scala swift
|
||||||
|
# terraform toml typescript typescript_vts vue
|
||||||
|
# yaml zig
|
||||||
|
# (This list may be outdated. For the current list, see values of Language enum here:
|
||||||
|
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
|
||||||
|
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
|
||||||
|
# Note:
|
||||||
|
# - For C, use cpp
|
||||||
|
# - For JavaScript, use typescript
|
||||||
|
# - For Free Pascal/Lazarus, use pascal
|
||||||
|
# Special requirements:
|
||||||
|
# Some languages require additional setup/installations.
|
||||||
|
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
|
||||||
|
# When using multiple languages, the first language server that supports a given file will be used for that file.
|
||||||
|
# The first language is the default language and the respective language server will be used as a fallback.
|
||||||
|
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
|
||||||
|
languages:
|
||||||
|
- bash
|
||||||
|
|||||||
Submodule .windows-mcp deleted from a1a56eab56
File diff suppressed because it is too large
Load Diff
1095
CLAUDE_TEMPLATE.md
1095
CLAUDE_TEMPLATE.md
File diff suppressed because it is too large
Load Diff
@@ -1,329 +0,0 @@
|
|||||||
# MCP Documentation Consolidation Summary
|
|
||||||
|
|
||||||
> **Completed**: 2025-10-20
|
|
||||||
> **Action**: Consolidated all MCP server documentation into ONE comprehensive file
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What Was Done
|
|
||||||
|
|
||||||
All MCP server documentation has been **consolidated into a single file** at the root level:
|
|
||||||
|
|
||||||
### 📄 New Consolidated File
|
|
||||||
|
|
||||||
**[MCP_SERVERS_GUIDE.md](MCP_SERVERS_GUIDE.md)** (Root Level)
|
|
||||||
|
|
||||||
This comprehensive guide includes:
|
|
||||||
- ✅ Complete overview of all 8 MCP servers
|
|
||||||
- ✅ Quick start and installation instructions
|
|
||||||
- ✅ Detailed capabilities for each server
|
|
||||||
- ✅ Configuration examples
|
|
||||||
- ✅ Usage patterns by agent and command
|
|
||||||
- ✅ Best practices
|
|
||||||
- ✅ Troubleshooting guide
|
|
||||||
- ✅ Advanced topics
|
|
||||||
|
|
||||||
**Size**: ~800 lines | **Reading Time**: ~20 minutes
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Files Consolidated
|
|
||||||
|
|
||||||
The new guide consolidates content from these previous files:
|
|
||||||
|
|
||||||
### Removed Files (Content Now in MCP_SERVERS_GUIDE.md)
|
|
||||||
|
|
||||||
1. ❌ **MCP_SETUP_TOOLS.md** (root) - Quick reference for setup tools
|
|
||||||
2. ❌ **.claude/tools/README-MCP-SETUP.md** - Comprehensive setup guide
|
|
||||||
3. ❌ **.claude/tools/QUICK-START.md** - Quick start guide
|
|
||||||
|
|
||||||
**Total Removed**: 3 files
|
|
||||||
|
|
||||||
### Preserved Files (Still Relevant)
|
|
||||||
|
|
||||||
These files were **kept** as they serve different purposes:
|
|
||||||
|
|
||||||
1. ✅ **.claude/agents/MCP_USAGE_TEMPLATES.md** - Quick reference template for agents/commands
|
|
||||||
- **Purpose**: Copy-paste reference for adding MCP usage to agents
|
|
||||||
- **Different from**: MCP_SERVERS_GUIDE.md (which is comprehensive documentation)
|
|
||||||
- **Renamed and moved**: From `.claude/MCP_CORRECT_USAGE_GUIDE.md` to agents folder
|
|
||||||
|
|
||||||
2. ✅ **.mcp.json** - Configuration file (not documentation)
|
|
||||||
|
|
||||||
3. ✅ **.claude/tools/setup-all-mcp-servers.ps1** - Setup script (Windows)
|
|
||||||
|
|
||||||
4. ✅ **.claude/tools/setup-all-mcp-servers.sh** - Setup script (Linux/Mac)
|
|
||||||
|
|
||||||
5. ✅ **.claude/tools/test-mcp-servers.ps1** - Test script (Windows)
|
|
||||||
|
|
||||||
6. ✅ **.claude/tools/test-mcp-servers.sh** - Test script (Linux/Mac)
|
|
||||||
|
|
||||||
7. ✅ **.claude/tools/README.md** - Tools directory index (updated to reference new guide)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Documentation Structure After Consolidation
|
|
||||||
|
|
||||||
### Root Level Documentation
|
|
||||||
|
|
||||||
```
|
|
||||||
Claude Code Setup/
|
|
||||||
├── README.md # Main project README
|
|
||||||
├── QUICKSTART.md # Quick start guide
|
|
||||||
├── CLAUDE.md # Project instructions
|
|
||||||
├── CLAUDE_CODE_SETUP_COMPLETE.md # Complete Claude Code documentation
|
|
||||||
├── MCP_SERVERS_GUIDE.md # ⭐ NEW: Complete MCP documentation
|
|
||||||
└── .mcp.json # MCP configuration
|
|
||||||
```
|
|
||||||
|
|
||||||
### MCP-Related Files
|
|
||||||
|
|
||||||
```
|
|
||||||
.claude/
|
|
||||||
├── agents/
|
|
||||||
│ └── MCP_USAGE_TEMPLATES.md # Copy-paste templates for agents
|
|
||||||
└── tools/
|
|
||||||
├── README.md # Tools directory index (updated)
|
|
||||||
├── setup-all-mcp-servers.ps1 # Windows setup
|
|
||||||
├── setup-all-mcp-servers.sh # Linux/Mac setup
|
|
||||||
├── test-mcp-servers.ps1 # Windows test
|
|
||||||
└── test-mcp-servers.sh # Linux/Mac test
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Benefits of Consolidation
|
|
||||||
|
|
||||||
### Before (Multiple Files)
|
|
||||||
|
|
||||||
**Problems**:
|
|
||||||
- ❌ MCP documentation scattered across 4+ files
|
|
||||||
- ❌ Duplicate information in multiple locations
|
|
||||||
- ❌ Unclear which file to read first
|
|
||||||
- ❌ Inconsistent formatting and depth
|
|
||||||
- ❌ Hard to maintain consistency
|
|
||||||
|
|
||||||
**User Experience**:
|
|
||||||
- "Where do I find MCP server capabilities?"
|
|
||||||
- "Is this the complete guide or just a quick reference?"
|
|
||||||
- "Which file should I read?"
|
|
||||||
|
|
||||||
### After (Single File)
|
|
||||||
|
|
||||||
**Advantages**:
|
|
||||||
- ✅ ONE comprehensive source of truth
|
|
||||||
- ✅ All MCP information in one place
|
|
||||||
- ✅ Clear table of contents with navigation
|
|
||||||
- ✅ Consistent formatting throughout
|
|
||||||
- ✅ Easy to search with Ctrl+F
|
|
||||||
- ✅ Single file to maintain
|
|
||||||
|
|
||||||
**User Experience**:
|
|
||||||
- "I need MCP info" → Open MCP_SERVERS_GUIDE.md
|
|
||||||
- Clear, comprehensive, complete
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Where to Find MCP Information Now
|
|
||||||
|
|
||||||
### For Complete MCP Documentation
|
|
||||||
|
|
||||||
**Read**: [MCP_SERVERS_GUIDE.md](MCP_SERVERS_GUIDE.md)
|
|
||||||
|
|
||||||
**Contains**:
|
|
||||||
- Overview of all 8 servers
|
|
||||||
- Installation and setup
|
|
||||||
- Complete tool documentation for each server
|
|
||||||
- Configuration examples
|
|
||||||
- Usage patterns
|
|
||||||
- Best practices
|
|
||||||
- Troubleshooting
|
|
||||||
|
|
||||||
### For Quick Reference Template
|
|
||||||
|
|
||||||
**Read**: [.claude/agents/MCP_USAGE_TEMPLATES.md](.claude/agents/MCP_USAGE_TEMPLATES.md)
|
|
||||||
|
|
||||||
**Contains**:
|
|
||||||
- Copy-paste MCP sections for agents
|
|
||||||
- Quick usage examples
|
|
||||||
- Decision tree for memory selection
|
|
||||||
- File naming conventions
|
|
||||||
|
|
||||||
**Purpose**: Template for adding MCP usage to agent/command files
|
|
||||||
|
|
||||||
### For Setup Scripts
|
|
||||||
|
|
||||||
**Location**: `.claude/tools/`
|
|
||||||
|
|
||||||
**Files**:
|
|
||||||
- `setup-all-mcp-servers.ps1` (Windows)
|
|
||||||
- `setup-all-mcp-servers.sh` (Linux/Mac)
|
|
||||||
- `test-mcp-servers.ps1` (Test - Windows)
|
|
||||||
- `test-mcp-servers.sh` (Test - Linux/Mac)
|
|
||||||
|
|
||||||
**Quick Command**:
|
|
||||||
```powershell
|
|
||||||
# Windows
|
|
||||||
.\.claude\tools\setup-all-mcp-servers.ps1
|
|
||||||
|
|
||||||
# Linux/Mac
|
|
||||||
./.claude/tools/setup-all-mcp-servers.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### For Configuration
|
|
||||||
|
|
||||||
**Read/Edit**: `.mcp.json` (root level)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Updated References
|
|
||||||
|
|
||||||
All documentation files have been updated to reference the new consolidated guide:
|
|
||||||
|
|
||||||
### Updated Files
|
|
||||||
|
|
||||||
1. ✅ **README.md** - Added MCP_SERVERS_GUIDE.md to documentation section
|
|
||||||
2. ✅ **.claude/tools/README.md** - Updated to reference consolidated guide
|
|
||||||
3. ✅ **CLAUDE_CODE_SETUP_COMPLETE.md** - Still contains MCP section (high-level)
|
|
||||||
|
|
||||||
### Documentation Hierarchy
|
|
||||||
|
|
||||||
```
|
|
||||||
README.md
|
|
||||||
├─ Quick Overview
|
|
||||||
└─ Points to → MCP_SERVERS_GUIDE.md
|
|
||||||
|
|
||||||
MCP_SERVERS_GUIDE.md (ROOT)
|
|
||||||
└─ Complete MCP Documentation
|
|
||||||
├─ All 8 servers
|
|
||||||
├─ Setup & installation
|
|
||||||
├─ Usage guide
|
|
||||||
├─ Configuration
|
|
||||||
├─ Best practices
|
|
||||||
└─ Troubleshooting
|
|
||||||
|
|
||||||
.claude/agents/MCP_USAGE_TEMPLATES.md
|
|
||||||
└─ Quick Reference Template
|
|
||||||
└─ For agents and commands
|
|
||||||
|
|
||||||
.claude/tools/README.md
|
|
||||||
└─ Tools Directory Index
|
|
||||||
└─ Points to → MCP_SERVERS_GUIDE.md
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Quick Access Commands
|
|
||||||
|
|
||||||
### View MCP Documentation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# From root directory
|
|
||||||
cat MCP_SERVERS_GUIDE.md
|
|
||||||
|
|
||||||
# Search for specific server
|
|
||||||
grep -A 20 "### 1. Serena MCP" MCP_SERVERS_GUIDE.md
|
|
||||||
|
|
||||||
# View in editor
|
|
||||||
code MCP_SERVERS_GUIDE.md
|
|
||||||
```
|
|
||||||
|
|
||||||
### Setup MCP Servers
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Windows
|
|
||||||
.\.claude\tools\setup-all-mcp-servers.ps1
|
|
||||||
|
|
||||||
# Test installation
|
|
||||||
.\.claude\tools\test-mcp-servers.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
### Check Configuration
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# View MCP configuration
|
|
||||||
cat .mcp.json | jq .
|
|
||||||
|
|
||||||
# List available MCP servers
|
|
||||||
claude mcp list
|
|
||||||
|
|
||||||
# Test specific server
|
|
||||||
claude mcp test serena
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Migration Guide
|
|
||||||
|
|
||||||
### If You Were Using Old Files
|
|
||||||
|
|
||||||
**Before**: Reading `MCP_SETUP_TOOLS.md` or `.claude/tools/README-MCP-SETUP.md`
|
|
||||||
|
|
||||||
**Now**: Read [MCP_SERVERS_GUIDE.md](MCP_SERVERS_GUIDE.md) instead
|
|
||||||
|
|
||||||
**What Changed**:
|
|
||||||
- All content is now in MCP_SERVERS_GUIDE.md
|
|
||||||
- More comprehensive and better organized
|
|
||||||
- Includes everything from old files plus more
|
|
||||||
|
|
||||||
**Bookmarks to Update**:
|
|
||||||
- ❌ Old: `MCP_SETUP_TOOLS.md`
|
|
||||||
- ✅ New: `MCP_SERVERS_GUIDE.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Table of Contents - MCP_SERVERS_GUIDE.md
|
|
||||||
|
|
||||||
Here's what's in the new consolidated guide:
|
|
||||||
|
|
||||||
1. **Overview** - What are MCP servers and why use them
|
|
||||||
2. **Quick Start** - One-command setup and verification
|
|
||||||
3. **Installation & Setup** - Prerequisites and detailed setup
|
|
||||||
4. **Available MCP Servers** - Complete documentation for all 8 servers:
|
|
||||||
- Serena (code navigation + memory)
|
|
||||||
- Sequential Thinking (reasoning)
|
|
||||||
- Database Server (SQL)
|
|
||||||
- Context7 (documentation)
|
|
||||||
- Memory (knowledge graph)
|
|
||||||
- Fetch (web scraping)
|
|
||||||
- Windows MCP (desktop automation)
|
|
||||||
- Playwright (browser automation)
|
|
||||||
5. **Usage Guide** - Decision tree, agent patterns, command patterns
|
|
||||||
6. **Configuration** - .mcp.json, settings.json, environment variables
|
|
||||||
7. **Best Practices** - Memory management, efficiency, security
|
|
||||||
8. **Troubleshooting** - Common issues and solutions
|
|
||||||
9. **Advanced Topics** - Custom servers, performance, CI/CD
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
✅ **Created**: [MCP_SERVERS_GUIDE.md](MCP_SERVERS_GUIDE.md) - Single comprehensive guide (800+ lines)
|
|
||||||
|
|
||||||
✅ **Removed**: 3 redundant files
|
|
||||||
- MCP_SETUP_TOOLS.md
|
|
||||||
- .claude/tools/README-MCP-SETUP.md
|
|
||||||
- .claude/tools/QUICK-START.md
|
|
||||||
|
|
||||||
✅ **Updated**: 2 files to reference new guide
|
|
||||||
- README.md
|
|
||||||
- .claude/tools/README.md
|
|
||||||
|
|
||||||
✅ **Preserved**: Files still needed
|
|
||||||
- .claude/MCP_CORRECT_USAGE_GUIDE.md (quick reference template)
|
|
||||||
- Setup and test scripts
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Result
|
|
||||||
|
|
||||||
**Before**: MCP documentation spread across 4+ files
|
|
||||||
|
|
||||||
**After**: ONE comprehensive guide at root level
|
|
||||||
|
|
||||||
**User Experience**: Clear, complete, consolidated ✨
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Completed**: 2024-10-20
|
|
||||||
**Maintained by**: Claude Code Setup Project
|
|
||||||
1827
MCP_SERVERS_GUIDE.md
1827
MCP_SERVERS_GUIDE.md
File diff suppressed because it is too large
Load Diff
809
QUICKSTART.md
809
QUICKSTART.md
@@ -1,641 +1,326 @@
|
|||||||
# Claude Code Quickstart Guide
|
# Foundry VTT + Pathfinder 1e Quick Start Guide
|
||||||
|
|
||||||
> **Get started with Claude Code Setup in 5 minutes**
|
> **Get started with Foundry VTT development in 5 minutes**
|
||||||
> **Version**: 3.0.0 | **Last Updated**: 2025-10-20
|
>
|
||||||
|
> **Version**: 1.0.0 | **Last Updated**: 2025-01-30
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## What's Configured?
|
## 🚀 What You'll Do in 5 Minutes
|
||||||
|
|
||||||
This project has **Claude Code fully configured** with:
|
1. Install PF1 system dependencies (**1 min**)
|
||||||
|
2. Build the PF1 system (**2 min**)
|
||||||
✅ **8 MCP Servers** - Code navigation, memory, docs, automation
|
3. Launch Foundry VTT (**1 min**)
|
||||||
✅ **8 Specialized Agents** - Architecture, review, debug, security, testing
|
4. Create and test a macro (**1 min**)
|
||||||
✅ **9 Slash Commands** - Analyze, review, implement, test, optimize, adr
|
|
||||||
✅ **6 Output Styles** - Concise, professional, verbose, learning, explanatory, security
|
|
||||||
✅ **6 Event Hooks** - Session lifecycle, bash, file operations, stop tracking
|
|
||||||
✅ **Complete Templates** - Extend all features easily
|
|
||||||
✅ **Automatic Status Summaries** - Every response includes tool usage details
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Quick Commands
|
## Step 1: Install Dependencies (1 minute)
|
||||||
|
|
||||||
### Essential Commands
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
/help # Get help
|
# Navigate to PF1 system directory
|
||||||
/setup-info # See full configuration
|
cd src/foundryvtt-pathfinder1-v10.8
|
||||||
/cost # View token usage
|
|
||||||
/rewind # Undo changes (ESC ESC also works)
|
# Install npm packages
|
||||||
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
### Development Commands
|
✅ **Done!** Dependencies are installed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2: Build the PF1 System (2 minutes)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
/adr [list|view|create|update] # Manage Architectural Decision Records
|
# Still in src/foundryvtt-pathfinder1-v10.8
|
||||||
/analyze [path] # Comprehensive code analysis
|
|
||||||
/review [file-or-path] # Code review with best practices
|
# Production build (one-time)
|
||||||
/implement [feature] # Implement new features
|
npm run build
|
||||||
/test [file-path] # Run and analyze tests
|
|
||||||
/optimize [file] # Performance optimization
|
# OR for development (watches for changes)
|
||||||
/explain [file] # Detailed code explanation
|
npm run build:watch
|
||||||
/scaffold [type] [name] # Generate boilerplate
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Workflow Examples
|
✅ **Done!** System is built and ready.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3: Launch Foundry VTT (1 minute)
|
||||||
|
|
||||||
|
### Option A: Run Executable (Recommended)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Quick code review
|
# Navigate to Foundry directory
|
||||||
> /review src/components/
|
cd ../FoundryVTT-11.315
|
||||||
|
|
||||||
# Implement feature
|
# Run Foundry (Windows)
|
||||||
> /implement user authentication with JWT
|
.\foundryvtt.exe
|
||||||
|
|
||||||
# Run tests
|
# Or double-click foundryvtt.exe in File Explorer
|
||||||
> /test
|
```
|
||||||
|
|
||||||
# Get analysis
|
### Option B: Run via Node.js
|
||||||
> /analyze src/services/payment.ts
|
|
||||||
|
```bash
|
||||||
|
cd src/FoundryVTT-11.315
|
||||||
|
node resources/app/main.js
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ **Done!** Foundry VTT is running.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4: Test a Macro (1 minute)
|
||||||
|
|
||||||
|
### Create & Import Arcane Pool Macro
|
||||||
|
|
||||||
|
1. Open Foundry VTT in browser (usually http://localhost:30000)
|
||||||
|
2. Create or open a world
|
||||||
|
3. Click **Macro Directory** in the sidebar
|
||||||
|
4. Click **Create Macro**
|
||||||
|
5. Set **Macro Type** to "Script"
|
||||||
|
6. Copy content from `src/macro.js`
|
||||||
|
7. Click **Save Macro**
|
||||||
|
8. Drag macro to hotbar
|
||||||
|
|
||||||
|
### Test the Macro
|
||||||
|
|
||||||
|
1. Select a token on the canvas (any character)
|
||||||
|
2. Click the macro on the hotbar
|
||||||
|
3. You should see a notification or dialog
|
||||||
|
|
||||||
|
✅ **Done!** Your first macro works!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Next Steps (10-30 minutes)
|
||||||
|
|
||||||
|
### Option A: Build a Custom Macro
|
||||||
|
|
||||||
|
1. Open browser console (F12)
|
||||||
|
2. Try simple commands:
|
||||||
|
```javascript
|
||||||
|
// Get current token's actor
|
||||||
|
console.log(token.actor.name);
|
||||||
|
|
||||||
|
// Get actor's HP
|
||||||
|
console.log(token.actor.system.attributes.hp);
|
||||||
|
|
||||||
|
// Get Arcane Pool resource
|
||||||
|
console.log(token.actor.system.resources.classFeat_arcanePool);
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Create `src/macro_mytest.js`:
|
||||||
|
```javascript
|
||||||
|
(async () => {
|
||||||
|
if (!token) {
|
||||||
|
ui.notifications.warn("Select a token!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const actor = token.actor;
|
||||||
|
ui.notifications.info(`Selected: ${actor.name}`);
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Import macro in Foundry and test
|
||||||
|
|
||||||
|
### Option B: Modify the PF1 System
|
||||||
|
|
||||||
|
1. Edit a file in `src/foundryvtt-pathfinder1-v10.8/module/`
|
||||||
|
2. Run `npm run build` (already running in watch mode)
|
||||||
|
3. Reload Foundry (F5)
|
||||||
|
4. Test your change
|
||||||
|
|
||||||
|
### Option C: Explore the Code
|
||||||
|
|
||||||
|
1. Read [CLAUDE.md](CLAUDE.md) for complete documentation
|
||||||
|
2. Check out macro examples: `src/macro.js`, `src/macro_haste.js`
|
||||||
|
3. Review PF1 system structure: `src/foundryvtt-pathfinder1-v10.8/module/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Common Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build PF1 system
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Development mode (auto-rebuild)
|
||||||
|
npm run build:watch
|
||||||
|
|
||||||
|
# Lint code
|
||||||
|
npm run lint
|
||||||
|
|
||||||
|
# Format code
|
||||||
|
npm run format
|
||||||
|
|
||||||
|
# Extract compendium packs
|
||||||
|
npm run packs:extract
|
||||||
|
|
||||||
|
# Compile compendium packs
|
||||||
|
npm run packs:compile
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## MCP Servers (8 Available)
|
## 🎮 Foundry VTT Basics
|
||||||
|
|
||||||
### 🎯 Most Useful
|
### Global Objects Available in Macros
|
||||||
|
|
||||||
**Serena** - Code navigation + persistent memory
|
```javascript
|
||||||
```bash
|
game.actors // All actors
|
||||||
# Find code
|
game.items // All items
|
||||||
find_symbol("UserService")
|
game.macros // All macros
|
||||||
find_referencing_symbols("authenticate", "src/auth/")
|
game.scenes // All scenes
|
||||||
|
ui.notifications // Toast notifications
|
||||||
# Store knowledge (survives sessions)
|
canvas // Canvas/rendering
|
||||||
write_memory("adr-001-architecture", "Decision: Use microservices...")
|
|
||||||
read_memory("adr-001-architecture")
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Context7** - Real-time library docs
|
### Common Macro Pattern
|
||||||
```bash
|
|
||||||
# Get current framework documentation
|
|
||||||
resolve-library-id("react")
|
|
||||||
get-library-docs("/facebook/react")
|
|
||||||
```
|
|
||||||
|
|
||||||
**Memory Graph** - Temporary session context
|
```javascript
|
||||||
```bash
|
(async () => {
|
||||||
# Build context for current task (cleared after session)
|
// 1. Validate
|
||||||
create_entities([{name: "UserService", type: "Class"}])
|
if (!token) {
|
||||||
create_relations([{from: "UserService", to: "AuthMiddleware"}])
|
ui.notifications.warn("Select a token!");
|
||||||
```
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
### 🔧 Automation
|
// 2. Get data
|
||||||
|
const actor = token.actor;
|
||||||
|
const hp = actor.system.attributes.hp;
|
||||||
|
|
||||||
**Playwright** - Browser automation
|
// 3. Do something
|
||||||
**Windows MCP** - Desktop automation
|
await actor.update({
|
||||||
**Fetch** - Web scraping
|
"system.attributes.hp.value": hp.value - 10
|
||||||
|
});
|
||||||
|
|
||||||
### 💾 Databases
|
// 4. Feedback
|
||||||
|
ui.notifications.info("Damage applied!");
|
||||||
**Database Server** - General database queries
|
})();
|
||||||
|
|
||||||
### 🧠 Reasoning
|
|
||||||
|
|
||||||
**Sequential Thinking** - Complex problem solving with extended thinking
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Agents (8 Specialized)
|
|
||||||
|
|
||||||
### How to Use
|
|
||||||
|
|
||||||
**Automatic**: Just mention the domain
|
|
||||||
```bash
|
|
||||||
> "I need to design a microservices architecture"
|
|
||||||
# → Architect agent automatically invoked
|
|
||||||
```
|
|
||||||
|
|
||||||
**Manual**: Explicitly request
|
|
||||||
```bash
|
|
||||||
> "Use the security-analyst agent to review this code"
|
|
||||||
# → Security analyst explicitly invoked
|
|
||||||
```
|
|
||||||
|
|
||||||
### Available Agents
|
|
||||||
|
|
||||||
| Agent | Use For | Keywords |
|
|
||||||
|-------|---------|----------|
|
|
||||||
| **architect** | System design, technical planning | architecture, design, scalability |
|
|
||||||
| **code-reviewer** | Code quality, best practices | review, quality, standards |
|
|
||||||
| **debugger** | Bug diagnosis, troubleshooting | debug, error, bug, issue |
|
|
||||||
| **documentation-writer** | Technical docs, README | documentation, docs, readme |
|
|
||||||
| **project-manager** | Task breakdown, coordination | project, manage, coordinate |
|
|
||||||
| **refactoring-specialist** | Code improvement, cleanup | refactor, improve, cleanup |
|
|
||||||
| **security-analyst** | Security analysis, vulnerabilities | security, vulnerability, audit |
|
|
||||||
| **test-engineer** | Testing strategy, test generation | test, testing, coverage |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Output Styles (6 Available)
|
|
||||||
|
|
||||||
Change how Claude responds:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
/output-style concise # Brief, minimal explanation
|
|
||||||
/output-style professional # Formal, business-appropriate
|
|
||||||
/output-style verbose # Detailed, comprehensive
|
|
||||||
/output-style explanatory # Educational insights
|
|
||||||
/output-style learning # Interactive - Claude teaches YOU
|
|
||||||
/output-style security-reviewer # Security-focused analysis
|
|
||||||
/output-style default # Return to standard
|
|
||||||
```
|
|
||||||
|
|
||||||
### Quick Guide
|
|
||||||
|
|
||||||
- **Quick fixes**: Use `concise`
|
|
||||||
- **Learning**: Use `learning` or `explanatory`
|
|
||||||
- **Reports**: Use `professional`
|
|
||||||
- **Deep understanding**: Use `verbose`
|
|
||||||
- **Security work**: Use `security-reviewer`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Advanced Features
|
|
||||||
|
|
||||||
### Extended Thinking
|
|
||||||
|
|
||||||
For complex problems, use thinking keywords:
|
|
||||||
```bash
|
|
||||||
> "Think hard about the best database architecture"
|
|
||||||
> "Ultrathink: How should I optimize this algorithm?"
|
|
||||||
```
|
|
||||||
|
|
||||||
Levels: `think` → `think hard` → `think harder` → `ultrathink`
|
|
||||||
|
|
||||||
### Plan Mode
|
|
||||||
|
|
||||||
**Toggle**: Press `Tab` key
|
|
||||||
|
|
||||||
**Use**: Explore code safely before making changes
|
|
||||||
1. Enter plan mode (Tab)
|
|
||||||
2. Explore and understand
|
|
||||||
3. Exit plan mode (Tab)
|
|
||||||
4. Execute changes
|
|
||||||
|
|
||||||
### Checkpointing
|
|
||||||
|
|
||||||
**Access**: Press `ESC ESC` or `/rewind`
|
|
||||||
|
|
||||||
**Options**:
|
|
||||||
- Code only (keep conversation)
|
|
||||||
- Conversation only (keep files)
|
|
||||||
- Both (complete rollback)
|
|
||||||
|
|
||||||
**Retention**: 30 days
|
|
||||||
|
|
||||||
### Parallel Execution
|
|
||||||
|
|
||||||
Claude can run multiple operations simultaneously:
|
|
||||||
```bash
|
|
||||||
# Multiple file reads
|
|
||||||
> "Read src/auth/service.ts, src/auth/middleware.ts, and src/auth/utils.ts"
|
|
||||||
|
|
||||||
# Multiple agents
|
|
||||||
> "I need code review and security analysis"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Memory System
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
### Three Memory Types
|
### Macro Not Working?
|
||||||
|
|
||||||
**1. Project Instructions (CLAUDE.md)**
|
```javascript
|
||||||
- Team-shared project conventions
|
// Check browser console (F12)
|
||||||
- Auto-loaded every session
|
console.log(token); // Is token selected?
|
||||||
- Location: [CLAUDE.md](CLAUDE.md)
|
console.log(token.actor); // Does actor exist?
|
||||||
|
console.log(actor.system); // What data is available?
|
||||||
**2. Persistent Memory (Serena)**
|
|
||||||
- Survives across sessions
|
|
||||||
- Store ADRs, lessons, patterns
|
|
||||||
```bash
|
|
||||||
write_memory("name", "content")
|
|
||||||
read_memory("name")
|
|
||||||
list_memories()
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**3. Temporary Memory (Knowledge Graph)**
|
### Build Failed?
|
||||||
- Current session only
|
|
||||||
- Entity relationships
|
|
||||||
```bash
|
|
||||||
create_entities([...])
|
|
||||||
create_relations([...])
|
|
||||||
read_graph()
|
|
||||||
```
|
|
||||||
|
|
||||||
### When to Use What?
|
|
||||||
|
|
||||||
**Should it exist next week?**
|
|
||||||
- YES → Serena persistent memory
|
|
||||||
- NO → Knowledge graph
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Hooks (Automated Actions)
|
|
||||||
|
|
||||||
**5 hooks configured** - execute automatically:
|
|
||||||
|
|
||||||
| Hook | Trigger | Current Action |
|
|
||||||
|------|---------|----------------|
|
|
||||||
| session-start | Session begins | Create logs, log start |
|
|
||||||
| session-end | Session ends | Final logging |
|
|
||||||
| pre-bash | Before bash commands | Command logging |
|
|
||||||
| post-write | After file writes | Write logging, (auto-format optional) |
|
|
||||||
| user-prompt-submit | After prompt | Prompt tracking |
|
|
||||||
|
|
||||||
**Logs location**: `.claude/logs/`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Creating Custom Features
|
|
||||||
|
|
||||||
### Custom Command
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Copy template
|
# Clean and rebuild
|
||||||
cp .claude/commands/.COMMANDS_TEMPLATE.md .claude/commands/deploy.md
|
cd src/foundryvtt-pathfinder1-v10.8
|
||||||
|
rm -r dist node_modules
|
||||||
# 2. Edit file (add frontmatter and instructions)
|
npm install
|
||||||
---
|
npm run build
|
||||||
description: Deploy application to production
|
|
||||||
argument-hint: [environment]
|
|
||||||
allowed-tools: Bash(git *:*), Bash(npm *:*), Read(*)
|
|
||||||
---
|
|
||||||
|
|
||||||
# 3. Use it
|
|
||||||
> /deploy production
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Custom Agent
|
### Foundry Won't Start?
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Copy template
|
|
||||||
cp .claude/agents/.AGENT_TEMPLATE.md .claude/agents/api-tester.md
|
|
||||||
|
|
||||||
# 2. Configure frontmatter and instructions
|
|
||||||
---
|
|
||||||
name: api-tester
|
|
||||||
description: API testing and validation specialist
|
|
||||||
allowed-tools: Read(*), Bash(curl:*), Bash(npm test:*)
|
|
||||||
---
|
|
||||||
|
|
||||||
# 3. Use it
|
|
||||||
> "Use the api-tester agent to test our REST API"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Custom Output Style
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Copy template
|
|
||||||
cp .claude/output-styles/.OUTPUT_STYLES_TEMPLATE.md .claude/output-styles/debugging-mode.md
|
|
||||||
|
|
||||||
# 2. Define behavior
|
|
||||||
---
|
|
||||||
name: debugging-mode
|
|
||||||
description: Systematic debugging with detailed analysis
|
|
||||||
---
|
|
||||||
|
|
||||||
# 3. Activate
|
|
||||||
> /output-style debugging-mode
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Common Workflows
|
|
||||||
|
|
||||||
### Feature Development
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Architecture
|
|
||||||
> "Use architect agent to design payment integration"
|
|
||||||
|
|
||||||
# 2. Implement
|
|
||||||
> /implement Stripe payment integration
|
|
||||||
|
|
||||||
# 3. Test
|
|
||||||
> /test src/payments/
|
|
||||||
|
|
||||||
# 4. Review
|
|
||||||
> /review src/payments/
|
|
||||||
|
|
||||||
# 5. Document
|
|
||||||
> "Use documentation-writer agent to document payment flow"
|
|
||||||
|
|
||||||
# 6. Commit
|
|
||||||
> "Create git commit"
|
|
||||||
|
|
||||||
# After each step, you'll see a status summary showing:
|
|
||||||
# - What was done
|
|
||||||
# - Which agents/commands/MCP servers were used
|
|
||||||
# - Files modified
|
|
||||||
```
|
|
||||||
|
|
||||||
### Bug Fixing
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Debug
|
|
||||||
> "Use debugger agent: [paste error]"
|
|
||||||
|
|
||||||
# 2. Extended thinking (for complex bugs)
|
|
||||||
> "Think hard about this race condition"
|
|
||||||
|
|
||||||
# 3. Review fix
|
|
||||||
> /review [fixed file]
|
|
||||||
|
|
||||||
# 4. Test
|
|
||||||
> /test
|
|
||||||
```
|
|
||||||
|
|
||||||
### Code Review
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Standard review
|
|
||||||
> /review src/
|
|
||||||
|
|
||||||
# 2. Security check
|
|
||||||
> "Use security-analyst agent to check vulnerabilities"
|
|
||||||
|
|
||||||
# 3. Refactoring suggestions
|
|
||||||
> "Use refactoring-specialist agent for improvements"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Learning Codebase
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Use explanatory style
|
|
||||||
> /output-style explanatory
|
|
||||||
|
|
||||||
# 2. High-level questions
|
|
||||||
> "Explain the architecture of this project"
|
|
||||||
> "How does authentication work?"
|
|
||||||
|
|
||||||
# 3. Deep dive with Serena
|
|
||||||
> get_symbols_overview("src/core/engine.ts")
|
|
||||||
> find_symbol("Engine/initialize")
|
|
||||||
|
|
||||||
# 4. Store learnings
|
|
||||||
> write_memory("architecture-overview", "The system uses...")
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## File Shortcuts
|
|
||||||
|
|
||||||
### Reference Files
|
|
||||||
|
|
||||||
Use `@` to include files in prompts:
|
|
||||||
```bash
|
|
||||||
> "Review @src/auth/service.ts"
|
|
||||||
> "Explain @src/utils/*.ts"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Import in CLAUDE.md
|
|
||||||
|
|
||||||
Import additional context:
|
|
||||||
```markdown
|
|
||||||
@docs/architecture.md
|
|
||||||
@docs/coding-standards.md
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Configuration Quick Reference
|
|
||||||
|
|
||||||
### Key Files
|
|
||||||
|
|
||||||
| File | Purpose |
|
|
||||||
|------|---------|
|
|
||||||
| `.claude/settings.json` | Main configuration (shared) |
|
|
||||||
| `.claude/settings.local.json` | Personal config (not in git) |
|
|
||||||
| `.mcp.json` | MCP servers |
|
|
||||||
| `CLAUDE.md` | Project instructions |
|
|
||||||
| `.claude/agents/*.md` | Specialized agents |
|
|
||||||
| `.claude/commands/*.md` | Slash commands |
|
|
||||||
| `.claude/output-styles/*.md` | Response styles |
|
|
||||||
| `.claude/hooks/*.sh` | Automation scripts |
|
|
||||||
|
|
||||||
### Permissions
|
|
||||||
|
|
||||||
**Location**: `.claude/settings.json` → `permissions`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"allowed": ["Read(*)", "Write(*)", "Bash(git *:*)"],
|
|
||||||
"ask": ["Bash(npm install:*)"],
|
|
||||||
"denied": ["Bash(rm -rf /:*)"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Keyboard Shortcuts
|
|
||||||
|
|
||||||
- `Tab` - Toggle plan mode
|
|
||||||
- `ESC ESC` - Access checkpoints
|
|
||||||
- `Ctrl+C` - Interrupt Claude
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Agent Not Working
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check it exists
|
|
||||||
ls .claude/agents/
|
|
||||||
|
|
||||||
# Check description has keywords
|
|
||||||
cat .claude/agents/[name].md
|
|
||||||
|
|
||||||
# Try manual invocation
|
|
||||||
> "Use the [agent-name] agent to..."
|
|
||||||
|
|
||||||
# Restart Claude
|
|
||||||
```
|
|
||||||
|
|
||||||
### Command Not Found
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check it exists
|
|
||||||
ls .claude/commands/
|
|
||||||
|
|
||||||
# List available
|
|
||||||
> /help
|
|
||||||
|
|
||||||
# Restart Claude
|
|
||||||
```
|
|
||||||
|
|
||||||
### MCP Server Failed
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check configuration
|
|
||||||
cat .mcp.json | jq '.mcpServers'
|
|
||||||
|
|
||||||
# Test command manually
|
|
||||||
npx -y @modelcontextprotocol/server-sequential-thinking
|
|
||||||
|
|
||||||
# Check logs
|
# Check logs
|
||||||
cat .claude/logs/session.log
|
cat src/FoundryVTT-11.315/Data/Logs/foundry.log
|
||||||
|
|
||||||
|
# Try Node.js directly
|
||||||
|
cd src/FoundryVTT-11.315
|
||||||
|
node resources/app/main.js
|
||||||
```
|
```
|
||||||
|
|
||||||
### Permission Denied
|
### Port Already in Use?
|
||||||
|
|
||||||
|
Foundry defaults to port 30000. If in use:
|
||||||
```bash
|
```bash
|
||||||
# Check permissions
|
# Find process using port
|
||||||
cat .claude/settings.json | jq '.permissions'
|
netstat -ano | findstr :30000
|
||||||
|
|
||||||
# Add to allowed
|
# Kill process (Windows)
|
||||||
# Edit settings.json → permissions → allowed array
|
taskkill /PID <PID> /F
|
||||||
|
|
||||||
# Restart Claude
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Tips & Tricks
|
## 📖 Documentation
|
||||||
|
|
||||||
### 🚀 Performance
|
| Document | Purpose | Read Time |
|
||||||
|
|----------|---------|-----------|
|
||||||
1. **Use concise style** for quick tasks
|
| **[README.md](README.md)** | Project overview & setup | 10 min |
|
||||||
2. **Parallel operations** when possible
|
| **[CLAUDE.md](CLAUDE.md)** | Complete technical documentation | 30 min |
|
||||||
3. **Serena symbol tools** instead of full file reads
|
| **Foundry API** | https://foundryvtt.com/api/v11/ | Reference |
|
||||||
4. **Extended thinking** only for complex problems
|
| **PF1 GitHub** | https://github.com/Furyspark/foundryvtt-pathfinder1 | Reference |
|
||||||
|
|
||||||
### 🎯 Effectiveness
|
|
||||||
|
|
||||||
1. **Start broad, then narrow** - high-level first, details later
|
|
||||||
2. **Use appropriate tools** - agents for domains, commands for workflows
|
|
||||||
3. **Leverage memory** - store ADRs, lessons, patterns
|
|
||||||
4. **Reference files** with `@` syntax
|
|
||||||
|
|
||||||
### 🔐 Security
|
|
||||||
|
|
||||||
1. **Review hooks** before using
|
|
||||||
2. **Restrict sensitive tools** in permissions
|
|
||||||
3. **Use security-analyst** for audits
|
|
||||||
4. **Never commit secrets** to CLAUDE.md
|
|
||||||
|
|
||||||
### 📈 Learning
|
|
||||||
|
|
||||||
1. **Use explanatory style** for understanding
|
|
||||||
2. **Extended thinking** for complex topics
|
|
||||||
3. **Store learnings** in Serena memory
|
|
||||||
4. **Learning style** for hands-on practice
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Next Steps
|
## 🎯 Success Checklist
|
||||||
|
|
||||||
### New Users
|
After this quickstart, you should be able to:
|
||||||
|
|
||||||
1. Try basic commands: `/help`, `/setup-info`
|
- [x] Install and build PF1 system
|
||||||
2. Experiment with agents: "Use the [agent] agent to..."
|
- [x] Launch Foundry VTT
|
||||||
3. Try output styles: `/output-style learning`
|
- [x] Create a macro
|
||||||
4. Create your first custom command
|
- [x] Execute a macro in-game
|
||||||
|
- [x] Access browser console (F12)
|
||||||
### Experienced Users
|
- [x] Modify macro code
|
||||||
|
- [x] Understand basic actor/item API
|
||||||
1. Set up personal `.claude/settings.local.json`
|
|
||||||
2. Create project-specific agents
|
|
||||||
3. Configure hooks for your workflow
|
|
||||||
4. Leverage MCP servers fully
|
|
||||||
|
|
||||||
### Team Setup
|
|
||||||
|
|
||||||
1. Review and customize `CLAUDE.md`
|
|
||||||
2. Add team-specific commands
|
|
||||||
3. Configure permissions
|
|
||||||
4. Share setup via git
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Resources
|
## 🎓 Learn More
|
||||||
|
|
||||||
### Documentation
|
### Beginner Topics
|
||||||
|
- Creating your first macro
|
||||||
|
- Understanding actor/item structure
|
||||||
|
- Using the browser console
|
||||||
|
|
||||||
- **Complete Setup**: [CLAUDE_CODE_SETUP_COMPLETE.md](CLAUDE_CODE_SETUP_COMPLETE.md) - Full documentation
|
### Intermediate Topics
|
||||||
- **Templates Guide**: [.claude/TEMPLATES_README.md](.claude/TEMPLATES_README.md) - Template details
|
- Dialog-based user interfaces
|
||||||
- **MCP Servers**: [MCP_SERVERS_GUIDE.md](MCP_SERVERS_GUIDE.md) - Complete MCP documentation
|
- Buff management systems
|
||||||
- **MCP Templates**: [.claude/agents/MCP_USAGE_TEMPLATES.md](.claude/agents/MCP_USAGE_TEMPLATES.md) - Copy-paste templates for agents
|
- Macro chaining
|
||||||
- **Official Docs**: https://docs.claude.com/en/docs/claude-code/
|
|
||||||
|
|
||||||
### Templates
|
### Advanced Topics
|
||||||
|
- Modifying PF1 system code
|
||||||
|
- Adding compendium content
|
||||||
|
- Integrating Claude Code automation
|
||||||
|
|
||||||
- **Agent**: [.claude/agents/.AGENT_TEMPLATE.md](.claude/agents/.AGENT_TEMPLATE.md)
|
See [CLAUDE.md](CLAUDE.md) for detailed tutorials on all topics.
|
||||||
- **Command**: [.claude/commands/.COMMANDS_TEMPLATE.md](.claude/commands/.COMMANDS_TEMPLATE.md)
|
|
||||||
- **Skill**: [.claude/skills/.SKILL_TEMPLATE.md](.claude/skills/.SKILL_TEMPLATE.md)
|
|
||||||
- **Output Style**: [.claude/output-styles/.OUTPUT_STYLES_TEMPLATE.md](.claude/output-styles/.OUTPUT_STYLES_TEMPLATE.md)
|
|
||||||
- **Project**: [CLAUDE_TEMPLATE.md](CLAUDE_TEMPLATE.md)
|
|
||||||
|
|
||||||
### Get Help
|
|
||||||
|
|
||||||
1. `/help` - Built-in help
|
|
||||||
2. `/setup-info` - Configuration details
|
|
||||||
3. GitHub Issues: https://github.com/anthropics/claude-code/issues
|
|
||||||
4. Official Docs: https://docs.claude.com/en/docs/claude-code/
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Cheat Sheet
|
## 💡 Quick Tips
|
||||||
|
|
||||||
```bash
|
1. **Always select a token** - Most macros require a token to be selected
|
||||||
# Development
|
2. **Use F12 for debugging** - Browser console shows errors and logs
|
||||||
/adr [action] [id] # Manage ADRs
|
3. **F5 reloads Foundry** - Refresh after system changes
|
||||||
/analyze [path] # Code analysis
|
4. **Check notification** - Macros often provide user feedback via notifications
|
||||||
/review [path] # Code review
|
5. **Save often** - Macros are stored in Foundry's database
|
||||||
/implement [feature] # Feature implementation
|
|
||||||
/test [file] # Run tests
|
|
||||||
/optimize [file] # Optimize performance
|
|
||||||
/explain [file] # Explain code
|
|
||||||
|
|
||||||
# Agents (automatic or manual)
|
|
||||||
architect # System design
|
|
||||||
code-reviewer # Code review
|
|
||||||
debugger # Bug fixing
|
|
||||||
documentation-writer # Docs
|
|
||||||
security-analyst # Security
|
|
||||||
test-engineer # Testing
|
|
||||||
|
|
||||||
# Output Styles
|
|
||||||
/output-style concise # Brief
|
|
||||||
/output-style learning # Interactive
|
|
||||||
/output-style explanatory # Educational
|
|
||||||
/output-style security-reviewer # Security-focused
|
|
||||||
|
|
||||||
# Memory
|
|
||||||
write_memory(name, content) # Save (persistent)
|
|
||||||
read_memory(name) # Load (persistent)
|
|
||||||
create_entities([...]) # Build context (temporary)
|
|
||||||
|
|
||||||
# Extended Thinking
|
|
||||||
think / think hard / ultrathink
|
|
||||||
|
|
||||||
# Shortcuts
|
|
||||||
Tab # Plan mode toggle
|
|
||||||
ESC ESC # Checkpoints
|
|
||||||
@file # Reference file
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Version**: 3.0.0 | **Last Updated**: 2025-10-20
|
## 🆘 Need Help?
|
||||||
|
|
||||||
**Ready to start?** Run `claude` and try:
|
1. **Check [CLAUDE.md](CLAUDE.md)** - Complete documentation
|
||||||
```bash
|
2. **Review examples** - `src/macro.js`, `src/macro_haste.js`
|
||||||
> /setup-info
|
3. **Browser console (F12)** - Error messages and debugging
|
||||||
> "Use the architect agent to explain the project structure"
|
4. **Foundry logs** - `src/FoundryVTT-11.315/Data/Logs/foundry.log`
|
||||||
> /output-style learning
|
|
||||||
```
|
|
||||||
|
|
||||||
For complete documentation, see [CLAUDE_CODE_SETUP_COMPLETE.md](CLAUDE_CODE_SETUP_COMPLETE.md)
|
---
|
||||||
|
|
||||||
|
## ✅ Ready to Continue?
|
||||||
|
|
||||||
|
**Next steps:**
|
||||||
|
1. Try modifying the Arcane Pool macro
|
||||||
|
2. Create your own custom macro
|
||||||
|
3. Read [CLAUDE.md](CLAUDE.md) for advanced topics
|
||||||
|
4. Explore PF1 system code
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Questions?** Check [README.md](README.md) or [CLAUDE.md](CLAUDE.md)
|
||||||
|
|||||||
880
README.md
880
README.md
@@ -1,539 +1,383 @@
|
|||||||
# Claude Code Setup - Complete Claude Code Configuration
|
# Foundry VTT + Pathfinder 1e Development Environment
|
||||||
|
|
||||||
> **Production-ready Claude Code setup with comprehensive documentation**
|
> **A complete development environment for creating custom macros and automating Foundry VTT with Pathfinder 1e**
|
||||||
> **Version**: 3.0.0 | **Last Updated**: 2025-10-20
|
>
|
||||||
|
> **Version**: 1.0.0 | **Last Updated**: 2025-01-30 | **Repository**: [Gitea](https://gitea.gowlershome.dyndns.org/Gowler/FoundryVTT.git)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚀 Quick Start
|
## 🎯 Quick Start
|
||||||
|
|
||||||
**New to this project?** Start here: **[QUICKSTART.md](QUICKSTART.md)** (5-minute read)
|
**New to this project?** Start here: **[QUICKSTART.md](QUICKSTART.md)** (5-minute read)
|
||||||
|
|
||||||
**Need complete details?** See: **[CLAUDE_CODE_SETUP_COMPLETE.md](CLAUDE_CODE_SETUP_COMPLETE.md)** (comprehensive)
|
**Need complete details?** See: **[CLAUDE.md](CLAUDE.md)** (comprehensive project guide)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📋 What's Included
|
## 📋 What This Project Includes
|
||||||
|
|
||||||
This project demonstrates a **fully configured Claude Code setup** with:
|
This is a professional development environment for Foundry VTT + Pathfinder 1e that combines:
|
||||||
|
|
||||||
### Core Features (100% Configured)
|
### Core Components
|
||||||
|
|
||||||
✅ **8 MCP Servers**
|
✅ **Foundry VTT v11.315**
|
||||||
- Serena (code navigation + persistent memory)
|
- Complete Electron-based virtual tabletop platform
|
||||||
- Sequential Thinking (complex reasoning)
|
- Express.js server backend with NeDB database
|
||||||
- Context7 (real-time library docs)
|
- PIXI.js canvas rendering system
|
||||||
- Memory (knowledge graph)
|
- Socket.io real-time multiplayer support
|
||||||
- Playwright (browser automation)
|
|
||||||
- Windows MCP (desktop automation)
|
|
||||||
- Fetch (web scraping)
|
|
||||||
- Database Server
|
|
||||||
|
|
||||||
✅ **8 Specialized Agents**
|
✅ **Pathfinder 1e System v10.8**
|
||||||
- Architect, Code Reviewer, Debugger
|
- Full PF1 game rules implementation
|
||||||
- Documentation Writer, Project Manager
|
- Character sheets and NPC templates
|
||||||
- Refactoring Specialist, Security Analyst, Test Engineer
|
- Spell and feat compendiums
|
||||||
|
- Automated action resolution system
|
||||||
|
- Built with Vite + ES Modules + TypeScript
|
||||||
|
|
||||||
✅ **9 Slash Commands**
|
✅ **Custom Macros & Automation**
|
||||||
- `/analyze`, `/review`, `/implement`, `/test`
|
- Arcane Pool enhancement system (Magus class feature)
|
||||||
- `/optimize`, `/explain`, `/scaffold`, `/setup-info`, `/adr`
|
- Haste buff automation with attack interception
|
||||||
|
- Reusable macro patterns and templates
|
||||||
|
- Dialog-based user interfaces
|
||||||
|
|
||||||
✅ **6 Output Styles**
|
✅ **AI-Assisted Development**
|
||||||
- Concise, Professional, Verbose
|
- Claude Code integration with 8 MCP servers
|
||||||
- Explanatory, Learning, Security Reviewer
|
- Semantic code navigation (Serena)
|
||||||
|
- Complex problem solving (Sequential Thinking)
|
||||||
✅ **6 Event Hooks**
|
- Real-time library documentation (Context7)
|
||||||
- Session lifecycle (start/end)
|
- Persistent project memory (Knowledge Graph)
|
||||||
- Bash command interception
|
|
||||||
- File write logging
|
|
||||||
- User prompt tracking
|
|
||||||
- Stop tracking for summaries
|
|
||||||
|
|
||||||
✅ **Complete Templates**
|
|
||||||
- Agent Template, Command Template
|
|
||||||
- Skill Template, Output Style Template
|
|
||||||
- CLAUDE.md Project Template
|
|
||||||
|
|
||||||
✅ **Automatic Status Summaries**
|
|
||||||
- Every task completion includes detailed summary
|
|
||||||
- Shows agents, commands, MCP servers used
|
|
||||||
- Lists all files modified
|
|
||||||
- Promotes transparency and learning
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📚 Documentation
|
## 🚀 Getting Started
|
||||||
|
|
||||||
### For Users
|
### 1. Prerequisites
|
||||||
|
|
||||||
| Document | Purpose | Read Time |
|
- **Foundry VTT License** (required to download official builds)
|
||||||
|----------|---------|-----------|
|
- **Node.js** v16-19 (for PF1 system development)
|
||||||
| **[QUICKSTART.md](QUICKSTART.md)** | Get started quickly | 5 min |
|
- **npm** (Node Package Manager)
|
||||||
| **[CLAUDE_CODE_SETUP_COMPLETE.md](CLAUDE_CODE_SETUP_COMPLETE.md)** | Complete documentation | 30 min |
|
- **Git** (version control)
|
||||||
| **[MCP_SERVERS_GUIDE.md](MCP_SERVERS_GUIDE.md)** | Complete MCP server documentation | 20 min |
|
- **Visual Studio Code** (recommended IDE)
|
||||||
| **[CLAUDE.md](CLAUDE.md)** | Project instructions for Claude | Reference |
|
|
||||||
|
|
||||||
### For Developers
|
### 2. Initial Setup
|
||||||
|
|
||||||
| Document | Purpose |
|
|
||||||
|----------|---------|
|
|
||||||
| **[CLAUDE_TEMPLATE.md](CLAUDE_TEMPLATE.md)** | Template for new projects |
|
|
||||||
| **[.claude/TEMPLATES_README.md](.claude/TEMPLATES_README.md)** | Master template guide |
|
|
||||||
| **[.claude/agents/MCP_USAGE_TEMPLATES.md](.claude/agents/MCP_USAGE_TEMPLATES.md)** | MCP usage templates for agents |
|
|
||||||
| **[.claude/TEMPLATE_CAPABILITIES_ANALYSIS.md](.claude/TEMPLATE_CAPABILITIES_ANALYSIS.md)** | Template review & missing features |
|
|
||||||
|
|
||||||
### Specialized Guides
|
|
||||||
|
|
||||||
| Document | Purpose |
|
|
||||||
|----------|---------|
|
|
||||||
| **[MCP_SERVERS_GUIDE.md](MCP_SERVERS_GUIDE.md)** | Complete MCP server documentation |
|
|
||||||
| **[.claude/CHECKPOINTING_GUIDE.md](.claude/CHECKPOINTING_GUIDE.md)** | Session checkpointing |
|
|
||||||
| **[.claude/PLUGIN_SETUP.md](.claude/PLUGIN_SETUP.md)** | Plugin marketplace |
|
|
||||||
| **[.claude/STATUS_LINE_SETUP.md](.claude/STATUS_LINE_SETUP.md)** | Status line customization |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Key Capabilities
|
|
||||||
|
|
||||||
### Intelligent Code Navigation (Serena MCP)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Find symbols
|
# Navigate to project root
|
||||||
find_symbol("UserService")
|
cd "C:\DEV\Foundry2\Foundry_VTT"
|
||||||
|
|
||||||
# Find references
|
# Install PF1 system dependencies
|
||||||
find_referencing_symbols("authenticate", "src/auth/")
|
cd src/foundryvtt-pathfinder1-v10.8
|
||||||
|
npm install
|
||||||
|
|
||||||
# Store persistent knowledge
|
# Build the PF1 system
|
||||||
write_memory("adr-001-architecture", "Decision: Microservices...")
|
npm run build
|
||||||
|
|
||||||
|
# Or use watch mode for development
|
||||||
|
npm run build:watch
|
||||||
```
|
```
|
||||||
|
|
||||||
### Specialized Agents (8 Available)
|
### 3. Running Foundry VTT
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Automatic invocation
|
# Option 1: Run Foundry directly
|
||||||
> "I need to design a microservices architecture"
|
cd src/FoundryVTT-11.315
|
||||||
# → Architect agent activated
|
.\foundryvtt.exe
|
||||||
|
|
||||||
# Manual invocation
|
# Option 2: Launch via Node.js
|
||||||
> "Use the security-analyst agent to review this code"
|
node resources/app/main.js
|
||||||
```
|
```
|
||||||
|
|
||||||
### Development Workflows (8 Commands)
|
### 4. Creating & Using Macros
|
||||||
|
|
||||||
```bash
|
1. Open Foundry VTT
|
||||||
/analyze src/ # Code analysis
|
2. Click "Macro Directory" in the sidebar
|
||||||
/review src/ # Code review
|
3. Create a new macro (type: Script)
|
||||||
/implement feature # Build features
|
4. Copy content from `src/macro.js` or `src/macro_haste.js`
|
||||||
/test # Run tests
|
5. Save and drag to hotbar
|
||||||
/optimize file.ts # Performance tuning
|
|
||||||
```
|
|
||||||
|
|
||||||
### Extended Thinking
|
|
||||||
|
|
||||||
```bash
|
|
||||||
> "Think hard about the best database architecture"
|
|
||||||
> "Ultrathink: How to optimize this algorithm?"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Custom Output Modes (6 Styles)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
/output-style learning # Interactive learning
|
|
||||||
/output-style security-reviewer # Security focus
|
|
||||||
/output-style explanatory # Educational insights
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📁 Project Structure
|
## 📁 Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
Claude Code Setup/
|
Foundry_VTT/
|
||||||
|
├── src/
|
||||||
|
│ ├── FoundryVTT-11.315/ # Foundry core application
|
||||||
|
│ │ ├── resources/app/ # Electron app resources
|
||||||
|
│ │ ├── node_modules/ # Foundry dependencies
|
||||||
|
│ │ └── Data/ # User worlds & data (gitignored)
|
||||||
|
│ │
|
||||||
|
│ ├── foundryvtt-pathfinder1-v10.8/# PF1 system module
|
||||||
|
│ │ ├── module/ # System source code (ES modules)
|
||||||
|
│ │ ├── packs/ # Compendium databases
|
||||||
|
│ │ ├── public/system.json # System manifest
|
||||||
|
│ │ ├── package.json # Dev dependencies & scripts
|
||||||
|
│ │ └── vite.config.js # Build configuration
|
||||||
|
│ │
|
||||||
|
│ ├── macro.js # Arcane Pool macro
|
||||||
|
│ ├── macro_haste.js # Haste automation macro
|
||||||
|
│ └── zeratal.json # Example character data
|
||||||
|
│
|
||||||
|
├── CLAUDE.md # Complete project documentation
|
||||||
|
├── QUICKSTART.md # Quick start guide
|
||||||
├── README.md # This file
|
├── README.md # This file
|
||||||
├── QUICKSTART.md # Quick start guide (5 min)
|
├── .claude/ # Claude Code configuration
|
||||||
├── CLAUDE_CODE_SETUP_COMPLETE.md # Complete documentation (30 min)
|
|
||||||
├── CLAUDE.md # Project instructions
|
|
||||||
├── CLAUDE_TEMPLATE.md # Project template
|
|
||||||
│
|
|
||||||
├── .mcp.json # MCP servers configuration
|
├── .mcp.json # MCP servers configuration
|
||||||
│
|
└── .git/ # Git repository
|
||||||
└── .claude/ # Main configuration directory
|
|
||||||
├── settings.json # Shared configuration
|
|
||||||
├── settings.local.json # Personal configuration
|
|
||||||
│
|
|
||||||
├── agents/ # 8 specialized agents
|
|
||||||
│ ├── architect.md
|
|
||||||
│ ├── code-reviewer.md
|
|
||||||
│ ├── debugger.md
|
|
||||||
│ ├── documentation-writer.md
|
|
||||||
│ ├── project-manager.md
|
|
||||||
│ ├── refactoring-specialist.md
|
|
||||||
│ ├── security-analyst.md
|
|
||||||
│ ├── test-engineer.md
|
|
||||||
│ └── .AGENT_TEMPLATE.md
|
|
||||||
│
|
|
||||||
├── commands/ # 9 slash commands
|
|
||||||
│ ├── adr.md
|
|
||||||
│ ├── analyze.md
|
|
||||||
│ ├── explain.md
|
|
||||||
│ ├── implement.md
|
|
||||||
│ ├── optimize.md
|
|
||||||
│ ├── review.md
|
|
||||||
│ ├── scaffold.md
|
|
||||||
│ ├── setup-info.md
|
|
||||||
│ ├── test.md
|
|
||||||
│ └── .COMMANDS_TEMPLATE.md
|
|
||||||
│
|
|
||||||
├── output-styles/ # 6 custom styles
|
|
||||||
│ ├── concise.md
|
|
||||||
│ ├── professional.md
|
|
||||||
│ ├── verbose.md
|
|
||||||
│ ├── explanatory.md
|
|
||||||
│ ├── learning.md
|
|
||||||
│ ├── security-reviewer.md
|
|
||||||
│ └── .OUTPUT_STYLES_TEMPLATE.md
|
|
||||||
│
|
|
||||||
├── skills/ # Skills directory
|
|
||||||
│ └── .SKILL_TEMPLATE.md
|
|
||||||
│
|
|
||||||
├── hooks/ # 5 event hooks
|
|
||||||
│ ├── session-start.sh
|
|
||||||
│ ├── session-end.sh
|
|
||||||
│ ├── pre-bash.sh
|
|
||||||
│ ├── post-write.sh
|
|
||||||
│ └── user-prompt-submit.sh
|
|
||||||
│
|
|
||||||
├── tools/ # Utility scripts
|
|
||||||
│ ├── start-memory.ps1
|
|
||||||
│ └── statusline.sh
|
|
||||||
│
|
|
||||||
├── logs/ # Session logs
|
|
||||||
│
|
|
||||||
└── [Documentation Files]
|
|
||||||
├── TEMPLATES_README.md
|
|
||||||
├── MCP_CORRECT_USAGE_GUIDE.md
|
|
||||||
├── TEMPLATE_CAPABILITIES_ANALYSIS.md
|
|
||||||
├── CHECKPOINTING_GUIDE.md
|
|
||||||
├── PLUGIN_SETUP.md
|
|
||||||
└── STATUS_LINE_SETUP.md
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**See [CLAUDE.md](CLAUDE.md) for detailed project structure and file descriptions.**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚦 Getting Started
|
## 🛠️ Development Workflow
|
||||||
|
|
||||||
### 1. First Time Setup (1 minute)
|
### Building the PF1 System
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone or open this project
|
cd src/foundryvtt-pathfinder1-v10.8
|
||||||
cd "Claude Code Setup"
|
|
||||||
|
|
||||||
# Start Claude Code
|
# Production build
|
||||||
claude
|
npm run build
|
||||||
|
|
||||||
# Verify setup
|
# Development build with hot reload
|
||||||
> /setup-info
|
npm run build:watch
|
||||||
|
|
||||||
|
# Lint code
|
||||||
|
npm run lint
|
||||||
|
|
||||||
|
# Format code
|
||||||
|
npm run format
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Try Basic Commands (2 minutes)
|
### Creating a New Macro
|
||||||
|
|
||||||
```bash
|
All macros follow the IIFE (Immediately Invoked Function Expression) pattern:
|
||||||
# Get help
|
|
||||||
> /help
|
|
||||||
|
|
||||||
# See what's configured
|
```javascript
|
||||||
> /setup-info
|
(async () => {
|
||||||
|
// 1. Validation
|
||||||
# Try a command
|
if (!token) {
|
||||||
> /analyze src/
|
ui.notifications.warn("You must select a token!");
|
||||||
|
return;
|
||||||
# Try an agent
|
|
||||||
> "Use the architect agent to explain the project structure"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Learn Core Features (5 minutes)
|
|
||||||
|
|
||||||
Read **[QUICKSTART.md](QUICKSTART.md)** for:
|
|
||||||
- Essential commands
|
|
||||||
- MCP server usage
|
|
||||||
- Agent invocation
|
|
||||||
- Output styles
|
|
||||||
- Memory system
|
|
||||||
|
|
||||||
### 4. Deep Dive (30 minutes)
|
|
||||||
|
|
||||||
Read **[CLAUDE_CODE_SETUP_COMPLETE.md](CLAUDE_CODE_SETUP_COMPLETE.md)** for complete details on:
|
|
||||||
- All MCP servers and capabilities
|
|
||||||
- Agent configuration and usage
|
|
||||||
- Slash command reference
|
|
||||||
- Hooks system
|
|
||||||
- Templates
|
|
||||||
- Advanced features
|
|
||||||
- Best practices
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💡 Common Use Cases
|
|
||||||
|
|
||||||
### Feature Development
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Architecture
|
|
||||||
> "Use architect agent to design payment integration"
|
|
||||||
|
|
||||||
# 2. Implement
|
|
||||||
> /implement Stripe payment integration
|
|
||||||
|
|
||||||
# 3. Test
|
|
||||||
> /test src/payments/
|
|
||||||
|
|
||||||
# 4. Review
|
|
||||||
> /review src/payments/
|
|
||||||
|
|
||||||
# 5. Document
|
|
||||||
> "Use documentation-writer agent to document payment flow"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Bug Fixing
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Debug
|
|
||||||
> "Use debugger agent: [paste error]"
|
|
||||||
|
|
||||||
# 2. Extended thinking (for complex bugs)
|
|
||||||
> "Think hard about this race condition"
|
|
||||||
|
|
||||||
# 3. Review fix
|
|
||||||
> /review [fixed file]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Code Review
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Standard review
|
|
||||||
> /review src/
|
|
||||||
|
|
||||||
# 2. Security check
|
|
||||||
> "Use security-analyst agent to check vulnerabilities"
|
|
||||||
|
|
||||||
# 3. Refactoring suggestions
|
|
||||||
> "Use refactoring-specialist agent for improvements"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Learning Codebase
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Use explanatory style
|
|
||||||
> /output-style explanatory
|
|
||||||
|
|
||||||
# 2. High-level questions
|
|
||||||
> "Explain the architecture of this project"
|
|
||||||
|
|
||||||
# 3. Deep dive with Serena
|
|
||||||
> get_symbols_overview("src/core/engine.ts")
|
|
||||||
|
|
||||||
# 4. Store learnings
|
|
||||||
> write_memory("architecture-overview", "The system uses...")
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎓 Learning Path
|
|
||||||
|
|
||||||
### Beginner (Day 1)
|
|
||||||
|
|
||||||
1. Read [QUICKSTART.md](QUICKSTART.md)
|
|
||||||
2. Try basic commands (`/help`, `/setup-info`)
|
|
||||||
3. Experiment with one agent
|
|
||||||
4. Try one output style
|
|
||||||
|
|
||||||
### Intermediate (Week 1)
|
|
||||||
|
|
||||||
1. Use all slash commands
|
|
||||||
2. Work with multiple agents
|
|
||||||
3. Try extended thinking
|
|
||||||
4. Use Serena MCP for code navigation
|
|
||||||
|
|
||||||
### Advanced (Month 1)
|
|
||||||
|
|
||||||
1. Create custom commands
|
|
||||||
2. Build custom agents
|
|
||||||
3. Configure hooks for your workflow
|
|
||||||
4. Leverage all MCP servers
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 Customization
|
|
||||||
|
|
||||||
### Add a Custom Command
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Copy template
|
|
||||||
cp .claude/commands/.COMMANDS_TEMPLATE.md .claude/commands/my-command.md
|
|
||||||
|
|
||||||
# Edit and configure
|
|
||||||
# Use it
|
|
||||||
> /my-command
|
|
||||||
```
|
|
||||||
|
|
||||||
### Add a Custom Agent
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Copy template
|
|
||||||
cp .claude/agents/.AGENT_TEMPLATE.md .claude/agents/my-agent.md
|
|
||||||
|
|
||||||
# Configure and use
|
|
||||||
> "Use the my-agent agent to..."
|
|
||||||
```
|
|
||||||
|
|
||||||
### Add a Custom Output Style
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Copy template
|
|
||||||
cp .claude/output-styles/.OUTPUT_STYLES_TEMPLATE.md .claude/output-styles/my-style.md
|
|
||||||
|
|
||||||
# Activate
|
|
||||||
> /output-style my-style
|
|
||||||
```
|
|
||||||
|
|
||||||
See [.claude/TEMPLATES_README.md](.claude/TEMPLATES_README.md) for detailed guides.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🤝 Team Setup
|
|
||||||
|
|
||||||
### Sharing This Setup
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Everything is in git, just commit and push
|
|
||||||
git add .claude/ .mcp.json CLAUDE.md
|
|
||||||
git commit -m "Add Claude Code configuration"
|
|
||||||
git push
|
|
||||||
|
|
||||||
# Team members pull and get full setup
|
|
||||||
git pull
|
|
||||||
```
|
|
||||||
|
|
||||||
### Team Customization
|
|
||||||
|
|
||||||
**Shared (in git)**:
|
|
||||||
- `.claude/settings.json`
|
|
||||||
- `.claude/agents/`
|
|
||||||
- `.claude/commands/`
|
|
||||||
- `.mcp.json`
|
|
||||||
- `CLAUDE.md`
|
|
||||||
|
|
||||||
**Personal (not in git)**:
|
|
||||||
- `.claude/settings.local.json`
|
|
||||||
- `~/.claude/` (user-wide)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 Setup Completeness
|
|
||||||
|
|
||||||
**Current**: ~95% of Claude Code capabilities implemented
|
|
||||||
|
|
||||||
**Implemented**:
|
|
||||||
- ✅ MCP servers
|
|
||||||
- ✅ Agents/Subagents
|
|
||||||
- ✅ Slash commands
|
|
||||||
- ✅ Output styles
|
|
||||||
- ✅ Hooks
|
|
||||||
- ✅ Templates
|
|
||||||
- ✅ Memory system
|
|
||||||
- ✅ Extended thinking
|
|
||||||
- ✅ Plan mode
|
|
||||||
- ✅ Checkpointing
|
|
||||||
- ✅ Plugin marketplace
|
|
||||||
|
|
||||||
**Optional (not implemented)**:
|
|
||||||
- ⚠️ Enterprise SSO/Analytics
|
|
||||||
- ⚠️ Headless mode (CI/CD)
|
|
||||||
- ⚠️ Network proxies/certificates
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🖥️ Platform Notes
|
|
||||||
|
|
||||||
### Windows-Only Components
|
|
||||||
|
|
||||||
The following components are **Windows-specific** and will not work on Mac/Linux:
|
|
||||||
|
|
||||||
- **Windows MCP** ([.windows-mcp/](.windows-mcp/)) - Desktop automation using Python/uv
|
|
||||||
- Provides Windows UI automation, PowerShell execution, clipboard operations
|
|
||||||
- **Impact on other platforms**: Will show error on startup but won't affect other MCP servers
|
|
||||||
- **Solution for Mac/Linux**: Ignore Windows MCP errors or disable in `.mcp.json`
|
|
||||||
|
|
||||||
- **PowerShell Scripts**:
|
|
||||||
- [.claude/tools/start-memory.ps1](.claude/tools/start-memory.ps1) - Memory MCP launcher
|
|
||||||
- Various hook scripts using PowerShell commands
|
|
||||||
- **Alternative**: Create bash equivalents for cross-platform support
|
|
||||||
|
|
||||||
### Cross-Platform Components
|
|
||||||
|
|
||||||
All other MCP servers work on all platforms:
|
|
||||||
- ✅ Serena (Python/uvx)
|
|
||||||
- ✅ Sequential Thinking (Node.js/npx)
|
|
||||||
- ✅ Database Server (Node.js/npx)
|
|
||||||
- ✅ Context7 (Node.js/npx)
|
|
||||||
- ✅ Memory (works via Node.js on all platforms - PowerShell launcher is Windows-only convenience)
|
|
||||||
- ✅ Fetch (Python/uvx)
|
|
||||||
- ✅ Playwright (Node.js/npx)
|
|
||||||
|
|
||||||
### Disabling Windows MCP on Mac/Linux
|
|
||||||
|
|
||||||
If you're on Mac/Linux and want to disable Windows MCP to avoid startup errors:
|
|
||||||
|
|
||||||
**Option 1: Comment out in .mcp.json**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
// "windows-mcp": {
|
|
||||||
// "command": "uv",
|
|
||||||
// "args": ["--directory", "./.windows-mcp", "run", "main.py"]
|
|
||||||
// },
|
|
||||||
// ... other servers
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// 2. Data access
|
||||||
|
const actor = token.actor;
|
||||||
|
const arcanePool = actor.system.resources.classFeat_arcanePool;
|
||||||
|
|
||||||
|
// 3. Business logic
|
||||||
|
async function doSomething() {
|
||||||
|
// Implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Execute
|
||||||
|
await doSomething();
|
||||||
|
|
||||||
|
// 5. User feedback
|
||||||
|
ui.notifications.info("Done!");
|
||||||
|
})();
|
||||||
```
|
```
|
||||||
|
|
||||||
**Option 2: Disable in settings**
|
**See [CLAUDE.md § Macro Development Patterns](CLAUDE.md#macro-development-patterns) for detailed macro examples.**
|
||||||
```json
|
|
||||||
// In .claude/settings.json or .claude/settings.local.json
|
### Debugging Macros
|
||||||
{
|
|
||||||
"mcpServers": {
|
Press **F12** to open browser console:
|
||||||
"windows-mcp": {
|
- Use **Console** tab for logs and errors
|
||||||
"enabled": false
|
- Use **Network** tab for API calls
|
||||||
}
|
- Add `console.log()` for debugging
|
||||||
}
|
- Check error messages in notifications
|
||||||
}
|
|
||||||
|
### Testing Changes
|
||||||
|
|
||||||
|
1. Make changes to PF1 system or macros
|
||||||
|
2. Run `npm run build` to rebuild
|
||||||
|
3. Reload Foundry VTT (F5)
|
||||||
|
4. Test macros or features
|
||||||
|
5. Check console (F12) for errors
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Documentation
|
||||||
|
|
||||||
|
| Document | Purpose | Read Time |
|
||||||
|
|----------|---------|-----------|
|
||||||
|
| **[CLAUDE.md](CLAUDE.md)** | Complete project guide & API reference | 30 min |
|
||||||
|
| **[QUICKSTART.md](QUICKSTART.md)** | Quick start setup guide | 5 min |
|
||||||
|
| **[Manual_dmgtracking.md](Manual_dmgtracking.md)** | Damage tracking implementation | Reference |
|
||||||
|
| **[ARCANE_POOL_ANALYSIS.md](ARCANE_POOL_ANALYSIS.md)** | Arcane Pool macro analysis | Reference |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎮 Foundry VTT & PF1 API
|
||||||
|
|
||||||
|
### Global Objects (Available in Macros)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
game // Main game instance
|
||||||
|
game.actors // Actor collection
|
||||||
|
game.items // Item collection
|
||||||
|
game.scenes // Scene collection
|
||||||
|
game.macros // Macro collection
|
||||||
|
game.settings // Settings registry
|
||||||
|
|
||||||
|
ui // UI manager
|
||||||
|
ui.notifications // Toast notifications
|
||||||
|
ui.chat // Chat sidebar
|
||||||
|
|
||||||
|
canvas // Canvas rendering system
|
||||||
|
CONFIG // Global configuration
|
||||||
|
CONFIG.PF1 // PF1-specific config
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common API Patterns
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Get documents
|
||||||
|
const actor = game.actors.get(actorId);
|
||||||
|
const item = game.items.getName("Item Name");
|
||||||
|
const doc = fromUuidSync("Actor.abc123.Item.def456");
|
||||||
|
|
||||||
|
// Update documents
|
||||||
|
await actor.update({
|
||||||
|
"system.hp.value": 50,
|
||||||
|
"system.attributes.ac.total": 25
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find items
|
||||||
|
const buffs = actor.items.filter(i => i.type === "buff");
|
||||||
|
const haste = actor.items.find(i => i.name === "Haste");
|
||||||
|
|
||||||
|
// Use hooks
|
||||||
|
Hooks.on("updateActor", (actor, change, options, userId) => {
|
||||||
|
console.log(`${actor.name} was updated`);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**See [CLAUDE.md § Foundry VTT API Reference](CLAUDE.md#foundry-vtt-api-reference) for complete API documentation.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Git Workflow
|
||||||
|
|
||||||
|
### Check Status
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git status # Show working tree status
|
||||||
|
git log --oneline # Show recent commits
|
||||||
|
git diff # Show unstaged changes
|
||||||
|
```
|
||||||
|
|
||||||
|
### Make Changes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create feature branch
|
||||||
|
git checkout -b feature/my-feature
|
||||||
|
|
||||||
|
# Make changes...
|
||||||
|
|
||||||
|
# Commit
|
||||||
|
git add src/
|
||||||
|
git commit -m "feat(macro): add new feature"
|
||||||
|
|
||||||
|
# Push
|
||||||
|
git push -u origin feature/my-feature
|
||||||
|
```
|
||||||
|
|
||||||
|
### Commit Message Format
|
||||||
|
|
||||||
|
Follow [Conventional Commits](https://www.conventionalcommits.org/):
|
||||||
|
|
||||||
|
```
|
||||||
|
<type>(<scope>): <subject>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Types**: `feat`, `fix`, `docs`, `refactor`, `test`, `chore`, `style`
|
||||||
|
**Scopes**: `macro`, `system`, `core`, `config`, `docs`
|
||||||
|
|
||||||
|
**Examples**:
|
||||||
|
```bash
|
||||||
|
feat(macro): add arcane pool enhancement UI
|
||||||
|
fix(system): correct haste buff duration
|
||||||
|
docs(readme): update setup instructions
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🐛 Troubleshooting
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
Quick fixes:
|
### Foundry Won't Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Agent not working?
|
# Check logs in Data/Logs/
|
||||||
> "Use the [agent-name] agent to..." # Manual invocation
|
cat "src/FoundryVTT-11.315/Data/Logs/foundry.log"
|
||||||
|
|
||||||
# Command not found?
|
# Try running directly
|
||||||
> /help # List available commands
|
cd src/FoundryVTT-11.315
|
||||||
|
node resources/app/main.js
|
||||||
# MCP server failed?
|
|
||||||
cat .mcp.json | jq '.mcpServers' # Check configuration
|
|
||||||
|
|
||||||
# Permission denied?
|
|
||||||
cat .claude/settings.json | jq '.permissions' # Check permissions
|
|
||||||
|
|
||||||
# Windows MCP error on Mac/Linux?
|
|
||||||
# This is normal - see Platform Notes above
|
|
||||||
```
|
```
|
||||||
|
|
||||||
See [CLAUDE_CODE_SETUP_COMPLETE.md](CLAUDE_CODE_SETUP_COMPLETE.md#troubleshooting) for detailed troubleshooting.
|
### Macro Not Working
|
||||||
|
|
||||||
|
1. **Check token selection**: Click a token on the canvas
|
||||||
|
2. **Check console (F12)**: Look for error messages
|
||||||
|
3. **Verify actor exists**: `console.log(token.actor)`
|
||||||
|
4. **Check syntax**: Paste into browser console
|
||||||
|
5. **Clear cache**: Hard refresh (Ctrl+Shift+R)
|
||||||
|
|
||||||
|
### Build Failures
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd src/foundryvtt-pathfinder1-v10.8
|
||||||
|
|
||||||
|
# Clean and rebuild
|
||||||
|
rm -r dist node_modules
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Git Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Reset to last commit (discard changes)
|
||||||
|
git reset --hard HEAD
|
||||||
|
|
||||||
|
# Pull latest changes
|
||||||
|
git pull origin main
|
||||||
|
|
||||||
|
# Check remote
|
||||||
|
git remote -v
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Common Tasks
|
||||||
|
|
||||||
|
### Add a New Macro
|
||||||
|
|
||||||
|
1. Create `src/macro_myfeature.js`
|
||||||
|
2. Follow IIFE pattern (see examples)
|
||||||
|
3. Test in Foundry VTT
|
||||||
|
4. Commit: `git add src/macro_myfeature.js && git commit -m "feat(macro): add myfeature"`
|
||||||
|
|
||||||
|
### Modify PF1 System
|
||||||
|
|
||||||
|
1. Edit files in `src/foundryvtt-pathfinder1-v10.8/module/`
|
||||||
|
2. Run `npm run build:watch`
|
||||||
|
3. Reload Foundry VTT (F5)
|
||||||
|
4. Test changes
|
||||||
|
5. Commit: `git add src/foundryvtt-pathfinder1-v10.8/ && git commit -m "feat(system): describe change"`
|
||||||
|
|
||||||
|
### Add Compendium Content
|
||||||
|
|
||||||
|
1. Extract packs: `npm run packs:extract`
|
||||||
|
2. Edit JSON files
|
||||||
|
3. Compile packs: `npm run packs:compile`
|
||||||
|
4. Commit changes
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -541,50 +385,102 @@ See [CLAUDE_CODE_SETUP_COMPLETE.md](CLAUDE_CODE_SETUP_COMPLETE.md#troubleshootin
|
|||||||
|
|
||||||
### Official Documentation
|
### Official Documentation
|
||||||
|
|
||||||
- **Claude Code Docs**: https://docs.claude.com/en/docs/claude-code/
|
- **Foundry VTT API v11**: https://foundryvtt.com/api/v11/
|
||||||
- **GitHub Issues**: https://github.com/anthropics/claude-code/issues
|
- **Foundry VTT Docs**: https://foundryvtt.com/kb/
|
||||||
|
- **PF1 System GitHub**: https://github.com/Furyspark/foundryvtt-pathfinder1
|
||||||
|
|
||||||
### This Project
|
### Project Documentation
|
||||||
|
|
||||||
- **Quick Start**: [QUICKSTART.md](QUICKSTART.md)
|
- **[CLAUDE.md](CLAUDE.md)** - Complete project guide (technology stack, API reference, configuration)
|
||||||
- **Complete Guide**: [CLAUDE_CODE_SETUP_COMPLETE.md](CLAUDE_CODE_SETUP_COMPLETE.md)
|
- **[QUICKSTART.md](QUICKSTART.md)** - Setup and first steps
|
||||||
- **MCP Servers**: [MCP_SERVERS_GUIDE.md](MCP_SERVERS_GUIDE.md)
|
- **[ARCANE_POOL_ANALYSIS.md](ARCANE_POOL_ANALYSIS.md)** - Arcane Pool macro implementation
|
||||||
- **Templates**: [.claude/TEMPLATES_README.md](.claude/TEMPLATES_README.md)
|
- **[Manual_dmgtracking.md](Manual_dmgtracking.md)** - Damage tracking system
|
||||||
- **MCP Templates**: [.claude/agents/MCP_USAGE_TEMPLATES.md](.claude/agents/MCP_USAGE_TEMPLATES.md)
|
|
||||||
|
### Community
|
||||||
|
|
||||||
|
- **Foundry VTT Discord**: https://discord.gg/foundryvtt
|
||||||
|
- **Foundry VTT Reddit**: https://reddit.com/r/FoundryVTT
|
||||||
|
- **PF1 System Issues**: https://github.com/Furyspark/foundryvtt-pathfinder1/issues
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎉 Success Checklist
|
## 🤝 Contributing
|
||||||
|
|
||||||
After setup, you should be able to:
|
### Code Style
|
||||||
|
|
||||||
- [x] Run `/setup-info` and see full configuration
|
- Use `camelCase` for variables and functions
|
||||||
- [x] Use slash commands like `/analyze`, `/review`, `/test`
|
- Use `PascalCase` for classes
|
||||||
- [x] Invoke agents automatically or manually
|
- Use `UPPER_SNAKE_CASE` for constants
|
||||||
- [x] Change output styles with `/output-style`
|
- Follow ESLint rules: `npm run lint`
|
||||||
- [x] Use Serena MCP for code navigation
|
- Format code: `npm run format`
|
||||||
- [x] Store persistent memories
|
|
||||||
- [x] Use extended thinking for complex problems
|
|
||||||
- [x] Access checkpoints with ESC ESC
|
|
||||||
|
|
||||||
**Ready?** Start with [QUICKSTART.md](QUICKSTART.md)!
|
### Before Committing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Lint and format
|
||||||
|
npm run lint
|
||||||
|
npm run format
|
||||||
|
|
||||||
|
# Build to check for errors
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Commit
|
||||||
|
git add .
|
||||||
|
git commit -m "feat(...): description"
|
||||||
|
git push
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Project Statistics
|
||||||
|
|
||||||
|
- **Foundry VTT Version**: v11.315
|
||||||
|
- **PF1 System Version**: v10.8
|
||||||
|
- **Runtime**: Node.js v16-19 (Electron)
|
||||||
|
- **Languages**: JavaScript (ES6+), TypeScript definitions
|
||||||
|
- **Repository**: https://gitea.gowlershome.dyndns.org/Gowler/FoundryVTT.git
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📝 Version History
|
## 📝 Version History
|
||||||
|
|
||||||
- **v3.0.0** (2025-10-20): Complete documentation overhaul, consolidated docs, added /adr command
|
- **v1.0.0** (2025-01-30) - Initial project setup with Foundry VTT v11.315 and PF1e v10.8
|
||||||
- **v2.0.0** (2025-10-17): Added output styles, plugins, status line
|
|
||||||
- **v1.0.0**: Initial comprehensive setup
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📄 License
|
## 🎓 Learning Path
|
||||||
|
|
||||||
This configuration setup is provided as-is for educational and development purposes. Feel free to use, modify, and share.
|
### Day 1: Setup
|
||||||
|
1. Read [QUICKSTART.md](QUICKSTART.md)
|
||||||
|
2. Install dependencies
|
||||||
|
3. Build PF1 system
|
||||||
|
4. Launch Foundry VTT
|
||||||
|
|
||||||
|
### Week 1: Basics
|
||||||
|
1. Create a simple macro
|
||||||
|
2. Test macro in game
|
||||||
|
3. Understand actor/item API
|
||||||
|
4. Explore Foundry console (F12)
|
||||||
|
|
||||||
|
### Month 1: Advanced
|
||||||
|
1. Build complex macros with dialogs
|
||||||
|
2. Modify PF1 system code
|
||||||
|
3. Add compendium content
|
||||||
|
4. Integrate Claude Code for automation
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Questions?** Check the documentation files or see [troubleshooting](#troubleshooting)
|
## 💬 Questions?
|
||||||
|
|
||||||
|
- Check [CLAUDE.md](CLAUDE.md) for detailed documentation
|
||||||
|
- See [QUICKSTART.md](QUICKSTART.md) for setup help
|
||||||
|
- Review macro examples: `src/macro.js`, `src/macro_haste.js`
|
||||||
|
- Check browser console (F12) for errors
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
**Ready to start?** → **[QUICKSTART.md](QUICKSTART.md)**
|
**Ready to start?** → **[QUICKSTART.md](QUICKSTART.md)**
|
||||||
|
|
||||||
|
**Need details?** → **[CLAUDE.md](CLAUDE.md)**
|
||||||
|
|
||||||
|
**Have questions?** Check the troubleshooting section above.
|
||||||
|
|||||||
Binary file not shown.
@@ -1,105 +0,0 @@
|
|||||||
/**
|
|
||||||
* Activate currency history tracking that stores entries in flags only.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const TARGET_ACTOR = game.actors.getName("Zeratal") ?? null;
|
|
||||||
const ACTOR_ID = TARGET_ACTOR?.id ?? "put-actor-id-here";
|
|
||||||
const CURRENCY_PATH = "system.currency";
|
|
||||||
const FLAG_SCOPE = "world";
|
|
||||||
const FLAG_KEY = "pf1CurrencyHistory";
|
|
||||||
const MAX_ROWS = 50;
|
|
||||||
const COIN_ORDER = ["pp", "gp", "sp", "cp"];
|
|
||||||
|
|
||||||
if (!ACTOR_ID || ACTOR_ID === "put-actor-id-here") {
|
|
||||||
return ui.notifications.warn("Set ACTOR_ID before running the macro.");
|
|
||||||
}
|
|
||||||
|
|
||||||
game.pf1 ??= {};
|
|
||||||
game.pf1.currencyHistoryFlags ??= { current: {}, hooks: {} };
|
|
||||||
const state = game.pf1.currencyHistoryFlags;
|
|
||||||
|
|
||||||
const logKey = `pf1-currency-history-flags-${ACTOR_ID}`;
|
|
||||||
if (state.hooks[logKey]) {
|
|
||||||
Hooks.off("updateActor", state.hooks[logKey]);
|
|
||||||
delete state.hooks[logKey];
|
|
||||||
}
|
|
||||||
|
|
||||||
primeSnapshot(game.actors.get(ACTOR_ID) ?? TARGET_ACTOR);
|
|
||||||
|
|
||||||
state.hooks[logKey] = Hooks.on("updateActor", (actor, change, options, userId) => {
|
|
||||||
if (actor.id !== ACTOR_ID) return;
|
|
||||||
|
|
||||||
const newCurrency = readCurrency(actor);
|
|
||||||
if (!newCurrency) return;
|
|
||||||
|
|
||||||
const previous = state.current[ACTOR_ID];
|
|
||||||
if (!previous) {
|
|
||||||
state.current[ACTOR_ID] = newCurrency;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (currencyEquals(previous, newCurrency)) return;
|
|
||||||
|
|
||||||
const diff = diffCurrency(previous, newCurrency);
|
|
||||||
const user = game.users.get(userId);
|
|
||||||
|
|
||||||
state.current[ACTOR_ID] = newCurrency;
|
|
||||||
|
|
||||||
appendHistoryEntry(actor, {
|
|
||||||
value: formatCurrency(newCurrency),
|
|
||||||
diff: formatCurrency(diff, true),
|
|
||||||
user: user?.name ?? "System",
|
|
||||||
}).catch((err) => console.error("Currency History Flags | Failed to append entry", err));
|
|
||||||
});
|
|
||||||
|
|
||||||
const actorName = game.actors.get(ACTOR_ID)?.name ?? TARGET_ACTOR?.name ?? ACTOR_ID;
|
|
||||||
ui.notifications.info(`Currency flag history tracking active for ${actorName}.`);
|
|
||||||
|
|
||||||
async function appendHistoryEntry(actor, entry) {
|
|
||||||
const existing = (await actor.getFlag(FLAG_SCOPE, FLAG_KEY)) ?? [];
|
|
||||||
existing.unshift({
|
|
||||||
timestamp: Date.now(),
|
|
||||||
value: entry.value,
|
|
||||||
diff: entry.diff,
|
|
||||||
user: entry.user,
|
|
||||||
});
|
|
||||||
if (existing.length > MAX_ROWS) existing.splice(MAX_ROWS);
|
|
||||||
|
|
||||||
await actor.setFlag(FLAG_SCOPE, FLAG_KEY, existing);
|
|
||||||
}
|
|
||||||
|
|
||||||
function primeSnapshot(actor) {
|
|
||||||
const currency = readCurrency(actor);
|
|
||||||
if (!currency) return;
|
|
||||||
state.current[ACTOR_ID] = currency;
|
|
||||||
}
|
|
||||||
|
|
||||||
function readCurrency(actor) {
|
|
||||||
if (!actor) return null;
|
|
||||||
const base = foundry.utils.getProperty(actor, CURRENCY_PATH) ?? actor.system?.currency;
|
|
||||||
if (!base) return null;
|
|
||||||
const clone = {};
|
|
||||||
for (const coin of COIN_ORDER) clone[coin] = Number(base[coin] ?? 0);
|
|
||||||
return clone;
|
|
||||||
}
|
|
||||||
|
|
||||||
function currencyEquals(a, b) {
|
|
||||||
return COIN_ORDER.every((coin) => (a[coin] ?? 0) === (b[coin] ?? 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
function diffCurrency(prev, next) {
|
|
||||||
const diff = {};
|
|
||||||
for (const coin of COIN_ORDER) diff[coin] = (next[coin] ?? 0) - (prev[coin] ?? 0);
|
|
||||||
return diff;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatCurrency(obj, skipZero = false) {
|
|
||||||
return COIN_ORDER.map((coin) => obj[coin] ?? 0)
|
|
||||||
.map((value, idx) => {
|
|
||||||
if (skipZero && value === 0) return null;
|
|
||||||
const label = COIN_ORDER[idx];
|
|
||||||
const v = skipZero && value > 0 ? `+${value}` : `${value}`;
|
|
||||||
return `${label}:${v}`;
|
|
||||||
})
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(" ");
|
|
||||||
}
|
|
||||||
@@ -1,300 +0,0 @@
|
|||||||
/**
|
|
||||||
* Track currency changes for a single actor and store them in ===Currency History=== notes.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const TARGET_ACTOR = game.actors.getName("Zeratal") ?? null;
|
|
||||||
const ACTOR_ID = TARGET_ACTOR?.id ?? "put-actor-id-here";
|
|
||||||
const CURRENCY_PATH = "system.currency";
|
|
||||||
const UPDATE_CONTEXT_FLAG = "currencyHistory";
|
|
||||||
const FLAG_SCOPE = "world";
|
|
||||||
const FLAG_KEY = "pf1CurrencyHistory";
|
|
||||||
const BLOCK_NAME = "pf1-currency-history";
|
|
||||||
const MAX_ROWS = 50;
|
|
||||||
const BLOCK_START = "<!-- CURRENCY_HISTORY_START -->";
|
|
||||||
const BLOCK_END = "<!-- CURRENCY_HISTORY_END -->";
|
|
||||||
const COIN_ORDER = ["pp", "gp", "sp", "cp"];
|
|
||||||
|
|
||||||
if (!ACTOR_ID || ACTOR_ID === "put-actor-id-here") {
|
|
||||||
return ui.notifications.warn("Set ACTOR_ID before running the macro.");
|
|
||||||
}
|
|
||||||
|
|
||||||
game.pf1 ??= {};
|
|
||||||
game.pf1.currencyHistory ??= {};
|
|
||||||
const state = game.pf1.currencyHistory;
|
|
||||||
state.sources ??= new Map();
|
|
||||||
state.current ??= {};
|
|
||||||
state.hooks ??= {};
|
|
||||||
|
|
||||||
const logKey = `pf1-currency-history-${ACTOR_ID}`;
|
|
||||||
|
|
||||||
if (state.hooks[logKey]) {
|
|
||||||
Hooks.off("updateActor", state.hooks[logKey]);
|
|
||||||
delete state.hooks[logKey];
|
|
||||||
}
|
|
||||||
|
|
||||||
ensureDamageSourceTracking();
|
|
||||||
primeCurrencySnapshot(game.actors.get(ACTOR_ID) ?? TARGET_ACTOR);
|
|
||||||
|
|
||||||
state.hooks[logKey] = Hooks.on("updateActor", (actor, change, options, userId) => {
|
|
||||||
if (actor.id !== ACTOR_ID) return;
|
|
||||||
if (options?.[UPDATE_CONTEXT_FLAG]) return;
|
|
||||||
|
|
||||||
const newCurrency = readCurrencyValue(actor);
|
|
||||||
if (!newCurrency) return;
|
|
||||||
|
|
||||||
const previous = state.current[ACTOR_ID];
|
|
||||||
if (!previous) {
|
|
||||||
state.current[ACTOR_ID] = newCurrency;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currencyEquals(previous, newCurrency)) return;
|
|
||||||
|
|
||||||
const diff = diffCurrency(previous, newCurrency);
|
|
||||||
const diffText = formatCurrency(diff, true);
|
|
||||||
const user = game.users.get(userId);
|
|
||||||
const source = consumeDamageSource(actor.id) ?? "Manual change";
|
|
||||||
|
|
||||||
state.current[ACTOR_ID] = newCurrency;
|
|
||||||
|
|
||||||
appendHistoryRow(actor, {
|
|
||||||
value: formatCurrency(newCurrency),
|
|
||||||
diffText,
|
|
||||||
user,
|
|
||||||
source,
|
|
||||||
snapshot: newCurrency,
|
|
||||||
}).catch((err) => console.error("Currency History | Failed to append entry", err));
|
|
||||||
});
|
|
||||||
|
|
||||||
const actorName = game.actors.get(ACTOR_ID)?.name ?? TARGET_ACTOR?.name ?? ACTOR_ID;
|
|
||||||
ui.notifications.info(`Currency history tracking active for ${actorName}.`);
|
|
||||||
|
|
||||||
async function appendHistoryRow(actor, entry) {
|
|
||||||
const existing = (await actor.getFlag(FLAG_SCOPE, FLAG_KEY)) ?? [];
|
|
||||||
const newEntry = {
|
|
||||||
timestamp: Date.now(),
|
|
||||||
value: entry.value,
|
|
||||||
diff: entry.diffText,
|
|
||||||
user: entry.user?.name ?? "System",
|
|
||||||
source: entry.source ?? "",
|
|
||||||
};
|
|
||||||
existing.unshift(newEntry);
|
|
||||||
if (existing.length > MAX_ROWS) existing.splice(MAX_ROWS);
|
|
||||||
|
|
||||||
const notes = actor.system.details?.notes?.value ?? "";
|
|
||||||
const block = renderHistoryBlock(existing);
|
|
||||||
const updatedNotes = injectHistoryBlock(notes, block);
|
|
||||||
|
|
||||||
await actor.update(
|
|
||||||
{
|
|
||||||
[`flags.${FLAG_SCOPE}.${FLAG_KEY}`]: existing,
|
|
||||||
"system.details.notes.value": updatedNotes,
|
|
||||||
},
|
|
||||||
{ [UPDATE_CONTEXT_FLAG]: true }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderHistoryBlock(entries) {
|
|
||||||
const rows = entries
|
|
||||||
.map(
|
|
||||||
(e) => `
|
|
||||||
<tr data-ts="${e.timestamp}">
|
|
||||||
<td>${new Date(e.timestamp).toLocaleString()}</td>
|
|
||||||
<td>${e.value}</td>
|
|
||||||
<td>${e.diff}</td>
|
|
||||||
<td>${e.user ?? ""}</td>
|
|
||||||
<td>${e.source ?? ""}</td>
|
|
||||||
</tr>`
|
|
||||||
)
|
|
||||||
.join("");
|
|
||||||
|
|
||||||
const blockId = `${BLOCK_NAME}-${ACTOR_ID}`;
|
|
||||||
return `
|
|
||||||
${BLOCK_START}
|
|
||||||
<section data-history-block="${BLOCK_NAME}" data-history-id="${blockId}">
|
|
||||||
<details open>
|
|
||||||
<summary>===Currency History=== (click to collapse)</summary>
|
|
||||||
<div class="history-controls">
|
|
||||||
<label>From <input type="date" data-filter="from"></label>
|
|
||||||
<label>To <input type="date" data-filter="to"></label>
|
|
||||||
</div>
|
|
||||||
<table class="currency-history" data-currency-history="1">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Timestamp</th>
|
|
||||||
<th>Totals</th>
|
|
||||||
<th>Δ</th>
|
|
||||||
<th>User</th>
|
|
||||||
<th>Source</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
${rows}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</details>
|
|
||||||
<script type="text/javascript">
|
|
||||||
(function(){
|
|
||||||
const root = document.currentScript.closest('section[data-history-block="${BLOCK_NAME}"]');
|
|
||||||
if (!root) return;
|
|
||||||
const rows = Array.from(root.querySelectorAll("tbody tr"));
|
|
||||||
const fromInput = root.querySelector('input[data-filter="from"]');
|
|
||||||
const toInput = root.querySelector('input[data-filter="to"]');
|
|
||||||
function applyFilters(){
|
|
||||||
const from = fromInput?.value ? Date.parse(fromInput.value) : null;
|
|
||||||
const to = toInput?.value ? Date.parse(toInput.value) + 86399999 : null;
|
|
||||||
rows.forEach((row) => {
|
|
||||||
const ts = Number(row.dataset.ts);
|
|
||||||
const visible = (!from || ts >= from) && (!to || ts <= to);
|
|
||||||
row.style.display = visible ? "" : "none";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
fromInput?.addEventListener("input", applyFilters);
|
|
||||||
toInput?.addEventListener("input", applyFilters);
|
|
||||||
applyFilters();
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</section>
|
|
||||||
${BLOCK_END}`.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function injectHistoryBlock(notes, block) {
|
|
||||||
const blockPattern = new RegExp(`${escapeRegExp(BLOCK_START)}[\\s\\S]*?${escapeRegExp(BLOCK_END)}`, "g");
|
|
||||||
let cleaned = notes.replace(blockPattern, "");
|
|
||||||
if (cleaned === notes) {
|
|
||||||
const sectionPattern = new RegExp(`<section[^>]*data-history-block="${BLOCK_NAME}"[^>]*>[\\s\\S]*?<\\/section>`, "g");
|
|
||||||
cleaned = notes.replace(sectionPattern, "");
|
|
||||||
}
|
|
||||||
cleaned = cleaned.trim();
|
|
||||||
const separator = cleaned.length && !cleaned.endsWith("\n") ? "\n\n" : "";
|
|
||||||
return `${cleaned}${separator}${block}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function consumeDamageSource(actorId) {
|
|
||||||
const entry = state.sources.get(actorId);
|
|
||||||
if (!entry) return null;
|
|
||||||
state.sources.delete(actorId);
|
|
||||||
return entry.label;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureDamageSourceTracking() {
|
|
||||||
if (state.applyDamageWrapped) return;
|
|
||||||
|
|
||||||
const ActorPF = pf1?.documents?.actor?.ActorPF;
|
|
||||||
if (!ActorPF?.applyDamage) return;
|
|
||||||
|
|
||||||
const original = ActorPF.applyDamage;
|
|
||||||
ActorPF.applyDamage = async function wrappedApplyDamage(value, options = {}) {
|
|
||||||
try {
|
|
||||||
noteDamageSource(value, options);
|
|
||||||
} catch (err) {
|
|
||||||
console.warn("Currency History | Failed to record source", err);
|
|
||||||
}
|
|
||||||
return original.call(this, value, options);
|
|
||||||
};
|
|
||||||
|
|
||||||
state.applyDamageWrapped = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function noteDamageSource(value, options) {
|
|
||||||
const actors = resolveActorTargets(options?.targets);
|
|
||||||
if (!actors.some((a) => a?.id === ACTOR_ID)) return;
|
|
||||||
|
|
||||||
const label = buildSourceLabel(value, options);
|
|
||||||
if (!label) return;
|
|
||||||
state.sources.set(ACTOR_ID, { label, ts: Date.now() });
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildSourceLabel(value, options = {}) {
|
|
||||||
const info = options.message?.flags?.pf1?.identifiedInfo ?? {};
|
|
||||||
const metadata = options.message?.flags?.pf1?.metadata ?? {};
|
|
||||||
const fromChatFlavor = options.message?.flavor?.trim();
|
|
||||||
|
|
||||||
const actorDoc = resolveActorFromMetadata(metadata);
|
|
||||||
const itemDoc = actorDoc ? resolveItemFromMetadata(actorDoc, metadata) : null;
|
|
||||||
const actorName = actorDoc?.name ?? options.message?.speaker?.alias ?? null;
|
|
||||||
const actionName = info.actionName ?? info.name ?? itemDoc?.name ?? fromChatFlavor ?? null;
|
|
||||||
|
|
||||||
if (actorName && actionName) return `${actorName} -> ${actionName}`;
|
|
||||||
if (actionName) return actionName;
|
|
||||||
if (actorName) return actorName;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function primeCurrencySnapshot(actor) {
|
|
||||||
const currency = readCurrencyValue(actor);
|
|
||||||
if (!currency) return;
|
|
||||||
state.current[ACTOR_ID] = currency;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveActorTargets(targets) {
|
|
||||||
let list = [];
|
|
||||||
if (Array.isArray(targets) && targets.length) list = targets;
|
|
||||||
else list = canvas?.tokens?.controlled ?? [];
|
|
||||||
|
|
||||||
return list
|
|
||||||
.map((entry) => {
|
|
||||||
if (!entry) return null;
|
|
||||||
if (entry instanceof Actor) return entry;
|
|
||||||
if (entry.actor) return entry.actor;
|
|
||||||
if (entry.document?.actor) return entry.document.actor;
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
.filter((actor) => actor instanceof Actor && actor.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function readCurrencyValue(actor) {
|
|
||||||
if (!actor) return null;
|
|
||||||
const base = foundry.utils.getProperty(actor, CURRENCY_PATH) ?? actor.system?.currency;
|
|
||||||
if (!base) return null;
|
|
||||||
const clone = {};
|
|
||||||
for (const coin of COIN_ORDER) clone[coin] = Number(base[coin] ?? 0);
|
|
||||||
return clone;
|
|
||||||
}
|
|
||||||
|
|
||||||
function currencyEquals(a, b) {
|
|
||||||
return COIN_ORDER.every((coin) => (a[coin] ?? 0) === (b[coin] ?? 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
function diffCurrency(prev, next) {
|
|
||||||
const diff = {};
|
|
||||||
for (const coin of COIN_ORDER) diff[coin] = (next[coin] ?? 0) - (prev[coin] ?? 0);
|
|
||||||
return diff;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatCurrency(obj, skipZero = false) {
|
|
||||||
return COIN_ORDER.map((coin) => obj[coin] ?? 0)
|
|
||||||
.map((value, idx) => {
|
|
||||||
if (skipZero && value === 0) return null;
|
|
||||||
const label = COIN_ORDER[idx];
|
|
||||||
const v = skipZero ? (value > 0 ? `+${value}` : `${value}`) : value;
|
|
||||||
return `${label}:${v}`;
|
|
||||||
})
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveActorFromMetadata(metadata = {}) {
|
|
||||||
if (!metadata.actor) return null;
|
|
||||||
|
|
||||||
if (typeof fromUuidSync === "function") {
|
|
||||||
try {
|
|
||||||
const doc = fromUuidSync(metadata.actor);
|
|
||||||
if (doc instanceof Actor) return doc;
|
|
||||||
} catch (err) {
|
|
||||||
console.warn("Currency History | Failed to resolve actor UUID", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = metadata.actor.split(".").pop();
|
|
||||||
return game.actors.get(id) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveItemFromMetadata(actor, metadata = {}) {
|
|
||||||
if (!metadata.item || !(actor?.items instanceof Collection)) return null;
|
|
||||||
return actor.items.get(metadata.item) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeRegExp(str) {
|
|
||||||
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
||||||
}
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
/**
|
|
||||||
* Activate HP history tracking that stores entries in flags only.
|
|
||||||
* Pair with macro_enable-history-tab.js to view the data inside a History tab.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const TARGET_ACTOR = game.actors.getName("Zeratal") ?? null;
|
|
||||||
const ACTOR_ID = TARGET_ACTOR?.id ?? "put-actor-id-here";
|
|
||||||
const HP_PATH = "system.attributes.hp.value";
|
|
||||||
const FLAG_SCOPE = "world";
|
|
||||||
const FLAG_KEY = "pf1HpHistory";
|
|
||||||
const UPDATE_CONTEXT_FLAG = "hpHistoryFlags";
|
|
||||||
const MAX_ROWS = 50;
|
|
||||||
|
|
||||||
if (!ACTOR_ID || ACTOR_ID === "put-actor-id-here") {
|
|
||||||
return ui.notifications.warn("Set ACTOR_ID before running the macro.");
|
|
||||||
}
|
|
||||||
|
|
||||||
game.pf1 ??= {};
|
|
||||||
game.pf1.hpHistoryFlags ??= { sources: new Map(), current: {}, hooks: {} };
|
|
||||||
const state = game.pf1.hpHistoryFlags;
|
|
||||||
|
|
||||||
const logKey = `pf1-hp-history-flags-${ACTOR_ID}`;
|
|
||||||
if (state.hooks[logKey]) {
|
|
||||||
Hooks.off("updateActor", state.hooks[logKey]);
|
|
||||||
delete state.hooks[logKey];
|
|
||||||
}
|
|
||||||
|
|
||||||
ensureDamageSourceTracking();
|
|
||||||
primeSnapshot(game.actors.get(ACTOR_ID) ?? TARGET_ACTOR);
|
|
||||||
|
|
||||||
state.hooks[logKey] = Hooks.on("updateActor", (actor, change, options, userId) => {
|
|
||||||
if (actor.id !== ACTOR_ID) return;
|
|
||||||
if (options?.[UPDATE_CONTEXT_FLAG]) return;
|
|
||||||
|
|
||||||
const newHP = readHpValue(actor);
|
|
||||||
if (newHP === null) return;
|
|
||||||
|
|
||||||
const previous = state.current[ACTOR_ID];
|
|
||||||
if (previous === undefined) {
|
|
||||||
state.current[ACTOR_ID] = newHP;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (Object.is(newHP, previous)) return;
|
|
||||||
|
|
||||||
const diff = newHP - previous;
|
|
||||||
const diffText = diff >= 0 ? `+${diff}` : `${diff}`;
|
|
||||||
const user = game.users.get(userId);
|
|
||||||
const source = consumeDamageSource(actor.id) ?? inferManualSource(diff);
|
|
||||||
|
|
||||||
state.current[ACTOR_ID] = newHP;
|
|
||||||
|
|
||||||
appendHistoryEntry(actor, {
|
|
||||||
hp: newHP,
|
|
||||||
diff: diffText,
|
|
||||||
user: user?.name ?? "System",
|
|
||||||
source: source ?? "",
|
|
||||||
}).catch((err) => console.error("HP History Flags | Failed to append entry", err));
|
|
||||||
});
|
|
||||||
|
|
||||||
const actorName = game.actors.get(ACTOR_ID)?.name ?? TARGET_ACTOR?.name ?? ACTOR_ID;
|
|
||||||
ui.notifications.info(`HP flag history tracking active for ${actorName}.`);
|
|
||||||
|
|
||||||
async function appendHistoryEntry(actor, entry) {
|
|
||||||
const existing = (await actor.getFlag(FLAG_SCOPE, FLAG_KEY)) ?? [];
|
|
||||||
existing.unshift({
|
|
||||||
timestamp: Date.now(),
|
|
||||||
hp: entry.hp,
|
|
||||||
diff: entry.diff,
|
|
||||||
user: entry.user,
|
|
||||||
source: entry.source,
|
|
||||||
});
|
|
||||||
if (existing.length > MAX_ROWS) existing.splice(MAX_ROWS);
|
|
||||||
|
|
||||||
await actor.setFlag(FLAG_SCOPE, FLAG_KEY, existing);
|
|
||||||
}
|
|
||||||
|
|
||||||
function consumeDamageSource(actorId) {
|
|
||||||
const entry = state.sources.get(actorId);
|
|
||||||
if (!entry) return null;
|
|
||||||
state.sources.delete(actorId);
|
|
||||||
return entry.label;
|
|
||||||
}
|
|
||||||
|
|
||||||
function inferManualSource(diff) {
|
|
||||||
if (!Number.isFinite(diff) || diff === 0) return null;
|
|
||||||
return diff > 0 ? "Manual healing" : "Manual damage";
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureDamageSourceTracking() {
|
|
||||||
if (state.applyDamageWrapped) return;
|
|
||||||
|
|
||||||
const ActorPF = pf1?.documents?.actor?.ActorPF;
|
|
||||||
if (!ActorPF?.applyDamage) return;
|
|
||||||
|
|
||||||
const original = ActorPF.applyDamage;
|
|
||||||
ActorPF.applyDamage = async function wrappedApplyDamage(value, options = {}) {
|
|
||||||
try {
|
|
||||||
noteDamageSource(value, options);
|
|
||||||
} catch (err) {
|
|
||||||
console.warn("HP History Flags | Failed to record source", err);
|
|
||||||
}
|
|
||||||
return original.call(this, value, options);
|
|
||||||
};
|
|
||||||
|
|
||||||
state.applyDamageWrapped = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function noteDamageSource(value, options) {
|
|
||||||
const actors = resolveActorTargets(options?.targets);
|
|
||||||
if (!actors.some((a) => a?.id === ACTOR_ID)) return;
|
|
||||||
|
|
||||||
const label = buildSourceLabel(value, options);
|
|
||||||
if (!label) return;
|
|
||||||
state.sources.set(ACTOR_ID, { label, ts: Date.now() });
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildSourceLabel(value, options = {}) {
|
|
||||||
const info = options.message?.flags?.pf1?.identifiedInfo ?? {};
|
|
||||||
const metadata = options.message?.flags?.pf1?.metadata ?? {};
|
|
||||||
const fromChatFlavor = options.message?.flavor?.trim();
|
|
||||||
|
|
||||||
const actorDoc = resolveActorFromMetadata(metadata);
|
|
||||||
const itemDoc = actorDoc ? resolveItemFromMetadata(actorDoc, metadata) : null;
|
|
||||||
const actorName = actorDoc?.name ?? options.message?.speaker?.alias ?? null;
|
|
||||||
const actionName = info.actionName ?? info.name ?? itemDoc?.name ?? fromChatFlavor ?? null;
|
|
||||||
|
|
||||||
let label = null;
|
|
||||||
if (actorName && actionName) label = `${actorName} -> ${actionName}`;
|
|
||||||
else if (actionName) label = actionName;
|
|
||||||
else if (actorName) label = actorName;
|
|
||||||
else label = value < 0 ? "Healing" : "Damage";
|
|
||||||
|
|
||||||
if (options.isCritical) label += " (Critical)";
|
|
||||||
if (options.asNonlethal) label += " [Nonlethal]";
|
|
||||||
|
|
||||||
return label;
|
|
||||||
}
|
|
||||||
|
|
||||||
function primeSnapshot(actor) {
|
|
||||||
const hp = readHpValue(actor);
|
|
||||||
if (hp === null) return;
|
|
||||||
state.current[ACTOR_ID] = hp;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveActorTargets(targets) {
|
|
||||||
let list = [];
|
|
||||||
if (Array.isArray(targets) && targets.length) list = targets;
|
|
||||||
else list = canvas?.tokens?.controlled ?? [];
|
|
||||||
|
|
||||||
return list
|
|
||||||
.map((entry) => {
|
|
||||||
if (!entry) return null;
|
|
||||||
if (entry instanceof Actor) return entry;
|
|
||||||
if (entry.actor) return entry.actor;
|
|
||||||
if (entry.document?.actor) return entry.document.actor;
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
.filter((actor) => actor instanceof Actor && actor.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function readHpValue(actor) {
|
|
||||||
if (!actor) return null;
|
|
||||||
|
|
||||||
const candidates = [
|
|
||||||
HP_PATH,
|
|
||||||
HP_PATH.replace(/^system\./, "data."),
|
|
||||||
HP_PATH.replace(/^system\./, ""),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const path of candidates) {
|
|
||||||
const value = foundry.utils.getProperty(actor, path);
|
|
||||||
if (value !== undefined) {
|
|
||||||
const numeric = Number(value);
|
|
||||||
return Number.isFinite(numeric) ? numeric : null;
|
|
||||||
}
|
|
||||||
if (actor.system) {
|
|
||||||
const trimmed = path.startsWith("system.") ? path.slice(7) : path;
|
|
||||||
const systemValue = foundry.utils.getProperty(actor.system, trimmed);
|
|
||||||
if (systemValue !== undefined) {
|
|
||||||
const numeric = Number(systemValue);
|
|
||||||
return Number.isFinite(numeric) ? numeric : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveActorFromMetadata(metadata = {}) {
|
|
||||||
if (!metadata.actor) return null;
|
|
||||||
|
|
||||||
if (typeof fromUuidSync === "function") {
|
|
||||||
try {
|
|
||||||
const doc = fromUuidSync(metadata.actor);
|
|
||||||
if (doc instanceof Actor) return doc;
|
|
||||||
} catch (err) {
|
|
||||||
console.warn("HP History Flags | Failed to resolve actor UUID", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = metadata.actor.split(".").pop();
|
|
||||||
return game.actors.get(id) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveItemFromMetadata(actor, metadata = {}) {
|
|
||||||
if (!metadata.item || !(actor?.items instanceof Collection)) return null;
|
|
||||||
return actor.items.get(metadata.item) ?? null;
|
|
||||||
}
|
|
||||||
@@ -1,369 +0,0 @@
|
|||||||
/**
|
|
||||||
* Activate HP history tracking for a single actor.
|
|
||||||
* Instead of sending chat messages, HP changes are written into an
|
|
||||||
* ===HP History=== table inside the actor's Notes field.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const TARGET_ACTOR = game.actors.getName("Zeratal") ?? null;
|
|
||||||
const ACTOR_ID = TARGET_ACTOR?.id ?? "put-actor-id-here";
|
|
||||||
const HP_PATH = "system.attributes.hp.value";
|
|
||||||
const UPDATE_CONTEXT_FLAG = "hpHistory";
|
|
||||||
const FLAG_SCOPE = "world";
|
|
||||||
const FLAG_KEY = "pf1HpHistory";
|
|
||||||
const BLOCK_NAME = "pf1-hp-history";
|
|
||||||
const MAX_ROWS = 50;
|
|
||||||
const BLOCK_START = "<!-- HP_HISTORY_START -->";
|
|
||||||
const BLOCK_END = "<!-- HP_HISTORY_END -->";
|
|
||||||
|
|
||||||
if (!ACTOR_ID || ACTOR_ID === "put-actor-id-here") {
|
|
||||||
return ui.notifications.warn("Set ACTOR_ID before running the macro.");
|
|
||||||
}
|
|
||||||
|
|
||||||
game.pf1 ??= {};
|
|
||||||
game.pf1.hpHistory ??= {};
|
|
||||||
const state = game.pf1.hpHistory;
|
|
||||||
state.sources ??= new Map();
|
|
||||||
state.current ??= {};
|
|
||||||
state.hooks ??= {};
|
|
||||||
|
|
||||||
const logKey = `pf1-hp-history-${ACTOR_ID}`;
|
|
||||||
|
|
||||||
// Remove any previous hook for this actor so the macro is idempotent.
|
|
||||||
if (state.hooks[logKey]) {
|
|
||||||
Hooks.off("updateActor", state.hooks[logKey]);
|
|
||||||
delete state.hooks[logKey];
|
|
||||||
}
|
|
||||||
|
|
||||||
ensureDamageSourceTracking();
|
|
||||||
primeHpSnapshot(game.actors.get(ACTOR_ID) ?? TARGET_ACTOR);
|
|
||||||
|
|
||||||
state.hooks[logKey] = Hooks.on("updateActor", (actor, change, options, userId) => {
|
|
||||||
if (actor.id !== ACTOR_ID) return;
|
|
||||||
if (options?.[UPDATE_CONTEXT_FLAG]) return; // Ignore our own notes updates.
|
|
||||||
|
|
||||||
const newHP = readHpValue(actor);
|
|
||||||
if (newHP === null) return;
|
|
||||||
|
|
||||||
const previous = state.current[ACTOR_ID];
|
|
||||||
if (previous === undefined) {
|
|
||||||
state.current[ACTOR_ID] = newHP;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (Object.is(newHP, previous)) return;
|
|
||||||
|
|
||||||
const diff = newHP - previous;
|
|
||||||
const diffText = diff >= 0 ? `+${diff}` : `${diff}`;
|
|
||||||
const user = game.users.get(userId);
|
|
||||||
const source = consumeDamageSource(actor.id) ?? inferManualSource(diff);
|
|
||||||
|
|
||||||
state.current[ACTOR_ID] = newHP;
|
|
||||||
|
|
||||||
appendHistoryRow(actor, {
|
|
||||||
previous,
|
|
||||||
newHP,
|
|
||||||
diff,
|
|
||||||
diffText,
|
|
||||||
user,
|
|
||||||
source,
|
|
||||||
}).catch((err) => console.error("HP History | Failed to append entry", err));
|
|
||||||
});
|
|
||||||
|
|
||||||
const actorName = game.actors.get(ACTOR_ID)?.name ?? TARGET_ACTOR?.name ?? ACTOR_ID;
|
|
||||||
ui.notifications.info(`HP history tracking active for ${actorName}.`);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Append an entry to the Notes table.
|
|
||||||
* @param {Actor} actor
|
|
||||||
* @param {object} entry
|
|
||||||
*/
|
|
||||||
async function appendHistoryRow(actor, entry) {
|
|
||||||
const existing = (await actor.getFlag(FLAG_SCOPE, FLAG_KEY)) ?? [];
|
|
||||||
const newEntry = {
|
|
||||||
timestamp: Date.now(),
|
|
||||||
hp: entry.newHP,
|
|
||||||
diff: entry.diffText,
|
|
||||||
user: entry.user?.name ?? "System",
|
|
||||||
source: entry.source ?? "",
|
|
||||||
};
|
|
||||||
existing.unshift(newEntry);
|
|
||||||
if (existing.length > MAX_ROWS) existing.splice(MAX_ROWS);
|
|
||||||
|
|
||||||
const notes = actor.system.details?.notes?.value ?? "";
|
|
||||||
const block = renderHistoryBlock(existing);
|
|
||||||
const updatedNotes = injectHistoryBlock(notes, block);
|
|
||||||
|
|
||||||
await actor.update(
|
|
||||||
{
|
|
||||||
[`flags.${FLAG_SCOPE}.${FLAG_KEY}`]: existing,
|
|
||||||
"system.details.notes.value": updatedNotes,
|
|
||||||
},
|
|
||||||
{ [UPDATE_CONTEXT_FLAG]: true }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Consume and clear the stored damage source for this actor, if any.
|
|
||||||
* @param {string} actorId
|
|
||||||
*/
|
|
||||||
function consumeDamageSource(actorId) {
|
|
||||||
const entry = state.sources.get(actorId);
|
|
||||||
if (!entry) return null;
|
|
||||||
state.sources.delete(actorId);
|
|
||||||
return entry.label;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provide a fallback label when the change was manual or unidentified.
|
|
||||||
* @param {number} diff
|
|
||||||
*/
|
|
||||||
function inferManualSource(diff) {
|
|
||||||
if (!Number.isFinite(diff) || diff === 0) return null;
|
|
||||||
return diff > 0 ? "Manual healing" : "Manual damage";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure ActorPF.applyDamage is wrapped so we can capture originating chat data.
|
|
||||||
*/
|
|
||||||
function ensureDamageSourceTracking() {
|
|
||||||
if (state.applyDamageWrapped) return;
|
|
||||||
|
|
||||||
const ActorPF = pf1?.documents?.actor?.ActorPF;
|
|
||||||
if (!ActorPF?.applyDamage) return;
|
|
||||||
|
|
||||||
const original = ActorPF.applyDamage;
|
|
||||||
ActorPF.applyDamage = async function wrappedApplyDamage(value, options = {}) {
|
|
||||||
try {
|
|
||||||
noteDamageSource(value, options);
|
|
||||||
} catch (err) {
|
|
||||||
console.warn("HP History | Failed to record damage source", err);
|
|
||||||
}
|
|
||||||
return original.call(this, value, options);
|
|
||||||
};
|
|
||||||
|
|
||||||
state.applyDamageWrapped = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Record the best-guess label for the HP change if the tracked actor is among the targets.
|
|
||||||
* @param {number} value
|
|
||||||
* @param {object} options
|
|
||||||
*/
|
|
||||||
function noteDamageSource(value, options) {
|
|
||||||
const actors = resolveActorTargets(options?.targets);
|
|
||||||
if (!actors.some((a) => a?.id === ACTOR_ID)) return;
|
|
||||||
|
|
||||||
const label = buildSourceLabel(value, options);
|
|
||||||
if (!label) return;
|
|
||||||
state.sources.set(ACTOR_ID, { label, ts: Date.now() });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Try to describe where the HP change originated.
|
|
||||||
* @param {number} value
|
|
||||||
* @param {object} options
|
|
||||||
*/
|
|
||||||
function buildSourceLabel(value, options = {}) {
|
|
||||||
const info = options.message?.flags?.pf1?.identifiedInfo ?? {};
|
|
||||||
const metadata = options.message?.flags?.pf1?.metadata ?? {};
|
|
||||||
const fromChatFlavor = options.message?.flavor?.trim();
|
|
||||||
|
|
||||||
const actorDoc = resolveActorFromMetadata(metadata);
|
|
||||||
const itemDoc = actorDoc ? resolveItemFromMetadata(actorDoc, metadata) : null;
|
|
||||||
const actorName = actorDoc?.name ?? options.message?.speaker?.alias ?? null;
|
|
||||||
const actionName = info.actionName ?? info.name ?? itemDoc?.name ?? fromChatFlavor ?? null;
|
|
||||||
|
|
||||||
let label = null;
|
|
||||||
if (actorName && actionName) label = `${actorName} -> ${actionName}`;
|
|
||||||
else if (actionName) label = actionName;
|
|
||||||
else if (actorName) label = actorName;
|
|
||||||
else label = value < 0 ? "Healing" : "Damage";
|
|
||||||
|
|
||||||
if (options.isCritical) label += " (Critical)";
|
|
||||||
if (options.asNonlethal) label += " [Nonlethal]";
|
|
||||||
|
|
||||||
return label;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store the actor's current HP so the first change has a baseline.
|
|
||||||
* @param {Actor|null} actor
|
|
||||||
*/
|
|
||||||
function primeHpSnapshot(actor) {
|
|
||||||
const hp = readHpValue(actor);
|
|
||||||
if (hp === null) return;
|
|
||||||
state.current[ACTOR_ID] = hp;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve actors targeted by the damage application.
|
|
||||||
* Falls back to currently controlled tokens if no explicit targets were supplied.
|
|
||||||
* @param {Array<Actor|Token>} targets
|
|
||||||
* @returns {Actor[]}
|
|
||||||
*/
|
|
||||||
function resolveActorTargets(targets) {
|
|
||||||
let list = [];
|
|
||||||
if (Array.isArray(targets) && targets.length) list = targets;
|
|
||||||
else list = canvas?.tokens?.controlled ?? [];
|
|
||||||
|
|
||||||
return list
|
|
||||||
.map((entry) => {
|
|
||||||
if (!entry) return null;
|
|
||||||
if (entry instanceof Actor) return entry;
|
|
||||||
if (entry.actor) return entry.actor;
|
|
||||||
if (entry.document?.actor) return entry.document.actor;
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
.filter((actor) => actor instanceof Actor && actor.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read HP from the actor using several possible data paths.
|
|
||||||
* @param {Actor|null} actor
|
|
||||||
* @returns {number|null}
|
|
||||||
*/
|
|
||||||
function readHpValue(actor) {
|
|
||||||
if (!actor) return null;
|
|
||||||
|
|
||||||
const candidates = [
|
|
||||||
HP_PATH,
|
|
||||||
HP_PATH.replace(/^system\./, "data."),
|
|
||||||
HP_PATH.replace(/^system\./, ""),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const path of candidates) {
|
|
||||||
const value = foundry.utils.getProperty(actor, path);
|
|
||||||
if (value !== undefined) {
|
|
||||||
const numeric = Number(value);
|
|
||||||
return Number.isFinite(numeric) ? numeric : null;
|
|
||||||
}
|
|
||||||
if (actor.system) {
|
|
||||||
const trimmed = path.startsWith("system.") ? path.slice(7) : path;
|
|
||||||
const systemValue = foundry.utils.getProperty(actor.system, trimmed);
|
|
||||||
if (systemValue !== undefined) {
|
|
||||||
const numeric = Number(systemValue);
|
|
||||||
return Number.isFinite(numeric) ? numeric : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch the actor referenced in the chat card metadata, if any.
|
|
||||||
* @param {object} metadata
|
|
||||||
* @returns {Actor|null}
|
|
||||||
*/
|
|
||||||
function resolveActorFromMetadata(metadata = {}) {
|
|
||||||
if (!metadata.actor) return null;
|
|
||||||
|
|
||||||
if (typeof fromUuidSync === "function") {
|
|
||||||
try {
|
|
||||||
const doc = fromUuidSync(metadata.actor);
|
|
||||||
if (doc instanceof Actor) return doc;
|
|
||||||
} catch (err) {
|
|
||||||
console.warn("HP History | Failed to resolve actor UUID", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = metadata.actor.split(".").pop();
|
|
||||||
return game.actors.get(id) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch the item referenced in the chat card metadata from the given actor.
|
|
||||||
* @param {Actor} actor
|
|
||||||
* @param {object} metadata
|
|
||||||
* @returns {Item|null}
|
|
||||||
*/
|
|
||||||
function resolveItemFromMetadata(actor, metadata = {}) {
|
|
||||||
if (!metadata.item || !(actor?.items instanceof Collection)) return null;
|
|
||||||
return actor.items.get(metadata.item) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render the HP history heading and table HTML.
|
|
||||||
* @param {Array<object>} entries
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function renderHistoryBlock(entries) {
|
|
||||||
const rows = entries
|
|
||||||
.map(
|
|
||||||
(e) => `
|
|
||||||
<tr data-ts="${e.timestamp}">
|
|
||||||
<td>${new Date(e.timestamp).toLocaleString()}</td>
|
|
||||||
<td>${e.hp}</td>
|
|
||||||
<td>${e.diff}</td>
|
|
||||||
<td>${e.user ?? ""}</td>
|
|
||||||
<td>${e.source ?? ""}</td>
|
|
||||||
</tr>`
|
|
||||||
)
|
|
||||||
.join("");
|
|
||||||
|
|
||||||
const blockId = `${BLOCK_NAME}-${ACTOR_ID}`;
|
|
||||||
const table = `
|
|
||||||
${BLOCK_START}
|
|
||||||
<section data-history-block="${BLOCK_NAME}" data-history-id="${blockId}">
|
|
||||||
<details open>
|
|
||||||
<summary>===HP History=== (click to collapse)</summary>
|
|
||||||
<div class="history-controls">
|
|
||||||
<label>From <input type="date" data-filter="from"></label>
|
|
||||||
<label>To <input type="date" data-filter="to"></label>
|
|
||||||
</div>
|
|
||||||
<table class="hp-history" data-hp-history="1">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Timestamp</th>
|
|
||||||
<th>HP</th>
|
|
||||||
<th>Δ</th>
|
|
||||||
<th>User</th>
|
|
||||||
<th>Source</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
${rows}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</details>
|
|
||||||
<script type="text/javascript">
|
|
||||||
(function(){
|
|
||||||
const root = document.currentScript.closest('section[data-history-block="${BLOCK_NAME}"]');
|
|
||||||
if (!root) return;
|
|
||||||
const rows = Array.from(root.querySelectorAll("tbody tr"));
|
|
||||||
const fromInput = root.querySelector('input[data-filter="from"]');
|
|
||||||
const toInput = root.querySelector('input[data-filter="to"]');
|
|
||||||
function applyFilters(){
|
|
||||||
const from = fromInput?.value ? Date.parse(fromInput.value) : null;
|
|
||||||
const to = toInput?.value ? Date.parse(toInput.value) + 86399999 : null;
|
|
||||||
rows.forEach((row) => {
|
|
||||||
const ts = Number(row.dataset.ts);
|
|
||||||
const visible = (!from || ts >= from) && (!to || ts <= to);
|
|
||||||
row.style.display = visible ? "" : "none";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
fromInput?.addEventListener("input", applyFilters);
|
|
||||||
toInput?.addEventListener("input", applyFilters);
|
|
||||||
applyFilters();
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</section>
|
|
||||||
${BLOCK_END}`.trim();
|
|
||||||
|
|
||||||
return table;
|
|
||||||
}
|
|
||||||
|
|
||||||
function injectHistoryBlock(notes, block) {
|
|
||||||
const blockPattern = new RegExp(`${escapeRegExp(BLOCK_START)}[\\s\\S]*?${escapeRegExp(BLOCK_END)}`, "g");
|
|
||||||
let cleaned = notes.replace(blockPattern, "");
|
|
||||||
if (cleaned === notes) {
|
|
||||||
const sectionPattern = new RegExp(`<section[^>]*data-history-block="${BLOCK_NAME}"[^>]*>[\\s\\S]*?<\\/section>`, "g");
|
|
||||||
cleaned = notes.replace(sectionPattern, "");
|
|
||||||
}
|
|
||||||
cleaned = cleaned.trim();
|
|
||||||
const separator = cleaned.length && !cleaned.endsWith("\n") ? "\n\n" : "";
|
|
||||||
return `${cleaned}${separator}${block}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeRegExp(str) {
|
|
||||||
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
||||||
}
|
|
||||||
@@ -1,241 +0,0 @@
|
|||||||
/**
|
|
||||||
* Activate HP tracking for a single actor.
|
|
||||||
* Drop this script into a Foundry macro.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Change this lookup (or set ACTOR_ID directly) to match the actor you want to monitor.
|
|
||||||
const TARGET_ACTOR = game.actors.getName("Zeratal") ?? null;
|
|
||||||
const ACTOR_ID = TARGET_ACTOR?.id ?? "put-actor-id-here"; // e.g. replace string with actor id
|
|
||||||
const HP_PATH = "system.attributes.hp.value";
|
|
||||||
|
|
||||||
if (!ACTOR_ID || ACTOR_ID === "put-actor-id-here") {
|
|
||||||
return ui.notifications.warn("Set ACTOR_ID before running the macro.");
|
|
||||||
}
|
|
||||||
|
|
||||||
game.pf1 ??= {};
|
|
||||||
game.pf1.hpLogger ??= {};
|
|
||||||
game.pf1.hpLogger.sources ??= new Map();
|
|
||||||
game.pf1.hpLogger.current ??= {};
|
|
||||||
|
|
||||||
const logKey = `pf1-hp-log-${ACTOR_ID}`;
|
|
||||||
|
|
||||||
// Remove existing hook for the same actor so re-running stays idempotent.
|
|
||||||
if (game.pf1.hpLogger[logKey]) {
|
|
||||||
Hooks.off("updateActor", game.pf1.hpLogger[logKey]);
|
|
||||||
delete game.pf1.hpLogger[logKey];
|
|
||||||
}
|
|
||||||
|
|
||||||
ensureDamageSourceTracking();
|
|
||||||
primeHpSnapshot(game.actors.get(ACTOR_ID) ?? TARGET_ACTOR);
|
|
||||||
|
|
||||||
game.pf1.hpLogger[logKey] = Hooks.on("updateActor", (actor, change, options, userId) => {
|
|
||||||
if (actor.id !== ACTOR_ID) return;
|
|
||||||
|
|
||||||
const newHP = readHpValue(actor);
|
|
||||||
if (newHP === null) return;
|
|
||||||
|
|
||||||
const previous = game.pf1.hpLogger.current[ACTOR_ID];
|
|
||||||
if (previous === undefined) {
|
|
||||||
game.pf1.hpLogger.current[ACTOR_ID] = newHP;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (Object.is(newHP, previous)) return; // No HP change.
|
|
||||||
|
|
||||||
const diff = newHP - previous;
|
|
||||||
const diffText = diff >= 0 ? `+${diff}` : `${diff}`;
|
|
||||||
const user = game.users.get(userId);
|
|
||||||
const source = consumeDamageSource(actor.id) ?? inferManualSource(diff);
|
|
||||||
const sourceLine = source ? `<br><em>Source: ${source}</em>` : "";
|
|
||||||
|
|
||||||
ChatMessage.create({
|
|
||||||
content: `<strong>HP Monitor</strong><br>${actor.name} HP: ${previous} -> ${newHP} (${diffText})${user ? ` by ${user.name}` : ""}${sourceLine}`,
|
|
||||||
speaker: { alias: "System" },
|
|
||||||
});
|
|
||||||
|
|
||||||
game.pf1.hpLogger.current[ACTOR_ID] = newHP;
|
|
||||||
});
|
|
||||||
|
|
||||||
const actorName = game.actors.get(ACTOR_ID)?.name ?? TARGET_ACTOR?.name ?? ACTOR_ID;
|
|
||||||
ui.notifications.info(`HP change hook active for ${actorName}.`);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Consume and clear the stored damage source for this actor, if any.
|
|
||||||
* @param {string} actorId
|
|
||||||
*/
|
|
||||||
function consumeDamageSource(actorId) {
|
|
||||||
const entry = game.pf1.hpLogger.sources.get(actorId);
|
|
||||||
if (!entry) return null;
|
|
||||||
game.pf1.hpLogger.sources.delete(actorId);
|
|
||||||
return entry.label;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provide a fallback label when the change was manual or unidentified.
|
|
||||||
* @param {number} diff
|
|
||||||
*/
|
|
||||||
function inferManualSource(diff) {
|
|
||||||
if (!Number.isFinite(diff) || diff === 0) return null;
|
|
||||||
return diff > 0 ? "Manual healing" : "Manual damage";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure ActorPF.applyDamage is wrapped so we can capture the originating chat card.
|
|
||||||
*/
|
|
||||||
function ensureDamageSourceTracking() {
|
|
||||||
const state = game.pf1.hpLogger;
|
|
||||||
if (state.applyDamageWrapped) return;
|
|
||||||
|
|
||||||
const ActorPF = pf1?.documents?.actor?.ActorPF;
|
|
||||||
if (!ActorPF?.applyDamage) return;
|
|
||||||
|
|
||||||
const original = ActorPF.applyDamage;
|
|
||||||
ActorPF.applyDamage = async function wrappedApplyDamage(value, options = {}) {
|
|
||||||
try {
|
|
||||||
noteDamageSource(value, options);
|
|
||||||
} catch (err) {
|
|
||||||
console.warn("HP Logger | Failed to record damage source", err);
|
|
||||||
}
|
|
||||||
return original.call(this, value, options);
|
|
||||||
};
|
|
||||||
|
|
||||||
state.applyDamageWrapped = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Record the best-guess label for the HP change if the tracked actor is among the targets.
|
|
||||||
* @param {number} value
|
|
||||||
* @param {object} options
|
|
||||||
*/
|
|
||||||
function noteDamageSource(value, options) {
|
|
||||||
const actors = resolveActorTargets(options?.targets);
|
|
||||||
if (!actors.some((a) => a?.id === ACTOR_ID)) return;
|
|
||||||
|
|
||||||
const label = buildSourceLabel(value, options);
|
|
||||||
if (!label) return;
|
|
||||||
game.pf1.hpLogger.sources.set(ACTOR_ID, { label, ts: Date.now() });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Try to describe where the HP change originated.
|
|
||||||
* @param {number} value
|
|
||||||
* @param {object} options
|
|
||||||
*/
|
|
||||||
function buildSourceLabel(value, options = {}) {
|
|
||||||
const info = options.message?.flags?.pf1?.identifiedInfo ?? {};
|
|
||||||
const metadata = options.message?.flags?.pf1?.metadata ?? {};
|
|
||||||
const fromChatFlavor = options.message?.flavor?.trim();
|
|
||||||
|
|
||||||
const actorDoc = resolveActorFromMetadata(metadata);
|
|
||||||
const itemDoc = actorDoc ? resolveItemFromMetadata(actorDoc, metadata) : null;
|
|
||||||
const actorName = actorDoc?.name ?? options.message?.speaker?.alias ?? null;
|
|
||||||
const actionName = info.actionName ?? info.name ?? itemDoc?.name ?? fromChatFlavor ?? null;
|
|
||||||
|
|
||||||
let label = null;
|
|
||||||
if (actorName && actionName) label = `${actorName} -> ${actionName}`;
|
|
||||||
else if (actionName) label = actionName;
|
|
||||||
else if (actorName) label = actorName;
|
|
||||||
else label = value < 0 ? "Healing" : "Damage";
|
|
||||||
|
|
||||||
if (options.isCritical) label += " (Critical)";
|
|
||||||
if (options.asNonlethal) label += " [Nonlethal]";
|
|
||||||
|
|
||||||
return label;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store the actor's current HP so the first change has a baseline.
|
|
||||||
* @param {Actor|null} actor
|
|
||||||
*/
|
|
||||||
function primeHpSnapshot(actor) {
|
|
||||||
const hp = readHpValue(actor);
|
|
||||||
if (hp === null) return;
|
|
||||||
game.pf1.hpLogger.current[ACTOR_ID] = hp;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve actors targeted by the damage application.
|
|
||||||
* Falls back to currently controlled tokens if no explicit targets were supplied.
|
|
||||||
* @param {Array<Actor|Token>} targets
|
|
||||||
* @returns {Actor[]}
|
|
||||||
*/
|
|
||||||
function resolveActorTargets(targets) {
|
|
||||||
let list = [];
|
|
||||||
if (Array.isArray(targets) && targets.length) list = targets;
|
|
||||||
else list = canvas?.tokens?.controlled ?? [];
|
|
||||||
|
|
||||||
return list
|
|
||||||
.map((entry) => {
|
|
||||||
if (!entry) return null;
|
|
||||||
if (entry instanceof Actor) return entry;
|
|
||||||
if (entry.actor) return entry.actor;
|
|
||||||
if (entry.document?.actor) return entry.document.actor;
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
.filter((actor) => actor instanceof Actor && actor.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read HP from the actor using several possible data paths.
|
|
||||||
* @param {Actor|null} actor
|
|
||||||
* @returns {number|null}
|
|
||||||
*/
|
|
||||||
function readHpValue(actor) {
|
|
||||||
if (!actor) return null;
|
|
||||||
|
|
||||||
const candidates = [
|
|
||||||
HP_PATH,
|
|
||||||
HP_PATH.replace(/^system\./, "data."),
|
|
||||||
HP_PATH.replace(/^system\./, ""),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const path of candidates) {
|
|
||||||
const value = foundry.utils.getProperty(actor, path);
|
|
||||||
if (value !== undefined) {
|
|
||||||
const numeric = Number(value);
|
|
||||||
return Number.isFinite(numeric) ? numeric : null;
|
|
||||||
}
|
|
||||||
if (actor.system) {
|
|
||||||
const trimmed = path.startsWith("system.") ? path.slice(7) : path;
|
|
||||||
const systemValue = foundry.utils.getProperty(actor.system, trimmed);
|
|
||||||
if (systemValue !== undefined) {
|
|
||||||
const numeric = Number(systemValue);
|
|
||||||
return Number.isFinite(numeric) ? numeric : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch the actor referenced in the chat card metadata, if any.
|
|
||||||
* @param {object} metadata
|
|
||||||
* @returns {Actor|null}
|
|
||||||
*/
|
|
||||||
function resolveActorFromMetadata(metadata = {}) {
|
|
||||||
if (!metadata.actor) return null;
|
|
||||||
|
|
||||||
if (typeof fromUuidSync === "function") {
|
|
||||||
try {
|
|
||||||
const doc = fromUuidSync(metadata.actor);
|
|
||||||
if (doc instanceof Actor) return doc;
|
|
||||||
} catch (err) {
|
|
||||||
console.warn("HP Logger | Failed to resolve actor UUID", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: assume uuid ends with actor id
|
|
||||||
const id = metadata.actor.split(".").pop();
|
|
||||||
return game.actors.get(id) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch the item referenced in the chat card metadata from the given actor.
|
|
||||||
* @param {Actor} actor
|
|
||||||
* @param {object} metadata
|
|
||||||
* @returns {Item|null}
|
|
||||||
*/
|
|
||||||
function resolveItemFromMetadata(actor, metadata = {}) {
|
|
||||||
if (!metadata.item || !(actor?.items instanceof Collection)) return null;
|
|
||||||
return actor.items.get(metadata.item) ?? null;
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
/**
|
|
||||||
* Activate XP history tracking that stores entries in flags only.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const TARGET_ACTOR = game.actors.getName("Zeratal") ?? null;
|
|
||||||
const ACTOR_ID = TARGET_ACTOR?.id ?? "put-actor-id-here";
|
|
||||||
const XP_PATH = "system.details.xp.value";
|
|
||||||
const FLAG_SCOPE = "world";
|
|
||||||
const FLAG_KEY = "pf1XpHistory";
|
|
||||||
const MAX_ROWS = 50;
|
|
||||||
|
|
||||||
if (!ACTOR_ID || ACTOR_ID === "put-actor-id-here") {
|
|
||||||
return ui.notifications.warn("Set ACTOR_ID before running the macro.");
|
|
||||||
}
|
|
||||||
|
|
||||||
game.pf1 ??= {};
|
|
||||||
game.pf1.xpHistoryFlags ??= { current: {}, hooks: {} };
|
|
||||||
const state = game.pf1.xpHistoryFlags;
|
|
||||||
|
|
||||||
const logKey = `pf1-xp-history-flags-${ACTOR_ID}`;
|
|
||||||
if (state.hooks[logKey]) {
|
|
||||||
Hooks.off("updateActor", state.hooks[logKey]);
|
|
||||||
delete state.hooks[logKey];
|
|
||||||
}
|
|
||||||
|
|
||||||
primeSnapshot(game.actors.get(ACTOR_ID) ?? TARGET_ACTOR);
|
|
||||||
|
|
||||||
state.hooks[logKey] = Hooks.on("updateActor", (actor, change, options, userId) => {
|
|
||||||
if (actor.id !== ACTOR_ID) return;
|
|
||||||
|
|
||||||
const newXP = readXpValue(actor);
|
|
||||||
if (newXP === null) return;
|
|
||||||
|
|
||||||
const previous = state.current[ACTOR_ID];
|
|
||||||
if (previous === undefined) {
|
|
||||||
state.current[ACTOR_ID] = newXP;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (Object.is(newXP, previous)) return;
|
|
||||||
|
|
||||||
const diff = newXP - previous;
|
|
||||||
const diffText = diff >= 0 ? `+${diff}` : `${diff}`;
|
|
||||||
const user = game.users.get(userId);
|
|
||||||
|
|
||||||
state.current[ACTOR_ID] = newXP;
|
|
||||||
|
|
||||||
appendHistoryEntry(actor, {
|
|
||||||
value: newXP,
|
|
||||||
diff: diffText,
|
|
||||||
user: user?.name ?? "System",
|
|
||||||
}).catch((err) => console.error("XP History Flags | Failed to append entry", err));
|
|
||||||
});
|
|
||||||
|
|
||||||
const actorName = game.actors.get(ACTOR_ID)?.name ?? TARGET_ACTOR?.name ?? ACTOR_ID;
|
|
||||||
ui.notifications.info(`XP flag history tracking active for ${actorName}.`);
|
|
||||||
|
|
||||||
async function appendHistoryEntry(actor, entry) {
|
|
||||||
const existing = (await actor.getFlag(FLAG_SCOPE, FLAG_KEY)) ?? [];
|
|
||||||
existing.unshift({
|
|
||||||
timestamp: Date.now(),
|
|
||||||
value: entry.value,
|
|
||||||
diff: entry.diff,
|
|
||||||
user: entry.user,
|
|
||||||
});
|
|
||||||
if (existing.length > MAX_ROWS) existing.splice(MAX_ROWS);
|
|
||||||
|
|
||||||
await actor.setFlag(FLAG_SCOPE, FLAG_KEY, existing);
|
|
||||||
}
|
|
||||||
|
|
||||||
function primeSnapshot(actor) {
|
|
||||||
const xp = readXpValue(actor);
|
|
||||||
if (xp === null) return;
|
|
||||||
state.current[ACTOR_ID] = xp;
|
|
||||||
}
|
|
||||||
|
|
||||||
function readXpValue(actor) {
|
|
||||||
if (!actor) return null;
|
|
||||||
const direct = foundry.utils.getProperty(actor, XP_PATH);
|
|
||||||
if (direct !== undefined) {
|
|
||||||
const numeric = Number(direct);
|
|
||||||
return Number.isFinite(numeric) ? numeric : null;
|
|
||||||
}
|
|
||||||
const sys = foundry.utils.getProperty(actor.system ?? {}, XP_PATH.replace(/^system\./, ""));
|
|
||||||
if (sys !== undefined) {
|
|
||||||
const numeric = Number(sys);
|
|
||||||
return Number.isFinite(numeric) ? numeric : null;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@@ -1,285 +0,0 @@
|
|||||||
/**
|
|
||||||
* Track XP changes for a single actor and record them inside ===XP History=== notes.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const TARGET_ACTOR = game.actors.getName("Zeratal") ?? null;
|
|
||||||
const ACTOR_ID = TARGET_ACTOR?.id ?? "put-actor-id-here";
|
|
||||||
const XP_PATH = "system.details.xp.value";
|
|
||||||
const UPDATE_CONTEXT_FLAG = "xpHistory";
|
|
||||||
const FLAG_SCOPE = "world";
|
|
||||||
const FLAG_KEY = "pf1XpHistory";
|
|
||||||
const BLOCK_NAME = "pf1-xp-history";
|
|
||||||
const MAX_ROWS = 50;
|
|
||||||
const BLOCK_START = "<!-- XP_HISTORY_START -->";
|
|
||||||
const BLOCK_END = "<!-- XP_HISTORY_END -->";
|
|
||||||
|
|
||||||
if (!ACTOR_ID || ACTOR_ID === "put-actor-id-here") {
|
|
||||||
return ui.notifications.warn("Set ACTOR_ID before running the macro.");
|
|
||||||
}
|
|
||||||
|
|
||||||
game.pf1 ??= {};
|
|
||||||
game.pf1.xpHistory ??= {};
|
|
||||||
const state = game.pf1.xpHistory;
|
|
||||||
state.sources ??= new Map();
|
|
||||||
state.current ??= {};
|
|
||||||
state.hooks ??= {};
|
|
||||||
|
|
||||||
const logKey = `pf1-xp-history-${ACTOR_ID}`;
|
|
||||||
|
|
||||||
if (state.hooks[logKey]) {
|
|
||||||
Hooks.off("updateActor", state.hooks[logKey]);
|
|
||||||
delete state.hooks[logKey];
|
|
||||||
}
|
|
||||||
|
|
||||||
ensureDamageSourceTracking();
|
|
||||||
primeSnapshot(game.actors.get(ACTOR_ID) ?? TARGET_ACTOR);
|
|
||||||
|
|
||||||
state.hooks[logKey] = Hooks.on("updateActor", (actor, change, options, userId) => {
|
|
||||||
if (actor.id !== ACTOR_ID) return;
|
|
||||||
if (options?.[UPDATE_CONTEXT_FLAG]) return;
|
|
||||||
|
|
||||||
const newXP = readXpValue(actor);
|
|
||||||
if (newXP === null) return;
|
|
||||||
|
|
||||||
const previous = state.current[ACTOR_ID];
|
|
||||||
if (previous === undefined) {
|
|
||||||
state.current[ACTOR_ID] = newXP;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (Object.is(previous, newXP)) return;
|
|
||||||
|
|
||||||
const diff = newXP - previous;
|
|
||||||
const diffText = diff >= 0 ? `+${diff}` : `${diff}`;
|
|
||||||
const user = game.users.get(userId);
|
|
||||||
const source = consumeDamageSource(actor.id) ?? inferManualSource(diff);
|
|
||||||
|
|
||||||
state.current[ACTOR_ID] = newXP;
|
|
||||||
|
|
||||||
appendHistoryRow(actor, {
|
|
||||||
value: newXP,
|
|
||||||
diffText,
|
|
||||||
user,
|
|
||||||
source,
|
|
||||||
}).catch((err) => console.error("XP History | Failed to append entry", err));
|
|
||||||
});
|
|
||||||
|
|
||||||
const actorName = game.actors.get(ACTOR_ID)?.name ?? TARGET_ACTOR?.name ?? ACTOR_ID;
|
|
||||||
ui.notifications.info(`XP history tracking active for ${actorName}.`);
|
|
||||||
|
|
||||||
async function appendHistoryRow(actor, entry) {
|
|
||||||
const existing = (await actor.getFlag(FLAG_SCOPE, FLAG_KEY)) ?? [];
|
|
||||||
const newEntry = {
|
|
||||||
timestamp: Date.now(),
|
|
||||||
value: entry.value,
|
|
||||||
diff: entry.diffText,
|
|
||||||
user: entry.user?.name ?? "System",
|
|
||||||
source: entry.source ?? "",
|
|
||||||
};
|
|
||||||
existing.unshift(newEntry);
|
|
||||||
if (existing.length > MAX_ROWS) existing.splice(MAX_ROWS);
|
|
||||||
|
|
||||||
const notes = actor.system.details?.notes?.value ?? "";
|
|
||||||
const block = renderHistoryBlock(existing);
|
|
||||||
const updatedNotes = injectHistoryBlock(notes, block);
|
|
||||||
|
|
||||||
await actor.update(
|
|
||||||
{
|
|
||||||
[`flags.${FLAG_SCOPE}.${FLAG_KEY}`]: existing,
|
|
||||||
"system.details.notes.value": updatedNotes,
|
|
||||||
},
|
|
||||||
{ [UPDATE_CONTEXT_FLAG]: true }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderHistoryBlock(entries) {
|
|
||||||
const rows = entries
|
|
||||||
.map(
|
|
||||||
(e) => `
|
|
||||||
<tr data-ts="${e.timestamp}">
|
|
||||||
<td>${new Date(e.timestamp).toLocaleString()}</td>
|
|
||||||
<td>${e.value}</td>
|
|
||||||
<td>${e.diff}</td>
|
|
||||||
<td>${e.user ?? ""}</td>
|
|
||||||
<td>${e.source ?? ""}</td>
|
|
||||||
</tr>`
|
|
||||||
)
|
|
||||||
.join("");
|
|
||||||
|
|
||||||
const blockId = `${BLOCK_NAME}-${ACTOR_ID}`;
|
|
||||||
return `
|
|
||||||
${BLOCK_START}
|
|
||||||
<section data-history-block="${BLOCK_NAME}" data-history-id="${blockId}">
|
|
||||||
<details open>
|
|
||||||
<summary>===XP History=== (click to collapse)</summary>
|
|
||||||
<div class="history-controls">
|
|
||||||
<label>From <input type="date" data-filter="from"></label>
|
|
||||||
<label>To <input type="date" data-filter="to"></label>
|
|
||||||
</div>
|
|
||||||
<table class="xp-history" data-xp-history="1">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Timestamp</th>
|
|
||||||
<th>XP</th>
|
|
||||||
<th>Δ</th>
|
|
||||||
<th>User</th>
|
|
||||||
<th>Source</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
${rows}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</details>
|
|
||||||
<script type="text/javascript">
|
|
||||||
(function(){
|
|
||||||
const root = document.currentScript.closest('section[data-history-block="${BLOCK_NAME}"]');
|
|
||||||
if (!root) return;
|
|
||||||
const rows = Array.from(root.querySelectorAll("tbody tr"));
|
|
||||||
const fromInput = root.querySelector('input[data-filter="from"]');
|
|
||||||
const toInput = root.querySelector('input[data-filter="to"]');
|
|
||||||
function applyFilters(){
|
|
||||||
const from = fromInput?.value ? Date.parse(fromInput.value) : null;
|
|
||||||
const to = toInput?.value ? Date.parse(toInput.value) + 86399999 : null;
|
|
||||||
rows.forEach((row) => {
|
|
||||||
const ts = Number(row.dataset.ts);
|
|
||||||
const visible = (!from || ts >= from) && (!to || ts <= to);
|
|
||||||
row.style.display = visible ? "" : "none";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
fromInput?.addEventListener("input", applyFilters);
|
|
||||||
toInput?.addEventListener("input", applyFilters);
|
|
||||||
applyFilters();
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</section>
|
|
||||||
${BLOCK_END}`.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function injectHistoryBlock(notes, block) {
|
|
||||||
const blockPattern = new RegExp(`${escapeRegExp(BLOCK_START)}[\\s\\S]*?${escapeRegExp(BLOCK_END)}`, "g");
|
|
||||||
let cleaned = notes.replace(blockPattern, "");
|
|
||||||
if (cleaned === notes) {
|
|
||||||
const sectionPattern = new RegExp(`<section[^>]*data-history-block="${BLOCK_NAME}"[^>]*>[\\s\\S]*?<\\/section>`, "g");
|
|
||||||
cleaned = notes.replace(sectionPattern, "");
|
|
||||||
}
|
|
||||||
cleaned = cleaned.trim();
|
|
||||||
const separator = cleaned.length && !cleaned.endsWith("\n") ? "\n\n" : "";
|
|
||||||
return `${cleaned}${separator}${block}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function consumeDamageSource(actorId) {
|
|
||||||
const entry = state.sources.get(actorId);
|
|
||||||
if (!entry) return null;
|
|
||||||
state.sources.delete(actorId);
|
|
||||||
return entry.label;
|
|
||||||
}
|
|
||||||
|
|
||||||
function inferManualSource(diff) {
|
|
||||||
if (!Number.isFinite(diff) || diff === 0) return null;
|
|
||||||
return diff > 0 ? "Manual increase" : "Manual decrease";
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureDamageSourceTracking() {
|
|
||||||
if (state.applyDamageWrapped) return;
|
|
||||||
|
|
||||||
const ActorPF = pf1?.documents?.actor?.ActorPF;
|
|
||||||
if (!ActorPF?.applyDamage) return;
|
|
||||||
|
|
||||||
const original = ActorPF.applyDamage;
|
|
||||||
ActorPF.applyDamage = async function wrappedApplyDamage(value, options = {}) {
|
|
||||||
try {
|
|
||||||
noteDamageSource(value, options);
|
|
||||||
} catch (err) {
|
|
||||||
console.warn("XP History | Failed to record source", err);
|
|
||||||
}
|
|
||||||
return original.call(this, value, options);
|
|
||||||
};
|
|
||||||
|
|
||||||
state.applyDamageWrapped = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function noteDamageSource(value, options) {
|
|
||||||
const actors = resolveActorTargets(options?.targets);
|
|
||||||
if (!actors.some((a) => a?.id === ACTOR_ID)) return;
|
|
||||||
|
|
||||||
const label = buildSourceLabel(value, options);
|
|
||||||
if (!label) return;
|
|
||||||
state.sources.set(ACTOR_ID, { label, ts: Date.now() });
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildSourceLabel(value, options = {}) {
|
|
||||||
const info = options.message?.flags?.pf1?.identifiedInfo ?? {};
|
|
||||||
const metadata = options.message?.flags?.pf1?.metadata ?? {};
|
|
||||||
const fromChatFlavor = options.message?.flavor?.trim();
|
|
||||||
|
|
||||||
const actorDoc = resolveActorFromMetadata(metadata);
|
|
||||||
const itemDoc = actorDoc ? resolveItemFromMetadata(actorDoc, metadata) : null;
|
|
||||||
const actorName = actorDoc?.name ?? options.message?.speaker?.alias ?? null;
|
|
||||||
const actionName = info.actionName ?? info.name ?? itemDoc?.name ?? fromChatFlavor ?? null;
|
|
||||||
|
|
||||||
let label = null;
|
|
||||||
if (actorName && actionName) label = `${actorName} -> ${actionName}`;
|
|
||||||
else if (actionName) label = actionName;
|
|
||||||
else if (actorName) label = actorName;
|
|
||||||
else label = value < 0 ? "Decrease" : "Increase";
|
|
||||||
|
|
||||||
if (options.isCritical) label += " (Critical)";
|
|
||||||
if (options.asNonlethal) label += " [Nonlethal]";
|
|
||||||
|
|
||||||
return label;
|
|
||||||
}
|
|
||||||
|
|
||||||
function primeSnapshot(actor) {
|
|
||||||
const xp = readXpValue(actor);
|
|
||||||
if (xp === null) return;
|
|
||||||
state.current[ACTOR_ID] = xp;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveActorTargets(targets) {
|
|
||||||
let list = [];
|
|
||||||
if (Array.isArray(targets) && targets.length) list = targets;
|
|
||||||
else list = canvas?.tokens?.controlled ?? [];
|
|
||||||
|
|
||||||
return list
|
|
||||||
.map((entry) => {
|
|
||||||
if (!entry) return null;
|
|
||||||
if (entry instanceof Actor) return entry;
|
|
||||||
if (entry.actor) return entry.actor;
|
|
||||||
if (entry.document?.actor) return entry.document.actor;
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
.filter((actor) => actor instanceof Actor && actor.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function readXpValue(actor) {
|
|
||||||
if (!actor) return null;
|
|
||||||
const value = foundry.utils.getProperty(actor, XP_PATH) ?? foundry.utils.getProperty(actor.system ?? {}, XP_PATH.replace(/^system\./, ""));
|
|
||||||
if (value === undefined) return null;
|
|
||||||
const numeric = Number(value);
|
|
||||||
return Number.isFinite(numeric) ? numeric : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveActorFromMetadata(metadata = {}) {
|
|
||||||
if (!metadata.actor) return null;
|
|
||||||
|
|
||||||
if (typeof fromUuidSync === "function") {
|
|
||||||
try {
|
|
||||||
const doc = fromUuidSync(metadata.actor);
|
|
||||||
if (doc instanceof Actor) return doc;
|
|
||||||
} catch (err) {
|
|
||||||
console.warn("XP History | Failed to resolve actor UUID", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = metadata.actor.split(".").pop();
|
|
||||||
return game.actors.get(id) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveItemFromMetadata(actor, metadata = {}) {
|
|
||||||
if (!metadata.item || !(actor?.items instanceof Collection)) return null;
|
|
||||||
return actor.items.get(metadata.item) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeRegExp(str) {
|
|
||||||
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
/**
|
|
||||||
* Remove the HP history button hooks added by macro_enable-history-dialog.js.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const SHEET_EVENTS = [
|
|
||||||
"renderActorSheetPFCharacter",
|
|
||||||
"renderActorSheetPFNPC",
|
|
||||||
"renderActorSheetPFNPCLoot",
|
|
||||||
"renderActorSheetPFNPCLite",
|
|
||||||
"renderActorSheetPFTrap",
|
|
||||||
"renderActorSheetPFVehicle",
|
|
||||||
"renderActorSheetPFHaunt",
|
|
||||||
"renderActorSheetPFBasic",
|
|
||||||
];
|
|
||||||
|
|
||||||
const hooks = game.pf1?.historyDialog?.renderHooks ?? [];
|
|
||||||
if (!hooks.length) {
|
|
||||||
return ui.notifications.info("History dialog hooks are not active.");
|
|
||||||
}
|
|
||||||
|
|
||||||
hooks.forEach((hook, idx) => {
|
|
||||||
if (hook == null) return;
|
|
||||||
const isObject = typeof hook === "object";
|
|
||||||
const event = isObject ? hook.event : SHEET_EVENTS[idx];
|
|
||||||
const id = isObject ? hook.id : hook;
|
|
||||||
if (event && id !== undefined) Hooks.off(event, id);
|
|
||||||
});
|
|
||||||
|
|
||||||
delete game.pf1.historyDialog.renderHooks;
|
|
||||||
if (game.pf1?.historyDialog?.observers instanceof Map) {
|
|
||||||
for (const observer of game.pf1.historyDialog.observers.values()) {
|
|
||||||
observer?.disconnect();
|
|
||||||
}
|
|
||||||
game.pf1.historyDialog.observers.clear();
|
|
||||||
}
|
|
||||||
game.pf1.historyDialog.observers = new Map();
|
|
||||||
|
|
||||||
ui.notifications.info("History dialog hooks disabled. Reopen actor sheets to remove the button.");
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
/**
|
|
||||||
* Remove the History tab hooks added by macro_enable-history-tab.js.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const hooks = game.pf1?.historyTab?.renderHooks ?? (game.pf1?.historyTab?.renderHook ? [{ event: null, id: game.pf1.historyTab.renderHook }] : []);
|
|
||||||
if (!hooks.length) {
|
|
||||||
return ui.notifications.info("History tab hooks are not active.");
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const hook of hooks) {
|
|
||||||
if (!hook) continue;
|
|
||||||
Hooks.off(hook.event ?? "renderActorSheet", hook.id ?? hook);
|
|
||||||
}
|
|
||||||
|
|
||||||
delete game.pf1.historyTab.renderHooks;
|
|
||||||
delete game.pf1.historyTab.renderHook;
|
|
||||||
|
|
||||||
if (game.pf1.historyTab.observers instanceof Map) {
|
|
||||||
for (const observer of game.pf1.historyTab.observers.values()) {
|
|
||||||
observer?.disconnect();
|
|
||||||
}
|
|
||||||
delete game.pf1.historyTab.observers;
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.notifications.info("History tab hooks disabled. Close and reopen actor sheets to remove the tab.");
|
|
||||||
@@ -1,239 +0,0 @@
|
|||||||
/**
|
|
||||||
* Adds small log buttons to PF1 actor sheets (HP, XP, Currency headers).
|
|
||||||
* Buttons open a modal showing HP / XP / Currency logs stored in world flags.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const HISTORY_BUTTON_CLASS = "pf1-history-btn";
|
|
||||||
const BUTTON_TITLE = "Open Log";
|
|
||||||
const BUTTON_ICON = '<i class="fas fa-clock-rotate-left"></i>';
|
|
||||||
const BUTTON_STYLE =
|
|
||||||
"position:absolute;right:6px;top:4px;border:none;background:transparent;padding:0;width:18px;height:18px;display:flex;align-items:center;justify-content:center;color:inherit;cursor:pointer;";
|
|
||||||
|
|
||||||
const LOG_TARGETS = [
|
|
||||||
{ key: "hp", tab: "hp", finder: findHpContainer },
|
|
||||||
{ key: "xp", tab: "xp", finder: findXpContainer },
|
|
||||||
{ key: "currency", tab: "currency", finder: findCurrencyContainer },
|
|
||||||
];
|
|
||||||
|
|
||||||
const SHEET_EVENTS = [
|
|
||||||
"renderActorSheetPFCharacter",
|
|
||||||
"renderActorSheetPFNPC",
|
|
||||||
"renderActorSheetPFNPCLoot",
|
|
||||||
"renderActorSheetPFNPCLite",
|
|
||||||
"renderActorSheetPFTrap",
|
|
||||||
"renderActorSheetPFVehicle",
|
|
||||||
"renderActorSheetPFHaunt",
|
|
||||||
"renderActorSheetPFBasic",
|
|
||||||
];
|
|
||||||
|
|
||||||
game.pf1 ??= {};
|
|
||||||
game.pf1.historyDialog ??= {};
|
|
||||||
game.pf1.historyDialog.renderHooks ??= [];
|
|
||||||
game.pf1.historyDialog.observers ??= new Map();
|
|
||||||
|
|
||||||
if (game.pf1.historyDialog.renderHooks.length) {
|
|
||||||
return ui.notifications.info("Log buttons already active.");
|
|
||||||
}
|
|
||||||
|
|
||||||
game.pf1.historyDialog.renderHooks = SHEET_EVENTS.map((event) => {
|
|
||||||
const id = Hooks.on(event, (sheet) => {
|
|
||||||
const delays = [0, 100, 250];
|
|
||||||
delays.forEach((delay) =>
|
|
||||||
setTimeout(() => {
|
|
||||||
attachButtons(sheet);
|
|
||||||
}, delay)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
return { event, id };
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.notifications.info("PF1 log buttons enabled. Check the HP/XP/Currency headers.");
|
|
||||||
|
|
||||||
function attachButtons(sheet) {
|
|
||||||
for (const targetCfg of LOG_TARGETS) {
|
|
||||||
const container = targetCfg.finder(sheet.element);
|
|
||||||
if (!container?.length) continue;
|
|
||||||
addButton(container, sheet, targetCfg.tab, `${sheet.id}-${targetCfg.key}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addButton(container, sheet, tab, observerKey) {
|
|
||||||
if (!container.length) return;
|
|
||||||
if (container.find(`.${HISTORY_BUTTON_CLASS}[data-log-tab="${tab}"]`).length) return;
|
|
||||||
|
|
||||||
if (container.css("position") === "static") container.css("position", "relative");
|
|
||||||
|
|
||||||
const button = $(`
|
|
||||||
<button type="button" class="${HISTORY_BUTTON_CLASS}" data-log-tab="${tab}"
|
|
||||||
title="${BUTTON_TITLE}" style="${BUTTON_STYLE}">
|
|
||||||
${BUTTON_ICON}
|
|
||||||
</button>
|
|
||||||
`);
|
|
||||||
button.on("click", () => openHistoryDialog(sheet.actor, tab));
|
|
||||||
container.append(button);
|
|
||||||
|
|
||||||
observeContainer(container, sheet, tab, observerKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
function observeContainer(container, sheet, tab, key) {
|
|
||||||
if (game.pf1.historyDialog.observers.has(key)) return;
|
|
||||||
|
|
||||||
const observer = new MutationObserver(() => addButton(container, sheet, tab, key));
|
|
||||||
observer.observe(container[0], { childList: true });
|
|
||||||
game.pf1.historyDialog.observers.set(key, observer);
|
|
||||||
|
|
||||||
const cleanup = () => {
|
|
||||||
observer.disconnect();
|
|
||||||
game.pf1.historyDialog.observers.delete(key);
|
|
||||||
sheet.element.off("remove", cleanup);
|
|
||||||
};
|
|
||||||
sheet.element.on("remove", cleanup);
|
|
||||||
}
|
|
||||||
|
|
||||||
function findHpContainer(root) {
|
|
||||||
const header = root
|
|
||||||
.find("h3, label")
|
|
||||||
.filter((_, el) => el.textContent.trim() === "Hit Points")
|
|
||||||
.first();
|
|
||||||
if (!header.length) return null;
|
|
||||||
return header.parent().length ? header.parent() : header.closest(".attribute.hitpoints").first();
|
|
||||||
}
|
|
||||||
|
|
||||||
function findXpContainer(root) {
|
|
||||||
const box = root.find(".info-box.experience").first();
|
|
||||||
if (box.length) return box;
|
|
||||||
const header = root
|
|
||||||
.find("h5, label")
|
|
||||||
.filter((_, el) => el.textContent.trim().toLowerCase() === "experience")
|
|
||||||
.first();
|
|
||||||
return header.length ? header.parent() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function findCurrencyContainer(root) {
|
|
||||||
const header = root
|
|
||||||
.find("h3, label")
|
|
||||||
.filter((_, el) => el.textContent.trim().toLowerCase() === "currency")
|
|
||||||
.first();
|
|
||||||
if (header.length) return header.parent().length ? header.parent() : header;
|
|
||||||
return root.find(".currency").first();
|
|
||||||
}
|
|
||||||
|
|
||||||
function openHistoryDialog(actor, initialTab = "hp") {
|
|
||||||
if (!actor) return;
|
|
||||||
const content = buildHistoryContent(actor);
|
|
||||||
const dlg = new Dialog(
|
|
||||||
{
|
|
||||||
title: `${actor.name}: Log`,
|
|
||||||
content,
|
|
||||||
buttons: { close: { label: "Close" } },
|
|
||||||
render: (html) => setupTabs(html, initialTab),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
width: 720,
|
|
||||||
classes: ["pf1-history-dialog"],
|
|
||||||
}
|
|
||||||
);
|
|
||||||
dlg.render(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildHistoryContent(actor) {
|
|
||||||
const configs = [
|
|
||||||
{
|
|
||||||
id: "hp",
|
|
||||||
label: "HP",
|
|
||||||
flag: "pf1HpHistory",
|
|
||||||
columns: [
|
|
||||||
{ label: "Timestamp", render: (e) => formatDate(e.timestamp) },
|
|
||||||
{ label: "HP", render: (e) => e.hp },
|
|
||||||
{ label: "Δ", render: (e) => e.diff },
|
|
||||||
{ label: "User", render: (e) => e.user ?? "" },
|
|
||||||
{ label: "Source", render: (e) => e.source ?? "" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "xp",
|
|
||||||
label: "XP",
|
|
||||||
flag: "pf1XpHistory",
|
|
||||||
columns: [
|
|
||||||
{ label: "Timestamp", render: (e) => formatDate(e.timestamp) },
|
|
||||||
{ label: "XP", render: (e) => e.value },
|
|
||||||
{ label: "Δ", render: (e) => e.diff },
|
|
||||||
{ label: "User", render: (e) => e.user ?? "" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "currency",
|
|
||||||
label: "Currency",
|
|
||||||
flag: "pf1CurrencyHistory",
|
|
||||||
columns: [
|
|
||||||
{ label: "Timestamp", render: (e) => formatDate(e.timestamp) },
|
|
||||||
{ label: "Totals", render: (e) => e.value },
|
|
||||||
{ label: "Δ", render: (e) => e.diff },
|
|
||||||
{ label: "User", render: (e) => e.user ?? "" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const tabs = configs
|
|
||||||
.map((cfg) => `<a class="history-tab" data-history-tab="${cfg.id}">${cfg.label}</a>`)
|
|
||||||
.join("");
|
|
||||||
|
|
||||||
const panels = configs
|
|
||||||
.map((cfg) => {
|
|
||||||
const entries = actor.getFlag("world", cfg.flag) ?? [];
|
|
||||||
const table = renderHistoryTable(entries, cfg.columns, cfg.id);
|
|
||||||
return `<section class="history-panel" data-history-panel="${cfg.id}">${table}</section>`;
|
|
||||||
})
|
|
||||||
.join("");
|
|
||||||
|
|
||||||
return `
|
|
||||||
<section class="history-dialog-root" data-history-root="${actor.id}">
|
|
||||||
<style>
|
|
||||||
.history-dialog-tabs { display:flex; gap:6px; margin-bottom:8px; }
|
|
||||||
.history-tab { flex:1; text-align:center; padding:4px 0; border:1px solid #b5b3a4; border-radius:3px; cursor:pointer; background:#ddd; }
|
|
||||||
.history-tab.active { background:#fff; font-weight:bold; }
|
|
||||||
</style>
|
|
||||||
<nav class="history-dialog-tabs">${tabs}</nav>
|
|
||||||
<div class="history-dialog-panels">${panels}</div>
|
|
||||||
</section>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupTabs(html, initialTab) {
|
|
||||||
const buttons = Array.from(html[0].querySelectorAll(".history-tab"));
|
|
||||||
const panels = Array.from(html[0].querySelectorAll(".history-panel"));
|
|
||||||
const activate = (target) => {
|
|
||||||
buttons.forEach((btn) => btn.classList.toggle("active", btn.dataset.historyTab === target));
|
|
||||||
panels.forEach((panel) => (panel.style.display = panel.dataset.historyPanel === target ? "block" : "none"));
|
|
||||||
};
|
|
||||||
buttons.forEach((btn) => btn.addEventListener("click", () => activate(btn.dataset.historyTab)));
|
|
||||||
activate(initialTab);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderHistoryTable(entries, columns, id) {
|
|
||||||
if (!entries.length) {
|
|
||||||
return `<p class="history-empty">No ${id.toUpperCase()} history recorded.</p>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = entries
|
|
||||||
.map(
|
|
||||||
(entry) => `
|
|
||||||
<tr>
|
|
||||||
${columns.map((col) => `<td>${col.render(entry) ?? ""}</td>`).join("")}
|
|
||||||
</tr>`
|
|
||||||
)
|
|
||||||
.join("");
|
|
||||||
|
|
||||||
return `
|
|
||||||
<table class="history-table history-${id}">
|
|
||||||
<thead>
|
|
||||||
<tr>${columns.map((col) => `<th>${col.label}</th>`).join("")}</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
${rows}
|
|
||||||
</tbody>
|
|
||||||
</table>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDate(ts) {
|
|
||||||
return ts ? new Date(ts).toLocaleString() : "";
|
|
||||||
}
|
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
/**
|
|
||||||
* Add a History tab to PF1 actor sheets with HP/XP/Currency subtabs.
|
|
||||||
* Requires the corresponding flag-tracking macros to populate data.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const TAB_ID = "pf1-history";
|
|
||||||
const TAB_LABEL = "History";
|
|
||||||
|
|
||||||
if (!game.system.id.includes("pf1")) {
|
|
||||||
ui.notifications.warn("This macro is intended for the PF1 system.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
game.pf1 ??= {};
|
|
||||||
game.pf1.historyTab ??= {};
|
|
||||||
|
|
||||||
const PF1_SHEET_HOOKS = [
|
|
||||||
"renderActorSheetPFCharacter",
|
|
||||||
"renderActorSheetPFNPC",
|
|
||||||
"renderActorSheetPFNPCLoot",
|
|
||||||
"renderActorSheetPFNPCLite",
|
|
||||||
"renderActorSheetPFTrap",
|
|
||||||
"renderActorSheetPFVehicle",
|
|
||||||
"renderActorSheetPFHaunt",
|
|
||||||
"renderActorSheetPFBasic",
|
|
||||||
];
|
|
||||||
|
|
||||||
game.pf1.historyTab.renderHooks ??= [];
|
|
||||||
if (game.pf1.historyTab.renderHooks.length) {
|
|
||||||
return ui.notifications.info("History tab hooks already active.");
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const hook of PF1_SHEET_HOOKS) {
|
|
||||||
const id = Hooks.on(hook, (sheet, html) => {
|
|
||||||
const delays = [0, 50, 200];
|
|
||||||
delays.forEach((delay) =>
|
|
||||||
setTimeout(() => {
|
|
||||||
try {
|
|
||||||
injectHistoryTab(sheet);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("PF1 History Tab | Failed to render history tab", err);
|
|
||||||
}
|
|
||||||
}, delay)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
game.pf1.historyTab.renderHooks.push({ event: hook, id });
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.notifications.info("PF1 History tab enabled. Reopen actor sheets to see the new tab.");
|
|
||||||
|
|
||||||
function injectHistoryTab(sheet) {
|
|
||||||
if (!sheet?.element?.length) return;
|
|
||||||
console.log("PF1 History Tab | Injecting for", sheet.actor?.name, sheet.id);
|
|
||||||
const actor = sheet.actor;
|
|
||||||
const nav = sheet.element.find('.sheet-navigation.tabs[data-group="primary"]').first();
|
|
||||||
const body = sheet.element.find(".sheet-body").first();
|
|
||||||
if (!nav.length || !body.length) return;
|
|
||||||
|
|
||||||
ensureHistoryTab(nav);
|
|
||||||
|
|
||||||
const existing = body.find(`.tab[data-tab="${TAB_ID}"]`).first();
|
|
||||||
const content = buildHistoryContent(actor);
|
|
||||||
if (existing.length) existing.html(content);
|
|
||||||
else body.append(`<div class="tab history-tab" data-tab="${TAB_ID}" data-group="primary">${content}</div>`);
|
|
||||||
|
|
||||||
sheet._tabs?.forEach((tabs) => tabs.bind(sheet.element[0]));
|
|
||||||
|
|
||||||
setupObserver(sheet, nav);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureHistoryTab(nav) {
|
|
||||||
if (nav.find(`[data-tab="${TAB_ID}"]`).length) return;
|
|
||||||
nav.append(`<a class="item" data-group="primary" data-tab="${TAB_ID}">${TAB_LABEL}</a>`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupObserver(sheet, nav) {
|
|
||||||
game.pf1.historyTab.observers ??= new Map();
|
|
||||||
const key = sheet.id;
|
|
||||||
if (game.pf1.historyTab.observers.has(key)) return;
|
|
||||||
|
|
||||||
const observer = new MutationObserver(() => {
|
|
||||||
const currentNav = sheet.element.find('.sheet-navigation.tabs[data-group="primary"]').first();
|
|
||||||
if (!currentNav.length) return;
|
|
||||||
ensureHistoryTab(currentNav);
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(sheet.element[0], { childList: true, subtree: true });
|
|
||||||
game.pf1.historyTab.observers.set(key, observer);
|
|
||||||
|
|
||||||
const closeHandler = () => {
|
|
||||||
observer.disconnect();
|
|
||||||
game.pf1.historyTab.observers.delete(key);
|
|
||||||
sheet.element.off("remove", closeHandler);
|
|
||||||
};
|
|
||||||
sheet.element.on("remove", closeHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildHistoryContent(actor) {
|
|
||||||
const configs = [
|
|
||||||
{
|
|
||||||
id: "hp",
|
|
||||||
label: "HP",
|
|
||||||
flag: "pf1HpHistory",
|
|
||||||
columns: [
|
|
||||||
{ label: "Timestamp", render: (e) => formatDate(e.timestamp) },
|
|
||||||
{ label: "HP", render: (e) => e.hp },
|
|
||||||
{ label: "Δ", render: (e) => e.diff },
|
|
||||||
{ label: "User", render: (e) => e.user ?? "" },
|
|
||||||
{ label: "Source", render: (e) => e.source ?? "" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "xp",
|
|
||||||
label: "XP",
|
|
||||||
flag: "pf1XpHistory",
|
|
||||||
columns: [
|
|
||||||
{ label: "Timestamp", render: (e) => formatDate(e.timestamp) },
|
|
||||||
{ label: "XP", render: (e) => e.value },
|
|
||||||
{ label: "Δ", render: (e) => e.diff },
|
|
||||||
{ label: "User", render: (e) => e.user ?? "" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "currency",
|
|
||||||
label: "Currency",
|
|
||||||
flag: "pf1CurrencyHistory",
|
|
||||||
columns: [
|
|
||||||
{ label: "Timestamp", render: (e) => formatDate(e.timestamp) },
|
|
||||||
{ label: "Totals", render: (e) => e.value },
|
|
||||||
{ label: "Δ", render: (e) => e.diff },
|
|
||||||
{ label: "User", render: (e) => e.user ?? "" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const nav = configs
|
|
||||||
.map((cfg, idx) => `<a class="history-subtab ${idx === 0 ? "active" : ""}" data-history-subtab="${cfg.id}">${cfg.label}</a>`)
|
|
||||||
.join("");
|
|
||||||
|
|
||||||
const panels = configs
|
|
||||||
.map((cfg, idx) => {
|
|
||||||
const entries = actor.getFlag("world", cfg.flag) ?? [];
|
|
||||||
const table = renderHistoryTable(entries, cfg.columns, cfg.id);
|
|
||||||
return `<section class="history-panel" data-history-panel="${cfg.id}" style="display:${idx === 0 ? "block" : "none"}">${table}</section>`;
|
|
||||||
})
|
|
||||||
.join("");
|
|
||||||
|
|
||||||
return `
|
|
||||||
<section class="pf1-history-root" data-history-block="history-tab">
|
|
||||||
<nav class="history-subnav">
|
|
||||||
${nav}
|
|
||||||
</nav>
|
|
||||||
<div class="history-panels">
|
|
||||||
${panels}
|
|
||||||
</div>
|
|
||||||
<script type="text/javascript">
|
|
||||||
(function(){
|
|
||||||
const root = document.currentScript.closest('[data-history-block="history-tab"]');
|
|
||||||
if (!root) return;
|
|
||||||
const tabs = Array.from(root.querySelectorAll('[data-history-subtab]'));
|
|
||||||
const panels = Array.from(root.querySelectorAll('[data-history-panel]'));
|
|
||||||
tabs.forEach((btn) => {
|
|
||||||
btn.addEventListener('click', () => {
|
|
||||||
const target = btn.dataset.historySubtab;
|
|
||||||
tabs.forEach((n) => n.classList.toggle('active', n === btn));
|
|
||||||
panels.forEach((panel) => (panel.style.display = panel.dataset.historyPanel === target ? 'block' : 'none'));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</section>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderHistoryTable(entries, columns, id) {
|
|
||||||
if (!entries.length) return `<p class="history-empty">No ${id.toUpperCase()} history recorded.</p>`;
|
|
||||||
|
|
||||||
const rows = entries
|
|
||||||
.map(
|
|
||||||
(entry) => `
|
|
||||||
<tr data-ts="${entry.timestamp}">
|
|
||||||
${columns.map((col) => `<td>${col.render(entry) ?? ""}</td>`).join("")}
|
|
||||||
</tr>`
|
|
||||||
)
|
|
||||||
.join("");
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="history-filters">
|
|
||||||
<label>From <input type="date" data-filter="from-${id}"></label>
|
|
||||||
<label>To <input type="date" data-filter="to-${id}"></label>
|
|
||||||
</div>
|
|
||||||
<table class="history-table history-${id}">
|
|
||||||
<thead>
|
|
||||||
<tr>${columns.map((col) => `<th>${col.label}</th>`).join("")}</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
${rows}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<script type="text/javascript">
|
|
||||||
(function(){
|
|
||||||
const root = document.currentScript.closest('.history-panel[data-history-panel="${id}"]');
|
|
||||||
if (!root) return;
|
|
||||||
const rows = Array.from(root.querySelectorAll("tbody tr"));
|
|
||||||
const fromInput = root.querySelector('input[data-filter="from-${id}"]');
|
|
||||||
const toInput = root.querySelector('input[data-filter="to-${id}"]');
|
|
||||||
function applyFilters(){
|
|
||||||
const from = fromInput?.value ? Date.parse(fromInput.value) : null;
|
|
||||||
const to = toInput?.value ? Date.parse(toInput.value) + 86399999 : null;
|
|
||||||
rows.forEach((row) => {
|
|
||||||
const ts = Number(row.dataset.ts);
|
|
||||||
const visible = (!from || ts >= from) && (!to || ts <= to);
|
|
||||||
row.style.display = visible ? "" : "none";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
fromInput?.addEventListener("input", applyFilters);
|
|
||||||
toInput?.addEventListener("input", applyFilters);
|
|
||||||
applyFilters();
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDate(ts) {
|
|
||||||
return new Date(ts).toLocaleString();
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
/**
|
|
||||||
* Stop currency flag history tracking (tab-based version).
|
|
||||||
*/
|
|
||||||
|
|
||||||
const TARGET_ACTOR = game.actors.getName("Zeratal") ?? null;
|
|
||||||
const ACTOR_ID = TARGET_ACTOR?.id ?? "put-actor-id-here";
|
|
||||||
|
|
||||||
if (!ACTOR_ID || ACTOR_ID === "put-actor-id-here") {
|
|
||||||
return ui.notifications.warn("Set ACTOR_ID before running the macro.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = game.pf1?.currencyHistoryFlags;
|
|
||||||
if (!state) return ui.notifications.info("Currency flag history tracking is not active.");
|
|
||||||
|
|
||||||
const logKey = `pf1-currency-history-flags-${ACTOR_ID}`;
|
|
||||||
const handler = state.hooks?.[logKey];
|
|
||||||
|
|
||||||
if (!handler) {
|
|
||||||
return ui.notifications.info(`No currency flag history hook found for ${TARGET_ACTOR?.name ?? ACTOR_ID}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
Hooks.off("updateActor", handler);
|
|
||||||
delete state.hooks[logKey];
|
|
||||||
if (state.current) delete state.current[ACTOR_ID];
|
|
||||||
|
|
||||||
const actorName = game.actors.get(ACTOR_ID)?.name ?? TARGET_ACTOR?.name ?? ACTOR_ID;
|
|
||||||
ui.notifications.info(`Currency flag history tracking disabled for ${actorName}.`);
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
/**
|
|
||||||
* Stop currency history tracking for the configured actor.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const TARGET_ACTOR = game.actors.getName("Zeratal") ?? null;
|
|
||||||
const ACTOR_ID = TARGET_ACTOR?.id ?? "put-actor-id-here";
|
|
||||||
|
|
||||||
if (!ACTOR_ID || ACTOR_ID === "put-actor-id-here") {
|
|
||||||
return ui.notifications.warn("Set ACTOR_ID before running the macro.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = game.pf1?.currencyHistory;
|
|
||||||
if (!state) return ui.notifications.info("Currency history tracking is not active.");
|
|
||||||
|
|
||||||
const logKey = `pf1-currency-history-${ACTOR_ID}`;
|
|
||||||
const handler = state.hooks?.[logKey];
|
|
||||||
|
|
||||||
if (!handler) {
|
|
||||||
return ui.notifications.info(`No currency history hook found for ${TARGET_ACTOR?.name ?? ACTOR_ID}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
Hooks.off("updateActor", handler);
|
|
||||||
delete state.hooks[logKey];
|
|
||||||
state.sources?.delete(ACTOR_ID);
|
|
||||||
if (state.current) delete state.current[ACTOR_ID];
|
|
||||||
|
|
||||||
const actorName = game.actors.get(ACTOR_ID)?.name ?? TARGET_ACTOR?.name ?? ACTOR_ID;
|
|
||||||
ui.notifications.info(`Currency history tracking disabled for ${actorName}.`);
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
/**
|
|
||||||
* Stop HP flag history tracking (tab-based version).
|
|
||||||
*/
|
|
||||||
|
|
||||||
const TARGET_ACTOR = game.actors.getName("Zeratal") ?? null;
|
|
||||||
const ACTOR_ID = TARGET_ACTOR?.id ?? "put-actor-id-here";
|
|
||||||
|
|
||||||
if (!ACTOR_ID || ACTOR_ID === "put-actor-id-here") {
|
|
||||||
return ui.notifications.warn("Set ACTOR_ID before running the macro.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = game.pf1?.hpHistoryFlags;
|
|
||||||
if (!state) return ui.notifications.info("HP flag history tracking is not active.");
|
|
||||||
|
|
||||||
const logKey = `pf1-hp-history-flags-${ACTOR_ID}`;
|
|
||||||
const handler = state.hooks?.[logKey];
|
|
||||||
|
|
||||||
if (!handler) {
|
|
||||||
return ui.notifications.info(`No HP flag history hook found for ${TARGET_ACTOR?.name ?? ACTOR_ID}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
Hooks.off("updateActor", handler);
|
|
||||||
delete state.hooks[logKey];
|
|
||||||
state.sources?.delete(ACTOR_ID);
|
|
||||||
if (state.current) delete state.current[ACTOR_ID];
|
|
||||||
|
|
||||||
const actorName = game.actors.get(ACTOR_ID)?.name ?? TARGET_ACTOR?.name ?? ACTOR_ID;
|
|
||||||
ui.notifications.info(`HP flag history tracking disabled for ${actorName}.`);
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
/**
|
|
||||||
* Deactivate the HP history tracker and stop writing to the Notes table.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const TARGET_ACTOR = game.actors.getName("Zeratal") ?? null;
|
|
||||||
const ACTOR_ID = TARGET_ACTOR?.id ?? "put-actor-id-here";
|
|
||||||
|
|
||||||
if (!ACTOR_ID || ACTOR_ID === "put-actor-id-here") {
|
|
||||||
return ui.notifications.warn("Set ACTOR_ID before running the macro.");
|
|
||||||
}
|
|
||||||
|
|
||||||
game.pf1 ??= {};
|
|
||||||
const state = game.pf1.hpHistory;
|
|
||||||
|
|
||||||
if (!state) {
|
|
||||||
return ui.notifications.info("HP history tracking is not active.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const logKey = `pf1-hp-history-${ACTOR_ID}`;
|
|
||||||
const handler = state.hooks?.[logKey];
|
|
||||||
|
|
||||||
if (!handler) {
|
|
||||||
return ui.notifications.info(`No HP history hook found for ${TARGET_ACTOR?.name ?? ACTOR_ID}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
Hooks.off("updateActor", handler);
|
|
||||||
delete state.hooks[logKey];
|
|
||||||
state.sources?.delete(ACTOR_ID);
|
|
||||||
if (state.current) delete state.current[ACTOR_ID];
|
|
||||||
|
|
||||||
const actorName = game.actors.get(ACTOR_ID)?.name ?? TARGET_ACTOR?.name ?? ACTOR_ID;
|
|
||||||
ui.notifications.info(`HP history tracking disabled for ${actorName}.`);
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
/**
|
|
||||||
* Deactivate HP tracking for the configured actor.
|
|
||||||
* Use after running macro_activate-hp-tracking.js to stop logging.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const TARGET_ACTOR = game.actors.getName("Zeratal") ?? null;
|
|
||||||
const ACTOR_ID = TARGET_ACTOR?.id ?? "put-actor-id-here";
|
|
||||||
|
|
||||||
if (!ACTOR_ID || ACTOR_ID === "put-actor-id-here") {
|
|
||||||
return ui.notifications.warn("Set ACTOR_ID before running the macro.");
|
|
||||||
}
|
|
||||||
|
|
||||||
game.pf1 ??= {};
|
|
||||||
game.pf1.hpLogger ??= {};
|
|
||||||
|
|
||||||
const logKey = `pf1-hp-log-${ACTOR_ID}`;
|
|
||||||
const handler = game.pf1.hpLogger[logKey];
|
|
||||||
|
|
||||||
if (!handler) {
|
|
||||||
return ui.notifications.info(`No HP tracking hook found for ${TARGET_ACTOR?.name ?? ACTOR_ID}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
Hooks.off("updateActor", handler);
|
|
||||||
delete game.pf1.hpLogger[logKey];
|
|
||||||
|
|
||||||
if (game.pf1.hpLogger.sources instanceof Map) {
|
|
||||||
game.pf1.hpLogger.sources.delete(ACTOR_ID);
|
|
||||||
}
|
|
||||||
if (game.pf1.hpLogger.current) {
|
|
||||||
delete game.pf1.hpLogger.current[ACTOR_ID];
|
|
||||||
}
|
|
||||||
|
|
||||||
const actorName = game.actors.get(ACTOR_ID)?.name ?? TARGET_ACTOR?.name ?? ACTOR_ID;
|
|
||||||
ui.notifications.info(`HP tracking disabled for ${actorName}.`);
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
/**
|
|
||||||
* Stop XP flag history tracking (tab-based version).
|
|
||||||
*/
|
|
||||||
|
|
||||||
const TARGET_ACTOR = game.actors.getName("Zeratal") ?? null;
|
|
||||||
const ACTOR_ID = TARGET_ACTOR?.id ?? "put-actor-id-here";
|
|
||||||
|
|
||||||
if (!ACTOR_ID || ACTOR_ID === "put-actor-id-here") {
|
|
||||||
return ui.notifications.warn("Set ACTOR_ID before running the macro.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = game.pf1?.xpHistoryFlags;
|
|
||||||
if (!state) return ui.notifications.info("XP flag history tracking is not active.");
|
|
||||||
|
|
||||||
const logKey = `pf1-xp-history-flags-${ACTOR_ID}`;
|
|
||||||
const handler = state.hooks?.[logKey];
|
|
||||||
|
|
||||||
if (!handler) {
|
|
||||||
return ui.notifications.info(`No XP flag history hook found for ${TARGET_ACTOR?.name ?? ACTOR_ID}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
Hooks.off("updateActor", handler);
|
|
||||||
delete state.hooks[logKey];
|
|
||||||
if (state.current) delete state.current[ACTOR_ID];
|
|
||||||
|
|
||||||
const actorName = game.actors.get(ACTOR_ID)?.name ?? TARGET_ACTOR?.name ?? ACTOR_ID;
|
|
||||||
ui.notifications.info(`XP flag history tracking disabled for ${actorName}.`);
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
/**
|
|
||||||
* Stop XP history tracking for the configured actor.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const TARGET_ACTOR = game.actors.getName("Zeratal") ?? null;
|
|
||||||
const ACTOR_ID = TARGET_ACTOR?.id ?? "put-actor-id-here";
|
|
||||||
|
|
||||||
if (!ACTOR_ID || ACTOR_ID === "put-actor-id-here") {
|
|
||||||
return ui.notifications.warn("Set ACTOR_ID before running the macro.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = game.pf1?.xpHistory;
|
|
||||||
if (!state) return ui.notifications.info("XP history tracking is not active.");
|
|
||||||
|
|
||||||
const logKey = `pf1-xp-history-${ACTOR_ID}`;
|
|
||||||
const handler = state.hooks?.[logKey];
|
|
||||||
|
|
||||||
if (!handler) {
|
|
||||||
return ui.notifications.info(`No XP history hook found for ${TARGET_ACTOR?.name ?? ACTOR_ID}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
Hooks.off("updateActor", handler);
|
|
||||||
delete state.hooks[logKey];
|
|
||||||
state.sources?.delete(ACTOR_ID);
|
|
||||||
if (state.current) delete state.current[ACTOR_ID];
|
|
||||||
|
|
||||||
const actorName = game.actors.get(ACTOR_ID)?.name ?? TARGET_ACTOR?.name ?? ACTOR_ID;
|
|
||||||
ui.notifications.info(`XP history tracking disabled for ${actorName}.`);
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"title": "Gowler's Tracking Ledger",
|
"title": "Gowler's Tracking Ledger",
|
||||||
"description": "Adds HP/XP/Currency log buttons to PF1 sheets and opens the tracking dialog preloaded with the actor's logs.",
|
"description": "Adds HP/XP/Currency log buttons to PF1 sheets and opens the tracking dialog preloaded with the actor's logs.",
|
||||||
"version": "0.1.21",
|
"version": "1.3.2",
|
||||||
"authors": [
|
"authors": [
|
||||||
{ "name": "Gowler", "url": "https://foundryvtt.com" }
|
{ "name": "Gowler", "url": "https://foundryvtt.com" }
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
const MODULE_ID = "gowlers-tracking-ledger";
|
const MODULE_ID = "gowlers-tracking-ledger";
|
||||||
const MODULE_VERSION = "1.2.1";
|
const MODULE_VERSION = "1.3.3";
|
||||||
const TRACK_SETTING = "actorSettings";
|
const TRACK_SETTING = "actorSettings";
|
||||||
const FLAG_SCOPE = "world";
|
const FLAG_SCOPE = "world";
|
||||||
const MAX_HISTORY_ROWS = 100;
|
const MAX_HISTORY_ROWS = 100;
|
||||||
@@ -15,6 +15,33 @@ const SETTINGS_VERSION = 2;
|
|||||||
const COIN_ORDER = ["pp", "gp", "sp", "cp"];
|
const COIN_ORDER = ["pp", "gp", "sp", "cp"];
|
||||||
const ENCOUNTER_FLAG = "pf1EncounterHistory";
|
const ENCOUNTER_FLAG = "pf1EncounterHistory";
|
||||||
const DAMAGE_DEALT_FLAG = "pf1DamageDealtHistory";
|
const DAMAGE_DEALT_FLAG = "pf1DamageDealtHistory";
|
||||||
|
const DEBUG_LOGS = false;
|
||||||
|
const HISTORY_PAGE_OPTIONS = [10, 20, 50, "all"];
|
||||||
|
|
||||||
|
const DAMAGE_ICON_MAP = {
|
||||||
|
slashing: { icon: "ra ra-sword", color: "#e3c000" },
|
||||||
|
piercing: { icon: "ra ra-spear-head", color: "#2c7be5" },
|
||||||
|
bludgeoning: { icon: "ra ra-large-hammer", color: "#e03131" },
|
||||||
|
fire: { icon: "ra ra-fire", color: "#f76707" },
|
||||||
|
cold: { icon: "ra ra-snowflake", color: "#3bc9db" },
|
||||||
|
electricity: { icon: "ra ra-lightning-bolt", color: "#f0c419" },
|
||||||
|
acid: { icon: "ra ra-round-bottom-flask", color: "#2f9e44" },
|
||||||
|
sonic: { icon: "ra ra-megaphone", color: "#22b8cf" },
|
||||||
|
force: { icon: "ra ra-crystal-ball", color: "#845ef7" },
|
||||||
|
negative: { icon: "ra ra-skull", color: "#7950f2" },
|
||||||
|
positive: { icon: "ra ra-sun", color: "#fab005" },
|
||||||
|
healing: { icon: "ra ra-health", color: "#4caf50" },
|
||||||
|
precision: { icon: "ra ra-target-arrows", color: "#000" },
|
||||||
|
nonlethal: { icon: "ra ra-hand", color: "#000" },
|
||||||
|
untyped: { icon: "ra ra-uncertainty", color: "#666" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const log = {
|
||||||
|
debug: (...args) => DEBUG_LOGS && console.debug("[GowlersTracking]", ...args),
|
||||||
|
info: (...args) => console.info("[GowlersTracking]", ...args),
|
||||||
|
warn: (...args) => console.warn("[GowlersTracking]", ...args),
|
||||||
|
error: (...args) => console.error("[GowlersTracking]", ...args),
|
||||||
|
};
|
||||||
|
|
||||||
const STAT_CONFIGS = {
|
const STAT_CONFIGS = {
|
||||||
hp: {
|
hp: {
|
||||||
@@ -86,6 +113,7 @@ const ledgerState = {
|
|||||||
damageOverlay: null,
|
damageOverlay: null,
|
||||||
damageMeterIncludeNPCs: true,
|
damageMeterIncludeNPCs: true,
|
||||||
damageMeterMode: "encounter", // "encounter" or "all"
|
damageMeterMode: "encounter", // "encounter" or "all"
|
||||||
|
lastNoGmWarningTime: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
function getHistoryPageState(actorId, tabId) {
|
function getHistoryPageState(actorId, tabId) {
|
||||||
@@ -116,12 +144,12 @@ function setActiveHistoryTab(actorId, tabId) {
|
|||||||
|
|
||||||
// Global tab switching function for history dialog
|
// Global tab switching function for history dialog
|
||||||
window.switchHistoryTab = function(tabId, el = null) {
|
window.switchHistoryTab = function(tabId, el = null) {
|
||||||
console.log("[GowlersTracking] Tab switched to:", tabId);
|
log.debug("Tab switched to:", tabId);
|
||||||
|
|
||||||
// Find the root element using the data-history-root attribute
|
// Find the root element using the data-history-root attribute
|
||||||
const root = el?.closest?.('[data-history-root]') ?? document.querySelector('[data-history-root]');
|
const root = el?.closest?.('[data-history-root]') ?? document.querySelector('[data-history-root]');
|
||||||
if (!root) {
|
if (!root) {
|
||||||
console.warn("[GowlersTracking] History root element not found");
|
log.warn("History root element not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,16 +167,16 @@ window.switchHistoryTab = function(tabId, el = null) {
|
|||||||
|
|
||||||
if (activeButton) {
|
if (activeButton) {
|
||||||
activeButton.classList.add('active');
|
activeButton.classList.add('active');
|
||||||
console.log("[GowlersTracking] Tab button activated:", tabId);
|
log.debug("Tab button activated:", tabId);
|
||||||
} else {
|
} else {
|
||||||
console.warn("[GowlersTracking] Tab button not found for:", tabId);
|
log.warn("Tab button not found for:", tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activePanel) {
|
if (activePanel) {
|
||||||
activePanel.style.display = 'block';
|
activePanel.style.display = 'block';
|
||||||
console.log("[GowlersTracking] Tab panel displayed:", tabId);
|
log.debug("Tab panel displayed:", tabId);
|
||||||
} else {
|
} else {
|
||||||
console.warn("[GowlersTracking] Tab panel not found for:", tabId);
|
log.warn("Tab panel not found for:", tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const actorId = root?.getAttribute('data-history-root');
|
const actorId = root?.getAttribute('data-history-root');
|
||||||
@@ -265,12 +293,12 @@ Hooks.once("init", () => {
|
|||||||
|
|
||||||
registerSettings();
|
registerSettings();
|
||||||
registerSettingsMenu();
|
registerSettingsMenu();
|
||||||
|
registerSceneControls(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
Hooks.once("ready", async () => {
|
Hooks.once("ready", async () => {
|
||||||
if (game.system.id !== "pf1") return;
|
if (game.system.id !== "pf1") return;
|
||||||
await initializeModule();
|
await initializeModule();
|
||||||
registerSceneControls();
|
|
||||||
// Expose NPC toggle helper for damage meter checkbox
|
// Expose NPC toggle helper for damage meter checkbox
|
||||||
window.GowlersTrackingDamageMeterToggleNPCs = (checked) => {
|
window.GowlersTrackingDamageMeterToggleNPCs = (checked) => {
|
||||||
ledgerState.damageMeterIncludeNPCs = !!checked;
|
ledgerState.damageMeterIncludeNPCs = !!checked;
|
||||||
@@ -302,8 +330,9 @@ async function initializeModule() {
|
|||||||
try {
|
try {
|
||||||
const message = options.message;
|
const message = options.message;
|
||||||
if (message) {
|
if (message) {
|
||||||
|
const storedValue = computeDamageValue(value, options);
|
||||||
const source = buildSourceLabel(value, options);
|
const source = buildSourceLabel(value, options);
|
||||||
console.log("[GowlersTracking] Storing message for matching: source=", source, "value=", value);
|
console.log("[GowlersTracking] Storing message for matching: source=", source, "value=", storedValue);
|
||||||
|
|
||||||
// Extract damage details for breakdown information
|
// Extract damage details for breakdown information
|
||||||
const damageDetails = extractDamageDetails(value, options);
|
const damageDetails = extractDamageDetails(value, options);
|
||||||
@@ -312,7 +341,7 @@ async function initializeModule() {
|
|||||||
ledgerState.recentMessages.push({
|
ledgerState.recentMessages.push({
|
||||||
message: message,
|
message: message,
|
||||||
source: source,
|
source: source,
|
||||||
value: value, // Keep sign! Negative = damage, Positive = healing
|
value: storedValue, // Keep sign! Negative = damage, Positive = healing
|
||||||
damageDetails: damageDetails,
|
damageDetails: damageDetails,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
});
|
});
|
||||||
@@ -500,16 +529,15 @@ async function initializeModule() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log detailed inspection for debugging
|
if (DEBUG_LOGS) {
|
||||||
console.log("[GowlersTracking] Message inspection for damage details:");
|
log.debug("Message inspection for damage details:");
|
||||||
console.log("[GowlersTracking] - pf1Flags keys:", Object.keys(pf1Flags));
|
log.debug(" - pf1Flags keys:", Object.keys(pf1Flags));
|
||||||
console.log("[GowlersTracking] - Full metadata:", JSON.stringify(metadata, null, 2));
|
log.debug(" - Full metadata:", JSON.stringify(metadata, null, 2));
|
||||||
console.log("[GowlersTracking] - message.rolls:", message.rolls?.length > 0 ? "Present" : "None");
|
log.debug(" - message.rolls:", message.rolls?.length > 0 ? "Present" : "None");
|
||||||
console.log("[GowlersTracking] - message.content length:", message.content?.length ?? 0);
|
log.debug(" - message.content length:", message.content?.length ?? 0);
|
||||||
|
|
||||||
// Log HTML content snippet for analysis
|
|
||||||
if (message.content) {
|
if (message.content) {
|
||||||
console.log("[GowlersTracking] - HTML preview (first 1000 chars):", message.content.substring(0, 1000));
|
log.debug(" - HTML preview (first 1000 chars):", message.content.substring(0, 1000));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -577,7 +605,9 @@ function registerSettingsMenu() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerSceneControls() {
|
function registerSceneControls(forceRefresh = false) {
|
||||||
|
if (ledgerState.controlsRegistered) return;
|
||||||
|
ledgerState.controlsRegistered = true;
|
||||||
Hooks.on("getSceneControlButtons", (controls) => {
|
Hooks.on("getSceneControlButtons", (controls) => {
|
||||||
const tokenControls = controls.find((c) => c.name === "token");
|
const tokenControls = controls.find((c) => c.name === "token");
|
||||||
if (!tokenControls) return;
|
if (!tokenControls) return;
|
||||||
@@ -607,6 +637,26 @@ function registerSceneControls() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
if (forceRefresh && ui?.controls) {
|
||||||
|
try {
|
||||||
|
ui.controls.initialize();
|
||||||
|
ui.controls.render(true);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("[GowlersTracking] Failed to refresh controls after registration", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeDamageValue(value, options = {}) {
|
||||||
|
const base = Number(value) || 0;
|
||||||
|
if (!Array.isArray(options.instances) || !options.instances.length) return base;
|
||||||
|
const sum = options.instances.reduce((total, inst) => {
|
||||||
|
const parts = [inst.total, inst.value, inst.amount].filter((v) => Number.isFinite(v));
|
||||||
|
return total + (parts.length ? parts[0] : 0);
|
||||||
|
}, 0);
|
||||||
|
if (!sum) return base;
|
||||||
|
const sign = base < 0 ? -1 : 1;
|
||||||
|
return sign * Math.abs(sum);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openDamageMeterOverlay() {
|
function openDamageMeterOverlay() {
|
||||||
@@ -982,7 +1032,7 @@ function buildXpBreakdownTooltip(actor, xpEntry) {
|
|||||||
|
|
||||||
function buildHistoryContent(actor, tabArg) {
|
function buildHistoryContent(actor, tabArg) {
|
||||||
const initialTab = tabArg ?? getActiveHistoryTab(actor.id, "hp"); // Explicitly capture the tab parameter
|
const initialTab = tabArg ?? getActiveHistoryTab(actor.id, "hp"); // Explicitly capture the tab parameter
|
||||||
console.log("[GowlersTracking] buildHistoryContent called with initialTab:", initialTab);
|
log.debug("buildHistoryContent called with initialTab:", initialTab);
|
||||||
setActiveHistoryTab(actor.id, initialTab);
|
setActiveHistoryTab(actor.id, initialTab);
|
||||||
const canConfigure = game.user?.isGM;
|
const canConfigure = game.user?.isGM;
|
||||||
const configs = [
|
const configs = [
|
||||||
@@ -994,7 +1044,7 @@ function buildHistoryContent(actor, tabArg) {
|
|||||||
{ label: "Timestamp", render: (entry) => formatDate(entry.timestamp) },
|
{ label: "Timestamp", render: (entry) => formatDate(entry.timestamp) },
|
||||||
{ label: "HP", render: (entry) => entry.value },
|
{ label: "HP", render: (entry) => entry.value },
|
||||||
{
|
{
|
||||||
label: "Δ",
|
label: "Delta",
|
||||||
render: (entry) => entry.diff,
|
render: (entry) => entry.diff,
|
||||||
getTitle: (entry) => entry.damageBreakdown ? `${entry.damageBreakdown}` : ""
|
getTitle: (entry) => entry.damageBreakdown ? `${entry.damageBreakdown}` : ""
|
||||||
},
|
},
|
||||||
@@ -1100,15 +1150,15 @@ function buildHistoryContent(actor, tabArg) {
|
|||||||
currentPage = totalPages;
|
currentPage = totalPages;
|
||||||
setHistoryPageState(actor.id, cfg.id, currentPage, rowsPerPage);
|
setHistoryPageState(actor.id, cfg.id, currentPage, rowsPerPage);
|
||||||
}
|
}
|
||||||
const selectOptions = [10, 20, 50, "all"]
|
const selectOptions = HISTORY_PAGE_OPTIONS
|
||||||
.map((value) => `<option value="${value}" ${String(value) === String(rowsPerPage) ? "selected" : ""}>${value === "all" ? "All" : `${value} rows`}</option>`)
|
.map((value) => `<option value="${value}" ${String(value) === String(rowsPerPage) ? "selected" : ""}>${value === "all" ? "All" : `${value} rows`}</option>`)
|
||||||
.join("");
|
.join("");
|
||||||
return `<div class="history-panel tab ${isActive}" data-history-panel="${cfg.id}" data-page="${currentPage}" data-page-size="${rowsPerPage}" style="display:${display}">
|
return `<div class="history-panel tab ${isActive}" data-history-panel="${cfg.id}" data-page="${currentPage}" data-page-size="${rowsPerPage}" style="display:${display}">
|
||||||
${renderHistoryTable(entries, cfg.columns, cfg.id, rowsPerPage, currentPage)}
|
${renderHistoryTable(entries, cfg.columns, cfg.id, rowsPerPage, currentPage)}
|
||||||
<div class="history-panel-pagination" style="display: flex; gap: 8px; align-items: center; justify-content: center; margin-top: 12px; padding-top: 8px; border-top: 1px solid #e0e0e0;">
|
<div class="history-panel-pagination" style="display: flex; gap: 8px; align-items: center; justify-content: center; margin-top: 12px; padding-top: 8px; border-top: 1px solid #e0e0e0;">
|
||||||
<button type="button" data-action="history-page" data-direction="prev" onclick="window.historyPageNav(this, 'prev')" style="padding: 4px 8px; cursor: pointer;" ${totalPages <= 1 ? 'disabled' : ''}>« Prev</button>
|
<button type="button" data-action="history-page" data-direction="prev" onclick="window.historyPageNav(this, 'prev')" style="padding: 4px 8px; cursor: pointer;" ${totalPages <= 1 ? 'disabled' : ''}>< Prev</button>
|
||||||
<span data-page-info style="font-size: 0.85em; min-width: 100px; text-align: center;">Page ${currentPage} / ${Math.max(1, totalPages)}</span>
|
<span data-page-info style="font-size: 0.85em; min-width: 100px; text-align: center;">Page ${currentPage} / ${Math.max(1, totalPages)}</span>
|
||||||
<button type="button" data-action="history-page" data-direction="next" onclick="window.historyPageNav(this, 'next')" style="padding: 4px 8px; cursor: pointer;" ${totalPages <= 1 ? 'disabled' : ''}>Next »</button>
|
<button type="button" data-action="history-page" data-direction="next" onclick="window.historyPageNav(this, 'next')" style="padding: 4px 8px; cursor: pointer;" ${totalPages <= 1 ? 'disabled' : ''}>Next ></button>
|
||||||
<select data-history-page-size onchange="window.historyChangePageSize(this)" style="padding: 2px 4px;">
|
<select data-history-page-size onchange="window.historyChangePageSize(this)" style="padding: 2px 4px;">
|
||||||
${selectOptions}
|
${selectOptions}
|
||||||
</select>
|
</select>
|
||||||
@@ -1179,29 +1229,44 @@ function renderHistoryTable(entries, columns, id, rowsPerPage = 10, currentPage
|
|||||||
</table>`;
|
</table>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function canWriteActor(actor) {
|
||||||
|
if (!actor) return false;
|
||||||
|
if (game.user?.isGM) return true;
|
||||||
|
const hasActiveGm = (game.users ?? []).some((u) => u.isGM && u.active);
|
||||||
|
if (!hasActiveGm) {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - (ledgerState.lastNoGmWarningTime || 0) > 10000) {
|
||||||
|
ledgerState.lastNoGmWarningTime = now;
|
||||||
|
ui.notifications?.error?.("Gowler's Tracking Ledger requires an active GM to record updates.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
async function recordHistoryEntry(actor, statId, previous, nextValue, userId, options = {}, change = {}) {
|
async function recordHistoryEntry(actor, statId, previous, nextValue, userId, options = {}, change = {}) {
|
||||||
const config = STAT_CONFIGS[statId];
|
const config = STAT_CONFIGS[statId];
|
||||||
if (!config) return;
|
if (!config) return;
|
||||||
|
if (!canWriteActor(actor)) return;
|
||||||
|
|
||||||
const diffValue = config.diff(previous, nextValue);
|
const diffValue = config.diff(previous, nextValue);
|
||||||
|
|
||||||
// COMPREHENSIVE DEBUG LOGGING FOR DAMAGE REPORTING
|
if (DEBUG_LOGS) {
|
||||||
console.log("[GowlersTracking] ===== recordHistoryEntry DEBUG =====");
|
log.debug("===== recordHistoryEntry DEBUG =====");
|
||||||
console.log("[GowlersTracking] statId:", statId);
|
log.debug("statId:", statId);
|
||||||
console.log("[GowlersTracking] Previous:", previous, "Next:", nextValue, "Diff:", diffValue);
|
log.debug("Previous:", previous, "Next:", nextValue, "Diff:", diffValue);
|
||||||
console.log("[GowlersTracking] Actor:", actor.name, "(" + actor.id + ")");
|
log.debug("Actor:", actor.name, "(" + actor.id + ")");
|
||||||
console.log("[GowlersTracking] userId:", userId);
|
log.debug("userId:", userId);
|
||||||
console.log("[GowlersTracking] Full options object:", options);
|
log.debug("Full options object:", options);
|
||||||
console.log("[GowlersTracking] Full change object:", change);
|
log.debug("Full change object:", change);
|
||||||
|
|
||||||
// Log all keys in options for inspection
|
|
||||||
if (Object.keys(options).length > 0) {
|
if (Object.keys(options).length > 0) {
|
||||||
console.log("[GowlersTracking] Options keys:", Object.keys(options));
|
log.debug("Options keys:", Object.keys(options));
|
||||||
for (const key of Object.keys(options)) {
|
for (const key of Object.keys(options)) {
|
||||||
console.log(`[GowlersTracking] options.${key}:`, options[key]);
|
log.debug(`options.${key}:`, options[key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log("[GowlersTracking] ===== end debug =====");
|
log.debug("===== end debug =====");
|
||||||
|
}
|
||||||
|
|
||||||
// Determine encounter ID: use active combat, or if none, check if combat just ended
|
// Determine encounter ID: use active combat, or if none, check if combat just ended
|
||||||
let encounterId = game.combat?.id ?? null;
|
let encounterId = game.combat?.id ?? null;
|
||||||
@@ -1239,28 +1304,29 @@ async function recordHistoryEntry(actor, statId, previous, nextValue, userId, op
|
|||||||
else if (options?.encounter?.id) encounterId = options.encounter.id;
|
else if (options?.encounter?.id) encounterId = options.encounter.id;
|
||||||
else if (options?.encounter?._id) encounterId = options.encounter._id;
|
else if (options?.encounter?._id) encounterId = options.encounter._id;
|
||||||
|
|
||||||
// XP debug logging for data inspection
|
if (DEBUG_LOGS) {
|
||||||
try {
|
try {
|
||||||
console.log("[GowlersTracking][XP DEBUG] -----");
|
log.debug("[XP DEBUG] -----");
|
||||||
console.log("[GowlersTracking][XP DEBUG] encounterId:", encounterId, "manualXp:", manualXp, "hasExplicitEncounter:", hasExplicitEncounter);
|
log.debug("[XP DEBUG] encounterId:", encounterId, "manualXp:", manualXp, "hasExplicitEncounter:", hasExplicitEncounter);
|
||||||
console.log("[GowlersTracking][XP DEBUG] options keys:", Object.keys(options || {}));
|
log.debug("[XP DEBUG] options keys:", Object.keys(options || {}));
|
||||||
console.log("[GowlersTracking][XP DEBUG] change keys:", Object.keys(change || {}));
|
log.debug("[XP DEBUG] change keys:", Object.keys(change || {}));
|
||||||
if (options) console.log("[GowlersTracking][XP DEBUG] options JSON:", JSON.stringify(options, null, 2));
|
if (options) log.debug("[XP DEBUG] options JSON:", JSON.stringify(options, null, 2));
|
||||||
if (change) console.log("[GowlersTracking][XP DEBUG] change JSON keys:", Object.keys(change));
|
if (change) log.debug("[XP DEBUG] change JSON keys:", Object.keys(change));
|
||||||
if (change?.system?.details?.xp) console.log("[GowlersTracking][XP DEBUG] change.system.details.xp:", change.system.details.xp);
|
if (change?.system?.details?.xp) log.debug("[XP DEBUG] change.system.details.xp:", change.system.details.xp);
|
||||||
if (change?.system?.attributes?.hp) console.log("[GowlersTracking][XP DEBUG] change.system.attributes.hp (truncated):", JSON.stringify(change.system.attributes.hp, null, 2).substring(0, 500));
|
if (change?.system?.attributes?.hp) log.debug("[XP DEBUG] change.system.attributes.hp (truncated):", JSON.stringify(change.system.attributes.hp, null, 2).substring(0, 500));
|
||||||
const pf1Flags = actor.getFlag("pf1") ?? {};
|
const pf1Flags = actor.getFlag("pf1") ?? {};
|
||||||
if (pf1Flags && Object.keys(pf1Flags).length) console.log("[GowlersTracking][XP DEBUG] actor pf1 flags keys:", Object.keys(pf1Flags));
|
if (pf1Flags && Object.keys(pf1Flags).length) log.debug("[XP DEBUG] actor pf1 flags keys:", Object.keys(pf1Flags));
|
||||||
if (game.combat) console.log("[GowlersTracking][XP DEBUG] active combat flags:", game.combat.flags ?? "(none)");
|
if (game.combat) log.debug("[XP DEBUG] active combat flags:", game.combat.flags ?? "(none)");
|
||||||
const encounterFlag = actor.getFlag(FLAG_SCOPE, ENCOUNTER_FLAG) ?? [];
|
const encounterFlag = actor.getFlag(FLAG_SCOPE, ENCOUNTER_FLAG) ?? [];
|
||||||
console.log("[GowlersTracking][XP DEBUG] actor encounter flag count:", encounterFlag.length);
|
log.debug("[XP DEBUG] actor encounter flag count:", encounterFlag.length);
|
||||||
if (encounterFlag.length && encounterId) {
|
if (encounterFlag.length && encounterId) {
|
||||||
const encounterEntry = encounterFlag.find((e) => e.encounterID === encounterId);
|
const encounterEntry = encounterFlag.find((e) => e.encounterID === encounterId);
|
||||||
console.log("[GowlersTracking][XP DEBUG] matched encounter entry:", encounterEntry ?? "(not found)");
|
log.debug("[XP DEBUG] matched encounter entry:", encounterEntry ?? "(not found)");
|
||||||
}
|
}
|
||||||
console.log("[GowlersTracking][XP DEBUG] -----");
|
log.debug("[XP DEBUG] -----");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("[GowlersTracking][XP DEBUG] logging failed:", e);
|
log.warn("[XP DEBUG] logging failed:", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1584,30 +1650,20 @@ function isNonlethalType(type) {
|
|||||||
return String(type ?? "").toLowerCase() === "nonlethal";
|
return String(type ?? "").toLowerCase() === "nonlethal";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPrimaryDamageType(part) {
|
||||||
|
return (part?.types && part.types[0]) || part?.customTypes?.[0] || part?.materials?.[0] || "untyped";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDamageIconMeta(type) {
|
||||||
|
return DAMAGE_ICON_MAP[type?.toLowerCase?.()] ?? DAMAGE_ICON_MAP.untyped;
|
||||||
|
}
|
||||||
|
|
||||||
function formatDamagePartsWithIcons(parts) {
|
function formatDamagePartsWithIcons(parts) {
|
||||||
if (!Array.isArray(parts) || !parts.length) return "";
|
if (!Array.isArray(parts) || !parts.length) return "";
|
||||||
const iconMap = {
|
|
||||||
slashing: { icon: "ra ra-sword", color: "#e3c000" },
|
|
||||||
piercing: { icon: "ra ra-spear-head", color: "#2c7be5" },
|
|
||||||
bludgeoning: { icon: "ra ra-large-hammer", color: "#e03131" },
|
|
||||||
fire: { icon: "ra ra-fire", color: "#f76707" },
|
|
||||||
cold: { icon: "ra ra-snowflake", color: "#3bc9db" },
|
|
||||||
electricity: { icon: "ra ra-lightning-bolt", color: "#f0c419" },
|
|
||||||
acid: { icon: "ra ra-round-bottom-flask", color: "#2f9e44" },
|
|
||||||
sonic: { icon: "ra ra-megaphone", color: "#22b8cf" },
|
|
||||||
force: { icon: "ra ra-crystal-ball", color: "#845ef7" },
|
|
||||||
negative: { icon: "ra ra-skull", color: "#7950f2" },
|
|
||||||
positive: { icon: "ra ra-sun", color: "#fab005" },
|
|
||||||
healing: { icon: "ra ra-health", color: "#4caf50" },
|
|
||||||
precision: { icon: "ra ra-target-arrows", color: "#000" },
|
|
||||||
nonlethal: { icon: "ra ra-hand", color: "#000" },
|
|
||||||
untyped: { icon: "ra ra-uncertainty", color: "#666" },
|
|
||||||
};
|
|
||||||
return parts
|
return parts
|
||||||
.map((p) => {
|
.map((p) => {
|
||||||
const baseType = (p.types && p.types[0]) || p.customTypes?.[0] || p.materials?.[0] || "untyped";
|
const meta = getDamageIconMeta(getPrimaryDamageType(p));
|
||||||
const mapEntry = iconMap[baseType?.toLowerCase?.()] ?? iconMap.untyped;
|
const icon = `<i class="${meta.icon}" style="color:${meta.color};"></i>`;
|
||||||
const icon = `<i class="${mapEntry.icon}" style="color:${mapEntry.color};"></i>`;
|
|
||||||
const amt = Number.isFinite(p.total) ? p.total : p.formula ?? "?";
|
const amt = Number.isFinite(p.total) ? p.total : p.formula ?? "?";
|
||||||
return `<span style="display:inline-flex; align-items:center; gap:4px; margin-right:6px;">${icon}<span>${amt}</span></span>`;
|
return `<span style="display:inline-flex; align-items:center; gap:4px; margin-right:6px;">${icon}<span>${amt}</span></span>`;
|
||||||
})
|
})
|
||||||
@@ -1615,27 +1671,10 @@ function formatDamagePartsWithIcons(parts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderDamageBar(composition = [], total = 0) {
|
function renderDamageBar(composition = [], total = 0) {
|
||||||
const iconMap = {
|
|
||||||
slashing: { icon: "ra ra-sword", color: "#e3c000" },
|
|
||||||
piercing: { icon: "ra ra-spear-head", color: "#2c7be5" },
|
|
||||||
bludgeoning: { icon: "ra ra-large-hammer", color: "#e03131" },
|
|
||||||
fire: { icon: "ra ra-fire", color: "#f76707" },
|
|
||||||
cold: { icon: "ra ra-snowflake", color: "#3bc9db" },
|
|
||||||
electricity: { icon: "ra ra-lightning-bolt", color: "#f0c419" },
|
|
||||||
acid: { icon: "ra ra-round-bottom-flask", color: "#2f9e44" },
|
|
||||||
sonic: { icon: "ra ra-megaphone", color: "#22b8cf" },
|
|
||||||
force: { icon: "ra ra-crystal-ball", color: "#845ef7" },
|
|
||||||
negative: { icon: "ra ra-skull", color: "#7950f2" },
|
|
||||||
positive: { icon: "ra ra-sun", color: "#fab005" },
|
|
||||||
healing: { icon: "ra ra-health", color: "#4caf50" },
|
|
||||||
precision: { icon: "ra ra-target-arrows", color: "#000" },
|
|
||||||
nonlethal: { icon: "ra ra-hand", color: "#000" },
|
|
||||||
untyped: { icon: "ra ra-uncertainty", color: "#666" },
|
|
||||||
};
|
|
||||||
if (!total || !composition.length) return "";
|
if (!total || !composition.length) return "";
|
||||||
const segments = composition.map((c) => {
|
const segments = composition.map((c) => {
|
||||||
const pct = Math.max(2, Math.round((c.value / total) * 100));
|
const pct = Math.max(2, Math.round((c.value / total) * 100));
|
||||||
const entry = iconMap[c.type?.toLowerCase?.()] ?? iconMap.untyped;
|
const entry = getDamageIconMeta(c.type);
|
||||||
return `<div style="width:${pct}%; background:${entry.color}; height:10px; display:flex; align-items:center; justify-content:center;">
|
return `<div style="width:${pct}%; background:${entry.color}; height:10px; display:flex; align-items:center; justify-content:center;">
|
||||||
<i class="${entry.icon}" style="color:#fff; font-size:10px;"></i>
|
<i class="${entry.icon}" style="color:#fff; font-size:10px;"></i>
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -1715,6 +1754,7 @@ function resolveActorFromMetadataSafe(metadata = {}) {
|
|||||||
|
|
||||||
async function recordDamageDealt(attacker, entry) {
|
async function recordDamageDealt(attacker, entry) {
|
||||||
if (!attacker?.id) return;
|
if (!attacker?.id) return;
|
||||||
|
if (!canWriteActor(attacker)) return;
|
||||||
try {
|
try {
|
||||||
const existing = (await attacker.getFlag(FLAG_SCOPE, DAMAGE_DEALT_FLAG)) ?? [];
|
const existing = (await attacker.getFlag(FLAG_SCOPE, DAMAGE_DEALT_FLAG)) ?? [];
|
||||||
existing.unshift(entry);
|
existing.unshift(entry);
|
||||||
@@ -2095,6 +2135,7 @@ class TrackingLedgerConfig extends FormApplication {
|
|||||||
this._pageMeta = { totalPages: 1, hasPrev: false, hasNext: false };
|
this._pageMeta = { totalPages: 1, hasPrev: false, hasNext: false };
|
||||||
this._actorRefs = null;
|
this._actorRefs = null;
|
||||||
this._filterDebounceTimer = null;
|
this._filterDebounceTimer = null;
|
||||||
|
this._pendingFilterCaret = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get pageSize() {
|
get pageSize() {
|
||||||
@@ -2248,6 +2289,10 @@ class TrackingLedgerConfig extends FormApplication {
|
|||||||
this._page = 0;
|
this._page = 0;
|
||||||
TrackingLedgerConfig._lastFilter = this._filter;
|
TrackingLedgerConfig._lastFilter = this._filter;
|
||||||
TrackingLedgerConfig._lastPage = this._page;
|
TrackingLedgerConfig._lastPage = this._page;
|
||||||
|
const el = event.currentTarget;
|
||||||
|
const selStart = Number.isFinite(el.selectionStart) ? el.selectionStart : (this._filter?.length ?? 0);
|
||||||
|
const selEnd = Number.isFinite(el.selectionEnd) ? el.selectionEnd : selStart;
|
||||||
|
this._pendingFilterCaret = { start: selStart, end: selEnd };
|
||||||
|
|
||||||
// Debounce render to preserve focus
|
// Debounce render to preserve focus
|
||||||
clearTimeout(this._filterDebounceTimer);
|
clearTimeout(this._filterDebounceTimer);
|
||||||
@@ -2255,8 +2300,17 @@ class TrackingLedgerConfig extends FormApplication {
|
|||||||
this.render(false);
|
this.render(false);
|
||||||
}, 300);
|
}, 300);
|
||||||
});
|
});
|
||||||
// Keep focus on filter input after render
|
// Keep focus/caret on filter input after render
|
||||||
filterInput.trigger("focus");
|
setTimeout(() => {
|
||||||
|
const el = filterInput[0];
|
||||||
|
if (!el) return;
|
||||||
|
const caret = this._pendingFilterCaret;
|
||||||
|
el.focus();
|
||||||
|
const posStart = Number.isFinite(caret?.start) ? caret.start : el.value.length;
|
||||||
|
const posEnd = Number.isFinite(caret?.end) ? caret.end : posStart;
|
||||||
|
try { el.setSelectionRange(posStart, posEnd); } catch (e) { /* ignore */ }
|
||||||
|
this._pendingFilterCaret = null;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
html.find("[data-page-size]").on("change", (event) => {
|
html.find("[data-page-size]").on("change", (event) => {
|
||||||
const value = event.currentTarget.value;
|
const value = event.currentTarget.value;
|
||||||
@@ -2353,16 +2407,7 @@ class TrackingLedgerConfig extends FormApplication {
|
|||||||
defaultYes: false,
|
defaultYes: false,
|
||||||
});
|
});
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
try {
|
await clearAllHistoriesWithProgress();
|
||||||
const actors = collectAllActorDocuments();
|
|
||||||
const tokens = collectAllTokens();
|
|
||||||
for (const actor of actors) await clearDocumentHistory(actor);
|
|
||||||
for (const token of tokens) await clearDocumentHistory(token);
|
|
||||||
ui.notifications?.info?.("History cleared for all actors.");
|
|
||||||
} catch (err) {
|
|
||||||
console.error("[GowlersTracking] Failed to clear all histories", err);
|
|
||||||
ui.notifications?.error?.("Failed to clear all histories; see console.");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
html.find("[data-action=\"clear-history\"]").on("click", async (event) => {
|
html.find("[data-action=\"clear-history\"]").on("click", async (event) => {
|
||||||
@@ -2408,15 +2453,98 @@ async function clearDocumentHistory(doc) {
|
|||||||
DAMAGE_DEALT_FLAG,
|
DAMAGE_DEALT_FLAG,
|
||||||
ENCOUNTER_FLAG,
|
ENCOUNTER_FLAG,
|
||||||
];
|
];
|
||||||
for (const key of flagKeys) {
|
await Promise.all(
|
||||||
|
flagKeys.map(async (key) => {
|
||||||
try {
|
try {
|
||||||
await doc.unsetFlag(FLAG_SCOPE, key);
|
await doc.unsetFlag(FLAG_SCOPE, key);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(`[GowlersTracking] Failed to clear flag ${key} for ${doc.name ?? doc.id}`, err);
|
console.warn(`[GowlersTracking] Failed to clear flag ${key} for ${doc.name ?? doc.id}`, err);
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
primeDocBaseline(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearAllHistoriesWithProgress() {
|
||||||
|
try {
|
||||||
|
const entries = collectClearTargets();
|
||||||
|
if (!entries.length) {
|
||||||
|
ui.notifications?.info?.("No actor or token histories found to clear.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple progress dialog
|
||||||
|
let progressEl = null;
|
||||||
|
let statusEl = null;
|
||||||
|
const dialog = new Dialog({
|
||||||
|
title: "Clearing Histories",
|
||||||
|
content: `
|
||||||
|
<div style="display:flex; flex-direction:column; gap:8px; padding:4px;">
|
||||||
|
<progress value="0" max="${entries.length}" style="width:100%;"></progress>
|
||||||
|
<div data-status style="font-size:12px;">Starting...</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
buttons: {},
|
||||||
|
close: () => {},
|
||||||
|
render: (html) => {
|
||||||
|
progressEl = html[0].querySelector("progress");
|
||||||
|
statusEl = html[0].querySelector("[data-status]");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dialog.render(true);
|
||||||
|
|
||||||
|
let completed = 0;
|
||||||
|
for (const { doc, label } of entries) {
|
||||||
|
if (statusEl) statusEl.textContent = `Clearing: ${label}`;
|
||||||
|
await clearDocumentHistory(doc);
|
||||||
|
completed += 1;
|
||||||
|
if (progressEl) progressEl.value = completed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusEl) statusEl.textContent = "Done.";
|
||||||
|
setTimeout(() => dialog.close(), 500);
|
||||||
|
ui.notifications?.info?.("History cleared for all actors and tokens.");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[GowlersTracking] Failed to clear all histories", err);
|
||||||
|
ui.notifications?.error?.("Failed to clear all histories; see console.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function collectClearTargets() {
|
||||||
|
const targets = new Map();
|
||||||
|
|
||||||
|
// Core actors in the directory (player characters and linked NPCs)
|
||||||
|
for (const actor of collectAllActorDocuments()) {
|
||||||
|
const key = `actor:${actor.uuid ?? actor.id}`;
|
||||||
|
if (!targets.has(key)) targets.set(key, { doc: actor, label: `${actor.name ?? actor.id} (Actor)` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tokens on scenes (covers unlinked NPCs and token-specific flags)
|
||||||
|
for (const token of collectAllTokens()) {
|
||||||
|
const tokenKey = `token:${token.uuid ?? token.id}:${token.parent?.uuid ?? "scene"}`;
|
||||||
|
if (!targets.has(tokenKey)) {
|
||||||
|
const sceneName = token.parent?.name ? ` - ${token.parent.name}` : "";
|
||||||
|
targets.set(tokenKey, { doc: token, label: `${token.name ?? token.id}${sceneName} (Token)` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include the token's actor (unlinked actor copies need clearing too)
|
||||||
|
const tokenActor = token.actor;
|
||||||
|
if (tokenActor) {
|
||||||
|
const actorKey = `actor:${tokenActor.uuid ?? tokenActor.id}`;
|
||||||
|
if (!targets.has(actorKey)) targets.set(actorKey, { doc: tokenActor, label: `${tokenActor.name ?? tokenActor.id} (Actor)` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(targets.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
function primeDocBaseline(doc) {
|
||||||
|
if (!doc) return;
|
||||||
|
const actor = doc instanceof Actor ? doc : doc.actor;
|
||||||
|
if (actor) primeActor(actor);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function collectAllActorDocuments() {
|
function collectAllActorDocuments() {
|
||||||
const actors = new Map();
|
const actors = new Map();
|
||||||
for (const a of game.actors.contents ?? []) {
|
for (const a of game.actors.contents ?? []) {
|
||||||
@@ -2486,6 +2614,7 @@ async function onCombatEnd(combat) {
|
|||||||
*/
|
*/
|
||||||
async function updateEncounterSummary(actor, combat, status = "ongoing") {
|
async function updateEncounterSummary(actor, combat, status = "ongoing") {
|
||||||
if (!actor || !combat) return;
|
if (!actor || !combat) return;
|
||||||
|
if (!canWriteActor(actor)) return;
|
||||||
|
|
||||||
console.log(`[GowlersTracking] updateEncounterSummary for ${actor.name}, combat ${combat.id}, status: ${status}`);
|
console.log(`[GowlersTracking] updateEncounterSummary for ${actor.name}, combat ${combat.id}, status: ${status}`);
|
||||||
|
|
||||||
@@ -2584,7 +2713,7 @@ function sendChatNotification(statId, actor, previous, nextValue, entry) {
|
|||||||
<div style="font-family: var(--font-primary); font-size: 12px; line-height: 1.4;">
|
<div style="font-family: var(--font-primary); font-size: 12px; line-height: 1.4;">
|
||||||
<div style="font-weight: bold; margin-bottom: 4px;">${title} Update</div>
|
<div style="font-weight: bold; margin-bottom: 4px;">${title} Update</div>
|
||||||
<div><strong>Actor:</strong> ${actor.name}</div>
|
<div><strong>Actor:</strong> ${actor.name}</div>
|
||||||
<div><strong>Value:</strong> ${prevText} → ${nextText} (${entry.diff})</div>
|
<div><strong>Value:</strong> ${prevText} -> ${nextText} (${entry.diff})</div>
|
||||||
<div><strong>Source:</strong> ${source}</div>
|
<div><strong>Source:</strong> ${source}</div>
|
||||||
<div><strong>Encounter:</strong> ${encounter}</div>
|
<div><strong>Encounter:</strong> ${encounter}</div>
|
||||||
${detailHtml}
|
${detailHtml}
|
||||||
Submodule src/modules/folkens-macros-pf1 deleted from 514c37005a
2
src/modules/folkens-macros-pf1/.gitattributes
vendored
Normal file
2
src/modules/folkens-macros-pf1/.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Auto detect text files and perform LF normalization
|
||||||
|
* text=auto
|
||||||
674
src/modules/folkens-macros-pf1/LICENSE
Normal file
674
src/modules/folkens-macros-pf1/LICENSE
Normal file
@@ -0,0 +1,674 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
|
GNU General Public License for most of our software; it applies also to
|
||||||
|
any other work released this way by its authors. You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to prevent others from denying you
|
||||||
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
|
or can get the source code. And you must show them these terms so they
|
||||||
|
know their rights.
|
||||||
|
|
||||||
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
|
that there is no warranty for this free software. For both users' and
|
||||||
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
|
changed, so that their problems will not be attributed erroneously to
|
||||||
|
authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run
|
||||||
|
modified versions of the software inside them, although the manufacturer
|
||||||
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
|
protecting users' freedom to change the software. The systematic
|
||||||
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
|
products. If such problems arise substantially in other domains, we
|
||||||
|
stand ready to extend this provision to those domains in future versions
|
||||||
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents.
|
||||||
|
States should not allow patents to restrict development and use of
|
||||||
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
|
avoid the special danger that patents applied to a free program could
|
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the special requirements of the GNU Affero General Public License,
|
||||||
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU General Public License from time to time. Such new versions will
|
||||||
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program does terminal interaction, make it output a short
|
||||||
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
<program> Copyright (C) <year> <name of author>
|
||||||
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, your program's commands
|
||||||
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
The GNU General Public License does not permit incorporating your program
|
||||||
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
|
may consider it more useful to permit linking proprietary applications with
|
||||||
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License. But first, please read
|
||||||
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||||
68
src/modules/folkens-macros-pf1/README.md
Normal file
68
src/modules/folkens-macros-pf1/README.md
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
I wrote these macros for my blind players. They use them along with a mod called “Advanced Macros” to trigger skills, checks, and such from their character sheets via chat. Advanced-Macros also requires the mod “lib-wrapper” to function.
|
||||||
|
|
||||||
|
I try to keep all the macros as short as possible, to speed up the play for these players. Below is a key of all the macros present. With a little experimentation, you can probably create some of your own! Whenever possible, I used the same abbreviation the pf1 system devs do for an item eg perception becomes per because that’s what they use.
|
||||||
|
|
||||||
|
Installation:
|
||||||
|
Install the mod.
|
||||||
|
Turn the mod on (requires advanced-macros and lib-wrapper mods).
|
||||||
|
Look under compendiums for “Folken’s PF1 Blind Macros.”
|
||||||
|
Right click it and left click “Import all content.”
|
||||||
|
|
||||||
|
Try them out!
|
||||||
|
|
||||||
|
|
||||||
|
All macros are written to be character agnostic. Meaning they will run off the sheet of whatever actor you are currently assigned. If you’re GM, you need to have a token selected for them to work.
|
||||||
|
|
||||||
|
Skills:
|
||||||
|
Acrobatics: acr
|
||||||
|
Appraise: apr
|
||||||
|
Bluff: blf
|
||||||
|
Climb: clm
|
||||||
|
Disable Device: dev
|
||||||
|
Diplomacy: dip
|
||||||
|
Disguise: dis
|
||||||
|
Escape Artist: esc
|
||||||
|
Fly: fly
|
||||||
|
Handle Animal: han
|
||||||
|
Heal: hea
|
||||||
|
Intimidate: int
|
||||||
|
Knowledge Arcana: kar
|
||||||
|
Knowledge Dungeoneering: kdu
|
||||||
|
Knowledge Engineering: ken
|
||||||
|
Knowledge Geography: kge
|
||||||
|
Knowledge History: khi
|
||||||
|
Knowledge Local: klo
|
||||||
|
Knowledge Nature: kna
|
||||||
|
Knowledge Nobility: kno
|
||||||
|
Knowledge Planes: kpl
|
||||||
|
Knowledge Religion: kre
|
||||||
|
Linguistics: lin
|
||||||
|
Perception: per
|
||||||
|
Profession: prf2 (actor must have a profession created)
|
||||||
|
Ride: rid
|
||||||
|
Sense Motive: sen
|
||||||
|
Sleight of Hand: sle
|
||||||
|
Spellcraft: spl
|
||||||
|
Stealth: ste
|
||||||
|
Survival: sur
|
||||||
|
Swim: swm
|
||||||
|
Use Magic Device: umd
|
||||||
|
|
||||||
|
Checks:
|
||||||
|
Concentration Check Primary Spellbook: conc
|
||||||
|
Concentration Check Secondary Spellbook: conc2
|
||||||
|
Initiative Check: ini
|
||||||
|
Save, Fortitude: for
|
||||||
|
Save, Reflex: ref
|
||||||
|
Save, Will: wil
|
||||||
|
Stabilize Check: stab
|
||||||
|
|
||||||
|
Items & Spells: For these they must be individually dragged down to the button bar, then renamed. They will be character-specific.
|
||||||
|
|
||||||
|
|
||||||
|
Then right-click that macro in the bar and rename it to something simple to type. Eg Wand of Cure Light Wounds becomes wclw
|
||||||
|
|
||||||
|
|
||||||
|
That’s it! WIth these macros installed / created, your players can trigger them from a chat prompt as long as “Advanced Macros” mod is on. Anyone can use them, as long as they have an actor assigned or a token selected.
|
||||||
|
|
||||||
|
Some checks currently take your focus & cursor off the chat prompt. I’m working with the mod / fvtt devs to fix this! They are the real experts on this stuff!
|
||||||
36
src/modules/folkens-macros-pf1/module.json
Normal file
36
src/modules/folkens-macros-pf1/module.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"id": "folkens-macros-pf1",
|
||||||
|
"title": "Folkens PF1 Macros for the Blind",
|
||||||
|
"description": "text-activated macros useful to blind players, actor agnostic",
|
||||||
|
"version": "1.0",
|
||||||
|
"packs": [
|
||||||
|
{
|
||||||
|
"label": "Folken's PF1 Blind Macros",
|
||||||
|
"name": "folkenpf1blindmacros",
|
||||||
|
"path": "packs/macro.db",
|
||||||
|
"type": "Macro",
|
||||||
|
"private": false,
|
||||||
|
"flags": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"manifest": "https://github.com/folken88/folkens-macros-pf1/releases/download/modrelease/module.json",
|
||||||
|
"download": "https://github.com/folken88/folkens-macros-pf1/releases/download/modrelease/folkens-macros-pf1.zip",
|
||||||
|
"compatibility": {
|
||||||
|
"minimum": "10",
|
||||||
|
"verified": "10"
|
||||||
|
},
|
||||||
|
"relationships": {
|
||||||
|
"requires": [
|
||||||
|
{
|
||||||
|
"id": "advanced-macros",
|
||||||
|
"type": "module",
|
||||||
|
"compatibility": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "lib-wrapper",
|
||||||
|
"type": "module",
|
||||||
|
"compatibility": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
47
src/modules/folkens-macros-pf1/packs/macro.db
Normal file
47
src/modules/folkens-macros-pf1/packs/macro.db
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{"name":"spl","type":"script","author":"n8REKaKbUodLowfK","img":"icons/magic/symbols/runes-star-blue.webp","scope":"global","command":"actor.rollSkill(\"spl\", { skipDialog: true });","flags":{"furnace":{"runAsGM":false},"combat-utility-belt":{"macroTrigger":""},"scene-packer":{"sourceId":"Macro.YyARz0Slj31ig5wy","defaultPermission":2},"advanced-macros":{"runAsGM":false},"core":{"sourceId":"Macro.YyARz0Slj31ig5wy"}},"ownership":{"default":2,"IqGUPfrQK7VNUbWe":3,"n8REKaKbUodLowfK":3,"QGzUrxr480shRjdU":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665424036013,"modifiedTime":1665424036013,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"2nE3APlrpTgNRHZQ"}
|
||||||
|
{"name":"kno","type":"script","author":"n8REKaKbUodLowfK","img":"icons/sundries/flags/banner-symbol-sun-gold-red.webp","scope":"global","command":"actor.rollSkill(\"kno\", { skipDialog: true });","flags":{"furnace":{"runAsGM":false},"scene-packer":{"sourceId":"Macro.ObB4GCaRu7SfMMej"},"core":{"sourceId":"Macro.ObB4GCaRu7SfMMej"}},"ownership":{"default":2,"IqGUPfrQK7VNUbWe":3,"QGzUrxr480shRjdU":3,"n8REKaKbUodLowfK":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665424027371,"modifiedTime":1665424027371,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"3sJ6dtqVJXCGnjcC"}
|
||||||
|
{"name":"ste","type":"script","author":"n8REKaKbUodLowfK","img":"icons/creatures/mammals/humanoid-cat-skulking-teal.webp","scope":"global","command":"actor.rollSkill(\"ste\", { skipDialog: true });","flags":{"furnace":{"runAsGM":false},"combat-utility-belt":{"macroTrigger":""},"scene-packer":{"sourceId":"Macro.rTbbQ5OrDUUUDjvh"},"core":{"sourceId":"Macro.rTbbQ5OrDUUUDjvh"}},"ownership":{"default":2,"IqGUPfrQK7VNUbWe":3,"QGzUrxr480shRjdU":3,"n8REKaKbUodLowfK":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665424036630,"modifiedTime":1665424036630,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"4Ac033ifbfqa9pqb"}
|
||||||
|
{"name":"clm","type":"script","author":"n8REKaKbUodLowfK","img":"icons/sundries/misc/ladder-improvised.webp","scope":"global","command":"actor.rollSkill(\"clm\", { skipDialog: true });","flags":{"furnace":{"runAsGM":false},"scene-packer":{"sourceId":"Macro.TUmn2RgV9pZdlu6f"},"core":{"sourceId":"Macro.TUmn2RgV9pZdlu6f"}},"ownership":{"default":2,"IqGUPfrQK7VNUbWe":3,"QGzUrxr480shRjdU":3,"n8REKaKbUodLowfK":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665424014825,"modifiedTime":1665424014825,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"6M8Okjbm1IgMTYmV"}
|
||||||
|
{"name":"blf","type":"script","author":"n8REKaKbUodLowfK","img":"icons/sundries/gaming/playing-cards.webp","scope":"global","command":"actor.rollSkill(\"blf\", { skipDialog: true });","flags":{"furnace":{"runAsGM":false},"scene-packer":{"sourceId":"Macro.S35oZNKpyd5UDAw2"},"core":{"sourceId":"Macro.S35oZNKpyd5UDAw2"}},"ownership":{"default":2,"IqGUPfrQK7VNUbWe":3,"QGzUrxr480shRjdU":3,"n8REKaKbUodLowfK":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665424014104,"modifiedTime":1665424014104,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"B1mrUYjFqbOJ2N9Y"}
|
||||||
|
{"name":"intc","type":"script","author":"n8REKaKbUodLowfK","img":"icons/sundries/books/book-embossed-spiral-purple-white.webp","scope":"global","command":"actor.rollAbilityTest (\"int\", {skipDialogue:true});","ownership":{"default":0,"n8REKaKbUodLowfK":3},"flags":{"advanced-macros":{"runAsGM":false},"core":{"sourceId":"Macro.l5PmvaH0FlefnR3f"}},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665421727927,"modifiedTime":1665446652847,"lastModifiedBy":"kvH0iCMGDwLyA2bo"},"folder":null,"sort":0,"_id":"Bku0LOAn3ieUnfyc"}
|
||||||
|
{"name":"int","type":"script","author":"n8REKaKbUodLowfK","img":"icons/creatures/magical/construct-iron-stomping-yellow.webp","scope":"global","command":"actor.rollSkill(\"int\", { skipDialog: true });","flags":{"furnace":{"runAsGM":false},"scene-packer":{"sourceId":"Macro.XS37UKw1ccIMiEsV"},"core":{"sourceId":"Macro.XS37UKw1ccIMiEsV"}},"ownership":{"default":2,"IqGUPfrQK7VNUbWe":3,"QGzUrxr480shRjdU":3,"n8REKaKbUodLowfK":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665424021005,"modifiedTime":1665446649715,"lastModifiedBy":"kvH0iCMGDwLyA2bo"},"folder":null,"sort":0,"_id":"C8gbIJCErPK2TEX3"}
|
||||||
|
{"name":"wis","type":"script","author":"n8REKaKbUodLowfK","img":"icons/creatures/birds/raptor-owl-flying-moon.webp","scope":"global","command":"actor.rollAbilityTest (\"wis\", {skipDialogue:true});","ownership":{"default":0,"n8REKaKbUodLowfK":3},"flags":{"advanced-macros":{"runAsGM":false},"core":{"sourceId":"Macro.b3bm8Ker4jXPe2tr"}},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665421746906,"modifiedTime":1665424211187,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"D9DA2RkK41DgYniX"}
|
||||||
|
{"name":"ken","type":"script","author":"n8REKaKbUodLowfK","img":"icons/commodities/tech/blueprint.webp","scope":"global","command":"actor.rollSkill(\"ken\", { skipDialog: true });","flags":{"furnace":{"runAsGM":false},"scene-packer":{"sourceId":"Macro.bfeOax5AONO7hWia"},"core":{"sourceId":"Macro.bfeOax5AONO7hWia"}},"ownership":{"default":2,"IqGUPfrQK7VNUbWe":3,"QGzUrxr480shRjdU":3,"n8REKaKbUodLowfK":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665424023022,"modifiedTime":1665424023022,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"FaFI9XpzyBS5st52"}
|
||||||
|
{"name":"cha","type":"script","author":"n8REKaKbUodLowfK","img":"icons/skills/social/diplomacy-peace-alliance.webp","scope":"global","command":"actor.rollAbilityTest (\"cha\", {skipDialogue:true});","ownership":{"default":0,"n8REKaKbUodLowfK":3},"flags":{"advanced-macros":{"runAsGM":false},"core":{"sourceId":"Macro.eNWBVTsMGSaNA37q"}},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665421737094,"modifiedTime":1665424108487,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"GaFK7bpM7STT3c9k"}
|
||||||
|
{"name":"sen","type":"script","author":"n8REKaKbUodLowfK","img":"icons/magic/perception/third-eye-blue-red.webp","scope":"global","command":"actor.rollSkill(\"sen\", { skipDialog: true });","flags":{"furnace":{"runAsGM":false},"scene-packer":{"sourceId":"Macro.VACatOAtlLyDrdQ4"},"core":{"sourceId":"Macro.VACatOAtlLyDrdQ4"}},"ownership":{"default":2,"IqGUPfrQK7VNUbWe":3,"QGzUrxr480shRjdU":3,"n8REKaKbUodLowfK":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665424032911,"modifiedTime":1665424032911,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"JB4K8VoaLUN6HBR9"}
|
||||||
|
{"name":"ref","type":"script","author":"n8REKaKbUodLowfK","img":"icons/svg/falling.svg","scope":"global","command":"actor.rollSavingThrow (\"ref\", {skipDialogue:true});","flags":{"furnace":{"runAsGM":false},"scene-packer":{"sourceId":"Macro.RWOczORxSMHKQzeQ"},"advanced-macros":{"runAsGM":false},"core":{"sourceId":"Macro.Mk0IuCSbMinpg5fg"}},"ownership":{"default":2,"IqGUPfrQK7VNUbWe":3,"QGzUrxr480shRjdU":3,"n8REKaKbUodLowfK":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665419763071,"modifiedTime":1665419763071,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"KCx14hfM2aBXfHKC"}
|
||||||
|
{"name":"kpl","type":"script","author":"n8REKaKbUodLowfK","img":"icons/creatures/unholy/demon-fire-horned-mask.webp","scope":"global","command":"actor.rollSkill(\"kpl\", { skipDialog: true });","flags":{"furnace":{"runAsGM":false},"scene-packer":{"sourceId":"Macro.MqQ5KTxV6yxzladX"},"core":{"sourceId":"Macro.MqQ5KTxV6yxzladX"}},"ownership":{"default":2,"IqGUPfrQK7VNUbWe":3,"QGzUrxr480shRjdU":3,"n8REKaKbUodLowfK":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665424027987,"modifiedTime":1665424027987,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"M52daPEx4dN8HiSx"}
|
||||||
|
{"name":"for","type":"script","author":"n8REKaKbUodLowfK","img":"icons/svg/castle.svg","scope":"global","command":"actor.rollSavingThrow (\"fort\", {skipDialogue:true});","flags":{"furnace":{"runAsGM":false},"scene-packer":{"sourceId":"Macro.Xx5FxMYl963U1rsb"},"core":{"sourceId":"Macro.Xx5FxMYl963U1rsb"}},"ownership":{"default":2,"IqGUPfrQK7VNUbWe":3,"QGzUrxr480shRjdU":3,"n8REKaKbUodLowfK":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665419729583,"modifiedTime":1665419729583,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"NL6XP9ywtrcNIPkt"}
|
||||||
|
{"name":"lin","type":"script","author":"n8REKaKbUodLowfK","img":"icons/sundries/documents/document-sealed-signatures-red.webp","scope":"global","command":"actor.rollSkill(\"lin\", { skipDialog: true });","flags":{"furnace":{"runAsGM":false},"scene-packer":{"sourceId":"Macro.kDAa7dIsPE5fMblw"},"core":{"sourceId":"Macro.kDAa7dIsPE5fMblw"}},"ownership":{"default":2,"IqGUPfrQK7VNUbWe":3,"QGzUrxr480shRjdU":3,"n8REKaKbUodLowfK":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665424029258,"modifiedTime":1665424029258,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"OXkY37sKy7leZIPC"}
|
||||||
|
{"name":"dex","type":"script","author":"n8REKaKbUodLowfK","img":"icons/creatures/amphibians/frog-water-teal.webp","scope":"global","command":"actor.rollAbilityTest (\"dex\", {skipDialogue:true});","ownership":{"default":0,"n8REKaKbUodLowfK":3},"flags":{"advanced-macros":{"runAsGM":false},"core":{"sourceId":"Macro.StBBmY44jm0viyyA"}},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665421709517,"modifiedTime":1665424189403,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"PTWCQTLCthvTJx5B"}
|
||||||
|
{"name":"han","type":"script","img":"icons/creatures/mammals/ox-buffalo-horned-green.webp","command":"actor.rollSkill(\"han\", { skipDialog: true });","flags":{"pf1":{"skillMacro":true},"advanced-macros":{"runAsGM":false},"core":{"sourceId":"Macro.s94B55Q1Rz9RGbpk"}},"author":"n8REKaKbUodLowfK","scope":"global","ownership":{"default":0,"QGzUrxr480shRjdU":3,"n8REKaKbUodLowfK":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1663881240379,"modifiedTime":1665424019589,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"Pdf0605m4dZlH5HG"}
|
||||||
|
{"name":"per","type":"script","author":"n8REKaKbUodLowfK","img":"icons/creatures/eyes/lizard-single-slit-blue.webp","scope":"global","command":"actor.rollSkill(\"per\", { skipDialog: true });","flags":{"furnace":{"runAsGM":false},"combat-utility-belt":{"macroTrigger":""},"scene-packer":{"sourceId":"Macro.kumhmgkreLmN1ShJ"},"core":{"sourceId":"Macro.kumhmgkreLmN1ShJ"}},"ownership":{"default":2,"QGzUrxr480shRjdU":3,"n8REKaKbUodLowfK":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665424030241,"modifiedTime":1665424030241,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"QpYy9KdENZH48uHu"}
|
||||||
|
{"name":"kna","type":"script","author":"n8REKaKbUodLowfK","img":"icons/creatures/abilities/paw-print-orange.webp","scope":"global","command":"actor.rollSkill(\"kna\", { skipDialog: true });","flags":{"furnace":{"runAsGM":false},"combat-utility-belt":{"macroTrigger":""},"scene-packer":{"sourceId":"Macro.AHQwizDFENNukY6L"},"core":{"sourceId":"Macro.AHQwizDFENNukY6L"}},"ownership":{"default":2,"IqGUPfrQK7VNUbWe":3,"QGzUrxr480shRjdU":3,"n8REKaKbUodLowfK":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665424026772,"modifiedTime":1665424026772,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"RCiDwBBk4BHfdXiN"}
|
||||||
|
{"name":"swm","type":"script","author":"n8REKaKbUodLowfK","img":"icons/creatures/fish/fish-marlin-swordfight-blue.webp","scope":"global","command":"actor.rollSkill(\"swm\", { skipDialog: true });","flags":{"furnace":{"runAsGM":false},"scene-packer":{"sourceId":"Macro.3peVFMOKyrrF7cHO"},"core":{"sourceId":"Macro.3peVFMOKyrrF7cHO"}},"ownership":{"default":2,"IqGUPfrQK7VNUbWe":3,"QGzUrxr480shRjdU":3,"n8REKaKbUodLowfK":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665424037905,"modifiedTime":1665424037905,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"TGlugK1n4sssSe9P"}
|
||||||
|
{"name":"wil","type":"script","author":"n8REKaKbUodLowfK","img":"icons/svg/stoned.svg","scope":"global","command":"actor.rollSavingThrow (\"will\", {skipDialogue:true});","flags":{"furnace":{"runAsGM":false},"scene-packer":{"sourceId":"Macro.eUSm371nJfPPqcay","defaultPermission":1},"core":{"sourceId":"Macro.eUSm371nJfPPqcay"}},"ownership":{"default":2,"IqGUPfrQK7VNUbWe":3,"QGzUrxr480shRjdU":3,"n8REKaKbUodLowfK":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665419854116,"modifiedTime":1665419854116,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"Vy48N8cAAM76aec9"}
|
||||||
|
{"name":"kar","type":"script","author":"n8REKaKbUodLowfK","img":"icons/magic/symbols/rune-sigil-rough-white-teal.webp","scope":"global","command":"actor.rollSkill(\"kar\", { skipDialog: true });","flags":{"furnace":{"runAsGM":false},"scene-packer":{"sourceId":"Macro.WOW0pCqOBKFErRhG"},"core":{"sourceId":"Macro.WOW0pCqOBKFErRhG"}},"ownership":{"default":2,"IqGUPfrQK7VNUbWe":3,"QGzUrxr480shRjdU":3,"n8REKaKbUodLowfK":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665424021679,"modifiedTime":1665424021679,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"W8mIW9HFax3sv0Ls"}
|
||||||
|
{"name":"con","type":"script","author":"n8REKaKbUodLowfK","img":"icons/creatures/abilities/bear-roar-bite-brown.webp","scope":"global","command":"actor.rollAbilityTest (\"con\", {skipDialogue:true});","ownership":{"default":0,"n8REKaKbUodLowfK":3},"flags":{"advanced-macros":{"runAsGM":false},"core":{"sourceId":"Macro.BTcNFzDauUolabrd"}},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665421718140,"modifiedTime":1665424185505,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"X9Kh2eukynAZJw47"}
|
||||||
|
{"name":"str","type":"script","author":"n8REKaKbUodLowfK","img":"icons/skills/social/intimidation-impressing.webp","scope":"global","command":"actor.rollAbilityTest (\"str\", {skipDialogue:true});","ownership":{"default":0,"n8REKaKbUodLowfK":3},"flags":{"advanced-macros":{"runAsGM":false},"core":{"sourceId":"Macro.waqlvKHJQyd9zasg"}},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665420587231,"modifiedTime":1665424213804,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"XsS81F8BfPELTJkX"}
|
||||||
|
{"name":"apr","type":"script","author":"kvH0iCMGDwLyA2bo","img":"icons/commodities/treasure/bust-carved-stone.webp","scope":"global","command":"actor.rollSkill(\"apr\", {skipDialogue:true});","flags":{"combat-utility-belt":{"macroTrigger":""},"furnace":{"runAsGM":false},"scene-packer":{"sourceId":"Macro.nTjdVp4yuSGn7FVP"},"core":{"sourceId":"Macro.nTjdVp4yuSGn7FVP"},"advanced-macros":{"runAsGM":false}},"ownership":{"default":2,"IqGUPfrQK7VNUbWe":3,"QGzUrxr480shRjdU":3,"n8REKaKbUodLowfK":3,"kvH0iCMGDwLyA2bo":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665424011987,"modifiedTime":1665446536111,"lastModifiedBy":"kvH0iCMGDwLyA2bo"},"folder":null,"sort":0,"_id":"Y7xPe034AjJbLvmv"}
|
||||||
|
{"name":"stab","type":"script","author":"n8REKaKbUodLowfK","img":"icons/svg/heal.svg","scope":"global","command":"const tokens = canvas.tokens.controlled;\n\nif (tokens.length !== 1) {\n ui.notifications.warn(\"Select a token.\");\n} else {\n const actor = tokens[0].actor;\n const conMod = actor.data.data.abilities.con.mod;\n const hp = actor.data.data.attributes.hp.value;\n\n const penalty = hp < 0 ? Math.abs(hp) : 0;\n\n const roll = new Roll(`1d20 + ${conMod} - ${penalty}`);\n roll.roll();\n roll.toMessage({\n flavor: `Rolling a stabilise check! DC 10.`,\n speaker: { alias: actor.data.name },\n });\n}","flags":{"advanced-macros":{"runAsGM":false},"core":{"sourceId":"Macro.3XDMCX09ZVMsivA7"}},"ownership":{"default":0,"n8REKaKbUodLowfK":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665421244191,"modifiedTime":1665421244191,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"YpKs4OKf2jyKREy6"}
|
||||||
|
{"name":"rid","type":"script","author":"n8REKaKbUodLowfK","img":"icons/equipment/feet/boots-collared-leather-blue.webp","scope":"global","command":"actor.rollSkill(\"rid\", { skipDialog: true });","flags":{"furnace":{"runAsGM":false},"scene-packer":{"sourceId":"Macro.6fWDBgma02ckZyoA"},"core":{"sourceId":"Macro.6fWDBgma02ckZyoA"}},"ownership":{"default":2,"IqGUPfrQK7VNUbWe":3,"QGzUrxr480shRjdU":3,"n8REKaKbUodLowfK":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665424032222,"modifiedTime":1665424032222,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"c6PRC1dHUY5ac2I4"}
|
||||||
|
{"name":"prof1","type":"script","author":"n8REKaKbUodLowfK","img":"icons/tools/instruments/drum-standing-glowing-green.webp","scope":"global","command":"actor.rollSkill(\"prf.subSkills.prf1\", { skipDialog: true });","ownership":{"default":0,"n8REKaKbUodLowfK":3},"flags":{"advanced-macros":{"runAsGM":false},"core":{"sourceId":"Macro.QqnGjjJisVeY7dUW"}},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665420095579,"modifiedTime":1665424031604,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"cqolkKZ3gVS9Xf1P"}
|
||||||
|
{"name":"dis","type":"script","author":"n8REKaKbUodLowfK","img":"icons/environment/people/cleric-grey.webp","scope":"global","command":"actor.rollSkill(\"dis\", { skipDialog: true });","flags":{"furnace":{"runAsGM":false},"core":{"sourceId":"Macro.xNs0vMJ7jfiTEmwW"}},"ownership":{"default":2,"IqGUPfrQK7VNUbWe":3,"QGzUrxr480shRjdU":3,"n8REKaKbUodLowfK":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665424016624,"modifiedTime":1665424016624,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"eRQJJd8vS7W6eyzv"}
|
||||||
|
{"name":"kre","type":"script","author":"n8REKaKbUodLowfK","img":"icons/commodities/treasure/totem-wooden-glowing-green.webp","scope":"global","command":"actor.rollSkill(\"kre\", { skipDialog: true });","flags":{"furnace":{"runAsGM":false},"scene-packer":{"sourceId":"Macro.MMSG02vUumEBNPOY"},"core":{"sourceId":"Macro.MMSG02vUumEBNPOY"}},"ownership":{"default":2,"IqGUPfrQK7VNUbWe":3,"QGzUrxr480shRjdU":3,"n8REKaKbUodLowfK":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665424028642,"modifiedTime":1665424028642,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"eWlekF8g390dS20D"}
|
||||||
|
{"name":"kdu","type":"script","author":"n8REKaKbUodLowfK","img":"icons/environment/settlement/well.webp","scope":"global","command":"actor.rollSkill(\"kdu\", { skipDialog: true });","flags":{"furnace":{"runAsGM":false},"scene-packer":{"sourceId":"Macro.Pb7sX8R5Tn5NrPIg"},"core":{"sourceId":"Macro.Pb7sX8R5Tn5NrPIg"}},"ownership":{"default":2,"IqGUPfrQK7VNUbWe":3,"QGzUrxr480shRjdU":3,"n8REKaKbUodLowfK":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665424022378,"modifiedTime":1665424022378,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"ezS8Y1dW3KroBABz"}
|
||||||
|
{"name":"acr","type":"script","author":"n8REKaKbUodLowfK","img":"systems/pf1/icons/feats/acrobatic.jpg","scope":"global","command":"actor.rollSkill(\"acr\", {skipDialogue:true});","flags":{"furnace":{"runAsGM":false},"combat-utility-belt":{"macroTrigger":""},"scene-packer":{"sourceId":"Macro.rZiTMGN1R1z8kaK0"},"core":{"sourceId":"Macro.rZiTMGN1R1z8kaK0"}},"ownership":{"default":2,"IqGUPfrQK7VNUbWe":3,"QGzUrxr480shRjdU":3,"n8REKaKbUodLowfK":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665424008410,"modifiedTime":1665424008410,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"hUqtD3qTQ4kQouz1"}
|
||||||
|
{"name":"kge","type":"script","author":"n8REKaKbUodLowfK","img":"icons/environment/wilderness/cave-entrance-mountain-blue.webp","scope":"global","command":"actor.rollSkill(\"kge\", { skipDialog: true });","flags":{"furnace":{"runAsGM":false},"scene-packer":{"sourceId":"Macro.7eOX00yvxdLiGWqD"},"core":{"sourceId":"Macro.7eOX00yvxdLiGWqD"}},"ownership":{"default":2,"IqGUPfrQK7VNUbWe":3,"QGzUrxr480shRjdU":3,"n8REKaKbUodLowfK":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665424023639,"modifiedTime":1665424023639,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"i7jV0mCp1g3NpwCF"}
|
||||||
|
{"name":"fly","type":"script","author":"n8REKaKbUodLowfK","img":"icons/creatures/birds/dove-pigeon-flying-white.webp","scope":"global","command":"actor.rollSkill(\"fly\", { skipDialog: true });","flags":{"furnace":{"runAsGM":false},"combat-utility-belt":{"macroTrigger":""},"scene-packer":{"sourceId":"Macro.LBpQYkxNGEW2OGbZ"},"core":{"sourceId":"Macro.LBpQYkxNGEW2OGbZ"}},"ownership":{"default":2,"IqGUPfrQK7VNUbWe":3,"QGzUrxr480shRjdU":3,"n8REKaKbUodLowfK":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665424017963,"modifiedTime":1665424017963,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"l1yvCzlkftnVTp1A"}
|
||||||
|
{"name":"conc2","type":"script","author":"n8REKaKbUodLowfK","img":"systems/pf1/icons/skills/light_01.jpg","scope":"global","command":"actor.rollConcentration(\"secondary\");","flags":{"pf1":{"miscMacro":true},"advanced-macros":{"runAsGM":false},"core":{"sourceId":"Macro.NTpXSvr36bovHOQz"}},"ownership":{"default":2,"n8REKaKbUodLowfK":3,"QGzUrxr480shRjdU":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665419620939,"modifiedTime":1665419620939,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"lDOOQJxSw2apoE8n"}
|
||||||
|
{"name":"slt","type":"script","img":"icons/skills/social/theft-pickpocket-bribery-brown.webp","command":"actor.rollSkill(\"slt\", { skipDialog: true });","flags":{"pf1":{"skillMacro":true},"advanced-macros":{"runAsGM":false},"core":{"sourceId":"Macro.L20TVvP5cxvC82s2"}},"author":"n8REKaKbUodLowfK","scope":"global","ownership":{"default":0,"n8REKaKbUodLowfK":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1663881608057,"modifiedTime":1665424035288,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"lzHy3hpeznuSnZea"}
|
||||||
|
{"name":"ini","type":"script","author":"n8REKaKbUodLowfK","img":"icons/magic/time/hourglass-tilted-glowing-gold.webp","scope":"global","command":"actor.rollInitiative({ createCombatants: true });","flags":{"furnace":{"runAsGM":false},"scene-packer":{"sourceId":"Macro.Gaa5iwoQXjN2HZ6L"},"advanced-macros":{"runAsGM":false},"core":{"sourceId":"Macro.Gaa5iwoQXjN2HZ6L"}},"ownership":{"default":2,"n8REKaKbUodLowfK":3,"QGzUrxr480shRjdU":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665424199676,"modifiedTime":1665424199676,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"mkVSTzaKV2f162y7"}
|
||||||
|
{"name":"khi","type":"script","author":"n8REKaKbUodLowfK","img":"icons/sundries/books/book-worn-blue.webp","scope":"global","command":"actor.rollSkill(\"khi\", { skipDialog: true });","flags":{"furnace":{"runAsGM":false},"scene-packer":{"sourceId":"Macro.Wx7UwhoUal1wCZjA"},"core":{"sourceId":"Macro.Wx7UwhoUal1wCZjA"}},"ownership":{"default":2,"IqGUPfrQK7VNUbWe":3,"QGzUrxr480shRjdU":3,"n8REKaKbUodLowfK":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665424024308,"modifiedTime":1665424024308,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"mx8aHRGLLhlRoI8C"}
|
||||||
|
{"name":"esc","type":"script","author":"n8REKaKbUodLowfK","img":"icons/sundries/survival/cuffs-shackles-steel.webp","scope":"global","command":"actor.rollSkill(\"esc\", { skipDialog: true });","flags":{"furnace":{"runAsGM":false},"scene-packer":{"sourceId":"Macro.8wvJDKK8H7jVlJlx"},"core":{"sourceId":"Macro.8wvJDKK8H7jVlJlx"}},"ownership":{"default":2,"IqGUPfrQK7VNUbWe":3,"QGzUrxr480shRjdU":3,"n8REKaKbUodLowfK":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665424017294,"modifiedTime":1665424017294,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"olU4bqW7ihZ2Mfz5"}
|
||||||
|
{"name":"conc","type":"script","author":"n8REKaKbUodLowfK","img":"systems/pf1/icons/skills/light_01.jpg","scope":"global","command":"actor.rollConcentration(\"primary\");","flags":{"pf1":{"miscMacro":true},"advanced-macros":{"runAsGM":false},"core":{"sourceId":"Macro.2OALp3wXJDomDiYE"}},"ownership":{"default":2,"n8REKaKbUodLowfK":3,"QGzUrxr480shRjdU":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665419602579,"modifiedTime":1665419602579,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"pTEsjc6Y56tJyr9W"}
|
||||||
|
{"name":"umd","type":"script","author":"n8REKaKbUodLowfK","img":"icons/magic/perception/hand-eye-pink.webp","scope":"global","command":"actor.rollSkill(\"umd\", { skipDialog: true });","flags":{"furnace":{"runAsGM":false},"scene-packer":{"sourceId":"Macro.RA4qk9fCMiY6TJOK"},"core":{"sourceId":"Macro.RA4qk9fCMiY6TJOK"}},"ownership":{"default":2,"IqGUPfrQK7VNUbWe":3,"QGzUrxr480shRjdU":3,"n8REKaKbUodLowfK":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665424038643,"modifiedTime":1665424038643,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"rIUF3hzuMsLQ6Yrg"}
|
||||||
|
{"name":"hea","type":"script","author":"n8REKaKbUodLowfK","img":"icons/consumables/potions/bottle-corked-red.webp","scope":"global","command":"actor.rollSkill(\"hea\", { skipDialog: true });","flags":{"furnace":{"runAsGM":false},"scene-packer":{"sourceId":"Macro.GiOorWNKRTBsb2EK"},"core":{"sourceId":"Macro.GiOorWNKRTBsb2EK"}},"ownership":{"default":2,"IqGUPfrQK7VNUbWe":3,"QGzUrxr480shRjdU":3,"n8REKaKbUodLowfK":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665424020290,"modifiedTime":1665424020290,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"tP7ADUgtdGm4Fpd4"}
|
||||||
|
{"name":"dev","type":"script","author":"n8REKaKbUodLowfK","img":"systems/pf1/icons/items/inventory/lock.jpg","scope":"global","command":"actor.rollSkill(\"dev\", { skipDialog: true });","flags":{"furnace":{"runAsGM":false},"scene-packer":{"sourceId":"Macro.6W87xYaPv3NExSSL"},"core":{"sourceId":"Macro.6W87xYaPv3NExSSL"}},"ownership":{"default":2,"IqGUPfrQK7VNUbWe":3,"QGzUrxr480shRjdU":3,"n8REKaKbUodLowfK":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665424015408,"modifiedTime":1665424015408,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"tjy8rHLhduYjA4xI"}
|
||||||
|
{"name":"prf","type":"script","author":"n8REKaKbUodLowfK","img":"icons/tools/instruments/drum-standing-glowing-green.webp","scope":"global","command":"actor.rollSkill(\"prf\", { skipDialog: true });","flags":{"combat-utility-belt":{"macroTrigger":""},"furnace":{"runAsGM":false},"scene-packer":{"sourceId":"Macro.Tjz54EamjivJnkIP"},"core":{"sourceId":"Macro.Tjz54EamjivJnkIP"}},"ownership":{"default":2,"IqGUPfrQK7VNUbWe":3,"QGzUrxr480shRjdU":3,"n8REKaKbUodLowfK":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665424030954,"modifiedTime":1665424030954,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"tmHcv0bPslTk5e0U"}
|
||||||
|
{"name":"dip","type":"script","author":"n8REKaKbUodLowfK","img":"icons/skills/social/diplomacy-handshake.webp","scope":"global","command":"actor.rollSkill(\"dip\", { skipDialog: true });","flags":{"furnace":{"runAsGM":false},"scene-packer":{"sourceId":"Macro.i8YrSVuAsL3Zh5E4"},"core":{"sourceId":"Macro.i8YrSVuAsL3Zh5E4"}},"ownership":{"default":2,"IqGUPfrQK7VNUbWe":3,"QGzUrxr480shRjdU":3,"n8REKaKbUodLowfK":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665424016020,"modifiedTime":1665424016020,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"vIf5wXpGyFcH2iuz"}
|
||||||
|
{"name":"sur","type":"script","author":"n8REKaKbUodLowfK","img":"icons/sundries/survival/stake-rough-heavy-brown.webp","scope":"global","command":"actor.rollSkill(\"sur\", { skipDialog: true });","flags":{"furnace":{"runAsGM":false},"combat-utility-belt":{"macroTrigger":""},"core":{"sourceId":"Macro.DOphe9O53fE4wQfg"}},"ownership":{"default":2,"IqGUPfrQK7VNUbWe":3,"QGzUrxr480shRjdU":3,"n8REKaKbUodLowfK":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665424037257,"modifiedTime":1665424037257,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"xDr2pm4If2F4G77I"}
|
||||||
|
{"name":"klo","type":"script","author":"n8REKaKbUodLowfK","img":"icons/environment/settlement/tavern.webp","scope":"global","command":"actor.rollSkill(\"klo\", { skipDialog: true });","flags":{"furnace":{"runAsGM":false},"core":{"sourceId":"Macro.DhbkGFx4yWpvJFAv"}},"ownership":{"default":2,"IqGUPfrQK7VNUbWe":3,"QGzUrxr480shRjdU":3,"n8REKaKbUodLowfK":3},"_stats":{"systemId":"pf1","systemVersion":"0.82.2","coreVersion":"10.287","createdTime":1665424026138,"modifiedTime":1665424026138,"lastModifiedBy":"n8REKaKbUodLowfK"},"folder":null,"sort":0,"_id":"xv7ZDNI5BP4OSC0x"}
|
||||||
@@ -107,7 +107,7 @@ Hooks.once('init', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const hideShowForPlayers = game.settings.get('searchanywhere', 'settingPlayers');
|
const hideShowForPlayers = game.settings.get('searchanywhere', 'settingPlayers');
|
||||||
if(hideShowForPlayers && !new User(game.data.users.find(user => user._id === game.data.userId)).isGM) {
|
if(hideShowForPlayers && !game.user.isGM) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -668,7 +668,7 @@ class AutoCompletionField {
|
|||||||
let folder = entity.folder;
|
let folder = entity.folder;
|
||||||
while (folder !== null && !excluded) {
|
while (folder !== null && !excluded) {
|
||||||
excluded = excludedFolders.includes(folder.id);
|
excluded = excludedFolders.includes(folder.id);
|
||||||
folder = folder.data.parent ? game.folders.get(folder.data.parent.id) : null;
|
folder = folder.folder ? game.folders.get(folder.folder.id) : null;
|
||||||
}
|
}
|
||||||
return excluded;
|
return excluded;
|
||||||
}
|
}
|
||||||
@@ -1042,11 +1042,11 @@ class EntitySuggestionData {
|
|||||||
get content() {
|
get content() {
|
||||||
let content = '';
|
let content = '';
|
||||||
switch (this.entityType) {
|
switch (this.entityType) {
|
||||||
case 'Actor': content = safeGet(this.entity, 'data.data.details.biography.value'); break;
|
case 'Actor': content = safeGet(this.entity, 'system.details.biography.value'); break;
|
||||||
case 'Item': content = safeGet(this.entity, 'data.data.description.value'); break;
|
case 'Item': content = safeGet(this.entity, 'system.description.value'); break;
|
||||||
case 'JournalEntry': content = safeGet(this.entity, 'data.content'); break;
|
case 'JournalEntry': content = safeGet(this.entity, 'content'); break;
|
||||||
case 'RollTable': content = safeGet(this.entity, 'data.content'); break;
|
case 'RollTable': content = safeGet(this.entity, 'description'); break;
|
||||||
case 'Playlist': content = safeGet(this.entity, 'data.description'); break;
|
case 'Playlist': content = safeGet(this.entity, 'description'); break;
|
||||||
}
|
}
|
||||||
return stripHtml(content);
|
return stripHtml(content);
|
||||||
}
|
}
|
||||||
@@ -1065,7 +1065,7 @@ class EntitySuggestionData {
|
|||||||
|
|
||||||
get image() {
|
get image() {
|
||||||
switch (this.entityType) {
|
switch (this.entityType) {
|
||||||
case 'Macro': return this.entity.data.img;
|
case 'Macro': return this.entity.img;
|
||||||
case 'JournalEntry': return 'modules/searchanywhere/icons/book.svg';
|
case 'JournalEntry': return 'modules/searchanywhere/icons/book.svg';
|
||||||
case 'RollTable': return 'icons/svg/d20-grey.svg';
|
case 'RollTable': return 'icons/svg/d20-grey.svg';
|
||||||
}
|
}
|
||||||
@@ -1161,7 +1161,7 @@ class EntitySuggestionData {
|
|||||||
|
|
||||||
toSheet(sheetTd) {
|
toSheet(sheetTd) {
|
||||||
let sheet = game.searchAnywhere.openedSheets.get(parseInt(sheetTd));
|
let sheet = game.searchAnywhere.openedSheets.get(parseInt(sheetTd));
|
||||||
sheet.actor.createEmbeddedDocuments("Item", [this.entity.data.toJSON()]);
|
sheet.actor.createEmbeddedDocuments("Item", [this.entity.toObject()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
toTable(sheetTd) {
|
toTable(sheetTd) {
|
||||||
@@ -1172,7 +1172,7 @@ class EntitySuggestionData {
|
|||||||
collection: this.entity.entity,
|
collection: this.entity.entity,
|
||||||
text: this.entity.name,
|
text: this.entity.name,
|
||||||
resultId: this.entity.id,
|
resultId: this.entity.id,
|
||||||
img: this.entity.data.img || null
|
img: this.entity.img || null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1347,7 +1347,7 @@ class CompendiumSuggestionData {
|
|||||||
this.pack.getDocument(this.entry._id)
|
this.pack.getDocument(this.entry._id)
|
||||||
.then(entity => {
|
.then(entity => {
|
||||||
let sheet = game.searchAnywhere.openedSheets.get(parseInt(sheetTd));
|
let sheet = game.searchAnywhere.openedSheets.get(parseInt(sheetTd));
|
||||||
sheet.actor.createEmbeddedDocuments("Item", [entity.data.toJSON()]);
|
sheet.actor.createEmbeddedDocuments("Item", [entity.toObject()]);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error(`Unable to add compendium item to sheet: ${err}`);
|
console.error(`Unable to add compendium item to sheet: ${err}`);
|
||||||
@@ -1364,7 +1364,7 @@ class CompendiumSuggestionData {
|
|||||||
collection: this.pack.collection,
|
collection: this.pack.collection,
|
||||||
text: entity.name,
|
text: entity.name,
|
||||||
resultId: entity.id,
|
resultId: entity.id,
|
||||||
img: entity.data.img || null
|
img: entity.img || null
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user