Merge branch 'review/slice-9-cd-recheck'
This commit is contained in:
@@ -66,6 +66,33 @@
|
||||
|
||||
- Черта «общительный» задаёт `talkCircleBonus: 1`, но `MaxSize` клампит к 4 при уже `talkCircleMax` 4 — поле мёртвое, пока верхний потолок не ниже 4. Менять ванильный размер кружка ревью не стало.
|
||||
|
||||
## Срез 9. Этапы C–D — перепроверка конфликта, присутствия, речи и romance
|
||||
|
||||
- **Фазы:** 44–47
|
||||
- **Проверен на:** `9972b30`, 2026-08-20
|
||||
- **Пути:** `src/HSchool.Ai/Conflict.cs`, `src/HSchool.Simulation/TalkCircleSystem.cs`, `src/HSchool.Simulation/PresenceSystem.cs`, `src/HSchool.Simulation/AffinitySystem.cs`, `src/HSchool.Simulation/MorningOpinions.cs`, `src/HSchool.People/Affinity.cs`, `src/HSchool.People/OrientationGenerator.cs`, `src/HSchool.Content/SpeechRules.cs`, `src/HSchool.Protocol/ProtocolCodec.cs`, `src/HSchool.Client/src/net/protocol.ts`, `src/HSchool.Client/src/format/talkCircle.ts`, `src/HSchool.Client/src/ui/dressRulesPanel.ts`, `src/HSchool.Client/src/ui/personCardNow.ts`, `src/HSchool.Client/src/ui/personCardConnections.ts`, `src/HSchool.Server/mods/romance/`, `src/HSchool.Server/Game/PresenceFrame.cs`, `src/HSchool.Server/Api/SpeechRulesModels.cs`, `docs/protocol.md`, `tests/HSchool.Ai.Tests/ConflictTests.cs`, `tests/HSchool.Simulation.Tests/QuarrelFightTests.cs`, `tests/HSchool.Simulation.Tests/TalkCircleTests.cs`, `tests/HSchool.Simulation.Tests/MorningOpinionsTests.cs`, `tests/HSchool.Protocol.Tests/ProtocolCodecTests.cs`, `tests/HSchool.People.Tests/AffinityTests.cs`, `tests/HSchool.People.Tests/OrientationGeneratorTests.cs`, `tests/HSchool.Content.Tests/RomancePackTests.cs`, `tests/HSchool.AppHost.Tests/RomancePackTests.cs`, `tests/HSchool.AppHost.Tests/SpeechRulesApiTests.cs`
|
||||
- **Итог:** перепроверка после `19554d4` (дальше сели A–B recheck в `TalkCircleSystem` и правки `protocol.md` у портретов). Обещания 44–47 на месте; прошлые правки (сейв ссоры, id жертвы задиры) живы. Дописано 3 теста; исправлено мнение жертвы о заступнике. `Seed.Mix` в политиковых тестах не трогали. A–B не переписывали. Хостовые `RomancePackTests` / `SpeechRulesApiTests` не гонялись.
|
||||
- **Открыто:** строка локации для тем пака `romance` по-прежнему `defName`; ванильный `quarrelChance` 1 съедает `conflictChance` черт на ссоре.
|
||||
|
||||
### Что проверено
|
||||
|
||||
Каждый пункт «Тесты, без которых фаза не закрыта» у 44–47 имеет отдельный тест. Ссора/драка — поля черт, не `defName`; skip пустого двора бросает кружок; нужды «здоровье» нет. Кадр присутствия v9 несёт id кружка и темы, имён нет; «Сейчас» собирает ту же строку из directory. Политика речи — с завтра; POST сегодня не переписывает живой кружок. Ваниль и `example` без романтических тем; пара только 18+; учитель → ученик не получает симпатию. Отказ врагу в чужой кружок (A–B) не ломает заступника: ссора идёт через `FindOpenConflict`, не `JoinCircle`. `dotnet test` с `--filter` по `QuarrelFightTests`, `ConflictTests` и `AffinityTests` зелёный.
|
||||
|
||||
### Что дописано
|
||||
|
||||
- `QuarrelFightTests.Defender_AndVictim_OpinionsMoveAsAllies`
|
||||
- `ConflictTests.QuarrelOpinion_IsStrongerThanRudeTalk`
|
||||
- `AffinityTests.RosterJson_RoundTripsCrushesAndPartner`
|
||||
|
||||
### Что исправлено
|
||||
|
||||
- Конец ссоры с заступником писал жертве к заступнику тот же штраф, что к задире. Теперь жертва и защитник — союзники: мнение друг к другу растёт, к задире падает.
|
||||
|
||||
### Открытое
|
||||
|
||||
- Клиентский `talkTopicLabel` знает только ванильные ключи. Тема пака в локации — сырой id; в логе карточки подпись берётся из каталога. Вшивать строки пака в ванильный словарь ревью не стало.
|
||||
- Ванильный `quarrelChance` уже 1, поэтому `conflictChance` задиры и вспыльчивого не двигает частоту ссоры (кламп 0…1). На драке поле живое (`fightChance` 0.1). Менять ванильный шанс ссоры ревью не стало.
|
||||
|
||||
## Срез 9. Этапы C–D — конфликт, присутствие, речь и romance
|
||||
|
||||
- **Фазы:** 44–47
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
| Срез 7. Этап B–C — климат, жизнь одежды и правила | `549634b` | [07-inventory](07-inventory/reviewed.md) |
|
||||
| Срез 8. Сессия и темп | `7100065` | [08-session](08-session/reviewed.md) |
|
||||
| Срез 9. Этапы A–B — мнения и разговоры | `a80c6f0` | [09-social](09-social/reviewed.md) |
|
||||
| Срез 9. Этапы C–D — конфликт, присутствие, речь и romance | `19554d4` | [09-social](09-social/reviewed.md) |
|
||||
| Срез 9. Этапы C–D — конфликт, присутствие, речь и romance | `9972b30` | [09-social](09-social/reviewed.md) |
|
||||
| Срез 10. Этап A — страховка | `cecd79c` | [10-craft](10-craft/reviewed.md) |
|
||||
| Срез 10. Этапы B–C — работник, карточка и дамп | `34f28f9` | [10-craft](10-craft/reviewed.md) |
|
||||
| Связность урока (вне очереди) 48, 50–53 | `efa27ab` | [off-queue](off-queue/reviewed.md) |
|
||||
|
||||
@@ -1056,11 +1056,7 @@ internal static class TalkCircleSystem
|
||||
OpinionStore.Get(person, other.Id) ?? 0);
|
||||
var now = OpinionStore.Get(person, other.Id) ?? 0;
|
||||
var shift = delta;
|
||||
if (circle.VictimId is not null
|
||||
&& other.Id.Equals(circle.VictimId, StringComparison.Ordinal)
|
||||
&& circle.OpinionBaseline.TryGetValue(PairKey(person.Id, circle.VictimId), out var viewOfVictim)
|
||||
&& Conflict.ShouldDefend(viewOfVictim, rules)
|
||||
&& !person.Id.Equals(circle.VictimId, StringComparison.Ordinal))
|
||||
if (AllyOfVictim(circle, person.Id, rules) && AllyOfVictim(circle, other.Id, rules))
|
||||
{
|
||||
shift = Math.Max(2, -delta / 4);
|
||||
}
|
||||
@@ -1076,6 +1072,26 @@ internal static class TalkCircleSystem
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Victim and anyone who joined on their side. Clash penalty is for opposite sides; allies
|
||||
/// get a small plus instead — otherwise the victim's view of the defender falls like a fight.
|
||||
/// </summary>
|
||||
private static bool AllyOfVictim(ActiveTalkCircle circle, string personId, BehaviorDef? rules)
|
||||
{
|
||||
if (circle.VictimId is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (personId.Equals(circle.VictimId, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return circle.OpinionBaseline.TryGetValue(PairKey(personId, circle.VictimId), out var viewOfVictim)
|
||||
&& Conflict.ShouldDefend(viewOfVictim, rules);
|
||||
}
|
||||
|
||||
private static void ApplyApology(School school, IReadOnlyList<Person> people, BehaviorDef? rules)
|
||||
{
|
||||
foreach (var person in people)
|
||||
|
||||
@@ -52,6 +52,16 @@ public class ConflictTests
|
||||
Assert.True(restored <= baseline);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QuarrelOpinion_IsStrongerThanRudeTalk()
|
||||
{
|
||||
var (catalog, _) = Fixtures.Vanilla();
|
||||
var quarrel = -Conflict.QuarrelOpinionDelta(catalog.BehaviorRules);
|
||||
var rude = Math.Abs(catalog.Topics["TopicRude"].OpinionShift);
|
||||
Assert.True(quarrel > rude);
|
||||
Assert.True(-Conflict.FightOpinionDelta(catalog.BehaviorRules) > quarrel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Catalog_HasNoHealthNeed()
|
||||
{
|
||||
|
||||
@@ -122,6 +122,30 @@ public class AffinityTests
|
||||
Assert.True((OpinionStore.Get(pupil, teacher.Id) ?? 0) < before);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RosterJson_RoundTripsCrushesAndPartner()
|
||||
{
|
||||
var catalog = Fixtures.RomanceCatalog();
|
||||
var left = Staff("s1", female: false, "Bisexual");
|
||||
var right = Staff("s2", female: true, "Bisexual");
|
||||
SetOpinion(left, right.Id, 80);
|
||||
SetOpinion(right, left.Id, 80);
|
||||
var roster = new Roster([left, right], [], []);
|
||||
Affinity.Refresh(catalog, roster, AsOf);
|
||||
Assert.Equal(right.Id, left.Bonds!.PartnerId);
|
||||
|
||||
var json = RosterJson.Serialize(RosterDocument.From(1, roster));
|
||||
Assert.Contains("\"partnerId\"", json, StringComparison.Ordinal);
|
||||
Assert.Contains("\"crushes\"", json, StringComparison.Ordinal);
|
||||
var loaded = RosterJson.Parse(json).ToRoster();
|
||||
var loadedLeft = loaded.People.First(person => person.Id.Equals(left.Id, StringComparison.Ordinal));
|
||||
var loadedRight = loaded.People.First(person => person.Id.Equals(right.Id, StringComparison.Ordinal));
|
||||
Assert.Equal(right.Id, loadedLeft.Bonds?.PartnerId);
|
||||
Assert.Equal(left.Id, loadedRight.Bonds?.PartnerId);
|
||||
Assert.Contains(right.Id, loadedLeft.Bonds!.Crushes);
|
||||
Assert.Contains(left.Id, loadedRight.Bonds!.Crushes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdjacentYearStudents_CanHaveSympathy()
|
||||
{
|
||||
|
||||
@@ -230,6 +230,34 @@ public class QuarrelFightTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Defender_AndVictim_OpinionsMoveAsAllies()
|
||||
{
|
||||
var (school, pupils) = ThreePupilsOnBreak();
|
||||
using (school)
|
||||
{
|
||||
var bully = pupils[0];
|
||||
var victim = pupils[1];
|
||||
var defender = pupils[2];
|
||||
OpinionStore.Set(bully, victim.Id, -55);
|
||||
OpinionStore.Set(victim, bully.Id, -55);
|
||||
OpinionStore.Set(defender, victim.Id, 70);
|
||||
PlaceAt(school, bully.Id, "corridor-1");
|
||||
PlaceAt(school, victim.Id, "corridor-1");
|
||||
PlaceAt(school, defender.Id, "corridor-1");
|
||||
|
||||
Assert.True(school.TryStartAction(bully.Id, TalkActions.Quarrel));
|
||||
TickWhile(school, personId => ActivityOf(school, personId) == TalkActions.Quarrel, bully.Id, victim.Id, defender.Id);
|
||||
|
||||
Assert.True((OpinionStore.Get(defender, victim.Id) ?? 0) > 70);
|
||||
Assert.True((OpinionStore.Get(victim, defender.Id) ?? 0) > 0);
|
||||
Assert.True((OpinionStore.Get(defender, bully.Id) ?? 0) < 0);
|
||||
Assert.True((OpinionStore.Get(victim, bully.Id) ?? 0) < -55);
|
||||
Assert.True((OpinionStore.Get(bully, victim.Id) ?? 0) < -55);
|
||||
Assert.True((OpinionStore.Get(bully, defender.Id) ?? 0) < 0);
|
||||
}
|
||||
}
|
||||
|
||||
private static (School School, Person First, Person Second) TwoPupilsOnBreak()
|
||||
{
|
||||
var (catalog, map) = Vanilla();
|
||||
|
||||
Reference in New Issue
Block a user