feat: add proposal interaction handling to social system
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { World } from '../../ecs/World.js';
|
||||
import { socialSystem } from '../socialSystem.js';
|
||||
import type { Position, Needs, Movement, NPCBrain, SocialState, EntityId, Stats, StatModifiers } from '@dflike/shared';
|
||||
import type { Position, Needs, Movement, NPCBrain, SocialState, EntityId, Stats, StatModifiers, Relationships, RelationshipData } from '@dflike/shared';
|
||||
import {
|
||||
AWARENESS_RADIUS,
|
||||
HUNGER_THRESHOLD,
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
SOCIAL_PAIR_COOLDOWN,
|
||||
Direction,
|
||||
} from '@dflike/shared';
|
||||
import { createBondRegistry, hasBond } from '../bondRegistry.js';
|
||||
import { relationshipConfig } from '../../config/relationshipConfig.js';
|
||||
|
||||
function createNPC(
|
||||
world: World, x: number, y: number,
|
||||
@@ -457,4 +459,162 @@ describe('socialSystem', () => {
|
||||
expect(positiveCount).toBeGreaterThan(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('proposal interactions', () => {
|
||||
it('should prioritize proposal target over closest NPC', () => {
|
||||
const world = new World();
|
||||
world.setSingleton('bondRegistry', createBondRegistry());
|
||||
const a = createNPC(world, 5, 5);
|
||||
const b = createNPC(world, 6, 5); // closer
|
||||
const c = createNPC(world, 7, 5); // proposal target
|
||||
|
||||
const socialA = world.getComponent<SocialState>(a, 'socialState')!;
|
||||
socialA.pendingProposal = { targetId: c, type: 'partner' };
|
||||
|
||||
socialSystem(world);
|
||||
|
||||
expect(socialA.partnerId).toBe(c);
|
||||
expect(socialA.isProposalInteraction).toBe(true);
|
||||
});
|
||||
|
||||
it('should transition from emoting to proposing when isProposalInteraction is set', () => {
|
||||
const world = new World();
|
||||
world.setSingleton('bondRegistry', createBondRegistry());
|
||||
const a = createNPC(world, 5, 5);
|
||||
const b = createNPC(world, 6, 5);
|
||||
|
||||
const socialA = world.getComponent<SocialState>(a, 'socialState')!;
|
||||
socialA.phase = 'emoting';
|
||||
socialA.partnerId = b;
|
||||
socialA.phaseTimer = 1;
|
||||
socialA.outcome = 'positive';
|
||||
socialA.isProposalInteraction = true;
|
||||
|
||||
const socialB = world.getComponent<SocialState>(b, 'socialState')!;
|
||||
socialB.phase = 'emoting';
|
||||
socialB.partnerId = a;
|
||||
socialB.phaseTimer = 1;
|
||||
socialB.outcome = 'positive';
|
||||
|
||||
socialSystem(world);
|
||||
|
||||
expect(socialA.phase).toBe('proposing');
|
||||
expect(socialB.phase).toBe('proposing');
|
||||
});
|
||||
|
||||
it('should form partner bond on accepted proposal', () => {
|
||||
const world = new World();
|
||||
const registry = createBondRegistry();
|
||||
world.setSingleton('bondRegistry', registry);
|
||||
const a = createNPC(world, 5, 5);
|
||||
const b = createNPC(world, 6, 5);
|
||||
|
||||
// Give both relationship components with high values
|
||||
world.addComponent<Relationships>(a, 'relationships', new Map<number, RelationshipData>([
|
||||
[b, { value: 85, interactions: 10, lastInteractionTick: 0, status: 'active' }]
|
||||
]));
|
||||
world.addComponent<Relationships>(b, 'relationships', new Map<number, RelationshipData>([
|
||||
[a, { value: 85, interactions: 10, lastInteractionTick: 0, status: 'active' }]
|
||||
]));
|
||||
|
||||
// Set up proposal interaction in proposing phase about to complete
|
||||
const socialA = world.getComponent<SocialState>(a, 'socialState')!;
|
||||
socialA.phase = 'proposing';
|
||||
socialA.partnerId = b;
|
||||
socialA.phaseTimer = 1;
|
||||
socialA.isProposalInteraction = true;
|
||||
|
||||
const socialB = world.getComponent<SocialState>(b, 'socialState')!;
|
||||
socialB.phase = 'proposing';
|
||||
socialB.partnerId = a;
|
||||
socialB.phaseTimer = 1;
|
||||
|
||||
// Add stats for acceptance (high empathy/sociability, low temperament)
|
||||
addStats(world, b, { empathy: 15, sociability: 15, temperament: 5 });
|
||||
|
||||
// Mock Math.random to ensure acceptance
|
||||
vi.spyOn(Math, 'random').mockReturnValue(0.1);
|
||||
|
||||
socialSystem(world);
|
||||
|
||||
vi.restoreAllMocks();
|
||||
|
||||
expect(hasBond(registry, a, b, 'partner')).toBe(true);
|
||||
expect(socialA.phase).toBe('none');
|
||||
});
|
||||
|
||||
it('should apply rejection penalties when receiver value below threshold', () => {
|
||||
const world = new World();
|
||||
const registry = createBondRegistry();
|
||||
world.setSingleton('bondRegistry', registry);
|
||||
const a = createNPC(world, 5, 5);
|
||||
const b = createNPC(world, 6, 5);
|
||||
|
||||
// Proposer high, receiver low
|
||||
world.addComponent<Relationships>(a, 'relationships', new Map<number, RelationshipData>([
|
||||
[b, { value: 85, interactions: 10, lastInteractionTick: 0, status: 'active' }]
|
||||
]));
|
||||
world.addComponent<Relationships>(b, 'relationships', new Map<number, RelationshipData>([
|
||||
[a, { value: 50, interactions: 10, lastInteractionTick: 0, status: 'active' }]
|
||||
]));
|
||||
|
||||
const socialA = world.getComponent<SocialState>(a, 'socialState')!;
|
||||
socialA.phase = 'proposing';
|
||||
socialA.partnerId = b;
|
||||
socialA.phaseTimer = 1;
|
||||
socialA.isProposalInteraction = true;
|
||||
|
||||
const socialB = world.getComponent<SocialState>(b, 'socialState')!;
|
||||
socialB.phase = 'proposing';
|
||||
socialB.partnerId = a;
|
||||
socialB.phaseTimer = 1;
|
||||
|
||||
addStats(world, a, { courage: 10, temperament: 10, sociability: 10 });
|
||||
addStats(world, b, { temperament: 10, sociability: 10 });
|
||||
|
||||
socialSystem(world);
|
||||
|
||||
expect(hasBond(registry, a, b, 'partner')).toBe(false);
|
||||
// addStats already adds statModifiers, so penalty mods should be applied
|
||||
const relsA = world.getComponent<Relationships>(a, 'relationships')!;
|
||||
expect(relsA.get(b)!.value).toBeLessThan(85); // proposer took hit
|
||||
const relsB = world.getComponent<Relationships>(b, 'relationships')!;
|
||||
expect(relsB.get(a)!.value).toBeLessThan(50); // rejecter took smaller hit
|
||||
expect(socialA.proposalCooldown).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should set proposal cooldown scaled by courage on rejection', () => {
|
||||
const world = new World();
|
||||
world.setSingleton('bondRegistry', createBondRegistry());
|
||||
const a = createNPC(world, 5, 5);
|
||||
const b = createNPC(world, 6, 5);
|
||||
|
||||
world.addComponent<Relationships>(a, 'relationships', new Map<number, RelationshipData>([
|
||||
[b, { value: 85, interactions: 10, lastInteractionTick: 0, status: 'active' }]
|
||||
]));
|
||||
world.addComponent<Relationships>(b, 'relationships', new Map<number, RelationshipData>([
|
||||
[a, { value: 50, interactions: 10, lastInteractionTick: 0, status: 'active' }]
|
||||
]));
|
||||
|
||||
const socialA = world.getComponent<SocialState>(a, 'socialState')!;
|
||||
socialA.phase = 'proposing';
|
||||
socialA.partnerId = b;
|
||||
socialA.phaseTimer = 1;
|
||||
socialA.isProposalInteraction = true;
|
||||
|
||||
const socialB = world.getComponent<SocialState>(b, 'socialState')!;
|
||||
socialB.phase = 'proposing';
|
||||
socialB.partnerId = a;
|
||||
socialB.phaseTimer = 1;
|
||||
|
||||
// High courage = shorter cooldown
|
||||
addStats(world, a, { courage: 18, temperament: 10, sociability: 10 });
|
||||
addStats(world, b, { temperament: 10, sociability: 10 });
|
||||
|
||||
socialSystem(world);
|
||||
|
||||
// Base 500, courage 18: 500 * (1 - (18-10)*0.04) = 500 * 0.68 = 340
|
||||
expect(socialA.proposalCooldown).toBe(340);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,11 +2,16 @@ import {
|
||||
AWARENESS_RADIUS, FACING_DURATION, PAUSING_DURATION, EMOTING_DURATION,
|
||||
SOCIAL_GLOBAL_COOLDOWN, SOCIAL_PAIR_COOLDOWN,
|
||||
HUNGER_THRESHOLD, ENERGY_THRESHOLD, Direction,
|
||||
PROPOSAL_EMOTING_DURATION,
|
||||
type SocialState, type Position, type Movement, type NPCBrain, type Needs, type EntityId,
|
||||
type Stats, type StatModifiers,
|
||||
} from '@dflike/shared';
|
||||
import type { Relationships } from '@dflike/shared';
|
||||
import type { World } from '../ecs/World.js';
|
||||
import { getEffectiveStat } from './statHelpers.js';
|
||||
import type { BondRegistry } from './bondRegistry.js';
|
||||
import { addBond } from './bondRegistry.js';
|
||||
import { relationshipConfig as cfg } from '../config/relationshipConfig.js';
|
||||
|
||||
function directionTo(from: Position, to: Position): number {
|
||||
const dx = to.x - from.x;
|
||||
@@ -100,75 +105,182 @@ export function socialSystem(world: World): void {
|
||||
partnerSocial.phaseTimer = EMOTING_DURATION;
|
||||
partnerSocial.outcome = Math.random() < positiveChanceB ? 'positive' : 'negative';
|
||||
} else if (social.phase === 'emoting') {
|
||||
// Check if this is a proposal interaction
|
||||
if (social.isProposalInteraction || partnerSocial.isProposalInteraction) {
|
||||
// Transition to proposing phase (ring emoji on client)
|
||||
social.phase = 'proposing';
|
||||
social.phaseTimer = PROPOSAL_EMOTING_DURATION;
|
||||
partnerSocial.phase = 'proposing';
|
||||
partnerSocial.phaseTimer = PROPOSAL_EMOTING_DURATION;
|
||||
} else {
|
||||
// Normal emoting→done transition
|
||||
const socA = getEffectiveStat(world, e, 'sociability');
|
||||
social.globalCooldown = Math.round(SOCIAL_GLOBAL_COOLDOWN * (1 - (socA - 10) * 0.04));
|
||||
social.pairCooldowns.set(partnerId!, SOCIAL_PAIR_COOLDOWN);
|
||||
|
||||
const socB = getEffectiveStat(world, partnerId!, 'sociability');
|
||||
partnerSocial.globalCooldown = Math.round(SOCIAL_GLOBAL_COOLDOWN * (1 - (socB - 10) * 0.04));
|
||||
partnerSocial.pairCooldowns.set(e, SOCIAL_PAIR_COOLDOWN);
|
||||
|
||||
// Baseline stat drift
|
||||
const statsA = world.getComponent<Stats>(e, 'stats');
|
||||
if (statsA) {
|
||||
if (social.outcome === 'positive') {
|
||||
statsA.sociability = Math.max(3, Math.min(18, statsA.sociability + 0.05));
|
||||
statsA.empathy = Math.max(3, Math.min(18, statsA.empathy + 0.05));
|
||||
} else {
|
||||
statsA.temperament = Math.max(3, Math.min(18, statsA.temperament + 0.02));
|
||||
}
|
||||
}
|
||||
const statsB = world.getComponent<Stats>(partnerId!, 'stats');
|
||||
if (statsB) {
|
||||
if (partnerSocial.outcome === 'positive') {
|
||||
statsB.sociability = Math.max(3, Math.min(18, statsB.sociability + 0.05));
|
||||
statsB.empathy = Math.max(3, Math.min(18, statsB.empathy + 0.05));
|
||||
} else {
|
||||
statsB.temperament = Math.max(3, Math.min(18, statsB.temperament + 0.02));
|
||||
}
|
||||
}
|
||||
|
||||
// Event-based transient modifiers
|
||||
const modsA = world.getComponent<StatModifiers>(e, 'statModifiers');
|
||||
if (modsA) {
|
||||
if (social.outcome === 'positive') {
|
||||
modsA.modifiers.push({ stat: 'sociability', value: 1, remaining: 100 });
|
||||
} else {
|
||||
modsA.modifiers.push({ stat: 'sociability', value: -1, remaining: 100 });
|
||||
modsA.modifiers.push({ stat: 'empathy', value: -1, remaining: 100 });
|
||||
}
|
||||
}
|
||||
const modsB = world.getComponent<StatModifiers>(partnerId!, 'statModifiers');
|
||||
if (modsB) {
|
||||
if (partnerSocial.outcome === 'positive') {
|
||||
modsB.modifiers.push({ stat: 'sociability', value: 1, remaining: 100 });
|
||||
} else {
|
||||
modsB.modifiers.push({ stat: 'sociability', value: -1, remaining: 100 });
|
||||
modsB.modifiers.push({ stat: 'empathy', value: -1, remaining: 100 });
|
||||
}
|
||||
}
|
||||
|
||||
// Emit completed interaction for relationship system
|
||||
social.lastOutcome = {
|
||||
partnerId: partnerId!,
|
||||
outcome: social.outcome!,
|
||||
tick: 0,
|
||||
};
|
||||
partnerSocial.lastOutcome = {
|
||||
partnerId: e,
|
||||
outcome: partnerSocial.outcome!,
|
||||
tick: 0,
|
||||
};
|
||||
|
||||
social.phase = 'none';
|
||||
social.partnerId = null;
|
||||
social.phaseTimer = 0;
|
||||
social.outcome = null;
|
||||
|
||||
partnerSocial.phase = 'none';
|
||||
partnerSocial.partnerId = null;
|
||||
partnerSocial.phaseTimer = 0;
|
||||
partnerSocial.outcome = null;
|
||||
}
|
||||
} else if (social.phase === 'proposing') {
|
||||
// Resolve the proposal
|
||||
const registry = world.getSingleton<BondRegistry>('bondRegistry');
|
||||
// Determine who is the initiator (the one with isProposalInteraction=true)
|
||||
const initiator = social.isProposalInteraction ? e : partnerId!;
|
||||
const receiver = social.isProposalInteraction ? partnerId! : e;
|
||||
const initiatorSocial = social.isProposalInteraction ? social : partnerSocial;
|
||||
const receiverSocial = social.isProposalInteraction ? partnerSocial : social;
|
||||
|
||||
const relsReceiver = world.getComponent<Relationships>(receiver, 'relationships');
|
||||
const receiverValue = relsReceiver?.get(initiator)?.value ?? 0;
|
||||
|
||||
let accepted = false;
|
||||
if (receiverValue >= cfg.proposalAcceptanceThreshold) {
|
||||
const recEmpathy = getEffectiveStat(world, receiver, 'empathy');
|
||||
const recSociability = getEffectiveStat(world, receiver, 'sociability');
|
||||
const recTemperament = getEffectiveStat(world, receiver, 'temperament');
|
||||
const chance = cfg.proposalBaseAcceptanceChance
|
||||
+ (recEmpathy - 10) * cfg.proposalEmpathyWeight
|
||||
+ (recSociability - 10) * cfg.proposalSociabilityWeight
|
||||
+ (recTemperament - 10) * cfg.proposalTemperamentWeight;
|
||||
accepted = Math.random() < chance;
|
||||
}
|
||||
|
||||
if (accepted && registry) {
|
||||
addBond(registry, initiator, receiver, 'partner', 0);
|
||||
// Sentiment boost on both sides
|
||||
const relsInit = world.getComponent<Relationships>(initiator, 'relationships');
|
||||
const relInit = relsInit?.get(receiver);
|
||||
if (relInit) relInit.value = Math.min(100, relInit.value + cfg.proposalAcceptanceBonus);
|
||||
const relRec = relsReceiver?.get(initiator);
|
||||
if (relRec) relRec.value = Math.min(100, relRec.value + cfg.proposalAcceptanceBonus);
|
||||
// Positive transient mods on both
|
||||
const modsInit = world.getComponent<StatModifiers>(initiator, 'statModifiers');
|
||||
if (modsInit) {
|
||||
modsInit.modifiers.push({ stat: 'sociability', value: cfg.proposalAcceptanceSociabilityMod, remaining: cfg.proposalModDecayTicks });
|
||||
modsInit.modifiers.push({ stat: 'empathy', value: cfg.proposalAcceptanceEmpathyMod, remaining: cfg.proposalModDecayTicks });
|
||||
}
|
||||
const modsRec = world.getComponent<StatModifiers>(receiver, 'statModifiers');
|
||||
if (modsRec) {
|
||||
modsRec.modifiers.push({ stat: 'sociability', value: cfg.proposalAcceptanceSociabilityMod, remaining: cfg.proposalModDecayTicks });
|
||||
modsRec.modifiers.push({ stat: 'empathy', value: cfg.proposalAcceptanceEmpathyMod, remaining: cfg.proposalModDecayTicks });
|
||||
}
|
||||
social.outcome = 'positive';
|
||||
partnerSocial.outcome = 'positive';
|
||||
} else {
|
||||
// Rejection penalties
|
||||
const relsInit = world.getComponent<Relationships>(initiator, 'relationships');
|
||||
const relInit = relsInit?.get(receiver);
|
||||
if (relInit) {
|
||||
relInit.value = Math.max(-100, relInit.value - cfg.baseDeltaNegative * cfg.proposalRejectionMultiplier);
|
||||
}
|
||||
const relRec = relsReceiver?.get(initiator);
|
||||
if (relRec) {
|
||||
relRec.value = Math.max(-100, relRec.value - cfg.baseDeltaNegative * cfg.proposalRejectionRejecterMultiplier);
|
||||
}
|
||||
// Proposal cooldown on initiator, scaled by courage
|
||||
const courage = getEffectiveStat(world, initiator, 'courage');
|
||||
initiatorSocial.proposalCooldown = Math.round(
|
||||
cfg.baseProposalCooldown * (1 - (courage - 10) * cfg.courageCooldownWeight)
|
||||
);
|
||||
// Negative transient mods on proposer
|
||||
const modsInit = world.getComponent<StatModifiers>(initiator, 'statModifiers');
|
||||
if (modsInit) {
|
||||
modsInit.modifiers.push({ stat: 'sociability', value: cfg.proposalRejectionSociabilityMod, remaining: cfg.proposalModDecayTicks });
|
||||
modsInit.modifiers.push({ stat: 'empathy', value: cfg.proposalRejectionEmpathyMod, remaining: cfg.proposalModDecayTicks });
|
||||
}
|
||||
social.outcome = 'negative';
|
||||
partnerSocial.outcome = 'negative';
|
||||
}
|
||||
|
||||
// Set cooldowns (same as normal interaction end)
|
||||
const socA = getEffectiveStat(world, e, 'sociability');
|
||||
social.globalCooldown = Math.round(SOCIAL_GLOBAL_COOLDOWN * (1 - (socA - 10) * 0.04));
|
||||
social.pairCooldowns.set(partnerId!, SOCIAL_PAIR_COOLDOWN);
|
||||
|
||||
const socB = getEffectiveStat(world, partnerId!, 'sociability');
|
||||
partnerSocial.globalCooldown = Math.round(SOCIAL_GLOBAL_COOLDOWN * (1 - (socB - 10) * 0.04));
|
||||
partnerSocial.pairCooldowns.set(e, SOCIAL_PAIR_COOLDOWN);
|
||||
|
||||
// Baseline stat drift
|
||||
const statsA = world.getComponent<Stats>(e, 'stats');
|
||||
if (statsA) {
|
||||
if (social.outcome === 'positive') {
|
||||
statsA.sociability = Math.max(3, Math.min(18, statsA.sociability + 0.05));
|
||||
statsA.empathy = Math.max(3, Math.min(18, statsA.empathy + 0.05));
|
||||
} else {
|
||||
statsA.temperament = Math.max(3, Math.min(18, statsA.temperament + 0.02));
|
||||
}
|
||||
}
|
||||
const statsB = world.getComponent<Stats>(partnerId!, 'stats');
|
||||
if (statsB) {
|
||||
if (partnerSocial.outcome === 'positive') {
|
||||
statsB.sociability = Math.max(3, Math.min(18, statsB.sociability + 0.05));
|
||||
statsB.empathy = Math.max(3, Math.min(18, statsB.empathy + 0.05));
|
||||
} else {
|
||||
statsB.temperament = Math.max(3, Math.min(18, statsB.temperament + 0.02));
|
||||
}
|
||||
}
|
||||
|
||||
// Event-based transient modifiers
|
||||
const modsA = world.getComponent<StatModifiers>(e, 'statModifiers');
|
||||
if (modsA) {
|
||||
if (social.outcome === 'positive') {
|
||||
modsA.modifiers.push({ stat: 'sociability', value: 1, remaining: 100 });
|
||||
} else {
|
||||
modsA.modifiers.push({ stat: 'sociability', value: -1, remaining: 100 });
|
||||
modsA.modifiers.push({ stat: 'empathy', value: -1, remaining: 100 });
|
||||
}
|
||||
}
|
||||
const modsB = world.getComponent<StatModifiers>(partnerId!, 'statModifiers');
|
||||
if (modsB) {
|
||||
if (partnerSocial.outcome === 'positive') {
|
||||
modsB.modifiers.push({ stat: 'sociability', value: 1, remaining: 100 });
|
||||
} else {
|
||||
modsB.modifiers.push({ stat: 'sociability', value: -1, remaining: 100 });
|
||||
modsB.modifiers.push({ stat: 'empathy', value: -1, remaining: 100 });
|
||||
}
|
||||
}
|
||||
|
||||
// Emit completed interaction for relationship system
|
||||
social.lastOutcome = {
|
||||
partnerId: partnerId!,
|
||||
outcome: social.outcome!,
|
||||
tick: 0,
|
||||
};
|
||||
partnerSocial.lastOutcome = {
|
||||
partnerId: e,
|
||||
outcome: partnerSocial.outcome!,
|
||||
tick: 0,
|
||||
};
|
||||
// Emit lastOutcome for relationship system
|
||||
social.lastOutcome = { partnerId: partnerId!, outcome: social.outcome!, tick: 0 };
|
||||
partnerSocial.lastOutcome = { partnerId: e, outcome: partnerSocial.outcome!, tick: 0 };
|
||||
|
||||
// Reset both entities
|
||||
social.phase = 'none';
|
||||
social.partnerId = null;
|
||||
social.phaseTimer = 0;
|
||||
social.outcome = null;
|
||||
social.isProposalInteraction = false;
|
||||
social.pendingProposal = null;
|
||||
|
||||
partnerSocial.phase = 'none';
|
||||
partnerSocial.partnerId = null;
|
||||
partnerSocial.phaseTimer = 0;
|
||||
partnerSocial.outcome = null;
|
||||
partnerSocial.isProposalInteraction = false;
|
||||
partnerSocial.pendingProposal = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -181,6 +293,40 @@ export function socialSystem(world: World): void {
|
||||
|
||||
const pos = world.getComponent<Position>(e, 'position')!;
|
||||
|
||||
// Check for pending proposal target first
|
||||
if (social.pendingProposal) {
|
||||
const target = social.pendingProposal.targetId;
|
||||
const targetPos = world.getComponent<Position>(target, 'position');
|
||||
const targetSocial = world.getComponent<SocialState>(target, 'socialState');
|
||||
if (targetPos && targetSocial && targetSocial.phase === 'none'
|
||||
&& !claimedThisTick.has(target) && targetSocial.globalCooldown <= 0) {
|
||||
const dist = manhattanDist(pos, targetPos);
|
||||
const perceptionA = getEffectiveStat(world, e, 'perception');
|
||||
const awarenessA = AWARENESS_RADIUS + (perceptionA - 10);
|
||||
if (dist <= awarenessA) {
|
||||
social.phase = 'facing';
|
||||
social.partnerId = target;
|
||||
social.phaseTimer = FACING_DURATION;
|
||||
social.isProposalInteraction = true;
|
||||
|
||||
targetSocial.phase = 'facing';
|
||||
targetSocial.partnerId = e;
|
||||
targetSocial.phaseTimer = FACING_DURATION;
|
||||
|
||||
const movA = world.getComponent<Movement>(e, 'movement')!;
|
||||
const movB = world.getComponent<Movement>(target, 'movement')!;
|
||||
movA.direction = directionTo(pos, targetPos);
|
||||
movA.state = 'idle';
|
||||
movB.direction = directionTo(targetPos, pos);
|
||||
movB.state = 'idle';
|
||||
|
||||
claimedThisTick.add(e);
|
||||
claimedThisTick.add(target);
|
||||
continue; // skip normal target scan
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let closestDist = Infinity;
|
||||
let closestTarget: EntityId | null = null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user