@its-not-rocket-science/ananke 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +135 -0
- package/LICENSE +21 -0
- package/README.md +2199 -0
- package/STABLE_API.md +266 -0
- package/dist/src/anatomy/anatomy-compiler.d.ts +14 -0
- package/dist/src/anatomy/anatomy-compiler.js +277 -0
- package/dist/src/anatomy/anatomy-contracts.d.ts +94 -0
- package/dist/src/anatomy/anatomy-contracts.js +1 -0
- package/dist/src/anatomy/anatomy-helpers.d.ts +82 -0
- package/dist/src/anatomy/anatomy-helpers.js +233 -0
- package/dist/src/anatomy/anatomy-schema.d.ts +28 -0
- package/dist/src/anatomy/anatomy-schema.js +388 -0
- package/dist/src/anatomy/index.d.ts +4 -0
- package/dist/src/anatomy/index.js +4 -0
- package/dist/src/archetypes.d.ts +87 -0
- package/dist/src/archetypes.js +285 -0
- package/dist/src/arena.d.ts +173 -0
- package/dist/src/arena.js +695 -0
- package/dist/src/bridge/bridge-engine.d.ts +46 -0
- package/dist/src/bridge/bridge-engine.js +252 -0
- package/dist/src/bridge/index.d.ts +4 -0
- package/dist/src/bridge/index.js +5 -0
- package/dist/src/bridge/interpolation.d.ts +64 -0
- package/dist/src/bridge/interpolation.js +130 -0
- package/dist/src/bridge/mapping.d.ts +33 -0
- package/dist/src/bridge/mapping.js +54 -0
- package/dist/src/bridge/types.d.ts +94 -0
- package/dist/src/bridge/types.js +2 -0
- package/dist/src/campaign.d.ts +141 -0
- package/dist/src/campaign.js +235 -0
- package/dist/src/channels.d.ts +15 -0
- package/dist/src/channels.js +20 -0
- package/dist/src/chronicle.d.ts +124 -0
- package/dist/src/chronicle.js +232 -0
- package/dist/src/collective-activities.d.ts +154 -0
- package/dist/src/collective-activities.js +247 -0
- package/dist/src/competence/acoustic.d.ts +101 -0
- package/dist/src/competence/acoustic.js +242 -0
- package/dist/src/competence/catalogue.d.ts +30 -0
- package/dist/src/competence/catalogue.js +241 -0
- package/dist/src/competence/crafting.d.ts +35 -0
- package/dist/src/competence/crafting.js +88 -0
- package/dist/src/competence/engineering.d.ts +53 -0
- package/dist/src/competence/engineering.js +108 -0
- package/dist/src/competence/framework.d.ts +68 -0
- package/dist/src/competence/framework.js +694 -0
- package/dist/src/competence/index.d.ts +12 -0
- package/dist/src/competence/index.js +13 -0
- package/dist/src/competence/interspecies.d.ts +81 -0
- package/dist/src/competence/interspecies.js +108 -0
- package/dist/src/competence/language.d.ts +79 -0
- package/dist/src/competence/language.js +115 -0
- package/dist/src/competence/naturalist.d.ts +97 -0
- package/dist/src/competence/naturalist.js +187 -0
- package/dist/src/competence/navigation.d.ts +24 -0
- package/dist/src/competence/navigation.js +48 -0
- package/dist/src/competence/performance.d.ts +125 -0
- package/dist/src/competence/performance.js +210 -0
- package/dist/src/competence/teaching.d.ts +64 -0
- package/dist/src/competence/teaching.js +121 -0
- package/dist/src/competence/willpower.d.ts +74 -0
- package/dist/src/competence/willpower.js +114 -0
- package/dist/src/crafting/index.d.ts +55 -0
- package/dist/src/crafting/index.js +229 -0
- package/dist/src/crafting/manufacturing.d.ts +83 -0
- package/dist/src/crafting/manufacturing.js +165 -0
- package/dist/src/crafting/materials.d.ts +53 -0
- package/dist/src/crafting/materials.js +120 -0
- package/dist/src/crafting/recipes.d.ts +75 -0
- package/dist/src/crafting/recipes.js +233 -0
- package/dist/src/crafting/workshops.d.ts +61 -0
- package/dist/src/crafting/workshops.js +170 -0
- package/dist/src/debug.d.ts +86 -0
- package/dist/src/debug.js +76 -0
- package/dist/src/derive.d.ts +21 -0
- package/dist/src/derive.js +88 -0
- package/dist/src/describe.d.ts +29 -0
- package/dist/src/describe.js +276 -0
- package/dist/src/dialogue.d.ts +122 -0
- package/dist/src/dialogue.js +266 -0
- package/dist/src/dist.d.ts +20 -0
- package/dist/src/dist.js +39 -0
- package/dist/src/downtime.d.ts +89 -0
- package/dist/src/downtime.js +391 -0
- package/dist/src/economy.d.ts +116 -0
- package/dist/src/economy.js +182 -0
- package/dist/src/emotional-contagion.d.ts +142 -0
- package/dist/src/emotional-contagion.js +274 -0
- package/dist/src/equipment.d.ts +206 -0
- package/dist/src/equipment.js +598 -0
- package/dist/src/faction.d.ts +102 -0
- package/dist/src/faction.js +237 -0
- package/dist/src/generate.d.ts +35 -0
- package/dist/src/generate.js +166 -0
- package/dist/src/index.d.ts +42 -0
- package/dist/src/index.js +54 -0
- package/dist/src/inheritance.d.ts +69 -0
- package/dist/src/inheritance.js +136 -0
- package/dist/src/inventory.d.ts +194 -0
- package/dist/src/inventory.js +637 -0
- package/dist/src/item-durability.d.ts +69 -0
- package/dist/src/item-durability.js +308 -0
- package/dist/src/legend.d.ts +97 -0
- package/dist/src/legend.js +269 -0
- package/dist/src/lod.d.ts +9 -0
- package/dist/src/lod.js +84 -0
- package/dist/src/metrics.d.ts +51 -0
- package/dist/src/metrics.js +91 -0
- package/dist/src/model3d.d.ts +138 -0
- package/dist/src/model3d.js +214 -0
- package/dist/src/mythology.d.ts +101 -0
- package/dist/src/mythology.js +308 -0
- package/dist/src/narrative-render.d.ts +42 -0
- package/dist/src/narrative-render.js +194 -0
- package/dist/src/narrative-stress.d.ts +123 -0
- package/dist/src/narrative-stress.js +183 -0
- package/dist/src/narrative.d.ts +44 -0
- package/dist/src/narrative.js +257 -0
- package/dist/src/party.d.ts +70 -0
- package/dist/src/party.js +226 -0
- package/dist/src/polity.d.ts +262 -0
- package/dist/src/polity.js +398 -0
- package/dist/src/presets.d.ts +42 -0
- package/dist/src/presets.js +170 -0
- package/dist/src/progression.d.ts +170 -0
- package/dist/src/progression.js +256 -0
- package/dist/src/quest-generators.d.ts +76 -0
- package/dist/src/quest-generators.js +534 -0
- package/dist/src/quest.d.ts +239 -0
- package/dist/src/quest.js +520 -0
- package/dist/src/relationships-effects.d.ts +75 -0
- package/dist/src/relationships-effects.js +219 -0
- package/dist/src/relationships.d.ts +104 -0
- package/dist/src/relationships.js +347 -0
- package/dist/src/replay.d.ts +47 -0
- package/dist/src/replay.js +82 -0
- package/dist/src/rng.d.ts +9 -0
- package/dist/src/rng.js +37 -0
- package/dist/src/settlement-services.d.ts +67 -0
- package/dist/src/settlement-services.js +267 -0
- package/dist/src/settlement.d.ts +143 -0
- package/dist/src/settlement.js +419 -0
- package/dist/src/sim/action.d.ts +28 -0
- package/dist/src/sim/action.js +12 -0
- package/dist/src/sim/aging.d.ts +95 -0
- package/dist/src/sim/aging.js +243 -0
- package/dist/src/sim/ai/decide.d.ts +10 -0
- package/dist/src/sim/ai/decide.js +267 -0
- package/dist/src/sim/ai/perception.d.ts +12 -0
- package/dist/src/sim/ai/perception.js +54 -0
- package/dist/src/sim/ai/personality.d.ts +54 -0
- package/dist/src/sim/ai/personality.js +202 -0
- package/dist/src/sim/ai/presets.d.ts +2 -0
- package/dist/src/sim/ai/presets.js +28 -0
- package/dist/src/sim/ai/system.d.ts +6 -0
- package/dist/src/sim/ai/system.js +13 -0
- package/dist/src/sim/ai/targeting.d.ts +8 -0
- package/dist/src/sim/ai/targeting.js +42 -0
- package/dist/src/sim/ai/types.d.ts +14 -0
- package/dist/src/sim/ai/types.js +1 -0
- package/dist/src/sim/body.d.ts +9 -0
- package/dist/src/sim/body.js +32 -0
- package/dist/src/sim/bodyplan.d.ts +161 -0
- package/dist/src/sim/bodyplan.js +677 -0
- package/dist/src/sim/capability.d.ts +135 -0
- package/dist/src/sim/capability.js +8 -0
- package/dist/src/sim/combat.d.ts +21 -0
- package/dist/src/sim/combat.js +77 -0
- package/dist/src/sim/commandBuilders.d.ts +11 -0
- package/dist/src/sim/commandBuilders.js +39 -0
- package/dist/src/sim/commands.d.ts +71 -0
- package/dist/src/sim/commands.js +8 -0
- package/dist/src/sim/condition.d.ts +35 -0
- package/dist/src/sim/condition.js +21 -0
- package/dist/src/sim/cone.d.ts +40 -0
- package/dist/src/sim/cone.js +44 -0
- package/dist/src/sim/context.d.ts +68 -0
- package/dist/src/sim/context.js +1 -0
- package/dist/src/sim/density.d.ts +14 -0
- package/dist/src/sim/density.js +33 -0
- package/dist/src/sim/disease.d.ts +141 -0
- package/dist/src/sim/disease.js +353 -0
- package/dist/src/sim/entity.d.ts +251 -0
- package/dist/src/sim/entity.js +19 -0
- package/dist/src/sim/events.d.ts +25 -0
- package/dist/src/sim/events.js +5 -0
- package/dist/src/sim/explosion.d.ts +40 -0
- package/dist/src/sim/explosion.js +40 -0
- package/dist/src/sim/formation-unit.d.ts +138 -0
- package/dist/src/sim/formation-unit.js +197 -0
- package/dist/src/sim/formation.d.ts +12 -0
- package/dist/src/sim/formation.js +54 -0
- package/dist/src/sim/frontage.d.ts +30 -0
- package/dist/src/sim/frontage.js +84 -0
- package/dist/src/sim/grapple.d.ts +100 -0
- package/dist/src/sim/grapple.js +480 -0
- package/dist/src/sim/hazard.d.ts +104 -0
- package/dist/src/sim/hazard.js +201 -0
- package/dist/src/sim/hydrostatic.d.ts +58 -0
- package/dist/src/sim/hydrostatic.js +117 -0
- package/dist/src/sim/impairment.d.ts +20 -0
- package/dist/src/sim/impairment.js +162 -0
- package/dist/src/sim/indexing.d.ts +7 -0
- package/dist/src/sim/indexing.js +7 -0
- package/dist/src/sim/injury.d.ts +54 -0
- package/dist/src/sim/injury.js +66 -0
- package/dist/src/sim/intent.d.ts +26 -0
- package/dist/src/sim/intent.js +7 -0
- package/dist/src/sim/kernel.d.ts +45 -0
- package/dist/src/sim/kernel.js +1992 -0
- package/dist/src/sim/kinds.d.ts +64 -0
- package/dist/src/sim/kinds.js +56 -0
- package/dist/src/sim/knockback.d.ts +50 -0
- package/dist/src/sim/knockback.js +82 -0
- package/dist/src/sim/limb.d.ts +48 -0
- package/dist/src/sim/limb.js +78 -0
- package/dist/src/sim/medical.d.ts +32 -0
- package/dist/src/sim/medical.js +33 -0
- package/dist/src/sim/morale.d.ts +69 -0
- package/dist/src/sim/morale.js +92 -0
- package/dist/src/sim/mount.d.ts +150 -0
- package/dist/src/sim/mount.js +225 -0
- package/dist/src/sim/nutrition.d.ts +74 -0
- package/dist/src/sim/nutrition.js +168 -0
- package/dist/src/sim/occlusion.d.ts +8 -0
- package/dist/src/sim/occlusion.js +71 -0
- package/dist/src/sim/push.d.ts +11 -0
- package/dist/src/sim/push.js +79 -0
- package/dist/src/sim/ranged.d.ts +44 -0
- package/dist/src/sim/ranged.js +69 -0
- package/dist/src/sim/seeds.d.ts +3 -0
- package/dist/src/sim/seeds.js +16 -0
- package/dist/src/sim/sensory-extended.d.ts +103 -0
- package/dist/src/sim/sensory-extended.js +181 -0
- package/dist/src/sim/sensory.d.ts +38 -0
- package/dist/src/sim/sensory.js +109 -0
- package/dist/src/sim/skills.d.ts +70 -0
- package/dist/src/sim/skills.js +69 -0
- package/dist/src/sim/sleep.d.ts +107 -0
- package/dist/src/sim/sleep.js +215 -0
- package/dist/src/sim/spatial.d.ts +8 -0
- package/dist/src/sim/spatial.js +59 -0
- package/dist/src/sim/step/capability.d.ts +8 -0
- package/dist/src/sim/step/capability.js +77 -0
- package/dist/src/sim/step/concentration.d.ts +9 -0
- package/dist/src/sim/step/concentration.js +25 -0
- package/dist/src/sim/step/effects.d.ts +17 -0
- package/dist/src/sim/step/effects.js +96 -0
- package/dist/src/sim/step/energy.d.ts +3 -0
- package/dist/src/sim/step/energy.js +31 -0
- package/dist/src/sim/step/hazards.d.ts +4 -0
- package/dist/src/sim/step/hazards.js +19 -0
- package/dist/src/sim/step/injury.d.ts +10 -0
- package/dist/src/sim/step/injury.js +353 -0
- package/dist/src/sim/step/morale.d.ts +11 -0
- package/dist/src/sim/step/morale.js +130 -0
- package/dist/src/sim/step/movement.d.ts +5 -0
- package/dist/src/sim/step/movement.js +172 -0
- package/dist/src/sim/step/push.d.ts +11 -0
- package/dist/src/sim/step/push.js +79 -0
- package/dist/src/sim/step/substances.d.ts +3 -0
- package/dist/src/sim/step/substances.js +75 -0
- package/dist/src/sim/substance.d.ts +38 -0
- package/dist/src/sim/substance.js +57 -0
- package/dist/src/sim/systemic-toxicology.d.ts +109 -0
- package/dist/src/sim/systemic-toxicology.js +263 -0
- package/dist/src/sim/team.d.ts +9 -0
- package/dist/src/sim/team.js +37 -0
- package/dist/src/sim/tech.d.ts +36 -0
- package/dist/src/sim/tech.js +46 -0
- package/dist/src/sim/terrain.d.ts +121 -0
- package/dist/src/sim/terrain.js +141 -0
- package/dist/src/sim/testing.d.ts +13 -0
- package/dist/src/sim/testing.js +100 -0
- package/dist/src/sim/thermoregulation.d.ts +77 -0
- package/dist/src/sim/thermoregulation.js +161 -0
- package/dist/src/sim/tick.d.ts +3 -0
- package/dist/src/sim/tick.js +3 -0
- package/dist/src/sim/toxicology.d.ts +52 -0
- package/dist/src/sim/toxicology.js +104 -0
- package/dist/src/sim/trace.d.ts +141 -0
- package/dist/src/sim/trace.js +1 -0
- package/dist/src/sim/tuning.d.ts +16 -0
- package/dist/src/sim/tuning.js +42 -0
- package/dist/src/sim/vec3.d.ts +14 -0
- package/dist/src/sim/vec3.js +31 -0
- package/dist/src/sim/weapon_dynamics.d.ts +102 -0
- package/dist/src/sim/weapon_dynamics.js +142 -0
- package/dist/src/sim/weather.d.ts +95 -0
- package/dist/src/sim/weather.js +105 -0
- package/dist/src/sim/world.d.ts +52 -0
- package/dist/src/sim/world.js +1 -0
- package/dist/src/sim/wound-aging.d.ts +120 -0
- package/dist/src/sim/wound-aging.js +223 -0
- package/dist/src/species.d.ts +106 -0
- package/dist/src/species.js +664 -0
- package/dist/src/story-arcs.d.ts +17 -0
- package/dist/src/story-arcs.js +276 -0
- package/dist/src/tech-diffusion.d.ts +80 -0
- package/dist/src/tech-diffusion.js +185 -0
- package/dist/src/traits.d.ts +25 -0
- package/dist/src/traits.js +178 -0
- package/dist/src/types.d.ts +117 -0
- package/dist/src/types.js +1 -0
- package/dist/src/units.d.ts +41 -0
- package/dist/src/units.js +64 -0
- package/dist/src/weapons.d.ts +20 -0
- package/dist/src/weapons.js +824 -0
- package/dist/src/world-generation.d.ts +52 -0
- package/dist/src/world-generation.js +301 -0
- package/package.json +74 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// src/relationships-effects.ts — Phase 42: Relationship Effects Integration
|
|
2
|
+
//
|
|
3
|
+
// Integration of relationship graph with morale, teaching, and combat systems.
|
|
4
|
+
import { q, SCALE } from "./units.js";
|
|
5
|
+
import { getRelationship, recordRelationshipEvent, recordBetrayal, computeTeachingRelationshipMultiplier, } from "./relationships.js";
|
|
6
|
+
/**
|
|
7
|
+
* Compute morale impacts for all observers of a combat event.
|
|
8
|
+
*/
|
|
9
|
+
export function computeCombatMoraleImpacts(graph, world, impact) {
|
|
10
|
+
const impacts = [];
|
|
11
|
+
// Get all potential observers (simplified: all entities in world)
|
|
12
|
+
// In practice, this would use spatial queries for entities within perception range
|
|
13
|
+
const observers = world.entities;
|
|
14
|
+
for (const observer of observers) {
|
|
15
|
+
if (observer.id === impact.attackerId || observer.id === impact.targetId)
|
|
16
|
+
continue;
|
|
17
|
+
// Check relationship with victim
|
|
18
|
+
const rVictim = getRelationship(graph, observer.id, impact.targetId);
|
|
19
|
+
if (rVictim && rVictim.affinity_Q > q(0.3)) {
|
|
20
|
+
// Friend was hit
|
|
21
|
+
const severity = impact.energy_J > 500 ? "friend_killed" : "friend_injured";
|
|
22
|
+
impacts.push({
|
|
23
|
+
observerId: observer.id,
|
|
24
|
+
targetId: impact.targetId,
|
|
25
|
+
event: severity,
|
|
26
|
+
delta_Q: Math.round(-rVictim.affinity_Q * (severity === "friend_killed" ? 0.5 : 0.2)),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
// Check relationship with attacker
|
|
30
|
+
const rAttacker = getRelationship(graph, observer.id, impact.attackerId);
|
|
31
|
+
if (rAttacker && rAttacker.affinity_Q < -q(0.3)) {
|
|
32
|
+
// Enemy succeeded in hitting someone
|
|
33
|
+
impacts.push({
|
|
34
|
+
observerId: observer.id,
|
|
35
|
+
targetId: impact.attackerId,
|
|
36
|
+
event: "enemy_defeated",
|
|
37
|
+
delta_Q: Math.round(rAttacker.affinity_Q * 0.1), // Negative = bad for morale
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return impacts;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Compute morale impact from betrayal detection.
|
|
45
|
+
*/
|
|
46
|
+
export function computeBetrayalMoraleImpacts(graph, world, attackerId, victimId, tick) {
|
|
47
|
+
const impacts = [];
|
|
48
|
+
// Record betrayal in relationship graph
|
|
49
|
+
const betrayalResult = recordBetrayal(graph, attackerId, victimId, tick);
|
|
50
|
+
if (!betrayalResult.isBetrayal)
|
|
51
|
+
return impacts;
|
|
52
|
+
// Find all witnesses who care about the victim
|
|
53
|
+
const witnesses = world.entities.filter((e) => e.id !== attackerId && e.id !== victimId);
|
|
54
|
+
for (const witness of witnesses) {
|
|
55
|
+
const r = getRelationship(graph, witness.id, victimId);
|
|
56
|
+
if (r && r.affinity_Q > q(0.2)) {
|
|
57
|
+
impacts.push({
|
|
58
|
+
observerId: witness.id,
|
|
59
|
+
targetId: victimId,
|
|
60
|
+
event: "betrayal",
|
|
61
|
+
delta_Q: betrayalResult.witnessMoralePenalty_Q,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return impacts;
|
|
66
|
+
}
|
|
67
|
+
// ── Teaching Integration ──────────────────────────────────────────────────────
|
|
68
|
+
/**
|
|
69
|
+
* Compute total teaching effectiveness multiplier.
|
|
70
|
+
* Combines base skill with relationship factors.
|
|
71
|
+
*/
|
|
72
|
+
export function computeTeachingEffectiveness(graph, teacherId, learnerId, baseEffectiveness) {
|
|
73
|
+
const relationshipMul = computeTeachingRelationshipMultiplier(graph, teacherId, learnerId);
|
|
74
|
+
return baseEffectiveness * relationshipMul;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Compute combat decision factors based on relationship.
|
|
78
|
+
*/
|
|
79
|
+
export function getCombatDecisionFactors(graph, entityId, targetId) {
|
|
80
|
+
const r = getRelationship(graph, entityId, targetId);
|
|
81
|
+
if (!r) {
|
|
82
|
+
return {
|
|
83
|
+
willProtect: false,
|
|
84
|
+
willAvoidHarm: false,
|
|
85
|
+
aggressionModifier: 0,
|
|
86
|
+
coordinationBonus: 0,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const affinityNorm = r.affinity_Q / SCALE.Q; // -1 to 1
|
|
90
|
+
const trustNorm = r.trust_Q / SCALE.Q; // 0 to 1
|
|
91
|
+
return {
|
|
92
|
+
willProtect: r.affinity_Q > q(0.5) && r.trust_Q > q(0.3),
|
|
93
|
+
willAvoidHarm: r.affinity_Q > q(0.2),
|
|
94
|
+
aggressionModifier: -affinityNorm * 0.5, // Negative affinity = more aggressive
|
|
95
|
+
coordinationBonus: r.bond === "mentor" || r.bond === "student"
|
|
96
|
+
? 0.2
|
|
97
|
+
: trustNorm * 0.1,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Determine if entity should switch targets to protect an ally.
|
|
102
|
+
*/
|
|
103
|
+
export function shouldProtectAlly(graph, protectorId, allyId, threatId) {
|
|
104
|
+
const rAlly = getRelationship(graph, protectorId, allyId);
|
|
105
|
+
const rThreat = getRelationship(graph, protectorId, threatId);
|
|
106
|
+
if (!rAlly)
|
|
107
|
+
return false;
|
|
108
|
+
// Will protect if ally is friend and threat is not a closer friend
|
|
109
|
+
const allyValue = rAlly.affinity_Q + rAlly.trust_Q;
|
|
110
|
+
const threatValue = rThreat
|
|
111
|
+
? rThreat.affinity_Q + rThreat.trust_Q
|
|
112
|
+
: -SCALE.Q; // Unknown = neutral/negative
|
|
113
|
+
return allyValue > q(0.5) && allyValue > threatValue;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Compute what dialogue options are available.
|
|
117
|
+
*/
|
|
118
|
+
export function getDialogueAvailability(graph, speakerId, listenerId) {
|
|
119
|
+
const r = getRelationship(graph, speakerId, listenerId);
|
|
120
|
+
if (!r) {
|
|
121
|
+
return {
|
|
122
|
+
canAskFavors: false,
|
|
123
|
+
canNegotiate: true,
|
|
124
|
+
canIntimidate: false,
|
|
125
|
+
persuasionBonus: 0,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const affinityNorm = r.affinity_Q / SCALE.Q;
|
|
129
|
+
const trustNorm = r.trust_Q / SCALE.Q;
|
|
130
|
+
return {
|
|
131
|
+
canAskFavors: r.affinity_Q > q(0.4) && r.trust_Q > q(0.3),
|
|
132
|
+
canNegotiate: r.affinity_Q > -q(0.3), // Can negotiate unless enemies
|
|
133
|
+
canIntimidate: r.trust_Q < q(0.3) || r.affinity_Q < -q(0.2),
|
|
134
|
+
persuasionBonus: affinityNorm * 0.2 + trustNorm * 0.1,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
// ── Event Recording ───────────────────────────────────────────────────────────
|
|
138
|
+
/**
|
|
139
|
+
* Record events from combat resolution.
|
|
140
|
+
*/
|
|
141
|
+
export function recordCombatOutcome(graph, impact, outcome, tick) {
|
|
142
|
+
if (outcome === "hit") {
|
|
143
|
+
// If significant damage, record as negative event for relationship
|
|
144
|
+
if (impact.energy_J > 100) {
|
|
145
|
+
recordRelationshipEvent(graph, impact.attackerId, impact.targetId, {
|
|
146
|
+
tick,
|
|
147
|
+
type: "insult", // Attacking is insulting
|
|
148
|
+
magnitude_Q: Math.min(q(0.1), Math.round(impact.energy_J / 1000)),
|
|
149
|
+
description: `Attacked in combat (${outcome})`,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Record cooperation between entities.
|
|
156
|
+
*/
|
|
157
|
+
export function recordCooperation(graph, entityA, entityB, success, tick) {
|
|
158
|
+
recordRelationshipEvent(graph, entityA, entityB, {
|
|
159
|
+
tick,
|
|
160
|
+
type: "fought_alongside",
|
|
161
|
+
magnitude_Q: success ? q(0.15) : q(0.05),
|
|
162
|
+
description: success ? "Successful cooperation" : "Attempted cooperation",
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Record that one entity saved another.
|
|
167
|
+
*/
|
|
168
|
+
export function recordRescue(graph, rescuerId, rescuedId, tick) {
|
|
169
|
+
recordRelationshipEvent(graph, rescuerId, rescuedId, {
|
|
170
|
+
tick,
|
|
171
|
+
type: "saved",
|
|
172
|
+
magnitude_Q: q(0.4),
|
|
173
|
+
description: "Saved from danger",
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
// ── Group Formation ────────────────────────────────────────────────────────────
|
|
177
|
+
/** Find entities that form a cohesive group based on relationships. */
|
|
178
|
+
export function findCohesiveGroup(graph, seedEntityId, minAffinity_Q = q(0.2)) {
|
|
179
|
+
const group = new Set([seedEntityId]);
|
|
180
|
+
const toCheck = [seedEntityId];
|
|
181
|
+
while (toCheck.length > 0) {
|
|
182
|
+
const current = toCheck.pop();
|
|
183
|
+
const relationships = Array.from(graph.relationships.values()).filter((r) => (r.entityA === current || r.entityB === current) && r.affinity_Q >= minAffinity_Q);
|
|
184
|
+
for (const r of relationships) {
|
|
185
|
+
const other = r.entityA === current ? r.entityB : r.entityA;
|
|
186
|
+
if (!group.has(other)) {
|
|
187
|
+
group.add(other);
|
|
188
|
+
toCheck.push(other);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return Array.from(group);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Check if a group of entities can work together effectively.
|
|
196
|
+
*/
|
|
197
|
+
export function computeGroupCohesion(graph, groupIds) {
|
|
198
|
+
if (groupIds.length < 2)
|
|
199
|
+
return { cohesion_Q: q(0.5), trust_Q: q(0.5) };
|
|
200
|
+
let totalAffinity = 0;
|
|
201
|
+
let totalTrust = 0;
|
|
202
|
+
let pairCount = 0;
|
|
203
|
+
for (let i = 0; i < groupIds.length; i++) {
|
|
204
|
+
for (let j = i + 1; j < groupIds.length; j++) {
|
|
205
|
+
const rel = getRelationship(graph, groupIds[i], groupIds[j]);
|
|
206
|
+
if (rel) {
|
|
207
|
+
totalAffinity += rel.affinity_Q;
|
|
208
|
+
totalTrust += rel.trust_Q;
|
|
209
|
+
pairCount++;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (pairCount === 0)
|
|
214
|
+
return { cohesion_Q: q(0.5), trust_Q: q(0.5) };
|
|
215
|
+
return {
|
|
216
|
+
cohesion_Q: Math.round(totalAffinity / pairCount),
|
|
217
|
+
trust_Q: Math.round(totalTrust / pairCount),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { Q } from "./units.js";
|
|
2
|
+
/** Types of events that can affect relationships. */
|
|
3
|
+
export type RelationshipEventType = "met" | "fought_alongside" | "betrayed" | "saved" | "deceived" | "gift_given" | "insult" | "bonded" | "separated";
|
|
4
|
+
/** A single event in the relationship history. */
|
|
5
|
+
export interface RelationshipEvent {
|
|
6
|
+
tick: number;
|
|
7
|
+
type: RelationshipEventType;
|
|
8
|
+
magnitude_Q: Q;
|
|
9
|
+
description?: string;
|
|
10
|
+
}
|
|
11
|
+
/** The nature of the social bond between two entities. */
|
|
12
|
+
export type SocialBond = "none" | "acquaintance" | "friend" | "close_friend" | "rival" | "enemy" | "mentor" | "student" | "family" | "romantic_partner";
|
|
13
|
+
/** Relationship between two entities. */
|
|
14
|
+
export interface Relationship {
|
|
15
|
+
entityA: number;
|
|
16
|
+
entityB: number;
|
|
17
|
+
/** Affinity: -1.0 (hatred) to +1.0 (love) */
|
|
18
|
+
affinity_Q: Q;
|
|
19
|
+
/** Trust: 0.0 (none) to 1.0 (absolute) */
|
|
20
|
+
trust_Q: Q;
|
|
21
|
+
/** Current social bond classification */
|
|
22
|
+
bond: SocialBond;
|
|
23
|
+
/** Chronicle of interactions */
|
|
24
|
+
history: RelationshipEvent[];
|
|
25
|
+
/** Tick when relationship was established */
|
|
26
|
+
establishedAtTick: number;
|
|
27
|
+
/** Last interaction tick */
|
|
28
|
+
lastInteractionTick: number;
|
|
29
|
+
}
|
|
30
|
+
/** Global relationship graph storage. */
|
|
31
|
+
export interface RelationshipGraph {
|
|
32
|
+
relationships: Map<string, Relationship>;
|
|
33
|
+
/** Entity-specific indices for quick lookup */
|
|
34
|
+
entityIndex: Map<number, Set<string>>;
|
|
35
|
+
}
|
|
36
|
+
/** Create a new empty relationship graph. */
|
|
37
|
+
export declare function createRelationshipGraph(): RelationshipGraph;
|
|
38
|
+
/**
|
|
39
|
+
* Get relationship between two entities.
|
|
40
|
+
* Returns undefined if no relationship exists.
|
|
41
|
+
*/
|
|
42
|
+
export declare function getRelationship(graph: RelationshipGraph, entityA: number, entityB: number): Relationship | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Check if two entities have an existing relationship.
|
|
45
|
+
*/
|
|
46
|
+
export declare function hasRelationship(graph: RelationshipGraph, entityA: number, entityB: number): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Get all relationships for an entity.
|
|
49
|
+
*/
|
|
50
|
+
export declare function getEntityRelationshipsList(graph: RelationshipGraph, entityId: number): Relationship[];
|
|
51
|
+
/**
|
|
52
|
+
* Get all entities related to a given entity.
|
|
53
|
+
*/
|
|
54
|
+
export declare function getRelatedEntities(graph: RelationshipGraph, entityId: number): number[];
|
|
55
|
+
/**
|
|
56
|
+
* Establish a new relationship between two entities.
|
|
57
|
+
*/
|
|
58
|
+
export declare function establishRelationship(graph: RelationshipGraph, entityA: number, entityB: number, tick: number, initialAffinity_Q?: Q, initialTrust_Q?: Q): Relationship;
|
|
59
|
+
/**
|
|
60
|
+
* Record a relationship event and update affinity/trust.
|
|
61
|
+
*/
|
|
62
|
+
export declare function recordRelationshipEvent(graph: RelationshipGraph, entityA: number, entityB: number, event: Omit<RelationshipEvent, "tick"> & {
|
|
63
|
+
tick: number;
|
|
64
|
+
}): Relationship | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* Classify the social bond based on affinity, trust, and history.
|
|
67
|
+
*/
|
|
68
|
+
export declare function classifyBond(affinity_Q: Q, trust_Q: Q, history: RelationshipEvent[]): SocialBond;
|
|
69
|
+
/**
|
|
70
|
+
* Apply time-based decay to relationships.
|
|
71
|
+
* Call periodically (e.g., daily or weekly in simulation time).
|
|
72
|
+
*/
|
|
73
|
+
export declare function decayRelationships(graph: RelationshipGraph, currentTick: number, decayRatePerTick?: number): void;
|
|
74
|
+
/** Check if entity A would consider entity B a friend. */
|
|
75
|
+
export declare function isFriend(graph: RelationshipGraph, entityA: number, entityB: number): boolean;
|
|
76
|
+
/** Check if entity A would consider entity B an enemy. */
|
|
77
|
+
export declare function isEnemy(graph: RelationshipGraph, entityA: number, entityB: number): boolean;
|
|
78
|
+
/** Check if entity A trusts entity B enough for combat cooperation. */
|
|
79
|
+
export declare function hasCombatTrust(graph: RelationshipGraph, entityA: number, entityB: number): boolean;
|
|
80
|
+
/** Get the effective relationship modifier for morale effects. */
|
|
81
|
+
export declare function getMoraleModifier(graph: RelationshipGraph, observer: number, target: number): Q;
|
|
82
|
+
/** Result of betrayal check. */
|
|
83
|
+
export interface BetrayalResult {
|
|
84
|
+
isBetrayal: boolean;
|
|
85
|
+
severity_Q: Q;
|
|
86
|
+
/** Morale penalty for witnesses who care about the victim */
|
|
87
|
+
witnessMoralePenalty_Q: Q;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Check if harming someone constitutes betrayal.
|
|
91
|
+
*/
|
|
92
|
+
export declare function checkBetrayal(graph: RelationshipGraph, attackerId: number, victimId: number): BetrayalResult;
|
|
93
|
+
/**
|
|
94
|
+
* Record a betrayal event and update relationships.
|
|
95
|
+
*/
|
|
96
|
+
export declare function recordBetrayal(graph: RelationshipGraph, attackerId: number, victimId: number, tick: number): BetrayalResult;
|
|
97
|
+
/**
|
|
98
|
+
* Compute teaching effectiveness multiplier based on relationship.
|
|
99
|
+
*/
|
|
100
|
+
export declare function computeTeachingRelationshipMultiplier(graph: RelationshipGraph, teacherId: number, learnerId: number): number;
|
|
101
|
+
/** Serialize relationship graph to JSON-friendly format. */
|
|
102
|
+
export declare function serializeRelationshipGraph(graph: RelationshipGraph): unknown;
|
|
103
|
+
/** Deserialize relationship graph. */
|
|
104
|
+
export declare function deserializeRelationshipGraph(data: unknown): RelationshipGraph;
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
// src/relationships.ts — Phase 42: Personal Relationship Graph
|
|
2
|
+
//
|
|
3
|
+
// Individual-to-individual relationships — the social fabric that makes RPGs feel alive.
|
|
4
|
+
// Affects morale, teaching effectiveness, betrayal probability, and dialogue options.
|
|
5
|
+
import { SCALE, q, clampQ } from "./units.js";
|
|
6
|
+
/** Key for storing relationships (ordered pair). */
|
|
7
|
+
function relationshipKey(entityA, entityB) {
|
|
8
|
+
// Always store with smaller ID first for consistency
|
|
9
|
+
return entityA < entityB ? `${entityA}:${entityB}` : `${entityB}:${entityA}`;
|
|
10
|
+
}
|
|
11
|
+
/** Create a new empty relationship graph. */
|
|
12
|
+
export function createRelationshipGraph() {
|
|
13
|
+
return {
|
|
14
|
+
relationships: new Map(),
|
|
15
|
+
entityIndex: new Map(),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** Get or create entity index entry. */
|
|
19
|
+
function getEntityRelationships(graph, entityId) {
|
|
20
|
+
let set = graph.entityIndex.get(entityId);
|
|
21
|
+
if (!set) {
|
|
22
|
+
set = new Set();
|
|
23
|
+
graph.entityIndex.set(entityId, set);
|
|
24
|
+
}
|
|
25
|
+
return set;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Get relationship between two entities.
|
|
29
|
+
* Returns undefined if no relationship exists.
|
|
30
|
+
*/
|
|
31
|
+
export function getRelationship(graph, entityA, entityB) {
|
|
32
|
+
if (entityA === entityB)
|
|
33
|
+
return undefined;
|
|
34
|
+
const key = relationshipKey(entityA, entityB);
|
|
35
|
+
return graph.relationships.get(key);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Check if two entities have an existing relationship.
|
|
39
|
+
*/
|
|
40
|
+
export function hasRelationship(graph, entityA, entityB) {
|
|
41
|
+
return getRelationship(graph, entityA, entityB) !== undefined;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get all relationships for an entity.
|
|
45
|
+
*/
|
|
46
|
+
export function getEntityRelationshipsList(graph, entityId) {
|
|
47
|
+
const keys = graph.entityIndex.get(entityId);
|
|
48
|
+
if (!keys)
|
|
49
|
+
return [];
|
|
50
|
+
return Array.from(keys)
|
|
51
|
+
.map((key) => graph.relationships.get(key))
|
|
52
|
+
.filter((r) => r !== undefined);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Get all entities related to a given entity.
|
|
56
|
+
*/
|
|
57
|
+
export function getRelatedEntities(graph, entityId) {
|
|
58
|
+
const relationships = getEntityRelationshipsList(graph, entityId);
|
|
59
|
+
return relationships.map((r) => (r.entityA === entityId ? r.entityB : r.entityA));
|
|
60
|
+
}
|
|
61
|
+
// ── Relationship Creation & Modification ──────────────────────────────────────
|
|
62
|
+
/**
|
|
63
|
+
* Establish a new relationship between two entities.
|
|
64
|
+
*/
|
|
65
|
+
export function establishRelationship(graph, entityA, entityB, tick, initialAffinity_Q = q(0), initialTrust_Q = q(0.1)) {
|
|
66
|
+
if (entityA === entityB) {
|
|
67
|
+
throw new Error("Cannot establish relationship with self");
|
|
68
|
+
}
|
|
69
|
+
const key = relationshipKey(entityA, entityB);
|
|
70
|
+
// Check if relationship already exists
|
|
71
|
+
const existing = graph.relationships.get(key);
|
|
72
|
+
if (existing)
|
|
73
|
+
return existing;
|
|
74
|
+
const affinity_Q = clampQ(initialAffinity_Q, -SCALE.Q, SCALE.Q);
|
|
75
|
+
const trust_Q = clampQ(initialTrust_Q, q(0), SCALE.Q);
|
|
76
|
+
const relationship = {
|
|
77
|
+
entityA,
|
|
78
|
+
entityB,
|
|
79
|
+
affinity_Q,
|
|
80
|
+
trust_Q,
|
|
81
|
+
bond: classifyBond(affinity_Q, trust_Q, []),
|
|
82
|
+
history: [],
|
|
83
|
+
establishedAtTick: tick,
|
|
84
|
+
lastInteractionTick: tick,
|
|
85
|
+
};
|
|
86
|
+
graph.relationships.set(key, relationship);
|
|
87
|
+
getEntityRelationships(graph, entityA).add(key);
|
|
88
|
+
getEntityRelationships(graph, entityB).add(key);
|
|
89
|
+
return relationship;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Record a relationship event and update affinity/trust.
|
|
93
|
+
*/
|
|
94
|
+
export function recordRelationshipEvent(graph, entityA, entityB, event) {
|
|
95
|
+
if (entityA === entityB)
|
|
96
|
+
return undefined;
|
|
97
|
+
const key = relationshipKey(entityA, entityB);
|
|
98
|
+
let relationship = graph.relationships.get(key);
|
|
99
|
+
// Auto-establish relationship if it doesn't exist
|
|
100
|
+
if (!relationship) {
|
|
101
|
+
relationship = establishRelationship(graph, entityA, entityB, event.tick);
|
|
102
|
+
}
|
|
103
|
+
// Add event to history
|
|
104
|
+
const evt = {
|
|
105
|
+
tick: event.tick,
|
|
106
|
+
type: event.type,
|
|
107
|
+
magnitude_Q: event.magnitude_Q,
|
|
108
|
+
};
|
|
109
|
+
if (event.description !== undefined)
|
|
110
|
+
evt.description = event.description;
|
|
111
|
+
relationship.history.push(evt);
|
|
112
|
+
// Update affinity and trust based on event type
|
|
113
|
+
const { affinityDelta, trustDelta } = computeDeltasFromEvent(event.type, event.magnitude_Q);
|
|
114
|
+
relationship.affinity_Q = clampQ((relationship.affinity_Q + affinityDelta), -SCALE.Q, SCALE.Q);
|
|
115
|
+
relationship.trust_Q = clampQ((relationship.trust_Q + trustDelta), q(0), SCALE.Q);
|
|
116
|
+
relationship.lastInteractionTick = event.tick;
|
|
117
|
+
// Reclassify bond
|
|
118
|
+
relationship.bond = classifyBond(relationship.affinity_Q, relationship.trust_Q, relationship.history);
|
|
119
|
+
return relationship;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Compute affinity and trust deltas from event type.
|
|
123
|
+
*/
|
|
124
|
+
function computeDeltasFromEvent(type, magnitude_Q) {
|
|
125
|
+
// Scale: magnitude_Q (0-10000) * factor -> delta_Q
|
|
126
|
+
// Multipliers are scaled to produce meaningful changes (0.5-1.0 range possible)
|
|
127
|
+
const mul = (n) => Math.round(magnitude_Q * n);
|
|
128
|
+
switch (type) {
|
|
129
|
+
case "met":
|
|
130
|
+
return { affinityDelta: mul(0.1), trustDelta: mul(0.05) };
|
|
131
|
+
case "fought_alongside":
|
|
132
|
+
return { affinityDelta: mul(0.3), trustDelta: mul(0.6) };
|
|
133
|
+
case "saved":
|
|
134
|
+
return { affinityDelta: mul(0.8), trustDelta: mul(0.7) };
|
|
135
|
+
case "betrayed":
|
|
136
|
+
// Severe penalty - can swing positive to negative
|
|
137
|
+
return { affinityDelta: mul(-1.8), trustDelta: mul(-2.0) };
|
|
138
|
+
case "deceived":
|
|
139
|
+
return { affinityDelta: mul(-0.6), trustDelta: mul(-1.0) };
|
|
140
|
+
case "gift_given":
|
|
141
|
+
return { affinityDelta: mul(0.2), trustDelta: mul(0.1) };
|
|
142
|
+
case "insult":
|
|
143
|
+
return { affinityDelta: mul(-0.3), trustDelta: mul(-0.1) };
|
|
144
|
+
case "bonded":
|
|
145
|
+
return { affinityDelta: mul(0.6), trustDelta: mul(0.5) };
|
|
146
|
+
case "separated":
|
|
147
|
+
// Gradual decay - handled by decay function instead
|
|
148
|
+
return { affinityDelta: q(0), trustDelta: q(0) };
|
|
149
|
+
default:
|
|
150
|
+
return { affinityDelta: q(0), trustDelta: q(0) };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Classify the social bond based on affinity, trust, and history.
|
|
155
|
+
*/
|
|
156
|
+
export function classifyBond(affinity_Q, trust_Q, history) {
|
|
157
|
+
// Check for negative bonds first
|
|
158
|
+
if (affinity_Q < -q(0.6) && trust_Q < q(0.3))
|
|
159
|
+
return "enemy";
|
|
160
|
+
if (affinity_Q < -q(0.3) && affinity_Q >= -q(0.6))
|
|
161
|
+
return "rival";
|
|
162
|
+
// Check for special bonds in history
|
|
163
|
+
const hasMentorEvent = history.some((e) => e.type === "bonded" && e.magnitude_Q > q(0.7));
|
|
164
|
+
if (hasMentorEvent) {
|
|
165
|
+
return affinity_Q > q(0.5) ? "mentor" : "student";
|
|
166
|
+
}
|
|
167
|
+
// Check for romantic bond
|
|
168
|
+
const romanticEvent = history.some((e) => e.type === "bonded" && e.description?.includes("romantic"));
|
|
169
|
+
if (romanticEvent && affinity_Q > q(0.7) && trust_Q > q(0.6))
|
|
170
|
+
return "romantic_partner";
|
|
171
|
+
// Check for family
|
|
172
|
+
const familyEvent = history.some((e) => e.description?.includes("family"));
|
|
173
|
+
if (familyEvent)
|
|
174
|
+
return "family";
|
|
175
|
+
// Standard positive bonds
|
|
176
|
+
if (affinity_Q >= q(0.7) && trust_Q >= q(0.5))
|
|
177
|
+
return "close_friend";
|
|
178
|
+
if (affinity_Q >= q(0.3))
|
|
179
|
+
return "friend";
|
|
180
|
+
// Acquaintance covers neutral to slightly positive, and slightly negative
|
|
181
|
+
if (affinity_Q >= -q(0.3))
|
|
182
|
+
return "acquaintance";
|
|
183
|
+
return "none";
|
|
184
|
+
}
|
|
185
|
+
// ── Relationship Decay ────────────────────────────────────────────────────────
|
|
186
|
+
/**
|
|
187
|
+
* Apply time-based decay to relationships.
|
|
188
|
+
* Call periodically (e.g., daily or weekly in simulation time).
|
|
189
|
+
*/
|
|
190
|
+
export function decayRelationships(graph, currentTick, decayRatePerTick = 0.0001) {
|
|
191
|
+
for (const relationship of graph.relationships.values()) {
|
|
192
|
+
const timeSinceInteraction = currentTick - relationship.lastInteractionTick;
|
|
193
|
+
if (timeSinceInteraction > 1000) {
|
|
194
|
+
// Start decay after 1000 ticks of no interaction
|
|
195
|
+
const decayAmount = Math.round(timeSinceInteraction * decayRatePerTick * SCALE.Q);
|
|
196
|
+
// Decay affinity toward neutral (0)
|
|
197
|
+
if (relationship.affinity_Q > 0) {
|
|
198
|
+
relationship.affinity_Q = Math.max(0, relationship.affinity_Q - decayAmount);
|
|
199
|
+
}
|
|
200
|
+
else if (relationship.affinity_Q < 0) {
|
|
201
|
+
relationship.affinity_Q = Math.min(0, relationship.affinity_Q + decayAmount);
|
|
202
|
+
}
|
|
203
|
+
// Decay trust slightly (people forget trust too)
|
|
204
|
+
relationship.trust_Q = Math.max(0, relationship.trust_Q - decayAmount / 2);
|
|
205
|
+
// Reclassify bond
|
|
206
|
+
relationship.bond = classifyBond(relationship.affinity_Q, relationship.trust_Q, relationship.history);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// ── Relationship Queries ──────────────────────────────────────────────────────
|
|
211
|
+
/** Check if entity A would consider entity B a friend. */
|
|
212
|
+
export function isFriend(graph, entityA, entityB) {
|
|
213
|
+
const r = getRelationship(graph, entityA, entityB);
|
|
214
|
+
if (!r)
|
|
215
|
+
return false;
|
|
216
|
+
return r.affinity_Q >= q(0.3) && ["acquaintance", "friend", "close_friend", "mentor", "student", "romantic_partner", "family"].includes(r.bond);
|
|
217
|
+
}
|
|
218
|
+
/** Check if entity A would consider entity B an enemy. */
|
|
219
|
+
export function isEnemy(graph, entityA, entityB) {
|
|
220
|
+
const r = getRelationship(graph, entityA, entityB);
|
|
221
|
+
if (!r)
|
|
222
|
+
return false;
|
|
223
|
+
return r.affinity_Q < -q(0.3) || r.bond === "enemy" || r.bond === "rival";
|
|
224
|
+
}
|
|
225
|
+
/** Check if entity A trusts entity B enough for combat cooperation. */
|
|
226
|
+
export function hasCombatTrust(graph, entityA, entityB) {
|
|
227
|
+
const r = getRelationship(graph, entityA, entityB);
|
|
228
|
+
if (!r)
|
|
229
|
+
return false;
|
|
230
|
+
return r.trust_Q >= q(0.4);
|
|
231
|
+
}
|
|
232
|
+
/** Get the effective relationship modifier for morale effects. */
|
|
233
|
+
export function getMoraleModifier(graph, observer, target) {
|
|
234
|
+
const r = getRelationship(graph, observer, target);
|
|
235
|
+
if (!r)
|
|
236
|
+
return q(0);
|
|
237
|
+
// Positive affinity = morale boost when target succeeds
|
|
238
|
+
// Negative affinity = morale boost when target fails
|
|
239
|
+
return r.affinity_Q;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Check if harming someone constitutes betrayal.
|
|
243
|
+
*/
|
|
244
|
+
export function checkBetrayal(graph, attackerId, victimId) {
|
|
245
|
+
const r = getRelationship(graph, attackerId, victimId);
|
|
246
|
+
if (!r) {
|
|
247
|
+
return {
|
|
248
|
+
isBetrayal: false,
|
|
249
|
+
severity_Q: q(0),
|
|
250
|
+
witnessMoralePenalty_Q: q(0),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
// Betrayal occurs when affinity is positive but attacker harms victim
|
|
254
|
+
const isBetrayal = r.affinity_Q > q(0.5);
|
|
255
|
+
if (!isBetrayal) {
|
|
256
|
+
return {
|
|
257
|
+
isBetrayal: false,
|
|
258
|
+
severity_Q: q(0),
|
|
259
|
+
witnessMoralePenalty_Q: q(0),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
// Severity based on how positive the relationship was (0.6 to 1.0 range, as Q)
|
|
263
|
+
// Higher affinity = more severe betrayal = larger magnitude
|
|
264
|
+
const severity_Q = Math.round(SCALE.Q * 0.6 + r.affinity_Q * 0.4);
|
|
265
|
+
// Witnesses who cared about the victim take morale hit (0.3 to 0.7 range, as Q)
|
|
266
|
+
const witnessMoralePenalty_Q = Math.round(SCALE.Q * 0.3 + r.affinity_Q * 0.4);
|
|
267
|
+
return {
|
|
268
|
+
isBetrayal: true,
|
|
269
|
+
severity_Q,
|
|
270
|
+
witnessMoralePenalty_Q,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Record a betrayal event and update relationships.
|
|
275
|
+
*/
|
|
276
|
+
export function recordBetrayal(graph, attackerId, victimId, tick) {
|
|
277
|
+
const result = checkBetrayal(graph, attackerId, victimId);
|
|
278
|
+
if (result.isBetrayal) {
|
|
279
|
+
recordRelationshipEvent(graph, attackerId, victimId, {
|
|
280
|
+
tick,
|
|
281
|
+
type: "betrayed",
|
|
282
|
+
magnitude_Q: result.severity_Q,
|
|
283
|
+
description: "Betrayed during combat",
|
|
284
|
+
});
|
|
285
|
+
// Also damage attacker's reputation with victim's friends
|
|
286
|
+
const victimFriends = getEntityRelationshipsList(graph, victimId)
|
|
287
|
+
.filter((r) => r.affinity_Q > q(0.3) && r.entityA !== attackerId && r.entityB !== attackerId);
|
|
288
|
+
for (const friendRel of victimFriends) {
|
|
289
|
+
const friendId = friendRel.entityA === victimId ? friendRel.entityB : friendRel.entityA;
|
|
290
|
+
recordRelationshipEvent(graph, attackerId, friendId, {
|
|
291
|
+
tick,
|
|
292
|
+
type: "betrayed",
|
|
293
|
+
magnitude_Q: Math.round(result.severity_Q * 0.5),
|
|
294
|
+
description: `Betrayed ${victimId}`,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return result;
|
|
299
|
+
}
|
|
300
|
+
// ── Teaching Integration ──────────────────────────────────────────────────────
|
|
301
|
+
/**
|
|
302
|
+
* Compute teaching effectiveness multiplier based on relationship.
|
|
303
|
+
*/
|
|
304
|
+
export function computeTeachingRelationshipMultiplier(graph, teacherId, learnerId) {
|
|
305
|
+
const r = getRelationship(graph, teacherId, learnerId);
|
|
306
|
+
if (!r)
|
|
307
|
+
return 1.0;
|
|
308
|
+
// Base multiplier from affinity
|
|
309
|
+
let multiplier = 1.0 + (r.affinity_Q / SCALE.Q) * 0.3;
|
|
310
|
+
// Bonus for mentor/student bond
|
|
311
|
+
if (r.bond === "mentor" || r.bond === "student") {
|
|
312
|
+
multiplier += 0.2;
|
|
313
|
+
}
|
|
314
|
+
// Trust affects how well learner accepts teaching
|
|
315
|
+
multiplier += (r.trust_Q / SCALE.Q) * 0.2;
|
|
316
|
+
return Math.max(0.5, Math.min(1.5, multiplier));
|
|
317
|
+
}
|
|
318
|
+
// ── Serialization ─────────────────────────────────────────────────────────────
|
|
319
|
+
/** Serialize relationship graph to JSON-friendly format. */
|
|
320
|
+
export function serializeRelationshipGraph(graph) {
|
|
321
|
+
return {
|
|
322
|
+
relationships: Array.from(graph.relationships.entries()),
|
|
323
|
+
entityIndex: Array.from(graph.entityIndex.entries()).map(([entityId, keys]) => [
|
|
324
|
+
entityId,
|
|
325
|
+
Array.from(keys),
|
|
326
|
+
]),
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
/** Deserialize relationship graph. */
|
|
330
|
+
export function deserializeRelationshipGraph(data) {
|
|
331
|
+
const graph = createRelationshipGraph();
|
|
332
|
+
if (typeof data !== "object" || data === null) {
|
|
333
|
+
return graph;
|
|
334
|
+
}
|
|
335
|
+
const d = data;
|
|
336
|
+
if (Array.isArray(d.relationships)) {
|
|
337
|
+
for (const [key, rel] of d.relationships) {
|
|
338
|
+
graph.relationships.set(key, rel);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (Array.isArray(d.entityIndex)) {
|
|
342
|
+
for (const [entityId, keys] of d.entityIndex) {
|
|
343
|
+
graph.entityIndex.set(entityId, new Set(keys));
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return graph;
|
|
347
|
+
}
|