diff --git a/README.md b/README.md index 7040c35..9ed911a 100644 --- a/README.md +++ b/README.md @@ -84,17 +84,16 @@ AI-piloted ships quickly colonized whole galaxies. A ship gains experience during battles. When reaching a certain amount of experience points, a ship will automatically level up (which is, gain 1 level). Each level up will grant -upgrade points that may be spent on Attributes. +upgrade points that may be spent to unlock options. -A ship starts at level 1. There is no upper limit to level value (except 99, for display sake, -but it may not be reached in a classic campaign). +A ship starts at level 1, and may reach up to level 10. ### In-combat values (HSP) In combat, a ship's vitals are represented by the HSP system (Hull-Shield-Power): * **Hull** - Amount of damage that a ship can sustain before having to engage emergency stasis -* **Shield** - Amount of damage that the shield equipments may absorb to protect the Hull +* **Shield** - Amount of damage that the shields may absorb to protect the Hull * **Power** - Available action points (some actions require more power than others) These values will be changed by various effects (usage of equipments, sustained damage...). @@ -115,54 +114,12 @@ Attributes represent a ship's ability to use its HSP system and weapons: * **Maneuverability** - Ability to move first and fast * **Precision** - Ability to target far and good -These attributes are the sum of all currently applied effects (being permanent by an equipped item, -or a temporary effect caused by a weapon or a drone). +These attributes are the sum of all currently applied effects (permanent effects from the ship design, +or temporary effects caused by a weapon or a drone). -For example, a ship that equips a power generator with "power generation +3", but has a sticky effect -of "power generation -1" from a previous weapon hit, will have an effective power generation of 2. +## Battle actions -### Skills - -Skills represent a ship's ability to use equipments: - -* **Materials** - Usage of physical materials such as bullets, shells... -* **Photons** - Forces of light, and electromagnetic radiation -* **Antimatter** - Manipulation of matter and antimatter particles -* **Quantum** - Application of quantum uncertainty principle -* **Gravity** - Interaction with gravitational forces -* **Time** - Control of relativity's time properties - -Each equipment has minimal skill requirements to be used. For example, a weapon may require "materials >= 2" -and "photons >= 3" to be equipped. A ship that does not meet these requirements will not be able to use -the equipment. - -Skills are defined by the player, using points given while leveling up. -As for attributes, skill values may also be altered by equipments. - -If an equipped item has a requirement of "time skill >= 2", that the ship has "time skill" of exactly 2, and -that a temporary effect of "time skill -1" is active, the requirement is no longer fulfilled and the equipped -item is then temporarily disabled (no more effects and cannot be used), until the "time skill -1" effect is lifted. - -## Equipments - -### Overheat/Cooldown - -Equipments may overheat, and need to cooldown for some time, during which it cannot be used. - -If an equipment has "overheat 2 / cooldown 3", using it twice in the same turn will cause it to -overheat. It then needs three "end of turns" to cool down and be available again. Using this equipment -only once per turn is safe, and will never overheat it. - -If an equipment has multiple actions associated, any of these actions will increase the shared heat. - -*Not done yet :* Some equipments may have a "cumulative overheat", meaning that the heat is stored between turns, -cooling down 1 point at the end of turn. - -*Not done yet :* Some equipments may have a "stacked overheat", which -is similar to "cumulative overheat", except it does not cool down at -the end of turn (it will only start cooling down after being overheated). - -## Drones +### Drones Drones are static objects, deployed by ships, that apply effects in a circular zone around themselves. @@ -173,12 +130,13 @@ Drones are fully autonomous, and once deployed, are not controlled by their owne They are small and cannot be the direct target of weapons. -*Not done yet :* They are not affected by area effects, -except for area damage and area effects specifically designed for drones. +### Overheat/Cooldown -## Dockyards +Equipments may overheat, and need to cooldown for some time, during which it cannot be used. -Dockyards are locations where ships can dock to buy or sell equipments, meet other ships and find jobs. +If an action has "overheat 2 / cooldown 3", using it twice in the same turn will cause it to +overheat. It then needs three "end of turns" to cool down and be available again. Using this action +only once per turn is safe, and will never overheat it. ## Keyboard shortcuts diff --git a/TODO.md b/TODO.md index d24ff87..b4b873c 100644 --- a/TODO.md +++ b/TODO.md @@ -8,6 +8,8 @@ Menu/settings/saves * Allow to delete cloud saves * Fix cloud save games with "Level 0 - 0 ships" * Store the game version in saves (for future work on compatibility) +* Add simple options to quick battle (fleet level / difficulty) +* Add optional fleet customization (both player and enemy) to quick battle Map/story --------- @@ -17,39 +19,27 @@ Map/story * Allow to cancel secondary missions * Forbid to end up with more than 5 ships in the fleet because of escorts * Fix problems when several dialogs are active at the same time -* Handle case where cargo is full to give a reward (give money?) +* Add a zoom level, to see the location only Character sheet --------------- -* Add a randomization button in creation view +* Fix the hover/on not working on fleet members +* Improve tooltip content * Replace the close icon by a validation icon in creation view -* Allow to cancel spent skill points (and confirm when closing the sheet) -* Highlight matched/unmatched skills when dragging an equipment to a slot -* Highlight attribute changes when dragging an equipment to a slot -* Propose to auto upgrade skills if enough points are available, when equipping an equipment with unmatched skills -* Improve eye-catching for shop and loot section -* Highlight allowed destinations during drag-and-drop -* Effective skill is sometimes not updated when upgrading base skill -* Add merged cargo display for the whole fleet * Allow to change/buy ship model +* Allow to rename a personality (in creation view only) * Add personality indicators (editable in creation view) -* Add filters and sort options for cargo and shop -* Display level and slot type on equipment -* Fixed tooltips not being visible in loot mode (at the end of battle) Battle ------ * Add a voluntary retreat option -* Add scroll buttons when there are too many actions * Toggle bar/text display in power section of action bar -* Display effects description instead of attribute changes * Show a cooldown indicator on move action icon, if the simulation would cause the engine to overheat * Add engine trail effect, and sound * Allow to skip animations, and allow no animation mode * Find incentives to move from starting position (permanent drones or anomalies?) -* Add a "loot all" button (on the character sheet or outcome dialog?) * Mark targetting in error when target is refused by the action (there is already an arrow for this) * Allow to undo last moves * Add a battle log display @@ -61,10 +51,9 @@ Battle * Add a turn count marker in the ship list * BattleChecks should be done proactively when all diffs have been simulated by an action, in addition to reactively after applying -Ships models and equipments ---------------------------- +Ships models and actions +------------------------ -* Add permanent effects and actions to ship models * Add critical hit/miss (or indicate lucky/unlucky throws) * Add damage over time effect (tricky to make intuitive) * Add actions with cost dependent of distance (like current move actions) @@ -73,11 +62,14 @@ Ships models and equipments * Add mines equivalent (drones that apply only at the end) * RepelEffect should apply on ships in a good order (distance decreasing) * Add hull points to drones and make them take area damage -* Quality modifiers should be based on an "quality difference" to reach +* Add a target type filter (all, enemies, allies, self or not) +* Shields should be able to absorb (some type of) damage, even with 1 remaining +* Add a balance testing page, using AI battles with or without an upgrade, to help in balancing Artificial Intelligence ----------------------- +* Fix tendency to use moves for nothing * Produce interesting "angle" areas * Evaluate active effects * Account for luck @@ -92,7 +84,8 @@ Artificial Intelligence Common UI --------- -* UIBuilder.button should be able to handle hover and pushed images +* Fix calling setHoverClick several times on the same button not working as expected +* Fix tooltip remaining when the hovered object is hidden by animations * If ProgressiveMessage animation performance is bad, show the text directly * Add caret/focus to text input * Mobile: think UI layout so that fingers do not block the view (right and left handed) @@ -105,7 +98,6 @@ Technical * Pack all images in atlases, and split them by stage * Pack sounds * Add toggles for shaders, automatically disable them if too slow, and initially disable them on mobile -* Replace jasmine with mocha+chai Network ------- diff --git a/graphics/exported/action/damageprotector.png b/graphics/exported/action/damageprotector.png new file mode 100644 index 0000000..d65f79b Binary files /dev/null and b/graphics/exported/action/damageprotector.png differ diff --git a/graphics/exported/equipment/forcefield.png b/graphics/exported/action/forcefield.png similarity index 100% rename from graphics/exported/equipment/forcefield.png rename to graphics/exported/action/forcefield.png diff --git a/graphics/exported/equipment/fractalhull.png b/graphics/exported/action/fractalhull.png similarity index 100% rename from graphics/exported/equipment/fractalhull.png rename to graphics/exported/action/fractalhull.png diff --git a/graphics/exported/equipment/gatlinggun.png b/graphics/exported/action/gatlinggun.png similarity index 100% rename from graphics/exported/equipment/gatlinggun.png rename to graphics/exported/action/gatlinggun.png diff --git a/graphics/exported/equipment/gravitshield.png b/graphics/exported/action/gravitshield.png similarity index 100% rename from graphics/exported/equipment/gravitshield.png rename to graphics/exported/action/gravitshield.png diff --git a/graphics/exported/equipment/hardcoatedhull.png b/graphics/exported/action/hardcoatedhull.png similarity index 100% rename from graphics/exported/equipment/hardcoatedhull.png rename to graphics/exported/action/hardcoatedhull.png diff --git a/graphics/exported/equipment/invertershield.png b/graphics/exported/action/invertershield.png similarity index 100% rename from graphics/exported/equipment/invertershield.png rename to graphics/exported/action/invertershield.png diff --git a/graphics/exported/equipment/ionthruster.png b/graphics/exported/action/ionthruster.png similarity index 100% rename from graphics/exported/equipment/ionthruster.png rename to graphics/exported/action/ionthruster.png diff --git a/graphics/exported/equipment/ironhull.png b/graphics/exported/action/ironhull.png similarity index 100% rename from graphics/exported/equipment/ironhull.png rename to graphics/exported/action/ironhull.png diff --git a/graphics/exported/equipment/kelvingenerator.png b/graphics/exported/action/kelvingenerator.png similarity index 100% rename from graphics/exported/equipment/kelvingenerator.png rename to graphics/exported/action/kelvingenerator.png diff --git a/graphics/exported/equipment/nuclearreactor.png b/graphics/exported/action/nuclearreactor.png similarity index 100% rename from graphics/exported/equipment/nuclearreactor.png rename to graphics/exported/action/nuclearreactor.png diff --git a/graphics/exported/equipment/powerdepleter.png b/graphics/exported/action/powerdepleter.png similarity index 100% rename from graphics/exported/equipment/powerdepleter.png rename to graphics/exported/action/powerdepleter.png diff --git a/graphics/exported/action/precisionboost.png b/graphics/exported/action/precisionboost.png new file mode 100644 index 0000000..db2a508 Binary files /dev/null and b/graphics/exported/action/precisionboost.png differ diff --git a/graphics/exported/equipment/prokhorovlaser.png b/graphics/exported/action/prokhorovlaser.png similarity index 100% rename from graphics/exported/equipment/prokhorovlaser.png rename to graphics/exported/action/prokhorovlaser.png diff --git a/graphics/exported/equipment/repairdrone.png b/graphics/exported/action/repairdrone.png similarity index 100% rename from graphics/exported/equipment/repairdrone.png rename to graphics/exported/action/repairdrone.png diff --git a/graphics/exported/equipment/rocketengine.png b/graphics/exported/action/rocketengine.png similarity index 100% rename from graphics/exported/equipment/rocketengine.png rename to graphics/exported/action/rocketengine.png diff --git a/graphics/exported/equipment/shieldtransfer.png b/graphics/exported/action/shieldtransfer.png similarity index 100% rename from graphics/exported/equipment/shieldtransfer.png rename to graphics/exported/action/shieldtransfer.png diff --git a/graphics/exported/equipment/submunitionmissile.png b/graphics/exported/action/submunitionmissile.png similarity index 100% rename from graphics/exported/equipment/submunitionmissile.png rename to graphics/exported/action/submunitionmissile.png diff --git a/graphics/exported/equipment/voidhawkengine.png b/graphics/exported/action/voidhawkengine.png similarity index 100% rename from graphics/exported/equipment/voidhawkengine.png rename to graphics/exported/action/voidhawkengine.png diff --git a/graphics/exported/character/slot-hull.png b/graphics/exported/attribute/hull_capacity.png similarity index 100% rename from graphics/exported/character/slot-hull.png rename to graphics/exported/attribute/hull_capacity.png diff --git a/graphics/exported/character/attribute-maneuvrability.png b/graphics/exported/attribute/maneuvrability.png similarity index 100% rename from graphics/exported/character/attribute-maneuvrability.png rename to graphics/exported/attribute/maneuvrability.png diff --git a/graphics/exported/character/slot-power.png b/graphics/exported/attribute/power_capacity.png similarity index 100% rename from graphics/exported/character/slot-power.png rename to graphics/exported/attribute/power_capacity.png diff --git a/graphics/exported/character/slot-weapon.png b/graphics/exported/attribute/precision.png similarity index 100% rename from graphics/exported/character/slot-weapon.png rename to graphics/exported/attribute/precision.png diff --git a/graphics/exported/character/slot-shield.png b/graphics/exported/attribute/shield_capacity.png similarity index 100% rename from graphics/exported/character/slot-shield.png rename to graphics/exported/attribute/shield_capacity.png diff --git a/graphics/exported/battle/tooltip/ship-portrait.png b/graphics/exported/battle/tooltip/ship-portrait.png index 38b2a81..a5a784a 100644 Binary files a/graphics/exported/battle/tooltip/ship-portrait.png and b/graphics/exported/battle/tooltip/ship-portrait.png differ diff --git a/graphics/exported/character/attribute-precision.png b/graphics/exported/character/attribute-precision.png deleted file mode 100644 index 39732c5..0000000 Binary files a/graphics/exported/character/attribute-precision.png and /dev/null differ diff --git a/graphics/exported/character/attribute.png b/graphics/exported/character/attribute.png deleted file mode 100644 index f0f042f..0000000 Binary files a/graphics/exported/character/attribute.png and /dev/null differ diff --git a/graphics/exported/character/cargo-slot.png b/graphics/exported/character/cargo-slot.png deleted file mode 100644 index 122c231..0000000 Binary files a/graphics/exported/character/cargo-slot.png and /dev/null differ diff --git a/graphics/exported/character/close-button-hover.png b/graphics/exported/character/close-button-hover.png new file mode 100644 index 0000000..9928590 Binary files /dev/null and b/graphics/exported/character/close-button-hover.png differ diff --git a/graphics/exported/character/close-button.png b/graphics/exported/character/close-button.png new file mode 100644 index 0000000..63793f4 Binary files /dev/null and b/graphics/exported/character/close-button.png differ diff --git a/graphics/exported/character/close.png b/graphics/exported/character/close.png deleted file mode 100644 index 09ce13c..0000000 Binary files a/graphics/exported/character/close.png and /dev/null differ diff --git a/graphics/exported/character/entry.png b/graphics/exported/character/entry.png new file mode 100644 index 0000000..3679966 Binary files /dev/null and b/graphics/exported/character/entry.png differ diff --git a/graphics/exported/character/equipment-slot.png b/graphics/exported/character/equipment-slot.png deleted file mode 100644 index 2ae86c9..0000000 Binary files a/graphics/exported/character/equipment-slot.png and /dev/null differ diff --git a/graphics/exported/character/experience.png b/graphics/exported/character/experience.png deleted file mode 100644 index e1deb79..0000000 Binary files a/graphics/exported/character/experience.png and /dev/null differ diff --git a/graphics/exported/character/initial.png b/graphics/exported/character/initial.png new file mode 100644 index 0000000..48354ba Binary files /dev/null and b/graphics/exported/character/initial.png differ diff --git a/graphics/exported/character/level-display.png b/graphics/exported/character/level-display.png new file mode 100644 index 0000000..aa294cc Binary files /dev/null and b/graphics/exported/character/level-display.png differ diff --git a/graphics/exported/character/level-experience.png b/graphics/exported/character/level-experience.png new file mode 100644 index 0000000..a294a77 Binary files /dev/null and b/graphics/exported/character/level-experience.png differ diff --git a/graphics/exported/character/level-separator.png b/graphics/exported/character/level-separator.png new file mode 100644 index 0000000..f030a29 Binary files /dev/null and b/graphics/exported/character/level-separator.png differ diff --git a/graphics/exported/character/level-upgrades.png b/graphics/exported/character/level-upgrades.png new file mode 100644 index 0000000..1078520 Binary files /dev/null and b/graphics/exported/character/level-upgrades.png differ diff --git a/graphics/exported/character/name-button-hover.png b/graphics/exported/character/name-button-hover.png new file mode 100644 index 0000000..ed8af5c Binary files /dev/null and b/graphics/exported/character/name-button-hover.png differ diff --git a/graphics/exported/character/name-button.png b/graphics/exported/character/name-button.png new file mode 100644 index 0000000..61f7b1b Binary files /dev/null and b/graphics/exported/character/name-button.png differ diff --git a/graphics/exported/character/name-display.png b/graphics/exported/character/name-display.png new file mode 100644 index 0000000..e609e62 Binary files /dev/null and b/graphics/exported/character/name-display.png differ diff --git a/graphics/exported/character/portrait-hover.png b/graphics/exported/character/portrait-hover.png new file mode 100644 index 0000000..f2edc46 Binary files /dev/null and b/graphics/exported/character/portrait-hover.png differ diff --git a/graphics/exported/character/portrait-on.png b/graphics/exported/character/portrait-on.png new file mode 100644 index 0000000..fd00cb1 Binary files /dev/null and b/graphics/exported/character/portrait-on.png differ diff --git a/graphics/exported/character/portrait.png b/graphics/exported/character/portrait.png new file mode 100644 index 0000000..db705c9 Binary files /dev/null and b/graphics/exported/character/portrait.png differ diff --git a/graphics/exported/character/price-tag.png b/graphics/exported/character/price-tag.png deleted file mode 100644 index 3f2a705..0000000 Binary files a/graphics/exported/character/price-tag.png and /dev/null differ diff --git a/graphics/exported/character/random-off.png b/graphics/exported/character/random-off.png deleted file mode 100644 index 2cf3713..0000000 Binary files a/graphics/exported/character/random-off.png and /dev/null differ diff --git a/graphics/exported/character/random-on.png b/graphics/exported/character/random-on.png deleted file mode 100644 index 643a80c..0000000 Binary files a/graphics/exported/character/random-on.png and /dev/null differ diff --git a/graphics/exported/character/rename-off.png b/graphics/exported/character/rename-off.png deleted file mode 100644 index 25ab2ca..0000000 Binary files a/graphics/exported/character/rename-off.png and /dev/null differ diff --git a/graphics/exported/character/rename-on.png b/graphics/exported/character/rename-on.png deleted file mode 100644 index 5df07e8..0000000 Binary files a/graphics/exported/character/rename-on.png and /dev/null differ diff --git a/graphics/exported/character/ship-column.png b/graphics/exported/character/ship-column.png new file mode 100644 index 0000000..18426ef Binary files /dev/null and b/graphics/exported/character/ship-column.png differ diff --git a/graphics/exported/character/ship-description.png b/graphics/exported/character/ship-description.png new file mode 100644 index 0000000..a82cb88 Binary files /dev/null and b/graphics/exported/character/ship-description.png differ diff --git a/graphics/exported/character/ship-model.png b/graphics/exported/character/ship-model.png new file mode 100644 index 0000000..87250ec Binary files /dev/null and b/graphics/exported/character/ship-model.png differ diff --git a/graphics/exported/character/ship-selected.png b/graphics/exported/character/ship-selected.png deleted file mode 100644 index 0344dcf..0000000 Binary files a/graphics/exported/character/ship-selected.png and /dev/null differ diff --git a/graphics/exported/character/ship.png b/graphics/exported/character/ship.png deleted file mode 100644 index 3be54f7..0000000 Binary files a/graphics/exported/character/ship.png and /dev/null differ diff --git a/graphics/exported/character/skill-upgrade.png b/graphics/exported/character/skill-upgrade.png deleted file mode 100644 index f8af540..0000000 Binary files a/graphics/exported/character/skill-upgrade.png and /dev/null differ diff --git a/graphics/exported/character/slot-engine.png b/graphics/exported/character/slot-engine.png deleted file mode 100644 index 8c07f2e..0000000 Binary files a/graphics/exported/character/slot-engine.png and /dev/null differ diff --git a/graphics/exported/character/upgrade-available.png b/graphics/exported/character/upgrade-available.png deleted file mode 100644 index b503034..0000000 Binary files a/graphics/exported/character/upgrade-available.png and /dev/null differ diff --git a/graphics/exported/character/upgrade-hover.png b/graphics/exported/character/upgrade-hover.png new file mode 100644 index 0000000..a404bd7 Binary files /dev/null and b/graphics/exported/character/upgrade-hover.png differ diff --git a/graphics/exported/character/upgrade-locked.png b/graphics/exported/character/upgrade-locked.png new file mode 100644 index 0000000..2ea8b00 Binary files /dev/null and b/graphics/exported/character/upgrade-locked.png differ diff --git a/graphics/exported/character/upgrade-on.png b/graphics/exported/character/upgrade-on.png new file mode 100644 index 0000000..e8c3625 Binary files /dev/null and b/graphics/exported/character/upgrade-on.png differ diff --git a/graphics/exported/character/upgrade-point.png b/graphics/exported/character/upgrade-point.png new file mode 100644 index 0000000..4e835fc Binary files /dev/null and b/graphics/exported/character/upgrade-point.png differ diff --git a/graphics/exported/character/upgrade.png b/graphics/exported/character/upgrade.png new file mode 100644 index 0000000..2c462d5 Binary files /dev/null and b/graphics/exported/character/upgrade.png differ diff --git a/graphics/exported/character/validate.png b/graphics/exported/character/validate.png deleted file mode 100644 index 6bf0b1a..0000000 Binary files a/graphics/exported/character/validate.png and /dev/null differ diff --git a/graphics/exported/character/value-hull.png b/graphics/exported/character/value-hull.png deleted file mode 100644 index 9914911..0000000 Binary files a/graphics/exported/character/value-hull.png and /dev/null differ diff --git a/graphics/exported/character/value-power.png b/graphics/exported/character/value-power.png deleted file mode 100644 index 9f2b41c..0000000 Binary files a/graphics/exported/character/value-power.png and /dev/null differ diff --git a/graphics/exported/equipment/damageprotector.png b/graphics/exported/equipment/damageprotector.png deleted file mode 100644 index b5af24a..0000000 Binary files a/graphics/exported/equipment/damageprotector.png and /dev/null differ diff --git a/graphics/exported/map/options-hover.png b/graphics/exported/map/options-hover.png new file mode 100644 index 0000000..2f58e29 Binary files /dev/null and b/graphics/exported/map/options-hover.png differ diff --git a/graphics/exported/map/options.png b/graphics/exported/map/options.png new file mode 100644 index 0000000..6dd77aa Binary files /dev/null and b/graphics/exported/map/options.png differ diff --git a/graphics/exported/map/zoom-in-hover.png b/graphics/exported/map/zoom-in-hover.png new file mode 100644 index 0000000..cf414b9 Binary files /dev/null and b/graphics/exported/map/zoom-in-hover.png differ diff --git a/graphics/exported/map/zoom-in.png b/graphics/exported/map/zoom-in.png new file mode 100644 index 0000000..ee29ce3 Binary files /dev/null and b/graphics/exported/map/zoom-in.png differ diff --git a/graphics/exported/map/zoom-out-hover.png b/graphics/exported/map/zoom-out-hover.png new file mode 100644 index 0000000..2d45a82 Binary files /dev/null and b/graphics/exported/map/zoom-out-hover.png differ diff --git a/graphics/exported/map/zoom-out.png b/graphics/exported/map/zoom-out.png new file mode 100644 index 0000000..2d25281 Binary files /dev/null and b/graphics/exported/map/zoom-out.png differ diff --git a/graphics/ui/actions.svg b/graphics/ui/actions.svg index 92c568f..92b6bef 100644 --- a/graphics/ui/actions.svg +++ b/graphics/ui/actions.svg @@ -16,7 +16,7 @@ version="1.1" inkscape:version="0.92.1 r15371" sodipodi:docname="actions.svg" - inkscape:export-filename="/home/michael/workspace/perso/spacetac/graphics/exported/equipment/kelvingenerator.png" + inkscape:export-filename="/home/michael/workspace/perso/spacetac/graphics/exported/action/damageprotector.png" inkscape:export-xdpi="90" inkscape:export-ydpi="90" viewBox="0 0 256 256" @@ -3114,7 +3114,7 @@ inkscape:groupmode="layer" id="layer11" inkscape:label="DamageProtector" - style="display:none"> + style="display:inline"> + style="display:none"> diff --git a/graphics/ui/battle.svg b/graphics/ui/battle.svg index d797fdb..9d1d191 100644 --- a/graphics/ui/battle.svg +++ b/graphics/ui/battle.svg @@ -16,7 +16,7 @@ viewBox="0 0 1920 1080" id="svg2" version="1.1" - inkscape:version="0.92.2 (unknown)" + inkscape:version="0.92.1 r15371" sodipodi:docname="battle.svg" inkscape:export-filename="/home/michael/workspace/perso/spacetac/graphics/exported/battle/actionbar/power-generated.png" inkscape:export-xdpi="96" @@ -3200,6 +3200,42 @@ y1="123.97214" x2="111.73327" y2="123.97214" /> + + + + + + + + style="display:inline;opacity:1;fill:#43535c;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3.5999999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;enable-background:new;filter:url(#filter6088)" /> + inkscape:export-ydpi="96" + enable-background="new"> - + id="defs6038"> + id="linearGradient4877" + inkscape:collect="always"> + style="stop-color:#000000;stop-opacity:0" /> + + + + style="stop-color:#000000;stop-opacity:0" /> - + id="linearGradient14964" + inkscape:collect="always"> + style="stop-color:#263342;stop-opacity:1;" /> + style="stop-color:#737a81;stop-opacity:0.90980393" + offset="0.08748852" + id="stop14958" /> + - - - - - - - - - - - - + style="stop-color:#263342;stop-opacity:0;" /> + id="linearGradient14922"> + id="stop14918" /> + id="stop14926" + offset="0.08748852" + style="stop-color:#39434d;stop-opacity:0.90980393" /> + style="stop-color:#263342;stop-opacity:0.85098039;" + offset="0.17361853" + id="stop14928" /> - + id="stop14920" /> + id="linearGradient14908"> + id="stop14904" /> + id="stop14914" + offset="0.09682183" + style="stop-color:#7f8994;stop-opacity:1" /> + id="stop14912" + offset="0.24839224" + style="stop-color:#d5c5ae;stop-opacity:1" /> + + id="stop14906" /> + id="linearGradient14594"> + id="stop14590" /> - + id="stop14592" /> + id="linearGradient14194"> + id="stop14190" /> - - - - - - + id="stop14192" /> + id="linearGradient13911"> + id="stop13907" /> + id="stop13917" + offset="0.06290146" + style="stop-color:#ced3d9;stop-opacity:1;" /> + + id="stop13909" /> + id="linearGradient13887"> + id="stop13883" /> + + + + + id="stop13885" /> + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + id="filter12710" + x="-0.0038717121" + width="1.0077434" + y="-0.013324509" + height="1.026649"> + stdDeviation="0.39191675" + id="feGaussianBlur12712" /> + id="filter12807" + x="-0.2388" + width="1.4776" + y="-0.2388" + height="1.4776"> - - - - - - - - - - - - - - - - - - - - - - - - - + stdDeviation="45.732132" + id="feGaussianBlur12809" /> - - - - - - - - - - - - - - - - - - - - - - - - - + gradientTransform="matrix(2.0864198,0,0,1,-824.28045,0)" + x1="750.42859" + y1="231.19315" + x2="993.42859" + y2="231.19315" /> + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + id="filter7193-0"> + id="feComposite7185-37" /> + id="feGaussianBlur7187-59" /> + id="feComposite7191-28" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + inkscape:object-nodes="true" + inkscape:object-paths="false" + inkscape:snap-bbox="true" + inkscape:bbox-nodes="false" + showguides="true" + inkscape:snap-bbox-edge-midpoints="false" + inkscape:bbox-paths="false" + inkscape:snap-bbox-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-page="true" + borderlayer="true" + inkscape:measure-start="118.5,884" + inkscape:measure-end="120.5,869.5" + inkscape:lockguides="false"> + + id="metadata6041"> @@ -828,1256 +930,1622 @@ - - - - - - - - - | ATTRIBUTES | - | SKILLS | - - - - - - - - - - - - - - - - + id="g15007"> + y="11.249983" + x="0" + height="1080" + width="1920" + id="rect4710" + style="fill:#0d121b;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> - - + transform="translate(-3.7618647,-11.249983)" + id="g4708" + style="display:inline"> + id="g4649"> - Z + style="opacity:0.61100003;filter:url(#filter12807)" + transform="translate(48.438931,343.82216)" + id="g12757"> + - + id="g12748" + transform="translate(3.5355276,-9.1923828)"> + + - + style="fill:#ced3d9;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 308.76187,152.4881 h 260 v 260 h -260 z" + id="rect13446" + inkscape:connector-curvature="0" /> - - - - - - - - - - - - - - - - - - - - - - - Cargo - - - - - - - - - - Artana's Fury - - - - - - Lootable items + id="g14100" + transform="translate(0,30)"> + + + + + + + + + + Avenger + + + + + + + + - Sell for 150 + id="text7309-3-9" + y="967.43896" + x="436.77335" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:24px;line-height:26.59000158%;font-family:DAGGERSQUARE;-inkscape-font-specification:DAGGERSQUARE;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#e7ebf0;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter13789)" + xml:space="preserve">Artana + + + + + + + + + + + + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + transform="translate(0.33518013,-11.014591)" + id="g7557"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + id="rect12398-2" + d="m 952.33519,43.249922 h 846.00001 l 49,115.999998 H 933.33519 Z" + style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#e2e9d1;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + + + - 150 + id="g13268" + transform="translate(65.406609,-59.943224)" + inkscape:export-filename="/home/michael/workspace/perso/spacetac/graphics/exported/character/level-separator.png" + inkscape:export-xdpi="96" + inkscape:export-ydpi="96"> + + + x="0" /> + + + + + + + - - - - - + style="display:inline;fill:#fafbfd;fill-opacity:0.1255319" + id="use7546-1" + transform="translate(423.739,637.5569)" + inkscape:export-filename="/home/michael/workspace/perso/spacetac/graphics/exported/character/upgrade-on.png" + inkscape:export-xdpi="96" + inkscape:export-ydpi="96"> - - - - - - - - - - - - Level + + - Available points + id="g13533" + transform="translate(0.33514404,-1.7e-5)" + inkscape:export-filename="/home/michael/workspace/perso/spacetac/graphics/exported/character/upgrade-locked.png" + inkscape:export-xdpi="96" + inkscape:export-ydpi="96"> + + + + + + + id="g14081"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 12 + + Upgrade points + + + + ... + + Hardened Hull + + + + SecondaryGatling + + + + + + + + + + + + + + + + Gatling Gun + + + + 12 + + + + + + + Base equipment + + + + + + + - + + + + + + + + + + Level 9 + + - - - - + Attributes + Actions - - - - - 35 - 0 - 2 - - Initiative - Hull capacity - Shield capacity - Power capacity - Initial power - Power recovery - Materials - Electronics - Energy - Human - Gravity - Time - - - - - - - - - 25 810 - - - - - X - - - - - - - - - - - - - - ? - - - - - ... - - - - + id="path9627" + d="m 1920,11.249983 h -80 l 80,189.999997 V 11.249983" + style="display:inline;opacity:1;vector-effect:none;fill:url(#linearGradient14924);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3.00099993;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:10.69999981;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> OK + id="tspan7860" + x="496.83798" + y="22.792192" + style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';fill:#d5c5ae;fill-opacity:1;stroke-width:0.26458335px">X + + + + X + + + + + ... + + + + + + + + diff --git a/graphics/ui/map.svg b/graphics/ui/map.svg index 3d45445..fa53cc2 100644 --- a/graphics/ui/map.svg +++ b/graphics/ui/map.svg @@ -804,7 +804,7 @@ height="1.0909113"> - - - + transform="rotate(-90,315.37748,110.48112)"> + transform="translate(82.550003,-4.2333336)"> @@ -2192,31 +2179,12 @@ - - - - - + inkscape:label="Character sheet" /> + style="display:none"> diff --git a/graphics/ui/pics/action.png b/graphics/ui/pics/action.png new file mode 100644 index 0000000..4fe14e6 Binary files /dev/null and b/graphics/ui/pics/action.png differ diff --git a/graphics/exported/character/value-shield.png b/graphics/ui/pics/attribute.png similarity index 100% rename from graphics/exported/character/value-shield.png rename to graphics/ui/pics/attribute.png diff --git a/graphics/ui/pics/portrait.png b/graphics/ui/pics/portrait.png new file mode 100644 index 0000000..8b99142 Binary files /dev/null and b/graphics/ui/pics/portrait.png differ diff --git a/graphics/ui/pics/sprite.png b/graphics/ui/pics/sprite.png new file mode 100644 index 0000000..8e6599a Binary files /dev/null and b/graphics/ui/pics/sprite.png differ diff --git a/out/assets/images/character/sheet.png b/out/assets/images/character/sheet.png index 2e629c8..0ee053b 100644 Binary files a/out/assets/images/character/sheet.png and b/out/assets/images/character/sheet.png differ diff --git a/out/assets/images/map/buttons.png b/out/assets/images/map/buttons.png deleted file mode 100644 index 5a2a9a7..0000000 Binary files a/out/assets/images/map/buttons.png and /dev/null differ diff --git a/out/loot.html b/out/loot.html deleted file mode 100644 index 98590de..0000000 --- a/out/loot.html +++ /dev/null @@ -1,129 +0,0 @@ - - - - - - SpaceTac - Loot Generator Samples - - - - - - - - -
-

SpaceTac - Loot Generator Samples

- - - -
-
- - - - - \ No newline at end of file diff --git a/src/common b/src/common index 38c0670..2882c79 160000 --- a/src/common +++ b/src/common @@ -1 +1 @@ -Subproject commit 38c06700cfb3d03a1f7309a6200ab0ca7c0ee9d8 +Subproject commit 2882c791e8af845517fd030c6e789296d347213d diff --git a/src/core/Battle.spec.ts b/src/core/Battle.spec.ts index 4d06895..4ee12f4 100644 --- a/src/core/Battle.spec.ts +++ b/src/core/Battle.spec.ts @@ -68,7 +68,7 @@ module TK.SpaceTac { var ship3 = new Ship(fleet2, "ship3"); var battle = new Battle(fleet1, fleet2); - battle.ships.list().forEach(ship => TestTools.setShipHP(ship, 10, 0)); + battle.ships.list().forEach(ship => TestTools.setShipModel(ship, 10, 0)); // Check empty play_order case check.equals(battle.playing_ship, null); @@ -118,7 +118,7 @@ module TK.SpaceTac { check.equals(battle.ships.list().filter(ship => ship.alive), [ship1, ship2, ship3, ship4], "alive ships"); }); - let result = battle.applyOneAction(nn(weapon.action).id, Target.newFromLocation(0, 0)); + let result = battle.applyOneAction(weapon.id, Target.newFromLocation(0, 0)); check.equals(result, true, "action applied successfully"); check.in("after weapon", check => { check.same(battle.playing_ship, ship3, "playing ship"); @@ -135,7 +135,7 @@ module TK.SpaceTac { let ship3 = new Ship(fleet2, "F2S1"); var battle = new Battle(fleet1, fleet2); - battle.ships.list().forEach(ship => TestTools.setShipHP(ship, 10, 0)); + battle.ships.list().forEach(ship => TestTools.setShipModel(ship, 10, 0)); battle.start(); battle.play_order = [ship3, ship2, ship1]; check.equals(battle.ended, false); @@ -154,41 +154,6 @@ module TK.SpaceTac { } }); - test.case("wear down equipment at the end of battle", check => { - let fleet1 = new Fleet(); - let ship1a = fleet1.addShip(); - let equ1a = TestTools.addWeapon(ship1a); - let ship1b = fleet1.addShip(); - let equ1b = TestTools.addWeapon(ship1b); - let fleet2 = new Fleet(); - let ship2a = fleet2.addShip(); - let equ2a = TestTools.addWeapon(ship2a); - let eng2a = TestTools.addEngine(ship2a, 50); - - let battle = new Battle(fleet1, fleet2); - battle.ships.list().forEach(ship => TestTools.setShipHP(ship, 10, 0)); - battle.start(); - - check.equals(equ1a.wear, 0); - check.equals(equ1b.wear, 0); - check.equals(equ2a.wear, 0); - check.equals(eng2a.wear, 0); - - range(8).forEach(() => battle.advanceToNextShip()); - - check.equals(equ1a.wear, 0); - check.equals(equ1b.wear, 0); - check.equals(equ2a.wear, 0); - check.equals(eng2a.wear, 0); - - battle.endBattle(null); - - check.equals(equ1a.wear, 3); - check.equals(equ1b.wear, 3); - check.equals(equ2a.wear, 3); - check.equals(eng2a.wear, 3); - }); - test.case("handles a draw in end battle", check => { var fleet1 = new Fleet(); var fleet2 = new Fleet(); @@ -316,7 +281,7 @@ module TK.SpaceTac { let battle = new Battle(); let ship = battle.fleets[0].addShip(); - check.equals(imaterialize(battle.iAreaEffects(100, 50)), []); + check.equals(imaterialize(battle.iAreaEffects(100, 50)), [], "initial"); let drone1 = new Drone(ship); drone1.x = 120; @@ -331,22 +296,22 @@ module TK.SpaceTac { drone2.effects = [new DamageEffect(14)]; battle.addDrone(drone2); - check.equals(imaterialize(battle.iAreaEffects(100, 50)), [drone1.effects[0]]); + check.equals(imaterialize(battle.iAreaEffects(100, 50)), [drone1.effects[0]], "drone effects"); - let eq1 = ship.addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon)); - eq1.action = new ToggleAction(eq1, 0, 500, [new AttributeEffect("maneuvrability", 1)]); - (eq1.action).activated = true; - let eq2 = ship.addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon)); - eq2.action = new ToggleAction(eq2, 0, 500, [new AttributeEffect("maneuvrability", 2)]); - (eq2.action).activated = false; - let eq3 = ship.addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon)); - eq3.action = new ToggleAction(eq3, 0, 100, [new AttributeEffect("maneuvrability", 3)]); - (eq3.action).activated = true; + let eq1 = new ToggleAction("eq1", { power: 0, radius: 500, effects: [new AttributeEffect("maneuvrability", 1)] }); + ship.actions.addCustom(eq1); + ship.actions.toggle(eq1, true); + let eq2 = new ToggleAction("eq2", { power: 0, radius: 500, effects: [new AttributeEffect("maneuvrability", 2)] }); + ship.actions.addCustom(eq2); + ship.actions.toggle(eq2, false); + let eq3 = new ToggleAction("eq3", { power: 0, radius: 100, effects: [new AttributeEffect("maneuvrability", 3)] }); + ship.actions.addCustom(eq3); + ship.actions.toggle(eq3, true); check.equals(imaterialize(battle.iAreaEffects(100, 50)), [ drone1.effects[0], - (eq1.action).effects[0], - ]); + eq1.effects[0], + ], "drone and toggle effects"); }); test.case("is serializable", check => { diff --git a/src/core/Battle.ts b/src/core/Battle.ts index fcc1ad7..fe6886c 100644 --- a/src/core/Battle.ts +++ b/src/core/Battle.ts @@ -274,9 +274,8 @@ module TK.SpaceTac { this.outcome = null; this.cycle = 1; this.placeShips(); - this.stats.addFleetsValue(this.fleets[0], this.fleets[1]); - this.throwInitiative(); iforeach(this.iships(), ship => ship.restoreInitialState()); + this.throwInitiative(); this.setPlayingShip(this.play_order[0]); } @@ -354,7 +353,7 @@ module TK.SpaceTac { applyOneAction(action_id: RObjectId, target?: Target): boolean { let ship = this.playing_ship; if (ship) { - let action = ship.getAction(action_id); + let action = ship.actions.getById(action_id); if (action) { if (!target) { target = action.getDefaultTarget(ship); diff --git a/src/core/BattleCheats.spec.ts b/src/core/BattleCheats.spec.ts index 8e99910..1b40c32 100644 --- a/src/core/BattleCheats.spec.ts +++ b/src/core/BattleCheats.spec.ts @@ -21,21 +21,5 @@ module TK.SpaceTac.Specs { check.same(nn(battle.outcome).winner, battle.fleets[1], "winner"); check.equals(any(battle.fleets[0].ships, ship => ship.alive), false, "all allies dead"); }) - - test.case("adds an equipment", check => { - let battle = new Battle(); - let ship = new Ship(); - TestTools.setShipPlaying(battle, ship); - ship.upgradeSkill("skill_materials"); - let cheats = new BattleCheats(battle, battle.fleets[0].player); - - check.equals(ship.listEquipment(), []); - - cheats.equip("Iron Hull"); - - let result = ship.listEquipment(); - check.equals(result.length, 1); - check.containing(result[0], { name: "Iron Hull", level: 1 }); - }) }) } diff --git a/src/core/BattleCheats.ts b/src/core/BattleCheats.ts index dd50c7c..11a179a 100644 --- a/src/core/BattleCheats.ts +++ b/src/core/BattleCheats.ts @@ -36,23 +36,5 @@ module TK.SpaceTac { }); this.battle.endBattle(first(this.battle.fleets, fleet => !this.player.is(fleet.player))); } - - /** - * Add an equipment to current playing ship - */ - equip(name: string): void { - let ship = this.battle.playing_ship; - if (ship) { - let generator = new LootGenerator(); - generator.setTemplateFilter(template => template.name == name); - - let equipment = generator.generateHighest(ship.skills); - if (equipment) { - let slot_type = nn(equipment.slot_type); - let slot = ship.getFreeSlot(slot_type) || ship.addSlot(slot_type); - slot.attach(equipment); - } - } - } } } diff --git a/src/core/BattleOutcome.spec.ts b/src/core/BattleOutcome.spec.ts index 06d7c24..0a2939b 100644 --- a/src/core/BattleOutcome.spec.ts +++ b/src/core/BattleOutcome.spec.ts @@ -1,61 +1,5 @@ module TK.SpaceTac.Specs { testing("BattleOutcome", test => { - test.case("generates loot from defeated ships", check => { - var fleet1 = new Fleet(); - fleet1.addShip(new Ship()); - var fleet2 = new Fleet(); - fleet2.addShip(new Ship()); - fleet2.addShip(new Ship()); - fleet2.addShip(new Ship()); - fleet2.addShip(new Ship()); - - fleet2.ships[2].level.forceLevel(5); - fleet2.ships[3].level.forceLevel(5); - - fleet2.ships[0].addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon, "0a")); - fleet2.ships[0].addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon, "0b")); - fleet2.ships[1].addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon, "1a")); - fleet2.ships[2].addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon, "2b")); - fleet2.ships[3].addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon, "3b")); - - var battle = new Battle(fleet1, fleet2); - var outcome = new BattleOutcome(fleet1); - - var random = new SkewedRandomGenerator([ - 0.6, // standard loot on first ship - 0, // - take first equipment - 0, // leave second ship alone - 0.95, // lucky loot on third ship - 0, // - lower end of level range (ship has 5, so range is 4-6) - 0.5, // - common quality - 0, // - take first generated equipment (there is only one anyway) - 0.96, // lucky loot on fourth ship - 0.999, // - higher end of level range - 0.98 // - premium quality - ]); - - // Force lucky finds with one template - var looter = new LootGenerator(random, false); - var template = new LootTemplate(SlotType.Power, "Nuclear Reactor"); - template.setSkillsRequirements({ "skill_photons": istep(4) }); - template.addAttributeEffect("power_capacity", istep(1)); - looter.templates = [template]; - check.patch(outcome, "getLootGenerator", () => looter); - - outcome.createLoot(battle, random); - - check.equals(outcome.loot.length, 3); - check.equals(outcome.loot[0].name, "0a"); - check.equals(outcome.loot[1].name, "Nuclear Reactor"); - check.equals(outcome.loot[1].level, 4); - check.same(outcome.loot[1].quality, EquipmentQuality.COMMON); - check.equals(outcome.loot[1].requirements, { "skill_photons": 7 }); - check.equals(outcome.loot[2].name, "Nuclear Reactor"); - check.equals(outcome.loot[2].level, 6); - check.same(outcome.loot[2].quality, EquipmentQuality.PREMIUM); - check.equals(outcome.loot[2].requirements, { "skill_photons": 9 }); - }); - test.case("grants experience", check => { let fleet1 = new Fleet(); let ship1a = fleet1.addShip(new Ship()); diff --git a/src/core/BattleOutcome.ts b/src/core/BattleOutcome.ts index 7fbbcb2..b303888 100644 --- a/src/core/BattleOutcome.ts +++ b/src/core/BattleOutcome.ts @@ -6,47 +6,14 @@ module TK.SpaceTac { */ export class BattleOutcome { // Indicates if the battle is a draw (no winner) - draw: boolean; + draw: boolean // Victorious fleet - winner: Fleet | null; - - // Retrievable loot - loot: Equipment[]; + winner: Fleet | null constructor(winner: Fleet | null) { this.winner = winner; this.draw = winner ? false : true; - this.loot = []; - } - - /** - * Fill loot from defeated fleet - */ - createLoot(battle: Battle, random = RandomGenerator.global): void { - this.loot = []; - - battle.fleets.forEach(fleet => { - if (this.winner && !this.winner.player.is(fleet.player)) { - fleet.ships.forEach(ship => { - var luck = random.random(); - if (luck > 0.9) { - // Salvage a supposedly transported item - var transported = this.generateLootItem(random, ship.level.get()); - if (transported) { - this.loot.push(transported); - } - } else if (luck > 0.5) { - // Salvage one equipped item - var token = ship.getRandomEquipment(random); - if (token) { - token.detach(); - this.loot.push(token); - } - } - }); - } - }); } /** @@ -63,41 +30,5 @@ module TK.SpaceTac { }); }); } - - /** - * Create a loot generator for lucky finds - */ - getLootGenerator(random: RandomGenerator): LootGenerator { - return new LootGenerator(random); - } - - /** - * Generate a loot item for the winner fleet - * - * The equipment will be in the dead ship range - */ - generateLootItem(random: RandomGenerator, base_level: number): Equipment | null { - let generator = this.getLootGenerator(random); - let level = random.randInt(Math.max(base_level - 1, 1), base_level + 1); - let quality = random.random(); - return generator.generate(level, this.getQuality(quality)); - } - - /** - * Get the quality enum matching a 0-1 value - */ - getQuality(quality: number): EquipmentQuality { - if (quality < 0.1) { - return EquipmentQuality.WEAK; - } else if (quality > 0.99) { - return EquipmentQuality.LEGENDARY; - } else if (quality > 0.95) { - return EquipmentQuality.PREMIUM; - } else if (quality > 0.8) { - return EquipmentQuality.FINE; - } else { - return EquipmentQuality.COMMON; - } - } } } diff --git a/src/core/BattleStats.spec.ts b/src/core/BattleStats.spec.ts index 0aa6398..b0137f6 100644 --- a/src/core/BattleStats.spec.ts +++ b/src/core/BattleStats.spec.ts @@ -71,26 +71,5 @@ module TK.SpaceTac.Specs { stats.processLog(battle.log, battle.fleets[0], true); check.equals(stats.stats, { "Drones deployed": [1, 1] }); }) - - test.case("evaluates equipment depreciation", check => { - let stats = new BattleStats(); - let battle = new Battle(); - let attacker = battle.fleets[0].addShip(); - let defender = battle.fleets[1].addShip(); - - let equ1 = TestTools.addEngine(attacker, 50); - equ1.price = 1000; - let equ2 = TestTools.addEngine(defender, 50); - equ2.price = 1100; - - stats.addFleetsValue(attacker.fleet, defender.fleet); - check.equals(stats.stats, { "Equipment wear (zotys)": [1000, 1100] }); - - equ1.price = 500; - equ2.price = 800; - - stats.addFleetsValue(attacker.fleet, defender.fleet, false); - check.equals(stats.stats, { "Equipment wear (zotys)": [500, 300] }); - }) }) } diff --git a/src/core/BattleStats.ts b/src/core/BattleStats.ts index 6713dd3..d61c1a0 100644 --- a/src/core/BattleStats.ts +++ b/src/core/BattleStats.ts @@ -58,23 +58,5 @@ module TK.SpaceTac { } } } - - /** - * Get the raw value of a fleet - */ - private getFleetValue(fleet: Fleet): number { - return sum(fleet.ships.map(ship => { - return sum(ship.listEquipment().map(equipment => equipment.getPrice())); - })); - } - - /** - * Store the fleets' value, for equipment wear display - */ - addFleetsValue(attacker: Fleet, defender: Fleet, positive = true): void { - let sgn = positive ? 1 : -1; - this.addStat("Equipment wear (zotys)", sgn * this.getFleetValue(attacker), true); - this.addStat("Equipment wear (zotys)", sgn * this.getFleetValue(defender), false); - } } } diff --git a/src/core/Drone.spec.ts b/src/core/Drone.spec.ts index fae684f..28e6660 100644 --- a/src/core/Drone.spec.ts +++ b/src/core/Drone.spec.ts @@ -5,17 +5,17 @@ module TK.SpaceTac { test.case("applies area effects when deployed", check => { let battle = TestTools.createBattle(); let ship = nn(battle.playing_ship); - TestTools.setShipAP(ship, 10); - let weapon = TestTools.addWeapon(ship); - weapon.action = new DeployDroneAction(weapon, 2, 300, 30, [new AttributeEffect("precision", 15)]); + TestTools.setShipModel(ship, 100, 0, 10); + let weapon = new DeployDroneAction("testdrone", { power: 2 }, { deploy_distance: 300, drone_radius: 30, drone_effects: [new AttributeEffect("precision", 15)] }); + ship.actions.addCustom(weapon); let engine = TestTools.addEngine(ship, 1000); TestTools.actionChain(check, battle, [ - [ship, nn(weapon.action), Target.newFromLocation(150, 50)], // deploy out of effects radius - [ship, nn(engine.action), Target.newFromLocation(110, 50)], // move out of effects radius - [ship, nn(engine.action), Target.newFromLocation(130, 50)], // move in effects radius - [ship, nn(weapon.action), Target.newFromShip(ship)], // recall - [ship, nn(weapon.action), Target.newFromLocation(130, 70)], // deploy in effects radius + [ship, weapon, Target.newFromLocation(150, 50)], // deploy out of effects radius + [ship, engine, Target.newFromLocation(110, 50)], // move out of effects radius + [ship, engine, Target.newFromLocation(130, 50)], // move in effects radius + [ship, weapon, Target.newFromShip(ship)], // recall + [ship, weapon, Target.newFromLocation(130, 70)], // deploy in effects radius ], [ check => { check.equals(ship.active_effects.count(), 0, "active effects"); @@ -56,9 +56,9 @@ module TK.SpaceTac { drone.effects = [ new DamageEffect(5), - new AttributeEffect("skill_quantum", 1) + new AttributeEffect("precision", 1) ] - check.equals(drone.getDescription(), "While deployed:\n• do 5 damage\n• quantum skill +1"); + check.equals(drone.getDescription(), "While deployed:\n• do 5 damage\n• precision +1"); }); }); } diff --git a/src/core/Equipment.spec.ts b/src/core/Equipment.spec.ts deleted file mode 100644 index 87a918f..0000000 --- a/src/core/Equipment.spec.ts +++ /dev/null @@ -1,117 +0,0 @@ -module TK.SpaceTac.Specs { - testing("Equipment", test => { - test.case("generates a full name", check => { - let equipment = new Equipment(SlotType.Weapon, "rayofdeath"); - check.equals(equipment.getFullName(), "rayofdeath Mk1"); - - equipment.name = "Ray of Death"; - check.equals(equipment.getFullName(), "Ray of Death Mk1"); - - equipment.quality = EquipmentQuality.LEGENDARY; - check.equals(equipment.getFullName(), "Legendary Ray of Death Mk1"); - }); - - test.case("checks capabilities requirements", check => { - var equipment = new Equipment(); - var ship = new Ship(); - - check.equals(equipment.canBeEquipped(ship.attributes), true); - - equipment.requirements["skill_time"] = 2; - - check.equals(equipment.canBeEquipped(ship.attributes), false); - - TestTools.setAttribute(ship, "skill_time", 1); - - check.equals(equipment.canBeEquipped(ship.attributes), false); - - TestTools.setAttribute(ship, "skill_time", 2); - - check.equals(equipment.canBeEquipped(ship.attributes), true); - - TestTools.setAttribute(ship, "skill_time", 3); - - check.equals(equipment.canBeEquipped(ship.attributes), true); - - // Second requirement - equipment.requirements["skill_materials"] = 3; - - check.equals(equipment.canBeEquipped(ship.attributes), false); - - TestTools.setAttribute(ship, "skill_materials", 4); - - check.equals(equipment.canBeEquipped(ship.attributes), true); - }); - - test.case("generates a description of the effects", check => { - let equipment = new Equipment(); - check.equals(equipment.getEffectsDescription(), "does nothing"); - - let action = new TriggerAction(equipment, [new DamageEffect(50)], 1, 200, 0); - equipment.action = action; - check.equals(equipment.getEffectsDescription(), "Fire (power 1, range 200km):\n• do 50 damage on target"); - - action = new TriggerAction(equipment, [new DamageEffect(50)], 1, 200, 20); - equipment.action = action; - check.equals(equipment.getEffectsDescription(), "Fire (power 1, range 200km):\n• do 50 damage in 20km radius"); - - action = new TriggerAction(equipment, [ - new DamageEffect(50), - new StickyEffect(new AttributeLimitEffect("shield_capacity", 200), 3) - ], 1, 200, 0); - equipment.action = action; - check.equals(equipment.getEffectsDescription(), "Fire (power 1, range 200km):\n• do 50 damage on target\n• limit shield capacity to 200 for 3 turns on target"); - }); - - test.case("gets a minimal level, based on skills requirements", check => { - let equipment = new Equipment(); - check.equals(equipment.getMinimumLevel(), 1); - - equipment.requirements["skill_quantum"] = 10; - check.equals(equipment.getMinimumLevel(), 1); - - equipment.requirements["skill_time"] = 1; - check.equals(equipment.getMinimumLevel(), 2); - - equipment.requirements["skill_gravity"] = 2; - check.equals(equipment.getMinimumLevel(), 2); - - equipment.requirements["skill_antimatter"] = 4; - check.equals(equipment.getMinimumLevel(), 3); - }); - - test.case("weighs the price, taking wear into account", check => { - let equipment = new Equipment(); - check.equals(equipment.getPrice(), 0); - - equipment.price = 100; - check.equals(equipment.getPrice(), 100); - - equipment.addWear(1); - check.equals(equipment.getPrice(), 99); - - equipment.addWear(10); - check.equals(equipment.getPrice(), 97); - - equipment.addWear(89); - check.equals(equipment.getPrice(), 83); - - equipment.addWear(400); - check.equals(equipment.getPrice(), 50); - - equipment.addWear(12500); - check.equals(equipment.getPrice(), 3); - }); - - test.case("builds a full textual description", check => { - let equipment = new Equipment(); - equipment.name = "Super Equipment"; - equipment.requirements["skill_gravity"] = 2; - equipment.effects.push(new AttributeEffect("skill_time", 3)); - equipment.wear = 50; - - let result = equipment.getFullDescription(); - check.equals(result, "Second hand\n\nRequires:\n• gravity skill 2\n\nWhen equipped:\n• time skill +3"); - }); - }); -} diff --git a/src/core/Equipment.ts b/src/core/Equipment.ts deleted file mode 100644 index 5e85629..0000000 --- a/src/core/Equipment.ts +++ /dev/null @@ -1,180 +0,0 @@ -module TK.SpaceTac { - /** - * Quality of loot. - */ - export enum EquipmentQuality { - WEAK, - COMMON, - FINE, - PREMIUM, - LEGENDARY - } - - // Piece of equipment to attach in slots - export class Equipment extends RObject { - // Type of slot this equipment can fit in - slot_type: SlotType | null - - // Actual slot this equipment is attached to - attached_to: Slot | null = null - - // Identifiable equipment code (may be used by UI to customize visual effects) - code: string - - // Equipment name - name: string - - // Equipment generic description - description = "" - - // Indicative equipment level - level = 1 - - // Indicative equipment quality - quality = EquipmentQuality.COMMON - - // Base price - price = 0 - - // Minimum skills to be able to equip this - requirements: { [key: string]: number } = {} - - // Permanent effects on the ship that equips this - effects: BaseEffect[] = [] - - // Action available when equipped - action: BaseAction | null = null - - // Equipment wear due to usage in battles (will lower the sell price) - wear = 0 - - // Cooldown needed by the equipment - cooldown = new Cooldown() - - // Basic constructor - constructor(slot: SlotType | null = null, code = "equipment") { - super(); - - this.slot_type = slot; - this.code = code; - this.name = code; - } - - jasmineToString() { - return this.attached_to ? `${this.attached_to.ship.getName()} - ${this.name}` : this.name; - } - - /** - * Get the fully qualified name (e.g. "Level 4 Strong Ray of Death") - */ - getFullName(): string { - let name = this.name; - if (this.quality != EquipmentQuality.COMMON) { - name = capitalize(EquipmentQuality[this.quality].toLowerCase()) + " " + name; - } - return `${name} Mk${this.level}`; - } - - /** - * Get the full textual description for this equipment (without the full name). - */ - getFullDescription(): string { - let requirements: string[] = []; - iteritems(this.requirements, (skill, value) => { - if (isShipAttribute(skill) && value > 0) { - requirements.push(`• ${SHIP_VALUES_NAMES[skill]} ${value}`); - } - }); - - let description = this.getEffectsDescription(); - if (this.description) { - description += "\n\n" + this.description; - } - if (requirements.length > 0) { - description = "Requires:\n" + requirements.join("\n") + "\n\n" + description; - } - if (this.cooldown.overheat > 0) { - description = `${this.cooldown}\n\n${description}`; - } - if (this.wear > 0) { - description = (this.wear >= 100 ? "Worn" : "Second hand") + "\n\n" + description; - } - return description; - } - - /** - * Get the minimum level at which the requirements in skill may be fulfilled. - * - * This is informative and is not directly enforced. It will only be enforced by skills requirements. - */ - getMinimumLevel(): number { - let points = sum(values(this.requirements)); - return ShipLevel.getLevelForPoints(points); - } - - /** - * Get the equipment price value. - */ - getPrice(): number { - return Math.floor(this.price * 500 / (500 + this.wear)); - } - - /** - * Returns true if the equipment can be equipped on a ship with given skills. - * - * This checks *requirements* against the effective (modified) skills. - * - * This does not check where the equipment currently is (except if is it already attached and should be detached first). - */ - canBeEquipped(skills: ShipAttributes, check_unattached = true): boolean { - if (check_unattached && this.attached_to) { - return false; - } else { - var able = true; - iteritems(this.requirements, (attr, minvalue) => { - if (isShipAttribute(attr) && skills[attr].get() < minvalue) { - able = false; - } - }); - return able; - } - } - - /** - * Detach from the slot it is attached to - */ - detach(): void { - if (this.attached_to) { - this.attached_to.attached = null; - this.attached_to = null; - } - } - - /** - * Get a human readable description of the effects of this equipment - */ - getEffectsDescription(): string { - let parts: string[] = []; - - if (this.effects.length > 0) { - parts.push(["When equipped:"].concat(this.effects.map(effect => "• " + effect.getDescription())).join("\n")); - } - - if (this.action) { - let action_desc = this.action.getEffectsDescription(); - if (action_desc != "") { - parts.push(action_desc); - } - } - - return parts.length > 0 ? parts.join("\n\n") : "does nothing"; - } - - /** - * Add equipment wear - */ - addWear(factor: number): void { - this.wear += factor; - } - } -} diff --git a/src/core/Fleet.spec.ts b/src/core/Fleet.spec.ts index 0b51652..94fc765 100644 --- a/src/core/Fleet.spec.ts +++ b/src/core/Fleet.spec.ts @@ -164,40 +164,5 @@ module TK.SpaceTac { ship4.setDead(); check.equals(fleet.isAlive(), false); }); - - test.case("adds cargo in first empty slot", check => { - let fleet = new Fleet(); - let ship1 = fleet.addShip(); - ship1.cargo_space = 1; - let ship2 = fleet.addShip(); - ship2.cargo_space = 2; - - check.equals(ship1.cargo, []); - check.equals(ship2.cargo, []); - - let equipment1 = new Equipment(); - let result = fleet.addCargo(equipment1); - check.equals(result, true); - check.equals(ship1.cargo, [equipment1]); - check.equals(ship2.cargo, []); - - let equipment2 = new Equipment(); - result = fleet.addCargo(equipment2); - check.equals(result, true); - check.equals(ship1.cargo, [equipment1]); - check.equals(ship2.cargo, [equipment2]); - - let equipment3 = new Equipment(); - result = fleet.addCargo(equipment3); - check.equals(result, true); - check.equals(ship1.cargo, [equipment1]); - check.equals(ship2.cargo, [equipment2, equipment3]); - - let equipment4 = new Equipment(); - result = fleet.addCargo(equipment4); - check.equals(result, false); - check.equals(ship1.cargo, [equipment1]); - check.equals(ship2.cargo, [equipment2, equipment3]); - }); }); } diff --git a/src/core/Fleet.ts b/src/core/Fleet.ts index 1c0281c..bce1616 100644 --- a/src/core/Fleet.ts +++ b/src/core/Fleet.ts @@ -145,19 +145,5 @@ module TK.SpaceTac { return any(this.ships, ship => ship.alive); } } - - /** - * Add an equipment to the first available cargo slot - * - * Returns true on success, false if no empty cargo slot was available. - */ - addCargo(equipment: Equipment): boolean { - let ship = first(this.ships, ship => ship.getFreeCargoSpace() > 0); - if (ship) { - return ship.addCargo(equipment); - } else { - return false; - } - } } } diff --git a/src/core/FleetGenerator.ts b/src/core/FleetGenerator.ts index 2e47981..f9dddca 100644 --- a/src/core/FleetGenerator.ts +++ b/src/core/FleetGenerator.ts @@ -15,7 +15,7 @@ module TK.SpaceTac { var fleet = new Fleet(player); var ship_generator = new ShipGenerator(this.random); - let models = this.random.sample(ShipModel.getDefaultCollection(), ship_count); + let models = this.random.sample(BaseModel.getDefaultCollection(), ship_count); range(ship_count).forEach(i => { var ship = ship_generator.generate(level, models[i] || null, upgrade); diff --git a/src/core/GameSession.spec.ts b/src/core/GameSession.spec.ts index 0ad1b7b..32cf2ec 100644 --- a/src/core/GameSession.spec.ts +++ b/src/core/GameSession.spec.ts @@ -52,11 +52,9 @@ module TK.SpaceTac.Specs { let battle = nn(session.getBattle()); battle.endBattle(session.player.fleet); - let spyloot = check.patch(nn(battle.outcome), "createLoot", null); session.setBattleEnded(); check.notequals(session.getBattle(), null); check.equals(location1.encounter, null); - check.called(spyloot, 1); // Defeat case location2.encounter = new Fleet(); @@ -66,11 +64,9 @@ module TK.SpaceTac.Specs { battle = nn(session.getBattle()); battle.endBattle(null); - spyloot = check.patch(nn(battle.outcome), "createLoot", null); session.setBattleEnded(); check.notequals(session.getBattle(), null); check.notequals(location2.encounter, null); - check.called(spyloot, 0); }); test.case("generates a new campaign", check => { diff --git a/src/core/GameSession.ts b/src/core/GameSession.ts index a8e86be..4ac96d6 100644 --- a/src/core/GameSession.ts +++ b/src/core/GameSession.ts @@ -152,11 +152,6 @@ module TK.SpaceTac { // Reset ships status iforeach(battle.iships(), ship => ship.restoreInitialState()); - // In case of victory for current player, generate loot - if (battle.outcome.winner == this.player.fleet) { - battle.outcome.createLoot(battle); - } - // If the battle happened in a star location, keep it informed let location = this.universe.getLocation(this.player.fleet.location); if (location) { diff --git a/src/core/LootGenerator.spec.ts b/src/core/LootGenerator.spec.ts deleted file mode 100644 index b97053a..0000000 --- a/src/core/LootGenerator.spec.ts +++ /dev/null @@ -1,28 +0,0 @@ -/// - -module TK.SpaceTac.Specs { - class TestTemplate extends LootTemplate { - constructor() { - super(SlotType.Shield, "Hexagrid Shield"); - - this.setSkillsRequirements({ "skill_time": istep(2) }); - } - } - - testing("LootGenerator", test => { - test.case("generates items within a given level range", check => { - var generator = new LootGenerator(); - generator.templates = [new TestTemplate()]; - generator.random = new SkewedRandomGenerator([0.5]); - - var equipment = generator.generate(2); - if (equipment) { - check.same(equipment.slot_type, SlotType.Shield); - check.equals(equipment.name, "Hexagrid Shield"); - check.equals(equipment.requirements, { "skill_time": 3 }); - } else { - check.fail("No equipment generated"); - } - }); - }); -} diff --git a/src/core/LootGenerator.ts b/src/core/LootGenerator.ts deleted file mode 100644 index f6cb341..0000000 --- a/src/core/LootGenerator.ts +++ /dev/null @@ -1,79 +0,0 @@ -module TK.SpaceTac { - /** - * Equipment generator from loot templates - * - * Loot templates are automatically populated from the "SpaceTac.Equipments" namespace - */ - export class LootGenerator { - // List of available templates - templates: LootTemplate[] - - // Random generator that will be used - random: RandomGenerator - - // Filter to select a subset of templates - templatefilter: (template: LootTemplate) => boolean - - constructor(random = RandomGenerator.global, populate: boolean = true) { - this.templates = []; - this.random = random; - this.templatefilter = () => true; - - if (populate) { - this.populate(); - } - } - - /** - * Set the template filter for next generations - */ - setTemplateFilter(filter: (template: LootTemplate) => boolean) { - this.templatefilter = filter; - } - - // Fill the list of templates - populate(): void { - let templates: LootTemplate[] = []; - let namespace: any = TK.SpaceTac.Equipments; - for (var template_name in namespace) { - if (template_name && template_name.indexOf("Abstract") != 0) { - let template_class = namespace[template_name]; - let template: LootTemplate = new template_class(); - templates.push(template); - } - } - this.templates = templates; - } - - // Generate a random equipment for a specific level - // If slot is specified, it will generate an equipment for this slot type specifically - // If no equipment could be generated from available templates, null is returned - generate(level: number, quality = EquipmentQuality.COMMON, slot: SlotType | null = null): Equipment | null { - // Generate equipments matching conditions, with each template - let templates = this.templates.filter(this.templatefilter).filter(template => slot == null || slot == template.slot); - let equipments = templates.map(template => template.generate(level, quality, this.random)); - - // No equipment could be generated with given conditions - if (equipments.length === 0) { - return null; - } - - // Pick a random equipment - return this.random.choice(equipments); - } - - /** - * Generate a random equipment of highest level, from a given set of skills - */ - generateHighest(skills: ShipSkills, quality = EquipmentQuality.COMMON, slot: SlotType | null = null): Equipment | null { - let templates = this.templates.filter(this.templatefilter).filter(template => slot == null || slot == template.slot); - let candidates = nna(templates.map(template => template.generateHighest(skills, quality, this.random))); - if (candidates.length) { - let chosen = this.random.weighted(candidates.map(equ => equ.level)); - return candidates[chosen]; - } else { - return null; - } - } - } -} diff --git a/src/core/LootQualityModifiers.ts b/src/core/LootQualityModifiers.ts deleted file mode 100644 index ed44fc4..0000000 --- a/src/core/LootQualityModifiers.ts +++ /dev/null @@ -1,112 +0,0 @@ -module TK.SpaceTac { - /** - * Modifiers of basic loot, to obtain different quality levels - */ - export class LootQualityModifiers { - /** - * Generic quality modifier - */ - static applyStandard(equipment: Equipment, quality: EquipmentQuality, random: RandomGenerator): boolean { - // Collect available modifiers - let modifiers: Function[] = []; - - let factor = 1; - if (quality == EquipmentQuality.WEAK) { - factor = 0.8; - } else if (quality == EquipmentQuality.FINE) { - factor = 1.1; - } else if (quality == EquipmentQuality.PREMIUM) { - factor = 1.3; - } else if (quality == EquipmentQuality.LEGENDARY) { - factor = 1.6; - } - - if (quality == EquipmentQuality.WEAK && any(values(equipment.requirements), value => value > 0)) { - modifiers.push(() => { - iteritems(copy(equipment.requirements), (skill, value) => { - equipment.requirements[skill] = Math.max(equipment.requirements[skill] + 1, Math.floor(equipment.requirements[skill] / factor)); - }); - }); - } - - function simpleFactor(obj: T, attr: keyof T, inverse = false) { - let val = obj[attr]; - if (val && val != 0) { - let nval = Math.round((inverse ? (1 / factor) : factor) * val); - if (nval != val) { - modifiers.push(() => (obj)[attr] = nval); - } - } - } - - function effectFactor(effect: BaseEffect) { - if (effect instanceof ValueEffect) { - simpleFactor(effect, 'value_on'); - simpleFactor(effect, 'value_off'); - simpleFactor(effect, 'value_start'); - simpleFactor(effect, 'value_end'); - } else if (effect instanceof AttributeEffect || effect instanceof AttributeMultiplyEffect) { - simpleFactor(effect, 'value'); - } else if (effect instanceof AttributeLimitEffect) { - simpleFactor(effect, 'value', true); - } else if (effect instanceof StickyEffect) { - simpleFactor(effect, 'duration'); - effectFactor(effect.base); - } else if (effect instanceof DamageEffect) { - simpleFactor(effect, 'base'); - simpleFactor(effect, 'span'); - } else if (effect instanceof RepelEffect) { - simpleFactor(effect, 'value'); - } else if (effect instanceof DamageModifierEffect) { - simpleFactor(effect, 'factor'); - } else if (effect instanceof ValueTransferEffect) { - simpleFactor(effect, 'amount'); - } else if (effect instanceof CooldownEffect) { - simpleFactor(effect, 'cooling'); - simpleFactor(effect, 'maxcount'); - } - } - - equipment.effects.forEach(effectFactor); - - if (equipment.action instanceof TriggerAction) { - simpleFactor(equipment.action, 'power', true); - simpleFactor(equipment.action, 'blast'); - simpleFactor(equipment.action, 'range'); - equipment.action.effects.forEach(effectFactor); - } - - if (equipment.action instanceof ToggleAction) { - simpleFactor(equipment.action, 'power', true); - simpleFactor(equipment.action, 'radius'); - equipment.action.effects.forEach(effectFactor); - } - - if (equipment.action instanceof DeployDroneAction) { - simpleFactor(equipment.action, 'deploy_distance'); - simpleFactor(equipment.action, 'drone_radius'); - equipment.action.drone_effects.forEach(effectFactor); - } - - if (equipment.action instanceof MoveAction) { - simpleFactor(equipment.action, 'distance_per_power'); - simpleFactor(equipment.action, 'maneuvrability_factor', true); - } - - if (equipment.cooldown.overheat) { - simpleFactor(equipment.cooldown, 'overheat', true); - simpleFactor(equipment.cooldown, 'cooling', true); - } - - // Choose a random one - if (modifiers.length > 0) { - let chosen = random.choice(modifiers); - chosen(); - equipment.price = Math.ceil(equipment.price * factor * factor); - return true; - } else { - return false; - } - } - } -} \ No newline at end of file diff --git a/src/core/LootTemplate.spec.ts b/src/core/LootTemplate.spec.ts deleted file mode 100644 index 2fe43f6..0000000 --- a/src/core/LootTemplate.spec.ts +++ /dev/null @@ -1,192 +0,0 @@ -/// - -module TK.SpaceTac.Specs { - class FakeEffect extends BaseEffect { - fakevalue: number - constructor(val = 5) { - super("fake"); - this.fakevalue = val; - } - } - - function strip(obj: T, attr: keyof T): any { - let result: any = {}; - copyfields(obj, result); - delete result[attr]; - return result; - } - - function strip_id(effect: RObject): any { - if (effect instanceof StickyEffect) { - let result = strip(effect, "id"); - result.base = strip_id(result.base); - return result; - } else { - return strip(effect, "id"); - } - } - - export function compare_effects(check: TestContext, effects1: BaseEffect[], effects2: BaseEffect[]): void { - check.equals(effects1.map(strip_id), effects2.map(strip_id), "effects"); - } - - export function compare_action(check: TestContext, action1: BaseAction | null, action2: BaseAction | null): void { - if (action1 === null || action2 === null) { - check.equals(action1, action2, "action"); - } else { - check.equals(strip_id(action1), strip_id(action2), "action"); - } - } - - export function compare_trigger_action(check: TestContext, action1: BaseAction | null, action2: TriggerAction | null): void { - if (action1 === null || action2 === null || !(action1 instanceof TriggerAction)) { - check.equals(action1, action2, "action"); - } else { - check.equals(strip_id(strip(action1, "effects")), strip_id(strip(action2, "effects")), "action"); - compare_effects(check, action1.effects, action2.effects); - } - } - - export function compare_toggle_action(check: TestContext, action1: BaseAction | null, action2: ToggleAction | null): void { - if (action1 === null || action2 === null || !(action1 instanceof ToggleAction)) { - check.equals(action1, action2, "action"); - } else { - check.equals(strip_id(strip(action1, "effects")), strip_id(strip(action2, "effects")), "action"); - compare_effects(check, action1.effects, action2.effects); - } - } - - export function compare_drone_action(check: TestContext, action1: BaseAction | null, action2: DeployDroneAction | null): void { - if (action1 === null || action2 === null || !(action1 instanceof DeployDroneAction)) { - check.equals(action1, action2, "action"); - } else { - check.equals(strip_id(strip(action1, "drone_effects")), strip_id(strip(action2, "drone_effects")), "action"); - compare_effects(check, action1.drone_effects, action2.drone_effects); - } - } - - testing("LootTemplate", test => { - test.case("generates equipment with correct information", check => { - let template = new LootTemplate(SlotType.Power, "Power Generator", "A great power generator !"); - let result = template.generate(2, EquipmentQuality.PREMIUM); - - check.equals(result.slot_type, SlotType.Power); - check.equals(result.code, "powergenerator"); - check.equals(result.name, "Power Generator"); - check.equals(result.price, 350); - check.equals(result.level, 2); - check.equals(result.quality, EquipmentQuality.COMMON); - check.equals(result.description, "A great power generator !"); - - template.addAttributeEffect("power_capacity", istep(10)); - result = template.generate(1, EquipmentQuality.COMMON); - check.equals(result.quality, EquipmentQuality.COMMON); - compare_effects(check, result.effects, [new AttributeEffect("power_capacity", 10)]); - result = template.generate(1, EquipmentQuality.PREMIUM); - check.equals(result.quality, EquipmentQuality.PREMIUM); - compare_effects(check, result.effects, [new AttributeEffect("power_capacity", 13)]); - }); - - test.case("applies requirements on skills", check => { - let template = new LootTemplate(SlotType.Hull, "Hull"); - template.setSkillsRequirements({ "skill_photons": istep(1), "skill_gravity": istep(2, istep(1)) }); - - let result = template.generate(1); - check.equals(result.requirements, { - "skill_photons": 1, - "skill_gravity": 2 - }); - - result = template.generate(2); - check.equals(result.requirements, { - "skill_photons": 2, - "skill_gravity": 3 - }); - - result = template.generate(10); - check.equals(result.requirements, { - "skill_photons": 10, - "skill_gravity": 47 - }); - }); - - test.case("applies cooldown", check => { - let template = new LootTemplate(SlotType.Weapon, "Weapon"); - template.setCooldown(istep(1), istep(2)); - - let result = template.generate(1); - check.equals(result.cooldown.overheat, 1); - check.equals(result.cooldown.cooling, 2); - - result = template.generate(2); - check.equals(result.cooldown.overheat, 2); - check.equals(result.cooldown.cooling, 3); - - result = template.generate(10); - check.equals(result.cooldown.overheat, 10); - check.equals(result.cooldown.cooling, 11); - }); - - test.case("applies attributes permenant effects", check => { - let template = new LootTemplate(SlotType.Shield, "Shield"); - template.addAttributeEffect("shield_capacity", irange(undefined, 50, 10)); - - let result = template.generate(1); - compare_effects(check, result.effects, [new AttributeEffect("shield_capacity", 50)]); - - result = template.generate(2); - compare_effects(check, result.effects, [new AttributeEffect("shield_capacity", 60)]); - }); - - test.case("adds move actions", check => { - let template = new LootTemplate(SlotType.Engine, "Engine"); - template.addMoveAction(irange(undefined, 100, 10), istep(50, irepeat(10)), irepeat(95)); - - let result = template.generate(1); - compare_action(check, result.action, new MoveAction(result, 100, 50, 95)); - - result = template.generate(2); - compare_action(check, result.action, new MoveAction(result, 110, 60, 95)); - }); - - test.case("adds fire actions", check => { - let template = new LootTemplate(SlotType.Weapon, "Weapon"); - template.addTriggerAction(istep(1), [ - new EffectTemplate(new FakeEffect(3), { "fakevalue": istep(8) }) - ], istep(100), istep(50), istep(10)); - - let result = template.generate(1); - compare_trigger_action(check, result.action, new TriggerAction(result, [new FakeEffect(8)], 1, 100, 50, 10)); - - result = template.generate(2); - compare_trigger_action(check, result.action, new TriggerAction(result, [new FakeEffect(9)], 2, 101, 51, 11)); - }); - - test.case("adds drone actions", check => { - let template = new LootTemplate(SlotType.Weapon, "Weapon"); - template.addDroneAction(istep(1), istep(100), istep(50), [ - new EffectTemplate(new FakeEffect(3), { "fakevalue": istep(8) }) - ]); - - let result = template.generate(1); - compare_drone_action(check, result.action, new DeployDroneAction(result, 1, 100, 50, [new FakeEffect(8)])); - - result = template.generate(2); - compare_drone_action(check, result.action, new DeployDroneAction(result, 2, 101, 51, [new FakeEffect(9)])); - }); - - test.case("checks the presence of damaging effects", check => { - let template = new LootTemplate(SlotType.Weapon, "Weapon"); - check.equals(template.hasDamageEffect(), false); - - template.addAttributeEffect("maneuvrability", irepeat(1)); - check.equals(template.hasDamageEffect(), false); - - template.addTriggerAction(irepeat(1), [new EffectTemplate(new BaseEffect("test"), {})], irepeat(50), irepeat(50)); - check.equals(template.hasDamageEffect(), false); - - template.addTriggerAction(irepeat(1), [new EffectTemplate(new DamageEffect(20), {})], irepeat(50), irepeat(50)); - check.equals(template.hasDamageEffect(), true); - }); - }); -} diff --git a/src/core/LootTemplate.ts b/src/core/LootTemplate.ts deleted file mode 100644 index 1419dea..0000000 --- a/src/core/LootTemplate.ts +++ /dev/null @@ -1,249 +0,0 @@ -module TK.SpaceTac { - /** - * A leveled value is an iterator yielding the desired value for each level (first item is for level 1, and so on) - */ - type LeveledValue = Iterator; - type LeveledModifiers = {[P in keyof T]?: LeveledValue } - - /** - * Modifiers of generated equipment - */ - type QualityModifier = (equipment: Equipment, quality: EquipmentQuality, random: RandomGenerator) => boolean; - type CommonModifier = (equipment: Equipment, level: number) => void; - - /** - * Resolve a leveled value - */ - function resolveForLevel(value: LeveledValue, level: number): number { - let lvalue = iat(value, level - 1) || 0; - return Math.floor(lvalue); - } - - /** - * Balanced generic leveled value - */ - export function leveled(base: number, increment = base * 0.4, exponent = 0.2): LeveledValue { - return istep(base, istep(increment, irepeat(increment * exponent))); - } - - /** - * Template used to generate a BaseEffect for a given ship level - */ - export class EffectTemplate { - // Basic instance of the effect - effect: T; - - // Effect value modifiers - modifiers: [keyof T, LeveledValue][]; - - constructor(effect: T, modifiers: LeveledModifiers) { - this.effect = effect; - this.modifiers = []; - - iteritems(modifiers, (key, value) => { - if (effect.hasOwnProperty(key) && value) { - this.addModifier(key, value); - } - }); - } - - // Add a value modifier for the effect - addModifier(name: keyof T, value: LeveledValue) { - this.modifiers.push([name, value]); - } - - // Generate an effect with a given level - generate(level: number): T { - let result = copy(this.effect); - this.modifiers.forEach(modifier => { - let [name, value] = modifier; - result[name] = resolveForLevel(value, level); - }); - return result; - } - } - - /** - * Template used to generate a BaseEffect for a given ship level - */ - export class StickyEffectTemplate extends EffectTemplate { - duration: LeveledValue; - - constructor(effect: T, modifiers: { [attr: string]: LeveledValue }, duration: LeveledValue) { - super(effect, modifiers); - - this.duration = duration; - } - - generate(level: number): StickyEffect { - let result = copy(this.effect); - this.modifiers.forEach(modifier => { - let [name, value] = modifier; - (result)[name] = resolveForLevel(value, level); - }); - return new StickyEffect(result, resolveForLevel(this.duration, level)); - } - } - - /** - * Template used to generate a loot equipment - */ - export class LootTemplate { - // Type of slot this equipment will fit in - slot: SlotType - - // Base name that will be given to generated equipment - name: string - - // Generic description of the equipment - description: string - - // Base price - price: LeveledValue - - // Modifiers applied to obtain the "common" equipment, based on level - protected base_modifiers: CommonModifier[] - - // Modifiers applied to "common" equipment to obtain a specific quality - protected quality_modifiers: QualityModifier[] - - constructor(slot: SlotType, name: string, description = "", base_price = 100) { - this.slot = slot; - this.name = name; - this.description = description; - this.price = leveled(base_price, base_price * 2.5, 1); - this.base_modifiers = []; - this.quality_modifiers = [LootQualityModifiers.applyStandard]; - } - - /** - * Generate a new equipment of a given level and quality - */ - generate(level: number, quality = EquipmentQuality.COMMON, random = RandomGenerator.global): Equipment { - let result = new Equipment(this.slot, (this.name || "").toLowerCase().replace(/ /g, "")); - - result.level = level; - result.name = this.name; - result.description = this.description; - result.price = resolveForLevel(this.price, level); - - this.base_modifiers.forEach(modifier => modifier(result, level)); - - if (quality == EquipmentQuality.COMMON) { - result.quality = quality; - } else { - let quality_applied = this.quality_modifiers.map(modifier => modifier(result, quality, random)); - result.quality = any(quality_applied, x => x) ? quality : EquipmentQuality.COMMON; - } - - return result; - } - - /** - * Generate the highest equipment level, for a given set of skills - */ - generateHighest(skills: ShipSkills, quality = EquipmentQuality.COMMON, random = RandomGenerator.global): Equipment | null { - let level = 1; - let equipment: Equipment | null = null; - let attributes = new ShipAttributes(); - keys(skills).forEach(skill => attributes[skill].addModifier(skills[skill].get())); - do { - let nequipment = this.generate(level, quality, random); - if (nequipment.canBeEquipped(attributes)) { - equipment = nequipment; - } else { - break; - } - level += 1; - } while (level < 100); - return equipment; - } - - /** - * Set skill requirements that will be added to each level of equipment. - */ - setSkillsRequirements(skills: { [skill: string]: LeveledValue }): void { - this.base_modifiers.push((equipment, level) => { - iteritems(skills, (skill, value) => { - let resolved = resolveForLevel(value, level); - if (resolved > 0) { - equipment.requirements[skill] = (equipment.requirements[skill] || 0) + resolved; - } - }); - }); - } - - /** - * Set the overheat/cooldown - */ - setCooldown(overheat: LeveledValue, cooldown: LeveledValue): void { - this.base_modifiers.push((equipment, level) => { - equipment.cooldown.configure(resolveForLevel(overheat, level), resolveForLevel(cooldown, level)); - }); - } - - /** - * Add a permanent attribute effect, when the item is equipped. - */ - addAttributeEffect(attribute: keyof ShipAttributes, value: LeveledValue): void { - this.base_modifiers.push((equipment, level) => { - let resolved = resolveForLevel(value, level); - if (resolved != 0) { - equipment.effects.push(new AttributeEffect(attribute, resolved)); - } - }); - } - - /** - * Add a move action. - */ - addMoveAction(distance_per_power: LeveledValue, safety_distance: LeveledValue = irepeat(120), maneuvrability_factor: LeveledValue = irepeat(80)): void { - this.base_modifiers.push((equipment, level) => { - equipment.action = new MoveAction(equipment, resolveForLevel(distance_per_power, level), resolveForLevel(safety_distance, level), resolveForLevel(maneuvrability_factor, level)); - }); - } - - /** - * Add a trigger action. - */ - addTriggerAction(power: LeveledValue, effects: EffectTemplate[], range: LeveledValue = irepeat(0), blast: LeveledValue = irepeat(0), angle: LeveledValue = irepeat(0), aim: LeveledValue = irepeat(0), evasion: LeveledValue = irepeat(0), luck: LeveledValue = irepeat(0)): void { - this.base_modifiers.push((equipment, level) => { - let reffects = effects.map(effect => effect.generate(level)); - equipment.action = new TriggerAction(equipment, reffects, resolveForLevel(power, level), resolveForLevel(range, level), resolveForLevel(blast, level), resolveForLevel(angle, level), resolveForLevel(aim, level), resolveForLevel(evasion, level), resolveForLevel(luck, level)); - }); - } - - /** - * Add a deploy drone action. - */ - addDroneAction(power: LeveledValue, range: LeveledValue, radius: LeveledValue, effects: EffectTemplate[]): void { - this.base_modifiers.push((equipment, level) => { - let reffects = effects.map(effect => effect.generate(level)); - equipment.action = new DeployDroneAction(equipment, resolveForLevel(power, level), resolveForLevel(range, level), resolveForLevel(radius, level), reffects); - }); - } - - /** - * Add a toggle action. - */ - addToggleAction(power: LeveledValue, radius: LeveledValue, effects: EffectTemplate[]): void { - this.base_modifiers.push((equipment, level) => { - let reffects = effects.map(effect => effect.generate(level)); - equipment.action = new ToggleAction(equipment, resolveForLevel(power, level), resolveForLevel(radius, level), reffects); - }); - } - - /** - * Check if the template has any damage effect (to know if is an offensive weapon) - */ - hasDamageEffect(): boolean { - let example = this.generate(1); - let action = example.action; - if (action instanceof TriggerAction || action instanceof DeployDroneAction) { - return any(action.effects, effect => effect instanceof DamageEffect || (effect instanceof StickyEffect && effect.base instanceof DamageEffect)); - } else { - return false; - } - } - } -} diff --git a/src/core/MoveFireSimulator.spec.ts b/src/core/MoveFireSimulator.spec.ts index 5a90329..1964b27 100644 --- a/src/core/MoveFireSimulator.spec.ts +++ b/src/core/MoveFireSimulator.spec.ts @@ -3,9 +3,9 @@ module TK.SpaceTac.Specs { function simpleWeaponCase(distance = 10, ship_ap = 5, weapon_ap = 3, engine_distance = 5): [Ship, MoveFireSimulator, BaseAction] { let ship = new Ship(); - TestTools.setShipAP(ship, ship_ap); + TestTools.setShipModel(ship, 100, 0, ship_ap); TestTools.addEngine(ship, engine_distance); - let action = new TriggerAction(new Equipment(), [], weapon_ap, distance); + let action = new TriggerAction("weapon", { power: weapon_ap, range: distance }); let simulator = new MoveFireSimulator(ship); return [ship, simulator, action]; } @@ -21,7 +21,6 @@ module TK.SpaceTac.Specs { let engine4 = TestTools.addEngine(ship, 70); let best = simulator.findBestEngine(); check.same(best, engine3); - check.equals((nn(best).action).distance_per_power, 150); }); test.case("fires directly when in range", check => { @@ -65,7 +64,7 @@ module TK.SpaceTac.Specs { check.same(result.can_fire, true, 'can_fire'); check.same(result.total_fire_ap, 3, 'total_fire_ap'); - let move_action = ship.listEquipment(SlotType.Engine)[0].action; + let move_action = ship.actions.listAll().filter(action => action instanceof MoveAction)[0]; check.equals(result.parts, [ { action: move_action, target: new Target(ship.arena_x + 5, ship.arena_y, null), ap: 1, possible: true }, { action: action, target: new Target(ship.arena_x + 15, ship.arena_y, null), ap: 3, possible: true } @@ -111,7 +110,7 @@ module TK.SpaceTac.Specs { let battle = new Battle(); battle.fleets[0].addShip(ship); let ship1 = battle.fleets[0].addShip(); - let moveaction = nn(simulator.findBestEngine()).action; + let moveaction = nn(simulator.findBestEngine()); (moveaction).safety_distance = 30; battle.ship_separation = 30; @@ -143,7 +142,7 @@ module TK.SpaceTac.Specs { check.same(result.can_fire, false, 'can_fire'); check.same(result.total_fire_ap, 2, 'total_fire_ap'); - let move_action = ship.listEquipment(SlotType.Engine)[0].action; + let move_action = ship.actions.listAll().filter(action => action instanceof MoveAction)[0]; check.equals(result.parts, [ { action: move_action, target: new Target(ship.arena_x + 10, ship.arena_y, null), ap: 2, possible: true }, { action: action, target: new Target(ship.arena_x + 18, ship.arena_y, null), ap: 2, possible: false } @@ -152,7 +151,7 @@ module TK.SpaceTac.Specs { test.case("does nothing if trying to move in the same spot", check => { let [ship, simulator, action] = simpleWeaponCase(); - let move_action = nn(ship.listEquipment(SlotType.Engine)[0].action) + let move_action = ship.actions.listAll().filter(action => action instanceof MoveAction)[0]; let result = simulator.simulateAction(move_action, new Target(ship.arena_x, ship.arena_y, null)); check.equals(result.success, false); check.equals(result.need_move, false); diff --git a/src/core/MoveFireSimulator.ts b/src/core/MoveFireSimulator.ts index a2dd89c..6d1d6c2 100644 --- a/src/core/MoveFireSimulator.ts +++ b/src/core/MoveFireSimulator.ts @@ -53,14 +53,14 @@ module TK.SpaceTac { } /** - * Find the best available engine for moving + * Find the best available move action */ - findBestEngine(): Equipment | null { - let engines = this.ship.listEquipment(SlotType.Engine); - if (engines.length == 0) { + findBestEngine(): MoveAction | null { + let actions = this.ship.actions.listAll().filter(action => action instanceof MoveAction); + if (actions.length == 0) { return null; } else { - return maxBy(engines, engine => (engine.action instanceof MoveAction) ? engine.action.getDistanceByActionPoint(this.ship) : 0); + return maxBy(actions, action => action.getDistanceByActionPoint(this.ship)); } } @@ -141,9 +141,9 @@ module TK.SpaceTac { } } else { let engine = this.findBestEngine(); - if (engine && engine.action instanceof MoveAction) { + if (engine) { let approach_radius = action.getRangeRadius(this.ship); - let approach = this.getApproach(engine.action, target, approach_radius, move_margin); + let approach = this.getApproach(engine, target, approach_radius, move_margin); if (approach instanceof Target) { result.need_move = true; move_target = approach; @@ -162,13 +162,13 @@ module TK.SpaceTac { // Check move AP if (result.need_move && move_target) { let engine = this.findBestEngine(); - if (engine && engine.action) { - result.total_move_ap = engine.action.getActionPointsUsage(this.ship, move_target); + if (engine) { + result.total_move_ap = engine.getActionPointsUsage(this.ship, move_target); result.can_move = ap > 0; result.can_end_move = result.total_move_ap <= ap; result.move_location = move_target; // TODO Split in "this turn" part and "next turn" part if needed - result.parts.push({ action: engine.action, target: move_target, ap: result.total_move_ap, possible: result.can_move }); + result.parts.push({ action: engine, target: move_target, ap: result.total_move_ap, possible: result.can_move }); ap -= result.total_move_ap; } diff --git a/src/core/Ship.spec.ts b/src/core/Ship.spec.ts index b289bf5..ccf81ba 100644 --- a/src/core/Ship.spec.ts +++ b/src/core/Ship.spec.ts @@ -5,7 +5,7 @@ module TK.SpaceTac.Specs { check.equals(ship.getName(false), "Ship"); check.equals(ship.getName(true), "Level 1 Ship"); - ship.model = new ShipModel("test", "Hauler"); + ship.model = new BaseModel("test", "Hauler"); check.equals(ship.getName(false), "Hauler"); check.equals(ship.getName(true), "Level 1 Hauler"); @@ -34,56 +34,17 @@ module TK.SpaceTac.Specs { check.nears(ship.arena_angle, 1.2); }); - test.case("lists available actions from attached equipment", check => { - var ship = new Ship(null, "Test"); - var actions: BaseAction[]; - var slot: Slot; - var equipment: Equipment; + test.case("applies permanent effects of ship model on attributes", check => { + let model = new BaseModel(); + let ship = new Ship(null, null, model); - actions = ship.getAvailableActions(); - check.equals(actions.length, 1); - check.equals(actions[0].code, "endturn"); - - slot = ship.addSlot(SlotType.Engine); - equipment = new Equipment(slot.type); - equipment.action = new MoveAction(equipment); - slot.attach(equipment); - - slot = ship.addSlot(SlotType.Weapon); - equipment = new Equipment(slot.type); - slot.attach(equipment); - - slot = ship.addSlot(SlotType.Power); - equipment = new Equipment(slot.type); - equipment.action = new TriggerAction(equipment); - slot.attach(equipment); - - actions = ship.getAvailableActions(); - check.equals(actions.length, 3); - check.equals(actions[0].code, "move"); - check.equals(actions[1].code, "fire-equipment"); - check.equals(actions[2].code, "endturn"); - }); - - test.case("applies permanent effects of equipments on attributes", check => { - var ship = new Ship(null, "Test"); - var slot: Slot; - var equipment: Equipment; - - slot = ship.addSlot(SlotType.Power); - equipment = new Equipment(); - equipment.slot_type = slot.type; - equipment.effects.push(new AttributeEffect("power_capacity", 4)); - slot.attach(equipment); - - slot = ship.addSlot(SlotType.Power); - equipment = new Equipment(); - equipment.slot_type = slot.type; - equipment.effects.push(new AttributeEffect("power_capacity", 5)); - slot.attach(equipment); + check.patch(model, "getEffects", () => [ + new AttributeEffect("power_capacity", 4), + new AttributeEffect("power_capacity", 5), + ]); ship.updateAttributes(); - check.equals(ship.attributes.power_capacity.get(), 9); + check.equals(ship.getAttribute("power_capacity"), 9); }); test.case("repairs hull and recharges shield", check => { @@ -120,38 +81,6 @@ module TK.SpaceTac.Specs { check.equals(ship.isAbleToPlay(false), false); }); - test.case("counts attached equipment", check => { - var ship = new Ship(); - - check.equals(ship.getEquipmentCount(), 0); - - ship.addSlot(SlotType.Hull).attach(new Equipment(SlotType.Hull)); - ship.addSlot(SlotType.Shield); - ship.addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon)); - - check.equals(ship.getEquipmentCount(), 2); - }); - - test.case("can pick a random attached equipment", check => { - var ship = new Ship(); - - check.equals(ship.getRandomEquipment(), null); - - ship.addSlot(SlotType.Hull).attach(new Equipment(SlotType.Hull)); - ship.addSlot(SlotType.Shield); - ship.addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon)); - - var random = new SkewedRandomGenerator([0.2]); - var picked = ship.getRandomEquipment(random); - check.notequals(picked, null); - check.same(picked, ship.slots[0].attached); - - random = new SkewedRandomGenerator([0.999999]); - picked = ship.getRandomEquipment(random); - check.notequals(picked, null); - check.same(picked, ship.slots[2].attached); - }); - test.case("checks if a ship is inside a given circle", check => { let ship = new Ship(); ship.arena_x = 5; @@ -166,159 +95,21 @@ module TK.SpaceTac.Specs { check.equals(ship.isInCircle(12, -4, 5), false); }); - test.case("stores items in cargo space", check => { - let ship = new Ship(); - let equipment1 = new Equipment(); - let equipment2 = new Equipment(); - - let result = ship.addCargo(equipment1); - check.equals(result, false); - check.equals(ship.cargo, []); - check.equals(ship.getFreeCargoSpace(), 0); - - ship.setCargoSpace(1); - check.equals(ship.getFreeCargoSpace(), 1); - - result = ship.addCargo(equipment1); - check.equals(result, true); - check.equals(ship.cargo, [equipment1]); - check.equals(ship.getFreeCargoSpace(), 0); - - result = ship.addCargo(equipment1); - check.equals(result, false); - check.equals(ship.cargo, [equipment1]); - - result = ship.addCargo(equipment2); - check.equals(result, false); - check.equals(ship.cargo, [equipment1]); - - ship.setCargoSpace(2); - check.equals(ship.getFreeCargoSpace(), 1); - - result = ship.addCargo(equipment2); - check.equals(result, true); - check.equals(ship.cargo, [equipment1, equipment2]); - check.equals(ship.getFreeCargoSpace(), 0); - - ship.setCargoSpace(1); - check.equals(ship.cargo, [equipment1]); - check.equals(ship.getFreeCargoSpace(), 0); - - ship.setCargoSpace(2); - check.equals(ship.cargo, [equipment1]); - check.equals(ship.getFreeCargoSpace(), 1); - }); - - test.case("equips items from cargo", check => { - let ship = new Ship(); - let equipment = new Equipment(SlotType.Weapon); - let slot = ship.addSlot(SlotType.Weapon); - check.equals(ship.listEquipment(), []); - - let result = ship.equip(equipment); - check.equals(result, false); - check.equals(ship.listEquipment(), []); - - ship.setCargoSpace(1); - ship.addCargo(equipment); - - result = ship.equip(equipment); - check.equals(result, true); - check.equals(ship.listEquipment(SlotType.Weapon), [equipment]); - check.equals(equipment.attached_to, slot); - }); - - test.case("removes equipped items", check => { - let ship = new Ship(); - let equipment = new Equipment(SlotType.Weapon); - let slot = ship.addSlot(SlotType.Weapon); - slot.attach(equipment); - - check.equals(ship.listEquipment(), [equipment]); - check.same(slot.attached, equipment); - check.same(equipment.attached_to, slot); - - let result = ship.unequip(equipment); - check.equals(result, false); - check.equals(ship.listEquipment(), [equipment]); - check.equals(ship.cargo, []); - - ship.setCargoSpace(10); - - result = ship.unequip(equipment); - check.equals(result, true); - check.equals(ship.listEquipment(), []); - check.equals(ship.cargo, [equipment]); - check.equals(slot.attached, null); - check.equals(equipment.attached_to, null); - - result = ship.unequip(equipment); - check.equals(result, false); - check.equals(ship.listEquipment(), []); - check.equals(ship.cargo, [equipment]); - }); - - test.case("checks equipment requirements", check => { - let ship = new Ship(); - let equipment = new Equipment(SlotType.Hull); - check.equals(ship.canEquip(equipment), null); - - ship.addSlot(SlotType.Engine); - check.equals(ship.canEquip(equipment), null); - - let slot = ship.addSlot(SlotType.Hull); - check.same(ship.canEquip(equipment), slot); - - equipment.requirements["skill_photons"] = 2; - check.equals(ship.canEquip(equipment), null); - - ship.upgradeSkill("skill_photons"); - check.equals(ship.canEquip(equipment), null); - - ship.upgradeSkill("skill_photons"); - check.same(ship.canEquip(equipment), slot); - - slot.attach(new Equipment(SlotType.Hull)); - check.equals(ship.canEquip(equipment), null); - }); - - test.case("allow skills upgrading from current level", check => { - let ship = new Ship(); - check.equals(ship.level.get(), 1); - check.equals(ship.getAvailableUpgradePoints(), 10); - - ship.level.forceLevel(2); - check.equals(ship.level.get(), 2); - check.equals(ship.getAvailableUpgradePoints(), 15); - - check.equals(ship.getAttribute("skill_photons"), 0); - ship.upgradeSkill("skill_photons"); - check.equals(ship.getAttribute("skill_photons"), 1); - - range(50).forEach(() => ship.upgradeSkill("skill_gravity")); - check.equals(ship.getAttribute("skill_photons"), 1); - check.equals(ship.getAttribute("skill_gravity"), 14); - check.equals(ship.getAvailableUpgradePoints(), 0); - - ship.updateAttributes(); - check.equals(ship.getAttribute("skill_photons"), 1); - check.equals(ship.getAttribute("skill_gravity"), 14); - }); - test.case("restores as new at the end of battle", check => { let ship = new Ship(); - TestTools.setShipHP(ship, 10, 20); - TestTools.setShipAP(ship, 5, 0); + TestTools.setShipModel(ship, 10, 20, 5); ship.setValue("hull", 5); ship.setValue("shield", 15); ship.setValue("power", 2); ship.active_effects.add(new StickyEffect(new AttributeLimitEffect("power_capacity", 3), 12)); ship.updateAttributes(); - let action1 = new BaseAction(); - let action2 = new ToggleAction(new Equipment()); - action2.activated = true; - let action3 = new ToggleAction(new Equipment()); - check.patch(ship, "getAvailableActions", () => [action1, action2, action3]); + let action1 = new BaseAction("action1"); + ship.actions.addCustom(action1); + let action2 = new ToggleAction("action2"); + ship.actions.addCustom(action2); + ship.actions.toggle(action2, true); + let action3 = new ToggleAction("action3"); + ship.actions.addCustom(action3); check.in("before", check => { check.equals(ship.getValue("hull"), 5, "hull"); @@ -326,8 +117,8 @@ module TK.SpaceTac.Specs { check.equals(ship.getValue("power"), 2, "power"); check.equals(ship.active_effects.count(), 1, "effects count"); check.equals(ship.getAttribute("power_capacity"), 3, "power capacity"); - check.equals(action2.activated, true, "action 2 activation"); - check.equals(action3.activated, false, "action 3 activation"); + check.equals(ship.actions.isToggled(action2), true, "action 2 activation"); + check.equals(ship.actions.isToggled(action3), false, "action 3 activation"); }); ship.restoreInitialState(); @@ -338,8 +129,8 @@ module TK.SpaceTac.Specs { check.equals(ship.getValue("power"), 5, "power"); check.equals(ship.active_effects.count(), 0, "effects count"); check.equals(ship.getAttribute("power_capacity"), 5, "power capacity"); - check.equals(action2.activated, false, "action 2 activation"); - check.equals(action3.activated, false, "action 3 activation"); + check.equals(ship.actions.isToggled(action2), false, "action 2 activation"); + check.equals(ship.actions.isToggled(action3), false, "action 3 activation"); }); }); @@ -347,11 +138,8 @@ module TK.SpaceTac.Specs { let ship = new Ship(); check.equals(imaterialize(ship.ieffects()), []); - let equipment = ship.addSlot(SlotType.Engine).attach(new Equipment(SlotType.Engine)); - check.equals(imaterialize(ship.ieffects()), []); - let effect1 = new AttributeEffect("precision", 4); - equipment.effects.push(effect1); + check.patch(ship.model, "getEffects", () => [effect1]); check.equals(imaterialize(ship.ieffects()), [effect1]); let effect2 = new AttributeLimitEffect("precision", 2); @@ -361,22 +149,19 @@ module TK.SpaceTac.Specs { test.case("gets a textual description of an attribute", check => { let ship = new Ship(); - check.equals(ship.getAttributeDescription("skill_photons"), "Forces of light, and electromagnetic radiation"); + check.equals(ship.getAttributeDescription("maneuvrability"), "Ability to move first, fast and to evade weapons"); - let equipment = new Equipment(SlotType.Engine); - equipment.effects = [new AttributeEffect("skill_photons", 4)]; - equipment.name = "Photonic engine"; - ship.addSlot(SlotType.Engine).attach(equipment); - check.equals(ship.getAttribute("skill_photons"), 4); - check.equals(ship.getAttributeDescription("skill_photons"), "Forces of light, and electromagnetic radiation\n\nPhotonic engine Mk1: +4"); + check.patch(ship.model, "getEffects", () => [new AttributeEffect("maneuvrability", 4)]); + ship.updateAttributes(); + check.equals(ship.getAttribute("maneuvrability"), 4); + check.equals(ship.getAttributeDescription("maneuvrability"), "Ability to move first, fast and to evade weapons\n\nLevel 1 Ship: +4"); - ship.level.forceLevelUp(); - ship.upgradeSkill("skill_photons"); - ship.upgradeSkill("skill_photons"); - check.equals(ship.getAttributeDescription("skill_photons"), "Forces of light, and electromagnetic radiation\n\nLevelled up: +2\nPhotonic engine Mk1: +4"); + ship.active_effects.add(new StickyEffect(new AttributeLimitEffect("maneuvrability", 3))); + check.equals(ship.getAttributeDescription("maneuvrability"), "Ability to move first, fast and to evade weapons\n\nLevel 1 Ship: +4\nSticky effect: limit to 3"); - ship.active_effects.add(new StickyEffect(new AttributeLimitEffect("skill_photons", 3))); - check.equals(ship.getAttributeDescription("skill_photons"), "Forces of light, and electromagnetic radiation\n\nLevelled up: +2\nPhotonic engine Mk1: +4\n???: limit to 3"); + ship.active_effects.remove(ship.active_effects.list()[0]); + ship.active_effects.add(new AttributeEffect("maneuvrability", -1)); + check.equals(ship.getAttributeDescription("maneuvrability"), "Ability to move first, fast and to evade weapons\n\nLevel 1 Ship: +4\nActive effect: -1"); }); test.case("produces death diffs", check => { @@ -388,20 +173,18 @@ module TK.SpaceTac.Specs { new ShipDeathDiff(battle, ship), ]); - let effect1 = ship.active_effects.add(new AttributeEffect("skill_quantum", 2)); - let effect2 = ship.active_effects.add(new StickyEffect(new AttributeEffect("skill_materials", 4))); - let weapon1 = TestTools.addWeapon(ship); - weapon1.action = new ToggleAction(weapon1, 3); - let weapon2 = TestTools.addWeapon(ship); - let action = weapon2.action = new ToggleAction(weapon2, 3); - action.activated = true; + let effect1 = ship.active_effects.add(new AttributeEffect("precision", 2)); + let effect2 = ship.active_effects.add(new StickyEffect(new AttributeLimitEffect("maneuvrability", 1))); + let action1 = ship.actions.addCustom(new ToggleAction("weapon1", { power: 3 })); + let action2 = ship.actions.addCustom(new ToggleAction("weapon2", { power: 3 })); + ship.actions.toggle(action2, true); check.equals(ship.getDeathDiffs(battle), [ new ShipEffectRemovedDiff(ship, effect1), - new ShipAttributeDiff(ship, "skill_quantum", {}, { cumulative: 2 }), + new ShipAttributeDiff(ship, "precision", {}, { cumulative: 2 }), new ShipEffectRemovedDiff(ship, effect2), - new ShipAttributeDiff(ship, "skill_materials", {}, { cumulative: 4 }), - new ShipActionToggleDiff(ship, action, false), + new ShipAttributeDiff(ship, "maneuvrability", {}, { limit: 1 }), + new ShipActionToggleDiff(ship, action2, false), new ShipValueDiff(ship, "hull", -1), new ShipDeathDiff(battle, ship), ]); diff --git a/src/core/Ship.ts b/src/core/Ship.ts index c7d7977..62333fb 100644 --- a/src/core/Ship.ts +++ b/src/core/Ship.ts @@ -5,19 +5,18 @@ module TK.SpaceTac { * A single ship in a fleet */ export class Ship extends RObject { + // Ship model + model: BaseModel + // Fleet this ship is a member of fleet: Fleet // Level of this ship level = new ShipLevel() - skills = new ShipSkills() // Name of the ship, null if unimportant name: string | null - // Code of the ShipModel used to create it - model: ShipModel - // Flag indicating if the ship is alive alive: boolean @@ -31,16 +30,12 @@ module TK.SpaceTac { // Facing direction in the arena arena_angle: number - // Active effects (sticky or area) + // Available actions + actions = new ActionList() + + // Active effects (sticky, self or area) active_effects = new RObjectContainer() - // List of slots, able to contain equipment - slots: Slot[] - - // Cargo - cargo_space: number = 0 - cargo: Equipment[] = [] - // Ship attributes attributes = new ShipAttributes() @@ -53,27 +48,27 @@ module TK.SpaceTac { // Boolean set to true if the ship is currently playing its turn playing = false - // Priority in play_order - play_priority = 0; + // Priority in current battle's play_order (used as sort key) + play_priority = 0 // Create a new ship inside a fleet - constructor(fleet: Fleet | null = null, name: string | null = null, model = new ShipModel("default", "Ship", 1, 0, false, 0)) { + constructor(fleet: Fleet | null = null, name: string | null = null, model = new BaseModel()) { super(); this.fleet = fleet || new Fleet(); this.name = name; this.alive = true; - this.slots = []; this.arena_x = 0; this.arena_y = 0; this.arena_angle = 0; - this.fleet.addShip(this); - this.model = model; - this.setCargoSpace(model.cargo); - model.slots.forEach(slot => this.addSlot(slot)); + + this.updateAttributes(); + this.actions.updateFromShip(this); + + this.fleet.addShip(this); } /** @@ -112,7 +107,7 @@ module TK.SpaceTac { // String repr jasmineToString(): string { - return "Ship " + this.name; + return this.getName(); } // Make an initiative throw, to resolve play order in a battle @@ -134,63 +129,43 @@ module TK.SpaceTac { return player.is(this.fleet.player); } - // get the current battle this ship is engaged in + /** + * Get the battle this ship is currently engaged in + */ getBattle(): Battle | null { return this.fleet.battle; } /** - * Get the list of actions available - * - * This list does not filter out actions unavailable due to insufficient AP, it only filters out actions that - * are not allowed/available at all on the ship + * Get the list of activated upgrades */ - getAvailableActions(): BaseAction[] { - var actions: BaseAction[] = []; + getUpgrades(): ModelUpgrade[] { + return this.model.getActivatedUpgrades(this.level.get(), this.level.getUpgrades()); + } - if (this.alive) { - let slots = [SlotType.Engine, SlotType.Power, SlotType.Hull, SlotType.Shield, SlotType.Weapon]; - slots.forEach(slot => { - this.listEquipment(slot).forEach(equipment => { - if (equipment.action) { - actions.push(equipment.action) - } - }); - }); + /** + * Toggle an upgrade + */ + activateUpgrade(upgrade: ModelUpgrade, on: boolean): void { + if (on && (upgrade.cost || 0) > this.getAvailableUpgradePoints()) { + return; } - - actions.push(EndTurnAction.SINGLETON); - return actions; + this.level.activateUpgrade(upgrade, on); + this.updateAttributes(); + this.actions.updateFromShip(this); } - /** - * Get an available action by its ID - */ - getAction(action_id: RObjectId): BaseAction | null { - return first(this.getAvailableActions(), action => action.is(action_id)); - } - - /** - * Get the number of upgrade points available to improve skills + /** + * Get the number of upgrade points available */ getAvailableUpgradePoints(): number { - let used = keys(SHIP_SKILLS).map(skill => this.skills[skill].get()).reduce((a, b) => a + b, 0); - return this.level.getSkillPoints() - used; + let upgrades = this.getUpgrades(); + return this.level.getUpgradePoints() - sum(upgrades.map(upgrade => upgrade.cost || 0)); } /** - * Try to upgrade a skill by 1 point or more + * Add an event to the battle log, if any */ - upgradeSkill(skill: keyof ShipSkills, points = 1) { - if (this.getBattle()) { - console.error("Cannot upgrade skill during battle"); - } else if (this.getAvailableUpgradePoints() >= points) { - this.skills[skill].addModifier(points); - this.updateAttributes(); - } - } - - // Add an event to the battle log, if any addBattleEvent(event: BaseBattleDiff): void { var battle = this.getBattle(); if (battle && battle.log) { @@ -226,9 +201,11 @@ module TK.SpaceTac { return this.attributes[name].get(); } - // Initialize the action points counter - // This should be called once at the start of a battle - // If no value is provided, the attribute power_capacity will be used + /** + * Initialize the action points counter + * This should be called once at the start of a battle + * If no value is provided, the attribute power_capacity will be used + */ private initializePower(value: number | null = null): void { if (value === null) { value = this.getAttribute("power_capacity"); @@ -236,35 +213,18 @@ module TK.SpaceTac { this.setValue("power", value); } - /** - * Consumes action points - * - * Return true if it was possible, false if there wasn't enough points. - */ - useActionPoints(value: number): boolean { - if (this.getValue("power") >= value) { - this.setValue("power", -value, true); - return true; - } else { - return false; - } - } - /** * Method called at the start of battle, to restore a pristine condition on the ship */ restoreInitialState() { this.alive = true; + + this.actions.updateFromShip(this); this.active_effects = new RObjectContainer(); + this.updateAttributes(); this.restoreHealth(); this.initializePower(); - this.listEquipment().forEach(equipment => equipment.cooldown.reset()); - this.getAvailableActions().forEach(action => { - if (action instanceof ToggleAction) { - action.activated = false; - } - }); } /** @@ -348,178 +308,12 @@ module TK.SpaceTac { } /** - * Get cargo space not occupied by items - */ - getFreeCargoSpace(): number { - return this.cargo_space - this.cargo.length; - } - - /** - * Set the available cargo space. - */ - setCargoSpace(cargo: number) { - this.cargo_space = cargo; - this.cargo.splice(this.cargo_space); - } - - /** - * Add an equipment to cargo space - * - * Returns true if successful - */ - addCargo(item: Equipment): boolean { - if (this.cargo.length < this.cargo_space) { - return add(this.cargo, item); - } else { - return false; - } - } - - /** - * Remove an item from cargo space - * - * Returns true if successful - */ - removeCargo(item: Equipment): boolean { - return remove(this.cargo, item); - } - - /** - * Equip an item from cargo to the first available slot - * - * Returns true if successful - */ - equip(item: Equipment, from_cargo = true): boolean { - let free_slot = this.canEquip(item); - - if (free_slot && (!from_cargo || remove(this.cargo, item))) { - free_slot.attach(item); - if (item.attached_to == free_slot && free_slot.attached == item) { - this.updateAttributes(); - return true; - } else { - return false; - } - } else { - return false; - } - } - - /** - * Check if a ship is able to equip en item, and return the slot it may fit in, or null - */ - canEquip(item: Equipment): Slot | null { - let free_slot = first(this.slots, slot => slot.type == item.slot_type && !slot.attached); - if (free_slot) { - if (item.canBeEquipped(this.attributes)) { - return free_slot; - } else { - return null; - } - } else { - return null; - } - } - - /** - * Remove an equipped item, returning it to cargo - * - * Returns true if successful - */ - unequip(item: Equipment, to_cargo = true): boolean { - if (item.attached_to && item.attached_to.attached == item && (!to_cargo || this.cargo.length < this.cargo_space)) { - item.detach(); - if (to_cargo) { - add(this.cargo, item); - } - - this.updateAttributes(); - - return true; - } else { - return false; - } - } - - /** - * Add an empty equipment slot of the given type - */ - addSlot(type: SlotType): Slot { - var result = new Slot(this, type); - this.slots.push(result); - return result; - } - - /** - * List all equipments attached to slots of a given type (any slot type if null) - */ - listEquipment(slottype: SlotType | null = null): Equipment[] { - return nna(this.slots.filter(slot => slot.attached && (slottype == null || slot.type == slottype)).map(slot => slot.attached)); - } - - /** - * Get the first free slot of a given type, null if none is available - */ - getFreeSlot(type: SlotType): Slot | null { - return first(this.slots, slot => slot.type == type && slot.attached == null); - } - - // Get the number of attached equipments - getEquipmentCount(): number { - var result = 0; - this.slots.forEach((slot: Slot) => { - if (slot.attached) { - result++; - } - }); - return result; - } - - // Get a random attached equipment, null if no equipment is attached - getRandomEquipment(random = RandomGenerator.global): Equipment | null { - var count = this.getEquipmentCount(); - if (count === 0) { - return null; - } else { - var picked = random.randInt(0, count - 1); - var result: Equipment | null = null; - var index = 0; - this.slots.forEach((slot: Slot) => { - if (slot.attached) { - if (index === picked) { - result = slot.attached; - } - index++; - } - }); - return result; - } - } - - /** - * Get the list of equipped items - */ - listEquipments(): Equipment[] { - return nna(this.slots.map(slot => slot.attached)); - } - - /** - * Get an equipment by its ID - */ - getEquipment(id: RObjectId): Equipment | null { - return first(this.listEquipments(), equipment => equipment.id === id); - } - - /** - * Update attributes, taking into account attached equipment and active effects + * Update attributes, taking into account model's permanent effects and active effects */ updateAttributes(): void { // Reset attributes keys(this.attributes).forEach(attr => this.attributes[attr].reset()); - // Apply base skills - keys(this.skills).forEach(skill => this.attributes[skill].addModifier(this.skills[skill].get())); - // Apply attribute effects iforeach(this.ieffects(), effect => { if (effect instanceof AttributeEffect) { @@ -542,14 +336,28 @@ module TK.SpaceTac { } } + /** + * Get actions from the ship model + */ + getModelActions(): BaseAction[] { + return this.model.getActions(this.level.get(), this.level.getUpgrades()); + } + + /** + * Get permanent effects from the ship model + */ + getModelEffects(): BaseEffect[] { + return this.model.getEffects(this.level.get(), this.level.getUpgrades()); + } + /** * Iterator over all effects active for this ship. * - * This combines the permanent effects from equipment, with sticky and area effects. + * This combines the permanent effects from ship model, with sticky and area effects. */ ieffects(): Iterator { return ichain( - ichainit(imap(iarray(this.slots), slot => slot.attached ? iarray(slot.attached.effects) : IEMPTY)), + iarray(this.getModelEffects()), imap(this.active_effects.iterator(), effect => (effect instanceof StickyEffect) ? effect.base : effect) ); } @@ -558,8 +366,8 @@ module TK.SpaceTac { * Iterator over toggle actions */ iToggleActions(only_active = false): Iterator { - return >ifilter(iarray(this.getAvailableActions()), action => { - return (action instanceof ToggleAction && (action.activated || !only_active)); + return >ifilter(iarray(this.actions.listAll()), action => { + return (action instanceof ToggleAction && (!only_active || this.actions.isToggled(action))); }); } @@ -594,22 +402,16 @@ module TK.SpaceTac { } } - if (attribute in this.skills) { - let skill = this.skills[attribute]; - if (skill.get()) { - diffs.push(`Levelled up: +${skill.get()}`); - } - } - - this.slots.forEach(slot => { - if (slot.attached) { - let equipment = slot.attached; - equipment.effects.forEach(effect => addEffect(equipment.getFullName(), effect)); - } + this.getModelEffects().forEach(effect => { + addEffect(`Level ${this.level.get()} ${this.model.name}`, effect); }); this.active_effects.list().forEach(effect => { - addEffect("???", (effect instanceof StickyEffect) ? effect.base : effect) + if (effect instanceof StickyEffect) { + addEffect("Sticky effect", effect.base); + } else { + addEffect("Active effect", effect); + } }); let sources = diffs.concat(limits).join("\n"); diff --git a/src/core/ShipGenerator.spec.ts b/src/core/ShipGenerator.spec.ts index a67fa63..3f9f5db 100644 --- a/src/core/ShipGenerator.spec.ts +++ b/src/core/ShipGenerator.spec.ts @@ -2,19 +2,10 @@ module TK.SpaceTac.Specs { testing("ShipGenerator", test => { test.case("can use ship model", check => { var gen = new ShipGenerator(); - var model = new ShipModel("test", "Test", 1, 2, true, 3); - var ship = gen.generate(1, model, false); + var model = new BaseModel("test", "Test"); + var ship = gen.generate(3, model, false); check.same(ship.model, model); - check.equals(ship.cargo_space, 2); - check.equals(ship.slots.length, 7); - check.same(ship.slots[0].type, SlotType.Hull); - check.same(ship.slots[1].type, SlotType.Shield); - check.same(ship.slots[2].type, SlotType.Power); - check.same(ship.slots[3].type, SlotType.Engine); - check.same(ship.slots[4].type, SlotType.Weapon); - check.same(ship.slots[5].type, SlotType.Weapon); - check.same(ship.slots[6].type, SlotType.Weapon); - check.equals(ship.getAttribute("skill_materials"), 1); + check.same(ship.level.get(), 3); }); }); } diff --git a/src/core/ShipGenerator.ts b/src/core/ShipGenerator.ts index 83d5ab9..1681260 100644 --- a/src/core/ShipGenerator.ts +++ b/src/core/ShipGenerator.ts @@ -4,7 +4,7 @@ module TK.SpaceTac { */ export class ShipGenerator { // Random number generator used - random: RandomGenerator; + random: RandomGenerator constructor(random = RandomGenerator.global) { this.random = random; @@ -13,47 +13,35 @@ module TK.SpaceTac { /** * Generate a ship of a givel level. * - * If *upgrade* is true, the ship's upgrade points will be randomly spent before chosing equipment - * - * If *force_damage_equipment, at least one "damaging" weapon will be chosen + * If *upgrade* is true, random levelling options will be chosen */ - generate(level: number, model: ShipModel | null = null, upgrade = true, force_damage_equipment = true): Ship { + generate(level: number, model: BaseModel | null = null, upgrade = true): Ship { if (!model) { // Get a random model - model = ShipModel.getRandomModel(level, this.random); + model = BaseModel.getRandomModel(level, this.random); } - let result = new Ship(null, undefined, model); - let loot = new LootGenerator(this.random); + let result = new Ship(null, null, model); - // Set all skills to 1 (to be able to use at least basic equipment) - keys(result.skills).forEach(skill => result.upgradeSkill(skill)); - - // Level upgrade result.level.forceLevel(level); if (upgrade) { - while (result.getAvailableUpgradePoints() > 0) { - result.upgradeSkill(this.random.choice(keys(SHIP_SKILLS))); - } - } + let iteration = 0; + while (iteration < 100) { + iteration += 1; - // Fill equipment slots - result.slots.forEach(slot => { - if (slot.type == SlotType.Weapon && force_damage_equipment) { - loot.setTemplateFilter(template => template.hasDamageEffect()); - force_damage_equipment = false; - } + let points = result.getAvailableUpgradePoints(); + let upgrades = model.getAvailableUpgrades(result.level.get()).filter(upgrade => { + return (upgrade.cost || 0) <= points && !result.level.hasUpgrade(upgrade); + }); - let equipment = loot.generateHighest(result.skills, EquipmentQuality.COMMON, slot.type); - if (equipment) { - slot.attach(equipment) - if (slot.attached !== equipment) { - console.error("Cannot attach generated equipment to slot", equipment, slot); + if (upgrades.length > 0) { + let upgrade = this.random.choice(upgrades); + result.activateUpgrade(upgrade, true); + } else { + break; } } - - loot.setTemplateFilter(() => true); - }); + } return result; } diff --git a/src/core/ShipLevel.spec.ts b/src/core/ShipLevel.spec.ts index f115c40..364283d 100644 --- a/src/core/ShipLevel.spec.ts +++ b/src/core/ShipLevel.spec.ts @@ -4,7 +4,7 @@ module TK.SpaceTac.Specs { let level = new ShipLevel(); check.equals(level.get(), 1); check.equals(level.getNextGoal(), 100); - check.equals(level.getSkillPoints(), 10); + check.equals(level.getUpgradePoints(), 0); level.addExperience(60); // 60 check.equals(level.get(), 1); @@ -15,21 +15,21 @@ module TK.SpaceTac.Specs { check.equals(level.checkLevelUp(), true); check.equals(level.get(), 2); check.equals(level.getNextGoal(), 300); - check.equals(level.getSkillPoints(), 15); + check.equals(level.getUpgradePoints(), 6); level.addExperience(200); // 330 check.equals(level.get(), 2); check.equals(level.checkLevelUp(), true); check.equals(level.get(), 3); check.equals(level.getNextGoal(), 600); - check.equals(level.getSkillPoints(), 20); + check.equals(level.getUpgradePoints(), 9); level.addExperience(320); // 650 check.equals(level.get(), 3); check.equals(level.checkLevelUp(), true); check.equals(level.get(), 4); check.equals(level.getNextGoal(), 1000); - check.equals(level.getSkillPoints(), 25); + check.equals(level.getUpgradePoints(), 12); }); test.case("forces a given level", check => { @@ -39,24 +39,5 @@ module TK.SpaceTac.Specs { level.forceLevel(10); check.equals(level.get(), 10); }); - - test.case("computes needed level for given points", check => { - check.equals(ShipLevel.getLevelForPoints(0), 1); - check.equals(ShipLevel.getLevelForPoints(1), 1); - check.equals(ShipLevel.getLevelForPoints(2), 1); - check.equals(ShipLevel.getLevelForPoints(5), 1); - check.equals(ShipLevel.getLevelForPoints(10), 1); - check.equals(ShipLevel.getLevelForPoints(11), 2); - check.equals(ShipLevel.getLevelForPoints(15), 2); - check.equals(ShipLevel.getLevelForPoints(16), 3); - check.equals(ShipLevel.getLevelForPoints(526), 105); - }); - - test.case("computes needed points for given level", check => { - check.equals(ShipLevel.getPointsForLevel(1), 10); - check.equals(ShipLevel.getPointsForLevel(2), 15); - check.equals(ShipLevel.getPointsForLevel(3), 20); - check.equals(ShipLevel.getPointsForLevel(105), 530); - }); }); } diff --git a/src/core/ShipLevel.ts b/src/core/ShipLevel.ts index 5896534..23d04cc 100644 --- a/src/core/ShipLevel.ts +++ b/src/core/ShipLevel.ts @@ -1,10 +1,11 @@ module TK.SpaceTac { /** - * Level and experience system for a ship. + * Level and experience system for a ship, with enabled upgrades. */ export class ShipLevel { - private level = 1; - private experience = 0; + private level = 1 + private experience = 0 + private upgrades: string[] = [] /** * Get current level @@ -20,6 +21,13 @@ module TK.SpaceTac { return this.experience; } + /** + * Get the activated upgrades + */ + getUpgrades(): string[] { + return acopy(this.upgrades); + } + /** * Get the next experience goal to reach, to gain one level */ @@ -73,30 +81,32 @@ module TK.SpaceTac { } /** - * Get skill points given by current level + * Get upgrade points given by current level + * + * This does not deduce activated upgrades usage */ - getSkillPoints(): number { - return 5 + 5 * this.level; + getUpgradePoints(): number { + return this.level > 1 ? (3 * this.level) : 0; } /** - * Get the level needed to have a given total of points + * (De)Activate an upgrade + * + * This does not check the upgrade points needed */ - static getLevelForPoints(points: number): number { - let lev = new ShipLevel(); - while (lev.getSkillPoints() < points) { - lev.forceLevelUp(); + activateUpgrade(upgrade: ModelUpgrade, active: boolean): void { + if (active) { + add(this.upgrades, upgrade.code); + } else { + remove(this.upgrades, upgrade.code); } - return lev.level; } /** - * Get the points available at a given level + * Check if an upgrade is active */ - static getPointsForLevel(level: number): number { - let lev = new ShipLevel(); - lev.forceLevel(level); - return lev.getSkillPoints(); + hasUpgrade(upgrade: ModelUpgrade): boolean { + return contains(this.upgrades, upgrade.code); } } } diff --git a/src/core/ShipModel.spec.ts b/src/core/ShipModel.spec.ts deleted file mode 100644 index 55e5b8c..0000000 --- a/src/core/ShipModel.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -module TK.SpaceTac.Specs { - testing("ShipModel", test => { - test.case("picks random models from default collection", check => { - check.patch(console, "error", null); - check.patch(ShipModel, "getDefaultCollection", iterator([ - [new ShipModel("a")], - [], - [new ShipModel("a"), new ShipModel("b")], - [new ShipModel("a")], - [], - ])); - - check.equals(ShipModel.getRandomModel(), new ShipModel("a"), "pick from a one-item list"); - check.equals(ShipModel.getRandomModel(), new ShipModel(), "pick from an empty list"); - - check.equals(sorted(ShipModel.getRandomModels(2), (a, b) => cmp(a.code, b.code)), [new ShipModel("a"), new ShipModel("b")], "sample from good-sized list"); - check.equals(ShipModel.getRandomModels(2), [new ShipModel("a"), new ShipModel("a")], "sample from too small list"); - check.equals(ShipModel.getRandomModels(2), [new ShipModel(), new ShipModel()], "sample from empty list"); - }); - }); -} diff --git a/src/core/ShipModel.ts b/src/core/ShipModel.ts deleted file mode 100644 index a67ae86..0000000 --- a/src/core/ShipModel.ts +++ /dev/null @@ -1,91 +0,0 @@ -module TK.SpaceTac { - // A model of ship - // It defines the ship looks, and available slots for equipment - export class ShipModel { - // Code to identify the model - code: string; - - // Human-readable model name - name: string; - - // Minimal level to use this model - level: number; - - // Cargo space - cargo: number; - - // Available slots - slots: SlotType[]; - - constructor(code = "unknown", name = "Unknown", level = 1, cargo = 6, default_slots = true, weapon_slots = 2) { - this.code = code; - this.name = name; - this.level = level; - this.cargo = cargo; - this.slots = default_slots ? [SlotType.Hull, SlotType.Shield, SlotType.Power, SlotType.Engine] : []; - range(weapon_slots).forEach(() => this.slots.push(SlotType.Weapon)); - } - - // Get the default ship model collection available in-game - static getDefaultCollection(): ShipModel[] { - // TODO Store in cache - var result: ShipModel[] = []; - - result.push(new ShipModel("breeze", "Breeze")); - result.push(new ShipModel("creeper", "Creeper")); - result.push(new ShipModel("tomahawk", "Tomahawk")); - result.push(new ShipModel("avenger", "Avenger")); - result.push(new ShipModel("commodore", "Commodore")); - result.push(new ShipModel("falcon", "Falcon")); - result.push(new ShipModel("flea", "Flea")); - result.push(new ShipModel("jumper", "Jumper")); - result.push(new ShipModel("rhino", "Rhino")); - result.push(new ShipModel("trapper", "Trapper")); - result.push(new ShipModel("xander", "Xander")); - - return result; - } - - /** - * Pick a random model in the default collection - */ - static getRandomModel(level?: number, random = RandomGenerator.global): ShipModel { - let collection = this.getDefaultCollection(); - if (level) { - collection = collection.filter(model => model.level <= level); - } - - if (collection.length == 0) { - console.error("Couldn't pick a random model"); - return new ShipModel(); - } else { - return random.choice(collection); - } - } - - /** - * Pick random models in the default collection - * - * At first it tries to pick unique models, then fill with duplicates - */ - static getRandomModels(count: number, level?: number, random = RandomGenerator.global): ShipModel[] { - let collection = this.getDefaultCollection(); - if (level) { - collection = collection.filter(model => model.level <= level); - } - - if (collection.length == 0) { - console.error("Couldn't pick a random model"); - return range(count).map(() => new ShipModel()); - } else { - let result: ShipModel[] = []; - while (count > 0) { - let picked = random.sample(collection, Math.min(count, collection.length)); - result = result.concat(picked); - count -= picked.length; - } - return result; - } - } - } -} diff --git a/src/core/ShipValue.ts b/src/core/ShipValue.ts index 34a6465..26d4a14 100644 --- a/src/core/ShipValue.ts +++ b/src/core/ShipValue.ts @@ -7,16 +7,9 @@ module TK.SpaceTac { "hull": "Physical structure of the ship", "shield": "Shield around the ship that may absorb damage", "power": "Power available to supply the equipments", - "skill_materials": "Usage of physical materials such as bullets, shells...", - "skill_photons": "Forces of light, and electromagnetic radiation", - "skill_antimatter": "Manipulation of matter and antimatter particles", - "skill_quantum": "Application of quantum uncertainty principle", - "skill_gravity": "Interaction with gravitational forces", - "skill_time": "Control of relativity's time properties", "hull_capacity": "Maximal Hull value before the ship risks collapsing", "shield_capacity": "Maximal Shield value to protect the hull from damage", "power_capacity": "Maximal Power value to use equipment", - "power_generation": "Power generated at the end of the ship's turn", "maneuvrability": "Ability to move first, fast and to evade weapons", "precision": "Ability to target far and aim good", } @@ -25,16 +18,9 @@ module TK.SpaceTac { "hull": "hull", "shield": "shield", "power": "power", - "skill_materials": "materials skill", - "skill_photons": "photons skill", - "skill_antimatter": "antimatter skill", - "skill_quantum": "quantum skill", - "skill_gravity": "gravity skill", - "skill_time": "time skill", "hull_capacity": "hull capacity", "shield_capacity": "shield capacity", "power_capacity": "power capacity", - "power_generation": "power generation", "maneuvrability": "maneuvrability", "precision": "precision", } @@ -126,31 +112,16 @@ module TK.SpaceTac { } } - /** - * Set of upgradable skills for a ship - */ - export class ShipSkills { - // Skills - skill_materials = new ShipAttribute() - skill_photons = new ShipAttribute() - skill_antimatter = new ShipAttribute() - skill_quantum = new ShipAttribute() - skill_gravity = new ShipAttribute() - skill_time = new ShipAttribute() - } - /** * Set of ShipAttribute for a ship */ - export class ShipAttributes extends ShipSkills { + export class ShipAttributes { // Maximal hull value hull_capacity = new ShipAttribute() // Maximal shield value shield_capacity = new ShipAttribute() // Maximal power value power_capacity = new ShipAttribute() - // Power value recovered each turn - power_generation = new ShipAttribute() // Ability to move first and fast maneuvrability = new ShipAttribute() // Ability to fire far and good @@ -169,7 +140,6 @@ module TK.SpaceTac { /** * Static attributes and values object for property queries */ - export const SHIP_SKILLS = new ShipSkills(); export const SHIP_ATTRIBUTES = new ShipAttributes(); export const SHIP_VALUES = new ShipValues(); diff --git a/src/core/Shop.spec.ts b/src/core/Shop.spec.ts index 7cfdd27..b773b8d 100644 --- a/src/core/Shop.spec.ts +++ b/src/core/Shop.spec.ts @@ -1,36 +1,5 @@ module TK.SpaceTac.Specs { testing("Shop", test => { - test.case("generates a stock", check => { - let shop = new Shop(); - check.equals((shop).stock.length, 0); - check.greater(shop.getStock().length, 20); - }); - - test.case("buys and sells items", check => { - let equ1 = new Equipment(SlotType.Shield, "shield"); - equ1.price = 50; - let equ2 = new Equipment(SlotType.Hull, "hull"); - equ2.price = 150; - let shop = new Shop(1, [equ1, equ2], 0); - let fleet = new Fleet(); - fleet.credits = 1000; - check.patch(shop, "getPrice", () => 800); - - let result = shop.sellToFleet(equ1, fleet); - check.equals(result, true); - check.equals(shop.getStock(), [equ2]); - check.equals(fleet.credits, 200); - result = shop.sellToFleet(equ2, fleet); - check.equals(result, false); - check.equals(shop.getStock(), [equ2]); - check.equals(fleet.credits, 200); - - result = shop.buyFromFleet(equ1, fleet); - check.equals(result, true); - check.equals(shop.getStock(), [equ1, equ2]); - check.equals(fleet.credits, 1000); - }); - test.case("generates secondary missions", check => { let universe = new Universe(); universe.generate(4); diff --git a/src/core/Shop.ts b/src/core/Shop.ts index 44cdb53..b515876 100644 --- a/src/core/Shop.ts +++ b/src/core/Shop.ts @@ -1,6 +1,4 @@ module TK.SpaceTac { - type ShopStockCallback = (stock: Equipment[]) => Equipment[] - /** * A shop is a place to buy/sell equipments */ @@ -8,109 +6,15 @@ module TK.SpaceTac { // Average level of equipment private level: number - // Approximative number of equipments - private count: number - - // Equipment in stock - private stock: Equipment[] - // Random generator private random: RandomGenerator // Available missions private missions: Mission[] = [] - // Callback when the equipment changes - private onchange: ShopStockCallback - - constructor(level = 1, stock: Equipment[] = [], count = 40, onchange?: ShopStockCallback) { + constructor(level = 1) { this.level = level; - this.stock = stock; - this.count = count; this.random = new RandomGenerator(); - this.onchange = onchange || (stock => stock); - } - - postUnserialize() { - // functions are not serializable - this.onchange = (stock => stock); - } - - /** - * Get available stock to display (sorted by level then price by default) - */ - getStock() { - if (this.stock.length < this.count * 0.5) { - let count = this.random.randInt(Math.floor(this.count * 0.8), Math.ceil(this.count * 1.2)); - this.stock = this.stock.concat(this.generateStock(count - this.stock.length, this.level, this.random)); - } - - return sorted(this.stock, (a, b) => (a.level == b.level) ? cmp(a.getPrice(), b.getPrice()) : cmp(a.level, b.level)); - } - - /** - * Generate a random stock - * - * *level* is the preferential level, but equipment around it may be generated - */ - private generateStock(items: number, level: number, random = RandomGenerator.global): Equipment[] { - let generator = new LootGenerator(random); - - return nna(range(items).map(() => { - let equlevel = random.weighted(range(level + 3).map(i => i + 1).map(i => (i > level) ? 1 : i)) + 1; - let quality = random.weighted([1, 7, 2]); - return generator.generate(equlevel, quality); - })); - } - - /** - * Update the stock after a buying or selling occured - */ - refreshStock() { - this.stock = this.onchange(this.stock); - } - - /** - * Get the buy/sell price for an equipment - */ - getPrice(equipment: Equipment): number { - return equipment.getPrice(); - } - - /** - * A fleet buys an item - * - * This does not put the item anywhere on the fleet, only remove the item from stock, and make the payment - */ - sellToFleet(equipment: Equipment, fleet: Fleet) { - let price = this.getPrice(equipment); - if (price <= fleet.credits) { - if (remove(this.stock, equipment)) { - this.refreshStock(); - fleet.credits -= price; - return true; - } else { - return false; - } - } else { - return false; - } - } - - /** - * A fleet sells an item - * - * This does not check if the item is anywhere on the fleet, only add the item to the shop stock, and make the payment - */ - buyFromFleet(equipment: Equipment, fleet: Fleet) { - let price = this.getPrice(equipment); - if (add(this.stock, equipment)) { - this.refreshStock(); - fleet.credits += price; - return true; - } else { - return false; - } } /** diff --git a/src/core/Slot.spec.ts b/src/core/Slot.spec.ts deleted file mode 100644 index deb5b74..0000000 --- a/src/core/Slot.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -module TK.SpaceTac.Specs { - testing("Slot", test => { - test.case("checks equipment type", check => { - check.patch(console, "warn", null); - - var ship = new Ship(); - var slot = ship.addSlot(SlotType.Engine); - - var equipment = new Equipment(); - equipment.slot_type = SlotType.Weapon; - - check.equals(slot.attached, null); - slot.attach(equipment); - check.equals(slot.attached, null); - - equipment.slot_type = SlotType.Engine; - - slot.attach(equipment); - check.same(slot.attached, equipment); - }); - - test.case("checks equipment capabilities", check => { - check.patch(console, "warn", null); - - var ship = new Ship(); - var slot = ship.addSlot(SlotType.Shield); - - var equipment = new Equipment(); - equipment.slot_type = SlotType.Shield; - equipment.requirements["skill_gravity"] = 5; - - check.equals(slot.attached, null); - slot.attach(equipment); - check.equals(slot.attached, null); - - TestTools.setAttribute(ship, "skill_gravity", 6); - - slot.attach(equipment); - check.same(slot.attached, equipment); - }); - }); -} diff --git a/src/core/Slot.ts b/src/core/Slot.ts deleted file mode 100644 index cedc82f..0000000 --- a/src/core/Slot.ts +++ /dev/null @@ -1,45 +0,0 @@ -module TK.SpaceTac { - // Types of slots - export enum SlotType { - Hull, - Shield, - Power, - Engine, - Weapon - } - - // Slot to attach an equipment to a ship - export class Slot { - // Link to the ship - ship: Ship; - - // Type of slot - type: SlotType; - - // Currently attached equipment, null if none - attached: Equipment | null; - - // Create an empty slot for a ship - constructor(ship: Ship, type: SlotType) { - this.ship = ship; - this.type = type; - this.attached = null; - } - - // Attach an equipment in this slot - attach(equipment: Equipment): Equipment { - if (this.type === equipment.slot_type && equipment.canBeEquipped(this.ship.attributes)) { - this.attached = equipment; - equipment.attached_to = this; - - if (this.ship) { - this.ship.updateAttributes(); - } - } else { - console.warn("Equipment cannot be attached to slot", equipment, this); - } - return equipment; - } - } - -} diff --git a/src/core/TestTools.spec.ts b/src/core/TestTools.spec.ts index 3ff61f9..21b6680 100644 --- a/src/core/TestTools.spec.ts +++ b/src/core/TestTools.spec.ts @@ -1,33 +1,23 @@ module TK.SpaceTac.Specs { testing("TestTools", test => { - test.case("set ship power", check => { - let ship = new Ship(); - - check.equals(ship.getAttribute("power_capacity"), 0); - check.equals(ship.getAttribute("power_generation"), 0); - check.equals(ship.getValue("power"), 0); - - TestTools.setShipAP(ship, 12, 4); - - check.equals(ship.getAttribute("power_capacity"), 12); - check.equals(ship.getAttribute("power_generation"), 4); - check.equals(ship.getValue("power"), 12); - }); - - test.case("set ship health", check => { + test.case("set ship health and power", check => { let ship = new Ship(); check.equals(ship.getAttribute("hull_capacity"), 0); check.equals(ship.getAttribute("shield_capacity"), 0); + check.equals(ship.getAttribute("power_capacity"), 0); check.equals(ship.getValue("hull"), 0); check.equals(ship.getValue("shield"), 0); + check.equals(ship.getValue("power"), 0); - TestTools.setShipHP(ship, 100, 200); + TestTools.setShipModel(ship, 100, 200, 12); check.equals(ship.getAttribute("hull_capacity"), 100); check.equals(ship.getAttribute("shield_capacity"), 200); + check.equals(ship.getAttribute("power_capacity"), 12); check.equals(ship.getValue("hull"), 100); check.equals(ship.getValue("shield"), 200); + check.equals(ship.getValue("power"), 12); }); }); } \ No newline at end of file diff --git a/src/core/TestTools.ts b/src/core/TestTools.ts index 158d95d..77fd0c8 100644 --- a/src/core/TestTools.ts +++ b/src/core/TestTools.ts @@ -15,100 +15,72 @@ module TK.SpaceTac { } var battle = new Battle(fleet1, fleet2); - battle.ships.list().forEach(ship => TestTools.setShipHP(ship, 1, 0)); + battle.ships.list().forEach(ship => TestTools.setShipModel(ship, 1, 0)); battle.play_order = fleet1.ships.concat(fleet2.ships); battle.setPlayingShip(battle.play_order[0]); return battle; } - // Get or add an equipment of a given slot type - static getOrGenEquipment(ship: Ship, slot: SlotType, template: LootTemplate, force_generate = false): Equipment { - var equipped = ship.listEquipment(slot); - var equipment: Equipment; - if (force_generate || equipped.length === 0) { - equipment = template.generate(1); - equipment.requirements = {}; - ship.addSlot(slot).attach(equipment); - } else { - equipment = equipped[0]; - } - - return equipment; - } - /** * Add an engine, allowing a ship to move *distance*, for each action points */ - static addEngine(ship: Ship, distance: number): Equipment { - let equipment = ship.addSlot(SlotType.Engine).attach(new Equipment(SlotType.Engine)); - equipment.action = new MoveAction(equipment, distance); - return equipment; + static addEngine(ship: Ship, distance: number): MoveAction { + let action = new MoveAction("Engine", { distance_per_power: distance }); + ship.actions.addCustom(action); + return action; } /** * Add a weapon to a ship */ - static addWeapon(ship: Ship, damage = 100, power_usage = 1, max_distance = 100, blast = 0, angle = 0): Equipment { - var equipment = ship.addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon)); - equipment.action = new TriggerAction(equipment, [new DamageEffect(damage)], power_usage, max_distance, blast, angle); - return equipment; + static addWeapon(ship: Ship, damage = 100, power_usage = 1, max_distance = 100, blast = 0, angle = 0): TriggerAction { + let action = new TriggerAction("Weapon", { + effects: [new DamageEffect(damage)], + power: power_usage, + range: max_distance, + blast: blast, + angle: angle, + }); + ship.actions.addCustom(action); + return action; } - // Set the current playing ship + /** + * Force the current playing ship + */ static setShipPlaying(battle: Battle, ship: Ship): void { add(battle.play_order, ship); battle.play_index = battle.play_order.indexOf(ship); ship.playing = true; } - // Set a ship action points, adding/updating an equipment if needed - static setShipAP(ship: Ship, points: number, recovery: number = 0): Equipment { - var equipment = this.getOrGenEquipment(ship, SlotType.Power, new Equipments.NuclearReactor()); + /** + * Set a ship attributes (by changing its model) + */ + static setShipModel(ship: Ship, hull: number, shield = 0, power = 0, level = 1, actions: BaseAction[] = [], effects: BaseEffect[] = []) { + let model = new BaseModel(); + ship.level.forceLevel(level); + ship.model = model; - equipment.effects.forEach(effect => { - if (effect instanceof AttributeEffect) { - if (effect.attrcode === "power_capacity") { - effect.value = points; - } else if (effect.attrcode === "power_generation") { - effect.value = recovery; - } - } - }); + // TODO Use a BaseModel subclass would be prettier + model.getActions = () => actions; + model.getEffects = () => effects.concat([ + new AttributeEffect("hull_capacity", hull), + new AttributeEffect("shield_capacity", shield), + new AttributeEffect("power_capacity", power), + ]); - ship.updateAttributes(); - ship.setValue("power", points); - - return equipment; - } - - // Set a ship hull and shield points, adding/updating an equipment if needed - static setShipHP(ship: Ship, hull_points: number, shield_points: number): [Equipment, Equipment] { - var hull = TestTools.getOrGenEquipment(ship, SlotType.Hull, new Equipments.IronHull()); - var shield = TestTools.getOrGenEquipment(ship, SlotType.Shield, new Equipments.ForceField()); - - hull.effects.forEach(effect => { - if (effect instanceof AttributeEffect) { - if (effect.attrcode === "hull_capacity") { - effect.value = hull_points; - } - } - }); - shield.effects.forEach((effect: BaseEffect) => { - if (effect instanceof AttributeEffect) { - if (effect.attrcode === "shield_capacity") { - effect.value = shield_points; - } - } - }); + ship.actions.updateFromShip(ship); ship.updateAttributes(); ship.restoreHealth(); - - return [hull, shield]; + ship.setValue("power", power); } /** * Force a ship attribute to a given value + * + * Beware that a call to ship.updateAttributes() may cancel this */ static setAttribute(ship: Ship, name: keyof ShipAttributes, value: number): void { let attr = ship.attributes[name]; @@ -157,4 +129,60 @@ module TK.SpaceTac { } } } + + function strip(obj: T, attr: keyof T): any { + let result: any = {}; + copyfields(obj, result); + delete result[attr]; + return result; + } + + function strip_id(effect: RObject): any { + if (effect instanceof StickyEffect) { + let result = strip(effect, "id"); + result.base = strip_id(result.base); + return result; + } else { + return strip(effect, "id"); + } + } + + export function compare_effects(check: TestContext, effects1: BaseEffect[], effects2: BaseEffect[]): void { + check.equals(effects1.map(strip_id), effects2.map(strip_id), "effects"); + } + + export function compare_action(check: TestContext, action1: BaseAction | null, action2: BaseAction | null): void { + if (action1 === null || action2 === null) { + check.equals(action1, action2, "action"); + } else { + check.equals(strip_id(action1), strip_id(action2), "action"); + } + } + + export function compare_trigger_action(check: TestContext, action1: BaseAction | null, action2: TriggerAction | null): void { + if (action1 === null || action2 === null || !(action1 instanceof TriggerAction)) { + check.equals(action1, action2, "action"); + } else { + check.equals(strip_id(strip(action1, "effects")), strip_id(strip(action2, "effects")), "action"); + compare_effects(check, action1.effects, action2.effects); + } + } + + export function compare_toggle_action(check: TestContext, action1: BaseAction | null, action2: ToggleAction | null): void { + if (action1 === null || action2 === null || !(action1 instanceof ToggleAction)) { + check.equals(action1, action2, "action"); + } else { + check.equals(strip_id(strip(action1, "effects")), strip_id(strip(action2, "effects")), "action"); + compare_effects(check, action1.effects, action2.effects); + } + } + + export function compare_drone_action(check: TestContext, action1: BaseAction | null, action2: DeployDroneAction | null): void { + if (action1 === null || action2 === null || !(action1 instanceof DeployDroneAction)) { + check.equals(action1, action2, "action"); + } else { + check.equals(strip_id(strip(action1, "drone_effects")), strip_id(strip(action2, "drone_effects")), "action"); + compare_effects(check, action1.drone_effects, action2.drone_effects); + } + } } diff --git a/src/core/actions/ActionList.spec.ts b/src/core/actions/ActionList.spec.ts new file mode 100644 index 0000000..5f43614 --- /dev/null +++ b/src/core/actions/ActionList.spec.ts @@ -0,0 +1,45 @@ +module TK.SpaceTac.Specs { + testing("ActionList", test => { + test.case("lists actions from ship", check => { + let actions = new ActionList(); + check.equals(actions.listAll(), [EndTurnAction.SINGLETON]); + + let model = new BaseModel(); + let ship = new Ship(null, null, model); + actions.updateFromShip(ship); + check.equals(actions.listAll(), [EndTurnAction.SINGLETON]); + + let action1 = new BaseAction("test1"); + let action2 = new BaseAction("test2"); + let mock = check.patch(model, "getActions", () => [action1, action2]); + ship.level.forceLevel(3); + actions.updateFromShip(ship); + check.equals(actions.listAll(), [action1, action2, EndTurnAction.SINGLETON]); + check.called(mock, [[3, []]]); + + let up1: ModelUpgrade = { code: "up1" }; + let up2: ModelUpgrade = { code: "up2" }; + check.patch(model, "getLevelUpgrades", () => [up1, up2]); + ship.level.activateUpgrade(up1, true); + actions.updateFromShip(ship); + check.equals(actions.listAll(), [action1, action2, EndTurnAction.SINGLETON]); + check.called(mock, [[3, ["up1"]]]); + }) + + test.case("lists toggled actions", check => { + let actions = new ActionList(); + check.equals(actions.listToggled(), [], "init"); + + let action1 = new ToggleAction("test1"); + let action2 = new ToggleAction("test2"); + (actions).from_model = [new BaseAction(), action1, action2]; + check.equals(actions.listToggled(), [], "actions added but not toggled"); + + actions.toggle(action1, true); + check.equals(actions.listToggled(), [action1], "action1 is toggled"); + + actions.toggle(new ToggleAction("test3"), true); + check.equals(actions.listToggled(), [action1], "action3 cannot be toggled"); + }) + }) +} diff --git a/src/core/actions/ActionList.ts b/src/core/actions/ActionList.ts new file mode 100644 index 0000000..52a5879 --- /dev/null +++ b/src/core/actions/ActionList.ts @@ -0,0 +1,132 @@ +module TK.SpaceTac { + /** + * List of actions, that may be used by a ship to keep track of available actions + * + * This manages usage count, toggles and cooldown ... + */ + export class ActionList { + // Available actions + private from_model: BaseAction[] = [] + private custom: BaseAction[] = [] + + // Toggled actions + private toggled = new RObjectContainer() + + // Active cooldowns + private cooldowns: { [action: number]: Cooldown } = {} + + /** + * Add a custom action + */ + addCustom(action: T): T { + add(this.custom, action); + return action; + } + + /** + * List all actions + */ + listAll(): BaseAction[] { + return this.from_model.concat(this.custom).concat([EndTurnAction.SINGLETON]); + } + + /** + * List all currently toggled actions + */ + listToggled(): ToggleAction[] { + let result: ToggleAction[] = []; + + this.listAll().forEach(action => { + if (action instanceof ToggleAction && this.isToggled(action)) { + result.push(action); + } + }); + + return result; + } + + /** + * List all currently overheated actions + */ + listOverheated(): BaseAction[] { + return this.listAll().filter(action => this.getCooldown(action).heat > 0); + } + + /** + * Get an action by its ID + */ + getById(action_id: RObjectId): BaseAction | null { + return first(this.listAll(), action => action.is(action_id)); + } + + /** + * Check if a toggle action is currently active + */ + isToggled(action: ToggleAction): boolean { + return this.toggled.get(action.id) != null; + } + + /** + * Toggle the status of an action + */ + toggle(action: ToggleAction, active: boolean): boolean { + if (this.getById(action.id)) { + if (active) { + this.toggled.add(action); + } else { + this.toggled.remove(action); + } + } + + return this.toggled.get(action.id) != null; + } + + /** + * Get the cooldown associated with an action + */ + getCooldown(action: BaseAction): Cooldown { + if (this.getById(action.id)) { + if (typeof this.cooldowns[action.id] == "undefined") { + this.cooldowns[action.id] = copy(action.getCooldown()); + } + return this.cooldowns[action.id]; + } else { + console.warn("Action not found, fake cooldown returned", action, this); + return new Cooldown(); + } + } + + /** + * Store an usage count for an action + */ + storeUsage(action: BaseAction, usage = 1): void { + this.getCooldown(action).use(usage); + } + + /** + * Check if an action may be used (in regards to cooldown) + * + * This does not take power into account + */ + isUsable(action: BaseAction): boolean { + if (this.getById(action.id)) { + return this.getCooldown(action).canUse(); + } else { + return false; + } + } + + /** + * Update the actions from a ship. + * + * Beware that this will change the actions IDs. It should typically be done at a battle start. + * This will reset cooldown, toggles and custom actions. + */ + updateFromShip(ship: Ship) { + this.from_model = ship.getModelActions(); + this.toggled = new RObjectContainer(); + this.cooldowns = {}; + this.custom = []; + } + } +} diff --git a/src/core/actions/BaseAction.spec.ts b/src/core/actions/BaseAction.spec.ts index 9574f0a..e6dc0b0 100644 --- a/src/core/actions/BaseAction.spec.ts +++ b/src/core/actions/BaseAction.spec.ts @@ -3,10 +3,9 @@ module TK.SpaceTac.Specs { test.case("may be applied and reverted", check => { let battle = TestTools.createBattle(); let ship = nn(battle.playing_ship); - TestTools.setShipAP(ship, 10, 4); - let equipment = TestTools.addWeapon(ship, 0, 3, 100, 50); - let action = nn(equipment.action); - action.cooldown.configure(2, 1); + TestTools.setShipModel(ship, 100, 0, 10); + let action = TestTools.addWeapon(ship, 0, 3, 100, 50); + action.configureCooldown(2, 1); TestTools.actionChain(check, battle, [ [ship, action, Target.newFromLocation(0, 0)], @@ -15,110 +14,82 @@ module TK.SpaceTac.Specs { ], [ check => { check.equals(ship.getValue("power"), 10, "power"); - check.equals(action.cooldown.uses, 0, "uses"); - check.equals(action.cooldown.heat, 0, "heat"); + let cooldown = ship.actions.getCooldown(action); + check.equals(cooldown.uses, 0, "uses"); + check.equals(cooldown.heat, 0, "heat"); }, check => { check.equals(ship.getValue("power"), 7, "power"); - check.equals(action.cooldown.uses, 1, "uses"); - check.equals(action.cooldown.heat, 0, "heat"); + let cooldown = ship.actions.getCooldown(action); + check.equals(cooldown.uses, 1, "uses"); + check.equals(cooldown.heat, 0, "heat"); }, check => { check.equals(ship.getValue("power"), 4, "power"); - check.equals(action.cooldown.uses, 2, "uses"); - check.equals(action.cooldown.heat, 1, "heat"); + let cooldown = ship.actions.getCooldown(action); + check.equals(cooldown.uses, 2, "uses"); + check.equals(cooldown.heat, 1, "heat"); }, check => { - check.equals(ship.getValue("power"), 8, "power"); - check.equals(action.cooldown.uses, 0, "uses"); - check.equals(action.cooldown.heat, 0, "heat"); + check.equals(ship.getValue("power"), 10, "power"); + let cooldown = ship.actions.getCooldown(action); + check.equals(cooldown.uses, 0, "uses"); + check.equals(cooldown.heat, 0, "heat"); }, ]); }) - test.case("checks if equipment can be used with remaining AP", check => { - var equipment = new Equipment(SlotType.Hull); - var action = new BaseAction("test", equipment); + test.case("checks against remaining AP", check => { + let action = new BaseAction("test"); check.patch(action, "getActionPointsUsage", () => 3); - var ship = new Ship(); - ship.addSlot(SlotType.Hull).attach(equipment); + let ship = new Ship(); + check.equals(action.checkCannotBeApplied(ship), "action not available"); + + ship.actions.addCustom(action); check.equals(action.checkCannotBeApplied(ship), "not enough power"); ship.setValue("power", 5); - check.equals(action.checkCannotBeApplied(ship), null); check.equals(action.checkCannotBeApplied(ship, 4), null); check.equals(action.checkCannotBeApplied(ship, 3), null); check.equals(action.checkCannotBeApplied(ship, 2), "not enough power"); ship.setValue("power", 3); - check.equals(action.checkCannotBeApplied(ship), null); ship.setValue("power", 2); - check.equals(action.checkCannotBeApplied(ship), "not enough power"); }) - test.case("checks if equipment can be used with overheat", check => { - let equipment = new Equipment(); - let action = new BaseAction("test", equipment); + test.case("checks against overheat", check => { + let action = new BaseAction("test"); let ship = new Ship(); + ship.actions.addCustom(action); + let cooldown = ship.actions.getCooldown(action); check.equals(action.checkCannotBeApplied(ship), null); - check.same(action.getUsesBeforeOverheat(), Infinity); - equipment.cooldown.use(); + cooldown.use(); check.equals(action.checkCannotBeApplied(ship), null); - check.same(action.getUsesBeforeOverheat(), Infinity); - equipment.cooldown.configure(2, 3); + cooldown.configure(2, 3); check.equals(action.checkCannotBeApplied(ship), null); - check.equals(action.getUsesBeforeOverheat(), 2); - equipment.cooldown.use(); + cooldown.use(); check.equals(action.checkCannotBeApplied(ship), null); - check.equals(action.getUsesBeforeOverheat(), 1); - check.equals(action.getCooldownDuration(), 0); - equipment.cooldown.use(); + cooldown.use(); check.equals(action.checkCannotBeApplied(ship), "overheated"); - check.equals(action.getUsesBeforeOverheat(), 0); - check.equals(action.getCooldownDuration(), 3); - equipment.cooldown.cool(); + cooldown.cool(); check.equals(action.checkCannotBeApplied(ship), "overheated"); - check.equals(action.getCooldownDuration(), 2); - equipment.cooldown.cool(); + cooldown.cool(); check.equals(action.checkCannotBeApplied(ship), "overheated"); - check.equals(action.getCooldownDuration(), 1); - equipment.cooldown.cool(); + cooldown.cool(); check.equals(action.checkCannotBeApplied(ship), null); - check.equals(action.getCooldownDuration(), 0); - check.equals(action.getCooldownDuration(true), 3); - }) - - test.case("wears down equipment and power generators", check => { - let battle = TestTools.createBattle(); - let ship = battle.play_order[0]; - TestTools.setShipAP(ship, 10); - let power = ship.listEquipment(SlotType.Power)[0]; - let equipment = new Equipment(SlotType.Weapon); - let action = new BaseAction("test", equipment); - equipment.action = action; - ship.addSlot(SlotType.Weapon).attach(equipment); - - check.patch(action, "checkTarget", (ship: Ship, target: Target) => target); - - check.equals(power.wear, 0, "power wear"); - check.equals(equipment.wear, 0, "equipment wear"); - action.apply(battle, ship); - - check.equals(power.wear, 1, "power wear"); - check.equals(equipment.wear, 1, "equipment wear"); }) }); } diff --git a/src/core/actions/BaseAction.ts b/src/core/actions/BaseAction.ts index f7ef471..54cf2a6 100644 --- a/src/core/actions/BaseAction.ts +++ b/src/core/actions/BaseAction.ts @@ -24,31 +24,34 @@ module TK.SpaceTac { */ export class BaseAction extends RObject { // Identifier code for the type of action - code: string + readonly code: string - // Equipment that triggers this action - equipment: Equipment | null + // Full name of the action + readonly name: string + + // Cooldown configuration + private cooldown = new Cooldown() // Create the action - constructor(code = "nothing", equipment: Equipment | null = null) { + constructor(name = "Nothing", code?: string) { super(); - this.code = code; - this.equipment = equipment; + this.code = code ? code : name.toLowerCase().replace(" ", ""); + this.name = name; } /** * Get the verb for this action */ - getVerb(): string { - return "Idle"; + getVerb(ship: Ship): string { + return "Do"; } /** - * Get the relevent cooldown for this action + * Get the full title for this action (verb and name) */ - get cooldown(): Cooldown { - return this.equipment ? this.equipment.cooldown : new Cooldown(); + getTitle(ship: Ship): string { + return `${this.getVerb(ship)} ${this.name}`; } /** @@ -66,18 +69,18 @@ module TK.SpaceTac { } /** - * Get the number of turns this action is unavailable, because of overheating + * Configure the cooldown for this action */ - getCooldownDuration(estimated = false): number { - let cooldown = this.cooldown; - return estimated ? this.cooldown.cooling : this.cooldown.heat; + configureCooldown(overheat: number, cooling: number): void { + this.cooldown.configure(overheat, cooling); } /** - * Get the number of remaining uses before overheat, infinity if there is no overheat + * Get the cooldown configuration */ - getUsesBeforeOverheat(): number { - return this.cooldown.getRemainingUses(); + getCooldown(): Cooldown { + // TODO Split configuration (readonly) and usage + return this.cooldown; } /** @@ -94,6 +97,10 @@ module TK.SpaceTac { return "ship not playing"; } + if (!ship.actions.getById(this.id)) { + return "action not available"; + } + // Check AP usage if (remaining_ap === null) { remaining_ap = ship.getValue("power"); @@ -104,7 +111,7 @@ module TK.SpaceTac { } // Check cooldown - if (!this.cooldown.canUse()) { + if (!ship.actions.isUsable(this)) { return "overheated"; } diff --git a/src/core/actions/DeployDroneAction.spec.ts b/src/core/actions/DeployDroneAction.spec.ts index 7d5abe8..7937cb5 100644 --- a/src/core/actions/DeployDroneAction.spec.ts +++ b/src/core/actions/DeployDroneAction.spec.ts @@ -1,18 +1,22 @@ module TK.SpaceTac.Specs { testing("DeployDroneAction", test => { test.case("stores useful information", check => { - let equipment = new Equipment(SlotType.Weapon, "testdrone"); - let action = new DeployDroneAction(equipment); + let ship = new Ship(); + let action = new DeployDroneAction("testdrone"); + ship.actions.addCustom(action); - check.equals(action.code, "deploy-testdrone"); - check.equals(action.getVerb(), "Deploy"); - check.same(action.equipment, equipment); + check.equals(action.code, "testdrone"); + check.equals(action.getVerb(ship), "Deploy"); + + ship.actions.toggle(action, true); + check.equals(action.getVerb(ship), "Recall"); }); test.case("allows to deploy in range", check => { let ship = new Ship(); ship.setArenaPosition(0, 0); - let action = new DeployDroneAction(new Equipment(), 0, 8); + let action = new DeployDroneAction("testdrone", { power: 0 }, { deploy_distance: 8 }); + ship.actions.addCustom(action); check.equals(action.checkTarget(ship, new Target(8, 0, null)), new Target(8, 0, null)); check.equals(action.checkTarget(ship, new Target(12, 0, null)), new Target(8, 0, null)); @@ -26,12 +30,10 @@ module TK.SpaceTac.Specs { let battle = TestTools.createBattle(); let ship = battle.play_order[0]; ship.setArenaPosition(0, 0); - TestTools.setShipAP(ship, 3); + TestTools.setShipModel(ship, 100, 0, 3); - let equipment = new Equipment(SlotType.Weapon, "testdrone"); - let action = new DeployDroneAction(equipment, 2, 8, 4, [new DamageEffect(50)]); - equipment.action = action; - ship.addSlot(SlotType.Weapon).attach(equipment); + let action = new DeployDroneAction("testdrone", { power: 2 }, { deploy_distance: 8, drone_radius: 4, drone_effects: [new DamageEffect(50)] }); + ship.actions.addCustom(action); TestTools.actionChain(check, battle, [ [ship, action, new Target(5, 0)], diff --git a/src/core/actions/DeployDroneAction.ts b/src/core/actions/DeployDroneAction.ts index eb49519..c1bbe1e 100644 --- a/src/core/actions/DeployDroneAction.ts +++ b/src/core/actions/DeployDroneAction.ts @@ -1,12 +1,10 @@ /// module TK.SpaceTac { - /** - * Action to deploy a drone in space - * - * This is a toggled action, meaning that deploying a drone requires a permanent power supply from the ship + /** + * Configuration of a toggle action */ - export class DeployDroneAction extends ToggleAction { + export interface DeployDroneActionConfig { // Maximal distance the drone may be deployed deploy_distance: number @@ -15,21 +13,39 @@ module TK.SpaceTac { // Effects applied to ships in range of the drone drone_effects: BaseEffect[] + } - constructor(equipment: Equipment, power = 1, deploy_distance = 0, radius = 0, effects: BaseEffect[] = []) { - super(equipment, power, 0, [], `deploy-${equipment.code}`); + /** + * Action to deploy a drone in space + * + * This is a toggled action, meaning that deploying a drone requires a permanent power supply from the ship + */ + export class DeployDroneAction extends ToggleAction implements DeployDroneActionConfig { + deploy_distance = 0 + drone_radius = 0 + drone_effects: BaseEffect[] = [] - this.deploy_distance = deploy_distance; - this.drone_radius = radius; - this.drone_effects = effects; + constructor(name: string, toggle_config?: Partial, drone_config?: Partial, code?: string) { + super(name, toggle_config, code); + + if (drone_config) { + this.configureDrone(drone_config); + } } - getVerb(): string { - return this.activated ? "Recall" : "Deploy"; + /** + * Configure the deployed drone + */ + configureDrone(config: Partial): void { + copyfields(config, this); + } + + getVerb(ship: Ship): string { + return ship.actions.isToggled(this) ? "Recall" : "Deploy"; } getTargettingMode(ship: Ship): ActionTargettingMode { - return this.activated ? ActionTargettingMode.SELF : ActionTargettingMode.SPACE; + return ship.actions.isToggled(this) ? ActionTargettingMode.SELF : ActionTargettingMode.SPACE; } getDefaultTarget(ship: Ship): Target { @@ -42,7 +58,7 @@ module TK.SpaceTac { } getRangeRadius(ship: Ship): number { - return this.activated ? 0 : this.deploy_distance; + return ship.actions.isToggled(this) ? 0 : this.deploy_distance; } filterImpactedShips(source: ArenaLocation, target: Target, ships: Ship[]): Ship[] { @@ -57,7 +73,7 @@ module TK.SpaceTac { getSpecificDiffs(ship: Ship, battle: Battle, target: Target): BaseBattleDiff[] { let result = super.getSpecificDiffs(ship, battle, target); - if (this.activated) { + if (ship.actions.isToggled(this)) { let drone = first(battle.drones.list(), idrone => this.is(idrone.parent)); if (drone) { result.push(new DroneRecalledDiff(drone)); @@ -65,7 +81,7 @@ module TK.SpaceTac { return []; } } else { - let drone = new Drone(ship, this.equipment.code); + let drone = new Drone(ship, this.code); drone.parent = this; drone.x = target.x; drone.y = target.y; diff --git a/src/core/actions/EndTurnAction.spec.ts b/src/core/actions/EndTurnAction.spec.ts index 7fcdc8e..3200d0b 100644 --- a/src/core/actions/EndTurnAction.spec.ts +++ b/src/core/actions/EndTurnAction.spec.ts @@ -8,6 +8,9 @@ module TK.SpaceTac.Specs { battle.setPlayingShip(battle.play_order[0]); let action = new EndTurnAction(); + check.equals(action.checkCannotBeApplied(battle.play_order[0]), "action not available"); + + action = EndTurnAction.SINGLETON; check.equals(action.checkCannotBeApplied(battle.play_order[0]), null); check.equals(action.checkCannotBeApplied(battle.play_order[1]), "ship not playing"); }); @@ -36,23 +39,23 @@ module TK.SpaceTac.Specs { test.case("generates power for previous ship", check => { let battle = TestTools.createBattle(1, 1); let [ship1, ship2] = battle.play_order; - TestTools.setShipAP(ship1, 10, 3); - let weapon = TestTools.addWeapon(ship1); - weapon.action = new ToggleAction(weapon, 2); + TestTools.setShipModel(ship1, 100, 0, 10); + let toggle = new ToggleAction("toggle", { power: 2 }); + ship1.actions.addCustom(toggle); ship1.setValue("power", 6); TestTools.actionChain(check, battle, [ - [ship1, weapon.action, Target.newFromShip(ship1)], - [ship1, weapon.action, Target.newFromShip(ship1)], - [ship1, EndTurnAction.SINGLETON, Target.newFromShip(ship1)], - [ship2, EndTurnAction.SINGLETON, Target.newFromShip(ship2)], - [ship1, weapon.action, Target.newFromShip(ship1)], - [ship1, EndTurnAction.SINGLETON, Target.newFromShip(ship1)], - [ship2, EndTurnAction.SINGLETON, Target.newFromShip(ship2)], - [ship1, EndTurnAction.SINGLETON, Target.newFromShip(ship1)], - [ship2, EndTurnAction.SINGLETON, Target.newFromShip(ship2)], - [ship1, weapon.action, Target.newFromShip(ship1)], - [ship1, EndTurnAction.SINGLETON, Target.newFromShip(ship1)], + [ship1, toggle, undefined], + [ship1, toggle, undefined], + [ship1, EndTurnAction.SINGLETON, undefined], + [ship2, EndTurnAction.SINGLETON, undefined], + [ship1, toggle, undefined], + [ship1, EndTurnAction.SINGLETON, undefined], + [ship2, EndTurnAction.SINGLETON, undefined], + [ship1, EndTurnAction.SINGLETON, undefined], + [ship2, EndTurnAction.SINGLETON, undefined], + [ship1, toggle, undefined], + [ship1, EndTurnAction.SINGLETON, undefined], ], [ check => { check.equals(ship1.getValue("power"), 6, "power value"); @@ -67,15 +70,15 @@ module TK.SpaceTac.Specs { check.same(battle.playing_ship, ship1); }, check => { - check.equals(ship1.getValue("power"), 9, "power value"); + check.equals(ship1.getValue("power"), 10, "power value"); check.same(battle.playing_ship, ship2); }, check => { - check.equals(ship1.getValue("power"), 9, "power value"); + check.equals(ship1.getValue("power"), 10, "power value"); check.same(battle.playing_ship, ship1); }, check => { - check.equals(ship1.getValue("power"), 7, "power value"); + check.equals(ship1.getValue("power"), 8, "power value"); check.same(battle.playing_ship, ship1); }, check => { @@ -87,11 +90,11 @@ module TK.SpaceTac.Specs { check.same(battle.playing_ship, ship1); }, check => { - check.equals(ship1.getValue("power"), 9, "power value"); + check.equals(ship1.getValue("power"), 8, "power value"); check.same(battle.playing_ship, ship2); }, check => { - check.equals(ship1.getValue("power"), 9, "power value"); + check.equals(ship1.getValue("power"), 8, "power value"); check.same(battle.playing_ship, ship1); }, check => { @@ -110,13 +113,18 @@ module TK.SpaceTac.Specs { let ship = battle.play_order[0]; let equ1 = TestTools.addWeapon(ship); - equ1.cooldown.configure(1, 3); - equ1.cooldown.use(); + equ1.configureCooldown(1, 3); + let cd1 = ship.actions.getCooldown(equ1); + cd1.use(); + let equ2 = TestTools.addWeapon(ship); - equ2.cooldown.configure(1, 2); - equ2.cooldown.use(); + equ2.configureCooldown(1, 2); + let cd2 = ship.actions.getCooldown(equ2); + cd2.use(); + let equ3 = TestTools.addWeapon(ship); - equ3.cooldown.use(); + let cd3 = ship.actions.getCooldown(equ3); + cd3.use(); TestTools.actionChain(check, battle, [ [ship, EndTurnAction.SINGLETON, Target.newFromShip(ship)], @@ -124,24 +132,24 @@ module TK.SpaceTac.Specs { [ship, EndTurnAction.SINGLETON, Target.newFromShip(ship)], ], [ check => { - check.equals(equ1.cooldown.heat, 3, "equ1 heat"); - check.equals(equ2.cooldown.heat, 2, "equ2 heat"); - check.equals(equ3.cooldown.heat, 0, "equ3 heat"); + check.equals(cd1.heat, 3, "equ1 heat"); + check.equals(cd2.heat, 2, "equ2 heat"); + check.equals(cd3.heat, 0, "equ3 heat"); }, check => { - check.equals(equ1.cooldown.heat, 2, "equ1 heat"); - check.equals(equ2.cooldown.heat, 1, "equ2 heat"); - check.equals(equ3.cooldown.heat, 0, "equ3 heat"); + check.equals(cd1.heat, 2, "equ1 heat"); + check.equals(cd2.heat, 1, "equ2 heat"); + check.equals(cd3.heat, 0, "equ3 heat"); }, check => { - check.equals(equ1.cooldown.heat, 1, "equ1 heat"); - check.equals(equ2.cooldown.heat, 0, "equ2 heat"); - check.equals(equ3.cooldown.heat, 0, "equ3 heat"); + check.equals(cd1.heat, 1, "equ1 heat"); + check.equals(cd2.heat, 0, "equ2 heat"); + check.equals(cd3.heat, 0, "equ3 heat"); }, check => { - check.equals(equ1.cooldown.heat, 0, "equ1 heat"); - check.equals(equ2.cooldown.heat, 0, "equ2 heat"); - check.equals(equ3.cooldown.heat, 0, "equ3 heat"); + check.equals(cd1.heat, 0, "equ1 heat"); + check.equals(cd2.heat, 0, "equ2 heat"); + check.equals(cd3.heat, 0, "equ3 heat"); } ]); }); diff --git a/src/core/actions/EndTurnAction.ts b/src/core/actions/EndTurnAction.ts index c419c2e..ac29b3f 100644 --- a/src/core/actions/EndTurnAction.ts +++ b/src/core/actions/EndTurnAction.ts @@ -4,28 +4,27 @@ module TK.SpaceTac { /** * Action to end the ship's turn * - * This action is not provided by an equipment and is always available + * This action is always available (through its singleton) */ export class EndTurnAction extends BaseAction { // Singleton that may be used for all ships static SINGLETON = new EndTurnAction(); constructor() { - super("endturn"); + super("End turn"); } - getVerb(): string { - return "End ship's turn"; + getVerb(ship: Ship): string { + return this.name; + } + + getTitle(ship: Ship): string { + return this.name; } getActionPointsUsage(ship: Ship, target: Target | null): number { let toggled_cost = isum(imap(ship.iToggleActions(true), action => action.power)); - let power_diff = ship.getAttribute("power_generation") - toggled_cost; - let power_excess = ship.getValue("power") + power_diff - ship.getAttribute("power_capacity"); - if (power_excess > 0) { - power_diff -= power_excess; - } - return -power_diff; + return ship.getValue("power") + toggled_cost - ship.getAttribute("power_capacity"); } getSpecificDiffs(ship: Ship, battle: Battle, target: Target): BaseBattleDiff[] { @@ -33,10 +32,12 @@ module TK.SpaceTac { let result: BaseBattleDiff[] = []; let new_ship = battle.getNextShip(); - // Cool down equipment - ship.listEquipment().filter(equ => equ.cooldown.heat > 0).forEach(equ => { - result.push(new ShipCooldownDiff(ship, equ, 1)); - }); + // Cool down actions + ship.actions.listAll().forEach(action => { + if (ship.actions.getCooldown(action).heat > 0) { + result.push(new ShipCooldownDiff(ship, action, 1)); + } + }) // "On turn end" effects iforeach(ship.active_effects.iterator(), effect => { diff --git a/src/core/actions/MoveAction.spec.ts b/src/core/actions/MoveAction.spec.ts index 152bf59..681ff45 100644 --- a/src/core/actions/MoveAction.spec.ts +++ b/src/core/actions/MoveAction.spec.ts @@ -7,8 +7,8 @@ module TK.SpaceTac.Specs { ship.setValue("power", 6); ship.arena_x = 0; ship.arena_y = 0; - var engine = new Equipment(); - var action = new MoveAction(engine, 10); + var action = new MoveAction("Engine", { distance_per_power: 10 }); + ship.actions.addCustom(action); check.equals(action.getDistanceByActionPoint(ship), 10); @@ -26,7 +26,8 @@ module TK.SpaceTac.Specs { test.case("forbids targetting a ship", check => { var ship1 = new Ship(null, "Test1"); var ship2 = new Ship(null, "Test2"); - var action = new MoveAction(new Equipment()); + var action = new MoveAction(); + ship1.actions.addCustom(action); var result = action.checkTarget(ship1, Target.newFromShip(ship1)); check.equals(result, null); @@ -39,13 +40,11 @@ module TK.SpaceTac.Specs { let battle = TestTools.createBattle(); let ship = battle.play_order[0]; ship.setArenaPosition(500, 600) - TestTools.setShipAP(ship, 20); + TestTools.setShipModel(ship, 100, 0, 20); ship.setValue("power", 5); - let engine = new Equipment(SlotType.Engine); - let action = new MoveAction(engine, 1); - engine.action = action; - ship.addSlot(SlotType.Engine).attach(engine); + let action = new MoveAction("Engine", { distance_per_power: 1 }); + ship.actions.addCustom(action); TestTools.actionChain(check, battle, [ [ship, action, Target.newFromLocation(510, 605)], @@ -67,11 +66,11 @@ module TK.SpaceTac.Specs { var battle = TestTools.createBattle(1, 1); var ship = battle.fleets[0].ships[0]; var enemy = battle.fleets[1].ships[0]; - TestTools.setShipAP(ship, 100); + TestTools.setShipModel(ship, 100, 0, 100); ship.setArenaPosition(500, 500); enemy.setArenaPosition(1000, 500); - var action = new MoveAction(new Equipment(), 1000, 200); + var action = new MoveAction("Engine", { distance_per_power: 1000, safety_distance: 200 }); var result = action.checkLocationTarget(ship, Target.newFromLocation(700, 500)); check.equals(result, Target.newFromLocation(700, 500)); @@ -94,11 +93,11 @@ module TK.SpaceTac.Specs { var ship = battle.fleets[0].ships[0]; var enemy1 = battle.fleets[1].ships[0]; var enemy2 = battle.fleets[1].ships[1]; - TestTools.setShipAP(ship, 100); + TestTools.setShipModel(ship, 100, 0, 100); enemy1.setArenaPosition(0, 800); enemy2.setArenaPosition(0, 1000); - var action = new MoveAction(new Equipment(), 1000, 150); + var action = new MoveAction("Engine", { distance_per_power: 1000, safety_distance: 150 }); var result = action.checkLocationTarget(ship, Target.newFromLocation(0, 1100)); check.equals(result, Target.newFromLocation(0, 650)); @@ -109,11 +108,11 @@ module TK.SpaceTac.Specs { var ship = battle.fleets[0].ships[0]; var enemy1 = battle.fleets[1].ships[0]; var enemy2 = battle.fleets[1].ships[1]; - TestTools.setShipAP(ship, 100); + TestTools.setShipModel(ship, 100, 0, 100); enemy1.setArenaPosition(0, 500); enemy2.setArenaPosition(0, 800); - var action = new MoveAction(new Equipment(), 1000, 600); + var action = new MoveAction("Engine", { distance_per_power: 1000, safety_distance: 600 }); let result = action.checkLocationTarget(ship, Target.newFromLocation(0, 1000)); check.equals(result, null); @@ -124,7 +123,7 @@ module TK.SpaceTac.Specs { test.case("applies ship maneuvrability to determine distance per power point", check => { let ship = new Ship(); - let action = new MoveAction(new Equipment(), 100, undefined, 60); + let action = new MoveAction("Engine", { distance_per_power: 100, maneuvrability_factor: 60 }); TestTools.setAttribute(ship, "maneuvrability", 0); check.nears(action.getDistanceByActionPoint(ship), 40); TestTools.setAttribute(ship, "maneuvrability", 1); @@ -134,7 +133,7 @@ module TK.SpaceTac.Specs { TestTools.setAttribute(ship, "maneuvrability", 10); check.nears(action.getDistanceByActionPoint(ship), 90); - action = new MoveAction(new Equipment(), 100, undefined, 0); + action = new MoveAction("Engine", { distance_per_power: 100, maneuvrability_factor: 0 }); TestTools.setAttribute(ship, "maneuvrability", 0); check.nears(action.getDistanceByActionPoint(ship), 100); TestTools.setAttribute(ship, "maneuvrability", 10); @@ -142,13 +141,13 @@ module TK.SpaceTac.Specs { }); test.case("builds a textual description", check => { - let action = new MoveAction(new Equipment(), 58, 0, 0); + let action = new MoveAction("Engine", { distance_per_power: 58, safety_distance: 0 }); check.equals(action.getEffectsDescription(), "Move: 58km per power point"); - action = new MoveAction(new Equipment(), 58, 12, 0); + action = new MoveAction("Engine", { distance_per_power: 58, safety_distance: 12 }); check.equals(action.getEffectsDescription(), "Move: 58km per power point (safety: 12km)"); - action = new MoveAction(new Equipment(), 58, 12, 80); + action = new MoveAction("Engine", { distance_per_power: 58, safety_distance: 12, maneuvrability_factor: 80 }); check.equals(action.getEffectsDescription(), "Move: 12-58km per power point (safety: 12km)"); }); }); diff --git a/src/core/actions/MoveAction.ts b/src/core/actions/MoveAction.ts index 7231c3c..3d10618 100644 --- a/src/core/actions/MoveAction.ts +++ b/src/core/actions/MoveAction.ts @@ -1,25 +1,47 @@ module TK.SpaceTac { + /** + * Configuration of a trigger action + */ + export interface MoveActionConfig { + // Distance allowed for each power point (raw, without applying maneuvrability) + distance_per_power: number + // Safety distance from other ships + safety_distance: number + // Impact of maneuvrability (in % of distance) + maneuvrability_factor: number + } + /** * Action to move the ship to a specific location */ - export class MoveAction extends BaseAction { - constructor( - // Mandatory equipment - readonly equipment: Equipment, - // Distance allowed for each power point (raw, without applying maneuvrability) - readonly distance_per_power = 0, - // Safety distance from other ships - readonly safety_distance = 120, - // Impact of maneuvrability (in % of distance) - readonly maneuvrability_factor = 0 - ) { - super("move", equipment); + export class MoveAction extends BaseAction implements MoveActionConfig { + distance_per_power = 0 + safety_distance = 120 + maneuvrability_factor = 0 + + constructor(name = "Engine", config?: Partial, code = "move") { + super(name, code); + + if (config) { + this.configureEngine(config); + } } - getVerb(): string { + /** + * Configure the engine + */ + configureEngine(config: Partial): void { + copyfields(config, this); + } + + getVerb(ship: Ship): string { return "Move"; } + getTitle(ship: Ship): string { + return `Use ${this.name}`; + } + getTargettingMode(ship: Ship): ActionTargettingMode { return ActionTargettingMode.SPACE; } @@ -121,7 +143,7 @@ module TK.SpaceTac { protected getSpecificDiffs(ship: Ship, battle: Battle, target: Target): BaseBattleDiff[] { let angle = (arenaDistance(target, ship.location) < 0.00001) ? ship.arena_angle : arenaAngle(ship.location, target); let destination = new ArenaLocationAngle(target.x, target.y, angle); - return [new ShipMoveDiff(ship, ship.location, destination, this.equipment)]; + return [new ShipMoveDiff(ship, ship.location, destination, this)]; } getEffectsDescription(): string { diff --git a/src/core/actions/ToggleAction.spec.ts b/src/core/actions/ToggleAction.spec.ts index 0a9148d..7d65717 100644 --- a/src/core/actions/ToggleAction.spec.ts +++ b/src/core/actions/ToggleAction.spec.ts @@ -1,21 +1,25 @@ module TK.SpaceTac.Specs { testing("ToggleAction", test => { test.case("returns correct targetting mode", check => { - let action = new ToggleAction(new Equipment(), 1, 0, []); - check.same(action.getTargettingMode(new Ship()), ActionTargettingMode.SELF_CONFIRM); + let action = new ToggleAction("testtoggle"); + let ship = new Ship(); + ship.actions.addCustom(action); - action.activated = true; - check.same(action.getTargettingMode(new Ship()), ActionTargettingMode.SELF_CONFIRM); + check.same(action.getTargettingMode(ship), ActionTargettingMode.SELF_CONFIRM); - action = new ToggleAction(new Equipment(), 1, 50, []); - check.same(action.getTargettingMode(new Ship()), ActionTargettingMode.SURROUNDINGS); + ship.actions.toggle(action, true); + check.same(action.getTargettingMode(ship), ActionTargettingMode.SELF_CONFIRM); - action.activated = true; - check.same(action.getTargettingMode(new Ship()), ActionTargettingMode.SELF_CONFIRM); + action = new ToggleAction("testtoggle", { radius: 50 }); + ship.actions.addCustom(action); + check.same(action.getTargettingMode(ship), ActionTargettingMode.SURROUNDINGS); + + ship.actions.toggle(action, true); + check.same(action.getTargettingMode(ship), ActionTargettingMode.SELF_CONFIRM); }) test.case("collects impacted ships", check => { - let action = new ToggleAction(new Equipment(), 1, 50, []); + let action = new ToggleAction("testtoggle", { radius: 50 }); let battle = new Battle(); let ship1 = battle.fleets[0].addShip(); ship1.setArenaPosition(0, 0); diff --git a/src/core/actions/ToggleAction.ts b/src/core/actions/ToggleAction.ts index db10160..ff35dfc 100644 --- a/src/core/actions/ToggleAction.ts +++ b/src/core/actions/ToggleAction.ts @@ -1,35 +1,49 @@ /// module TK.SpaceTac { + /** + * Configuration of a toggle action + */ + export interface ToggleActionConfig { + // Power consumption (while active) + power: number + // Effect radius + radius: number + // Effects applied + effects: BaseEffect[] + } + /** * Action to toggle some effects on the ship or around it, until next turn start * * Toggle actions consume power when activated, and restore it when deactivated */ export class ToggleAction extends BaseAction { - // Current activation status - activated = false + power = 1 + radius = 0 + effects: BaseEffect[] = [] - constructor( - // Mandatory equipment - readonly equipment: Equipment, - // Power consumption (while active) - readonly power = 1, - // Effect radius - readonly radius = 0, - // Effects applied - readonly effects: BaseEffect[] = [], - code = `toggle-${equipment.code}` - ) { - super(code, equipment); + constructor(name: string, config?: Partial, code?: string) { + super(name, code); + + if (config) { + this.configureToggle(config); + } } - getVerb(): string { - return this.activated ? "Deactivate" : "Activate"; + /** + * Configure the toggling + */ + configureToggle(config: Partial): void { + copyfields(config, this); + } + + getVerb(ship: Ship): string { + return ship.actions.isToggled(this) ? "Deactivate" : "Activate"; } getTargettingMode(ship: Ship): ActionTargettingMode { - if (this.activated || !this.radius) { + if (ship.actions.isToggled(this) || !this.radius) { return ActionTargettingMode.SELF_CONFIRM; } else { return ActionTargettingMode.SURROUNDINGS; @@ -37,7 +51,7 @@ module TK.SpaceTac { } getActionPointsUsage(ship: Ship, target: Target | null): number { - return this.activated ? -this.power : this.power; + return ship.actions.isToggled(this) ? -this.power : this.power; } getRangeRadius(ship: Ship): number { @@ -53,14 +67,16 @@ module TK.SpaceTac { } getSpecificDiffs(ship: Ship, battle: Battle, target: Target): BaseBattleDiff[] { + let activated = ship.actions.isToggled(this); + let result: BaseBattleDiff[] = [ - new ShipActionToggleDiff(ship, this, !this.activated) + new ShipActionToggleDiff(ship, this, !activated) ]; let ships = this.getImpactedShips(ship, target, ship.location); ships.forEach(iship => { this.effects.forEach(effect => { - if (this.activated) { + if (activated) { result.push(new ShipEffectRemovedDiff(iship, effect)); result = result.concat(effect.getOffDiffs(iship)); } else { diff --git a/src/core/actions/TriggerAction.spec.ts b/src/core/actions/TriggerAction.spec.ts index 402b884..33c7d4b 100644 --- a/src/core/actions/TriggerAction.spec.ts +++ b/src/core/actions/TriggerAction.spec.ts @@ -1,25 +1,23 @@ module TK.SpaceTac.Specs { testing("TriggerAction", test => { test.case("constructs correctly", check => { - let equipment = new Equipment(SlotType.Weapon, "testweapon"); - let action = new TriggerAction(equipment, [], 4, 30, 10); - - check.equals(action.code, "fire-testweapon"); + let action = new TriggerAction("testweapon", { power: 4, range: 30, blast: 10 }); + check.equals(action.code, "testweapon"); check.equals(action.getVerb(), "Fire"); - check.same(action.equipment, equipment); + + action = new TriggerAction("testweapon", { blast: 10 }); + check.equals(action.getVerb(), "Trigger"); }) test.case("applies effects to alive ships in blast radius", check => { let fleet = new Fleet(); let ship = new Ship(fleet, "ship"); - let equipment = new Equipment(SlotType.Weapon, "testweapon"); let effect = new BaseEffect("testeffect"); let mock_apply = check.patch(effect, "getOnDiffs"); - let action = new TriggerAction(equipment, [effect], 5, 100, 10); - equipment.action = action; - ship.addSlot(SlotType.Weapon).attach(equipment); + let action = new TriggerAction("testweapon", { power: 5, range: 100, blast: 10, effects: [effect] }); - TestTools.setShipAP(ship, 10); + TestTools.setShipModel(ship, 100, 0, 10); + ship.actions.addCustom(action); let ship1 = new Ship(fleet, "ship1"); ship1.setArenaPosition(65, 72); @@ -45,8 +43,7 @@ module TK.SpaceTac.Specs { ship1.setArenaPosition(50, 10); let ship2 = new Ship(); ship2.setArenaPosition(150, 10); - let weapon = TestTools.addWeapon(ship1, 1, 0, 100, 30); - let action = nn(weapon.action); + let action = TestTools.addWeapon(ship1, 1, 0, 100, 30); let target = action.checkTarget(ship1, new Target(150, 10)); check.equals(target, new Target(150, 10)); @@ -74,14 +71,14 @@ module TK.SpaceTac.Specs { ship3.setArenaPosition(0, 30); let ships = [ship1, ship2, ship3]; - let action = new TriggerAction(new Equipment(), [], 1, 50); + let action = new TriggerAction("testaction", { range: 50 }); check.equals(action.filterImpactedShips({ x: 0, y: 0 }, Target.newFromShip(ship2), ships), [ship2]); check.equals(action.filterImpactedShips({ x: 0, y: 0 }, Target.newFromLocation(10, 50), ships), []); - action = new TriggerAction(new Equipment(), [], 1, 50, 40); + action = new TriggerAction("testaction", { range: 50, blast: 40 }); check.equals(action.filterImpactedShips({ x: 0, y: 0 }, Target.newFromLocation(20, 20), ships), [ship1, ship3]); - action = new TriggerAction(new Equipment(), [], 1, 100, 0, 30); + action = new TriggerAction("testaction", { range: 100, angle: 30 }); check.equals(action.filterImpactedShips({ x: 0, y: 51 }, Target.newFromLocation(30, 50), ships), [ship1, ship2]); }) @@ -93,7 +90,7 @@ module TK.SpaceTac.Specs { TestTools.setAttribute(ship1, "precision", precision); TestTools.setAttribute(ship2, "maneuvrability", maneuvrability); - let action = new TriggerAction(new Equipment(), [], 1, 0, 0, 0, precision_factor, maneuvrability_factor); + let action = new TriggerAction("testaction", { aim: precision_factor, evasion: maneuvrability_factor }); check.nears(action.getSuccessFactor(ship1, ship2), result, 3, `precision ${precision} (weight ${precision_factor}), maneuvrability ${maneuvrability} (weight ${maneuvrability_factor})`); } @@ -134,7 +131,7 @@ module TK.SpaceTac.Specs { function verify(success_base: number, luck: number, random: number, expected: number) { let ship1 = new Ship(); let ship2 = new Ship(); - let action = new TriggerAction(new Equipment(), [], 1, 0, 0, 0, 0, 0, luck); + let action = new TriggerAction("testaction", { luck: luck }); check.patch(action, "getSuccessFactor", () => success_base); check.nears(action.getEffectiveSuccess(ship1, ship2, new SkewedRandomGenerator([random])), expected, 5, `success ${success_base}, luck ${luck}, random ${random}`); @@ -163,28 +160,26 @@ module TK.SpaceTac.Specs { test.case("guesses targetting mode", check => { let ship = new Ship(); - let equ = new Equipment(); - let action = new TriggerAction(equ, []); + let action = new TriggerAction("testaction"); check.equals(action.getTargettingMode(ship), ActionTargettingMode.SELF_CONFIRM, "self"); - action = new TriggerAction(equ, [], 1, 50); + action = new TriggerAction("testaction", { range: 50 }); check.equals(action.getTargettingMode(ship), ActionTargettingMode.SHIP, "ship"); - action = new TriggerAction(equ, [], 1, 50, 20); + action = new TriggerAction("testaction", { range: 50, blast: 20 }); check.equals(action.getTargettingMode(ship), ActionTargettingMode.SPACE, "blast"); - action = new TriggerAction(equ, [], 1, 0, 20); + action = new TriggerAction("testaction", { blast: 20 }); check.equals(action.getTargettingMode(ship), ActionTargettingMode.SURROUNDINGS, "surroundings"); - action = new TriggerAction(equ, [], 1, 50, 0, 15); + action = new TriggerAction("testaction", { range: 50, angle: 15 }); check.equals(action.getTargettingMode(ship), ActionTargettingMode.SPACE, "angle"); }) test.case("rotates toward the target", check => { let battle = TestTools.createBattle(); let ship = battle.play_order[0]; - let weapon = TestTools.addWeapon(ship, 1, 0, 100, 30); - let action = nn(weapon.action); + let action = TestTools.addWeapon(ship, 1, 0, 100, 30); check.patch(action, "checkTarget", (ship: Ship, target: Target) => target); check.equals(ship.arena_angle, 0); @@ -198,32 +193,32 @@ module TK.SpaceTac.Specs { }) test.case("builds a textual description", check => { - let effects: BaseEffect[] = []; - let action = new TriggerAction(new Equipment(), effects, 0); + let action = new TriggerAction(); check.equals(action.getEffectsDescription(), ""); - effects.push(new AttributeMultiplyEffect("precision", 20)); + let effects: BaseEffect[] = [new AttributeMultiplyEffect("precision", 20)]; + action.configureTrigger({ effects: effects, power: 0 }); check.equals(action.getEffectsDescription(), "Trigger:\n• precision +20% on self"); - action = new TriggerAction(new Equipment(), effects, 2); + action.configureTrigger({ effects: effects, power: 2 }); check.equals(action.getEffectsDescription(), "Trigger (power 2):\n• precision +20% on self"); - action = new TriggerAction(new Equipment(), effects, 2, 120); + action.configureTrigger({ effects: effects, power: 2, range: 120 }); check.equals(action.getEffectsDescription(), "Fire (power 2, range 120km):\n• precision +20% on target"); - action = new TriggerAction(new Equipment(), effects, 2, 120, 0, 0, 10); + action.configureTrigger({ effects: effects, power: 2, range: 120, aim: 10 }); check.equals(action.getEffectsDescription(), "Fire (power 2, range 120km, aim +10%):\n• precision +20% on target"); - action = new TriggerAction(new Equipment(), effects, 2, 120, 0, 0, 10, 35); + action.configureTrigger({ effects: effects, power: 2, range: 120, aim: 10, evasion: 35 }); check.equals(action.getEffectsDescription(), "Fire (power 2, range 120km, aim +10%, evasion -35%):\n• precision +20% on target"); - action = new TriggerAction(new Equipment(), effects, 2, 120, 0, 80, 10, 35); + action.configureTrigger({ effects: effects, power: 2, range: 120, aim: 10, evasion: 35, angle: 80 }); check.equals(action.getEffectsDescription(), "Fire (power 2, range 120km, aim +10%, evasion -35%):\n• precision +20% in 80° arc"); - action = new TriggerAction(new Equipment(), effects, 2, 120, 100, 80, 10, 35); + action.configureTrigger({ effects: effects, power: 2, range: 120, aim: 10, evasion: 35, blast: 100, angle: 80 }); check.equals(action.getEffectsDescription(), "Fire (power 2, range 120km, aim +10%, evasion -35%):\n• precision +20% in 100km radius"); - action = new TriggerAction(new Equipment(), effects, 2, 120, 100, 80, 10, 35, 15); + action.configureTrigger({ effects: effects, power: 2, range: 120, aim: 10, evasion: 35, blast: 100, angle: 80, luck: 15 }); check.equals(action.getEffectsDescription(), "Fire (power 2, range 120km, aim +10%, evasion -35%, luck ±15%):\n• precision +20% in 100km radius"); }) }); diff --git a/src/core/actions/TriggerAction.ts b/src/core/actions/TriggerAction.ts index 0d44156..a18fd17 100644 --- a/src/core/actions/TriggerAction.ts +++ b/src/core/actions/TriggerAction.ts @@ -1,34 +1,56 @@ /// module TK.SpaceTac { + /** + * Configuration of a trigger action + */ + export interface TriggerActionConfig { + // Effects applied on target + effects: BaseEffect[] + // Power consumption + power: number + // Maximal range of the weapon (distance to target) + range: number + // Radius around the target that will be impacted + blast: number + // Angle of the area between the source and the target that will be impacted + angle: number + // Influence of "precision" of firing ship (0..100) + aim: number + // Influence of "maneuvrability" of impacted ship (0..100) + evasion: number + // Influence of luck (0..100) + luck: number + } + /** * Action to trigger an equipment (for example a weapon), with an optional target * * The target will be resolved as a list of ships, on which all the action effects will be applied */ - export class TriggerAction extends BaseAction { - constructor( - // Mandatory equipment - readonly equipment: Equipment, - // Effects applied on target - readonly effects: BaseEffect[] = [], - // Power consumption - readonly power = 1, - // Maximal range of the weapon (distance to target) - readonly range = 0, - // Radius around the target that will be impacted - readonly blast = 0, - // Angle of the area between the source and the target that will be impacted - readonly angle = 0, - // Influence of "precision" of firing ship (0..100) - readonly aim = 0, - // Influence of "maneuvrability" of impacted ship (0..100) - readonly evasion = 0, - // Influence of luck - readonly luck = 0, - code = `fire-${equipment.code}` - ) { - super(code, equipment); + export class TriggerAction extends BaseAction implements TriggerActionConfig { + effects: BaseEffect[] = [] + power = 1 + range = 0 + blast = 0 + angle = 0 + aim = 0 + evasion = 0 + luck = 0 + + constructor(name?: string, config?: Partial, code?: string) { + super(name, code); + + if (config) { + this.configureTrigger(config); + } + } + + /** + * Configure the triggering and effects of this action + */ + configureTrigger(config: Partial) { + copyfields(config, this); } getVerb(): string { @@ -180,13 +202,13 @@ module TK.SpaceTac { let angle = arenaAngle(ship.location, target); if (Math.abs(angularDifference(angle, ship.arena_angle)) > 1e-6) { let destination = new ArenaLocationAngle(ship.arena_x, ship.arena_y, angle); - let engine = first(ship.listEquipment(SlotType.Engine), () => true); + let engine = first(ship.actions.listAll(), action => action instanceof MoveAction); result.push(new ShipMoveDiff(ship, ship.location, destination, engine)); } // Fire a projectile - if (this.equipment && this.equipment.slot_type == SlotType.Weapon) { - result.push(new ProjectileFiredDiff(ship, this.equipment, target)); + if (this.range) { + result.push(new ProjectileFiredDiff(ship, this, target)); } } diff --git a/src/core/ai/Maneuver.spec.ts b/src/core/ai/Maneuver.spec.ts index a3d9bf0..bc1fc4b 100644 --- a/src/core/ai/Maneuver.spec.ts +++ b/src/core/ai/Maneuver.spec.ts @@ -6,23 +6,23 @@ module TK.SpaceTac.Specs { let ship2 = battle.fleets[1].addShip(); let ship3 = battle.fleets[1].addShip(); let ship4 = battle.fleets[1].addShip(); + ship1.setArenaPosition(0, 0); + TestTools.setShipModel(ship1, 20, 20, 10); + ship2.setArenaPosition(500, 0); + TestTools.setShipModel(ship2, 70, 100); + ship3.setArenaPosition(560, 0); + TestTools.setShipModel(ship3, 80, 30); + ship4.setArenaPosition(640, 0); + TestTools.setShipModel(ship4, 30, 30); + let weapon = TestTools.addWeapon(ship1, 50, 2, 200, 100); let engine = TestTools.addEngine(ship1, 100); - ship1.setArenaPosition(0, 0); - TestTools.setShipHP(ship1, 20, 20); - TestTools.setShipAP(ship1, 10); - ship2.setArenaPosition(500, 0); - TestTools.setShipHP(ship2, 70, 100); - ship3.setArenaPosition(560, 0); - TestTools.setShipHP(ship3, 80, 30); - ship4.setArenaPosition(640, 0); - TestTools.setShipHP(ship4, 30, 30); - let maneuver = new Maneuver(ship1, nn(weapon.action), Target.newFromLocation(530, 0)); - check.contains(maneuver.effects, new ShipActionUsedDiff(ship1, nn(engine.action), Target.newFromLocation(331, 0)), "engine use"); + let maneuver = new Maneuver(ship1, weapon, Target.newFromLocation(530, 0)); + check.contains(maneuver.effects, new ShipActionUsedDiff(ship1, engine, Target.newFromLocation(331, 0)), "engine use"); check.contains(maneuver.effects, new ShipValueDiff(ship1, "power", -4), "engine power"); check.contains(maneuver.effects, new ShipMoveDiff(ship1, ship1.location, new ArenaLocationAngle(331, 0), engine), "move"); - check.contains(maneuver.effects, new ShipActionUsedDiff(ship1, nn(weapon.action), Target.newFromLocation(530, 0)), "weapon use"); + check.contains(maneuver.effects, new ShipActionUsedDiff(ship1, weapon, Target.newFromLocation(530, 0)), "weapon use"); check.contains(maneuver.effects, new ProjectileFiredDiff(ship1, weapon, Target.newFromLocation(530, 0)), "weapon power"); check.contains(maneuver.effects, new ShipValueDiff(ship1, "power", -2), "weapon power"); check.contains(maneuver.effects, new ShipValueDiff(ship2, "shield", -50), "ship2 shield value"); diff --git a/src/core/ai/TacticalAIHelpers.spec.ts b/src/core/ai/TacticalAIHelpers.spec.ts index 836c37e..5781eb2 100644 --- a/src/core/ai/TacticalAIHelpers.spec.ts +++ b/src/core/ai/TacticalAIHelpers.spec.ts @@ -7,7 +7,7 @@ module TK.SpaceTac.Specs { let ship1a = battle.fleets[1].addShip(new Ship(null, "1A")); let ship1b = battle.fleets[1].addShip(new Ship(null, "1B")); - TestTools.setShipAP(ship0a, 10); + TestTools.setShipModel(ship0a, 100, 0, 10); TestTools.setShipPlaying(battle, ship0a); let result = imaterialize(TacticalAIHelpers.produceDirectShots(ship0a, battle)); @@ -17,10 +17,10 @@ module TK.SpaceTac.Specs { let weapon2 = TestTools.addWeapon(ship0a, 15); result = imaterialize(TacticalAIHelpers.produceDirectShots(ship0a, battle)); check.equals(result.length, 4); - check.contains(result, new Maneuver(ship0a, nn(weapon1.action), Target.newFromShip(ship1a))); - check.contains(result, new Maneuver(ship0a, nn(weapon1.action), Target.newFromShip(ship1b))); - check.contains(result, new Maneuver(ship0a, nn(weapon2.action), Target.newFromShip(ship1a))); - check.contains(result, new Maneuver(ship0a, nn(weapon2.action), Target.newFromShip(ship1b))); + check.contains(result, new Maneuver(ship0a, weapon1, Target.newFromShip(ship1a))); + check.contains(result, new Maneuver(ship0a, weapon1, Target.newFromShip(ship1b))); + check.contains(result, new Maneuver(ship0a, weapon2, Target.newFromShip(ship1a))); + check.contains(result, new Maneuver(ship0a, weapon2, Target.newFromShip(ship1b))); }); test.case("produces random moves inside a grid", check => { @@ -29,7 +29,7 @@ module TK.SpaceTac.Specs { battle.height = 100; let ship = battle.fleets[0].addShip(); - TestTools.setShipAP(ship, 10); + TestTools.setShipModel(ship, 100, 0, 10); TestTools.setShipPlaying(battle, ship); let result = imaterialize(TacticalAIHelpers.produceRandomMoves(ship, battle, 2, 1)); @@ -39,10 +39,10 @@ module TK.SpaceTac.Specs { result = imaterialize(TacticalAIHelpers.produceRandomMoves(ship, battle, 2, 1, new SkewedRandomGenerator([0.5], true))); check.equals(result, [ - new Maneuver(ship, nn(engine.action), Target.newFromLocation(25, 25)), - new Maneuver(ship, nn(engine.action), Target.newFromLocation(75, 25)), - new Maneuver(ship, nn(engine.action), Target.newFromLocation(25, 75)), - new Maneuver(ship, nn(engine.action), Target.newFromLocation(75, 75)), + new Maneuver(ship, engine, Target.newFromLocation(25, 25)), + new Maneuver(ship, engine, Target.newFromLocation(75, 25)), + new Maneuver(ship, engine, Target.newFromLocation(25, 75)), + new Maneuver(ship, engine, Target.newFromLocation(75, 75)), ]); }); @@ -51,8 +51,9 @@ module TK.SpaceTac.Specs { let ship = battle.fleets[0].addShip(); let weapon = TestTools.addWeapon(ship, 50, 1, 1000, 105); - TestTools.setShipAP(ship, 10); + TestTools.setShipModel(ship, 100, 0, 10); TestTools.setShipPlaying(battle, ship); + ship.actions.addCustom(weapon); let result = imaterialize(TacticalAIHelpers.produceInterestingBlastShots(ship, battle)); check.equals(result.length, 0); @@ -68,8 +69,8 @@ module TK.SpaceTac.Specs { result = imaterialize(TacticalAIHelpers.produceInterestingBlastShots(ship, battle)); check.equals(result, [ - new Maneuver(ship, nn(weapon.action), Target.newFromLocation(600, 0)), - new Maneuver(ship, nn(weapon.action), Target.newFromLocation(600, 0)), + new Maneuver(ship, weapon, Target.newFromLocation(600, 0)), + new Maneuver(ship, weapon, Target.newFromLocation(600, 0)), ]); let enemy3 = battle.fleets[1].addShip(); @@ -77,8 +78,8 @@ module TK.SpaceTac.Specs { result = imaterialize(TacticalAIHelpers.produceInterestingBlastShots(ship, battle)); check.equals(result, [ - new Maneuver(ship, nn(weapon.action), Target.newFromLocation(600, 0)), - new Maneuver(ship, nn(weapon.action), Target.newFromLocation(600, 0)), + new Maneuver(ship, weapon, Target.newFromLocation(600, 0)), + new Maneuver(ship, weapon, Target.newFromLocation(600, 0)), ]); }); @@ -86,7 +87,7 @@ module TK.SpaceTac.Specs { let battle = new Battle(); let ship = battle.fleets[0].addShip(); let weapon = TestTools.addWeapon(ship, 50, 5, 100); - let action = nn(weapon.action); + let action = weapon; let engine = TestTools.addEngine(ship, 25); let maneuver = new Maneuver(ship, new BaseAction("fake"), new Target(0, 0), 0); @@ -95,11 +96,11 @@ module TK.SpaceTac.Specs { maneuver = new Maneuver(ship, action, Target.newFromLocation(100, 0), 0); check.same(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver), -Infinity); - TestTools.setShipAP(ship, 4); + TestTools.setShipModel(ship, 100, 0, 4, 1, [engine, action]); maneuver = new Maneuver(ship, action, Target.newFromLocation(100, 0), 0); check.same(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver), -Infinity); - TestTools.setShipAP(ship, 10); + TestTools.setShipModel(ship, 100, 0, 10, 1, [engine, action]); maneuver = new Maneuver(ship, action, Target.newFromLocation(100, 0), 0); check.equals(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver), 0.5); // 5 power remaining on 10 @@ -116,37 +117,37 @@ module TK.SpaceTac.Specs { test.case("evaluates the drawback of doing nothing", check => { let battle = new Battle(); let ship = battle.fleets[0].addShip(); - TestTools.setShipAP(ship, 10, 5); + TestTools.setShipModel(ship, 100, 0, 10); let engine = TestTools.addEngine(ship, 50); let weapon = TestTools.addWeapon(ship, 10, 2, 100, 10); - let maneuver = new Maneuver(ship, nn(weapon.action), Target.newFromLocation(0, 0)); - check.equals(TacticalAIHelpers.evaluateIdling(ship, battle, maneuver), -0.3); + let maneuver = new Maneuver(ship, weapon, Target.newFromLocation(0, 0)); + check.equals(TacticalAIHelpers.evaluateIdling(ship, battle, maneuver), 0.5); - maneuver = new Maneuver(ship, nn(engine.action), Target.newFromLocation(0, 0)); - check.equals(TacticalAIHelpers.evaluateIdling(ship, battle, maneuver), -0.5); + maneuver = new Maneuver(ship, engine, Target.newFromLocation(0, 0)); + check.equals(TacticalAIHelpers.evaluateIdling(ship, battle, maneuver), 0); + + maneuver = new Maneuver(ship, EndTurnAction.SINGLETON, Target.newFromShip(ship)); + check.equals(TacticalAIHelpers.evaluateIdling(ship, battle, maneuver), -1); ship.setValue("power", 2); - maneuver = new Maneuver(ship, nn(weapon.action), Target.newFromLocation(0, 0)); - check.equals(TacticalAIHelpers.evaluateIdling(ship, battle, maneuver), 0.5); - - maneuver = new Maneuver(ship, nn(engine.action), Target.newFromLocation(0, 0)); - check.equals(TacticalAIHelpers.evaluateIdling(ship, battle, maneuver), 0); + maneuver = new Maneuver(ship, EndTurnAction.SINGLETON, Target.newFromShip(ship)); + check.equals(TacticalAIHelpers.evaluateIdling(ship, battle, maneuver), -0.2); }); test.case("evaluates damage to enemies", check => { let battle = new Battle(); let ship = battle.fleets[0].addShip(); let weapon = TestTools.addWeapon(ship, 50, 5, 500, 100); - let action = nn(weapon.action); + let action = weapon; let enemy1 = battle.fleets[1].addShip(); enemy1.setArenaPosition(250, 0); - TestTools.setShipHP(enemy1, 50, 25); + TestTools.setShipModel(enemy1, 50, 25); let enemy2 = battle.fleets[1].addShip(); enemy2.setArenaPosition(300, 0); - TestTools.setShipHP(enemy2, 25, 0); + TestTools.setShipModel(enemy2, 25, 0); // no enemies hurt let maneuver = new Maneuver(ship, action, Target.newFromLocation(100, 0)); @@ -164,11 +165,11 @@ module TK.SpaceTac.Specs { test.case("evaluates ship clustering", check => { let battle = new Battle(); let ship = battle.fleets[0].addShip(); + TestTools.setShipModel(ship, 100, 0, 10); TestTools.addEngine(ship, 100); - TestTools.setShipAP(ship, 10); let weapon = TestTools.addWeapon(ship, 100, 1, 100, 10); - let maneuver = new Maneuver(ship, nn(weapon.action), Target.newFromLocation(200, 0), 0.5); + let maneuver = new Maneuver(ship, weapon, Target.newFromLocation(200, 0), 0.5); check.nears(maneuver.simulation.move_location.x, 100.5, 1); check.equals(maneuver.simulation.move_location.y, 0); check.equals(TacticalAIHelpers.evaluateClustering(ship, battle, maneuver), 0); @@ -190,7 +191,7 @@ module TK.SpaceTac.Specs { let battle = new Battle(undefined, undefined, 200, 100); let ship = battle.fleets[0].addShip(); let weapon = TestTools.addWeapon(ship, 1, 1, 400); - let action = nn(weapon.action); + let action = weapon; ship.setArenaPosition(0, 0); let maneuver = new Maneuver(ship, action, new Target(0, 0), 0); @@ -214,19 +215,27 @@ module TK.SpaceTac.Specs { let ship = battle.fleets[0].addShip(); let weapon = TestTools.addWeapon(ship, 1, 1, 400); - let maneuver = new Maneuver(ship, nn(weapon.action), new Target(0, 0)); + let maneuver = new Maneuver(ship, weapon, new Target(0, 0)); check.equals(TacticalAIHelpers.evaluateOverheat(ship, battle, maneuver), 0); - weapon.cooldown.configure(1, 1); + weapon.configureCooldown(1, 1); + ship.actions.updateFromShip(ship); + ship.actions.addCustom(weapon); check.equals(TacticalAIHelpers.evaluateOverheat(ship, battle, maneuver), -0.4); - weapon.cooldown.configure(1, 2); + weapon.configureCooldown(1, 2); + ship.actions.updateFromShip(ship); + ship.actions.addCustom(weapon); check.equals(TacticalAIHelpers.evaluateOverheat(ship, battle, maneuver), -0.8); - weapon.cooldown.configure(1, 3); + weapon.configureCooldown(1, 3); + ship.actions.updateFromShip(ship); + ship.actions.addCustom(weapon); check.equals(TacticalAIHelpers.evaluateOverheat(ship, battle, maneuver), -1); - weapon.cooldown.configure(2, 1); + weapon.configureCooldown(2, 1); + ship.actions.updateFromShip(ship); + ship.actions.addCustom(weapon); check.equals(TacticalAIHelpers.evaluateOverheat(ship, battle, maneuver), 0); }); }); diff --git a/src/core/ai/TacticalAIHelpers.ts b/src/core/ai/TacticalAIHelpers.ts index 7b3177f..7a36be2 100644 --- a/src/core/ai/TacticalAIHelpers.ts +++ b/src/core/ai/TacticalAIHelpers.ts @@ -14,7 +14,7 @@ module TK.SpaceTac { * Get a list of all playable actions (like the actionbar for player) for a ship */ function getPlayableActions(ship: Ship): Iterator { - let actions = ship.getAvailableActions(); + let actions = ship.actions.listAll(); return ifilter(iarray(actions), action => !action.checkCannotBeApplied(ship)); } @@ -142,10 +142,11 @@ module TK.SpaceTac { * Evaluate doing nothing, between -1 and 1 */ static evaluateIdling(ship: Ship, battle: Battle, maneuver: Maneuver): number { - let lost = ship.getValue("power") - maneuver.getPowerUsage() + ship.getAttribute("power_generation") - ship.getAttribute("power_capacity"); - if (lost > 0) { - return -lost / ship.getAttribute("power_capacity"); - } else if (maneuver.action instanceof TriggerAction || maneuver.action instanceof DeployDroneAction) { + if (maneuver.action instanceof EndTurnAction) { + return -ship.getValue("power") / ship.getAttribute("power_capacity"); + } else if (maneuver.action instanceof TriggerAction || maneuver.action instanceof ToggleAction) { + // TODO Evaluate if drone is useful + // TODO Check there are "interesting" effects if (maneuver.effects.length == 0) { return -1; } else { @@ -215,8 +216,9 @@ module TK.SpaceTac { * Evaluate the cost of overheating an equipment */ static evaluateOverheat(ship: Ship, battle: Battle, maneuver: Maneuver): number { - if (maneuver.action.equipment && maneuver.action.equipment.cooldown.willOverheat()) { - return -Math.min(1, 0.4 * maneuver.action.equipment.cooldown.cooling); + let cooldown = ship.actions.getCooldown(maneuver.action); + if (cooldown.willOverheat()) { + return -Math.min(1, 0.4 * cooldown.cooling); } else { return 0; } diff --git a/src/core/diffs/EndBattleDiff.spec.ts b/src/core/diffs/EndBattleDiff.spec.ts index 5c18bbe..a2ae94c 100644 --- a/src/core/diffs/EndBattleDiff.spec.ts +++ b/src/core/diffs/EndBattleDiff.spec.ts @@ -6,13 +6,6 @@ module TK.SpaceTac.Specs { let ship1 = battle.fleets[0].addShip(); let ship2 = battle.fleets[1].addShip(); - let equ1 = new Equipment(SlotType.Weapon); - equ1.price = 10000; - ship1.addSlot(SlotType.Weapon).attach(equ1); - let equ2 = new Equipment(SlotType.Weapon); - equ2.price = 20000; - ship2.addSlot(SlotType.Weapon).attach(equ2); - battle.start(); TestTools.diffChain(check, battle, [ @@ -21,20 +14,10 @@ module TK.SpaceTac.Specs { check => { check.equals(battle.ended, false, "battle is ongoing"); check.equals(battle.outcome, null, "battle has no outcome"); - check.equals(equ1.wear, 0, "equipment1 wear"); - check.equals(equ2.wear, 0, "equipment2 wear"); - check.equals(battle.stats.getImportant(1), [ - { name: 'Equipment wear (zotys)', attacker: 10000, defender: 20000 } - ], "stats stores equipment value"); }, check => { check.equals(battle.ended, true, "battle is ended"); check.same(nn(battle.outcome).winner, battle.fleets[1], "battle has an outcome"); - check.equals(equ1.wear, 4, "equipment1 wear"); - check.equals(equ2.wear, 4, "equipment2 wear"); - check.equals(battle.stats.getImportant(1), [ - { name: 'Equipment wear (zotys)', attacker: 80, defender: 159 } - ], "stats stores equipment wear"); }, ]); }); diff --git a/src/core/diffs/EndBattleDiff.ts b/src/core/diffs/EndBattleDiff.ts index 9483d85..9a9540e 100644 --- a/src/core/diffs/EndBattleDiff.ts +++ b/src/core/diffs/EndBattleDiff.ts @@ -22,26 +22,10 @@ module TK.SpaceTac { apply(battle: Battle): void { battle.outcome = this.outcome; - - iforeach(battle.iships(), ship => { - ship.listEquipment().forEach(equipment => { - equipment.addWear(this.cycles); - }); - }); - - battle.stats.addFleetsValue(battle.fleets[0], battle.fleets[1], false); } revert(battle: Battle): void { battle.outcome = null; - - battle.stats.addFleetsValue(battle.fleets[0], battle.fleets[1], true); - - iforeach(battle.iships(), ship => { - ship.listEquipment().forEach(equipment => { - equipment.addWear(-this.cycles); - }); - }); } } } diff --git a/src/core/diffs/ProjectileFiredDiff.ts b/src/core/diffs/ProjectileFiredDiff.ts index 0483956..e2af613 100644 --- a/src/core/diffs/ProjectileFiredDiff.ts +++ b/src/core/diffs/ProjectileFiredDiff.ts @@ -7,13 +7,13 @@ module TK.SpaceTac { * This does not do anything, and is just there for animations */ export class ProjectileFiredDiff extends BaseBattleShipDiff { - equipment: RObjectId + action: RObjectId target: Target - constructor(ship: Ship, equipment: Equipment, target: Target) { + constructor(ship: Ship, action: TriggerAction, target: Target) { super(ship); - this.equipment = equipment.id; + this.action = action.id; this.target = target; } } diff --git a/src/core/diffs/ShipActionToggleDiff.spec.ts b/src/core/diffs/ShipActionToggleDiff.spec.ts index 8fcdd26..dbbc135 100644 --- a/src/core/diffs/ShipActionToggleDiff.spec.ts +++ b/src/core/diffs/ShipActionToggleDiff.spec.ts @@ -4,23 +4,22 @@ module TK.SpaceTac.Specs { let battle = new Battle(); let ship = battle.fleets[0].addShip(); - let generator = TestTools.setShipAP(ship, 10); - let weapon = TestTools.addWeapon(ship, 50, 3, 10, 20); - let action = new ToggleAction(weapon, 2); - weapon.action = action; + let generator = TestTools.setShipModel(ship, 100, 0, 10); + let action = new ToggleAction("testtoggle", { power: 2 }); + ship.actions.addCustom(action); TestTools.diffChain(check, battle, [ new ShipActionToggleDiff(ship, action, true), new ShipActionToggleDiff(ship, action, false), ], [ check => { - check.equals(action.activated, false, "not activated"); + check.equals(ship.actions.isToggled(action), false, "not activated"); }, check => { - check.equals(action.activated, true, "activated"); + check.equals(ship.actions.isToggled(action), true, "activated"); }, check => { - check.equals(action.activated, false, "not activated"); + check.equals(ship.actions.isToggled(action), false, "not activated"); }, ]); }); diff --git a/src/core/diffs/ShipActionToggleDiff.ts b/src/core/diffs/ShipActionToggleDiff.ts index 5374978..c3d26d9 100644 --- a/src/core/diffs/ShipActionToggleDiff.ts +++ b/src/core/diffs/ShipActionToggleDiff.ts @@ -19,12 +19,13 @@ module TK.SpaceTac { } applyOnShip(ship: Ship, battle: Battle): void { - let action = ship.getAction(this.action); + let action = ship.actions.getById(this.action); if (action && action instanceof ToggleAction) { - if (action.activated == this.activated) { + let activated = ship.actions.isToggled(action); + if (activated == this.activated) { console.warn("Diff not applied - action already in good state", this, action); } else { - action.activated = this.activated; + ship.actions.toggle(action, this.activated); } } else { console.error("Diff not applied - action not found on ship", this, ship); diff --git a/src/core/diffs/ShipActionUsedDiff.spec.ts b/src/core/diffs/ShipActionUsedDiff.spec.ts index fdf74d9..5271582 100644 --- a/src/core/diffs/ShipActionUsedDiff.spec.ts +++ b/src/core/diffs/ShipActionUsedDiff.spec.ts @@ -4,28 +4,23 @@ module TK.SpaceTac.Specs { let battle = new Battle(); let ship = battle.fleets[0].addShip(); - let generator = TestTools.setShipAP(ship, 10); + let generator = TestTools.setShipModel(ship, 100, 0, 10); let weapon = TestTools.addWeapon(ship, 50, 3, 10, 20); - weapon.cooldown.configure(2, 1); + weapon.configureCooldown(2, 1); + let cooldown = ship.actions.getCooldown(weapon); TestTools.diffChain(check, battle, [ - new ShipActionUsedDiff(ship, nn(weapon.action), Target.newFromShip(ship)), - new ShipActionUsedDiff(ship, nn(weapon.action), Target.newFromShip(ship)), + new ShipActionUsedDiff(ship, weapon, Target.newFromShip(ship)), + new ShipActionUsedDiff(ship, weapon, Target.newFromShip(ship)), ], [ check => { - check.equals(weapon.cooldown.getRemainingUses(), 2, "cooldown"); - check.equals(weapon.wear, 0, "weapon wear"); - check.equals(generator.wear, 0, "generator wear"); + check.equals(cooldown.getRemainingUses(), 2, "cooldown"); }, check => { - check.equals(weapon.cooldown.getRemainingUses(), 1, "cooldown"); - check.equals(weapon.wear, 1, "weapon wear"); - check.equals(generator.wear, 1, "generator wear"); + check.equals(cooldown.getRemainingUses(), 1, "cooldown"); }, check => { - check.equals(weapon.cooldown.getRemainingUses(), 0, "cooldown"); - check.equals(weapon.wear, 2, "weapon wear"); - check.equals(generator.wear, 2, "generator wear"); + check.equals(cooldown.getRemainingUses(), 0, "cooldown"); }, ]); }); diff --git a/src/core/diffs/ShipActionUsedDiff.ts b/src/core/diffs/ShipActionUsedDiff.ts index c92ea6e..41a73b3 100644 --- a/src/core/diffs/ShipActionUsedDiff.ts +++ b/src/core/diffs/ShipActionUsedDiff.ts @@ -23,40 +23,30 @@ module TK.SpaceTac { } protected applyOnShip(ship: Ship, battle: Battle): void { - let action = first(ship.getAvailableActions(), action => action.is(this.action)); + let action = ship.actions.getById(this.action); if (!action) { console.error("Action failed - not found on ship", this, ship); return; } - if (action.cooldown.canUse()) { - action.cooldown.use(1); + if (ship.actions.isUsable(action)) { + ship.actions.storeUsage(action, 1); } else { console.error("Action apply failed - in cooldown", this, ship); return; } - - if (action.equipment) { - action.equipment.addWear(1); - ship.listEquipment(SlotType.Power).forEach(equipment => equipment.addWear(1)); - } } protected revertOnShip(ship: Ship, battle: Battle): void { - let action = first(ship.getAvailableActions(), action => action.is(this.action)); + let action = ship.actions.getById(this.action); if (!action) { console.error("Action revert failed - not found on ship", this, ship); return; } - action.cooldown.use(-1); - - if (action.equipment) { - action.equipment.addWear(-1); - ship.listEquipment(SlotType.Power).forEach(equipment => equipment.addWear(-1)); - } + ship.actions.storeUsage(action, -1); } } } diff --git a/src/core/diffs/ShipCooldownDiff.spec.ts b/src/core/diffs/ShipCooldownDiff.spec.ts index 9f3213f..a310ee8 100644 --- a/src/core/diffs/ShipCooldownDiff.spec.ts +++ b/src/core/diffs/ShipCooldownDiff.spec.ts @@ -4,24 +4,25 @@ module TK.SpaceTac.Specs { let battle = TestTools.createBattle(); let ship = battle.play_order[0]; let weapon = TestTools.addWeapon(ship); - weapon.cooldown.configure(1, 3); - weapon.cooldown.use(); + weapon.configureCooldown(1, 3); + let cooldown = ship.actions.getCooldown(weapon); + cooldown.use(); TestTools.diffChain(check, battle, [ new ShipCooldownDiff(ship, weapon, 1), new ShipCooldownDiff(ship, weapon, 2), ], [ check => { - check.equals(weapon.cooldown.heat, 3, "heat"); - check.equals(weapon.cooldown.uses, 1, "uses"); + check.equals(cooldown.heat, 3, "heat"); + check.equals(cooldown.uses, 1, "uses"); }, check => { - check.equals(weapon.cooldown.heat, 2, "heat"); - check.equals(weapon.cooldown.uses, 1, "uses"); + check.equals(cooldown.heat, 2, "heat"); + check.equals(cooldown.uses, 1, "uses"); }, check => { - check.equals(weapon.cooldown.heat, 0, "heat"); - check.equals(weapon.cooldown.uses, 0, "uses"); + check.equals(cooldown.heat, 0, "heat"); + check.equals(cooldown.uses, 0, "uses"); }, ]); }); diff --git a/src/core/diffs/ShipCooldownDiff.ts b/src/core/diffs/ShipCooldownDiff.ts index 4268130..62ae861 100644 --- a/src/core/diffs/ShipCooldownDiff.ts +++ b/src/core/diffs/ShipCooldownDiff.ts @@ -2,43 +2,45 @@ module TK.SpaceTac { /** - * A ship's equipment cools down + * A ship's action cools down */ export class ShipCooldownDiff extends BaseBattleShipDiff { - // Equipment to cool - equipment: RObjectId + // Action to cool + action: RObjectId // Quantity of heat to dissipate heat: number - constructor(ship: Ship | RObjectId, equipment: Equipment | RObjectId, heat: number) { + constructor(ship: Ship | RObjectId, action: BaseAction | RObjectId, heat: number) { super(ship); - this.equipment = (equipment instanceof Equipment) ? equipment.id : equipment; + this.action = (action instanceof BaseAction) ? action.id : action; this.heat = heat; } applyOnShip(ship: Ship, battle: Battle) { - let equipment = ship.getEquipment(this.equipment); - if (equipment) { - equipment.cooldown.heat -= this.heat; - if (equipment.cooldown.heat == 0) { - equipment.cooldown.uses = 0; + let action = ship.actions.getById(this.action); + if (action) { + let cooldown = ship.actions.getCooldown(action); + cooldown.heat -= this.heat; + if (cooldown.heat == 0) { + cooldown.uses = 0; } } else { - console.error("Cannot apply diff, equipment not found", this); + console.error("Cannot apply diff, action not found", this, ship.actions); } } revertOnShip(ship: Ship, battle: Battle) { - let equipment = ship.getEquipment(this.equipment); - if (equipment) { - if (equipment.cooldown.heat == 0) { - equipment.cooldown.uses = equipment.cooldown.overheat; + let action = ship.actions.getById(this.action); + if (action) { + let cooldown = ship.actions.getCooldown(action); + if (cooldown.heat == 0) { + cooldown.uses = cooldown.overheat; } - equipment.cooldown.heat += this.heat; + cooldown.heat += this.heat; } else { - console.error("Cannot revert diff, equipment not found", this); + console.error("Cannot revert diff, action not found", this, ship.actions); } } } diff --git a/src/core/diffs/ShipDamageDiff.spec.ts b/src/core/diffs/ShipDamageDiff.spec.ts index 8f121ca..a6a4888 100644 --- a/src/core/diffs/ShipDamageDiff.spec.ts +++ b/src/core/diffs/ShipDamageDiff.spec.ts @@ -3,7 +3,7 @@ module TK.SpaceTac.Specs { test.case("applies and reverts", check => { let battle = TestTools.createBattle(); let ship = battle.play_order[0]; - let [hull, shield] = TestTools.setShipHP(ship, 80, 100); + TestTools.setShipModel(ship, 80, 100); TestTools.diffChain(check, battle, [ new ShipDamageDiff(ship, 0, 10), @@ -11,26 +11,18 @@ module TK.SpaceTac.Specs { new ShipDamageDiff(ship, 30, 90), ], [ check => { - check.equals(hull.wear, 0, "hull wear"); - check.equals(shield.wear, 0, "shield wear"); check.equals(ship.getValue("hull"), 80, "hull value"); check.equals(ship.getValue("shield"), 100, "shield value"); }, check => { - check.equals(hull.wear, 0, "hull wear"); - check.equals(shield.wear, 1, "shield wear"); check.equals(ship.getValue("hull"), 80, "hull value"); check.equals(ship.getValue("shield"), 100, "shield value"); }, check => { - check.equals(hull.wear, 2, "hull wear"); - check.equals(shield.wear, 1, "shield wear"); check.equals(ship.getValue("hull"), 80, "hull value"); check.equals(ship.getValue("shield"), 100, "shield value"); }, check => { - check.equals(hull.wear, 5, "hull wear"); - check.equals(shield.wear, 10, "shield wear"); check.equals(ship.getValue("hull"), 80, "hull value"); check.equals(ship.getValue("shield"), 100, "shield value"); }, diff --git a/src/core/diffs/ShipDamageDiff.ts b/src/core/diffs/ShipDamageDiff.ts index 99b52ba..f4f8192 100644 --- a/src/core/diffs/ShipDamageDiff.ts +++ b/src/core/diffs/ShipDamageDiff.ts @@ -4,7 +4,7 @@ module TK.SpaceTac { /** * A ship takes damage (to hull or shield) * - * This does not apply the damage on ship values (there are ShipValueDiff for this), but apply equipment wear. + * This is only informative, and does not apply the damage on ship values (there are ShipValueDiff for this). */ export class ShipDamageDiff extends BaseBattleShipDiff { // Damage to hull @@ -23,23 +23,5 @@ module TK.SpaceTac { this.shield = shield; this.theoretical = theoretical; } - - protected applyOnShip(ship: Ship, battle: Battle): void { - if (this.shield > 0) { - ship.listEquipment(SlotType.Shield).forEach(equipment => equipment.addWear(Math.ceil(this.shield * 0.1))); - } - if (this.hull > 0) { - ship.listEquipment(SlotType.Hull).forEach(equipment => equipment.addWear(Math.ceil(this.hull * 0.1))); - } - } - - protected revertOnShip(ship: Ship, battle: Battle): void { - if (this.shield > 0) { - ship.listEquipment(SlotType.Shield).forEach(equipment => equipment.addWear(-Math.ceil(this.shield * 0.1))); - } - if (this.hull > 0) { - ship.listEquipment(SlotType.Hull).forEach(equipment => equipment.addWear(-Math.ceil(this.hull * 0.1))); - } - } } } diff --git a/src/core/diffs/ShipMoveDiff.spec.ts b/src/core/diffs/ShipMoveDiff.spec.ts index 204cb26..3054df5 100644 --- a/src/core/diffs/ShipMoveDiff.spec.ts +++ b/src/core/diffs/ShipMoveDiff.spec.ts @@ -5,7 +5,7 @@ module TK.SpaceTac.Specs { let ship = battle.fleets[0].addShip(); check.equals(ship.location, new ArenaLocationAngle(0, 0, 0)); - let engine = new Equipment(); + let engine = new MoveAction(); let event = new ShipMoveDiff(ship, ship.location, new ArenaLocationAngle(50, 20, 1.2), engine); event.apply(battle); check.equals(ship.location, new ArenaLocationAngle(50, 20, 1.2)); diff --git a/src/core/diffs/ShipMoveDiff.ts b/src/core/diffs/ShipMoveDiff.ts index 32bb740..62183bc 100644 --- a/src/core/diffs/ShipMoveDiff.ts +++ b/src/core/diffs/ShipMoveDiff.ts @@ -12,9 +12,9 @@ module TK.SpaceTac { end: ArenaLocationAngle // Engine used - engine: Equipment | null + engine: MoveAction | null - constructor(ship: Ship | RObjectId, start: ArenaLocationAngle, end: ArenaLocationAngle, engine: Equipment | null = null) { + constructor(ship: Ship | RObjectId, start: ArenaLocationAngle, end: ArenaLocationAngle, engine: MoveAction | null = null) { super(ship); this.start = start; diff --git a/src/core/effects/AttributeMultiplyEffect.spec.ts b/src/core/effects/AttributeMultiplyEffect.spec.ts index adfb26b..138463f 100644 --- a/src/core/effects/AttributeMultiplyEffect.spec.ts +++ b/src/core/effects/AttributeMultiplyEffect.spec.ts @@ -22,8 +22,8 @@ module TK.SpaceTac { }); test.case("has a description", check => { - let effect = new AttributeMultiplyEffect("power_generation", 20); - check.equals(effect.getDescription(), "power generation +20%"); + let effect = new AttributeMultiplyEffect("power_capacity", 20); + check.equals(effect.getDescription(), "power capacity +20%"); }); }); } diff --git a/src/core/effects/CooldownEffect.spec.ts b/src/core/effects/CooldownEffect.spec.ts index e850226..8b34063 100644 --- a/src/core/effects/CooldownEffect.spec.ts +++ b/src/core/effects/CooldownEffect.spec.ts @@ -4,29 +4,29 @@ module TK.SpaceTac { let battle = new Battle(); let ship = battle.fleets[0].addShip(); let weapons = [TestTools.addWeapon(ship), TestTools.addWeapon(ship), TestTools.addWeapon(ship)]; - weapons.forEach(weapon => weapon.cooldown.configure(1, 3)); - check.equals(weapons.map(weapon => weapon.cooldown.heat), [0, 0, 0]); + weapons.forEach(weapon => weapon.configureCooldown(1, 3)); + check.equals(weapons.map(weapon => ship.actions.getCooldown(weapon).heat), [0, 0, 0]); let effect = new CooldownEffect(0, 0); battle.applyDiffs(effect.getOnDiffs(ship, ship, 1)); - check.equals(weapons.map(weapon => weapon.cooldown.heat), [0, 0, 0]); + check.equals(weapons.map(weapon => ship.actions.getCooldown(weapon).heat), [0, 0, 0]); - weapons.forEach(weapon => weapon.cooldown.use()); - check.equals(weapons.map(weapon => weapon.cooldown.heat), [3, 3, 3]); + weapons.forEach(weapon => ship.actions.storeUsage(weapon)); + check.equals(weapons.map(weapon => ship.actions.getCooldown(weapon).heat), [3, 3, 3]); battle.applyDiffs(effect.getOnDiffs(ship, ship, 1)); - check.equals(weapons.map(weapon => weapon.cooldown.heat), [0, 0, 0]); + check.equals(weapons.map(weapon => ship.actions.getCooldown(weapon).heat), [0, 0, 0]); - weapons.forEach(weapon => weapon.cooldown.use()); - check.equals(weapons.map(weapon => weapon.cooldown.heat), [3, 3, 3]); + weapons.forEach(weapon => ship.actions.storeUsage(weapon)); + check.equals(weapons.map(weapon => ship.actions.getCooldown(weapon).heat), [3, 3, 3]); effect = new CooldownEffect(1, 0); battle.applyDiffs(effect.getOnDiffs(ship, ship, 1)); - check.equals(weapons.map(weapon => weapon.cooldown.heat), [2, 2, 2]); + check.equals(weapons.map(weapon => ship.actions.getCooldown(weapon).heat), [2, 2, 2]); effect = new CooldownEffect(1, 2); battle.applyDiffs(effect.getOnDiffs(ship, ship, 1)); - check.equals(weapons.map(weapon => weapon.cooldown.heat).sort(), [1, 1, 2]); + check.equals(weapons.map(weapon => ship.actions.getCooldown(weapon).heat).sort(), [1, 1, 2]); }) test.case("builds a textual description", check => { diff --git a/src/core/effects/CooldownEffect.ts b/src/core/effects/CooldownEffect.ts index f3f6524..3e6981c 100644 --- a/src/core/effects/CooldownEffect.ts +++ b/src/core/effects/CooldownEffect.ts @@ -19,14 +19,14 @@ module TK.SpaceTac { } getOnDiffs(ship: Ship, source: Ship | Drone, success: number): BaseBattleDiff[] { - let equipments = ship.listEquipment().filter(equ => equ.cooldown.heat > 0); + let actions = ship.actions.listOverheated(); - if (this.maxcount && equipments.length > this.maxcount) { + if (this.maxcount && actions.length > this.maxcount) { let random = RandomGenerator.global; - equipments = random.sample(equipments, this.maxcount); + actions = random.sample(actions, this.maxcount); } - return equipments.map(equ => new ShipCooldownDiff(ship, equ, this.cooling || equ.cooldown.heat)); + return actions.map(action => new ShipCooldownDiff(ship, action, this.cooling || ship.actions.getCooldown(action).heat)); } isBeneficial(): boolean { diff --git a/src/core/effects/DamageEffect.spec.ts b/src/core/effects/DamageEffect.spec.ts index 4f6d582..d05a7ee 100644 --- a/src/core/effects/DamageEffect.spec.ts +++ b/src/core/effects/DamageEffect.spec.ts @@ -1,36 +1,30 @@ module TK.SpaceTac.Specs { testing("DamageEffect", test => { - test.case("applies damage and wear", check => { + test.case("applies damage", check => { let battle = new Battle(); let ship = battle.fleets[0].addShip(); + TestTools.setShipModel(ship, 150, 400); - TestTools.setShipHP(ship, 150, 400); - let hull = ship.listEquipment(SlotType.Hull)[0]; - let shield = ship.listEquipment(SlotType.Shield)[0]; - ship.restoreHealth(); - - function checkValues(desc: string, hull_value: number, shield_value: number, hull_wear: number, shield_wear: number) { + function checkValues(desc: string, hull_value: number, shield_value: number) { check.in(desc, check => { check.equals(ship.getValue("hull"), hull_value, "hull value"); check.equals(ship.getValue("shield"), shield_value, "shield value"); - check.equals(hull.wear, hull_wear, "hull wear"); - check.equals(shield.wear, shield_wear, "shield wear"); }); } - checkValues("initial", 150, 400, 0, 0); + checkValues("initial", 150, 400); battle.applyDiffs(new DamageEffect(50).getOnDiffs(ship, ship, 1)); - checkValues("after 50 damage", 150, 350, 0, 5); + checkValues("after 50 damage", 150, 350); battle.applyDiffs(new DamageEffect(250).getOnDiffs(ship, ship, 1)); - checkValues("after 250 damage", 150, 100, 0, 30); + checkValues("after 250 damage", 150, 100); battle.applyDiffs(new DamageEffect(201).getOnDiffs(ship, ship, 1)); - checkValues("after 201 damage", 49, 0, 11, 40); + checkValues("after 201 damage", 49, 0); battle.applyDiffs(new DamageEffect(8000).getOnDiffs(ship, ship, 1)); - checkValues("after 8000 damage", 0, 0, 16, 40); + checkValues("after 8000 damage", 0, 0); }); test.case("gets a textual description", check => { @@ -40,7 +34,7 @@ module TK.SpaceTac.Specs { test.case("applies damage modifiers", check => { let ship = new Ship(); - TestTools.setShipHP(ship, 1000, 1000); + TestTools.setShipModel(ship, 1000, 1000); let damage = new DamageEffect(200); check.equals(damage.getEffectiveDamage(ship, 1), new ShipDamageDiff(ship, 0, 200)); diff --git a/src/core/effects/ValueTransferEffect.spec.ts b/src/core/effects/ValueTransferEffect.spec.ts index ffc7f7f..a211bbc 100644 --- a/src/core/effects/ValueTransferEffect.spec.ts +++ b/src/core/effects/ValueTransferEffect.spec.ts @@ -3,10 +3,10 @@ module TK.SpaceTac.Specs { test.case("takes or gives value", check => { let battle = new Battle(); let ship1 = battle.fleets[0].addShip(); - TestTools.setShipHP(ship1, 100, 50); + TestTools.setShipModel(ship1, 100, 50); ship1.setValue("hull", 10); let ship2 = battle.fleets[0].addShip(); - TestTools.setShipHP(ship2, 100, 50); + TestTools.setShipModel(ship2, 100, 50); let effect = new ValueTransferEffect("hull", -30); battle.applyDiffs(effect.getOnDiffs(ship2, ship1, 1)); diff --git a/src/core/equipments/DamageProtector.spec.ts b/src/core/equipments/DamageProtector.spec.ts deleted file mode 100644 index 0530595..0000000 --- a/src/core/equipments/DamageProtector.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -module TK.SpaceTac.Specs { - testing("DamageProtector", test => { - test.case("generates equipment based on level", check => { - let template = new Equipments.DamageProtector(); - - let equipment = template.generate(1); - check.equals(equipment.requirements, { "skill_time": 3 }); - compare_toggle_action(check, equipment.action, new ToggleAction(equipment, 2, 300, [ - new DamageModifierEffect(-17) - ])); - - equipment = template.generate(2); - check.equals(equipment.requirements, { "skill_time": 4 }); - compare_toggle_action(check, equipment.action, new ToggleAction(equipment, 2, 310, [ - new DamageModifierEffect(-22) - ])); - - equipment = template.generate(3); - check.equals(equipment.requirements, { "skill_time": 5 }); - compare_toggle_action(check, equipment.action, new ToggleAction(equipment, 2, 322, [ - new DamageModifierEffect(-28) - ])); - - equipment = template.generate(10); - check.equals(equipment.requirements, { "skill_time": 22 }); - compare_toggle_action(check, equipment.action, new ToggleAction(equipment, 8, 462, [ - new DamageModifierEffect(-60) - ])); - }); - }); -} diff --git a/src/core/equipments/DamageProtector.ts b/src/core/equipments/DamageProtector.ts deleted file mode 100644 index 860c658..0000000 --- a/src/core/equipments/DamageProtector.ts +++ /dev/null @@ -1,14 +0,0 @@ -/// - -module TK.SpaceTac.Equipments { - export class DamageProtector extends LootTemplate { - constructor() { - super(SlotType.Weapon, "Damage Protector", "Extend a time-displacement subfield, to reduce damage taken by ships around", 145); - - this.setSkillsRequirements({ "skill_time": leveled(3) }); - this.addToggleAction(leveled(2, 0.4), leveled(300, 10), [ - new EffectTemplate(new DamageModifierEffect(), { factor: imap(leveled(-20), x => x * (100 / (100 - x))) }) - ]); - } - } -} diff --git a/src/core/equipments/Engines.spec.ts b/src/core/equipments/Engines.spec.ts deleted file mode 100644 index 77ca0df..0000000 --- a/src/core/equipments/Engines.spec.ts +++ /dev/null @@ -1,99 +0,0 @@ -module TK.SpaceTac.Specs { - testing("Engines", test => { - test.case("generates RocketEngine based on level", check => { - let template = new Equipments.RocketEngine(); - - let equipment = template.generate(1); - check.equals(equipment.requirements, { "skill_materials": 1 }); - compare_effects(check, equipment.effects, [new AttributeEffect("maneuvrability", 2)]); - check.equals(equipment.cooldown, new Cooldown(2, 0)); - compare_action(check, equipment.action, new MoveAction(equipment, 200, 120, 70)); - check.equals(equipment.price, 120); - - equipment = template.generate(2); - check.equals(equipment.requirements, { "skill_materials": 2 }); - compare_effects(check, equipment.effects, [new AttributeEffect("maneuvrability", 2)]); - check.equals(equipment.cooldown, new Cooldown(2, 0)); - compare_action(check, equipment.action, new MoveAction(equipment, 210, 120, 70)); - check.equals(equipment.price, 420); - - equipment = template.generate(3); - check.equals(equipment.requirements, { "skill_materials": 3 }); - compare_effects(check, equipment.effects, [new AttributeEffect("maneuvrability", 3)]); - check.equals(equipment.cooldown, new Cooldown(2, 0)); - compare_action(check, equipment.action, new MoveAction(equipment, 220, 120, 70)); - check.equals(equipment.price, 1020); - - equipment = template.generate(10); - check.equals(equipment.requirements, { "skill_materials": 17 }); - compare_effects(check, equipment.effects, [new AttributeEffect("maneuvrability", 14)]); - check.equals(equipment.cooldown, new Cooldown(2, 0)); - compare_action(check, equipment.action, new MoveAction(equipment, 290, 120, 70)); - check.equals(equipment.price, 13620); - }); - - test.case("generates IonThruster based on level", check => { - let template = new Equipments.IonThruster(); - - let equipment = template.generate(1); - check.equals(equipment.requirements, { "skill_photons": 1 }); - compare_effects(check, equipment.effects, [new AttributeEffect("maneuvrability", 1)]); - check.equals(equipment.cooldown, new Cooldown(3, 1)); - compare_action(check, equipment.action, new MoveAction(equipment, 120, 120, 80)); - check.equals(equipment.price, 150); - - equipment = template.generate(2); - check.equals(equipment.requirements, { "skill_photons": 2 }); - compare_effects(check, equipment.effects, [new AttributeEffect("maneuvrability", 2)]); - check.equals(equipment.cooldown, new Cooldown(3, 1)); - compare_action(check, equipment.action, new MoveAction(equipment, 130, 120, 80)); - check.equals(equipment.price, 525); - - equipment = template.generate(3); - check.equals(equipment.requirements, { "skill_photons": 3 }); - compare_effects(check, equipment.effects, [new AttributeEffect("maneuvrability", 3)]); - check.equals(equipment.cooldown, new Cooldown(3, 1)); - compare_action(check, equipment.action, new MoveAction(equipment, 140, 120, 80)); - check.equals(equipment.price, 1275); - - equipment = template.generate(10); - check.equals(equipment.requirements, { "skill_photons": 17 }); - compare_effects(check, equipment.effects, [new AttributeEffect("maneuvrability", 17)]); - check.equals(equipment.cooldown, new Cooldown(3, 1)); - compare_action(check, equipment.action, new MoveAction(equipment, 210, 120, 80)); - check.equals(equipment.price, 17025); - }); - - test.case("generates VoidhawkEngine based on level", check => { - let template = new Equipments.VoidhawkEngine(); - - let equipment = template.generate(1); - check.equals(equipment.requirements, { "skill_gravity": 2 }); - compare_effects(check, equipment.effects, [new AttributeEffect("maneuvrability", -3)]); - check.equals(equipment.cooldown, new Cooldown(1, 0)); - compare_action(check, equipment.action, new MoveAction(equipment, 2000, 270, 0)); - check.equals(equipment.price, 300); - - equipment = template.generate(2); - check.equals(equipment.requirements, { "skill_gravity": 3 }); - compare_effects(check, equipment.effects, [new AttributeEffect("maneuvrability", -4)]); - check.equals(equipment.cooldown, new Cooldown(1, 0)); - compare_action(check, equipment.action, new MoveAction(equipment, 2000, 245, 0)); - check.equals(equipment.price, 1050); - - equipment = template.generate(3); - check.equals(equipment.requirements, { "skill_gravity": 5 }); - compare_effects(check, equipment.effects, [new AttributeEffect("maneuvrability", -4)]); - check.equals(equipment.cooldown, new Cooldown(1, 0)); - compare_action(check, equipment.action, new MoveAction(equipment, 2000, 224, 0)); - check.equals(equipment.price, 2550); - - equipment = template.generate(10); - check.equals(equipment.requirements, { "skill_gravity": 26 }); - compare_effects(check, equipment.effects, [new AttributeEffect("maneuvrability", -5)]); - check.equals(equipment.cooldown, new Cooldown(2, 0)); - compare_action(check, equipment.action, new MoveAction(equipment, 2000, 155, 0)); - check.equals(equipment.price, 34050); - }); - }); -} diff --git a/src/core/equipments/Engines.ts b/src/core/equipments/Engines.ts deleted file mode 100644 index 0a1fd32..0000000 --- a/src/core/equipments/Engines.ts +++ /dev/null @@ -1,36 +0,0 @@ -/// - -module TK.SpaceTac.Equipments { - export class RocketEngine extends LootTemplate { - constructor() { - super(SlotType.Engine, "Rocket Engine", "First-era conventional deep-space engine, based on gas exhausts pushed through a nozzle", 120); - - this.setSkillsRequirements({ "skill_materials": leveled(1, 1) }); - this.setCooldown(irepeat(2), leveled(0)); - this.addAttributeEffect("maneuvrability", leveled(2)); - this.addMoveAction(leveled(200, 10, 0), undefined, irepeat(70)); - } - } - - export class IonThruster extends LootTemplate { - constructor() { - super(SlotType.Engine, "Ion Thruster", "Electric propulsion based on accelerating ions through an electrostatic grid", 150); - - this.setSkillsRequirements({ "skill_photons": leveled(1, 1) }); - this.setCooldown(irepeat(3), irepeat(1)); - this.addAttributeEffect("maneuvrability", leveled(1, 1)); - this.addMoveAction(leveled(120, 10, 0)); - } - } - - export class VoidhawkEngine extends LootTemplate { - constructor() { - super(SlotType.Engine, "VoidHawk Engine", "Mid-range gravity field warp generator, allowing to make small jumps", 300); - - this.setSkillsRequirements({ "skill_gravity": leveled(2, 1.5) }); - this.setCooldown(leveled(1, 0.2, 0), irepeat(0)); - this.addAttributeEffect("maneuvrability", leveled(-3, -0.1)); - this.addMoveAction(irepeat(2000), imap(leveled(1), x => 420 - (300 * x / (x + 1))), irepeat(0)); - } - } -} diff --git a/src/core/equipments/Generators.spec.ts b/src/core/equipments/Generators.spec.ts deleted file mode 100644 index 2894b9e..0000000 --- a/src/core/equipments/Generators.spec.ts +++ /dev/null @@ -1,83 +0,0 @@ -module TK.SpaceTac.Specs { - testing("Generators", test => { - test.case("generates NuclearReactor based on level", check => { - let template = new Equipments.NuclearReactor(); - - let equipment = template.generate(1); - check.equals(equipment.requirements, { "skill_photons": 1 }); - compare_effects(check, equipment.effects, [ - new AttributeEffect("maneuvrability", 1), - new AttributeEffect("power_capacity", 7), - new AttributeEffect("power_generation", 4), - ]); - check.equals(equipment.price, 395); - - equipment = template.generate(2); - check.equals(equipment.requirements, { "skill_photons": 3 }); - compare_effects(check, equipment.effects, [ - new AttributeEffect("maneuvrability", 2), - new AttributeEffect("power_capacity", 7), - new AttributeEffect("power_generation", 5), - ]); - check.equals(equipment.price, 1382); - - equipment = template.generate(3); - check.equals(equipment.requirements, { "skill_photons": 5 }); - compare_effects(check, equipment.effects, [ - new AttributeEffect("maneuvrability", 3), - new AttributeEffect("power_capacity", 8), - new AttributeEffect("power_generation", 5), - ]); - check.equals(equipment.price, 3357); - - equipment = template.generate(10); - check.equals(equipment.requirements, { "skill_photons": 33 }); - compare_effects(check, equipment.effects, [ - new AttributeEffect("maneuvrability", 10), - new AttributeEffect("power_capacity", 15), - new AttributeEffect("power_generation", 12), - ]); - check.equals(equipment.price, 44832); - }) - - test.case("generates KelvinGenerator based on level", check => { - let template = new Equipments.KelvinGenerator(); - - let equipment = template.generate(1); - check.equals(equipment.requirements, { "skill_time": 1 }); - compare_effects(check, equipment.effects, [ - new AttributeEffect("power_capacity", 5), - new AttributeEffect("power_generation", 4), - ]); - compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new CooldownEffect(1, 1)])); - check.equals(equipment.price, 420); - - equipment = template.generate(2); - check.equals(equipment.requirements, { "skill_time": 2 }); - compare_effects(check, equipment.effects, [ - new AttributeEffect("power_capacity", 6), - new AttributeEffect("power_generation", 4), - ]); - compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new CooldownEffect(1, 1)])); - check.equals(equipment.price, 1470); - - equipment = template.generate(3); - check.equals(equipment.requirements, { "skill_time": 4, "skill_gravity": 1 }); - compare_effects(check, equipment.effects, [ - new AttributeEffect("power_capacity", 6), - new AttributeEffect("power_generation", 5), - ]); - compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new CooldownEffect(1, 1)])); - check.equals(equipment.price, 3570); - - equipment = template.generate(10); - check.equals(equipment.requirements, { "skill_time": 28, "skill_gravity": 6 }); - compare_effects(check, equipment.effects, [ - new AttributeEffect("power_capacity", 13), - new AttributeEffect("power_generation", 12), - ]); - compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new CooldownEffect(4, 7)], 7)); - check.equals(equipment.price, 47670); - }) - }) -} diff --git a/src/core/equipments/Generators.ts b/src/core/equipments/Generators.ts deleted file mode 100644 index ad11d2f..0000000 --- a/src/core/equipments/Generators.ts +++ /dev/null @@ -1,27 +0,0 @@ -/// - -module TK.SpaceTac.Equipments { - export class NuclearReactor extends LootTemplate { - constructor() { - super(SlotType.Power, "Nuclear Reactor", "A standard nuclear power core, drawing power from atom fusion cycles", 395); - - this.setSkillsRequirements({ "skill_photons": leveled(1, 2) }); - this.addAttributeEffect("maneuvrability", leveled(1, 1, 0)); - this.addAttributeEffect("power_capacity", leveled(7, 0.5)); - this.addAttributeEffect("power_generation", leveled(4.5, 0.5)); - } - } - - export class KelvinGenerator extends LootTemplate { - constructor() { - super(SlotType.Power, "Kelvin Generator", "A power generator operating at ultra-low temperature, improving equipment cooldown", 420); - - this.setSkillsRequirements({ "skill_time": leveled(1, 1.7), "skill_gravity": leveled(0.3, 0.4) }); - this.addAttributeEffect("power_capacity", leveled(5.5, 0.5)); - this.addAttributeEffect("power_generation", leveled(4, 0.5)); - this.addTriggerAction(leveled(1, 0.4), [ - new EffectTemplate(new CooldownEffect(), { cooling: leveled(1, 0.2), maxcount: leveled(1, 0.4) }) - ]) - } - } -} diff --git a/src/core/equipments/Hulls.spec.ts b/src/core/equipments/Hulls.spec.ts deleted file mode 100644 index 5579838..0000000 --- a/src/core/equipments/Hulls.spec.ts +++ /dev/null @@ -1,107 +0,0 @@ -module TK.SpaceTac.Specs { - testing("Hulls", test => { - test.case("generates IronHull based on level", check => { - let template = new Equipments.IronHull(); - - let equipment = template.generate(1); - check.equals(equipment.requirements, { "skill_materials": 1 }); - compare_effects(check, equipment.effects, [new AttributeEffect("hull_capacity", 100)]); - check.equals(equipment.price, 100); - - equipment = template.generate(2); - check.equals(equipment.requirements, { "skill_materials": 2 }); - compare_effects(check, equipment.effects, [new AttributeEffect("hull_capacity", 140)]); - check.equals(equipment.price, 350); - - equipment = template.generate(3); - check.equals(equipment.requirements, { "skill_materials": 3 }); - compare_effects(check, equipment.effects, [new AttributeEffect("hull_capacity", 188)]); - check.equals(equipment.price, 850); - - equipment = template.generate(10); - check.equals(equipment.requirements, { "skill_materials": 17 }); - compare_effects(check, equipment.effects, [new AttributeEffect("hull_capacity", 748)]); - check.equals(equipment.price, 11350); - }); - - test.case("generates HardCoatedHull based on level", check => { - let template = new Equipments.HardCoatedHull(); - - let equipment = template.generate(1); - check.equals(equipment.requirements, { "skill_materials": 2 }); - compare_effects(check, equipment.effects, [ - new AttributeEffect("hull_capacity", 130), - new AttributeEffect("maneuvrability", -2), - ]); - check.equals(equipment.price, 124); - - equipment = template.generate(2); - check.equals(equipment.requirements, { "skill_materials": 5 }); - compare_effects(check, equipment.effects, [ - new AttributeEffect("hull_capacity", 182), - new AttributeEffect("maneuvrability", -3), - ]); - check.equals(equipment.price, 434); - - equipment = template.generate(3); - check.equals(equipment.requirements, { "skill_materials": 8 }); - compare_effects(check, equipment.effects, [ - new AttributeEffect("hull_capacity", 244), - new AttributeEffect("maneuvrability", -5), - ]); - check.equals(equipment.price, 1054); - - equipment = template.generate(10); - check.equals(equipment.requirements, { "skill_materials": 50 }); - compare_effects(check, equipment.effects, [ - new AttributeEffect("hull_capacity", 972), - new AttributeEffect("maneuvrability", -19), - ]); - check.equals(equipment.price, 14074); - }); - - test.case("generates FractalHull based on level", check => { - let template = new Equipments.FractalHull(); - - let equipment = template.generate(1); - check.equals(equipment.requirements, { "skill_quantum": 1 }); - compare_effects(check, equipment.effects, [ - new AttributeEffect("hull_capacity", 60), - new AttributeEffect("precision", 2), - ]); - compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new ValueEffect("hull", 60)], 1)); - check.equals(equipment.cooldown, new Cooldown(1, 4)); - check.equals(equipment.price, 250); - - equipment = template.generate(2); - check.equals(equipment.requirements, { "skill_quantum": 3 }); - compare_effects(check, equipment.effects, [ - new AttributeEffect("hull_capacity", 84), - new AttributeEffect("precision", 2), - ]); - compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new ValueEffect("hull", 84)], 1)); - check.equals(equipment.cooldown, new Cooldown(1, 4)); - check.equals(equipment.price, 875); - - equipment = template.generate(3); - check.equals(equipment.requirements, { "skill_quantum": 5 }); - compare_effects(check, equipment.effects, [ - new AttributeEffect("hull_capacity", 112), - new AttributeEffect("precision", 3), - ]); - compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new ValueEffect("hull", 112)], 1)); - check.equals(equipment.cooldown, new Cooldown(1, 4)); - check.equals(equipment.price, 2125); - - equipment = template.generate(10); - check.equals(equipment.requirements, { "skill_quantum": 33 }); - compare_effects(check, equipment.effects, [ - new AttributeEffect("hull_capacity", 448), - new AttributeEffect("precision", 14), - ]); - compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new ValueEffect("hull", 448)], 2)); - check.equals(equipment.cooldown, new Cooldown(1, 4)); - check.equals(equipment.price, 28375); - }); - }); -} diff --git a/src/core/equipments/Hulls.ts b/src/core/equipments/Hulls.ts deleted file mode 100644 index 6955b41..0000000 --- a/src/core/equipments/Hulls.ts +++ /dev/null @@ -1,36 +0,0 @@ -/// - -module TK.SpaceTac.Equipments { - export class IronHull extends LootTemplate { - constructor() { - super(SlotType.Hull, "Iron Hull", "Protective hull, based on layered iron alloys"); - - this.setSkillsRequirements({ "skill_materials": leveled(1, 1) }); - this.addAttributeEffect("hull_capacity", leveled(100)); - } - } - - export class HardCoatedHull extends LootTemplate { - constructor() { - super(SlotType.Hull, "Hard Coated Hull", "Hardened hull, with titanium coating", 124); - - this.setSkillsRequirements({ "skill_materials": leveled(2, 3) }); - this.addAttributeEffect("hull_capacity", leveled(130)); - this.addAttributeEffect("maneuvrability", leveled(-2, -1)); - } - } - - export class FractalHull extends LootTemplate { - constructor() { - super(SlotType.Hull, "Fractal Hull", "Hull composed of recursively bound quantum patches", 250); - - this.setSkillsRequirements({ "skill_quantum": leveled(1, 2) }); - this.addAttributeEffect("hull_capacity", leveled(60)); - this.addAttributeEffect("precision", leveled(2)); - this.addTriggerAction(leveled(1, 0.1), [ - new EffectTemplate(new ValueEffect("hull"), { value_on: leveled(60) }) - ]) - this.setCooldown(irepeat(1), irepeat(4)); - } - } -} diff --git a/src/core/equipments/PowerDepleter.spec.ts b/src/core/equipments/PowerDepleter.spec.ts deleted file mode 100644 index 80eaba0..0000000 --- a/src/core/equipments/PowerDepleter.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -module TK.SpaceTac.Specs { - testing("PowerDepleter", test => { - test.case("generates equipment based on level", check => { - let template = new Equipments.PowerDepleter(); - - let equipment = template.generate(1); - check.equals(equipment.requirements, { "skill_antimatter": 1 }); - compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [ - new StickyEffect(new AttributeLimitEffect("power_capacity", 3), 1) - ], 4, 460, 0)); - check.equals(equipment.price, 100); - - equipment = template.generate(2); - check.equals(equipment.requirements, { "skill_antimatter": 2 }); - compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [ - new StickyEffect(new AttributeLimitEffect("power_capacity", 3), 1) - ], 4, 490, 0)); - check.equals(equipment.price, 350); - - equipment = template.generate(3); - check.equals(equipment.requirements, { "skill_antimatter": 4 }); - compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [ - new StickyEffect(new AttributeLimitEffect("power_capacity", 3), 1) - ], 4, 526, 0)); - check.equals(equipment.price, 850); - - equipment = template.generate(10); - check.equals(equipment.requirements, { "skill_antimatter": 25 }); - compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [ - new StickyEffect(new AttributeLimitEffect("power_capacity", 3), 1) - ], 4, 946, 0)); - check.equals(equipment.price, 11350); - }); - }); -} diff --git a/src/core/equipments/PowerDepleter.ts b/src/core/equipments/PowerDepleter.ts deleted file mode 100644 index 9cb5b11..0000000 --- a/src/core/equipments/PowerDepleter.ts +++ /dev/null @@ -1,15 +0,0 @@ -/// - -module TK.SpaceTac.Equipments { - export class PowerDepleter extends LootTemplate { - constructor() { - super(SlotType.Weapon, "Power Depleter", "Direct-hit weapon that creates an antimatter well near the target, sucking its power surplus"); - - this.setSkillsRequirements({ "skill_antimatter": leveled(1, 1.5) }); - this.setCooldown(irepeat(2), irepeat(3)); - this.addTriggerAction(irepeat(4), [ - new StickyEffectTemplate(new AttributeLimitEffect("power_capacity"), { "value": irepeat(3) }, irepeat(1)) - ], leveled(460, 30)); - } - } -} diff --git a/src/core/equipments/RawWeapons.spec.ts b/src/core/equipments/RawWeapons.spec.ts deleted file mode 100644 index da1e939..0000000 --- a/src/core/equipments/RawWeapons.spec.ts +++ /dev/null @@ -1,87 +0,0 @@ -module TK.SpaceTac.Specs { - testing("RawWeapons", test => { - test.case("generates GatlingGun based on level", check => { - let template = new Equipments.GatlingGun(); - - let equipment = template.generate(1); - check.equals(equipment.requirements, { "skill_materials": 1 }); - compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(30, 20)], 3, 400, 0, 0, 60, 20, 15)); - check.equals(equipment.price, 100); - check.equals(equipment.cooldown, new Cooldown(2, 2)); - - equipment = template.generate(2); - check.equals(equipment.requirements, { "skill_materials": 2 }); - compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(42, 28)], 3, 412, 0, 0, 60, 20, 15)); - check.equals(equipment.price, 350); - check.equals(equipment.cooldown, new Cooldown(2, 2)); - - equipment = template.generate(3); - check.equals(equipment.requirements, { "skill_materials": 4 }); - compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(56, 37)], 3, 426, 0, 0, 60, 20, 15)); - check.equals(equipment.price, 850); - check.equals(equipment.cooldown, new Cooldown(2, 2)); - - equipment = template.generate(10); - check.equals(equipment.requirements, { "skill_materials": 23 }); - compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(224, 149)], 3, 594, 0, 0, 60, 20, 15)); - check.equals(equipment.price, 11350); - check.equals(equipment.cooldown, new Cooldown(2, 2)); - }); - - test.case("generates SubMunitionMissile based on level", check => { - let template = new Equipments.SubMunitionMissile(); - - let equipment = template.generate(1); - check.equals(equipment.requirements, { "skill_materials": 1, "skill_photons": 1 }); - compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(26, 4)], 4, 500, 150, 0, 30, 40, 10)); - check.equals(equipment.cooldown, new Cooldown(1, 0)); - check.equals(equipment.price, 163); - - equipment = template.generate(2); - check.equals(equipment.requirements, { "skill_materials": 2, "skill_photons": 1 }); - compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(28, 5)], 4, 520, 155, 0, 30, 40, 10)); - check.equals(equipment.cooldown, new Cooldown(1, 0)); - check.equals(equipment.price, 570); - - equipment = template.generate(3); - check.equals(equipment.requirements, { "skill_materials": 3, "skill_photons": 2 }); - compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(30, 6)], 4, 544, 161, 0, 30, 40, 10)); - check.equals(equipment.cooldown, new Cooldown(1, 0)); - check.equals(equipment.price, 1385); - - equipment = template.generate(10); - check.equals(equipment.requirements, { "skill_materials": 20, "skill_photons": 13 }); - compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(58, 20)], 4, 824, 231, 0, 30, 40, 10)); - check.equals(equipment.cooldown, new Cooldown(1, 0)); - check.equals(equipment.price, 18500); - }); - - test.case("generates ProkhorovLaser based on level", check => { - let template = new Equipments.ProkhorovLaser(); - - let equipment = template.generate(1); - check.equals(equipment.requirements, { "skill_photons": 1, "skill_quantum": 1 }); - compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(20, 25)], 5, 300, 0, 40, 45, 60, 20)); - check.equals(equipment.cooldown, new Cooldown(1, 1)); - check.equals(equipment.price, 152); - - equipment = template.generate(2); - check.equals(equipment.requirements, { "skill_antimatter": 1, "skill_photons": 2, "skill_quantum": 2 }); - compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(28, 35)], 5, 310, 0, 42, 45, 60, 20)); - check.equals(equipment.cooldown, new Cooldown(1, 1)); - check.equals(equipment.price, 532); - - equipment = template.generate(3); - check.equals(equipment.requirements, { "skill_antimatter": 1, "skill_photons": 4, "skill_quantum": 3 }); - compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(37, 47)], 5, 322, 0, 44, 45, 60, 20)); - check.equals(equipment.cooldown, new Cooldown(1, 1)); - check.equals(equipment.price, 1292); - - equipment = template.generate(10); - check.equals(equipment.requirements, { "skill_antimatter": 11, "skill_photons": 23, "skill_quantum": 20 }); - compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new DamageEffect(149, 187)], 5, 462, 0, 72, 45, 60, 20)); - check.equals(equipment.cooldown, new Cooldown(1, 1)); - check.equals(equipment.price, 17252); - }); - }); -} diff --git a/src/core/equipments/RawWeapons.ts b/src/core/equipments/RawWeapons.ts deleted file mode 100644 index 3a5cf5d..0000000 --- a/src/core/equipments/RawWeapons.ts +++ /dev/null @@ -1,40 +0,0 @@ -/// - -module TK.SpaceTac.Equipments { - export class GatlingGun extends LootTemplate { - constructor() { - super(SlotType.Weapon, "Gatling Gun", "Mechanical weapon using loads of metal bullets propelled by guided explosions"); - - this.setSkillsRequirements({ "skill_materials": leveled(1, 1.4) }); - this.setCooldown(irepeat(2), irepeat(2)); - this.addTriggerAction(irepeat(3), [ - new EffectTemplate(new DamageEffect(), { base: leveled(30), span: leveled(20) }) - ], leveled(400, 12), undefined, undefined, irepeat(60), irepeat(20), irepeat(15)); - } - } - - export class SubMunitionMissile extends LootTemplate { - constructor() { - super(SlotType.Weapon, "SubMunition Missile", "Explosive missile releasing small shelled payloads, that will in turn explode on impact", 163); - - this.setSkillsRequirements({ "skill_materials": leveled(1, 1.2), "skill_photons": leveled(1, 0.8) }); - this.setCooldown(irepeat(1), irepeat(0)); - this.addTriggerAction(irepeat(4), [ - new EffectTemplate(new DamageEffect(), { base: leveled(26, 2), span: leveled(4, 1) }) - ], leveled(500, 20), leveled(150, 5), undefined, irepeat(30), irepeat(40), irepeat(10)); - } - } - - export class ProkhorovLaser extends LootTemplate { - constructor() { - super(SlotType.Weapon, "Prokhorov Laser", "Powerful mid-range perforating laser, using antimatter to contain the tremendous photonic energy", 152); - - // TODO increased damage to hull - this.setSkillsRequirements({ "skill_antimatter": leveled(0.3, 0.7), "skill_quantum": leveled(1, 1.2), "skill_photons": leveled(1, 1.4) }); - this.setCooldown(irepeat(1), irepeat(1)); - this.addTriggerAction(irepeat(5), [ - new EffectTemplate(new DamageEffect(), { base: leveled(20), span: leveled(25) }) - ], leveled(300, 10), irepeat(0), leveled(40, 2), irepeat(45), irepeat(60), irepeat(20)); - } - } -} diff --git a/src/core/equipments/RepairDrone.spec.ts b/src/core/equipments/RepairDrone.spec.ts deleted file mode 100644 index e709ada..0000000 --- a/src/core/equipments/RepairDrone.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -module TK.SpaceTac.Specs { - testing("RepairDrone", test => { - test.case("generates equipment based on level", check => { - let template = new Equipments.RepairDrone(); - - let equipment = template.generate(1); - check.equals(equipment.requirements, { "skill_quantum": 1 }); - compare_drone_action(check, equipment.action, new DeployDroneAction(equipment, 3, 300, 150, [ - new ValueEffect("hull", 0, 0, 0, 30) - ])); - - equipment = template.generate(2); - check.equals(equipment.requirements, { "skill_quantum": 4 }); - compare_drone_action(check, equipment.action, new DeployDroneAction(equipment, 3, 310, 155, [ - new ValueEffect("hull", 0, 0, 0, 42) - ])); - - equipment = template.generate(3); - check.equals(equipment.requirements, { "skill_quantum": 7 }); - compare_drone_action(check, equipment.action, new DeployDroneAction(equipment, 3, 322, 161, [ - new ValueEffect("hull", 0, 0, 0, 56) - ])); - - equipment = template.generate(10); - check.equals(equipment.requirements, { "skill_quantum": 49 }); - compare_drone_action(check, equipment.action, new DeployDroneAction(equipment, 6, 462, 231, [ - new ValueEffect("hull", 0, 0, 0, 224) - ])); - }); - }); -} diff --git a/src/core/equipments/RepairDrone.ts b/src/core/equipments/RepairDrone.ts deleted file mode 100644 index 0c3fe2d..0000000 --- a/src/core/equipments/RepairDrone.ts +++ /dev/null @@ -1,17 +0,0 @@ -/// - -module TK.SpaceTac.Equipments { - /** - * Drone that repairs damage done to the hull. - */ - export class RepairDrone extends LootTemplate { - constructor() { - super(SlotType.Weapon, "Repair Drone", "Drone able to repair small hull breaches, using quantum patches", 190); - - this.setSkillsRequirements({ "skill_quantum": leveled(1, 3) }); - this.addDroneAction(leveled(3, 0.2), leveled(300, 10), leveled(150, 5), [ - new EffectTemplate(new ValueEffect("hull"), { "value_end": leveled(30) }) - ]); - } - } -} \ No newline at end of file diff --git a/src/core/equipments/ShieldTransfer.spec.ts b/src/core/equipments/ShieldTransfer.spec.ts deleted file mode 100644 index 3edcfcb..0000000 --- a/src/core/equipments/ShieldTransfer.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -module TK.SpaceTac.Specs { - testing("ShieldTransfer", test => { - test.case("generates equipment based on level", check => { - let template = new Equipments.ShieldTransfer(); - - let equipment = template.generate(1); - check.equals(equipment.requirements, { "skill_gravity": 2 }); - check.equals(equipment.cooldown, new Cooldown(3, 3)); - compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [ - new ValueTransferEffect("shield", -40) - ], 3, 0, 250)); - - equipment = template.generate(2); - check.equals(equipment.requirements, { "skill_gravity": 3 }); - check.equals(equipment.cooldown, new Cooldown(3, 3)); - compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [ - new ValueTransferEffect("shield", -44) - ], 3, 0, 270)); - - equipment = template.generate(3); - check.equals(equipment.requirements, { "skill_gravity": 5 }); - check.equals(equipment.cooldown, new Cooldown(3, 3)); - compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [ - new ValueTransferEffect("shield", -49) - ], 3, 0, 294)); - - equipment = template.generate(10); - check.equals(equipment.requirements, { "skill_gravity": 26 }); - check.equals(equipment.cooldown, new Cooldown(3, 3)); - compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [ - new ValueTransferEffect("shield", -105) - ], 3, 0, 574)); - }) - }) -} diff --git a/src/core/equipments/ShieldTransfer.ts b/src/core/equipments/ShieldTransfer.ts deleted file mode 100644 index c8f8f3b..0000000 --- a/src/core/equipments/ShieldTransfer.ts +++ /dev/null @@ -1,15 +0,0 @@ -/// - -module TK.SpaceTac.Equipments { - export class ShieldTransfer extends LootTemplate { - constructor() { - super(SlotType.Weapon, "Shield Transfer", "Generates small gravity wells between the ship's and the target's shields, stealing physical properties and energy"); - - this.setSkillsRequirements({ "skill_gravity": leveled(2, 1.5) }); - this.setCooldown(irepeat(3), irepeat(3)); - this.addTriggerAction(irepeat(3), [ - new EffectTemplate(new ValueTransferEffect("shield"), { "amount": leveled(-40, -4) }) - ], irepeat(0), leveled(250, 20)); - } - } -} diff --git a/src/core/equipments/Shields.spec.ts b/src/core/equipments/Shields.spec.ts deleted file mode 100644 index 66fbb3a..0000000 --- a/src/core/equipments/Shields.spec.ts +++ /dev/null @@ -1,99 +0,0 @@ -module TK.SpaceTac.Specs { - testing("Shields", test => { - test.case("generates ForceField based on level", check => { - let template = new Equipments.ForceField(); - - let equipment = template.generate(1); - check.equals(equipment.requirements, { "skill_photons": 1 }); - compare_effects(check, equipment.effects, [new AttributeEffect("shield_capacity", 80)]); - check.equals(equipment.price, 95); - - equipment = template.generate(2); - check.equals(equipment.requirements, { "skill_photons": 3 }); - compare_effects(check, equipment.effects, [new AttributeEffect("shield_capacity", 112)]); - check.equals(equipment.price, 332); - - equipment = template.generate(3); - check.equals(equipment.requirements, { "skill_photons": 5 }); - compare_effects(check, equipment.effects, [new AttributeEffect("shield_capacity", 150)]); - check.equals(equipment.price, 807); - - equipment = template.generate(10); - check.equals(equipment.requirements, { "skill_photons": 33 }); - compare_effects(check, equipment.effects, [new AttributeEffect("shield_capacity", 598)]); - check.equals(equipment.price, 10782); - }); - - test.case("generates GravitShield based on level", check => { - let template = new Equipments.GravitShield(); - - let equipment = template.generate(1); - check.equals(equipment.requirements, { "skill_gravity": 2 }); - compare_effects(check, equipment.effects, [ - new AttributeEffect("shield_capacity", 60), - ]); - compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new RepelEffect(100)], 2, 0, 300)); - check.equals(equipment.price, 140); - - equipment = template.generate(2); - check.equals(equipment.requirements, { "skill_gravity": 5 }); - compare_effects(check, equipment.effects, [ - new AttributeEffect("shield_capacity", 84), - ]); - compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new RepelEffect(105)], 2, 0, 310)); - check.equals(equipment.price, 490); - - equipment = template.generate(3); - check.equals(equipment.requirements, { "skill_gravity": 8 }); - compare_effects(check, equipment.effects, [ - new AttributeEffect("shield_capacity", 112), - ]); - compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new RepelEffect(111)], 2, 0, 322)); - check.equals(equipment.price, 1190); - - equipment = template.generate(10); - check.equals(equipment.requirements, { "skill_gravity": 50 }); - compare_effects(check, equipment.effects, [ - new AttributeEffect("shield_capacity", 448), - ]); - compare_trigger_action(check, equipment.action, new TriggerAction(equipment, [new RepelEffect(181)], 2, 0, 462)); - check.equals(equipment.price, 15890); - }); - - test.case("generates InverterShield based on level", check => { - let template = new Equipments.InverterShield(); - - let equipment = template.generate(1); - check.equals(equipment.requirements, { "skill_antimatter": 2, "skill_time": 1 }); - compare_effects(check, equipment.effects, [ - new AttributeEffect("shield_capacity", 140), - new AttributeEffect("power_capacity", -1), - ]); - check.equals(equipment.price, 258); - - equipment = template.generate(2); - check.equals(equipment.requirements, { "skill_antimatter": 3, "skill_time": 2 }); - compare_effects(check, equipment.effects, [ - new AttributeEffect("shield_capacity", 196), - new AttributeEffect("power_capacity", -1), - ]); - check.equals(equipment.price, 903); - - equipment = template.generate(3); - check.equals(equipment.requirements, { "skill_antimatter": 5, "skill_time": 3 }); - compare_effects(check, equipment.effects, [ - new AttributeEffect("shield_capacity", 263), - new AttributeEffect("power_capacity", -1), - ]); - check.equals(equipment.price, 2193); - - equipment = template.generate(10); - check.equals(equipment.requirements, { "skill_antimatter": 26, "skill_time": 17 }); - compare_effects(check, equipment.effects, [ - new AttributeEffect("shield_capacity", 1047), - new AttributeEffect("power_capacity", -4), - ]); - check.equals(equipment.price, 29283); - }); - }); -} diff --git a/src/core/equipments/Shields.ts b/src/core/equipments/Shields.ts deleted file mode 100644 index b35c4ce..0000000 --- a/src/core/equipments/Shields.ts +++ /dev/null @@ -1,34 +0,0 @@ -/// - -module TK.SpaceTac.Equipments { - export class ForceField extends LootTemplate { - constructor() { - super(SlotType.Shield, "Force Field", "A basic force field, generated by radiating waves of compressed energy", 95); - - this.setSkillsRequirements({ "skill_photons": leveled(1, 2) }); - this.addAttributeEffect("shield_capacity", leveled(80)); - } - } - - export class GravitShield extends LootTemplate { - constructor() { - super(SlotType.Shield, "Gravit Shield", "A shield able to repel damage and enemies using micro-gravity wells", 140); - - this.setSkillsRequirements({ "skill_gravity": leveled(2, 3) }); - this.addAttributeEffect("shield_capacity", leveled(60)); - this.addTriggerAction(irepeat(2), [ - new EffectTemplate(new RepelEffect(), { value: leveled(100, 5) }) - ], irepeat(0), leveled(300, 10)); - } - } - - export class InverterShield extends LootTemplate { - constructor() { - super(SlotType.Shield, "Inverter Shield", "An antimatter shield that tries to cancel inbound energy", 258); - - this.setSkillsRequirements({ "skill_antimatter": leveled(2, 1.5), "skill_time": leveled(1, 1) }); - this.addAttributeEffect("shield_capacity", leveled(140)); - this.addAttributeEffect("power_capacity", leveled(-0.2, -0.2)); - } - } -} diff --git a/src/core/missions/Mission.spec.ts b/src/core/missions/Mission.spec.ts index 83c168f..fd93c20 100644 --- a/src/core/missions/Mission.spec.ts +++ b/src/core/missions/Mission.spec.ts @@ -38,16 +38,11 @@ module TK.SpaceTac.Specs { mission.reward = 720; check.equals(mission.getRewardText(), "720 zotys"); - - mission.reward = new Equipment(); - mission.reward.name = "Super Equipment"; - check.equals(mission.getRewardText(), "Super Equipment Mk1"); }) test.case("gives the reward on completion", check => { let fleet = new Fleet(); let ship = fleet.addShip(); - ship.cargo_space = 5; fleet.credits = 150; let mission = new Mission(new Universe(), fleet); @@ -58,14 +53,6 @@ module TK.SpaceTac.Specs { mission.setCompleted(); check.equals(fleet.credits, 225); - - mission = new Mission(new Universe(), fleet); - mission.reward = new Equipment(); - check.equals(ship.cargo, []); - mission.setCompleted(); - check.equals(mission.completed, true); - check.equals(fleet.credits, 225); - check.equals(ship.cargo, [mission.reward]); }) }) } diff --git a/src/core/missions/Mission.ts b/src/core/missions/Mission.ts index 9f43a1c..9c75b55 100644 --- a/src/core/missions/Mission.ts +++ b/src/core/missions/Mission.ts @@ -1,8 +1,8 @@ module TK.SpaceTac { /** - * Reward for a mission (either an equipment or money) + * Reward for a mission (currently, only money) */ - export type MissionReward = Equipment | number + export type MissionReward = number /** * Level of difficulty for a mission @@ -84,11 +84,7 @@ module TK.SpaceTac { */ getRewardText(): string { if (this.reward) { - if (this.reward instanceof Equipment) { - return this.reward.getFullName(); - } else { - return `${this.reward} zotys`; - } + return `${this.reward} zotys`; } else { return "-"; } @@ -146,11 +142,7 @@ module TK.SpaceTac { if (!this.completed) { this.completed = true; if (this.reward) { - if (this.reward instanceof Equipment) { - this.fleet.addCargo(this.reward); - } else { - this.fleet.credits += this.reward; - } + this.fleet.credits += this.reward; } } } diff --git a/src/core/missions/MissionGenerator.spec.ts b/src/core/missions/MissionGenerator.spec.ts index 01531fd..2969286 100644 --- a/src/core/missions/MissionGenerator.spec.ts +++ b/src/core/missions/MissionGenerator.spec.ts @@ -76,39 +76,5 @@ module TK.SpaceTac.Specs { check.same(mission.difficulty, MissionDifficulty.easy); check.equals(mission.value, 6400); }) - - test.case("generates equipment reward", check => { - let generator = new MissionGenerator(new Universe(), new StarLocation()); - let template = new LootTemplate(SlotType.Weapon, "Test Weapon"); - generator.equipment_generator.templates = [template]; - - template.price = irepeat(350); - let result = generator.tryGenerateEquipmentReward(500); - check.equals(result, null); - - template.price = irepeat(800); - result = generator.tryGenerateEquipmentReward(500); - check.equals(result, null); - - template.price = irepeat(500); - result = generator.tryGenerateEquipmentReward(500); - check.notequals(result, null); - }) - - test.case("falls back to money reward when no suitable equipment have been generated", check => { - let generator = new MissionGenerator(new Universe(), new StarLocation()); - generator.equipment_generator.templates = []; - - let result = generator.generateReward(15000); - check.equals(result, 15000); - - let template = new LootTemplate(SlotType.Weapon, "Test Weapon"); - template.price = irepeat(15000); - generator.equipment_generator.templates.push(template); - - generator.random = new SkewedRandomGenerator([0], true); - result = generator.generateReward(15000); - check.equals(result instanceof Equipment, true); - }) }); } diff --git a/src/core/missions/MissionGenerator.ts b/src/core/missions/MissionGenerator.ts index fe123ab..e004481 100644 --- a/src/core/missions/MissionGenerator.ts +++ b/src/core/missions/MissionGenerator.ts @@ -21,13 +21,11 @@ module TK.SpaceTac { universe: Universe around: StarLocation random: RandomGenerator - equipment_generator: LootGenerator constructor(universe: Universe, around: StarLocation, random = RandomGenerator.global) { this.universe = universe; this.around = around; this.random = random; - this.equipment_generator = new LootGenerator(this.random); } /** @@ -64,49 +62,12 @@ module TK.SpaceTac { return result; } - /** - * Try to generate an equipment of given value - */ - tryGenerateEquipmentReward(value: number): Equipment | null { - let minvalue = value * 0.8; - let maxvalue = value * 1.2; - let qualities = [EquipmentQuality.FINE, EquipmentQuality.PREMIUM, EquipmentQuality.LEGENDARY]; - - let candidates: Equipment[] = []; - for (let pass = 0; pass < 10; pass++) { - let equipment: Equipment | null; - let level = 1; - do { - let quality = qualities[this.random.weighted([15, 12, 2])]; - equipment = this.equipment_generator.generate(level, quality); - if (equipment && equipment.getPrice() >= minvalue && equipment.getPrice() <= maxvalue) { - candidates.push(equipment); - } - level += 1; - } while (equipment && equipment.getPrice() < maxvalue * 1.5 && level < 20); - } - - if (candidates.length > 0) { - return this.random.choice(candidates); - } else { - return null; - } - } - /** * Generate a reward */ generateReward(value: number): MissionReward { - if (this.random.bool()) { - let equipment = this.tryGenerateEquipmentReward(value); - if (equipment) { - return equipment; - } else { - return value; - } - } else { - return value; - } + // TODO + return value; } /** diff --git a/src/core/missions/MissionPartEscort.spec.ts b/src/core/missions/MissionPartEscort.spec.ts index 6e69613..436ffd5 100644 --- a/src/core/missions/MissionPartEscort.spec.ts +++ b/src/core/missions/MissionPartEscort.spec.ts @@ -48,7 +48,7 @@ module TK.SpaceTac.Specs { let enemy = new Fleet(); enemy.addShip(); let battle = new Battle(fleet, enemy); - battle.ships.list().forEach(ship => TestTools.setShipHP(ship, 10, 0)); + battle.ships.list().forEach(ship => TestTools.setShipModel(ship, 10, 0)); battle.start(); battle.performChecks(); check.equals(battle.ended, false); diff --git a/src/core/models/BaseModel.spec.ts b/src/core/models/BaseModel.spec.ts new file mode 100644 index 0000000..c35e4bf --- /dev/null +++ b/src/core/models/BaseModel.spec.ts @@ -0,0 +1,59 @@ +module TK.SpaceTac.Specs { + testing("BaseModel", test => { + test.case("picks random models from default collection", check => { + check.patch(console, "error", null); + check.patch(BaseModel, "getDefaultCollection", iterator([ + [new BaseModel("a")], + [], + [new BaseModel("a"), new BaseModel("b")], + [new BaseModel("a")], + [], + ])); + + check.equals(BaseModel.getRandomModel(), new BaseModel("a"), "pick from a one-item list"); + check.equals(BaseModel.getRandomModel(), new BaseModel(), "pick from an empty list"); + + check.equals(sorted(BaseModel.getRandomModels(2), (a, b) => cmp(a.code, b.code)), [new BaseModel("a"), new BaseModel("b")], "sample from good-sized list"); + check.equals(BaseModel.getRandomModels(2), [new BaseModel("a"), new BaseModel("a")], "sample from too small list"); + check.equals(BaseModel.getRandomModels(2), [new BaseModel(), new BaseModel()], "sample from empty list"); + }); + + test.case("makes upgrades available by level", check => { + let model = new BaseModel(); + + function verify(desc: string, level: number, specific: string[], available: string[], activated: string[], chosen: string[] = []) { + check.in(`${desc} level ${level}`, check => { + check.equals(model.getLevelUpgrades(level).map(u => u.code), specific, "specific"); + check.equals(model.getAvailableUpgrades(level).map(u => u.code), available, "available"); + check.equals(model.getActivatedUpgrades(level, chosen).map(u => u.code), activated, "activated"); + }); + } + + verify("initial", 1, [], [], []); + + check.patch(model, "getLevelUpgrades", (level: number): ModelUpgrade[] => { + if (level == 1) { + return [ + { code: "l1" }, + ]; + } else if (level == 2) { + return [ + { code: "l2a" }, + { code: "l2b" } + ]; + } else { + return []; + } + }); + + verify("standard", 0, [], [], []); + verify("standard", 1, ["l1"], ["l1"], ["l1"]); + verify("standard", 2, ["l2a", "l2b"], ["l1", "l2a", "l2b"], ["l1"]); + verify("standard", 3, [], ["l1", "l2a", "l2b"], ["l1"]); + + verify("with actives", 1, ["l1"], ["l1"], ["l1"], ["l2a", "l666"]); + verify("with actives", 2, ["l2a", "l2b"], ["l1", "l2a", "l2b"], ["l1", "l2a"], ["l2a", "l666"]); + verify("with actives", 3, [], ["l1", "l2a", "l2b"], ["l1", "l2a"], ["l2a", "l666"]); + }); + }); +} diff --git a/src/core/models/BaseModel.ts b/src/core/models/BaseModel.ts new file mode 100644 index 0000000..8665f68 --- /dev/null +++ b/src/core/models/BaseModel.ts @@ -0,0 +1,182 @@ +module TK.SpaceTac { + /** + * Single upgrade for a ship + * + * Upgrades allow for customizing a model, and are unlocked at given levels + */ + export type ModelUpgrade = { + // Displayable upgrade name, should be unique on the model + code: string + // Upgrade points cost (may be used to balance upgrades) + cost?: number + // Optional list of upgrade codes that must be activated for this one to be available + depends?: string[] + // Optional list of upgrade codes that this upgrade will fully replace + replaces?: string[] + // Optional list of upgrade codes that conflicts with this upgrade + conflicts?: string[] + // List of actions that this upgrade offers + actions?: BaseAction[] + // List of effects that this upgrade offers + effects?: BaseEffect[] + } + + /** + * Base class for ship models. + * + * A model defines the ship's design, actions, permanent effects, and levelling options. + */ + export class BaseModel { + constructor( + // Code to identify the model + readonly code = "default", + // Human-readable model name + readonly name = "Ship" + ) { } + + /** + * Check if this model is available at a given level + */ + isAvailable(level: number): boolean { + // TODO + return true; + } + + /** + * Get a textual description of the model + */ + getDescription(): string { + return ""; + } + + /** + * Get basic level upgrades + */ + protected getStandardUpgrades(level: number): ModelUpgrade[] { + return [ + { code: `Hull upgrade Lv${level - 1}`, effects: [new AttributeEffect("hull_capacity", 5)], cost: 2 }, + { code: `Shield upgrade Lv${level - 1}`, effects: [new AttributeEffect("shield_capacity", 5)], cost: 2 }, + { code: `Power upgrade Lv${level - 1}`, effects: [new AttributeEffect("power_capacity", 1)], cost: 3 }, + ]; + } + + /** + * Get the list of upgrades unlocked at a given level + */ + getLevelUpgrades(level: number): ModelUpgrade[] { + return []; + } + + /** + * Get the list of upgrades activated, given a ship level and an upgrade set + */ + getActivatedUpgrades(level: number, upgrade_codes: string[]): ModelUpgrade[] { + let result: ModelUpgrade[] = []; + + range(level).forEach(i => { + let upgrades = this.getLevelUpgrades(i + 1); + if (i == 0) { + result = result.concat(upgrades); + } else { + // TODO Apply depends, replaces and conflicts + upgrades.forEach(upgrade => { + if (contains(upgrade_codes, upgrade.code)) { + result.push(upgrade); + } + }); + } + }); + + return result; + } + + /** + * Get the list of available upgrades, given a ship level + * + * This does not filter the upgrades on dependencies + */ + getAvailableUpgrades(level: number): ModelUpgrade[] { + return flatten(range(level).map(i => this.getLevelUpgrades(i + 1))); + } + + /** + * Get the list of actions at a given level and upgrades set + * + * This does not include an "end turn" action. + */ + getActions(level: number, upgrade_codes: string[]): BaseAction[] { + return flatten(this.getActivatedUpgrades(level, upgrade_codes).map(upgrade => upgrade.actions || [])); + } + + /** + * Get the list of permanent effects at a given level and upgrades set + */ + getEffects(level: number, upgrade_codes: string[]): BaseEffect[] { + return flatten(this.getActivatedUpgrades(level, upgrade_codes).map(upgrade => upgrade.effects || [])); + } + + /** + * Get the default ship model collection available in-game + * + * This scans the current namespace for model classes starting with 'Model'. + */ + static getDefaultCollection(): BaseModel[] { + let result: BaseModel[] = []; + let namespace: any = TK.SpaceTac; + + for (let class_name in namespace) { + if (class_name && class_name.indexOf("Model") == 0) { + let model_class = namespace[class_name]; + if (model_class.prototype instanceof BaseModel) { + let model = new model_class(); + result.push(model); + } + } + } + + return result; + } + + /** + * Pick a random model in the default collection + */ + static getRandomModel(level?: number, random = RandomGenerator.global): BaseModel { + let collection = BaseModel.getDefaultCollection(); + if (level) { + collection = collection.filter(model => model.isAvailable(level)); + } + + if (collection.length == 0) { + console.error("Couldn't pick a random ship model"); + return new BaseModel(); + } else { + return random.choice(collection); + } + } + + /** + * Pick random models in the default collection + * + * At first it tries to pick unique models, then fill with duplicates + */ + static getRandomModels(count: number, level?: number, random = RandomGenerator.global): BaseModel[] { + let collection = BaseModel.getDefaultCollection(); + if (level) { + collection = collection.filter(model => model.isAvailable(level)); + } + + if (collection.length == 0) { + console.error("Couldn't pick a random model"); + return range(count).map(() => new BaseModel()); + } else { + let result: BaseModel[] = []; + while (count > 0) { + let picked = random.sample(collection, Math.min(count, collection.length)); + result = result.concat(picked); + count -= picked.length; + } + return result; + } + } + } +} diff --git a/src/core/models/ModelAvenger.ts b/src/core/models/ModelAvenger.ts new file mode 100644 index 0000000..a0cd08f --- /dev/null +++ b/src/core/models/ModelAvenger.ts @@ -0,0 +1,120 @@ +/// + +module TK.SpaceTac { + export class ModelAvenger extends BaseModel { + constructor() { + super("avenger", "Avenger"); + } + + getDescription(): string { + return "A heavy ship, dedicated to firing high precision charged shots across great distances."; + } + + getLevelUpgrades(level: number): ModelUpgrade[] { + let engine = new MoveAction("Engine", { + distance_per_power: 50, + safety_distance: 250, + }); + engine.configureCooldown(1, 1); + + // TODO Weapons should be less efficient in short range + + let charged_shot = new TriggerAction("Charged Shot", { + effects: [new DamageEffect(30, 20)], + power: 3, + range: 900, + aim: 90, evasion: 40, luck: 20 + }, "gatlinggun"); + charged_shot.configureCooldown(2, 2); + + let long_range_missile = new TriggerAction("Long Range Missile", { + effects: [new DamageEffect(15, 25)], + power: 4, + range: 700, blast: 120, + aim: 70, evasion: 20, luck: 50 + }, "submunitionmissile"); + long_range_missile.configureCooldown(1, 2); + + let shield_booster = new TriggerAction("Shield Booster", { + effects: [ + new StickyEffect(new AttributeEffect("shield_capacity", 50), 2), + new ValueEffect("shield", 70), + ], + power: 2 + }, "forcefield"); + shield_booster.configureCooldown(1, 4); + + if (level == 1) { + return [ + { + code: "Base Attributes", + effects: [ + new AttributeEffect("precision", 8), + new AttributeEffect("maneuvrability", 0), + new AttributeEffect("hull_capacity", 80), + new AttributeEffect("shield_capacity", 20), + new AttributeEffect("power_capacity", 8), + ] + }, + { + code: "Main Engine", + actions: [engine] + }, + { + code: "Charged Shot", + actions: [charged_shot] + }, + { + code: "Long Range Missile", + actions: [long_range_missile] + }, + ]; + } else if (level == 2) { + return [ + { + code: "Laser Targetting", + cost: 1, + effects: [new AttributeEffect("precision", 2)] + }, + { + code: "Basic Countermeasures", + cost: 1, + effects: [new AttributeEffect("maneuvrability", 2)] + }, + { + code: "Targetting Assist", + cost: 3, + actions: [new ToggleAction("Targetting Assist", { + power: 3, + radius: 300, + effects: [new AttributeEffect("precision", 2)] + }, "precisionboost")] + }, + ]; + } else if (level == 3) { + return [ + { + code: "Gyroscopic Stabilizers", + cost: 1, + effects: [ + new AttributeEffect("precision", 3), + new AttributeEffect("maneuvrability", -2) + ] + }, + { + code: "Shield Booster", + cost: 3, + actions: [shield_booster] + }, + { + code: "Hard Coated Hull", + cost: 2, + effects: [new AttributeEffect("hull_capacity", 10)] + }, + ]; + } else { + return this.getStandardUpgrades(level); + } + } + } +} diff --git a/src/core/models/ModelBreeze.ts b/src/core/models/ModelBreeze.ts new file mode 100644 index 0000000..b25b7c1 --- /dev/null +++ b/src/core/models/ModelBreeze.ts @@ -0,0 +1,66 @@ +/// + +module TK.SpaceTac { + export class ModelBreeze extends BaseModel { + constructor() { + super("breeze", "Breeze"); + } + + getDescription(): string { + return "A swift piece of maneuvrability, able to go deep behind enemy lines, and come back without a scratch."; + } + + getLevelUpgrades(level: number): ModelUpgrade[] { + if (level == 1) { + let engine = new MoveAction("Engine", { + distance_per_power: 300, + safety_distance: 100, + maneuvrability_factor: 60 + }); + engine.configureCooldown(2, 1); + + let gatling = new TriggerAction("Gatling Gun", { + effects: [new DamageEffect(35, 20)], + power: 2, + range: 200, + aim: 30, evasion: 10, luck: 20 + }, "gatlinggun"); + gatling.configureCooldown(3, 1); + + let shield_steal = new TriggerAction("Shield Steal", { + effects: [new ValueTransferEffect("shield", -40)], + power: 1, + blast: 300 + }, "shieldtransfer"); + shield_steal.configureCooldown(1, 2); + + return [ + { + code: "Base Attributes", + effects: [ + new AttributeEffect("precision", 3), + new AttributeEffect("maneuvrability", 12), + new AttributeEffect("hull_capacity", 30), + new AttributeEffect("shield_capacity", 50), + new AttributeEffect("power_capacity", 7), + ] + }, + { + code: "Main Engine", + actions: [engine] + }, + { + code: "Gatling Gun", + actions: [gatling] + }, + { + code: "Shield Steal", + actions: [shield_steal] + }, + ]; + } else { + return this.getStandardUpgrades(level); + } + } + } +} diff --git a/src/core/models/ModelCommodore.ts b/src/core/models/ModelCommodore.ts new file mode 100644 index 0000000..da5a3a6 --- /dev/null +++ b/src/core/models/ModelCommodore.ts @@ -0,0 +1,63 @@ +/// + +module TK.SpaceTac { + export class ModelCommodore extends BaseModel { + constructor() { + super("commodore", "Commodore"); + } + + getDescription(): string { + return "A devil whirlwind, very dangerous to surround."; + } + + getLevelUpgrades(level: number): ModelUpgrade[] { + if (level == 1) { + let engine = new MoveAction("Engine", { + distance_per_power: 150, + }); + + let laser = new TriggerAction("Wingspan Laser", { + effects: [new DamageEffect(25, 25)], + power: 4, + range: 250, angle: 140, + aim: 30, evasion: 45, luck: 30, + }, "prokhorovlaser"); + laser.configureCooldown(3, 1); + + let power_steal = new TriggerAction("Power Thief", { + effects: [new ValueTransferEffect("power", -1)], + power: 1, + blast: 250 + }, "powerdepleter"); + power_steal.configureCooldown(1, 1); + + return [ + { + code: "Base Attributes", + effects: [ + new AttributeEffect("precision", 5), + new AttributeEffect("maneuvrability", 6), + new AttributeEffect("hull_capacity", 70), + new AttributeEffect("shield_capacity", 40), + new AttributeEffect("power_capacity", 8), + ] + }, + { + code: "Main Engine", + actions: [engine] + }, + { + code: "Wingspan Laser", + actions: [laser] + }, + { + code: "Power Thief", + actions: [power_steal] + }, + ]; + } else { + return this.getStandardUpgrades(level); + } + } + } +} diff --git a/src/core/models/ModelCreeper.ts b/src/core/models/ModelCreeper.ts new file mode 100644 index 0000000..fbbe81d --- /dev/null +++ b/src/core/models/ModelCreeper.ts @@ -0,0 +1,74 @@ +/// + +module TK.SpaceTac { + export class ModelCreeper extends BaseModel { + constructor() { + super("creeper", "Creeper"); + } + + getDescription(): string { + return "A fast ship, with low firepower but extensive support modules."; + } + + getLevelUpgrades(level: number): ModelUpgrade[] { + if (level == 1) { + let engine = new MoveAction("Engine", { + distance_per_power: 220, + }); + + let gatling = new TriggerAction("Gatling Gun", { + effects: [new DamageEffect(15, 10)], + power: 2, + range: 200, + }, "gatlinggun"); + gatling.configureCooldown(1, 1); + + let repulse = new TriggerAction("Repulser", { + effects: [new RepelEffect(150)], + power: 2, + blast: 350, + }, "gravitshield"); + repulse.configureCooldown(1, 1); + + let repairdrone = new DeployDroneAction("Repair Drone", { power: 3 }, { + deploy_distance: 300, + drone_radius: 150, + drone_effects: [ + new ValueEffect("hull", undefined, undefined, undefined, 30) + ] + }, "repairdrone"); + + return [ + { + code: "Base Attributes", + effects: [ + new AttributeEffect("precision", 3), + new AttributeEffect("maneuvrability", 12), + new AttributeEffect("hull_capacity", 30), + new AttributeEffect("shield_capacity", 50), + new AttributeEffect("power_capacity", 7), + ] + }, + { + code: "Main Engine", + actions: [engine] + }, + { + code: "Gatling Gun", + actions: [gatling] + }, + { + code: "Repulser", + actions: [repulse] + }, + { + code: "Repair Drone", + actions: [repairdrone] + }, + ]; + } else { + return this.getStandardUpgrades(level); + } + } + } +} diff --git a/src/core/models/ModelFalcon.ts b/src/core/models/ModelFalcon.ts new file mode 100644 index 0000000..177cd27 --- /dev/null +++ b/src/core/models/ModelFalcon.ts @@ -0,0 +1,62 @@ +/// + +module TK.SpaceTac { + export class ModelFalcon extends BaseModel { + constructor() { + super("falcon", "Falcon"); + } + + getDescription(): string { + return "A ship with an efficient targetting system, allowing to hit multiple foes."; + } + + getLevelUpgrades(level: number): ModelUpgrade[] { + if (level == 1) { + let engine = new MoveAction("Engine", { + distance_per_power: 130, + }); + + let missile = new TriggerAction("SubMunition Missile", { + effects: [new DamageEffect(10, 10)], + power: 2, + range: 250, blast: 150, + }, "submunitionmissile"); + missile.configureCooldown(2, 2); + + let gatling = new TriggerAction("Gatling Gun", { + effects: [new DamageEffect(10, 10)], + power: 1, + range: 350, + }, "gatlinggun"); + gatling.configureCooldown(3, 2); + + return [ + { + code: "Base Attributes", + effects: [ + new AttributeEffect("precision", 8), + new AttributeEffect("maneuvrability", 4), + new AttributeEffect("hull_capacity", 50), + new AttributeEffect("shield_capacity", 50), + new AttributeEffect("power_capacity", 9), + ] + }, + { + code: "Main Engine", + actions: [engine] + }, + { + code: "Submunition Missile", + actions: [missile] + }, + { + code: "Gatling Gun", + actions: [gatling] + }, + ]; + } else { + return this.getStandardUpgrades(level); + } + } + } +} diff --git a/src/core/models/ModelFlea.ts b/src/core/models/ModelFlea.ts new file mode 100644 index 0000000..2a66c61 --- /dev/null +++ b/src/core/models/ModelFlea.ts @@ -0,0 +1,60 @@ +/// + +module TK.SpaceTac { + export class ModelFlea extends BaseModel { + constructor() { + super("flea", "Flea"); + } + + getDescription(): string { + return "An agile but weak ship, specialized in disruptive technologies."; + } + + getLevelUpgrades(level: number): ModelUpgrade[] { + if (level == 1) { + let engine = new MoveAction("Engine", { + distance_per_power: 400, + }); + + let depleter = new TriggerAction("Power Depleter", { + effects: [new StickyEffect(new AttributeLimitEffect("power_capacity", 3))], + power: 2, + range: 450, + }, "powerdepleter"); + depleter.configureCooldown(1, 1); + + let gatling = new TriggerAction("Gatling Gun", { + effects: [new DamageEffect(5, 30)], + }, "gatlinggun"); + gatling.configureCooldown(2, 1); + + return [ + { + code: "Base Attributes", + effects: [ + new AttributeEffect("precision", 0), + new AttributeEffect("maneuvrability", 15), + new AttributeEffect("hull_capacity", 25), + new AttributeEffect("shield_capacity", 45), + new AttributeEffect("power_capacity", 8), + ] + }, + { + code: "Main Engine", + actions: [engine] + }, + { + code: "Power Depleter", + actions: [depleter] + }, + { + code: "Gatling Gun", + actions: [gatling] + }, + ]; + } else { + return this.getStandardUpgrades(level); + } + } + } +} diff --git a/src/core/models/ModelJumper.ts b/src/core/models/ModelJumper.ts new file mode 100644 index 0000000..fdd597d --- /dev/null +++ b/src/core/models/ModelJumper.ts @@ -0,0 +1,72 @@ +/// + +module TK.SpaceTac { + export class ModelJumper extends BaseModel { + constructor() { + super("jumper", "Jumper"); + } + + getDescription(): string { + return "A mid-range action ship, with support abilities."; + } + + getLevelUpgrades(level: number): ModelUpgrade[] { + if (level == 1) { + let engine = new MoveAction("Engine", { + distance_per_power: 200, + safety_distance: 160, + }); + + let missile = new TriggerAction("SubMunition Missile", { + effects: [new DamageEffect(30, 30)], + power: 3, + range: 400, blast: 120, + aim: 70, evasion: 30, luck: 10, + }, "submunitionmissile"); + + let protector = new TriggerAction("Damage Reductor", { + effects: [new StickyEffect(new DamageModifierEffect(-20), 2)], + power: 3, + range: 300, blast: 150 + }, "damageprotector"); + protector.configureCooldown(1, 3); + + let hull_regrowth = new ToggleAction("Hull Regrowth", { + power: 2, + effects: [new ValueEffect("hull", 0, 0, 10)] + }, "fractalhull"); + + return [ + { + code: "Base Attributes", + effects: [ + new AttributeEffect("precision", 9), + new AttributeEffect("maneuvrability", 3), + new AttributeEffect("hull_capacity", 40), + new AttributeEffect("shield_capacity", 40), + new AttributeEffect("power_capacity", 6), + ] + }, + { + code: "Main Engine", + actions: [engine] + }, + { + code: "Missile", + actions: [missile] + }, + { + code: "Damage Reductor", + actions: [protector] + }, + { + code: "Hull Regrowth", + actions: [hull_regrowth] + }, + ]; + } else { + return this.getStandardUpgrades(level); + } + } + } +} diff --git a/src/core/models/ModelRhino.ts b/src/core/models/ModelRhino.ts new file mode 100644 index 0000000..cdd31ad --- /dev/null +++ b/src/core/models/ModelRhino.ts @@ -0,0 +1,62 @@ +/// + +module TK.SpaceTac { + export class ModelRhino extends BaseModel { + constructor() { + super("rhino", "Rhino"); + } + + getDescription(): string { + return "A sturdy ship, able to sustain massive damage."; + } + + getLevelUpgrades(level: number): ModelUpgrade[] { + if (level == 1) { + let engine = new MoveAction("Engine", { + distance_per_power: 140, + }); + + let gatling = new TriggerAction("Gatling Gun", { + effects: [new DamageEffect(30, 20)], + power: 3, + range: 400, + }, "gatlinggun"); + gatling.configureCooldown(2, 2); + + let laser = new TriggerAction("Prokhorov Laser", { + effects: [new DamageEffect(25, 25)], + power: 4, + range: 250, angle: 60, + aim: 30, evasion: 45, luck: 30, + }, "prokhorovlaser"); + + return [ + { + code: "Base Attributes", + effects: [ + new AttributeEffect("precision", 4), + new AttributeEffect("maneuvrability", 3), + new AttributeEffect("hull_capacity", 100), + new AttributeEffect("shield_capacity", 20), + new AttributeEffect("power_capacity", 9), + ] + }, + { + code: "Main Engine", + actions: [engine] + }, + { + code: "Gatling Gun", + actions: [gatling] + }, + { + code: "Prokhorov Laser", + actions: [laser] + }, + ]; + } else { + return this.getStandardUpgrades(level); + } + } + } +} diff --git a/src/core/models/ModelTomahawk.ts b/src/core/models/ModelTomahawk.ts new file mode 100644 index 0000000..bb3a20b --- /dev/null +++ b/src/core/models/ModelTomahawk.ts @@ -0,0 +1,91 @@ +/// + +module TK.SpaceTac { + export class ModelTomahawk extends BaseModel { + constructor() { + super("tomahawk", "Tomahawk"); + } + + getDescription(): string { + return "A ship compensating its somewhat weak equipments with high power and usability."; + } + + getLevelUpgrades(level: number): ModelUpgrade[] { + if (level == 1) { + let engine = new MoveAction("Engine", { + distance_per_power: 160, + }); + + let gatling1 = new TriggerAction("Primary Gatling", { + effects: [new DamageEffect(15, 15)], + power: 2, range: 400 + }, "gatlinggun"); + gatling1.configureCooldown(1, 1); + + let gatling2 = new TriggerAction("Secondary Gatling", { + effects: [new DamageEffect(20, 15)], + power: 1, range: 200 + }, "gatlinggun"); + gatling2.configureCooldown(1, 1); + + let missile = new TriggerAction("Diffuse Missiles", { + effects: [new DamageEffect(10, 18)], + power: 2, + range: 200, blast: 100, + }, "submunitionmissile"); + missile.configureCooldown(1, 1); + + let laser = new TriggerAction("Low-power Laser", { + effects: [new DamageEffect(20, 20)], + power: 2, + range: 200, angle: 30 + }, "prokhorovlaser"); + laser.configureCooldown(1, 1); + + let cooler = new TriggerAction("Circuits Cooler", { + effects: [new CooldownEffect(1, 1)], + power: 1, + }, "kelvingenerator"); + + return [ + { + code: "Base Attributes", + effects: [ + new AttributeEffect("precision", 8), + new AttributeEffect("maneuvrability", 3), + new AttributeEffect("hull_capacity", 60), + new AttributeEffect("shield_capacity", 40), + new AttributeEffect("power_capacity", 11), + ] + }, + { + code: "Main Engine", + actions: [engine] + }, + { + code: "Primary Gatling", + actions: [gatling1] + }, + { + code: "Secondary Gatling", + actions: [gatling2] + }, + { + code: "SubMunition Missile", + actions: [missile] + }, + { + code: "Laser", + actions: [laser] + }, + { + code: "Cooler", + actions: [cooler] + }, + ]; + } else { + return this.getStandardUpgrades(level); + } + } + } +} diff --git a/src/core/models/ModelTrapper.ts b/src/core/models/ModelTrapper.ts new file mode 100644 index 0000000..8b7d8c2 --- /dev/null +++ b/src/core/models/ModelTrapper.ts @@ -0,0 +1,72 @@ +/// + +module TK.SpaceTac { + export class ModelTrapper extends BaseModel { + constructor() { + super("trapper", "Trapper"); + } + + getDescription(): string { + return "A mostly defensive ship, used to protect allies from enemy fire."; + } + + getLevelUpgrades(level: number): ModelUpgrade[] { + if (level == 1) { + let engine = new MoveAction("Engine", { + distance_per_power: 220, + }); + engine.configureCooldown(1, 1); + + let protector = new ToggleAction("Damage Protector", { + power: 4, + radius: 300, + effects: [new DamageModifierEffect(-35)] + }); + + let depleter = new TriggerAction("Power Depleter", { + effects: [new StickyEffect(new AttributeLimitEffect("power_capacity", 3))], + power: 2, + range: 200, + }, "powerdepleter"); + depleter.configureCooldown(1, 1); + + let missile = new TriggerAction("Defense Missiles", { + effects: [new DamageEffect(25, 30)], + power: 3, + range: 200, blast: 200, + }, "submunitionmissile"); + + return [ + { + code: "Base Attributes", + effects: [ + new AttributeEffect("precision", 3), + new AttributeEffect("maneuvrability", 2), + new AttributeEffect("hull_capacity", 40), + new AttributeEffect("shield_capacity", 70), + new AttributeEffect("power_capacity", 8), + ] + }, + { + code: "Main Engine", + actions: [engine] + }, + { + code: "Damage Protector", + actions: [protector] + }, + { + code: "Power Depleter", + actions: [depleter] + }, + { + code: "SubMunition Missile", + actions: [missile] + }, + ]; + } else { + return this.getStandardUpgrades(level); + } + } + } +} diff --git a/src/core/models/ModelXander.ts b/src/core/models/ModelXander.ts new file mode 100644 index 0000000..8a69e26 --- /dev/null +++ b/src/core/models/ModelXander.ts @@ -0,0 +1,72 @@ +/// + +module TK.SpaceTac { + export class ModelXander extends BaseModel { + constructor() { + super("xander", "Xander"); + } + + getDescription(): string { + return "A ship with impressive survival capabilities."; + } + + getLevelUpgrades(level: number): ModelUpgrade[] { + if (level == 1) { + let engine = new MoveAction("Engine", { + distance_per_power: 150, + }); + + let laser = new TriggerAction("Prokhorov Laser", { + effects: [new DamageEffect(20, 40)], + power: 3, + range: 250, angle: 80, + aim: 30, evasion: 45, luck: 30, + }); + + let hull = new TriggerAction("Hull Shedding", { + effects: [new ValueEffect("hull", 120)], + power: 1 + }, "fractalhull"); + hull.configureCooldown(1, 4); + + // TODO Is currently always used by move-fire simulator + let disengage = new MoveAction("Disengage", { + distance_per_power: 1000, + safety_distance: 200, + }, "ionthruster"); + disengage.configureCooldown(1, 3); + + return [ + { + code: "Base Attributes", + effects: [ + new AttributeEffect("precision", 8), + new AttributeEffect("maneuvrability", 5), + new AttributeEffect("hull_capacity", 80), + new AttributeEffect("shield_capacity", 15), + new AttributeEffect("power_capacity", 7), + ] + }, + { + code: "Main Engine", + actions: [engine] + }, + { + code: "Prokhorov Laser", + actions: [laser] + }, + { + code: "Fractal Hull", + actions: [hull] + }, + { + code: "Disengage", + actions: [disengage] + }, + ]; + } else { + return this.getStandardUpgrades(level); + } + } + } +} diff --git a/src/ui/AssetLoading.ts b/src/ui/AssetLoading.ts index 98e239a..7f1f27c 100644 --- a/src/ui/AssetLoading.ts +++ b/src/ui/AssetLoading.ts @@ -118,7 +118,6 @@ module TK.SpaceTac.UI { this.loadSheet("map/action.png", 323, 192); this.loadImage("map/orbit.png"); this.loadImage("map/boundaries.png"); - this.loadSheet("map/buttons.png", 115, 191); this.loadSheet("map/mission-action.png", 192, 56); this.loadSound("music/division.mp3"); diff --git a/src/ui/BaseView.ts b/src/ui/BaseView.ts index 164313c..ca91e7a 100644 --- a/src/ui/BaseView.ts +++ b/src/ui/BaseView.ts @@ -240,10 +240,10 @@ module TK.SpaceTac.UI { /** * Get an image from atlases */ - getImageInfo(name: string): { key: string, frame: number } { + getImageInfo(name: string): { key: string, frame: number, exists: boolean } { // TODO Cache if (this.game.cache.checkImageKey(name)) { - return { key: name, frame: 0 }; + return { key: name, frame: 0, exists: true }; } else { let i = 1; while (this.game.cache.checkImageKey(`atlas-${i}`)) { @@ -251,11 +251,11 @@ module TK.SpaceTac.UI { let frames = data.getFrames(); let frame = first(frames, frame => AssetLoading.getKey(frame.name) == `graphics-exported-${name}`); if (frame) { - return { key: `atlas-${i}`, frame: frame.index }; + return { key: `atlas-${i}`, frame: frame.index, exists: true }; } i++; } - return { key: `-missing-${name}`, frame: 0 }; + return { key: `-missing-${name}`, frame: 0, exists: false }; } } diff --git a/src/ui/battle/ActionBar.spec.ts b/src/ui/battle/ActionBar.spec.ts index 8e997cc..7531c0a 100644 --- a/src/ui/battle/ActionBar.spec.ts +++ b/src/ui/battle/ActionBar.spec.ts @@ -30,7 +30,7 @@ module TK.SpaceTac.UI.Specs { TestTools.addWeapon(ship, 10, 1, 100); bar.setShip(ship); check.equals(bar.action_icons.length, 3); - check.equals(bar.action_icons[1].action.code, "fire-equipment"); + check.equals(bar.action_icons[1].action.code, "weapon"); }); test.case("updates power points display", check => { @@ -54,7 +54,7 @@ module TK.SpaceTac.UI.Specs { // not owned ship let ship = new Ship(); - TestTools.setShipAP(ship, 8); + TestTools.setShipModel(ship, 100, 0, 8); bar.setShip(ship); checkpoints("not owned ship"); diff --git a/src/ui/battle/ActionBar.ts b/src/ui/battle/ActionBar.ts index 41086af..958b75b 100644 --- a/src/ui/battle/ActionBar.ts +++ b/src/ui/battle/ActionBar.ts @@ -83,7 +83,7 @@ module TK.SpaceTac.UI { } else if (diff instanceof ShipCooldownDiff) { return { background: async () => { - let icons = this.action_icons.filter(icon => icon.action.equipment && icon.action.equipment.is(diff.equipment)); + let icons = this.action_icons.filter(icon => icon.action.is(diff.action)); icons.forEach(icon => icon.refresh()); } } @@ -226,11 +226,7 @@ module TK.SpaceTac.UI { this.clearAll(); if (ship && this.battleview.player.is(ship.fleet.player) && ship.alive) { - var actions = ship.getAvailableActions(); - actions.forEach((action: BaseAction) => { - this.addAction(ship, action); - }); - + ship.actions.listAll().forEach(action => this.addAction(ship, action)); this.ship = ship; } else { this.ship = null; diff --git a/src/ui/battle/ActionIcon.spec.ts b/src/ui/battle/ActionIcon.spec.ts index 49bcf80..03617ce 100644 --- a/src/ui/battle/ActionIcon.spec.ts +++ b/src/ui/battle/ActionIcon.spec.ts @@ -32,8 +32,8 @@ module TK.SpaceTac.UI.Specs { test.case("displays disabled and fading states", check => { let bar = testgame.view.action_bar; let ship = new Ship(); - TestTools.setShipAP(ship, 5); - let action = nn(TestTools.addWeapon(ship, 50, 3).action); + TestTools.setShipModel(ship, 100, 0, 5); + let action = TestTools.addWeapon(ship, 50, 3); let icon = new ActionIcon(bar, ship, action, 0); check.equals(icon.container.name, "battle-actionbar-frame-enabled", "5/5"); @@ -70,11 +70,9 @@ module TK.SpaceTac.UI.Specs { test.case("displays toggle state", check => { let bar = testgame.view.action_bar; let ship = new Ship(); - TestTools.setShipAP(ship, 5); - let equipment = new Equipment(SlotType.Weapon); - ship.addSlot(SlotType.Weapon).attach(equipment); - let action = new ToggleAction(equipment, 2); - equipment.action = action; + TestTools.setShipModel(ship, 100, 0, 5); + let action = new ToggleAction("toggle", { power: 2 }); + ship.actions.addCustom(action); let icon = new ActionIcon(bar, ship, action, 0); check.equals(icon.img_bottom.name, "battle-actionbar-bottom-enabled", "initial"); @@ -82,7 +80,7 @@ module TK.SpaceTac.UI.Specs { check.equals(icon.img_sticky.name, "battle-actionbar-sticky-untoggled", "initial"); check.same(icon.img_sticky.visible, true, "initial"); - action.activated = true; + ship.actions.toggle(action, true); icon.refresh(); check.equals(icon.img_bottom.name, "battle-actionbar-bottom-toggled", "initial"); check.equals(icon.img_power.name, "battle-actionbar-consumption-toggled", "initial"); @@ -93,9 +91,10 @@ module TK.SpaceTac.UI.Specs { test.case("displays overheat/cooldown", check => { let bar = testgame.view.action_bar; let ship = new Ship(); - TestTools.setShipAP(ship, 5); - let action = nn(TestTools.addWeapon(ship, 50, 3).action); - action.cooldown.configure(1, 3); + let action = new TriggerAction("weapon"); + + action.configureCooldown(1, 3); + TestTools.setShipModel(ship, 100, 0, 5, 1, [action]); let icon = new ActionIcon(bar, ship, action, 0); check.same(icon.img_sticky.visible, false, "initial"); check.equals(icon.img_sticky.name, "battle-actionbar-sticky-untoggled", "initial"); @@ -106,14 +105,16 @@ module TK.SpaceTac.UI.Specs { check.equals(icon.img_sticky.name, "battle-actionbar-sticky-overheat", "overheat"); check.same(icon.img_sticky.children.length, 3, "overheat"); - action.cooldown.configure(1, 12); + action.configureCooldown(1, 12); + TestTools.setShipModel(ship, 100, 0, 5, 1, [action]); icon.refresh(action); check.same(icon.img_sticky.visible, true, "superheat"); check.equals(icon.img_sticky.name, "battle-actionbar-sticky-overheat", "superheat"); check.same(icon.img_sticky.children.length, 5, "superheat"); - action.cooldown.configure(1, 4); - action.cooldown.use(); + action.configureCooldown(1, 4); + TestTools.setShipModel(ship, 100, 0, 5, 1, [action]); + ship.actions.getCooldown(action).use(); icon.refresh(action); check.same(icon.img_sticky.visible, true, "cooling"); check.equals(icon.img_sticky.name, "battle-actionbar-sticky-disabled", "cooling"); @@ -125,8 +126,8 @@ module TK.SpaceTac.UI.Specs { let bar = testgame.view.action_bar; let ship = new Ship(); - TestTools.setShipAP(ship, 5); - let action = nn(TestTools.addWeapon(ship, 50, 3).action); + TestTools.setShipModel(ship, 100, 0, 5); + let action = TestTools.addWeapon(ship, 50, 3); let icon = new ActionIcon(bar, ship, action, 0); check.same(icon.img_targetting.visible, false, "initial"); diff --git a/src/ui/battle/ActionIcon.ts b/src/ui/battle/ActionIcon.ts index f41c6d4..b2fddde 100644 --- a/src/ui/battle/ActionIcon.ts +++ b/src/ui/battle/ActionIcon.ts @@ -50,7 +50,7 @@ module TK.SpaceTac.UI { let builder = new UIBuilder(this.view, this.container); // Action icon - this.img_action = builder.image([`action-${action.code}`, `equipment-${action.equipment ? action.equipment.code : "---"}`]); + this.img_action = builder.image(`action-${action.code}`); this.img_action.anchor.set(0.5); this.img_action.scale.set(0.35); this.img_action.alpha = 0.2; @@ -147,7 +147,7 @@ module TK.SpaceTac.UI { this.processSelection(Target.newFromShip(this.ship)); } else { // Switch to targetting mode (will apply action when a target is selected) - this.view.enterTargettingMode(this.action, mode); + this.view.enterTargettingMode(this.ship, this.action, mode); } } @@ -170,13 +170,14 @@ module TK.SpaceTac.UI { refresh(used: BaseAction | null = null, power_consumption = 0): void { let disabled = bool(this.action.checkCannotBeApplied(this.ship)); let selected = (used === this.action); - let toggled = (this.action instanceof ToggleAction) && this.action.activated; + let toggled = (this.action instanceof ToggleAction) && this.ship.actions.isToggled(this.action); let fading = bool(this.action.checkCannotBeApplied(this.ship, this.ship.getValue("power") - power_consumption)); - let cooldown = this.action.cooldown.heat; + let cooldown = this.ship.actions.getCooldown(this.action); + let heat = cooldown.heat; let targetting = used !== null; - if (this.action == used && this.action.cooldown.willOverheat()) { + if (this.action == used && cooldown.willOverheat()) { fading = true; - cooldown = this.action.cooldown.cooling; + heat = cooldown.cooling; } // inputs @@ -242,7 +243,7 @@ module TK.SpaceTac.UI { } // right - if (toggled != this.toggled || disabled != this.disabled || cooldown != this.cooldown) { + if (toggled != this.toggled || disabled != this.disabled || heat != this.cooldown) { destroyChildren(this.img_sticky); if (this.action instanceof ToggleAction) { if (toggled) { @@ -251,13 +252,13 @@ module TK.SpaceTac.UI { this.view.changeImage(this.img_sticky, "battle-actionbar-sticky-untoggled"); } this.img_sticky.visible = !disabled; - } else if (cooldown) { + } else if (heat) { if (disabled) { this.view.changeImage(this.img_sticky, "battle-actionbar-sticky-disabled"); } else { this.view.changeImage(this.img_sticky, "battle-actionbar-sticky-overheat"); } - range(Math.min(cooldown - 1, 4)).forEach(i => { + range(Math.min(heat - 1, 4)).forEach(i => { this.img_sticky.addChild(this.view.newImage("battle-actionbar-cooldown-one", 0, 2 - i * 7)); }); this.img_sticky.addChild(this.view.newImage("battle-actionbar-cooldown-front", -4, -20)); @@ -272,7 +273,7 @@ module TK.SpaceTac.UI { this.targetting = targetting; this.fading = fading; this.toggled = toggled; - this.cooldown = cooldown; + this.cooldown = heat; } } } diff --git a/src/ui/battle/ActionTooltip.spec.ts b/src/ui/battle/ActionTooltip.spec.ts index ebb6c9b..70f20fa 100644 --- a/src/ui/battle/ActionTooltip.spec.ts +++ b/src/ui/battle/ActionTooltip.spec.ts @@ -7,26 +7,21 @@ module TK.SpaceTac.UI.Specs { test.case("displays action information", check => { let tooltip = new Tooltip(testgame.view); let ship = new Ship(); - TestTools.setShipAP(ship, 10); + TestTools.setShipModel(ship, 100, 0, 10); - let action1 = new MoveAction(new Equipment()); - nn(action1.equipment).name = "Engine"; - check.patch(action1, "getVerb", () => "Move"); - let action2 = new TriggerAction(new Equipment(), [new DamageEffect(12)], 2, 50, 0); - nn(action2.equipment).name = "Weapon"; - check.patch(action2, "getVerb", () => "Fire"); + let action1 = new MoveAction("Thruster"); + let action2 = new TriggerAction("Superweapon", { effects: [new DamageEffect(12)], power: 2, range: 50 }); let action3 = new EndTurnAction(); - check.patch(action3, "getVerb", () => "End turn"); ActionTooltip.fill(tooltip.getBuilder(), ship, action1, 0); - checkText(check, (tooltip).container.content.children[1], "Engine"); + checkText(check, (tooltip).container.content.children[1], "Use Thruster"); checkText(check, (tooltip).container.content.children[2], "Cost: 1 power per 0km"); checkText(check, (tooltip).container.content.children[3], "Move: 0km per power point (safety: 120km)"); checkText(check, (tooltip).container.content.children[4], "[ 1 ]"); tooltip.hide(); ActionTooltip.fill(tooltip.getBuilder(), ship, action2, 1); - checkText(check, (tooltip).container.content.children[1], "Weapon"); + checkText(check, (tooltip).container.content.children[1], "Fire Superweapon"); checkText(check, (tooltip).container.content.children[2], "Cost: 2 power"); checkText(check, (tooltip).container.content.children[3], "Fire (power 2, range 50km):\n• do 12 damage on target"); checkText(check, (tooltip).container.content.children[4], "[ 2 ]"); diff --git a/src/ui/battle/ActionTooltip.ts b/src/ui/battle/ActionTooltip.ts index ab95c64..f888698 100644 --- a/src/ui/battle/ActionTooltip.ts +++ b/src/ui/battle/ActionTooltip.ts @@ -9,10 +9,10 @@ module TK.SpaceTac.UI { static fill(filler: TooltipBuilder, ship: Ship, action: BaseAction, position: number) { let builder = filler.styled({ size: 20 }); - let icon = builder.image([`equipment-${action.equipment ? action.equipment.code : "---"}`, `action-${action.code}`]); + let icon = builder.image(`action-${action.code}`); icon.scale.set(0.5); - builder.text(action.equipment ? action.equipment.name : action.getVerb(), 150, 0, { size: 24 }); + builder.text(action.getTitle(ship), 150, 0, { size: 24 }); let cost = ""; if (action instanceof MoveAction) { @@ -37,8 +37,8 @@ module TK.SpaceTac.UI { builder.text(cost, 150, 40, { color: "#ffdd4b" }); } - if (action.equipment && action.equipment.cooldown.overheat) { - let cooldown = action.equipment.cooldown; + let cooldown = ship.actions.getCooldown(action); + if (cooldown.overheat) { if (cooldown.heat > 0) { builder.text("Cooling down ...", 150, 80, { color: "#d8894d" }); } else if (cooldown.willOverheat() && cost != "Not enough power") { @@ -49,7 +49,7 @@ module TK.SpaceTac.UI { builder.text("Unavailable until next turn if used", 150, 80, { color: "#d8894d" }); } } - } else if (action instanceof ToggleAction && action.activated) { + } else if (action instanceof ToggleAction && ship.actions.isToggled(action)) { builder.text(`Activated`, 150, 80, { color: "#dbe748" }); } diff --git a/src/ui/battle/Arena.ts b/src/ui/battle/Arena.ts index 606cb2c..e7eafca 100644 --- a/src/ui/battle/Arena.ts +++ b/src/ui/battle/Arena.ts @@ -65,9 +65,12 @@ module TK.SpaceTac.UI { this.range_hint.setLayer(this.layer_hints); this.addShipSprites(); + view.battle.drones.list().forEach(drone => this.addDrone(drone, false)); this.container.onDestroy.add(() => this.destroy()); + view.log_processor.register(diff => this.checkDroneDeployed(diff)); + view.log_processor.register(diff => this.checkDroneRecalled(diff)); view.log_processor.watchForShipChange(ship => { return { foreground: async () => { @@ -292,5 +295,45 @@ module TK.SpaceTac.UI { getBoundaries(): IBounded { return this.boundaries; } + + /** + * Check if a new drone as been deployed + */ + private checkDroneDeployed(diff: BaseBattleDiff): LogProcessorDelegate { + if (diff instanceof DroneDeployedDiff) { + return { + foreground: async (animate) => { + let duration = this.addDrone(diff.drone, animate); + if (duration) { + this.view.gameui.audio.playOnce("battle-drone-deploy"); + if (animate) { + await this.view.timer.sleep(duration); + } + } + } + } + } else { + return {}; + } + } + + /** + * Check if a drone as been recalled + */ + private checkDroneRecalled(diff: BaseBattleDiff): LogProcessorDelegate { + if (diff instanceof DroneRecalledDiff) { + return { + foreground: async () => { + let duration = this.removeDrone(diff.drone); + if (duration) { + this.view.gameui.audio.playOnce("battle-drone-destroy"); + await this.view.timer.sleep(duration); + } + } + } + } else { + return {}; + } + } } } diff --git a/src/ui/battle/ArenaDrone.ts b/src/ui/battle/ArenaDrone.ts index e5c196b..3ed4639 100644 --- a/src/ui/battle/ArenaDrone.ts +++ b/src/ui/battle/ArenaDrone.ts @@ -39,8 +39,7 @@ module TK.SpaceTac.UI { this.activation.visible = false; this.add(this.activation); - let name = this.view.getFirstImage(`equipment-${drone.code}`, `battle-actions-deploy-${drone.code}`); - this.sprite = this.view.newButton(name); + this.sprite = this.view.newButton(`action-${drone.code}`); this.sprite.anchor.set(0.5, 0.5); this.sprite.scale.set(0.1, 0.1); this.add(this.sprite); diff --git a/src/ui/battle/ArenaShip.spec.ts b/src/ui/battle/ArenaShip.spec.ts index 9e61ebb..26907c7 100644 --- a/src/ui/battle/ArenaShip.spec.ts +++ b/src/ui/battle/ArenaShip.spec.ts @@ -10,11 +10,11 @@ module TK.SpaceTac.UI.Specs { check.equals(sprite.effects_messages.children.length, 0); - sprite.displayAttributeChanged(new ShipAttributeDiff(ship, "power_generation", { cumulative: -4 }, {})); + sprite.displayAttributeChanged(new ShipAttributeDiff(ship, "power_capacity", { cumulative: -4 }, {})); check.equals(sprite.effects_messages.children.length, 1); let t1 = sprite.effects_messages.getChildAt(0); - check.equals(t1.text, "power generation -4"); + check.equals(t1.text, "power capacity -4"); }); test.case("adds sticky effects display", check => { diff --git a/src/ui/battle/ArenaShip.ts b/src/ui/battle/ArenaShip.ts index 81c0ffa..5f7db3a 100644 --- a/src/ui/battle/ArenaShip.ts +++ b/src/ui/battle/ArenaShip.ts @@ -210,15 +210,13 @@ module TK.SpaceTac.UI { } else if (diff instanceof ShipActionToggleDiff) { return { foreground: async (animate, timer) => { - let action = this.ship.getAction(diff.action); - if (action && action.equipment) { - let equname = action.equipment.name; - + let action = this.ship.actions.getById(diff.action); + if (action) { if (animate) { if (diff.activated) { - await this.displayEffect(`${equname} ON`, true); + await this.displayEffect(`${action.name} ON`, true); } else { - await this.displayEffect(`${equname} OFF`, false); + await this.displayEffect(`${action.name} OFF`, false); } } @@ -228,19 +226,9 @@ module TK.SpaceTac.UI { } } } else if (diff instanceof ShipActionUsedDiff) { - let action = this.ship.getAction(diff.action); + let action = this.ship.actions.getById(diff.action); if (action) { - if (!(action instanceof ToggleAction) && action.equipment) { - let equipment = action.equipment; - return { - foreground: async (animate, timer) => { - if (animate) { - await this.displayEffect(equipment.name, true); - await timer.sleep(300); - } - } - } - } else if (action instanceof EndTurnAction) { + if (action instanceof EndTurnAction) { return { foreground: async (animate, timer) => { if (animate) { @@ -249,6 +237,16 @@ module TK.SpaceTac.UI { } } } + } else if (!(action instanceof ToggleAction)) { + let action_name = action.name; + return { + foreground: async (animate, timer) => { + if (animate) { + await this.displayEffect(action_name, true); + await timer.sleep(300); + } + } + } } else { return {}; } @@ -408,13 +406,11 @@ module TK.SpaceTac.UI { */ updateEffectsRadius(): void { this.effects_radius.clear(); - this.ship.getAvailableActions().forEach(action => { - if (action instanceof ToggleAction && action.activated) { - this.effects_radius.lineStyle(2, 0xe9f2f9, 0.3); - this.effects_radius.beginFill(0xe9f2f9, 0.0); - this.effects_radius.drawCircle(0, 0, action.radius * 2); - this.effects_radius.endFill(); - } + this.ship.actions.listToggled().forEach(action => { + this.effects_radius.lineStyle(2, 0xe9f2f9, 0.3); + this.effects_radius.beginFill(0xe9f2f9, 0.0); + this.effects_radius.drawCircle(0, 0, action.radius * 2); + this.effects_radius.endFill(); }); } } diff --git a/src/ui/battle/BattleView.spec.ts b/src/ui/battle/BattleView.spec.ts index 9ff98c7..597d8ac 100644 --- a/src/ui/battle/BattleView.spec.ts +++ b/src/ui/battle/BattleView.spec.ts @@ -34,15 +34,16 @@ module TK.SpaceTac.UI.Specs { check.equals(battleview.targetting.active, false); battleview.setInteractionEnabled(true); - let weapon = TestTools.addWeapon(nn(battleview.battle.playing_ship), 10); - battleview.enterTargettingMode(nn(weapon.action), ActionTargettingMode.SPACE); + let ship = nn(battleview.battle.playing_ship); + let weapon = TestTools.addWeapon(ship, 10); + battleview.enterTargettingMode(ship, weapon, ActionTargettingMode.SPACE); check.equals(battleview.targetting.active, true); battleview.cursorHovered(new ArenaLocation(5, 8), null); check.equals(battleview.targetting.target, Target.newFromLocation(5, 8)); check.equals(battleview.ship_hovered, null); - let ship = battleview.battle.play_order[3]; + ship = battleview.battle.play_order[3]; battleview.cursorHovered(ship.location, ship); check.equals(battleview.targetting.target, Target.newFromLocation(ship.arena_x, ship.arena_y)); check.equals(battleview.ship_hovered, null); diff --git a/src/ui/battle/BattleView.ts b/src/ui/battle/BattleView.ts index a523b21..5116d88 100644 --- a/src/ui/battle/BattleView.ts +++ b/src/ui/battle/BattleView.ts @@ -119,7 +119,7 @@ module TK.SpaceTac.UI { this.layer_borders, this.getWidth() - 112, 0); this.ship_list.bindToLog(this.log_processor); this.ship_tooltip = new ShipTooltip(this); - this.character_sheet = new CharacterSheet(this, -this.getWidth()); + this.character_sheet = new CharacterSheet(this); this.layer_sheets.add(this.character_sheet); // Targetting info @@ -192,7 +192,7 @@ module TK.SpaceTac.UI { let ship = this.actual_battle.playing_ship; if (ship) { - let ship_action = first(ship.getAvailableActions(), ac => ac.is(action)); + let ship_action = ship.actions.getById(action.id); if (ship_action) { let result = this.actual_battle.applyOneAction(action.id, target); if (result) { @@ -200,7 +200,7 @@ module TK.SpaceTac.UI { } return result; } else { - console.error("Action not found in available list", action, ship.getAvailableActions()); + console.error("Action not found in available list", action, ship.actions); return false; } } else { @@ -327,20 +327,20 @@ module TK.SpaceTac.UI { // Enter targetting mode // While in this mode, the Targetting object will receive hover and click events, and handle them - enterTargettingMode(action: BaseAction, mode: ActionTargettingMode): Targetting | null { + enterTargettingMode(ship: Ship, action: BaseAction, mode: ActionTargettingMode): Targetting | null { if (!this.interacting) { return null; } this.setShipHovered(null); - this.targetting.setAction(action, mode); + this.targetting.setAction(ship, action, mode); return this.targetting; } // Exit targetting mode exitTargettingMode(): void { - this.targetting.setAction(null); + this.targetting.setAction(null, null); } /** diff --git a/src/ui/battle/LogProcessor.ts b/src/ui/battle/LogProcessor.ts index e510227..ccda5e5 100644 --- a/src/ui/battle/LogProcessor.ts +++ b/src/ui/battle/LogProcessor.ts @@ -44,8 +44,6 @@ module TK.SpaceTac.UI { this.register((diff) => this.checkProjectileFired(diff)); this.register((diff) => this.checkShipDeath(diff)); this.register((diff) => this.checkBattleEnded(diff)); - this.register((diff) => this.checkDroneDeployed(diff)); - this.register((diff) => this.checkDroneRecalled(diff)); } /** @@ -136,7 +134,7 @@ module TK.SpaceTac.UI { changed = true; } else if (!immediate && diff instanceof ShipActionEndedDiff) { let ship = this.view.battle.getShip(diff.ship_id); - if (ship && ship.getAction(diff.action) instanceof EndTurnAction) { + if (ship && ship.actions.getById(diff.action) instanceof EndTurnAction) { changed = true; } } @@ -260,9 +258,9 @@ module TK.SpaceTac.UI { if (diff instanceof ProjectileFiredDiff) { let ship = this.view.battle.getShip(diff.ship_id); if (ship) { - let equipment = ship.getEquipment(diff.equipment); - if (equipment && equipment.slot_type == SlotType.Weapon) { - let effect = new WeaponEffect(this.view.arena, ship, diff.target, equipment); + let action = ship.actions.getById(diff.action); + if (action && action instanceof TriggerAction) { + let effect = new WeaponEffect(this.view.arena, ship, diff.target, action); return { foreground: async (animate, timer) => { if (animate) { @@ -316,62 +314,6 @@ module TK.SpaceTac.UI { return {}; } - - /** - * Check if a new drone as been deployed - */ - private checkDroneDeployed(diff: BaseBattleDiff): LogProcessorDelegate { - if (diff instanceof DroneDeployedDiff) { - return { - foreground: async (animate) => { - let duration = this.view.arena.addDrone(diff.drone, animate); - if (duration) { - this.view.gameui.audio.playOnce("battle-drone-deploy"); - if (animate) { - await this.view.timer.sleep(duration); - } - } - } - } - } else { - return {}; - } - } - - /** - * Check if a drone as been recalled - */ - private checkDroneRecalled(diff: BaseBattleDiff): LogProcessorDelegate { - if (diff instanceof DroneRecalledDiff) { - return { - foreground: async () => { - let duration = this.view.arena.removeDrone(diff.drone); - if (duration) { - this.view.gameui.audio.playOnce("battle-drone-destroy"); - await this.view.timer.sleep(duration); - } - } - } - } else { - return {}; - } - } - - // Drone applied - /*private processDroneAppliedEvent(event: DroneAppliedDiff): number { - let drone = this.view.arena.findDrone(event.drone); - if (drone) { - let duration = drone.setApplied(); - - if (duration) { - this.view.gameui.audio.playOnce("battle-drone-activate"); - } - - return duration; - } else { - return 0; - } - }*/ } /** diff --git a/src/ui/battle/OutcomeDialog.ts b/src/ui/battle/OutcomeDialog.ts index e4b472d..41fc694 100644 --- a/src/ui/battle/OutcomeDialog.ts +++ b/src/ui/battle/OutcomeDialog.ts @@ -54,25 +54,9 @@ module TK.SpaceTac.UI { parent.exitBattle(); }); } else if (victory) { - if (this.outcome.loot.length) { - this.addActionButton(535, "Loot equipment", "Open character sheet to loot equipment from defeated fleet", () => { - let sheet = new CharacterSheet(this.view, undefined, undefined, () => { - sheet.destroy(true); - this.refreshContent(); - }); - sheet.show(this.player.fleet.ships[0], false, undefined, true); - sheet.setLoot(outcome.loot); - this.view.add.existing(sheet); - }); - - this.addActionButton(957, "Back to map", "Exit the battle and go back to the map", () => { - parent.exitBattle(); - }); - } else { - this.addActionButton(747, "Back to map", "Exit the battle and go back to the map", () => { - parent.exitBattle(); - }); - } + this.addActionButton(747, "Back to map", "Exit the battle and go back to the map", () => { + parent.exitBattle(); + }); } else { this.addActionButton(535, "Revert battle", "Go back to where the fleet was before the battle happened", () => { parent.revertBattle(); diff --git a/src/ui/battle/ShipList.spec.ts b/src/ui/battle/ShipList.spec.ts index 86a5342..5f37d9b 100644 --- a/src/ui/battle/ShipList.spec.ts +++ b/src/ui/battle/ShipList.spec.ts @@ -27,7 +27,7 @@ module TK.SpaceTac.UI.Specs { }); let ship = battle.fleets[0].addShip(); - TestTools.setShipHP(ship, 10, 0); + TestTools.setShipModel(ship, 10, 0); list.setShipsFromBattle(battle, false); check.in("one ship added but not in play order", check => { check.equals(list.items.length, 1, "item count"); @@ -41,7 +41,7 @@ module TK.SpaceTac.UI.Specs { }); ship = battle.fleets[1].addShip(); - TestTools.setShipHP(ship, 10, 0); + TestTools.setShipModel(ship, 10, 0); battle.throwInitiative(); list.setShipsFromBattle(battle, false); check.in("ship added in the other fleet", check => { @@ -65,7 +65,7 @@ module TK.SpaceTac.UI.Specs { }); ship = battle.fleets[1].addShip(); - TestTools.setShipHP(ship, 10, 0); + TestTools.setShipModel(ship, 10, 0); battle.throwInitiative(); battle.setPlayingShip(battle.play_order[0]); list.setShipsFromBattle(battle, false); diff --git a/src/ui/battle/ShipTooltip.spec.ts b/src/ui/battle/ShipTooltip.spec.ts index 0a7d91b..5cb3206 100644 --- a/src/ui/battle/ShipTooltip.spec.ts +++ b/src/ui/battle/ShipTooltip.spec.ts @@ -5,11 +5,10 @@ module TK.SpaceTac.UI.Specs { test.case("fills ship details", check => { let tooltip = new ShipTooltip(testgame.view); let ship = testgame.view.battle.play_order[2]; + TestTools.setShipModel(ship, 58, 140, 12); ship.name = "Fury"; - ship.model = new ShipModel("fake", "Fury"); - ship.listEquipment().forEach(equ => equ.detach()); - TestTools.setShipHP(ship, 58, 140); - TestTools.setShipAP(ship, 12); + ship.model = new BaseModel("fake", "Fury"); + check.patch(ship.model, "getDescription", () => "Super ship model !"); TestTools.addWeapon(ship, 50); TestTools.setAttribute(ship, "precision", 7); TestTools.setAttribute(ship, "maneuvrability", 3); @@ -24,12 +23,12 @@ module TK.SpaceTac.UI.Specs { let images = collectImages((tooltip).container); let texts = collectTexts((tooltip).container); check.contains(images, "ship-fake-portrait"); - check.contains(images, "equipment-equipment"); + check.contains(images, "action-weapon"); check.equals(texts, [ "Level 1 Fury", "Plays in 2 turns", "7", "3", "9", "max", "12", "57", "max", "58", "100", "max", "140", - "Active effects", "• hull capacity +50", "• damage -15% for 3 turns", "• limit precision to 10", - "Weapons", "equipment Mk1" + "Weapon", "• hull capacity +50", "• damage -15% for 3 turns", "• limit precision to 10", + "Super ship model !" ]); }); }); diff --git a/src/ui/battle/ShipTooltip.ts b/src/ui/battle/ShipTooltip.ts index c5f4e23..f0d9782 100644 --- a/src/ui/battle/ShipTooltip.ts +++ b/src/ui/battle/ShipTooltip.ts @@ -23,10 +23,9 @@ module TK.SpaceTac.UI { builder.configure(10, 6, this.battleview.arena.getBoundaries()); - let portrait_bg = builder.image("battle-tooltip-ship-portrait", -18, -18); + let portrait_bg = builder.image("battle-tooltip-ship-portrait", 0, 0); builder.in(portrait_bg, builder => { - let portrait = builder.image(`ship-${ship.model.code}-portrait`, portrait_bg.width / 2, portrait_bg.height / 2); - portrait.anchor.set(0.5); + let portrait = builder.image(`ship-${ship.model.code}-portrait`, 1, 1); portrait.scale.set(0.75); }); @@ -37,36 +36,32 @@ module TK.SpaceTac.UI { let turns = this.battleview.battle.getPlayOrder(ship); builder.text((turns == 0) ? "Playing" : ((turns == 1) ? "Plays next" : `Plays in ${turns} turns`), 230, 36, { color: "#cccccc", size: 18 }); - ShipTooltip.addValue(builder, 0, "#aa6f33", "character-attribute-precision", ship.getAttribute("precision")); - ShipTooltip.addValue(builder, 1, "#c1f06b", "character-attribute-maneuvrability", ship.getAttribute("maneuvrability")); - ShipTooltip.addValue(builder, 2, "#ffdd4b", "character-value-power", ship.getValue("power"), ship.getAttribute("power_capacity")); - ShipTooltip.addValue(builder, 3, "#eb4e4a", "character-value-hull", ship.getValue("hull"), ship.getAttribute("hull_capacity")); - ShipTooltip.addValue(builder, 4, "#2ad8dc", "character-value-shield", ship.getValue("shield"), ship.getAttribute("shield_capacity")); + ShipTooltip.addValue(builder, 0, "#aa6f33", "attribute-precision", ship.getAttribute("precision")); + ShipTooltip.addValue(builder, 1, "#c1f06b", "attribute-maneuvrability", ship.getAttribute("maneuvrability")); + ShipTooltip.addValue(builder, 2, "#ffdd4b", "attribute-power_capacity", ship.getValue("power"), ship.getAttribute("power_capacity")); + ShipTooltip.addValue(builder, 3, "#eb4e4a", "attribute-hull_capacity", ship.getValue("hull"), ship.getAttribute("hull_capacity")); + ShipTooltip.addValue(builder, 4, "#2ad8dc", "attribute-shield_capacity", ship.getValue("shield"), ship.getAttribute("shield_capacity")); let iy = 210; - let effects = ship.active_effects.list(); - if (effects.length > 0) { - builder.text("Active effects", 0, iy, { color: "#ffffff", size: 18, bold: true }); - iy += 30; - effects.forEach(effect => { - builder.text(`• ${effect.getDescription()}`, 0, iy, { color: effect.isBeneficial() ? "#afe9c6" : "#e9afaf" }); - iy += 26; - }); - } - let weapons = ship.listEquipment(SlotType.Weapon); - if (weapons.length > 0) { - builder.text("Weapons", 0, iy, { size: 18, bold: true }); - iy += 30; - weapons.forEach(weapon => { - let icon = builder.image(`equipment-${weapon.code}`, 0, iy); - icon.scale.set(0.1); - builder.text(weapon.getFullName(), 32, iy); - iy += 26; - }); - } + ship.actions.listAll().forEach(action => { + if (!(action instanceof EndTurnAction) && !(action instanceof MoveAction)) { + let icon = builder.image(`action-${action.code}`, 0, iy); + icon.scale.set(0.15); + builder.text(action.name, 46, iy + 8); + iy += 40; + } + }); + + ship.active_effects.list().forEach(effect => { + builder.text(`• ${effect.getDescription()}`, 0, iy, { color: effect.isBeneficial() ? "#afe9c6" : "#e9afaf" }); + iy += 32; + }); + + builder.text(ship.model.getDescription(), 0, iy + 4, { size: 14, color: "#999999", width: 540 }); } else { - builder.text("Emergency Stasis Protocol\nship disabled", 140, 36, { color: "#a899db", size: 20, center: true, vcenter: true }); + builder.text("Emergency Stasis Protocol\nship disabled", 140, 36, + { color: "#a899db", size: 20, center: true, vcenter: true }); } let sprite = this.battleview.arena.findShipSprite(ship); diff --git a/src/ui/battle/Targetting.spec.ts b/src/ui/battle/Targetting.spec.ts index 5bce90b..cc4af9e 100644 --- a/src/ui/battle/Targetting.spec.ts +++ b/src/ui/battle/Targetting.spec.ts @@ -16,12 +16,12 @@ module TK.SpaceTac.UI.Specs { ship.setArenaPosition(10, 20); let weapon = TestTools.addWeapon(ship); let engine = TestTools.addEngine(ship, 12); - targetting.setAction(weapon.action); + targetting.setAction(ship, weapon); let drawvector = check.patch(targetting, "drawVector", null); let part = { - action: nn(weapon.action), + action: weapon, target: new Target(50, 30), ap: 5, possible: true @@ -36,8 +36,8 @@ module TK.SpaceTac.UI.Specs { [0x8e8e8e, 10, 20, 50, 30, 0] ]); - targetting.action = engine.action; - part.action = nn(engine.action); + targetting.action = engine; + part.action = engine; targetting.drawPart(part, true, null); check.called(drawvector, [ [0xe09c47, 10, 20, 50, 30, 12] @@ -48,7 +48,7 @@ module TK.SpaceTac.UI.Specs { let targetting = newTargetting(); let ship = nn(testgame.view.battle.playing_ship); let impacts = targetting.impact_indicators; - let action = new TriggerAction(new Equipment(), [], 1, 0, 50); + let action = new TriggerAction("weapon", { range: 50 }); let collect = check.patch(action, "getImpactedShips", iterator([ [new Ship(), new Ship(), new Ship()], @@ -85,7 +85,7 @@ module TK.SpaceTac.UI.Specs { let engine = TestTools.addEngine(ship, 8000); let weapon = TestTools.addWeapon(ship, 30, 5, 100, 50); - targetting.setAction(weapon.action); + targetting.setAction(ship, weapon); targetting.setTarget(Target.newFromLocation(156, 65)); check.patch(targetting, "simulate", () => { @@ -99,8 +99,8 @@ module TK.SpaceTac.UI.Specs { result.need_fire = true; result.can_fire = true; result.parts = [ - { action: nn(engine.action), target: Target.newFromLocation(80, 20), ap: 1, possible: true }, - { action: nn(weapon.action), target: Target.newFromLocation(156, 65), ap: 5, possible: true } + { action: engine, target: Target.newFromLocation(80, 20), ap: 1, possible: true }, + { action: weapon, target: Target.newFromLocation(156, 65), ap: 5, possible: true } ] targetting.simulation = result; }); @@ -121,30 +121,30 @@ module TK.SpaceTac.UI.Specs { test.case("snaps on ships according to targetting mode", check => { let targetting = newTargetting(); let playing_ship = nn(testgame.view.battle.playing_ship); - let action = TestTools.addWeapon(playing_ship).action; + let action = TestTools.addWeapon(playing_ship); let ship1 = testgame.view.battle.play_order[1]; let ship2 = testgame.view.battle.play_order[2]; ship1.setArenaPosition(8000, 50); ship2.setArenaPosition(8000, 230); - targetting.setAction(action, ActionTargettingMode.SPACE); + targetting.setAction(playing_ship, action, ActionTargettingMode.SPACE); targetting.setTargetFromLocation({ x: 8000, y: 60 }); check.equals(targetting.target, Target.newFromLocation(8000, 60), "space"); - targetting.setAction(action, ActionTargettingMode.SHIP); + targetting.setAction(playing_ship, action, ActionTargettingMode.SHIP); targetting.setTargetFromLocation({ x: 8000, y: 60 }); check.equals(targetting.target, Target.newFromShip(ship1), "ship 1"); targetting.setTargetFromLocation({ x: 8100, y: 200 }); check.equals(targetting.target, Target.newFromShip(ship2), "ship 2"); - targetting.setAction(action, ActionTargettingMode.SURROUNDINGS); + targetting.setAction(playing_ship, action, ActionTargettingMode.SURROUNDINGS); targetting.setTargetFromLocation({ x: 8000, y: 60 }); check.equals(targetting.target, new Target(8000, 60, playing_ship), "surroundings 1"); targetting.setTargetFromLocation({ x: playing_ship.arena_x + 10, y: playing_ship.arena_y - 20 }); check.equals(targetting.target, Target.newFromShip(playing_ship), "surroundings 2"); - targetting.setAction(action, ActionTargettingMode.SELF); + targetting.setAction(playing_ship, action, ActionTargettingMode.SELF); targetting.setTargetFromLocation({ x: 8000, y: 60 }); check.equals(targetting.target, Target.newFromShip(playing_ship), "self 1"); targetting.setTargetFromLocation({ x: 0, y: 0 }); @@ -155,26 +155,25 @@ module TK.SpaceTac.UI.Specs { let targetting = newTargetting(); let ship = nn(testgame.view.battle.playing_ship); ship.setArenaPosition(0, 0); - ship.listEquipment(SlotType.Engine).forEach(engine => engine.detach()); - TestTools.setShipAP(ship, 8); - let move = TestTools.addEngine(ship, 100).action; - let fire = TestTools.addWeapon(ship, 50, 2, 300, 100).action; + TestTools.setShipModel(ship, 100, 0, 8); + let move = TestTools.addEngine(ship, 100); + let fire = TestTools.addWeapon(ship, 50, 2, 300, 100); let last_call: any = null; check.patch(targetting.range_hint, "clear", () => last_call = null); check.patch(targetting.range_hint, "update", (ship: Ship, action: BaseAction, radius: number) => last_call = [ship, action, radius]); // move action - targetting.setAction(move); + targetting.setAction(ship, move); targetting.setTargetFromLocation({ x: 200, y: 0 }); check.equals(last_call, [ship, move, 800]); // fire action - targetting.setAction(fire); + targetting.setAction(ship, fire); targetting.setTargetFromLocation({ x: 200, y: 0 }); check.equals(last_call, [ship, fire, undefined]); // move+fire - targetting.setAction(fire); + targetting.setAction(ship, fire); targetting.setTargetFromLocation({ x: 400, y: 0 }); check.equals(last_call, [ship, move, 600]); }); diff --git a/src/ui/battle/Targetting.ts b/src/ui/battle/Targetting.ts index 4a7bc02..124a267 100644 --- a/src/ui/battle/Targetting.ts +++ b/src/ui/battle/Targetting.ts @@ -258,8 +258,8 @@ module TK.SpaceTac.UI { } } else { let engine = new MoveFireSimulator(this.ship).findBestEngine(); - if (engine && engine.action) { - move_action = engine.action; + if (engine) { + move_action = engine; } } if (move_action) { @@ -295,9 +295,9 @@ module TK.SpaceTac.UI { /** * Set the current targetting action, or null to stop targetting */ - setAction(action: BaseAction | null, mode?: ActionTargettingMode): void { - if (action && action.equipment && action.equipment.attached_to && action.equipment.attached_to.ship) { - this.ship = action.equipment.attached_to.ship; + setAction(ship: Ship | null, action: BaseAction | null, mode?: ActionTargettingMode): void { + if (action && ship && ship.actions.getById(action.id)) { + this.ship = ship; this.action = action; this.mode = (typeof mode == "undefined") ? action.getTargettingMode(this.ship) : mode; diff --git a/src/ui/battle/WeaponEffect.spec.ts b/src/ui/battle/WeaponEffect.spec.ts index 9168a92..21994b0 100644 --- a/src/ui/battle/WeaponEffect.spec.ts +++ b/src/ui/battle/WeaponEffect.spec.ts @@ -17,7 +17,7 @@ module TK.SpaceTac.UI.Specs { let battleview = testgame.view; battleview.timer = new Timer(); - let effect = new WeaponEffect(battleview.arena, new Ship(), new Target(0, 0), new Equipment()); + let effect = new WeaponEffect(battleview.arena, new Ship(), new Target(0, 0), new TriggerAction("weapon")); effect.shieldImpactEffect({ x: 10, y: 10 }, { x: 20, y: 15 }, 1000, 3000, true); let layer = battleview.arena.layer_weapon_effects; @@ -35,7 +35,7 @@ module TK.SpaceTac.UI.Specs { battleview.timer = new Timer(); let ship = nn(battleview.battle.playing_ship); - let effect = new WeaponEffect(battleview.arena, new Ship(), Target.newFromShip(ship), new Equipment()); + let effect = new WeaponEffect(battleview.arena, new Ship(), Target.newFromShip(ship), new TriggerAction("weapon")); effect.gunEffect(); let layer = battleview.arena.layer_weapon_effects; @@ -50,9 +50,8 @@ module TK.SpaceTac.UI.Specs { let ship = nn(battleview.battle.playing_ship); ship.setArenaPosition(50, 30); - let weapon = new Equipment(); - weapon.action = new TriggerAction(weapon, [new DamageEffect()], 1, 500); - check.patch(weapon.action, "getImpactedShips", () => [ship]); + let weapon = new TriggerAction("weapon", { effects: [new DamageEffect()], range: 500 }); + check.patch(weapon, "getImpactedShips", () => [ship]); let dest = new Ship(); let effect = new WeaponEffect(battleview.arena, dest, Target.newFromShip(dest), weapon); @@ -80,7 +79,7 @@ module TK.SpaceTac.UI.Specs { let battleview = testgame.view; battleview.timer = new Timer(); - let effect = new WeaponEffect(battleview.arena, new Ship(), Target.newFromLocation(50, 50), new Equipment()); + let effect = new WeaponEffect(battleview.arena, new Ship(), Target.newFromLocation(50, 50), new TriggerAction("weapon")); effect.gunEffect(); checkEmitters("gun effect started", 1); @@ -97,7 +96,7 @@ module TK.SpaceTac.UI.Specs { let battleview = testgame.view; battleview.timer = new Timer(); - let effect = new WeaponEffect(battleview.arena, new Ship(), Target.newFromLocation(31, 49), new Equipment()); + let effect = new WeaponEffect(battleview.arena, new Ship(), Target.newFromLocation(31, 49), new TriggerAction("weapon")); let result = effect.angularLaser({ x: 20, y: 30 }, 300, Math.PI / 4, -Math.PI / 2, 5); check.equals(result, 200); diff --git a/src/ui/battle/WeaponEffect.ts b/src/ui/battle/WeaponEffect.ts index bf868df..29ed604 100644 --- a/src/ui/battle/WeaponEffect.ts +++ b/src/ui/battle/WeaponEffect.ts @@ -33,9 +33,9 @@ module TK.SpaceTac.UI { private destination: IArenaLocation // Weapon used - private weapon: Equipment + private action: TriggerAction - constructor(arena: Arena, ship: Ship, target: Target, weapon: Equipment) { + constructor(arena: Arena, ship: Ship, target: Target, action: TriggerAction) { this.ui = arena.game; this.arena = arena; this.view = arena.view; @@ -43,7 +43,7 @@ module TK.SpaceTac.UI { this.layer = arena.layer_weapon_effects; this.ship = ship; this.target = target; - this.weapon = weapon; + this.action = action; this.source = Target.newFromShip(this.ship); this.destination = this.target; @@ -56,15 +56,15 @@ module TK.SpaceTac.UI { */ start(): number { // Fire effect - let effect = this.getEffectForWeapon(this.weapon.code, this.weapon.action); + let effect = this.getEffectForWeapon(this.action.code, this.action); let duration = effect(); // Damage effect - let action = this.weapon.action; - if (action instanceof TriggerAction && any(action.effects, effect => effect instanceof DamageEffect)) { + let action = this.action; + if (any(action.effects, effect => effect instanceof DamageEffect)) { let ships = action.getImpactedShips(this.ship, this.target, this.source); let source = action.blast ? this.target : this.source; - let damage_duration = this.damageEffect(source, ships, duration * 0.4, this.weapon.code == "gatlinggun"); + let damage_duration = this.damageEffect(source, ships, duration * 0.4, this.action.code == "gatlinggun"); duration = Math.max(duration, damage_duration); } @@ -177,7 +177,7 @@ module TK.SpaceTac.UI { missile.rotation = arenaAngle(this.source, this.destination); this.layer.add(missile); - let blast_radius = (this.weapon.action instanceof TriggerAction) ? this.weapon.action.blast : 0; + let blast_radius = this.action.blast; let projectile_duration = arenaDistance(this.source, this.destination) * 1.5; let tween = this.ui.tweens.create(missile); diff --git a/src/ui/character/CharacterCargo.spec.ts b/src/ui/character/CharacterCargo.spec.ts deleted file mode 100644 index 5eb6841..0000000 --- a/src/ui/character/CharacterCargo.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -module TK.SpaceTac.UI.Specs { - testing("CharacterCargo", test => { - let testgame = setupEmptyView(test); - - test.case("checks conditions for adding/removing equipment", check => { - let view = testgame.view; - let sheet = new CharacterSheet(view); - let ship = new Ship(); - sheet.show(ship); - - let source = new CharacterCargo(sheet, 0, 0); - let equipment = new CharacterEquipment(sheet, new Equipment(), source); - - let destination = new CharacterCargo(sheet, 0, 0); - check.equals(destination.addEquipment(equipment, null, true), { success: false, info: 'put in cargo', error: 'not enough cargo space' }); - ship.setCargoSpace(1); - check.equals(destination.addEquipment(equipment, null, true), { success: true, info: 'put in cargo' }); - ship.critical = true; - check.equals(destination.addEquipment(equipment, null, true), { success: false, info: 'put in cargo', error: 'not a fleet member' }); - ship.critical = false; - - check.equals(source.removeEquipment(equipment, null, true), { success: false, info: 'remove from cargo', error: 'not in cargo!' }); - ship.addCargo(equipment.item); - check.equals(source.removeEquipment(equipment, null, true), { success: true, info: 'remove from cargo' }); - ship.critical = true; - check.equals(source.removeEquipment(equipment, null, true), { success: false, info: 'remove from cargo', error: 'not a fleet member' }); - }); - }); -} diff --git a/src/ui/character/CharacterCargo.ts b/src/ui/character/CharacterCargo.ts deleted file mode 100644 index 6b86481..0000000 --- a/src/ui/character/CharacterCargo.ts +++ /dev/null @@ -1,69 +0,0 @@ -/// - -module TK.SpaceTac.UI { - /** - * Display a ship cargo slot - */ - export class CharacterCargo extends Phaser.Image implements CharacterEquipmentContainer { - sheet: CharacterSheet; - - constructor(sheet: CharacterSheet, x: number, y: number) { - let info = sheet.view.getImageInfo("character-cargo-slot"); - super(sheet.game, x, y, info.key, info.frame); - - this.sheet = sheet; - } - - jasmineToString() { - return "CharacterCargo"; - } - - /** - * CharacterEquipmentContainer interface - */ - isInside(x: number, y: number): boolean { - return this.getBounds().contains(x, y); - } - getEquipmentAnchor(): { x: number, y: number, scale: number, alpha: number } { - return { - x: this.x + (this.parent ? this.parent.x : 0) + 98 * this.scale.x, - y: this.y + (this.parent ? this.parent.y : 0) + 98 * this.scale.y, - scale: this.scale.x * 1.25, - alpha: this.alpha, - } - } - getPriceOffset(): number { - return 66; - } - addEquipment(equipment: CharacterEquipment, source: CharacterEquipmentContainer | null, test: boolean): CharacterEquipmentTransfer { - let info = "put in cargo"; - if (this.sheet.ship.critical) { - return { success: false, info: info, error: "not a fleet member" }; - } if (this.sheet.ship.getFreeCargoSpace() > 0) { - if (test) { - return { success: true, info: info }; - } else { - let success = this.sheet.ship.addCargo(equipment.item); - return { success: success, info: info }; - } - } else { - return { success: false, info: info, error: "not enough cargo space" }; - } - } - removeEquipment(equipment: CharacterEquipment, destination: CharacterEquipmentContainer | null, test: boolean): CharacterEquipmentTransfer { - let info = "remove from cargo"; - if (this.sheet.ship.critical) { - return { success: false, info: info, error: "not a fleet member" }; - } else if (contains(this.sheet.ship.cargo, equipment.item)) { - if (test) { - return { success: true, info: info }; - } else { - let success = this.sheet.ship.removeCargo(equipment.item); - return { success: success, info: info }; - } - } else { - return { success: false, info: info, error: "not in cargo!" }; - } - } - } -} diff --git a/src/ui/character/CharacterEquipment.spec.ts b/src/ui/character/CharacterEquipment.spec.ts deleted file mode 100644 index 2a17b7d..0000000 --- a/src/ui/character/CharacterEquipment.spec.ts +++ /dev/null @@ -1,159 +0,0 @@ -module TK.SpaceTac.UI.Specs { - testing("CharacterEquipment", test => { - let testgame = setupEmptyView(test); - - class FakeContainer implements CharacterEquipmentContainer { - name: string; - x: number; - inside: CharacterEquipment | null; - constructor(name: string, x: number) { - this.name = name; - this.x = x; - this.inside = null; - } - jasmineToString() { - return this.name; - } - isInside(x: number, y: number): boolean { - return x == this.x; - } - getEquipmentAnchor(): { x: number, y: number, scale: number, alpha: number } { - return { x: this.x, y: 0, scale: 0.5, alpha: 1 }; - } - getPriceOffset(): number { - return 12; - } - addEquipment(equipment: CharacterEquipment, source: CharacterEquipmentContainer | null, test: boolean): CharacterEquipmentTransfer { - if (this.x < 150) { - if (!test) { - this.inside = equipment; - } - return { success: true, info: "" }; - } else { - return { success: false, info: "" }; - } - } - removeEquipment(equipment: CharacterEquipment, destination: CharacterEquipmentContainer | null, test: boolean): CharacterEquipmentTransfer { - if (this.inside === equipment) { - if (!test) { - this.inside = null; - } - return { success: true, info: "" }; - } else { - return { success: false, info: "" }; - } - } - } - - function createBasicCase(positions: number[]): [CharacterSheet, CharacterEquipment, FakeContainer[], Mock] { - let view = testgame.view; - let sheet = new CharacterSheet(view); - sheet.show(new Ship()); - let refresh = test.check.patch(sheet, "refresh", null); - - let containers = positions.map((x, idx) => new FakeContainer(`container${idx + 1}`, x)); - let equipment = new CharacterEquipment(sheet, new Equipment(), containers[0]); - containers[0].inside = equipment; - equipment.setupDragDrop(); - test.check.patch(sheet, "iEquipmentContainers", () => iarray(containers)); - - return [sheet, equipment, containers, refresh]; - } - - test.case("handles drag-and-drop to move equipment", check => { - let [sheet, equipment, [container1, container2, container3], refresh] = createBasicCase([0, 100, 200]); - - check.same(equipment.inputEnabled, true, "Input should be enabled"); - check.same(equipment.input.draggable, true, "Equipment should be draggable"); - check.same(equipment.container, container1); - check.equals(equipment.x, 0); - check.equals(equipment.scale.x, 0.25); - - // drop on nothing - check.equals(equipment.alpha, 1); - equipment.events.onDragStart.dispatch(); - check.equals(equipment.alpha, 0.8); - equipment.x = 812; - equipment.events.onDragStop.dispatch(); - check.same(equipment.container, container1); - check.equals(equipment.x, 0); - check.called(refresh, 0); - - // drop on accepting destination - equipment.events.onDragStart.dispatch(); - equipment.x = 100; - equipment.events.onDragStop.dispatch(); - check.same(equipment.container, container2); - check.equals(equipment.x, 100); - check.equals(container1.inside, null); - check.same(container2.inside, equipment); - check.called(refresh, 1); - - // drop on refusing destination - equipment.events.onDragStart.dispatch(); - equipment.x = 200; - equipment.events.onDragStop.dispatch(); - check.same(equipment.container, container2); - check.equals(equipment.x, 100); - check.same(container2.inside, equipment); - check.equals(container3.inside, null); - check.called(refresh, 0); - - // broken destination, should return to source - let log = check.patch(console, "error", null); - check.patch(container3, "addEquipment", (equ: any, src: any, test: boolean) => { return { success: test } }); - equipment.events.onDragStart.dispatch(); - equipment.x = 200; - equipment.events.onDragStop.dispatch(); - check.same(equipment.container, container2); - check.equals(equipment.x, 100); - check.called(refresh, 0); - check.called(log, [ - ['Destination container refused to accept equipment', equipment, container2, container3] - ]); - - // broken destination and source, item is lost ! - check.patch(container2, "addEquipment", (equ: any, src: any, test: boolean) => { return { success: test } }); - equipment.events.onDragStart.dispatch(); - equipment.x = 200; - equipment.events.onDragStop.dispatch(); - check.same(equipment.container, container3); - check.equals(equipment.x, 200); - check.called(refresh, 1); - check.called(log, [ - ['Destination container refused to accept equipment', equipment, container2, container3], - ['Equipment lost in bad exchange!', equipment, container2, container3] - ]); - }); - - test.case("defines the sheet's action message", check => { - let [sheet, equipment, [container1, container2], refresh] = createBasicCase([0, 1]); - - check.patch(container1, "removeEquipment", iterator([ - { success: true, info: "detach" }, - { success: false, info: "detach", error: "cannot detach" }, - { success: true, info: "detach" }, - { success: false, info: "detach", error: "cannot detach" } - ])) - check.patch(container2, "addEquipment", iterator([ - { success: true, info: "attach" }, - { success: true, info: "attach" }, - { success: false, info: "attach", error: "cannot attach" }, - { success: false, info: "attach", error: "cannot attach" } - ])) - - check.equals(sheet.action_message.text, ""); - equipment.events.onDragStart.dispatch(); - check.equals(sheet.action_message.text, ""); - equipment.x = 1; - equipment.events.onDragUpdate.dispatch(); - check.equals(sheet.action_message.text, "Detach, attach"); - equipment.events.onDragUpdate.dispatch(); - check.equals(sheet.action_message.text, "Detach, attach (cannot detach)"); - equipment.events.onDragUpdate.dispatch(); - check.equals(sheet.action_message.text, "Detach, attach (cannot attach)"); - equipment.events.onDragUpdate.dispatch(); - check.equals(sheet.action_message.text, "Detach, attach (cannot detach)"); - }); - }); -} diff --git a/src/ui/character/CharacterEquipment.ts b/src/ui/character/CharacterEquipment.ts deleted file mode 100644 index b66ce31..0000000 --- a/src/ui/character/CharacterEquipment.ts +++ /dev/null @@ -1,213 +0,0 @@ -module TK.SpaceTac.UI { - /** - * Interface for any graphical area that may contain or receive an equipment - */ - export interface CharacterEquipmentContainer { - /** - * Check if a point in the character sheet is inside the container - */ - isInside(x: number, y: number): boolean - /** - * Get a centric anchor point and scaling to snap the equipment - */ - getEquipmentAnchor(): { x: number, y: number, scale: number, alpha: number } - /** - * Get a vertical offset to position the price tag - */ - getPriceOffset(): number - /** - * Add an equipment to the container - */ - addEquipment(equipment: CharacterEquipment, source: CharacterEquipmentContainer | null, test: boolean): CharacterEquipmentTransfer - /** - * Remove an equipment from the container - */ - removeEquipment(equipment: CharacterEquipment, destination: CharacterEquipmentContainer | null, test: boolean): CharacterEquipmentTransfer - } - - /** - * Result of an equipment transfer operation - */ - export type CharacterEquipmentTransfer = { - success: boolean, - info: string, - error?: string - } - - function mergeTransfer(leave: CharacterEquipmentTransfer, enter: CharacterEquipmentTransfer): CharacterEquipmentTransfer { - return { - success: leave.success && enter.success, - info: [leave.info, enter.info].join(', '), - error: leave.error || enter.error - } - } - - /** - * Display a ship equipment, either attached to a slot, in cargo, or being dragged down - */ - export class CharacterEquipment extends Phaser.Button { - sheet: CharacterSheet - item: Equipment - container: CharacterEquipmentContainer - price: number - - constructor(sheet: CharacterSheet, equipment: Equipment, container: CharacterEquipmentContainer) { - let icon = sheet.view.getImageInfo(`equipment-${equipment.code}`); - super(sheet.game, 0, 0, icon.key, undefined, undefined, icon.frame, icon.frame); - - this.sheet = sheet; - this.item = equipment; - this.container = container; - this.price = 0; - - this.anchor.set(0.5, 0.5); - - if (sheet.isInteractive()) { - this.setupDragDrop(); - } - this.snapToContainer(); - - sheet.view.tooltip.bind(this, filler => this.fillTooltip(filler)); - } - - jasmineToString() { - return this.item.jasmineToString(); - } - - /** - * Find the container under a specific screen location - */ - findContainerAt(x: number, y: number): CharacterEquipmentContainer | null { - return ifirst(this.sheet.iEquipmentContainers(), container => container.isInside(x, y)); - } - - /** - * Display a price tag - */ - setPrice(price: number) { - if (!price || this.price) { - return; - } - this.price = price; - - let tag = this.sheet.view.newImage("character-price-tag"); - let yoffset = this.container.getPriceOffset(); - tag.position.set(0, -yoffset * 2 + tag.height); - tag.anchor.set(0.5, 0.5); - tag.scale.set(2, 2); - tag.alpha = 0.85; - this.addChild(tag); - - let text = this.sheet.view.newText(price.toString(), -8, 2, 18, "#ffffcc"); - tag.addChild(text); - } - - /** - * Snap in place to its current container - */ - snapToContainer() { - let info = this.container.getEquipmentAnchor(); - this.position.set(info.x, info.y); - this.scale.set(0.5 * info.scale, 0.5 * info.scale); - this.alpha = info.alpha; - } - - /** - * Enable dragging to another slot - */ - setupDragDrop() { - if (this.container.removeEquipment(this, null, true)) { - this.sheet.view.inputs.setDragDrop(this, () => { - // Drag - this.scale.set(0.5, 0.5); - this.alpha = 0.8; - }, () => { - // Drop - this.sheet.setActionMessage(); - let destination = this.findContainerAt(this.x, this.y); - if (destination && destination != this.container) { - if (this.applyDragDrop(this.container, destination, false).success) { - this.container = destination; - this.snapToContainer(); - this.setupDragDrop(); - this.sheet.refresh(); // TODO Only if required (destination is "virtual") - } else { - this.snapToContainer(); - } - } else { - this.snapToContainer(); - } - }, () => { - // Update - let destination = this.findContainerAt(this.x, this.y); - if (destination && destination != this.container) { - let simulation = this.applyDragDrop(this.container, destination, true); - let message = capitalize(simulation.info); - if (simulation.error) { - message += ` (${simulation.error})`; - } - this.sheet.setActionMessage(message, simulation.success ? "#ffffff" : "#f04240"); - } else { - this.sheet.setActionMessage(); - } - }); - } else { - this.sheet.view.inputs.setDragDrop(this); - } - } - - /** - * Apply drag and drop between two containers - * - * Return true if something changed (or would change, if test=true). - */ - applyDragDrop(source: CharacterEquipmentContainer, destination: CharacterEquipmentContainer, test: boolean): CharacterEquipmentTransfer { - let transfer_test = mergeTransfer(source.removeEquipment(this, destination, true), destination.addEquipment(this, source, true)); - - if (test) { - return transfer_test; - } else if (transfer_test.success) { - let transfer_out = source.removeEquipment(this, destination, false); - if (transfer_out.success) { - let transfer_in = destination.addEquipment(this, source, false); - let transfer = mergeTransfer(transfer_out, transfer_in); - if (transfer_in.success) { - return transfer; - } else { - console.error("Destination container refused to accept equipment", this, source, destination); - // Go back to source - let transfer_back = source.addEquipment(this, null, false); - if (transfer_back.success) { - return transfer; - } else { - console.error("Equipment lost in bad exchange!", this, source, destination); - return { - success: true, - info: transfer.info, - error: "Equipment was critically damaged in transfer!" - }; - } - } - } else { - console.error("Source container refused to give away equipment", this, source, destination); - return transfer_out; - } - } else { - return transfer_test; - } - } - - /** - * Fill a tooltip with equipment data - */ - fillTooltip(filler: TooltipBuilder): boolean { - let title = this.item.getFullName(); - if (this.item.slot_type !== null) { - title += ` (${SlotType[this.item.slot_type]})`; - } - filler.text(title, 0, 0, { color: "#cccccc", size: 20, bold: true }); - filler.text(this.item.getFullDescription(), 0, 40, { color: "#cccccc", size: 18, width: 700 }); - return true; - } - } -} diff --git a/src/ui/character/CharacterFleetMember.spec.ts b/src/ui/character/CharacterFleetMember.spec.ts deleted file mode 100644 index 8e6b5fa..0000000 --- a/src/ui/character/CharacterFleetMember.spec.ts +++ /dev/null @@ -1,68 +0,0 @@ -module TK.SpaceTac.UI.Specs { - testing("CharacterFleetMember", test => { - let testgame = setupEmptyView(test); - - test.case("transfers equipment to another ship", check => { - let view = testgame.view; - let sheet = new CharacterSheet(view); - - let fleet = new Fleet(); - let ship1 = fleet.addShip(); - ship1.setCargoSpace(3); - let equ1 = new Equipment(SlotType.Engine, "engine1"); - ship1.addCargo(equ1); - let equ2 = new Equipment(SlotType.Engine, "engine2"); - ship1.addCargo(equ2); - let equ3 = new Equipment(SlotType.Engine, "engine3"); - ship1.addCargo(equ3); - let ship2 = fleet.addShip(); - let ship2engine = ship2.addSlot(SlotType.Engine); - ship2.setCargoSpace(1); - - sheet.show(ship1); - check.equals(sheet.portraits.length, 2); - check.equals(sheet.layer_equipments.length, 3); - check.equals(sheet.ship_cargo.length, 3); - - // First item fits in the free slot - let source = sheet.ship_cargo.children[0]; - let dest = sheet.portraits.children[1]; - let equ = sheet.layer_equipments.children[0]; - check.same(dest.ship, ship2); - check.same(equ.item, equ1); - check.contains(ship1.cargo, equ1); - check.equals(ship2engine.attached, null); - equ.applyDragDrop(source, dest, false); - check.notcontains(ship1.cargo, equ1); - check.same(ship2engine.attached, equ1); - - // Second item goes to cargo - source = sheet.ship_cargo.children[0]; - dest = sheet.portraits.children[1]; - equ = sheet.layer_equipments.children[1]; - check.same(dest.ship, ship2); - check.same(equ.item, equ2); - check.contains(ship1.cargo, equ2); - check.notcontains(ship2.cargo, equ2); - equ.applyDragDrop(source, dest, false); - check.notcontains(ship1.cargo, equ2); - check.contains(ship2.cargo, equ2); - - // Third item has no more room - source = sheet.ship_cargo.children[0]; - dest = sheet.portraits.children[1]; - equ = sheet.layer_equipments.children[2]; - check.same(dest.ship, ship2); - check.same(equ.item, equ3); - check.contains(ship1.cargo, equ3); - equ.applyDragDrop(source, dest, false); - check.contains(ship1.cargo, equ3); - - // Cannot transfer to escorted ship - ship2.setCargoSpace(2); - check.equals(equ.applyDragDrop(source, dest, true), { success: true, info: 'remove from cargo, transfer to Player 2', error: undefined }); - ship2.critical = true; - check.equals(equ.applyDragDrop(source, dest, true), { success: false, info: 'remove from cargo, transfer to Player 2', error: 'not a fleet member' }); - }); - }); -} diff --git a/src/ui/character/CharacterFleetMember.ts b/src/ui/character/CharacterFleetMember.ts deleted file mode 100644 index ee32497..0000000 --- a/src/ui/character/CharacterFleetMember.ts +++ /dev/null @@ -1,90 +0,0 @@ -/// - -module TK.SpaceTac.UI { - /** - * Display a fleet member in the side of character sheet - */ - export class CharacterFleetMember extends Phaser.Button implements CharacterEquipmentContainer { - sheet: CharacterSheet; - ship: Ship; - levelup: Phaser.Image; - - constructor(sheet: CharacterSheet, x: number, y: number, ship: Ship) { - let info = sheet.view.getImageInfo("character-ship"); - super(sheet.game, x, y, info.key, () => sheet.show(ship), null, info.frame, info.frame); - this.anchor.set(0.5, 0.5); - - this.sheet = sheet; - this.ship = ship; - - let portrait_pic = sheet.view.newImage(`ship-${ship.model.code}-portrait`); - portrait_pic.anchor.set(0.5, 0.5); - this.addChild(portrait_pic); - - this.levelup = sheet.view.newImage("character-upgrade-available", this.width / 2 - 40, -this.height / 2 + 40); - this.levelup.anchor.set(1, 0); - this.levelup.visible = this.ship.getAvailableUpgradePoints() > 0; - this.addChild(this.levelup); - - sheet.view.tooltip.bindDynamicText(this, () => ship.getName()); - } - - /** - * Set the selected state of the ship - */ - setSelected(selected: boolean) { - this.sheet.view.changeImage(this, selected ? "character-ship-selected" : "character-ship"); - this.sheet.view.animations.setVisible(this.levelup, this.ship.getAvailableUpgradePoints() > 0, 200); - } - - /** - * CharacterEquipmentContainer interface - */ - isInside(x: number, y: number): boolean { - return this.getBounds().contains(x, y) && this.ship !== this.sheet.ship; - } - getEquipmentAnchor(): { x: number, y: number, scale: number, alpha: number } { - // not needed, equipment is never shown snapped in the slot - return { x: 0, y: 0, scale: 1, alpha: 1 }; - } - getPriceOffset(): number { - return 0; - } - addEquipment(equipment: CharacterEquipment, source: CharacterEquipmentContainer | null, test: boolean): CharacterEquipmentTransfer { - let info = `transfer to ${this.ship.name}`; - if (this.ship.critical) { - return { success: false, info: info, error: "not a fleet member" }; - } else if (this.ship != this.sheet.ship && equipment.item.slot_type !== null) { - // First, try to equip - let slot = this.ship.getFreeSlot(equipment.item.slot_type); - if (slot && equipment.item.canBeEquipped(this.ship.attributes, false)) { - info = `equip on ${this.ship.name}`; - if (test) { - return { success: true, info: info }; - } else { - let success = this.ship.equip(equipment.item, false); - return { success: true, info: info }; - } - } - - // If cannot be equipped, go to cargo - if (this.ship.getFreeCargoSpace() > 0) { - if (test) { - return { success: true, info: info }; - } else { - let success = this.ship.addCargo(equipment.item); - return { success: success, info: info }; - } - } else { - return { success: false, info: info, error: "not enough cargo space" }; - } - } else { - return { success: false, info: info, error: "drop on cargo or slots" }; - } - } - removeEquipment(equipment: CharacterEquipment, destination: CharacterEquipmentContainer | null, test: boolean): CharacterEquipmentTransfer { - // should never happen - return { success: false, info: "" }; - } - } -} diff --git a/src/ui/character/CharacterLootSlot.spec.ts b/src/ui/character/CharacterLootSlot.spec.ts deleted file mode 100644 index 1d26dd5..0000000 --- a/src/ui/character/CharacterLootSlot.spec.ts +++ /dev/null @@ -1,43 +0,0 @@ -module TK.SpaceTac.UI.Specs { - testing("CharacterLootSlot", test => { - let testgame = setupEmptyView(test); - - test.case("takes or discard loot", check => { - let view = testgame.view; - let sheet = new CharacterSheet(view); - - let fleet = new Fleet(); - let ship = fleet.addShip(); - ship.setCargoSpace(2); - let equ1 = new Equipment(SlotType.Shield, "equ1"); - ship.addCargo(equ1) - - let equ2 = new Equipment(SlotType.Weapon, "equ2"); - let loot = [equ2]; - sheet.setLoot(loot); - sheet.show(ship); - - check.equals(ship.cargo, [equ1]); - check.equals(loot, [equ2]); - - let cargo_slot = sheet.ship_cargo.children[0]; - check.equals(cargo_slot instanceof CharacterCargo, true); - let loot_slot = sheet.loot_slots.children[0]; - check.equals(loot_slot instanceof CharacterLootSlot, true); - - // loot to cargo - let equ2s = sheet.layer_equipments.children[1]; - check.same(equ2s.item, equ2); - equ2s.applyDragDrop(loot_slot, cargo_slot, false); - check.equals(ship.cargo, [equ1, equ2]); - check.equals(loot, []); - - // discard to cargo - let equ1s = sheet.layer_equipments.children[0]; - check.same(equ1s.item, equ1); - equ1s.applyDragDrop(cargo_slot, loot_slot, false); - check.equals(ship.cargo, [equ2]); - check.equals(loot, [equ1]); - }); - }); -} diff --git a/src/ui/character/CharacterLootSlot.ts b/src/ui/character/CharacterLootSlot.ts deleted file mode 100644 index 60ce21d..0000000 --- a/src/ui/character/CharacterLootSlot.ts +++ /dev/null @@ -1,29 +0,0 @@ -/// -/// - -module TK.SpaceTac.UI { - /** - * Display a loot slot - */ - export class CharacterLootSlot extends CharacterCargo { - addEquipment(equipment: CharacterEquipment, source: CharacterEquipmentContainer | null, test: boolean): CharacterEquipmentTransfer { - if (!test) { - add(this.sheet.loot_items, equipment.item); - } - return { success: true, info: "leave equipment" }; - } - removeEquipment(equipment: CharacterEquipment, destination: CharacterEquipmentContainer | null, test: boolean): CharacterEquipmentTransfer { - let info = "Loot equipment"; - if (contains(this.sheet.loot_items, equipment.item)) { - if (test) { - return { success: true, info: info }; - } else { - let success = remove(this.sheet.loot_items, equipment.item); - return { success: success, info: info }; - } - } else { - return { success: false, info: info, error: "not lootable!" }; - } - } - } -} diff --git a/src/ui/character/CharacterPortrait.ts b/src/ui/character/CharacterPortrait.ts new file mode 100644 index 0000000..2d71047 --- /dev/null +++ b/src/ui/character/CharacterPortrait.ts @@ -0,0 +1,25 @@ +module TK.SpaceTac.UI { + /** + * Display the portrait of a fleet member on a character sheet + */ + export class CharacterPortrait { + constructor(readonly ship: Ship) { + } + + /** + * Draw the portrait (anchored at the center) + */ + draw(builder: UIBuilder, x: number, y: number, onselect: () => void): UIButton { + let button = builder.button("character-portrait", x, y, onselect, this.ship.getName(), identity); + button.anchor.set(0.5); + + builder.in(button, builder => { + // FIXME Under hover/on + let portrait = builder.image(`ship-${this.ship.model.code}-portrait`, 0, 0, true); + portrait.scale.set(0.5); + }); + + return button; + } + } +} diff --git a/src/ui/character/CharacterSheet.spec.ts b/src/ui/character/CharacterSheet.spec.ts index 9cf3044..6fa6bb4 100644 --- a/src/ui/character/CharacterSheet.spec.ts +++ b/src/ui/character/CharacterSheet.spec.ts @@ -6,116 +6,28 @@ module TK.SpaceTac.UI.Specs { test.case("displays fleet and ship information", check => { let view = testgame.view; - let sheet = new CharacterSheet(view, -1000); + check.patch(view, "getWidth", () => 1240); + let sheet = new CharacterSheet(view); - check.equals(sheet.x, -1000); + check.equals(sheet.x, -1240); let fleet = new Fleet(); let ship1 = fleet.addShip(); - ship1.addSlot(SlotType.Hull); - ship1.addSlot(SlotType.Engine); - ship1.addSlot(SlotType.Shield); - ship1.addSlot(SlotType.Weapon); - ship1.setCargoSpace(3); ship1.name = "Ship 1"; let ship2 = fleet.addShip(); - ship2.addSlot(SlotType.Hull); ship2.name = "Ship 2"; - ship2.setCargoSpace(2); sheet.show(ship1, false); check.equals(sheet.x, 0); - check.equals(sheet.portraits.length, 2); + check.equals(sheet.group_portraits.length, 2); - check.equals(sheet.ship_name.text, "Ship 1"); - check.equals(sheet.ship_slots.length, 4); - check.equals(sheet.ship_cargo.length, 3); + check.equals(sheet.text_name.text, "Ship 1"); - let portrait = sheet.portraits.getChildAt(1); + let portrait = as(Phaser.Button, sheet.group_portraits.getChildAt(1)); portrait.onInputUp.dispatch(); - check.equals(sheet.ship_name.text, "Ship 2"); - check.equals(sheet.ship_slots.length, 1); - check.equals(sheet.ship_cargo.length, 2); - }); - - test.case("moves equipment around", check => { - let fleet = new Fleet(); - let ship = fleet.addShip(); - ship.setCargoSpace(2); - let equ1 = TestTools.addEngine(ship, 1); - let equ2 = new Equipment(SlotType.Weapon); - ship.addCargo(equ2); - let equ3 = new Equipment(SlotType.Hull); - let equ4 = new Equipment(SlotType.Power); - let loot = [equ3, equ4]; - ship.addSlot(SlotType.Weapon); - - let sheet = new CharacterSheet(testgame.view); - sheet.show(ship, false); - - check.equals(sheet.loot_slots.visible, false); - check.equals(sheet.layer_equipments.children.length, 2); - - sheet.setLoot(loot); - - check.equals(sheet.loot_slots.visible, true); - check.equals(sheet.layer_equipments.children.length, 4); - - let findsprite = (equ: Equipment) => nn(first(sheet.layer_equipments.children, sp => sp.item == equ)); - let draddrop = (sp: CharacterEquipment, dest: CharacterCargo | CharacterSlot) => { - sp.applyDragDrop(sp.container, dest, false); - } - - // Unequip - let sprite = findsprite(equ1); - check.notequals(equ1.attached_to, null); - check.equals(ship.cargo.length, 1); - draddrop(sprite, sheet.ship_cargo.children[0]); - check.equals(equ1.attached_to, null); - check.equals(ship.cargo.length, 2); - check.contains(ship.cargo, equ1); - - // Equip - sprite = findsprite(equ2); - check.equals(equ2.attached_to, null); - check.contains(ship.cargo, equ2); - draddrop(sprite, sheet.ship_slots.children[0]); - check.same(equ2.attached_to, ship.slots[1]); - check.notcontains(ship.cargo, equ2); - - // Loot - sprite = findsprite(equ3); - check.equals(equ3.attached_to, null); - check.notcontains(ship.cargo, equ3); - check.contains(loot, equ3); - draddrop(sprite, sheet.ship_cargo.children[0]); - check.equals(equ3.attached_to, null); - check.contains(ship.cargo, equ3); - check.notcontains(loot, equ3); - - // Can't loop - no cargo space available - sprite = findsprite(equ4); - check.notcontains(ship.cargo, equ4); - check.contains(loot, equ4); - draddrop(sprite, sheet.ship_cargo.children[0]); - check.notcontains(ship.cargo, equ4); - check.contains(loot, equ4); - - // Discard - sprite = findsprite(equ1); - check.contains(ship.cargo, equ1); - check.notcontains(loot, equ1); - draddrop(sprite, sheet.ship_cargo.children[0]); - check.equals(equ1.attached_to, null); - check.notcontains(loot, equ1); - - // Can't equip - no slot available - sprite = findsprite(equ3); - check.equals(equ3.attached_to, null); - draddrop(sprite, sheet.ship_slots.children[0]); - check.equals(equ3.attached_to, null); + check.equals(sheet.text_name.text, "Ship 2"); }); test.case("controls global interactivity state", check => { @@ -141,19 +53,5 @@ module TK.SpaceTac.UI.Specs { check.equals(sheet.isInteractive(), true, "interactivity reenabled"); }); }); - - test.case("fits slots in area", check => { - let result = CharacterSheet.getSlotPositions(6, 300, 200, 100, 100); - check.equals(result, { - positions: [{ x: 0, y: 0 }, { x: 100, y: 0 }, { x: 200, y: 0 }, { x: 0, y: 100 }, { x: 100, y: 100 }, { x: 200, y: 100 }], - scaling: 1 - }); - - result = CharacterSheet.getSlotPositions(6, 299, 199, 100, 100); - check.equals(result, { - positions: [{ x: 0, y: 0 }, { x: 100, y: 0 }, { x: 200, y: 0 }, { x: 0, y: 100 }, { x: 100, y: 100 }, { x: 200, y: 100 }], - scaling: 0.99 - }); - }); }); } diff --git a/src/ui/character/CharacterSheet.ts b/src/ui/character/CharacterSheet.ts index 7a9fd05..bc5e92e 100644 --- a/src/ui/character/CharacterSheet.ts +++ b/src/ui/character/CharacterSheet.ts @@ -1,9 +1,4 @@ module TK.SpaceTac.UI { - export type CharacterEquipmentDrop = { - message: string - callback: (equipment: Equipment) => any - } - /** * Character sheet, displaying ship characteristics */ @@ -18,153 +13,236 @@ module TK.SpaceTac.UI { builder: UIBuilder // X positions - xshown: number - xhidden: number + xshown = 0 + xhidden = -2000 + + // Groups + group_portraits: Phaser.Group + group_attributes: Phaser.Image + group_actions: Phaser.Image + group_upgrades: Phaser.Group // Close button close_button: Phaser.Button // Currently displayed fleet - fleet!: Fleet + fleet?: Fleet // Currently displayed ship - ship!: Ship + ship?: Ship - // Ship name - ship_name: Phaser.Text + // Variable data + image_portrait: Phaser.Image + text_model: Phaser.Text + text_description: Phaser.Text + text_name: Phaser.Text + text_level: Phaser.Text + text_upgrade_points: Phaser.Text + valuebar_experience: ValueBar - // Ship level - ship_level: Phaser.Text - ship_experience: ValueBar - - // Ship skill upgrade - ship_upgrade_points: Phaser.Text - layer_upgrades: Phaser.Group - - // Ship slots - ship_slots: Phaser.Group - - // Ship cargo - ship_cargo: Phaser.Group - - // Dynamic texts - mode_title: UIText - action_message: UIText - - // Loot items - loot_slots: Phaser.Group - loot_items: Equipment[] = [] - loot_page = 0 - loot_next: Phaser.Button - loot_prev: Phaser.Button - - // Shop - shop: Shop | null = null - - // Fleet portraits - members: CharacterFleetMember[] = [] - portraits: Phaser.Group - - // Layers - layer_attibutes: Phaser.Group - layer_equipments: Phaser.Group - - // Credits - credits: Phaser.Text - - // Attributes and skills - attributes: { [key: string]: Phaser.Text } = {} - - constructor(view: BaseView, xhidden = -2000, xshown = 0, onclose?: Function) { + constructor(view: BaseView, onclose?: Function) { super(view.game, 0, 0, "character-sheet"); this.view = view; - this.builder = new UIBuilder(view, this); + this.builder = new UIBuilder(view, this).styled({ color: "#e7ebf0", size: 16, shadow: true }); - this.x = xhidden; - this.xshown = xshown; - this.xhidden = xhidden; + this.xhidden = -this.view.getWidth(); + this.x = this.xhidden; this.inputEnabled = true; if (!onclose) { onclose = () => this.hide(); } - this.close_button = this.builder.button("character-close", 1920, 0, onclose, "Close the character sheet"); + this.close_button = this.builder.button("character-close-button", 1920, 0, onclose, "Close the character sheet"); this.close_button.anchor.set(1, 0); - this.builder.text("Cargo", 1566, 36, { size: 24 }); - this.builder.text("Level", 420, 1052, { size: 24 }); - this.builder.text("Available points", 894, 1052, { size: 24 }); + this.image_portrait = this.builder.image("translucent", 435, 271, true); - this.ship_name = this.builder.text("", 758, 48, { size: 30 }); - this.ship_level = this.builder.text("", 554, 1052, { size: 30 }); - this.ship_upgrade_points = this.builder.text("", 1068, 1052, { size: 30 }); - this.ship_slots = this.builder.group("slots", 372, 120); - this.ship_cargo = this.builder.group("cargo", 1240, 86); - this.loot_slots = this.builder.group("loot", 1270, 670); - this.loot_slots.visible = false; - this.portraits = this.builder.group("portraits", 152, 0); - this.credits = this.builder.text("", 136, 38, { size: 30 }); - this.mode_title = this.builder.text("", 1566, 648, { size: 18 }); - this.action_message = this.builder.text("", 1566, 1056, { size: 18 }); - this.loot_next = this.builder.button("common-arrow-right", 1890, 850, () => this.paginate(1), "Show next items"); - this.loot_next.anchor.set(0.5); - this.loot_prev = this.builder.button("common-arrow-left", 1238, 850, () => this.paginate(-1), "Show previous items"); - this.loot_prev.anchor.set(0.5); + this.builder.image("character-entry", 28, 740); - this.ship_experience = new ValueBar(this.view, "character-experience", ValueBarOrientation.EAST, 516, 1067); - this.addChild(this.ship_experience.node); + this.group_portraits = this.builder.group("portraits", 90, 755); - this.layer_attibutes = this.builder.group("attributes"); - this.layer_upgrades = this.builder.group("upgrades"); - this.layer_equipments = this.builder.group("equipments"); + let model_bg = this.builder.image("character-ship-model", 434, 500, true); + this.text_model = this.builder.in(model_bg).text("", 0, 0, { size: 28 }); - let x1 = 402; - let x2 = 802; - let y = 640; - this.addAttribute("hull_capacity", x1, y); - this.addAttribute("shield_capacity", x1, y + 64); - this.addAttribute("power_capacity", x1, y + 128); - this.addAttribute("power_generation", x1, y + 192); - this.addAttribute("maneuvrability", x1, y + 256); - this.addAttribute("precision", x1, y + 320); - this.addAttribute("skill_materials", x2, y); - this.addAttribute("skill_photons", x2, y + 64); - this.addAttribute("skill_antimatter", x2, y + 128); - this.addAttribute("skill_quantum", x2, y + 192); - this.addAttribute("skill_gravity", x2, y + 256); - this.addAttribute("skill_time", x2, y + 320); + let description_bg = this.builder.image("character-ship-description", 434, 654, true); + this.text_description = this.builder.in(description_bg).text("", 0, 0, { color: "#a0afc3", width: 510 }); + + this.group_attributes = this.builder.image("character-ship-column", 30, 30); + this.group_actions = this.builder.image("character-ship-column", 698, 30); + + let name_bg = this.builder.image("character-name-display", 434, 940, true); + this.text_name = this.builder.in(name_bg).text("", 0, 0, { size: 28 }); + + this.builder.button("character-name-button", 656, 890, () => this.renamePersonality(), "Rename personality"); + + let points_bg = this.builder.image("character-level-upgrades", 582, 986); + this.builder.in(points_bg, builder => { + builder.text("Upgrade points", 46, 10, { center: false, vcenter: false }); + builder.image("character-upgrade-point", 147, 59, true); + }); + this.text_upgrade_points = this.builder.in(points_bg).text("", 106, 60, { size: 28 }); + + let level_bg = this.builder.image("character-level-display", 434, 1032, true); + this.text_level = this.builder.in(level_bg).text("", 0, 4, { size: 28 }); + this.valuebar_experience = this.builder.in(level_bg).valuebar("character-level-experience", -level_bg.width * 0.5, -level_bg.height * 0.5); + + this.group_upgrades = this.builder.group("upgrades"); + + this.refreshUpgrades(); + this.refreshAttributes(); + this.refreshActions(); } /** * Check if the sheet should be interactive */ isInteractive(): boolean { - return this.ship ? (!this.ship.critical && this.interactive) : false; + return this.ship ? (this.interactive && !this.ship.critical) : false; } /** - * Add an attribute display + * Open a dialog to rename the ship's personality */ - private addAttribute(attribute: keyof ShipAttributes, x: number, y: number) { + renamePersonality(): void { + // TODO + } + + /** + * Refresh the ship information display + */ + private refreshShipInfo(): void { + if (this.ship) { + let ship = this.ship; + this.builder.change(this.image_portrait, `ship-${ship.model.code}-portrait`); + this.text_name.setText(ship.name || ""); + this.text_model.setText(ship.model.name); + this.text_level.setText(`Level ${ship.level.get()}`); + this.text_description.setText(ship.model.getDescription()); + this.text_upgrade_points.setText(`${ship.getAvailableUpgradePoints()}`); + this.valuebar_experience.setValue(ship.level.getExperience(), ship.level.getNextGoal()); + } + } + + /** + * Refresh the upgrades display + */ + private refreshUpgrades(): void { + let builder = this.builder.in(this.group_upgrades); + builder.clear(); + + if (!this.ship) { + return; + } let ship = this.ship; - let builder = this.builder.in(this.layer_attibutes); + let initial = builder.image("character-initial", 970, 30); - let button = builder.button("character-attribute", x, y, undefined, () => ship.getAttributeDescription(attribute)); + // Base equipment (level 1) + builder.styled({ center: false, vcenter: false }).in(initial, builder => { + builder.text("Base equipment", 32, 8, { color: "#e2e9d1" }); - let attrname = capitalize(SHIP_VALUES_NAMES[attribute]); - builder.in(button).text(attrname, 120, 22, { size: 20, color: "#c9d8ef", stroke_width: 1, stroke_color: "#395665" }); + builder.in(builder.group("attributes"), builder => { + let effects = cfilter(ship.model.getEffects(1, []), AttributeEffect); + effects.forEach(effect => { + let button = builder.button(`attribute-${effect.attrcode}`, 0, 8, undefined, + `${capitalize(SHIP_VALUES_NAMES[effect.attrcode])} - ${SHIP_VALUES_DESCRIPTIONS[effect.attrcode]}`); - let value = builder.in(button).text("", 264, 24, { size: 18, bold: true }); + builder.in(button, builder => { + builder.text(`${effect.value}`, 56, 8, { size: 22 }); + }); + }); + builder.distribute("x", 236, 870); + }); - this.attributes[attribute] = value; + builder.in(builder.group("actions"), builder => { + let actions = ship.model.getActions(1, []); + actions.forEach(action => { + let button = builder.button("translucent", 0, 66, undefined, action.getEffectsDescription()); - if (SHIP_SKILLS.hasOwnProperty(attribute)) { - this.builder.in(this.layer_upgrades).button("character-skill-upgrade", x + 292, y, () => { - ship.upgradeSkill(attribute); - this.refresh(); - }, `Spend one point to upgrade ${attrname}`); + builder.in(button, builder => { + let icon = builder.image(`action-${action.code}`); + icon.scale.set(0.1875); + if (actions.length < 5) { + builder.text(`${action.name}`, 56, 12, { size: 16 }); + } + }); + }); + builder.distribute("x", 28, 888); + }); + }); + + // Level number + range(10).forEach(i => { + builder.text(`${i + 1}`, 920, i == 0 ? 92 : (110 + i * 100), { + center: true, + vcenter: true, + size: 28, + color: ship.level.get() >= (i + 1) ? "#e7ebf0" : "#808285" + }); + }); + + // Level upgrades + range(9).forEach(i => { + builder.image("character-level-separator", 844, 154 + i * 100); + + let level = i + 2; + let upgrades = ship.model.getLevelUpgrades(level); + upgrades.forEach((upgrade, j) => { + let onchange = (selected: boolean) => { + this.refreshShipInfo(); // TODO Only upgrade points + this.refreshActions(); + this.refreshAttributes(); + }; + new CharacterUpgrade(ship, upgrade, level).draw(builder, 970 + j * 315, 170 + i * 100, + this.isInteractive() ? onchange : undefined); + }); + }); + } + + /** + * Refresh the attributes display + */ + private refreshAttributes(): void { + let builder = this.builder.in(this.group_attributes); + builder.clear(); + + builder.text("Attributes", 74, 20, { color: "#a0afc3" }); + + if (this.ship) { + let ship = this.ship; + builder.in(builder.group("items"), builder => { + keys(SHIP_ATTRIBUTES).forEach(attribute => { + let button = builder.button(`attribute-${attribute}`, 24, 0, undefined, + ship.getAttributeDescription(attribute)); + + builder.in(button).text(`${ship.getAttribute(attribute)}`, 78, 27, { size: 22 }); + }); + builder.distribute("y", 40, 688); + }); + } + } + + /** + * Refresh the actions display + */ + private refreshActions(): void { + let builder = this.builder.in(this.group_actions); + builder.clear(); + + builder.text("Actions", 74, 20, { color: "#a0afc3" }); + + if (this.ship) { + let ship = this.ship; + builder.in(builder.group("items"), builder => { + let actions = ship.actions.listAll().filter(action => !(action instanceof EndTurnAction)); + actions.forEach(action => { + let button = builder.button(`action-${action.code}`, 24, 0, undefined, + action.getEffectsDescription()); + button.scale.set(0.375); + }); + builder.distribute("y", 40, 688); + }); } } @@ -172,29 +250,25 @@ module TK.SpaceTac.UI { * Update the fleet sidebar */ updateFleet(fleet: Fleet) { - if (fleet != this.fleet || fleet.ships.length != this.members.length) { - this.portraits.removeAll(true); - this.members = []; + if (fleet !== this.fleet || fleet.ships.length != this.group_portraits.length) { + destroyChildren(this.group_portraits); this.fleet = fleet; + + let builder = this.builder.in(this.group_portraits); + fleet.ships.forEach((ship, idx) => { + let button: UIButton + button = new CharacterPortrait(ship).draw(builder, 64 + idx * 140, 64, () => { + if (button) { + builder.select(button); + this.ship = ship; + this.refreshShipInfo(); + this.refreshActions(); + this.refreshAttributes(); + this.refreshUpgrades(); + } + }); + }); } - - fleet.ships.forEach((ship, idx) => { - let portrait = this.members[idx]; - if (!portrait) { - portrait = new CharacterFleetMember(this, 0, idx * 320, ship); - this.portraits.add(portrait); - this.members.push(portrait); - } - portrait.setSelected(ship == this.ship); - }); - - this.credits.setText(fleet.credits.toString()); - - this.portraits.scale.set(980 * this.portraits.scale.x / this.portraits.height, 980 * this.portraits.scale.y / this.portraits.height); - if (this.portraits.width > 308) { - this.portraits.scale.set(308 * this.portraits.scale.x / this.portraits.width, 308 * this.portraits.scale.y / this.portraits.width); - } - this.portraits.y = 80 + 160 * this.portraits.scale.x; } /** @@ -213,60 +287,13 @@ module TK.SpaceTac.UI { this.interactive = interactive; } - this.layer_equipments.removeAll(true); - this.setActionMessage(); - - let upgrade_points = ship.getAvailableUpgradePoints(); - - this.ship_name.setText(ship.getName(false)); - this.ship_level.setText(ship.level.get().toString()); - this.ship_experience.setValue(ship.level.getExperience(), ship.level.getNextGoal()); - this.ship_upgrade_points.setText(upgrade_points.toString()); - this.layer_upgrades.visible = this.isInteractive() && upgrade_points > 0; - - iteritems(ship.attributes, (key, value: ShipAttribute) => { - let text = this.attributes[key]; - if (text) { - text.setText(value.get().toString()); - } - }); - - let slotsinfo = CharacterSheet.getSlotPositions(ship.slots.length, 800, 454, 200, 200); - this.ship_slots.removeAll(true); - ship.slots.forEach((slot, idx) => { - let slot_display = new CharacterSlot(this, slotsinfo.positions[idx].x, slotsinfo.positions[idx].y, slot.type); - slot_display.scale.set(slotsinfo.scaling, slotsinfo.scaling); - slot_display.alpha = this.isInteractive() ? 1 : 0.5; - this.ship_slots.add(slot_display); - - if (slot.attached) { - let equipment = new CharacterEquipment(this, slot.attached, slot_display); - this.layer_equipments.add(equipment); - } - }); - - slotsinfo = CharacterSheet.getSlotPositions(ship.cargo_space, 638, 496, 200, 200); - this.ship_cargo.removeAll(true); - range(ship.cargo_space).forEach(idx => { - let cargo_slot = new CharacterCargo(this, slotsinfo.positions[idx].x, slotsinfo.positions[idx].y); - cargo_slot.scale.set(slotsinfo.scaling, slotsinfo.scaling); - cargo_slot.alpha = this.isInteractive() ? 1 : 0.5; - this.ship_cargo.add(cargo_slot); - - if (idx < ship.cargo.length) { - let equipment = new CharacterEquipment(this, ship.cargo[idx], cargo_slot); - this.layer_equipments.add(equipment); - } - }); - - this.updateLoot(); + this.refreshShipInfo(); + this.refreshUpgrades(); + this.refreshAttributes(); + this.refreshActions(); this.updateFleet(ship.fleet); - if (this.shop) { - this.updatePrices(this.shop); - } - if (sound) { this.view.audio.playOnce("ui-dialog-open"); } @@ -282,14 +309,6 @@ module TK.SpaceTac.UI { * Hide the sheet */ hide(animate = true) { - this.loot_page = 0; - this.loot_items = []; - this.shop = null; - this.loot_slots.visible = false; - this.mode_title.visible = false; - - this.members.forEach(member => member.setSelected(false)); - this.view.audio.playOnce("ui-dialog-close"); if (animate) { @@ -299,113 +318,6 @@ module TK.SpaceTac.UI { } } - /** - * Set the action message (mainly used while dragging equipment to explain what is happening) - */ - setActionMessage(message = "", color = "#ffffff"): void { - if (message != this.action_message.text) { - this.action_message.setText(message); - this.action_message.fill = color; - } - } - - /** - * Set the list of lootable equipment - * - * The list of equipments may be altered if items are taken from it - * - * This list will be shown until sheet is closed - */ - setLoot(loot: Equipment[]) { - this.loot_page = 0; - - this.loot_items = loot; - this.updateLoot(); - this.loot_slots.visible = true; - - this.mode_title.setText("Lootable items"); - this.mode_title.visible = true; - } - - /** - * Set the displayed shop - * - * This shop will be shown until sheet is closed - */ - setShop(shop: Shop, title = "Dockyard's equipment") { - this.loot_page = 0; - - this.shop = shop; - this.updateLoot(); - this.loot_slots.visible = true; - - this.mode_title.setText(title); - this.mode_title.visible = true; - } - - /** - * Update the price tags on each equipment, for a specific shop - */ - updatePrices(shop: Shop) { - this.layer_equipments.children.forEach(equipement => { - if (equipement instanceof CharacterEquipment) { - equipement.setPrice(shop.getPrice(equipement.item)); - } - }); - } - - /** - * Change the page displayed in loot/shop section - */ - paginate(offset: number) { - let items = this.shop ? this.shop.getStock() : this.loot_items; - this.loot_page = clamp(this.loot_page + offset, 0, 1 + Math.floor(items.length / 12)); - this.refresh(); - } - - /** - * Update the loot slots - */ - private updateLoot() { - let per_page = 12; - this.loot_slots.removeAll(true); - - let info = CharacterSheet.getSlotPositions(12, 588, 354, 196, 196); - let items = this.shop ? this.shop.getStock() : this.loot_items; - range(per_page).forEach(idx => { - let loot_slot = this.shop ? new CharacterShopSlot(this, info.positions[idx].x, info.positions[idx].y) : new CharacterLootSlot(this, info.positions[idx].x, info.positions[idx].y); - loot_slot.scale.set(info.scaling, info.scaling); - this.loot_slots.add(loot_slot); - - idx += per_page * this.loot_page; - - if (idx < items.length) { - let equipment = new CharacterEquipment(this, items[idx], loot_slot); - this.layer_equipments.add(equipment); - } - }); - - this.view.animations.setVisible(this.loot_prev, this.loot_page > 0, 200); - this.view.animations.setVisible(this.loot_next, (this.loot_page + 1) * per_page < items.length, 200); - } - - /** - * Get an iterator over equipment containers - */ - iEquipmentContainers(): Iterator { - let candidates = ichain( - iarray(this.portraits.children), - iarray(this.ship_slots.children), - iarray(this.ship_cargo.children), - ); - - if (this.loot_slots.visible) { - candidates = ichain(candidates, iarray(this.loot_slots.children)); - } - - return candidates; - } - /** * Refresh the sheet display */ @@ -414,32 +326,5 @@ module TK.SpaceTac.UI { this.show(this.ship, false, false); } } - - /** - * Get the positions and scaling for slots, to fit in a rectangle group. - */ - static getSlotPositions(count: number, areawidth: number, areaheight: number, slotwidth: number, slotheight: number): { positions: { x: number, y: number }[], scaling: number } { - // Find grid size - let rows = 2; - let columns = 3; - while (count > rows * columns) { - rows += 1; - columns += 1; - } - - // Find scaling - let scaling = 1; - while (slotwidth * scaling * columns > areawidth || slotheight * scaling * rows > areaheight) { - scaling *= 0.99; - } - - // Position - let positions = range(count).map(i => { - let row = Math.floor(i / columns); - let column = i % columns; - return { x: column * (areawidth - slotwidth * scaling) / (columns - 1), y: row * (areaheight - slotheight * scaling) / (rows - 1) }; - }); - return { positions: positions, scaling: scaling }; - } } } diff --git a/src/ui/character/CharacterShopSlot.spec.ts b/src/ui/character/CharacterShopSlot.spec.ts deleted file mode 100644 index 74d537f..0000000 --- a/src/ui/character/CharacterShopSlot.spec.ts +++ /dev/null @@ -1,56 +0,0 @@ -module TK.SpaceTac.UI.Specs { - testing("CharacterShopSlot", test => { - let testgame = setupEmptyView(test); - - test.case("buys and sell if bound to a shop", check => { - let view = testgame.view; - let sheet = new CharacterSheet(view); - - let fleet = new Fleet(); - fleet.credits = 100; - let ship = fleet.addShip(); - ship.setCargoSpace(2); - let equ1 = new Equipment(SlotType.Shield, "equ1"); - ship.addCargo(equ1); - - let equ2 = new Equipment(SlotType.Weapon, "equ2"); - let shop = new Shop(1, [equ2], 0); - check.patch(shop, "getPrice", () => 120); - sheet.setShop(shop); - sheet.show(ship); - - check.equals(ship.cargo, [equ1]); - check.equals(shop.stock, [equ2]); - check.equals(fleet.credits, 100); - - let cargo_slot = sheet.ship_cargo.children[0]; - check.equals(cargo_slot instanceof CharacterCargo, true); - let shop_slot = sheet.loot_slots.children[0]; - check.equals(shop_slot instanceof CharacterShopSlot, true); - - // sell - let equ1s = sheet.layer_equipments.children[0]; - check.same(equ1s.item, equ1); - equ1s.applyDragDrop(cargo_slot, shop_slot, false); - check.equals(ship.cargo, []); - check.equals(shop.stock, [equ2, equ1]); - check.equals(fleet.credits, 220); - - // buy - let equ2s = sheet.layer_equipments.children[1]; - check.same(equ2s.item, equ2); - equ2s.applyDragDrop(shop_slot, cargo_slot, false); - check.equals(ship.cargo, [equ2]); - check.equals(shop.stock, [equ1]); - check.equals(fleet.credits, 100); - - // not enough money - equ1s = sheet.layer_equipments.children[0]; - check.same(equ1s.item, equ1); - equ1s.applyDragDrop(shop_slot, cargo_slot, false); - check.equals(ship.cargo, [equ2]); - check.equals(shop.stock, [equ1]); - check.equals(fleet.credits, 100); - }); - }); -} diff --git a/src/ui/character/CharacterShopSlot.ts b/src/ui/character/CharacterShopSlot.ts deleted file mode 100644 index 69bc0ea..0000000 --- a/src/ui/character/CharacterShopSlot.ts +++ /dev/null @@ -1,46 +0,0 @@ -/// - -module TK.SpaceTac.UI { - /** - * Display a shop slot - */ - export class CharacterShopSlot extends CharacterLootSlot { - addEquipment(equipment: CharacterEquipment, source: CharacterEquipmentContainer | null, test: boolean): CharacterEquipmentTransfer { - let shop = this.sheet.shop; - if (shop && !contains(shop.getStock(), equipment.item)) { - let price = shop.getPrice(equipment.item); - let info = `sell for ${price} zotys`; - if (test) { - return { success: true, info: info }; - } else { - let success = shop.buyFromFleet(equipment.item, this.sheet.fleet); - return { success: success, info: info }; - } - } else { - return { success: false, info: "sell equipment", error: "it's already mine!" }; - } - } - - removeEquipment(equipment: CharacterEquipment, destination: CharacterEquipmentContainer | null, test: boolean): CharacterEquipmentTransfer { - let shop = this.sheet.shop; - if (shop && contains(shop.getStock(), equipment.item)) { - let price = shop.getPrice(equipment.item); - let info = `buy for ${price} zotys`; - if (destination) { - if (price > this.sheet.fleet.credits) { - return { success: false, info: info, error: "not enough zotys" }; - } else if (test) { - return { success: true, info: info }; - } else { - let success = shop.sellToFleet(equipment.item, this.sheet.fleet); - return { success: success, info: info }; - } - } else { - return { success: test, info: info }; - } - } else { - return { success: false, info: "buy equipment", error: "it's not mine to sell!" }; - } - } - } -} diff --git a/src/ui/character/CharacterSlot.spec.ts b/src/ui/character/CharacterSlot.spec.ts deleted file mode 100644 index ab1a0e4..0000000 --- a/src/ui/character/CharacterSlot.spec.ts +++ /dev/null @@ -1,48 +0,0 @@ -module TK.SpaceTac.UI.Specs { - testing("CharacterSlot", test => { - let testgame = setupEmptyView(test); - - test.case("allows dragging equipment", check => { - let view = testgame.view; - let ship = new Ship(); - ship.addSlot(SlotType.Hull); - let sheet = new CharacterSheet(view); - sheet.show(ship); - let source = new CharacterLootSlot(sheet, 0, 0); - sheet.addChild(source); - let equipment = new CharacterEquipment(sheet, new Equipment(SlotType.Engine), source); - - let slot = new CharacterSlot(sheet, 0, 0, SlotType.Engine); - check.equals(slot.addEquipment(equipment, source, true), { success: false, info: 'equip in engine slot', error: 'no free slot' }); - check.equals(slot.removeEquipment(equipment, source, true), { success: false, info: 'unequip from engine slot', error: 'not equipped!' }); - - ship.addSlot(SlotType.Engine); - check.equals(slot.addEquipment(equipment, source, true), { success: true, info: 'equip in engine slot' }); - - equipment.item.requirements["skill_time"] = 1; - check.equals(slot.addEquipment(equipment, source, true), { success: false, info: 'equip in engine slot', error: 'missing skills' }); - - ship.upgradeSkill("skill_time"); - check.equals(slot.addEquipment(equipment, source, true), { success: true, info: 'equip in engine slot' }); - - ship.critical = true; - check.equals(slot.addEquipment(equipment, source, true), { success: false, info: 'equip in engine slot', error: 'not a fleet member' }); - ship.critical = false; - - check.equals(ship.listEquipment(SlotType.Engine), []); - let result = slot.addEquipment(equipment, source, false); - check.equals(result, { success: true, info: 'equip in engine slot' }); - check.equals(ship.listEquipment(SlotType.Engine), [equipment.item]); - - check.equals(slot.removeEquipment(equipment, source, true), { success: true, info: 'unequip from engine slot' }); - ship.critical = true; - check.equals(slot.removeEquipment(equipment, source, true), { success: false, info: 'unequip from engine slot', error: 'not a fleet member' }); - ship.critical = false; - - result = slot.removeEquipment(equipment, source, false); - check.equals(result, { success: true, info: 'unequip from engine slot' }); - check.equals(ship.listEquipment(SlotType.Engine), []); - - }); - }); -} diff --git a/src/ui/character/CharacterSlot.ts b/src/ui/character/CharacterSlot.ts deleted file mode 100644 index 1712395..0000000 --- a/src/ui/character/CharacterSlot.ts +++ /dev/null @@ -1,73 +0,0 @@ -/// - -module TK.SpaceTac.UI { - /** - * Display a ship slot, with equipment attached to it - */ - export class CharacterSlot extends Phaser.Image implements CharacterEquipmentContainer { - sheet: CharacterSheet; - - constructor(sheet: CharacterSheet, x: number, y: number, slot: SlotType) { - let info = sheet.view.getImageInfo("character-equipment-slot"); - super(sheet.game, x, y, info.key, info.frame); - - this.sheet = sheet; - - let sloticon = sheet.view.newButton(`character-slot-${SlotType[slot].toLowerCase()}`, 150, 150); - sloticon.anchor.set(0.5); - this.addChild(sloticon); - sheet.view.tooltip.bindStaticText(sloticon, `${SlotType[slot]} slot`); - } - - - /** - * CharacterEquipmentContainer interface - */ - isInside(x: number, y: number): boolean { - return this.getBounds().contains(x, y); - } - getEquipmentAnchor(): { x: number, y: number, scale: number, alpha: number } { - return { - x: this.x + this.parent.x + 84 * this.scale.x, - y: this.y + this.parent.y + 83 * this.scale.y, - scale: this.scale.x, - alpha: this.alpha, - } - } - getPriceOffset(): number { - return 66; - } - addEquipment(equipment: CharacterEquipment, source: CharacterEquipmentContainer | null, test: boolean): CharacterEquipmentTransfer { - let info = equipment.item.slot_type ? `equip in ${SlotType[equipment.item.slot_type].toLowerCase()} slot` : "equip"; - if (this.sheet.ship.critical) { - return { success: false, info: info, error: "not a fleet member" }; - } else if (!equipment.item.canBeEquipped(this.sheet.ship.attributes, false)) { - return { success: false, info: info, error: "missing skills" }; - } else if (equipment.item.slot_type && !this.sheet.ship.getFreeSlot(equipment.item.slot_type)) { - return { success: false, info: info, error: "no free slot" }; - } else { - if (test) { - return { success: true, info: info }; - } else { - let success = this.sheet.ship.equip(equipment.item, false); - return { success: success, info: info }; - } - } - } - removeEquipment(equipment: CharacterEquipment, destination: CharacterEquipmentContainer | null, test: boolean): CharacterEquipmentTransfer { - let info = equipment.item.slot_type ? `unequip from ${SlotType[equipment.item.slot_type].toLowerCase()} slot` : "unequip"; - if (this.sheet.ship.critical) { - return { success: false, info: info, error: "not a fleet member" }; - } if (!contains(this.sheet.ship.listEquipment(equipment.item.slot_type), equipment.item)) { - return { success: false, info: info, error: "not equipped!" }; - } else { - if (test) { - return { success: true, info: info }; - } else { - let success = this.sheet.ship.unequip(equipment.item, false); - return { success: success, info: info }; - } - } - } - } -} diff --git a/src/ui/character/CharacterUpgrade.ts b/src/ui/character/CharacterUpgrade.ts new file mode 100644 index 0000000..f221c16 --- /dev/null +++ b/src/ui/character/CharacterUpgrade.ts @@ -0,0 +1,104 @@ +module TK.SpaceTac.UI { + /** + * Display a single upgrade options + */ + export class CharacterUpgrade { + constructor( + readonly ship: Ship, + readonly upgrade: ModelUpgrade, + readonly level: number + ) { + } + + /** + * Draw the upgrade button + */ + draw(builder: UIBuilder, x: number, y: number, onchange?: (selected: boolean) => void): void { + let active = this.ship.level.hasUpgrade(this.upgrade); + let enabled = onchange ? (this.ship.level.get() >= this.level) : active; + let tooltip = enabled ? ((filler: TooltipBuilder) => this.fillTooltip(filler)) : undefined; + let selector = (enabled && onchange) ? ((on: boolean) => this.activate(on, onchange)) : undefined; + let button = builder.button("character-upgrade", x, y, undefined, tooltip, selector); + + if (active) { + builder.switch(button, true); + } + + builder.in(button, builder => { + if (enabled) { + builder.text(this.upgrade.code, 166, 40, { size: 16, color: "#e7ebf0", width: 210 }); + + let icon = builder.image(this.getIcon(this.upgrade), 40, 40, true); + if (icon.width && icon.width > 64) { + icon.scale.set(64 / icon.width); + } + + range(this.upgrade.cost || 0).forEach(i => { + builder.image("character-upgrade-point", 275, 64 - i * 24, true); + }); + } else { + builder.image("character-upgrade-locked"); + } + }); + } + + /** + * Activate or deactivate the upgrade + */ + private activate(on: boolean, onchange: (selected: boolean) => void): boolean { + let oldval = this.ship.level.hasUpgrade(this.upgrade); + this.ship.activateUpgrade(this.upgrade, on); + let newval = this.ship.level.hasUpgrade(this.upgrade); + + if (newval != oldval) { + onchange(newval); + } + + return newval; + } + + /** + * Fill the tooltip for this upgrade + */ + private fillTooltip(builder: TooltipBuilder): boolean { + builder.text(this.upgrade.code, 0, 0, { size: 20 }); + + let y = 30; + + if (this.upgrade.effects) { + this.upgrade.effects.forEach(effect => { + builder.text(effect.getDescription(), 0, y); + y += 30; + }); + } + + if (this.upgrade.actions) { + this.upgrade.actions.forEach(action => { + builder.text(action.getEffectsDescription(), 0, y); + y += 60; + }); + } + + return true; + } + + /** + * Get an icon code for an upgrade + */ + private getIcon(upgrade: ModelUpgrade): string { + if (upgrade.actions && upgrade.actions.length) { + return `action-${upgrade.actions[0].code}`; + } else if (upgrade.effects && upgrade.effects.length) { + let effects = upgrade.effects; + let attr = first(effects, effect => effect instanceof AttributeEffect || effect instanceof AttributeMultiplyEffect); + if (attr && (attr instanceof AttributeEffect || attr instanceof AttributeMultiplyEffect)) { + return `attribute-${attr.attrcode}`; + } else { + return "translucent"; + } + } else { + return "translucent"; + } + } + } +} diff --git a/src/ui/character/FleetCreationView.spec.ts b/src/ui/character/FleetCreationView.spec.ts index 339a274..392faed 100644 --- a/src/ui/character/FleetCreationView.spec.ts +++ b/src/ui/character/FleetCreationView.spec.ts @@ -4,23 +4,6 @@ module TK.SpaceTac.UI.Specs { testing("FleetCreationView", test => { let testgame = setupSingleView(test, () => [new FleetCreationView, []]); - test.case("has a basic equipment shop with infinite stock", check => { - let shop = testgame.view.infinite_shop; - let itemcount = shop.getStock().length; - check.equals(unique(shop.getStock().map(equ => equ.code)).length, itemcount); - - let fleet = new Fleet(); - fleet.credits = 100000; - let item = shop.getStock()[0]; - shop.sellToFleet(item, fleet); - check.same(fleet.credits, 100000 - item.getPrice()); - check.same(shop.getStock().length, itemcount); - - shop.buyFromFleet(item, fleet); - check.equals(fleet.credits, 100000); - check.same(shop.getStock().length, itemcount); - }) - test.acase("validates the fleet creation", async check => { check.same(testgame.ui.session.isFleetCreated(), false, "no fleet created"); check.same(testgame.ui.session.player.fleet.ships.length, 0, "empty session fleet"); diff --git a/src/ui/character/FleetCreationView.ts b/src/ui/character/FleetCreationView.ts index 438f14d..7133823 100644 --- a/src/ui/character/FleetCreationView.ts +++ b/src/ui/character/FleetCreationView.ts @@ -12,22 +12,14 @@ module TK.SpaceTac.UI { create() { super.create(); - let models = ShipModel.getRandomModels(2); + let models = BaseModel.getRandomModels(2); this.built_fleet = new Fleet(); this.built_fleet.addShip(new Ship(null, MissionGenerator.generateCharacterName(), models[0])); this.built_fleet.addShip(new Ship(null, MissionGenerator.generateCharacterName(), models[1])); this.built_fleet.credits = this.built_fleet.ships.length * 1000; - let basic_equipments = () => { - let generator = new LootGenerator(); - let equipments = generator.templates.map(template => template.generate(1)); - return sortedBy(equipments, equipment => equipment.slot_type); - } - this.infinite_shop = new Shop(1, basic_equipments(), 0, basic_equipments); - - this.character_sheet = new CharacterSheet(this, undefined, undefined, () => this.validateFleet()); - this.character_sheet.setShop(this.infinite_shop, "Available stock (from Master Merchant Guild)"); + this.character_sheet = new CharacterSheet(this, () => this.validateFleet()); this.character_sheet.show(this.built_fleet.ships[0], false); this.getLayer("characters").add(this.character_sheet); } diff --git a/src/ui/common/Tooltip.ts b/src/ui/common/Tooltip.ts index 8469f05..df46847 100644 --- a/src/ui/common/Tooltip.ts +++ b/src/ui/common/Tooltip.ts @@ -2,6 +2,8 @@ module TK.SpaceTac.UI { + export type TooltipFiller = string | ((filler: TooltipBuilder) => string) | ((filler: TooltipBuilder) => boolean); + class TooltipContainer extends Phaser.Group { view: BaseView background: Phaser.Graphics @@ -169,6 +171,23 @@ module TK.SpaceTac.UI { this.bindDynamicText(obj, () => text); } + /** + * Show a tooltip for a component + */ + show(obj: Phaser.Button, content: TooltipFiller): void { + let builder = this.getBuilder(); + let scontent = (typeof content == "string") ? content : content(builder); + if (typeof scontent == "string") { + builder.text(scontent, 0, 0, { color: "#cccccc", size: 20 }); + } + + if (scontent) { + this.container.show(obj.getBounds()); + } else { + this.hide(); + } + } + /** * Hide the current tooltip */ diff --git a/src/ui/common/UIBuilder.spec.ts b/src/ui/common/UIBuilder.spec.ts index 50f0bd3..10b7d5a 100644 --- a/src/ui/common/UIBuilder.spec.ts +++ b/src/ui/common/UIBuilder.spec.ts @@ -109,7 +109,7 @@ module TK.SpaceTac.UI.Specs { builder.text("", 0, 0, {}); builder.text("", 0, 0, { shadow: true }); checkcomp(["View layers", "base", 0], Phaser.Text, "", { shadowColor: "rgba(0,0,0,0)" }); - checkcomp(["View layers", "base", 1], Phaser.Text, "", { shadowColor: "rgba(0,0,0,0.6)", shadowFill: true, shadowOffsetX: 3, shadowOffsetY: 4, shadowBlur: 6, shadowStroke: true }); + checkcomp(["View layers", "base", 1], Phaser.Text, "", { shadowColor: "rgba(0,0,0,0.6)", shadowFill: true, shadowOffsetX: 3, shadowOffsetY: 4, shadowBlur: 3, shadowStroke: true }); builder.clear(); builder.text("", 0, 0, {}); @@ -169,6 +169,42 @@ module TK.SpaceTac.UI.Specs { check.equals(a, 3); }) + test.case("can create toggle buttons", check => { + let builder = new UIBuilder(testgame.view); + + let mock = check.mockfunc("identity", (x: any) => x); + let button1 = builder.button("test-image1", 0, 0, undefined, undefined, mock.func); + let button2 = builder.button("test-image2"); + + let result = builder.switch(button2, true); + check.equals(result, false, "button2"); + check.called(mock, 0); + testClick(button2); + check.called(mock, 0); + + check.in("button1 on", check => { + result = builder.switch(button1, true); + check.equals(result, true); + check.called(mock, [[true]]); + }); + + check.in("button1 off", check => { + result = builder.switch(button1, false); + check.equals(result, false); + check.called(mock, [[false]]); + }); + + check.in("button1 first click", check => { + testClick(button1); + check.called(mock, [[true]]); + }); + + check.in("button1 second click", check => { + testClick(button1); + check.called(mock, [[false]]); + }); + }); + test.case("can create shaders", check => { let builder = new UIBuilder(testgame.view); @@ -241,5 +277,51 @@ module TK.SpaceTac.UI.Specs { checkcomp(["View layers", "base", 1], Phaser.Image, "test-mod-image"); checkcomp(["View layers", "base", 2], Phaser.Button, "test-mod-button"); }) + + test.case("distributes children along an axis", check => { + let builder = new UIBuilder(testgame.view); + builder = builder.in(builder.group("test")); + + let c1 = builder.text(""); + let c2 = builder.button("test"); + let c3 = builder.group("test"); + + check.equals(c1.x, 0); + check.equals(c1.y, 0); + check.equals(c2.x, 0); + check.equals(c2.y, 0); + check.equals(c3.x, 0); + check.equals(c3.y, 0); + + check.patch(UITools, "getScreenBounds", (obj: any) => { + if (obj === c1) { + return { width: 100, height: 51 }; + } else if (obj === c2) { + return { width: 20, height: 7 }; + } else if (obj === c3) { + return { width: 60, height: 11 }; + } else { + return { width: 0, height: 0 }; + } + }); + + builder.distribute("x", 100, 400); + + check.equals(c1.x, 130); + check.equals(c1.y, 0); + check.equals(c2.x, 260); + check.equals(c2.y, 0); + check.equals(c3.x, 310); + check.equals(c3.y, 0); + + builder.distribute("y", 60, 180); + + check.equals(c1.x, 130); + check.equals(c1.y, 73); + check.equals(c2.x, 260); + check.equals(c2.y, 137); + check.equals(c3.x, 310); + check.equals(c3.y, 156); + }) }) } diff --git a/src/ui/common/UIBuilder.ts b/src/ui/common/UIBuilder.ts index 63ebcc7..201ca3c 100644 --- a/src/ui/common/UIBuilder.ts +++ b/src/ui/common/UIBuilder.ts @@ -9,6 +9,7 @@ module TK.SpaceTac.UI { export type UIContainer = Phaser.Group | Phaser.Image export type ShaderValue = number | { x: number, y: number } + export type UIOnOffCallback = (on: boolean) => boolean /** * Text style interface @@ -53,6 +54,17 @@ module TK.SpaceTac.UI { width = 0 } + /** + * Button options + */ + export type UIButtonOptions = { + // Name of the hover picture (by default, the button name, with "-hover" appended) + hover_name?: string + + // Name of the "on" picture (by default, the button name, with "-on" appended) + on_name?: string + } + /** * Main UI builder tool */ @@ -101,11 +113,7 @@ module TK.SpaceTac.UI { * Clear the current container of all component */ clear(): void { - if (this.parent instanceof Phaser.Group) { - this.parent.removeAll(true); - } else { - this.parent.children.forEach(child => (child).destroy()); - } + destroyChildren(this.parent); } /** @@ -147,7 +155,7 @@ module TK.SpaceTac.UI { result.wordWrapWidth = style.width; } if (style.shadow) { - result.setShadow(3, 4, "rgba(0,0,0,0.6)", 6); + result.setShadow(3, 4, "rgba(0,0,0,0.6)", 3); } if (style.stroke_width) { result.stroke = style.stroke_color; @@ -176,24 +184,81 @@ module TK.SpaceTac.UI { } /** - * Add a clickable button + * Add a hoverable and/or clickable button + * + * If an image with "-hover" suffix is found in atlases, it will be used as hover mask (added as button child) */ - button(name: string, x = 0, y = 0, onclick?: Function, tooltip?: string | (() => string)): UIButton { + button(name: string, x = 0, y = 0, onclick?: Function, tooltip?: TooltipFiller, onoffcallback?: UIOnOffCallback, options: UIButtonOptions = {}): UIButton { let info = this.view.getImageInfo(name); - let result = new Phaser.Button(this.game, x, y, info.key, onclick || nop, null, info.frame, info.frame); + let result = new Phaser.Button(this.game, x, y, info.key, undefined, null, info.frame, info.frame); result.name = name; + let clickable = bool(onclick); result.input.useHandCursor = clickable; if (clickable) { UIComponent.setButtonSound(result); } - if (tooltip) { - if (typeof tooltip == "string") { - this.view.tooltip.bindStaticText(result, tooltip); - } else { - this.view.tooltip.bindDynamicText(result, tooltip); + + let onstatus = false; + + if (clickable || tooltip || onoffcallback) { + // On mask + let on_mask: Phaser.Image | null = null; + if (onoffcallback) { + let on_info = this.view.getImageInfo(options.on_name || (name + "-on")); + if (on_info.exists) { + on_mask = new Phaser.Image(this.game, 0, 0, on_info.key, on_info.frame); + on_mask.visible = false; + result.addChild(on_mask); + } + // TODO Find a better way to handle this (extend Button ?) + result.data.onoffcallback = (on: boolean): boolean => { + onstatus = onoffcallback(on); + if (on_mask) { + on_mask.anchor.set(result.anchor.x, result.anchor.y); + this.view.animations.setVisible(on_mask, onstatus, 100); + } + return onstatus; + } } + + // Hover mask + let hover_info = this.view.getImageInfo(options.hover_name || (name + "-hover")); + let hover_mask: Phaser.Image | null = null; + if (hover_info.exists) { + hover_mask = new Phaser.Image(this.game, 0, 0, hover_info.key, hover_info.frame); + hover_mask.visible = false; + result.addChild(hover_mask); + } + + this.view.inputs.setHoverClick(result, + () => { + if (tooltip) { + this.view.tooltip.show(result, tooltip); + } + if (hover_mask) { + hover_mask.anchor.set(result.anchor.x, result.anchor.y); + this.view.animations.show(hover_mask, 100); + } + }, + () => { + if (tooltip) { + this.view.tooltip.hide(); + } + if (hover_mask) { + this.view.animations.hide(hover_mask, 100) + } + }, + () => { + if (onclick) { + onclick(); + } + if (onoffcallback) { + this.switch(result, !onstatus); + } + }, 100); } + this.add(result); return result; } @@ -259,5 +324,52 @@ module TK.SpaceTac.UI { } } } + + /** + * Change the status on/off on a button + * + * Return the final effective status + */ + switch(button: UIButton, on: boolean): boolean { + if (button.data.onoffcallback) { + return button.data.onoffcallback(on); + } else { + return false; + } + } + + /** + * Select a single button inside the container, toggle its "on" status, and toggle all other button to "off" + * + * This is the equivalent of radio buttons + */ + select(button: UIButton): void { + this.parent.children.forEach(child => { + if (child instanceof Phaser.Button && child.data.onoffcallback && child !== button) { + child.data.onoffcallback(false); + } + }); + this.switch(button, true); + } + + /** + * Evenly distribute the children of this builder along an axis + */ + distribute(along: "x" | "y", start: number, end: number): void { + let sizes = this.parent.children.map(child => { + if (child instanceof Phaser.Image || child instanceof Phaser.Sprite || child instanceof Phaser.Group) { + return UITools.getScreenBounds(child)[along == "x" ? "width" : "height"]; + } else { + return 0; + } + }); + let spacing = ((end - start) - sum(sizes)) / (sizes.length + 1); + let offset = start; + this.parent.children.forEach((child, idx) => { + offset += spacing; + child[along] = Math.round(offset); + offset += sizes[idx]; + }); + } } } diff --git a/src/ui/common/UITools.spec.ts b/src/ui/common/UITools.spec.ts index d905309..42f0134 100644 --- a/src/ui/common/UITools.spec.ts +++ b/src/ui/common/UITools.spec.ts @@ -18,6 +18,70 @@ module TK.SpaceTac.UI.Specs { check.equals(parent.children.length, 0); }); + test.case("gets the screen boundaries of an object", check => { + let parent = testgame.view.add.group(); + + check.in("empty", check => { + check.containing(UITools.getScreenBounds(parent), { x: 0, y: 0, width: 0, height: 0 }, "parent"); + }); + + let child1 = testgame.view.add.graphics(10, 20, parent); + + check.in("empty child", check => { + check.containing(UITools.getScreenBounds(parent), { x: 0, y: 0, width: 0, height: 0 }, "parent"); + check.containing(UITools.getScreenBounds(child1), { x: 0, y: 0, width: 0, height: 0 }, "child1"); + }); + + child1.drawRect(20, 30, 40, 45); + + check.in("rectangle child", check => { + check.containing(UITools.getScreenBounds(parent), { x: 30, y: 50, width: 40, height: 45 }, "parent"); + check.containing(UITools.getScreenBounds(child1), { x: 30, y: 50, width: 40, height: 45 }, "child1"); + }); + + child1.scale.set(0.5, 0.2); + + check.in("scaled child", check => { + check.containing(UITools.getScreenBounds(parent), { x: 20, y: 26, width: 20, height: 9 }, "parent"); + check.containing(UITools.getScreenBounds(child1), { x: 20, y: 26, width: 20, height: 9 }, "child1"); + }); + + let child2 = testgame.view.add.graphics(-4, -15); + child1.addChild(child2); + + check.in("sub child empty", check => { + check.containing(UITools.getScreenBounds(parent), { x: 20, y: 26, width: 20, height: 9 }, "parent"); + check.containing(UITools.getScreenBounds(child1), { x: 20, y: 26, width: 20, height: 9 }, "child1"); + check.containing(UITools.getScreenBounds(child2), { x: 0, y: 0, width: 0, height: 0 }, "child2"); + }); + + child2.drawRect(0, 0, 10, 5); + + check.in("sub child rectangle", check => { + check.containing(UITools.getScreenBounds(parent), { x: 8, y: 17, width: 32, height: 18 }, "parent"); + check.containing(UITools.getScreenBounds(child1), { x: 8, y: 17, width: 32, height: 18 }, "child1"); + check.containing(UITools.getScreenBounds(child2), { x: 8, y: 17, width: 5, height: 1 }, "child2"); + }); + + let child3 = testgame.view.add.graphics(50, 51, parent); + + check.in("second child empty", check => { + check.containing(UITools.getScreenBounds(parent), { x: 8, y: 17, width: 42, height: 34 }, "parent"); + check.containing(UITools.getScreenBounds(child1), { x: 8, y: 17, width: 32, height: 18 }, "child1"); + check.containing(UITools.getScreenBounds(child2), { x: 8, y: 17, width: 5, height: 1 }, "child2"); + check.containing(UITools.getScreenBounds(child3), { x: 0, y: 0, width: 0, height: 0 }, "child3"); + }); + + child3.drawRect(1, 1, 1, 1); + + check.in("second child pixel", check => { + check.containing(UITools.getScreenBounds(parent), { x: 8, y: 17, width: 44, height: 36 }, "parent"); + check.containing(UITools.getScreenBounds(child1), { x: 8, y: 17, width: 32, height: 18 }, "child1"); + check.containing(UITools.getScreenBounds(child2), { x: 8, y: 17, width: 5, height: 1 }, "child2"); + check.containing(UITools.getScreenBounds(child3), { x: 51, y: 52, width: 1, height: 1 }, "child3"); + }); + }); + test.case("keeps objects inside bounds", check => { let image = testgame.view.add.graphics(150, 100); image.beginFill(0xff0000); @@ -29,6 +93,22 @@ module TK.SpaceTac.UI.Specs { check.equals(image.x, 100); check.equals(image.y, 100); }); + + test.case("draws a rectangle background around content", check => { + let group = testgame.view.add.group(); + + let content = testgame.view.add.graphics(0, 0, group); + content.drawRect(120, 90, 30, 20); + + let background = testgame.view.add.graphics(0, 0); + + let result = UITools.drawBackground(group, background, 3); + check.equals(result, [36, 26]); + + content.drawCircle(0, 0, 50); + result = UITools.drawBackground(group, background, 3); + check.equals(result, [181, 141]); + }); }); test.case("normalizes angles", check => { diff --git a/src/ui/common/UITools.ts b/src/ui/common/UITools.ts index 3735cf0..2dbf1bd 100644 --- a/src/ui/common/UITools.ts +++ b/src/ui/common/UITools.ts @@ -17,6 +17,24 @@ module TK.SpaceTac.UI { // Common UI tools functions export class UITools { + /** + * Get the screen bounding rectanle of a displayed object + * + * This is a workaround for bugs in getLocalBounds and getBounds + */ + static getScreenBounds(obj: Phaser.Image | Phaser.Sprite | Phaser.Group | Phaser.Graphics): IBounded { + obj.updateTransform(); + + let rects: IBounded[] = [obj.getBounds()]; + obj.children.forEach(child => { + if (child instanceof Phaser.Image || child instanceof Phaser.Sprite || child instanceof Phaser.Group || child instanceof Phaser.Graphics) { + rects.push(UITools.getScreenBounds(child)); + } + }); + + return rects.reduce(UITools.unionRects, { x: 0, y: 0, width: 0, height: 0 }); + } + /** * Get the position of an object, adjusted to remain inside a container */ @@ -52,7 +70,41 @@ module TK.SpaceTac.UI { } } - // Constraint an angle in radians the ]-pi;pi] range. + /** + * Compare two rectangles + */ + static compareRects(rct1: IBounded, rct2: IBounded) { + return rct1.x == rct2.x && rct1.y == rct2.y && rct1.width == rct2.width && rct1.height == rct2.height; + } + + /** + * Returns the bounding rectangle containing two other rectangles + */ + static unionRects(rct1: IBounded, rct2: IBounded): IBounded { + let result: IBounded; + if (rct1.width == 0 || rct1.height == 0) { + result = rct2; + } else if (rct2.width == 0 || rct2.height == 0) { + result = rct1; + } else { + let xmin = Math.min(rct1.x, rct2.x); + let xmax = Math.max(rct1.x + rct1.width, rct2.x + rct2.width); + let ymin = Math.min(rct1.y, rct2.y); + let ymax = Math.max(rct1.y + rct1.height, rct2.y + rct2.height); + + result = { x: xmin, y: ymin, width: xmax - xmin, height: ymax - ymin }; + } + + if (result.width == 0 || result.height == 0) { + return { x: 0, y: 0, width: 0, height: 0 }; + } else { + return result; + } + } + + /** + * Constraint an angle in radians the ]-pi;pi] range. + */ static normalizeAngle(angle: number): number { angle = angle % (2 * Math.PI); if (angle <= -Math.PI) { @@ -81,24 +133,32 @@ module TK.SpaceTac.UI { } /** - * Draw a background around a container - * - * Content's top-left corner is supposed to be at (0,0) + * Draw a background around a content */ static drawBackground(content: Phaser.Group | Phaser.Text, background: Phaser.Graphics, border = 6): [number, number] { - let bounds = content.getBounds(); - let width = bounds.width + 2 * border; - let height = bounds.height + 2 * border; + if (content.parent === background.parent) { + let bounds = content.getLocalBounds(); - if (background.width != width || background.height != height) { - background.clear(); - background.lineStyle(2, 0x404450); - background.beginFill(0x202225, 0.9); - background.drawRect(-border, -border, width, height); - background.endFill(); + let x = bounds.x - border; + let y = bounds.y - border; + let width = bounds.width + 2 * border; + let height = bounds.height + 2 * border; + + if (!(background.width && background.data.bg_bounds && UITools.compareRects(background.data.bg_bounds, bounds))) { + background.clear(); + background.lineStyle(2, 0x404450); + background.beginFill(0x202225, 0.9); + background.drawRect(x, y, width, height); + background.endFill(); + + background.data.bg_bounds = copy(bounds); + } + + return [width, height]; + } else { + console.error("Cannot draw background with different parents", content, background); + return [0, 0]; } - - return [width, height]; } } } diff --git a/src/ui/map/MissionsDialog.spec.ts b/src/ui/map/MissionsDialog.spec.ts index 7362f5e..9f8695f 100644 --- a/src/ui/map/MissionsDialog.spec.ts +++ b/src/ui/map/MissionsDialog.spec.ts @@ -34,11 +34,10 @@ module TK.SpaceTac.UI.Specs { mission = new Mission(universe); mission.title = "Do not do evil"; mission.setDifficulty(MissionDifficulty.easy, 1); - mission.reward = new Equipment(); - mission.reward.name = "Boy Scout Cap"; + mission.reward = 3500; shop_missions.push(mission); missions.refresh(); - checkTexts(missions, ["Proposed jobs", "Save the universe!", "Hard - Reward: 15000 zotys", "Do not do evil", "Easy - Reward: Boy Scout Cap Mk1"]); + checkTexts(missions, ["Proposed jobs", "Save the universe!", "Hard - Reward: 15000 zotys", "Do not do evil", "Easy - Reward: 3500 zotys"]); mission = new Mission(universe); mission.title = "Collect some money"; @@ -46,7 +45,7 @@ module TK.SpaceTac.UI.Specs { player.missions.addSecondary(mission, player.fleet); missions.refresh(); checkTexts(missions, ["Active jobs", "Collect some money", "Normal - Reward: -", - "Proposed jobs", "Save the universe!", "Hard - Reward: 15000 zotys", "Do not do evil", "Easy - Reward: Boy Scout Cap Mk1"]); + "Proposed jobs", "Save the universe!", "Hard - Reward: 15000 zotys", "Do not do evil", "Easy - Reward: 3500 zotys"]); mission = new Mission(universe, undefined, true); mission.title = "Kill the villain"; @@ -54,7 +53,7 @@ module TK.SpaceTac.UI.Specs { player.missions.main = mission; missions.refresh(); checkTexts(missions, ["Active jobs", "Collect some money", "Normal - Reward: -", - "Proposed jobs", "Save the universe!", "Hard - Reward: 15000 zotys", "Do not do evil", "Easy - Reward: Boy Scout Cap Mk1"]); + "Proposed jobs", "Save the universe!", "Hard - Reward: 15000 zotys", "Do not do evil", "Easy - Reward: 3500 zotys"]); }); }); } diff --git a/src/ui/map/StarSystemDisplay.ts b/src/ui/map/StarSystemDisplay.ts index af1f1a3..283bb5c 100644 --- a/src/ui/map/StarSystemDisplay.ts +++ b/src/ui/map/StarSystemDisplay.ts @@ -147,7 +147,7 @@ module TK.SpaceTac.UI { let factor = (zoom == 2) ? 1 : (zoom == 1 ? 5 : 15); this.view.tweens.create(this.label.scale).to({ x: factor, y: factor }, 500, Phaser.Easing.Cubic.InOut).start(); - let position = (zoom == 2) ? { x: -560, y: 440 } : { x: 0, y: (zoom == 1 ? 180 : 100) * factor }; + let position = (zoom == 2) ? { x: -680, y: 440 } : { x: 0, y: (zoom == 1 ? 180 : 100) * factor }; this.view.tweens.create(this.label.position).to(position, 500, Phaser.Easing.Cubic.InOut).start(); } } diff --git a/src/ui/map/UniverseMapView.ts b/src/ui/map/UniverseMapView.ts index 3abd824..4405bba 100644 --- a/src/ui/map/UniverseMapView.ts +++ b/src/ui/map/UniverseMapView.ts @@ -107,26 +107,13 @@ module TK.SpaceTac.UI { this.missions.setPosition(20, 720); this.missions.moveToLayer(this.layer_overlay); - this.zoom_in = new Phaser.Button(this.game, 1540, 172, "map-buttons", () => this.setZoom(this.zoom + 1), undefined, 3, 0); - this.zoom_in.anchor.set(0.5, 0.5); - UIComponent.setButtonSound(this.zoom_in); - this.layer_overlay.add(this.zoom_in); - this.tooltip.bindStaticText(this.zoom_in, "Zoom in"); - this.zoom_out = new Phaser.Button(this.game, 1540, 958, "map-buttons", () => this.setZoom(this.zoom - 1), undefined, 4, 1); - this.zoom_out.anchor.set(0.5, 0.5); - UIComponent.setButtonSound(this.zoom_out); - this.layer_overlay.add(this.zoom_out); - this.tooltip.bindStaticText(this.zoom_out, "Zoom out"); - this.button_options = new Phaser.Button(this.game, 1436, 69, "map-buttons", () => this.showOptions(), undefined, 5, 2); - this.button_options.angle = -90; - this.button_options.anchor.set(0.5, 0.5); - UIComponent.setButtonSound(this.button_options); - this.layer_overlay.add(this.button_options); - this.tooltip.bindStaticText(this.button_options, "Game options"); + builder.in(this.layer_overlay, builder => { + this.zoom_in = builder.button("map-zoom-in", 1787, 54, () => this.setZoom(this.zoom + 1), "Zoom in"); + this.zoom_out = builder.button("map-zoom-out", 1787, 840, () => this.setZoom(this.zoom - 1), "Zoom out"); + this.button_options = builder.button("map-options", 1628, 0, () => this.showOptions(), "Game options"); + }); - this.character_sheet = new CharacterSheet(this, this.getWidth() - 307); - this.character_sheet.show(this.player.fleet.ships[0], false); - this.character_sheet.hide(false); + this.character_sheet = new CharacterSheet(this); this.layer_overlay.add(this.character_sheet); this.conversation = new MissionConversationDisplay(this); @@ -286,7 +273,7 @@ module TK.SpaceTac.UI { this.setLinksAlpha(0.6, duration); this.zoom = 1; } else { - this.setCamera(current_star.x, current_star.y, current_star.radius * 2, duration); + this.setCamera(current_star.x - current_star.radius * 0.3, current_star.y, current_star.radius * 2, duration); this.setLinksAlpha(0.2, duration); this.zoom = 2; } @@ -323,7 +310,6 @@ module TK.SpaceTac.UI { openShop(): void { let location = this.session.getLocation(); if (this.interactive && location && location.shop) { - this.character_sheet.setShop(location.shop); this.character_sheet.show(this.player.fleet.ships[0]); } }