Re-package the aggregate: bundle Ward + ship the policing layer
Fold Ward into the single Institution install as its psychiatric-care layer. package.sh now builds Ward clean and vendors Ward.dll plus its defs and textures alongside the other layers, and the settings shim gains a Ward (psychiatric care) checkbox that writes the shared InstitutionSettings.ward flag. Re-assembling also carries in Justice's now-shipping policing defs (the constable work type and crime thoughts), so the aggregate finally bundles colony crime and the weighed arrest as well. README updated to five layers over one Core with policing shipped and Ward folded in; adds the changelog, the Workshop preview, and the preview generator.
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 276 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
# Changelog
|
||||
|
||||
Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning: [SemVer](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- **Institution** — the aggregate mod. One install bundles the whole suite: the **Core** propensity /
|
||||
record engine plus the **Contraband**, **Justice**, and **Gangs** feature layers, each behind its own
|
||||
in-game toggle (all on by default). The layers remain modular — the suite can be split back into
|
||||
separate mods at any time; this is packaging, not a merge.
|
||||
- **Settings panel** — a checkbox per layer:
|
||||
- **Contraband** — concealment, search, escape tunnels.
|
||||
- **Justice** — classification, deterrence, discipline & reform, parole, regime.
|
||||
- **Gangs** — prison contraband economies and social capstone.
|
||||
- **Policing** — colony crime and weighed arrests. A large behavioural change, so it carries its own
|
||||
switch (on by default): your own colonists commit crimes on the propensity spectrum, and a weighed
|
||||
arrest can end in a cell or a resisted breakout.
|
||||
|
||||
Toggling a layer off disables its systems in place, without uninstalling anything.
|
||||
|
||||
### Notes
|
||||
- Requires **Harmony**. The suite's only Harmony-using code is Justice's prisoner-recreation postfix.
|
||||
- The standalone **Foul Play** framework bridges into this suite when both are installed.
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Defs>
|
||||
|
||||
<JobDef>
|
||||
<defName>Institution_Investigate</defName>
|
||||
<driverClass>Contraband.JobDriver_Investigate</driverClass>
|
||||
<reportString>investigating a crime scene.</reportString>
|
||||
<casualInterruptible>true</casualInterruptible>
|
||||
</JobDef>
|
||||
|
||||
</Defs>
|
||||
@@ -0,0 +1,134 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Defs>
|
||||
|
||||
<!--
|
||||
The "locked cabinet of sedatives" in the treatment station's description, made real. Sedation is
|
||||
the other lever a ward has: not therapy, but a way to buy calm through a crisis. It costs a dose
|
||||
and a groggy, useless day from the patient — it settles a breakdown, it does not treat what
|
||||
causes them. A warden administers one to a patient at the edge of a break.
|
||||
-->
|
||||
<ThingDef ParentName="ResourceBase">
|
||||
<defName>Ward_Sedative</defName>
|
||||
<label>sedative</label>
|
||||
<description>A measured dose of fast-acting sedative. A warden can administer one to a patient on the edge of a breakdown to buy calm — at the cost of a groggy, near-useless day. It settles a crisis; it does not treat one.</description>
|
||||
<graphicData>
|
||||
<!-- Placeholder: reuse vanilla industrial medicine's art until Ward ships its own. -->
|
||||
<texPath>Things/Item/Resource/Medicine/MedicineIndustrial</texPath>
|
||||
<graphicClass>Graphic_StackCount</graphicClass>
|
||||
</graphicData>
|
||||
<stackLimit>25</stackLimit>
|
||||
<healthAffectsPrice>false</healthAffectsPrice>
|
||||
<statBases>
|
||||
<MarketValue>14</MarketValue>
|
||||
<Mass>0.05</Mass>
|
||||
<Flammability>0.7</Flammability>
|
||||
<DeteriorationRate>2</DeteriorationRate>
|
||||
</statBases>
|
||||
<thingCategories>
|
||||
<li>Medicine</li>
|
||||
</thingCategories>
|
||||
<tradeTags>
|
||||
<li>Medicine</li>
|
||||
</tradeTags>
|
||||
</ThingDef>
|
||||
|
||||
<RecipeDef>
|
||||
<defName>Ward_MakeSedative</defName>
|
||||
<label>make sedatives</label>
|
||||
<description>Synthesize a batch of sedatives from neutroamine.</description>
|
||||
<jobString>Making sedatives.</jobString>
|
||||
<workAmount>800</workAmount>
|
||||
<workSpeedStat>DrugSynthesisSpeed</workSpeedStat>
|
||||
<effectWorking>Cook</effectWorking>
|
||||
<soundWorking>Recipe_Drug</soundWorking>
|
||||
<workSkill>Intellectual</workSkill>
|
||||
<ingredients>
|
||||
<li>
|
||||
<filter>
|
||||
<thingDefs>
|
||||
<li>Neutroamine</li>
|
||||
</thingDefs>
|
||||
</filter>
|
||||
<count>1</count>
|
||||
</li>
|
||||
</ingredients>
|
||||
<fixedIngredientFilter>
|
||||
<thingDefs>
|
||||
<li>Neutroamine</li>
|
||||
</thingDefs>
|
||||
</fixedIngredientFilter>
|
||||
<products>
|
||||
<Ward_Sedative>4</Ward_Sedative>
|
||||
</products>
|
||||
<recipeUsers>
|
||||
<li>DrugLab</li>
|
||||
</recipeUsers>
|
||||
<researchPrerequisite>DrugProduction</researchPrerequisite>
|
||||
</RecipeDef>
|
||||
|
||||
<!--
|
||||
The sedated state: very calm (a large negative break-threshold offset = breaks far less), but
|
||||
groggy and barely productive, and gone in about a day. Deliberately a bad hediff — it is a crutch
|
||||
with a cost, not free stability.
|
||||
-->
|
||||
<HediffDef>
|
||||
<defName>Ward_Sedated</defName>
|
||||
<label>sedated</label>
|
||||
<description>Dosed with a sedative. Very unlikely to break — and too groggy to do much of anything.</description>
|
||||
<hediffClass>HediffWithComps</hediffClass>
|
||||
<defaultLabelColor>(0.6, 0.6, 0.9)</defaultLabelColor>
|
||||
<scenarioCanAdd>false</scenarioCanAdd>
|
||||
<isBad>true</isBad>
|
||||
<maxSeverity>1.0</maxSeverity>
|
||||
<comps>
|
||||
<!-- About one in-game day from a full dose to nothing. -->
|
||||
<li Class="HediffCompProperties_SeverityPerDay">
|
||||
<severityPerDay>-1.0</severityPerDay>
|
||||
</li>
|
||||
</comps>
|
||||
<stages>
|
||||
<li>
|
||||
<statOffsets>
|
||||
<!-- Negative = breaks at a much lower mood = breaks far less. -->
|
||||
<MentalBreakThreshold>-0.40</MentalBreakThreshold>
|
||||
</statOffsets>
|
||||
<capMods>
|
||||
<li>
|
||||
<capacity>Consciousness</capacity>
|
||||
<offset>-0.25</offset>
|
||||
</li>
|
||||
<li>
|
||||
<capacity>Moving</capacity>
|
||||
<offset>-0.15</offset>
|
||||
</li>
|
||||
</capMods>
|
||||
</li>
|
||||
</stages>
|
||||
</HediffDef>
|
||||
|
||||
<!--
|
||||
A NEW warden work-giver, above treatment in priority: a crisis pre-empts a counselling session.
|
||||
Like the treatment giver it inherits WorkGiver_Warden.ShouldSkip, so it respects prison-commons
|
||||
areas for free.
|
||||
-->
|
||||
<WorkGiverDef>
|
||||
<defName>Ward_WardenAdministerSedative</defName>
|
||||
<label>administer sedative</label>
|
||||
<giverClass>Ward.WorkGiver_Warden_Sedate</giverClass>
|
||||
<workType>Warden</workType>
|
||||
<verb>sedate</verb>
|
||||
<gerund>sedating</gerund>
|
||||
<priorityInType>90</priorityInType>
|
||||
<requiredCapacities>
|
||||
<li>Manipulation</li>
|
||||
</requiredCapacities>
|
||||
</WorkGiverDef>
|
||||
|
||||
<JobDef>
|
||||
<defName>Ward_AdministerSedative</defName>
|
||||
<driverClass>Ward.JobDriver_AdministerSedative</driverClass>
|
||||
<reportString>administering a sedative to TargetA.</reportString>
|
||||
<casualInterruptible>false</casualInterruptible>
|
||||
</JobDef>
|
||||
|
||||
</Defs>
|
||||
@@ -0,0 +1,165 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Defs>
|
||||
|
||||
<!--
|
||||
The warden's job. This is a NEW WorkGiver class, which is the reason Ward survives contact
|
||||
with the prison-mod ecosystem:
|
||||
|
||||
- Custom Prisoner Interactions patches the named vanilla warden givers (_Chat, _Convert,
|
||||
_Enslave, _ReleasePrisoner). It cannot see this one, so it cannot break it.
|
||||
- Prison Commons patches WorkGiver_Warden.ShouldSkip on the BASE class. Ward inherits
|
||||
from WorkGiver_Warden and does not override ShouldSkip, so Ward's warden work respects
|
||||
prison-commons areas for free, without Ward knowing Prison Commons exists.
|
||||
-->
|
||||
<WorkGiverDef>
|
||||
<defName>Ward_WardenPsychiatricCare</defName>
|
||||
<label>provide psychiatric care</label>
|
||||
<giverClass>Ward.WorkGiver_Warden_PsychiatricCare</giverClass>
|
||||
<workType>Warden</workType>
|
||||
<verb>treat</verb>
|
||||
<gerund>treating</gerund>
|
||||
<!-- Above chat (60) and below feeding: an untreated patient deteriorates, a bored one doesn't. -->
|
||||
<priorityInType>70</priorityInType>
|
||||
<requiredCapacities>
|
||||
<li>Talking</li>
|
||||
<li>Hearing</li>
|
||||
</requiredCapacities>
|
||||
</WorkGiverDef>
|
||||
|
||||
<!-- Distinct from the PrisonerInteractionModeDef's defName: [DefOf] binds fields to defNames
|
||||
by name, so two defs sharing one name cannot both be reached from a DefOf class. -->
|
||||
<JobDef>
|
||||
<defName>Ward_ProvidePsychiatricCare</defName>
|
||||
<driverClass>Ward.JobDriver_PsychiatricCare</driverClass>
|
||||
<reportString>treating TargetA.</reportString>
|
||||
<casualInterruptible>false</casualInterruptible>
|
||||
</JobDef>
|
||||
|
||||
<!--
|
||||
What treatment actually buys the patient: a lower chance of breaking. Severity decays, so
|
||||
an untreated patient slides back. This is the entire feedback loop: treat them and they
|
||||
stabilise, neglect them and they don't.
|
||||
-->
|
||||
<HediffDef>
|
||||
<defName>Ward_UnderTreatment</defName>
|
||||
<label>under psychiatric care</label>
|
||||
<description>This person is receiving regular counselling. They are less likely to suffer a mental break.</description>
|
||||
<hediffClass>HediffWithComps</hediffClass>
|
||||
<defaultLabelColor>(0.6, 0.8, 0.9)</defaultLabelColor>
|
||||
<scenarioCanAdd>false</scenarioCanAdd>
|
||||
<maxSeverity>1.0</maxSeverity>
|
||||
<isBad>false</isBad>
|
||||
<comps>
|
||||
<!-- Roughly a week from a full course back to nothing, with no wardens attending. -->
|
||||
<li Class="HediffCompProperties_SeverityPerDay">
|
||||
<severityPerDay>-0.15</severityPerDay>
|
||||
</li>
|
||||
</comps>
|
||||
<stages>
|
||||
<li>
|
||||
<minSeverity>0</minSeverity>
|
||||
<statOffsets>
|
||||
<!-- Negative offset = breaks at a LOWER mood = breaks less often. -->
|
||||
<MentalBreakThreshold>-0.10</MentalBreakThreshold>
|
||||
</statOffsets>
|
||||
</li>
|
||||
</stages>
|
||||
</HediffDef>
|
||||
|
||||
<ThoughtDef>
|
||||
<defName>Ward_Counselled</defName>
|
||||
<thoughtClass>Thought_Memory</thoughtClass>
|
||||
<durationDays>2</durationDays>
|
||||
<stackLimit>3</stackLimit>
|
||||
<stages>
|
||||
<li>
|
||||
<label>counselled</label>
|
||||
<description>Someone sat with me and listened. It helped, a little.</description>
|
||||
<baseMoodEffect>6</baseMoodEffect>
|
||||
</li>
|
||||
</stages>
|
||||
</ThoughtDef>
|
||||
|
||||
<!--
|
||||
The other half of the design. A ward you do not staff is not a neutral place to keep
|
||||
someone. It is a worse one. Held under psychiatric care and left untreated, the patient
|
||||
resents it, which pushes them back toward the break that got them committed.
|
||||
-->
|
||||
<ThoughtDef>
|
||||
<defName>Ward_Neglected</defName>
|
||||
<thoughtClass>Thought_Memory</thoughtClass>
|
||||
<durationDays>3</durationDays>
|
||||
<stackLimit>4</stackLimit>
|
||||
<stages>
|
||||
<li>
|
||||
<label>left in the ward</label>
|
||||
<description>They locked me in here for my own good and then forgot about me.</description>
|
||||
<baseMoodEffect>-8</baseMoodEffect>
|
||||
</li>
|
||||
</stages>
|
||||
</ThoughtDef>
|
||||
|
||||
<!--
|
||||
Recovery: the arc that turns commitment from a one-way park into a treatment with an ending.
|
||||
It only advances once the patient has no treatable condition left (see the job) and, like
|
||||
everything here, decays without sustained attention. When it reaches the "stabilised" stage the
|
||||
discharge alert fires: the patient is well enough to let go.
|
||||
-->
|
||||
<HediffDef>
|
||||
<defName>Ward_Recovery</defName>
|
||||
<label>recovering</label>
|
||||
<description>This person has worked through what they were committed for and is stabilising. Once fully recovered they can safely be discharged — held longer, they will start to resent it.</description>
|
||||
<hediffClass>HediffWithComps</hediffClass>
|
||||
<defaultLabelColor>(0.55, 0.85, 0.65)</defaultLabelColor>
|
||||
<scenarioCanAdd>false</scenarioCanAdd>
|
||||
<maxSeverity>1.0</maxSeverity>
|
||||
<isBad>false</isBad>
|
||||
<comps>
|
||||
<!-- Recovery has to be sustained too: a couple of days of no attention and it slips back. -->
|
||||
<li Class="HediffCompProperties_SeverityPerDay">
|
||||
<severityPerDay>-0.10</severityPerDay>
|
||||
</li>
|
||||
</comps>
|
||||
<stages>
|
||||
<li>
|
||||
<minSeverity>0</minSeverity>
|
||||
<label>on the mend</label>
|
||||
</li>
|
||||
<li>
|
||||
<minSeverity>0.999</minSeverity>
|
||||
<label>stabilised</label>
|
||||
</li>
|
||||
</stages>
|
||||
</HediffDef>
|
||||
|
||||
<!-- Applied as recovery progresses: the patient can feel it working. -->
|
||||
<ThoughtDef>
|
||||
<defName>Ward_Recovering</defName>
|
||||
<thoughtClass>Thought_Memory</thoughtClass>
|
||||
<durationDays>2</durationDays>
|
||||
<stackLimit>3</stackLimit>
|
||||
<stages>
|
||||
<li>
|
||||
<label>on the mend</label>
|
||||
<description>Whatever was wrong with me, it's easing. I think I might be getting better.</description>
|
||||
<baseMoodEffect>4</baseMoodEffect>
|
||||
</li>
|
||||
</stages>
|
||||
</ThoughtDef>
|
||||
|
||||
<!-- A breakthrough: the rare day the work pays off all at once. -->
|
||||
<ThoughtDef>
|
||||
<defName>Ward_Breakthrough</defName>
|
||||
<thoughtClass>Thought_Memory</thoughtClass>
|
||||
<durationDays>5</durationDays>
|
||||
<stackLimit>1</stackLimit>
|
||||
<stages>
|
||||
<li>
|
||||
<label>a breakthrough</label>
|
||||
<description>Something finally shifted. For the first time in a long while, I feel like myself.</description>
|
||||
<baseMoodEffect>10</baseMoodEffect>
|
||||
</li>
|
||||
</stages>
|
||||
</ThoughtDef>
|
||||
|
||||
</Defs>
|
||||
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Defs>
|
||||
|
||||
<!--
|
||||
The whole mod hangs off this def.
|
||||
|
||||
RimWorld.GuestStatus is a compiled 3-value enum (Guest / Prisoner / Slave). A mod cannot
|
||||
add a fourth. That is exactly why Hospitality carries ~74 Harmony patches: it wants a pawn
|
||||
who is in your colony but is not your colonist, the enum has no slot for one, so it keeps a
|
||||
shadow guest system alive by patching bed validity, allowed areas and the work JobGiver.
|
||||
|
||||
Ward does not pay that tax. A committed pawn IS a prisoner in vanilla's own sense of the
|
||||
word. Arresting your own colonist already performs the colonist to prisoner transition.
|
||||
All Ward adds is a new *mode* to hold them under, and PrisonerInteractionModeDef is a Def,
|
||||
not an enum. Prison Labor already proves this works: it ships seven of them.
|
||||
|
||||
Non-exclusive, so it stacks rather than competing for the radio button. That is what lets
|
||||
Ward coexist with Prison Labor's work modes and with Custom Prisoner Interactions instead
|
||||
of fighting them for the same slot.
|
||||
-->
|
||||
<PrisonerInteractionModeDef>
|
||||
<defName>Ward_PsychiatricCare</defName>
|
||||
<label>psychiatric care</label>
|
||||
<listOrder>150</listOrder> <!-- between ReduceResistance (100) and Release (200) -->
|
||||
<isNonExclusiveInteraction>true</isNonExclusiveInteraction>
|
||||
<mustBeAwake>false</mustBeAwake>
|
||||
<allowOnWildMan>false</allowOnWildMan>
|
||||
<allowInClassicIdeoMode>true</allowInClassicIdeoMode>
|
||||
</PrisonerInteractionModeDef>
|
||||
|
||||
</Defs>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Defs>
|
||||
|
||||
<!--
|
||||
RoomRoleDef.workerClass is a public field, so a new room role is XML plus one worker class
|
||||
and zero patches. Vanilla already ships PrisonCell, PrisonBarracks and Hospital on exactly
|
||||
this rail; the ward is the fourth sibling.
|
||||
|
||||
The score has to beat PrisonCell's, or a ward full of prisoner beds just reads as a cell.
|
||||
RoomRoleWorker_PsychWard only scores at all when a treatment station is present, so an
|
||||
ordinary prison cell is untouched and keeps its vanilla role.
|
||||
-->
|
||||
<RoomRoleDef>
|
||||
<defName>Ward_PsychWard</defName>
|
||||
<label>psychiatric ward</label>
|
||||
<workerClass>Ward.RoomRoleWorker_PsychWard</workerClass>
|
||||
<relatedStats>
|
||||
<li>Impressiveness</li>
|
||||
<li>Cleanliness</li>
|
||||
<li>Space</li>
|
||||
</relatedStats>
|
||||
</RoomRoleDef>
|
||||
|
||||
</Defs>
|
||||
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Defs>
|
||||
|
||||
<!--
|
||||
The thing that turns a room full of prisoner beds into a ward rather than a cell block.
|
||||
It is deliberately cheap: the cost of a ward is not the furniture, it is the warden hours
|
||||
and the labour you give up by not putting the patient to work.
|
||||
-->
|
||||
<ThingDef ParentName="BuildingBase">
|
||||
<defName>Ward_TreatmentStation</defName>
|
||||
<label>treatment station</label>
|
||||
<description>A desk, a chair bolted to the floor, and a locked cabinet of sedatives. A warden set to psychiatric care will use it to counsel patients held in this room.\n\nA room containing one is treated as a psychiatric ward rather than a prison cell. Whether that is a mercy depends entirely on how you run it.</description>
|
||||
<thingClass>Building</thingClass>
|
||||
<graphicData>
|
||||
<texPath>Things/Building/Ward/TreatmentStation</texPath>
|
||||
<!-- Single, not Multi: one texture, no _north/_east/_south/_west set to keep in sync. -->
|
||||
<graphicClass>Graphic_Single</graphicClass>
|
||||
<drawSize>(2,1)</drawSize>
|
||||
<damageData>
|
||||
<rect>(0,0.05,2,0.9)</rect>
|
||||
</damageData>
|
||||
</graphicData>
|
||||
<castEdgeShadows>true</castEdgeShadows>
|
||||
<altitudeLayer>Building</altitudeLayer>
|
||||
<passability>PassThroughOnly</passability>
|
||||
<pathCost>60</pathCost>
|
||||
<blockWind>false</blockWind>
|
||||
<fillPercent>0.4</fillPercent>
|
||||
<statBases>
|
||||
<MaxHitPoints>120</MaxHitPoints>
|
||||
<WorkToBuild>1600</WorkToBuild>
|
||||
<Flammability>1.0</Flammability>
|
||||
<Beauty>0</Beauty>
|
||||
</statBases>
|
||||
<size>(2,1)</size>
|
||||
<costList>
|
||||
<Steel>40</Steel>
|
||||
<MedicineIndustrial>2</MedicineIndustrial>
|
||||
</costList>
|
||||
<researchPrerequisites>
|
||||
<li>MedicineProduction</li>
|
||||
</researchPrerequisites>
|
||||
<designationCategory>Misc</designationCategory>
|
||||
<rotatable>true</rotatable>
|
||||
<selectable>true</selectable>
|
||||
<terrainAffordanceNeeded>Light</terrainAffordanceNeeded>
|
||||
<building>
|
||||
<ai_chillDestination>false</ai_chillDestination>
|
||||
</building>
|
||||
</ThingDef>
|
||||
|
||||
</Defs>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Defs>
|
||||
|
||||
<WorkGiverDef>
|
||||
<defName>Institution_Investigate</defName>
|
||||
<label>investigate crimes</label>
|
||||
<giverClass>Contraband.WorkGiver_Investigate</giverClass>
|
||||
<workType>Institution_Investigating</workType>
|
||||
<verb>investigate</verb>
|
||||
<gerund>investigating</gerund>
|
||||
<priorityInType>100</priorityInType>
|
||||
<requiredCapacities>
|
||||
<li>Manipulation</li>
|
||||
<li>Sight</li>
|
||||
</requiredCapacities>
|
||||
</WorkGiverDef>
|
||||
|
||||
</Defs>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Defs>
|
||||
|
||||
<WorkTypeDef>
|
||||
<defName>Institution_Investigating</defName>
|
||||
<labelShort>police</labelShort>
|
||||
<pawnLabel>Constable</pawnLabel>
|
||||
<gerundLabel>investigating</gerundLabel>
|
||||
<description>Working open crime scenes to gather evidence and identify the culprit.</description>
|
||||
<verb>investigate</verb>
|
||||
<naturalPriority>455</naturalPriority>
|
||||
<relevantSkills>
|
||||
<li>Intellectual</li>
|
||||
<li>Social</li>
|
||||
</relevantSkills>
|
||||
<workTags>
|
||||
<li>Intellectual</li>
|
||||
<li>Social</li>
|
||||
</workTags>
|
||||
</WorkTypeDef>
|
||||
|
||||
</Defs>
|
||||
@@ -20,15 +20,17 @@ Arrest is the hinge. Nothing hard-codes "prisoner" — every held-person system
|
||||
|---|---|---|---|
|
||||
| [Institution: Core](https://git.onetick.ninja/flan/rimworld-core) | the shared engine — **propensity** (nature × nurture), the **criminal record**, the **secured-context** predicate. Invisible on its own | — | shipping |
|
||||
| [Institution: Contraband](https://git.onetick.ninja/flan/rimworld-contraband) | the physical smuggling loop — conceal / improvise shivs / Prison-Architect tunnels / warden search / corruption | Core | shipping |
|
||||
| [Institution: Justice](https://git.onetick.ninja/flan/rimworld-justice) | corrections & policing — classification, deterrence, discipline, parole, regime (a policing/cops layer is planned here) | Core, Harmony | shipping |
|
||||
| [Institution: Justice](https://git.onetick.ninja/flan/rimworld-justice) | corrections & **policing** — classification, deterrence, discipline & reform, parole, regime, and colony crime → witnesses → a weighed arrest | Core, Harmony | shipping |
|
||||
| [Institution: Gangs](https://git.onetick.ninja/flan/rimworld-gangs) | gangs as **contraband economies** — joining by disposition, smuggling networks, rivalry, fights-as-crime | Core, Contraband, Justice | shipping |
|
||||
| [Institution: Ward](https://git.onetick.ninja/flan/rimworld-ward) | involuntary psychiatric commitment — the third pawn state, neither free nor guilty; also carries the suite's in-game test harness | — | shipping |
|
||||
| [Institution: Ward](https://git.onetick.ninja/flan/rimworld-ward) | involuntary **psychiatric care** — a prisoner committed for treatment, not punishment, on Core's shared treatment engine: counselling reduces the disorder, a recovery track leads to discharge, and neglect deteriorates. Folded into the suite as its psychiatric-care layer | Core, Harmony | shipping |
|
||||
| [Foul Play](https://git.onetick.ninja/flan/rimworld-foulplay) | throw a container of something off a body; a Noita-flasks liquid framework (vessels + substances) with the **Piss Nuke** flagship; bridges to the suite | standalone | shipping |
|
||||
|
||||
The original monolith split along its natural seams in 2026-07: the substrate became **Core**, the
|
||||
corrections/justice systems became **Justice**, and the gang layer became **Gangs**, leaving
|
||||
**Contraband** as the physical smuggling loop. Regime (prisoner recreation) folded into Justice; a
|
||||
policing/cops layer is where Justice grows next.
|
||||
**Contraband** as the physical smuggling loop. Regime (prisoner recreation) folded into Justice, which
|
||||
now also ships **policing** (colony crime → witnesses → a weighed arrest). **Ward** — once a standalone
|
||||
sister mod — is folded in as the psychiatric-care layer, consuming Core's shared treatment engine, so
|
||||
the suite is now five layers over one Core, bundled into a single install with a toggle each.
|
||||
|
||||
## Principles
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace InstitutionSuite
|
||||
{
|
||||
/// <summary>
|
||||
/// The single player-facing mod. It bundles the Institution feature layers -- Contraband,
|
||||
/// Justice, Gangs -- into one install with a checkbox per layer, and it owns NO gameplay code of
|
||||
/// Justice, Gangs, Ward -- into one install with a checkbox per layer, and it owns NO gameplay code of
|
||||
/// its own. All it does is read the player's choices and write them into Core's shared
|
||||
/// <see cref="Contraband.InstitutionSettings"/>, which the feature assemblies (shipped alongside
|
||||
/// in this same mod) read at their entry points.
|
||||
@@ -46,6 +46,10 @@ namespace InstitutionSuite
|
||||
l.CheckboxLabeled("Policing (colony crime)",
|
||||
ref settings.policing,
|
||||
"The pre-arrest layer: your own colonists commit crimes on a spectrum (rarer in small, tight colonies), witnesses and a constable's work name the culprit, and a weighed arrest can end in a cell -- or a resisted breakout. A big behavioural change; turn it off to keep the rest of Justice without colony crime.");
|
||||
l.Gap();
|
||||
l.CheckboxLabeled("Ward (psychiatric care)",
|
||||
ref settings.ward,
|
||||
"Commit a prisoner for treatment instead of punishment: sustained counselling in a proper ward reduces the disorder that broke them and builds a recovery track toward discharge. Neglect a committed patient and they deteriorate. Uses Harmony.");
|
||||
l.End();
|
||||
settings.Apply(); // live: a toggle takes effect immediately for the gated behaviour
|
||||
}
|
||||
@@ -57,6 +61,7 @@ namespace InstitutionSuite
|
||||
public bool justice = true;
|
||||
public bool gangs = true;
|
||||
public bool policing = true;
|
||||
public bool ward = true;
|
||||
|
||||
/// <summary>Mirror the choices into Core's shared flags that every feature reads.</summary>
|
||||
public void Apply()
|
||||
@@ -65,6 +70,7 @@ namespace InstitutionSuite
|
||||
Contraband.InstitutionSettings.justice = justice;
|
||||
Contraband.InstitutionSettings.gangs = gangs;
|
||||
Contraband.InstitutionSettings.policing = policing;
|
||||
Contraband.InstitutionSettings.ward = ward;
|
||||
}
|
||||
|
||||
public override void ExposeData()
|
||||
@@ -73,6 +79,7 @@ namespace InstitutionSuite
|
||||
Scribe_Values.Look(ref justice, "justice", true);
|
||||
Scribe_Values.Look(ref gangs, "gangs", true);
|
||||
Scribe_Values.Look(ref policing, "policing", true);
|
||||
Scribe_Values.Look(ref ward, "ward", true);
|
||||
base.ExposeData();
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,626 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate the Institution suite's Workshop Preview.png images. Pure stdlib -- no Pillow, no CUDA,
|
||||
no AI/diffusion, nothing ripped. Every pixel is drawn by the code below, exactly like the per-repo
|
||||
Tools/make_textures.py sprite generators, so the copyright story stays clean.
|
||||
|
||||
Tools/make_preview.py # run from anywhere; writes into every sibling repo
|
||||
|
||||
Reuses the same from-scratch machinery the texture generators use: a hand-rolled RGBA PNG writer
|
||||
(zlib + struct, see png()) and analytic signed-distance-field shapes supersampled for clean edges
|
||||
(see _raster). Nothing beyond the standard library, so it runs on a box with no image tools at all.
|
||||
|
||||
Writes About/Preview.png (640x360, a safe 16:9 Workshop size) into each of:
|
||||
|
||||
rimworld-institution flagship: a barred cell window over the nature->nurture crime spectrum
|
||||
rimworld-core the invisible engine: a nature x nurture node feeding a 0..1 spectrum bar
|
||||
rimworld-contraband a filed shiv against a brick wall breached by an escape tunnel
|
||||
rimworld-justice a level balance -- the corrections layer weighing what a pawn has done
|
||||
rimworld-gangs a network of pawn tokens, two rival clusters bridged by a contraband line
|
||||
|
||||
They are one SET: shared muted institutional green-grey / tin palette, the same embossed-tin border,
|
||||
the same faint grain, and a shared propensity spectrum ribbon (green -> amber -> red) along the
|
||||
bottom of every image -- the suite's signature, the spectrum every layer reads from.
|
||||
"""
|
||||
|
||||
import os
|
||||
import zlib
|
||||
import struct
|
||||
import math
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # rimworld-institution
|
||||
SUITE = os.path.dirname(REPO) # holds all the sibling repos
|
||||
|
||||
W, H = 640, 360
|
||||
AS = W / H # aspect; everything is normalised by HEIGHT so circles stay round
|
||||
SS = int(os.environ.get("SS", "3"))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PNG encoder + supersampled rasteriser (pure stdlib) -- same as make_textures.py
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def png(path, w, h, pixel_fn):
|
||||
"""Write an 8-bit RGBA PNG from scratch. pixel_fn(x, y) -> (r, g, b, a)."""
|
||||
raw = bytearray()
|
||||
for y in range(h):
|
||||
raw.append(0) # filter byte: 0 = None, once per scanline
|
||||
row = bytearray()
|
||||
for x in range(w):
|
||||
r, g, b, a = pixel_fn(x, y)
|
||||
row += bytes((r & 255, g & 255, b & 255, a & 255))
|
||||
raw += row
|
||||
|
||||
def chunk(typ, data):
|
||||
return (struct.pack(">I", len(data)) + typ + data
|
||||
+ struct.pack(">I", zlib.crc32(typ + data) & 0xffffffff))
|
||||
|
||||
ihdr = struct.pack(">IIBBBBB", w, h, 8, 6, 0, 0, 0) # 8-bit depth, colour type 6 = RGBA
|
||||
idat = zlib.compress(bytes(raw), 9)
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"\x89PNG\r\n\x1a\n")
|
||||
f.write(chunk(b"IHDR", ihdr))
|
||||
f.write(chunk(b"IDAT", idat))
|
||||
f.write(chunk(b"IEND", b""))
|
||||
|
||||
|
||||
def _raster(w, h, sample, ss=SS):
|
||||
"""Wrap an analytic sample(fx, fy) -> (r, g, b, a) into a supersampled pixel_fn."""
|
||||
inv = 1.0 / ss
|
||||
n = ss * ss
|
||||
|
||||
def pixel(x, y):
|
||||
pr = pg = pb = pa = 0.0
|
||||
for j in range(ss):
|
||||
fy = y + (j + 0.5) * inv
|
||||
for i in range(ss):
|
||||
r, g, b, a = sample(x + (i + 0.5) * inv, fy)
|
||||
if a:
|
||||
af = a * (1.0 / 255.0)
|
||||
pr += r * af
|
||||
pg += g * af
|
||||
pb += b * af
|
||||
pa += af
|
||||
if pa <= 1e-6:
|
||||
return (0, 0, 0, 0)
|
||||
return (_cb(pr / pa), _cb(pg / pa), _cb(pb / pa), _cb(pa / n * 255.0))
|
||||
|
||||
return pixel
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# colour + geometry helpers (normalised coords; u in 0..AS, v in 0..1)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _cb(x):
|
||||
x = int(x)
|
||||
return 0 if x < 0 else (255 if x > 255 else x)
|
||||
|
||||
|
||||
def _clamp01(x):
|
||||
return 0.0 if x < 0.0 else (1.0 if x > 1.0 else x)
|
||||
|
||||
|
||||
def _shade(c, f):
|
||||
return (_cb(c[0] * f), _cb(c[1] * f), _cb(c[2] * f))
|
||||
|
||||
|
||||
def _mix(a, b, t):
|
||||
return (_cb(a[0] + (b[0] - a[0]) * t),
|
||||
_cb(a[1] + (b[1] - a[1]) * t),
|
||||
_cb(a[2] + (b[2] - a[2]) * t))
|
||||
|
||||
|
||||
def _grain(ix, iy):
|
||||
"""Deterministic faint speckle so fills aren't dead-flat."""
|
||||
n = (ix * 374761393 + iy * 668265263) & 0xffffffff
|
||||
n ^= (n >> 13)
|
||||
n = (n * 1274126177) & 0xffffffff
|
||||
return ((n >> 9) & 7) - 3 # -3..+4
|
||||
|
||||
|
||||
def _grainy(c, u, v):
|
||||
g = _grain(int(u * 300), int(v * 300))
|
||||
return (_cb(c[0] + g), _cb(c[1] + g), _cb(c[2] + g))
|
||||
|
||||
|
||||
def _seg(px, py, ax, ay, bx, by):
|
||||
"""Distance from P to segment AB, and the clamped projection param t in [0, 1]."""
|
||||
dx, dy = bx - ax, by - ay
|
||||
l2 = dx * dx + dy * dy
|
||||
if l2 < 1e-12:
|
||||
return math.hypot(px - ax, py - ay), 0.0
|
||||
t = ((px - ax) * dx + (py - ay) * dy) / l2
|
||||
t = 0.0 if t < 0 else (1.0 if t > 1 else t)
|
||||
cx, cy = ax + dx * t, ay + dy * t
|
||||
return math.hypot(px - cx, py - cy), t
|
||||
|
||||
|
||||
def _ellipse(u, v, cx, cy, rx, ry):
|
||||
"""Approximate signed distance to an ellipse. Negative inside."""
|
||||
k = math.hypot((u - cx) / rx, (v - cy) / ry)
|
||||
return (k - 1.0) * min(rx, ry)
|
||||
|
||||
|
||||
def _rrect(u, v, cx, cy, hw, hh, r):
|
||||
"""Signed distance to a rounded rectangle. Negative inside."""
|
||||
qx = abs(u - cx) - (hw - r)
|
||||
qy = abs(v - cy) - (hh - r)
|
||||
ox = qx if qx > 0 else 0.0
|
||||
oy = qy if qy > 0 else 0.0
|
||||
return math.hypot(ox, oy) + min(max(qx, qy), 0.0) - r
|
||||
|
||||
|
||||
def _convex(u, v, pts):
|
||||
"""Signed distance to a convex polygon (negative inside). Winding-agnostic."""
|
||||
n = len(pts)
|
||||
cxp = sum(p[0] for p in pts) / n
|
||||
cyp = sum(p[1] for p in pts) / n
|
||||
dmax = -1e9
|
||||
for i in range(n):
|
||||
ax, ay = pts[i]
|
||||
bx, by = pts[(i + 1) % n]
|
||||
ex, ey = bx - ax, by - ay
|
||||
L = math.hypot(ex, ey) or 1e-9
|
||||
nx, ny = ey / L, -ex / L # a normal to the edge
|
||||
dp = (u - ax) * nx + (v - ay) * ny
|
||||
dc = (cxp - ax) * nx + (cyp - ay) * ny # which side the interior is on
|
||||
if dc > 0:
|
||||
dp = -dp
|
||||
if dp > dmax:
|
||||
dmax = dp
|
||||
return dmax
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# shared palette -- muted institutional green-grey / tin, dark outline
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
OUTLINE = (22, 26, 20)
|
||||
BG_TOP = (56, 64, 55)
|
||||
BG_BOT = (31, 37, 31)
|
||||
|
||||
TIN = (138, 142, 126)
|
||||
TIN_HI = (178, 182, 162)
|
||||
TIN_DK = (92, 96, 82)
|
||||
TIN_DDK = (62, 66, 56)
|
||||
|
||||
STEEL = (180, 184, 178)
|
||||
STEEL_HI = (210, 214, 208)
|
||||
STEEL_DK = (120, 124, 118)
|
||||
|
||||
BRICK = (94, 98, 88)
|
||||
BRICK2 = (110, 114, 102)
|
||||
MORTAR = (54, 58, 50)
|
||||
EARTH = (46, 39, 29)
|
||||
EARTH_DK = (24, 20, 14)
|
||||
|
||||
# the propensity spectrum: calm green (low) -> amber -> dangerous red (high)
|
||||
SPEC_LOW = (104, 150, 100)
|
||||
SPEC_MID = (198, 160, 74)
|
||||
SPEC_HI = (176, 68, 54)
|
||||
|
||||
|
||||
def spec(t):
|
||||
"""Colour along the nature->nurture propensity spectrum, t in 0..1."""
|
||||
t = _clamp01(t)
|
||||
if t < 0.5:
|
||||
return _mix(SPEC_LOW, SPEC_MID, t * 2.0)
|
||||
return _mix(SPEC_MID, SPEC_HI, (t - 0.5) * 2.0)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# shared frame + background + spectrum ribbon (identical on every preview)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
CX, CY = AS / 2.0, 0.5
|
||||
FR_M = 0.050 # outer margin
|
||||
FR_R = 0.038 # corner radius
|
||||
FR_W = 0.026 # frame band width
|
||||
FHW, FHH = CX - FR_M, CY - FR_M
|
||||
|
||||
IL, IR = FR_M + FR_W + 0.004, AS - FR_M - FR_W - 0.004 # inner content bounds
|
||||
IT, IB = FR_M + FR_W + 0.004, 1.0 - FR_M - FR_W - 0.004
|
||||
|
||||
RIB_B = IB - 0.006
|
||||
RIB_T = RIB_B - 0.036
|
||||
RIB_L, RIB_R = IL + 0.004, IR - 0.004
|
||||
|
||||
|
||||
def _bg(u, v):
|
||||
c = _mix(BG_TOP, BG_BOT, _clamp01(v))
|
||||
dx, dy = (u - CX) / CX, (v - CY) / CY
|
||||
c = _shade(c, 1.0 - 0.24 * _clamp01(dx * dx + dy * dy))
|
||||
return _grainy(c, u, v)
|
||||
|
||||
|
||||
def _frame(u, v, d):
|
||||
"""Embossed-tin border colour for a point whose rounded-rect distance is d."""
|
||||
if d > -0.004:
|
||||
return OUTLINE # crisp outer edge line
|
||||
p = (-0.004 - d) / (FR_W - 0.004) # 0 outer .. 1 inner
|
||||
if p > 0.90:
|
||||
return OUTLINE # crisp inner edge line
|
||||
nx, ny = (u - CX), (v - CY)
|
||||
lit = -(nx * 0.55 + ny * 0.85) # light from the top-left
|
||||
if lit > 0:
|
||||
c = _mix(TIN, TIN_HI, _clamp01(lit * 2.4))
|
||||
else:
|
||||
c = _mix(TIN, TIN_DDK, _clamp01(-lit * 2.4))
|
||||
return _grainy(c, u, v)
|
||||
|
||||
|
||||
def _ribbon(u, v):
|
||||
t = (u - RIB_L) / (RIB_R - RIB_L)
|
||||
c = _shade(spec(t), 0.86)
|
||||
if v - RIB_T < 0.004:
|
||||
return OUTLINE # dark lintel over the ribbon
|
||||
if RIB_B - v < 0.004:
|
||||
c = _shade(c, 0.62)
|
||||
if (t * 8.0) % 1.0 < 0.035: # eighth-tick divisions
|
||||
c = _shade(c, 0.68)
|
||||
return _grainy(c, u, v)
|
||||
|
||||
|
||||
def preview(motif):
|
||||
"""Compose background + shared ribbon + motif + border into one supersampled pixel_fn."""
|
||||
def sample(fx, fy):
|
||||
u, v = fx / H, fy / H
|
||||
d = _rrect(u, v, CX, CY, FHW, FHH, FR_R)
|
||||
if d > 0.004:
|
||||
return _grainy(_shade(BG_BOT, 0.72), u, v) + (255,) # rounded corner falloff
|
||||
if d > -FR_W:
|
||||
return _frame(u, v, d) + (255,)
|
||||
if RIB_T <= v <= RIB_B and RIB_L <= u <= RIB_R:
|
||||
return _ribbon(u, v) + (255,)
|
||||
m = motif(u, v)
|
||||
if m is not None:
|
||||
return m if len(m) == 4 else (m + (255,))
|
||||
return _bg(u, v) + (255,)
|
||||
|
||||
return _raster(W, H, sample)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# INSTITUTION -- the flagship. A barred cell window; behind the bars the whole
|
||||
# nature->nurture crime spectrum glows. Prison + a spectrum of crime, in one image.
|
||||
# ===========================================================================
|
||||
|
||||
WIN_CX, WIN_CY = CX, 0.45
|
||||
WIN_HW, WIN_HH = 0.62, 0.30
|
||||
NBARS = 6
|
||||
|
||||
|
||||
def m_institution(u, v):
|
||||
d = _rrect(u, v, WIN_CX, WIN_CY, WIN_HW, WIN_HH, 0.028)
|
||||
if d > 0.030:
|
||||
return None
|
||||
if d > -0.006:
|
||||
return OUTLINE # window outer edge
|
||||
if d > -0.036: # tin window frame, bevelled
|
||||
nx, ny = (u - WIN_CX), (v - WIN_CY)
|
||||
lit = -(nx * 0.5 + ny * 0.9)
|
||||
c = _mix(TIN, TIN_HI, _clamp01(lit * 3.0)) if lit > 0 else _mix(TIN, TIN_DK, _clamp01(-lit * 3.0))
|
||||
return _grainy(c, u, v)
|
||||
|
||||
L, R = WIN_CX - WIN_HW, WIN_CX + WIN_HW
|
||||
t = (u - L) / (R - L)
|
||||
|
||||
# bars: NBARS vertical + two horizontal, in front of the spectrum
|
||||
s = (2 * WIN_HW) / NBARS
|
||||
i = int((u - L) / s)
|
||||
xc = L + s * (i + 0.5)
|
||||
dvb = abs(u - xc)
|
||||
bw = 0.016
|
||||
hb = min(abs(v - (WIN_CY - 0.175)), abs(v - (WIN_CY + 0.175)))
|
||||
hh = 0.014
|
||||
|
||||
is_v = dvb < bw
|
||||
is_h = hb < hh
|
||||
if is_v or is_h:
|
||||
vertical = is_v and (not is_h or (dvb / bw) <= (hb / hh))
|
||||
if vertical:
|
||||
if dvb > bw - 0.0035:
|
||||
return OUTLINE
|
||||
p = (u - (xc - bw)) / (2 * bw) # 0 left .. 1 right
|
||||
c = _mix(STEEL_DK, STEEL, 1.0 - p * 0.85)
|
||||
c = _mix(c, STEEL_HI, _clamp01(0.5 - p) * 0.7)
|
||||
return _grainy(c, u, v)
|
||||
else:
|
||||
if hb > hh - 0.0035:
|
||||
return OUTLINE
|
||||
near = WIN_CY - 0.175 if abs(v - (WIN_CY - 0.175)) < abs(v - (WIN_CY + 0.175)) else WIN_CY + 0.175
|
||||
p = (v - (near - hh)) / (2 * hh)
|
||||
c = _mix(STEEL_DK, STEEL, 1.0 - p * 0.85)
|
||||
c = _mix(c, STEEL_HI, _clamp01(0.5 - p) * 0.7)
|
||||
return _grainy(c, u, v)
|
||||
|
||||
# interior: the spectrum, brighter through the middle so it reads as light behind bars
|
||||
base = spec(t)
|
||||
gy = 1.0 - _clamp01(abs(v - WIN_CY) / WIN_HH)
|
||||
base = _shade(base, 0.58 + 0.55 * gy)
|
||||
if d > -0.020: # inner shadow just inside the frame
|
||||
base = _shade(base, 0.80)
|
||||
return _grainy(base, u, v)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# CORE -- the invisible foundation. Two faint axes, NATURE (vertical) x NURTURE
|
||||
# (horizontal), meet at the engine node, which drives the 0..1 spectrum bar below.
|
||||
# Deliberately subdued: on its own it changes nothing you can see.
|
||||
# ===========================================================================
|
||||
|
||||
NODE_X, NODE_Y = CX, 0.38
|
||||
BAR_Y, BAR_HW, BAR_HH = 0.60, 0.62, 0.052
|
||||
|
||||
|
||||
def m_core(u, v):
|
||||
# 0..1 spectrum bar (the hero, low and central)
|
||||
db = _rrect(u, v, CX, BAR_Y, BAR_HW, BAR_HH, 0.024)
|
||||
if db <= 0.005:
|
||||
if db > -0.005:
|
||||
return OUTLINE
|
||||
t = (u - (CX - BAR_HW)) / (2 * BAR_HW)
|
||||
c = spec(t)
|
||||
if (t * 10.0) % 1.0 < 0.028 and abs(v - BAR_Y) > BAR_HH - 0.016:
|
||||
c = _shade(c, 0.6) # scale ticks along the rim
|
||||
if abs(v - (BAR_Y - BAR_HH * 0.45)) < 0.006:
|
||||
c = _mix(c, (255, 255, 255), 0.12) # a faint sheen line
|
||||
return _grainy(c, u, v)
|
||||
|
||||
# engine node at the crossing
|
||||
dn = math.hypot(u - NODE_X, v - NODE_Y)
|
||||
if dn < 0.058:
|
||||
if dn > 0.050:
|
||||
return OUTLINE
|
||||
c = _mix(TIN, STEEL_HI, _clamp01(1.0 - dn / 0.050))
|
||||
return _grainy(c, u, v)
|
||||
|
||||
# faint NATURE x NURTURE axes behind everything
|
||||
if abs(u - NODE_X) < 0.0035 and 0.14 < v < 0.62:
|
||||
return _grainy(TIN_DK, u, v)
|
||||
if abs(v - NODE_Y) < 0.0035 and (CX - 0.54) < u < (CX + 0.54):
|
||||
return _grainy(TIN_DK, u, v)
|
||||
# end caps on the axes -> small pips (nature high/low, nurture low/high)
|
||||
for (px, py) in ((NODE_X, 0.15), (NODE_X, 0.61), (CX - 0.54, NODE_Y), (CX + 0.54, NODE_Y)):
|
||||
if math.hypot(u - px, v - py) < 0.016:
|
||||
return _grainy(TIN, u, v)
|
||||
return None
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# CONTRABAND -- a filed shiv lying against a brick wall that an escape tunnel has
|
||||
# breached. The physical smuggling loop: a whittled weapon and a hole under the wall.
|
||||
# ===========================================================================
|
||||
|
||||
BX, BY, BRX, BRY = CX + 0.44, 0.54, 0.28, 0.25 # the tunnel breach
|
||||
|
||||
|
||||
def _brick(u, v):
|
||||
bh, bw = 0.078, 0.180
|
||||
row = int((v - IT) / bh)
|
||||
x = u + (bw * 0.5 if row % 2 else 0.0)
|
||||
col = int(x / bw)
|
||||
ly = (v - IT) - row * bh
|
||||
lx = x - col * bw
|
||||
if ly < 0.012 or lx < 0.012:
|
||||
return MORTAR
|
||||
h = (row * 928371 + col * 123457) & 255
|
||||
return _shade(_mix(BRICK, BRICK2, h / 255.0), 0.94 + 0.12 * (((h >> 3) & 3) / 3.0))
|
||||
|
||||
|
||||
def _breach(u, v):
|
||||
ang = math.atan2(v - BY, u - BX)
|
||||
wob = 0.028 * math.sin(ang * 7.0 + 0.7) + 0.020 * math.sin(ang * 3.0)
|
||||
k = math.hypot((u - BX) / (BRX + wob), (v - BY) / (BRY + wob))
|
||||
return (k - 1.0) * min(BRX, BRY)
|
||||
|
||||
|
||||
def _shiv(u, v):
|
||||
Bx, By = 0.30, 0.80 # cloth grip low-left
|
||||
Jx, Jy = 0.44, 0.60
|
||||
Tx, Ty = 0.78, 0.24 # filed point high-right
|
||||
db, tb = _seg(u, v, Jx, Jy, Tx, Ty)
|
||||
half = 0.030 * (1.0 - tb) + 0.004 * tb
|
||||
d_blade = db - half
|
||||
dg, tg = _seg(u, v, Bx, By, Jx, Jy)
|
||||
d_grip = dg - 0.040
|
||||
d = min(d_blade, d_grip)
|
||||
if d > 0:
|
||||
return None
|
||||
if d > -0.006:
|
||||
return OUTLINE
|
||||
if d_grip <= d_blade:
|
||||
wind = (110, 110, 104) if (tg * 4.3) % 1.0 < 0.34 else (150, 150, 142)
|
||||
return _grainy(_shade(wind, 0.95 + 0.10 * tg), u, v)
|
||||
bevel = 1.0 - _clamp01(db / max(1e-6, half))
|
||||
c = _mix(STEEL_DK, STEEL, _clamp01(bevel * 1.6))
|
||||
c = _mix(c, STEEL_HI, _clamp01(bevel - 0.55) * 1.4)
|
||||
return _grainy(c, u, v)
|
||||
|
||||
|
||||
def m_contraband(u, v):
|
||||
s = _shiv(u, v) # the shiv sits on top of the wall
|
||||
if s is not None:
|
||||
return s
|
||||
db = _breach(u, v)
|
||||
if db < 0: # tunnel mouth: dark earth, deeper = darker
|
||||
f = _clamp01(-db / BRY)
|
||||
return _grainy(_mix(EARTH, EARTH_DK, f), u, v)
|
||||
if db < 0.020: # broken, crumbling brick edge
|
||||
return _grainy(_shade(MORTAR, 0.7), u, v)
|
||||
return _grainy(_brick(u, v), u, v)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# JUSTICE -- a level balance. Corrections weighing what a pawn has actually done:
|
||||
# classification, deterrence, discipline. A gavel-less balance, held even.
|
||||
# ===========================================================================
|
||||
|
||||
J_BEAM_Y = 0.28
|
||||
J_BEAM_HW = 0.40
|
||||
J_BASE_Y = 0.74
|
||||
J_PAN_Y = 0.54
|
||||
|
||||
|
||||
def _pan(u, v, px):
|
||||
d = _ellipse(u, v, px, J_PAN_Y, 0.15, 0.058)
|
||||
if v < J_PAN_Y: # keep the lower half -> a shallow bowl
|
||||
d = max(d, (J_PAN_Y - v))
|
||||
return d
|
||||
|
||||
|
||||
def m_justice(u, v):
|
||||
OW = 0.006
|
||||
d_post = _rrect(u, v, CX, (J_BASE_Y + J_BEAM_Y) / 2.0, 0.018, (J_BASE_Y - J_BEAM_Y) / 2.0, 0.006)
|
||||
d_base = min(_rrect(u, v, CX, J_BASE_Y, 0.150, 0.026, 0.012),
|
||||
_convex(u, v, [(CX - 0.055, J_BASE_Y - 0.055), (CX + 0.055, J_BASE_Y - 0.055),
|
||||
(CX + 0.100, J_BASE_Y - 0.020), (CX - 0.100, J_BASE_Y - 0.020)]))
|
||||
d_beam = _rrect(u, v, CX, J_BEAM_Y, J_BEAM_HW, 0.015, 0.008)
|
||||
d_fulc = _convex(u, v, [(CX, J_BEAM_Y - 0.055), (CX + 0.052, J_BEAM_Y),
|
||||
(CX - 0.052, J_BEAM_Y)])
|
||||
d_panL = _pan(u, v, CX - J_BEAM_HW)
|
||||
d_panR = _pan(u, v, CX + J_BEAM_HW)
|
||||
|
||||
d_solid = min(d_post, d_base, d_beam, d_fulc, d_panL, d_panR)
|
||||
|
||||
# hangers: a thin line from each beam end down to its pan
|
||||
dcl, _ = _seg(u, v, CX - J_BEAM_HW, J_BEAM_Y, CX - J_BEAM_HW, J_PAN_Y - 0.05)
|
||||
dcr, _ = _seg(u, v, CX + J_BEAM_HW, J_BEAM_Y, CX + J_BEAM_HW, J_PAN_Y - 0.05)
|
||||
d_chain = min(dcl, dcr) - 0.004
|
||||
|
||||
if d_chain <= 0 and d_chain < d_solid:
|
||||
return _grainy(TIN_DK, u, v) if d_chain > -0.004 else _grainy(TIN, u, v)
|
||||
|
||||
if d_solid > 0:
|
||||
return None
|
||||
if d_solid > -OW:
|
||||
return OUTLINE
|
||||
# tin, lit from the top
|
||||
lit = 1.0 - _clamp01((v - J_BEAM_Y) / (J_BASE_Y - J_BEAM_Y))
|
||||
c = _mix(TIN_DK, TIN, 0.55 + 0.45 * lit)
|
||||
if d_solid <= d_panL or d_solid <= d_panR or d_solid <= d_beam:
|
||||
c = _mix(c, TIN_HI, 0.25 * lit)
|
||||
return _grainy(c, u, v)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# GANGS -- a contraband network. Pawn tokens band into two rival clusters (amber
|
||||
# vs red) that a bright contraband line bridges through a broker in the middle.
|
||||
# ===========================================================================
|
||||
|
||||
GANG_A, GANG_A_HI = (190, 150, 72), (216, 180, 98)
|
||||
GANG_B, GANG_B_HI = (176, 70, 56), (208, 100, 84)
|
||||
CONDUIT = (200, 202, 182)
|
||||
|
||||
NODES = [
|
||||
(CX - 0.50, 0.34, 'A'), (CX - 0.62, 0.50, 'A'), (CX - 0.52, 0.66, 'A'), (CX - 0.36, 0.48, 'A'),
|
||||
(CX, 0.44, 'X'),
|
||||
(CX + 0.36, 0.36, 'B'), (CX + 0.54, 0.50, 'B'), (CX + 0.42, 0.66, 'B'), (CX + 0.24, 0.52, 'B'),
|
||||
]
|
||||
EDGES = [
|
||||
(0, 1, 'A'), (1, 2, 'A'), (0, 3, 'A'), (3, 2, 'A'), (1, 3, 'A'),
|
||||
(5, 6, 'B'), (6, 7, 'B'), (5, 8, 'B'), (8, 7, 'B'), (6, 8, 'B'),
|
||||
(3, 4, 'X'), (4, 8, 'X'),
|
||||
]
|
||||
|
||||
|
||||
def _token(u, v, nx, ny, base, hi):
|
||||
d_head = math.hypot(u - nx, v - (ny - 0.024)) - 0.020
|
||||
d_body = _rrect(u, v, nx, ny + 0.014, 0.032, 0.024, 0.012)
|
||||
d = min(d_head, d_body)
|
||||
if d > 0:
|
||||
return None
|
||||
if d > -0.005:
|
||||
return OUTLINE
|
||||
return _grainy(hi if d_head < d_body else base, u, v)
|
||||
|
||||
|
||||
def m_gangs(u, v):
|
||||
for (nx, ny, g) in NODES: # tokens sit on top of the wires
|
||||
if abs(u - nx) < 0.06 and abs(v - ny) < 0.06:
|
||||
if g == 'A':
|
||||
r = _token(u, v, nx, ny, GANG_A, GANG_A_HI)
|
||||
elif g == 'B':
|
||||
r = _token(u, v, nx, ny, GANG_B, GANG_B_HI)
|
||||
else:
|
||||
r = _token(u, v, nx, ny, TIN, TIN_HI)
|
||||
if r is not None:
|
||||
return r
|
||||
|
||||
best, kind, bt = 1e9, None, 0.0
|
||||
for (i, j, k) in EDGES:
|
||||
ax, ay = NODES[i][0], NODES[i][1]
|
||||
bx, by = NODES[j][0], NODES[j][1]
|
||||
dd, tt = _seg(u, v, ax, ay, bx, by)
|
||||
if dd < best:
|
||||
best, kind, bt = dd, k, tt
|
||||
hw = 0.005
|
||||
if best < hw:
|
||||
if kind == 'X': # the contraband conduit: a bright dashed line
|
||||
return _grainy(CONDUIT, u, v) if int(bt * 24) % 2 == 0 else None
|
||||
col = _shade(GANG_A, 0.62) if kind == 'A' else _shade(GANG_B, 0.62)
|
||||
if best > hw - 0.0018:
|
||||
return _shade(col, 0.7)
|
||||
return _grainy(col, u, v)
|
||||
return None
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# verify: decode a PNG we wrote and confirm it is non-trivial (varied pixels)
|
||||
# ===========================================================================
|
||||
|
||||
def verify(path):
|
||||
data = open(path, "rb").read()
|
||||
assert data[:8] == b"\x89PNG\r\n\x1a\n", "not a PNG"
|
||||
i, idat, w, h = 8, b"", 0, 0
|
||||
while i < len(data):
|
||||
ln = struct.unpack(">I", data[i:i + 4])[0]
|
||||
typ = data[i + 4:i + 8]
|
||||
body = data[i + 8:i + 8 + ln]
|
||||
if typ == b"IHDR":
|
||||
w, h = struct.unpack(">II", body[:8])
|
||||
elif typ == b"IDAT":
|
||||
idat += body
|
||||
i += 12 + ln
|
||||
raw = zlib.decompress(idat) # rows: filter byte 0 + w*4 RGBA bytes
|
||||
stride = w * 4 + 1
|
||||
seen = set()
|
||||
lo = [255, 255, 255]
|
||||
hi = [0, 0, 0]
|
||||
for y in range(0, h, 7): # sparse sample is plenty to prove variety
|
||||
base = y * stride + 1
|
||||
for x in range(0, w, 7):
|
||||
o = base + x * 4
|
||||
px = (raw[o], raw[o + 1], raw[o + 2])
|
||||
seen.add(px)
|
||||
for c in range(3):
|
||||
lo[c] = min(lo[c], px[c])
|
||||
hi[c] = max(hi[c], px[c])
|
||||
spread = max(hi[c] - lo[c] for c in range(3))
|
||||
return {"bytes": len(data), "w": w, "h": h, "distinct": len(seen), "spread": spread}
|
||||
|
||||
|
||||
TARGETS = [
|
||||
("institution", m_institution, "barred cell window over the nature->nurture crime spectrum"),
|
||||
("core", m_core, "nature x nurture engine node driving a 0..1 spectrum bar"),
|
||||
("contraband", m_contraband, "filed shiv against a brick wall breached by an escape tunnel"),
|
||||
("justice", m_justice, "a level balance weighing what a pawn has done"),
|
||||
("gangs", m_gangs, "two rival pawn clusters bridged by a contraband line"),
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
for name, motif, desc in TARGETS:
|
||||
path = os.path.join(SUITE, "rimworld-" + name, "About", "Preview.png")
|
||||
png(path, W, H, preview(motif))
|
||||
info = verify(path)
|
||||
ok = "OK" if info["distinct"] > 200 and info["spread"] > 80 else "?? CHECK"
|
||||
print(f"{ok} {os.path.relpath(path, SUITE)} "
|
||||
f"{info['w']}x{info['h']} {info['bytes']:,}B "
|
||||
f"distinct~{info['distinct']} spread={info['spread']} -- {desc}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+14
-1
@@ -2,7 +2,7 @@
|
||||
# Assemble the shippable, drop-in "Institution" mod into this repo.
|
||||
#
|
||||
# The Institution suite's code lives in SEPARATE sibling repos (rimworld-core, -contraband, -justice,
|
||||
# -gangs) as independent projects/DLLs -- that is what keeps it modular and splittable. For DISTRIBUTION
|
||||
# -gangs, -ward) as independent projects/DLLs -- that is what keeps it modular and splittable. For DISTRIBUTION
|
||||
# they are vendored into ONE mod folder (this repo) with one About.xml and a settings shim that toggles
|
||||
# each layer. This script rebuilds the sibling DLLs clean (no self-test) and copies them + Contraband's
|
||||
# Defs in here, so the repo is a complete installable mod.
|
||||
@@ -19,6 +19,7 @@ CORE="$SIB/rimworld-core"
|
||||
CB="$SIB/rimworld-contraband"
|
||||
JUST="$SIB/rimworld-justice"
|
||||
GANGS="$SIB/rimworld-gangs"
|
||||
WARD="$SIB/rimworld-ward"
|
||||
|
||||
command -v dotnet >/dev/null || { echo "dotnet not on PATH" >&2; exit 1; }
|
||||
|
||||
@@ -27,6 +28,7 @@ dotnet build "$CORE/Source/Core/Core.csproj" -c Release -v q --n
|
||||
dotnet build "$JUST/Source/Justice/Justice.csproj" -c Release -v q --nologo
|
||||
dotnet build "$CB/Source/Contraband/Contraband.csproj" -c Release -v q --nologo
|
||||
dotnet build "$GANGS/Source/Gangs/Gangs.csproj" -c Release -v q --nologo
|
||||
dotnet build "$WARD/Source/Ward/Ward.csproj" -c Release -v q --nologo
|
||||
|
||||
echo "==> vendoring DLLs into Assemblies/"
|
||||
mkdir -p "$HERE/Assemblies"
|
||||
@@ -36,6 +38,7 @@ cp "$CORE/Assemblies/InstitutionCore.dll" "$HERE/Assemblies/"
|
||||
cp "$CB/Assemblies/Contraband.dll" "$HERE/Assemblies/"
|
||||
cp "$JUST/Assemblies/InstitutionJustice.dll" "$HERE/Assemblies/"
|
||||
cp "$GANGS/Assemblies/InstitutionGangs.dll" "$HERE/Assemblies/"
|
||||
cp "$WARD/Assemblies/Ward.dll" "$HERE/Assemblies/"
|
||||
|
||||
echo "==> vendoring the feature content"
|
||||
for d in Defs Patches Languages Textures; do
|
||||
@@ -47,6 +50,16 @@ if [[ -e "$JUST/Defs" ]]; then
|
||||
mkdir -p "$HERE/Defs"
|
||||
cp -r "$JUST/Defs/." "$HERE/Defs/"
|
||||
fi
|
||||
# Ward ships defs (interaction mode, hediffs, treatment station, room role) and its own textures.
|
||||
# All its files are *_Ward.xml or under a Ward/ texture subdir, so nothing collides with the others.
|
||||
if [[ -e "$WARD/Defs" ]]; then
|
||||
mkdir -p "$HERE/Defs"
|
||||
cp -r "$WARD/Defs/." "$HERE/Defs/"
|
||||
fi
|
||||
if [[ -e "$WARD/Textures" ]]; then
|
||||
mkdir -p "$HERE/Textures"
|
||||
cp -r "$WARD/Textures/." "$HERE/Textures/"
|
||||
fi
|
||||
|
||||
echo "==> done. Assemblies:"
|
||||
ls -1 "$HERE/Assemblies/" | sed 's/^/ /'
|
||||
|
||||
Reference in New Issue
Block a user