From 74c0cf68562803f09a19abe2a91ab8ea6329f5f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Lemaire?= Date: Tue, 27 Jun 2017 17:42:28 +0200 Subject: [PATCH] Base structure for missions and main story arc --- graphics/ui/map.svg | 330 ++++++++++++++++++---- out/assets/images/map/missions.png | Bin 0 -> 11544 bytes src/core/GameSession.ts | 12 +- src/core/Player.ts | 15 +- src/core/StarLocation.ts | 7 + src/core/missions/ActiveMissions.spec.ts | 56 ++++ src/core/missions/ActiveMissions.ts | 44 +++ src/core/missions/MainStory.spec.ts | 34 +++ src/core/missions/MainStory.ts | 23 ++ src/core/missions/Mission.spec.ts | 36 +++ src/core/missions/Mission.ts | 47 +++ src/core/missions/MissionPart.ts | 20 ++ src/core/missions/MissionPartGoTo.spec.ts | 27 ++ src/core/missions/MissionPartGoTo.ts | 20 ++ src/ui/Preload.ts | 3 +- src/ui/common/UIComponent.ts | 6 +- src/ui/map/ActiveMissionsDisplay.spec.ts | 23 ++ src/ui/map/ActiveMissionsDisplay.ts | 31 ++ src/ui/map/UniverseMapView.ts | 10 + 19 files changed, 678 insertions(+), 66 deletions(-) create mode 100644 out/assets/images/map/missions.png create mode 100644 src/core/missions/ActiveMissions.spec.ts create mode 100644 src/core/missions/ActiveMissions.ts create mode 100644 src/core/missions/MainStory.spec.ts create mode 100644 src/core/missions/MainStory.ts create mode 100644 src/core/missions/Mission.spec.ts create mode 100644 src/core/missions/Mission.ts create mode 100644 src/core/missions/MissionPart.ts create mode 100644 src/core/missions/MissionPartGoTo.spec.ts create mode 100644 src/core/missions/MissionPartGoTo.ts create mode 100644 src/ui/map/ActiveMissionsDisplay.spec.ts create mode 100644 src/ui/map/ActiveMissionsDisplay.ts diff --git a/graphics/ui/map.svg b/graphics/ui/map.svg index fa1cd53..3586ea9 100644 --- a/graphics/ui/map.svg +++ b/graphics/ui/map.svg @@ -231,7 +231,7 @@ fy="142.875" r="125.15247" gradientUnits="userSpaceOnUse" - gradientTransform="translate(-79.375026)" /> + gradientTransform="translate(-32.808351)" /> - - - + + + + + + + + + + image/svg+xml - + @@ -900,7 +936,7 @@ @@ -927,7 +963,7 @@ @@ -949,17 +985,17 @@ inkscape:export-filename="/home/michael/workspace/perso/spacetac/out/assets/images/map/location-planet.png" inkscape:export-xdpi="96" inkscape:export-ydpi="96" - transform="translate(-79.375026)"> + transform="translate(-32.808351)"> + style="opacity:0.44200003;fill:url(#radialGradient7460);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.3233197;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> @@ -983,7 +1019,8 @@ id="g5591" inkscape:export-filename="/home/michael/workspace/perso/spacetac/out/assets/images/map/location-warp.png" inkscape:export-xdpi="96" - inkscape:export-ydpi="96"> + inkscape:export-ydpi="96" + transform="translate(46.566669)"> + id="g9054" + transform="translate(46.566669)"> + transform="translate(-32.808351)"> + style="opacity:1;fill:url(#radialGradient7464);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.56499994;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + transform="translate(-32.808351)"> + transform="translate(-32.808351)"> + transform="rotate(135.15474,259.29761,143.11838)" /> @@ -1272,7 +1310,8 @@ id="g5568" inkscape:export-filename="/home/michael/workspace/perso/spacetac/out/assets/images/map/current-location.png" inkscape:export-xdpi="96" - inkscape:export-ydpi="96"> + inkscape:export-ydpi="96" + transform="translate(46.566669)"> 3 + + + Find the smugglers' hideout + + + Keep Acalanth alive + + + ! + + + + Deliver the intel + + + + + + + Go back to Warna Sector for reward + + + + + TH|5aA*BAAJ&{kOJO9NlqH@ z@!wV0U6BH*K?2GfcmM$CL;o368VfoJq!PhXK}7~(2c85KfD&nBs{;T~0u-brw0+jj z1AIM6M}3d#I=+=3!HPpuLhaJ~gl;pL>=qf~s;o82C#Omqr#oMybT?QsI5Gsj@HUv9 z(Z1{kXz1psG`5rsUSqBPM4zCh(@bUE+=VisM9Oc2+J%Sr@Kp0&eh0yQd^|q}+%M{xE+YJ4^8S_QUQt?;q|77DM2CO=$y=}tBsU0 zQqcf1z7|1ogw;PJE37?LN+}9a!4XaWCa`I5x8jmz$C7L~Q)Fg!$N@n`QtwKbv&F*K z2!h3o^h^sdo$FU7%OAN(4d&!Gw^M)mn|KQ0#Y z(Q7IGR)p#Uh-$dxzsnwzG~TMtmPwrBZ~{c>2~r0Iv`f9${QCw^Z-m`>bVwgJ{I8ZF zmP^+dV8o**Wr!O;9s40ZcbuDDQ^a=taoXcbk4E%;@RXh!%~mcwC4T3iF?32pX5GD! zvr8DszD*esGXn}KW-Bg`5gRH>M54@$veJL>buXmwCO*GVvk+#i2qwx>e(^guX_liA z;+>V-_~))Xr~`!SmvdA`u{hbCSfu29ZW}6UfP6=Ilv2}IPLg(bbqB*zS3BI*!&Atu znQfkra2pX-$6v`Vi#B`Ne5Aab>0fR2oWWaw$noWANcG&wC`s512Zx)L=B>LuAl!Nk zjb>sP-CjapbJ@`N6KmD!r=LpR21%fU(RMr#Jxk{>hqgC*rPlM{-&jEXx1;sD!8}(j zz%fRZspF+itV$*#zSP20l{sS>EnFiZVhJOV&ujc6{;x}QYXwfXNZ#`aru{{7?{`KZ z!`$aVd5|CZJvU7B`XZCvs#f5)P;ML|Os#brcVnv=EvNJEyH`Rq^gFqCE zxtTby^fIqf(-mvyj~^;uh@%}am}BCMFP+u48&12=xtS*r<(eaBnjM!-b*ciEO zkBDQkM5d%oeYQGAua4M|IPeW1IdW9n{#E8-*X9ldc_d!DB+@oHCe@B5Eah}0;nIg+ zbm~dGrq{sEMI4;5Xfc$Q32ANDd&dX3n{AP=Ew2XzP|t2fN+CR`1MJSD1C=zkEhn={ zQH7(((Dx)WH88_%eSl;gkva9o(=Fj2EM-?C>tH&hZ=ya7R0NXNs~XB_)UaRFAqp5~%x$}4-!arP6GDm9qC3o=&?nP_xJ zLoK|swQG8~;GX(tC&N0UAGdm|NMJKR7gkcufzT#5gB09mL>z#z7YvXE*?&sprr?;a ztEP#JC-M>f^ml_)khI*lnw_S@i{pYy=GW`ftTm1rn+miLA0c}ZIt|U?aeHci=$a1Q z6OvTT<=HF$C2;+H#;LmZ11)3O?lAh3BfjJiaXBoC`L3)aDSN~{8cXL8$M56!5sFH= z$2H>xLO<&2dG`&Nz8_?+nmSq%$}$Vw{|&h8uA^9kJC)QG(Rt2v;WwxULshG}$-B*} zw86mGX3FObh)u-be!51@GN92RnNI8f?_Ee?Ir!_Pe~Mx<;y`1|jg#X(IS#!G3Y(K0 zCA$vQE^8@>mMX?>iLxQm_J;{zb6WgoDPO5*m22C>Zw>B6Q)sGiK(H7K$$Kpo1vypt zPi|w0^Qsc~LO-d$TFcOLH~+lY)~uk8egZu?m(A}e>G~s^3mvLtl=<7C+UW2Mrn=|q zVEW#>DQ5e{P3O(sI5>h~UO=Gpmd)c>aEud*gnL#vQ<_4UK+;4oLWUKgqAV^9r^O;c zP4vuk>-Z;4MdNE(nq&^q%{TV=6ga?z=`A6|hEFY&5ubb>JGrruPU(!hX6ap(mdK@U z+hhfE>SuX}2E5F8SUZWJt@_TiEwAil%CtHJ*WJ#x8Ae8wWj=aK5X5BVzOmrO?OGo~ynC_cg^?(9sRm_Z}#U6pWx{D}RJ8cmHd^SdgyrsY$p37Mw1+mb2M_<8B z2IQkBU?l(Va{FO~$k9=Hi+7twQ|36ZOV_lzESIb~G9UR7=92%2I}fTiCA?_La4&m1 z^QyBQC@1PwF(r5RJpETTpUdh}HVdD9EL;7HxtD!)<;=3&W}f+JmP{v-qW>XC#yF(w zzyT!XdIj5^#j`0=f3_zxeve`P`T60SjlHAuK(r+S&ZVzJUzj6>Qdf}K6v3R&J@#%> zIL;_~v0pznt}4@99gpvzhO-8LjEsl`-hG}r8Fc%Zds350vJe-Yl5keRlxZv!R=f@! zyan!(8*eS(RGZYbwim?A1X)3qbOOwqAyHe@Or4IaD_P7JjX4~dXt6R+E^oj7?4U*= zbKgam;PE}Vx6U#4E*4$$2JLue)|$jU6Ld$SriQ&w=&N7H)dcFK#o$wO!uvJB}|O|3uOK8(FS< z3hN1H{10*{X}w0dYM*c_`zb z{Hm!ZKIM8EJ57?B+HRhuJ1V@cTEaG;cvud9w%L#ej{u`EZb3}V2=ECA6uxU8Rd7=f z@PB8$UAC$aXKyFnk5j`{B{<|)J$!m%-w>JT=xtQ3$7K=T4L4t7I}f`VTKaK34@_lG zEA~t-o_9dry82zpWacR2h_HHv)>OoLFnE1LY? zv6TyS;)}uVZ5uQf(1@70KJK!fNqa5`Po9eTdkfG7O{1{dq?A+Agwr++>ag2RXW;U$ z=#@&L8XX+fmA4k5rA?9Ccb;B_-OWT(Q+oCugF2qx#$kc0rO^ZhVk9zjc#KTn zDN|5lHS~>;^SPM6CD1ritEt993*_t_BsEO5)6dGE z(8I&lR*DJ&^oObghz;<7MHcrhBc8!WP<%Sn3Rc*3iJqiVn8v-ROh2|x2?|r3IdLOv zNpm|no60>TUKMOp_eoylPD|=siiu4J+<%xl)U}NgAmPz8`Vulw{Fy&bwTbfnmbA(Z z`QPvJ-H*1m$=j*~ZZ~1rcf-3>|z}Ll1emhiMAf;zU!^4dOi@^yFaAIvzVN8!O)55STB_x+2nLRZ zC(-yoN_S29(?{$=biW2BeaK8mY}gDO^%{nb&hUfjTbxO^zvw-cdv4Tc;@u%7nFChg zo_m(?BOj~ww%(3!9hW*d6hh*)s{_}sik1L8rZn)T^*ewdHK_0zKqAq-wh z%tkY|?PMDq2^%)i7HA4?{@6X~-30KxB;cQQvesGeyJ;D2-@6Y$nAW93;|1DLwmmm0 z{CU*ZQGe9*X0G1CkTAN9`JCh}WbPSq)tq4z?VT7>n%`xzc|d%veF{f*O?M7(}# z;&ArELst5w0huRAIJfiA@{pg8FWIMaOWKJ_1Tz*K9A*G}-Fr#ljvagGjoTdoRs zz$&{?eGE!iN^BU$UxZBtvUMhfyXX=#6sOQZ1j#2I{e6JI_sb7ygl9ijMgws2$k9xH zlj#%v_^;`Z|Km6Xz7dmBJNfeUwiwBR z#S-2uUomVag~Kux82UZ)?*~amAKC3wQ@!;m@2-2hTN8f8@xbACQ#!~D177X#H`@15 zaDXhQ2KeC6N4h(n&okhUrhf|`Ma&yr_ofs*N{Jo93AY^hg88bh1kFnk17B(is5xdc z9&#v9QMX%r--+%f5ZZlH8g`;TVVQ97H>B-GCwyoKjC)O65UZ$^7RAMz*@P%!j_inA z&1o+`!X_AvPkrheVKC)r4bsBR-)k_x%@`{UWy0j=W@YsFh%gjS+VM6=#-s}WVo%tO zBCxZcWxz%KmN~5J^vfzcZ&YL-KIP``dH?L1>fIo8#fk zA4Tue^2tGDp!pt`m1446+Q8}VrYY~@OZ9Z7(%*3_%wbmb--<9WNz6B7M~d_@V$g*@ z+K$XwjT&!2jv2n*8}WFL!l;;b5jeR)dIAcyi$t%D@pEbhym4uNdBU(WW)XwO=8-Rz z*bxLudslwjH`Uv`?McsEEbv6S0WUAyI-ekmljKwb47`G$-MOXJ^Q6PzqcnoIlW)Z3 zs9x)3ON9ho29Ppb(TP}@5*&Xfx_+P2yg$LowWnF%}v>*4^) zWtP{8KNib!^uqgAym2PMZMfnZQ<|`p)QE^|RadXGR7oiCyq}B`4r(ZfTQVz6rHGvc zyvyMcM4$SuR&N$D&{8ZA)Yw0L2Y!eXem}h~z{Ncdpe9egEYSP= z1f?cl@m^eWX{c0v$X4!GFjW-rZ3MONT`bT{zcxp}7RuPU5yvONQ;#5a>oLl|F2Y8m zmOB>DzzJ3{G0};B>PGQ$ri=P9dGLfbr^SWFZRV?Bv-0bjXb?KD^M*%co43+m2=(CV$MJT32xmlf1Q^L#AdV;XYjK0qlM5w3*EN+u1?xi9c z=CAVZXX3Q2S*P_-aK*jv7onmmx6$_5;mg)jOKU|fcHy|HTMcC3R=8F zHUJRTFa9Oo^o=L1p`|!`WPO{{Lb;6-U>Su)mRdzcPRmWm@h8|;m^&`PGgXuLw+H}0 zm}-X)8of&(F_0~^N9Cclf2RsX_-u0pl9a09r&5@(FypkC0;h8D_GONjK+RG&T0~)> zg5tJmW5Fe<)LFB>!Xu91VPT+0ES+(S`}hMr!c!t(!S<~QZ76C>JJAUb&WWezmXyad-xCB1V_-^=e9rj zrBYyn!J%hyHu(WziEqNP&^gsmg&HywMA*gsllGZ}@AK;G+!5KDSh9%7ti;8QiXL&y zb9SaaDj1y*BvGN}ACoeQPK1vkwt9;|y4H%Kvlv@%cCEOt#;&{y(f~c1t*cpGR4z&9 z%n|YynZYCB4ANK6I_cL6Czd0Yz?!evR-YU<02$>Y?GY5+o^}ODg&GOs^*$M(OEj|X z4P8s|yS-u}BgZz_oiI;Un;<4GSC)kqNet>D%E+iT2HIq@UI$u_l3R>CW1=E30gCOo zzX`h$Ci3_|0}d0i7B5e5ms1vYQky zzW_@zqS4N4){;P+7)94GUgtJK_zzn1TSI70_Z^t#!u41q(BbbMDkUK6T{E-3s|Ym= z|Do5S1*ZAcMHIK?#>Ma>xZGL+X*DvTIC)wtI~I;vu`}YL;A<#5DGX3t{iQvFO0B7K zi8uGcsdgYvoO#K-dHDEBl2w|rIU=ss=q_sFR&L-ZIY612aU^uuJ4L{Q04s9OPk*Qz zqn4rO!WTA!{rB!zQTNa0r)r7DyJf_P$#BlQ5HD7i@l;(-RE4!MXF?N94rf^&{7;4| zu#qqfi4>)_s^nz&#&*tws3Sjf(k&O{z%FQ(tCcpy-~sxQ3{6D=lfK&OC;c`bI3{MM zRyaqSMgPg#Y_%`he>FVr03tyTwA?yHtm;2fc+GuMNNSByWxf5CoERevG|YO73bbr}c3ELn=9D16cCxDtZ-B{_#YF_6J*ELKO4%xoI*Lho!Zxw@!-ee!^o}g99#Nr^kz1cQ0Tx4|gRpACI{t`zG6C-@t;IG(QN(b`9{-g$9g~Lmq?!Z6Y&s402Pk?rPz1ZxM7yG@%SVTEV zzY59|vQ!UbkKArPciqw#t0_H01R`Z(G24EAP@ z&avQ+&WFzXy!p68QEOxQfwu))U7?DZT6z(YJbvR_CK0zYO6r{?RU4jgDJq{X(Rn=e zCS>}nZZlhvqAY6V0RbMuIn+U{(d&OV%RN#zHz;S);288D^kPar-iNr$75-5;bi(Yb7Ah9Pi3LC;o*e_?Y+-_%8#n!UW04>P@K@?mM{kAnVB zE4r*spn=6H^JvlNryC&~+_uf!o(YpS3-5e%y@qj*Mi1h=ACnh93Qq6PqM{W`K+) zC>@S^=PK!~#R`v!txnlr54R1cJ#gpyD%_9m(7w1pt^(@*7ZHD-YwejJM&wQYWpaZJ zGIeqoQk26hz1P=)+! zx7*2@?7;eO>b^#E$J(7=OQ~kcH&g-HHpPVac7vQwiocjO zG<6j$_z3T!{=CrPTN-ZT9!p z`mb8s?!-;5EyRw#(fm>($S(7OQrM_2yRTWAoT;JRG zR?gdBG0EI>3Uf0AeTFszk>&9^#`pm7htxXZe|IU7?c9eTZd9JMTvb=6!S8hle* zt5CTsoI zAyO>zk~xm-VOwU)^AW`u#;^lch54|ea!M6$Bmpj-vL)p@Erl;Gc)f$zSO4z0<5&ec zh`z;jf*O*j!J&dbVSuH5nPpK;P?2;Rd&K!!uoR_NmADI(D1ED0aOZt3ZwG|Igv>YokUcz zQ(?;Qm?JB7$t_t(q!+0|2deE_|Eq8*cb96h{KGl#Vr(>~0zg#QChdEtXHaPc!5Ljy#F-OqQC zY)e@;H4kLaC&qnYdsNX}~xS=5OkI{WCS-Z1ait4S2MU z3wW0LyS(;o(E^7*Gy=IN4OC%BPQV_|oY7212Zan+UR}B>&_LcwQ#x|1y5MGC${Q&| zX)X+Y9B95O$PRW3?Jhegr^t3;FlxI7(3r5%%Dc`EH!a3^InU3_YkyPMusZW|{Weg& zb-($QIL$h|WNq+hZNU2!RU|ULC<7q3oI#=rUdJMEVHIC-65DYevDMJncD_jKg$pKK zX^nii|H|+~?EfR>pp&9jeQsM|d(OfN{WIML+=C5I+f%?$gwEWHFRH*x)4hA}+!=e{ zsc*e1YytMld;rL$mSHVx65HXsdb zI31@XHV1Yg0Jzw!xK)_iF}Vr$OpH8y&Y>yWw|3Us7W! zHm_{4zYTCl^9oPjxsO13Q-J{nlK|k|wbU%58(!iW!K(1@>dhP6QRn^tls(>>Ep)8p zGsx6i0P(YtH(5gwO$N9$ycdp!_R(HzI5jQ+7X=wz57)a>Kz-09f+qxjXun~=!voE> zT#y0I-E#zGMO-iUeLl+09Zn-&ES8vGOWVU@u?VK28Jl2N6ME?>g#lgV%bQy9{g7Cy zp8RzhZ3rtMB;mq)`$!;vB_uoluFm}hSWWd$`_-?5x9b@JqMoLOO7J_q$s`Vw8G-f& z41A~s@=m{){X@~m-h94i=iyR}txA8slR=2&?mzmFEWzhu)| z9kx`s*#1mPV1YAB!{HB)-UPLXr+&NsTzm^pXYHb2zNnF=8vAC-clc1!UIo?Z?No8C z>^v^c8Mb%-bUk}S&M-`8ef2cO6ig}P<34^)+bpWVuamjQpLTR4Wd17<<*U1EP16Gq zfk4*5B9>-cKh}a8U~8Gd-RvQ5j2$;0p&^r3VaLexBgx_TXM$iJu|2N_0SW)QF5^?=h z(#9`?r4{oF0Qk;^mPB^~+AC&2wf*8w0N_tFL1IIbU3|fXQ__xSFXgZ?NwFsSKJ_1& zmQ&Iaakl>v$tqJD5B0f2dF!h{Xl^lz%&?RKYmDMA&ayUPH`>mNsz(T$#)2ii7W|h; zpmy(H4I>xyS~CI^`GaW$8Q$KL^Amem*sJZ2kBq6phiGC1QbJ5O4x;zLU1_kCV zz|OuS=J3#OZ5{|mw!8+SGbU6k<_Hu+r7?)v{(ErS=J(`(rcH;QLQha!>BJlpJE5SV z0N2>)(TEqnSuv)6zI$R{k0GP@#9f<%Mu6`&AA<7nb{VwgQl!k7f9!NY81?WAIY3-T z=QZ#pXm;@BvG98tXSzT-eRokqDEOFl)N*0TdFcEByp#?v6cD~`25@GJuN3k|-pE_# zsPTi^K}6_{eL3x{c-aKqSFLcT5vD6|@7~&)_Zxe|0947v!ne2k2;Inq*#FeCd^V}e zy!bXq<1TjKOHR__kCcKEqiON(gGFaTZqx~dm&SFyy*7F^_B_R8EC!)9 z`+*%69hYcI+~-L>(QZUj_j0i)qyG?2GgUkMK$q4TDP2cE!SV9%>+-tO;XkVkbgS}v zVNvk6@_e?Im-BwI6^J4HM`2Ve9q&=MK0@xZ+H_0XtK5iG3@!U3l%Nx+xJF~D z-h17U|8N$Fp%_A%vu6m=@R@3BCde(hDt6UDe3nBGbb29y(AM;d*5ukVQhr}|_f_t# zN1_E8w5kKo9wx}^h*;Q3EeBJcI1XVs&=II(%R71)fcuIrflv_k1ice+=i+?x)O0mG zHAA&I-Ta{Yy7S$1fW3iv=6a?6Nr^VdoeCBm?RMf%&<;K(v=Bl+M`r}4r0Z8BSx1-z zJc0x$M3mI<5(YzDznU1~tsQ}R9Zg=eQ}S=j&e^({q$^iLC|6G)o7x?9OxNz~7-t(D zNknpAPZXxQT&RRXTiu4!PMmH@zQNu?>Sk#pK2(A?R+cm(JusDn5pZXBvLTtadtD!% z67TSeEiJ_;2x(S*F*icrYwHU*#k+?7A{BMvv=b$zz$Xx_c2QwlpfZBdI)o z-i;8<&9C=a;}r|w1(7f;cfe^aeCoVjg5>LW!Yw^l44J6|P8Q=g-#BeIcZXg?E_>t4 zK@%;#S>DSVZ7=zeP|)9jp|_XJ4AIR1YQax7xJy~%;8hO${^5L%ouShxT%vog5gdX9 zDizjyk_>u*r6^eOYG(KIshO``r~r&07nm&GlI(F{*mUwbXrSTyr(a8`UlQ}Ju zd5w(91ay@{#`Q>ql0-50f)CkZSX1TiH`SKNpfF7IMA3KD1p=)2Up}|-df4{vdBrF> ztuBLi1|E?Ce72J87HKs5?aV$}x z6#UUvYB5N<)=7f!cn%W1qgn)`8@1t*jc#Q$EXs?!;is&}LH~Y~P0PEeI+zbnOc57w^qGNo1QreZrMS(Ce(7 zf3(D*JoLeM00$u3R+ZvpaQmFkgHST4`B{VSs`>x!WBp)v?}Y{6(CzuDyRd7juBsuj4!*?QT=Sf5*)&s5F@twF0B?+1>L(>PsZpi$Dy;HAnM z>C^GKV&|_vI19I)ua8aTaDy9Io!fvV>VwQujko|s?FCo>Au7C6e3v>9vp4YVqqyi@ zbGL~}rYw4S6#a5ID@9K8^WV__A(exo9_N({OV-F4=7EfnIB*fejEtQdlC3TkP}lc{lV_}2RkLmTiSb?0`ZCgW=Ehqq>6aMA4ctf|5Tw9wXGCXW)m z8&MsY-6&kUFB*%_p7*!9C*zK<9S4t@woLze@0-93a)O5iu!gq7vFyI>-D5yC&Ewps>4z%)5^D5&^MBkT?K^8fk|^&Z$@(4GkZO zmx<_^W>9?rW@P5;UpV z%EvU5=7@ro9n;qFGOj=SF#(9Tp0j=_GqBh~<8L;DCry8{HhhHzz@74?e7i7>Oc})} zIa~=8oL$Zt+Tx&9SWBWU^WgPF$k4z9=}*D_FFZ!a>N&C!+gQa_Z#S;^*;y73k*Kkq zOt?Mwl;A70xVN|gN@A<~Q#C@p(?P)gd_)s}pp}?&n|laRIuSCw<(yrvsYV}C=s{et zm3rrYPL?DQFWIg&2WO)N8(kjX@8Zg6PZ@{HDCs7@uIy4n;kKp-#irNNdJwQHInC|!-~E#b9F2os&cO-r=Zj-^ z08k!0S*ZB-_%LH?el-6u_AC8V>ypXO8FbT)@q>Zo(m1SRErs9zie}CCJKFVX;^K5b#jZ44U6jjb405RLE8f|G}o`# zzuL4=egFTcQbui(bd%?@^?Ay4>AAb%?q_^7OZ_Db7AdX}aHC>sst$OC3|@r#F>qY^ z=eli%c<_>QVELrmZuju_Y3mWwUfnP1LkGZXP6f>Mv8`V_Tre#yH1UcPrCLeXwpry5y$D54rhM+Phw& kv;WVr;6F@RtREDWQp^6jB@cX%Gt2-58CB^Yl4ha*2T$+Vj{pDw literal 0 HcmV?d00001 diff --git a/src/core/GameSession.ts b/src/core/GameSession.ts index 9e8f427..596d1cb 100644 --- a/src/core/GameSession.ts +++ b/src/core/GameSession.ts @@ -56,7 +56,7 @@ module TS.SpaceTac { * * If *fleet* is false, the player fleet will be empty, and needs to be set with *setCampaignFleet*. */ - startNewGame(fleet = true): void { + startNewGame(fleet = true, story = false): void { this.universe = new Universe(); this.universe.generate(); @@ -67,14 +67,16 @@ module TS.SpaceTac { this.player = new Player(this.universe); if (fleet) { - this.setCampaignFleet(); + this.setCampaignFleet(null, story); } } /** * Set the initial campaign fleet, null for a default fleet + * + * If *story* is true, the main story arc will be started. */ - setCampaignFleet(fleet: Fleet | null = null) { + setCampaignFleet(fleet: Fleet | null = null, story = true) { if (fleet) { this.player.fleet = fleet; } else { @@ -84,6 +86,10 @@ module TS.SpaceTac { this.player.fleet.setLocation(this.start_location); this.player.fleet.credits = 500; + + if (story) { + this.player.missions.startMainStory(this.universe, this.player.fleet); + } } // Start a new "quick battle" game diff --git a/src/core/Player.ts b/src/core/Player.ts index 9e40afb..f5ffd9f 100644 --- a/src/core/Player.ts +++ b/src/core/Player.ts @@ -2,16 +2,19 @@ module TS.SpaceTac { // One player (human or IA) export class Player { // Player's name - name: string; + name: string // Universe in which we are playing - universe: Universe; + universe: Universe // Current fleet - fleet: Fleet; + fleet: Fleet // List of visited star systems - visited: StarLocation[] = []; + visited: StarLocation[] = [] + + // Active missions + missions = new ActiveMissions() // Create a player, with an empty fleet constructor(universe: Universe = new Universe(), name = "Player") { @@ -44,9 +47,12 @@ module TS.SpaceTac { /** * Set a star location as visited. + * + * This should always be called for any location, even if it was already marked visited. */ setVisited(location: StarLocation): void { add(this.visited, location); + this.missions.checkStatus(this.fleet, this.universe); } // Get currently played battle, null when none is in progress @@ -55,6 +61,7 @@ module TS.SpaceTac { } setBattle(battle: Battle | null): void { this.fleet.setBattle(battle); + this.missions.checkStatus(this.fleet, this.universe); } /** diff --git a/src/core/StarLocation.ts b/src/core/StarLocation.ts index a6f56c1..b5b07a9 100644 --- a/src/core/StarLocation.ts +++ b/src/core/StarLocation.ts @@ -51,6 +51,13 @@ module TS.SpaceTac { this.shop = new Shop(level); } + /** + * Check if the location is clear of encounter + */ + isClear(): boolean { + return this.encounter_gen && this.encounter === null; + } + // Set the jump destination of a WARP location setJumpDestination(jump_dest: StarLocation): void { if (this.type === StarLocationType.WARP) { diff --git a/src/core/missions/ActiveMissions.spec.ts b/src/core/missions/ActiveMissions.spec.ts new file mode 100644 index 0000000..d1e2c1f --- /dev/null +++ b/src/core/missions/ActiveMissions.spec.ts @@ -0,0 +1,56 @@ +module TS.SpaceTac.Specs { + describe("ActiveMissions", () => { + it("starts the main story arc", function () { + let missions = new ActiveMissions(); + expect(missions.main).toBeNull(); + + let session = new GameSession(); + session.startNewGame(true, false); + + missions.startMainStory(session.universe, session.player.fleet); + expect(missions.main).not.toBeNull(); + }) + + it("gets the current list of missions, and updates them", function () { + let missions = new ActiveMissions(); + + missions.main = new Mission([new MissionPart("Do something")]); + missions.secondary = [ + new Mission([new MissionPart("Maybe do something")]), + new Mission([new MissionPart("Surely do something")]) + ]; + + expect(missions.getCurrent().map(mission => mission.current_part.title)).toEqual([ + "Do something", + "Maybe do something", + "Surely do something", + ]); + + let universe = new Universe(); + let fleet = new Fleet(); + missions.checkStatus(fleet, universe); + + expect(missions.getCurrent().map(mission => mission.current_part.title)).toEqual([ + "Do something", + "Maybe do something", + "Surely do something", + ]); + + spyOn(missions.secondary[0].current_part, "checkCompleted").and.returnValue(true); + missions.checkStatus(fleet, universe); + + expect(missions.getCurrent().map(mission => mission.current_part.title)).toEqual([ + "Do something", + "Surely do something", + ]); + + spyOn(missions.main.current_part, "checkCompleted").and.returnValue(true); + missions.checkStatus(fleet, universe); + + expect(missions.getCurrent().map(mission => mission.current_part.title)).toEqual([ + "Surely do something", + ]); + expect(missions.main).toBeNull(); + }) + }) +} diff --git a/src/core/missions/ActiveMissions.ts b/src/core/missions/ActiveMissions.ts new file mode 100644 index 0000000..4994d98 --- /dev/null +++ b/src/core/missions/ActiveMissions.ts @@ -0,0 +1,44 @@ +module TS.SpaceTac { + /** + * A list of active missions + */ + export class ActiveMissions { + main: Mission | null = null + secondary: Mission[] = [] + + constructor() { + } + + /** + * Start the main story arc + */ + startMainStory(universe: Universe, fleet: Fleet) { + this.main = new MainStory(universe, fleet); + } + + /** + * Get the current list of active missions + */ + getCurrent(): Mission[] { + let result: Mission[] = []; + if (this.main) { + result.push(this.main); + } + return result.concat(this.secondary); + } + + /** + * Check status for all active missions + * + * This will remove ended missions + */ + checkStatus(fleet: Fleet, universe: Universe): void { + if (this.main) { + if (!this.main.checkStatus(fleet, universe)) { + this.main = null; + } + } + this.secondary = this.secondary.filter(mission => mission.checkStatus(fleet, universe)); + } + } +} diff --git a/src/core/missions/MainStory.spec.ts b/src/core/missions/MainStory.spec.ts new file mode 100644 index 0000000..509e2d7 --- /dev/null +++ b/src/core/missions/MainStory.spec.ts @@ -0,0 +1,34 @@ +module TS.SpaceTac.Specs { + describe("MainStory", () => { + function checkPart(story: Mission, index: number, title: string) { + expect(story.parts.indexOf(story.current_part)).toBe(index); + expect(story.current_part.title).toMatch(title); + expect(story.completed).toBe(false); + } + + function goTo(fleet: Fleet, location: StarLocation, win_encounter = true) { + fleet.setLocation(location, true); + if (fleet.battle) { + fleet.battle.endBattle(win_encounter ? fleet : fleet.battle.fleets[1]); + if (win_encounter) { + fleet.player.exitBattle(); + } else { + fleet.player.revertBattle(); + } + } + } + + it("can be completed", function () { + let session = new GameSession(); + session.startNewGame(true, true); + let fleet = nn(session.player.fleet); + + let missions = session.player.missions; + let story = nn(missions.main); + checkPart(story, 0, "^Find your contact in .* system$"); + + goTo(fleet, (story.current_part).destination); + expect(story.completed).toBe(true); + }) + }) +} diff --git a/src/core/missions/MainStory.ts b/src/core/missions/MainStory.ts new file mode 100644 index 0000000..eb0279a --- /dev/null +++ b/src/core/missions/MainStory.ts @@ -0,0 +1,23 @@ +/// + +module TS.SpaceTac { + function randomLocation(stars: Star[], excludes: StarLocation[] = []) { + let random = RandomGenerator.global; + let star = stars.length == 1 ? stars[0] : random.choice(stars); + return RandomGenerator.global.choice(star.locations.filter(loc => !contains(excludes, loc))); + } + + /** + * Main story arc + */ + export class MainStory extends Mission { + constructor(universe: Universe, fleet: Fleet) { + let random = RandomGenerator.global; + let location = nn(fleet.location); + + super([ + new MissionPartGoTo(randomLocation([location.star], [location]), "Find your contact") + ], true); + } + } +} diff --git a/src/core/missions/Mission.spec.ts b/src/core/missions/Mission.spec.ts new file mode 100644 index 0000000..00926f9 --- /dev/null +++ b/src/core/missions/Mission.spec.ts @@ -0,0 +1,36 @@ +module TS.SpaceTac.Specs { + describe("Mission", () => { + it("check step status", function () { + let mission = new Mission([ + new MissionPart("Part 1"), + new MissionPart("Part 2") + ]); + let universe = new Universe(); + let fleet = new Fleet(); + + expect(mission.current_part).toBe(mission.parts[0]); + + let result = mission.checkStatus(fleet, universe); + expect(result).toBe(true); + expect(mission.current_part).toBe(mission.parts[0]); + + spyOn(mission.parts[0], "checkCompleted").and.returnValues(false, true); + + result = mission.checkStatus(fleet, universe); + expect(result).toBe(true); + expect(mission.current_part).toBe(mission.parts[0]); + result = mission.checkStatus(fleet, universe); + expect(result).toBe(true); + expect(mission.current_part).toBe(mission.parts[1]); + result = mission.checkStatus(fleet, universe); + expect(result).toBe(true); + expect(mission.current_part).toBe(mission.parts[1]); + + spyOn(mission.parts[1], "checkCompleted").and.returnValue(true); + + result = mission.checkStatus(fleet, universe); + expect(result).toBe(false); + expect(mission.current_part).toBe(mission.parts[1]); + }) + }) +} diff --git a/src/core/missions/Mission.ts b/src/core/missions/Mission.ts new file mode 100644 index 0000000..f57e1e9 --- /dev/null +++ b/src/core/missions/Mission.ts @@ -0,0 +1,47 @@ +module TS.SpaceTac { + /** + * A mission (or quest) assigned to the player + */ + export class Mission { + // Indicator that the quest is part of the main story arc + main: boolean + + // Parts of the mission + parts: MissionPart[] + + // Current part + current_part: MissionPart + + // Indicator that the mission is completed + completed: boolean + + constructor(parts: MissionPart[], main = false) { + this.main = main; + this.parts = parts; + this.current_part = parts[0]; + this.completed = false; + } + + /** + * Check the status for current part, and move on to next part if necessary. + * + * Returns true if the mission is still active. + */ + checkStatus(fleet: Fleet, universe: Universe): boolean { + if (this.completed) { + return false; + } else if (this.current_part.checkCompleted(fleet, universe)) { + let current_index = this.parts.indexOf(this.current_part); + if (current_index < 0 || current_index >= this.parts.length - 1) { + this.completed = true; + return false; + } else { + this.current_part = this.parts[current_index + 1]; + return true; + } + } else { + return true; + } + } + } +} diff --git a/src/core/missions/MissionPart.ts b/src/core/missions/MissionPart.ts new file mode 100644 index 0000000..0db9646 --- /dev/null +++ b/src/core/missions/MissionPart.ts @@ -0,0 +1,20 @@ +module TS.SpaceTac { + /** + * An abstract part of a mission, describing the goal + */ + export class MissionPart { + // Very short description + title: string + + constructor(title: string) { + this.title = title; + } + + /** + * Abstract checking if the part is completed + */ + checkCompleted(fleet: Fleet, universe: Universe): boolean { + return false; + } + } +} diff --git a/src/core/missions/MissionPartGoTo.spec.ts b/src/core/missions/MissionPartGoTo.spec.ts new file mode 100644 index 0000000..a6d79e7 --- /dev/null +++ b/src/core/missions/MissionPartGoTo.spec.ts @@ -0,0 +1,27 @@ +module TS.SpaceTac.Specs { + describe("MissionPartGoTo", () => { + it("completes when the fleet is at location, without encounter", function () { + let destination = new StarLocation(new Star(null, 0, 0, "Atanax")); + destination.encounter_random = new SkewedRandomGenerator([0], true); + let part = new MissionPartGoTo(destination, "Collect gems"); + + let universe = new Universe(); + let fleet = new Fleet(); + + expect(part.title).toEqual("Collect gems in Atanax system"); + expect(part.checkCompleted(fleet, universe)).toBe(false, "Init location"); + + fleet.setLocation(destination, true); + expect(part.checkCompleted(fleet, universe)).toBe(false, "Encounter not clear"); + + destination.clearEncounter(); + expect(part.checkCompleted(fleet, universe)).toBe(true, "Encouter cleared"); + + fleet.setLocation(new StarLocation(), true); + expect(part.checkCompleted(fleet, universe)).toBe(false, "Went to another system"); + + fleet.setLocation(destination, true); + expect(part.checkCompleted(fleet, universe)).toBe(true, "Back at destination"); + }) + }) +} diff --git a/src/core/missions/MissionPartGoTo.ts b/src/core/missions/MissionPartGoTo.ts new file mode 100644 index 0000000..9639195 --- /dev/null +++ b/src/core/missions/MissionPartGoTo.ts @@ -0,0 +1,20 @@ +/// + +module TS.SpaceTac { + /** + * A mission part that requires the fleet to go to a specific location + */ + export class MissionPartGoTo extends MissionPart { + destination: StarLocation + + constructor(destination: StarLocation, directive: string, hint = true) { + super(hint ? `${directive} in ${destination.star.name} system` : directive); + + this.destination = destination; + } + + checkCompleted(fleet: Fleet, universe: Universe): boolean { + return fleet.location === this.destination && this.destination.isClear(); + } + } +} diff --git a/src/ui/Preload.ts b/src/ui/Preload.ts index adb91c5..8425d29 100644 --- a/src/ui/Preload.ts +++ b/src/ui/Preload.ts @@ -68,7 +68,8 @@ module TS.SpaceTac.UI { this.loadImage("map/location-star.png"); this.loadImage("map/location-planet.png"); this.loadImage("map/location-warp.png"); - this.loadSheet("map/status.png", 32, 32); + this.loadSheet("map/status.png", 32); + this.loadSheet("map/missions.png", 70); this.loadImage("character/sheet.png"); this.loadImage("character/close.png"); this.loadImage("character/ship.png"); diff --git a/src/ui/common/UIComponent.ts b/src/ui/common/UIComponent.ts index 8b56244..4e7e9ee 100644 --- a/src/ui/common/UIComponent.ts +++ b/src/ui/common/UIComponent.ts @@ -202,12 +202,10 @@ module TS.SpaceTac.UI { /** * Add a static text. */ - addText(x: number, y: number, content: string, color = "#ffffff", size = 16, bold = false, center = true, width = 0): void { + addText(x: number, y: number, content: string, color = "#ffffff", size = 16, bold = false, center = true, width = 0, vcenter = center): void { let style = { font: `${bold ? "bold " : ""}${size}pt Arial`, fill: color, align: center ? "center" : "left" }; let text = new Phaser.Text(this.view.game, x, y, content, style); - if (center) { - text.anchor.set(0.5, 0.5); - } + text.anchor.set(center ? 0.5 : 0, vcenter ? 0.5 : 0); if (width) { text.wordWrap = true; text.wordWrapWidth = width; diff --git a/src/ui/map/ActiveMissionsDisplay.spec.ts b/src/ui/map/ActiveMissionsDisplay.spec.ts new file mode 100644 index 0000000..9da1deb --- /dev/null +++ b/src/ui/map/ActiveMissionsDisplay.spec.ts @@ -0,0 +1,23 @@ +module TS.SpaceTac.UI.Specs { + describe("ActiveMissionsDisplay", function () { + let testgame = setupEmptyView(); + + it("displays active missions", function () { + let view = testgame.baseview; + let missions = new ActiveMissions(); + let display = new ActiveMissionsDisplay(view, missions); + + let container = (display).container; + expect(container.children.length).toBe(0); + + missions.secondary.push(new Mission([ + new MissionPart("Get back to base") + ])); + + display.update(); + expect(container.children.length).toBe(2); + expect(container.children[0] instanceof Phaser.Image).toBe(true); + checkText(container.children[1], "Get back to base"); + }); + }); +} diff --git a/src/ui/map/ActiveMissionsDisplay.ts b/src/ui/map/ActiveMissionsDisplay.ts new file mode 100644 index 0000000..c8aedbf --- /dev/null +++ b/src/ui/map/ActiveMissionsDisplay.ts @@ -0,0 +1,31 @@ +/// + +module TS.SpaceTac.UI { + /** + * Widget to display the active missions list + */ + export class ActiveMissionsDisplay extends UIComponent { + private missions: ActiveMissions + + constructor(parent: BaseView, missions: ActiveMissions) { + super(parent, 520, 210); + this.missions = missions; + + this.update(); + } + + /** + * Update the current missions list + */ + update() { + this.clearContent(); + + let active = this.missions.getCurrent(); + let offset = 245 - active.length * 70; + active.forEach((mission, idx) => { + this.addImage(35, offset + 70 * idx, "map-missions"); + this.addText(90, offset + 70 * idx, mission.current_part.title, "#d2e1f3", 22, false, false, 430, true); + }); + } + } +} \ No newline at end of file diff --git a/src/ui/map/UniverseMapView.ts b/src/ui/map/UniverseMapView.ts index b2e8d15..99b1582 100644 --- a/src/ui/map/UniverseMapView.ts +++ b/src/ui/map/UniverseMapView.ts @@ -34,6 +34,9 @@ module TS.SpaceTac.UI { // Actions for selected location actions: MapLocationMenu + // Active missions + missions: ActiveMissionsDisplay + // Character sheet character_sheet: CharacterSheet @@ -91,6 +94,10 @@ module TS.SpaceTac.UI { this.actions.setPosition(30, 30); this.actions.moveToLayer(this.layer_overlay); + this.missions = new ActiveMissionsDisplay(this, this.player.missions); + 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); this.layer_overlay.add(this.zoom_in); @@ -153,6 +160,8 @@ module TS.SpaceTac.UI { this.actions.setFromLocation(this.player.fleet.location, this); + this.missions.update(); + if (interactive) { this.setInteractionEnabled(true); } @@ -258,6 +267,7 @@ module TS.SpaceTac.UI { setInteractionEnabled(enabled: boolean) { this.interactive = enabled && !this.session.spectator; this.actions.setVisible(enabled && this.zoom == 2, 300); + this.missions.setVisible(enabled && this.zoom == 2, 300); this.animations.setVisible(this.zoom_in, enabled && this.zoom < 2, 300); this.animations.setVisible(this.zoom_out, enabled && this.zoom > 0, 300); this.animations.setVisible(this.character_sheet, enabled, 300);