Typist.pk3
Table of Contents
- 1. Event Handler
- 2. Player Handler
- 3. Server
- 4. World Changer
- 5. Activatable
- 6. Answer
- 7. Answer State
- 8. Character
- 9. Clock
- 10. Event Reporters
- 11. Input Manager
- 12. Key Processor
- 13. Known Target
- 14. Lesson
- 15. Math
- 16. Mode
- 17. Origin
- 18. Player
- 19. Question
- 20. Settings
- 21. Stale Marker
- 22. Strings
- 23. Target
- 24. Target Widget
- 25. Colors
- 26. View
- 27. Effect
- 28. Options Menu
- 29. Mod setup
- 30. Tests
Typist.pk3 turns FPS games into typing exercises.
TODO: add instructions.
1. Event Handler
1.1. EventHandler
// Entry point for Typist.pk3. class tt_EventHandler : EventHandler { override void worldTick() { _playerHandler.tick(); _server.tick(); self.IsUiProcessor = _playerHandler.isCapturingKeys(); } override bool uiProcess(UiEvent event) { let character = tt_Character.of(event.type, event.keyChar, event.isCtrl); _playerHandler.processKey(character); return false; } override bool inputProcess(InputEvent event) { _playerHandler.processInput(event.type); return false; } override void playerEntered(PlayerEvent event) { if (gameState != GS_Level && gameState != GS_StartUp) return; self.RequireMouse = true; if (_server == NULL) _server = tt_Server.of(); int playerNumber = event.playerNumber; _server.addPlayer(playerNumber); tt_GameTweaks.tweakPlayer(players[playerNumber]); if (playerNumber == consolePlayer) _playerHandler = tt_PlayerSupervisor.of(consolePlayer); } override void worldThingDied(WorldEvent event) { _playerHandler.reportDead(event.Thing); } override void worldLoaded(WorldEvent event) { bool isTitlemap = (level.mapName ~== "TITLEMAP"); if (isTitlemap) destroy(); } override void worldUnloaded(WorldEvent event) { self.IsUiProcessor = false; } override void renderOverlay(RenderEvent event) { _playerHandler.draw(event); } override void consoleProcess(ConsoleEvent event) { string command = event.Name; if (command.left(3) != "tt_") return; if (command == "tt_unlock_mode" ) _playerHandler.unlockMode(); else if (command == "tt_force_combat" ) _playerHandler.forceCombat(); else if (command == "tt_reset_targets") _playerHandler.reset(consolePlayer); } override void networkCommandProcess(NetworkCommand command) { if (command.command == "tt_target") { double x = command.readDouble(); double y = command.readDouble(); double z = command.readDouble(); _server.react(command.player, (x, y, z)); } } private tt_PlayerHandler _playerHandler; private tt_Server _server; }
1.2. GameTweaks
// Buddha server bool tt_buddha_enabled = true;
// Handles game tweaks. class tt_GameTweaks play { static void tweakPlayer(PlayerInfo player) { let pawn = player.mo; if (pawn == NULL) return; makeInvulnerable(pawn); increaseDamage(pawn); decreaseIncomingDamage(pawn); protectFromSelfDamage(pawn); disableSeekingMissiles(pawn); } // Still lose health down to 1 point. static private void makeInvulnerable(PlayerPawn pawn) { if (tt_buddha_enabled) pawn.giveInventory("tt_Buddha", 1); } static private void increaseDamage(PlayerPawn pawn) { double originalDamage = getDefaultByType(pawn.getClass()).damageMultiply; pawn.damageMultiply = originalDamage * 10; } static private void decreaseIncomingDamage(PlayerPawn pawn) { double originalFactor = getDefaultByType(pawn.getClass()).damageFactor; pawn.damageFactor = originalFactor / 2; } static private void protectFromSelfDamage(PlayerPawn pawn) { pawn.selfDamageFactor = 0; } static private void disableSeekingMissiles(PlayerPawn pawn) { pawn.bCantSeek = true; } }
1.3. Buddha
class tt_Buddha : PowerBuddha { Default { // https://zdoom.org/wiki/Powerup_properties Powerup.Duration 0x7FFFFFFD; +INVENTORY.UNDROPPABLE; } }
2. Player Handler
2.1. PlayerHandler
// Handles the game for one player. class tt_PlayerHandler abstract { abstract void reset(int playerNumber); abstract void processKey(tt_Character character); // Type is from InputEvent.EGenericEvent. abstract void processInput(int type); abstract void tick(); abstract void reportDead(Actor dead); abstract bool isCapturingKeys(); abstract void unlockMode(); abstract void forceCombat(); ui abstract void draw(RenderEvent event); }
2.2. PlayerSupervisor
// General settings user int tt_view_scale = 1; user bool tt_fast_confirmation = false; // Command settings user string tt_command_pass_through = "/pass"; // Sound settings user bool tt_sound_enabled = true; user int tt_sound_theme = 1; user bool tt_sound_typing_enabled = true;
// Handles Typist.pk3 features for one player. class tt_PlayerSupervisor : tt_PlayerHandler { static tt_PlayerSupervisor of(int playerNumber) { let result = new("tt_PlayerSupervisor"); result.reset(playerNumber); return result; } override void reset(int playerNumber) { let playerSource = tt_PlayerSourceImpl.of(playerNumber); let clock = tt_TotalClock .of(); let soundPlayer = tt_PlayerSoundPlayer .of(playerSource, tt_BoolCvar.of(playerSource, "tt_sound_enabled"), tt_IntCvar.of(playerSource, "tt_sound_theme")); let answerReporter = tt_SoundAnswerReporter .of(soundPlayer); let modeReporter = tt_SoundModeReporter .of(soundPlayer); let isTypingEnabled = tt_BoolCvar.of(playerSource, "tt_sound_typing_enabled"); let keyPressReporter = tt_SoundKeyPressReporter.of(soundPlayer, isTypingEnabled); let manualModeSource = tt_SettableMode .of(); let playerInput = tt_PlayerInput .of(manualModeSource, keyPressReporter); let deathReporter = tt_DeathReporter.of(); let originSource = tt_PlayerOriginSource.of(playerSource); let targetRadar = tt_TargetRadar .of(originSource); let radarStaleMarker = tt_StaleMarkerImpl .of(clock); let radarCacheDirty = tt_TargetSourceCache .of(targetRadar, radarStaleMarker); let radarCache = tt_TargetSourcePruner.of(radarCacheDirty); let lesson = makeLesson(playerSource); let targetRegistry = makeTargetRegistry(radarCache, lesson, deathReporter, clock); let answerStateSource = tt_PressedAnswerState.of(); let visibleTargetSource = tt_VisibleKnownTargetSource.of(targetRegistry, playerSource); let pressMatcher = tt_QuestionAnswerMatcher .of(visibleTargetSource, playerInput, answerStateSource); let hastyMatcher = tt_HastyQuestionAnswerMatcher .of(visibleTargetSource, playerInput, answerReporter); let fastConfirmation = tt_BoolCvar.of(playerSource, "tt_fast_confirmation"); let targetOriginSource = tt_OriginSourceCache .of(tt_SelectableOriginSource.of(hastyMatcher, pressMatcher, fastConfirmation), tt_StaleMarkerImpl.of(clock)); let projector = tt_Projector .of(visibleTargetSource, playerSource); let widgetRegistry = tt_TargetWidgetRegistry.of(projector); let widgetSorter = tt_SorterByDistance .of(widgetRegistry, originSource); let autoModeSource = tt_AutoModeSource.of(visibleTargetSource); Array<tt_ModeSource> modeSources = { tt_AutomapModeSource.of(), manualModeSource, tt_DelayedCombatModeSource.of(clock, autoModeSource, radarCache), autoModeSource }; let modeSource = tt_ReportedModeSource.of(modeReporter, tt_ModeCascade.of(modeSources)); let inputManager = tt_PassThroughInputManager .of(tt_InputByModeManager.of(modeSource, playerInput)); Array<tt_Activatable> commands = { tt_PassThrough.of(inputManager, tt_StringCvar.of(playerSource, "tt_command_pass_through")) }; let commandDispatcher = tt_CommandDispatcher.of(playerInput, commands, answerReporter, answerStateSource, fastConfirmation); let oldModeSource = tt_SettableMode.of(); let inputBlockAfterCombat = tt_InputBlockAfterCombat .of(playerInput, modeSource, oldModeSource); let scaleSetting = tt_PositiveIntCvar.of(playerSource, "tt_view_scale"); let infoPanel = tt_InfoPanel.of(modeSource, playerInput, commandDispatcher, visibleTargetSource, scaleSetting); Array<tt_View> views = { tt_TargetOverlay.of(widgetSorter, playerInput, scaleSetting, modeSource), tt_Frame.of(modeSource), infoPanel }; let targetSender = tt_TargetOriginSender.of(targetOriginSource); Array<tt_Effect> effects = { tt_Gunner.of(targetOriginSource, targetSender), tt_AnswerResetter.of(answerStateSource, playerInput), tt_MatchWatcher.of(answerStateSource, answerReporter, targetOriginSource) }; Array<tt_KeyProcessor> keyProcessors = {inputBlockAfterCombat, answerStateSource}; _keyProcessor = tt_KeyProcessors.of(keyProcessors); _deathReporter = deathReporter; _targetRegistry = targetRegistry; _view = tt_ConditionalView.of(tt_Views.of(views)); _modeSource = modeSource; _targetWidgetSource = projector; _commandDispatcher = commandDispatcher; _manualModeSource = manualModeSource; _inputManager = inputManager; _oldModeSource = oldModeSource; _inputBlockAfterCombat = inputBlockAfterCombat; _effects = tt_Effects.of(effects); } override void processKey(tt_Character character) { _keyProcessor.processKey(character); } override void processInput(int type) { _inputManager.processInput(type); } override void tick() { _commandDispatcher.activate(); _inputManager.manageInput(); _inputBlockAfterCombat.update(); _oldModeSource.setMode(_modeSource.getMode()); _effects.doEffect(); } override void reportDead(Actor dead) { _deathReporter.reportDead(dead); } override bool isCapturingKeys() { return _inputManager.isCapturingKeys(); } override void unlockMode() { _manualModeSource.setMode(tt_Mode.None); } override void forceCombat() { _manualModeSource.setMode(tt_Mode.Combat); } override void draw(RenderEvent event) { _view.draw(event); }
// Mixed Lesson configuration user bool tt_is_english_enabled = true; user bool tt_is_random_enabled = false; user bool tt_is_maths_enabled = false; user bool tt_is_custom_enabled = false;
private static tt_Lesson makeLesson(tt_PlayerSource playerSource) { let randomLessonSettings = tt_RandomCharactersLessonSettingsImpl.of(playerSource); Array<tt_SwitchableLesson> lessons = { tt_SwitchableLesson.of(tt_BoolCvar.of(playerSource, "tt_is_random_enabled"), tt_RandomCharactersLesson.of(randomLessonSettings)), tt_SwitchableLesson.of(tt_BoolCvar.of(playerSource, "tt_is_maths_enabled"), tt_MathsLesson.of()), tt_SwitchableLesson.of(tt_BoolCvar.of(playerSource, "tt_is_english_enabled"), tt_StringSet.of("tt_1000")), tt_SwitchableLesson.of(tt_BoolCvar.of(playerSource, "tt_is_custom_enabled"), tt_StringSet.of("typist_custom_text")) }; return tt_MixedLesson.of(lessons); } private static tt_KnownTargetSource makeTargetRegistry( tt_TargetSource targetSource, tt_Lesson lesson, tt_TargetSource deathReporter, tt_Clock clock) { let registry = tt_TargetRegistry .of(targetSource, lesson, deathReporter); let staleMarker = tt_StaleMarkerImpl.of(clock); let registryCache = tt_KnownTargetSourceCache.of(registry, staleMarker); return registryCache; } private tt_KeyProcessor _keyProcessor; private tt_KnownTargetSource _targetRegistry; private tt_DeathReporter _deathReporter; private tt_View _view; private tt_ModeSource _modeSource; private tt_TargetWidgetSource _targetWidgetSource; private tt_CommandDispatcher _commandDispatcher; private tt_ModeStorage _manualModeSource; private tt_PassThroughInputManager _inputManager; private tt_SettableMode _oldModeSource; private tt_InputBlockAfterCombat _inputBlockAfterCombat; private tt_Effect _effects; }
3. Server
3.1. Server
class tt_Server { static tt_Server of() { let result = new("tt_Server"); result._globalChangers = tt_WorldChangers.ofNone(); return result; } void addPlayer(int playerNumber) { let playerSource = tt_PlayerSourceImpl .of(playerNumber); let originSource = tt_PlayerOriginSource .of(playerSource); let targetOriginSource = tt_ExternalOriginSource.of(); let targetRadar = tt_TargetRadar .of(originSource); let radarStaleMarker = tt_StaleMarkerImpl .of(tt_TotalClock.of()); let radarCacheDirty = tt_TargetSourceCache .of(targetRadar, radarStaleMarker); let radarCache = tt_TargetSourcePruner .of(radarCacheDirty); let autoAimSetting = tt_FloatCvar .of(playerSource, "autoaim"); Array<tt_WorldChanger> targetChangers = { tt_HorizontalAimer.of(targetOriginSource, playerSource), tt_VerticalAimer.of(targetOriginSource, playerSource, autoAimSetting), tt_Firer.of(playerSource) }; _targetSources[playerNumber] = targetOriginSource; _targetChangers[playerNumber] = tt_WorldChangers.of(targetChangers); Array<tt_WorldChanger> globalChangers = { tt_ProjectileSpeedController.of(originSource, playerSource), tt_EnemySpeedController.of(radarCache, playerSource) }; _globalChangers.add(tt_WorldChangers.of(globalChangers)); } play void react(int playerNumber, vector3 targetOrigin) { _targetSources[playerNumber].setOrigin(tt_Origin.of(targetOrigin)); _targetChangers[playerNumber].changeWorld(); } play void tick() { _globalChangers.changeWorld(); } tt_ExternalOriginSource _targetSources[MAXPLAYERS]; tt_WorldChanger _targetChangers[MAXPLAYERS]; tt_WorldChangers _globalChangers; }
4. World Changer
4.1. WorldChanger
// This interface represents entities that change the world state. class tt_WorldChanger abstract { play abstract void changeWorld(); }
4.2. WorldChangers
// Implements tt_WorldChanger by executing several instances of tt_WorldChanger. class tt_WorldChangers : tt_WorldChanger { static tt_WorldChangers of(Array<tt_WorldChanger> changers) { let result = new("tt_WorldChangers"); result._changers.move(changers); return result; } static tt_WorldChangers ofNone() { return new("tt_WorldChangers"); } void add(tt_WorldChanger changer) { _changers.push(changer); } override void changeWorld() { foreach (changer : _changers) changer.changeWorld(); } private Array<tt_WorldChanger> _changers; }
4.3. EnemySpeedController
// Implements tt_WorldChanger by slowing down enemies. class tt_EnemySpeedController : tt_WorldChanger { static tt_EnemySpeedController of(tt_TargetSource targetSource, tt_PlayerSource playerSource) { let result = new("tt_EnemySpeedController"); result._targetSource = targetSource; result._playerSource = playerSource; return result; } override void changeWorld() { let targets = _targetSource.getTargets(); uint nTargets = targets.size(); int player = _playerSource.getNumber(); for (uint i = 0; i < nTargets; ++i) { let enemy = targets.at(i).getActor(); if (!tt_VelocityStorage.isSlowedDown(enemy, player)) tt_VelocityStorage.slowDown(enemy, player); } } private tt_TargetSource _targetSource; private tt_PlayerSource _playerSource; }
4.4. ProjectileSpeedController
// Implements tt_WorldChanger by slowing down projectiles that fly towards the player. // // When a projectile is no longer flying towards the player, its speed is // restored. class tt_ProjectileSpeedController : tt_WorldChanger { static tt_ProjectileSpeedController of(tt_OriginSource playerOriginSource, tt_PlayerSource playerSource) { let result = new("tt_ProjectileSpeedController"); result._playerOriginSource = playerOriginSource; result._playerSource = playerSource; return result; } override void changeWorld() { let origin = _playerOriginSource.getOrigin().getVector(); let playerRadius = _playerSource.getPawn().radius; int player = _playerSource.getNumber(); foreach (Actor a : ThinkerIterator.Create("Actor", Thinker.STAT_DEFAULT)) if (a.bMissile) controlProjectile(a, origin, playerRadius, player); } private play void controlProjectile(Actor a, vector3 playerOrigin, double playerRadius, int player) { bool isInRange = tt_Math.isInEffectiveRange(a.pos, playerOrigin); if (isInRange && isMovingTowardsPlayer(a, playerOrigin, playerRadius)) { if (!tt_VelocityStorage.isSlowedDown(a, player)) tt_VelocityStorage.slowDown(a, player); } else if (tt_VelocityStorage.isSlowedDown(a, player)) { tt_VelocityStorage.restoreVelocity(a, player); } } private play bool isMovingTowardsPlayer(Actor projectile, vector3 playerPos, double playerRadius) { vector3 vel = projectile.vel; if (vel == (0, 0, 0)) { return false; } // doesn't move double oldDistance = (projectile.pos - vel - playerPos).length(); double distance = (projectile.pos - playerPos).length(); if (distance > oldDistance) { return false; } // moves from player // http://mathworld.wolfram.com/Point-LineDistance3-Dimensional.html vector3 x10 = projectile.pos - playerPos; vector3 prod = tt_Math.crossProduct(vel, x10); double lineDistance = prod.length() / vel.length(); double hitDistance = playerRadius + projectile.radius; bool willTouchPlayer = (hitDistance >= lineDistance); return willTouchPlayer; } private tt_OriginSource _playerOriginSource; private tt_PlayerSource _playerSource; }
4.5. VelocityStorage
// This is a helper class that allows storing the velocity. // TODO: rewrite with a Behavior? class tt_VelocityStorage : Inventory { static bool isSlowedDown(Actor other, int byPlayer) { let storage = tt_VelocityStorage(other.findInventory("tt_VelocityStorage")); if (storage == NULL) return false; return storage._byWhichPlayer[byPlayer]; } static void slowDown(Actor other, int byPlayer) { let storage = tt_VelocityStorage(other.findInventory("tt_VelocityStorage")); if (storage != NULL) { storage._byWhichPlayer[byPlayer] = true; return; } storage = tt_VelocityStorage(Actor.spawn("tt_VelocityStorage")); storage._velocity = other.vel; storage._speed = other.speed; other.addInventory(storage); other.vel *= VELOCITY_SCALE_FACTOR; other.speed *= VELOCITY_SCALE_FACTOR; } static void restoreVelocity(Actor other, int byPlayer) { let storage = tt_VelocityStorage(other.findInventory("tt_VelocityStorage")); storage._byWhichPlayer[byPlayer] = false; if (storage.countByPlayers() == 0) { other.vel = storage._velocity; other.speed = storage._speed; other.removeInventory(storage); storage.destroy(); } } private int countByPlayers() { int result = 0; foreach (byPlayer : _byWhichPlayer) result += byPlayer; return result; } // TODO: make velocity scale factor configurable for projectiles and enemies. // for actors it was 0.2. const VELOCITY_SCALE_FACTOR = 0.1; private vector3 _velocity; private double _speed; private bool[MAXPLAYERS] _byWhichPlayer; }
4.6. HorizontalAimer
// Implements tt_WorldChanger interface by rotating the player. class tt_HorizontalAimer : tt_WorldChanger { static tt_HorizontalAimer of(tt_OriginSource targetOriginSource, tt_PlayerSource playerSource) { let result = new("tt_HorizontalAimer"); result._targetOriginSource = targetOriginSource; result._playerSource = playerSource; return result; } override void changeWorld() { let targetOrigin = _targetOriginSource.getOrigin(); if (targetOrigin == NULL) { return; } let pawn = _playerSource.getPawn(); if (pawn == NULL) { return; } vector3 myPosition = pawn.pos; vector3 otherPosition = targetOrigin.getVector(); double angle = AngleTo(myPosition.XY, otherPosition.XY); pawn.A_SetAngle(angle, SPF_INTERPOLATE); } private static double AngleTo(vector2 myPosition, vector2 otherPosition) { vector2 xy = otherPosition - myPosition; return vectorAngle(xy.x, xy.y); } private tt_OriginSource _targetOriginSource; private tt_PlayerSource _playerSource; }
{
let tag = "tt_HorizontalAimer";
Array<tt_Origin> targetPositions;
Array<double> angles;
targetPositions.push(tt_Origin.of(( 100, 100, 0))); angles.push( 45);
targetPositions.push(tt_Origin.of((-100, -100, 0))); angles.push(-135);
targetPositions.push(tt_Origin.of(( 0, 0, 0))); angles.push( 0);
players[consolePlayer].mo.SetOrigin((0, 0, 0), false);
int nTargetPositions = targetPositions.size();
for (int i = 0; i < nTargetPositions; ++i)
{
let originSource = tt_OriginSourceMock.of();
let playerSource = tt_PlayerSourceMock.of();
let aimer = tt_HorizontalAimer.of(originSource, playerSource);
let targetOrigin = targetPositions[i];
let pawn = players[consolePlayer].mo;
double angle = angles[i];
originSource.expect_getOrigin(targetOrigin);
playerSource.expect_getPawn(pawn);
// Just for a visual check.
spawn("DoomImp", targetOrigin.getVector());
aimer.changeWorld();
let message = string.format("%s: pawn is oriented at the target, angle: %f",
tag,
angle);
it(message, AssertEval(pawn.angle, "~==", angles[i]));
assertSatisfaction(originSource.getSatisfaction(), tag);
assertSatisfaction(playerSource.getSatisfaction(), tag);
cleanUpSpawned();
}
}
4.7. VerticalAimer
// Implements tt_WorldChanger interface by adjusting the player pitch // (horizontal angle). If autoaim is enabled, no pitch adjustment is done. class tt_VerticalAimer : tt_WorldChanger { static tt_VerticalAimer of(tt_OriginSource targetOriginSource, tt_PlayerSource playerSource, tt_FloatSetting autoAimSetting) { let result = new("tt_VerticalAimer"); result._targetOriginSource = targetOriginSource; result._playerSource = playerSource; result._autoAimSetting = autoAimSetting; return result; } override void changeWorld() { if (!isAutoAimEnabled()) setPitch(); } private play bool isAutoAimEnabled() { return _autoAimSetting.getFloat() > 34.5; } private play void setPitch() { let targetOrigin = _targetOriginSource.getOrigin(); if (targetOrigin == NULL) { return; } let pawn = _playerSource.getPawn(); if (pawn == NULL) { return; } vector3 myPosition = pawn.pos; myPosition.z += pawn.Height / 2 + pawn.AttackZOffset; vector3 otherPosition = targetOrigin.getVector(); vector3 diff = myPosition - otherPosition; double angle = Atan2(diff.z, sqrt(diff.x * diff.x + diff.y * diff.y)); pawn.A_SetPitch(angle, SPF_INTERPOLATE); } private tt_OriginSource _targetOriginSource; private tt_PlayerSource _playerSource; private tt_FloatSetting _autoAimSetting; }
{
let tag = "tt_VerticalAimer";
let targetOriginSource = tt_OriginSourceMock.of();
let playerSource = tt_PlayerSourceMock.of();
let autoAimSetting = tt_FloatSettingMock.of();
let aimer = tt_VerticalAimer.of(targetOriginSource, playerSource, autoAimSetting);
let targetOrigin = tt_Origin.of((550, 500, 500));
let pawn = players[consolePlayer].mo;
pawn.SetOrigin((0, 0, 0), false);
targetOriginSource.expect_getOrigin(targetOrigin);
playerSource .expect_getPawn(pawn);
autoAimSetting .expect_getFloat(0);
aimer.changeWorld();
assertSatisfaction(targetOriginSource.getSatisfaction(), tag);
assertSatisfaction(playerSource.getSatisfaction(), tag);
assertSatisfaction(autoAimSetting.getSatisfaction(), tag);
}
4.8. FirerImpl
// Implements tt_WorldChanger by making the player pawn fire a shot. class tt_Firer : tt_WorldChanger { static tt_Firer of(tt_PlayerSource playerSource) { let result = new("tt_Firer"); result._playerSource = playerSource; return result; } override void changeWorld() { let playerInfo = _playerSource.getInfo(); bool isReady = isWeaponReady(playerInfo); if (isReady) { let pawn = _playerSource.getPawn(); State stat = NULL; playerInfo.cmd.buttons |= BT_ATTACK; pawn.FireWeapon(stat); } } private static bool isWeaponReady(PlayerInfo player) { bool isReady = (player.WeaponState & WF_WEAPONREADY) || (player.WeaponState & WF_WEAPONREADYALT) || player.attackDown; return isReady; } private tt_PlayerSource _playerSource; }
4.8.1. Test
{
let tag = "tt_Firer";
let playerSource = tt_PlayerSourceMock.of();
let firer = tt_Firer.of(playerSource);
PlayerInfo info = players[consolePlayer];
let pawn = info.mo;
playerSource.expect_getInfo(info);
playerSource.expect_getPawn(pawn);
int nBullets = pawn.countInv("Clip");
it(tag .. ": must be 50 bullets before firing", AssertEval(nBullets, "==", 50));
firer.changeWorld();
assertSatisfaction(playerSource.getSatisfaction(), tag);
// Note: this relies on sv_fastweapons 2.
nBullets = pawn.countInv("Clip");
it(tag .. ": must spend 1 bullet after firing", AssertEval(nBullets, "==", 49));
}
5. Activatable
5.1. Activatable
// This interface represents a game element that can be activated by the same // way the target is damaged. Such elements can be considered generic targets. class tt_Activatable abstract { abstract void activate(); abstract tt_Strings getCommands(); abstract bool isVisible(); }
5.1.1. Mock
class tt_ActivatableMock : tt_Activatable { static tt_ActivatableMock of() { return new("tt_ActivatableMock"); } mixin tt_Mock; override void activate() { if (_mock_activate_expectation == NULL) _mock_activate_expectation = _mock_addExpectation("activate"); ++_mock_activate_expectation.called; } void expect_activate(int expected = 1) { if (_mock_activate_expectation == NULL) _mock_activate_expectation = _mock_addExpectation("activate"); _mock_activate_expectation.expected = expected; _mock_activate_expectation.called = 0; } private tt_Expectation _mock_activate_expectation; override tt_Strings getCommands() { if (_mock_getCommands_expectation == NULL) _mock_getCommands_expectation = _mock_addExpectation("getCommands"); ++_mock_getCommands_expectation.called; return _mock_getCommands; } void expect_getCommands(tt_Strings value, int expected = 1) { if (_mock_getCommands_expectation == NULL) _mock_getCommands_expectation = _mock_addExpectation("getCommands"); _mock_getCommands_expectation.expected = expected; _mock_getCommands_expectation.called = 0; _mock_getCommands = value; } private tt_Strings _mock_getCommands; private tt_Expectation _mock_getCommands_expectation; override bool isVisible() { if (_mock_isVisible_expectation == NULL) _mock_isVisible_expectation = _mock_addExpectation("isVisible"); ++_mock_isVisible_expectation.called; return _mock_isVisible; } void expect_isVisible(bool value, int expected = 1) { if (_mock_isVisible_expectation == NULL) _mock_isVisible_expectation = _mock_addExpectation("isVisible"); _mock_isVisible_expectation.expected = expected; _mock_isVisible_expectation.called = 0; _mock_isVisible = value; } private bool _mock_isVisible; private tt_Expectation _mock_isVisible_expectation; }
5.2. PassThrough
class tt_PassThrough : tt_Activatable { static tt_PassThrough of(tt_PassThroughInputManager passThroughInputManager, tt_StringCvar passThroughSetting) { let result = new("tt_PassThrough"); result._inputManager = passThroughInputManager; result._passThroughSetting = passThroughSetting; return result; } override void activate() { _inputManager.setPassThrough(); } override tt_Strings getCommands() { return tt_Strings.ofOne(_passThroughSetting.get()); } override bool isVisible() { return true; } private tt_PassThroughInputManager _inputManager; private tt_StringSetting _passThroughSetting; }
5.3. CommandDispatcher
// Contains Activatables and activates() ones with commands matching answer. class tt_CommandDispatcher : tt_Activatable { static tt_CommandDispatcher of(tt_AnswerSource answerSource, Array<tt_Activatable> activatables, tt_AnswerReporter answerReporter, tt_AnswerStateSource answerStateSource, tt_BoolSetting fastConfirmation) { let result = new("tt_CommandDispatcher"); result._answerSource = answerSource; result._activatables.Copy(activatables); result._answerReporter = answerReporter; result._answerStateSource = answerStateSource; result._fastConfirmation = fastConfirmation; return result; } override void activate() { let answerState = _answerStateSource.getAnswerState(); if (!answerState.isReady() && !_fastConfirmation.get()) return; let answer = _answerSource.getAnswer(); let answerString = answer.getString(); foreach (activatable : _activatables) { bool isActivated = tryActivate(activatable, answerString); if (isActivated) { _answerReporter.reportMatch(); _answerSource.reset(); _answerStateSource.reset(); return; } } } override tt_Strings getCommands() { let result = tt_Strings.of(); foreach (activatable : _activatables) { if (!activatable.isVisible()) continue; let commands = activatable.getCommands(); uint nCommands = commands.size(); for (uint c = 0; c < nCommands; ++c) result.add(commands.at(c)); } return result; } override bool isVisible() { return true; } private bool tryActivate(tt_Activatable activatable, string answer) { let commands = activatable.getCommands(); uint nCommands = commands.size(); for (uint c = 0; c < nCommands; ++c) { string command = commands.at(c); bool isMatching = (command == answer); if (isMatching) { activatable.activate(); return true; } } return false; } private tt_AnswerSource _answerSource; private Array<tt_Activatable> _activatables; private tt_AnswerReporter _answerReporter; private tt_AnswerStateSource _answerStateSource; private tt_BoolSetting _fastConfirmation; }
5.3.1. Test
{
let tag = "tt_CommandDispatcher: checkActivate";
let env = tt_CommandDispatcherTestEnvironment.of();
let str = "Hello";
let answer = tt_Answer.of(str);
env.answerSource.expect_getAnswer(answer);
let commands1 = tt_Strings.of();
let commands2 = tt_Strings.of();
commands2.add(str);
env.activatable1.expect_getCommands(commands1);
env.activatable2.expect_getCommands(commands2);
env.activatable2.expect_activate();
env.answerReporter.expect_reportMatch();
env.answerStateSource
.expect_getAnswerState(tt_AnswerState.of(tt_AnswerState.Ready));
env.answerStateSource.expect_reset();
env.answerSource.expect_reset();
env.commandDispatcher.activate();
assertSatisfaction(env.getSatisfaction(), tag);
}
{
let tag = "tt_CommandDispatcher: checkGetCommands";
let env = tt_CommandDispatcherTestEnvironment.of();
let commands1 = tt_Strings.of();
let commands2 = tt_Strings.of();
commands1.add("1");
commands1.add("2");
commands2.add("3");
commands2.add("4");
env.activatable1.expect_getCommands(commands1);
env.activatable2.expect_getCommands(commands2);
env.activatable1.expect_isVisible(true);
env.activatable2.expect_isVisible(true);
let allCommands = env.commandDispatcher.getCommands();
let size = allCommands.size();
it("tt_CommandDispatcher: check get commands: All commands are collected",
AssertEval(size, "==", 4));
it("tt_CommandDispatcher: check get commands: The first command is collected",
Assert(allCommands.contains("1")));
it("tt_CommandDispatcher: check get commands: The second command is collected",
Assert(allCommands.contains("2")));
it("tt_CommandDispatcher: check get commands: The third command is collected",
Assert(allCommands.contains("3")));
it("tt_CommandDispatcher: check get commands: The forth command is collected",
Assert(allCommands.contains("4")));
assertSatisfaction(env.getSatisfaction(), tag);
}
class tt_CommandDispatcherTestEnvironment { static tt_CommandDispatcherTestEnvironment of() { let result = new("tt_CommandDispatcherTestEnvironment"); result.activatable1 = tt_ActivatableMock.of(); result.activatable2 = tt_ActivatableMock.of(); Array<tt_Activatable> activatables = {result.activatable1, result.activatable2}; result.answerSource = tt_AnswerSourceMock .of(); result.answerReporter = tt_AnswerReporterMock .of(); result.answerStateSource = tt_AnswerStateSourceMock.of(); result.fastConfirmation = tt_BoolSettingMock.of(); result.commandDispatcher = tt_CommandDispatcher.of(result.answerSource, activatables, result.answerReporter, result.answerStateSource, result.fastConfirmation); return result; } tt_Satisfaction getSatisfaction() const { return activatable1.getSatisfaction() .add(activatable2.getSatisfaction()) .add(answerSource.getSatisfaction()) .add(answerReporter.getSatisfaction()) .add(answerStateSource.getSatisfaction()) .add(fastConfirmation.getSatisfaction()); } tt_ActivatableMock activatable1; tt_ActivatableMock activatable2; tt_AnswerSourceMock answerSource; tt_AnswerReporterMock answerReporter; tt_AnswerStateSourceMock answerStateSource; tt_BoolSettingMock fastConfirmation; tt_CommandDispatcher commandDispatcher; }
6. Answer
6.1. Answer
// Represents an answer to a tt_Question. // See tt_Question. class tt_Answer { static tt_Answer of(String answer = "") { let result = new("tt_Answer"); result._answer = answer; return result; } string getString() const { return _answer; } void append(string character) { _answer = _answer .. character; } void deleteLastCharacter() { _answer.deleteLastCharacter(); } private string _answer; }
6.2. AnswerSource
// This interface represents a source of answers. class tt_AnswerSource : tt_KeyProcessor abstract { abstract tt_Answer getAnswer(); // Clears answer. abstract void reset(); }
6.2.1. Mock
class tt_AnswerSourceMock : tt_AnswerSource { static tt_AnswerSourceMock of() { return new("tt_AnswerSourceMock"); } mixin tt_Mock; override tt_Answer getAnswer() { if (_mock_getAnswer_expectation == NULL) _mock_getAnswer_expectation = _mock_addExpectation("getAnswer"); ++_mock_getAnswer_expectation.called; return _mock_getAnswer; } void expect_getAnswer(tt_Answer value, int expected = 1) { if (_mock_getAnswer_expectation == NULL) _mock_getAnswer_expectation = _mock_addExpectation("getAnswer"); _mock_getAnswer_expectation.expected = expected; _mock_getAnswer_expectation.called = 0; _mock_getAnswer = value; } private tt_Answer _mock_getAnswer; private tt_Expectation _mock_getAnswer_expectation; override void reset() { if (_mock_reset_expectation == NULL) _mock_reset_expectation = _mock_addExpectation("reset"); ++_mock_reset_expectation.called; } void expect_reset(int expected = 1) { if (_mock_reset_expectation == NULL) _mock_reset_expectation = _mock_addExpectation("reset"); _mock_reset_expectation.expected = expected; _mock_reset_expectation.called = 0; } private tt_Expectation _mock_reset_expectation; override void processKey(tt_Character character) { if (_mock_processKey_expectation == NULL) _mock_processKey_expectation = _mock_addExpectation("processKey"); ++_mock_processKey_expectation.called; } void expect_processKey(int expected = 1) { if (_mock_processKey_expectation == NULL) _mock_processKey_expectation = _mock_addExpectation("processKey"); _mock_processKey_expectation.expected = expected; _mock_processKey_expectation.called = 0; } private tt_Expectation _mock_processKey_expectation; }
6.3. InputBlockAfterCombat
// Implements tt_AnswerSource by taking another tt_AnswerSource, // and only passing keys to it if a key was pressed down after the game mode // has changed to Combat. class tt_InputBlockAfterCombat : tt_AnswerSource { static tt_InputBlockAfterCombat of(tt_AnswerSource answerSource, tt_ModeSource modeSource, tt_ModeSource oldModeSource) { let result = new("tt_InputBlockAfterCombat"); result._answerSource = answerSource; result._modeSource = modeSource; result._oldModeSource = oldModeSource; result._isLocked = false; return result; } void update() { int mode = _modeSource.getMode(); int oldMode = _oldModeSource.getMode(); if (oldMode != tt_Mode.Combat && mode == tt_Mode.Combat) { _isLocked = true; } } override tt_Answer getAnswer() { return _answerSource.getAnswer(); } override void processKey(tt_Character character) { if (character.getEventType() == UiEvent.Type_KeyDown) { _isLocked = false; } if (!_isLocked) { _answerSource.processKey(character); } } override void reset() {} private tt_AnswerSource _answerSource; private tt_ModeSource _modeSource; private tt_ModeSource _oldModeSource; private bool _isLocked; }
6.4. PlayerInput
// Implements tt_AnswerSource by receiving player key inputs and // composing an answer from them. class tt_PlayerInput : tt_AnswerSource { static tt_PlayerInput of(tt_ModeStorage modeStorage, tt_KeyPressReporter keyPressReporter) { let result = new("tt_PlayerInput"); result._modeStorage = modeStorage; result._keyPressReporter = keyPressReporter; result._answer = tt_Answer.of(); return result; } override tt_Answer getAnswer() { return _answer; } override void processKey(tt_Character character) { int type = character.getType(); switch (type) { case tt_Character.NONE: break; case tt_Character.PRINTABLE: _answer.append(character.getCharacter()); _keyPressReporter.report(); break; case tt_Character.BACKSPACE: _answer.deleteLastCharacter(); break; case tt_Character.CTRL_BACKSPACE: reset(); break; case tt_Character.ESCAPE: _modeStorage.setMode(tt_Mode.Explore); break; } } override void reset() { _answer = tt_Answer.of(); } private tt_ModeStorage _modeStorage; private tt_KeyPressReporter _keyPressReporter; private tt_Answer _answer; }
6.4.1. Test
{
let tag = "tt_PlayerInputTest: testPlayerInputCheckInput";
let env = tt_PlayerInputTestEnvironment.of();
string input = "abc";
env.throwStringIntoInput(input);
let answer = env.playerInput.getAnswer();
let answerString = answer.getString();
it(tag .. ": input must be an answer", Assert(input == answerString));
}
{
let tag = "tt_PlayerInputTest: testPlayerInputCheckReset";
let env = tt_PlayerInputTestEnvironment.of();
int TYPE_CHAR = UiEvent.Type_Char;
string input1 = "abc";
string input2 = "def";
env.throwStringIntoInput(input1);
let reset = tt_Character.of(TYPE_CHAR, tt_su_Ascii.BACKSPACE, true);
env.playerInput.processKey(reset);
env.throwStringIntoInput(input2);
let answer = env.playerInput.getAnswer();
let answerString = answer.getString();
it(tag .. ": second input must be an answer", Assert(input2 == answerString));
}
{
let tag = "tt_PlayerInputTest: testBackspace";
let env = tt_PlayerInputTestEnvironment.of();
int TYPE_CHAR = UiEvent.Type_Char;
let backspace = tt_Character.of(TYPE_CHAR, tt_su_Ascii.BACKSPACE, false);
let letterA = tt_Character.of(TYPE_CHAR, tt_su_Ascii.LATIN_SMALL_LETTER_A, false);
//env.playerInput.reset();
env.playerInput.processKey(backspace);
env.playerInput.processKey(letterA);
env.playerInput.processKey(backspace);
env.playerInput.processKey(letterA);
let answer = env.playerInput.getAnswer();
let answerString = answer.getString();
it(tag .. ": input after backspace must be valid", Assert(answerString == "a"));
}
{
let tag = "tt_PlayerInputTest: testCtrlBackspace";
let env = tt_PlayerInputTestEnvironment.of();
int TYPE_CHAR = UiEvent.Type_Char;
let ctrlBackspace = tt_Character.of(TYPE_CHAR, tt_su_Ascii.BACKSPACE, true);
let letterA = tt_Character.of(TYPE_CHAR, tt_su_Ascii.LATIN_SMALL_LETTER_A, false);
env.playerInput.processKey(letterA);
env.playerInput.processKey(letterA);
env.playerInput.processKey(ctrlBackspace);
let answer = env.playerInput.getAnswer();
let answerString = answer.getString();
it(tag .. ": input after ctrl-backspace must be empty", Assert(answerString == ""));
}
class tt_PlayerInputTestEnvironment { static tt_PlayerInputTestEnvironment of() { let result = new("tt_PlayerInputTestEnvironment"); result.modeStorage = tt_ModeStorageMock.of(); result.keyPressReporter = tt_KeyPressReporterMock.of(); result.playerInput = tt_PlayerInput.of(result.modeStorage, result.keyPressReporter); return result; } tt_Satisfaction getSatisfaction() const { return modeStorage.getSatisfaction().add(keyPressReporter.getSatisfaction()); } void throwStringIntoInput(string str) { uint inputSize = str.length(); for (uint i = 0; i < inputSize; ++i) { let character = tt_Character.of(TYPE_CHAR, str.ByteAt(i), false); playerInput.processKey(character); } let enter = tt_Character.of(TYPE_CHAR, tt_su_Ascii.CARRIAGE_RETURN_CR, false); playerInput.processKey(enter); } const TYPE_CHAR = UiEvent.Type_Char; tt_ModeStorageMock modeStorage; tt_KeyPressReporterMock keyPressReporter; tt_PlayerInput playerInput; }
7. Answer State
7.1. AnswerState
// Represents Answer state. // See tt_Answer class. class tt_AnswerState { static tt_AnswerState of(int state) { let result = new("tt_AnswerState"); result._state = state; return result; } enum _ { Unknown, Preparing, Ready, Finished } bool isReady() const { return (_state >= Ready); } bool isFinished() const { return (_state == Finished); } bool isEqual(tt_AnswerState other) { return _state == other._state; } private int _state; }
7.2. AnswerStateSource
// This interface provides access to tt_AnswerState. class tt_AnswerStateSource : tt_KeyProcessor abstract { abstract tt_AnswerState getAnswerState(); abstract void reset(); }
7.2.1. Mock
class tt_AnswerStateSourceMock : tt_AnswerStateSource { static tt_AnswerStateSourceMock of() { return new("tt_AnswerStateSourceMock"); } mixin tt_Mock; override tt_AnswerState getAnswerState() { if (_mock_getAnswerState_expectation == NULL) _mock_getAnswerState_expectation = _mock_addExpectation("getAnswerState"); ++_mock_getAnswerState_expectation.called; return _mock_getAnswerState; } void expect_getAnswerState(tt_AnswerState value, int expected = 1) { if (_mock_getAnswerState_expectation == NULL) _mock_getAnswerState_expectation = _mock_addExpectation("getAnswerState"); _mock_getAnswerState_expectation.expected = expected; _mock_getAnswerState_expectation.called = 0; _mock_getAnswerState = value; } private tt_AnswerState _mock_getAnswerState; private tt_Expectation _mock_getAnswerState_expectation; override void reset() { if (_mock_reset_expectation == NULL) _mock_reset_expectation = _mock_addExpectation("reset"); ++_mock_reset_expectation.called; } void expect_reset(int expected = 1) { if (_mock_reset_expectation == NULL) _mock_reset_expectation = _mock_addExpectation("reset"); _mock_reset_expectation.expected = expected; _mock_reset_expectation.called = 0; } private tt_Expectation _mock_reset_expectation; override void processKey(tt_Character character) { if (_mock_processKey_expectation == NULL) _mock_processKey_expectation = _mock_addExpectation("processKey"); ++_mock_processKey_expectation.called; } void expect_processKey(int expected = 1) { if (_mock_processKey_expectation == NULL) _mock_processKey_expectation = _mock_addExpectation("processKey"); _mock_processKey_expectation.expected = expected; _mock_processKey_expectation.called = 0; } private tt_Expectation _mock_processKey_expectation; }
7.3. PressedAnswerState
// Implements tt_AnswerState by observing Enter and Space keys. // // The state is: // - Preparing when no Enter or Space key is pressed. // - Ready when Enter or Space key is pressed, but not yet released. // - Finished when Enter or Space key is released. // // Note: space acts the same as Enter key, see tt_Character class for details. class tt_PressedAnswerState : tt_AnswerStateSource { static tt_PressedAnswerState of() { let result = new("tt_PressedAnswerState"); result._answerState = DEFAULT_STATE; return result; } override void processKey(tt_Character character) { switch (character.getType()) { case tt_Character.ENTER: _answerState = tt_AnswerState.Ready; break; case tt_Character.ENTER_UP: _answerState = tt_AnswerState.Finished; break; case tt_Character.NONE: break; default: _answerState = tt_AnswerState.Preparing; break; } } override tt_AnswerState getAnswerState() { return tt_AnswerState.of(_answerState); } override void reset() { _answerState = DEFAULT_STATE; } const DEFAULT_STATE = tt_AnswerState.Preparing; private int _answerState; }
8. Character
8.1. Character
// Represents a character. class tt_Character { static tt_Character of(int type, int code, bool isCtrl) { let result = new("tt_Character"); result._eventType = type; //Console.printf("type: %d, code: %d", type, code); // Normally, KeyUp events aren't registered, but releasing Enter or Space // key has special meaning, important for Hold Fire feature. if (type == UiEvent.Type_KeyUp && (code == tt_su_Ascii.CARRIAGE_RETURN_CR || code == tt_su_Ascii.SPACE)) { result._type = ENTER_UP; return result; } bool isChar = (type == UiEvent.Type_Char); bool isDown = (type == UiEvent.Type_KeyDown); bool isRepeat = (type == UiEvent.Type_KeyRepeat); bool isControl = (code == tt_su_Ascii.BACKSPACE || code == tt_su_Ascii.CARRIAGE_RETURN_CR || code == tt_su_Ascii.SPACE || code == tt_su_Ascii.ESCAPE); if (!isChar && !((isDown || isRepeat) && isControl)) { result._type = NONE; return result; } if (code == tt_su_Ascii.BACKSPACE) result._type = isCtrl ? CTRL_BACKSPACE : BACKSPACE; else if (code == tt_su_Ascii.DELETE) result._type = CTRL_BACKSPACE; else if (code == tt_su_Ascii.CARRIAGE_RETURN_CR) result._type = ENTER; else if (code == tt_su_Ascii.SPACE) result._type = ENTER; else if (code == tt_su_Ascii.ESCAPE) result._type = ESCAPE; else if (code < tt_su_Ascii.FIRST_PRINTABLE) result._type = NONE; else { result._type = PRINTABLE; result._character = string.format("%c", code); } return result; } enum _ { NONE, PRINTABLE, BACKSPACE, CTRL_BACKSPACE, ENTER, ENTER_UP, ESCAPE, } int getType() const { return _type; } string getCharacter() const { return _character; } int getEventType() const { return _eventType; } private int _type; private string _character; private int _eventType; }
8.1.1. Tests
{
int TYPE_CHAR = UiEvent.Type_Char;
let c = tt_Character.of(TYPE_CHAR, tt_su_Ascii.LATIN_SMALL_LETTER_A, false);
it("tt_Character: Small character", Assert(c.getType() == tt_Character.PRINTABLE));
it("tt_Character: Small character", Assert(c.getCharacter() == "a"));
}
{
int TYPE_CHAR = UiEvent.Type_Char;
let c = tt_Character.of(TYPE_CHAR, tt_su_Ascii.LATIN_CAPITAL_LETTER_A, false);
it("tt_Character: Big character", Assert(c.getType() == tt_Character.PRINTABLE));
it("tt_Character: Big character", Assert(c.getCharacter() == "A"));
}
{
int TYPE_CHAR = UiEvent.Type_Char;
let c = tt_Character.of(TYPE_CHAR, tt_su_Ascii.DIGIT_FOUR, false);
it("tt_Character: Number", Assert(c.getType() == tt_Character.PRINTABLE));
it("tt_Character: Number", Assert(c.getCharacter() == "4"));
}
{
int TYPE_CHAR = UiEvent.Type_Char;
let c = tt_Character.of(TYPE_CHAR, tt_su_Ascii.BACKSPACE, false);
it("tt_Character: Backspace", Assert(c.getType() == tt_Character.BACKSPACE));
}
{
int TYPE_CHAR = UiEvent.Type_Char;
let c = tt_Character.of(TYPE_CHAR, tt_su_Ascii.CHARACTER_NULL, false);
it("tt_Character: Non-printable", Assert(c.getType() == tt_Character.NONE));
}
{
int TYPE_CHAR = UiEvent.Type_Char;
let c = tt_Character.of(TYPE_CHAR, tt_su_Ascii.BACKSPACE, true);
it( "tt_Character: Ctrl-Backspace",
Assert(c.getType() == tt_Character.CTRL_BACKSPACE));
}
{
int TYPE_CHAR = UiEvent.Type_Char;
let c = tt_Character.of(TYPE_CHAR, tt_su_Ascii.CARRIAGE_RETURN_CR, true);
it("tt_Character: Enter", Assert(c.getType() == tt_Character.ENTER));
}
9. Clock
9.1. Clock
// Provides access to time. class tt_Clock abstract { // Provides access to getting points in time. // Returns a moment in time. abstract int getNow(); // Provides a way to determine how many ticks passed since a moment in time. // moment: a moment in time, received from getNow(). // Returns a number of ticks since moment. abstract int since(int moment); }
9.1.1. Mock
class tt_ClockMock : tt_Clock { static tt_ClockMock of() { return new("tt_ClockMock"); } mixin tt_Mock; override int getNow() { if (_mock_getNow_expectation == NULL) _mock_getNow_expectation = _mock_addExpectation("getNow"); ++_mock_getNow_expectation.called; return _mock_getNow; } void expect_getNow(int value, int expected = 1) { if (_mock_getNow_expectation == NULL) _mock_getNow_expectation = _mock_addExpectation("getNow"); _mock_getNow_expectation.expected = expected; _mock_getNow_expectation.called = 0; _mock_getNow = value; } private int _mock_getNow; private tt_Expectation _mock_getNow_expectation; override int since(int moment) { if (_mock_since_expectation == NULL) _mock_since_expectation = _mock_addExpectation("since"); ++_mock_since_expectation.called; return _mock_since; } void expect_since(int value, int expected = 1) { if (_mock_since_expectation == NULL) _mock_since_expectation = _mock_addExpectation("since"); _mock_since_expectation.expected = expected; _mock_since_expectation.called = 0; _mock_since = value; } private int _mock_since; private tt_Expectation _mock_since_expectation; }
9.2. TotalClock
// Implements tt_Clock by getting total time since game start. class tt_TotalClock : tt_Clock { static tt_TotalClock of() { let result = new("tt_TotalClock"); return result; } override int getNow() { return Level.totalTime; } override int since(int moment) { return getNow() - moment; } }
9.2.1. Test
{
let clock = tt_TotalClock.of();
int now1 = clock.getNow();
int now2 = clock.getNow();
it("tt_TotalClock: now is now", AssertEval(now1, "==", now2));
int duration = clock.since(now1);
it("tt_TotalClock: no time passed", AssertEval(duration, "==", 0));
}
10. Event Reporters
10.1. AnswerReporter
// Interface for reporting answer matching events. class tt_AnswerReporter abstract { abstract void reportMatch(); abstract void reportNotMatch(); }
10.1.1. Mock
class tt_AnswerReporterMock : tt_AnswerReporter { static tt_AnswerReporterMock of() { return new("tt_AnswerReporterMock"); } mixin tt_Mock; override void reportMatch() { if (_mock_reportMatch_expectation == NULL) _mock_reportMatch_expectation = _mock_addExpectation("reportMatch"); ++_mock_reportMatch_expectation.called; } void expect_reportMatch(int expected = 1) { if (_mock_reportMatch_expectation == NULL) _mock_reportMatch_expectation = _mock_addExpectation("reportMatch"); _mock_reportMatch_expectation.expected = expected; _mock_reportMatch_expectation.called = 0; } private tt_Expectation _mock_reportMatch_expectation; override void reportNotMatch() { if (_mock_reportNotMatch_expectation == NULL) _mock_reportNotMatch_expectation = _mock_addExpectation("reportNotMatch"); ++_mock_reportNotMatch_expectation.called; } void expect_reportNotMatch(int expected = 1) { if (_mock_reportNotMatch_expectation == NULL) _mock_reportNotMatch_expectation = _mock_addExpectation("reportNotMatch"); _mock_reportNotMatch_expectation.expected = expected; _mock_reportNotMatch_expectation.called = 0; } private tt_Expectation _mock_reportNotMatch_expectation; }
10.2. SoundAnswerReporter
// Implements tt_AnswerReporter by playing a sound. class tt_SoundAnswerReporter : tt_AnswerReporter { static tt_SoundAnswerReporter of(tt_SoundPlayer soundPlayer) { let result = new("tt_SoundAnswerReporter"); result._soundPlayer = soundPlayer; return result; } override void reportMatch() { _soundPlayer.playSound("tt/match"); } override void reportNotMatch() { _soundPlayer.playSound("tt/not-match"); } private tt_SoundPlayer _soundPlayer; }
10.3. KeyPressReporter
// Interface for reporting key press events. class tt_KeyPressReporter abstract { abstract void report(); }
10.3.1. Mock
class tt_KeyPressReporterMock : tt_KeyPressReporter { static tt_KeyPressReporterMock of() { return new("tt_KeyPressReporterMock"); } mixin tt_Mock; override void report() { if (_mock_report_expectation == NULL) _mock_report_expectation = _mock_addExpectation("report"); ++_mock_report_expectation.called; } void expect_report(int expected = 1) { if (_mock_report_expectation == NULL) _mock_report_expectation = _mock_addExpectation("report"); _mock_report_expectation.expected = expected; _mock_report_expectation.called = 0; } private tt_Expectation _mock_report_expectation; }
10.4. SoundKeyPressReporter
// Implements tt_KeyPressReporter by playing a sound. // The sound won't play if it's disabled in settings. class tt_SoundKeyPressReporter : tt_KeyPressReporter { static tt_SoundKeyPressReporter of(tt_SoundPlayer soundPlayer, tt_BoolSetting isEnabledSetting) { let result = new("tt_SoundKeyPressReporter"); result._soundPlayer = soundPlayer; result._isEnabledSetting = isEnabledSetting; return result; } override void report() { if (_isEnabledSetting.get()) _soundPlayer.playSound("tt/click"); } private tt_SoundPlayer _soundPlayer; private tt_BoolSetting _isEnabledSetting; }
10.5. ModeReporter
// Interface for reporting mode change events. class tt_ModeReporter abstract { abstract void report(int mode); }
10.5.1. Mock
class tt_ModeReporterMock : tt_ModeReporter { static tt_ModeReporterMock of() { return new("tt_ModeReporterMock"); } mixin tt_Mock; override void report(int mode) { if (_mock_report_expectation == NULL) _mock_report_expectation = _mock_addExpectation("report"); ++_mock_report_expectation.called; } void expect_report(int expected = 1) { if (_mock_report_expectation == NULL) _mock_report_expectation = _mock_addExpectation("report"); _mock_report_expectation.expected = expected; _mock_report_expectation.called = 0; } private tt_Expectation _mock_report_expectation; }
10.6. SoundModeReporter
// Implements tt_ModeReporter by playing the corresponding sound for each mode. class tt_SoundModeReporter : tt_ModeReporter { static tt_SoundModeReporter of(tt_SoundPlayer soundPlayer) { let result = new("tt_SoundModeReporter"); result._soundPlayer = soundPlayer; return result; } override void report(int mode) { switch (mode) { case tt_Mode.Unknown: Console.printf("%s: report: unknown mode!", getClassName()); break; case tt_Mode.Combat: _soundPlayer.playSound("tt/combat"); break; case tt_Mode.Explore: _soundPlayer.playSound("tt/explore"); break; case tt_Mode.None: break; } } private tt_SoundPlayer _soundPlayer; }
10.7. SoundPlayer
// This is an interface for playing sounds. class tt_SoundPlayer abstract { abstract void playSound(String soundId); }
10.8. PlayerSoundPlayer
// Implements tt_SoundPlayer by playing sounds for a player. // The sounds won't play if they are disabled in settings. class tt_PlayerSoundPlayer : tt_SoundPlayer { static tt_PlayerSoundPlayer of(tt_PlayerSource playerSource, tt_BoolSetting enabledSetting, tt_IntSetting themeSetting) { let result = new("tt_PlayerSoundPlayer"); result._playerSource = playerSource; result._enabledSetting = enabledSetting; result._themeSetting = themeSetting; return result; } override void playSound(String soundId) { if (isDisabled()) return; let player = _playerSource.getPawn(); int theme = _themeSetting.get(); soundId.appendFormat("%d", theme); player.a_StartSound(soundId, CHAN_AUTO, SOUND_FLAGS); } private bool isDisabled() { return (!_enabledSetting.get()); } const SOUND_FLAGS = CHANF_UI | CHANF_OVERLAP | CHANF_LOCAL; private tt_PlayerSource _playerSource; private tt_BoolSetting _enabledSetting; private tt_IntSetting _themeSetting; }
10.9. Sounds
// Global Typist sound settings //////////////////////////////////////////////// // Do not randomize pitch shift value. $pitchshiftrange 0 // 1. Default sound theme ////////////////////////////////////////////////////// tt/combat1 "sounds/Default/danger1.ogg" tt/explore1 "sounds/Default/safe1.ogg" tt/click1-1 "sounds/Default/typea1.ogg" tt/click1-2 "sounds/Default/typea2.ogg" tt/click1-3 "sounds/Default/typea3.ogg" tt/click1-4 "sounds/Default/typea4.ogg" tt/click1-5 "sounds/Default/typea5.ogg" tt/match1 "sounds/Default/success1.ogg" tt/not-match1 "sounds/Default/fail1.ogg" $random tt/click1 { tt/click1-1 tt/click1-2 tt/click1-3 tt/click1-4 tt/click1-5 } $volume tt/combat1 0.4 $volume tt/explore1 0.6 $volume tt/match1 0.4 // 2. SNES sound theme ///////////////////////////////////////////////////////// tt/combat2 "sounds/SNES/danger2.ogg" tt/explore2 "sounds/SNES/safe2.ogg" tt/click2-1 "sounds/SNES/typeb1.ogg" tt/click2-2 "sounds/SNES/typeb2.ogg" tt/click2-3 "sounds/SNES/typeb3.ogg" tt/click2-4 "sounds/SNES/typeb4.ogg" tt/click2-5 "sounds/SNES/typeb5.ogg" tt/match2 "sounds/SNES/success2.ogg" tt/not-match2 "sounds/SNES/sneserrors.ogg" $random tt/click2 { tt/click2-1 tt/click2-2 tt/click2-3 tt/click2-4 tt/click2-5 } // 4. Dakka sound theme //////////////////////////////////////////////////////// tt/combat4 "sounds/Dakka/danger4.ogg" tt/explore4 "sounds/Dakka/safe4.ogg" tt/click4-1 "sounds/Dakka/typed1.ogg" tt/click4-2 "sounds/Dakka/typed2.ogg" tt/click4-3 "sounds/Dakka/typed3.ogg" tt/click4-4 "sounds/Dakka/typed4.ogg" tt/click4-5 "sounds/Dakka/typed5.ogg" tt/match4 "sounds/Dakka/success4.ogg" tt/not-match4 "sounds/Dakka/fail4.ogg" $random tt/click4 { tt/click4-1 tt/click4-2 tt/click4-3 tt/click4-4 tt/click4-5 } // 5. GroceryStore sound theme ///////////////////////////////////////////////// tt/combat5 "sounds/GroceryStore/danger5.ogg" tt/explore5 "sounds/GroceryStore/safe5.ogg" tt/click5-1 "sounds/GroceryStore/typee1.ogg" tt/click5-2 "sounds/GroceryStore/typee2.ogg" tt/click5-3 "sounds/GroceryStore/typee3.ogg" tt/click5-4 "sounds/GroceryStore/typee4.ogg" tt/click5-5 "sounds/GroceryStore/typee5.ogg" tt/match5 "sounds/GroceryStore/success5.ogg" tt/not-match5 "sounds/GroceryStore/fail5.ogg" $random tt/click5 { tt/click5-1 tt/click5-2 tt/click5-3 tt/click5-4 tt/click5-5 } $volume tt/click5 0.2
11. Input Manager
11.1. InputByModeManager
// Implements tt_InputManager by examining the current and old Typist mode. // Input is reset when the game mode is changed. class tt_InputByModeManager : tt_InputManager { static tt_InputByModeManager of(tt_ModeSource modeSource, tt_PlayerInput playerInput) { let result = new("tt_InputByModeManager"); result._modeSource = modeSource; result._playerInput = playerInput; result._oldMode = tt_Mode.Unknown; return result; } override void manageInput() { int mode = _modeSource.getMode(); bool isCapturingKeys = (mode == tt_Mode.Combat); bool wasCapturingKeys = (_oldMode != tt_Mode.Combat); if (wasCapturingKeys && isCapturingKeys == false) { _playerInput.reset(); } _oldMode = mode; } override bool isCapturingKeys() { int mode = _modeSource.getMode(); return (mode == tt_Mode.Combat); } private tt_ModeSource _modeSource; private tt_PlayerInput _playerInput; private int _oldMode; }
11.2. PassThroughInputManager
// Doesn't capture keys when pass throug is set, otherwise acts as base. class tt_PassThroughInputManager : tt_InputManager { static tt_PassThroughInputManager of(tt_InputManager base) { let result = new("tt_PassThroughInputManager"); result._base = base; result._passThrough = PassThroughDisabled; return result; } override void manageInput() { _base.manageInput(); } override bool isCapturingKeys() { if (_passThrough != PassThroughDisabled) return false; return _base.isCapturingKeys(); } void setPassThrough() { _passThrough = WaitingForKeyDown; } void processInput(int type) { switch (_passThrough) { case PassThroughDisabled: return; case WaitingForKeyDown: if (type == InputEvent.Type_KeyDown) _passThrough = WaitingForKeyUp; return; case WaitingForKeyUp: if (type == InputEvent.Type_KeyUp) _passThrough = PassThroughDisabled; return; } } private tt_InputManager _base; private int _passThrough; enum _ { PassThroughDisabled, WaitingForKeyDown, WaitingForKeyUp } }
11.3. InputManager
// Helps managing user input. class tt_InputManager abstract { abstract void manageInput(); abstract bool isCapturingKeys(); }
12. Key Processor
12.1. KeyProcessor
// This interface represents an entity that processes input keys. class tt_KeyProcessor abstract { abstract void processKey(tt_Character character); }
12.2. KeyProcessors
// Implements tt_KeyProcessor interface by calling several instances // of tt_KeyProcessor. class tt_KeyProcessors : tt_KeyProcessor { static tt_KeyProcessors of(Array<tt_KeyProcessor> keyProcessors) { let result = new("tt_KeyProcessors"); result._keyProcessors.copy(keyProcessors); return result; } override void processKey(tt_Character character) { foreach (keyProcessor : _keyProcessors) keyProcessor.processKey(character); } private Array<tt_KeyProcessor> _keyProcessors; }
13. Known Target
13.1. KnownTarget
// Represents a target that already has been seen and registered. class tt_KnownTarget { static tt_KnownTarget of(tt_Target target, tt_Question question) { let result = new("tt_KnownTarget"); result._target = target; result._question = question; return result; } tt_Target getTarget() const { return _target; } tt_Question getQuestion() const { return _question; } private tt_Target _target; private tt_Question _question; }
13.2. KnownTargets
// Represents a list of known targets. class tt_KnownTargets { static tt_KnownTargets of() { return new("tt_KnownTargets"); } // Returns a target in this list. tt_KnownTarget at(uint index) const { return _targets[index]; } // Returns a number of targets in this list. uint size() const { return _targets.size(); } // Returns true if this target list contains a target with the specified id. bool contains(tt_Target target) const { return (find(target) != size()); } tt_KnownTarget findTarget(tt_Target target) const { uint index = find(target); return (index == size()) ? NULL : at(index); } // Adds a target to this list. void add(tt_KnownTarget target) { _targets.push(target); } void addMany(tt_KnownTargets targets) { uint nTargets = targets.size(); for (uint i = 0; i < nTargets; ++i) { _targets.push(targets.at(i)); } } // Removes a target from the list. // If the target is not in the list, does nothing. void remove(tt_Target target) { uint index = find(target); if (index != size()) { _targets.Delete(index); } } // Searches for a target with a particular id. // Returns index on success, the total number of targets on failure. private uint find(tt_Target target) const { uint nTargets = size(); for (uint i = 0; i < nTargets; ++i) { if (_targets[i].getTarget().isEqual(target)) { return i; } } return nTargets; } private Array<tt_KnownTarget> _targets; }
13.3. KnownTargetSource
// This interface represents a source of known targets. // See tt_KnownTarget. class tt_KnownTargetSource abstract { // Returns the currently registered (known) targets. abstract tt_KnownTargets getTargets() const; // Returns true if there are no targets in this source. abstract bool isEmpty() const; }
13.3.1. Mock
class tt_KnownTargetSourceMock : tt_KnownTargetSource { static tt_KnownTargetSourceMock of() { return new("tt_KnownTargetSourceMock"); } mixin tt_Mock; override tt_KnownTargets getTargets() { if (_mock_getTargets_expectation == NULL) _mock_getTargets_expectation = _mock_addExpectation("getTargets"); ++_mock_getTargets_expectation.called; return _mock_getTargets; } void expect_getTargets(tt_KnownTargets value, int expected = 1) { if (_mock_getTargets_expectation == NULL) _mock_getTargets_expectation = _mock_addExpectation("getTargets"); _mock_getTargets_expectation.expected = expected; _mock_getTargets_expectation.called = 0; _mock_getTargets = value; } private tt_KnownTargets _mock_getTargets; private tt_Expectation _mock_getTargets_expectation; override bool isEmpty() { if (_mock_isEmpty_expectation == NULL) _mock_isEmpty_expectation = _mock_addExpectation("isEmpty"); ++_mock_isEmpty_expectation.called; return _mock_isEmpty; } void expect_isEmpty(bool value, int expected = 1) { if (_mock_isEmpty_expectation == NULL) _mock_isEmpty_expectation = _mock_addExpectation("isEmpty"); _mock_isEmpty_expectation.expected = expected; _mock_isEmpty_expectation.called = 0; _mock_isEmpty = value; } private bool _mock_isEmpty; private tt_Expectation _mock_isEmpty_expectation; }
13.4. KnownTargetSourceCache
// Implements tt_KnownTargetSource by reading other // tt_KnownTargetSource only if the data is stale. class tt_KnownTargetSourceCache : tt_KnownTargetSource { static tt_KnownTargetSourceCache of(tt_KnownTargetSource targetSource, tt_StaleMarker staleMarker) { let result = new("tt_KnownTargetSourceCache"); result._targetSource = targetSource; result._staleMarker = staleMarker; return result; } override tt_KnownTargets getTargets() { ensureUpdated(); return _targets; } override bool isEmpty() { ensureUpdated(); return (_targets.size() == 0); } private void ensureUpdated() { if (_staleMarker.isStale()) { _targets = _targetSource.getTargets(); } } private tt_KnownTargetSource _targetSource; private tt_StaleMarker _staleMarker; private tt_KnownTargets _targets; }
13.5. TargetRegistry
// Implements tt_KnownTargetSource by reading from targets from // tt_TargetSource, assigning them questions, and storing them. // // Deactivated targets are removed from storage. class tt_TargetRegistry : tt_KnownTargetSource { static tt_TargetRegistry of(tt_TargetSource targetSource, tt_Lesson lesson, tt_TargetSource disabledTargetSource) { let result = new("tt_TargetRegistry"); result._targetSource = targetSource; result._lesson = lesson; result._disabledTargetSource = disabledTargetSource; result._registry = tt_KnownTargets.of(); return result; } override tt_KnownTargets getTargets() { update(); return _registry; } override bool isEmpty() { update(); return (_registry.size() == 0); } private void update() { let newTargets = _targetSource.getTargets(); merge(newTargets); let disabledTargets = _disabledTargetSource.getTargets(); subtract(disabledTargets); pruneNulls(); } // Adds targets that are not already registered to the registry. // // Given that tt_KnownTargets.contains() is O(n), this function is O(n^2). // Optimization possible. private void merge(tt_Targets targets) { uint nTargets = targets.size(); let newKnownTargets = tt_KnownTargets.of(); for (uint i = 0; i < nTargets; ++i) { let target = targets.at(i); let existing = _registry.findTarget(target); if (existing == NULL) { let knownTarget = makeKnownTarget(target); if (knownTarget != NULL) { newKnownTargets.add(knownTarget); } } } _registry.addMany(newKnownTargets); } // Given that tt_KnownTargets.remove() is at least O(n), this function is // at least O(n^2). // Optimization possible. private void subtract(tt_Targets targets) { uint nTargets = targets.size(); for (uint i = 0; i < nTargets; ++i) { _registry.remove(targets.at(i)); } } private tt_KnownTarget makeKnownTarget(tt_Target target) const { let question = _lesson.getQuestion(); if (question == NULL) { return NULL; } let newKnownTarget = tt_KnownTarget.of(target, question); return newKnownTarget; } private void pruneNulls() { let pruned = tt_KnownTargets.of(); uint nTargets = _registry.size(); for (uint i = 0; i < nTargets; ++i) { let target = _registry.at(i).getTarget(); let targetActor = target.getActor(); if (targetActor != NULL) { pruned.add(_registry.at(i)); } } _registry = pruned; } private tt_TargetSource _targetSource; private tt_Lesson _lesson; private tt_TargetSource _disabledTargetSource; private tt_KnownTargets _registry; }
13.5.1. Test
{
let tag = "tt_TargetRegistry: emptyCheck";
let env = tt_TargetRegistryTestEnvironment.of();
env.targetSource .expect_getTargets(tt_Targets.of());
env.disabledTargetSource.expect_getTargets(tt_Targets.of());
it(tag .. ": is empty", Assert(env.targetRegistry.isEmpty()));
assertSatisfaction(env.getSatisfaction(), tag);
}
{
let tag = "tt_TargetRegistry: addCheck";
let env = tt_TargetRegistryTestEnvironment.of();
let target1 = tt_Target.of(spawn("Demon", (0, 0, 0)));
let target2 = tt_Target.of(spawn("Demon", (0, 0, 0)));
let targets = tt_Targets.of();
targets.add(target1);
targets.add(target2);
env.targetSource.expect_getTargets(targets);
env.disabledTargetSource.expect_getTargets(tt_Targets.of());
env.lesson.expect_getQuestion(tt_QuestionMock.of(), 2);
let knownTargets = env.targetRegistry.getTargets();
it(tag .. ": is two targets", AssertEval(knownTargets.size(), "==", 2));
assertSatisfaction(env.getSatisfaction(), tag);
cleanUpSpawned();
}
{
let tag = "tt_TargetRegistry: addExistingCheck";
let env = tt_TargetRegistryTestEnvironment.of();
// First, add a single target.
let demon1 = spawn("Demon", (0, 0, 0));
let target = tt_Target.of(demon1);
let targets = tt_Targets.of();
targets.add(target);
env.targetSource.expect_getTargets(targets);
env.disabledTargetSource.expect_getTargets(tt_Targets.of());
env.lesson.expect_getQuestion(tt_QuestionMock.of());
let knownTargets = env.targetRegistry.getTargets();
it(tag .. ": is one target", AssertEval(knownTargets.size(), "==", 1));
assertSatisfaction(env.getSatisfaction(), tag);
// Second, add the same target again. Only a single target must remain
// registered.
env.targetSource.expect_getTargets(targets);
env.disabledTargetSource.expect_getTargets(tt_Targets.of());
env.lesson.expect_getQuestion(NULL, 0);
knownTargets = env.targetRegistry.getTargets();
it(tag .. ": is one target", AssertEval(knownTargets.size(), "==", 1));
assertSatisfaction(env.getSatisfaction(), tag);
cleanUpSpawned();
}
{
let tag = "tt_TargetRegistry: remove";
let env = tt_TargetRegistryTestEnvironment.of();
// First, add two targets.
let demon1 = spawn("Demon", (0, 0, 0));
let demon2 = spawn("Demon", (0, 0, 0));
let target1 = tt_Target.of(demon1);
let target2 = tt_Target.of(demon2);
let targets = tt_Targets.of();
targets.add(target1);
targets.add(target2);
env.targetSource.expect_getTargets(targets);
env.disabledTargetSource.expect_getTargets(tt_Targets.of());
env.lesson.expect_getQuestion(tt_QuestionMock.of(), 2);
let knownTargets = env.targetRegistry.getTargets();
it(tag .. ": is two targets", AssertEval(knownTargets.size(), "==", 2));
assertSatisfaction(env.getSatisfaction(), tag);
// Second, remove one target.
let disabledTarget = tt_Target.of(demon1);
let disabledTargets = tt_Targets.of();
disabledTargets.add(disabledTarget);
env.targetSource.expect_getTargets(tt_Targets.of());
env.disabledTargetSource.expect_getTargets(disabledTargets);
env.lesson.expect_getQuestion(NULL, 0);
knownTargets = env.targetRegistry.getTargets();
it(tag .. ": is one target now", AssertEval(knownTargets.size(), "==", 1));
assertSatisfaction(env.getSatisfaction(), tag);
cleanUpSpawned();
}
class tt_TargetRegistryTestEnvironment { static tt_TargetRegistryTestEnvironment of() { let result = new("tt_TargetRegistryTestEnvironment"); result.targetSource = tt_TargetSourceMock.of(); result.lesson = tt_LessonMock.of(); result.disabledTargetSource = tt_TargetSourceMock.of(); result.targetRegistry = tt_TargetRegistry.of(result.targetSource, result.lesson, result.disabledTargetSource); return result; } tt_Satisfaction getSatisfaction() const { return targetSource.getSatisfaction() .add(lesson.getSatisfaction()) .add(disabledTargetSource.getSatisfaction()); } tt_TargetSourceMock targetSource; tt_LessonMock lesson; tt_TargetSourceMock disabledTargetSource; tt_KnownTargetSource targetRegistry; }
13.6. VisibleKnownTargetSource
// Implements tt_KnownTargetSource by providing only targets visible to player. // Doesn't cache. class tt_VisibleKnownTargetSource : tt_KnownTargetSource { static tt_VisibleKnownTargetSource of(tt_KnownTargetSource base, tt_PlayerSource playerSource) { let result = new("tt_VisibleKnownTargetSource"); result._base = base; result._playerSource = playerSource; return result; } override tt_KnownTargets getTargets() const { let result = tt_KnownTargets.of(); if (_base.isEmpty()) return result; let pawn = _playerSource.getPawn(); let baseTargets = _base.getTargets(); uint targetCount = baseTargets.size(); for (uint i = 0; i < targetCount; ++i) { let target = baseTargets.at(i); if (isVisible(target, pawn)) result.add(target); } return result; } override bool isEmpty() const { if (_base.isEmpty()) return true; let pawn = _playerSource.getPawn(); let baseTargets = _base.getTargets(); uint targetCount = baseTargets.size(); for (uint i = 0; i < targetCount; ++i) if (isVisible(baseTargets.at(i), pawn)) return false; return true; } // Play-const hack: Actor.isVisible(...) is not const, but should be. private play bool isVisible(tt_KnownTarget target, Actor pawn) const { return pawn.isVisible(target.getTarget().getActor(), ALL_AROUND); } const ALL_AROUND = 1; // true private tt_KnownTargetSource _base; private tt_PlayerSource _playerSource; }
13.6.1. Tests
{
let tag = "tt_VisibleKnownTargetSource: no targets";
let env = tt_VisibleKnownTargetSourceTestEnvironment.of();
env.baseSource.expect_isEmpty(true, 2);
bool isEmpty = env.source.isEmpty();
let targets = env.source.getTargets();
it(tag .. "-> empty", Assert(isEmpty));
it(tag .. "-> no targets", AssertEval(targets.size(), "==", 0));
assertSatisfaction(env.getSatisfaction(), tag);
}
{
let tag = "tt_VisibleKnownTargetSource: visible targets";
let env = tt_VisibleKnownTargetSourceTestEnvironment.of();
let knownTargets = tt_KnownTargets.of();
let target = tt_Target.of(spawn("Demon", (0, 0, 0)));
let question = tt_QuestionMock.of();
let knownTarget = tt_KnownTarget.of(target, question);
knownTargets.add(knownTarget);
env.baseSource .expect_isEmpty(false, 2);
env.baseSource .expect_getTargets(knownTargets, 2);
env.playerSource.expect_getPawn(players[consolePlayer].mo, 2);
bool isEmpty = env.source.isEmpty();
let targets = env.source.getTargets();
it(tag .. "-> not empty", Assert(!isEmpty));
it(tag .. "-> targets", AssertEval(targets.size(), "==", 1));
assertSatisfaction(env.getSatisfaction(), tag);
cleanUpSpawned();
}
{
let tag = "tt_VisibleKnownTargetSource: invisible targets";
let env = tt_VisibleKnownTargetSourceTestEnvironment.of();
let knownTargets = tt_KnownTargets.of();
let target = tt_Target.of(spawn("Demon", (9999999, 0, 0)));
let question = tt_QuestionMock.of();
let knownTarget = tt_KnownTarget.of(target, question);
knownTargets.add(knownTarget);
env.baseSource .expect_isEmpty(false, 2);
env.baseSource .expect_getTargets(knownTargets, 2);
env.playerSource.expect_getPawn(players[consolePlayer].mo, 2);
bool isEmpty = env.source.isEmpty();
let targets = env.source.getTargets();
it(tag .. "-> empty", Assert(isEmpty));
it(tag .. "-> no targets", AssertEval(targets.size(), "==", 0));
assertSatisfaction(env.getSatisfaction(), tag);
cleanUpSpawned();
}
class tt_VisibleKnownTargetSourceTestEnvironment { static tt_VisibleKnownTargetSourceTestEnvironment of() { let result = new("tt_VisibleKnownTargetSourceTestEnvironment"); result.baseSource = tt_KnownTargetSourceMock.of(); result.playerSource = tt_PlayerSourceMock.of(); result.source = tt_VisibleKnownTargetSource.of(result.baseSource, result.playerSource); return result; } tt_Satisfaction getSatisfaction() const { return baseSource.getSatisfaction().add(playerSource.getSatisfaction()); } tt_KnownTargetSourceMock baseSource; tt_PlayerSourceMock playerSource; tt_VisibleKnownTargetSource source; }
14. Lesson
14.1. Lesson
// Interface for getting Questions. class tt_Lesson abstract { abstract tt_Question getQuestion(); }
14.1.1. Mock
class tt_LessonMock : tt_Lesson { static tt_LessonMock of() { return new("tt_LessonMock"); } mixin tt_Mock; override tt_Question getQuestion() { if (_mock_getQuestion_expectation == NULL) _mock_getQuestion_expectation = _mock_addExpectation("getQuestion"); ++_mock_getQuestion_expectation.called; return _mock_getQuestion; } void expect_getQuestion(tt_Question value, int expected = 1) { if (_mock_getQuestion_expectation == NULL) _mock_getQuestion_expectation = _mock_addExpectation("getQuestion"); _mock_getQuestion_expectation.expected = expected; _mock_getQuestion_expectation.called = 0; _mock_getQuestion = value; } private tt_Question _mock_getQuestion; private tt_Expectation _mock_getQuestion_expectation; }
14.2. MathsLesson
// Implements tt_Lesson by composing arithmetic tasks. class tt_MathsLesson : tt_Lesson { static tt_MathsLesson of() { let result = new("tt_MathsLesson"); return result; } override tt_Question getQuestion() { int operation = random[typist](Addition, Division); switch (operation) { case Addition: return makeAdditionQuestion(); case Subtraction: return makeSubtractionQuestion(); case Multiplication: return makeMultiplicationQuestion(); case Division: return makeDivisionQuestion(); } Console.printf("%s: getQuestion: unknown operation!", getClassName()); return NULL; } private tt_Question makeAdditionQuestion() { int leftAddend = random[typist](11, 49); int rightAddend = random[typist](11, 50); int sum = leftAddend + rightAddend; string description = string.format("%d + %d", leftAddend, rightAddend); string answer = string.format("%d", sum); let question = tt_Match.of(answer, description); return question; } private tt_Question makeSubtractionQuestion() { int minuend = random[typist](50, 99); int subtrahend = random[typist](11, 50); int difference = minuend - subtrahend; string description = string.format("%d - %d", minuend, subtrahend); string answer = string.format("%d", difference); let question = tt_Match.of(answer, description); return question; } private tt_Question makeMultiplicationQuestion() { int multiplicand = random[typist](2, 9); int multiplier = random[typist](2, 9); int product = multiplicand * multiplier; string description = string.format("%d * %d", multiplicand, multiplier); string answer = string.format("%d", product); let question = tt_Match.of(answer, description); return question; } private tt_Question makeDivisionQuestion() { int quotient = random[typist](2, 9); int divisor = random[typist](2, 9); int dividend = quotient * divisor; string description = string.format("%d / %d", dividend, divisor); string answer = string.format("%d", quotient); let question = tt_Match.of(answer, description); return question; } enum Operations { Addition, Subtraction, Multiplication, Division, } }
{
let question = tt_MathsLesson.of().getQuestion();
it("tt_MathsLesson: question isn't equal to the answer",
AssertFalse(question.isRight(question.getDescription())));
}
14.3. MixedLesson
class tt_SwitchableLesson { static tt_SwitchableLesson of(tt_BoolSetting setting, tt_Lesson lesson) { let result = new("tt_SwitchableLesson"); result._setting = setting; result._lesson = lesson; return result; } bool isEnabled() { return _setting.get(); } tt_Lesson lesson() { return _lesson; } private tt_BoolSetting _setting; private tt_Lesson _lesson; } class tt_MixedLesson : tt_Lesson { static tt_MixedLesson of(Array<tt_SwitchableLesson> lessons) { let result = new("tt_MixedLesson"); result._lessons.move(lessons); return result; } override tt_Question getQuestion() { _enabledLessons.clear(); foreach (lesson : _lessons) if (lesson.isEnabled()) _enabledLessons.push(lesson.lesson()); uint nEnabledLessons = _enabledLessons.size(); if (nEnabledLessons == 0) { Console.printf("All lessons disabled"); return tt_FallbackQuestion.of(); } uint randomLessonIndex = random[typist](0, nEnabledLessons - 1); return _enabledLessons[randomLessonIndex].getQuestion(); } private Array<tt_SwitchableLesson> _lessons; private Array<tt_Lesson> _enabledLessons; }
14.4. RandomCharactersLesson
// Implements tt_Lesson by composing a question from groups // of characters enabled by settings. class tt_RandomCharactersLesson : tt_Lesson { static tt_RandomCharactersLesson of(tt_RandomCharactersLessonSettings settings) { let result = new("tt_RandomCharactersLesson"); result._settings = settings; return result; } override tt_Question getQuestion() { string characters = composeCharacterRange(); int length = _settings.getLessonLength(); string picked = pick(characters, length); if (picked.length() == 0) { Console.printf("Random characters lesson: no characters enabled"); return tt_FallbackQuestion.of(); } return tt_Match.of(picked, picked); } // This function is guaranteed to return non-empty strings. private string composeCharacterRange() { string characters; if (_settings.isUppercaseLettersEnabled()) characters.appendFormat("%s", UPPERCASE_LETTERS); if (_settings.isLowercaseLettersEnabled()) characters.appendFormat("%s", LOWERCASE_LETTERS); if (_settings.isNumbersEnabled()) characters.appendFormat("%s", NUMBERS); if (_settings.isPunctuationEnabled()) characters.appendFormat("%s", PUNCTUATION); if (_settings.isSymbolsEnabled()) { // GZDoom cannot handle "\\" in a string, so add it manually. characters.AppendFormat("%s%c", SYMBOLS, tt_su_Ascii.REVERSE_SOLIDUS); } if (_settings.isCustomCharactersEnabled()) { characters.AppendFormat("%s", _settings.getCustomCharacters()); } return characters; } // This function is guaranteed to return non-empty strings. private static string pick(string characters, int number) { if (characters.length() == 0) return ""; string result; int lastCharacter = characters.CodePointCount() - 1; for (int i = 0; i < number; ++i) { int randomIndex = random[typist](0, lastCharacter); int character = getCodePointAt(characters, randomIndex); result.appendFormat("%c", character); } return result; } // Attention! O(n) private static int getCodePointAt(String str, int index) { int letterCode; int charPos = 0; for (int i = 0; i <= index; ++i) { [letterCode, charPos] = str.GetNextCodePoint(charPos); } return letterCode; } const LOWERCASE_LETTERS = "abcdefghijklmnopqrstuvwxyz"; const UPPERCASE_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; const NUMBERS = "0123456789"; const PUNCTUATION = ",.();:-'\"?!/"; const SYMBOLS = "~`@#$%^&*+=[]{}<>|"; private tt_RandomCharactersLessonSettings _settings; }
14.5. RandomNumberSource
// Implements tt_Lesson by producing questions that contain // string composed from random numbers and should match exactly to the answers. class tt_RandomNumberSource : tt_Lesson { static tt_RandomNumberSource of() { let result = new("tt_RandomNumberSource"); return result; } override tt_Question getQuestion() { let stringLength = 3; let str = ""; for (int i = 0; i < stringLength; ++i) { int number = random[typist](tt_su_Ascii.DIGIT_ZERO, tt_su_Ascii.DIGIT_NINE); str.AppendFormat("%c", number); } let question = tt_Match.of(str, str); return question; } }
14.6. StringSet
// Implements tt_Lesson by reading a lump with words and // randomly selecting words from this lump. class tt_StringSet : tt_Lesson { static tt_StringSet of(String lumpName) { int lump = Wads.findLump(lumpName, 0, Wads.AnyNamespace); string contents = Wads.readLump(lump); Array<string> words; tt_su_su.splitByWords(contents, words); Array<string> filteredWords; filterWords(words, filteredWords); let result = new("tt_StringSet"); result._lumpName = lumpName; result._words.move(filteredWords); return result; } override tt_Question getQuestion() { int nWords = int(_words.size()); if (nWords == 0) { Console.printf("%s: getQuestion: no words in lump %s.", getClassName(), _lumpName); return tt_FallbackQuestion.of(); } int wordIndex = random[typist](0, nWords - 1); string word = _words[wordIndex]; let question = tt_Match.of(word, word); return question; } // Removes too short words, removes duplicates. private static void filterWords(Array<String> input, out Array<String> result) { // Use map to remove duplicates. Map<string, int> wordSet; foreach (word : input) { if (word.codePointCount() > 1) wordSet.insert(word, 0); } foreach (word, value : wordSet) result.push(word); } private string _lumpName; private Array<string> _words; }
{
let stringSet = tt_StringSet.of("tt_test_words");
let question = stringSet.getQuestion();
string description = question.getDescription();
it("tt_StringSet: Question must be valid", AssertNotNull(question));
it("tt_StringSet: Description", Assert(description == "привет"));
}
привет
15. Math
// Namespace for math-related functions. class tt_Math { static bool isInEffectiveRange(vector3 p1, vector3 p2) { double distance = (p1 - p2).length(); bool inRange = distance < MAX_DISTANCE; return inRange; } static vector3 crossProduct(vector3 u, vector3 v) { vector3 result; result.x = u.y * v.z - u.z * v.y; result.y = u.z * v.x - u.x * v.z; result.z = u.x * v.y - u.y * v.x; return result; } // Max effective distance. const MAX_DISTANCE = 700; }
16. Mode
16.1. Mode
// Represents the mode in which Typist operates. class tt_Mode { enum _ { Unknown, // Should never be used. Only for detecting uninitialized variables. Combat, // Typist is focused on destroying the targets. Explore, // Typist is focused on movement and exploration. None, // None of the above. } }
16.2. ModeSource
// This interface represents a source of modes. // See: tt_Mode. class tt_ModeSource abstract { abstract int getMode(); }
16.2.1. Mock
class tt_ModeSourceMock : tt_ModeSource { static tt_ModeSourceMock of() { return new("tt_ModeSourceMock"); } mixin tt_Mock; override int getMode() { if (_mock_getMode_expectation == NULL) _mock_getMode_expectation = _mock_addExpectation("getMode"); ++_mock_getMode_expectation.called; return _mock_getMode; } void expect_getMode(int value, int expected = 1) { if (_mock_getMode_expectation == NULL) _mock_getMode_expectation = _mock_addExpectation("getMode"); _mock_getMode_expectation.expected = expected; _mock_getMode_expectation.called = 0; _mock_getMode = value; } private int _mock_getMode; private tt_Expectation _mock_getMode_expectation; }
16.3. AutoModeSource
// Implements tt_ModeSource by examining the specified tt_KnownTargetSource. class tt_AutoModeSource : tt_ModeSource { static tt_AutoModeSource of(tt_KnownTargetSource knownTargetSource) { let result = new("tt_AutoModeSource"); result._knownTargetSource = knownTargetSource; return result; } override int getMode() { return _knownTargetSource.isEmpty() ? tt_Mode.Explore : tt_Mode.Combat; } private tt_KnownTargetSource _knownTargetSource; }
16.3.1. Tests
{
let tag = "tt_AutoModeSource: no targets";
let knownTargetSource = tt_KnownTargetSourceMock.of();
let autoModeSource = tt_AutoModeSource.of(knownTargetSource);
knownTargetSource.expect_isEmpty(true);
int mode = autoModeSource.getMode();
it(tag .. ": no targets -> Explore", AssertEval(mode, "==", tt_Mode.Explore));
assertSatisfaction(knownTargetSource.getSatisfaction(), tag);
}
{
let tag = "tt_AutoModeSource: targets";
let knownTargetSource = tt_KnownTargetSourceMock.of();
let autoModeSource = tt_AutoModeSource.of(knownTargetSource);
knownTargetSource.expect_isEmpty(false);
int mode = autoModeSource.getMode();
it(tag .. ": targets -> Combat", AssertEval(mode, "==", tt_Mode.Combat));
assertSatisfaction(knownTargetSource.getSatisfaction(), tag);
}
16.4. DelayedCombatModeSource
// Implements tt_ModeSource by reading other tt_ModeSource, and switching to // Exploration mode only if some time has passed or there is no enemies around. class tt_DelayedCombatModeSource : tt_ModeSource { static tt_DelayedCombatModeSource of(tt_Clock clock, tt_ModeSource modeSource, tt_TargetSource targetSource) { let result = new("tt_DelayedCombatModeSource"); result._clock = clock; result._modeSource = modeSource; result._targetSource = targetSource; result._switchDetected = false; result._oldMode = tt_Mode.None; result._switchToExploreMoment = 0; return result; } override int getMode() { int topMode = _modeSource.getMode(); if (topMode != tt_Mode.Explore) { // let others decide. _oldMode = topMode; return tt_Mode.None; } bool wasCombat = _oldMode == tt_Mode.Combat; bool isExplore = topMode == tt_Mode.Explore; bool areEnemiesAround = !_targetSource.getTargets().isEmpty(); if (wasCombat && isExplore && areEnemiesAround) { _switchDetected = true; _switchToExploreMoment = _clock.getNow(); } _oldMode = topMode; if (!_switchDetected) { return tt_Mode.None; } bool timeIsUp = _clock.since(_switchToExploreMoment) > DELAY; if (timeIsUp) { _switchDetected = false; } return timeIsUp ? tt_Mode.None : tt_Mode.Combat; } const DELAY = TICRATE * 1; // 1 second private tt_Clock _clock; private tt_ModeSource _modeSource; private tt_TargetSource _targetSource; private int _switchDetected; private int _oldMode; private int _switchToExploreMoment; }
// C - Combat Mode // E - Exploration Mode // N - None Mode (let other decide) // // |-----|-----|---------|-------------|--------|-------------------------| // | old | new | enemies | time is up? | result | test | // |-----|-----|---------|-------------|--------|-------------------------| // | * | C | * | * | None | checkNewCombat | // | C | E | no | * | None | checkNoEnemies | // | C | E | yes | no | Combat | checkEnemiesStillCombat | // | C | E | yes | yes | None | checkEnemiesTimeIsUp | // | E | * | * | * | None | checkOldExploration | // |-----|-----|---------|-------------|--------|-------------------------| { let tag = "tt_DelayedCombatModeSource: checkNewCombat"; let env = tt_DelayedCombatModeSourceTestEnvironment.of(); env.modeSource.expect_getMode(tt_Mode.Combat, 2); env.clock.expect_getNow(0, 0); env.clock.expect_since(0, 0); int result1 = env.delay.getMode(); it(tag .. ": new combat -> None", AssertEval(result1, "==", tt_Mode.None)); int result2 = env.delay.getMode(); it(tag .. ": again, combat -> None", AssertEval(result2, "==", tt_Mode.None)); assertSatisfaction(env.getSatisfaction(), tag); } { let tag = "tt_DelayedCombatModeSource: checkNoEnemies"; let env = tt_DelayedCombatModeSourceTestEnvironment.of(); // set up history: it was combat. env.modeSource.expect_getMode(tt_Mode.Combat); env.delay.getMode(); env.modeSource.expect_getMode(tt_Mode.Explore); env.targetSource.expect_getTargets(tt_Targets.of()); int result = env.delay.getMode(); it(tag .. ": no enemies", AssertEval(result, "==", tt_Mode.None)); assertSatisfaction(env.getSatisfaction(), tag); } { let tag = "tt_DelayedCombatModeSource: checkEnemiesStillCombat"; let env = tt_DelayedCombatModeSourceTestEnvironment.of(); // set up history: it was combat. env.modeSource.expect_getMode(tt_Mode.Combat); env.delay.getMode(); { // set expectations env.modeSource.expect_getMode(tt_Mode.Explore); let targets = tt_Targets.of(); targets.add(NULL); env.targetSource.expect_getTargets(targets); env.clock.expect_getNow(0); env.clock.expect_since(0); } int result = env.delay.getMode(); it(tag .. ": still combat", AssertEval(result, "==", tt_Mode.Combat)); assertSatisfaction(env.getSatisfaction(), tag); } { let tag = "tt_DelayedCombatModeSource: checkEnemiesTimeIsUp"; let env = tt_DelayedCombatModeSourceTestEnvironment.of(); // set up history: it was combat. env.modeSource.expect_getMode(tt_Mode.Combat); env.delay.getMode(); { // set expectations env.modeSource.expect_getMode(tt_Mode.Explore); let targets = tt_Targets.of(); targets.add(NULL); env.targetSource.expect_getTargets(targets); env.clock.expect_getNow(0); env.clock.expect_since(999); } int result = env.delay.getMode(); it(tag .. ": no more combat", AssertEval(result, "==", tt_Mode.None)); assertSatisfaction(env.getSatisfaction(), tag); } { let tag = "tt_DelayedCombatModeSource: checkOldExploration"; let env = tt_DelayedCombatModeSourceTestEnvironment.of(); env.modeSource.expect_getMode(tt_Mode.Explore, 2); env.clock.expect_getNow(0, 0); env.clock.expect_since(0, 0); env.targetSource.expect_getTargets(tt_Targets.of(), 2); int result1 = env.delay.getMode(); it(tag .. ": old Exploration -> None", AssertEval(result1, "==", tt_Mode.None)); int result2 = env.delay.getMode(); it(tag .. ": again, old Exploration -> None", AssertEval(result2, "==", tt_Mode.None)); assertSatisfaction(env.getSatisfaction(), tag); }
class tt_DelayedCombatModeSourceTestEnvironment { static tt_DelayedCombatModeSourceTestEnvironment of() { let result = new("tt_DelayedCombatModeSourceTestEnvironment"); result.clock = tt_ClockMock.of(); result.modeSource = tt_ModeSourceMock.of(); result.targetSource = tt_TargetSourceMock.of(); result.delay = tt_DelayedCombatModeSource.of(result.clock, result.modeSource, result.targetSource); return result; } tt_Satisfaction getSatisfaction() const { return clock.getSatisfaction() .add(modeSource.getSatisfaction()) .add(targetSource.getSatisfaction()); } tt_ClockMock clock; tt_ModeSourceMock modeSource; tt_TargetSourceMock targetSource; tt_DelayedCombatModeSource delay; }
16.5. ModeCascade
// Implements ModeSource by taking the first mode from ModeSources // list that is not NONE. class tt_ModeCascade : tt_ModeSource { static tt_ModeCascade of(Array<tt_ModeSource> modeSources) { let result = new("tt_ModeCascade"); result._modeSources.move(modeSources); return result; } override int getMode() { foreach (source : _modeSources) { int mode = source.getMode(); if (mode != tt_Mode.None) return mode; } return tt_Mode.None; } private Array<tt_ModeSource> _modeSources; }
{
Array<tt_ModeSource> sources;
let cascade = tt_ModeCascade.of(sources);
int mode = cascade.getMode();
it("tt_ModeCascade: check zero sources: No source -> no mode",
AssertEval(mode, "==", tt_Mode.None));
}
{
let source1 = tt_ModeSourceMock.of();
let source2 = tt_ModeSourceMock.of();
source1.expect_getMode(tt_Mode.Explore);
source2.expect_getMode(tt_Mode.Combat);
Array<tt_ModeSource> sources = {source1, source2};
int mode = tt_ModeCascade.of(sources).getMode();
it("tt_ModeCascade: check cascade first: Must be the first mode",
AssertEval(mode, "==", tt_Mode.Explore));
}
{
let source1 = tt_ModeSourceMock.of();
let source2 = tt_ModeSourceMock.of();
source1.expect_getMode(tt_Mode.None);
source2.expect_getMode(tt_Mode.Combat);
Array<tt_ModeSource> sources = {source1, source2};
int mode = tt_ModeCascade.of(sources).getMode();
it("tt_ModeCascade: check cascade second: Must be the second mode",
AssertEval(mode, "==", tt_Mode.Combat));
}
16.6. ModeStorage
// This is an interface for storing and retrieving mode. class tt_ModeStorage : tt_ModeSource abstract { abstract void setMode(int mode); }
16.6.1. Mock
class tt_ModeStorageMock : tt_ModeStorage { static tt_ModeStorageMock of() { return new("tt_ModeStorageMock"); } mixin tt_Mock; override void setMode(int mode) { if (_mock_setMode_expectation == NULL) _mock_setMode_expectation = _mock_addExpectation("setMode"); ++_mock_setMode_expectation.called; } void expect_setMode(int expected = 1) { if (_mock_setMode_expectation == NULL) _mock_setMode_expectation = _mock_addExpectation("setMode"); _mock_setMode_expectation.expected = expected; _mock_setMode_expectation.called = 0; } private tt_Expectation _mock_setMode_expectation; override int getMode() { if (_mock_getMode_expectation == NULL) _mock_getMode_expectation = _mock_addExpectation("getMode"); ++_mock_getMode_expectation.called; return _mock_getMode; } void expect_getMode(int value, int expected = 1) { if (_mock_getMode_expectation == NULL) _mock_getMode_expectation = _mock_addExpectation("getMode"); _mock_getMode_expectation.expected = expected; _mock_getMode_expectation.called = 0; _mock_getMode = value; } private int _mock_getMode; private tt_Expectation _mock_getMode_expectation; }
16.7. ReportedModeSource
// Implements tt_ModeSource by reading other mode source, and // reporting an event when the mode has changed. class tt_ReportedModeSource : tt_ModeSource { static tt_ReportedModeSource of(tt_ModeReporter reporter, tt_ModeSource modeSource) { let result = new("tt_ReportedModeSource"); result._reporter = reporter; result._modeSource = modeSource; result._oldMode = tt_Mode.None; return result; } override int getMode() { int newMode = _modeSource.getMode(); if (newMode != _oldMode) { if (_oldMode != tt_Mode.None) { _reporter.report(newMode); } _oldMode = newMode; } return newMode; } private tt_ModeReporter _reporter; private tt_ModeSource _modeSource; private int _oldMode; }
{
let tag = "tt_ReportedModeSource: checkInitial";
let env = tt_ReportedModeSourceTestEnvironment.of();
int expected = tt_Mode.Explore;
env.modeSource.expect_getMode(expected);
int mode = env.reportedMode.getMode();
it(tag .. ": explore after init", AssertEval(mode, "==", expected));
assertSatisfaction(env.getSatisfaction(), tag);
}
{
let tag = "tt_ReportedModeSource: checkExplorationToCombat";
let env = tt_ReportedModeSourceTestEnvironment.of();
env.reporter.expect_report();
env.modeSource.expect_getMode(tt_Mode.Explore);
int mode1 = env.reportedMode.getMode();
env.modeSource.expect_getMode(tt_Mode.Combat);
int mode2 = env.reportedMode.getMode();
assertSatisfaction(env.getSatisfaction(), tag);
}
{
let tag = "tt_ReportedModeSource: checkCombatToExploration";
let env = tt_ReportedModeSourceTestEnvironment.of();
env.reporter.expect_report();
env.modeSource.expect_getMode(tt_Mode.Combat);
int mode1 = env.reportedMode.getMode();
env.modeSource.expect_getMode(tt_Mode.Explore);
int mode2 = env.reportedMode.getMode();
assertSatisfaction(env.getSatisfaction(), tag);
}
class tt_ReportedModeSourceTestEnvironment { static tt_ReportedModeSourceTestEnvironment of() { let result = new("tt_ReportedModeSourceTestEnvironment"); result.reporter = tt_ModeReporterMock.of(); result.modeSource = tt_ModeSourceMock.of(); result.reportedMode = tt_ReportedModeSource.of(result.reporter, result.modeSource); return result; } tt_Satisfaction getSatisfaction() const { return reporter.getSatisfaction().add(modeSource.getSatisfaction()); } tt_ModeReporterMock reporter; tt_ModeSourceMock modeSource; tt_ReportedModeSource reportedMode; }
16.8. SettableMode
// Implements ModeStorage by simply storing the mode that was set. class tt_SettableMode : tt_ModeStorage { static tt_SettableMode of() { let result = new("tt_SettableMode"); result._mode = tt_Mode.None; return result; } override int getMode() { return _mode; } override void setMode(int mode) { _mode = mode; } private int _mode; }
{
let settableMode = tt_SettableMode.of();
int before = tt_Mode.Combat;
settableMode.setMode(before);
int after = settableMode.getMode();
it("tt_SettableMode: mode must be the same", AssertEval(before, "==", after));
}
16.9. AutomapModeSource
// Implements tt_ModeSource by choosing Explore mode on the automap. class tt_AutomapModeSource : tt_ModeSource { static tt_AutomapModeSource of() { return new("tt_AutomapModeSource"); } override int getMode() { return automapActive ? tt_Mode.Explore : tt_Mode.None; } }
17. Origin
17.1. Origin
// Represents a point in space. // Note that the Origin position cannot change once set. class tt_Origin { static tt_Origin of(vector3 pos) { let result = new("tt_Origin"); result._pos = pos; return result; } vector3 getVector() const { return _pos; } private vector3 _pos; }
17.2. OriginSource
// This interface represents a source of origins. class tt_OriginSource abstract { // Returns the origin (coordinate in 3D space). // Getting the origin doesn't change it. abstract tt_Origin getOrigin(); }
17.2.1. Mock
class tt_OriginSourceMock : tt_OriginSource { static tt_OriginSourceMock of() { return new("tt_OriginSourceMock"); } mixin tt_Mock; override tt_Origin getOrigin() { if (_mock_getOrigin_expectation == NULL) _mock_getOrigin_expectation = _mock_addExpectation("getOrigin"); ++_mock_getOrigin_expectation.called; return _mock_getOrigin; } void expect_getOrigin(tt_Origin value, int expected = 1) { if (_mock_getOrigin_expectation == NULL) _mock_getOrigin_expectation = _mock_addExpectation("getOrigin"); _mock_getOrigin_expectation.expected = expected; _mock_getOrigin_expectation.called = 0; _mock_getOrigin = value; } private tt_Origin _mock_getOrigin; private tt_Expectation _mock_getOrigin_expectation; }
17.3. HastyQuestionAnswerMatcher
// Implements OriginSource by finding an origin for a known target // that fits to for the answer. class tt_HastyQuestionAnswerMatcher : tt_OriginSource { static tt_HastyQuestionAnswerMatcher of(tt_KnownTargetSource knownTargetSource, tt_AnswerSource answerSource, tt_AnswerReporter reporter) { let result = new("tt_HastyQuestionAnswerMatcher"); result._knownTargetSource = knownTargetSource; result._answerSource = answerSource; result._reporter = reporter; return result; } override tt_Origin getOrigin() { let targets = _knownTargetSource.getTargets(); if (targets == NULL || targets.size() == 0) { return NULL; } let answer = _answerSource.getAnswer(); if (answer == NULL) { return NULL; } let result = findMatchedTarget(targets, answer); if (result != NULL) { _reporter.reportMatch(); _answerSource.reset(); } return result; } private tt_Origin findMatchedTarget(tt_KnownTargets targets, tt_Answer answer) { string answerString = answer.getString(); uint nTargets = targets.size(); for (uint i = 0; i < nTargets; ++i) { let target = targets.at(i); let question = target.getQuestion(); if (!question.isRight(answerString)) continue; let result = target.getTarget().getPosition(); return result; } return NULL; } private tt_KnownTargetSource _knownTargetSource; private tt_AnswerSource _answerSource; private tt_AnswerReporter _reporter; }
17.4. OriginSourceCache
// Implements OriginSource by reading other OriginSource only if origin is stale. class tt_OriginSourceCache : tt_OriginSource { static tt_OriginSourceCache of(tt_OriginSource originSource, tt_StaleMarker staleMarker) { let result = new("tt_OriginSourceCache"); result._originSource = originSource; result._staleMarker = staleMarker; result._origin = NULL; return result; } override tt_Origin getOrigin() { if (_staleMarker.isStale()) { _origin = _originSource.getOrigin(); } return _origin; } private tt_OriginSource _originSource; private tt_StaleMarker _staleMarker; private tt_Origin _origin; }
17.5. PlayerOriginSource
// Implements tt_OriginSource by providing the center of the player pawn. class tt_PlayerOriginSource : tt_OriginSource { static tt_PlayerOriginSource of(tt_PlayerSource playerSource) { let result = new("tt_PlayerOriginSource"); result._playerSource = playerSource; return result; } override tt_Origin getOrigin() { let pawn = _playerSource.getPawn(); let pos = pawn.pos; pos.z += pawn.height / 2; return tt_Origin.of(pos); } private tt_PlayerSource _playerSource; }
{
let tag = "tt_PlayerOriginSource";
double x = 1;
double y = 2;
double z = 3;
let player = PlayerPawn(spawn("DoomPlayer", (x, y, z)));
let playerSource = tt_PlayerSourceMock.of();
let originSource = tt_PlayerOriginSource.of(playerSource);
playerSource.expect_getPawn(player);
let origin = originSource.getOrigin().getVector();
it(tag .. ": X matches", AssertEval(x, "==", origin.x));
it(tag .. ": Y matches", AssertEval(y, "==", origin.y));
it(tag .. ": Z in range", AssertEval(z, "<=", origin.z));
it(tag .. ": Z in range", AssertEval(z + player.Height, ">=", origin.z));
assertSatisfaction(playerSource.getSatisfaction(), tag);
cleanUpSpawned();
}
17.6. QuestionAnswerMatcher
// Implements OriginSource by finding an origin for a known target that fits to // for the answer. Searches far matching target only if answer state is Ready. class tt_QuestionAnswerMatcher : tt_OriginSource { static tt_QuestionAnswerMatcher of(tt_KnownTargetSource knownTargetSource, tt_AnswerSource answerSource, tt_AnswerStateSource answerStateSource) { let result = new("tt_QuestionAnswerMatcher"); result._knownTargetSource = knownTargetSource; result._answerSource = answerSource; result._answerStateSource = answerStateSource; return result; } override tt_Origin getOrigin() { let targets = _knownTargetSource.getTargets(); if (targets == NULL || targets.size() == 0) { return NULL; } let answer = _answerSource.getAnswer(); if (answer == NULL) { return NULL; } let answerState = _answerStateSource.getAnswerState(); if (!answerState.isReady()) { return NULL; } let result = findMatchedTarget(targets, answer); return result; } private tt_Origin findMatchedTarget(tt_KnownTargets targets, tt_Answer answer) { string answerString = answer.getString(); uint nTargets = targets.size(); for (uint i = 0; i < nTargets; ++i) { let target = targets.at(i); let question = target.getQuestion(); if (!question.isRight(answerString)) continue; let result = target.getTarget().getPosition(); return result; } return NULL; } private tt_KnownTargetSource _knownTargetSource; private tt_AnswerSource _answerSource; private tt_AnswerStateSource _answerStateSource; }
17.6.1. Test
{
let tag = "tt_QuestionAnswerMatcher: checkNullKnownTargets";
let env = tt_QuestionAnswerMatcherTestEnvironment.of();
env.targetSource.expect_getTargets(NULL);
let origin = env.matcher.getOrigin();
it(tag .. ": NULL known targets -> NULL origin", AssertNull(origin));
assertSatisfaction(env.getSatisfaction(), tag);
}
{
let tag = "tt_QuestionAnswerMatcher: checkZeroKnownTargets";
let env = tt_QuestionAnswerMatcherTestEnvironment.of();
let targets = tt_KnownTargets.of();
env.targetSource.expect_getTargets(targets);
let origin = env.matcher.getOrigin();
it(tag .. "Zero known targets -> NULL origin", AssertNull(origin));
assertSatisfaction(env.getSatisfaction(), tag);
}
{
let tag = "tt_QuestionAnswerMatcher: checkNullKnownTarget";
let env = tt_QuestionAnswerMatcherTestEnvironment.of();
let targets = tt_KnownTargets.of();
targets.add(NULL);
env.targetSource.expect_getTargets(targets);
env.answerSource.expect_getAnswer(NULL);
let origin = env.matcher.getOrigin();
it(tag .. ": NULL known target -> NULL origin", AssertNull(origin));
assertSatisfaction(env.getSatisfaction(), tag);
}
{
let tag = "tt_QuestionAnswerMatcher: checkNullAnswer";
let env = tt_QuestionAnswerMatcherTestEnvironment.of();
let knownTargets = tt_KnownTargets.of();
let target = tt_Target.of(NULL);
let question = tt_QuestionMock.of();
let knownTarget = tt_KnownTarget.of(target, question);
knownTargets.add(knownTarget);
env.targetSource.expect_getTargets(knownTargets);
env.answerSource.expect_getAnswer(NULL);
let origin = env.matcher.getOrigin();
it(tag .. ": NULL answer -> NULL origin", AssertNull(origin));
assertSatisfaction(env.getSatisfaction(), tag);
}
{
let tag = "tt_QuestionAnswerMatcher: checkKnownTargetAndAnswerMatch";
let env = tt_QuestionAnswerMatcherTestEnvironment.of();
let knownTargets = tt_KnownTargets.of();
let target = tt_Target.of(spawn("Demon", (0, 0, 0)));
let question = tt_QuestionMock.of();
let knownTarget = tt_KnownTarget.of(target, question);
knownTargets.add(knownTarget);
env.targetSource.expect_getTargets(knownTargets);
question.expect_isRight(true);
let answer = tt_Answer.of("abc");
env.answerSource.expect_getAnswer(answer);
env.stateSource.expect_getAnswerState(tt_AnswerState.of(tt_AnswerState.Ready));
let origin = env.matcher.getOrigin();
assertSatisfaction(question.getSatisfaction(), tag);
it(tag .. ": match: valid origin", AssertNotNull(origin));
assertSatisfaction(env.getSatisfaction(), tag);
cleanUpSpawned();
}
{
let tag = "tt_QuestionAnswerMatcher: checkKnownTargetAndAnswerNoMatch";
let env = tt_QuestionAnswerMatcherTestEnvironment.of();
let knownTargets = tt_KnownTargets.of();
let target = tt_Target.of(NULL);
let question = tt_QuestionMock.of();
let knownTarget = tt_KnownTarget.of(target, question);
knownTargets.add(knownTarget);
env.targetSource.expect_getTargets(knownTargets);
question.expect_isRight(false);
let answer = tt_Answer.of("abc");
env.answerSource.expect_getAnswer(answer);
env.stateSource.expect_getAnswerState(tt_AnswerState.of(tt_AnswerState.Ready));
let origin = env.matcher.getOrigin();
assertSatisfaction(question.getSatisfaction(), tag);
it(tag .. ": no match: NULL origin" , AssertNull(origin));
assertSatisfaction(env.getSatisfaction(), tag);
}
class tt_QuestionAnswerMatcherTestEnvironment { static tt_QuestionAnswerMatcherTestEnvironment of() { let result = new("tt_QuestionAnswerMatcherTestEnvironment"); result.targetSource = tt_KnownTargetSourceMock.of(); result.answerSource = tt_AnswerSourceMock.of(); result.stateSource = tt_AnswerStateSourceMock.of(); result.matcher = tt_QuestionAnswerMatcher.of(result.targetSource, result.answerSource, result.stateSource); return result; } tt_Satisfaction getSatisfaction() const { return targetSource.getSatisfaction() .add(answerSource.getSatisfaction()) .add(stateSource.getSatisfaction()); } tt_KnownTargetSourceMock targetSource; tt_AnswerSourceMock answerSource; tt_AnswerStateSourceMock stateSource; tt_QuestionAnswerMatcher matcher; }
17.7. SelectableOriginSource
// Implements OriginSource by selecting one of the two supplied // origin sources based on a value of tt_BoolSetting. class tt_SelectableOriginSource : tt_OriginSource { static tt_SelectableOriginSource of(tt_OriginSource source1, tt_OriginSource source2, tt_BoolSetting fastConfirmation) { let result = new("tt_SelectableOriginSource"); result._source1 = source1; result._source2 = source2; result._fastConfirmation = fastConfirmation; return result; } override tt_Origin getOrigin() { return _fastConfirmation.get() ? _source1.getOrigin() : _source2.getOrigin(); } private tt_OriginSource _source1; private tt_OriginSource _source2; private tt_BoolSetting _fastConfirmation; }
17.8. ExternalOriginSource
// Implements tt_OriginSource by receiving the source from elsewhere. class tt_ExternalOriginSource : tt_OriginSource { static tt_ExternalOriginSource of() { let result = new("tt_ExternalOriginSource"); return result; } override tt_Origin getOrigin() { return _origin; } void setOrigin(tt_Origin origin) { _origin = origin; } private tt_Origin _origin; }
18. Player
18.1. PlayerSource
// Interface for getting player info and player pawn. class tt_PlayerSource abstract { abstract PlayerInfo getInfo(); abstract PlayerPawn getPawn(); abstract int getNumber(); }
18.1.1. Mock
class tt_PlayerSourceMock : tt_PlayerSource { static tt_PlayerSourceMock of() { return new("tt_PlayerSourceMock"); } mixin tt_Mock; override PlayerInfo getInfo() { if (_mock_getInfo_expectation == NULL) _mock_getInfo_expectation = _mock_addExpectation("getInfo"); ++_mock_getInfo_expectation.called; return _mock_getInfo; } void expect_getInfo(PlayerInfo value, int expected = 1) { if (_mock_getInfo_expectation == NULL) _mock_getInfo_expectation = _mock_addExpectation("getInfo"); _mock_getInfo_expectation.expected = expected; _mock_getInfo_expectation.called = 0; _mock_getInfo = value; } private PlayerInfo _mock_getInfo; private tt_Expectation _mock_getInfo_expectation; override PlayerPawn getPawn() { if (_mock_getPawn_expectation == NULL) _mock_getPawn_expectation = _mock_addExpectation("getPawn"); ++_mock_getPawn_expectation.called; return _mock_getPawn; } void expect_getPawn(PlayerPawn value, int expected = 1) { if (_mock_getPawn_expectation == NULL) _mock_getPawn_expectation = _mock_addExpectation("getPawn"); _mock_getPawn_expectation.expected = expected; _mock_getPawn_expectation.called = 0; _mock_getPawn = value; } private PlayerPawn _mock_getPawn; private tt_Expectation _mock_getPawn_expectation; override int getNumber() { if (_mock_getNumber_expectation == NULL) _mock_getNumber_expectation = _mock_addExpectation("getNumber"); ++_mock_getNumber_expectation.called; return _mock_getNumber; } void expect_getNumber(int value, int expected = 1) { if (_mock_getNumber_expectation == NULL) _mock_getNumber_expectation = _mock_addExpectation("getNumber"); _mock_getNumber_expectation.expected = expected; _mock_getNumber_expectation.called = 0; _mock_getNumber = value; } private int _mock_getNumber; private tt_Expectation _mock_getNumber_expectation; }
18.2. PlayerSourceImpl
// Implements tt_PlayerSource by returning player by player number. class tt_PlayerSourceImpl : tt_PlayerSource { static tt_PlayerSourceImpl of(int playerNumber) { let result = new ("tt_PlayerSourceImpl"); result._playerNumber = playerNumber; return result; } override PlayerInfo getInfo() { return players[_playerNumber]; } override PlayerPawn getPawn() { return getInfo().mo; } override int getNumber() { return _playerNumber; } private int _playerNumber; }
18.2.1. Test
{
// Info, unlike pawns, exist even for non-existent players.
for (int playerNumber = 0; playerNumber < MAXPLAYERS; ++playerNumber)
{
let source = tt_PlayerSourceImpl.of(playerNumber);
let info = source.getInfo();
let note = "tt_PlayerSourceImpl: player info (%d) must be not NULL";
it(string.format(note, playerNumber), Assert(info != NULL));
}
}
{
let source = tt_PlayerSourceImpl.of(consolePlayer);
let pawn = source.getPawn();
let note = "tt_PlayerSourceImpl: must get main player (%d) actor";
it(string.format(note, consolePlayer), AssertNotNull(pawn));
}
{
let note = "tt_PlayerSourceImpl: other player (%d) must be null";
// Since tests are run on single-player game, no other players must exist.
for (int i = 1; i < MAXPLAYERS; ++i)
{
int playerNumber = (consolePlayer + i) % MAXPLAYERS;
let source = tt_PlayerSourceImpl.of(playerNumber);
let pawn = source.getPawn();
it(string.format(note, playerNumber), AssertNull(pawn));
}
}
19. Question
19.1. Question
// This interface represents a question. class tt_Question abstract { abstract bool isRight(string answer); abstract string getDescription(); abstract string getHintFor(string answer); }
19.1.1. Mock
class tt_QuestionMock : tt_Question { static tt_QuestionMock of() { return new("tt_QuestionMock"); } mixin tt_Mock; override bool isRight(string answer) { if (_mock_isRight_expectation == NULL) _mock_isRight_expectation = _mock_addExpectation("isRight"); ++_mock_isRight_expectation.called; return _mock_isRight; } void expect_isRight(bool value, int expected = 1) { if (_mock_isRight_expectation == NULL) _mock_isRight_expectation = _mock_addExpectation("isRight"); _mock_isRight_expectation.expected = expected; _mock_isRight_expectation.called = 0; _mock_isRight = value; } private bool _mock_isRight; private tt_Expectation _mock_isRight_expectation; override string getDescription() { if (_mock_getDescription_expectation == NULL) _mock_getDescription_expectation = _mock_addExpectation("getDescription"); ++_mock_getDescription_expectation.called; return _mock_getDescription; } void expect_getDescription(string value, int expected = 1) { if (_mock_getDescription_expectation == NULL) _mock_getDescription_expectation = _mock_addExpectation("getDescription"); _mock_getDescription_expectation.expected = expected; _mock_getDescription_expectation.called = 0; _mock_getDescription = value; } private string _mock_getDescription; private tt_Expectation _mock_getDescription_expectation; override string getHintFor(string answer) { if (_mock_getHintFor_expectation == NULL) _mock_getHintFor_expectation = _mock_addExpectation("getHintFor"); ++_mock_getHintFor_expectation.called; return _mock_getHintFor; } void expect_getHintFor(string value, int expected = 1) { if (_mock_getHintFor_expectation == NULL) _mock_getHintFor_expectation = _mock_addExpectation("getHintFor"); _mock_getHintFor_expectation.expected = expected; _mock_getHintFor_expectation.called = 0; _mock_getHintFor = value; } private string _mock_getHintFor; private tt_Expectation _mock_getHintFor_expectation; }
19.2. FallbackQuestion
class tt_FallbackQuestion : tt_Question { static tt_FallbackQuestion of() { return new("tt_FallbackQuestion"); } override bool isRight(string answer) { return false; } override string getDescription() { return StringTable.localize("TT_FALLBACK_QUESTION"); } override string getHintFor(string answer) { return getDescription(); } }
19.3. Match
// Implements tt_Question. The answer is right for this kind of question if it // matches the string contained in this question. class tt_Match : tt_Question { static tt_Match of(string answer, string description) { let result = new("tt_Match"); result._answer = answer; result._description = description; return result; } override bool isRight(string answer) { return (_answer == answer); } override string getDescription() { return _description; } override string getHintFor(string answer) { return getColoredMatch(_answer, answer); } private static string getColoredMatch(string origin, string matched) { string result; int originLength = origin .codePointCount(); int matchedLength = matched.codePointCount(); int nChars = min(originLength, matchedLength); int originPos = 0; int matchedPos = 0; for (int i = 0; i < nChars; ++i) { let [originCode, nextOriginPos ] = origin .getNextCodePoint(originPos ); let [matchedCode, nextMatchedPos] = matched.getNextCodePoint(matchedPos); int colorCode = (originCode == matchedCode) ? tt_su_Ascii.HYPHEN_MINUS // Use the base color. : tt_TextColorCodes.WrongAnswer; result.appendFormat("\c%c%c", colorCode, matchedCode); originPos = nextOriginPos; matchedPos = nextMatchedPos; } // Everything that is beyond origin is wrong. if (matchedLength > originLength) { result.appendFormat("\c%c%s", tt_TextColorCodes.WrongAnswer, matched.mid(matchedPos)); } return result; } private string _answer; private string _description; }
20. Settings
20.1. BoolSetting
class tt_BoolSetting abstract { abstract bool isDefined(); abstract bool get(); }
20.1.1. Mock
class tt_BoolSettingMock : tt_BoolSetting { static tt_BoolSettingMock of() { return new("tt_BoolSettingMock"); } mixin tt_Mock; override bool isDefined() { if (_mock_isDefined_expectation == NULL) _mock_isDefined_expectation = _mock_addExpectation("isDefined"); ++_mock_isDefined_expectation.called; return _mock_isDefined; } void expect_isDefined(bool value, int expected = 1) { if (_mock_isDefined_expectation == NULL) _mock_isDefined_expectation = _mock_addExpectation("isDefined"); _mock_isDefined_expectation.expected = expected; _mock_isDefined_expectation.called = 0; _mock_isDefined = value; } private bool _mock_isDefined; private tt_Expectation _mock_isDefined_expectation; override bool get() { if (_mock_get_expectation == NULL) _mock_get_expectation = _mock_addExpectation("get"); ++_mock_get_expectation.called; return _mock_get; } void expect_get(bool value, int expected = 1) { if (_mock_get_expectation == NULL) _mock_get_expectation = _mock_addExpectation("get"); _mock_get_expectation.expected = expected; _mock_get_expectation.called = 0; _mock_get = value; } private bool _mock_get; private tt_Expectation _mock_get_expectation; }
20.2. IntSetting
class tt_IntSetting abstract { abstract bool isDefined(); abstract int get(); }
20.3. FloatSetting
class tt_FloatSetting abstract { abstract bool isDefined(); abstract double getFloat(); }
20.3.1. Mock
class tt_FloatSettingMock : tt_FloatSetting { static tt_FloatSettingMock of() { return new("tt_FloatSettingMock"); } mixin tt_Mock; override bool isDefined() { if (_mock_isDefined_expectation == NULL) _mock_isDefined_expectation = _mock_addExpectation("isDefined"); ++_mock_isDefined_expectation.called; return _mock_isDefined; } void expect_isDefined(bool value, int expected = 1) { if (_mock_isDefined_expectation == NULL) _mock_isDefined_expectation = _mock_addExpectation("isDefined"); _mock_isDefined_expectation.expected = expected; _mock_isDefined_expectation.called = 0; _mock_isDefined = value; } private bool _mock_isDefined; private tt_Expectation _mock_isDefined_expectation; override double getFloat() { if (_mock_getFloat_expectation == NULL) _mock_getFloat_expectation = _mock_addExpectation("getFloat"); ++_mock_getFloat_expectation.called; return _mock_getFloat; } void expect_getFloat(double value, int expected = 1) { if (_mock_getFloat_expectation == NULL) _mock_getFloat_expectation = _mock_addExpectation("getFloat"); _mock_getFloat_expectation.expected = expected; _mock_getFloat_expectation.called = 0; _mock_getFloat = value; } private double _mock_getFloat; private tt_Expectation _mock_getFloat_expectation; }
20.4. StringSetting
class tt_StringSetting abstract { abstract bool isDefined(); abstract string get(); }
20.5. BoolCvar
// Provides access to a user or server bool Cvar. class tt_BoolCvar : tt_BoolSetting { static tt_BoolCvar of(tt_PlayerSource playerSource, string name) { let result = new("tt_BoolCvar"); result._cvar = Cvar.getCvar(name, playerSource.getInfo()); if (result._cvar != NULL && result._cvar.getRealType() != Cvar.CVAR_Bool) throwAbortException("%s Cvar is not bool", name); return result; } override bool isDefined() { return (_cvar != NULL); } override bool get() { return _cvar.getInt(); } private Cvar _cvar; }
20.6. PositiveIntCvar
// Provides access to a user or server bool Cvar. Protects against values < 1. class tt_PositiveIntCvar : tt_IntSetting { static tt_PositiveIntCvar of(tt_PlayerSource playerSource, string name) { let result = new("tt_PositiveIntCvar"); result._cvar = Cvar.getCvar(name, playerSource.getInfo()); if (result._cvar != NULL && result._cvar.getRealType() != Cvar.CVAR_Int) throwAbortException("%s Cvar is not int", name); return result; } override bool isDefined() { return (_cvar != NULL); } override int get() { return max(1, _cvar.getInt()); } private Cvar _cvar; }
20.7. IntCvar
// Provides access to a user or server bool Cvar. class tt_IntCvar : tt_IntSetting { static tt_IntCvar of(tt_PlayerSource playerSource, string name) { let result = new("tt_IntCvar"); result._cvar = Cvar.getCvar(name, playerSource.getInfo()); if (result._cvar != NULL && result._cvar.getRealType() != Cvar.CVAR_Int) throwAbortException("%s Cvar is not int", name); return result; } override bool isDefined() { return (_cvar != NULL); } override int get() { return _cvar.getInt(); } private Cvar _cvar; }
20.8. FloatCvar
// Provides access to a user or server float Cvar. class tt_FloatCvar : tt_FloatSetting { static tt_FloatCvar of(tt_PlayerSource playerSource, string name) { let result = new("tt_FloatCvar"); result._cvar = Cvar.getCvar(name, playerSource.getInfo()); if (result._cvar != NULL && result._cvar.getRealType() != Cvar.CVAR_Float) throwAbortException("%s Cvar is not float", name); return result; } override bool isDefined() { return (_cvar != NULL); } override double getFloat() { return _cvar.getFloat(); } private Cvar _cvar; }
20.9. StringCvar
// Provides access to a user or server string Cvar. class tt_StringCvar : tt_StringSetting { static tt_StringCvar of(tt_PlayerSource playerSource, string name) { let result = new("tt_StringCvar"); result._cvar = Cvar.getCvar(name, playerSource.getInfo()); if (result._cvar != NULL && result._cvar.getRealType() != Cvar.CVAR_String) throwAbortException("%s Cvar is not string", name); return result; } override bool isDefined() { return (_cvar != NULL); } override string get() { return _cvar.getString(); } private Cvar _cvar; }
20.10. RandomCharactersLessonSettings
// Represents settings for tt_RandomCharactersLesson. class tt_RandomCharactersLessonSettings abstract { abstract int getLessonLength(); abstract bool isUppercaseLettersEnabled(); abstract bool isLowercaseLettersEnabled(); abstract bool isNumbersEnabled(); abstract bool isPunctuationEnabled(); abstract bool isSymbolsEnabled(); abstract bool isCustomCharactersEnabled(); abstract string getCustomCharacters(); }
20.10.1. RandomCharactersLessonSettingsImpl
// Random Character Lesson configuration user int tt_rc_length = 3; user bool tt_rc_uppercase_letters_enabled = false; user bool tt_rc_lowercase_letters_enabled = true; user bool tt_rc_numbers_enabled = false; user bool tt_rc_punctuation_enabled = false; user bool tt_rc_symbols_enabled = false; user bool tt_rc_custom_enabled = false; user string tt_rc_custom = "";
// Implements tt_RandomCharactersLessonSettings by returning Cvar contents. class tt_RandomCharactersLessonSettingsImpl : tt_RandomCharactersLessonSettings { static tt_RandomCharactersLessonSettingsImpl of(tt_PlayerSource playerSource) { let result = new("tt_RandomCharactersLessonSettingsImpl"); result._lessonLength = tt_PositiveIntCvar.of(playerSource, "tt_rc_length"); result._isUppercaseEnabled = tt_BoolCvar.of(playerSource, "tt_rc_uppercase_letters_enabled"); result._isLowercaseEnabled = tt_BoolCvar.of(playerSource, "tt_rc_lowercase_letters_enabled"); result._isNumbersEnabled = tt_BoolCvar.of(playerSource, "tt_rc_numbers_enabled"); result._isPunctuationEnabled = tt_BoolCvar.of(playerSource, "tt_rc_punctuation_enabled"); result._isSymbolsEnabled = tt_BoolCvar.of(playerSource, "tt_rc_symbols_enabled"); result._isCustomEnabled = tt_BoolCvar.of(playerSource, "tt_rc_custom_enabled"); result._customCharacters = tt_StringCvar.of(playerSource, "tt_rc_custom"); return result; } override int getLessonLength() { return _lessonLength.get(); } override bool isUppercaseLettersEnabled() { return _isUppercaseEnabled.get(); } override bool isLowercaseLettersEnabled() { return _isLowercaseEnabled.get(); } override bool isNumbersEnabled() { return _isNumbersEnabled.get(); } override bool isPunctuationEnabled() { return _isPunctuationEnabled.get(); } override bool isSymbolsEnabled() { return _isSymbolsEnabled.get(); } override bool isCustomCharactersEnabled() { return _isCustomEnabled.get(); } override string getCustomCharacters() { return _customCharacters.get(); } private tt_PositiveIntCvar _lessonLength; private tt_BoolCvar _isUppercaseEnabled; private tt_BoolCvar _isLowercaseEnabled; private tt_BoolCvar _isNumbersEnabled; private tt_BoolCvar _isPunctuationEnabled; private tt_BoolCvar _isSymbolsEnabled; private tt_BoolCvar _isCustomEnabled; private tt_StringCvar _customCharacters; }
21. Stale Marker
21.1. StaleMarker
// This interface provides information when its instance becomes stale. class tt_StaleMarker abstract { // Update stale status. // Attention! Calling this function may change the state of tt_StaleMarker. // Returns true if this instance is currently stale. abstract bool isStale(); }
21.2. StaleMarkerImpl
// Implements tt_StaleMarker by observing a tt_Clock. class tt_StaleMarkerImpl : tt_StaleMarker { // Creates an instance of tt_StaleMarkerImpl. // clock: dependency, a clock to be observed. // updateTicks: in how much ticks this marker becomes stale. static tt_StaleMarkerImpl of(tt_Clock clock, int updateTicks = 1) { let result = new("tt_StaleMarkerImpl"); result._clock = clock; result._updateTicks = updateTicks; result._isEmpty = true; result._oldMoment = 0; return result; } override bool isStale() { if (!shouldUpdate()) return false; _oldMoment = _clock.getNow(); _isEmpty = false; return true; } private bool shouldUpdate() const { if (_isEmpty) return true; int passed = _clock.since(_oldMoment); bool isObsolete = (passed >= _updateTicks); return isObsolete; } private tt_Clock _clock; private int _updateTicks; private bool _isEmpty; private int _oldMoment; }
21.2.1. Tests
{
let tag = "tt_StaleMarker: checkFirstRead";
let env = tt_StaleMarkerImplTestEnvironment.of();
env.clock.expect_getNow(0);
bool isStale = env.staleMarker.isStale();
it(tag .. ": first read: stale", Assert(isStale));
assertSatisfaction(env.getSatisfaction(), tag);
}
{
let tag = "tt_StaleMarker: checkNotYetStale";
let env = tt_StaleMarkerImplTestEnvironment.of();
env.clock.expect_getNow(0);
bool isStale1 = env.staleMarker.isStale();
env.clock.expect_since(0);
bool isStale2 = env.staleMarker.isStale();
it(tag .. ": same tick: not stale", Assert(!isStale2));
assertSatisfaction(env.getSatisfaction(), tag);
}
{
let tag = "tt_StaleMarker: checkAlreadyStale";
let env = tt_StaleMarkerImplTestEnvironment.of();
env.clock.expect_getNow(0, 2);
bool isStale1 = env.staleMarker.isStale();
env.clock.expect_since(1);
bool isStale2 = env.staleMarker.isStale();
it(tag .. ": new tick: stale", Assert(isStale2));
assertSatisfaction(env.getSatisfaction(), tag);
}
class tt_StaleMarkerImplTestEnvironment { static tt_StaleMarkerImplTestEnvironment of() { let result = new("tt_StaleMarkerImplTestEnvironment"); result.clock = tt_ClockMock.of(); result.staleMarker = tt_StaleMarkerImpl.of(result.clock, 1); return result; } tt_Satisfaction getSatisfaction() const { return clock.getSatisfaction(); } tt_ClockMock clock; tt_StaleMarker staleMarker; }
22. Strings
22.1. Strings
// Represents a set of strings. class tt_Strings { static tt_Strings of() { let result = new("tt_Strings"); return result; } static tt_Strings ofOne(String s) { let result = new("tt_Strings"); result.add(s); return result; } uint size() const { return _strings.size(); } string at(uint i) const { return _strings[i]; } bool contains(String str) const { uint foundIndex = _strings.Find(str); bool isFound = (foundIndex != size()); return isFound; } void add(String str) { _strings.push(str); } private Array<String> _strings; }
22.1.1. Test
{
let strings = tt_Strings.of();
let size = strings.size();
it("tt_Strings: New Strings is empty", AssertEval(size, "==", 0));
}
{
let strings = tt_Strings.of();
let str = "a";
strings.add(str);
let size = strings.size();
it("tt_Strings: Element must be added", AssertEval(size, "==", 1));
it("tt_Strings: Element must be the same", Assert(strings.at(0) == str));
}
23. Target
23.1. Target
// Represents an attack target. class tt_Target { static tt_Target of(Actor a) { let result = new("tt_Target"); result._actor = a; return result; } // Get position in game space of this target. tt_Origin getPosition() const { vector3 position = _actor.pos; position.z += _actor.height / 2; let result = tt_Origin.of(position); return result; } bool isEqual(tt_Target other) const { return other._actor == _actor; } Actor getActor() const { return _actor; } private Actor _actor; }
23.2. Targets
// Represent a list of Targets. class tt_Targets { static tt_Targets of() { return new("tt_Targets"); } // Returns a target in this list. tt_Target at(uint index) const { return _targets[index]; } // Returns a number of targets in this list. uint size() const { return _targets.size(); } // Returns true if this target list contains a target with the specified id. bool contains(tt_Target target) const { return find(target) != size(); } bool isEmpty() const { return (size() == 0); } // Adds a target to this list. void add(tt_Target target) { _targets.push(target); } // Searches for a target with a particular id. // Returns index on success, the total number of targets on failure. private uint find(tt_Target target) const { uint nTargets = size(); for (uint i = 0; i < nTargets; ++i) if (_targets[i].isEqual(target)) return i; return nTargets; } private Array<tt_Target> _targets; }
23.3. TargetSource
// This interface represents a source of targets. // See: tt_Target. class tt_TargetSource abstract { abstract tt_Targets getTargets(); }
23.3.1. Mock
class tt_TargetSourceMock : tt_TargetSource { static tt_TargetSourceMock of() { return new("tt_TargetSourceMock"); } mixin tt_Mock; override tt_Targets getTargets() { if (_mock_getTargets_expectation == NULL) _mock_getTargets_expectation = _mock_addExpectation("getTargets"); ++_mock_getTargets_expectation.called; return _mock_getTargets; } void expect_getTargets(tt_Targets value, int expected = 1) { if (_mock_getTargets_expectation == NULL) _mock_getTargets_expectation = _mock_addExpectation("getTargets"); _mock_getTargets_expectation.expected = expected; _mock_getTargets_expectation.called = 0; _mock_getTargets = value; } private tt_Targets _mock_getTargets; private tt_Expectation _mock_getTargets_expectation; }
23.4. TargetRadar
// Implements tt_TargetSource by scanning the world around the // supplied origin for actors suitable to be targets. class tt_TargetRadar : tt_TargetSource { static tt_TargetRadar of(tt_OriginSource originSource) { let result = new("tt_TargetRadar"); result._originSource = originSource; return result; } override tt_Targets getTargets() { let result = tt_Targets.of(); let origin = _originSource.getOrigin().getVector(); let iterator = ThinkerIterator.Create("Actor", Thinker.STAT_DEFAULT); Actor a; while (a = Actor(iterator.Next())) { if (tt_Math.isInEffectiveRange(a.pos, origin) && isSuitableForTargeting(a)) { result.add(tt_Target.of(a)); } } return result; } private static bool isSuitableForTargeting(Actor a) { bool isMonster = a.bIsMonster; bool isAlive = (a.Health > 0); bool isFriendly = a.bFriendly; bool isMissile = a.bMissile; bool isDamageable = !a.bNoDamage; bool isNoDamage = (a.Damage == 0); bool isMissileSuitable = false; bool isSuitable = ( ( (isMonster && isDamageable) || (isMissile && !isNoDamage && isMissileSuitable) ) && isAlive && !isFriendly ); return isSuitable; } private tt_OriginSource _originSource; }
23.4.1. Test
{
let tag = "tt_TargetRadar: checkActorsAround";
let env = tt_TargetRadarTestEnvironment.of();
Array<Actor> actors =
{
spawn("DoomImp", ( 5, 0, 0)),
spawn("DoomImp", (-5, 0, 0)),
spawn("DoomImp", ( 0, 5, 0)),
spawn("DoomImp", ( 0, -5, 0)),
spawn("DoomImp", ( 0, 0, 5)),
spawn("DoomImp", ( 0, 0, -5))
};
env.originSource.expect_getOrigin(tt_Origin.of((0, 0, 0)));
let targets = env.targetRadar.getTargets();
uint nActors = actors.size();
for (uint i = 0; i < nActors; ++i)
{
let a = tt_Target.of(actors[i]);
it(string.format(tag .. ": actor %d is present in list", i),
Assert(targets.contains(a)));
}
assertSatisfaction(env.originSource.getSatisfaction(), tag);
cleanUpSpawned();
}
{
let tag = "tt_TargetRadar: checkDistantActor";
let env = tt_TargetRadarTestEnvironment.of();
env.originSource.expect_getOrigin(tt_Origin.of((0, 0, 0)));
let distantActor = spawn("DoomImp", (1000, 0, 0));
let distantTarget = tt_Target.of(distantActor);
let targets = env.targetRadar.getTargets();
it(tag .. ": distant actor is not in list",
AssertFalse(targets.contains(distantTarget)));
assertSatisfaction(env.originSource.getSatisfaction(), tag);
cleanUpSpawned();
}
{
let tag = "tt_TargetRadar: checkNonLivingActor";
let env = tt_TargetRadarTestEnvironment.of();
env.originSource.expect_getOrigin(tt_Origin.of((0, 0, 0)));
let nonLiving = spawn("Medikit", (1, 0, 0));
let targets = env.targetRadar.getTargets();
let nonLivingTarget = tt_Target.of(nonLiving);
it(tag .. ": non-living actor is not in list",
AssertFalse(targets.contains(nonLivingTarget)));
assertSatisfaction(env.originSource.getSatisfaction(), tag);
cleanUpSpawned();
}
{
let tag = "tt_TargetRadar: checkDeadActor";
let env = tt_TargetRadarTestEnvironment.of();
env.originSource.expect_getOrigin(tt_Origin.of((0, 0, 0)));
let deadActor = spawnDead("DoomImp", (1, 0, 0));
let targets = env.targetRadar.getTargets();
let deadTarget = tt_Target.of(deadActor);
it(tag .. ": dead actor is not in list",
AssertFalse(targets.contains(deadTarget)));
assertSatisfaction(env.originSource.getSatisfaction(), tag);
cleanUpSpawned();
}
class tt_TargetRadarTestEnvironment { static tt_TargetRadarTestEnvironment of() { let result = new("tt_TargetRadarTestEnvironment"); result.originSource = tt_OriginSourceMock.of(); result.targetRadar = tt_TargetRadar.of(result.originSource); return result; } tt_OriginSourceMock originSource; tt_TargetRadar targetRadar; }
23.5. DeathReporter
// Implements tt_TargetSource by collecting reports of // dead things as a list of DisabledTargets. class tt_DeathReporter : tt_TargetSource { static tt_DeathReporter of() { let result = new("tt_DeathReporter"); result._targets = tt_Targets.of(); return result; } void reportDead(Actor thing) { let newDisabled = tt_Target.of(thing); _targets.add(newDisabled); } override tt_Targets getTargets() { let result = _targets; _targets = tt_Targets.of(); return result; } private tt_Targets _targets; }
{
let _deathReporter = tt_DeathReporter.of();
let targetsBefore = _deathReporter.getTargets();
it("tt_DeathReporter: No targets before reporting",
AssertEval(targetsBefore.size(), "==", 0));
let something = spawn("DoomImp", (0, 0, 0));
_deathReporter.reportDead(something);
let targetsAfter = _deathReporter.getTargets();
it("tt_DeathReporter: Single target after reporting",
AssertEval(targetsAfter.size(), "==", 1));
let targetsAfterAfter = _deathReporter.getTargets();
it("tt_DeathReporter: No new targets",
AssertEval(targetsAfterAfter.size(), "==", 0));
cleanUpSpawned();
}
23.6. TargetSourceCache
// Implements tt_TargetSource by calling other tt_TargetSource only // if previously received target is stale. class tt_TargetSourceCache : tt_TargetSource { static tt_TargetSourceCache of(tt_TargetSource targetSource, tt_StaleMarker staleMarker) { let result = new("tt_TargetSourceCache"); result._targetSource = targetSource; result._staleMarker = staleMarker; return result; } override tt_Targets getTargets() { if (_staleMarker.isStale()) { _targets = _targetSource.getTargets(); } return _targets; } private tt_TargetSource _targetSource; private tt_StaleMarker _staleMarker; private tt_Targets _targets; }
23.7. TargetSourcePruner
// Implements tt_TargetSource by pruning other tt_TargetSource from // targets with null actors. class tt_TargetSourcePruner : tt_TargetSource { static tt_TargetSourcePruner of(tt_TargetSource targetSource) { let result = new("tt_TargetSourcePruner"); result._targetSource = targetSource; return result; } override tt_Targets getTargets() { let targets = _targetSource.getTargets(); tt_Targets result = tt_Targets.of(); uint nTargets = targets.size(); for (uint i = 0; i < nTargets; ++i) { tt_Target target = targets.at(i); if (target.getActor() != NULL) { result.add(target); } } return result; } private tt_TargetSource _targetSource; }
24. Target Widget
24.1. TargetWidget
// Represents a target displayed on the screen. class tt_TargetWidget { static tt_TargetWidget of(tt_KnownTarget target, vector2 position) { let result = new("tt_TargetWidget"); result._target = target; result._position = position; return result; } tt_KnownTarget getTarget() const { return _target; } vector2 getPosition() const { return _position; } double getDistanceTo(vector3 other) { let worldPosition = _target.getTarget().getPosition().getVector(); let distance = (worldPosition - other).Length(); return distance; } void setPosition(vector2 position) { _position = position; } private tt_KnownTarget _target; private vector2 _position; }
24.2. TargetWidgets
// Represents a list of target widgets. class tt_TargetWidgets { static tt_TargetWidgets of() { return new("tt_TargetWidgets"); } // Returns a target in this list. tt_TargetWidget at(uint index) const { return _widgets[index]; } // Returns a number of targets in this list. uint size() const { return _widgets.size(); } tt_TargetWidget find(tt_Target id) const { foreach (widget : _widgets) if (widget.getTarget().getTarget().isEqual(id)) return widget; return NULL; } bool containsWidget(tt_TargetWidget widget) const { foreach (widgetItem : _widgets) if (widget == widgetItem) return true; return false; } tt_TargetWidgets copy() const { let result = tt_TargetWidgets.of(); result._widgets.Reserve(size()); result._widgets.Copy(_widgets); return result; } // Adds a target to this list. void add(tt_TargetWidget widget) { _widgets.push(widget); } void set(uint i, tt_TargetWidget widget) { _widgets[i] = widget; } private Array<tt_TargetWidget> _widgets; }
24.3. TargetWidgetSource
// This interface provides a source of target widgets. class tt_TargetWidgetSource abstract { // Get a list of target widgets. // Returns a list of target widgets. ui abstract tt_TargetWidgets getWidgets(RenderEvent event); }
24.4. Projector
// Implements TargetWidgetSource by accumulating Target Widgets. // Attention: this class has no tests. Modifications must be checked manually. class tt_Projector : tt_TargetWidgetSource { static tt_Projector of(tt_KnownTargetSource knownTargetSource, tt_PlayerSource playerSource) { let result = new("tt_Projector"); result._knownTargetSource = knownTargetSource; result._playerSource = playerSource; result._cvarRenderer = tt_IntCvar.of(playerSource, "vid_rendermode"); result._glProjection = new("tt_le_GlScreen"); result._swProjection = new("tt_le_SwScreen"); return result; } override tt_TargetWidgets getWidgets(RenderEvent event) { let targets = _knownTargetSource.getTargets(); let info = _playerSource.getInfo(); let result = tt_TargetWidgets.of(); prepareProjection(); _projection.CacheResolution(); _projection.CacheFov(info.fov); _projection.OrientForRenderOverlay(event); _projection.BeginProjection(); tt_le_Viewport viewport; viewport.FromHud(); uint nTargets = targets.size(); for (uint i = 0; i < nTargets; ++i) { let target = targets.at(i); let targetActor = target.getTarget().getActor(); if (targetActor == NULL) { continue; } vector3 targetPos = target.getTarget().getPosition().getVector(); vector2 position; bool isPositionSuccessful; [position, isPositionSuccessful] = makeDrawPos(targetPos, viewport); if (isPositionSuccessful) { let widget = tt_TargetWidget.of(target, position); result.add(widget); } } return result; } // Calculates the screen position (draw position). // Returns screen position and success flag. private ui vector2, bool makeDrawPos(vector3 targetPos, tt_le_Viewport viewport) { _projection.ProjectWorldPos(targetPos); if(!_projection.IsInFront()) { return (0, 0), false; } vector2 drawPos = viewport.SceneToWindow(_projection.ProjectToNormal()); return drawPos, true; } private void prepareProjection() { if(_cvarRenderer.isDefined()) { switch (_cvarRenderer.get()) { case 0: case 1: _projection = _swProjection; break; default: _projection = _glProjection; break; } } else // cannot get render mode. { _projection = _glProjection; } } private tt_KnownTargetSource _knownTargetSource; private tt_PlayerSource _playerSource; private tt_le_ProjScreen _projection; private tt_le_GlScreen _glProjection; private tt_le_SwScreen _swProjection; private transient bool _isInitialized; private tt_IntCvar _cvarRenderer; }
24.5. SorterByDistance
// Implements TargetWidgetSource by taking another TargetWidgetSource // and sorting the widgets from it by a distance to origin from OriginSource. // // Sorting algorithm: merge sort // https://en.wikipedia.org/wiki/Merge_sort class tt_SorterByDistance : tt_TargetWidgetSource { static tt_SorterByDistance of(tt_TargetWidgetSource targetWidgetSource, tt_OriginSource originSource) { let result = new("tt_SorterByDistance"); result._targetWidgetSource = targetWidgetSource; result._originSource = originSource; return result; } override tt_TargetWidgets getWidgets(RenderEvent event) { let widgets = _targetWidgetSource.getWidgets(event); let origin = _originSource.getOrigin().getVector(); let sorted = sort(widgets, origin); return sorted; } static tt_TargetWidgets sort(tt_TargetWidgets widgets, vector3 origin) { let result = widgets; let workplace = widgets.copy(); TopDownSplitMerge(workplace, 0, widgets.size(), result, origin); return result; } private static void TopDownSplitMerge(tt_TargetWidgets B, uint begin, uint end, tt_TargetWidgets A, vector3 origin) { if ((end - begin) < 2) // if run size == 1 consider it sorted { return; } // split the run longer than 1 item into halves uint middle = (end + begin) / 2; // mid point // recursively sort both runs from array A into B TopDownSplitMerge(A, begin, middle, B, origin); // sort the left run TopDownSplitMerge(A, middle, end, B, origin); // sort the right run // merge the resulting runs from array B into A TopDownMerge(B, begin, middle, end, A, origin); } private static void TopDownMerge(tt_TargetWidgets A, uint begin, uint middle, uint end, tt_TargetWidgets B, vector3 origin) { uint i = begin; uint j = middle; // While there are elements in the left or right runs... for (uint k = begin; k < end; ++k) { // If left run head exists and is >= existing right run head. if (i < middle && (j >= end || A.at(i).getDistanceTo(origin) >= A.at(j).getDistanceTo(origin))) { B.set(k, A.at(i)); ++i; } else { B.set(k, A.at(j)); ++j; } } } private tt_TargetWidgetSource _targetWidgetSource; private tt_OriginSource _originSource; }
{
let tag = "tt_SorterByDistance : checkEmpty";
let before = tt_TargetWidgets.of();
let origin = tt_Origin.of((0, 0, 0));
let after = tt_SorterByDistance.sort(before, origin.getVector());
it(tag .. ": empty collection must remain empty",
AssertEval(after.size(), "==", 0));
}
{
let tag = "tt_SorterByDistance : checkSorted";
let origin = tt_Origin.of((0, 0, 0));
let before = tt_TargetWidgets.of();
before.add(tt_SorterByDistanceTest.createAtPosition(spawn("Demon", (0, 0, 2))));
before.add(tt_SorterByDistanceTest.createAtPosition(spawn("Demon", (0, 0, 1))));
before.add(tt_SorterByDistanceTest.createAtPosition(spawn("Demon", (0, 0, 0))));
it(tag .. ": Before: sorted",
Assert(tt_SorterByDistanceTest.isSorted(before, origin.getVector())));
let after = tt_SorterByDistance.sort(before, origin.getVector());
it(tag .. ": size of collection must the same",
AssertEval(after.size(), "==", before.size()));
it(tag .. ": contains same elements",
Assert(tt_SorterByDistanceTest.isSameElements(before, after)));
it(tag .. ": after: sorted",
Assert(tt_SorterByDistanceTest.isSorted(after, origin.getVector())));
cleanUpSpawned();
}
{
let tag = "tt_SorterByDistance : checkReverse";
let origin = tt_Origin.of((0, 0, 0));
let before = tt_TargetWidgets.of();
before.add(tt_SorterByDistanceTest.createAtPosition(spawn("Demon", (0, 0, 0))));
before.add(tt_SorterByDistanceTest.createAtPosition(spawn("Demon", (0, 0, 1))));
before.add(tt_SorterByDistanceTest.createAtPosition(spawn("Demon", (0, 0, 2))));
it(tag .. ": before: not sorted",
Assert(!tt_SorterByDistanceTest.isSorted(before, origin.getVector())));
let after = tt_SorterByDistance.sort(before, origin.getVector());
it(tag .. ": size of collection must the same",
AssertEval(after.size(), "==", before.size()));
it(tag .. ": contains same elements",
Assert(tt_SorterByDistanceTest.isSameElements(before, after)));
it(tag .. ": after: sorted",
Assert(tt_SorterByDistanceTest.isSorted(after, origin.getVector())));
cleanUpSpawned();
}
{
let tag = "tt_SorterByDistance : middle";
let origin = tt_Origin.of((0, 0, 0));
let before = tt_TargetWidgets.of();
before.add(tt_SorterByDistanceTest.createAtPosition(spawn("Demon", (0, 0, 1))));
before.add(tt_SorterByDistanceTest.createAtPosition(spawn("Demon", (0, 0, 2))));
before.add(tt_SorterByDistanceTest.createAtPosition(spawn("Demon", (0, 0, 0))));
it(tag .. ": before: not sorted",
Assert(!tt_SorterByDistanceTest.isSorted(before, origin.getVector())));
let after = tt_SorterByDistance.sort(before, origin.getVector());
it(tag .. ": size of collection must the same",
AssertEval(after.size(), "==", before.size()));
it(tag .. ": contains same elements",
Assert(tt_SorterByDistanceTest.isSameElements(before, after)));
it(tag .. ": after: sorted",
Assert(tt_SorterByDistanceTest.isSorted(after, origin.getVector())));
cleanUpSpawned();
}
class tt_SorterByDistanceTest { static bool isSorted(tt_TargetWidgets targetWidgets, vector3 origin) { uint nWidgets = targetWidgets.size(); if (nWidgets < 2) return true; for (uint i = 1; i < nWidgets; ++i) { let prevDistance = targetWidgets.at(i - 1).getDistanceTo(origin); let thisDistance = targetWidgets.at(i ).getDistanceTo(origin); if (prevDistance < thisDistance) return false; } return true; } static bool isSameElements(tt_TargetWidgets t1, tt_TargetWidgets t2) { uint nWidgets1 = t1.size(); uint nWidgets2 = t2.size(); if (nWidgets1 != nWidgets2) return false; for (uint i = 0; i < nWidgets1; ++i) if (!t2.containsWidget(t1.at(i))) return false; for (uint i = 0; i < nWidgets2; ++i) if (!t1.containsWidget(t2.at(i))) return false; return true; } static tt_TargetWidget createAtPosition(Actor anActor) { let target = tt_Target.of(anActor); let question = tt_QuestionMock.of(); let knownTarget = tt_KnownTarget.of(target, question); let widget = tt_TargetWidget.of(knownTarget, (0, 0)); return widget; } }
24.6. TargetWidgetRegistry
// Implements TargetWidgetSource by storing target widgets, getting // new widgets from the source, and updating the coordinates of the widgets that // are already registered. class tt_TargetWidgetRegistry : tt_TargetWidgetSource { static tt_TargetWidgetRegistry of(tt_TargetWidgetSource source) { let result = new("tt_TargetWidgetRegistry"); result._source = source; result._registry = tt_TargetWidgets.of(); return result; } override tt_TargetWidgets getWidgets(RenderEvent event) { let widgets = _source.getWidgets(event); let newRegistry = tt_TargetWidgets.of(); uint nWidgets = widgets.size(); for (uint i = 0; i < nWidgets; ++i) { let widget = widgets.at(i); let target = widget.getTarget().getTarget(); let existing = _registry.find(target); if (existing == NULL) { newRegistry.add(widget); } else { newRegistry.add(existing); let newPosition = widget.getPosition(); let existingPosition = existing.getPosition(); let middle = (newPosition * 0.3 + existingPosition * 0.7); existing.setPosition(middle); } } // Widgets that are not new or not updated are thrown away. _registry = newRegistry; return _registry; } private tt_TargetWidgetSource _source; private tt_TargetWidgets _registry; }
25. Colors
Colors can be set externally:
- Copy
tt_colors.zs, - In the copy, set new colors,
- Load the copy after Typist.pk3 (
-file Typist.pk3 tt_colors.zs).
class tt_TextColors { enum _ { Base = Font.CR_WHITE, } } // See https://zdoom.org/wiki/Print#Colors for possible colors. class tt_TextColorCodes { enum _ { WrongAnswer = tt_su_Ascii.LATIN_SMALL_LETTER_G, // red } } class tt_RgbColors { enum _ { Dim = 0x000000, // Dims the background for text boxes. Question = 0xF4AF31, // Base color for question boxes. AnswerCombat = 0xFF0000, // Base color for answer boxes in Combat mode. AnswerExploration = 0x999999, // Base color for answer boxes in Exploration mode. } }
26. View
26.1. View
// This interface represents a view - something that displays something. class tt_View abstract { ui abstract void draw(RenderEvent event); }
26.2. Views
// Implements View by allowing several Views to be drawn. class tt_Views : tt_View { static tt_Views of(Array<tt_View> views) { let result = new("tt_Views"); result._views.move(views); return result; } override void draw(RenderEvent event) { foreach (view : _views) view.draw(event); } private Array<tt_View> _views; }
26.3. Frame
class tt_Frame : tt_View { static tt_Frame of(tt_ModeSource modeSource) { let result = new("tt_Frame"); result._modeSource = modeSource; result._alphaInterpolator = tt_DoubleInterpolator.of(); return result; } override void draw(RenderEvent event) { double destination = (_modeSource.getMode() == tt_Mode.Combat) ? 1.0 : 0.0; _alphaInterpolator.reset(destination, 0.1); // TODO: untie from framerate? _alphaInterpolator.update(); double alpha = _alphaInterpolator.getValue(); if (alpha ~== 0.0) return; let frameTexture = TexMan.checkForTexture("tt-frame", TexMan.Type_Any); int screenWidth = Screen.getWidth(); int screenHeight = Screen.getHeight(); int frameWidth = screenWidth / 32; Screen.drawTexture( frameTexture , NOT_ANIMATED , 0 , 0 , DTA_DestWidth , frameWidth , DTA_DestHeight , screenHeight , DTA_KeepRatio , true , DTA_VirtualWidth , screenWidth , DTA_VirtualHeight , screenHeight , DTA_Alpha , alpha ); Screen.drawTexture( frameTexture , NOT_ANIMATED , screenWidth - frameWidth , 0 , DTA_FlipX , true , DTA_DestWidth , frameWidth , DTA_DestHeight , screenHeight , DTA_KeepRatio , true , DTA_VirtualWidth , screenWidth , DTA_VirtualHeight , screenHeight , DTA_Alpha , alpha ); } const NOT_ANIMATED = 0; // false private tt_ModeSource _modeSource; private tt_DoubleInterpolator _alphaInterpolator; }
class tt_DoubleInterpolator { static tt_DoubleInterpolator of() { return new("tt_DoubleInterpolator"); } ui void update() { _currentValue = (_destination > _currentValue) ? min(_destination, _currentValue + _step) : max(_destination, _currentValue - _step); } ui double getValue() const { return _currentValue; } ui void reset(double destination, double step) { _destination = destination; _step = step; } private ui double _destination; private ui double _currentValue; private ui double _step; }
26.4. ConditionalView
// Implements a view by taking another view, and calling draw() // only if conditions are met. // // The list of conditions: // - not in a menu // - automap is closed // // Attention! This class reads data from global scope. class tt_ConditionalView : tt_View { static tt_ConditionalView of(tt_View view) { let result = new("tt_ConditionalView"); result._view = view; return result; } override void draw(RenderEvent event) { if (!menuActive && !automapActive) _view.draw(event); } private tt_View _view; }
26.5. InfoPanel
// Implements View by collecting and displaying various information: // - game mode // - list of commands // - current input string // - several targets class tt_InfoPanel : tt_View { static tt_InfoPanel of(tt_ModeSource modeSource, tt_AnswerSource answerSource, tt_Activatable activatable, tt_KnownTargetSource knownTargetSource, tt_IntSetting scaleSetting) { let result = new("tt_InfoPanel"); result._modeSource = modeSource; result._answerSource = answerSource; result._activatable = activatable; result._targetSource = knownTargetSource; result._scaleSetting = scaleSetting; return result; } override void draw(RenderEvent _) { let targets = _targetSource.getTargets(); let targetCount = targets.size(); let commands = _activatable.getCommands(); let commandCount = commands.size(); if (targetCount == 0 && commandCount == 0) return; int scale = _scaleSetting.get(); int screenWidth = Screen.getWidth(); int halfScreen = screenWidth / 2; int y = int(Y_START + (VERTICAL_MARGIN * scale)); let answer = _answerSource.getAnswer().getString(); int color = tt_Drawing.getColorForMode(_modeSource.getMode()); int xRight = halfScreen; int xLeft = halfScreen; // 1. Draw the first target in the center. if (targetCount > 0) { let question = targets.at(0).getQuestion(); string questionString = question.getDescription(); string hintedAnswer = question.getHintFor(answer); tt_Drawing.drawTarget((halfScreen, y), questionString, hintedAnswer, scale, tt_Drawing.CENTERED, color); // TODO: make drawTarget return target width? int targetWidth = tt_Drawing.getWidthForTarget(questionString, hintedAnswer, scale); xRight += targetWidth / 2 + HORIZONTAL_MARGIN + tt_Drawing.FRAME; xLeft -= targetWidth / 2 + HORIZONTAL_MARGIN - tt_Drawing.FRAME; } // 2. Draw the targets to the right while there is space. uint i = 1; for (; i < targetCount; ++i) { let question = targets.at(i).getQuestion(); string questionString = question.getDescription(); string hintedAnswer = question.getHintFor(answer); int targetWidth = tt_Drawing.getWidthForTarget(questionString, hintedAnswer, scale); if (xRight + targetWidth > screenWidth) break; tt_Drawing.drawTarget((xRight, y), questionString, hintedAnswer, scale, tt_Drawing.NOT_CENTERED, color); xRight += targetWidth + HORIZONTAL_MARGIN; } // 3. Draw the commands to the left while there is space. for (uint c = 0; c < commandCount; ++c) { let command = commands.at(c); let question = tt_Match.of(command, command); let hintedAnswer = question.getHintFor(answer); int targetWidth = tt_Drawing.getWidthForTarget(command, hintedAnswer, scale); bool isCentered = targetCount == 0 && c == 0; let position = isCentered ? (halfScreen, y) : (xLeft - targetWidth, y); if (xLeft - targetWidth < 0) break; tt_Drawing.drawTarget(position, command, hintedAnswer, scale, isCentered, color); xLeft -= targetWidth + HORIZONTAL_MARGIN; } // 4. Draw the remaining targets to the left while there is space. for (; i < targetCount; ++i) { let question = targets.at(i).getQuestion(); string questionString = question.getDescription(); string hintedAnswer = question.getHintFor(answer); int targetWidth = tt_Drawing.getWidthForTarget(questionString, hintedAnswer, scale); if (xLeft - targetWidth < 0) break; tt_Drawing.drawTarget((xLeft - targetWidth, y), questionString, hintedAnswer, scale, tt_Drawing.NOT_CENTERED, color); xLeft -= targetWidth + HORIZONTAL_MARGIN; } } const Y_START = 10; const HORIZONTAL_MARGIN = 2; const VERTICAL_MARGIN = 20; private tt_ModeSource _modeSource; private tt_AnswerSource _answerSource; private tt_Activatable _activatable; private tt_KnownTargetSource _targetSource; private tt_IntSetting _scaleSetting; }
26.6. TargetOverlay
// Implement tt_View by getting a list of Target Widgets and drawing them. class tt_TargetOverlay : tt_View { static tt_TargetOverlay of(tt_TargetWidgetSource targetWidgetSource, tt_AnswerSource answerSource, tt_IntSetting scaleSetting, tt_ModeSource modeSource) { let result = new("tt_TargetOverlay"); result._targetWidgetSource = targetWidgetSource; result._answerSource = answerSource; result._scaleSetting = scaleSetting; result._modeSource = modeSource; return result; } override void draw(RenderEvent event) { let widgets = _targetWidgetSource.getWidgets(event); let answer = _answerSource.getAnswer().getString(); int mode = _modeSource.getMode(); int color = tt_Drawing.getColorForMode(mode); int scale = _scaleSetting.get(); uint nWidgets = widgets.size(); for (uint i = 0; i < nWidgets; ++i) { let widget = widgets.at(i); let question = widget.getTarget().getQuestion(); string questionString = question.getDescription(); string hintedAnswer = question.getHintFor(answer); let position = widget.getPosition(); tt_Drawing.drawTarget(position, questionString, hintedAnswer, scale, CENTERED, color); } } const CENTERED = 1; // true private tt_TargetWidgetSource _targetWidgetSource; private tt_AnswerSource _answerSource; private tt_IntSetting _scaleSetting; private tt_ModeSource _modeSource; }
26.7. Drawing
// Namespace for common drawing functions. // TODO: clean up this mess. class tt_Drawing ui { enum _ { NOT_CENTERED = 0, CENTERED = 1 } static int getWidthForTarget(String question, string answer, int scale) { Font fnt = NewSmallFont; int width = max(fnt.StringWidth(question), fnt.StringWidth(answer)); int borderWidth = makeBorderWidth(width) * scale; return borderWidth; } static void drawTarget(vector2 pos, string question, string answer, int scale, bool isCentered, int color) { Font fnt = NewSmallFont; int screenWidth = Screen.GetWidth() / scale; int screenHeight = Screen.GetHeight() / scale; let position = pos / scale; int height = fnt.GetHeight(); int width = max(fnt.StringWidth(question), fnt.StringWidth(answer)); double xStart = isCentered ? (position.x - width / 2) : position.x; int x = int(Clamp(xStart, FRAME, screenWidth - FRAME - width )); int y = int(Clamp(position.y, FRAME, screenHeight - FRAME * 3 - height * 2)); drawBoxes(x, y, width, height, screenWidth, screenHeight, color); drawText(x, y, height, fnt, question, answer, screenWidth, screenHeight); } static int getColorForMode(int mode) { return (mode == tt_Mode.Combat) ? tt_RgbColors.AnswerCombat : tt_RgbColors.AnswerExploration; } private static int makeBorderWidth(int width) { int borderWidth = FRAME * 2 + width; return borderWidth; } private static void drawBoxes(int x, int y, int width, int lineHeight, int screenWidth, int screenHeight, int color) { // TODO: replace with null? // Texture is necessary for drawing, but in fact it isn't used. // The color is specified with DTA_FillColor. let dummyTexture = TexMan.CheckForTexture("tt-white", TexMan.Type_Any); drawBox(dummyTexture, x, y, width, lineHeight, screenWidth, screenHeight, tt_RgbColors.Question); int lowerY = y + lineHeight + FRAME * 2; drawBox(dummyTexture, x, lowerY, width, lineHeight, screenWidth, screenHeight, color); } private static void drawBox( TextureID tex , int x , int y , int width , int lineHeight , int screenWidth , int screenHeight , int color ) { // TODO: replace drawTexture with drawShapeFill // Screen.drawShapeFill(Color col, double amount, Shape2D s) { // border int borderX = x - FRAME; int borderY = y - FRAME; int borderWidth = makeBorderWidth(width); int borderHeight = FRAME * 2 + lineHeight; Screen.DrawTexture( tex , NOT_ANIMATED , borderX , borderY , DTA_DestWidth , borderWidth , DTA_DestHeight , borderHeight , DTA_FillColor , color , DTA_KeepRatio , true , DTA_VirtualWidth , screenWidth , DTA_VirtualHeight , screenHeight , DTA_Alpha , BORDER_ALPHA ); } { // background int backgroundX = x - PADDING; int backgroundY = y - PADDING; int backgroundWidth = PADDING * 2 + width; int backgroundHeight = PADDING * 2 + lineHeight; Screen.DrawTexture( tex , NOT_ANIMATED , backgroundX , backgroundY , DTA_DestWidth , backgroundWidth , DTA_DestHeight , backgroundHeight , DTA_FillColor , tt_RgbColors.Dim , DTA_KeepRatio , true , DTA_VirtualWidth , screenWidth , DTA_VirtualHeight , screenHeight , DTA_Alpha , BACKGROUND_ALPHA ); } } private static void drawText( int x , int y , int height , Font fnt , string question , string answer , int screenWidth , int screenHeight ) { Screen.DrawText( fnt , tt_TextColors.Base , x , y , "$" .. question , DTA_KeepRatio , true , DTA_VirtualWidth , screenWidth , DTA_VirtualHeight , screenHeight ); Screen.DrawText( fnt , tt_TextColors.Base , x , y + height + FRAME * 2 , "$" .. answer , DTA_KeepRatio , true , DTA_VirtualWidth , screenWidth , DTA_VirtualHeight , screenHeight ); } const BORDER = 1; const PADDING = 2; const FRAME = BORDER + PADDING; const NOT_ANIMATED = 0; // false const BORDER_ALPHA = 0.2; const BACKGROUND_ALPHA = 0.2; }
27. Effect
27.1. Effect
// Interface for any non-play effects. class tt_Effect abstract { abstract void doEffect(); }
27.1.1. Mock
class tt_EffectMock : tt_Effect { static tt_EffectMock of() { return new("tt_EffectMock"); } mixin tt_Mock; override void doEffect() { if (_mock_doEffect_expectation == NULL) _mock_doEffect_expectation = _mock_addExpectation("doEffect"); ++_mock_doEffect_expectation.called; } void expect_doEffect(int expected = 1) { if (_mock_doEffect_expectation == NULL) _mock_doEffect_expectation = _mock_addExpectation("doEffect"); _mock_doEffect_expectation.expected = expected; _mock_doEffect_expectation.called = 0; } private tt_Expectation _mock_doEffect_expectation; }
27.1.2. Effects
class tt_Effects : tt_Effect { static tt_Effects of(Array<tt_Effect> effects) { let result = new("tt_Effects"); result._effects.move(effects); return result; } override void doEffect() { foreach (effect : _effects) effect.doEffect(); } private Array<tt_Effect> _effects; }
27.1.3. Gunner
// Implements tt_Effect by calling other tt_Effect if there is some tt_Origin. class tt_Gunner : tt_Effect { static tt_Gunner of(tt_OriginSource originSource, tt_Effect effect) { let result = new("tt_Gunner"); result._originSource = originSource; result._effect = effect; return result; } override void doEffect() { if (_originSource.getOrigin() != NULL) _effect.doEffect(); } private tt_OriginSource _originSource; private tt_Effect _effect; }
- Tests
{ let tag = "tt_Gunner: null origin"; let env = tt_GunnerTestEnvironment.of(); env.originSource.expect_getOrigin(NULL); env.gunner.doEffect(); assertSatisfaction(env.getSatisfaction(), tag); } { let tag = "tt_Gunner: valid origin"; let env = tt_GunnerTestEnvironment.of(); let origin = tt_Origin.of((0, 0, 0)); env.originSource.expect_getOrigin(origin); env.effect.expect_doEffect(); env.gunner.doEffect(); assertSatisfaction(env.getSatisfaction(), tag); }class tt_GunnerTestEnvironment { static tt_GunnerTestEnvironment of() { let result = new("tt_GunnerTestEnvironment"); result.originSource = tt_OriginSourceMock.of(); result.effect = tt_EffectMock.of(); result.gunner = tt_Gunner.of(result.originSource, result.effect); return result; } tt_Satisfaction getSatisfaction() const { return originSource.getSatisfaction() .add(effect.getSatisfaction()); } tt_OriginSourceMock originSource; tt_EffectMock effect; tt_Gunner gunner; }
27.2. AnswerResetter
class tt_AnswerResetter : tt_Effect { static tt_AnswerResetter of(tt_AnswerStateSource answerStateSource, tt_AnswerSource answerSource) { let result = new("tt_AnswerResetter"); result._answerStateSource = answerStateSource; result._answerSource = answerSource; result._oldAnswerState = tt_AnswerState.of(tt_AnswerState.Unknown); return result; } override void doEffect() { let newAnswerState = _answerStateSource.getAnswerState(); if (!_oldAnswerState.isFinished() && newAnswerState.isFinished()) { _answerStateSource.reset(); _answerSource.reset(); } _oldAnswerState = newAnswerState; } private tt_AnswerStateSource _answerStateSource; private tt_AnswerSource _answerSource; private tt_AnswerState _oldAnswerState; }
27.3. MatchWatcher
// Watches for answer state source and reports match or not match // when the state changes from Preparing to Ready. // // Match is determined by tt_OriginSource result being not NULL. class tt_MatchWatcher : tt_Effect { static tt_MatchWatcher of(tt_AnswerStateSource answerStateSource, tt_AnswerReporter answerReporter, tt_OriginSource originSource) { let result = new("tt_MatchWatcher"); result._answerStateSource = answerStateSource; result._answerReporter = answerReporter; result._originSource = originSource; result._oldAnswerState = tt_AnswerState.of(tt_AnswerState.Unknown); return result; } override void doEffect() { let newAnswerState = _answerStateSource.getAnswerState(); if (!_oldAnswerState.isReady() && newAnswerState.isReady()) { let isMatched = (_originSource.getOrigin() != NULL); if (isMatched) { _answerReporter.reportMatch(); } else { _answerReporter.reportNotMatch(); } } _oldAnswerState = newAnswerState; } private tt_AnswerStateSource _answerStateSource; private tt_AnswerReporter _answerReporter; private tt_OriginSource _originSource; private tt_AnswerState _oldAnswerState; }
27.4. TargetOriginSender
class tt_TargetOriginSender : tt_Effect { static tt_TargetOriginSender of(tt_OriginSource targetOriginSource) { let result = new("tt_TargetOriginSender"); result._targetOriginSource = targetOriginSource; return result; } override void doEffect() { vector3 origin = _targetOriginSource.getOrigin().getVector(); EventHandler.sendNetworkCommand("tt_target", NET_DOUBLE, origin.x, NET_DOUBLE, origin.y, NET_DOUBLE, origin.z); } private tt_OriginSource _targetOriginSource; }
28. Options Menu
28.1. Options
TODO: rewrite with PlainTranslator.
AddOptionMenu OptionsMenu { tt_AnimatedSubmenu "$TT_TITLE", tt_Options } OptionMenu tt_Options { Title "$TT_TITLE" Submenu "$TT_CONTROLS_TITLE_OUT" , tt_Controls StaticText "" Submenu "General Options" , tt_GeneralOptions Submenu "$TT_LESSON_OPTIONS_TITLE_OUT" , tt_LessonOptions Submenu "$TT_SOUND_OPTIONS_TITLE_OUT" , tt_SoundOptions } OptionMenu tt_GeneralOptions { Title "Typist.pk3 General Options" StaticText "" Option "Automatic word confirmation", tt_fast_confirmation, OnOff StaticText "" Slider "$TT_TARGET_SCALE", tt_view_scale, 1, 4, 1, 0 StaticText "" StaticText "Reduce distractions" StaticText "" Option "$TT_BUDDHA_ENABLED", tt_buddha_enabled, OnOff StaticText "$TT_BUDDHA_NOTE" StaticText "" Option "Infinite ammo", sv_infiniteammo, OnOff StaticText "" Option "HUD", screenblocks, tt_HudValues Option "Show score", tt_lp_show, OnOff Slider "Pain flash", blood_fade_scalar, 0, 1.0, 0.1 Slider "Pickup flash", pickup_fade_scalar, 0, 1.0, 0.1 } OptionValue tt_HudValues { 10, "Standard" 11, "Alternative" 12, "No HUD" } OptionMenu tt_LessonOptions { Title "$TT_LESSON_OPTIONS_TITLE_IN" Option "$TT_LESSON_1000" , tt_is_english_enabled , OnOff Option "$TT_LESSON_RANDOM" , tt_is_random_enabled , OnOff Option "$TT_LESSON_MATH" , tt_is_maths_enabled , OnOff Option "$TT_CUSTOM_TEXT" , tt_is_custom_enabled , OnOff StaticText "" Submenu "$TT_RANDOM_LESSON_TITLE", tt_RandomLesson StaticText "" Command "$TT_APPLY", tt_reset_targets StaticText "$TT_QUESTION_SOURCE_NOTE" StaticText "" StaticText "$TT_CUSTOM_LESSON_HELP_TEXT" } OptionMenu tt_Controls { Title "$TT_CONTROLS_TITLE_IN" Control "$TT_UNLOCK", tt_unlock_mode Control "$TT_COMBAT", tt_force_combat StaticText "" TextField "Pass Through command", tt_command_pass_through StaticText "Allows a single action key to be pressed without exiting Combat mode." StaticText "" Control "$TT_SCORE", zc_top } OptionMenu tt_SoundOptions { Title "$TT_SOUND_OPTIONS_TITLE_IN" Option "$TT_SOUND_ENABLED" , tt_sound_enabled , OnOff Option "$TT_SOUND_TYPING_ENABLED" , tt_sound_typing_enabled , OnOff Option "$TT_SOUND_THEME" , tt_sound_theme , tt_SoundThemes } OptionMenu tt_RandomLesson { Title "$TT_RANDOM_LESSON_TITLE_FULL" Slider "$TT_RANDOM_LESSON_LENGTH", tt_rc_length, 1, 10, 1, 0 StaticText "" Option "$TT_RANDOM_LESSON_UPPERCASE" , tt_rc_uppercase_letters_enabled , OnOff Option "$TT_RANDOM_LESSON_LOWERCASE" , tt_rc_lowercase_letters_enabled , OnOff Option "$TT_RANDOM_LESSON_NUMBERS" , tt_rc_numbers_enabled , OnOff Option "$TT_RANDOM_LESSON_PUNCTUATION" , tt_rc_punctuation_enabled , OnOff Option "$TT_RANDOM_LESSON_SYMBOLS" , tt_rc_symbols_enabled , OnOff StaticText "" Option "$TT_RANDOM_LESSON_CUSTOM" , tt_rc_custom_enabled , OnOff TextField "$TT_RANDOM_LESSON_CUSTOM_CHARS" , tt_rc_custom }
OptionValue tt_SoundThemes { 1, "Default" 2, "SNES" 4, "Dakka" 5, "Grocery Store" } // Score Menu OptionMenu tt_lp_TopMenu { class tt_lp_Top Title "Top Points" }
28.2. Keys
Alias tt_unlock_mode "event tt_unlock_mode" Alias tt_force_combat "event tt_force_combat" Alias tt_reset_targets "event tt_reset_targets" Alias zc_top "openMenu tt_lp_TopMenu" AddKeySection "$TT_TITLE" tt_keys AddMenuKey "$TT_UNLOCK" tt_unlock_mode AddMenuKey "$TT_COMBAT" tt_force_combat AddMenuKey "$TT_SCORE" zc_top
28.3. AnimatedSubmenu
class OptionMenuItemtt_AnimatedSubmenu : OptionMenuItemSubmenu { // Signature mirrors OptionMenuItemSubmenu.Init(). OptionMenuItemtt_AnimatedSubmenu Init( string label , Name command , int param = 0 , bool centered = false ) { Super.Init(label, command, param, centered); _originalLabel = stringTable.Localize(label); _originalLength = _originalLabel.CodePointCount(); _period = DELAY_TICS + _originalLength * CHARACTER_TIMEOUT_TICS; return self; } override int Draw(OptionMenuDescriptor desc, int y, int indent, bool selected) { int highlightedLetterIndex = _state / CHARACTER_TIMEOUT_TICS; if (highlightedLetterIndex < _originalLength) { int letterCode; int charPos = 0; for (int i = 0; i < highlightedLetterIndex; ++i) { [letterCode, charPos] = _originalLabel.GetNextCodePoint(charPos); } string left = _originalLabel.Left(charPos); [letterCode, charPos] = _originalLabel.GetNextCodePoint(charPos); string right = _originalLabel.Mid(charPos, _originalLabel.Length() - charPos); mLabel = string.format("%s\cd%c\c-%s", left, letterCode, right); } else { mLabel = _originalLabel; } ++_state; if (_state >= _period) { _state = 0; } return Super.Draw(desc, y, indent, selected); } const DELAY_TICS = 5 * TICRATE; const CHARACTER_TIMEOUT_TICS = 3; private int _state; private int _period; private string _originalLabel; private int _originalLength; }
28.4. Language
[enu default] // Menus /////////////////////////////////////////////////////////////////////// TT_TITLE = "Typist.pk3 v0.7.4"; TT_UNLOCK = "Unlock Game Mode"; TT_COMBAT = "Force Combat Mode"; TT_SCORE = "Open Score"; TT_TARGET_SCALE = "Target text scale"; TT_QUESTION_SOURCE = "Lesson"; TT_APPLY = "Apply"; TT_QUESTION_SOURCE_NOTE = "Or existing targets will retain question from the previous Lesson."; TT_CONTROLS_TITLE_OUT = "Controls"; TT_CONTROLS_TITLE_IN = "Typist.pk3 Controls"; TT_BUDDHA_ENABLED = "Player cannot die"; TT_BUDDHA_NOTE = "Applies on new level start."; TT_LESSON_OPTIONS_TITLE_OUT = "Lesson Options"; TT_LESSON_OPTIONS_TITLE_IN = "Typist.pk3 Lesson Options"; TT_LESSON_MATH = "Arithmetic"; TT_LESSON_1000 = "1000 Basic English Words"; TT_CUSTOM_TEXT = "Custom Text"; TT_CUSTOM_LESSON_HELP_TEXT = "\cfHow to set up Custom Text lesson\c-\ \ 1. Find any text or book in ASCII .txt file (UTF-8 may also work).\ 2. Rename text file to `typist_custom_text.txt`.\ 3. Load `typist_custom_text.txt` with GZDoom alongside Typist.pk3.\ 4. Select Custom Text in Typist options menu."; TT_NIX_LESSON = "*nix Command Line"; TT_LESSON_RANDOM = "Random Characters"; TT_RANDOM_LESSON_TITLE = "Random Characters Lesson Configuration"; TT_RANDOM_LESSON_LENGTH = "Length"; TT_RANDOM_LESSON_UPPERCASE = "A-Z"; TT_RANDOM_LESSON_LOWERCASE = "a-z"; TT_RANDOM_LESSON_NUMBERS = "0-9"; TT_RANDOM_LESSON_PUNCTUATION = "Punctuation"; TT_RANDOM_LESSON_SYMBOLS = "Other characters"; TT_RANDOM_LESSON_CUSTOM = "Custom string"; TT_RANDOM_LESSON_CUSTOM_CHARS = "Custom string:"; // Sound menus ///////////////////////////////////////////////////////////////// TT_SOUND_OPTIONS_TITLE_OUT = "Sound Options"; TT_SOUND_OPTIONS_TITLE_IN = "Typist.pk3 Sound Options"; TT_SOUND_THEME = "Sound theme"; TT_SOUND_ENABLED = "Sound effects"; TT_SOUND_TYPING_ENABLED = "Typing sound"; // Helper string TT_FALLBACK_QUESTION = "<empty lesson>";
29. Mod setup
version 4.14.2 #include "zscript/menu/tt_option_menu_item_animated_submenu.zs" #include "zscript/effect/tt_target_origin_sender.zs" #include "zscript/effect/tt_match_watcher.zs" #include "zscript/effect/tt_answer_resetter.zs" #include "zscript/effect/tt_gunner.zs" #include "zscript/effect/tt_effect.zs" #include "zscript/view/tt_drawing.zs" #include "zscript/view/tt_target_overlay.zs" #include "zscript/view/tt_info_panel.zs" #include "zscript/view/tt_conditional_view.zs" #include "zscript/interpolator/tt_double_interpolator.zs" #include "zscript/view/tt_frame.zs" #include "zscript/view/tt_views.zs" #include "zscript/view/tt_view.zs" #include "zscript/target_widget/tt_target_widget_registry.zs" #include "zscript/target_widget/tt_sorter_by_distance.zs" #include "zscript/target_widget/tt_projector.zs" #include "zscript/target_widget/tt_target_widget_source.zs" #include "zscript/target_widget/tt_target_widgets.zs" #include "zscript/target_widget/tt_target_widget.zs" #include "zscript/target/tt_target_source_pruner.zs" #include "zscript/target/tt_target_source_cache.zs" #include "zscript/target/tt_death_reporter.zs" #include "zscript/target/tt_target_radar.zs" #include "zscript/target/tt_target_source.zs" #include "zscript/target/tt_targets.zs" #include "zscript/target/tt_target.zs" #include "zscript/strings/tt_strings.zs" #include "zscript/stale_marker/tt_stale_marker_impl.zs" #include "zscript/stale_marker/tt_stale_marker.zs" #include "zscript/settings/tt_random_characters_lesson_settings_impl.zs" #include "zscript/settings/tt_random_characters_lesson_settings.zs" #include "zscript/settings/tt_cvars.zs" #include "zscript/settings/tt_settings.zs" #include "zscript/settings/tt_setting.zs" #include "zscript/question/tt_match.zs" #include "zscript/question/tt_question.zs" #include "zscript/player/tt_player_source_impl.zs" #include "zscript/player/tt_player_source.zs" #include "zscript/origin/tt_selectable_origin_source.zs" #include "zscript/origin/tt_question_answer_matcher.zs" #include "zscript/origin/tt_player_origin_source.zs" #include "zscript/origin/tt_origin_source_cache.zs" #include "zscript/origin/tt_hasty_question_answer_matcher.zs" #include "zscript/origin/tt_origin_source.zs" #include "zscript/origin/tt_origin.zs" #include "zscript/mode/tt_automap_mode_source.zs" #include "zscript/mode/tt_settable_mode.zs" #include "zscript/mode/tt_reported_mode_source.zs" #include "zscript/mode/tt_mode_storage.zs" #include "zscript/mode/tt_mode_cascade.zs" #include "zscript/mode/tt_delayed_combat_mode_source.zs" #include "zscript/mode/tt_auto_mode_source.zs" #include "zscript/mode/tt_mode_source.zs" #include "zscript/mode/tt_mode.zs" #include "zscript/math/tt_math.zs" #include "zscript/lesson/tt_string_set.zs" #include "zscript/lesson/tt_random_number_source.zs" #include "zscript/lesson/tt_random_characters_lesson.zs" #include "zscript/lesson/tt_mixed_lesson.zs" #include "zscript/lesson/tt_maths_lesson.zs" #include "zscript/lesson/tt_question_source.zs" #include "zscript/known_target/tt_visible_known_target_source.zs" #include "zscript/known_target/tt_target_registry.zs" #include "zscript/known_target/tt_known_target_source_cache.zs" #include "zscript/known_target/tt_known_target_source.zs" #include "zscript/known_target/tt_known_targets.zs" #include "zscript/known_target/tt_known_target.zs" #include "zscript/key_processor/tt_key_processors.zs" #include "zscript/key_processor/tt_key_processor.zs" #include "zscript/input_manager/tt_input_manager.zs" #include "zscript/input_manager/tt_pass_through_input_manager.zs" #include "zscript/input_manager/tt_input_by_mode_manager.zs" #include "zscript/event_reporters/tt_player_sound_player.zs" #include "zscript/event_reporters/tt_sound_player.zs" #include "zscript/event_reporters/tt_sound_mode_reporter.zs" #include "zscript/event_reporters/tt_mode_reporter.zs" #include "zscript/event_reporters/tt_sound_key_press_reporter.zs" #include "zscript/event_reporters/tt_key_press_reporter.zs" #include "zscript/event_reporters/tt_sound_answer_reporter.zs" #include "zscript/event_reporters/tt_answer_reporter.zs" #include "zscript/clock/tt_total_clock.zs" #include "zscript/clock/tt_clock.zs" #include "zscript/character/tt_character.zs" #include "zscript/answer_state/tt_pressed_answer_state.zs" #include "zscript/answer_state/tt_answer_state_source.zs" #include "zscript/answer_state/tt_answer_state.zs" #include "zscript/answer/tt_player_input.zs" #include "zscript/answer/tt_input_block_after_combat.zs" #include "zscript/answer/tt_answer_source.zs" #include "zscript/answer/tt_answer.zs" #include "zscript/activatable/tt_command_dispatcher.zs" #include "zscript/activatable/tt_pass_through.zs" #include "zscript/activatable/tt_activatable.zs" #include "zscript/firer/tt_firer.zs" #include "zscript/aimer/tt_vertical_aimer.zs" #include "zscript/aimer/tt_horizontal_aimer.zs" #include "zscript/world_changer/tt_velocity_storage.zs" #include "zscript/world_changer/tt_projectile_speed_controller.zs" #include "zscript/world_changer/tt_enemy_speed_controller.zs" #include "zscript/world_changer/tt_world_changers.zs" #include "zscript/world_changer/tt_world_changer.zs" #include "zscript/server/tt_server.zs" #include "zscript/player_handler/tt_player_supervisor.zs" #include "zscript/player_handler/tt_player_handler.zs" #include "zscript/buddha/tt_buddha.zs" #include "zscript/game_tweaks/tt_game_tweaks.zs" #include "zscript/event_handler/tt_event_handler.zs" #include "tt_colors.zs" #include "zscript/tt_le_libeye.zs" #include "zscript/tt_lp_LazyPoints.zs" #include "zscript/tt_su_StringUtils.zs"
GameInfo
{
EventHandlers =
"tt_lp_Dispatcher",
"tt_lp_StaticView",
"tt_EventHandler"
}
// Variables for score nosave string tt_lp_score = ""; user bool tt_lp_show = true;
30. Tests
version 4.14.2 #include "zscript/test.zs" #include "zscript/environments.zs" #include "zscript/mocks.zs"
class tt_Test : Clematis { override void testSuites() { Describe("Typist tests"); addTests(); EndDescribe(); } play void addTests() const { { let tag = "tt_HorizontalAimer"; Array<tt_Origin> targetPositions; Array<double> angles; targetPositions.push(tt_Origin.of(( 100, 100, 0))); angles.push( 45); targetPositions.push(tt_Origin.of((-100, -100, 0))); angles.push(-135); targetPositions.push(tt_Origin.of(( 0, 0, 0))); angles.push( 0); players[consolePlayer].mo.SetOrigin((0, 0, 0), false); int nTargetPositions = targetPositions.size(); for (int i = 0; i < nTargetPositions; ++i) { let originSource = tt_OriginSourceMock.of(); let playerSource = tt_PlayerSourceMock.of(); let aimer = tt_HorizontalAimer.of(originSource, playerSource); let targetOrigin = targetPositions[i]; let pawn = players[consolePlayer].mo; double angle = angles[i]; originSource.expect_getOrigin(targetOrigin); playerSource.expect_getPawn(pawn); // Just for a visual check. spawn("DoomImp", targetOrigin.getVector()); aimer.changeWorld(); let message = string.format("%s: pawn is oriented at the target, angle: %f", tag, angle); it(message, AssertEval(pawn.angle, "~==", angles[i])); assertSatisfaction(originSource.getSatisfaction(), tag); assertSatisfaction(playerSource.getSatisfaction(), tag); cleanUpSpawned(); } } { let tag = "tt_VerticalAimer"; let targetOriginSource = tt_OriginSourceMock.of(); let playerSource = tt_PlayerSourceMock.of(); let autoAimSetting = tt_FloatSettingMock.of(); let aimer = tt_VerticalAimer.of(targetOriginSource, playerSource, autoAimSetting); let targetOrigin = tt_Origin.of((550, 500, 500)); let pawn = players[consolePlayer].mo; pawn.SetOrigin((0, 0, 0), false); targetOriginSource.expect_getOrigin(targetOrigin); playerSource .expect_getPawn(pawn); autoAimSetting .expect_getFloat(0); aimer.changeWorld(); assertSatisfaction(targetOriginSource.getSatisfaction(), tag); assertSatisfaction(playerSource.getSatisfaction(), tag); assertSatisfaction(autoAimSetting.getSatisfaction(), tag); } { let tag = "tt_Firer"; let playerSource = tt_PlayerSourceMock.of(); let firer = tt_Firer.of(playerSource); PlayerInfo info = players[consolePlayer]; let pawn = info.mo; playerSource.expect_getInfo(info); playerSource.expect_getPawn(pawn); int nBullets = pawn.countInv("Clip"); it(tag .. ": must be 50 bullets before firing", AssertEval(nBullets, "==", 50)); firer.changeWorld(); assertSatisfaction(playerSource.getSatisfaction(), tag); // Note: this relies on sv_fastweapons 2. nBullets = pawn.countInv("Clip"); it(tag .. ": must spend 1 bullet after firing", AssertEval(nBullets, "==", 49)); } { let tag = "tt_CommandDispatcher: checkActivate"; let env = tt_CommandDispatcherTestEnvironment.of(); let str = "Hello"; let answer = tt_Answer.of(str); env.answerSource.expect_getAnswer(answer); let commands1 = tt_Strings.of(); let commands2 = tt_Strings.of(); commands2.add(str); env.activatable1.expect_getCommands(commands1); env.activatable2.expect_getCommands(commands2); env.activatable2.expect_activate(); env.answerReporter.expect_reportMatch(); env.answerStateSource .expect_getAnswerState(tt_AnswerState.of(tt_AnswerState.Ready)); env.answerStateSource.expect_reset(); env.answerSource.expect_reset(); env.commandDispatcher.activate(); assertSatisfaction(env.getSatisfaction(), tag); } { let tag = "tt_CommandDispatcher: checkGetCommands"; let env = tt_CommandDispatcherTestEnvironment.of(); let commands1 = tt_Strings.of(); let commands2 = tt_Strings.of(); commands1.add("1"); commands1.add("2"); commands2.add("3"); commands2.add("4"); env.activatable1.expect_getCommands(commands1); env.activatable2.expect_getCommands(commands2); env.activatable1.expect_isVisible(true); env.activatable2.expect_isVisible(true); let allCommands = env.commandDispatcher.getCommands(); let size = allCommands.size(); it("tt_CommandDispatcher: check get commands: All commands are collected", AssertEval(size, "==", 4)); it("tt_CommandDispatcher: check get commands: The first command is collected", Assert(allCommands.contains("1"))); it("tt_CommandDispatcher: check get commands: The second command is collected", Assert(allCommands.contains("2"))); it("tt_CommandDispatcher: check get commands: The third command is collected", Assert(allCommands.contains("3"))); it("tt_CommandDispatcher: check get commands: The forth command is collected", Assert(allCommands.contains("4"))); assertSatisfaction(env.getSatisfaction(), tag); } { let tag = "tt_PlayerInputTest: testPlayerInputCheckInput"; let env = tt_PlayerInputTestEnvironment.of(); string input = "abc"; env.throwStringIntoInput(input); let answer = env.playerInput.getAnswer(); let answerString = answer.getString(); it(tag .. ": input must be an answer", Assert(input == answerString)); } { let tag = "tt_PlayerInputTest: testPlayerInputCheckReset"; let env = tt_PlayerInputTestEnvironment.of(); int TYPE_CHAR = UiEvent.Type_Char; string input1 = "abc"; string input2 = "def"; env.throwStringIntoInput(input1); let reset = tt_Character.of(TYPE_CHAR, tt_su_Ascii.BACKSPACE, true); env.playerInput.processKey(reset); env.throwStringIntoInput(input2); let answer = env.playerInput.getAnswer(); let answerString = answer.getString(); it(tag .. ": second input must be an answer", Assert(input2 == answerString)); } { let tag = "tt_PlayerInputTest: testBackspace"; let env = tt_PlayerInputTestEnvironment.of(); int TYPE_CHAR = UiEvent.Type_Char; let backspace = tt_Character.of(TYPE_CHAR, tt_su_Ascii.BACKSPACE, false); let letterA = tt_Character.of(TYPE_CHAR, tt_su_Ascii.LATIN_SMALL_LETTER_A, false); //env.playerInput.reset(); env.playerInput.processKey(backspace); env.playerInput.processKey(letterA); env.playerInput.processKey(backspace); env.playerInput.processKey(letterA); let answer = env.playerInput.getAnswer(); let answerString = answer.getString(); it(tag .. ": input after backspace must be valid", Assert(answerString == "a")); } { let tag = "tt_PlayerInputTest: testCtrlBackspace"; let env = tt_PlayerInputTestEnvironment.of(); int TYPE_CHAR = UiEvent.Type_Char; let ctrlBackspace = tt_Character.of(TYPE_CHAR, tt_su_Ascii.BACKSPACE, true); let letterA = tt_Character.of(TYPE_CHAR, tt_su_Ascii.LATIN_SMALL_LETTER_A, false); env.playerInput.processKey(letterA); env.playerInput.processKey(letterA); env.playerInput.processKey(ctrlBackspace); let answer = env.playerInput.getAnswer(); let answerString = answer.getString(); it(tag .. ": input after ctrl-backspace must be empty", Assert(answerString == "")); } { int TYPE_CHAR = UiEvent.Type_Char; let c = tt_Character.of(TYPE_CHAR, tt_su_Ascii.LATIN_SMALL_LETTER_A, false); it("tt_Character: Small character", Assert(c.getType() == tt_Character.PRINTABLE)); it("tt_Character: Small character", Assert(c.getCharacter() == "a")); } { int TYPE_CHAR = UiEvent.Type_Char; let c = tt_Character.of(TYPE_CHAR, tt_su_Ascii.LATIN_CAPITAL_LETTER_A, false); it("tt_Character: Big character", Assert(c.getType() == tt_Character.PRINTABLE)); it("tt_Character: Big character", Assert(c.getCharacter() == "A")); } { int TYPE_CHAR = UiEvent.Type_Char; let c = tt_Character.of(TYPE_CHAR, tt_su_Ascii.DIGIT_FOUR, false); it("tt_Character: Number", Assert(c.getType() == tt_Character.PRINTABLE)); it("tt_Character: Number", Assert(c.getCharacter() == "4")); } { int TYPE_CHAR = UiEvent.Type_Char; let c = tt_Character.of(TYPE_CHAR, tt_su_Ascii.BACKSPACE, false); it("tt_Character: Backspace", Assert(c.getType() == tt_Character.BACKSPACE)); } { int TYPE_CHAR = UiEvent.Type_Char; let c = tt_Character.of(TYPE_CHAR, tt_su_Ascii.CHARACTER_NULL, false); it("tt_Character: Non-printable", Assert(c.getType() == tt_Character.NONE)); } { int TYPE_CHAR = UiEvent.Type_Char; let c = tt_Character.of(TYPE_CHAR, tt_su_Ascii.BACKSPACE, true); it( "tt_Character: Ctrl-Backspace", Assert(c.getType() == tt_Character.CTRL_BACKSPACE)); } { int TYPE_CHAR = UiEvent.Type_Char; let c = tt_Character.of(TYPE_CHAR, tt_su_Ascii.CARRIAGE_RETURN_CR, true); it("tt_Character: Enter", Assert(c.getType() == tt_Character.ENTER)); } { let clock = tt_TotalClock.of(); int now1 = clock.getNow(); int now2 = clock.getNow(); it("tt_TotalClock: now is now", AssertEval(now1, "==", now2)); int duration = clock.since(now1); it("tt_TotalClock: no time passed", AssertEval(duration, "==", 0)); } { let tag = "tt_TargetRegistry: emptyCheck"; let env = tt_TargetRegistryTestEnvironment.of(); env.targetSource .expect_getTargets(tt_Targets.of()); env.disabledTargetSource.expect_getTargets(tt_Targets.of()); it(tag .. ": is empty", Assert(env.targetRegistry.isEmpty())); assertSatisfaction(env.getSatisfaction(), tag); } { let tag = "tt_TargetRegistry: addCheck"; let env = tt_TargetRegistryTestEnvironment.of(); let target1 = tt_Target.of(spawn("Demon", (0, 0, 0))); let target2 = tt_Target.of(spawn("Demon", (0, 0, 0))); let targets = tt_Targets.of(); targets.add(target1); targets.add(target2); env.targetSource.expect_getTargets(targets); env.disabledTargetSource.expect_getTargets(tt_Targets.of()); env.lesson.expect_getQuestion(tt_QuestionMock.of(), 2); let knownTargets = env.targetRegistry.getTargets(); it(tag .. ": is two targets", AssertEval(knownTargets.size(), "==", 2)); assertSatisfaction(env.getSatisfaction(), tag); cleanUpSpawned(); } { let tag = "tt_TargetRegistry: addExistingCheck"; let env = tt_TargetRegistryTestEnvironment.of(); // First, add a single target. let demon1 = spawn("Demon", (0, 0, 0)); let target = tt_Target.of(demon1); let targets = tt_Targets.of(); targets.add(target); env.targetSource.expect_getTargets(targets); env.disabledTargetSource.expect_getTargets(tt_Targets.of()); env.lesson.expect_getQuestion(tt_QuestionMock.of()); let knownTargets = env.targetRegistry.getTargets(); it(tag .. ": is one target", AssertEval(knownTargets.size(), "==", 1)); assertSatisfaction(env.getSatisfaction(), tag); // Second, add the same target again. Only a single target must remain // registered. env.targetSource.expect_getTargets(targets); env.disabledTargetSource.expect_getTargets(tt_Targets.of()); env.lesson.expect_getQuestion(NULL, 0); knownTargets = env.targetRegistry.getTargets(); it(tag .. ": is one target", AssertEval(knownTargets.size(), "==", 1)); assertSatisfaction(env.getSatisfaction(), tag); cleanUpSpawned(); } { let tag = "tt_TargetRegistry: remove"; let env = tt_TargetRegistryTestEnvironment.of(); // First, add two targets. let demon1 = spawn("Demon", (0, 0, 0)); let demon2 = spawn("Demon", (0, 0, 0)); let target1 = tt_Target.of(demon1); let target2 = tt_Target.of(demon2); let targets = tt_Targets.of(); targets.add(target1); targets.add(target2); env.targetSource.expect_getTargets(targets); env.disabledTargetSource.expect_getTargets(tt_Targets.of()); env.lesson.expect_getQuestion(tt_QuestionMock.of(), 2); let knownTargets = env.targetRegistry.getTargets(); it(tag .. ": is two targets", AssertEval(knownTargets.size(), "==", 2)); assertSatisfaction(env.getSatisfaction(), tag); // Second, remove one target. let disabledTarget = tt_Target.of(demon1); let disabledTargets = tt_Targets.of(); disabledTargets.add(disabledTarget); env.targetSource.expect_getTargets(tt_Targets.of()); env.disabledTargetSource.expect_getTargets(disabledTargets); env.lesson.expect_getQuestion(NULL, 0); knownTargets = env.targetRegistry.getTargets(); it(tag .. ": is one target now", AssertEval(knownTargets.size(), "==", 1)); assertSatisfaction(env.getSatisfaction(), tag); cleanUpSpawned(); } { let tag = "tt_VisibleKnownTargetSource: no targets"; let env = tt_VisibleKnownTargetSourceTestEnvironment.of(); env.baseSource.expect_isEmpty(true, 2); bool isEmpty = env.source.isEmpty(); let targets = env.source.getTargets(); it(tag .. "-> empty", Assert(isEmpty)); it(tag .. "-> no targets", AssertEval(targets.size(), "==", 0)); assertSatisfaction(env.getSatisfaction(), tag); } { let tag = "tt_VisibleKnownTargetSource: visible targets"; let env = tt_VisibleKnownTargetSourceTestEnvironment.of(); let knownTargets = tt_KnownTargets.of(); let target = tt_Target.of(spawn("Demon", (0, 0, 0))); let question = tt_QuestionMock.of(); let knownTarget = tt_KnownTarget.of(target, question); knownTargets.add(knownTarget); env.baseSource .expect_isEmpty(false, 2); env.baseSource .expect_getTargets(knownTargets, 2); env.playerSource.expect_getPawn(players[consolePlayer].mo, 2); bool isEmpty = env.source.isEmpty(); let targets = env.source.getTargets(); it(tag .. "-> not empty", Assert(!isEmpty)); it(tag .. "-> targets", AssertEval(targets.size(), "==", 1)); assertSatisfaction(env.getSatisfaction(), tag); cleanUpSpawned(); } { let tag = "tt_VisibleKnownTargetSource: invisible targets"; let env = tt_VisibleKnownTargetSourceTestEnvironment.of(); let knownTargets = tt_KnownTargets.of(); let target = tt_Target.of(spawn("Demon", (9999999, 0, 0))); let question = tt_QuestionMock.of(); let knownTarget = tt_KnownTarget.of(target, question); knownTargets.add(knownTarget); env.baseSource .expect_isEmpty(false, 2); env.baseSource .expect_getTargets(knownTargets, 2); env.playerSource.expect_getPawn(players[consolePlayer].mo, 2); bool isEmpty = env.source.isEmpty(); let targets = env.source.getTargets(); it(tag .. "-> empty", Assert(isEmpty)); it(tag .. "-> no targets", AssertEval(targets.size(), "==", 0)); assertSatisfaction(env.getSatisfaction(), tag); cleanUpSpawned(); } { let question = tt_MathsLesson.of().getQuestion(); it("tt_MathsLesson: question isn't equal to the answer", AssertFalse(question.isRight(question.getDescription()))); } { let stringSet = tt_StringSet.of("tt_test_words"); let question = stringSet.getQuestion(); string description = question.getDescription(); it("tt_StringSet: Question must be valid", AssertNotNull(question)); it("tt_StringSet: Description", Assert(description == "привет")); } { let tag = "tt_AutoModeSource: no targets"; let knownTargetSource = tt_KnownTargetSourceMock.of(); let autoModeSource = tt_AutoModeSource.of(knownTargetSource); knownTargetSource.expect_isEmpty(true); int mode = autoModeSource.getMode(); it(tag .. ": no targets -> Explore", AssertEval(mode, "==", tt_Mode.Explore)); assertSatisfaction(knownTargetSource.getSatisfaction(), tag); } { let tag = "tt_AutoModeSource: targets"; let knownTargetSource = tt_KnownTargetSourceMock.of(); let autoModeSource = tt_AutoModeSource.of(knownTargetSource); knownTargetSource.expect_isEmpty(false); int mode = autoModeSource.getMode(); it(tag .. ": targets -> Combat", AssertEval(mode, "==", tt_Mode.Combat)); assertSatisfaction(knownTargetSource.getSatisfaction(), tag); } // C - Combat Mode // E - Exploration Mode // N - None Mode (let other decide) // // |-----|-----|---------|-------------|--------|-------------------------| // | old | new | enemies | time is up? | result | test | // |-----|-----|---------|-------------|--------|-------------------------| // | * | C | * | * | None | checkNewCombat | // | C | E | no | * | None | checkNoEnemies | // | C | E | yes | no | Combat | checkEnemiesStillCombat | // | C | E | yes | yes | None | checkEnemiesTimeIsUp | // | E | * | * | * | None | checkOldExploration | // |-----|-----|---------|-------------|--------|-------------------------| { let tag = "tt_DelayedCombatModeSource: checkNewCombat"; let env = tt_DelayedCombatModeSourceTestEnvironment.of(); env.modeSource.expect_getMode(tt_Mode.Combat, 2); env.clock.expect_getNow(0, 0); env.clock.expect_since(0, 0); int result1 = env.delay.getMode(); it(tag .. ": new combat -> None", AssertEval(result1, "==", tt_Mode.None)); int result2 = env.delay.getMode(); it(tag .. ": again, combat -> None", AssertEval(result2, "==", tt_Mode.None)); assertSatisfaction(env.getSatisfaction(), tag); } { let tag = "tt_DelayedCombatModeSource: checkNoEnemies"; let env = tt_DelayedCombatModeSourceTestEnvironment.of(); // set up history: it was combat. env.modeSource.expect_getMode(tt_Mode.Combat); env.delay.getMode(); env.modeSource.expect_getMode(tt_Mode.Explore); env.targetSource.expect_getTargets(tt_Targets.of()); int result = env.delay.getMode(); it(tag .. ": no enemies", AssertEval(result, "==", tt_Mode.None)); assertSatisfaction(env.getSatisfaction(), tag); } { let tag = "tt_DelayedCombatModeSource: checkEnemiesStillCombat"; let env = tt_DelayedCombatModeSourceTestEnvironment.of(); // set up history: it was combat. env.modeSource.expect_getMode(tt_Mode.Combat); env.delay.getMode(); { // set expectations env.modeSource.expect_getMode(tt_Mode.Explore); let targets = tt_Targets.of(); targets.add(NULL); env.targetSource.expect_getTargets(targets); env.clock.expect_getNow(0); env.clock.expect_since(0); } int result = env.delay.getMode(); it(tag .. ": still combat", AssertEval(result, "==", tt_Mode.Combat)); assertSatisfaction(env.getSatisfaction(), tag); } { let tag = "tt_DelayedCombatModeSource: checkEnemiesTimeIsUp"; let env = tt_DelayedCombatModeSourceTestEnvironment.of(); // set up history: it was combat. env.modeSource.expect_getMode(tt_Mode.Combat); env.delay.getMode(); { // set expectations env.modeSource.expect_getMode(tt_Mode.Explore); let targets = tt_Targets.of(); targets.add(NULL); env.targetSource.expect_getTargets(targets); env.clock.expect_getNow(0); env.clock.expect_since(999); } int result = env.delay.getMode(); it(tag .. ": no more combat", AssertEval(result, "==", tt_Mode.None)); assertSatisfaction(env.getSatisfaction(), tag); } { let tag = "tt_DelayedCombatModeSource: checkOldExploration"; let env = tt_DelayedCombatModeSourceTestEnvironment.of(); env.modeSource.expect_getMode(tt_Mode.Explore, 2); env.clock.expect_getNow(0, 0); env.clock.expect_since(0, 0); env.targetSource.expect_getTargets(tt_Targets.of(), 2); int result1 = env.delay.getMode(); it(tag .. ": old Exploration -> None", AssertEval(result1, "==", tt_Mode.None)); int result2 = env.delay.getMode(); it(tag .. ": again, old Exploration -> None", AssertEval(result2, "==", tt_Mode.None)); assertSatisfaction(env.getSatisfaction(), tag); } { Array<tt_ModeSource> sources; let cascade = tt_ModeCascade.of(sources); int mode = cascade.getMode(); it("tt_ModeCascade: check zero sources: No source -> no mode", AssertEval(mode, "==", tt_Mode.None)); } { let source1 = tt_ModeSourceMock.of(); let source2 = tt_ModeSourceMock.of(); source1.expect_getMode(tt_Mode.Explore); source2.expect_getMode(tt_Mode.Combat); Array<tt_ModeSource> sources = {source1, source2}; int mode = tt_ModeCascade.of(sources).getMode(); it("tt_ModeCascade: check cascade first: Must be the first mode", AssertEval(mode, "==", tt_Mode.Explore)); } { let source1 = tt_ModeSourceMock.of(); let source2 = tt_ModeSourceMock.of(); source1.expect_getMode(tt_Mode.None); source2.expect_getMode(tt_Mode.Combat); Array<tt_ModeSource> sources = {source1, source2}; int mode = tt_ModeCascade.of(sources).getMode(); it("tt_ModeCascade: check cascade second: Must be the second mode", AssertEval(mode, "==", tt_Mode.Combat)); } { let tag = "tt_ReportedModeSource: checkInitial"; let env = tt_ReportedModeSourceTestEnvironment.of(); int expected = tt_Mode.Explore; env.modeSource.expect_getMode(expected); int mode = env.reportedMode.getMode(); it(tag .. ": explore after init", AssertEval(mode, "==", expected)); assertSatisfaction(env.getSatisfaction(), tag); } { let tag = "tt_ReportedModeSource: checkExplorationToCombat"; let env = tt_ReportedModeSourceTestEnvironment.of(); env.reporter.expect_report(); env.modeSource.expect_getMode(tt_Mode.Explore); int mode1 = env.reportedMode.getMode(); env.modeSource.expect_getMode(tt_Mode.Combat); int mode2 = env.reportedMode.getMode(); assertSatisfaction(env.getSatisfaction(), tag); } { let tag = "tt_ReportedModeSource: checkCombatToExploration"; let env = tt_ReportedModeSourceTestEnvironment.of(); env.reporter.expect_report(); env.modeSource.expect_getMode(tt_Mode.Combat); int mode1 = env.reportedMode.getMode(); env.modeSource.expect_getMode(tt_Mode.Explore); int mode2 = env.reportedMode.getMode(); assertSatisfaction(env.getSatisfaction(), tag); } { let settableMode = tt_SettableMode.of(); int before = tt_Mode.Combat; settableMode.setMode(before); int after = settableMode.getMode(); it("tt_SettableMode: mode must be the same", AssertEval(before, "==", after)); } { let tag = "tt_PlayerOriginSource"; double x = 1; double y = 2; double z = 3; let player = PlayerPawn(spawn("DoomPlayer", (x, y, z))); let playerSource = tt_PlayerSourceMock.of(); let originSource = tt_PlayerOriginSource.of(playerSource); playerSource.expect_getPawn(player); let origin = originSource.getOrigin().getVector(); it(tag .. ": X matches", AssertEval(x, "==", origin.x)); it(tag .. ": Y matches", AssertEval(y, "==", origin.y)); it(tag .. ": Z in range", AssertEval(z, "<=", origin.z)); it(tag .. ": Z in range", AssertEval(z + player.Height, ">=", origin.z)); assertSatisfaction(playerSource.getSatisfaction(), tag); cleanUpSpawned(); } { let tag = "tt_QuestionAnswerMatcher: checkNullKnownTargets"; let env = tt_QuestionAnswerMatcherTestEnvironment.of(); env.targetSource.expect_getTargets(NULL); let origin = env.matcher.getOrigin(); it(tag .. ": NULL known targets -> NULL origin", AssertNull(origin)); assertSatisfaction(env.getSatisfaction(), tag); } { let tag = "tt_QuestionAnswerMatcher: checkZeroKnownTargets"; let env = tt_QuestionAnswerMatcherTestEnvironment.of(); let targets = tt_KnownTargets.of(); env.targetSource.expect_getTargets(targets); let origin = env.matcher.getOrigin(); it(tag .. "Zero known targets -> NULL origin", AssertNull(origin)); assertSatisfaction(env.getSatisfaction(), tag); } { let tag = "tt_QuestionAnswerMatcher: checkNullKnownTarget"; let env = tt_QuestionAnswerMatcherTestEnvironment.of(); let targets = tt_KnownTargets.of(); targets.add(NULL); env.targetSource.expect_getTargets(targets); env.answerSource.expect_getAnswer(NULL); let origin = env.matcher.getOrigin(); it(tag .. ": NULL known target -> NULL origin", AssertNull(origin)); assertSatisfaction(env.getSatisfaction(), tag); } { let tag = "tt_QuestionAnswerMatcher: checkNullAnswer"; let env = tt_QuestionAnswerMatcherTestEnvironment.of(); let knownTargets = tt_KnownTargets.of(); let target = tt_Target.of(NULL); let question = tt_QuestionMock.of(); let knownTarget = tt_KnownTarget.of(target, question); knownTargets.add(knownTarget); env.targetSource.expect_getTargets(knownTargets); env.answerSource.expect_getAnswer(NULL); let origin = env.matcher.getOrigin(); it(tag .. ": NULL answer -> NULL origin", AssertNull(origin)); assertSatisfaction(env.getSatisfaction(), tag); } { let tag = "tt_QuestionAnswerMatcher: checkKnownTargetAndAnswerMatch"; let env = tt_QuestionAnswerMatcherTestEnvironment.of(); let knownTargets = tt_KnownTargets.of(); let target = tt_Target.of(spawn("Demon", (0, 0, 0))); let question = tt_QuestionMock.of(); let knownTarget = tt_KnownTarget.of(target, question); knownTargets.add(knownTarget); env.targetSource.expect_getTargets(knownTargets); question.expect_isRight(true); let answer = tt_Answer.of("abc"); env.answerSource.expect_getAnswer(answer); env.stateSource.expect_getAnswerState(tt_AnswerState.of(tt_AnswerState.Ready)); let origin = env.matcher.getOrigin(); assertSatisfaction(question.getSatisfaction(), tag); it(tag .. ": match: valid origin", AssertNotNull(origin)); assertSatisfaction(env.getSatisfaction(), tag); cleanUpSpawned(); } { let tag = "tt_QuestionAnswerMatcher: checkKnownTargetAndAnswerNoMatch"; let env = tt_QuestionAnswerMatcherTestEnvironment.of(); let knownTargets = tt_KnownTargets.of(); let target = tt_Target.of(NULL); let question = tt_QuestionMock.of(); let knownTarget = tt_KnownTarget.of(target, question); knownTargets.add(knownTarget); env.targetSource.expect_getTargets(knownTargets); question.expect_isRight(false); let answer = tt_Answer.of("abc"); env.answerSource.expect_getAnswer(answer); env.stateSource.expect_getAnswerState(tt_AnswerState.of(tt_AnswerState.Ready)); let origin = env.matcher.getOrigin(); assertSatisfaction(question.getSatisfaction(), tag); it(tag .. ": no match: NULL origin" , AssertNull(origin)); assertSatisfaction(env.getSatisfaction(), tag); } { // Info, unlike pawns, exist even for non-existent players. for (int playerNumber = 0; playerNumber < MAXPLAYERS; ++playerNumber) { let source = tt_PlayerSourceImpl.of(playerNumber); let info = source.getInfo(); let note = "tt_PlayerSourceImpl: player info (%d) must be not NULL"; it(string.format(note, playerNumber), Assert(info != NULL)); } } { let source = tt_PlayerSourceImpl.of(consolePlayer); let pawn = source.getPawn(); let note = "tt_PlayerSourceImpl: must get main player (%d) actor"; it(string.format(note, consolePlayer), AssertNotNull(pawn)); } { let note = "tt_PlayerSourceImpl: other player (%d) must be null"; // Since tests are run on single-player game, no other players must exist. for (int i = 1; i < MAXPLAYERS; ++i) { int playerNumber = (consolePlayer + i) % MAXPLAYERS; let source = tt_PlayerSourceImpl.of(playerNumber); let pawn = source.getPawn(); it(string.format(note, playerNumber), AssertNull(pawn)); } } { let tag = "tt_StaleMarker: checkFirstRead"; let env = tt_StaleMarkerImplTestEnvironment.of(); env.clock.expect_getNow(0); bool isStale = env.staleMarker.isStale(); it(tag .. ": first read: stale", Assert(isStale)); assertSatisfaction(env.getSatisfaction(), tag); } { let tag = "tt_StaleMarker: checkNotYetStale"; let env = tt_StaleMarkerImplTestEnvironment.of(); env.clock.expect_getNow(0); bool isStale1 = env.staleMarker.isStale(); env.clock.expect_since(0); bool isStale2 = env.staleMarker.isStale(); it(tag .. ": same tick: not stale", Assert(!isStale2)); assertSatisfaction(env.getSatisfaction(), tag); } { let tag = "tt_StaleMarker: checkAlreadyStale"; let env = tt_StaleMarkerImplTestEnvironment.of(); env.clock.expect_getNow(0, 2); bool isStale1 = env.staleMarker.isStale(); env.clock.expect_since(1); bool isStale2 = env.staleMarker.isStale(); it(tag .. ": new tick: stale", Assert(isStale2)); assertSatisfaction(env.getSatisfaction(), tag); } { let strings = tt_Strings.of(); let size = strings.size(); it("tt_Strings: New Strings is empty", AssertEval(size, "==", 0)); } { let strings = tt_Strings.of(); let str = "a"; strings.add(str); let size = strings.size(); it("tt_Strings: Element must be added", AssertEval(size, "==", 1)); it("tt_Strings: Element must be the same", Assert(strings.at(0) == str)); } { let tag = "tt_TargetRadar: checkActorsAround"; let env = tt_TargetRadarTestEnvironment.of(); Array<Actor> actors = { spawn("DoomImp", ( 5, 0, 0)), spawn("DoomImp", (-5, 0, 0)), spawn("DoomImp", ( 0, 5, 0)), spawn("DoomImp", ( 0, -5, 0)), spawn("DoomImp", ( 0, 0, 5)), spawn("DoomImp", ( 0, 0, -5)) }; env.originSource.expect_getOrigin(tt_Origin.of((0, 0, 0))); let targets = env.targetRadar.getTargets(); uint nActors = actors.size(); for (uint i = 0; i < nActors; ++i) { let a = tt_Target.of(actors[i]); it(string.format(tag .. ": actor %d is present in list", i), Assert(targets.contains(a))); } assertSatisfaction(env.originSource.getSatisfaction(), tag); cleanUpSpawned(); } { let tag = "tt_TargetRadar: checkDistantActor"; let env = tt_TargetRadarTestEnvironment.of(); env.originSource.expect_getOrigin(tt_Origin.of((0, 0, 0))); let distantActor = spawn("DoomImp", (1000, 0, 0)); let distantTarget = tt_Target.of(distantActor); let targets = env.targetRadar.getTargets(); it(tag .. ": distant actor is not in list", AssertFalse(targets.contains(distantTarget))); assertSatisfaction(env.originSource.getSatisfaction(), tag); cleanUpSpawned(); } { let tag = "tt_TargetRadar: checkNonLivingActor"; let env = tt_TargetRadarTestEnvironment.of(); env.originSource.expect_getOrigin(tt_Origin.of((0, 0, 0))); let nonLiving = spawn("Medikit", (1, 0, 0)); let targets = env.targetRadar.getTargets(); let nonLivingTarget = tt_Target.of(nonLiving); it(tag .. ": non-living actor is not in list", AssertFalse(targets.contains(nonLivingTarget))); assertSatisfaction(env.originSource.getSatisfaction(), tag); cleanUpSpawned(); } { let tag = "tt_TargetRadar: checkDeadActor"; let env = tt_TargetRadarTestEnvironment.of(); env.originSource.expect_getOrigin(tt_Origin.of((0, 0, 0))); let deadActor = spawnDead("DoomImp", (1, 0, 0)); let targets = env.targetRadar.getTargets(); let deadTarget = tt_Target.of(deadActor); it(tag .. ": dead actor is not in list", AssertFalse(targets.contains(deadTarget))); assertSatisfaction(env.originSource.getSatisfaction(), tag); cleanUpSpawned(); } { let _deathReporter = tt_DeathReporter.of(); let targetsBefore = _deathReporter.getTargets(); it("tt_DeathReporter: No targets before reporting", AssertEval(targetsBefore.size(), "==", 0)); let something = spawn("DoomImp", (0, 0, 0)); _deathReporter.reportDead(something); let targetsAfter = _deathReporter.getTargets(); it("tt_DeathReporter: Single target after reporting", AssertEval(targetsAfter.size(), "==", 1)); let targetsAfterAfter = _deathReporter.getTargets(); it("tt_DeathReporter: No new targets", AssertEval(targetsAfterAfter.size(), "==", 0)); cleanUpSpawned(); } { let tag = "tt_SorterByDistance : checkEmpty"; let before = tt_TargetWidgets.of(); let origin = tt_Origin.of((0, 0, 0)); let after = tt_SorterByDistance.sort(before, origin.getVector()); it(tag .. ": empty collection must remain empty", AssertEval(after.size(), "==", 0)); } { let tag = "tt_SorterByDistance : checkSorted"; let origin = tt_Origin.of((0, 0, 0)); let before = tt_TargetWidgets.of(); before.add(tt_SorterByDistanceTest.createAtPosition(spawn("Demon", (0, 0, 2)))); before.add(tt_SorterByDistanceTest.createAtPosition(spawn("Demon", (0, 0, 1)))); before.add(tt_SorterByDistanceTest.createAtPosition(spawn("Demon", (0, 0, 0)))); it(tag .. ": Before: sorted", Assert(tt_SorterByDistanceTest.isSorted(before, origin.getVector()))); let after = tt_SorterByDistance.sort(before, origin.getVector()); it(tag .. ": size of collection must the same", AssertEval(after.size(), "==", before.size())); it(tag .. ": contains same elements", Assert(tt_SorterByDistanceTest.isSameElements(before, after))); it(tag .. ": after: sorted", Assert(tt_SorterByDistanceTest.isSorted(after, origin.getVector()))); cleanUpSpawned(); } { let tag = "tt_SorterByDistance : checkReverse"; let origin = tt_Origin.of((0, 0, 0)); let before = tt_TargetWidgets.of(); before.add(tt_SorterByDistanceTest.createAtPosition(spawn("Demon", (0, 0, 0)))); before.add(tt_SorterByDistanceTest.createAtPosition(spawn("Demon", (0, 0, 1)))); before.add(tt_SorterByDistanceTest.createAtPosition(spawn("Demon", (0, 0, 2)))); it(tag .. ": before: not sorted", Assert(!tt_SorterByDistanceTest.isSorted(before, origin.getVector()))); let after = tt_SorterByDistance.sort(before, origin.getVector()); it(tag .. ": size of collection must the same", AssertEval(after.size(), "==", before.size())); it(tag .. ": contains same elements", Assert(tt_SorterByDistanceTest.isSameElements(before, after))); it(tag .. ": after: sorted", Assert(tt_SorterByDistanceTest.isSorted(after, origin.getVector()))); cleanUpSpawned(); } { let tag = "tt_SorterByDistance : middle"; let origin = tt_Origin.of((0, 0, 0)); let before = tt_TargetWidgets.of(); before.add(tt_SorterByDistanceTest.createAtPosition(spawn("Demon", (0, 0, 1)))); before.add(tt_SorterByDistanceTest.createAtPosition(spawn("Demon", (0, 0, 2)))); before.add(tt_SorterByDistanceTest.createAtPosition(spawn("Demon", (0, 0, 0)))); it(tag .. ": before: not sorted", Assert(!tt_SorterByDistanceTest.isSorted(before, origin.getVector()))); let after = tt_SorterByDistance.sort(before, origin.getVector()); it(tag .. ": size of collection must the same", AssertEval(after.size(), "==", before.size())); it(tag .. ": contains same elements", Assert(tt_SorterByDistanceTest.isSameElements(before, after))); it(tag .. ": after: sorted", Assert(tt_SorterByDistanceTest.isSorted(after, origin.getVector()))); cleanUpSpawned(); } { let tag = "tt_Gunner: null origin"; let env = tt_GunnerTestEnvironment.of(); env.originSource.expect_getOrigin(NULL); env.gunner.doEffect(); assertSatisfaction(env.getSatisfaction(), tag); } { let tag = "tt_Gunner: valid origin"; let env = tt_GunnerTestEnvironment.of(); let origin = tt_Origin.of((0, 0, 0)); env.originSource.expect_getOrigin(origin); env.effect.expect_doEffect(); env.gunner.doEffect(); assertSatisfaction(env.getSatisfaction(), tag); } } // Note: don't forget to call cleanUpSpawned at the end of the test case! protected play Actor spawn(class<Actor> type, vector3 pos) const { let result = Actor.spawn(type, pos); _spawned.push(result); return result; } // Note: don't forget to call cleanUpSpawned at the end of the test case! protected play Actor spawnDead(class<Actor> type, vector3 pos) const { let result = Actor.spawn(type, pos); result.a_Die(); _spawned.push(result); return result; } protected play void cleanUpSpawned() const { foreach (anActor : _spawned) anActor.destroy(); _spawned.clear(); } protected void assertSatisfaction(tt_Satisfaction satisfaction, string tag) { foreach (mock, isSatisfied : satisfaction.values) it(tag .. ": " .. mock, Assert(isSatisfied)); } Array<Actor> _spawned; } class tt_Satisfaction { static tt_Satisfaction of() { return new("tt_Satisfaction"); } tt_Satisfaction add(tt_Satisfaction other) { foreach (tag, value : other.values) values.insert(tag, value); return self; } tt_Satisfaction push(string tag, bool value) { values.insert(tag, value); return self; } Map<string, bool> values; }
30.1. Mock Macro
class tt_Expectation { static tt_Expectation of(string methodName) { let result = new("tt_Expectation"); result.methodName = methodName; result.expected = 0; result.called = 0; return result; } void expect(int expectedCount) { expected = expectedCount; called = 0; } string methodName; int expected; int called; } mixin class tt_Mock { private tt_Expectation _mock_addExpectation(string methodName) { let result = tt_Expectation.of(methodName); _expectations.push(result); return result; } tt_Satisfaction getSatisfaction() const { let result = tt_Satisfaction.of(); let name = getClassName(); foreach (expectation : _expectations) { bool isSatisfied = expectation.expected == expectation.called; string status = name .. ": " .. expectation.methodName; if (!isSatisfied) { status.appendFormat(" (expected: %d, called: %d)", expectation.expected, expectation.called); } result.push(status, isSatisfied); } return result; } private Array<tt_Expectation> _expectations; }