diff --git a/CHANGELOG.md b/CHANGELOG.md index a7db4fae2..052db17c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## Unreleased + +### Added + +### Changed +- Fixed a rather pesky and persistent bug where changing racial or religious weights would not be reflected in the town. + ## 2.8.14 ### Added diff --git a/lib/faction/createRivals.ts b/lib/faction/createRivals.ts index 83e25c17e..23f3b9e2b 100644 --- a/lib/faction/createRivals.ts +++ b/lib/faction/createRivals.ts @@ -1,6 +1,6 @@ import { dice } from '../src/dice' -import { repeat, sumWeights } from '../src/utils' -import { validateWeight, weightRandom } from '../src/weightRandom' +import { repeat } from '../src/utils' +import { sumWeights, validateWeight, weightRandom } from '../src/weightRandom' import { WeightRecord } from '../types' import { factionData } from './factionData' import { Faction } from './_common' diff --git a/lib/faction/setFactionJoinStats.ts b/lib/faction/setFactionJoinStats.ts index d3bda418b..4e072e9f8 100644 --- a/lib/faction/setFactionJoinStats.ts +++ b/lib/faction/setFactionJoinStats.ts @@ -1,8 +1,8 @@ -import { assign, sumWeights } from '../src/utils' +import { assign } from '../src/utils' import { random } from '../src/random' import { Faction } from './_common' import { factionData } from './factionData' -import { weightRandom } from '../src/weightRandom' +import { sumWeights, weightRandom } from '../src/weightRandom' export function setFactionJoinStats (faction: Faction): void { console.log('determining joining stats...') diff --git a/lib/faction/setFactionResources.ts b/lib/faction/setFactionResources.ts index 537e5e487..152ee67c1 100644 --- a/lib/faction/setFactionResources.ts +++ b/lib/faction/setFactionResources.ts @@ -2,8 +2,8 @@ import { defineRollDataGetter } from '../src/defineRollDataGetter' import { dice, fm } from '../src/dice' import { random } from '../src/random' import { ThresholdTable } from '../src/rollFromTable' -import { repeat, sumWeights, keys, clamp } from '../src/utils' -import { weightRandom } from '../src/weightRandom' +import { repeat, keys, clamp } from '../src/utils' +import { weightRandom, sumWeights } from '../src/weightRandom' import { WeightRecord } from '../types' import { factionData, FactionResource } from './factionData' import { Faction } from './_common' diff --git a/lib/index.ts b/lib/index.ts index c60089e6a..139b8c2d3 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -87,6 +87,7 @@ export * from './npc-generation/setAge' export * from './npc-generation/setRace' export * from './npc-generation/swapNPCs' export * from './npc-generation/npcFinances' +export * from './npc-generation/randomiseNPC' export * from './npc-generation/traits/createPersonality' export * from './npc-generation/traits/getTraits' @@ -170,6 +171,8 @@ export * from './town/getTownArcana' export * from './town/getTownEconomics' export * from './town/getTownLaw' export * from './town/getTownMaterial' +export * from './town/getTownType' +export * from './town/getRaceReadout' export * from '../src/Town/js/getTownMilitary' export * from './town/getTownWelfare' export * from './town/politicsWeightedRoll' @@ -180,4 +183,6 @@ export * from './town/roads' export * from './town/townRender' export * from './town/updateTownSocioPolitics' export * from './town/townDemographics' +export * from './town/getDemographicPercentile' +export * from './town/setBaseDemographics' export * from './types' diff --git a/lib/npc-generation/randomiseNPC.test.ts b/lib/npc-generation/randomiseNPC.test.ts new file mode 100644 index 000000000..6a420fef2 --- /dev/null +++ b/lib/npc-generation/randomiseNPC.test.ts @@ -0,0 +1,14 @@ +// import { GenderName } from './genderData' +// import { AgeName, RaceName, raceTraits } from './raceTraits' +// import { randomiseNPC } from './randomiseNPC' + +// test('Randomise NPC base attributes', () => { +// const genderArray: GenderName[] = ['man', 'woman'] +// const raceArray = Object.keys(raceTraits) as RaceName[] +// const ageStageArray: AgeName[] = ['child', 'young adult', 'settled adult', 'elderly'] +// const npc = randomiseNPC() +// expect(genderArray).toContain(npc.gender) +// expect(raceArray).toContain(npc.race) +// expect(ageStageArray).toContain(npc.ageStage) +// expect(Object.keys(npc)).toContain(['ageStage', 'gender', 'race']) +// }) diff --git a/lib/npc-generation/randomiseNPC.ts b/lib/npc-generation/randomiseNPC.ts new file mode 100644 index 000000000..9b6c8e9e5 --- /dev/null +++ b/lib/npc-generation/randomiseNPC.ts @@ -0,0 +1,15 @@ +import { GenderName } from './genderData' +import { AgeName, RaceName, raceTraits } from './raceTraits' +import { random } from '../src/random' + +export const randomiseNPC = () => { + return { + gender: random(['man', 'woman']), + race: random(Object.keys(raceTraits)), + ageStage: random(['young adult', 'young adult', 'young adult', 'settled adult', 'settled adult', 'settled adult', 'elderly']) + } as { + gender: GenderName, + race: RaceName, + ageStage: AgeName + } +} diff --git a/lib/src/plague.ts b/lib/src/plague.ts new file mode 100644 index 000000000..0e0cd73ab --- /dev/null +++ b/lib/src/plague.ts @@ -0,0 +1,524 @@ +import { Town } from '@lib' + +interface Plague { + /** The name of the disease. */ + name: string + /** The symptoms of the disease. */ + symptoms: string + /** If there is a cure, what it is. */ + cure?: string | null + /** The severity of the disease. */ + severity?: number +} + +export const createPlague = (_town: Town): Plague => { + const plagueData = { + unique: [ + [ + { + name: 'Red Rot', + symptoms: 'A disease that is contracted by direct contact with a red ooze. The ooze attaches itself to the skin of the person and slowly rots the flesh away at the point of contact.', + cure: '' + }, + { + name: 'Gambling Addiction', + symptoms: 'Will save or compelled to gamble when presented with opportunity.', + cure: '' + }, + { + name: 'The Screaming Sickness', + symptoms: 'The victim moans and screams in their sleep, making it hard for those sleeping near the victim to get a good night’s rest.', + cure: '' + }, + { + name: 'Slate Fever', + symptoms: 'An uncommon sickness that causes skin to become hard and brittle. Originating from the underdark and eventually making its way to the surface, Slate Fever propagates through contact with mosses and fungi contaminated with the disease, as well as through contact with the affected skin of those with the sickness. Early symptoms include a slight grey discoloration of skin tone, uncomfortable stiffness, and unusual amounts of neck and back cracking when moving. The symptoms eventually progress to the point where the diseased becomes noticeably grey and stiff, causing them to move in small, jerk-like motions and have their speed reduced by half. In addition, the diseased develops exceptionally brittle skin that cracks easily and painfully. This gives them vulnerability to bludgeoning damage and causes them to frequently make Constitution saving throws to avoid taking 1d4 damage from shattering parts of their skin with excessive contact. The disease itself usually passes by itself over the course of one week resting and recuperating, but can be cured within 2 days if the diseased is properly treated with a combination of various tree saps. Catching this disease once results in the diseased being more vulnerable to it in the future.', + cure: '' + }, + { + name: "Spectre's Decay", + symptoms: "The victim's soul and body separate but continue to move in unison. Without a soul the body rapidly degenerates and decays over the course of a year, however wherever the flesh forms holes or rips the ghost of the person continues to stand. This continues until the body has completely decomposed and the ghost is left to wander the earth, unable to pass into an afterlife. This disease can rarely be contracted through contact with undead who have lingering souls.", + cure: '' + }, + { + name: 'Summoning Sickness', + symptoms: 'Due to a teleportation spell gone wrong, your stomach and head are all turned around. Disadvantage on ability checks and saving throws until all your parts manage to realign properly.', + cure: '' + }, + { + name: 'Casters Cough', + symptoms: 'Due to a nasty infection having spent too much time in a component pouch, you have a nasty magical cough. Whenever you cast a spell there is a 25 percent chance that you must roll on the Wild Surge Table.', + cure: '' + }, + { + name: 'Goblinitis', + symptoms: 'every hour you have a 50% chance of alternating between your normal form and that of a goblin.', + cure: '' + }, + { + name: 'Hydrophobia', + symptoms: 'you are afraid of any body of water larger than a small puddle. Disadvantage on ability checks when in or on water.', + cure: '' + }, + { + name: "Begger's Pox", + symptoms: 'weeping sores break out over your face, making you hard to look upon. Disadvantage in charisma checks without some form of face covering.', + cure: '' + }, + { + name: 'Drunken Fool', + symptoms: 'slurred speech and perpetual dizziness make you appear drunk, disadvantage on acrobatics checks and charisma checks unless engaged with someone drinking alcohol.', + cure: '' + }, + { + name: 'Sewer Rat Flu', + symptoms: 'When bitten by a sewer rat, the effect is just like the regular flu, but 3 times worse. PC vomits through out most of the day. To determine when PC vomits, roll a percentile die every 30 minutes in game, If even then vomit, if odd, nothing happens. Vomiting will force the PC to stop what ever action they are doing, and breaks concentration. Vomiting every 3rd time will also cough up blood. Roll 1d4 to calculate damage. The cure is to drink very hard alcohol, and let it dissolve the stomach acid poison. Another option is to survive 10 hours. A lesser restoration spell automatically cures it. Orcs, Half-orcs, and Drow are immune to this.', + cure: '' + }, + { + name: 'Transmutation Overload', + symptoms: 'this can happen if multiple transmutation spells occur on the same time on the same subject or subjects. it deforms the subject to something in the middle of all of them, but also makes the sick immune to transmutation spells, no more room for magic on this body! (based on the wonderful manga dorohedoro)', + cure: '' + }, + { + name: 'Magical Discord', + symptoms: "You begin to lose connection to the forces of magic. Each day your spellcasting ability drops by 1 point until you're cured.", + cure: '' + }, + { + name: "Vorel's Phage", + symptoms: "Disease starts by imposing one Exhaustion level on the affected creature, in which small patches of bumps errupt on the inflicted creature's skin and the joints swell painfully. After an incubation period of 1d4-1 nights, the creature gains another level of Exhaustion and the bumps become raised and fungal looking. On the firstboccurance of a new moon after the incubation period, the infected creature gains 2 more Exhaustion levels. If this kills the creature, it rises again after 1d4-1 nights as a wight. If a blood moon occurs at any point after incubation, the creature instead just succeed in a DC 15 Constitution saving throw or die immediately, raising in a minute as a wight. Exhaustion levels gained by this disease reappear every morning and only a miracle, wish or some other similar magic can permanently remove them without curing the disease.", + cure: '' + }, + { + name: "Swordbearer's Bane", + symptoms: "The disease is contracted by contact with an infected creature's blood. After the incubation period of 1d4 days, the hands and feet of the infected creature begin to swell and become tender. This causes the creature to lose 5ft of movement and imposes disadvantage on Dexterity (Slight of hand) checks. After another 7-1d4 days, the infection causes the hosts extremities to go numb. The infected creature's movement speed is halved and the creature is treated as if it were under the effect of a Bane spell until it is cured.", + cure: '' + }, + { + name: 'The Turquoise Death', + symptoms: 'This disease is contracted by contact with a creatures bodily fluids, and the disease remains infectious weeks after the fluid has evaporated, but only if rehydrated. After the initial 1d3 day incubation period the infected will start to notice heart palpitations, and will note slow recovery rates. The infected creature can no longer remove levels of exhaustion, and failed death saving throws permanently reduce the number of death saving throws the creature can fail in the future before death occurs. Both effects are permanent after the creature has gone without treatment for more than a month.', + cure: '' + }, + { + name: 'Hippogryff Hives', + symptoms: 'A an itchy rash that makes it unbearable to wear armor or heavy clothing until it’s treated. Highly contagious via skin to skin contact.', + cure: '' + }, + { + name: 'Tapeworm', + symptoms: 'Unnoticeable for the first month, for three months after must eat twice as much as normal each day. After three months, receives a penalty to constitution as you can no longer extract enough nutrients before the parasite consumes the food.', + cure: '' + }, + { + name: 'Monstrous Tapeworm', + symptoms: 'As above, but after six months, it eats it way out of the stomache, kills, then finally consumes the host. Treatment is recommended before that happens.', + cure: '' + }, + { + name: 'Brain Fever', + symptoms: 'caught from proximity to Underdark denizens. Causes paranoia (disadvantage on Charisma checks due to distrustfulness)', + cure: '' + }, + { + name: 'Carrion Fever', + symptoms: 'Causes migraines and an intense craving of raw meat.', + cure: '' + }, + { + name: 'Acidblight', + symptoms: 'Slowly causes blood and other bodily fluids to become more and more corrosive, dealing acid damage slowly over time.', + cure: '' + }, + { + name: 'The Yellow Plague', + symptoms: "Pus and bile ooze from the afflicted's pores. They gain a level of fatigue every day it is untreated.", + cure: '' + }, + { + name: "Ser Avidore's Fire", + symptoms: 'Magical multi-colored rashing of the skin. Can sometimes flare up and deal a type of magical damage to those afflicted. Common among magic users.', + cure: '' + }, + { + name: 'Cobblestone Sickness', + symptoms: 'After a bite from a cockatrice, the body will slowly turn to stone.', + cure: '' + }, + { + name: 'Two-Left-Feet Syndrome', + symptoms: 'Any kind of saving throw is disadvantaged as well as -4 for any number for any roll.', + cure: '' + }, + { + name: 'The Common Cold', + symptoms: 'Just a cold. You’ll cough and sneeze, but that’s about it', + cure: '' + }, + { + name: "Conjurer's Mania", + symptoms: "A mental illness that worsens over time. The afflicted believes that they are able to create life, and seeks out new ways to do so. Becomes obsessed with anything that they have 'created.' If the afflicted has no means for creating life, magically or otherwise, they may start to irrationally believe that inanimate objects are their living creations. May start believing other 'summoners' creations' are their own creations that have been stolen. May eventually progress to compulsion to steal newborn animals, monsters, or humanoids, or become unstable when seeing another magic user summoning a creature.", + cure: '' + }, + { + name: 'Third Eye Blind', + symptoms: 'Unable to communicate telepathically, but also unable to be targeted telepathically and is unaffected by any method that would not work on a creature without an intelligence. Recalling images from memory become hazy and unreliable. Penalties to any checks to recall visual information from memory. This disease tends to present itself in those who have been the target of many unwanted telepathic intrusions. Some humanoids are hereditarily predisposed to this disease, and may be afflicted upon after their first telepathic intrusion.', + cure: '' + }, + { + name: 'Desert Fever', + symptoms: 'Skin takes on a red tinge, fever & bouts of cold sweat. Treat unusually warm or cold weather as hazardous temperatures. Often a physical ailment caused by interacting with malicious djinn magic. May be transmitted orally.', + cure: '' + }, + { + name: "Displacer's Malalignment", + symptoms: "Appears to be the common cold, but is actually a disease that can be deadly for magic users & planar travelers. Afflicted must make a check to succeed any teleportation spell or ability. On a failed check, will move the afflicted 1d6ft in a random direction. Afflicted may be damaged or killed if teleported into a solid object. (If playing DnD, use the Teleportation spell's Mishap rules) This disease often causes magic users to be incredibly wary of those presenting symptoms of the common cold. Spread through ingesting of bodily fluids, most commonly from the sneezes or runny nose of the afflicted.", + cure: '' + }, + { + name: 'Woodland Mania', + symptoms: 'Afflicted believes to have gained the ability to speak with animals. The animals that they believe they are talking to may or may not exist, and all conversations tend to cause paranoia. Spread through bites, may be transmitted by animals.', + cure: '' + }, + { + name: 'Other-Otherworldy Whispers', + symptoms: 'A mental illness that causes the afflicted to believe that they are receiving knowledge from another dimension. In truth, the advice & knowledge that they get are delusional. The Afflicted tends to become a know-it-all, and the whispers encourage & bolster their cockiness.', + cure: '' + }, + { + name: 'Slippery sickness', + symptoms: "Your skin constantly oozes a greasy fluid. Without shoes, you're liable to trip. Without gloves, you're liable to drop objects. The upside is that monsters will have a hard time grabbing your skin.", + cure: '' + }, + { + name: 'Brilliant Urine Disease', + symptoms: 'You get dehydrated twice as quickly as usual, and your urine glows neon yellow, brightly enough that a pint glass of it could substitute for a torch. Spread through pollen in heavy forests, this disease is the bane of scouts, spies, and hunters.', + cure: '' + }, + { + name: 'Wild Stench', + symptoms: 'You give off an odor that is undetectable to humans and many other humanoid species like dwarves and orcs, but can be smelled by most mammals, fish, and insects from far away. They find it highly offensive and will flee from it. Only the bravest scent hounds can be convinced to track such a scent. It is rumored that wild stench is the partly successful result of a magical experiment to keep crops safe from wild animals. Curiously, dragons find it pleasant and may ask where you got that enchanting perfume.', + cure: '' + }, + { + name: 'Beard Agnosia', + symptoms: "You lose the ability to recognize a beard. You have great difficulty recalling or describing a beard, or noticing that somebody has grown or shaved a beard. In advanced cases, sufferers of beard agnosia can be looking at themselves in the mirror and be unable to tell whether they just shaved off their beard or they've gone a month without shaving it. Fortunately, this disease is less common in men than women. Unfortunately, it is more common in dwarves.", + cure: '' + }, + { + name: 'Puffs', + symptoms: "Fungi based parasite. Creature becomes infected after inhaling airborne spores. First sign of symptoms appears to be nothing more than mild acne. Soon, several of the 'zits' begin to grow larger. Not unnaturally large for such blemishes. Finally, a single 'puff' quickly grows to the size of a grape and bursts into a small cloud of spores that quickly spread in a breeze, infecting those around the host, as well as leaving a large 'pockmark' in it's place. The fungus, while growing inside the host, causes ever growing feelings of loss and loneliness, pushing the host to seek out more and more companionship. If the fungus is allowed to reach fruition and spore, the sense of loneliness transitions into a permanent and inconsolable state of depression.", + cure: '' + }, + { + name: 'Tiny Devil', + symptoms: 'when having an unprotected sexual intercourse with dwarfes you have a 35% possibility of getting this disease. Tieflings and succubusses are immune to this disease. Dwarfes do not suffer the effects of this disease but can infect other humanoids. If you have the Tiny Devil, your skin becomes very dry and making Athletics checks, Acrobatics checks and Dex sabing throws gives you 1d4 slashing damage(little wounds forms on your limbs, they can be cured only by magic). If you are a male, your ding ding dong burns very bad(no damage suffered unless you have sex. If so you take 1d4 fire damage per minute). To get rid of this disease: female: this disease will go away in 1d12 months. male: you need to cut your THING of infect 4d10 humanoids with this disease.', + cure: '' + }, + { + name: 'Death Luck', + symptoms: 'if you fail a saving throw with a 1 you have 10% chance of getting this disease(1d100, from 90-100). While you are infected you produce one Wild Magic effect every 2d12 hours and your skin hardenes(you have disadvantage on Dex checks aka all skills that count on dexterity and all saving throws but you gain +1 to your AC). This disease is contagious and can be transmitted through touch. It can be cured with 25 days of resting doing nothing but eating, drinking(alcohol might help) and sleeping.', + cure: '' + }, + { + name: 'The Dreaded Mallergy', + symptoms: 'Few have survived the infliction of this fabled contagion. Not much is understood about the Mallergy, but patients are struck with a deep sense of dread as they become bedridden and sweat constantly. 48 hours after the disease has set in they can no longer move. If they do not fight off the infection they succumb to asphyxiation and perish.', + cure: '' + }, + { + name: 'The Wariness', + symptoms: 'PCs develop this disease though interactions/attacks from psychic creatures. All insight checks result in the PC believing that others are lying to or trying to harm them.', + cure: '' + }, + { + name: 'The Illusionary Insomnia', + symptoms: 'The first symptom is an inability to sleep through non magical means. After 2 days the patient begins to hallucinate. After three days these hallucinations begin to manifest as magical illusions. As the patient nears a week without sleep, the illusions become more physical as their mental state deteriorates. The user finally falling asleep through magical means is the only known cure.', + cure: '' + }, + { + name: 'Marked Blood', + symptoms: 'After swimming in any sort of sewage, under a city or in a refuse dump, microscopic creatures have sublimated into your blood stream. They lie dormant until you move to a different sewage system. Once there you have to succeed in DC x wisdom saving throw or you use your action to slash yourself till your blood pours out.', + cure: '' + }, + { + name: 'Wraith Eyes', + symptoms: 'The person goes blind for random d20 periods of d10 minutes.', + cure: '' + }, + { + name: 'Necrotic Blight', + symptoms: 'The disease causes double the amount of necrotic damage to be dealt to this infected person.', + cure: '' + }, + { + name: 'Happy Cancer', + symptoms: 'This disease causes people to slowly die over a d20 period of days. At the end the infected is over joyed with the sweet release of death.', + cure: '' + }, + { + name: 'Ogre Poisoning', + symptoms: 'Turns the infected humanoid into a Ogre after d20 days of being infected without treatment.', + cure: '' + }, + { + name: 'Winter Insomnia', + symptoms: "When the temperature around the infected person drops below fifty degrees, the person will pass out and won't awake until the temperature increases.", + cure: '' + }, + { + name: 'Hostile Cough', + symptoms: "When someone infected coughs on another humanoid who isn't infected that person becomes hostile toward the person who coughed on them.", + cure: '' + }, + { + name: 'Soft Bones', + symptoms: "The player's AC drops two points until cured.", + cure: '' + }, + { + name: 'Golden Tumor', + symptoms: "The tumor starts as a small gold tumor then advances to the rest of the body. Once the disease has ran its course the subjects body is completely made of gold. Bet you didn't known were gold coins came from.", + cure: '' + }, + { + name: 'Swamp Rage', + symptoms: 'Pissed off about swamp people.', + cure: '' + }, + { + name: 'Beer Depression', + symptoms: 'When the infected person drinks more than 2d6 cups of beer they become depressed for 4d4 hours.', + cure: '' + }, + { + name: 'Demon Ears', + symptoms: 'This person can only hear what demons tell it.', + cure: '' + }, + { + name: 'Zombie Delirium', + symptoms: 'Becomes Delirious about the undead.', + cure: '' + }, + { + name: 'Rabbit Panic', + symptoms: "This person can't eat rabbit without being panicked for 2d8 hours.", + cure: '' + }, + { + name: 'Hemophilia', + symptoms: 'double bleed damage', + cure: '' + }, + { + name: 'Tetanus', + symptoms: 'disadvantage on dex throws after 1 day, total paralysis after 4 days, death check every day after the 7th day', + cure: '' + }, + { + name: 'Creeping cough', + symptoms: 'disadvantage on infiltration throws', + cure: '' + }, + { + name: 'Spasm of The Entrails', + symptoms: 'the PC cannot regenerate health by eating and resting, can be caught when fighting against animal abominations', + cure: '' + }, + { + name: 'Skin Rose', + symptoms: 'a red mark that slowly spread around a wound like a flower, after 2 weeks spawns little parasites that will drain the PC of his energy (disadvantage on CON throws). the pattern has to be burned deeply to kill the parasites. Take care of your handsome heroes', + cure: '' + }, + { + name: 'Leprosis', + symptoms: "the PC's skin start to slowly deteriorate and wounds don't cicatrize anymore, the PC also becomes slowly immune to physical pain.", + cure: '' + }, + { + name: 'Narcoleprosy', + symptoms: 'Your limbs and digits have an extreme tendency to fall asleep in relaxing conditions. If left untreated, your arms, legs, fingers, and toes can become shortened and deformed, as cartilage is absorbed into the body.', + cure: '' + }, + { + name: 'Humanoid Immunodeficiency Virus', + symptoms: "The diseased person suffers general flu-like symptoms (runny nose, coughing, aches in joints) for a few days that eventually pass. The true danger of this disease comes later. If the underlying virus is left untreated, the affected's immune system degrades tremendously, temporarily losing one ability point in Constitution each month until the disease is treated, gaining all lost Constitution back on the next long rest. If left untreated, the affected will also suffer a permanent level of exhaustion every 6 months that does not go away even after the disease is treated.", + cure: '' + }, + { + name: "Arboreal Petrification, or The Dryad's Rot", + symptoms: 'They say to slay a dryad is bad luck. But killing a dryad who is ill or favored by the gods will surely inflict this disease should any of her sap touch your skin. First, blindness and deafness slowly takes you, soon followed by sluggishness and fatigue. Then, your body begins to painfully turn, inside out, into the hard, coarse bark of an old dying tree. Your body rejects food, so you begin to starve. You require ten times the amount of water you normally do, or you will die of thirst. Your limbs become too heavy to move and your skin takes the flaky, layered appearance of wood chips. Your throat will eventually become thick and you will die of asphyxiation, and finally your body will be grown over with bark, and you will become a misshapen pile of vile sap and wood.', + cure: '' + }, + { + name: 'Hairasite', + symptoms: 'Many of the entities hairs have been replaced by long, light worms, latching on to the victim’s scalp. Disadvantage on all charisma checks until the victim takes fire damage or is magically healed.', + cure: '' + }, + { + name: 'Involuntary Mirror Touch Synthesia', + symptoms: 'Whenever the victim sees someone take damage, they take 1d4 psychic damage. If the victim is the one attacking, they take an additional 1d4+1', + cure: '' + }, + { + name: 'Chronic Shrinking', + symptoms: 'A possible effect of transmutation magic gone wrong. The victim shrinks to one half their previous height every week until cured. This disease is contagious if anyone is near the victim for an extended period of time.', + cure: '' + }, + { + name: 'Grave Grub', + symptoms: "In infection given from maggots that have grown inside an undead creature that were transferred to a living humanoid during some close-quarters encounter. Causes the infected to make a DC 15 Constitution saving throw, on a failure the infected's movement is reduced by half, their Constitution & Charisma score lower by 1 point and and they get Disadvantage on Persuasion checks due to visible greenish rot growing out of their ears, mouth, nostrils, and eye sockets. Every midnight they can make another Constitution saving throw, if they fail 3 in a row the humanoid suffers the effects permanently.", + cure: '' + }, + { + name: 'Magic Allergy', + symptoms: 'You experience a mild allergic reaction to prolonged exposure to anything even slightly magical. "Oh is that an achoo everburning torch? achoo"', + cure: '' + }, + { + name: 'Cheatersbane', + symptoms: 'you vomit vigorously when attempting sexual contact. May be made to explicitly allow intimacy with one particular person. Intended for suspicious spouses, but accidentally found a cult following among emetophiles.', + cure: '' + }, + { + name: 'Noblewind', + symptoms: 'you fart uncontrollably, but it smells sweet. Everybody notices.', + cure: '' + }, + { + name: 'Polychromia', + symptoms: 'each of your eyes view the world as if through a randomly-colored sheet of glass. Every day, the colors change.', + cure: '' + }, + { + name: "Bard's Revenge", + symptoms: 'your ears constantly hear a repeating brief tune.', + cure: '' + }, + { + name: 'The Twisted Tongue', + symptoms: "The patient begins to swear and insult others by chance. They can't controll it.", + cure: '' + }, + { + name: 'The Paralyzed Tongue', + symptoms: 'The patient is not able to move his tongue while talking. Very often they let their tongue stick out to of their mouth get rid of their pain too.', + cure: '' + }, + { + name: 'The Shaking Parrot', + symptoms: 'Whenever a trigger word comes up during a conversation (said by the patient or someone else). The patient repeats the word out loud. Sometimes they repeat the trigger word combined with a whole sentence (always the same). While they are triggered they jump around or shake their body or repeat one specific movement again and again.', + cure: '' + }, + { + name: "The Soldier's Fear", + symptoms: 'Whenever fear or high stress manifests in the mind of a warrior they begin to see illusions. If the fighter spots an enemy his will is testet (DC10 or DC15 if the infection is older that 2 weeks). If his mind fails he sees multiple enemys or enemys in a different more dangerous form.', + cure: '' + }, + { + name: 'The Golden Vision', + symptoms: 'One becomes obsessed with gold. The patient recognises the true worth of everything. They try to take possession of everything valuable to sell it for gold. They stop at nothing. They steal or use force. However if they get only a glace on gold pieces they lose the ability to count and just want to take the money with them and leave. They often begin to store their gold in a hidden place or bury it.', + cure: '' + }, + { + name: 'Oil of Ferrosix', + symptoms: 'Slowly turns those infected into mindless Warforged. Highly contagious but can be cured or halted by golems and Warforged.', + cure: '' + }, + { + name: 'Animaseperatism', + symptoms: 'Over the course of 1d6 days, your soul slowly exits the body. Over this time you may lose interest in a hobby or lose motivation in fighting the big bad, it can be stopped if you meet a cleric fast enough, but if not, you must acquire a vessel for the soul and roll disadvantage on all skill checks for 1d6 days.', + cure: '' + }, + { + name: 'Cephalogorgonism', + symptoms: 'colloquially referred to as getting medusaed, your hair turns to snake (yes all hair). The only known treatments are a buzzcut or using a lot of shampoo.', + cure: '' + }, + { + name: 'Medusa Rash', + symptoms: 'A slow growing petrification. Lasting as short a month to as long as several years the rash will slowly spread causing the flesh to turn to stone. The disease is 100% fatal with the disadvantages becoming more pronounced as full limbs become petrified.', + cure: '' + }, + { + name: 'The Vanishing Plague', + symptoms: 'Turns people invisible, highly contagious.', + cure: '' + }, + { + name: 'Tinder Sickness', + symptoms: 'Gives you a dry cough and vulnerability to fire damage, you explode if you are dropped to zero hit points by fire damage.', + cure: '' + }, + { + name: 'Slime stomach', + symptoms: 'A slime is in your stomach. It could have crawled in when you were sleeping, or maybe it was a spore in something you ate. It causes major pain, gives you bad acid reflux, and devours your food, leaving you malnourished. The only known solution is to swallow baking powder and pray to a god of healing.', + cure: '' + }, + { + name: 'Burning passion', + symptoms: 'A magical disease that causes you to get burns whenever you do something you love. first degree for eating your favorite food. Second degree for practicing your hobby. Third degree when you express love to your closest ones.', + cure: '' + }, + { + name: 'Frogtongue', + symptoms: "The afflicted's tongue gradually becomes malleable and sticky, like a ball of taffy, and the afflicted gradually loses the ability to pronounce words and enunciate. At its most advanced stages, the tongue can be protruded and retracted exactly as a frog's tongue, and the afflicted loses the ability to speak. Other symptoms include a craving for flies and an undying hatred for the wizard that crafted this ailment.", + cure: '' + }, + { + name: 'Mindbane', + symptoms: 'A magic psychic ailment passed by proximity with the afflicted, Mindbane shows those suffering it The Things Man Was Not Meant To See. Early stages of this ailment only seep into feverish nightmares, but later stages show unrelenting visions causing dissociation from reality, permanent psychosis, and even apparent death from insanity. This ailment can arise organically from contact with certain creatures from outside the mortal world.', + cure: '' + }, + { + name: 'Green-Skinned Death', + symptoms: "This disease causes the unfortunate afflicted's bones to decay inside their bodies, becoming brittle and snapping with regular usage and movement. The one tell-tale sign is that the afflicted's skin develops a pale green hue from the decay. The most common forms of death from this ailment are high blood toxicity and internal bleeding from a bone shard rupturing a vital organ.", + cure: '' + }, + { + name: 'Bloodfungus', + symptoms: "A parasitic mushroom spread by internal contact with spores, bloodfungus grows mycelium throughout the mortal body, feeding on the nutrients passed through the digestive tract and blood stream. After two months of relatively silent incubation, the mycelium will flower, bursting small fruiting-body stalks the size of an index finger from the skin along the afflicted's appendages, which flower in a day and quickly begin to release spores. Further, the mortal's nervous system is affected, producing extreme anxiety unless they seek out contact with other mortals.", + cure: '' + }, + { + name: 'Devil Rot', + symptoms: "A horrible disease caused by contact with devils and demons, the vicitm's flesh becomes hotter and hotter to the touch. While victims eventually spontaneously combust, in the meantime they gain resistance to fire and their touch does an additional 1d6 heat damage.", + cure: '' + }, + { + name: 'Moor plague', + symptoms: "An unplesant illness caused by scratches and cuts in foul swamps, this illness slowly turns the victim's skin green; additionally, the victim begins to emit the most unpleasant stench. Victims are Slowed while suffering this disease.", + cure: '' + }, + { + name: 'Salt plague', + symptoms: "This unpleasant disease feeds on the victim's salt levels. The victim eventually loses her humanity as her salt cravings become so intense that she becomes violent, seeking the salty blood of other victims.", + cure: '' + }, + { + name: 'White Fog', + symptoms: 'This illness attacks the eyes of the patient, slowly eroding his vision. Patients tend to see an increasingly opaque white fog obscuring their vision. Victims are Blind while suffering this disease.', + cure: '' + }, + { + name: 'Wyrm Flu', + symptoms: 'Supposedly an illness originally suffered by great dragons, this injury-transmitted disease is often suffered by those who survive attacks by wyrms and similar creatures. Victims are fatigued while suffering this disease.', + cure: '' + }, + { + name: 'Dragon fever', + symptoms: 'A magical disease contracted by being near Draconic influences for far too long. The symptoms start as a simple change in the eyes, making them take the look of a dragon, but other than that the paitent feels fine. If not treated then the second stage of the sickness begins, the patient becomes near obsessed with wealth and money, and will do anything to get it even at the expense of others. This may be subtle at first but once the third phase begins most people know exactly what has happened. The final phase involves a fever that if not treated constantly can kill the paitent. However if this last phase is survived, the patient finds themselves with some sign of Draconic nature, like horns or a small scales on parts of the body.', + cure: '' + }, + { + name: 'Qi Disconnect Disorder', + symptoms: 'You have advantage on physical skills made with just the right side of the body, disadvantage on all other attacks and skill checks. Cure with 1d4 days of meditation.', + cure: '' + + } + ] + ] + } + const selected = lib.random(plagueData.unique) + return selected +} diff --git a/lib/src/utils.ts b/lib/src/utils.ts index fec2ae8b2..36b6ead97 100644 --- a/lib/src/utils.ts +++ b/lib/src/utils.ts @@ -1,6 +1,5 @@ -import { DeepReadonly, WeightRecord } from '../types' +import { DeepReadonly } from '../types' import { randomFloat } from './randomFloat' -import { validateWeight } from './weightRandom' /** * An alternative, stricter typed version of `Object.keys`. @@ -147,18 +146,3 @@ export function repeat (fn: (index: number) => void, times: number) { export function capitalizeFirstLetter (text: string) { return text.charAt(0).toUpperCase() + text.slice(1) } - -export function sumWeights ( - defaultWeights: WeightRecord, - customWeights: WeightRecord -) { - const finalWeights: WeightRecord = {} - - for (const name of keys(customWeights)) { - const weight = validateWeight(customWeights[name]) - const defaultWeight = defaultWeights[name] ?? 0 - finalWeights[name] = defaultWeight + weight - } - - return finalWeights -} diff --git a/lib/src/weightRandom.ts b/lib/src/weightRandom.ts index 7c73da9ce..d7e2d4c55 100644 --- a/lib/src/weightRandom.ts +++ b/lib/src/weightRandom.ts @@ -31,3 +31,18 @@ export function validateWeight (weight?: number) { } throw new TypeError(`Weight "${weight}" is not a number.`) } + +export function sumWeights ( + defaultWeights: WeightRecord, + customWeights: WeightRecord +) { + const finalWeights: WeightRecord = {} + + for (const name of keys(customWeights)) { + const weight = validateWeight(customWeights[name]) + const defaultWeight = defaultWeights[name] ?? 0 + finalWeights[name] = defaultWeight + weight + } + + return finalWeights +} diff --git a/lib/town/chronology.ts b/lib/town/chronology.ts index 0baf24e0e..66a457caa 100644 --- a/lib/town/chronology.ts +++ b/lib/town/chronology.ts @@ -1,4 +1,5 @@ -import { Building, Deity, Faction, Family, NPC } from '@lib' +import { Building, createNamesake, createTippyFull, Deity, Faction, Family, Namesake, NPC, Town } from '@lib' +import { createPlague } from 'lib/src/plague' import { Location } from 'src/World/locations' export interface TownHistory { @@ -6,21 +7,22 @@ export interface TownHistory { } export interface TownEvent { - key: string - passageName?: string - name: string + /* Does the event have a special title? */ + title?: string + /* Description of what happened */ description: string about?: { - instigator?: string | NPC | Family | Faction | Deity + /* Who instigated the event? */ + instigator?: string | NPC | Family | Faction | Deity | Namesake + /* What did the event involve? */ target?: string | NPC | Family | Faction | Deity | Building + /* Where did the event take place? */ location?: string | Building | Location } time: { - /** Time that has elapsed since the event. */ - since: number - /** Time that the event started. */ - started: number - /** Total duration of the event. */ + /** Time in days that has elapsed since the event. */ + since?: number + /** Total duration in days of the event. */ duration: number } /** Was this event a good thing? */ @@ -29,6 +31,268 @@ export interface TownEvent { isGood?: boolean /** Was this event a good thing from an NPC's perspective? */ npcTest?(NPC: NPC): boolean + }, + effects?: { + /** What happens to the town when this event happens? */ + town: (town: Town) => void + impact: string } +} +interface TownEventMeta { + key: string + /* Does this event have a passage? */ + passageName?: string + name: string + probability: number + excludes(town: Town): boolean[] + event(town: Town): TownEvent + data: Record +} +interface TownEventsData { + data: { + [key: string]: TownEventMeta + } +} + +export const townEvents: TownEventsData = { + data: { + fire: { + key: 'fire', + name: 'fire', + probability: 1, + excludes (town: Town) { + return [ + town.location !== 'ice sheet' + ] + }, + event (town: Town) { + const event = {} as TownEvent + const names = townEvents.data.fire.data.names(town) + const selectedName: string | unknown = names[Math.floor(Math.random() * names.length)] + if (typeof selectedName === 'string') { + event.title = selectedName + } else { + Object.assign(event, selectedName) + } + Object.assign({ + description: `A massive fire broke out in ${town.name}.`, + about: { + location: town.location + }, + time: { + duration: lib.random([0.5, 1, 1, 2, 3, 5, 7]) + }, + sentiment: { + isGood: false + } + }, event) + return event + }, + data: { + names (town: Town) { + return [ + 'The Great Black Death', + 'Mournsday', + 'The Weeping Clouds', + 'The Devil\'s Revenge', + 'The Charring', + 'The Great Fire', + 'Conflagration', + { + title: 'The Sacking of the Shire', + description: `A district of ${town.name} was sacked by gangsters, with a pub setting off a chain reaction of fires.`, + about: { + instigator: 'gangsters', + location: town.location + }, + effects: { + impact: `Buildings in ${town.name} are built further apart than usual.` + } + }, + { + title: 'The Razing of the Library', + description: `A beautiful library in ${town.name} caught fire, and burnt to the ground.`, + about: { + location: 'a library' + }, + effects: { + impact: 'People are unusually literate here.' + } + } + ] + } + } + }, + flood: { + name: 'flood', + key: 'flood', + probability: 2, + excludes (town: Town) { + return [ + !['desert'].includes(town.location), + town.terrain !== 'arid' + ] + }, + event (town: Town) { + const names = [ + 'The Flood', + `${lib.createNamesake(town).lastName}'s Folly`, + 'The Great Flood', + 'The Flood of the Ages', + 'The Day the Banks Broke' + ] + const event = {} as TownEvent + const selectedName: string | unknown = names[Math.floor(Math.random() * names.length)] + if (typeof selectedName === 'string') { + event.title = selectedName + } + Object.assign({ + description: `A flood swept through ${town.name}.`, + about: { + location: town.location + }, + time: { + duration: lib.random([0, 1, 1, 2, 3, 5, 7]) + }, + sentiment: { + isGood: false + } + }, event) + return event + }, + data: {} + }, + locusts: { + name: 'locusts', + key: 'locusts', + probability: 2, + excludes (town: Town) { + return [ + !['ice sheet'].includes(town.location), + town.terrain !== 'polar', + town.currentSeason !== 'winter' + ] + }, + event (town: Town) { + const direction = lib.random(['north', 'south', 'east', 'west']).toUpperFirst() + const names = [ + 'The Locusts', + `The Locusts of the ${direction}`, + 'The Flying Plague', + 'The Hunger' + ] + const event = {} as TownEvent + const selectedName: string | unknown = lib.random(names) + if (typeof selectedName === 'string') { + event.title = selectedName + } + Object.assign({ + description: `A swarm of locusts swept through ${town.name}.`, + about: { + location: town.location + }, + time: { + duration: lib.random([4, 8, 12, 20, 30, 80, 160]) + }, + sentiment: { + isGood: false + } + }, event) + return event + }, + data: {} + }, + plague: { + name: 'plague', + key: 'plague', + probability: 2, + excludes (town: Town) { + return [ + town.roll.welfare < 90 + ] + }, + event (town: Town) { + const plague = createPlague(town) + const event = { + title: plague.name + } as TownEvent + Object.assign({ + description: `The ${town.type} had an outbreak of ${plague.name}.`, + about: { + location: town.location + }, + time: { + duration: lib.random([30, 80, 160, 260, 360, 480, 600]) + }, + sentiment: { + isGood: false + } + }, event) + return event + }, + data: {} + }, + terrorism: { + name: 'terrorism', + key: 'terrorism', + probability: 5, + excludes (town: Town) { + return [ + town.roll.welfare < 90, + town.roll.sin > 5, + town.roll.guardFunding < 90 + ] + }, + event (town: Town) { + const event = { + title: 'Terrorist Attack' + } as TownEvent + const reason = lib.random(townEvents.data.terrorist.data.reason) + const npc = createNamesake(town, { + note: `A terrorist that attacked ${town.name} because ${reason}` + }) + const readout = `A ${npc.race} called ${npc.firstName} ${npc.lastName}` + Object.assign({ + description: `A ${createTippyFull(readout, 'terrorist')} attacked the ${town.type} because ${reason}.`, + about: { + location: town.location + }, + time: { + duration: lib.random([30, 80, 160, 260, 360, 480, 600]) + }, + sentiment: { + isGood: false + } + }, event) + return event + }, + data: { + /** The terrorist attacked because ___ */ + reason: [ + 'they were mad', + 'they were cursed by a hag', + 'they were a foreign agent sent to cause chaos in these lands', + 'they were a psychopath', + 'they wanted to be infamous', + 'the town did not accept them', + 'they hated Mondays' + ] + } + } + + // fire + // flood + // terrible war + // plague + // great earthquake + // volcanic eruption + // meteor shower + // monster attack + // act of terrorism + // act of heroism + // political assassination + // religious miracle + // economic collapse + } } diff --git a/lib/town/getDemographicPercentile.ts b/lib/town/getDemographicPercentile.ts new file mode 100644 index 000000000..81b34c641 --- /dev/null +++ b/lib/town/getDemographicPercentile.ts @@ -0,0 +1,18 @@ +import { RaceName, Town } from '@lib' + +export const getDemographicPercentile = (town: Town) => { + console.log('Getting demographic percent.') + // Get an array of the demographic keys (race names). + const races = Object.keys(town.baseDemographics) as RaceName[] + // Calculate the sum of the raw demographic values. + const sum = races + .map((byRace) => town.baseDemographics[byRace]) + .reduce((acc, cur) => acc + cur, 0) + // Calculate the demographic percentages. + races.forEach((byRace) => { + const race: RaceName = byRace + town._demographicPercentile[race] = + (town.baseDemographics[race] / sum) * 100 + }) + return town._demographicPercentile +} diff --git a/lib/town/getPredominantRace.ts b/lib/town/getPredominantRace.ts index 05c78c2d1..9fa74adc7 100644 --- a/lib/town/getPredominantRace.ts +++ b/lib/town/getPredominantRace.ts @@ -1,6 +1,8 @@ import { sortArray } from '../src/sortArray' import { toTitleCase } from '../src/toTitleCase' import { raceTraits, RaceName } from '../npc-generation/raceTraits' +import { getRacesPercentile } from './townDemographics' +import { isPercentile } from './isPercentile' export interface PredominantInfo { /** Percentage of most populous race */ @@ -18,8 +20,16 @@ interface PredominantRace extends PredominantInfo { secondaryRace: RaceName; } +export function getPredominantRaceFromBase (baseDemographics: Record): PredominantRace { + const percentages = getRacesPercentile(baseDemographics) + return getPredominantRace(percentages) +} + export function getPredominantRace (percentages: Record): PredominantRace { console.log('Getting the predominant race...') + if (!isPercentile(percentages)) { + percentages = getRacesPercentile(percentages) + } // Pick out the primary & secondary Race name percentages. const [primary, secondary] = sortArray(percentages).reverse() diff --git a/lib/town/getRaceReadout.ts b/lib/town/getRaceReadout.ts new file mode 100644 index 000000000..3ed0fa5db --- /dev/null +++ b/lib/town/getRaceReadout.ts @@ -0,0 +1,5 @@ +import { Town } from '@lib' + +export const getRaceReadout = (town: Town) => { + return `${town.name} is ${lib.articles.output(lib.getTownType(town))} comprised ${lib.getPredominantRaceFromBase(town.baseDemographics).amountDescriptive}.` +} diff --git a/lib/town/getTownType.ts b/lib/town/getTownType.ts new file mode 100644 index 000000000..9b070936a --- /dev/null +++ b/lib/town/getTownType.ts @@ -0,0 +1,16 @@ +import { TownBasics, TownType } from '@lib' + +export const getTownType = (town: TownBasics): TownType => { + if (town.population > 6000) return 'city' + if (town.population > 3000) return 'town' + if (town.population > 1000) return 'village' + if (town.population > 30) return 'hamlet' + + // TODO: Remove unexpected side effect are bad. + if (town.population <= 30) { + console.log('Population is less than 30. Setting to 30.') + town.population = 30 + return 'hamlet' + } + return 'village' +} diff --git a/lib/town/isPercentile.ts b/lib/town/isPercentile.ts new file mode 100644 index 000000000..f5ea2a47d --- /dev/null +++ b/lib/town/isPercentile.ts @@ -0,0 +1,5 @@ +/** Tests whether the record adds up to 100. */ +export function isPercentile (record: Record): boolean { + const sum = Object.values(record).reduce((a, b) => a + b, 0) + return sum > 99 && sum < 100.1 +} diff --git a/lib/town/setBaseDemographics.ts b/lib/town/setBaseDemographics.ts new file mode 100644 index 000000000..ba051c8d2 --- /dev/null +++ b/lib/town/setBaseDemographics.ts @@ -0,0 +1,9 @@ +import { RaceName, Town } from '@lib' + +export const setBaseDemographics = (town: Town, newDemographics: Record) => { + console.log('Setting base demographics.') + Object.keys(newDemographics).forEach((byRace) => { + const race = byRace as RaceName + town._baseDemographics[race] = newDemographics[race] + }) +} diff --git a/lib/town/townDemographics.ts b/lib/town/townDemographics.ts index 63d5379f5..7474d4ee3 100644 --- a/lib/town/townDemographics.ts +++ b/lib/town/townDemographics.ts @@ -3,7 +3,7 @@ import { Town, TownBasics } from './_common' import { townData } from './townData' import { weightedRandomFetcher } from '../src/weightedRandomFetcher' import { RaceName } from '../npc-generation/raceTraits' -import { keys } from '../src/utils' +import { calcPercentage } from '../src/calcPercentage' export function townDemographics (town: TownBasics) { console.log(`Creating ${town.type} demographics.`) @@ -15,18 +15,68 @@ export function updateDemographics (town: TownBasics, newDemographics: Record { // Get an array of the demographic keys (race names). - const races = Object.keys(town.baseDemographics) as RaceName[] + const races = Object.keys(town._baseDemographics) as RaceName[] // Calculate the sum of the raw demographic values. const sum = races .map(byRace => town._baseDemographics[byRace]) .reduce((acc, cur) => acc + cur, 0) - // Calculate the demographic percentages. + // Calculate the demographic percentages. races.forEach(byRace => { town._demographicPercentile[byRace] = town._baseDemographics[byRace] / sum * 100 }) } + +/** Returns the percentage of a single race, humanized to add up to part of 100. + * @example return 68.87 + */ +export const getRacePercentile = (race: RaceName, baseDemographics: Record) => { + // Get an array of the demographic keys (race names). + const races = Object.keys(baseDemographics) as RaceName[] + // Calculate the sum of the raw demographic values. + const sum = races + .map(byRace => baseDemographics[byRace]) + .reduce((acc, cur) => acc + cur, 0) + const result = baseDemographics[race] / sum * 100 + return result +} + +/** Returns the percentags of all races, humanized to add up to 100. */ +export const getRacesPercentile = (baseDemographics: Record): Record => { + // Get an array of the demographic keys (race names). + const races = Object.keys(baseDemographics) as RaceName[] + // Calculate the sum of the raw demographic values. + const sum = races + .map(byRace => baseDemographics[byRace]) + .reduce((acc, cur) => acc + cur, 0) + const racePercentage = {} as Record + for (const race of races) { + racePercentage[race] = baseDemographics[race] / sum * 100 + } + return racePercentage +} + +/** Returns the population for all races */ +export const getRacesPopulation = (baseDemographics: Record, population: number): Record => { + const racePercentage = getRacesPercentile(baseDemographics) + const racePopulation = {} as Record + for (const temp of Object.keys(racePercentage)) { + const race = temp as RaceName + racePopulation[race] = getRacePopulation(race, baseDemographics, population) + } + return racePopulation +} + +/** Returns the population of a single race */ +export const getRacePopulation = (race: RaceName, baseDemographics: Record, population: number) => { + return calcPercentage(getRacePercentile(race, baseDemographics), population) +} diff --git a/src/Buildings/Components/NPCListboxes.twee b/src/Buildings/Components/NPCListboxes.twee index 78260a0dd..5c1068f1b 100644 --- a/src/Buildings/Components/NPCListboxes.twee +++ b/src/Buildings/Components/NPCListboxes.twee @@ -1,5 +1,5 @@ :: RandomNPCPrep [nobr] -<> +<> <> <> diff --git a/src/Buildings/Components/buildingRelationshipNpc.ts b/src/Buildings/Components/buildingRelationshipNpc.ts index 10cf4260e..5d01ab015 100644 --- a/src/Buildings/Components/buildingRelationshipNpc.ts +++ b/src/Buildings/Components/buildingRelationshipNpc.ts @@ -28,7 +28,6 @@ export const createReciprocalRelationshipNpc = ( args: Args) => { console.log('Creating a new NPC for this building.') console.log(relationshipTable, args, associatedNPC) - alert(JSON.stringify(args)) Object.assign({ base: {}, objectKey: 'building', @@ -44,7 +43,6 @@ export const createReciprocalRelationshipNpc = ( let base: Partial = {} if (relationship?.base) base = Object.assign(base, relationship.base, args?.base) - alert(JSON.stringify(base)) const npc = setup.createNPC(town, base) if (relationship?.relationships?.associatedNPC) { setup.createRelationship(town, associatedNPC, npc, { diff --git a/src/Buildings/Components/randomiseNPC.ts b/src/Buildings/Components/randomiseNPC.ts deleted file mode 100644 index 0dcea5cd6..000000000 --- a/src/Buildings/Components/randomiseNPC.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const randomiseNPC = () => { - return { - gender: ['man', 'woman'].random(), - race: Object.keys(lib.raceTraits).random(), - ageStage: ['young adult', 'young adult', 'young adult', 'settled adult', 'settled adult', 'settled adult', 'elderly'].random() - } -} diff --git a/src/Meta/Ennies.twee b/src/Meta/Ennies.twee deleted file mode 100644 index fad4b66b0..000000000 --- a/src/Meta/Ennies.twee +++ /dev/null @@ -1,26 +0,0 @@ -:: Ennies [nobr] -A small favour to ask of you... Eigengau's Generator has been nominated for an ENnie for Best Digital Aid. -This is a huge deal for me, and it's especially exciting to see the work I've been doing for the past few years now come to fruition. -I only get the one shot at the ENnie, though- products can't be re-submitted. -
-If you like the generator (and I truly hope that you do!), please consider voting for me. -It is open to the public, anonymous, and voting closes on the 27th of August. -It's super easy, and takes literally ten seconds (I timed!). -Just click the button, select 1 for "Eigengrau's Essential Establishment Generator", and hit "Vote"! -
-The results are announced on the 17th September. -
-<> -<> -<> \ No newline at end of file diff --git a/src/Settings/CSS/stylesheet.css b/src/Settings/CSS/stylesheet.css index c02945def..4fba5857e 100644 --- a/src/Settings/CSS/stylesheet.css +++ b/src/Settings/CSS/stylesheet.css @@ -196,7 +196,7 @@ html.two-columns #illustration-buffer { line-height: 1; } -#ennies { +#wiggle { position: fixed; top: 4em; padding: .5em; @@ -238,7 +238,7 @@ html.two-columns #illustration-buffer { 100% {-webkit-transform: rotate(0deg);} } -#ennies::before { +#wiggle::before { content: "\e80b "; padding-right: .4em; font-family: tme-fa-icons; @@ -249,6 +249,10 @@ html.two-columns #illustration-buffer { line-height: 1; } +span.macro-live { + display: inline-block; +} + .restart a { color: var(--text-link-hover); } @@ -503,7 +507,7 @@ margin-top: -0.8em; border-color: var(--text-normal); } -#ennies-sidebar { +#wiggle-sidebar { content: "\e80b "; padding-right: .4em; font-family: tme-fa-icons; diff --git a/src/Settings/PassageHeader.twee b/src/Settings/PassageHeader.twee index d9448bd0f..e6f977294 100644 --- a/src/Settings/PassageHeader.twee +++ b/src/Settings/PassageHeader.twee @@ -2,10 +2,4 @@
<> <> <> <>
-<><><><><> -:: EnnieHeader -<><><><><> <><><><><><> \ No newline at end of file +<><><> diff --git a/src/Settings/StoryStuff/StoryMenu.twee b/src/Settings/StoryStuff/StoryMenu.twee index a3347d65e..3bb562386 100644 --- a/src/Settings/StoryStuff/StoryMenu.twee +++ b/src/Settings/StoryStuff/StoryMenu.twee @@ -1,14 +1,6 @@ :: StoryMenu -<> - <> -<> - <><><><> <><><><> diff --git a/src/Settings/Tippy/tooltips.ts b/src/Settings/Tippy/tooltips.ts index 8728e98dc..7c2aee4a3 100644 --- a/src/Settings/Tippy/tooltips.ts +++ b/src/Settings/Tippy/tooltips.ts @@ -1,4 +1,5 @@ -import type { NPC, Building, Town, RaceName } from '@lib' +import { NPC, Building, Town, RaceName, getRacesPercentile } from '@lib' +import { isPercentile } from '../../../lib/town/isPercentile' export const makeTippyTitle = (span: HTMLElement, obj: any) => { if (obj.objectType) { @@ -119,6 +120,9 @@ export const createPercentageTooltip = (source: HTMLElement, target: string, con } export function createRaceHTML (percentages: Record, target: string, content?: string) { + if (!isPercentile(percentages)) { + percentages = getRacesPercentile(percentages) + } const array = lib.sortArray(percentages).reverse() const list = lib.formatPercentile(array as [string, number][]) const html = lib.formatArrayAsList(list) diff --git a/src/Start/RacesPercentageList.twee b/src/Start/RacesPercentageList.twee index 0e214da5f..c37935160 100644 --- a/src/Start/RacesPercentageList.twee +++ b/src/Start/RacesPercentageList.twee @@ -1,3 +1,3 @@ :: RacesPercentageList -<><> -<><><><><> \ No newline at end of file +<><> +<><><><><> \ No newline at end of file diff --git a/src/Start/Start.twee b/src/Start/Start.twee index 906bd49c8..3583ef175 100644 --- a/src/Start/Start.twee +++ b/src/Start/Start.twee @@ -25,4 +25,4 @@ header: `Welcome`, passage: 'Welcome' })>> -<> \ No newline at end of file +<> diff --git a/src/Tools/Exports/exportNovelAI.ts b/src/Tools/Exports/exportNovelAI.ts index cb79d4476..e76d37928 100644 --- a/src/Tools/Exports/exportNovelAI.ts +++ b/src/Tools/Exports/exportNovelAI.ts @@ -376,7 +376,6 @@ export function exportToNovelAI (town: Town, npcs: Record) { const placeholders = novel.placeholders.map(obj => { return obj.key }) - alert(JSON.stringify(placeholders)) novel.prompt = makePlaceholders(novel.prompt, placeholders) downloadObjectAsJson(novel, `The ${town.type} of ${town.name}`) return novel diff --git a/src/Town/components-edit/EditRaces.twee b/src/Town/components-edit/EditRaces.twee index 9c3a6f1ff..b53c9e8b0 100644 --- a/src/Town/components-edit/EditRaces.twee +++ b/src/Town/components-edit/EditRaces.twee @@ -1,23 +1,39 @@ :: EditRaces [nobr nofx] -<><> -
Racial Demographics -Town Population: <> -
-

$town.name is <> comprised <>.

-
- - - - - - +
+ Racial Demographics + <> + + Town Population: <> + +
+

+ <> +

+
+
RacePopulationPercentageRaw Number
+ + + + + -<> - - - - -<> -
RacePopulationPercentageRaw Number
<><><><>
<> + <> + <> + <> + <> + + <> + <> + <> + + + <> + + + + <> + <> + <> + <>
-<
> +/* <> */ diff --git a/src/Town/components-edit/EditReligion.twee b/src/Town/components-edit/EditReligion.twee index b97b906f0..6b380ba13 100644 --- a/src/Town/components-edit/EditReligion.twee +++ b/src/Town/components-edit/EditReligion.twee @@ -1,26 +1,42 @@ :: EditReligion [nofx nobr] -
Religion Demographics<><>$town.name <><>. - - - - - - - - -<> -<> -<><><> -/* <> */ -/* <><><> */ - - - - - - - - /* <> */ -<> -
DeityPopulationPercentageBase ProbabilityBonus / Penalty
<><><><><>
+
+ Religion Demographics +<> + <> + $town.name <>. + + + + + + + + + + <> + <> + <> + <> + <> + <> + /* <><><> */ + + + + + + + + <> + <> +
DeityPopulationPercentageBase ProbabilityBonus / Penalty
<><><> + + <> + + + + <> + +
+<
>
\ No newline at end of file diff --git a/src/Town/components-edit/Rerender.twee b/src/Town/components-edit/Rerender.twee index 5acf02c38..77049122a 100644 --- a/src/Town/components-edit/Rerender.twee +++ b/src/Town/components-edit/Rerender.twee @@ -1,6 +1,5 @@ :: Rerender <> -<> <> <> <> diff --git a/src/Town/components-edit/TownEdit.twee b/src/Town/components-edit/TownEdit.twee index 829b0d05e..2c32817bc 100644 --- a/src/Town/components-edit/TownEdit.twee +++ b/src/Town/components-edit/TownEdit.twee @@ -3,8 +3,10 @@ $(document).trigger(":liveupdate"); });>> <> -<> -<> +<> +<> <>
<> diff --git a/src/Town/components/TownListRaces.twee b/src/Town/components/TownListRaces.twee index bc089c351..adf3ce696 100644 --- a/src/Town/components/TownListRaces.twee +++ b/src/Town/components/TownListRaces.twee @@ -1,5 +1,5 @@ :: TownListRaces [nobr] -<> +/* <> */
$town.name is comprised <>. diff --git a/src/Town/js/createTown.ts b/src/Town/js/createTown.ts index 12bf984af..e39a57e3f 100644 --- a/src/Town/js/createTown.ts +++ b/src/Town/js/createTown.ts @@ -32,7 +32,7 @@ export const createTown = (base: TownBasics | Town) => { // @ts-ignore get type () { // @ts-ignore - return getTownType(this) + return lib.getTownType(this) }, set type (type) { console.log('type unnecessary') @@ -94,39 +94,22 @@ export const createTown = (base: TownBasics | Town) => { _politicalIdeology: politicalIdeology, _demographicPercentile: {} as Record, _baseDemographics: {} as Record, - // Clone the raw demographic data for the town type. - // _baseDemographics: clone(lib.townData.type['hamlet'].demographics.random().output), get baseDemographics () { console.log('Getting base demographics.') return this._baseDemographics }, set baseDemographics (newDemographics) { + // lib.setBaseDemographics(this as unknown as Town, newDemographics) console.log('Setting base demographics.') + // alert(JSON.stringify(newDemographics)) Object.keys(newDemographics).forEach((byRace) => { const race = byRace as RaceName this._baseDemographics[race] = newDemographics[race] }) - console.log(this.demographicPercentile) }, set demographicPercentile (data) { console.log('Useless demographicPercentile setter. ') }, get demographicPercentile () { - console.log('Getting demographic percent.') - - // Get an array of the demographic keys (race names). - const races = Object.keys(this.baseDemographics) as RaceName[] - - // Calculate the sum of the raw demographic values. - const sum = races - .map((byRace) => this.baseDemographics[byRace]) - .reduce((acc, cur) => acc + cur, 0) - - // Calculate the demographic percentages. - races.forEach((byRace) => { - const race: RaceName = byRace - this._demographicPercentile[race] = - (this.baseDemographics[race] / sum) * 100 - }) - return this._demographicPercentile + return lib.getDemographicPercentile(this as unknown as Town) }, get economicIdeology () { return this._economicIdeology @@ -282,21 +265,6 @@ export const createTown = (base: TownBasics | Town) => { return town as unknown as Town } -export const getTownType = (town: TownBasics): TownType => { - if (town.population > 6000) return 'city' - if (town.population > 3000) return 'town' - if (town.population > 1000) return 'village' - if (town.population > 30) return 'hamlet' - - // TODO: Remove unexpected side effect are bad. - if (town.population <= 30) { - console.log('Population is less than 30. Setting to 30.') - town.population = 30 - return 'hamlet' - } - return 'village' -} - function calculateTax (nominalTarget: number, economics: number) { return nominalTarget + (-1 / (economics + 0.1)) + (1 / (10 - economics)) } diff --git a/src/World/events.ts b/src/World/events.ts deleted file mode 100644 index 26bb8a891..000000000 --- a/src/World/events.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ -import type { Town } from '@lib' -import { Encounter, encounters } from './encounters' -import { BiomeName, Location, locations } from './locations' - -export const getLocation = (biome: BiomeName): Location => { - return lib.random(locations.filter(location => { - return location.available.includes(biome) - })) -} - -export const getEncounter = (biome: BiomeName): Encounter => { - return lib.random(encounters.filter(encounter => { - return encounter.available?.includes(biome) - })) -} - -export const getEventDescription = (event: Location | Encounter, town: Town, biome: BiomeName): string => { - if (event.function) { - return event.function(town, biome) - } - return event.summary -} diff --git a/src/World/locations.ts b/src/World/locations.ts index c87d3d0c6..a8f1d3594 100644 --- a/src/World/locations.ts +++ b/src/World/locations.ts @@ -1,14 +1,34 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import type { Town } from '@lib' -import { encounters } from './encounters' -import { getEncounter, getEventDescription } from './events' +import type { Town } from '@lib' +import { Encounter, encounters } from './encounters' export interface Location { summary: string available: BiomeName[] function?(town: Town, biome: BiomeName): string } +/* eslint-disable @typescript-eslint/ban-ts-comment */ + +export const getLocation = (biome: BiomeName): Location => { + return lib.random(locations.filter(location => { + return location.available.includes(biome) + })) +} + +export const getEncounter = (biome: BiomeName): Encounter => { + return lib.random(encounters.filter(encounter => { + return encounter.available?.includes(biome) + })) +} + +export const getEventDescription = (event: Location | Encounter, town: Town, biome: BiomeName): string => { + if (event.function) { + return event.function(town, biome) + } + return event.summary +} + export type BiomeName = | 'mountain' | 'desert' diff --git a/src/main.ts b/src/main.ts index 5599d6db2..70c8f06af 100644 --- a/src/main.ts +++ b/src/main.ts @@ -29,7 +29,7 @@ import { deleteNPC, deleteThrowawayNPCs } from './NPCGeneration/deleteNPC' import { getLifeEvents } from './NPCGeneration/getLifeEvents' import { openDialog, rerenderPage } from './Dialog/openDialog' import { addSettingButton } from './Settings/settingButton' -import { getLocation, getEncounter, getEventDescription } from './World/events' +import { getLocation, getEncounter, getEventDescription } from './World/locations' import { graveStone } from './World/graveStone' import { urlSeed, navigateToObj } from './World/urlSeed' import { deleteFaction } from './Factions/deleteFaction' @@ -37,7 +37,7 @@ import { leaderFaction } from './Factions/leader' import { plothooks } from './PlotHook/plothooks' import { createTownBiome } from './Town/js/createTownBiome' import { createTownName } from './Town/js/createTownName' -import { createTown, getTownType } from './Town/js/createTown' +import { createTown } from './Town/js/createTown' import { findViaKey, findIfExistsViaKey } from './Tools/findViaKey' import { createBlacksmithProject } from './Blacksmith/js/blacksmithProject' import { createSmithyName } from './Blacksmith/js/createSmithyName' @@ -56,7 +56,6 @@ import { createFaction } from './Factions/createFaction' import { getTownMilitary } from './Town/js/getTownMilitary' import { getPoliticalSourceDescription } from './Town/js/getPoliticalSourceDescription' import { exportToNovelAI } from './Tools/Exports/exportNovelAI' -import { randomiseNPC } from './Buildings/Components/randomiseNPC' // import { buildingTypes, createBuildingKeys, createNewBuilding } from './Town/js/createNewBuilding' declare global { @@ -109,7 +108,6 @@ declare global { createTownBiome: typeof createTownBiome createTownName: typeof createTownName createTown: typeof createTown - getTownType: typeof getTownType findViaKey: typeof findViaKey findIfExistsViaKey: typeof findIfExistsViaKey createBlacksmithProject: typeof createBlacksmithProject @@ -131,7 +129,6 @@ declare global { getTownMilitary: typeof getTownMilitary getPoliticalSourceDescription: typeof getPoliticalSourceDescription exportToNovelAI: typeof exportToNovelAI - randomiseNPC: typeof randomiseNPC // createBuildingKeys: typeof createBuildingKeys // createNewBuilding: typeof createNewBuilding } @@ -186,7 +183,6 @@ Object.assign(setup, { createTownBiome, createTownName, createTown, - getTownType, findViaKey, findIfExistsViaKey, createBlacksmithProject, @@ -207,8 +203,7 @@ Object.assign(setup, { createFaction, getTownMilitary, getPoliticalSourceDescription, - exportToNovelAI, - randomiseNPC + exportToNovelAI // createBuildingKeys, // createNewBuilding }) diff --git a/src/setup.d.ts b/src/setup.d.ts index 23ec4d308..3493d017a 100644 --- a/src/setup.d.ts +++ b/src/setup.d.ts @@ -27,7 +27,6 @@ export interface Setup { createTownBiome(base: Partial): TownBasics createTown(base: TownBasics): Town createCastle(town: Town): Building - getTownType(town: Town): string createTownName(town: Town): string } diff --git a/yarn.lock b/yarn.lock index 0261d71f7..085a3c9f0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1138,9 +1138,9 @@ ansi-escapes@^4.2.1, ansi-escapes@^4.3.0: type-fest "^0.21.3" ansi-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" - integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== ansi-styles@^3.2.1: version "3.2.1" @@ -4832,9 +4832,9 @@ semver-compare@^1.0.0: integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= semver-regex@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-3.1.2.tgz#34b4c0d361eef262e07199dbef316d0f2ab11807" - integrity sha512-bXWyL6EAKOJa81XG1OZ/Yyuq+oT0b2YLlxx7c+mrdYPaPbnj6WgVULXhinMIeZGufuUBu/eVRqXEhiv4imfwxA== + version "3.1.3" + resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-3.1.3.tgz#b2bcc6f97f63269f286994e297e229b6245d0dc3" + integrity sha512-Aqi54Mk9uYTjVexLnR67rTyBusmwd04cLkHy9hNvk3+G3nT2Oyg7E0l4XVbOaNwIvQ3hHeYxGcyEy+mKreyBFQ== "semver@2 || 3 || 4 || 5", semver@^5.5.0, semver@^5.6.0: version "5.7.1" @@ -5270,9 +5270,9 @@ through@^2.3.8: integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= tmpl@1.0.x: - version "1.0.4" - resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" - integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE= + version "1.0.5" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" + integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== to-fast-properties@^2.0.0: version "2.0.0"