diff --git a/art/portraits/auditor.svg b/art/portraits/auditor.svg new file mode 100644 index 0000000..9ed2fff --- /dev/null +++ b/art/portraits/auditor.svg @@ -0,0 +1,26 @@ + + The auditor + The auditor — baseline, SIZ 11, 7 hit locations, natural armour 4, modern. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + + + + + + + diff --git a/art/portraits/barghest.svg b/art/portraits/barghest.svg new file mode 100644 index 0000000..16ca5c2 --- /dev/null +++ b/art/portraits/barghest.svg @@ -0,0 +1,26 @@ + + Barghest + Barghest — baseline, SIZ 15, 7 hit locations, natural armour 3, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + + + + + + + diff --git a/art/portraits/boggart.svg b/art/portraits/boggart.svg new file mode 100644 index 0000000..22cff69 --- /dev/null +++ b/art/portraits/boggart.svg @@ -0,0 +1,20 @@ + + Boggart + Boggart — baseline, SIZ 5, 7 hit locations, natural armour 0, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/bp_callum.svg b/art/portraits/bp_callum.svg new file mode 100644 index 0000000..6ffef46 --- /dev/null +++ b/art/portraits/bp_callum.svg @@ -0,0 +1,20 @@ + + Callum Pike + Callum Pike — baseline, SIZ 15, 7 hit locations, natural armour 0, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/bp_conductor.svg b/art/portraits/bp_conductor.svg new file mode 100644 index 0000000..521e36d --- /dev/null +++ b/art/portraits/bp_conductor.svg @@ -0,0 +1,26 @@ + + The Conductor + The Conductor — baseline, SIZ 14, 7 hit locations, natural armour 4, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + + + + + + + diff --git a/art/portraits/bp_elin.svg b/art/portraits/bp_elin.svg new file mode 100644 index 0000000..cb6517b --- /dev/null +++ b/art/portraits/bp_elin.svg @@ -0,0 +1,20 @@ + + Dr Elin Shaw + Dr Elin Shaw — baseline, SIZ 11, 7 hit locations, natural armour 0, modern. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/bp_inspector.svg b/art/portraits/bp_inspector.svg new file mode 100644 index 0000000..5d9a290 --- /dev/null +++ b/art/portraits/bp_inspector.svg @@ -0,0 +1,26 @@ + + Ticket inspector + Ticket inspector — baseline, SIZ 12, 7 hit locations, natural armour 2, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + + + + + + + diff --git a/art/portraits/bp_leah.svg b/art/portraits/bp_leah.svg new file mode 100644 index 0000000..bd7837e --- /dev/null +++ b/art/portraits/bp_leah.svg @@ -0,0 +1,20 @@ + + Leah Hall + Leah Hall — baseline, SIZ 11, 7 hit locations, natural armour 0, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/bp_mara.svg b/art/portraits/bp_mara.svg new file mode 100644 index 0000000..2b9a932 --- /dev/null +++ b/art/portraits/bp_mara.svg @@ -0,0 +1,20 @@ + + Mara Venn + Mara Venn — baseline, SIZ 12, 7 hit locations, natural armour 0, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/bp_martin.svg b/art/portraits/bp_martin.svg new file mode 100644 index 0000000..e37388a --- /dev/null +++ b/art/portraits/bp_martin.svg @@ -0,0 +1,20 @@ + + Martin Cole + Martin Cole — baseline, SIZ 12, 7 hit locations, natural armour 0, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/bp_nia.svg b/art/portraits/bp_nia.svg new file mode 100644 index 0000000..2533361 --- /dev/null +++ b/art/portraits/bp_nia.svg @@ -0,0 +1,20 @@ + + Nia Mercer + Nia Mercer — baseline, SIZ 12, 7 hit locations, natural armour 0, modern. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/bp_owen.svg b/art/portraits/bp_owen.svg new file mode 100644 index 0000000..a601893 --- /dev/null +++ b/art/portraits/bp_owen.svg @@ -0,0 +1,20 @@ + + Owen Dey + Owen Dey — baseline, SIZ 12, 7 hit locations, natural armour 0, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/bp_sennen.svg b/art/portraits/bp_sennen.svg new file mode 100644 index 0000000..6f1a6bb --- /dev/null +++ b/art/portraits/bp_sennen.svg @@ -0,0 +1,20 @@ + + Sennen Roake + Sennen Roake — baseline, SIZ 10, 7 hit locations, natural armour 0, modern. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/carrion_file.svg b/art/portraits/carrion_file.svg new file mode 100644 index 0000000..a11906b --- /dev/null +++ b/art/portraits/carrion_file.svg @@ -0,0 +1,27 @@ + + The carrion file + The carrion file — baseline, SIZ 9, 7 hit locations, natural armour 6, anomalous. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + + + + + + + diff --git a/art/portraits/ceyla.svg b/art/portraits/ceyla.svg new file mode 100644 index 0000000..2d612f1 --- /dev/null +++ b/art/portraits/ceyla.svg @@ -0,0 +1,20 @@ + + Ceyla Marspont + Ceyla Marspont — baseline, SIZ 13, 7 hit locations, natural armour 0, modern. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/churchgrim.svg b/art/portraits/churchgrim.svg new file mode 100644 index 0000000..1895dfe --- /dev/null +++ b/art/portraits/churchgrim.svg @@ -0,0 +1,27 @@ + + Church grim + Church grim — baseline, SIZ 14, 7 hit locations, natural armour 5, anomalous. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + + + + + + + diff --git a/art/portraits/cleaner.svg b/art/portraits/cleaner.svg new file mode 100644 index 0000000..c8de275 --- /dev/null +++ b/art/portraits/cleaner.svg @@ -0,0 +1,20 @@ + + Cleaner (contractor) + Cleaner (contractor) — baseline, SIZ 12, 7 hit locations, natural armour 0, modern. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/cleaner_marksman.svg b/art/portraits/cleaner_marksman.svg new file mode 100644 index 0000000..845c88b --- /dev/null +++ b/art/portraits/cleaner_marksman.svg @@ -0,0 +1,20 @@ + + Cleaner (marksman) + Cleaner (marksman) — baseline, SIZ 12, 7 hit locations, natural armour 0, modern. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/cuckoo_array.svg b/art/portraits/cuckoo_array.svg new file mode 100644 index 0000000..207beac --- /dev/null +++ b/art/portraits/cuckoo_array.svg @@ -0,0 +1,27 @@ + + Cuckoo array + Cuckoo array — baseline, SIZ 20, 7 hit locations, natural armour 7, anomalous. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + + + + + + + diff --git a/art/portraits/dunn.svg b/art/portraits/dunn.svg new file mode 100644 index 0000000..139df54 --- /dev/null +++ b/art/portraits/dunn.svg @@ -0,0 +1,20 @@ + + Dunn Halvorsen + Dunn Halvorsen — baseline, SIZ 10, 7 hit locations, natural armour 0, modern. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/duplicate.svg b/art/portraits/duplicate.svg new file mode 100644 index 0000000..6b0ae8f --- /dev/null +++ b/art/portraits/duplicate.svg @@ -0,0 +1,20 @@ + + Duplicate (of an agent) + Duplicate (of an agent) — baseline, SIZ 12, 7 hit locations, natural armour 0, modern. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/hollow_man.svg b/art/portraits/hollow_man.svg new file mode 100644 index 0000000..655693a --- /dev/null +++ b/art/portraits/hollow_man.svg @@ -0,0 +1,26 @@ + + Hollow man + Hollow man — baseline, SIZ 12, 7 hit locations, natural armour 1, modern. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + + + + + + + diff --git a/art/portraits/keepers.svg b/art/portraits/keepers.svg new file mode 100644 index 0000000..7d48454 --- /dev/null +++ b/art/portraits/keepers.svg @@ -0,0 +1,20 @@ + + The Keepers (waystation) + The Keepers (waystation) — baseline, SIZ 12, 7 hit locations, natural armour 0, modern. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/kelpie.svg b/art/portraits/kelpie.svg new file mode 100644 index 0000000..6e57fc6 --- /dev/null +++ b/art/portraits/kelpie.svg @@ -0,0 +1,26 @@ + + Kelpie + Kelpie — baseline, SIZ 18, 7 hit locations, natural armour 2, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + + + + + + + diff --git a/art/portraits/la_edith.svg b/art/portraits/la_edith.svg new file mode 100644 index 0000000..cf64d74 --- /dev/null +++ b/art/portraits/la_edith.svg @@ -0,0 +1,20 @@ + + Edith Crane + Edith Crane — baseline, SIZ 10, 7 hit locations, natural armour 0, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/la_enderby.svg b/art/portraits/la_enderby.svg new file mode 100644 index 0000000..7f141d1 --- /dev/null +++ b/art/portraits/la_enderby.svg @@ -0,0 +1,20 @@ + + Sister Enderby + Sister Enderby — baseline, SIZ 11, 7 hit locations, natural armour 0, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/la_marrow.svg b/art/portraits/la_marrow.svg new file mode 100644 index 0000000..d94ca98 --- /dev/null +++ b/art/portraits/la_marrow.svg @@ -0,0 +1,26 @@ + + Old Marrow + Old Marrow — baseline, SIZ 22, 7 hit locations, natural armour 2, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + + + + + + + diff --git a/art/portraits/la_nowak.svg b/art/portraits/la_nowak.svg new file mode 100644 index 0000000..2d2d7ae --- /dev/null +++ b/art/portraits/la_nowak.svg @@ -0,0 +1,20 @@ + + Beata Nowak + Beata Nowak — baseline, SIZ 11, 7 hit locations, natural armour 0, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/la_sammy.svg b/art/portraits/la_sammy.svg new file mode 100644 index 0000000..95b68f4 --- /dev/null +++ b/art/portraits/la_sammy.svg @@ -0,0 +1,20 @@ + + Sammy, the boy in the wall + Sammy, the boy in the wall — baseline, SIZ 6, 7 hit locations, natural armour 0, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/la_tunnicliffe.svg b/art/portraits/la_tunnicliffe.svg new file mode 100644 index 0000000..27cd1f9 --- /dev/null +++ b/art/portraits/la_tunnicliffe.svg @@ -0,0 +1,20 @@ + + Vic Tunnicliffe + Vic Tunnicliffe — baseline, SIZ 13, 7 hit locations, natural armour 0, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/long_walker.svg b/art/portraits/long_walker.svg new file mode 100644 index 0000000..c4c57e2 --- /dev/null +++ b/art/portraits/long_walker.svg @@ -0,0 +1,27 @@ + + Long walker + Long walker — baseline, SIZ 22, 7 hit locations, natural armour 10, anomalous. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + + + + + + + diff --git a/art/portraits/od_bewley.svg b/art/portraits/od_bewley.svg new file mode 100644 index 0000000..665882b --- /dev/null +++ b/art/portraits/od_bewley.svg @@ -0,0 +1,20 @@ + + Mr Bewley + Mr Bewley — baseline, SIZ 12, 7 hit locations, natural armour 0, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/od_ferrand.svg b/art/portraits/od_ferrand.svg new file mode 100644 index 0000000..ba44092 --- /dev/null +++ b/art/portraits/od_ferrand.svg @@ -0,0 +1,20 @@ + + Mrs Ivy Ferrand + Mrs Ivy Ferrand — baseline, SIZ 11, 7 hit locations, natural armour 0, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/od_kearsley.svg b/art/portraits/od_kearsley.svg new file mode 100644 index 0000000..de0730a --- /dev/null +++ b/art/portraits/od_kearsley.svg @@ -0,0 +1,20 @@ + + Douglas Kearsley + Douglas Kearsley — baseline, SIZ 11, 7 hit locations, natural armour 0, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/od_odame.svg b/art/portraits/od_odame.svg new file mode 100644 index 0000000..cef077b --- /dev/null +++ b/art/portraits/od_odame.svg @@ -0,0 +1,20 @@ + + Councillor Hazel Odame + Councillor Hazel Odame — baseline, SIZ 11, 7 hit locations, natural armour 0, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/od_vasey.svg b/art/portraits/od_vasey.svg new file mode 100644 index 0000000..3466f0b --- /dev/null +++ b/art/portraits/od_vasey.svg @@ -0,0 +1,20 @@ + + Tony Vasey + Tony Vasey — baseline, SIZ 13, 7 hit locations, natural armour 0, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/quarantine_unit.svg b/art/portraits/quarantine_unit.svg new file mode 100644 index 0000000..53e5f74 --- /dev/null +++ b/art/portraits/quarantine_unit.svg @@ -0,0 +1,26 @@ + + Quarantine unit + Quarantine unit — baseline, SIZ 14, 7 hit locations, natural armour 9, modern. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + + + + + + + diff --git a/art/portraits/quiet_neighbours_npc.svg b/art/portraits/quiet_neighbours_npc.svg new file mode 100644 index 0000000..236f4e3 --- /dev/null +++ b/art/portraits/quiet_neighbours_npc.svg @@ -0,0 +1,20 @@ + + The Quiet Neighbours + The Quiet Neighbours — baseline, SIZ 8, 7 hit locations, natural armour 0, modern. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/rakhi.svg b/art/portraits/rakhi.svg new file mode 100644 index 0000000..0c7cab0 --- /dev/null +++ b/art/portraits/rakhi.svg @@ -0,0 +1,20 @@ + + Rakhi Oyelaran-Vost + Rakhi Oyelaran-Vost — baseline, SIZ 19, 7 hit locations, natural armour 0, modern. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/redcap.svg b/art/portraits/redcap.svg new file mode 100644 index 0000000..ecd2bdb --- /dev/null +++ b/art/portraits/redcap.svg @@ -0,0 +1,26 @@ + + Redcap + Redcap — baseline, SIZ 7, 7 hit locations, natural armour 2, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + + + + + + + diff --git a/art/portraits/revenant.svg b/art/portraits/revenant.svg new file mode 100644 index 0000000..5bcfb8a --- /dev/null +++ b/art/portraits/revenant.svg @@ -0,0 +1,27 @@ + + Revenant + Revenant — baseline, SIZ 13, 7 hit locations, natural armour 4, anomalous. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + + + + + + + diff --git a/art/portraits/surveyor.svg b/art/portraits/surveyor.svg new file mode 100644 index 0000000..5f89b9b --- /dev/null +++ b/art/portraits/surveyor.svg @@ -0,0 +1,26 @@ + + Surveyor + Surveyor — baseline, SIZ 13, 7 hit locations, natural armour 8, modern. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + + + + + + + diff --git a/art/portraits/the_choir.svg b/art/portraits/the_choir.svg new file mode 100644 index 0000000..c4d2f36 --- /dev/null +++ b/art/portraits/the_choir.svg @@ -0,0 +1,26 @@ + + The choir + The choir — baseline, SIZ 11, 7 hit locations, natural armour 1, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + + + + + + + diff --git a/art/portraits/the_tenant.svg b/art/portraits/the_tenant.svg new file mode 100644 index 0000000..fee525e --- /dev/null +++ b/art/portraits/the_tenant.svg @@ -0,0 +1,20 @@ + + The tenant + The tenant — baseline, SIZ 10, 7 hit locations, natural armour 0, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/the_understudy.svg b/art/portraits/the_understudy.svg new file mode 100644 index 0000000..a820a3b --- /dev/null +++ b/art/portraits/the_understudy.svg @@ -0,0 +1,26 @@ + + The understudy + The understudy — baseline, SIZ 12, 7 hit locations, natural armour 2, modern. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + + + + + + + diff --git a/art/portraits/tomas.svg b/art/portraits/tomas.svg new file mode 100644 index 0000000..bf56ab8 --- /dev/null +++ b/art/portraits/tomas.svg @@ -0,0 +1,20 @@ + + Tomas Ekwueme + Tomas Ekwueme — baseline, SIZ 12, 7 hit locations, natural armour 0, modern. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/tt_ambrose.svg b/art/portraits/tt_ambrose.svg new file mode 100644 index 0000000..5ef7b51 --- /dev/null +++ b/art/portraits/tt_ambrose.svg @@ -0,0 +1,20 @@ + + Josiah Ambrose + Josiah Ambrose — baseline, SIZ 14, 7 hit locations, natural armour 0, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/tt_bewley.svg b/art/portraits/tt_bewley.svg new file mode 100644 index 0000000..a204aae --- /dev/null +++ b/art/portraits/tt_bewley.svg @@ -0,0 +1,20 @@ + + Mr Bewley + Mr Bewley — baseline, SIZ 11, 7 hit locations, natural armour 0, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/tt_bligh.svg b/art/portraits/tt_bligh.svg new file mode 100644 index 0000000..c519792 --- /dev/null +++ b/art/portraits/tt_bligh.svg @@ -0,0 +1,20 @@ + + Hester Bligh + Hester Bligh — baseline, SIZ 11, 7 hit locations, natural armour 0, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/tt_coat.svg b/art/portraits/tt_coat.svg new file mode 100644 index 0000000..213956c --- /dev/null +++ b/art/portraits/tt_coat.svg @@ -0,0 +1,26 @@ + + Company guard + Company guard — baseline, SIZ 12, 7 hit locations, natural armour 1, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + + + + + + + diff --git a/art/portraits/tt_cordera_man.svg b/art/portraits/tt_cordera_man.svg new file mode 100644 index 0000000..c8edb35 --- /dev/null +++ b/art/portraits/tt_cordera_man.svg @@ -0,0 +1,20 @@ + + Cordera man (town) + Cordera man (town) — baseline, SIZ 13, 7 hit locations, natural armour 0, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/tt_kell.svg b/art/portraits/tt_kell.svg new file mode 100644 index 0000000..8065940 --- /dev/null +++ b/art/portraits/tt_kell.svg @@ -0,0 +1,20 @@ + + Marius Kell + Marius Kell — baseline, SIZ 13, 7 hit locations, natural armour 0, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/tt_merritt.svg b/art/portraits/tt_merritt.svg new file mode 100644 index 0000000..043411b --- /dev/null +++ b/art/portraits/tt_merritt.svg @@ -0,0 +1,26 @@ + + Cass Merritt + Cass Merritt — baseline, SIZ 13, 7 hit locations, natural armour 1, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + + + + + + + diff --git a/art/portraits/tt_prosser.svg b/art/portraits/tt_prosser.svg new file mode 100644 index 0000000..77b4a14 --- /dev/null +++ b/art/portraits/tt_prosser.svg @@ -0,0 +1,20 @@ + + Mr Prosser + Mr Prosser — baseline, SIZ 12, 7 hit locations, natural armour 0, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/tt_rance.svg b/art/portraits/tt_rance.svg new file mode 100644 index 0000000..8e1e4d5 --- /dev/null +++ b/art/portraits/tt_rance.svg @@ -0,0 +1,20 @@ + + Rance Doyle + Rance Doyle — baseline, SIZ 14, 7 hit locations, natural armour 0, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/tt_reyes.svg b/art/portraits/tt_reyes.svg new file mode 100644 index 0000000..c41a2ce --- /dev/null +++ b/art/portraits/tt_reyes.svg @@ -0,0 +1,20 @@ + + Ada Reyes + Ada Reyes — baseline, SIZ 10, 7 hit locations, natural armour 0, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/tt_shard.svg b/art/portraits/tt_shard.svg new file mode 100644 index 0000000..610653b --- /dev/null +++ b/art/portraits/tt_shard.svg @@ -0,0 +1,20 @@ + + Mr Shard + Mr Shard — baseline, SIZ 12, 7 hit locations, natural armour 0, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/tt_tom_doyle.svg b/art/portraits/tt_tom_doyle.svg new file mode 100644 index 0000000..80d8dd1 --- /dev/null +++ b/art/portraits/tt_tom_doyle.svg @@ -0,0 +1,20 @@ + + Tom Doyle + Tom Doyle — baseline, SIZ 12, 7 hit locations, natural armour 0, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/tt_vane.svg b/art/portraits/tt_vane.svg new file mode 100644 index 0000000..1197a53 --- /dev/null +++ b/art/portraits/tt_vane.svg @@ -0,0 +1,20 @@ + + Elias Vane + Elias Vane — baseline, SIZ 11, 7 hit locations, natural armour 0, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/art/portraits/tt_writman.svg b/art/portraits/tt_writman.svg new file mode 100644 index 0000000..07e3ec4 --- /dev/null +++ b/art/portraits/tt_writman.svg @@ -0,0 +1,20 @@ + + Cordera miner + Cordera miner — baseline, SIZ 13, 7 hit locations, natural armour 0, antique. Generated by tools/make-portraits.mjs; do not edit by hand. + + + + + + + + + + + + + + + + + diff --git a/package.json b/package.json index b7f3421..b26ad0f 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,8 @@ "scripts": { "build": "node tools/build-packs.mjs && node tools/update-readme.mjs", "icons": "node tools/make-icons.mjs", + "portraits": "node tools/make-portraits.mjs", + "mj": "node tools/mj-queue.mjs", "simulate": "node tools/simulate.mjs", "check": "node tools/check-rules.mjs && node tools/check-kits.mjs && node tools/check-lang.mjs && node tools/check-templates.mjs && node tools/check-behaviour.mjs && node tools/check-scenarios.mjs && node tools/check-creatures.mjs", "test": "npm run check", diff --git a/tools/build-packs.mjs b/tools/build-packs.mjs index 0549292..fa2467f 100644 --- a/tools/build-packs.mjs +++ b/tools/build-packs.mjs @@ -308,14 +308,35 @@ function buildActor(spec, type) { // A generated portrait if the spec names one, the generic silhouette otherwise. // The token gets the same face: a pregen you can recognise on the map is worth // more at a convention table than a correct one you cannot tell apart. + /* Resolved by stem and extension rather than by an exact filename. + * + * Two things changed. Portraits are no longer only .webp — tools/make-portraits.mjs + * writes .svg for every creature, and tools/mj-queue.mjs files .png when one is + * illustrated by hand — and a creature no longer has to NAME its portrait to get one: + * art found under its own key is used. That is what lets sixty creatures gain art + * without sixty edits to content.mjs. + * + * Order matters. A hand-made raster always beats the generated diagram, so upgrading a + * creature is a matter of dropping the file in; nothing has to be un-said in source. */ + const PORTRAIT_EXT = ["webp", "png", "jpg", "svg"]; + const findPortrait = stem => { + for (const ext of PORTRAIT_EXT) { + const rel = `art/portraits/${stem}.${ext}`; + if (existsSync(path.join(ROOT, rel))) return rel; + } + return null; + }; + let face = ICON.actor; - if (spec.portrait) { - const rel = `art/portraits/${spec.portrait}.webp`; - // A missing portrait is a broken image on a pregen sheet that nobody notices - // until it is on a projector at a table. Fail the build instead. - if (!existsSync(path.join(ROOT, rel))) throw new Error(`${spec.name}: no portrait at ${rel}`); - face = `systems/ringbrp/${rel}`; + const found = findPortrait(spec.portrait ?? spec.key); + // A spec that NAMES a portrait and has none is still a hard failure: that is a broken + // image on a pregen sheet, and nobody notices until it is on a projector at a table. + // A spec that names none and has none simply keeps the generic silhouette. + if (spec.portrait && !found) { + throw new Error(`${spec.name}: no portrait for "${spec.portrait}" ` + + `(looked for ${PORTRAIT_EXT.map(e => `.${e}`).join(", ")} in art/portraits/)`); } + if (found) face = `systems/ringbrp/${found}`; const actorDoc = { _id: actorId, diff --git a/tools/make-portraits.mjs b/tools/make-portraits.mjs new file mode 100644 index 0000000..a6a7d13 --- /dev/null +++ b/tools/make-portraits.mjs @@ -0,0 +1,410 @@ +/** + * Procedural creature portraits, drawn from the creature's own anatomy. + * + * Sixty of the seventy-six actors in this repository have no art at all. The sixteen + * that do are hand-made Midjourney plates for the duty roster; every monster in the + * bestiary and every scenario NPC falls back to a generic silhouette that tells a GM + * nothing and tells a player less. + * + * THE POINT IS NOT THAT IT IS FREE. It is that a portrait composed from + * anatomy.mjs CANNOT CONTRADICT THE STATBLOCK, because it is drawn from the same + * numbers the hit-location roll uses. A Cadence gets five bodies because its location + * table has five entries. A Vesh gets a sensory ridge and no head because it has no + * head to draw. Nobody has to remember to keep the art in step; there is nothing to + * keep in step. + * + * Everything visible is derived: + * + * silhouette BODY_ART[species].parts — 7 for a baseline, 6 for a vesh, 5 for a cadence + * height SIZ 5 (a boggart) to SIZ 22 (a long walker), planted on a groundline + * build STR against SIZ, the only build information a BRP statblock carries + * plating naturalArmour, drawn over every part, legible at 32px + * weapon mark the class of the creature's primary weapon, not its name + * treatment whether it is a person, folklore, recovered technology, or the far side + * hue anchored on that, so a wall of the bestiary sorts itself by eye + * + * Deterministic from the creature key: same creature, same file, so art does not churn + * in git. Output is SVG for the same reasons icons/ is — scalable, tiny, diffable, and + * no image toolchain on any of the three machines this repo is checked out on. + * + * THE HONEST TRADE. These read as diagrammatic: a species-accurate figure on a treated + * tile, not an illustration. Put one beside pc_holloway.webp and you will see it. That + * is the deal — every creature has correct art today, and tools/mj-queue.mjs upgrades + * any individual one to a drawn portrait when it earns the attention. + * + * node tools/make-portraits.mjs write art for every creature that has none + * node tools/make-portraits.mjs --force redraw everything, including existing SVGs + * node tools/make-portraits.mjs --only keepers,barghest + * node tools/make-portraits.mjs --only tt_ prefix match + * node tools/make-portraits.mjs --list what has art, what does not, and why + * + * --force never touches a .webp or .png. Hand-made art is never overwritten by this + * tool, whatever you ask it to do. + */ +import { writeFile, mkdir } from "node:fs/promises"; +import { existsSync } from "node:fs"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import { BODY_ART, LOCATION_TABLES } from "../anatomy.mjs"; +import { WEAPONS, ARMOURS, GEAR } from "./content.mjs"; +import { collectSpecs } from "./all-specs.mjs"; + +const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1")), ".."); +const OUT = path.join(ROOT, "art", "portraits"); + +const WEAPON = new Map(WEAPONS.map(w => [w.key, w])); +const ARMOUR = new Map(ARMOURS.map(a => [a.key, a])); +const GEARM = new Map(GEAR.map(g => [g.key, g])); + +/* ------------------------------------------------------------------ palette */ + +/* Hue anchors, taken from the icon set's own families. + * + * The first cut of this tool coloured by actor-vs-npc — hue 330 against 355 — and drew + * sixty tiles in one indistinguishable shade of maroon. Colour has to carry information + * or it is decoration, so it carries the one thing a GM most wants off a thumbnail: + * WHAT KIND OF THING IS THIS. Agency steel for people, brass for folklore, violet for + * recovered technology, red for the far side. A wall of the bestiary then sorts itself + * by eye before a single name is read. */ +const ERA_HUE = { modern: 205, antique: 35, future: 275, anomalous: 355 }; +const S = 256; +const hsl = (h, s, l) => `hsl(${h} ${s}% ${l}%)`; + +/** FNV-1a. Small, stable across node versions, and that is the whole requirement. */ +function hash(str) { + let h = 0x811c9dc5; + for (let i = 0; i < str.length; i++) { + h ^= str.charCodeAt(i); + h = Math.imul(h, 0x01000193) >>> 0; + } + return h >>> 0; +} + +/* ------------------------------------------------------------------ derivation */ + +/** Rank of era by how far from ordinary it is; the creature is drawn as its most exotic. */ +const ERA_RANK = { modern: 0, antique: 1, future: 2, anomalous: 3 }; + +/** + * Which tile treatment a creature gets. + * + * The icon set uses era to mean "which century did this object come from", because for + * an object that is the interesting question. For a creature it is not: the bestiary's + * natural weapons — grasp, punch — carry no era at all, so reading kit alone paints the + * entire far side "modern agency issue" and the whole wall of portraits goes flat. + * + * The question a GM actually has is whether the thing in front of them is a person. That + * one IS answerable from the spec, structurally rather than by keyword: + * + * a non-baseline species is anomalous by definition + * only natural weapons, well armoured is not a person — the far side, horror + * only natural weapons, lightly armoured is folklore — old, and older than the agency + * carrying manufactured kit is a person, and the kit dates them + * + * So a cleaner reads modern, a redcap reads antique, and a long walker's tile does not + * close. + */ +/** + * The author's own classification, if the spec carries one. Thirty of the seventy-six + * role lines open with a shouted category — "FOLKLORE. The black dog on the road out of + * the village" — and those are the three groupings the README already advertises. An + * authored label beats anything inferred from kit, so it is read first and the + * structural rules below only decide the ones that have none. + */ +export const ROLE_CLASS = { + FOLKLORE: "antique", // older than the agency + FORWARD: "future", // the far side, and the machinery it sends + HORROR: "anomalous", // should not exist, and does + // The rest are people, in various relationships to the department. + CIVILIAN: "modern", DEPARTMENT: "modern", BUREAU: "modern", + ENTRY: "modern", REAL: "modern" +}; + +/** Categories that are not people. Everything else in ROLE_CLASS is somebody. */ +export const MONSTER_TAGS = ["FOLKLORE", "FORWARD", "HORROR"]; + +/* The tag is not always followed by a full stop — "BUREAU, present day. Registry." — so + this matches the shouted word itself and stops at the first word boundary. Anchoring on + the period missed both Bureau staff and read them as monsters. */ +export const roleTagOf = spec => + (String(spec.role ?? "").match(/^([A-Z]{3,})\b/) ?? [, null])[1]; + +export const roleClassOf = spec => ROLE_CLASS[roleTagOf(spec)] ?? null; + +/** + * True when nothing this creature carries was made by anybody: a natural weapon weighs + * nothing and belongs to the family "brawl", and there is no gear and no armour. A + * barghest passes; a miner in 1881 does not, because a pick is a manufactured thing. + */ +export function carriesNothingManufactured(spec) { + const gearKeys = [ + ...(spec.gear ?? []).map(g => Array.isArray(g) ? g[0] : g), + ...(spec.extraGear ?? []).map(g => Array.isArray(g) ? g[0] : g) + ]; + if (gearKeys.length || (spec.armour ?? []).length) return false; + const weapons = [...(spec.weapons ?? []), ...(spec.extraWeapons ?? [])]; + // At least one, deliberately: [].every() is vacuously true, so an empty list once made + // this answer "yes" for anybody carrying nothing at all — which read Callum the bouncer, + // who has no kit listed, as a monster. The test is HAVING natural weapons, not lacking + // manufactured ones. + return weapons.length > 0 + && weapons.every(k => { const w = WEAPON.get(k); return w && w.fam === "brawl" && !w.mass; }); +} + +export function eraOf(spec) { + if ((spec.species ?? "baseline") !== "baseline") return "anomalous"; + + const authored = roleClassOf(spec); + if (authored) return authored; + + const gearKeys = [ + ...(spec.gear ?? []).map(g => Array.isArray(g) ? g[0] : g), + ...(spec.extraGear ?? []).map(g => Array.isArray(g) ? g[0] : g) + ]; + const weaponKeys = [...(spec.weapons ?? []), ...(spec.extraWeapons ?? [])]; + const armourKeys = spec.armour ?? []; + + const carriesNothingMade = carriesNothingManufactured(spec); + + if (carriesNothingMade) { + return (Number(spec.naturalArmour) || 0) >= 4 ? "anomalous" : "antique"; + } + + let best = "modern"; + for (const k of [...weaponKeys, ...armourKeys, ...gearKeys]) { + const era = (WEAPON.get(k) ?? ARMOUR.get(k) ?? GEARM.get(k))?.era; + if (era && (ERA_RANK[era] ?? 0) > ERA_RANK[best]) best = era; + } + return best; +} + +/** + * How tall to draw it. SIZ runs 5 (a boggart) to 22 (a long walker) across the repo, and + * the first cut compressed that into 0.68–1.14, where nothing read as bigger than + * anything else. A boggart should be visibly a thing you could kick. + */ +function heightScale(siz) { + const n = Number(siz) || 13; + const t = (Math.min(24, Math.max(4, n)) - 4) / 20; // 0..1 + return 0.46 + Math.pow(t, 0.75) * 0.54; // 0.46..1.00 of the well +} + +/** + * How broad to draw it, from STR against SIZ — the only build information a BRP + * statblock actually carries. The repo spans 0.73 (Old Marrow, who is eighty) to 2.83 + * (Sammy, who is a child and much stronger than he has any business being), so this is + * real signal rather than noise dressed up as signal. Normalised around 1.05, which is + * roughly where the duty roster sits, so an ordinary adult draws at ordinary width. + */ +function buildScale(ch) { + const str = Number(ch?.str) || 12, siz = Number(ch?.siz) || 12; + const r = Math.pow((str / siz) / 1.05, 0.55); + return Math.min(1.32, Math.max(0.78, r)); +} + +/** The creature's primary weapon, reduced to how it is held rather than what it is. */ +function weaponMark(spec) { + const key = (spec.weapons ?? [])[0] ?? (spec.extraWeapons ?? [])[0]; + const w = key ? WEAPON.get(key) : null; + if (!w) return null; + if (w.cls === "firearm" || w.cls === "energy") return "firearm"; + if (w.cls === "bow" || w.cls === "thrown") return "thrown"; + // A natural weapon weighs nothing and is part of the creature: claws, not a knife. + if (w.fam === "brawl" && !w.mass) return "natural"; + return "melee"; +} + +const MARKS = { + firearm: `M6 20h26v7H16l-4 9H6z`, + melee: `M4 34L30 8l5 5L9 39z`, + thrown: `M4 34a30 30 0 0 1 32-26`, + natural: `M6 8c8 6 12 16 12 30M18 6c8 6 12 16 12 30M30 8c6 6 9 14 9 26` +}; +const MARK_STROKE = new Set(["thrown", "natural"]); + +/* ------------------------------------------------------------------ drawing */ + +/** One anatomical part, in whichever primitive body-figure.hbs would have drawn it. */ +function part(shape, attrs) { + if (shape.t === "ellipse") { + return ``; + } + if (shape.t === "rect") { + return ``; + } + return ``; +} + +export function portraitSvg(spec, kind = "npc") { + const species = spec.species ?? "baseline"; + const art = BODY_ART[species] ?? BODY_ART.baseline; + const table = LOCATION_TABLES[species] ?? LOCATION_TABLES.baseline; + const era = eraOf(spec); + const muted = era === "antique"; + + // Hue: the era anchor, nudged by the key so a row of cleaners is not one flat colour, + // but never far enough to leave its family. + const hue = (ERA_HUE[era] + ((hash(spec.key) % 29) - 14) + 360) % 360; + + const bg1 = hsl(hue, muted ? 38 : 58, muted ? 27 : 30); + const bg2 = hsl(hue, muted ? 44 : 66, 17); + const ink = hsl(hue, muted ? 50 : 78, muted ? 78 : 84); + const mid = hsl(hue, muted ? 38 : 62, 48); + const rim = hsl(hue, 78, 62); + + /* --- era treatment, matching tools/make-icons.mjs --- */ + let corner = "", scan = ""; + let rimPath = ``; + if (era === "antique") corner = ``; + if (era === "future") { + corner = ``; + scan = ``; + } + if (era === "anomalous") { + rimPath = ``; + } + + /* --- the figure --- */ + /* Feet planted on a common groundline rather than centred, so a boggart is short + instead of merely small and floating, and height reads directly against the tile. + The well is the space between that groundline and the top margin: the largest + creature in the game fills it exactly and NOTHING is ever drawn outside it. The + long walker is SIZ 22 and was cropped at the scalp before this was a ratio of the + well rather than of the tile. */ + const GROUND = S * 0.90; + const WELL = GROUND - S * 0.07; + + const [, , vbW, vbH] = String(art.viewBox).split(/\s+/).map(Number); + const sy = (WELL / vbH) * heightScale(spec.ch?.siz); + const sx = sy * buildScale(spec.ch); + const tx = (S - vbW * sx) / 2; + const ty = GROUND - vbH * sy; + + const nat = Number(spec.naturalArmour) || 0; + const shapes = Object.entries(art.parts); + + const body = shapes + .map(([, shape]) => part(shape, `fill="${ink}" stroke="${bg2}" stroke-width="3" stroke-linejoin="round"`)) + .join("\n "); + + /* Natural armour, as plating over every part. Drawn in the figure's own coordinate + space so it tracks the anatomy — a Cadence gets five plated bodies, not one plated + blob. Scaled to be legible at 32px in an actor list, which is the size this is + actually looked at: nothing at 0, unmistakable at 10. */ + const plating = nat <= 0 ? "" : shapes + .map(([, shape]) => part(shape, + `fill="none" stroke="${rim}" stroke-width="${(2 + Math.min(nat, 12) * 0.85).toFixed(2)}" ` + + `opacity="${Math.min(0.92, 0.34 + nat * 0.06).toFixed(2)}" stroke-linejoin="round"`)) + .join("\n "); + + // Something to stand on, scaled with the figure, so size reads as size rather than as + // the picture being zoomed. + const groundR = vbW * sx * 0.52; + const ground = ``; + + /* --- weapon mark, bottom right --- */ + const mk = weaponMark(spec); + const mark = !mk ? "" : + `` + + (MARK_STROKE.has(mk) + ? `` + : ``) + + ``; + + return ` + ${esc(spec.name)} + ${esc(spec.name)} — ${species}, SIZ ${spec.ch?.siz ?? "?"}, ` + + `${table.locations.length} hit locations, natural armour ${nat}, ${era}. ` + + `Generated by tools/make-portraits.mjs; do not edit by hand. + + ${corner} + ${ground} + + ${body} + ${plating} + + ${mark} + ${rimPath} + ${scan} + +`; +} + +const esc = s => String(s).replace(/&/g, "&").replace(//g, ">"); + +/* ------------------------------------------------------------------ run */ + +/** Art already on disk for this stem, in any format, or null. */ +function existingArt(stem) { + for (const ext of ["webp", "png", "jpg", "svg"]) { + if (existsSync(path.join(OUT, `${stem}.${ext}`))) return `${stem}.${ext}`; + } + return null; +} + +/* Only when run directly. portraitSvg() is importable so that a species with no creature + in it yet can still be drawn and looked at — vesh and cadence have full geometry and + nothing has ever used either, so the first creature written in one should not also be + the first time anybody sees what it looks like. A module that writes files and exits + on import is no use for that. */ +const invokedDirectly = process.argv[1] + && import.meta.url === pathToFileURL(process.argv[1]).href; + +if (invokedDirectly) { + +const argv = process.argv.slice(2); +const flag = n => argv.includes(n); +const opt = (n, d) => { const i = argv.indexOf(n); return i >= 0 ? argv[i + 1] : d; }; +const FORCE = flag("--force"); +const LIST = flag("--list"); +const ONLY = (opt("--only", "") || "").split(",").map(s => s.trim()).filter(Boolean); + +const all = await collectSpecs(); +const selected = all.filter(({ spec }) => + !ONLY.length || ONLY.some(o => spec.key === o || spec.key.startsWith(o))); + +if (!selected.length) { + console.error(`nothing matched --only ${ONLY.join(",")}`); + process.exit(1); +} + +if (LIST) { + let has = 0, none = 0; + for (const { spec, kind } of selected) { + const stem = spec.portrait ?? spec.key; + const found = existingArt(stem); + if (found) has++; else none++; + console.log(`${found ? "art " : " "} ${spec.key.padEnd(24)} ${(found ?? "—").padEnd(24)}` + + `${(spec.species ?? "baseline").padEnd(9)} siz ${String(spec.ch?.siz ?? "?").padStart(2)} ${eraOf(spec)} ${kind}`); + } + console.log(`\n${has} with art, ${none} without, of ${selected.length}.`); + process.exit(0); +} + +await mkdir(OUT, { recursive: true }); + +let wrote = 0, skipped = 0, kept = 0; +for (const { spec, kind } of selected) { + const stem = spec.portrait ?? spec.key; + const found = existingArt(stem); + + // Hand-made art is never overwritten, --force or not. Regenerating a Midjourney plate + // into a diagram because a flag was passed is not a mistake anybody recovers from + // without a git checkout, and the tool should not be able to make it. + if (found && !found.endsWith(".svg")) { + if (ONLY.length) console.log(`keep ${spec.key.padEnd(24)} ${found} is hand-made — not replaced`); + kept++; + continue; + } + if (found && !FORCE) { skipped++; continue; } + + await writeFile(path.join(OUT, `${stem}.svg`), portraitSvg(spec, kind), "utf8"); + wrote++; +} + +console.log(`make-portraits: wrote ${wrote}, kept ${kept} hand-made, skipped ${skipped} existing` + + `${FORCE ? "" : " (use --force to redraw generated ones)"} — of ${selected.length}.`); + +}