Add Field Agents compendium: 8 pregen agents

Eight ready-to-play agents spanning the combat spectrum, each with stats,
skills, a bio, employer/education, two bonds, and appropriate firearms:
- SITKA (Special Operator, elite) ... MERCER (Forensic Scientist, support).
All are combat-capable (firearms 40-80). Registered as packs/field-agents.

Portraits: agents use a neutral placeholder token; build/gen-field-agents.mjs
auto-wires per-agent pencil portraits from assets/tokens/<name>.webp once
present (see assets/tokens/README.md for filenames). Build script committed
for reproducibility.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 22:24:54 +01:00
co-authored by Claude Fable 5
parent d5e62e08a7
commit 89a45732d3
28 changed files with 289 additions and 18 deletions
+24
View File
@@ -0,0 +1,24 @@
# Field Agent portraits
Drop the eight agent portraits here as **512×512 (or larger square) WebP or PNG**,
named exactly as below, then rebuild the pack:
node build/gen-field-agents.mjs
The build auto-detects each file and wires it to the matching agent as both the
character-sheet portrait and the token image. Until a file is present, that agent
uses the neutral placeholder.
| File | Agent | Profession |
|-------------------------|--------------------|---------------------------|
| agent-operator.webp | SITKA, MARCUS | Special Operator |
| agent-marine.webp | HOLLOWAY, DIANA | Force Recon Marine |
| agent-fed.webp | VANCE, THEODORE | FBI Special Agent |
| agent-detective.webp | ROURKE, BERNADETTE | Homicide Detective |
| agent-medic.webp | ELLARD, JONAH | Emergency Physician |
| agent-intel.webp | KAPLAN, SIMONE | Intelligence Case Officer |
| agent-scientist.webp | MERCER, ELLIOT | Forensic Scientist |
| agent-pilot.webp | CALLOWAY, REGINA | Contract Pilot |
WebP is preferred (smaller). PNG also works — if you use PNG, change the
extension in `build/gen-field-agents.mjs` (`assets/tokens/${a.token}.webp`).
+218
View File
@@ -0,0 +1,218 @@
import { extractPack, compilePack } from "/Users/timevans/Git/VRPG/foundry/vanity/node_modules/@foundryvtt/foundryvtt-cli/index.mjs";
import fs from "fs";
import path from "path";
const REPO = "/Users/timevans/Git/deltagreen2";
const OUT_SRC = "/private/tmp/claude-501/-Users-timevans-Library-Application-Support-FoundryVTT/0078d6e6-126b-4f13-b7f4-86feaf7bfbbc/scratchpad/agentsrc";
const PACK = `${REPO}/packs/field-agents`;
const PLACEHOLDER = "systems/deltagreen2/assets/icons/person-black-bg.svg";
// Deterministic id from seed.
function id(seed) {
let h = 0x811c9dc5;
for (let i = 0; i < seed.length; i++) { h ^= seed.charCodeAt(i); h = Math.imul(h, 0x01000193) >>> 0; }
let s = "", x = h; const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
for (let i = 0; i < 16; i++) { x = Math.imul(x ^ (x >>> 15), 0x2c1b3c6d) >>> 0; s += chars[x % chars.length]; }
return s;
}
// Load weapon templates from the firearms pack.
const wtmp = `${OUT_SRC}/_firearms`;
fs.rmSync(OUT_SRC, { recursive: true, force: true });
fs.mkdirSync(wtmp, { recursive: true });
await extractPack(`${REPO}/packs/firearms`, wtmp, { yaml: false });
const firearms = fs.readdirSync(wtmp).filter(f => f.endsWith(".json")).map(f => JSON.parse(fs.readFileSync(`${wtmp}/${f}`, "utf8")));
function weapon(name, ownerSeed) {
const w = firearms.find(x => x.name === name);
if (!w) throw new Error(`weapon not found: ${name}`);
const o = structuredClone(w);
delete o._key;
o._id = id(`${ownerSeed}-wpn-${name}`);
return o;
}
function unarmed(ownerSeed) {
return {
_id: id(`${ownerSeed}-unarmed`), name: "Unarmed Attack", type: "weapon",
img: "systems/deltagreen2/assets/icons/unarmed.svg",
system: { skill: "unarmed_combat", range: "0M", damage: "1D4", armorPiercing: 0, lethality: 0, isLethal: false, ammo: 0, ammoMax: 0, expense: "Standard", description: "" },
};
}
function bond(ownerSeed, name, relationship, score) {
return {
_id: id(`${ownerSeed}-bond-${name}`), name, type: "bond",
img: "systems/deltagreen2/assets/icons/person-black-bg.svg",
system: { score, relationship, description: "", hasBeenDamagedSinceLastHomeScene: false },
};
}
// ---- The eight agents ----
// combat tier noted for reference; all can fight (firearms >= 40).
const AGENTS = [
{
name: "SITKA, MARCUS", token: "agent-operator",
profession: "Special Operator", employer: "U.S. Army (det.) / Delta Green", nationality: "American", sex: "Male", age: "38", education: "Ranger School; SFQC",
stats: { str: 15, con: 15, dex: 16, int: 11, pow: 13, cha: 10 },
skills: { firearms: 80, unarmed_combat: 70, melee_weapons: 60, dodge: 60, athletics: 70, alertness: 60, demolitions: 40, heavy_weapons: 50, stealth: 55, survival: 55, first_aid: 45, navigate: 45, search: 40 },
typed: [{ label: "Explosives", group: "Military Science", proficiency: 50 }],
weapons: ["Submachine Gun (SMG)", "Heavy Pistol"],
bonds: [["Ex-wife (Karen Sitka)", "Ex-spouse", 11], ["Ranger buddy (SFC Dale Voss)", "Brother-in-arms", 13]],
bio: "Twenty years in the Regiment and then Delta before a burn notice he never got the paperwork for. Sitka is the man the cell puts on point when the door has to come down hard. He talks little, watches everything, and has already run the worst-case in his head before anyone else has drawn a weapon. What he saw on his last sanctioned deployment is the reason a man from the Program bought him a coffee and told him the truth.",
combat: "Elite",
},
{
name: "HOLLOWAY, DIANA", token: "agent-marine",
profession: "Force Recon Marine", employer: "USMC (Reserve)", nationality: "American", sex: "Female", age: "34", education: "Recon; MARSOC selection",
stats: { str: 16, con: 15, dex: 13, int: 11, pow: 12, cha: 11 },
skills: { firearms: 70, unarmed_combat: 60, melee_weapons: 55, dodge: 45, athletics: 60, heavy_weapons: 50, alertness: 50, survival: 45, navigate: 45, swim: 60, first_aid: 40, search: 40 },
typed: [{ label: "Air", group: "Military Science", proficiency: 40 }],
weapons: ["Rifle (Light)", "Medium Pistol"],
bonds: [["Father (Gunnery Sgt. Ray Holloway, ret.)", "Parent", 12], ["Fire team leader (Sgt. Ana Reyes)", "Comrade", 12]],
bio: "Holloway carries the load nobody else can and never mentions it. A career Marine from a Marine family, she reads terrain and threat by instinct and keeps her people moving when things come apart. She is the cell's spine in a firefight. Privately she keeps a running count of the things that walked away from wounds that should have stopped them, and the number is getting hard to explain.",
combat: "Heavy",
},
{
name: "VANCE, THEODORE", token: "agent-fed",
profession: "FBI Special Agent", employer: "Federal Bureau of Investigation", nationality: "American", sex: "Male", age: "41", education: "J.D.; FBI Academy",
stats: { str: 13, con: 12, dex: 13, int: 13, pow: 12, cha: 13 },
skills: { firearms: 60, unarmed_combat: 50, melee_weapons: 40, dodge: 40, alertness: 50, criminology: 60, law: 50, humint: 50, persuade: 50, search: 50, bureaucracy: 50, drive: 45, first_aid: 40 },
typed: [],
weapons: ["Medium Pistol"],
bonds: [["Wife (Ellen Vance)", "Spouse", 13], ["Field office partner (SA Marcus Doyle)", "Partner", 12]],
bio: "Twelve years working violent crime and public corruption made Vance a patient, methodical investigator who can also put ten rounds center-mass under stress. He runs the cell's interviews and its paper trail, and he is the one who signs the reports that keep everyone out of a cell. He believes in the rule of law. He is beginning to suspect the law has no statutes for what he now investigates.",
combat: "Strong",
},
{
name: "ROURKE, BERNADETTE", token: "agent-detective",
profession: "Homicide Detective", employer: "Metropolitan Police Dept.", nationality: "American", sex: "Female", age: "44", education: "B.A. Criminology; PD academy",
stats: { str: 12, con: 12, dex: 13, int: 12, pow: 13, cha: 12 },
skills: { firearms: 55, unarmed_combat: 50, melee_weapons: 40, dodge: 40, alertness: 55, criminology: 50, humint: 45, persuade: 40, search: 55, drive: 50, first_aid: 40, occult: 20 },
typed: [],
weapons: ["Medium Pistol"],
bonds: [["Son (Danny Rourke, 16)", "Child", 14], ["Old partner (Det. Sal Ferro, ret.)", "Mentor", 11]],
bio: "Twenty-two years and a wall of closed cases, Rourke has stood over more bodies than she can name and learned to read a room in a heartbeat. She is stubborn, sharp, and quietly furious about the ones that got away. Two of those cold cases share details she has never been able to file — details that Delta Green recognized the moment she said them aloud.",
combat: "Good",
},
{
name: "ELLARD, JONAH", token: "agent-medic",
profession: "Emergency Physician", employer: "County Trauma Center", nationality: "American", sex: "Male", age: "36", education: "M.D.; combat casualty care",
stats: { str: 12, con: 13, dex: 13, int: 14, pow: 12, cha: 11 },
skills: { firearms: 45, unarmed_combat: 40, dodge: 35, alertness: 45, first_aid: 70, medicine: 60, surgery: 50, pharmacy: 55, search: 40, science: 0, drive: 40 },
typed: [{ label: "Biology", group: "Science", proficiency: 45 }],
weapons: ["Medium Pistol"],
bonds: [["Sister (Dr. Naomi Ellard)", "Sibling", 12], ["Charge nurse (Whitney Cole)", "Colleague", 11]],
bio: "Ellard has kept people alive in trauma bays and once in the back of a moving truck with a flashlight in his teeth. He is the reason wounded Agents come home. He can hold a pistol steady and will if he must, but his hands were trained to close wounds, not open them. Some of what has come through his ER lately did not match any pathology he was taught, and he kept the samples.",
combat: "Moderate",
},
{
name: "KAPLAN, SIMONE", token: "agent-intel",
profession: "Intelligence Case Officer", employer: "Central Intelligence Agency (NOC)", nationality: "American", sex: "Female", age: "37", education: "M.A. International Relations; the Farm",
stats: { str: 11, con: 12, dex: 14, int: 14, pow: 13, cha: 14 },
skills: { firearms: 50, unarmed_combat: 45, dodge: 45, alertness: 50, stealth: 60, humint: 60, persuade: 55, disguise: 45, sigint: 40, search: 40, bureaucracy: 40, drive: 45 },
typed: [{ label: "Arabic", group: "Foreign Language", proficiency: 50 }, { label: "Russian", group: "Foreign Language", proficiency: 40 }],
weapons: ["Light Pistol"],
bonds: [["Handler (\"Mr. Okafor\")", "Handler", 11], ["Asset she couldn't save (memory)", "Guilt", 12]],
bio: "Kaplan has lived under three names on two continents and can walk into a hostile room and walk out with everyone's secrets. She recruits, she deceives, she disappears. She can shoot well enough to break contact and vanish, which is exactly the point. She was read into Delta Green because a network she ran started reporting things her analysts flatly refused to write down.",
combat: "Moderate",
},
{
name: "MERCER, ELLIOT", token: "agent-scientist",
profession: "Forensic Scientist", employer: "State Crime Laboratory", nationality: "American", sex: "Male", age: "33", education: "Ph.D. Molecular Biology",
stats: { str: 10, con: 11, dex: 12, int: 16, pow: 13, cha: 11 },
skills: { firearms: 40, unarmed_combat: 30, dodge: 35, alertness: 45, computer_science: 55, forensics: 60, first_aid: 30, search: 45, occult: 15, science: 0, pharmacy: 40 },
typed: [{ label: "Biology", group: "Science", proficiency: 65 }, { label: "Chemistry", group: "Science", proficiency: 55 }],
weapons: ["Light Pistol"],
bonds: [["Husband (Peter Mercer)", "Spouse", 13], ["Lab mentor (Dr. Ito)", "Mentor", 11]],
bio: "Mercer sees the story a scene leaves behind — trace, spatter, sequence — and reads it cold. He is the least dangerous man in the cell with a weapon and the most dangerous with a microscope. He carries a pistol because the others insisted, and he practices, badly, because he has now seen what happens to the people who don't. A tissue sample he could not classify is the reason he stopped sleeping.",
combat: "Support",
},
{
name: "CALLOWAY, REGINA", token: "agent-pilot",
profession: "Contract Pilot", employer: "Private aviation / former USAF", nationality: "American", sex: "Female", age: "39", education: "USAF flight school; ATP rating",
stats: { str: 12, con: 12, dex: 15, int: 13, pow: 12, cha: 12 },
skills: { firearms: 50, unarmed_combat: 40, dodge: 45, alertness: 50, drive: 50, navigate: 60, heavy_machiner: 45, athletics: 40, first_aid: 40, search: 40, sigint: 30 },
typed: [{ label: "Aircraft", group: "Pilot", proficiency: 65 }, { label: "Rotary Wing", group: "Pilot", proficiency: 50 }],
weapons: ["Medium Pistol"],
bonds: [["Daughter (Mia Calloway, 11)", "Child", 14], ["Wingman (Capt. Joe Brandt)", "Comrade", 11]],
bio: "Calloway can put an aircraft down on a road at night with the instruments dead, and she has. Cool, precise, and quietly reckless, she is the cell's way in and way out. On the ground she handles a pistol like the trained officer she was. She took a Program contract for the flight pay and the questions nobody would answer, and she is still deciding whether that was the worst mistake of her life.",
combat: "Moderate",
},
];
if (AGENTS.length !== 8) { console.error("need 8"); process.exit(1); }
// Base skill defaults so an agent's non-combat skills look like the pregens (partial override on top of schema defaults is fine).
fs.mkdirSync(OUT_SRC, { recursive: true });
let sort = 0;
for (const a of AGENTS) {
const seed = a.name;
const _id = id(`dg2-agent-${seed}`);
const { str, con, dex, int, pow, cha } = a.stats;
const hp = Math.ceil((str + con) / 2);
const san = pow * 5;
// Build skills object (only overrides; DataModel fills the rest with defaults).
const skills = {};
for (const [k, v] of Object.entries(a.skills)) {
if (v > 0) skills[k] = { proficiency: v };
}
const typedSkills = {};
a.typed.forEach((t, i) => {
typedSkills[`tskill_${String(i + 1).padStart(2, "0")}`] = { label: t.label, group: t.group, proficiency: t.proficiency, failure: false };
});
const items = [
...a.weapons.map((w) => weapon(w, seed)),
unarmed(seed),
...a.bonds.map(([n, r, s]) => bond(seed, n, r, s)),
];
// Embedded items need a hierarchical compile key: !actors.items!<actorId>.<itemId>
for (const it of items) {
it._key = `!actors.items!${_id}.${it._id}`;
}
// Use the pencil portrait for both sheet image and token once it exists on disk;
// otherwise fall back to the neutral placeholder.
const tokenRel = `assets/tokens/${a.token}.webp`;
const tokenImg = fs.existsSync(`${REPO}/${tokenRel}`)
? `systems/deltagreen2/${tokenRel}`
: PLACEHOLDER;
const doc = {
_id, _key: `!actors!${_id}`, name: a.name, type: "agent",
img: tokenImg,
system: {
statistics: {
str: { value: str }, con: { value: con }, dex: { value: dex },
int: { value: int }, pow: { value: pow }, cha: { value: cha },
},
health: { value: hp, min: 0, max: hp },
wp: { value: pow, min: 0, max: pow },
sanity: { value: san, currentBreakingPoint: san - pow, breakingPointHit: false },
skills, typedSkills,
biography: {
profession: a.profession, employer: a.employer, nationality: a.nationality,
sex: a.sex, age: a.age, education: a.education,
},
physical: { description: `<p>${a.bio}</p><p><em>Combat rating: ${a.combat}.</em></p>` },
},
prototypeToken: {
name: a.name,
texture: { src: tokenImg, anchorX: 0.5, anchorY: 0.5, fit: "contain", scaleX: 1, scaleY: 1 },
displayName: 20, actorLink: true, sight: { enabled: true },
},
items,
effects: [],
folder: null,
sort: (sort += 1000),
ownership: { default: 0 },
flags: { deltagreen2: { plannedToken: tokenImg, combat: a.combat } },
_stats: { systemId: "deltagreen2", systemVersion: "2.0.0" },
};
fs.writeFileSync(path.join(OUT_SRC, `${seed.replace(/[^a-z0-9]+/gi, "_")}_${_id}.json`), JSON.stringify(doc, null, 2));
}
fs.rmSync(wtmp, { recursive: true, force: true });
fs.rmSync(PACK, { recursive: true, force: true });
await compilePack(OUT_SRC, PACK, { yaml: false });
console.log(`Compiled ${AGENTS.length} agents into ${PACK}`);
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
MANIFEST-000006
+3
View File
@@ -0,0 +1,3 @@
2026/07/15-22:23:04.934660 170de3000 Recovering log #4
2026/07/15-22:23:04.935753 170de3000 Delete type=0 #4
2026/07/15-22:23:04.935801 170de3000 Delete type=3 #2
+5
View File
@@ -0,0 +1,5 @@
2026/07/15-22:22:50.755440 171b2b000 Delete type=3 #1
2026/07/15-22:22:50.756827 171bb7000 Level-0 table #5: started
2026/07/15-22:22:50.757226 171bb7000 Level-0 table #5: 15899 bytes OK
2026/07/15-22:22:50.757333 171bb7000 Delete type=0 #3
2026/07/15-22:22:50.757432 171bb7000 Manual compaction at level-0 from '!actors!3xs9ghs6mv1wy7ig' @ 72057594037927935 : 1 .. '!actors.items!vq78xlw4wwkiw4u4.zh5tsusgjddror4q' @ 0 : 0; will stop at (end)
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000002
MANIFEST-000006
+3 -5
View File
@@ -1,5 +1,3 @@
2026/07/15-21:04:44.645310 17171b000 Delete type=3 #1
2026/07/15-21:04:44.647621 1717a7000 Level-0 table #5: started
2026/07/15-21:04:44.648040 1717a7000 Level-0 table #5: 13451 bytes OK
2026/07/15-21:04:44.648151 1717a7000 Delete type=0 #3
2026/07/15-21:04:44.648243 1717a7000 Manual compaction at level-0 from '!items!0ggfh90orzn6ldrf' @ 72057594037927935 : 1 .. '!items!zzorhwhnj04fox00' @ 0 : 0; will stop at (end)
2026/07/15-22:02:36.943600 172963000 Recovering log #4
2026/07/15-22:02:36.944823 172963000 Delete type=0 #4
2026/07/15-22:02:36.944861 172963000 Delete type=3 #2
+5
View File
@@ -0,0 +1,5 @@
2026/07/15-22:02:25.536804 17327b000 Delete type=3 #1
2026/07/15-22:02:25.539278 173b13000 Level-0 table #5: started
2026/07/15-22:02:25.539775 173b13000 Level-0 table #5: 13451 bytes OK
2026/07/15-22:02:25.539885 173b13000 Delete type=0 #3
2026/07/15-22:02:25.539970 173b13000 Manual compaction at level-0 from '!items!0ggfh90orzn6ldrf' @ 72057594037927935 : 1 .. '!items!zzorhwhnj04fox00' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
View File
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000002
MANIFEST-000010
+3 -5
View File
@@ -1,5 +1,3 @@
2026/07/15-17:36:13.527610 1723af000 Delete type=3 #1
2026/07/15-17:36:13.528598 17243b000 Level-0 table #5: started
2026/07/15-17:36:13.528807 17243b000 Level-0 table #5: 3054 bytes OK
2026/07/15-17:36:13.528916 17243b000 Delete type=0 #3
2026/07/15-17:36:13.528992 17243b000 Manual compaction at level-0 from '!items!0Gz1jcY28Psezqrv' @ 72057594037927935 : 1 .. '!items!xb73X4SjsbuYs4D7' @ 0 : 0; will stop at (end)
2026/07/15-22:22:50.743988 170307000 Recovering log #9
2026/07/15-22:22:50.744961 170307000 Delete type=3 #8
2026/07/15-22:22:50.745008 170307000 Delete type=0 #9
+3
View File
@@ -0,0 +1,3 @@
2026/07/15-22:22:15.799508 17060f000 Recovering log #7
2026/07/15-22:22:15.800756 17060f000 Delete type=0 #7
2026/07/15-22:22:15.800799 17060f000 Delete type=3 #6
Binary file not shown.
Binary file not shown.
View File
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000002
MANIFEST-000006
+3 -5
View File
@@ -1,5 +1,3 @@
2026/07/15-17:36:14.183161 171397000 Delete type=3 #1
2026/07/15-17:36:14.206130 17243b000 Level-0 table #5: started
2026/07/15-17:36:14.226780 17243b000 Level-0 table #5: 3411818 bytes OK
2026/07/15-17:36:14.226999 17243b000 Delete type=0 #3
2026/07/15-17:36:14.227636 17243b000 Manual compaction at level-0 from '!actors!02uQjuR9EWKHVnMm' @ 72057594037927935 : 1 .. '!folders!w3IlJRYnQRCVCyhK' @ 0 : 0; will stop at (end)
2026/07/15-22:14:12.714607 16ebf7000 Recovering log #4
2026/07/15-22:14:12.716049 16ebf7000 Delete type=0 #4
2026/07/15-22:14:12.716099 16ebf7000 Delete type=3 #2
+5
View File
@@ -0,0 +1,5 @@
2026/07/15-17:36:14.183161 171397000 Delete type=3 #1
2026/07/15-17:36:14.206130 17243b000 Level-0 table #5: started
2026/07/15-17:36:14.226780 17243b000 Level-0 table #5: 3411818 bytes OK
2026/07/15-17:36:14.226999 17243b000 Delete type=0 #3
2026/07/15-17:36:14.227636 17243b000 Manual compaction at level-0 from '!actors!02uQjuR9EWKHVnMm' @ 72057594037927935 : 1 .. '!folders!w3IlJRYnQRCVCyhK' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
+13
View File
@@ -108,6 +108,7 @@
"armor",
"hand-to-hand-weapons",
"field-equipment",
"field-agents",
"agent-pregens",
"operation-code-name-generator-tables"
],
@@ -172,6 +173,18 @@
"ASSISTANT": "OWNER"
}
},
{
"name": "field-agents",
"label": "Field Agents (DG2)",
"system": "deltagreen2",
"path": "packs/field-agents",
"type": "Actor",
"flags": {},
"ownership": {
"PLAYER": "OBSERVER",
"ASSISTANT": "OWNER"
}
},
{
"name": "agent-pregens",
"label": "Agent Pregens",