Typist.pk3
Table of Contents
- 1. About
- 2. Event Handler
- 3. Player Handler
- 4. Server
- 5. World Changer
- 6. Activatable
- 7. Answer
- 8. Answer State
- 9. Character
- 10. Clock
- 11. Event Reporters
- 12. Input Manager
- 13. Key Processor
- 14. Known Target
- 15. Lesson
- 16. Math
- 17. Mode
- 18. Origin
- 19. Player
- 20. Question
- 21. Settings
- 22. Stale Marker
- 23. Strings
- 24. Target
- 25. Target Widget
- 26. Colors
- 27. View
- 28. Effect
- 29. Options Menu
- 30. Mod setup
- 31. Tests
1. About
Typist.pk3 turns FPS games into typing exercises.
1.1. How to play
The default game mode is Exploration. It is like normal game play, except when you find enemies. When there are enemies, the game switches to Combat mode. In Combat mode, instead of aiming and firing, you type. What to type is displayed on screen. When what you type matches with an enemy, Typist.pk3 aims and fires for you. The match is checked when you press Space or Enter key, and there is an option to match immediately. If matching with Space or Enter is chosen, Space or Enter can be held to fire continuously. Press Ctrl-Backspace to erase all input.
The Combat mode ends automatically when there are no enemies, or can be disabled
manually with Esc key. Disabling the Combat mode manually locks the game into
Exploration mode. The automatic mode switching can be enabled back with a key
(configured in Controls section).
To accommodate the slower pace of the game, the game is automatically changed:
- enemies move slower
- weapons damage is increased
- projectiles that fly towards the player move slower
- the player cannot die, only lose health only down to 1 point (can be disabled)
1.2. Features
- several predefined lessons:
- 1000 English words
- random characters (configurable)
- arithmetic operations
- custom words (a text file named
typist_custom_text.txtwith your words can be loaded together with Typist.pk3)
- "Pass-through" command (
/passby default): after you enter this command, the following keyboard key acts like in normal game, so you can switch weapons, move and do other actions while remaining in Combat mode - scoring, with high scores table
- options to enable infinite ammo, to disable enemy infighting, and to configure the HUD
- several sound themes for Typist.pk3 events (can be turned off)
- configurable colors: copy
tt_colors.zs, edit values, and load it together with Typist.pk3 - multiplayer
1.3. Key setup
Typist.pk3 doesn't require any special key setup. However, for smoother experience, it may be worth playing keyboard-only with the following assigned keys. This way, you'll waste no time switching from mouse to keyboard and back, and your fingers are almost at the right typing position.
- E - move forward
- S - strafe left
- D - move backward
- F - strafe right
- I - center view
- J - turn left
- K - turn around
- L - turn right
1.4. Compatibility
Typist.pk3 isn't coded specifically for any game, so there is a chance it is compatible with many GZDoom/UZDoom mods and games.
- Typist.pk3 probably won't play well with mods and games which rely on melee combat, because Typist.pk3 will only aim and fire, not move.
- Actors that are normally friendly, but changed their friendliness by scripts, don't count as targets.
- Guncaster and Guncaster Vindicated are not compatible with Typist.pk3. Reason: Guncaster reads player input directly from player, so weapon firing cannot be emulated like for other mods. Guncaster support cannot be added without modifications in Guncaster or GZDoom.
2. Event Handler
2.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 playerDisconnected(PlayerEvent event) { _server.removePlayer(event.playerNumber); } override void playerDied(PlayerEvent event) { _playerHandler.setMode(tt_Mode.Explore); } override void playerRespawned(PlayerEvent event) { _playerHandler.setMode(tt_Mode.None); } 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.setMode(tt_Mode.None); else if (command == "tt_force_combat" ) _playerHandler.setMode(tt_Mode.Combat); 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)); } } int getMode() const { return _playerHandler.getMode(); } private tt_PlayerHandler _playerHandler; private tt_Server _server; }
2.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; } }
2.3. Buddha
class tt_Buddha : PowerBuddha { Default { // https://zdoom.org/wiki/Powerup_properties Powerup.Duration 0x7FFFFFFD; +INVENTORY.UNDROPPABLE; } }
3. Player Handler
3.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(); // Mode is from tt_Mode. abstract void setMode(int mode); // Mode is from tt_Mode. abstract int getMode() const; ui abstract void draw(RenderEvent event); }
3.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 setMode(int mode) { _manualModeSource.setMode(mode); } override int getMode() const { return _modeSource.getMode(); } 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; }
4. Server
4.1. Server
class tt_Server { static tt_Server of() { let result = new("tt_Server"); result._globalChangers = tt_PlayerWorldChangers.of(); 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 freelookSetting = tt_BoolCvar .of(playerSource, "freelook"); Array<tt_WorldChanger> targetChangers = { tt_HorizontalAimer.of(targetOriginSource, playerSource), tt_VerticalAimer.of(targetOriginSource, playerSource, freelookSetting), 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(playerNumber, tt_WorldChangers.of(globalChangers)); } void removePlayer(int playerNumber) { _globalChangers.remove(playerNumber); } 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_PlayerWorldChangers _globalChangers; }
5. World Changer
5.1. WorldChanger
// This interface represents entities that change the world state. class tt_WorldChanger abstract { play abstract void changeWorld(); }
5.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; } void add(tt_WorldChanger changer) { _changers.push(changer); } override void changeWorld() { foreach (changer : _changers) changer.changeWorld(); } private Array<tt_WorldChanger> _changers; }
5.3. PlayerWorldChangers
// Implements tt_WorldChanger by executing not-NULL world changers. class tt_PlayerWorldChangers : tt_WorldChanger { static tt_PlayerWorldChangers of() { return new("tt_PlayerWorldChangers"); } void add(int playerNumber, tt_WorldChanger changer) { _changers[playerNumber] = changer; } void remove(int playerNumber) { _changers[playerNumber] = NULL; } override void changeWorld() { foreach (changer : _changers) if (changer != NULL) changer.changeWorld(); } private tt_WorldChanger _changers[MAXPLAYERS]; }
5.4. 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; }
5.5. 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 = vel cross 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; }
5.6. 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; }
5.7. 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 diff = level.vec2Diff(myPosition, otherPosition); return vectorAngle(diff.x, diff.y); } private tt_OriginSource _targetOriginSource; private tt_PlayerSource _playerSource; }
5.7.1. Test
{
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();
}
}
5.8. VerticalAimer
// Implements tt_WorldChanger interface by adjusting the player pitch // (horizontal angle). If freelook is disabled, no pitch adjustment is done. // TODO: fix when is an enemy is visible, but its middle is not. class tt_VerticalAimer : tt_WorldChanger { static tt_VerticalAimer of(tt_OriginSource targetOriginSource, tt_PlayerSource playerSource, tt_BoolSetting freelookSetting) { let result = new("tt_VerticalAimer"); result._targetOriginSource = targetOriginSource; result._playerSource = playerSource; result._freelookSetting = freelookSetting; return result; } override void changeWorld() { if (_freelookSetting.get()) setPitch(); } 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 = level.vec3Diff(myPosition, otherPosition); double pitch = -atan2(diff.z, diff.xy.Length()); pawn.a_SetPitch(pitch, SPF_INTERPOLATE); } private tt_OriginSource _targetOriginSource; private tt_PlayerSource _playerSource; private tt_BoolSetting _freelookSetting; }
5.8.1. Test
{
let tag = "tt_VerticalAimer: freelook";
let targetOriginSource = tt_OriginSourceMock.of();
let playerSource = tt_PlayerSourceMock.of();
let freelookSetting = tt_BoolSettingMock.of();
let aimer = tt_VerticalAimer.of(targetOriginSource, playerSource, freelookSetting);
let pawn = players[consolePlayer].mo;
pawn.setOrigin((0, 0, 0), false);
targetOriginSource.expect_getOrigin(tt_Origin.of((0, 10, 20)));
playerSource .expect_getPawn(pawn);
freelookSetting .expect_get(true);
aimer.changeWorld();
assertSatisfaction(targetOriginSource.getSatisfaction(), tag);
assertSatisfaction(playerSource.getSatisfaction(), tag);
assertSatisfaction(freelookSetting.getSatisfaction(), tag);
}
{
let tag = "tt_VerticalAimer: no freelook";
let targetOriginSource = tt_OriginSourceMock.of();
let playerSource = tt_PlayerSourceMock.of();
let freelookSetting = tt_BoolSettingMock.of();
let aimer = tt_VerticalAimer.of(targetOriginSource, playerSource, freelookSetting);
freelookSetting.expect_get(false);
aimer.changeWorld();
assertSatisfaction(targetOriginSource.getSatisfaction(), tag);
assertSatisfaction(playerSource.getSatisfaction(), tag);
assertSatisfaction(freelookSetting.getSatisfaction(), tag);
}
5.9. Firer
// 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; }
5.9.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));
}
6. Activatable
6.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(); }
6.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; }
6.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; result._commands = tt_Strings.ofOne(""); return result; } override void activate() { _inputManager.setPassThrough(); } override tt_Strings getCommands() { _commands.set(0, _passThroughSetting.get()); return _commands; } override bool isVisible() { return true; } private tt_PassThroughInputManager _inputManager; private tt_StringSetting _passThroughSetting; private tt_Strings _commands; }
6.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; result._commands = tt_Strings.of(); return result; } override void activate() { let answerState = _answerStateSource.getAnswerState(); if (!tt_AnswerState.isReady(answerState) && !_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() { _commands.clear(); foreach (activatable : _activatables) { if (!activatable.isVisible()) continue; let commands = activatable.getCommands(); uint nCommands = commands.size(); for (uint c = 0; c < nCommands; ++c) _commands.add(commands.at(c)); } return _commands; } 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; private tt_Strings _commands; }
6.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.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; }
7. Answer
7.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; }
7.2. AnswerSource
// This interface represents a source of answers. class tt_AnswerSource : tt_KeyProcessor abstract { abstract tt_Answer getAnswer(); // Clears answer. abstract void reset(); }
7.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; }
7.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; }
7.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; }
7.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; }
8. Answer State
8.1. AnswerState
// Represents Answer state. // See tt_Answer class. class tt_AnswerState { enum _ { Unknown, Preparing, Ready, Finished } static bool isReady(int state) { return state >= Ready; } static bool isFinished(int state) { return state == Finished; } }
8.2. AnswerStateSource
// This interface provides access to tt_AnswerState. class tt_AnswerStateSource : tt_KeyProcessor abstract { // Returns value from tt_AnswerState. abstract int getAnswerState(); abstract void reset(); }
8.2.1. Mock
class tt_AnswerStateSourceMock : tt_AnswerStateSource { static tt_AnswerStateSourceMock of() { return new("tt_AnswerStateSourceMock"); } mixin tt_Mock; override int getAnswerState() { if (_mock_getAnswerState_expectation == NULL) _mock_getAnswerState_expectation = _mock_addExpectation("getAnswerState"); ++_mock_getAnswerState_expectation.called; return _mock_getAnswerState; } void expect_getAnswerState(int 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 int _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; }
8.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 int getAnswerState() { return _answerState; } override void reset() { _answerState = DEFAULT_STATE; } const DEFAULT_STATE = tt_AnswerState.Preparing; private int _answerState; }
9. Character
9.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; }
9.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));
}
10. Clock
10.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); }
10.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; }
10.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; } }
10.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));
}
11. Event Reporters
11.1. AnswerReporter
// Interface for reporting answer matching events. class tt_AnswerReporter abstract { abstract void reportMatch(); abstract void reportNotMatch(); }
11.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; }
11.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; }
11.3. KeyPressReporter
// Interface for reporting key press events. class tt_KeyPressReporter abstract { abstract void report(); }
11.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; }
11.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; }
11.5. ModeReporter
// Interface for reporting mode change events. class tt_ModeReporter abstract { abstract void report(int mode); }
11.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; }
11.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; }
11.7. SoundPlayer
// This is an interface for playing sounds. class tt_SoundPlayer abstract { abstract void playSound(String soundId); }
11.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; }
11.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
12. Input Manager
12.1. InputManager
// Helps managing user input. class tt_InputManager abstract { abstract void manageInput(); abstract bool isCapturingKeys(); }
12.2. 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; }
12.3. 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 } }
13. Key Processor
13.1. KeyProcessor
// This interface represents an entity that processes input keys. class tt_KeyProcessor abstract { abstract void processKey(tt_Character character); }
13.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; }
14. Known Target
14.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; }
14.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); } } void clear() { _targets.clear(); } // 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; }
14.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; }
14.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; }
14.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; }
14.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(); result._newKnownTargets = tt_KnownTargets.of(); result._pruned = 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(); 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); _newKnownTargets.clear(); } // 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() { 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)); } tt_KnownTargets temp = _registry; _registry = _pruned; _pruned = temp; _pruned.clear(); } private tt_TargetSource _targetSource; private tt_Lesson _lesson; private tt_TargetSource _disabledTargetSource; private tt_KnownTargets _registry; private tt_KnownTargets _newKnownTargets; private tt_KnownTargets _pruned; }
14.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; }
14.6. VisibleKnownTargetSource
// Implements tt_KnownTargetSource by providing only targets visible to player. 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; result._targets = tt_KnownTargets.of(); return result; } override tt_KnownTargets getTargets() const { _targets.clear(); if (_base.isEmpty()) return _targets; 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)) _targets.add(target); } return _targets; } 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; private tt_KnownTargets _targets; }
14.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; }
15. Lesson
15.1. Lesson
// Interface for getting Questions. class tt_Lesson abstract { abstract tt_Question getQuestion(); }
15.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; }
15.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())));
}
15.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; }
15.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; }
15.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; } }
15.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 == "привет"));
}
привет
16. 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; } // Max effective distance. const MAX_DISTANCE = 700; }
17. Mode
17.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. } }
17.2. ModeSource
// This interface represents a source of modes. // See: tt_Mode. class tt_ModeSource abstract { abstract int getMode(); }
17.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; }
17.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; }
17.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);
}
17.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; }
17.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));
}
17.6. ModeStorage
// This is an interface for storing and retrieving mode. class tt_ModeStorage : tt_ModeSource abstract { abstract void setMode(int mode); }
17.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; }
17.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; }
17.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));
}
17.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; } }
18. Origin
18.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; } void setVector(vector3 value) { _pos = value; } private vector3 _pos; }
18.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(); }
18.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; }
18.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; }
18.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; }
18.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; result._origin = tt_Origin.of((0, 0, 0)); return result; } override tt_Origin getOrigin() { let pawn = _playerSource.getPawn(); let pos = pawn.pos; pos.z += pawn.height / 2; _origin.setVector(pos); return _origin; } private tt_PlayerSource _playerSource; private tt_Origin _origin; }
{
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();
}
18.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 (!tt_AnswerState.isReady(answerState)) { 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; }
18.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.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.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; }
18.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; }
18.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; }
19. Player
19.1. PlayerSource
// Interface for getting player info and player pawn. class tt_PlayerSource abstract { abstract PlayerInfo getInfo(); abstract PlayerPawn getPawn(); abstract int getNumber(); }
19.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; }
19.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; }
19.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));
}
}
20. Question
20.1. Question
// This interface represents a question. class tt_Question abstract { abstract bool isRight(string answer); abstract string getDescription(); abstract string getHintFor(string answer); }
20.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; }
20.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(); } }
20.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); } 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; }
21. Settings
21.1. BoolSetting
class tt_BoolSetting abstract { abstract bool isDefined(); abstract bool get(); }
21.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; }
21.2. IntSetting
class tt_IntSetting abstract { abstract bool isDefined(); abstract int get(); }
21.3. FloatSetting
class tt_FloatSetting abstract { abstract bool isDefined(); abstract double get(); }
21.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 get() { if (_mock_get_expectation == NULL) _mock_get_expectation = _mock_addExpectation("get"); ++_mock_get_expectation.called; return _mock_get; } void expect_get(double 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 double _mock_get; private tt_Expectation _mock_get_expectation; }
21.4. StringSetting
class tt_StringSetting abstract { abstract bool isDefined(); abstract string get(); }
21.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; }
21.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; }
21.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; }
21.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 get() { return _cvar.getFloat(); } private Cvar _cvar; }
21.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; }
21.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(); }
21.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; }
22. Stale Marker
22.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(); }
22.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; }
22.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; }
23. Strings
23.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]; } void set(uint i, string value) { _strings[i] = value; } bool contains(String str) const { uint foundIndex = _strings.Find(str); bool isFound = (foundIndex != size()); return isFound; } void add(String str) { _strings.push(str); } void clear() { _strings.clear(); } private Array<String> _strings; }
23.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));
}
24. Target
24.1. Target
// Represents an attack target. class tt_Target { static tt_Target of(Actor a) { let result = new("tt_Target"); result._actor = a; result._origin = tt_Origin.of((0, 0, 0)); return result; } // Get position in game space of this target. tt_Origin getPosition() const { vector3 position = _actor.pos; position.z += _actor.height / 2; _origin.setVector(position); return _origin; } bool isEqual(tt_Target other) const { return other._actor == _actor; } Actor getActor() const { return _actor; } private Actor _actor; private tt_Origin _origin; }
24.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); } void clear() { _targets.clear(); } // 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; }
24.3. TargetSource
// This interface represents a source of targets. // See: tt_Target. class tt_TargetSource abstract { abstract tt_Targets getTargets(); }
24.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; }
24.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; result._targets = tt_Targets.of(); return result; } override tt_Targets getTargets() { _targets.clear(); 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)) _targets.add(tt_Target.of(a)); } return _targets; } private static bool isSuitableForTargeting(Actor anActor) { bool isMonster = anActor.bIsMonster; bool isDamageable = !anActor.bNoDamage; bool isAlive = anActor.Health > 0; bool isFriendly = anActor.bFriendly; bool wasFriendly = getDefaultByType(anActor.getClass()).bFriendly; return isMonster && isDamageable && isAlive && !isFriendly && !wasFriendly; } private tt_OriginSource _originSource; private tt_Targets _targets; }
24.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; }
24.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._accumulatedTargets = tt_Targets.of(); result._resultTargets = tt_Targets.of(); return result; } void reportDead(Actor thing) { let newDisabled = tt_Target.of(thing); _accumulatedTargets.add(newDisabled); } override tt_Targets getTargets() { tt_Targets temp = _resultTargets; _resultTargets = _accumulatedTargets; _accumulatedTargets = temp; _accumulatedTargets.clear(); return _resultTargets; } private tt_Targets _accumulatedTargets; private tt_Targets _resultTargets; }
{
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();
}
24.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; }
24.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; result._targets = tt_Targets.of(); return result; } override tt_Targets getTargets() { let targets = _targetSource.getTargets(); _targets.clear(); uint nTargets = targets.size(); for (uint i = 0; i < nTargets; ++i) { tt_Target target = targets.at(i); if (target.getActor() != NULL) _targets.add(target); } return _targets; } private tt_TargetSource _targetSource; private tt_Targets _targets; }
25. Target Widget
25.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; }
25.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; } void clear() { _widgets.clear(); } private Array<tt_TargetWidget> _widgets; }
25.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); }
25.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"); result._widgets = tt_TargetWidgets.of(); return result; } override tt_TargetWidgets getWidgets(RenderEvent event) { let targets = _knownTargetSource.getTargets(); let info = _playerSource.getInfo(); prepareProjection(); _projection.CacheResolution(); _projection.CacheFov(info.fov); _projection.OrientForRenderOverlay(event); _projection.BeginProjection(); tt_le_Viewport viewport; viewport.FromHud(); _widgets.clear(); 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); _widgets.add(widget); } } return _widgets; } // 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; private tt_TargetWidgets _widgets; }
25.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) { if (widgets.size() == 0) return widgets; 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; } }
25.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(); result._newRegistry = tt_TargetWidgets.of(); return result; } override tt_TargetWidgets getWidgets(RenderEvent event) { let widgets = _source.getWidgets(event); 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. tt_TargetWidgets temp = _registry; _registry = _newRegistry; _newRegistry = temp; _newRegistry.clear(); return _registry; } private tt_TargetWidgetSource _source; private tt_TargetWidgets _registry; private tt_TargetWidgets _newRegistry; }
26. 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. } }
27. View
27.1. View
// This interface represents a view - something that displays something. class tt_View abstract { ui abstract void draw(RenderEvent event); }
27.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; }
27.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; } static int getWidth() { return Screen.getWidth() / 64; } override void draw(RenderEvent _) { 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; int screenWidth = Screen.getWidth(); int screenHeight = Screen.getHeight(); int width = getWidth(); int height = width; Color white = "FFFFFF"; Screen.dim(white, alpha, 0, 0, width, screenHeight); Screen.dim(white, alpha, width, 0, screenWidth - width * 2, height); Screen.dim(white, alpha, screenWidth - width, 0, width, screenHeight); Screen.dim(white, alpha, width, screenHeight - height, screenWidth - width * 2, height); } 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; }
27.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; }
27.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 scaledMargin = MARGIN * scale; int frameWidth = tt_Frame.getWidth(); int y = scaledMargin + frameWidth; let answer = _answerSource.getAnswer().getString(); int color = tt_Drawing.getColorForMode(_modeSource.getMode()); int xLeft = halfScreen; int xRight = 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); let [width, height] = tt_Drawing.getBoxSize(questionString, hintedAnswer, scale); tt_Drawing.drawTarget(halfScreen - width / 2, y, width, height, questionString, hintedAnswer, scale, color); xLeft = halfScreen - width / 2 - scaledMargin; xRight = xLeft + width + scaledMargin * 2; } // 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); let [width, height] = tt_Drawing.getBoxSize(questionString, hintedAnswer, scale); if (xRight + width > screenWidth - frameWidth) break; tt_Drawing.drawTarget(xRight, y, width, height, questionString, hintedAnswer, scale, color); xRight += width + scaledMargin; } // 3. Draw the commands to the left while there is space. for (uint c = 0; c < commandCount; ++c) { let command = commands.at(c); let hintedAnswer = tt_Match.getColoredMatch(command, answer); let [width, height] = tt_Drawing.getBoxSize(command, hintedAnswer, scale); bool isCentered = targetCount == 0 && c == 0; let x = isCentered ? halfScreen - width / 2 : xLeft - width; if (x < frameWidth) break; tt_Drawing.drawTarget(x, y, width, height, command, hintedAnswer, scale, color); xLeft -= width + scaledMargin; } // 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); let [width, height] = tt_Drawing.getBoxSize(questionString, hintedAnswer, scale); if (xLeft - width < frameWidth) break; tt_Drawing.drawTarget(xLeft - width, y, width, height, questionString, hintedAnswer, scale, color); xLeft -= width + scaledMargin; } } const MARGIN = 2; private tt_ModeSource _modeSource; private tt_AnswerSource _answerSource; private tt_Activatable _activatable; private tt_KnownTargetSource _targetSource; private tt_IntSetting _scaleSetting; }
27.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(); int screenWidth = Screen.getWidth(); int screenHeight = Screen.getHeight(); int frameWidth = tt_Frame.getWidth(); uint nWidgets = widgets.size(); for (uint i = 0; i < nWidgets; ++i) { let widget = widgets.at(i); let question = widget.getTarget().getQuestion(); let questionString = question.getDescription(); let hintedAnswer = question.getHintFor(answer); let position = widget.getPosition(); let [width, height] = tt_Drawing.getBoxSize(questionString, hintedAnswer, scale); int x = int(clamp(position.x - width / 2, frameWidth, screenWidth - width - frameWidth)); int y = int(clamp(position.y - height, frameWidth, screenHeight - height * 2 - frameWidth)); tt_Drawing.drawTarget(x, y, width, height, questionString, hintedAnswer, scale, color); } } private tt_TargetWidgetSource _targetWidgetSource; private tt_AnswerSource _answerSource; private tt_IntSetting _scaleSetting; private tt_ModeSource _modeSource; }
27.7. Drawing
// Namespace for common drawing functions. class tt_Drawing ui { static int getColorForMode(int mode) { return (mode == tt_Mode.Combat) ? tt_RgbColors.AnswerCombat : tt_RgbColors.AnswerExploration; } static int, int getBoxSize(string question, string answer, int scale) { // One extra BORDER for width: stringWidth tends to underestimate the width. let aFont = NewSmallFont; int height = scale * (BORDER * 4 + aFont.getHeight()); int width = scale * (BORDER * 5 + max(aFont.stringWidth(question), aFont.stringWidth(answer))); return width, height; } static void drawTarget(int x, int y, int width, int height, // Box height, target is two boxes. string question, string answer, int scale, Color answerColor) { drawBox(x, y, width, height, question, scale, tt_RgbColors.Question); drawBox(x, y + height, width, height, answer, scale, answerColor); } private static void drawBox(int x, int y, int width, int height, string text, int scale, Color aColor) { int scaledBorder = BORDER * scale; Color backgroundColor = Color(aColor.r / 2, aColor.g / 2, aColor.b / 2); Screen.dim(aColor, ALPHA, x, y, width, height); Screen.dim(backgroundColor, ALPHA, x + scaledBorder, y + scaledBorder, width - scaledBorder * 2, height - scaledBorder * 2, STYLE_Subtract); Screen.drawText(NewSmallFont, tt_TextColors.Base, x + scaledBorder * 2, y + scaledBorder * 2, text, DTA_ScaleX, scale, DTA_ScaleY, scale); } const BORDER = 2; const ALPHA = 0.2; }
28. Effect
28.1. Effect
// Interface for any non-play effects. class tt_Effect abstract { abstract void doEffect(); }
28.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; }
28.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; }
28.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; }
28.4. 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.Unknown; return result; } override void doEffect() { let newAnswerState = _answerStateSource.getAnswerState(); if (!tt_AnswerState.isFinished(_oldAnswerState) && tt_AnswerState.isFinished(newAnswerState)) { _answerStateSource.reset(); _answerSource.reset(); } _oldAnswerState = newAnswerState; } private tt_AnswerStateSource _answerStateSource; private tt_AnswerSource _answerSource; private int _oldAnswerState; }
28.5. 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.Unknown; return result; } override void doEffect() { let newAnswerState = _answerStateSource.getAnswerState(); if (!tt_AnswerState.isReady(_oldAnswerState) && tt_AnswerState.isReady(newAnswerState)) { 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 int _oldAnswerState; }
28.6. 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; }
29. Options Menu
29.1. Options
AddOptionMenu OptionsMenu { tt_AnimatedSubmenu "$TT_TITLE", tt_Options } OptionMenu tt_Options { tt_PlainTranslator Title "$TT_TITLE" Submenu "Controls", tt_Controls StaticText "" Submenu "General Options", tt_GeneralOptions Submenu "Lesson Options", tt_LessonOptions Submenu "Sound Options", tt_SoundOptions } OptionMenu tt_GeneralOptions { Title "Typist.pk3 General Options" StaticText "" Option "Automatic word confirmation", tt_fast_confirmation, OnOff StaticText "" Slider "Target text scale", tt_view_scale, 1, 4, 1, 0 StaticText "" StaticText "Reduce distractions" StaticText "" Option "Player cannot die", tt_buddha_enabled, OnOff StaticText "Applies on new level start." StaticText "" Option "Infinite ammo", sv_infiniteammo, OnOff Option "Enemy infighting", infighting, tt_InfightingValues 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" } OptionValue tt_InfightingValues { -1, "Never" 0, "Sometimes (normal)" 1, "Always" } OptionMenu tt_LessonOptions { Title "Lesson Options" Option "1000 Basic English Words", tt_is_english_enabled, OnOff Option "Random Characters", tt_is_random_enabled, OnOff Option "Arithmetic", tt_is_maths_enabled, OnOff Option "Custom Text", tt_is_custom_enabled, OnOff StaticText "" Submenu "Random Characters Lesson Configuration", tt_RandomLesson StaticText "" Command "Update targets", tt_reset_targets StaticText "" StaticText "How to set up Custom Text lesson" StaticText "$TT_CUSTOM_LESSON_HELP_TEXT" } OptionMenu tt_Controls { Title "Controls" 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 "Sound Options" Option "Sound effects", tt_sound_enabled, OnOff Option "Typing sound", tt_sound_typing_enabled, OnOff Option "Sound theme", tt_sound_theme, tt_SoundThemes } OptionMenu tt_RandomLesson { Title "Random Characters Lesson Configuration" Slider "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 "0-9" , tt_rc_numbers_enabled , OnOff Option "Punctuation" , tt_rc_punctuation_enabled , OnOff Option "Other characters" , tt_rc_symbols_enabled , OnOff StaticText "" Option "Custom string" , tt_rc_custom_enabled, OnOff TextField "Custom string:" , tt_rc_custom, tt_rc_custom_enabled }
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" }
29.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
29.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; }
29.4. Language
[enu default] TT_TITLE = "Typist.pk3"; TT_UNLOCK = "Unlock Game Mode"; TT_COMBAT = "Force Combat Mode"; TT_SCORE = "Open Score"; TT_CUSTOM_LESSON_HELP_TEXT = "\ 1. Find any text or book in a .txt file (ASCII or UTF-8).\ 2. Rename text file to `typist_custom_text.txt`.\ 3. Load `typist_custom_text.txt` alongside Typist.pk3.\ 4. Enable Custom Text in this menu."; TT_RANDOM_LESSON_UPPERCASE = "A-Z"; TT_RANDOM_LESSON_LOWERCASE = "a-z"; TT_FALLBACK_QUESTION = "<empty lesson>"; [ru] // Note: plain text in menus is translatable. TT_CONTROLS = "Управление";
30. Mod setup
version 4.14.2 #include "zscript/tt_view.zs" #include "zscript/tt_target_widget.zs" #include "zscript/tt_target.zs" #include "zscript/tt_strings.zs" #include "zscript/tt_stale_marker.zs" #include "zscript/tt_settings.zs" #include "zscript/tt_server.zs" #include "zscript/tt_question.zs" #include "zscript/tt_player.zs" #include "zscript/tt_origin.zs" #include "zscript/tt_mode.zs" #include "zscript/tt_option_menu_item_animated_submenu.zs" #include "zscript/tt_math.zs" #include "zscript/tt_lesson.zs" #include "zscript/tt_known_target.zs" #include "zscript/tt_key_processor.zs" #include "zscript/tt_interpolator.zs" #include "zscript/tt_input_manager.zs" #include "zscript/tt_event_reporters.zs" #include "zscript/tt_effect.zs" #include "zscript/tt_clock.zs" #include "zscript/tt_character.zs" #include "zscript/tt_answer_state.zs" #include "zscript/tt_answer.zs" #include "zscript/tt_world_changer.zs" #include "zscript/tt_player_handler.zs" #include "zscript/tt_buddha.zs" #include "zscript/tt_game_tweaks.zs" #include "zscript/tt_event_handler.zs" #include "zscript/tt_activatable.zs" #include "zscript/tt_lp_LazyPointsParameters.zs" #include "tt_colors.zs" #include "zscript/tt_le_libeye.zs" #include "zscript/tt_lp_LazyPoints.zs" #include "zscript/tt_su_StringUtils.zs" #include "zscript/tt_PlainTranslator.zs"
Modules: libeye, LazyPoints, StringUtils, PlainTranslator.
GameInfo
{
EventHandlers =
"tt_lp_Dispatcher",
"tt_lp_StaticView",
"tt_EventHandler"
}
// Variables for score nosave string tt_lp_score = ""; user bool tt_lp_show = true; server string tt_lp_parameters_class = "tt_lp_TypistParameters";
See LazyPoints Parameters documentation.
// LazyPoints customization. class tt_lp_TypistParameters : tt_lp_Parameters { override Font getFont() const { return Font.getFont("NewSmallFont"); } override int getBonusCountdown() const { return 3; } override int getYOffset() const { int scale = max(1, Cvar.getCvar("tt_view_scale", players[consolePlayer]).getInt()); let [width, height] = tt_Drawing.getBoxSize("", "", scale); return height * 2 + tt_InfoPanel.MARGIN; } override int getScale() const { return max(1, Cvar.getCvar("tt_view_scale", players[consolePlayer]).getInt()); } override bool isPickupBonusEnabled() const { return false; } override bool isScoringEnabledNow() const { let eventHandler = tt_EventHandler(EventHandler.find("tt_EventHandler")); return eventHandler.getMode() == tt_Mode.Combat; } }
31. 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: freelook"; let targetOriginSource = tt_OriginSourceMock.of(); let playerSource = tt_PlayerSourceMock.of(); let freelookSetting = tt_BoolSettingMock.of(); let aimer = tt_VerticalAimer.of(targetOriginSource, playerSource, freelookSetting); let pawn = players[consolePlayer].mo; pawn.setOrigin((0, 0, 0), false); targetOriginSource.expect_getOrigin(tt_Origin.of((0, 10, 20))); playerSource .expect_getPawn(pawn); freelookSetting .expect_get(true); aimer.changeWorld(); assertSatisfaction(targetOriginSource.getSatisfaction(), tag); assertSatisfaction(playerSource.getSatisfaction(), tag); assertSatisfaction(freelookSetting.getSatisfaction(), tag); } { let tag = "tt_VerticalAimer: no freelook"; let targetOriginSource = tt_OriginSourceMock.of(); let playerSource = tt_PlayerSourceMock.of(); let freelookSetting = tt_BoolSettingMock.of(); let aimer = tt_VerticalAimer.of(targetOriginSource, playerSource, freelookSetting); freelookSetting.expect_get(false); aimer.changeWorld(); assertSatisfaction(targetOriginSource.getSatisfaction(), tag); assertSatisfaction(playerSource.getSatisfaction(), tag); assertSatisfaction(freelookSetting.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.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.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.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; }