zGd~YSJR`AKAt^OAPazSaI7J~pFQ-yLHASH$GZ)p2Vg|>Q6b0vu#G=IHlGGvywNC2!Ju2sRkD8ICzm;qftA+R*HxCB`o
zYH8G`9A+;hC)l9H#a(-S(VrE{kLS{}*YI|F9nmCjeXXF=^C}`wn
z=3!S`P?TSgT2zvmisUsAGe5sLmBBeb7Zi~&S|P0{KUYDuSRp?TL?)Ifs1`Fg=jWAV
z=9MObV;RaxRVc_wtjx?yS18F>D9y{sPtH~-$;d2LC{9f-N-aSSQ3mIt)WnihD20e=
zm;g9-P&nzJc!Y^0V(Q3CEmtT`&CSe9EUJVl1BsOurxr1|q~@fSq$1>@e5f2Exw@ug
zmLODtd0;iJc_l@a3MHu(B@C`bMfpV#QX!?Z2;^IEDl9F?$xlqN0)>t1qlSsOiJ3VH
zi76>Xsl~+%=mO{o216c}4%{+xQWa7wl2cPtiWQ18t5Owm6Du-vOLG~(GN2?|UIa=N
zpgf@Ps9}z3@uP;hAjy){B19M!GkE5e6y>LsCWE8PC$ShcGQa|mAoocvOU+^M$xJE&
zg(H+k&eA#xs>LA8;FF)8nFk>i(h@UsQd2;j{L&HzpZwwyNd98*P0UM7Pi62;%}q)z
zf>Ol{zMwLrBtI{;n87zcCABEAB)^Ek7o@AWG&wo7xHzpeMsCnq(zB)_Pb!4H(@
z{E#CU!~w^Qe|erlK~a7>D4eYnzy*`4DnmeGadCNmQ3{-)01Hu=i~_zqg{}yDZVCVe
zV{vIxZf0?DW`15VLqKX#aeiK64st>-W(X)s1?6oRt$?pMfT~5f7NQrwV^P&$cPk`v
z!K6ZRer^FMJEZ6-MCO+&WF(fQDx@YB<)x;8ij|0fVm$`1LP(B7FpD9=nV@1IMIk>;
zK{W*;0+j%zL585jylgO$nU~HGl$xGdT#{M@WfVb5i=fop{IXQ!)Cm;<7w|!;#i=Fm
zay%#>RJ#O~g2-UZQY1JvCnY}@HN1mULCFSG`2<6X15hCcWr8Y*)SQ%JhTx3+(wr29
zk_<$l0M9-OMX8B7Ih6`YsR}9JawEkaSv@G(A=GN97Hi_rP_Cd_tQ%024=Nr&29!hc
z1laO&g~Yt#^3)=R;Oxu-g_6YLY=&TXN&<&%F+*@^5jf#MX}k#srUsVG;e0$b2bvn}
z2`2>PG!OwVvk+M{r&6IfKc_4;g&`y(RRJ}K5h0XW3{TD>;K~5u&&*xxpP?TSmn8Of~S(1~=5RzYznGDa|3?ca?i8;8qh^!w9D(^uAIP72yxa*-+G+Z2C
zZiFj>g(^Y-Pq3mZ!XB<5RmBY9`FW5;!4M89#z8diou>@RSB`4-3r{*w#f+91oG?gJ4
zQa3}W6osTpP_+-r^~DU348f3*UzAw|YS!d1~7n8E-qQAa2198cRB;)o
z5tE#l!w^uEpOuPg4-RL`FXl7i6yC4
z3PuKoCb|YDx<)1nhNe~~X4(dZ1_oRK{<=Y_WtkvT@TvpVo4O%IiFw62peoEtfy*Z`
zFTFG|Jyka(H8B^7Z>5lu8t>xD2^>N+JBrxqlZWLPQa>FGldS3pi_QDTm+TYgb)v6Vtz0hn8CYhBSORN+i?t|MBoSImaoDq;%lA&v0rBIfcT3(E#6(MP8
zrBDcJgM+G#VlD=7)g2K4>QaEBCNmYK^8jl7U`uBD`aFA
zm*f{^7AI$bG-l@Ir4~U9Ow3d8P0h?n%_}YenX03ZlvUR9)sKAJ?YR2G8<%a$CWst}zbxO%w%
zQZv(2^Wd?75M~HJyd)zhHIKm?(lbgdLa-Fdz$K$XBDjlG3MNA{iWuDTi%N=9OR7rq
z(iwsaGE;L>70OdHOHzw;6iRbJsiQbGPa!=uBQ-NOH4mD671D}QGpkBLE&vC-Yf*7Y
zY7WR^xLd#}2qG2;%01}(ba3ncol`5nc@Tg%PsADw;tRycpIim!WuM@np<+b}!3E&-UsO<3BJ1Cgo)2mt>@7Gk9dC7Ad4<
z=B1<-mw-x=#GIT|P?ZKsE{UbZ;NmSTzbGfQxHK;v;Y3ggm6DmNP>@($49@V73bt6G
zsx()@FEKfzI5{IH5mb7DREB4!rlb~Sv0u-9Q
zK`j_a8U~l+5GL4{o_XMs2b>-BK)uS$JO-D{R0YSp+|-;Dkax=yixeOck)2;ulnM&J
z{B%&?6IA%;7p3NPQ&SjV3~+}OQP?AOML~9@f-96FP!tt|
zO8(%)++0u+h4w{DQ;WcP2b_(H6kr(w+y^|oEiWfCIU}_QIWQ3|RA}$Agux}TSOLZX
zCoOo{h$)4qv_m(gs#F0-K^K-^l#yBls;PVqZv!P8aN!H8K2Zj{!tx;<4^Sx%WhvMK$TE0*fvFOQC&1|h((zPCOiE7$rRRK5TGvx>0rh4f6>LNRy#9iB
zXF>HIL?fh>%wupXN=#1$)yLpM9HTqS5Rj8tQiax?Ey_$SO3j0HX*0o12L{)?lC;#K
zJk)XkCI+stp(TYvRcX=TJ;~XissUsQs4@o|mYS2t;0~z{P>TytVk}Mt)d%2oh!Rtd
zrN!lm895A~0yh;*rlsbjfV!3WIXU1G0wjaT#O0Yq*`TDUke&)Dxl0tvQ;Skk^XwUd
zVLD(*8KDc@$Hk)+-py4=%&P*|p^#86M)0BO4I$uInwFlJl&WxePf}`8aRF!?Ej5n;
zrVJb%pmCq%40xLZRCa+l1d}3MA!b@ck;0!C(UsyzjZl9uz>;1HsF;HKEh!b;8d5+i
zN}vG`$@`$V0mT<0q|scQnOdBx01cDWJcaVaqLS1S1yI`-)KCD0AE>LGngi;8=O&hb
z8?-2e4x|+VDa{$&Q;QRGOVU8?5{L+*NP@*VykZ9hG`J3g$$$i~Rj{duvJaw!KskY8
z9-4R2%>qf|k0cD!aF=~Br3~(=<@tGeh#Dd!wW0*llSoU<0X1(F9P>)b^NUItA_Cyi
znv_`#ZfvTifZKbSdBy4A&;*qo$T?CWwFq2glrVrRQ$({3-h_bk1M>2dGoTe%Vs=Sp
z8L0Kl0Bdk7fVz&V#SDo_Nkys28L4>;ph!zg%m9@;pdJLI{Rd^0fL)|o3~7NfK=O5J
z9VOEVU>HR4GFH>7W7v(o+YQWeO>o$r;d&I!K@dYEm(@eUb+@6VxpMmzE$4KqVKb
x8I+l>$dH
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model to interact with Achievements-tables.
+ *
+ * @author Oliver Hanraths
+ */
+ class AchievementsModel extends \hhu\z\Model
+ {
+
+
+
+
+ /**
+ * Construct a new AchievementsModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get an Achievement by its URL.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @param string $achievementUrl URL-title of Achievement
+ * @return array Achievement data
+ */
+ public function getAchievementByUrl($seminaryId, $achievementUrl)
+ {
+ $data = $this->db->query(
+ 'SELECT achievements.id, achievementconditions.condition, title, url, description, progress, unachieved_achievementsmedia_id, achieved_achievementsmedia_id '.
+ 'FROM achievements '.
+ 'LEFT JOIN achievementconditions ON achievementconditions.id = achievements.achievementcondition_id '.
+ 'WHERE seminary_id = ? AND url = ?',
+ 'is',
+ $seminaryId, $achievementUrl
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($achievementUrl);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get all not yet achieved Achievements for a Seminary that can
+ * only be achieved once (only by one Character).
+ *
+ * @param int $seminaryId ID of Seminary
+ * @return array Achievements data
+ */
+ public function getUnachievedOnlyOnceAchievementsForSeminary($seminaryId)
+ {
+ return $this->db->query(
+ 'SELECT achievements.id, achievementconditions.condition, title, url, description, progress, hidden, only_once, all_conditions, deadline, unachieved_achievementsmedia_id, achieved_achievementsmedia_id '.
+ 'FROM achievements '.
+ 'LEFT JOIN achievementconditions ON achievementconditions.id = achievements.achievementcondition_id '.
+ 'WHERE achievements.seminary_id = ? AND only_once = 1 AND NOT EXISTS ('.
+ 'SELECT character_id '.
+ 'FROM achievements_characters '.
+ 'WHERE achievements_characters.achievement_id = achievements.id'.
+ ')',
+ 'i',
+ $seminaryId
+ );
+ }
+
+
+ /**
+ * Get all Achievements that have a deadline.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @return array Achievements data
+ */
+ public function getDeadlineAchievements($seminaryId)
+ {
+ return $this->db->query(
+ 'SELECT achievements.id, achievementconditions.condition, title, url, description, progress, hidden, only_once, all_conditions, deadline, unachieved_achievementsmedia_id, achieved_achievementsmedia_id '.
+ 'FROM achievements '.
+ 'LEFT JOIN achievementconditions ON achievementconditions.id = achievements.achievementcondition_id '.
+ 'WHERE achievements.seminary_id = ? AND deadline IS NOT NULL',
+ 'i',
+ $seminaryId
+ );
+ }
+
+
+ /**
+ * Get seldom Achievements.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @param int $count Number of Achievements to retrieve
+ * @return array List of seldom Achievements
+ */
+ public function getSeldomAchievements($seminaryId, $count, $alsoWithDeadline=true)
+ {
+ return $this->db->query(
+ 'SELECT id, title, url, description, progress, hidden, unachieved_achievementsmedia_id, achieved_achievementsmedia_id, count(DISTINCT character_id) AS c '.
+ 'FROM achievements_characters '.
+ 'LEFT JOIN achievements ON achievements.id = achievements_characters.achievement_id '.
+ 'WHERE achievements.seminary_id = ? AND only_once = 0 '.
+ (!$alsoWithDeadline ? 'AND achievements.deadline IS NULL ' : null).
+ 'GROUP BY achievement_id '.
+ 'ORDER BY count(DISTINCT character_id) ASC '.
+ 'LIMIT ?',
+ 'ii',
+ $seminaryId,
+ $count
+ );
+ }
+
+
+ /**
+ * Get all achieved Achievements for a Character.
+ *
+ * @param int $characterId ID of Character
+ * @return array Achievements data
+ */
+ public function getAchievedAchievementsForCharacter($characterId, $alsoWithDeadline=true)
+ {
+ return $this->db->query(
+ 'SELECT achievements.id, achievements_characters.created, achievements.title, achievements.url, achievements.description, achievements.progress, unachieved_achievementsmedia_id, achieved_achievementsmedia_id '.
+ 'FROM achievements '.
+ 'INNER JOIN achievements_characters ON achievements_characters.achievement_id = achievements.id '.
+ 'WHERE achievements_characters.character_id = ? '.
+ (!$alsoWithDeadline ? 'AND achievements.deadline IS NULL ' : null).
+ 'ORDER BY achievements_characters.created DESC',
+ 'i',
+ $characterId
+ );
+ }
+
+
+ /**
+ * Get all not yet achieved Achievements for a Character.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @param int $characterId ID of Character
+ * @return array Achievements data
+ */
+ public function getUnachhievedAchievementsForCharacter($seminaryId, $characterId, $includeOnlyOnce=false, $alsoWithDeadline=true)
+ {
+ return $this->db->query(
+ 'SELECT achievements.id, achievementconditions.condition, title, url, description, progress, hidden, only_once, all_conditions, deadline, unachieved_achievementsmedia_id, achieved_achievementsmedia_id '.
+ 'FROM achievements '.
+ 'LEFT JOIN achievementconditions ON achievementconditions.id = achievements.achievementcondition_id '.
+ 'WHERE achievements.seminary_id = ? AND only_once <= ? AND NOT EXISTS ('.
+ 'SELECT character_id '.
+ 'FROM achievements_characters '.
+ 'WHERE '.
+ 'achievements_characters.achievement_id = achievements.id AND '.
+ 'achievements_characters.character_id = ?'.
+ ') '.
+ (!$alsoWithDeadline ? 'AND achievements.deadline IS NULL ' : null).
+ 'ORDER BY achievements.pos ASC',
+ 'iii',
+ $seminaryId,
+ $includeOnlyOnce,
+ $characterId
+ );
+ }
+
+
+ /**
+ * Get the rank for the number of achieved Achievements.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @param int $xps Amount of achieved Achievements
+ * @return int Rank of Achievements count
+ */
+ public function getCountRank($seminaryId, $count)
+ {
+ $data = $this->db->query(
+ 'SELECT count(*) AS c '.
+ 'FROM ('.
+ 'SELECT count(DISTINCT achievement_id) '.
+ 'FROM achievements_characters '.
+ 'LEFT JOIN achievements ON achievements.id = achievements_characters.achievement_id '.
+ 'WHERE achievements.seminary_id = ? '.
+ 'GROUP BY character_id '.
+ 'HAVING count(DISTINCT achievement_id) > ?'.
+ ') AS ranking',
+ 'ii',
+ $seminaryId,
+ $count
+ );
+ if(!empty($data)) {
+ return $data[0]['c'] + 1;
+ }
+
+
+ return 1;
+ }
+
+
+ /**
+ * Get all date conditions for an Achievement.
+ *
+ * @param int $achievementId ID of Achievement
+ * @return array Date conditions
+ */
+ public function getAchievementConditionsDate($achievementId)
+ {
+ return $this->db->query(
+ 'SELECT `select` '.
+ 'FROM achievementconditions_date '.
+ 'WHERE achievement_id = ?',
+ 'i',
+ $achievementId
+ );
+ }
+
+
+ /**
+ * Check a date condition.
+ *
+ * @param string $select SELECT-string with date-functions
+ * @return boolean Result
+ */
+ public function checkAchievementConditionDate($select)
+ {
+ $data = $this->db->query(
+ 'SELECT ('.$select.') AS got '
+ );
+ if(!empty($data)) {
+ return ($data[0]['got'] == 1);
+ }
+
+
+ return false;
+ }
+
+
+ /**
+ * Get all Character conditions for an Achievement.
+ *
+ * @param int $achievementId ID of Achievement
+ * @return array Character conditions
+ */
+ public function getAchievementConditionsCharacter($achievementId)
+ {
+ return $this->db->query(
+ 'SELECT field, value '.
+ 'FROM achievementconditions_character '.
+ 'WHERE achievement_id = ?',
+ 'i',
+ $achievementId
+ );
+ }
+
+
+ /**
+ * Check a Character condition.
+ *
+ * @param string $field Field to check
+ * @param int $value The value the field has to match
+ * @param int $characterId ID of Character
+ * @return boolean Result
+ */
+ public function checkAchievementConditionCharacter($field, $value, $characterId)
+ {
+ $data = $this->db->query(
+ "SELECT ($field >= $value) AS got ".
+ 'FROM v_characters '.
+ 'WHERE user_id = ?',
+ 'i',
+ $characterId
+ );
+ if(!empty($data)) {
+ return ($data[0]['got'] == 1);
+ }
+
+
+ return false;
+ }
+
+
+ /**
+ * Get the progress for a Character condition.
+ *
+ * @param string $field Field to check
+ * @param int $value The value the field has to match
+ * @param int $characterId ID of Character
+ * @return float Percentage progress
+ */
+ public function getAchievementConditionCharacterProgress($field, $value, $characterId)
+ {
+ $data = $this->db->query(
+ "SELECT $field AS field ".
+ 'FROM v_characters '.
+ 'WHERE user_id = ?',
+ 'i',
+ $characterId
+ );
+ if(!empty($data)) {
+ return $data[0]['field'] / $value;
+ }
+
+
+ return 0;
+ }
+
+
+ /**
+ * Get all Quest conditions for an Achievement.
+ *
+ * @param int $achievementId ID of Achievement
+ * @return array Quest conditions
+ */
+ public function getAchievementConditionsQuest($achievementId)
+ {
+ return $this->db->query(
+ 'SELECT field, `count`, value, quest_id, status, groupby '.
+ 'FROM achievementconditions_quest '.
+ 'WHERE achievement_id = ?',
+ 'i',
+ $achievementId
+ );
+ }
+
+
+ /**
+ * Check a Quest condition.
+ *
+ * @param string $field Field to check
+ * @param boolean $count Conut field-value
+ * @param int $value The value the field has to match
+ * @param int $status Status of Quest or NULL
+ * @param string $groupby Field to group or NULL
+ * @param int $questId ID of related Quest or NULL
+ * @param int $characterId ID of Character
+ * @return boolean Result
+ */
+ public function checkAchievementConditionQuest($field, $count, $value, $status, $groupby, $questId, $characterId)
+ {
+ $data = $this->db->query(
+ 'SELECT ('.(
+ $count
+ ? "count($field) >= $value"
+ : "$field = $value"
+ ). ') AS got '.
+ 'FROM quests_characters '.
+ 'WHERE '.
+ 'character_id = ?'.
+ (!is_null($questId) ? " AND quest_id = $questId" : '').
+ (!is_null($status) ? " AND status = $status" : '').
+ (!is_null($groupby) ? " GROUP BY $groupby" : ''),
+ 'i',
+ $characterId
+ );
+ if(!empty($data)) {
+ foreach($data as &$datum) {
+ if($datum['got'] == 1) {
+ return true;
+ }
+ }
+ }
+
+
+ return false;
+ }
+
+
+ /**
+ * Get the progress for a Quest condition.
+ *
+ * @param string $field Field to check
+ * @param boolean $count Conut field-value
+ * @param int $value The value the field has to match
+ * @param int $status Status of Quest or NULL
+ * @param string $groupby Field to group or NULL
+ * @param int $questId ID of related Quest or NULL
+ * @param int $characterId ID of Character
+ * @return float Percentage progress
+ */
+ public function getAchievementConditionQuestProgress($field, $count, $value, $status, $groupby, $questId, $characterId)
+ {
+ $data = $this->db->query(
+ 'SELECT '.(
+ $count
+ ? "count($field)"
+ : "$field"
+ ). ' AS field '.
+ 'FROM quests_characters '.
+ 'WHERE '.
+ 'character_id = ?'.
+ (!is_null($questId) ? " AND quest_id = $questId" : '').
+ (!is_null($status) ? " AND status = $status" : '').
+ (!is_null($groupby) ? " GROUP BY $groupby" : ''),
+ 'i',
+ $characterId
+ );
+ if(!empty($data))
+ {
+ $maxField = 0;
+ foreach($data as &$datum) {
+ $maxField = max($maxField, intval($datum['field']));
+ }
+
+ return $maxField / $value;
+ }
+
+
+ return 0;
+ }
+
+
+ /**
+ * Get all Metaachievement conditions for an Achievement.
+ *
+ * @param int $achievementId ID of Achievement
+ * @return array Metaachievement conditions
+ */
+ public function getAchievementConditionsAchievement($achievementId)
+ {
+ return $this->db->query(
+ 'SELECT field, `count`, value, meta_achievement_id, groupby '.
+ 'FROM achievementconditions_achievement '.
+ 'WHERE achievement_id = ?',
+ 'i',
+ $achievementId
+ );
+ }
+
+
+ /**
+ * Check a Metaachievement condition.
+ *
+ * @param string $field Field to check
+ * @param boolean $count Conut field-value
+ * @param int $value The value the field has to match
+ * @param string $groupby Field to group or NULL
+ * @param int $metaAchievementId ID of related Achievement or NULL
+ * @param int $characterId ID of Character
+ * @return boolean Result
+ */
+ public function checkAchievementConditionAchievement($field, $count, $value, $groupby, $metaAchievementId, $characterId)
+ {
+ $data = $this->db->query(
+ 'SELECT ('.(
+ $count
+ ? "count($field) >= $value"
+ : "$field = $value"
+ ). ') AS got '.
+ 'FROM achievements_characters '.
+ 'WHERE '.
+ 'character_id = ?'.
+ (!is_null($metaAchievementId) ? " AND achievement_id = $metaAchievementId" : '').
+ (!is_null($groupby) ? " GROUP BY $groupby" : ''),
+ 'i',
+ $characterId
+ );
+ if(!empty($data)) {
+ foreach($data as &$datum) {
+ if($datum['got'] == 1) {
+ return true;
+ }
+ }
+ }
+
+
+ return false;
+ }
+
+
+ /**
+ * Get the progress for a Metaachievement condition.
+ *
+ * @param string $field Field to check
+ * @param boolean $count Conut field-value
+ * @param int $value The value the field has to match
+ * @param string $groupby Field to group or NULL
+ * @param int $metaAchievementId ID of related Achievement or NULL
+ * @param int $characterId ID of Character
+ * @return float Percentage progress
+ */
+ public function getAchievementConditionAchievementProgress($field, $count, $value, $groupby, $metaAchievementId, $characterId)
+ {
+ $data = $this->db->query(
+ 'SELECT '.(
+ $count
+ ? "count($field)"
+ : "$field"
+ ). ' AS field '.
+ 'FROM achievements_characters '.
+ 'WHERE '.
+ 'character_id = ?'.
+ (!is_null($metaAchievementId) ? " AND achievement_id = $metaAchievementId" : '').
+ (!is_null($groupby) ? " GROUP BY $groupby" : ''),
+ 'i',
+ $characterId
+ );
+ if(!empty($data))
+ {
+ $maxField = 0;
+ foreach($data as &$datum) {
+ $maxField = max($maxField, intval($datum['field']));
+ }
+
+ return $maxField / $value;
+ }
+
+
+ return 0;
+ }
+
+
+ /**
+ * Set an Achievement as achieved for a Character.
+ *
+ * @param int $achievementId ID of Achievement
+ * @param int $characterId ID of Character
+ */
+ public function setAchievementAchieved($achievementId, $characterId)
+ {
+ $this->db->query(
+ 'INSERT INTO achievements_characters '.
+ '(achievement_id, character_id) '.
+ 'VALUES '.
+ '(?, ?)',
+ 'ii',
+ $achievementId, $characterId
+ );
+ }
+
+
+ /**
+ * Check if a Character has achieved an Achievement.
+ *
+ * @param int $achievementId ID of Achievement
+ * @param int $characterId ID of Character
+ * @return boolean Whether Character has achieved the Achievement or not
+ */
+ public function hasCharacterAchievedAchievement($achievementId, $characterId)
+ {
+ $data = $this->db->query(
+ 'SELECT achievement_id, character_id, created '.
+ 'FROM achievements_characters '.
+ 'WHERE achievement_id = ? AND character_id = ?',
+ 'ii',
+ $achievementId, $characterId
+ );
+ if(!empty($data)) {
+ return $data[0];
+ }
+
+
+ return false;
+ }
+
+ }
+
+?>
diff --git a/models/AvatarsModel.inc b/models/AvatarsModel.inc
new file mode 100644
index 00000000..9c3d3701
--- /dev/null
+++ b/models/AvatarsModel.inc
@@ -0,0 +1,92 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model to interact with Avatars-tables.
+ *
+ * @author Oliver Hanraths
+ */
+ class AvatarsModel extends \hhu\z\Model
+ {
+
+
+
+
+ /**
+ * Construct a new AvatarsModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get an Avatar by its ID
+ *
+ * @param int $avatarId ID of Avatar
+ * @return array Avatar data
+ */
+ public function getAvatarById($avatarId)
+ {
+ $data = $this->db->query(
+ 'SELECT id, charactertype_id, xplevel_id, avatarpicture_id, small_avatarpicture_id '.
+ 'FROM avatars '.
+ 'WHERE id = ?',
+ 'i',
+ $avatarId
+ );
+ if(!empty($data)) {
+ return $data[0];
+ }
+
+
+ return null;
+ }
+
+
+ /**
+ * Get an Avatar by its Character type and XP-level.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @param string $charactertypeUrl URL-title of Character type
+ * @param int $xplevel XP-level
+ * @return array Avatar data
+ */
+ public function getAvatarByTypeAndLevel($seminaryId, $charactertypeUrl, $xplevel)
+ {
+ $data = $this->db->query(
+ 'SELECT avatars.id, charactertype_id, xplevel_id, avatarpicture_id, small_avatarpicture_id '.
+ 'FROM avatars '.
+ 'INNER JOIN charactertypes ON charactertypes.id = avatars.charactertype_id '.
+ 'INNER JOIN xplevels ON xplevels.id = avatars.xplevel_id AND xplevels.seminary_id = charactertypes.seminary_id '.
+ 'WHERE charactertypes.seminary_id = ? AND charactertypes.url = ? AND xplevels.level = ?',
+ 'isi',
+ $seminaryId,
+ $charactertypeUrl,
+ $xplevel
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($charactertypeUrl);
+ }
+
+
+ return $data[0];
+ }
+
+ }
+
+?>
diff --git a/models/CharactergroupsModel.inc b/models/CharactergroupsModel.inc
new file mode 100644
index 00000000..3f24a9dc
--- /dev/null
+++ b/models/CharactergroupsModel.inc
@@ -0,0 +1,235 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model of the CharactergroupsAgent to interact with
+ * Charactergroups-table.
+ *
+ * @author Oliver Hanraths
+ */
+ class CharactergroupsModel extends \hhu\z\Model
+ {
+
+
+
+
+ /**
+ * Construct a new CharactergroupsModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get Character groups-groups of a Seminary.
+ *
+ * @param int $seminaryId ID of the corresponding Seminary
+ * @return array Character groups-groups data
+ */
+ public function getGroupsroupsForSeminary($seminaryId)
+ {
+ return $this->db->query(
+ 'SELECT id, name, url, preferred '.
+ 'FROM charactergroupsgroups '.
+ 'WHERE seminary_id = ?',
+ 'i',
+ $seminaryId
+ );
+ }
+
+
+ /**
+ * Get a Character groups-group by its URL.
+ *
+ * @throws IdNotFoundException
+ * @param int $seminaryId ID of the corresponding Seminary
+ * @param string $groupsgroupUrl URL-name of the Character groups-group
+ * @return array Character groups-group data
+ */
+ public function getGroupsgroupByUrl($seminaryId, $groupsgroupUrl)
+ {
+ $data = $this->db->query(
+ 'SELECT id, name, url, preferred '.
+ 'FROM charactergroupsgroups '.
+ 'WHERE seminary_id = ? AND url = ?',
+ 'is',
+ $seminaryId, $groupsgroupUrl
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($groupsgroupUrl);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get a Character groups-group by its ID.
+ *
+ * @throws IdNotFoundException
+ * @param string $groupsgroupId ID of the Character groups-group
+ * @return array Character groups-group data
+ */
+ public function getGroupsgroupById($groupsgroupId)
+ {
+ $data = $this->db->query(
+ 'SELECT id, name, url, preferred '.
+ 'FROM charactergroupsgroups '.
+ 'WHERE id = ?',
+ 'i',
+ $groupsgroupId
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($groupsgroupId);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get Character groups for a Character groups-group.
+ *
+ * @param int $groupsgroupId ID of the Character groups-group
+ * @return array Character groups
+ */
+ public function getGroupsForGroupsgroup($groupsgroupId)
+ {
+ return $this->db->query(
+ 'SELECT id, name, url, xps, motto '.
+ 'FROM v_charactergroups '.
+ 'WHERE charactergroupsgroup_id = ?',
+ 'i',
+ $groupsgroupId
+ );
+ }
+
+
+ /**
+ * Get Character groups for a Character.
+ *
+ * @param int $characterId ID of the Character
+ * @return array Character groups
+ */
+ public function getGroupsForCharacter($characterId)
+ {
+ return $this->db->query(
+ 'SELECT charactergroups.id, charactergroups.charactergroupsgroup_id, charactergroups.name, charactergroups.url, charactergroups.xps, charactergroupsgroups.id AS charactergroupsgroup_id, charactergroupsgroups.name AS charactergroupsgroup_name, charactergroupsgroups.url AS charactergroupsgroup_url '.
+ 'FROM characters_charactergroups '.
+ 'LEFT JOIN v_charactergroups AS charactergroups ON charactergroups.id = characters_charactergroups.charactergroup_id '.
+ 'LEFT JOIN charactergroupsgroups ON charactergroupsgroups.id = charactergroups.charactergroupsgroup_id '.
+ 'WHERE characters_charactergroups.character_id = ?',
+ 'i',
+ $characterId
+ );
+ }
+
+
+ /**
+ * Get a Character group by its URL.
+ *
+ * @throws IdNotFoundException
+ * @param int $groupsgroupId ID of the Character groups-group
+ * @param string $groupUrl URL-name of the Character group
+ * @return array Character group data
+ */
+ public function getGroupByUrl($groupsgroupId, $groupUrl)
+ {
+ $data = $this->db->query(
+ 'SELECT id, name, url, xps, motto '.
+ 'FROM v_charactergroups '.
+ 'WHERE charactergroupsgroup_id = ? AND url = ?',
+ 'is',
+ $groupsgroupId, $groupUrl
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($groupUrl);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get the rank of a XP-value of a Character.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @param int $xps XP-value to get rank for
+ * @return int Rank of XP-value
+ */
+ public function getXPRank($groupsgroupId, $xps)
+ {
+ $data = $this->db->query(
+ 'SELECT count(id) AS c '.
+ 'FROM v_charactergroups '.
+ 'WHERE charactergroupsgroup_id = ? AND xps > ?',
+ 'id',
+ $groupsgroupId, $xps
+ );
+ if(!empty($data)) {
+ return $data[0]['c'] + 1;
+ }
+
+
+ return 1;
+ }
+
+
+ /**
+ * Add a Character to a Character group.
+ *
+ * @param int $groupId ID of Character group
+ * @param int $characterId ID of Character to add
+ */
+ public function addCharacterToCharactergroup($groupId, $characterId)
+ {
+ $this->db->query(
+ 'INSERT INTO characters_charactergroups '.
+ '(character_id, charactergroup_id) '.
+ 'VALUES '.
+ '(?, ?)',
+ 'ii',
+ $characterId,
+ $groupId
+ );
+ }
+
+
+ /**
+ * Remove a Character from a Character group.
+ *
+ * @param int $groupId ID of Character group
+ * @param int $characterId ID of Character to remove
+ */
+ public function removeCharacterFromCharactergroup($groupId, $characterId)
+ {
+ $this->db->query(
+ 'DELETE FROM characters_charactergroups '.
+ 'WHERE charactergroup_id = ? AND character_id = ?',
+ 'ii',
+ $groupId,
+ $characterId
+ );
+ }
+
+ }
+
+?>
diff --git a/models/CharactergroupsquestsModel.inc b/models/CharactergroupsquestsModel.inc
new file mode 100644
index 00000000..4490168d
--- /dev/null
+++ b/models/CharactergroupsquestsModel.inc
@@ -0,0 +1,136 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model of the CharactergroupsquestsAgent to interact with
+ * Charactergroupsquests-table.
+ *
+ * @author Oliver Hanraths
+ */
+ class CharactergroupsquestsModel extends \hhu\z\Model
+ {
+
+
+
+
+ /**
+ * Construct a new CharactergroupsquestsModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get Character groups Quests of a Character groups-groups.
+ *
+ * @param int $groupsgroupId ID of the Character groups-group
+ * @return array Character groups Quest data
+ */
+ public function getQuestsForCharactergroupsgroup($groupsgroupId)
+ {
+ return $this->db->query(
+ 'SELECT id, questgroups_id, title, url, xps '.
+ 'FROM charactergroupsquests '.
+ 'WHERE charactergroupsgroup_id = ?',
+ 'i',
+ $groupsgroupId
+ );
+ }
+
+
+ /**
+ * Get a Character groups Quest by its URL.
+ *
+ * @throws IdNotFoundException
+ * @param int $groupsgroupId ID of the Character groups-group
+ * @param string $questUrl URL-title of the Character groups Quest
+ * @return array Character groups Quest data
+ */
+ public function getQuestByUrl($groupsgroupId, $questUrl)
+ {
+ $data = $this->db->query(
+ 'SELECT id, questgroups_id, title, url, description, xps, rules, won_text, lost_text, questsmedia_id '.
+ 'FROM charactergroupsquests '.
+ 'WHERE charactergroupsgroup_id = ? AND url = ?',
+ 'is',
+ $groupsgroupId,
+ $questUrl
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($questUrl);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get the Character groups for a Quest.
+ *
+ * @param int $questId ID of the Character groups Quest
+ * @return array Character groups
+ */
+ public function getGroupsForQuest($questId)
+ {
+ $groups = $this->db->query(
+ 'SELECT charactergroups.id, charactergroups.name, charactergroups.url, charactergroupsquests_groups.created, charactergroupsquests_groups.xps_factor, charactergroupsquests.xps '.
+ 'FROM charactergroupsquests_groups '.
+ 'LEFT JOIN charactergroups ON charactergroups.id = charactergroupsquests_groups.charactergroup_id '.
+ 'LEFT JOIN charactergroupsquests ON charactergroupsquests.id = charactergroupsquests_groups.charactergroupsquest_id '.
+ 'WHERE charactergroupsquests_groups.charactergroupsquest_id = ?',
+ 'i',
+ $questId
+ );
+ foreach($groups as &$group) {
+ $group['xps'] = round($group['xps'] * $group['xps_factor'], 1);
+ }
+
+
+ return $groups;
+ }
+
+
+ /**
+ * Get Character groups Quests for a Character group.
+ *
+ * @param int $groupId ID of the Character group
+ * @return array Character groups Quests
+ */
+ public function getQuestsForGroup($groupId)
+ {
+ $quests = $this->db->query(
+ 'SELECT charactergroupsquests.id, charactergroupsquests_groups.created, charactergroupsquests.title, charactergroupsquests.url, charactergroupsquests.xps, charactergroupsquests_groups.xps_factor '.
+ 'FROM charactergroupsquests_groups '.
+ 'LEFT JOIN charactergroupsquests ON charactergroupsquests.id = charactergroupsquests_groups.charactergroupsquest_id '.
+ 'WHERE charactergroupsquests_groups.charactergroup_id = ?',
+ 'i',
+ $groupId
+ );
+ foreach($quests as &$quest) {
+ $quest['group_xps'] = round($quest['xps'] * $quest['xps_factor'], 1);
+ }
+
+
+ return $quests;
+ }
+
+
+ }
+
+?>
diff --git a/models/CharacterrolesModel.inc b/models/CharacterrolesModel.inc
new file mode 100644
index 00000000..752d4a0a
--- /dev/null
+++ b/models/CharacterrolesModel.inc
@@ -0,0 +1,100 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model to interact with characterroles-table.
+ *
+ * @author Oliver Hanraths
+ */
+ class CharacterrolesModel extends \hhu\z\Model
+ {
+
+
+
+
+ /**
+ * Construct a new CharacterrolesModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get all characterroles for a Character referenced by its ID.
+ *
+ * @param int $userId ID of an user
+ * @return array Characterroles for a Character
+ */
+ public function getCharacterrolesForCharacterById($characterId)
+ {
+ return $this->db->query(
+ 'SELECT characterroles.id, characterroles.created, characterroles.name '.
+ 'FROM characters_characterroles '.
+ 'LEFT JOIN characterroles ON characterroles.id = characters_characterroles.characterrole_id '.
+ 'WHERE characters_characterroles.character_id = ?',
+ 'i',
+ $characterId
+ );
+ }
+
+
+ /**
+ * Add a role to a Character.
+ *
+ * @param int $characterId ID of Character to add role to
+ * @param string $characterrole Role to add
+ */
+ public function addCharacterroleToCharacter($characterId, $characterrole)
+ {
+ $this->db->query(
+ 'INSERT IGNORE INTO characters_characterroles '.
+ '(character_id, characterrole_id) '.
+ 'SELECT ?, id '.
+ 'FROM characterroles '.
+ 'WHERE name = ?',
+ 'is',
+ $characterId,
+ $characterrole
+ );
+ }
+
+
+ /**
+ * Remove a role from a Character.
+ *
+ * @param int $characterId ID of Character to remove role from
+ * @param string $characterrole Role to remove
+ */
+ public function removeCharacterroleFromCharacter($characterId, $characterrole)
+ {
+ $this->db->query(
+ 'DELETE FROM characters_characterroles '.
+ 'WHERE character_id = ? AND characterrole_id = ('.
+ 'SELECT id '.
+ 'FROM characterroles '.
+ 'WHERE name = ?'.
+ ')',
+ 'is',
+ $characterId,
+ $characterrole
+ );
+ }
+
+ }
+
+?>
diff --git a/models/CharactersModel.inc b/models/CharactersModel.inc
new file mode 100644
index 00000000..eb7c1531
--- /dev/null
+++ b/models/CharactersModel.inc
@@ -0,0 +1,519 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model to interact with Characters-table.
+ *
+ * @author Oliver Hanraths
+ */
+ class CharactersModel extends \hhu\z\Model
+ {
+
+
+
+
+ /**
+ * Construct a new CharactersModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get all characters for an user.
+ *
+ * @param int $userId ID of the user
+ * @return array Characters
+ */
+ public function getCharactersForUser($userId)
+ {
+ return $this->db->query(
+ 'SELECT characters.id, characters.created, characters.charactertype_id, characters.name, characters.url, characters.xps, characters.xplevel, characters.avatar_id, charactertypes.name AS charactertype_name, charactertypes.url AS charactertype_url, seminaries.id AS seminary_id, seminaries.url AS seminary_url, seminaries.title AS seminary_title, seminaries.url AS seminary_url '.
+ 'FROM v_characters AS characters '.
+ 'LEFT JOIN charactertypes ON charactertypes.id = characters.charactertype_id '.
+ 'LEFT JOIN seminaries ON seminaries.id = charactertypes.seminary_id '.
+ 'WHERE user_id = ?',
+ 'i',
+ $userId
+ );
+ }
+
+
+ /**
+ * Get Characters for a Seminary.
+ *
+ * @param int $seminaryId ID of the Seminary
+ * @return array Characters
+ */
+ public function getCharactersForSeminary($seminaryId)
+ {
+ return $this->db->query(
+ 'SELECT characters.id, characters.created, characters.charactertype_id, characters.name, characters.url, characters.user_id, characters.xps, characters.xplevel, characters.avatar_id, charactertypes.name AS charactertype_name, charactertypes.url AS charactertype_url, seminaries.id AS seminary_url, seminaries.title AS seminary_title, seminaries.url AS seminary_url '.
+ 'FROM v_characters AS characters '.
+ 'LEFT JOIN charactertypes ON charactertypes.id = characters.charactertype_id '.
+ 'LEFT JOIN seminaries ON seminaries.id = charactertypes.seminary_id '.
+ 'WHERE seminaries.id = ?',
+ 'i',
+ $seminaryId
+ );
+ }
+
+
+ /**
+ * Get Characters for a Character group.
+ *
+ * @param int $groupId ID of the Character group
+ * @return array Characters
+ */
+ public function getCharactersForGroup($groupId)
+ {
+ return $this->db->query(
+ 'SELECT characters.id, characters.created, characters.charactertype_id, characters.name, characters.url, characters.user_id, characters.xps, characters.xplevel, characters.avatar_id, charactertypes.name AS charactertype_name, charactertypes.url AS charactertype_url '.
+ 'FROM v_characters AS characters '.
+ 'LEFT JOIN characters_charactergroups ON characters_charactergroups.character_id = characters.id '.
+ 'LEFT JOIN charactertypes ON charactertypes.id = characters.charactertype_id '.
+ 'WHERE characters_charactergroups.charactergroup_id = ?',
+ 'i',
+ $groupId
+ );
+ }
+
+
+ /**
+ * Get the character of a user for a Seminary.
+ *
+ * @throws IdNotFoundException
+ * @param int $userId ID of the user
+ * @param int $seminaryId ID of the Seminary
+ * @return array Character data
+ */
+ public function getCharacterForUserAndSeminary($userId, $seminaryId)
+ {
+ $data = $this->db->query(
+ 'SELECT characters.id, characters.created, characters.charactertype_id, characters.name, characters.url, characters.user_id, characters.xps, characters.xplevel, characters.avatar_id, charactertypes.name AS charactertype_name, charactertypes.url AS charactertype_url '.
+ 'FROM v_characters AS characters '.
+ 'LEFT JOIN charactertypes ON charactertypes.id = characters.charactertype_id '.
+ 'WHERE characters.user_id = ? AND charactertypes.seminary_id = ?',
+ 'ii',
+ $userId, $seminaryId
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($userId);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get a Character by its Url.
+ *
+ * @throws IdNotFoundException
+ * @param int $seminaryId ID of the Seminary
+ * @param string $characterUrl URL-name of the Character
+ * @return array Character data
+ */
+ public function getCharacterByUrl($seminaryId, $characterUrl)
+ {
+ $data = $this->db->query(
+ 'SELECT characters.id, characters.created, characters.charactertype_id, characters.name, characters.url, characters.user_id, characters.xps, characters.xplevel, characters.avatar_id, charactertypes.name AS charactertype_name, charactertypes.url AS charactertype_url '.
+ 'FROM v_characters AS characters '.
+ 'LEFT JOIN charactertypes ON charactertypes.id = characters.charactertype_id '.
+ 'WHERE charactertypes.seminary_id = ? AND characters.url = ?',
+ 'is',
+ $seminaryId, $characterUrl
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($characterUrl);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get a Character by its Id.
+ *
+ * @throws IdNotFoundException
+ * @param string $characterId ID of the Character
+ * @return array Character data
+ */
+ public function getCharacterById($characterId)
+ {
+ $data = $this->db->query(
+ 'SELECT characters.id, characters.created, characters.charactertype_id, characters.name, characters.url, characters.user_id, characters.xps, characters.xplevel, characters.avatar_id, charactertypes.name AS charactertype_name, charactertypes.url AS charactertype_url '.
+ 'FROM v_characters AS characters '.
+ 'LEFT JOIN charactertypes ON charactertypes.id = characters.charactertype_id '.
+ 'WHERE characters.id = ?',
+ 'i',
+ $characterId
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($characterUrl);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get Characters with the most amount of Achievements.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @param int $conut Amount of Characters to retrieve
+ * @return array List of Characters
+ */
+ public function getCharactersWithMostAchievements($seminaryId, $count, $alsoWithDeadline=true)
+ {
+ return $this->db->query(
+ 'SELECT characters.id, characters.created, characters.charactertype_id, characters.name, characters.url, characters.user_id, characters.xps, characters.xplevel, characters.avatar_id, charactertypes.name AS charactertype_name, charactertypes.url AS charactertype_url, count(DISTINCT achievement_id) AS c '.
+ 'FROM achievements_characters '.
+ 'LEFT JOIN achievements ON achievements.id = achievements_characters.achievement_id '.
+ 'LEFT JOIN v_characters AS characters ON characters.id = achievements_characters.character_id '.
+ 'LEFT JOIN charactertypes ON charactertypes.id = characters.charactertype_id '.
+ 'WHERE achievements.seminary_id = ? AND deadline IS NULL '.
+ (!$alsoWithDeadline ? 'AND achievements.deadline IS NULL ' : null).
+ 'GROUP BY character_id '.
+ 'ORDER BY count(DISTINCT achievement_id) DESC '.
+ 'LIMIT ?',
+ 'ii',
+ $seminaryId,
+ $count
+ );
+ }
+
+
+ /**
+ * Calculate only XPs for a Character achieved through Quests.
+ *
+ * @param int $characterId ID of Character
+ * @return int Quest-XPs for Character
+ */
+ public function getQuestXPsOfCharacter($characterId)
+ {
+ $data = $this->db->query(
+ 'SELECT quest_xps '.
+ 'FROM v_charactersxps '.
+ 'WHERE character_id = ?',
+ 'i',
+ $characterId
+ );
+ if(!empty($data)) {
+ return $data[0]['quest_xps'];
+ }
+
+
+ return 0;
+ }
+
+
+ /**
+ * Get the XP-level of a Character.
+ *
+ * @param string $characterId ID of the Character
+ * @return array XP-level of Character
+ */
+ public function getXPLevelOfCharacters($characterId)
+ {
+ $data = $this->db->query(
+ 'SELECT xplevels.xps, xplevels.level, xplevels.name '.
+ 'FROM v_charactersxplevels '.
+ 'INNER JOIN xplevels ON xplevels.id = v_charactersxplevels.xplevel_id '.
+ 'WHERE v_charactersxplevels.character_id = ?',
+ 'i',
+ $characterId
+ );
+ if(!empty($data)) {
+ return $data[0];
+ }
+
+
+ return null;
+ }
+
+
+ /**
+ * Get the rank of a XP-value of a Character.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @param int $xps XP-value to get rank for
+ * @return int Rank of XP-value
+ */
+ public function getXPRank($seminaryId, $xps)
+ {
+ $data = $this->db->query(
+ 'SELECT count(characters.id) AS c '.
+ 'FROM charactertypes '.
+ 'INNER JOIN v_characters AS characters ON characters.charactertype_id = charactertypes.id '.
+ 'INNER JOIN characters_characterroles ON characters_characterroles.character_id = characters.id '.
+ 'INNER JOIN characterroles ON characterroles.id = characters_characterroles.characterrole_id AND characterroles.name = ? '.
+ 'WHERE seminary_id = ? AND characters.xps > ?',
+ 'sid',
+ 'user',
+ $seminaryId, $xps
+ );
+ if(!empty($data)) {
+ return $data[0]['c'] + 1;
+ }
+
+
+ return 1;
+ }
+
+
+ /**
+ * Get the superior $count Characters in the ranking.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @param int $xps XP-value of Character
+ * @param int $count Count of Characters to determine
+ * @return array List of superior Characters
+ */
+ public function getSuperiorCharacters($seminaryId, $xps, $count)
+ {
+ return $this->db->query(
+ 'SELECT characters.id, characters.created, characters.charactertype_id, characters.name, characters.url, characters.xps, characters.xplevel, characters.avatar_id, charactertypes.name AS charactertype_name, charactertypes.url AS charactertype_url '.
+ 'FROM v_characters AS characters '.
+ 'INNER JOIN charactertypes ON charactertypes.id = characters.charactertype_id '.
+ 'INNER JOIN characters_characterroles ON characters_characterroles.character_id = characters.id '.
+ 'INNER JOIN characterroles ON characterroles.id = characters_characterroles.characterrole_id AND characterroles.name = ? '.
+ 'WHERE charactertypes.seminary_id = ? AND characters.xps > ? '.
+ 'ORDER BY characters.xps ASC '.
+ 'LIMIT ?',
+ 'sidd',
+ 'user',
+ $seminaryId, $xps, $count
+ );
+ }
+
+
+ /**
+ * Get the inferior $count Characters in the ranking.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @param int $xps XP-value of Character
+ * @param int $count Count of Characters to determine
+ * @return array List of inferior Characters
+ */
+ public function getInferiorCharacters($seminaryId, $xps, $count)
+ {
+ return $this->db->query(
+ 'SELECT characters.id, characters.created, characters.charactertype_id, characters.name, characters.url, characters.xps, characters.xplevel, characters.avatar_id, charactertypes.name AS charactertype_name, charactertypes.url AS charactertype_url '.
+ 'FROM v_characters AS characters '.
+ 'INNER JOIN charactertypes ON charactertypes.id = characters.charactertype_id '.
+ 'INNER JOIN characters_characterroles ON characters_characterroles.character_id = characters.id '.
+ 'INNER JOIN characterroles ON characterroles.id = characters_characterroles.characterrole_id AND characterroles.name = ? '.
+ 'WHERE charactertypes.seminary_id = ? AND characters.xps < ? '.
+ 'ORDER BY characters.xps DESC '.
+ 'LIMIT ?',
+ 'sidd',
+ 'user',
+ $seminaryId, $xps, $count
+ );
+ }
+
+
+ /**
+ * Get Characters that solved a Quest.
+ *
+ * @param int $questId ID of Quest to get Characters for
+ * @return array Characters data
+ */
+ public function getCharactersSolvedQuest($questId)
+ {
+ return $data = $this->db->query(
+ 'SELECT characters.id, characters.created, characters.charactertype_id, characters.name, characters.url, characters.user_id, characters.xps, characters.xplevel, charactertypes.name AS charactertype_name, charactertypes.url AS charactertype_url '.
+ 'FROM v_characters AS characters '.
+ 'LEFT JOIN charactertypes ON charactertypes.id = characters.charactertype_id '.
+ 'WHERE EXISTS ('.
+ 'SELECT character_id FROM quests_characters WHERE character_id = characters.id AND quest_id = ? AND status = ?'.
+ ')',
+ 'ii',
+ $questId,
+ QuestsModel::QUEST_STATUS_SOLVED
+ );
+ }
+
+
+ /**
+ * Get Characters that did not solv a Quest.
+ *
+ * @param int $questId ID of Quest to get Characters for
+ * @return array Characters data
+ */
+ public function getCharactersUnsolvedQuest($questId)
+ {
+ return $data = $this->db->query(
+ 'SELECT characters.id, characters.created, characters.charactertype_id, characters.name, characters.url, characters.user_id, characters.xps, characters.xplevel, charactertypes.name AS charactertype_name, charactertypes.url AS charactertype_url '.
+ 'FROM v_characters AS characters '.
+ 'LEFT JOIN charactertypes ON charactertypes.id = characters.charactertype_id '.
+ 'WHERE EXISTS ('.
+ 'SELECT character_id FROM quests_characters WHERE character_id = characters.id AND quest_id = ? AND status = ?'.
+ ') AND NOT EXISTS ('.
+ 'SELECT character_id FROM quests_characters WHERE character_id = characters.id AND quest_id = ? AND status = ?'.
+ ')',
+ 'iiii',
+ $questId, QuestsModel::QUEST_STATUS_UNSOLVED,
+ $questId, QuestsModel::QUEST_STATUS_SOLVED
+ );
+ }
+
+
+ /**
+ * Get Characters that sent a submission for a Quest.
+ *
+ * @param int $questId ID of Quest to get Characters for
+ * @return array Characters data
+ */
+ public function getCharactersSubmittedQuest($questId)
+ {
+ return $data = $this->db->query(
+ 'SELECT characters.id, characters.created, characters.charactertype_id, characters.name, characters.url, characters.user_id, characters.xps, characters.xplevel, charactertypes.name AS charactertype_name, charactertypes.url AS charactertype_url '.
+ 'FROM v_characters AS characters '.
+ 'LEFT JOIN charactertypes ON charactertypes.id = characters.charactertype_id '.
+ 'WHERE ('.
+ 'SELECT status '.
+ 'FROM quests_characters '.
+ 'WHERE quest_id = ? AND character_id = characters.id '.
+ 'ORDER BY created DESC '.
+ 'LIMIT 1'.
+ ') = ?',
+ 'ii',
+ $questId, QuestsModel::QUEST_STATUS_SUBMITTED
+ );
+ }
+
+
+ public function getXPLevelsForSeminary($seminaryId)
+ {
+ return $this->db->query(
+ 'SELECT id, xps, level, name '.
+ 'FROM xplevels '.
+ 'WHERE seminary_id = ? '.
+ 'ORDER BY level ASC',
+ 'i',
+ $seminaryId
+ );
+ }
+
+
+ /**
+ * Check if a Character name already exists.
+ *
+ * @param string $name Character name to check
+ * @return boolean Whether Character name exists or not
+ */
+ public function characterNameExists($name)
+ {
+ $data = $this->db->query(
+ 'SELECT count(id) AS c '.
+ 'FROM characters '.
+ 'WHERE name = ? OR url = ?',
+ 'ss',
+ $name,
+ \nre\core\Linker::createLinkParam($name)
+ );
+ if(!empty($data)) {
+ return ($data[0]['c'] > 0);
+ }
+
+
+ return false;
+ }
+
+
+ /**
+ * Create a new Character.
+ *
+ * @param int $userId User-ID that creates the new character
+ * @param int $charactertypeId ID of type of new Character
+ * @param string $characterName Name for the new Character
+ * @return int ID of Character
+ */
+ public function createCharacter($userId, $charactertypeId, $characterName)
+ {
+ $this->db->query(
+ 'INSERT INTO characters '.
+ '(user_id, charactertype_id, name, url) '.
+ 'VALUES '.
+ '(?, ?, ?, ?)',
+ 'iiss',
+ $userId,
+ $charactertypeId,
+ $characterName,
+ \nre\core\Linker::createLinkParam($characterName)
+ );
+
+
+ return $this->db->getInsertId();
+ }
+
+
+ /**
+ * Set the value of a Seminary field for a Character.
+ *
+ * @param int $characterId ID of Character
+ * @param int $seminarycharacterfieldId ID of seminarycharacterfield to set value of
+ * @param string $value Value to set
+ */
+ public function setSeminaryFieldOfCharacter($characterId, $seminarycharacterfieldId, $value)
+ {
+ $this->db->query(
+ 'INSERT INTO characters_seminarycharacterfields '.
+ '(character_id, seminarycharacterfield_id, value) '.
+ 'VALUES '.
+ '(?, ?, ?) '.
+ 'ON DUPLICATE KEY UPDATE '.
+ 'value = ?',
+ 'iiss',
+ $characterId,
+ $seminarycharacterfieldId,
+ $value,
+ $value
+ );
+ }
+
+
+ /**
+ * Get Characters with the given Character role.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @param string $characterrole Character role
+ * @return array List of users
+ */
+ public function getCharactersWithCharacterRole($seminaryId, $characterrole)
+ {
+ return $this->db->query(
+ 'SELECT characters.id, characters.created, characters.charactertype_id, characters.name, characters.url, characters.user_id, characters.xps, characters.xplevel, characters.avatar_id, charactertypes.name AS charactertype_name, charactertypes.url AS charactertype_url, seminaries.id AS seminary_url, seminaries.title AS seminary_title, seminaries.url AS seminary_url '.
+ 'FROM v_characters AS characters '.
+ 'LEFT JOIN charactertypes ON charactertypes.id = characters.charactertype_id '.
+ 'LEFT JOIN seminaries ON seminaries.id = charactertypes.seminary_id '.
+ 'LEFT JOIN characters_characterroles ON characters_characterroles.character_id = characters.id '.
+ 'LEFT JOIN characterroles ON characterroles.id = characters_characterroles.characterrole_id '.
+ 'WHERE seminaries.id = ? AND characterroles.name = ?',
+ 'is',
+ $seminaryId, $characterrole
+ );
+ }
+
+ }
+
+?>
diff --git a/models/CharactertypesModel.inc b/models/CharactertypesModel.inc
new file mode 100644
index 00000000..07c8d5e9
--- /dev/null
+++ b/models/CharactertypesModel.inc
@@ -0,0 +1,57 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model to interact with Charactertypes-table.
+ *
+ * @author Oliver Hanraths
+ */
+ class CharactertypesModel extends \hhu\z\Model
+ {
+
+
+
+
+ /**
+ * Construct a new CharactertypesModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get all Character types of a Seminary.
+ *
+ * @param int $seminaryId ID of Seminary to get types of
+ * @return array Character types
+ */
+ public function getCharacterTypesForSeminary($seminaryId)
+ {
+ return $this->db->query(
+ 'SELECT id, name, url '.
+ 'FROM charactertypes '.
+ 'WHERE seminary_id = ? '.
+ 'ORDER BY name ASC',
+ 'i',
+ $seminaryId
+ );
+ }
+
+ }
+
+?>
diff --git a/models/DatabaseModel.inc b/models/DatabaseModel.inc
new file mode 100644
index 00000000..51259f4f
--- /dev/null
+++ b/models/DatabaseModel.inc
@@ -0,0 +1,83 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\models;
+
+
+ /**
+ * Default implementation of a database model.
+ *
+ * @author coderkun
+ */
+ class DatabaseModel extends \nre\core\Model
+ {
+ /**
+ * Database connection
+ *
+ * @static
+ * @var DatabaseDriver
+ */
+ protected $db = NULL;
+
+
+
+
+ /**
+ * Construct a new datamase model.
+ *
+ * @throws DatamodelException
+ * @throws DriverNotFoundException
+ * @throws DriverNotValidException
+ * @param string $type Database type
+ * @param array $config Connection settings
+ */
+ function __construct($type, $config)
+ {
+ parent::__construct();
+
+ // Load database driver
+ $this->loadDriver($type);
+
+ // Establish database connection
+ $this->connect($type, $config);
+ }
+
+
+
+
+ /**
+ * Load the database driver.
+ *
+ * @throws DriverNotFoundException
+ * @throws DriverNotValidException
+ * @param string $driverName Name of the database driver
+ */
+ private function loadDriver($driverName)
+ {
+ \nre\core\Driver::load($driverName);
+ }
+
+
+ /**
+ * Establish a connection to the database.
+ *
+ * @throws DatamodelException
+ * @param string $driverName Name of the database driver
+ * @param array $config Connection settings
+ */
+ private function connect($driverName, $config)
+ {
+ $this->db = \nre\core\Driver::factory($driverName, $config);
+ }
+
+ }
+
+?>
diff --git a/models/MediaModel.inc b/models/MediaModel.inc
new file mode 100644
index 00000000..031df753
--- /dev/null
+++ b/models/MediaModel.inc
@@ -0,0 +1,195 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model to interact with the Media-tables.
+ *
+ * @author Oliver Hanraths
+ */
+ class MediaModel extends \hhu\z\Model
+ {
+
+
+
+
+ /**
+ * Construct a new MediaModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get a medium by its URL.
+ *
+ * @throws IdNotFoundException
+ * @param string $mediaURL URL-name of the Medium
+ * @return array Medium data
+ */
+ public function getMediaByUrl($mediaUrl)
+ {
+ $data = $this->db->query(
+ 'SELECT id, name, url, description, mimetype '.
+ 'FROM media '.
+ 'WHERE url = ?',
+ 's',
+ $mediaUrl
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($mediaUrl);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get a medium by its ID.
+ *
+ * @throws IdNotFoundException
+ * @param int $mediaId ID of the Medium
+ * @return array Medium data
+ */
+ public function getMediaById($mediaId)
+ {
+ $data = $this->db->query(
+ 'SELECT id, name, url, description, mimetype '.
+ 'FROM media '.
+ 'WHERE id = ?',
+ 'i',
+ $mediaId
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($mediaId);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get a Seminary medium by its URL.
+ *
+ * @throws IdNotFoundException
+ * @param int $seminaryId ID of the seminary
+ * @param string $seminaryMediaUrl URL-name of the Seminary medium
+ * @return array Seminary medium data
+ */
+ public function getSeminaryMediaByUrl($seminaryId, $seminaryMediaUrl)
+ {
+ $data = $this->db->query(
+ 'SELECT id, name, url, description, mimetype '.
+ 'FROM seminarymedia '.
+ 'WHERE url = ?',
+ 's',
+ $seminaryMediaUrl
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($seminaryMediaUrl);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get a Seminary medium by its ID.
+ *
+ * @throws IdNotFoundException
+ * @param int $seminaryMediaId ID of the Seminary medium
+ * @return array Seminary medium data
+ */
+ public function getSeminaryMediaById($mediaId)
+ {
+ $data = $this->db->query(
+ 'SELECT id, name, url, description, mimetype '.
+ 'FROM seminarymedia '.
+ 'WHERE id = ?',
+ 'i',
+ $mediaId
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($mediaId);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Create a new Questsmedia by creating a new Seminarymedia and
+ * adding it to the list of Questsmedia.
+ * TODO Currently only temporary for easier data import.
+ */
+ public function createQuestMedia($userId, $seminaryId, $filename, $description, $mimetype, $tmpFilename)
+ {
+ $uploadId = false;
+ $this->db->setAutocommit(false);
+
+ try {
+ // Create database record
+ $this->db->query(
+ 'INSERT INTO seminarymedia '.
+ '(created_user_id, seminary_id, name, url, description, mimetype) '.
+ 'VALUES '.
+ '(?, ? ,? ,?, ?, ?)',
+ 'iissss',
+ $userId,
+ $seminaryId,
+ $filename,
+ \nre\core\Linker::createLinkParam($filename),
+ $description,
+ $mimetype
+ );
+ $uploadId = $this->db->getInsertId();
+
+ $this->db->query(
+ 'INSERT INTO questsmedia '.
+ '(media_id, created_user_id) '.
+ 'VALUES '.
+ '(?, ?)',
+ 'ii',
+ $uploadId,
+ $userId
+ );
+
+ // Create filename
+ $filename = ROOT.DS.'seminarymedia'.DS.$uploadId;
+ if(!move_uploaded_file($tmpFilename, $filename))
+ {
+ $this->db->rollback();
+ $uploadId = false;
+ }
+ }
+ catch(\nre\exceptions\DatamodelException $e) {
+ $this->db->rollback();
+ $this->db->setAutocommit(true);
+ }
+
+
+ $this->db->setAutocommit(true);
+ return $uploadId;
+ }
+
+ }
+
+?>
diff --git a/models/QuestgroupsModel.inc b/models/QuestgroupsModel.inc
new file mode 100644
index 00000000..f3cd0949
--- /dev/null
+++ b/models/QuestgroupsModel.inc
@@ -0,0 +1,662 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model to interact with Questgroups-table.
+ *
+ * @author Oliver Hanraths
+ */
+ class QuestgroupsModel extends \hhu\z\Model
+ {
+ /**
+ * Required models
+ *
+ * @var array
+ */
+ public $models = array('questgroupshierarchy', 'quests', 'questtexts');
+
+
+
+
+ /**
+ * Construct a new QuestgroupsModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get all Questgroups for a Questgroup hierarchy.
+ *
+ * @param int $hierarchyId ID of the Questgroup hierarchy to get Questgroups for
+ * @param int $parentQuestgroupId ID of the parent Questgroup hierarchy
+ * @return array Questgroups for the given hierarchy
+ */
+ public function getQuestgroupsForHierarchy($hierarchyId, $parentQuestgroupId=null)
+ {
+ // Get Questgroups
+ $questgroups = array();
+ if(is_null($parentQuestgroupId))
+ {
+ $questgroups = $this->db->query(
+ 'SELECT questgroups.id, questgroups_questgroupshierarchy.questgroupshierarchy_id, questgroups_questgroupshierarchy.pos, questgroups.title, questgroups.url, questgroups.questgroupspicture_id '.
+ 'FROM questgroups_questgroupshierarchy '.
+ 'INNER JOIN questgroups ON questgroups.id = questgroups_questgroupshierarchy.questgroup_id '.
+ 'WHERE questgroups_questgroupshierarchy.questgroupshierarchy_id = ? AND questgroups_questgroupshierarchy.parent_questgroup_id IS NULL '.
+ 'ORDER BY questgroups_questgroupshierarchy.pos ASC',
+ 'i',
+ $hierarchyId
+ );
+ }
+ else
+ {
+ $questgroups = $this->db->query(
+ 'SELECT questgroups.id, questgroups_questgroupshierarchy.questgroupshierarchy_id, questgroups_questgroupshierarchy.pos, questgroups.title, questgroups.url, questgroups.questgroupspicture_id '.
+ 'FROM questgroups_questgroupshierarchy '.
+ 'INNER JOIN questgroups ON questgroups.id = questgroups_questgroupshierarchy.questgroup_id '.
+ 'WHERE questgroups_questgroupshierarchy.questgroupshierarchy_id = ? AND questgroups_questgroupshierarchy.parent_questgroup_id = ? '.
+ 'ORDER BY questgroups_questgroupshierarchy.pos ASC',
+ 'ii',
+ $hierarchyId, $parentQuestgroupId
+ );
+ }
+
+
+ // Return Questgroups
+ return $questgroups;
+ }
+
+
+ /**
+ * Get all Questgroups for a Seminary.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @return array List of Questgroups
+ */
+ public function getQuestgroupsForSeminary($seminaryId)
+ {
+ return $this->db->query(
+ 'SELECT id, title, url '.
+ 'FROM questgroups '.
+ 'WHERE seminary_id = ? '.
+ 'ORDER BY title ASC',
+ 'i',
+ $seminaryId
+ );
+ }
+
+
+ /**
+ * Get a Questgroup by its ID.
+ *
+ * @throws IdNotFoundException
+ * @param int $questgroupId ID of a Questgroup
+ * @return array Questgroup data
+ */
+ public function getQuestgroupById($questgroupId)
+ {
+ $data = $this->db->query(
+ 'SELECT id, title, url, questgroupspicture_id '.
+ 'FROM questgroups '.
+ 'WHERE questgroups.id = ?',
+ 'i',
+ $questgroupId
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($questgroupId);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get a Questgroup by its URL.
+ *
+ * @throws IdNotFoundException
+ * @param int $seminaryId ID of the corresponding seminary
+ * @param string $questgroupURL URL-title of a Questgroup
+ * @return array Questgroup data
+ */
+ public function getQuestgroupByUrl($seminaryId, $questgroupUrl)
+ {
+ $data = $this->db->query(
+ 'SELECT id, title, url, questgroupspicture_id '.
+ 'FROM questgroups '.
+ 'WHERE seminary_id = ? AND url = ?',
+ 'is',
+ $seminaryId, $questgroupUrl
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($questgroupUrl);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get texts of a Questgroup.
+ *
+ * @param int $questgroupId ID of a Questgroup
+ * @return array Texts of this Questgroup
+ */
+ public function getQuestgroupTexts($questgroupId)
+ {
+ return $this->db->query(
+ 'SELECT id, pos, text '.
+ 'FROM questgrouptexts '.
+ 'WHERE questgroup_id = ? '.
+ 'ORDER BY pos ASC',
+ 'i',
+ $questgroupId
+ );
+ }
+
+
+ /**
+ * Get first texts of a Questgroup.
+ *
+ * @param int $questgroupId ID of a Questgroup
+ * @return array First Text of this Questgroup
+ */
+ public function getFirstQuestgroupText($questgroupId)
+ {
+ $data = $this->db->query(
+ 'SELECT id, pos, text '.
+ 'FROM questgrouptexts '.
+ 'WHERE questgroup_id = ? '.
+ 'ORDER BY pos ASC '.
+ 'LIMIT 1',
+ 'i',
+ $questgroupId
+ );
+ if(!empty($data)) {
+ return $data[0];
+ }
+
+
+ return null;
+ }
+
+
+ /**
+ * Get the next Questgroup.
+ *
+ * Determine the next Questgroup. If there is no next Questgroup
+ * on the same level as the given Quest then the followed-up
+ * Questgroup from a higher hierarchy level is returned.
+ *
+ * @param int $questgroupId ID of Questgroup to get next Questgroup of
+ * @return array Questgroup data
+ */
+ public function getNextQuestgroup($questgroupId)
+ {
+ $currentQuestgroup = $this->getQuestgroupById($questgroupId);
+ $currentQuestgroup['hierarchy'] = $this->Questgroupshierarchy->getHierarchyForQuestgroup($currentQuestgroup['id']);
+ if(empty($currentQuestgroup['hierarchy'])) {
+ return null;
+ }
+
+ $nextQuestgroup = $this->_getNextQuestgroup($currentQuestgroup['hierarchy']['parent_questgroup_id'], $currentQuestgroup['hierarchy']['questgroup_pos']);
+ if(is_null($nextQuestgroup) && !is_null($currentQuestgroup['hierarchy']['parent_questgroup_id'])) {
+ $nextQuestgroup = $this->getNextQuestgroup($currentQuestgroup['hierarchy']['parent_questgroup_id']);
+ }
+
+
+ return $nextQuestgroup;
+ }
+
+
+ /**
+ * Get the previous Questgroup.
+ *
+ * Determine the previous Questgroup. If there is no previous
+ * Questgroup on the same level as the given Quest then the
+ * followed-up Questgroup from a higher hierarchy level is
+ * returned.
+ *
+ * @param int $questgroupId ID of Questgroup to get previous Questgroup of
+ * @return array Questgroup data
+ */
+ public function getPreviousQuestgroup($questgroupId)
+ {
+ $currentQuestgroup = $this->getQuestgroupById($questgroupId);
+ $currentQuestgroup['hierarchy'] = $this->Questgroupshierarchy->getHierarchyForQuestgroup($currentQuestgroup['id']);
+ if(empty($currentQuestgroup['hierarchy'])) {
+ return null;
+ }
+
+ $previousQuestgroup = $this->_getPreviousQuestgroup($currentQuestgroup['hierarchy']['parent_questgroup_id'], $currentQuestgroup['hierarchy']['questgroup_pos']);
+ if(is_null($previousQuestgroup) && !is_null($currentQuestgroup['hierarchy']['parent_questgroup_id'])) {
+ $previousQuestgroup = $this->getPreviousQuestgroup($currentQuestgroup['hierarchy']['parent_questgroup_id']);
+ }
+
+
+ return $previousQuestgroup;
+ }
+
+
+ /**
+ * Determine if the given Character has solved the Quests form
+ * this Questgroup.
+ *
+ * @param int $questgroupId ID of Questgroup to check
+ * @param int $characterId ID of Character to check
+ * @result boolean Whether Character has solved the Questgroup or not
+ */
+ public function hasCharacterSolvedQuestgroup($questgroupId, $characterId)
+ {
+ // Get data of Questgroup
+ $questgroup = $this->getQuestgroupById($questgroupId);
+
+ // Chack all Quests
+ $currentQuest = $this->Quests->getFirstQuestOfQuestgroup($questgroup['id']);
+ if(!is_null($currentQuest))
+ {
+ if(!$this->Quests->hasCharacterSolvedQuest($currentQuest['id'], $characterId)) {
+ return false;
+ }
+
+ // Get next Quests
+ $nextQuests = !is_null($currentQuest) ? $this->Quests->getNextQuests($currentQuest['id']) : null;
+ while(!is_null($currentQuest) && !empty($nextQuests))
+ {
+ // Get choosed Quest
+ $currentQuest = null;
+ foreach($nextQuests as &$nextQuest) {
+ if($this->Quests->hasCharacterEnteredQuest($nextQuest['id'], $characterId)) {
+ $currentQuest = $nextQuest;
+ }
+ }
+
+ // Check Quest
+ if(is_null($currentQuest)) {
+ return false;
+ }
+
+ // Check status
+ if(!$this->Quests->hasCharacterSolvedQuest($currentQuest['id'], $characterId)) {
+ return false;
+ }
+
+ $nextQuests = !is_null($currentQuest) ? $this->Quests->getNextQuests($currentQuest['id']) : null;
+ }
+ }
+
+ // Check all child Questgroups
+ $questgroup['hierarchy'] = $this->Questgroupshierarchy->getHierarchyForQuestgroup($questgroup['id']);
+ if(!empty($questgroup['hierarchy']))
+ {
+ $childQuestgroupshierarchy = $this->Questgroupshierarchy->getChildQuestgroupshierarchy($questgroup['hierarchy']['id']);
+ foreach($childQuestgroupshierarchy as &$hierarchy)
+ {
+ // Get Questgroups
+ $questgroups = $this->getQuestgroupsForHierarchy($hierarchy['id'], $questgroup['id']);
+ foreach($questgroups as &$group) {
+ if(!$this->hasCharacterSolvedQuestgroup($group['id'], $characterId)) {
+ return false;
+ }
+ }
+ }
+ }
+
+
+ return true;
+ }
+
+
+ /**
+ * Get all related Questgroups of a Questtext.
+ *
+ * @param int $questtextId ID of the questtext
+ * @return array Sidequests for the questtext
+ */
+ public function getRelatedQuestsgroupsOfQuesttext($questtextId)
+ {
+ return $this->db->query(
+ 'SELECT questgroups.id, questgroups_questtexts.questtext_id, questgroups.title, questgroups.url, questgroups_questtexts.entry_text '.
+ 'FROM questgroups_questtexts '.
+ 'INNER JOIN questgroups ON questgroups.id = questgroups_questtexts.questgroup_id '.
+ 'WHERE questgroups_questtexts.questtext_id = ?',
+ 'i',
+ $questtextId
+ );
+ }
+
+
+ /**
+ * Get all related Questgroups of a Quest.
+ *
+ * @param int $questId ID of the quest
+ * @return array Sidequests for the quest
+ */
+ public function getRelatedQuestsgroupsOfQuest($questId)
+ {
+ return $this->db->query(
+ 'SELECT questgroups_questtexts.questgroup_id AS id '.
+ 'FROM quests '.
+ 'INNER JOIN questtexts ON questtexts.quest_id = quests.id '.
+ 'INNER JOIN questgroups_questtexts ON questgroups_questtexts.questtext_id = questtexts.id '.
+ 'WHERE quests.id = ?',
+ 'i',
+ $questId
+ );
+
+ }
+
+
+ /**
+ * Calculate cumulated data for a Questgroup, its
+ * sub-Questgroups and all its Quests.
+ *
+ * @param int $questgroupId ID of Questgroup
+ * @param int $characterId ID of Character
+ * @param array $calculatedQuests IDs of already calculated Quests
+ * @return array Cumulated data for Questgroup
+ */
+ public function getCumulatedDataForQuestgroup($questgroupId, $characterId=null, &$calculatedQuests=array())
+ {
+ // Cumulated data
+ $data = array(
+ 'xps' => 0,
+ 'character_xps' => 0
+ );
+
+ // Current Questgroup
+ $questgroup = $this->getQuestgroupById($questgroupId);
+
+ // Quests of current Questgroup
+ $quest = $this->Quests->getFirstQuestOfQuestgroup($questgroup['id']);
+ if(!is_null($quest))
+ {
+ $questData = $this->getCumulatedDataForQuest($quest, $characterId, $calculatedQuests);
+ $data['xps'] += $questData['xps'];
+ $data['character_xps'] += $questData['character_xps'];
+ }
+
+ // XPs of child Questgroups
+ $questgroupHierarchy = $this->Questgroupshierarchy->getHierarchyForQuestgroup($questgroup['id']);
+ if(!empty($questgroupHierarchy))
+ {
+ $childQuestgroupshierarchy = $this->Questgroupshierarchy->getChildQuestgroupshierarchy($questgroupHierarchy['id']);
+ foreach($childQuestgroupshierarchy as &$hierarchy)
+ {
+ $questgroups = $this->getQuestgroupsForHierarchy($hierarchy['id'], $questgroup['id']);
+ foreach($questgroups as &$questgroup)
+ {
+ $childData = $this->getCumulatedDataForQuestgroup($questgroup['id'], $characterId, $calculatedQuests);
+ $data['xps'] += $childData['xps'];
+ $data['character_xps'] += $childData['character_xps'];
+ }
+ }
+ }
+
+
+ // Return cumulated data
+ return $data;
+ }
+
+
+ /**
+ * Calculate cumulated data of the given Quest, its following
+ * Quests and its related Questgroups.
+ *
+ * @param array $quest Quest data
+ * @param int $characterId ID of Character
+ * @param array $calculatedQuests IDs of already calculated Quests
+ * @return array Cumulated data for Quest
+ */
+ public function getCumulatedDataForQuest($quest, $characterId=null, &$calculatedQuests=array())
+ {
+ // Cumulated data
+ $data = array(
+ 'xps' => $quest['xps'],
+ 'character_xps' => (!is_null($characterId) && $this->Quests->hasCharacterSolvedQuest($quest['id'], $characterId)) ? $quest['xps'] : 0
+ );
+
+ // Related Questgroups
+ $relatedQuestgroups = $this->getRelatedQuestsgroupsOfQuest($quest['id']);
+ foreach($relatedQuestgroups as &$relatedQuestgroup)
+ {
+ $relatedData = $this->getCumulatedDataForQuestgroup($relatedQuestgroup['id'], $characterId, $calculatedQuests);
+ $data['xps'] += $relatedData['xps'];
+ $data['character_xps'] += $relatedData['character_xps'];
+ }
+
+ // Next Quests
+ $nextQuests = $this->Quests->getNextQuests($quest['id']);
+ $allNextData = array(
+ 'xps' => array(0),
+ 'character_xps' => array(0),
+ );
+ foreach($nextQuests as &$nextQuest)
+ {
+ if(!in_array($nextQuest['id'], $calculatedQuests))
+ {
+ $nextData = $this->getCumulatedDataForQuest($nextQuest, $characterId, $calculatedQuests);
+ $allNextData['xps'][] = $nextData['xps'];
+ $allNextData['character_xps'][] = $nextData['character_xps'];
+ $calculatedQuests[] = $nextQuest['id'];
+ }
+ }
+ $data['xps'] += max($allNextData['xps']);
+ $data['character_xps'] += max($allNextData['character_xps']);
+
+
+ // Return cumulated data
+ return $data;
+ }
+
+
+ /**
+ * Summarize XPs of all Quests for a Questgroup and its
+ * sub-Questgroups solved by a Character.
+ *
+ * @param int $questgroupId ID of Questgroup
+ * @param int $characterId ID of Character
+ * @return int Sum of XPs
+ */
+ public function getAchievedXPsForQuestgroup($questgroupId, $characterId)
+ {
+ // Sum of XPs
+ $xps = 0;
+
+ // Current Questgroup
+ $questgroup = $this->getQuestgroupById($questgroupId);
+
+ // Quests of current Questgroup
+ $quest = $this->Quests->getFirstQuestOfQuestgroup($questgroup['id']);
+ if(!is_null($quest)) {
+ $xps += $this->getAchievedXPsForQuest($quest, $characterId);
+ }
+
+ // XPs of child Questgroups
+ $questgroupHierarchy = $this->Questgroupshierarchy->getHierarchyForQuestgroup($questgroup['id']);
+ if(empty($questgroupHierarchy)) {
+ return $xps;
+ }
+ $childQuestgroupshierarchy = $this->Questgroupshierarchy->getChildQuestgroupshierarchy($questgroupHierarchy['id']);
+ foreach($childQuestgroupshierarchy as &$hierarchy)
+ {
+ $questgroups = $this->getQuestgroupsForHierarchy($hierarchy['id'], $questgroup['id']);
+ foreach($questgroups as &$questgroup) {
+ $xps += $this->getAchievedXPsForQuestgroup($questgroup['id'], $characterId);
+ }
+ }
+
+
+ // Return summarized XPs
+ return $xps;
+ }
+
+
+ /**
+ * Summarize XPs of the given Quest, its following Quests and
+ * its related Questgroups solved by a Character.
+ *
+ * @param int $quest Quest to summarize XPs for
+ * @param int $characterId ID of Character
+ * @return int Sum of XPs
+ */
+ public function getAchievedXPsForQuest($quest, $characterId)
+ {
+ $xps = 0;
+
+ // XPs for the given Quest
+ if($this->Quests->hasCharacterSolvedQuest($quest['id'], $characterId))
+ {
+ $xps += $quest['xps'];
+
+ // Next Quests
+ $nextQuests = $this->Quests->getNextQuests($quest['id']);
+ foreach($nextQuests as &$nextQuest)
+ {
+ if($this->Quests->hasCharacterEnteredQuest($nextQuest['id'], $characterId))
+ {
+ $xps += $this->getAchievedXPsForQuest($nextQuest, $characterId);
+ break;
+ }
+ }
+ }
+
+ // Related Questgroups
+ $relatedQuestgroups = $this->getRelatedQuestsgroupsOfQuest($quest['id']);
+ foreach($relatedQuestgroups as &$relatedQuestgroup) {
+ $xps += $this->getAchievedXPsForQuestgroup($relatedQuestgroup['id'], $characterId);
+ }
+
+
+ // Return summarized XPs
+ return $xps;
+ }
+
+
+ /**
+ * Create a new Questgroup.
+ *
+ * @param int $userId User-ID that creates the new character
+ * @param int $seminaryId ID of Seminary
+ * @param string $title Title for new Questgroup
+ * @return int ID of new Questgroup
+ */
+ public function createQuestgroup($userId, $seminaryId, $title)
+ {
+ $this->db->query(
+ 'INSERT INTO questgroups '.
+ '(created_user_id, seminary_id, title, url) '.
+ 'VALUES '.
+ '(?, ?, ?, ?)',
+ 'iiss',
+ $userId,
+ $seminaryId,
+ $title,
+ \nre\core\Linker::createLinkParam($title)
+ );
+
+
+ return $this->db->getInsertId();
+ }
+
+
+
+
+ /**
+ * Get the next (direct) Questgroup.
+ *
+ * @param int $parentQuestgroupId ID of parent Questgroup to get next Questgroup of
+ * @param int $questgroupPos Position of Questgroup to get next Questgroup of
+ * @return array Data of next Questgroup or NULL
+ */
+ private function _getNextQuestgroup($parentQuestgroupId, $questgroupPos)
+ {
+ if(!is_null($parentQuestgroupId))
+ {
+ $data = $this->db->query(
+ 'SELECT * '.
+ 'FROM questgroups_questgroupshierarchy '.
+ 'INNER JOIN questgroups ON questgroups.id = questgroups_questgroupshierarchy.questgroup_id '.
+ 'WHERE parent_questgroup_id = ? AND pos = ? + 1',
+ 'ii',
+ $parentQuestgroupId, $questgroupPos
+ );
+ }
+ else
+ {
+ $data = $this->db->query(
+ 'SELECT * '.
+ 'FROM questgroups_questgroupshierarchy '.
+ 'INNER JOIN questgroups ON questgroups.id = questgroups_questgroupshierarchy.questgroup_id '.
+ 'WHERE parent_questgroup_id IS NULL AND pos = ? + 1',
+ 'i',
+ $questgroupPos
+ );
+ }
+ if(empty($data)) {
+ return null;
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get the previous (direct) Questgroup.
+ *
+ * @param int $parentQuestgroupId ID of parent Questgroup to get previous Questgroup of
+ * @param int $questgroupPos Position of Questgroup to get previous Questgroup of
+ * @return array Data of previous Questgroup or NULL
+ */
+ private function _getPreviousQuestgroup($parentQuestgroupId, $questgroupPos)
+ {
+ if(!is_null($parentQuestgroupId))
+ {
+ $data = $this->db->query(
+ 'SELECT * '.
+ 'FROM questgroups_questgroupshierarchy '.
+ 'INNER JOIN questgroups ON questgroups.id = questgroups_questgroupshierarchy.questgroup_id '.
+ 'WHERE parent_questgroup_id = ? AND pos = ? - 1',
+ 'ii',
+ $parentQuestgroupId, $questgroupPos
+ );
+ }
+ else
+ {
+ $data = $this->db->query(
+ 'SELECT * '.
+ 'FROM questgroups_questgroupshierarchy '.
+ 'INNER JOIN questgroups ON questgroups.id = questgroups_questgroupshierarchy.questgroup_id '.
+ 'WHERE parent_questgroup_id IS NULL AND pos = ? - 1',
+ 'i',
+ $questgroupPos
+ );
+ }
+ if(empty($data)) {
+ return null;
+ }
+
+
+ return $data[0];
+ }
+
+ }
+
+?>
diff --git a/models/QuestgroupshierarchyModel.inc b/models/QuestgroupshierarchyModel.inc
new file mode 100644
index 00000000..c1dae116
--- /dev/null
+++ b/models/QuestgroupshierarchyModel.inc
@@ -0,0 +1,128 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model to interact with Questgroupshierarchy-table.
+ *
+ * @author Oliver Hanraths
+ */
+ class QuestgroupshierarchyModel extends \hhu\z\Model
+ {
+
+
+
+
+ /**
+ * Construct a new QuestgroupshierarchyModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get a Questgroup hierarchy by its ID.
+ *
+ * throws IdNotFoundException
+ * @param int $questgroupshierarchyId ID of a Questgroup hierarchy
+ * @return array Questgroup hierarchy
+ */
+ public function getHierarchyById($questgroupshierarchyId)
+ {
+ $data = $this->db->query(
+ 'SELECT id, seminary_id, parent_questgroupshierarchy_id, pos, title_singular, title_plural, url '.
+ 'FROM questgroupshierarchy '.
+ 'WHERE questgroupshierarchy.id = ?',
+ 'i',
+ $questgroupshierarchyId
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($questgroupshierarchyId);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get the toplevel hierarchy entries of a Seminary.
+ *
+ * @param int $seminaryId ID of the seminary to get hierarchy for
+ * @return array Toplevel hierarchy
+ */
+ public function getHierarchyOfSeminary($seminaryId)
+ {
+ return $this->db->query(
+ 'SELECT id, seminary_id, parent_questgroupshierarchy_id, pos, title_singular, title_plural, url '.
+ 'FROM questgroupshierarchy '.
+ 'WHERE '.
+ 'questgroupshierarchy.seminary_id = ? AND '.
+ 'questgroupshierarchy.parent_questgroupshierarchy_id IS NULL '.
+ 'ORDER BY questgroupshierarchy.pos ASC',
+ 'i',
+ $seminaryId
+ );
+ }
+
+
+ /**
+ * Get the Questgroup-Hierarchy for a Questgroup.
+ *
+ * @param int $questgroupId ID of Questgroup
+ * @return array Hierarchy for Questgroup
+ */
+ public function getHierarchyForQuestgroup($questgroupId)
+ {
+ $data = $this->db->query(
+ 'SELECT questgroups_questgroupshierarchy.parent_questgroup_id, questgroups_questgroupshierarchy.pos AS questgroup_pos, questgroupshierarchy.id, questgroupshierarchy.seminary_id, questgroupshierarchy.parent_questgroupshierarchy_id, questgroupshierarchy.pos, questgroupshierarchy.title_singular, questgroupshierarchy.title_plural, questgroupshierarchy.url '.
+ 'FROM questgroups_questgroupshierarchy '.
+ 'INNER JOIN questgroupshierarchy ON questgroupshierarchy.id = questgroups_questgroupshierarchy.questgroupshierarchy_id '.
+ 'WHERE questgroups_questgroupshierarchy.questgroup_id = ?',
+ 'i',
+ $questgroupId
+ );
+ if(!empty($data)) {
+ return $data[0];
+ }
+
+
+ return null;
+ }
+
+
+ /**
+ * Get the child hierarchy entries of a Questgroup hierarchy.
+ *
+ * @param int $questgroupshierarchyId ID of a Questgroup hierarchy
+ * @return array Child Questgroup hierarchy entries
+ */
+ public function getChildQuestgroupshierarchy($questgroupshierarchyId)
+ {
+ return $this->db->query(
+ 'SELECT id, seminary_id, parent_questgroupshierarchy_id, pos, title_singular, title_plural, url '.
+ 'FROM questgroupshierarchy '.
+ 'WHERE questgroupshierarchy.parent_questgroupshierarchy_id = ? '.
+ 'ORDER BY questgroupshierarchy.pos ASC',
+ 'i',
+ $questgroupshierarchyId
+ );
+ }
+
+ }
+
+?>
diff --git a/models/QuestsModel.inc b/models/QuestsModel.inc
new file mode 100644
index 00000000..d4175545
--- /dev/null
+++ b/models/QuestsModel.inc
@@ -0,0 +1,487 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model to interact with Quests-table.
+ *
+ * @author Oliver Hanraths
+ */
+ class QuestsModel extends \hhu\z\Model
+ {
+ /**
+ * Quest-status: Entered
+ *
+ * @var int;
+ */
+ const QUEST_STATUS_ENTERED = 0;
+ /**
+ * Quest-status: submitted
+ *
+ * @var int;
+ */
+ const QUEST_STATUS_SUBMITTED = 1;
+ /**
+ * Quest-status: Unsolved
+ *
+ * @var int;
+ */
+ const QUEST_STATUS_UNSOLVED = 2;
+ /**
+ * Quest-status: Solved
+ *
+ * @var int;
+ */
+ const QUEST_STATUS_SOLVED = 3;
+
+
+
+
+ /**
+ * Construct a new QuestsModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get a Quest and its data by its URL.
+ *
+ * @throws IdNotFoundException
+ * @param int $seminaryId ID of the corresponding Seminary
+ * @param int $questgroupId ID of the corresponding Questgroup
+ * @param string $questURL URL-title of a Quest
+ * @return array Quest data
+ */
+ public function getQuestByUrl($seminaryId, $questgroupId, $questUrl)
+ {
+ $data = $this->db->query(
+ 'SELECT quests.id, quests.questgroup_id, quests.questtype_id, quests.title, quests.url, quests.xps, quests.entry_text, quests.task, quests.wrong_text, quests.questsmedia_id '.
+ 'FROM quests '.
+ 'LEFT JOIN questgroups ON questgroups.id = quests.questgroup_id '.
+ 'WHERE questgroups.seminary_id = ? AND questgroups.id = ? AND quests.url = ?',
+ 'iis',
+ $seminaryId, $questgroupId, $questUrl
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($questUrl);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get a Quest and its data by its ID.
+ *
+ * @throws IdNotFoundException
+ * @param string $questId ID of a Quest
+ * @return array Quest data
+ */
+ public function getQuestById($questId)
+ {
+ $data = $this->db->query(
+ 'SELECT quests.id, quests.questgroup_id, quests.questtype_id, quests.title, quests.url, quests.xps, quests.entry_text, quests.task, quests.wrong_text, quests.questsmedia_id '.
+ 'FROM quests '.
+ 'LEFT JOIN questgroups ON questgroups.id = quests.questgroup_id '.
+ 'WHERE quests.id = ?',
+ 'i',
+ $questId
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($questId);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get the first Quest of a Qusetgroup.
+ *
+ * @param int $questId ID of Questgroup
+ * @return array Data of first Quest
+ */
+ public function getFirstQuestOfQuestgroup($questgroupId)
+ {
+ $data = $this->db->query(
+ 'SELECT id, questtype_id, title, url, xps, task '.
+ 'FROM quests '.
+ 'LEFT JOIN quests_previousquests ON quests_previousquests.quest_id = quests.id '.
+ 'WHERE questgroup_id = ? AND quests_previousquests.previous_quest_id IS NULL',
+ 'i',
+ $questgroupId
+ );
+ if(!empty($data)) {
+ return $data[0];
+ }
+
+
+ return null;
+ }
+
+
+ /**
+ * Get Quests that follow-up a Quest.
+ *
+ * @param int $questId ID of Quest to get next Quests of
+ * @return array Quests data
+ */
+ public function getNextQuests($questId)
+ {
+ return $this->db->query(
+ 'SELECT quests.id, quests.questtype_id, quests.title, quests.url, quests.xps, quests.entry_text, quests.task, questgroups.title AS questgroup_title, questgroups.url AS questgroup_url '.
+ 'FROM quests_previousquests '.
+ 'INNER JOIN quests ON quests.id = quests_previousquests.quest_id '.
+ 'INNER JOIN questgroups ON questgroups.id = quests.questgroup_id '.
+ 'WHERE quests_previousquests.previous_quest_id = ?',
+ 'i',
+ $questId
+ );
+ }
+
+
+ /**
+ * Get Quests that the given Quests follows-up to.
+ *
+ * @param int $questId ID of Quest to get previous Quests of
+ * @return array Quests data
+ */
+ public function getPreviousQuests($questId)
+ {
+ return $this->db->query(
+ 'SELECT quests.id, quests.title, quests.url, quests.entry_text, questgroups.title AS questgroup_title, questgroups.url AS questgroup_url '.
+ 'FROM quests_previousquests '.
+ 'INNER JOIN quests ON quests.id = quests_previousquests.previous_quest_id '.
+ 'INNER JOIN questgroups ON questgroups.id = quests.questgroup_id '.
+ 'WHERE quests_previousquests.quest_id = ?',
+ 'i',
+ $questId
+ );
+ }
+
+
+ /**
+ * Mark a Quest as entered for a Character.
+ *
+ * @param int $questId ID of Quest to mark as entered
+ * @param int $characterId ID of Character that entered the Quest
+ */
+ public function setQuestEntered($questId, $characterId)
+ {
+ $this->setQuestStatus($questId, $characterId, self::QUEST_STATUS_ENTERED, false);
+ }
+
+
+ /**
+ * Mark a Quest as submitted for a Character.
+ *
+ * @param int $questId ID of Quest to mark as unsolved
+ * @param int $characterId ID of Character that unsolved the Quest
+ */
+ public function setQuestSubmitted($questId, $characterId)
+ {
+ $this->setQuestStatus($questId, $characterId, self::QUEST_STATUS_SUBMITTED);
+ }
+
+
+ /**
+ * Mark a Quest as unsolved for a Character.
+ *
+ * @param int $questId ID of Quest to mark as unsolved
+ * @param int $characterId ID of Character that unsolved the Quest
+ */
+ public function setQuestUnsolved($questId, $characterId)
+ {
+ $this->setQuestStatus($questId, $characterId, self::QUEST_STATUS_UNSOLVED);
+ }
+
+
+ /**
+ * Mark a Quest as solved for a Character.
+ *
+ * @param int $questId ID of Quest to mark as solved
+ * @param int $characterId ID of Character that solved the Quest
+ */
+ public function setQuestSolved($questId, $characterId)
+ {
+ $this->setQuestStatus($questId, $characterId, self::QUEST_STATUS_SOLVED, false);
+ }
+
+
+ /**
+ * Determine if the given Character has entered a Quest.
+ *
+ * @param int $questId ID of Quest to check
+ * @param int $characterId ID of Character to check
+ * @result boolean Whether Character has entered the Quest or not
+ */
+ public function hasCharacterEnteredQuest($questId, $characterId)
+ {
+ $count = $this->db->query(
+ 'SELECT count(id) AS c '.
+ 'FROM quests_characters '.
+ 'WHERE quest_id = ? AND character_id = ? AND status IN (?,?,?)',
+ 'iiiii',
+ $questId,
+ $characterId,
+ self::QUEST_STATUS_ENTERED, self::QUEST_STATUS_SOLVED, self::QUEST_STATUS_UNSOLVED
+ );
+
+
+ return (!empty($count) && intval($count[0]['c']) > 0);
+ }
+
+
+ /**
+ * Determine if the given Character has tried to solve a Quest.
+ *
+ * @param int $questId ID of Quest to check
+ * @param int $characterId ID of Character to check
+ * @result boolean Whether Character has tried to solved the Quest or not
+ */
+ public function hasCharacterTriedQuest($questId, $characterId)
+ {
+ $count = $this->db->query(
+ 'SELECT count(id) AS c '.
+ 'FROM quests_characters '.
+ 'WHERE quest_id = ? AND character_id = ? AND status IN (?,?)',
+ 'iiii',
+ $questId,
+ $characterId,
+ self::QUEST_STATUS_SOLVED, self::QUEST_STATUS_UNSOLVED
+ );
+
+
+ return (!empty($count) && intval($count[0]['c']) > 0);
+ }
+
+
+ /**
+ * Determine if the given Character has solved the given Quest.
+ *
+ * @param int $questId ID of Quest to check
+ * @param int $characterId ID of Character to check
+ * @result boolean Whether Character has solved the Quest or not
+ */
+ public function hasCharacterSolvedQuest($questId, $characterId)
+ {
+ $count = $this->db->query(
+ 'SELECT count(id) AS c '.
+ 'FROM quests_characters '.
+ 'WHERE quest_id = ? AND character_id = ? AND status = ?',
+ 'iii',
+ $questId,
+ $characterId,
+ self::QUEST_STATUS_SOLVED
+ );
+
+
+ return (!empty($count) && intval($count[0]['c']) > 0);
+ }
+
+
+ /**
+ * Get the last Quests for a Character.
+ *
+ * @param int $characterId ID of Character
+ * @retrun array Quest data
+ */
+ public function getLastQuestForCharacter($characterId)
+ {
+ $data = $this->db->query(
+ 'SELECT quests.id, quests.questgroup_id, quests.questtype_id, quests.title, quests.url, quests.xps, quests.task, quests.wrong_text, quests.questsmedia_id '.
+ 'FROM quests_characters '.
+ 'LEFT JOIN quests ON quests.id = quests_characters.quest_id '.
+ 'WHERE quests_characters.character_id = ? AND quests_characters.status IN (?, ?, ?) '.
+ 'ORDER BY quests_characters.created desc '.
+ 'LIMIT 1',
+ 'iiii',
+ $characterId,
+ self::QUEST_STATUS_ENTERED, self::QUEST_STATUS_SUBMITTED, self::QUEST_STATUS_SOLVED
+ );
+ if(!empty($data)) {
+ return $data[0];
+ }
+
+
+ return null;
+ }
+
+
+ /**
+ * Get all Quests for a Seminary.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @return array Quests for this Seminary
+ */
+ public function getQuestsForSeminary($seminaryId)
+ {
+ return $this->db->query(
+ 'SELECT DISTINCT quests.id, quests.questgroup_id, quests.questtype_id, quests.title, quests.url, quests.xps, quests.task, quests.wrong_text, quests.questsmedia_id '.
+ 'FROM questgroups '.
+ 'INNER JOIN quests ON quests.questgroup_id = questgroups.id '.
+ 'WHERE questgroups.seminary_id = ?',
+ 'i',
+ $seminaryId
+ );
+ }
+
+
+ /**
+ * Get all Quests that are linked to a Questtopic.
+ *
+ * @param int $questtopicId ID of Questtopic
+ * @return array Quests for this Questtopic
+ */
+ public function getQuestsForQuesttopic($questtopicId)
+ {
+ return $this->db->query(
+ 'SELECT DISTINCT quests.id, quests.questgroup_id, quests.questtype_id, quests.title, quests.url, quests.xps, quests.task, quests.wrong_text, quests.questsmedia_id '.
+ 'FROM quests_questsubtopics '.
+ 'INNER JOIN quests ON quests.id = quests_questsubtopics.quest_id '.
+ 'WHERE quests_questsubtopics.questsubtopic_id = ?',
+ 'i',
+ $questtopicId
+ );
+ }
+
+
+
+
+ /**
+ * Mark a Quest for a Character.
+ *
+ * @param int $questId ID of Quest to mark
+ * @param int $characterId ID of Character to mark the Quest for
+ * @param int $status Quest status to mark
+ * @param boolean $repeated Insert although status is already set for this Quest and Character
+ */
+ private function setQuestStatus($questId, $characterId, $status, $repeated=true)
+ {
+ // Check if status is already set
+ if(!$repeated)
+ {
+ $count = $this->db->query(
+ 'SELECT count(*) AS c '.
+ 'FROM quests_characters '.
+ 'WHERE quest_id = ? AND character_id = ? AND status = ?',
+ 'iii',
+ $questId,
+ $characterId,
+ $status
+ );
+ if(!empty($count) && intval($count[0]['c']) > 0) {
+ return;
+ }
+ }
+
+ // Set status
+ $this->db->query(
+ 'INSERT INTO quests_characters '.
+ '(quest_id, character_id, status) '.
+ 'VALUES '.
+ '(?, ?, ?) ',
+ 'iii',
+ $questId,
+ $characterId,
+ $status
+ );
+ }
+
+
+ /**
+ * Get the last status of a Quest for a Character.
+ *
+ * @param int $questId ID of Quest
+ * @param int $characterId ID of Character to get status for
+ * @return int Last status
+ */
+ public function getLastQuestStatus($questId, $characterId)
+ {
+ $data = $this->db->query(
+ 'SELECT id, created, status '.
+ 'FROM quests_characters '.
+ 'WHERE quest_id = ? AND character_id = ? '.
+ 'ORDER BY created DESC '.
+ 'LIMIT 1',
+ 'ii',
+ $questId, $characterId
+ );
+ if(!empty($data)) {
+ return $data[0];
+ }
+
+
+ return null;
+ }
+
+
+ /**
+ * Create a new Quest.
+ *
+ * @param int $userId User-ID that creates the new character
+ * @param string $name Name for new Quest
+ * @param int $questgroupId ID of Questgroup
+ * @param int $questtypeId ID of Questtype
+ * @param int $xps XPs for new Quest
+ * @param string $entrytext Entrytext for new Quest
+ * @param string $wrongtext Wrongtext for new Quest
+ * @param string $task Task for new Quest
+ * @return int ID of new Quest
+ */
+ public function createQuest($userId, $name, $questgroupId, $questtypeId, $xps, $entrytext, $wrongtext, $task)
+ {
+ $this->db->query(
+ 'INSERT INTO quests '.
+ '(created_user_id, questgroup_id, questtype_id, title, url, xps, entry_text, wrong_text, task) '.
+ 'VALUES '.
+ '(?, ?, ?, ?, ?, ?, ?, ?, ?)',
+ 'iiississs',
+ $userId, $questgroupId, $questtypeId,
+ $name, \nre\core\Linker::createLinkParam($name),
+ $xps, $entrytext, $wrongtext, $task
+ );
+
+
+ return $this->db->getInsertId();
+ }
+
+
+ /**
+ * Set the media for a Quest.
+ *
+ * @param int $questId ID of Quest to set media for
+ * @param int $questmediaId ID of Questsmedia to set
+ */
+ public function setQuestmedia($questId, $questsmediaId)
+ {
+ $this->db->query(
+ 'UPDATE quests '.
+ 'SET questsmedia_id = ? '.
+ 'WHERE id = ?',
+ 'ii',
+ $questsmediaId,
+ $questId
+ );
+ }
+
+ }
+
+?>
diff --git a/models/QuesttextsModel.inc b/models/QuesttextsModel.inc
new file mode 100644
index 00000000..7aca28b9
--- /dev/null
+++ b/models/QuesttextsModel.inc
@@ -0,0 +1,220 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model to interact with Questtexts-table.
+ *
+ * @author Oliver Hanraths
+ */
+ class QuesttextsModel extends \hhu\z\Model
+ {
+
+
+
+
+ /**
+ * Construct a new QuesttextsModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get all Questtexts for a Quest.
+ *
+ * @param int $questId ID of the Quest
+ * @param string $questtexttypeUrl URL of the Questtexttype
+ * @return array All Questtexts for a Quest
+ */
+ public function getQuesttextsOfQuest($questId, $questtexttypeUrl=null)
+ {
+ if(is_null($questtexttypeUrl))
+ {
+ return $this->db->query(
+ 'SELECT questtexts.id, questtexts.text, questtexts.pos, questtexts.out_text, questtexts.abort_text, questtexts.questsmedia_id, questtexttypes.id AS type_id, questtexttypes.type, questtexttypes.url AS type_url '.
+ 'FROM questtexts '.
+ 'LEFT JOIN questtexttypes ON questtexttypes.id = questtexts.questtexttype_id '.
+ 'WHERE questtexts.quest_id = ?',
+ 'i',
+ $questId
+ );
+ }
+ else
+ {
+ return $this->db->query(
+ 'SELECT questtexts.id, questtexts.text, questtexts.pos, questtexts.out_text, questtexts.abort_text, questtexts.questsmedia_id, questtexttypes.id AS type_id, questtexttypes.type, questtexttypes.url AS type_url '.
+ 'FROM questtexts '.
+ 'LEFT JOIN questtexttypes ON questtexttypes.id = questtexts.questtexttype_id '.
+ 'WHERE questtexts.quest_id = ? and questtexttypes.url = ?',
+ 'is',
+ $questId,
+ $questtexttypeUrl
+ );
+ }
+ }
+
+
+ /**
+ * Get count of Questtexts for a Quest.
+ *
+ * @param int $questId ID of the Quest
+ * @param string $questtexttypeUrl URL of the Questtexttype
+ * @return int Amount of Questtexts for a Quest
+ */
+ public function getQuesttextCountOfQuest($questId, $questtexttypeUrl=null)
+ {
+ if(is_null($questtexttypeUrl))
+ {
+ $data = $this->db->query(
+ 'SELECT count(questtexts.id) AS c '.
+ 'FROM questtexts '.
+ 'LEFT JOIN questtexttypes ON questtexttypes.id = questtexts.questtexttype_id '.
+ 'WHERE questtexts.quest_id = ?',
+ 'i',
+ $questId
+ );
+ }
+ else
+ {
+ $data = $this->db->query(
+ 'SELECT count(questtexts.id) AS c '.
+ 'FROM questtexts '.
+ 'LEFT JOIN questtexttypes ON questtexttypes.id = questtexts.questtexttype_id '.
+ 'WHERE questtexts.quest_id = ? and questtexttypes.url = ?',
+ 'is',
+ $questId,
+ $questtexttypeUrl
+ );
+ }
+ if(!empty($data)) {
+ return $data[0]['c'];
+ }
+
+
+ return 0;
+ }
+
+
+ /**
+ * Get corresponding Questtext for a Sidequest.
+ *
+ * @throws IdNotFoundException
+ * @param int $sidequestId ID of the Sidequest to get the Questtext for
+ * @param array Questtext data
+ */
+ public function getRelatedQuesttextForQuestgroup($questgroupId)
+ {
+ $data = $this->db->query(
+ 'SELECT questtexts.id, questtexts.text, questtexts.pos, questtexts.quest_id, questtexttypes.id AS type_id, questtexttypes.type, questtexttypes.url AS type_url '.
+ 'FROM questgroups_questtexts '.
+ 'LEFT JOIN questtexts ON questtexts.id = questgroups_questtexts.questtext_id '.
+ 'LEFT JOIN questtexttypes ON questtexttypes.id = questtexts.questtexttype_id '.
+ 'WHERE questgroups_questtexts.questgroup_id = ?',
+ 'i',
+ $questgroupId
+ );
+ if(!empty($data)) {
+ return $data[0];
+ }
+
+
+ return null;
+ }
+
+
+ /**
+ * Get all registered Questtexttypes.
+ *
+ * @return array Registered Questtexttypes
+ */
+ public function getQuesttexttypes()
+ {
+ return $this->db->query(
+ 'SELECT id, type, url '.
+ 'FROM questtexttypes'
+ );
+ }
+
+
+ /**
+ * Get a Questtexttype by its URL.
+ *
+ * @param string $questtexttypeUrl URL-type of Questtexttype
+ * @return array Questtexttype data
+ */
+ public function getQuesttexttypeByUrl($questtexttypeUrl)
+ {
+ $data = $this->db->query(
+ 'SELECT id, type, url '.
+ 'FROM questtexttypes '.
+ 'WHERE url = ?',
+ 's',
+ $questtexttypeUrl
+ );
+ if(!empty($data)) {
+ return $data[0];
+ }
+
+
+ return null;
+ }
+
+
+ /**
+ * Add a list of Questtexts to a Quest.
+ *
+ * @param int $userId ID of user
+ * @param int $questId ID of Quest to add texts to
+ * @param string $questtexttypeUrl URL-type of Questtexttype of texts
+ * @param array $texts List of texts to add.
+ */
+ public function addQuesttextsToQuest($userId, $questId, $questtexttypeUrl, $texts)
+ {
+ $questtexttype = $this->getQuesttexttypeByUrl($questtexttypeUrl);
+ if(is_null($questtexttype)) {
+ return;
+ }
+ foreach($texts as &$text)
+ {
+ $pos = $this->db->query(
+ 'SELECT COALESCE(MAX(pos),0)+1 AS pos '.
+ 'FROM questtexts '.
+ 'WHERE quest_id = ?',
+ 'i',
+ $questId
+ );
+ $pos = $pos[0]['pos'];
+
+ $this->db->query(
+ 'INSERT INTO questtexts '.
+ '(created_user_id, quest_id, questtexttype_id, pos, text) '.
+ 'VALUES '.
+ '(?, ?, ?, ?, ?)',
+ 'iiiis',
+ $userId, $questId, $questtexttype['id'], $pos,
+ $text
+ );
+ }
+ }
+
+
+
+
+ }
+
+?>
diff --git a/models/QuesttopicsModel.inc b/models/QuesttopicsModel.inc
new file mode 100644
index 00000000..abd246f7
--- /dev/null
+++ b/models/QuesttopicsModel.inc
@@ -0,0 +1,154 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model to interact with Questtopics-table.
+ *
+ * @author Oliver Hanraths
+ */
+ class QuesttopicsModel extends \hhu\z\Model
+ {
+
+
+
+
+ /**
+ * Construct a new QuesttopicsModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get a Questtopic by its URL.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @param string $questtopicUrl URL-Title of Questtopic
+ * @return array Questtopic data
+ */
+ public function getQuesttopicByUrl($seminaryId, $questtopicUrl)
+ {
+ $data = $this->db->query(
+ 'SELECT id, title, url '.
+ 'FROM questtopics '.
+ 'WHERE seminary_id = ? AND url = ?',
+ 'is',
+ $seminaryId, $questtopicUrl
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($questtopicUrl);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get all Questtopics for a Seminary.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @return array List of Questtopics
+ */
+ public function getQuesttopicsForSeminary($seminaryId)
+ {
+ return $this->db->query(
+ 'SELECT id, title, url '.
+ 'FROM questtopics '.
+ 'WHERE seminary_id = ?',
+ 'i',
+ $seminaryId
+ );
+ }
+
+
+ /**
+ * Get count of Quests that are linked to a Questtopic.
+ *
+ * @param int $questtopicId ID of Questtopic
+ * @return int Count of Quests
+ */
+ public function getQuestCountForQuesttopic($questtopicId)
+ {
+ $data = $this->db->query(
+ 'SELECT count(DISTINCT quests_questsubtopics.quest_id) AS c ' .
+ 'FROM questsubtopics '.
+ 'LEFT JOIN quests_questsubtopics ON quests_questsubtopics.questsubtopic_id = questsubtopics.id '.
+ 'WHERE questsubtopics.questtopic_id = ?',
+ 'i',
+ $questtopicId
+ );
+ if(!empty($data)) {
+ return $data[0]['c'];
+ }
+
+
+ return 0;
+ }
+
+
+ /**
+ * Get count of Quests that are linked to a Questtopic and are
+ * unlocked by a Character.
+ *
+ * @param int $questtopicId ID of Questtopic
+ * @param int $characterId ID of Character
+ * @return int Count of Quests
+ */
+ public function getCharacterQuestCountForQuesttopic($questtopicId, $characterId)
+ {
+ $data = $this->db->query(
+ 'SELECT count(DISTINCT quests_characters.quest_id) AS c '.
+ 'FROM questsubtopics '.
+ 'LEFT JOIN quests_questsubtopics ON quests_questsubtopics.questsubtopic_id = questsubtopics.id '.
+ 'INNER JOIN quests_characters ON quests_characters.quest_id = quests_questsubtopics.quest_id AND quests_characters.character_id = ? AND quests_characters.status = 3 '.
+ 'WHERE questsubtopics.questtopic_id = ?',
+ 'ii',
+ $characterId,
+ $questtopicId
+ );
+ if(!empty($data)) {
+ return $data[0]['c'];
+ }
+
+
+ return 0;
+ }
+
+
+ /**
+ * Get all Questsubtopics for a Quest.
+ *
+ * @param int $questId ID of Quest
+ * @return array List of Questsubtopics
+ */
+ public function getQuestsubtopicsForQuest($questId)
+ {
+ return $this->db->query(
+ 'SELECT DISTINCT id, questtopic_id, title, url '.
+ 'FROM quests_questsubtopics '.
+ 'INNER JOIN questsubtopics ON questsubtopics.id = quests_questsubtopics.questsubtopic_id '.
+ 'WHERE quests_questsubtopics.quest_id = ?',
+ 'i',
+ $questId
+ );
+ }
+
+ }
+
+?>
diff --git a/models/QuesttypesModel.inc b/models/QuesttypesModel.inc
new file mode 100644
index 00000000..58b36648
--- /dev/null
+++ b/models/QuesttypesModel.inc
@@ -0,0 +1,77 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model to interact with Questtypes-table.
+ *
+ * @author Oliver Hanraths
+ */
+ class QuesttypesModel extends \hhu\z\Model
+ {
+
+
+
+
+ /**
+ * Construct a new QuesttypesModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get all registered Questtypes.
+ *
+ * @return array List of registered Questtypes
+ */
+ public function getQuesttypes()
+ {
+ return $this->db->query(
+ 'SELECT id, title, url, classname '.
+ 'FROM questtypes '.
+ 'ORDER BY title ASC'
+ );
+ }
+
+
+ /**
+ * Get a Questtype by its ID
+ *
+ * @param int $questtypeId ID of Questtype
+ * @return array Questtype data
+ */
+ public function getQuesttypeById($questtypeId)
+ {
+ $data = $this->db->query(
+ 'SELECT title, classname '.
+ 'FROM questtypes '.
+ 'WHERE id = ?',
+ 'i',
+ $questtypeId
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($questtypeId);
+ }
+
+
+ return $data = $data[0];
+ }
+
+ }
+
+?>
diff --git a/models/SeminariesModel.inc b/models/SeminariesModel.inc
new file mode 100644
index 00000000..207411df
--- /dev/null
+++ b/models/SeminariesModel.inc
@@ -0,0 +1,195 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model of the SeminariesAgent to list registered seminaries.
+ *
+ * @author Oliver Hanraths
+ */
+ class SeminariesModel extends \hhu\z\Model
+ {
+ /**
+ * Required models
+ *
+ * @var array
+ */
+ public $models = array('questgroupshierarchy', 'questgroups');
+
+
+
+
+ /**
+ * Construct a new SeminariesModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get registered seminaries.
+ *
+ * @return array Seminaries
+ */
+ public function getSeminaries()
+ {
+ // Get seminaries
+ return $this->db->query(
+ 'SELECT id, created, created_user_id, title, url, course, description, seminarymedia_id '.
+ 'FROM seminaries '.
+ 'ORDER BY created DESC'
+ );
+ }
+
+
+ /**
+ * Get a seminary and its data by its ID.
+ *
+ * @throws IdNotFoundException
+ * @param string $seminaryId ID of a seminary
+ * @return array Seminary
+ */
+ public function getSeminaryById($seminaryId)
+ {
+ $seminary = $this->db->query(
+ 'SELECT id, created, created_user_id, title, url, course, description, seminarymedia_id '.
+ 'FROM seminaries '.
+ 'WHERE id = ?',
+ 'i',
+ $seminaryId
+ );
+ if(empty($seminary)) {
+ throw new \nre\exceptions\IdNotFoundException($seminaryId);
+ }
+
+
+ return $seminary[0];
+ }
+
+
+ /**
+ * Get a seminary and its data by its URL-title.
+ *
+ * @throws IdNotFoundException
+ * @param string $seminaryUrl URL-Title of a seminary
+ * @return array Seminary
+ */
+ public function getSeminaryByUrl($seminaryUrl)
+ {
+ $seminary = $this->db->query(
+ 'SELECT id, created, created_user_id, title, url, course, description, seminarymedia_id '.
+ 'FROM seminaries '.
+ 'WHERE url = ?',
+ 's',
+ $seminaryUrl
+ );
+ if(empty($seminary)) {
+ throw new \nre\exceptions\IdNotFoundException($seminaryUrl);
+ }
+
+
+ return $seminary[0];
+ }
+
+
+ /*
+ * Calculate sum of XPs for a Seminary.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @return int Total sum of XPs
+ */
+ public function getTotalXPs($seminaryId)
+ {
+ $xps = 0;
+
+ // Questgroups
+ $questgroupshierarchy = $this->Questgroupshierarchy->getHierarchyOfSeminary($seminaryId);
+ foreach($questgroupshierarchy as &$hierarchy)
+ {
+ // Get Questgroups
+ $questgroups = $this->Questgroups->getQuestgroupsForHierarchy($hierarchy['id']);
+ foreach($questgroups as &$questgroup)
+ {
+ $data = $this->Questgroups->getCumulatedDataForQuestgroup($questgroup['id']);
+ $xps += $data['xps'];
+ }
+ }
+
+
+ return $xps;
+ }
+
+
+ /**
+ * Create a new seminary.
+ *
+ * @param string $title Title of seminary to create
+ * @param int $userId ID of creating user
+ * @return int ID of the newly created seminary
+ */
+ public function createSeminary($title, $userId)
+ {
+ $this->db->query(
+ 'INSERT INTO seminaries '.
+ '(created_user_id, title, url) '.
+ 'VALUES '.
+ '(?, ?, ?)',
+ 'iss',
+ $userId,
+ $title,
+ \nre\core\Linker::createLinkParam($title)
+ );
+
+
+ return $this->db->getInsertId();
+ }
+
+
+ /**
+ * Edit a seminary.
+ *
+ * @throws DatamodelException
+ * @param int $seminaryId ID of the seminary to delete
+ * @param string $title New title of seminary
+ */
+ public function editSeminary($seminaryId, $title)
+ {
+ $this->db->query(
+ 'UPDATE seminaries '.
+ 'SET title = ?, url = ? '.
+ 'WHERE id = ?',
+ 'ssi',
+ $title,
+ \nre\core\Linker::createLinkParam($title),
+ $seminaryId
+ );
+ }
+
+
+ /**
+ * Delete a seminary.
+ *
+ * @param int $seminaryId ID of the seminary to delete
+ */
+ public function deleteSeminary($seminaryId)
+ {
+ $this->db->query('DELETE FROM seminaries WHERE id = ?', 'i', $seminaryId);
+ }
+
+ }
+
+?>
diff --git a/models/SeminarycharacterfieldsModel.inc b/models/SeminarycharacterfieldsModel.inc
new file mode 100644
index 00000000..efad3bde
--- /dev/null
+++ b/models/SeminarycharacterfieldsModel.inc
@@ -0,0 +1,78 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model to interact with the Seminarycharacterfields-tables.
+ *
+ * @author Oliver Hanraths
+ */
+ class SeminarycharacterfieldsModel extends \hhu\z\Model
+ {
+
+
+
+
+ /**
+ * Construct a new SeminarycharacterfieldsModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get all Character fields of a Seminary.
+ *
+ * @param int $seminaryId ID of Seminary to get fields of
+ * @param array Seminary Character fields
+ */
+ public function getFieldsForSeminary($seminaryId)
+ {
+ return $this->db->query(
+ 'SELECT seminarycharacterfields.id, seminarycharacterfields.title, seminarycharacterfields.url, seminarycharacterfields.regex, seminarycharacterfields.required, seminarycharacterfieldtypes.id AS type_id, seminarycharacterfieldtypes.title AS type_title, seminarycharacterfieldtypes.url AS type_url '.
+ 'FROM seminarycharacterfields '.
+ 'LEFT JOIN seminarycharacterfieldtypes ON seminarycharacterfieldtypes.id = seminarycharacterfields.seminarycharacterfieldtype_id '.
+ 'WHERE seminarycharacterfields.seminary_id = ? '.
+ 'ORDER BY pos ASC',
+ 'i',
+ $seminaryId
+ );
+ }
+
+
+ /**
+ * Get Seminary Character fields of a Character.
+ *
+ * @param int $characterId ID of the Character
+ * @return array Seminary Character fields
+ */
+ public function getFieldsForCharacter($characterId)
+ {
+ return $this->db->query(
+ 'SELECT seminarycharacterfields.title, characters_seminarycharacterfields.value '.
+ 'FROM characters_seminarycharacterfields '.
+ 'LEFT JOIN seminarycharacterfields ON seminarycharacterfields.id = characters_seminarycharacterfields.seminarycharacterfield_id '.
+ 'LEFT JOIN seminarycharacterfieldtypes ON seminarycharacterfieldtypes.id = seminarycharacterfields.seminarycharacterfieldtype_id '.
+ 'WHERE characters_seminarycharacterfields.character_id = ?',
+ 'i',
+ $characterId
+ );
+ }
+
+ }
+
+?>
diff --git a/models/UploadsModel.inc b/models/UploadsModel.inc
new file mode 100644
index 00000000..fc4ec2fd
--- /dev/null
+++ b/models/UploadsModel.inc
@@ -0,0 +1,141 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model to handle files to upload.
+ *
+ * @author Oliver Hanraths
+ */
+ class UploadsModel extends \hhu\z\Model
+ {
+
+
+
+
+ /**
+ * Construct a new UploadsModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Upload a file and create a database record.
+ *
+ * @param int $userId ID of user that uploads the file
+ * @param int $seminaryId ID of Seminary
+ * @param string $name Name of file to upload
+ * @param string $filename Filename of file to upload
+ * @param string $tmpFilename Name of temporary uploaded file
+ * @param string $mimetype Mimetype of file to upload
+ * @return mixed ID of database record or false
+ */
+ public function uploadSeminaryFile($userId, $seminaryId, $name, $filename, $tmpFilename, $mimetype)
+ {
+ $uploadId = false;
+ $this->db->setAutocommit(false);
+
+ try {
+ // Create database record
+ $this->db->query(
+ 'INSERT INTO seminaryuploads '.
+ '(created_user_id, seminary_id, name, url, mimetype) '.
+ 'VALUES '.
+ '(?, ? ,? ,?, ?)',
+ 'iisss',
+ $userId,
+ $seminaryId,
+ $name,
+ \nre\core\Linker::createLinkParam($filename),
+ $mimetype
+ );
+ $uploadId = $this->db->getInsertId();
+
+ // Create filename
+ $filename = ROOT.DS.\nre\configs\AppConfig::$dirs['seminaryuploads'].DS.$filename;
+ if(!move_uploaded_file($tmpFilename, $filename))
+ {
+ $this->db->rollback();
+ $uploadId = false;
+ }
+ }
+ catch(\nre\exceptions\DatamodelException $e) {
+ $this->db->rollback();
+ $this->db->setAutocommit(true);
+ }
+
+
+ $this->db->setAutocommit(true);
+ return $uploadId;
+ }
+
+
+ /**
+ * Get an upload by its ID.
+ *
+ * @throws IdNotFoundException
+ * @param int $uploadId ID of the uploaded file
+ * @return array Upload data
+ */
+ public function getSeminaryuploadById($seminaryuploadId)
+ {
+ $data = $this->db->query(
+ 'SELECT id, created, created_user_id, seminary_id, name, url, mimetype, public '.
+ 'FROM seminaryuploads '.
+ 'WHERE id = ?',
+ 'i',
+ $seminaryuploadId
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($seminaryuploadId);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get an upload by its URL.
+ *
+ * @throws IdNotFoundException
+ * @param int $seminaryId ID of Seminary
+ * @param int $uploadId ID of the uploaded file
+ * @return array Upload data
+ */
+ public function getSeminaryuploadByUrl($seminaryId, $seminaryuploadUrl)
+ {
+ $data = $this->db->query(
+ 'SELECT id, created, created_user_id, seminary_id, name, url, mimetype, public '.
+ 'FROM seminaryuploads '.
+ 'WHERE seminary_id = ? AND url = ?',
+ 'is',
+ $seminaryId,
+ $seminaryuploadUrl
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($seminaryuploadUrl);
+ }
+
+
+ return $data[0];
+ }
+
+ }
+
+?>
diff --git a/models/UserrolesModel.inc b/models/UserrolesModel.inc
new file mode 100644
index 00000000..33121a21
--- /dev/null
+++ b/models/UserrolesModel.inc
@@ -0,0 +1,77 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model to interact with userroles-table.
+ *
+ * @author Oliver Hanraths
+ */
+ class UserrolesModel extends \hhu\z\Model
+ {
+
+
+
+
+ /**
+ * Construct a new UserrolesModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get all userroles for an user referenced by its ID.
+ *
+ * @param int $userId ID of an user
+ * @return array Userroles for an user
+ */
+ public function getUserrolesForUserById($userId)
+ {
+ return $this->db->query(
+ 'SELECT userroles.id, userroles.created, userroles.name '.
+ 'FROM users_userroles '.
+ 'LEFT JOIN userroles ON userroles.id = users_userroles.userrole_id '.
+ 'WHERE users_userroles.user_id = ?',
+ 'i',
+ $userId
+ );
+ }
+
+
+ /**
+ * Get all userroles for an user referenced by its URL-username.
+ *
+ * @param string $userUrl URL-Username of an user
+ * @return array Userroles for an user
+ */
+ public function getUserrolesForUserByUrl($userUrl)
+ {
+ return $this->db->query(
+ 'SELECT userroles.id, userroles.created, userroles.name '.
+ 'FROM users '.
+ 'LEFT JOIN users_userroles ON users_userroles.user_id = users.id '.
+ 'LEFT JOIN userroles ON userroles.id = users_userroles.userrole_id '.
+ 'WHERE users.url = ?',
+ 's',
+ $userUrl
+ );
+ }
+
+ }
+
+?>
diff --git a/models/UsersModel.inc b/models/UsersModel.inc
new file mode 100644
index 00000000..d2a69003
--- /dev/null
+++ b/models/UsersModel.inc
@@ -0,0 +1,348 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model of the UsersAgent to list users and get their data.
+ *
+ * @author Oliver Hanraths
+ */
+ class UsersModel extends \hhu\z\Model
+ {
+
+
+
+
+ /**
+ * Construct a new UsersModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get registered users.
+ *
+ * @return array Users
+ */
+ public function getUsers()
+ {
+ return $this->db->query(
+ 'SELECT id, created, username, url, surname, prename, email '.
+ 'FROM users '.
+ 'ORDER BY username ASC'
+ );
+ }
+
+
+ /**
+ * Get users with the given user role.
+ *
+ * @param string $userrole User role
+ * @return array List of users
+ */
+ public function getUsersWithRole($userrole)
+ {
+ return $this->db->query(
+ 'SELECT users.id, users.created, users.username, users.url, users.surname, users.prename, users.email '.
+ 'FROM users '.
+ 'LEFT JOIN users_userroles ON users_userroles.user_id = users.id '.
+ 'LEFT JOIN userroles ON userroles.id = users_userroles.userrole_id '.
+ 'WHERE userroles.name = ? '.
+ 'ORDER BY username ASC',
+ 's',
+ $userrole
+ );
+ }
+
+
+ /**
+ * Get a user and its data by its ID.
+ *
+ * @throws IdNotFoundException
+ * @param int $userId ID of an user
+ * @return array Userdata
+ */
+ public function getUserById($userId)
+ {
+ // Get user
+ $user = $this->db->query(
+ 'SELECT id, created, username, url, surname, prename, email '.
+ 'FROM users '.
+ 'WHERE id = ?',
+ 'i',
+ $userId
+ );
+ if(empty($user)) {
+ throw new \nre\exceptions\IdNotFoundException($userId);
+ }
+
+
+ return $user[0];
+ }
+
+
+ /**
+ * Get a user and its data by its URL-username.
+ *
+ * @throws IdNotFoundException
+ * @param string $userUrl URL-Username of an user
+ * @return array Userdata
+ */
+ public function getUserByUrl($userUrl)
+ {
+ // Get user
+ $user = $this->db->query(
+ 'SELECT id, created, username, url, surname, prename, email '.
+ 'FROM users '.
+ 'WHERE url = ?',
+ 's',
+ $userUrl
+ );
+ if(empty($user)) {
+ throw new \nre\exceptions\IdNotFoundException($userUrl);
+ }
+
+
+ return $user[0];
+ }
+
+
+ /**
+ * Log a user in if its credentials are valid.
+ *
+ * @throws DatamodelException
+ * @param string $username The name of the user to log in
+ * @param string $password Plaintext password of the user to log in
+ */
+ public function login($username, $password)
+ {
+ $data = $this->db->query('SELECT id, password FROM users WHERE username = ?', 's', $username);
+ if(!empty($data))
+ {
+ $data = $data[0];
+ if($this->verify($password, $data['password'])) {
+ return $data['id'];
+ }
+ }
+
+
+ return null;
+ }
+
+
+ /**
+ * Check if an username already exists.
+ *
+ * @param string $username Username to check
+ * @return boolean Whether username exists or not
+ */
+ public function usernameExists($username)
+ {
+ $data = $this->db->query(
+ 'SELECT count(id) AS c '.
+ 'FROM users '.
+ 'WHERE username = ? OR url = ?',
+ 'ss',
+ $username,
+ \nre\core\Linker::createLinkParam($username)
+ );
+ if(!empty($data)) {
+ return ($data[0]['c'] > 0);
+ }
+
+
+ return false;
+ }
+
+
+ /**
+ * Check if an e‑mail address already exists.
+ *
+ * @param string $email E‑mail address to check
+ * @return boolean Whether e‑mail address exists or not
+ */
+ public function emailExists($email)
+ {
+ $data = $this->db->query(
+ 'SELECT count(id) AS c '.
+ 'FROM users '.
+ 'WHERE email = ?',
+ 's',
+ $email
+ );
+ if(!empty($data)) {
+ return ($data[0]['c'] > 0);
+ }
+
+
+ return false;
+ }
+
+
+ /**
+ * Create a new user.
+ *
+ * @param string $username Username of the user to create
+ * @param string $email E‑Mail-Address of the user to create
+ * @param string $password Password of the user to create
+ * @return int ID of the newly created user
+ */
+ public function createUser($username, $prename, $surname, $email, $password)
+ {
+ $userId = null;
+ $this->db->setAutocommit(false);
+ try {
+ // Create user
+ $this->db->query(
+ 'INSERT INTO users '.
+ '(username, url, surname, prename, email, password) '.
+ 'VALUES '.
+ '(?, ?, ?, ?, ?, ?)',
+ 'ssssss',
+ $username,
+ \nre\core\Linker::createLinkParam($username),
+ $surname,
+ $prename,
+ $email,
+ $this->hash($password)
+ );
+ $userId = $this->db->getInsertId();
+
+ // Add role “user”
+ $this->db->query(
+ 'INSERT INTO users_userroles '.
+ '(user_id, userrole_id) '.
+ 'SELECT ?, userroles.id '.
+ 'FROM userroles '.
+ 'WHERE userroles.name = ?',
+ 'is',
+ $userId,
+ 'user'
+ );
+ }
+ catch(Exception $e) {
+ $this->db->rollback();
+ $this->db->setAutocommit(true);
+ throw $e;
+ }
+ $this->db->setAutocommit(true);
+
+
+ return $userId;
+ }
+
+
+ /**
+ * Edit a user.
+ *
+ * @throws DatamodelException
+ * @param int $userId ID of the user to delete
+ * @param string $username New name of user
+ * @param string $email Changed e‑mail-address of user
+ * @param string $password Changed plaintext password of user
+ */
+ public function editUser($userId, $username, $prename, $surname, $email, $password)
+ {
+ $this->db->setAutocommit(false);
+ try {
+ // Update user data
+ $this->db->query(
+ 'UPDATE users '.
+ 'SET username = ?, url = ?, prename = ?, surname = ?, email = ? '.
+ 'WHERE id = ?',
+ 'sssssi',
+ $username,
+ \nre\core\Linker::createLinkParam($username),
+ $prename,
+ $surname,
+ $email,
+ $userId
+ );
+
+ // Set new password
+ if(!empty($password))
+ {
+ $this->db->query(
+ 'UPDATE users '.
+ 'SET password = ? '.
+ 'WHERE id = ?',
+ 'si',
+ $this->hash($password),
+ $userId
+ );
+ }
+ }
+ catch(Exception $e) {
+ $this->db->rollback();
+ $this->db->setAutocommit(true);
+ throw $e;
+ }
+ $this->db->setAutocommit(true);
+ }
+
+
+ /**
+ * Delete a user.
+ *
+ * @param int $userId ID of the user to delete
+ */
+ public function deleteUser($userId)
+ {
+ $this->db->query('DELETE FROM users WHERE id = ?', 'i', $userId);
+ }
+
+
+
+
+ /**
+ * Hash a password.
+ *
+ * @param string $password Plaintext password
+ * @return string Hashed password
+ */
+ public function hash($password)
+ {
+ if(!function_exists('password_hash')) {
+ \hhu\z\lib\Password::load();
+ }
+
+
+ return password_hash($password, PASSWORD_DEFAULT);
+ }
+
+
+ /**
+ * Verify a password.
+ *
+ * @param string $password Plaintext password to verify
+ * @param string $hash Hashed password to match with
+ * @return boolean Verified
+ */
+ private function verify($password, $hash)
+ {
+ if(!function_exists('password_verify')) {
+ \hhu\z\lib\Password::load();
+ }
+
+
+ return password_verify($password, $hash);
+ }
+
+ }
+
+?>
diff --git a/questtypes/bossfight/BossfightQuesttypeAgent.inc b/questtypes/bossfight/BossfightQuesttypeAgent.inc
new file mode 100644
index 00000000..6d99ec44
--- /dev/null
+++ b/questtypes/bossfight/BossfightQuesttypeAgent.inc
@@ -0,0 +1,24 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * QuesttypeAgent for a boss-fight.
+ *
+ * @author Oliver Hanraths
+ */
+ class BossfightQuesttypeAgent extends \hhu\z\QuesttypeAgent
+ {
+ }
+
+?>
diff --git a/questtypes/bossfight/BossfightQuesttypeController.inc b/questtypes/bossfight/BossfightQuesttypeController.inc
new file mode 100644
index 00000000..4f8001e7
--- /dev/null
+++ b/questtypes/bossfight/BossfightQuesttypeController.inc
@@ -0,0 +1,258 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * Controller of the BossfightQuesttypeAgent for a boss-fight.
+ *
+ * @author Oliver Hanraths
+ */
+ class BossfightQuesttypeController extends \hhu\z\QuesttypeController
+ {
+ /**
+ * Required models
+ *
+ * @var array
+ */
+ public $models = array('media');
+
+
+
+
+ /**
+ * Save the answers of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param array $answers Character answers for the Quest
+ */
+ public function saveAnswersOfCharacter($seminary, $questgroup, $quest, $character, $answers)
+ {
+ // Prepare session
+ $this->prepareSession($quest['id']);
+
+ // Remove previous answers
+ $this->Bossfight->clearCharacterSubmissions($quest['id'], $character['id']);
+
+ // Save answers
+ foreach($_SESSION['quests'][$quest['id']]['stages'] as &$stage) {
+ $this->Bossfight->setCharacterSubmission($stage['id'], $character['id']);
+ }
+ }
+
+
+ /**
+ * Save additional data for the answers of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param array $data Additional (POST-) data
+ */
+ public function saveDataForCharacterAnswers($seminary, $questgroup, $quest, $character, $data)
+ {
+ }
+
+
+ /**
+ * Check if answers of a Character for a Quest match the correct ones.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @return boolean True/false for a right/wrong answer or null for moderator evaluation
+ */
+ public function matchAnswersOfCharacter($seminary, $questgroup, $quest, $character, $answers)
+ {
+ // Get Boss-Fight
+ $fight = $this->Bossfight->getBossFight($quest['id']);
+
+ // Prepare session
+ $this->prepareSession($quest['id']);
+
+ // Calculate lives
+ $lives = array(
+ 'character' => $fight['lives_character'],
+ 'boss' => $fight['lives_boss']
+ );
+ foreach($_SESSION['quests'][$quest['id']]['stages'] as &$stage)
+ {
+ $lives['character'] += $stage['livedrain_character'];
+ $lives['boss'] += $stage['livedrain_boss'];
+ }
+
+
+ return ($lives['boss'] == 0 && $lives['character'] > 0);
+ }
+
+
+ /**
+ * Action: quest.
+ *
+ * Display a stage with a text and the answers for the following
+ * stages.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param Exception $exception Character submission exception
+ */
+ public function quest($seminary, $questgroup, $quest, $character, $exception)
+ {
+ // Get Boss-Fight
+ $fight = $this->Bossfight->getBossFight($quest['id']);
+ if(!is_null($fight['boss_seminarymedia_id'])) {
+ $fight['bossmedia'] = $this->Media->getSeminaryMediaById($fight['boss_seminarymedia_id']);
+ }
+
+ // Prepare session
+ $this->prepareSession($quest['id']);
+
+ // Get Stage
+ if($this->request->getRequestMethod() == 'POST' && !is_null($this->request->getPostParam('submit_stages')))
+ {
+ $stages = $this->request->getPostParam('submit_stages');
+ $stageId = array_keys($stages)[0];
+ $stage = $this->Bossfight->getStageById($stageId);
+ }
+ else
+ {
+ $_SESSION['quests'][$quest['id']]['stages'] = array();
+ $stage = $this->Bossfight->getFirstStage($quest['id']);
+ }
+
+ // Store Stage in session
+ if(count($_SESSION['quests'][$quest['id']]['stages']) == 0 || $_SESSION['quests'][$quest['id']]['stages'][count($_SESSION['quests'][$quest['id']]['stages'])-1]['id'] != $stage['id']) {
+ $_SESSION['quests'][$quest['id']]['stages'][] = $stage;
+ }
+
+ // Calculate lives
+ $lives = array(
+ 'character' => $fight['lives_character'],
+ 'boss' => $fight['lives_boss']
+ );
+ foreach($_SESSION['quests'][$quest['id']]['stages'] as &$stage)
+ {
+ $lives['character'] += $stage['livedrain_character'];
+ $lives['boss'] += $stage['livedrain_boss'];
+ }
+
+ // Get Child-Stages
+ $childStages = $this->Bossfight->getChildStages($stage['id']);
+
+ // Get answer of Character
+ if($this->request->getGetParam('show-answer') == 'true') {
+ foreach($childStages as &$childStage) {
+ $childStage['answer'] = $this->Bossfight->getCharacterSubmission($childStage['id'], $character['id']);
+ }
+ }
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ $this->set('character', $character);
+ $this->set('fight', $fight);
+ $this->set('stage', $stage);
+ $this->set('lives', $lives);
+ $this->set('childStages', $childStages);
+ }
+
+
+ /**
+ * Action: submission.
+ *
+ * Display all stages with the answers the character has
+ * choosen.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ */
+ public function submission($seminary, $questgroup, $quest, $character)
+ {
+ // Get Boss-Fight
+ $fight = $this->Bossfight->getBossFight($quest['id']);
+ if(!is_null($fight['boss_seminarymedia_id'])) {
+ $fight['bossmedia'] = $this->Media->getSeminaryMediaById($fight['boss_seminarymedia_id']);
+ }
+
+ // Get stages
+ $stages = array();
+ $stage = $this->Bossfight->getFirstStage($quest['id']);
+ while(!is_null($stage))
+ {
+ $stages[] = $stage;
+
+ $childStages = $this->Bossfight->getChildStages($stage['id']);
+ $stage = null;
+ foreach($childStages as &$childStage)
+ {
+ if($this->Bossfight->getCharacterSubmission($childStage['id'], $character['id']))
+ {
+ $stage = $childStage;
+ break;
+ }
+ }
+ }
+
+ // Calculate lives
+ $stages[0]['lives'] = array(
+ 'character' => $fight['lives_character'],
+ 'boss' => $fight['lives_boss']
+ );
+ for($i=1; $i $stages[$i-1]['lives']['character'] + $stages[$i]['livedrain_character'],
+ 'boss' => $stages[$i-1]['lives']['boss'] + $stages[$i]['livedrain_boss'],
+ );
+ }
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ $this->set('character', $character);
+ $this->set('fight', $fight);
+ $this->set('stages', $stages);
+ }
+
+
+
+
+ /**
+ * Prepare the session to store stage information in
+ *
+ * @param int $questId ID of Quest
+ */
+ private function prepareSession($questId)
+ {
+ if(!array_key_exists('quests', $_SESSION)) {
+ $_SESSION['quests'] = array();
+ }
+ if(!array_key_exists($questId, $_SESSION['quests'])) {
+ $_SESSION['quests'][$questId] = array();
+ }
+ if(!array_key_exists('stages', $_SESSION['quests'][$questId])) {
+ $_SESSION['quests'][$questId]['stages'] = array();
+ }
+ }
+
+ }
+
+?>
diff --git a/questtypes/bossfight/BossfightQuesttypeModel.inc b/questtypes/bossfight/BossfightQuesttypeModel.inc
new file mode 100644
index 00000000..a05927f0
--- /dev/null
+++ b/questtypes/bossfight/BossfightQuesttypeModel.inc
@@ -0,0 +1,183 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * Model of the BossfightQuesttypeAgent for a boss-fight.
+ *
+ * @author Oliver Hanraths
+ */
+ class BossfightQuesttypeModel extends \hhu\z\QuesttypeModel
+ {
+
+
+
+
+ /**
+ * Get a Boss-Fight.
+ *
+ * @throws IdNotFoundException
+ * @param int $questId ID of Quest
+ * @return array Boss-Fight data
+ */
+ public function getBossFight($questId)
+ {
+ $data = $this->db->query(
+ 'SELECT bossname, boss_seminarymedia_id, lives_character, lives_boss '.
+ 'FROM questtypes_bossfight '.
+ 'WHERE quest_id = ?',
+ 'i',
+ $questId
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($questId);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get the first Stage to begin the Boss-Fight with.
+ *
+ * @param int $questId ID of Quest
+ * @return array Data of first Stage
+ */
+ public function getFirstStage($questId)
+ {
+ $data = $this->db->query(
+ 'SELECT id, text, question, livedrain_character, livedrain_boss '.
+ 'FROM questtypes_bossfight_stages '.
+ 'WHERE questtypes_bossfight_quest_id = ? AND parent_stage_id IS NULL',
+ 'i',
+ $questId
+ );
+ if(!empty($data)) {
+ return $data[0];
+ }
+
+
+ return null;
+ }
+
+
+ /**
+ * Get a Stage by its ID.
+ *
+ * @param int $stageId ID of Stage
+ * @return array Stage data or null
+ */
+ public function getStageById($stageId)
+ {
+ $data = $this->db->query(
+ 'SELECT id, text, question, livedrain_character, livedrain_boss '.
+ 'FROM questtypes_bossfight_stages '.
+ 'WHERE id = ?',
+ 'i',
+ $stageId
+ );
+ if(!empty($data)) {
+ return $data[0];
+ }
+
+
+ return null;
+ }
+
+
+ /**
+ * Get the follow-up Stages for a Stage.
+ *
+ * @param int $parentStageId ID of Stage to get follow-up Stages for
+ * @return array List of follow-up Stages
+ */
+ public function getChildStages($parentStageId)
+ {
+ return $this->db->query(
+ 'SELECT id, text, question, livedrain_character, livedrain_boss '.
+ 'FROM questtypes_bossfight_stages '.
+ 'WHERE parent_stage_id = ?',
+ 'i',
+ $parentStageId
+ );
+ }
+
+
+ /**
+ * Reset all Character submissions of a Boss-Fight.
+ *
+ * @param int $questId ID of Quest
+ * @param int $characterId ID of Character
+ */
+ public function clearCharacterSubmissions($questId, $characterId)
+ {
+ $this->db->query(
+ 'DELETE FROM questtypes_bossfight_stages_characters '.
+ 'WHERE questtypes_bossfight_stage_id IN ('.
+ 'SELECT id '.
+ 'FROM questtypes_bossfight_stages '.
+ 'WHERE questtypes_bossfight_quest_id = ?'.
+ ') AND character_id = ?',
+ 'ii',
+ $questId,
+ $characterId
+ );
+ }
+
+
+ /**
+ * Save Character’s submitted answer for one Boss-Fight-Stage.
+ *
+ * @param int $regexId ID of list
+ * @param int $characterId ID of Character
+ */
+ public function setCharacterSubmission($stageId, $characterId)
+ {
+ $this->db->query(
+ 'INSERT INTO questtypes_bossfight_stages_characters '.
+ '(questtypes_bossfight_stage_id, character_id) '.
+ 'VALUES '.
+ '(?, ?) '.
+ 'ON DUPLICATE KEY UPDATE '.
+ 'questtypes_bossfight_stage_id = ?',
+ 'iii',
+ $stageId, $characterId, $stageId
+ );
+ }
+
+
+ /**
+ * Get answer of one Boss-Fight-Stage submitted by Character.
+ *
+ * @param int $regexId ID of list
+ * @param int $characterId ID of Character
+ * @return boolean Stage taken
+ */
+ public function getCharacterSubmission($stageId, $characterId)
+ {
+ $data = $this->db->query(
+ 'SELECT questtypes_bossfight_stage_id '.
+ 'FROM questtypes_bossfight_stages_characters '.
+ 'WHERE questtypes_bossfight_stage_id = ? AND character_id = ? ',
+ 'ii',
+ $stageId, $characterId
+ );
+
+
+ return (!empty($data));
+ }
+
+ }
+
+?>
diff --git a/questtypes/bossfight/html/quest.tpl b/questtypes/bossfight/html/quest.tpl
new file mode 100644
index 00000000..e97569c2
--- /dev/null
+++ b/questtypes/bossfight/html/quest.tpl
@@ -0,0 +1,51 @@
+
+
+ =$character['name']?>
+
+
+ 0) : ?>
+
+
+
+
+ =_('lost')?>
+
+
+
+
+ =$fight['bossname']?>
+
+
+ 0) : ?>
+
+
+
+
+ =_('lost')?>
+
+
+
+
+
+=\hhu\z\Utils::t($stage['text'])?>
+
+
diff --git a/questtypes/bossfight/html/submission.tpl b/questtypes/bossfight/html/submission.tpl
new file mode 100644
index 00000000..3696964e
--- /dev/null
+++ b/questtypes/bossfight/html/submission.tpl
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+=$stage['question']?>
+
+
+ =$character['name']?>
+
+ 0) : ?>
+
+
+
+
+ =_('lost')?>
+
+
+
+
+ =$fight['bossname']?>
+
+ 0) : ?>
+
+
+
+
+ =_('lost')?>
+
+
+
+
+
diff --git a/questtypes/choiceinput/ChoiceinputQuesttypeAgent.inc b/questtypes/choiceinput/ChoiceinputQuesttypeAgent.inc
new file mode 100644
index 00000000..b418f01e
--- /dev/null
+++ b/questtypes/choiceinput/ChoiceinputQuesttypeAgent.inc
@@ -0,0 +1,24 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * QuesttypeAgent for choosing between predefined input values.
+ *
+ * @author Oliver Hanraths
+ */
+ class ChoiceinputQuesttypeAgent extends \hhu\z\QuesttypeAgent
+ {
+ }
+
+?>
diff --git a/questtypes/choiceinput/ChoiceinputQuesttypeController.inc b/questtypes/choiceinput/ChoiceinputQuesttypeController.inc
new file mode 100644
index 00000000..be75b852
--- /dev/null
+++ b/questtypes/choiceinput/ChoiceinputQuesttypeController.inc
@@ -0,0 +1,171 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * Controller of the ChoiceinputQuesttypeAgent for choosing between
+ * predefined input values.
+ *
+ * @author Oliver Hanraths
+ */
+ class ChoiceinputQuesttypeController extends \hhu\z\QuesttypeController
+ {
+
+
+
+
+ /**
+ * Save the answers of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param array $answers Character answers for the Quest
+ */
+ public function saveAnswersOfCharacter($seminary, $questgroup, $quest, $character, $answers)
+ {
+ // Get lists
+ $choiceLists = $this->Choiceinput->getChoiceinputLists($quest['id']);
+
+ // Save answers
+ foreach($choiceLists as &$list)
+ {
+ $pos = intval($list['number']) - 1;
+ $answer = (array_key_exists($pos, $answers)) ? $answers[$pos] : null;
+ $this->Choiceinput->setCharacterSubmission($list['id'], $character['id'], $answer);
+ }
+ }
+
+
+ /**
+ * Save additional data for the answers of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param array $data Additional (POST-) data
+ */
+ public function saveDataForCharacterAnswers($seminary, $questgroup, $quest, $character, $data)
+ {
+ }
+
+
+ /**
+ * Check if answers of a Character for a Quest match the correct ones.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @return boolean True/false for a right/wrong answer or null for moderator evaluation
+ */
+ public function matchAnswersOfCharacter($seminary, $questgroup, $quest, $character, $answers)
+ {
+ // Get lists
+ $choiceLists = $this->Choiceinput->getChoiceinputLists($quest['id']);
+
+ // Match lists with user answers
+ foreach($choiceLists as $i => &$list)
+ {
+ if(!array_key_exists($i, $answers)) {
+ return false;
+ }
+ if($list['questtypes_choiceinput_choice_id'] != $answers[$i]) {
+ return false;
+ }
+ }
+
+
+ // All answer right
+ return true;
+ }
+
+
+ /**
+ * Action: quest.
+ *
+ * Display a text with lists with predefined values.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param Exception $exception Character submission exception
+ */
+ public function quest($seminary, $questgroup, $quest, $character, $exception)
+ {
+ // Get Task
+ $task = $this->Choiceinput->getChoiceinputQuest($quest['id']);
+
+ // Process text
+ $textParts = preg_split('/(\$\$)/', ' '.$task['text'].' ');
+
+ // Get lists
+ $choiceLists = $this->Choiceinput->getChoiceinputLists($quest['id']);
+ foreach($choiceLists as &$list) {
+ $list['values'] = $this->Choiceinput->getChoiceinputChoices($list['id']);
+ }
+
+ // Get Character answers
+ if($this->request->getGetParam('show-answer') == 'true')
+ {
+ foreach($choiceLists as &$list) {
+ $list['answer'] = $this->Choiceinput->getCharacterSubmission($list['id'], $character['id']);
+ }
+ }
+
+
+ // Pass data to view
+ $this->set('texts', $textParts);
+ $this->set('choiceLists', $choiceLists);
+ }
+
+
+ /**
+ * Action: submission.
+ *
+ * Show the submission of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ */
+ public function submission($seminary, $questgroup, $quest, $character)
+ {
+ // Get Task
+ $task = $this->Choiceinput->getChoiceinputQuest($quest['id']);
+
+ // Process text
+ $textParts = preg_split('/(\$\$)/', ' '.$task['text'].' ');
+
+ // Get lists
+ $choiceLists = $this->Choiceinput->getChoiceinputLists($quest['id']);
+ foreach($choiceLists as &$list)
+ {
+ $list['values'] = $this->Choiceinput->getChoiceinputChoices($list['id']);
+ $list['answer'] = $this->Choiceinput->getCharacterSubmission($list['id'], $character['id']);
+ $list['right'] = ($list['questtypes_choiceinput_choice_id'] == $list['answer']);
+ }
+
+
+ // Pass data to view
+ $this->set('texts', $textParts);
+ $this->set('choiceLists', $choiceLists);
+ }
+
+ }
+
+?>
diff --git a/questtypes/choiceinput/ChoiceinputQuesttypeModel.inc b/questtypes/choiceinput/ChoiceinputQuesttypeModel.inc
new file mode 100644
index 00000000..e908600f
--- /dev/null
+++ b/questtypes/choiceinput/ChoiceinputQuesttypeModel.inc
@@ -0,0 +1,153 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * Model of the ChoiceinputQuesttypeAgent for choosing between
+ * predefined input values.
+ *
+ * @author Oliver Hanraths
+ */
+ class ChoiceinputQuesttypeModel extends \hhu\z\QuesttypeModel
+ {
+
+
+
+
+ /**
+ * Get choiceinput-text for a Quest.
+ *
+ * @param int $questId ID of Quest
+ * @return array Choiceinput-text
+ */
+ public function getChoiceinputQuest($questId)
+ {
+ $data = $this->db->query(
+ 'SELECT text '.
+ 'FROM questtypes_choiceinput '.
+ 'WHERE quest_id = ?',
+ 'i',
+ $questId
+ );
+ if(!empty($data)) {
+ return $data[0];
+ }
+
+
+ return null;
+ }
+
+
+ /**
+ * Get all lists of input values for a choiceinput-text.
+ *
+ * @param int $questId ID of Quest
+ * @return array List
+ */
+ public function getChoiceinputLists($questId)
+ {
+ return $this->db->query(
+ 'SELECT id, number, questtypes_choiceinput_choice_id '.
+ 'FROM questtypes_choiceinput_lists '.
+ 'WHERE questtypes_choiceinput_quest_id = ? '.
+ 'ORDER BY number ASC',
+ 'i',
+ $questId
+ );
+ }
+
+
+ /**
+ * Get the list of values for a choiceinput-list.
+ *
+ * @param int $listId ID of list
+ * @return array Input values
+ */
+ public function getChoiceinputChoices($listId)
+ {
+ return $this->db->query(
+ 'SELECT id, pos, text '.
+ 'FROM questtypes_choiceinput_choices '.
+ 'WHERE questtypes_choiceinput_list_id = ? '.
+ 'ORDER BY pos ASC',
+ 'i',
+ $listId
+ );
+ }
+
+
+ /**
+ * Save Character’s submitted answer for one choiceinput-list.
+ *
+ * @param int $regexId ID of list
+ * @param int $characterId ID of Character
+ * @param string $answer Submitted answer for this list
+ */
+ public function setCharacterSubmission($listId, $characterId, $answer)
+ {
+ if(is_null($answer))
+ {
+ $this->db->query(
+ 'INSERT INTO questtypes_choiceinput_lists_characters '.
+ '(questtypes_choiceinput_list_id, character_id, questtypes_choiceinput_choice_id) '.
+ 'VALUES '.
+ '(?, ?, NULL) '.
+ 'ON DUPLICATE KEY UPDATE '.
+ 'questtypes_choiceinput_choice_id = NULL',
+ 'ii',
+ $listId, $characterId
+ );
+ }
+ else
+ {
+ $this->db->query(
+ 'INSERT INTO questtypes_choiceinput_lists_characters '.
+ '(questtypes_choiceinput_list_id, character_id, questtypes_choiceinput_choice_id) '.
+ 'VALUES '.
+ '(?, ?, ?) '.
+ 'ON DUPLICATE KEY UPDATE '.
+ 'questtypes_choiceinput_choice_id = ?',
+ 'iiii',
+ $listId, $characterId, $answer, $answer
+ );
+ }
+ }
+
+
+ /**
+ * Get answer of one choiceinput-list submitted by Character.
+ *
+ * @param int $regexId ID of list
+ * @param int $characterId ID of Character
+ * @return int Submitted answer for this list or null
+ */
+ public function getCharacterSubmission($listId, $characterId)
+ {
+ $data = $this->db->query(
+ 'SELECT questtypes_choiceinput_choice_id '.
+ 'FROM questtypes_choiceinput_lists_characters '.
+ 'WHERE questtypes_choiceinput_list_id = ? AND character_id = ? ',
+ 'ii',
+ $listId, $characterId
+ );
+ if(!empty($data)) {
+ return $data[0]['questtypes_choiceinput_choice_id'];
+ }
+
+
+ return null;
+ }
+
+ }
+
+?>
diff --git a/questtypes/choiceinput/html/quest.tpl b/questtypes/choiceinput/html/quest.tpl
new file mode 100644
index 00000000..489cbc2f
--- /dev/null
+++ b/questtypes/choiceinput/html/quest.tpl
@@ -0,0 +1,15 @@
+
diff --git a/questtypes/choiceinput/html/submission.tpl b/questtypes/choiceinput/html/submission.tpl
new file mode 100644
index 00000000..40f60505
--- /dev/null
+++ b/questtypes/choiceinput/html/submission.tpl
@@ -0,0 +1,12 @@
+
diff --git a/questtypes/crossword/CrosswordQuesttypeAgent.inc b/questtypes/crossword/CrosswordQuesttypeAgent.inc
new file mode 100644
index 00000000..9b137fe9
--- /dev/null
+++ b/questtypes/crossword/CrosswordQuesttypeAgent.inc
@@ -0,0 +1,24 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * QuesttypeAgent for solving a crossword.
+ *
+ * @author Oliver Hanraths
+ */
+ class CrosswordQuesttypeAgent extends \hhu\z\QuesttypeAgent
+ {
+ }
+
+?>
diff --git a/questtypes/crossword/CrosswordQuesttypeController.inc b/questtypes/crossword/CrosswordQuesttypeController.inc
new file mode 100644
index 00000000..318271f9
--- /dev/null
+++ b/questtypes/crossword/CrosswordQuesttypeController.inc
@@ -0,0 +1,365 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * Controller of the CrosswordQuesttypeAgent for solving a crossword.
+ *
+ * @author Oliver Hanraths
+ */
+ class CrosswordQuesttypeController extends \hhu\z\QuesttypeController
+ {
+
+
+
+
+ /**
+ * Save the answers of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param array $answers Character answers for the Quest
+ */
+ public function saveAnswersOfCharacter($seminary, $questgroup, $quest, $character, $answers)
+ {
+ // Get words
+ $words = $this->Crossword->getWordsForQuest($quest['id']);
+
+ // Iterate words
+ foreach($words as &$word)
+ {
+ // Assemble answer for word
+ $answer = '';
+ if($word['vertical'])
+ {
+ $x = $word['pos_x'];
+ $startY = $word['pos_y'];
+ $endY = $startY + mb_strlen($word['word'], 'UTF-8') - 1;
+
+ foreach(range($startY, $endY) as $y)
+ {
+ if(array_key_exists($x, $answers) && array_key_exists($y, $answers[$x]) && !empty($answers[$x][$y])) {
+ $answer .= $answers[$x][$y];
+ }
+ else {
+ $answer .= ' ';
+ }
+ }
+ }
+ else
+ {
+ $startX = $word['pos_x'];
+ $endX = $startX + mb_strlen($word['word'], 'UTF-8') - 1;
+ $y = $word['pos_y'];
+
+ foreach(range($startX, $endX) as $x)
+ {
+ if(array_key_exists($x, $answers) && array_key_exists($y, $answers[$x]) && !empty($answers[$x][$y])) {
+ $answer .= $answers[$x][$y];
+ }
+ else {
+ $answer .= ' ';
+ }
+ }
+ }
+
+ // Save answer
+ $this->Crossword->setCharacterSubmission($word['id'], $character['id'], $answer);
+ }
+ }
+
+
+ /**
+ * Save additional data for the answers of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param array $data Additional (POST-) data
+ */
+ public function saveDataForCharacterAnswers($seminary, $questgroup, $quest, $character, $data)
+ {
+ }
+
+
+ /**
+ * Check if answers of a Character for a Quest match the correct ones.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @return boolean True/false for a right/wrong answer or null for moderator evaluation
+ */
+ public function matchAnswersOfCharacter($seminary, $questgroup, $quest, $character, $answers)
+ {
+ // Get words
+ $words = $this->Crossword->getWordsForQuest($quest['id']);
+
+ // Iterate words
+ foreach($words as &$word)
+ {
+ // Assemble answer for word
+ $answer = '';
+ if($word['vertical'])
+ {
+ $x = $word['pos_x'];
+ $startY = $word['pos_y'];
+ $endY = $startY + mb_strlen($word['word'], 'UTF-8') - 1;
+
+ foreach(range($startY, $endY) as $y)
+ {
+ if(array_key_exists($x, $answers) && array_key_exists($y, $answers[$x]) && !empty($answers[$x][$y])) {
+ $answer .= $answers[$x][$y];
+ }
+ else {
+ $answer .= ' ';
+ }
+ }
+ }
+ else
+ {
+ $startX = $word['pos_x'];
+ $endX = $startX + mb_strlen($word['word'], 'UTF-8') - 1;
+ $y = $word['pos_y'];
+
+ foreach(range($startX, $endX) as $x)
+ {
+ if(array_key_exists($x, $answers) && array_key_exists($y, $answers[$x]) && !empty($answers[$x][$y])) {
+ $answer .= $answers[$x][$y];
+ }
+ else {
+ $answer .= ' ';
+ }
+ }
+ }
+
+ // Check answer
+ if(mb_strtolower($word['word'], 'UTF-8') != mb_strtolower($answer, 'UTF-8')) {
+ return false;
+ }
+ }
+
+
+ // All answer right
+ return true;
+ }
+
+
+ /**
+ * Action: quest.
+ *
+ * Display a text with lists with predefined values.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param Exception $exception Character submission exception
+ */
+ public function quest($seminary, $questgroup, $quest, $character, $exception)
+ {
+ // Get words
+ $words = $this->Crossword->getWordsForQuest($quest['id']);
+
+ // Create 2D-matrix
+ $matrix = array();
+ $maxX = 0;
+ $maxY = 0;
+ foreach($words as $index => &$word)
+ {
+ if($this->request->getGetParam('show-answer') == 'true') {
+ $word['answer'] = $this->Crossword->getCharacterSubmission($word['id'], $character['id']);
+ }
+ // Insert word
+ if($word['vertical'])
+ {
+ $x = $word['pos_x'];
+ $startY = $word['pos_y'];
+ $endY = $startY + mb_strlen($word['word'], 'UTF-8') - 1;
+
+ $matrix = array_pad($matrix, $x+1, array());
+ $matrix[$x] = array_pad($matrix[$x], $endY+1, null);
+ $maxX = max($maxX, $x);
+ $maxY = max($maxY, $endY);
+
+ foreach(range($startY, $endY) as $y)
+ {
+ $matrix[$x][$y] = array(
+ 'char' => mb_substr($word['word'], $y-$startY, 1, 'UTF-8'),
+ 'indices' => (array_key_exists($x, $matrix) && array_key_exists($y, $matrix[$x]) && !is_null($matrix[$x][$y]) && array_key_exists('indices', $matrix[$x][$y])) ? $matrix[$x][$y]['indices'] : array(),
+ 'answer' => null
+ );
+ if($y == $startY) {
+ $matrix[$x][$y]['indices'][] = $index;
+ }
+ if(array_key_exists('answer', $word))
+ {
+ $answer = mb_substr($word['answer'], $y-$startY, 1, 'UTF-8');
+ if($answer != ' ') {
+ $matrix[$x][$y]['answer'] = $answer;
+ }
+ }
+ }
+ }
+ else
+ {
+ $startX = $word['pos_x'];
+ $endX = $startX + mb_strlen($word['word'], 'UTF-8') - 1;
+ $y = $word['pos_y'];
+
+ $matrix = array_pad($matrix, $endX+1, array());
+ $maxX = max($maxX, $endX);
+ $maxY = max($maxY, $y);
+
+ foreach(range($startX, $endX) as $x)
+ {
+ $matrix[$x] = array_pad($matrix[$x], $y+1, null);
+
+ $matrix[$x][$y] = array(
+ 'char' => mb_substr($word['word'], $x-$startX, 1, 'UTF-8'),
+ 'indices' => (array_key_exists($x, $matrix) && array_key_exists($y, $matrix[$x]) && !is_null($matrix[$x][$y]) && array_key_exists('indices', $matrix[$x][$y])) ? $matrix[$x][$y]['indices'] : array(),
+ 'answer' => null
+ );
+ if($x == $startX) {
+ $matrix[$x][$y]['indices'][] = $index;
+ }
+ if(array_key_exists('answer', $word))
+ {
+ $answer = mb_substr($word['answer'], $x-$startX, 1, 'UTF-8');
+ if($answer != ' ') {
+ $matrix[$x][$y]['answer'] = $answer;
+ }
+ }
+ }
+ }
+ }
+
+
+ // Pass data to view
+ $this->set('words', $words);
+ $this->set('maxX', $maxX);
+ $this->set('maxY', $maxY);
+ $this->set('matrix', $matrix);
+ }
+
+
+ /**
+ * Action: submission.
+ *
+ * Show the submission of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ */
+ public function submission($seminary, $questgroup, $quest, $character)
+ {
+ // Get words
+ $words = $this->Crossword->getWordsForQuest($quest['id']);
+
+ // Create 2D-matrix
+ $matrix = array();
+ $maxX = 0;
+ $maxY = 0;
+ foreach($words as $index => &$word)
+ {
+ // Character answer
+ $word['answer'] = $this->Crossword->getCharacterSubmission($word['id'], $character['id']);
+
+ // Insert word
+ if($word['vertical'])
+ {
+ $x = $word['pos_x'];
+ $startY = $word['pos_y'];
+ $endY = $startY + mb_strlen($word['word'], 'UTF-8') - 1;
+
+ $matrix = array_pad($matrix, $x+1, array());
+ $matrix[$x] = array_pad($matrix[$x], $endY+1, null);
+ $maxX = max($maxX, $x);
+ $maxY = max($maxY, $endY);
+
+ foreach(range($startY, $endY) as $y)
+ {
+ $matrix[$x][$y] = array(
+ 'char' => mb_substr($word['word'], $y-$startY, 1, 'UTF-8'),
+ 'indices' => (array_key_exists($x, $matrix) && array_key_exists($y, $matrix[$x]) && !is_null($matrix[$x][$y]) && array_key_exists('indices', $matrix[$x][$y])) ? $matrix[$x][$y]['indices'] : array(),
+ 'answer' => null,
+ 'right' => false
+ );
+ if($y == $startY) {
+ $matrix[$x][$y]['indices'][] = $index;
+ }
+
+ if(!is_null($word['answer']))
+ {
+ $answer = mb_substr($word['answer'], $y-$startY, 1, 'UTF-8');
+ if($answer != ' ')
+ {
+ $matrix[$x][$y]['answer'] = $answer;
+ $matrix[$x][$y]['right'] = (mb_strtolower($matrix[$x][$y]['char'], 'UTF-8') == mb_strtolower($answer, 'UTF-8'));
+ }
+ }
+ }
+ }
+ else
+ {
+ $startX = $word['pos_x'];
+ $endX = $startX + mb_strlen($word['word'], 'UTF-8') - 1;
+ $y = $word['pos_y'];
+
+ $matrix = array_pad($matrix, $endX+1, array());
+ $maxX = max($maxX, $endX);
+ $maxY = max($maxY, $y);
+
+ foreach(range($startX, $endX) as $x)
+ {
+ $matrix[$x] = array_pad($matrix[$x], $y+1, null);
+
+ $matrix[$x][$y] = array(
+ 'char' => mb_substr($word['word'], $x-$startX, 1, 'UTF-8'),
+ 'indices' => (array_key_exists($x, $matrix) && array_key_exists($y, $matrix[$x]) && !is_null($matrix[$x][$y]) && array_key_exists('indices', $matrix[$x][$y])) ? $matrix[$x][$y]['indices'] : array(),
+ 'answer' => null,
+ 'right' => false
+ );
+ if($x == $startX) {
+ $matrix[$x][$y]['indices'][] = $index;
+ }
+ if(!is_null($word['answer']))
+ {
+ $answer = mb_substr($word['answer'], $x-$startX, 1, 'UTF-8');
+ if($answer != ' ')
+ {
+ $matrix[$x][$y]['answer'] = $answer;
+ $matrix[$x][$y]['right'] = (mb_strtolower($matrix[$x][$y]['char'], 'UTF-8') == mb_strtolower($answer, 'UTF-8'));
+ }
+ }
+ }
+ }
+ }
+
+
+ // Pass data to view
+ $this->set('words', $words);
+ $this->set('maxX', $maxX);
+ $this->set('maxY', $maxY);
+ $this->set('matrix', $matrix);
+ }
+
+ }
+
+?>
diff --git a/questtypes/crossword/CrosswordQuesttypeModel.inc b/questtypes/crossword/CrosswordQuesttypeModel.inc
new file mode 100644
index 00000000..78a42821
--- /dev/null
+++ b/questtypes/crossword/CrosswordQuesttypeModel.inc
@@ -0,0 +1,93 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * Model of the CrosswordQuesttypeAgent for solving a crossword.
+ *
+ * @author Oliver Hanraths
+ */
+ class CrosswordQuesttypeModel extends \hhu\z\QuesttypeModel
+ {
+
+
+
+
+ /**
+ * Get all words for a crossword-Quest.
+ *
+ * @param int $questId ID of Quest
+ * @return array Words
+ */
+ public function getWordsForQuest($questId)
+ {
+ return $this->db->query(
+ 'SELECT id, question, word, vertical, pos_x, pos_y '.
+ 'FROM questtypes_crossword_words '.
+ 'WHERE quest_id = ? ',
+ 'i',
+ $questId
+ );
+ }
+
+
+ /**
+ * Save Character’s submitted answer for one crossword-word.
+ *
+ * @param int $regexId ID of word
+ * @param int $characterId ID of Character
+ * @param string $answer Submitted answer for this word
+ */
+ public function setCharacterSubmission($wordId, $characterId, $answer)
+ {
+ $this->db->query(
+ 'INSERT INTO questtypes_crossword_words_characters '.
+ '(questtypes_crossword_word_id, character_id, answer) '.
+ 'VALUES '.
+ '(?, ?, ?) '.
+ 'ON DUPLICATE KEY UPDATE '.
+ 'answer = ?',
+ 'iiss',
+ $wordId, $characterId, $answer,
+ $answer
+ );
+ }
+
+
+ /**
+ * Get answer of one crossword-word submitted by Character.
+ *
+ * @param int $regexId ID of lisword
+ * @param int $characterId ID of Character
+ * @return int Submitted answer for this word or null
+ */
+ public function getCharacterSubmission($wordId, $characterId)
+ {
+ $data = $this->db->query(
+ 'SELECT answer '.
+ 'FROM questtypes_crossword_words_characters '.
+ 'WHERE questtypes_crossword_word_id = ? AND character_id = ? ',
+ 'ii',
+ $wordId, $characterId
+ );
+ if(!empty($data)) {
+ return $data[0]['answer'];
+ }
+
+
+ return null;
+ }
+
+ }
+
+?>
diff --git a/questtypes/crossword/html/quest.tpl b/questtypes/crossword/html/quest.tpl
new file mode 100644
index 00000000..d202eb8f
--- /dev/null
+++ b/questtypes/crossword/html/quest.tpl
@@ -0,0 +1,49 @@
+
+
diff --git a/questtypes/crossword/html/submission.tpl b/questtypes/crossword/html/submission.tpl
new file mode 100644
index 00000000..c47e414d
--- /dev/null
+++ b/questtypes/crossword/html/submission.tpl
@@ -0,0 +1,48 @@
+
+
diff --git a/questtypes/dragndrop/DragndropQuesttypeAgent.inc b/questtypes/dragndrop/DragndropQuesttypeAgent.inc
new file mode 100644
index 00000000..c7d0abdf
--- /dev/null
+++ b/questtypes/dragndrop/DragndropQuesttypeAgent.inc
@@ -0,0 +1,24 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * QuesttypeAgent for Drag&Drop.
+ *
+ * @author Oliver Hanraths
+ */
+ class DragndropQuesttypeAgent extends \hhu\z\QuesttypeAgent
+ {
+ }
+
+?>
diff --git a/questtypes/dragndrop/DragndropQuesttypeController.inc b/questtypes/dragndrop/DragndropQuesttypeController.inc
new file mode 100644
index 00000000..9797f8fd
--- /dev/null
+++ b/questtypes/dragndrop/DragndropQuesttypeController.inc
@@ -0,0 +1,217 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * Controller of the DragndropQuesttypeAgent for Drag&Drop.
+ *
+ * @author Oliver Hanraths
+ */
+ class DragndropQuesttypeController extends \hhu\z\QuesttypeController
+ {
+ /**
+ * Required models
+ *
+ * @var array
+ */
+ public $models = array('media');
+
+
+
+
+ /**
+ * Save the answers of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param array $answers Character answers for the Quest
+ */
+ public function saveAnswersOfCharacter($seminary, $questgroup, $quest, $character, $answers)
+ {
+ // Get Drag&Drop field
+ $dndField = $this->Dragndrop->getDragndrop($quest['id']);
+
+ // Get Drops
+ $drops = $this->Dragndrop->getDrops($dndField['quest_id']);
+
+ // Save user answers
+ foreach($drops as &$drop)
+ {
+ // Determine user answer
+ $answer = null;
+ if(array_key_exists($drop['id'], $answers) && !empty($answers[$drop['id']]))
+ {
+ $a = intval(substr($answers[$drop['id']], 4));
+ if($a !== false && $a > 0) {
+ $answer = $a;
+ }
+ }
+
+ // Update database record
+ $this->Dragndrop->setCharacterSubmission($drop['id'], $character['id'], $answer);
+ }
+ }
+
+
+ /**
+ * Save additional data for the answers of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param array $data Additional (POST-) data
+ */
+ public function saveDataForCharacterAnswers($seminary, $questgroup, $quest, $character, $data)
+ {
+ }
+
+
+ /**
+ * Check if answers of a Character for a Quest match the correct ones.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @return boolean True/false for a right/wrong answer or null for moderator evaluation
+ */
+ public function matchAnswersOfCharacter($seminary, $questgroup, $quest, $character, $answers)
+ {
+ // Get Drag&Drop field
+ $dndField = $this->Dragndrop->getDragndrop($quest['id']);
+
+ // Get Drags
+ $drags = $this->Dragndrop->getDrags($dndField['quest_id'], true);
+
+ // Match drags with user answers
+ foreach($drags as &$drag)
+ {
+ $founds = array_keys($answers, 'drag'.$drag['id']);
+ if(count($founds) != 1) {
+ return false;
+ }
+ if(!$this->Dragndrop->dragMatchesDrop($drag['id'], $founds[0])) {
+ return false;
+ }
+ }
+
+
+ // Set status
+ return true;
+ }
+
+
+ /**
+ * Action: quest.
+ *
+ * Display a text with input fields and evaluate if user input
+ * matches with stored regular expressions.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param Exception $exception Character submission exception
+ */
+ public function quest($seminary, $questgroup, $quest, $character, $exception)
+ {
+ // Get Drag&Drop field
+ $dndField = $this->Dragndrop->getDragndrop($quest['id']);
+ $dndField['media'] = $this->Media->getSeminaryMediaById($dndField['questmedia_id']);
+
+ // Get Drags
+ $drags = array();
+ $dragsByIndex = $this->Dragndrop->getDrags($dndField['quest_id']);
+ foreach($dragsByIndex as &$drag) {
+ $drag['media'] = $this->Media->getSeminaryMediaById($drag['questmedia_id']);
+ $drags[$drag['id']] = $drag;
+ }
+
+ // Get Drops
+ $drops = $this->Dragndrop->getDrops($dndField['quest_id']);
+
+ // Get Character answers
+ if($this->request->getGetParam('show-answer') == 'true')
+ {
+ foreach($drops as &$drop)
+ {
+ $drop['answer'] = $this->Dragndrop->getCharacterSubmission($drop['id'], $character['id']);
+ if(!is_null($drop['answer']))
+ {
+ $drop['answer'] = $drags[$drop['answer']];
+ unset($drags[$drop['answer']['id']]);
+ }
+ }
+ }
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ $this->set('field', $dndField);
+ $this->set('drops', $drops);
+ $this->set('drags', $drags);
+ }
+
+
+ /**
+ * Action: submission.
+ *
+ * Show the submission of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ */
+ public function submission($seminary, $questgroup, $quest, $character)
+ {
+ // Get Drag&Drop field
+ $dndField = $this->Dragndrop->getDragndrop($quest['id']);
+ $dndField['media'] = $this->Media->getSeminaryMediaById($dndField['questmedia_id']);
+
+ // Get Drags
+ $drags = array();
+ $dragsByIndex = $this->Dragndrop->getDrags($dndField['quest_id']);
+ foreach($dragsByIndex as &$drag) {
+ $drag['media'] = $this->Media->getSeminaryMediaById($drag['questmedia_id']);
+ $drags[$drag['id']] = $drag;
+ }
+
+ // Get Drops
+ $drops = $this->Dragndrop->getDrops($dndField['quest_id']);
+
+ // Get Character answers
+ foreach($drops as &$drop)
+ {
+ $drop['answer'] = $this->Dragndrop->getCharacterSubmission($drop['id'], $character['id']);
+ if(!is_null($drop['answer']))
+ {
+ $drop['answer'] = $drags[$drop['answer']];
+ unset($drags[$drop['answer']['id']]);
+ }
+ }
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ $this->set('field', $dndField);
+ $this->set('drops', $drops);
+ $this->set('drags', $drags);
+ }
+
+ }
+
+?>
diff --git a/questtypes/dragndrop/DragndropQuesttypeModel.inc b/questtypes/dragndrop/DragndropQuesttypeModel.inc
new file mode 100644
index 00000000..2aadb335
--- /dev/null
+++ b/questtypes/dragndrop/DragndropQuesttypeModel.inc
@@ -0,0 +1,180 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * Model of the DragndropQuesttypeAgent for Drag&Drop.
+ *
+ * @author Oliver Hanraths
+ */
+ class DragndropQuesttypeModel extends \hhu\z\QuesttypeModel
+ {
+
+
+
+
+ /**
+ * Get Drag&Drop-field.
+ *
+ * @param int $questId ID of Quest
+ * @return array Drag&Drop-field
+ */
+ public function getDragndrop($questId)
+ {
+ $data = $this->db->query(
+ 'SELECT quest_id, questmedia_id, width, height '.
+ 'FROM questtypes_dragndrop '.
+ 'WHERE quest_id = ?',
+ 'i',
+ $questId
+ );
+ if(!empty($data)) {
+ return $data[0];
+ }
+
+
+ return null;
+ }
+
+
+ /**
+ * Get Drop-items.
+ *
+ * @param int $dragndropId ID of Drag&Drop-field
+ * @return array Drop-items
+ */
+ public function getDrops($dragndropId)
+ {
+ return $this->db->query(
+ 'SELECT id, top, `left`, width, height '. //, questtypes_dragndrop_drag_id '.
+ 'FROM questtypes_dragndrop_drops '.
+ 'WHERE questtypes_dragndrop_id = ?',
+ 'i',
+ $dragndropId
+ );
+ }
+
+
+ /**
+ * Get Drag-items.
+ *
+ * @param int $dragndropId ID of Drag&Drop-field
+ * @param boolean $onlyUsed Only Drag-items that are used for a Drop-item
+ * @return array Drag-items
+ */
+ public function getDrags($dragndropId, $onlyUsed=false)
+ {
+ return $this->db->query(
+ 'SELECT id, questmedia_id '.
+ 'FROM questtypes_dragndrop_drags '.
+ 'WHERE questtypes_dragndrop_id = ?'.
+ ($onlyUsed
+ ? ' AND EXISTS ('.
+ 'SELECT questtypes_dragndrop_drag_id '.
+ 'FROM questtypes_dragndrop_drops_drags '.
+ 'WHERE questtypes_dragndrop_drag_id = questtypes_dragndrop_drags.id'.
+ ')'
+ : null
+ ),
+ 'i',
+ $dragndropId
+ );
+ }
+
+
+ /**
+ * Check if a Drag-item mathes a Drop-item.
+ *
+ * @param int $dragId ID of Drag-field
+ * @param int $dropId ID of Drop-field
+ * @return boolean Drag-item is valid for Drop-item
+ */
+ public function dragMatchesDrop($dragId, $dropId)
+ {
+ $data = $this->db->query(
+ 'SELECT count(*) AS c '.
+ 'FROM questtypes_dragndrop_drops_drags '.
+ 'WHERE questtypes_dragndrop_drop_id = ? AND questtypes_dragndrop_drag_id = ?',
+ 'ii',
+ $dropId, $dragId
+ );
+ if(!empty($data)) {
+ return ($data[0]['c'] > 0);
+ }
+
+
+ return false;
+ }
+
+
+ /**
+ * Save Character’s submitted answer for one Drop-field.
+ *
+ * @param int $dropId ID of Drop-field
+ * @param int $characterId ID of Character
+ * @param string $answer Submitted Drag-field-ID for this field
+ */
+ public function setCharacterSubmission($dropId, $characterId, $answer)
+ {
+ if(is_null($answer))
+ {
+ $this->db->query(
+ 'DELETE FROM questtypes_dragndrop_drops_characters '.
+ 'WHERE questtypes_dragndrop_drop_id = ? AND character_id = ?',
+ 'ii',
+ $dropId, $characterId
+ );
+ }
+ else
+ {
+ $this->db->query(
+ 'INSERT INTO questtypes_dragndrop_drops_characters '.
+ '(questtypes_dragndrop_drop_id, character_id, questtypes_dragndrop_drag_id) '.
+ 'VALUES '.
+ '(?, ?, ?) '.
+ 'ON DUPLICATE KEY UPDATE '.
+ 'questtypes_dragndrop_drag_id = ?',
+ 'iiii',
+ $dropId, $characterId, $answer, $answer
+ );
+ }
+ }
+
+
+ /**
+ * Get Character’s saved answer for one Drop-field.
+ *
+ * @param int $dropId ID of Drop-field
+ * @param int $characterId ID of Character
+ * @return int ID of Drag-field or null
+ */
+ public function getCharacterSubmission($dropId, $characterId)
+ {
+ $data = $this->db->query(
+ 'SELECT questtypes_dragndrop_drag_id '.
+ 'FROM questtypes_dragndrop_drops_characters '.
+ 'WHERE questtypes_dragndrop_drop_id = ? AND character_id = ?',
+ 'ii',
+ $dropId, $characterId
+ );
+ if(!empty($data)) {
+ return $data[0]['questtypes_dragndrop_drag_id'];
+ }
+
+
+ return null;
+ }
+
+ }
+
+?>
diff --git a/questtypes/dragndrop/html/quest.tpl b/questtypes/dragndrop/html/quest.tpl
new file mode 100644
index 00000000..4cb10b60
--- /dev/null
+++ b/questtypes/dragndrop/html/quest.tpl
@@ -0,0 +1,17 @@
+
diff --git a/questtypes/dragndrop/html/submission.tpl b/questtypes/dragndrop/html/submission.tpl
new file mode 100644
index 00000000..c136fe1e
--- /dev/null
+++ b/questtypes/dragndrop/html/submission.tpl
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/questtypes/multiplechoice/MultiplechoiceQuesttypeAgent.inc b/questtypes/multiplechoice/MultiplechoiceQuesttypeAgent.inc
new file mode 100644
index 00000000..2789c5f6
--- /dev/null
+++ b/questtypes/multiplechoice/MultiplechoiceQuesttypeAgent.inc
@@ -0,0 +1,24 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * QuesttypeAgent for multiple choice.
+ *
+ * @author Oliver Hanraths
+ */
+ class MultiplechoiceQuesttypeAgent extends \hhu\z\QuesttypeAgent
+ {
+ }
+
+?>
diff --git a/questtypes/multiplechoice/MultiplechoiceQuesttypeController.inc b/questtypes/multiplechoice/MultiplechoiceQuesttypeController.inc
new file mode 100644
index 00000000..b025f758
--- /dev/null
+++ b/questtypes/multiplechoice/MultiplechoiceQuesttypeController.inc
@@ -0,0 +1,276 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * Controller of the MultiplechoiceQuesttypeAgent multiple choice.
+ *
+ * @author Oliver Hanraths
+ */
+ class MultiplechoiceQuesttypeController extends \hhu\z\QuesttypeController
+ {
+
+
+
+
+ /**
+ * Save the answers of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param array $answers Character answers for the Quest
+ */
+ public function saveAnswersOfCharacter($seminary, $questgroup, $quest, $character, $answers)
+ {
+ // Save temporary user answer of last question
+ $answers = (!is_array($answers)) ? array() : $answers;
+ $pos = $this->Multiplechoice->getQuestionsCountOfQuest($quest['id']);
+ $question = $this->Multiplechoice->getQuestionOfQuest($quest['id'], $pos);
+ $this->saveUserAnswers($quest['id'], $question['id'], $answers);
+
+ // Save answers
+ $questions = $this->Multiplechoice->getQuestionsOfQuest($quest['id']);
+ foreach($questions as &$question)
+ {
+ $userAnswers = $this->getUserAnswers($quest['id'], $question['id']);
+ $answers = $this->Multiplechoice->getAnswersOfQuestion($question['id']);
+ foreach($answers as &$answer)
+ {
+ $userAnswer = (array_key_exists($answer['pos']-1, $userAnswers)) ? true : false;
+ $this->Multiplechoice->setCharacterSubmission($answer['id'], $character['id'], $userAnswer);
+ }
+ }
+ }
+
+
+ /**
+ * Save additional data for the answers of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param array $data Additional (POST-) data
+ */
+ public function saveDataForCharacterAnswers($seminary, $questgroup, $quest, $character, $data)
+ {
+ }
+
+
+ /**
+ * Check if answers of a Character for a Quest match the correct ones.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @return boolean True/false for a right/wrong answer or null for moderator evaluation
+ */
+ public function matchAnswersOfCharacter($seminary, $questgroup, $quest, $character, $answers)
+ {
+ // Save temporary user answer of last question
+ $answers = (!is_array($answers)) ? array() : $answers;
+ $pos = $this->Multiplechoice->getQuestionsCountOfQuest($quest['id']);
+ $question = $this->Multiplechoice->getQuestionOfQuest($quest['id'], $pos);
+ $this->saveUserAnswers($quest['id'], $question['id'], $answers);
+
+ // Get questions
+ $questions = $this->Multiplechoice->getQuestionsOfQuest($quest['id']);
+
+ // Iterate questions
+ foreach($questions as &$question)
+ {
+ // Get answers
+ $userAnswers = $this->getUserAnswers($quest['id'], $question['id']);
+ $answers = $this->Multiplechoice->getAnswersOfQuestion($question['id']);
+
+ // Match answers with user answers
+ foreach($answers as &$answer)
+ {
+ if(is_null($answer['tick'])) {
+ continue;
+ }
+ if($answer['tick']) {
+ if(!array_key_exists($answer['pos']-1, $userAnswers)) {
+ return false;
+ }
+ }
+ else {
+ if(array_key_exists($answer['pos']-1, $userAnswers)) {
+ return false;
+ }
+ }
+ }
+ }
+
+
+ // All questions correct answerd
+ return true;
+ }
+
+
+ /**
+ * Action: quest.
+ *
+ * Display questions with a checkbox to let the user choose the
+ * right ones.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param Exception $exception Character submission exception
+ */
+ public function quest($seminary, $questgroup, $quest, $character, $exception)
+ {
+ // Get count of questions
+ $count = $this->Multiplechoice->getQuestionsCountOfQuest($quest['id']);
+
+ // Get position
+ $pos = 1;
+ if($this->request->getRequestMethod() == 'POST' && !is_null($this->request->getPostParam('submit-answer')))
+ {
+ if(!is_null($this->request->getPostParam('question')))
+ {
+ // Get current position
+ $pos = intval($this->request->getPostParam('question'));
+ if($pos < 0 || $pos > $count) {
+ throw new \nre\exceptions\ParamsNotValidException($pos);
+ }
+
+ // Save temporary answer of user
+ $question = $this->Multiplechoice->getQuestionOfQuest($quest['id'], $pos);
+ $answers = ($this->request->getRequestMethod() == 'POST' && !is_null($this->request->getPostParam('answers'))) ? $this->request->getPostParam('answers') : array();
+ $this->saveUserAnswers($quest['id'], $question['id'], $answers);
+
+ // Go to next position
+ $pos++;
+ }
+ else {
+ throw new \nre\exceptions\ParamsNotValidException('pos');
+ }
+ }
+
+ // Get current question
+ $question = $this->Multiplechoice->getQuestionOfQuest($quest['id'], $pos);
+
+ // Get answers
+ $question['answers'] = $this->Multiplechoice->getAnswersOfQuestion($question['id']);
+
+
+ // Get previous user answers
+ if($this->request->getGetParam('show-answer') == 'true')
+ {
+ foreach($question['answers'] as &$answer) {
+ $answer['useranswer'] = $this->Multiplechoice->getCharacterSubmission($answer['id'], $character['id']);
+ }
+ }
+
+
+ // Pass data to view
+ $this->set('question', $question);
+ $this->set('pos', $pos);
+ $this->set('count', $count);
+ }
+
+
+ /**
+ * Action: submission.
+ *
+ * Show the submission of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ */
+ public function submission($seminary, $questgroup, $quest, $character)
+ {
+ // Get questions
+ $questions = $this->Multiplechoice->getQuestionsOfQuest($quest['id']);
+
+ // Get answers
+ foreach($questions as &$question)
+ {
+ $question['answers'] = $this->Multiplechoice->getAnswersOfQuestion($question['id']);
+
+ // Get user answers
+ foreach($question['answers'] as &$answer) {
+ $answer['useranswer'] = $this->Multiplechoice->getCharacterSubmission($answer['id'], $character['id']);
+ }
+ }
+
+
+ // Pass data to view
+ $this->set('questions', $questions);
+ }
+
+
+
+
+ /**
+ * Save the answers of a user for a question temporary in the
+ * session.
+ *
+ * @param int $questId ID of Quest
+ * @param int $questionId ID of multiple choice question
+ * @param array $userAnswers Answers of user for the question
+ */
+ private function saveUserAnswers($questId, $questionId, $userAnswers)
+ {
+ // Ensure session structure
+ if(!array_key_exists('answers', $_SESSION)) {
+ $_SESSION['answers'] = array();
+ }
+ if(!array_key_exists($questId, $_SESSION['answers'])) {
+ $_SESSION['answers'][$questId] = array();
+ }
+ $_SESSION['answers'][$questId][$questionId] = array();
+
+ // Save answres
+ foreach($userAnswers as $pos => &$answer) {
+ $_SESSION['answers'][$questId][$questionId][$pos] = $answer;
+ }
+ }
+
+
+ /**
+ * Get the temporary saved answers of a user for a question.
+ *
+ * @param int $questId ID of Quest
+ * @param int $questionId ID of multiple choice question
+ * @return array Answers of user for the question
+ */
+ private function getUserAnswers($questId, $questionId)
+ {
+ // Ensure session structure
+ if(!array_key_exists('answers', $_SESSION)) {
+ $_SESSION['answers'] = array();
+ }
+ if(!array_key_exists($questId, $_SESSION['answers'])) {
+ $_SESSION['answers'][$questId] = array();
+ }
+ if(!array_key_exists($questionId, $_SESSION['answers'][$questId])) {
+ $_SESSION['answers'][$questId][$questionId] = array();
+ }
+
+
+ // Return answers
+ return $_SESSION['answers'][$questId][$questionId];
+ }
+
+ }
+
+?>
diff --git a/questtypes/multiplechoice/MultiplechoiceQuesttypeModel.inc b/questtypes/multiplechoice/MultiplechoiceQuesttypeModel.inc
new file mode 100644
index 00000000..6c4931b7
--- /dev/null
+++ b/questtypes/multiplechoice/MultiplechoiceQuesttypeModel.inc
@@ -0,0 +1,159 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * Model of the MultiplechoiceQuesttypeAgent for multiple choice.
+ *
+ * @author Oliver Hanraths
+ */
+ class MultiplechoiceQuesttypeModel extends \hhu\z\QuesttypeModel
+ {
+
+
+
+
+ /**
+ * Get the count of multiple choice questions for a Quest.
+ *
+ * @param int $questId ID of Quest to get count for
+ * @return int Conut of questions
+ */
+ public function getQuestionsCountOfQuest($questId)
+ {
+ $data = $this->db->query(
+ 'SELECT count(id) AS c '.
+ 'FROM questtypes_multiplechoice '.
+ 'WHERE quest_id = ?',
+ 'i',
+ $questId
+ );
+ if(!empty($data)) {
+ return $data[0]['c'];
+ }
+
+
+ return 0;
+ }
+
+
+ /**
+ * Get all multiple choice questions of a Quest.
+ *
+ * @param int $questId ID of Quest
+ * @return array Multiple choice questions
+ */
+ public function getQuestionsOfQuest($questId)
+ {
+ return $this->db->query(
+ 'SELECT id, pos, question '.
+ 'FROM questtypes_multiplechoice '.
+ 'WHERE quest_id = ?',
+ 'i',
+ $questId
+ );
+ }
+
+
+ /**
+ * Get one multiple choice question of a Quest.
+ *
+ * @param int $questId ID of Quest
+ * @param int $pos Position of question
+ * @return array Question data
+ */
+ public function getQuestionOfQuest($questId, $pos)
+ {
+ $data = $this->db->query(
+ 'SELECT id, pos, question '.
+ 'FROM questtypes_multiplechoice '.
+ 'WHERE quest_id = ? AND pos = ?',
+ 'ii',
+ $questId, $pos
+ );
+ if(!empty($data)) {
+ return $data[0];
+ }
+
+
+ return null;
+ }
+
+
+ /**
+ * Get all answers of a multiple choice question.
+ *
+ * @param int $questionId ID of multiple choice question
+ * @return array Answers of question
+ */
+ public function getAnswersOfQuestion($questionId)
+ {
+ return $this->db->query(
+ 'SELECT id, pos, answer, tick '.
+ 'FROM questtypes_multiplechoice_answers '.
+ 'WHERE questtypes_multiplechoice_id = ?',
+ 'i',
+ $questionId
+ );
+ }
+
+
+ /**
+ * Save Character’s submitted answer for one option.
+ *
+ * @param int $answerId ID of multiple choice answer
+ * @param int $characterId ID of Character
+ * @param boolean $answer Submitted answer for this option
+ */
+ public function setCharacterSubmission($answerId, $characterId, $answer)
+ {
+ $this->db->query(
+ 'INSERT INTO questtypes_multiplechoice_characters '.
+ '(questtypes_multiplechoice_answer_id, character_id, ticked) '.
+ 'VALUES '.
+ '(?, ?, ?) '.
+ 'ON DUPLICATE KEY UPDATE '.
+ 'ticked = ?',
+ 'iiii',
+ $answerId, $characterId, $answer, $answer
+ );
+ }
+
+
+ /**
+ * Get answer of one option submitted by Character.
+ *
+ * @param int $answerId ID of multiple choice answer
+ * @param int $characterId ID of Character
+ * @return boolean Submitted answer of Character or false
+ */
+ public function getCharacterSubmission($answerId, $characterId)
+ {
+ $data = $this->db->query(
+ 'SELECT ticked '.
+ 'FROM questtypes_multiplechoice_characters '.
+ 'WHERE questtypes_multiplechoice_answer_id = ? AND character_id = ? ',
+ 'ii',
+ $answerId, $characterId
+ );
+ if(!empty($data)) {
+ return $data[0]['ticked'];
+ }
+
+
+ return false;
+ }
+
+ }
+
+?>
diff --git a/questtypes/multiplechoice/html/quest.tpl b/questtypes/multiplechoice/html/quest.tpl
new file mode 100644
index 00000000..eba80286
--- /dev/null
+++ b/questtypes/multiplechoice/html/quest.tpl
@@ -0,0 +1,21 @@
+
diff --git a/questtypes/multiplechoice/html/submission.tpl b/questtypes/multiplechoice/html/submission.tpl
new file mode 100644
index 00000000..ebbbd493
--- /dev/null
+++ b/questtypes/multiplechoice/html/submission.tpl
@@ -0,0 +1,16 @@
+
+ &$question) : ?>
+
+ =$t->t($question['question'])?>
+
+
+
+ ☑☐
+ ✓×
+ =$answer['answer']?>
+
+
+
+
+
+
diff --git a/questtypes/submit/SubmitQuesttypeAgent.inc b/questtypes/submit/SubmitQuesttypeAgent.inc
new file mode 100644
index 00000000..68ca9ae5
--- /dev/null
+++ b/questtypes/submit/SubmitQuesttypeAgent.inc
@@ -0,0 +1,24 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * QuesttypeAgent for submitting.
+ *
+ * @author Oliver Hanraths
+ */
+ class SubmitQuesttypeAgent extends \hhu\z\QuesttypeAgent
+ {
+ }
+
+?>
diff --git a/questtypes/submit/SubmitQuesttypeController.inc b/questtypes/submit/SubmitQuesttypeController.inc
new file mode 100644
index 00000000..e064c542
--- /dev/null
+++ b/questtypes/submit/SubmitQuesttypeController.inc
@@ -0,0 +1,221 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * Controller of the SubmitQuesttypeAgent for a submit task.
+ *
+ * @author Oliver Hanraths
+ */
+ class SubmitQuesttypeController extends \hhu\z\QuesttypeController
+ {
+ /**
+ * Required models
+ *
+ * @var array
+ */
+ public $models = array('quests', 'uploads', 'users');
+
+
+
+
+ /**
+ * Save the answers of a Character for a Quest.
+ *
+ * @throws SubmissionNotValidException
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param array $answers Character answers for the Quest
+ */
+ public function saveAnswersOfCharacter($seminary, $questgroup, $quest, $character, $answers)
+ {
+ // Save answer
+ if(array_key_exists('answers', $_FILES))
+ {
+ $answer = $_FILES['answers'];
+
+ // Check error
+ if($answer['error'] !== 0) {
+ throw new \hhu\z\exceptions\SubmissionNotValidException(
+ new \hhu\z\exceptions\FileUploadException($answer['error'])
+ );
+ }
+
+ // Check mimetype
+ $mimetypes = $this->Submit->getAllowedMimetypes($seminary['id']);
+ $answerMimetype = null;
+ foreach($mimetypes as &$mimetype) {
+ if($mimetype['mimetype'] == $answer['type']) {
+ $answerMimetype = $mimetype;
+ break;
+ }
+ }
+ if(is_null($answerMimetype)) {
+ throw new \hhu\z\exceptions\SubmissionNotValidException(
+ new \hhu\z\exceptions\WrongFiletypeException($answer['type'])
+ );
+ }
+
+ // Check file size
+ if($answer['size'] > $answerMimetype['size']) {
+ throw new \hhu\z\exceptions\SubmissionNotValidException(
+ new \hhu\z\exceptions\MaxFilesizeException()
+ );
+ }
+
+ // Create filename
+ $filename = sprintf(
+ '%s,%s,%s.%s',
+ $character['url'],
+ mb_substr($quest['url'], 0, 32),
+ date('Ymd-His'),
+ mb_substr(mb_substr($answer['name'], strrpos($answer['name'], '.')+1), 0, 4)
+ );
+
+ // Save file
+ if(!$this->Submit->setCharacterSubmission($seminary['id'], $quest['id'], $this->Auth->getUserId(), $character['id'], $answer, $filename)) {
+ throw new \hhu\z\exceptions\SubmissionNotValidException(
+ new \hhu\z\exceptions\FileUploadException(error_get_last()['message'])
+ );
+ }
+ }
+ }
+
+
+ /**
+ * Save additional data for the answers of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param array $data Additional (POST-) data
+ */
+ public function saveDataForCharacterAnswers($seminary, $questgroup, $quest, $character, $data)
+ {
+ $this->Submit->addCommentForCharacterAnswer($this->Auth->getUserId(), $data['submission_id'], $data['comment']);
+ }
+
+
+ /**
+ * Check if answers of a Character for a Quest match the correct ones.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param array $answers Character answers for the Quest
+ * @return boolean True/false for a right/wrong answer or null for moderator evaluation
+ */
+ public function matchAnswersOfCharacter($seminary, $questgroup, $quest, $character, $answers)
+ {
+ // A moderator has to evaluate the answer
+ return null;
+ }
+
+
+ /**
+ * Action: quest.
+ *
+ * Display a big textbox to let the user enter a text that has
+ * to be evaluated by a moderator.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param Exception $exception Character submission exception
+ */
+ public function quest($seminary, $questgroup, $quest, $character, $exception)
+ {
+ // Answers (Submissions)
+ $characterSubmissions = $this->Submit->getCharacterSubmissions($quest['id'], $character['id']);
+ foreach($characterSubmissions as &$submission)
+ {
+ $submission['upload'] = $this->Uploads->getSeminaryuploadById($submission['upload_id']);
+ $submission['comments'] = $this->Submit->getCharacterSubmissionComments($submission['id']);
+ foreach($submission['comments'] as &$comment)
+ {
+ try {
+ $comment['user'] = $this->Users->getUserById($comment['created_user_id']);
+ $comment['user']['character'] = $this->Characters->getCharacterForUserAndSeminary($comment['user']['id'], $seminary['id']);
+ }
+ catch(\nre\exceptions\IdNotFoundException $e) {
+ }
+ }
+ }
+
+ // Has Character already solved Quest?
+ $solved = $this->Quests->hasCharacterSolvedQuest($quest['id'], $character['id']);
+
+ // Last Quest status for Character
+ $lastStatus = $this->Quests->getLastQuestStatus($quest['id'], $character['id']);
+
+ // Get allowed mimetypes
+ $mimetypes = $this->Submit->getAllowedMimetypes($seminary['id']);
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ $this->set('submissions', $characterSubmissions);
+ $this->set('solved', $solved);
+ $this->set('lastStatus', $lastStatus);
+ $this->set('mimetypes', $mimetypes);
+ $this->set('exception', $exception);
+ }
+
+
+ /**
+ * Action: submission.
+ *
+ * Show the submission of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ */
+ public function submission($seminary, $questgroup, $quest, $character)
+ {
+ // Get Character submissions
+ $submissions = $this->Submit->getCharacterSubmissions($quest['id'], $character['id']);
+ foreach($submissions as &$submission)
+ {
+ $submission['upload'] = $this->Uploads->getSeminaryuploadById($submission['upload_id']);
+ $submission['comments'] = $this->Submit->getCharacterSubmissionComments($submission['id']);
+ foreach($submission['comments'] as &$comment)
+ {
+ try {
+ $comment['user'] = $this->Users->getUserById($comment['created_user_id']);
+ $comment['user']['character'] = $this->Characters->getCharacterForUserAndSeminary($comment['user']['id'], $seminary['id']);
+ }
+ catch(\nre\exceptions\IdNotFoundException $e) {
+ }
+ }
+ }
+
+ // Status
+ $solved = $this->Quests->hasCharacterSolvedQuest($quest['id'], $character['id']);
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ $this->set('submissions', $submissions);
+ $this->set('solved', $solved);
+ }
+
+ }
+
+?>
diff --git a/questtypes/submit/SubmitQuesttypeModel.inc b/questtypes/submit/SubmitQuesttypeModel.inc
new file mode 100644
index 00000000..5d821655
--- /dev/null
+++ b/questtypes/submit/SubmitQuesttypeModel.inc
@@ -0,0 +1,142 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * Model of the SubmitQuesttypeAgent for a submit task.
+ *
+ * @author Oliver Hanraths
+ */
+ class SubmitQuesttypeModel extends \hhu\z\QuesttypeModel
+ {
+ /**
+ * Required models
+ *
+ * @var array
+ */
+ public $models = array('uploads');
+
+
+
+
+ /**
+ * Save Character’s submitted upload.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @param int $questId ID of Quest
+ * @param int $characterId ID of Character
+ * @param array $file Submitted upload
+ */
+ public function setCharacterSubmission($seminaryId, $questId, $userId, $characterId, $file, $filename)
+ {
+ // Save file on harddrive
+ $uploadId = $this->Uploads->uploadSeminaryFile($userId, $seminaryId, $file['name'], $filename, $file['tmp_name'], $file['type']);
+ if($uploadId === false) {
+ return false;
+ }
+
+ // Create database record
+ $this->db->query(
+ 'INSERT INTO questtypes_submit_characters '.
+ '(quest_id, character_id, upload_id) '.
+ 'VALUES '.
+ '(?, ?, ?) ',
+ 'iii',
+ $questId, $characterId, $uploadId
+ );
+
+
+ return true;
+ }
+
+
+ /**
+ * Add a comment for the answer of a Character.
+ *
+ * @param int $userId ID of user that comments
+ * @param int $submissionId ID of Character answer to comment
+ * @param string $comment Comment text
+ */
+ public function addCommentForCharacterAnswer($userId, $submissionId, $comment)
+ {
+ $this->db->query(
+ 'INSERT INTO questtypes_submit_characters_comments '.
+ '(created_user_id, questtypes_submit_character_id, comment) '.
+ 'VALUES '.
+ '(?, ?, ?)',
+ 'iis',
+ $userId,
+ $submissionId,
+ $comment
+ );
+ }
+
+
+ /**
+ * Get all uploads submitted by Character.
+ *
+ * @param int $questId ID of Quest
+ * @param int $characterId ID of Character
+ * @return array Text submitted by Character or NULL
+ */
+ public function getCharacterSubmissions($questId, $characterId)
+ {
+ return $this->db->query(
+ 'SELECT id, created, upload_id '.
+ 'FROM questtypes_submit_characters '.
+ 'WHERE quest_id = ? AND character_id = ? '.
+ 'ORDER BY created ASC',
+ 'ii',
+ $questId, $characterId
+ );
+ }
+
+
+ /**
+ * Get allowed mimetypes for uploading a file.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @return array Allowed mimetypes
+ */
+ public function getAllowedMimetypes($seminaryId)
+ {
+ return $this->db->query(
+ 'SELECT id, mimetype, size '.
+ 'FROM questtypes_submit_mimetypes '.
+ 'WHERE seminary_id = ?',
+ 'i',
+ $seminaryId
+ );
+ }
+
+
+ /**
+ * Get all comments for a Character submission.
+ *
+ * @param int $characterSubmissionId ID of Character submission
+ * @return array Comments for this submission
+ */
+ public function getCharacterSubmissionComments($characterSubmissionId)
+ {
+ return $this->db->query(
+ 'SELECT id, created, created_user_id, comment '.
+ 'FROM questtypes_submit_characters_comments '.
+ 'WHERE questtypes_submit_character_id = ?',
+ 'i',
+ $characterSubmissionId
+ );
+ }
+
+ }
+
+?>
diff --git a/questtypes/submit/html/quest.tpl b/questtypes/submit/html/quest.tpl
new file mode 100644
index 00000000..4581c40d
--- /dev/null
+++ b/questtypes/submit/html/quest.tpl
@@ -0,0 +1,52 @@
+
+
+ getNestedException() instanceof \hhu\z\exceptions\WrongFiletypeException) : ?>
+ =sprintf(_('File has wrong type “%s”'), $exception->getNestedException()->getType())?>
+ getNestedException() instanceof \hhu\z\exceptions\WrongFiletypeException) : ?>
+ =_('File exceeds size maximum')?>
+ getNestedException() instanceof \hhu\z\exceptions\FileUploadException) : ?>
+ =sprintf(_('Error during file upload: %s'), $exception->getNestedException()->getNestedMessage())?>
+
+ =$exception->getNestedException()->getMessage()?>
+
+
+
+ $submissions[count($submissions)-1]['created'])) : ?>
+
+
+
+ 0) : ?>
+=_('Past submissions')?>
+
+
+
+ =$submission['upload']['name']?>
+ =sprintf(_('submitted at %s on %s h'), $dateFormatter->format(new \DateTime($submission['created'])), $timeFormatter->format(new \DateTime($submission['created'])))?>
+
+ =_('This submission is waiting for approval')?>
+
+ 0) : ?>
+
+
+
+
+ =sprintf(_('Comment from %s on %s at %s'), $comment['user']['character']['name'], $dateFormatter->format(new \DateTime($comment['created'])), $timeFormatter->format(new \DateTime($comment['created'])))?>:
+
+ =\hhu\z\Utils::t($comment['comment'])?>
+
+
+
+
+
+
+
+
diff --git a/questtypes/submit/html/submission.tpl b/questtypes/submit/html/submission.tpl
new file mode 100644
index 00000000..f3c748ef
--- /dev/null
+++ b/questtypes/submit/html/submission.tpl
@@ -0,0 +1,33 @@
+ 0) : ?>
+
+
+
+ =$submission['upload']['name']?>
+ =sprintf(_('submitted at %s on %s h'), $dateFormatter->format(new \DateTime($submission['created'])), $timeFormatter->format(new \DateTime($submission['created'])))?>
+ 0) : ?>
+
+
+
+
+ =sprintf(_('Comment from %s on %s at %s'), $comment['user']['character']['name'], $dateFormatter->format(new \DateTime($comment['created'])), $timeFormatter->format(new \DateTime($comment['created'])))?>:
+
+ =\hhu\z\Utils::t($comment['comment'])?>
+
+
+
+
+
+
+
+
+
+
diff --git a/questtypes/textinput/TextinputQuesttypeAgent.inc b/questtypes/textinput/TextinputQuesttypeAgent.inc
new file mode 100644
index 00000000..8144c148
--- /dev/null
+++ b/questtypes/textinput/TextinputQuesttypeAgent.inc
@@ -0,0 +1,24 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * QuesttypeAgent for inserting text.
+ *
+ * @author Oliver Hanraths
+ */
+ class TextinputQuesttypeAgent extends \hhu\z\QuesttypeAgent
+ {
+ }
+
+?>
diff --git a/questtypes/textinput/TextinputQuesttypeController.inc b/questtypes/textinput/TextinputQuesttypeController.inc
new file mode 100644
index 00000000..02db2d8b
--- /dev/null
+++ b/questtypes/textinput/TextinputQuesttypeController.inc
@@ -0,0 +1,185 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * Controller of the TextinputQuesttypeAgent for for inserting text.
+ *
+ * @author Oliver Hanraths
+ */
+ class TextinputQuesttypeController extends \hhu\z\QuesttypeController
+ {
+
+
+
+
+ /**
+ * Save the answers of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param array $answers Character answers for the Quest
+ */
+ public function saveAnswersOfCharacter($seminary, $questgroup, $quest, $character, $answers)
+ {
+ // Get regexs
+ $regexs = $this->Textinput->getTextinputRegexs($quest['id']);
+
+ // Save answers
+ foreach($regexs as &$regex)
+ {
+ $pos = intval($regex['number']) - 1;
+ $answer = (array_key_exists($pos, $answers)) ? $answers[$pos] : '';
+ $this->Textinput->setCharacterSubmission($regex['id'], $character['id'], $answer);
+ }
+ }
+
+
+ /**
+ * Save additional data for the answers of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param array $data Additional (POST-) data
+ */
+ public function saveDataForCharacterAnswers($seminary, $questgroup, $quest, $character, $data)
+ {
+ }
+
+
+ /**
+ * Check if answers of a Character for a Quest match the correct ones.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @return boolean True/false for a right/wrong answer or null for moderator evaluation
+ */
+ public function matchAnswersOfCharacter($seminary, $questgroup, $quest, $character, $answers)
+ {
+ // Get right answers
+ $regexs = $this->Textinput->getTextinputRegexs($quest['id']);
+
+ // Match regexs with user answers
+ $allSolved = true;
+ foreach($regexs as $i => &$regex)
+ {
+ if(!array_key_exists($i, $answers))
+ {
+ $allSolved = false;
+ break;
+ }
+
+ if(!$this->isMatching($regex['regex'], $answers[$i]))
+ {
+ $allSolved = false;
+ break;
+ }
+ }
+
+
+ // Set status
+ return $allSolved;
+ }
+
+
+ /**
+ * Action: quest.
+ *
+ * Display a text with input fields and evaluate if user input
+ * matches with stored regular expressions.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param Exception $exception Character submission exception
+ */
+ public function quest($seminary, $questgroup, $quest, $character, $exception)
+ {
+ // Get Task
+ $task = $this->Textinput->getTextinputQuest($quest['id']);
+
+ // Process text
+ $textParts = preg_split('/(\$\$)/', ' '.$task['text'].' ', -1, PREG_SPLIT_NO_EMPTY);
+
+ // Get Character answers
+ $regexs = null;
+ if($this->request->getGetParam('show-answer') == 'true')
+ {
+ $regexs = $this->Textinput->getTextinputRegexs($quest['id']);
+ foreach($regexs as &$regex) {
+ $regex['answer'] = $this->Textinput->getCharacterSubmission($regex['id'], $character['id']);
+ }
+ }
+
+ // Has Character already solved Quest?
+ $solved = $this->Quests->hasCharacterSolvedQuest($quest['id'], $character['id']);
+
+
+ // Pass data to view
+ $this->set('texts', $textParts);
+ $this->set('regexs', $regexs);
+ }
+
+
+ /**
+ * Action: submission.
+ *
+ * Show the submission of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ */
+ public function submission($seminary, $questgroup, $quest, $character)
+ {
+ // Get Task
+ $task = $this->Textinput->getTextinputQuest($quest['id']);
+
+ // Process text
+ $textParts = preg_split('/(\$\$)/', $task['text'], -1, PREG_SPLIT_NO_EMPTY);
+
+ // Get Character answers
+ $regexs = $this->Textinput->getTextinputRegexs($quest['id']);
+ foreach($regexs as &$regex) {
+ $regex['answer'] = $this->Textinput->getCharacterSubmission($regex['id'], $character['id']);
+ $regex['right'] = $this->isMatching($regex['regex'], $regex['answer']);
+ }
+
+
+ // Pass data to view
+ $this->set('texts', $textParts);
+ $this->set('regexs', $regexs);
+ }
+
+
+
+
+ private function isMatching($regex, $answer)
+ {
+ $score = preg_match($regex, $answer);
+
+
+ return ($score !== false && $score > 0);
+ }
+
+ }
+
+?>
diff --git a/questtypes/textinput/TextinputQuesttypeModel.inc b/questtypes/textinput/TextinputQuesttypeModel.inc
new file mode 100644
index 00000000..3d00d038
--- /dev/null
+++ b/questtypes/textinput/TextinputQuesttypeModel.inc
@@ -0,0 +1,117 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * Model of the TextinputQuesttypeAgent for inserting text.
+ *
+ * @author Oliver Hanraths
+ */
+ class TextinputQuesttypeModel extends \hhu\z\QuesttypeModel
+ {
+
+
+
+
+ /**
+ * Get textinput-text for a Quest.
+ *
+ * @param int $questId ID of Quest
+ * @return array Textinput-text
+ */
+ public function getTextinputQuest($questId)
+ {
+ $data = $this->db->query(
+ 'SELECT text '.
+ 'FROM questtypes_textinput '.
+ 'WHERE quest_id = ?',
+ 'i',
+ $questId
+ );
+ if(!empty($data)) {
+ return $data[0];
+ }
+
+
+ return null;
+ }
+
+
+ /**
+ * Get regular expressions for a textinput-text.
+ *
+ * @param int $questId ID of Quest
+ * @return array Regexs
+ */
+ public function getTextinputRegexs($questId)
+ {
+ return $this->db->query(
+ 'SELECT id, number, regex '.
+ 'FROM questtypes_textinput_regexs '.
+ 'WHERE questtypes_textinput_quest_id = ? '.
+ 'ORDER BY number ASC',
+ 'i',
+ $questId
+ );
+ }
+
+
+ /**
+ * Save Character’s submitted answer for one textinput field.
+ *
+ * @param int $regexId ID of regex
+ * @param int $characterId ID of Character
+ * @param string $answer Submitted answer for this field
+ */
+ public function setCharacterSubmission($regexId, $characterId, $answer)
+ {
+ $this->db->query(
+ 'INSERT INTO questtypes_textinput_regexs_characters '.
+ '(questtypes_textinput_regex_id, character_id, value) '.
+ 'VALUES '.
+ '(?, ?, ?) '.
+ 'ON DUPLICATE KEY UPDATE '.
+ 'value = ?',
+ 'iiss',
+ $regexId, $characterId, $answer, $answer
+ );
+ }
+
+
+ /**
+ * Get answer of one regex input field submitted by Character.
+ *
+ * @param int $regexId ID of regex
+ * @param int $characterId ID of Character
+ * @return string Submitted answer for this field or empty string
+ */
+ public function getCharacterSubmission($regexId, $characterId)
+ {
+ $data = $this->db->query(
+ 'SELECT value '.
+ 'FROM questtypes_textinput_regexs_characters '.
+ 'WHERE questtypes_textinput_regex_id = ? AND character_id = ? ',
+ 'ii',
+ $regexId, $characterId
+ );
+ if(!empty($data)) {
+ return $data[0]['value'];
+ }
+
+
+ return '';
+ }
+
+ }
+
+?>
diff --git a/questtypes/textinput/html/quest.tpl b/questtypes/textinput/html/quest.tpl
new file mode 100644
index 00000000..6616ba4d
--- /dev/null
+++ b/questtypes/textinput/html/quest.tpl
@@ -0,0 +1,11 @@
+
+
+ &$text) : ?>
+ 0) : ?>
+
+
+ =$t->t($text)?>
+
+
+
+
diff --git a/questtypes/textinput/html/submission.tpl b/questtypes/textinput/html/submission.tpl
new file mode 100644
index 00000000..b3d606f4
--- /dev/null
+++ b/questtypes/textinput/html/submission.tpl
@@ -0,0 +1,7 @@
+ &$text) : ?>
+ 0) : ?>
+=$regexs[$i-1]['answer']?>
+✓✕
+
+=$t->t($text)?>
+
diff --git a/requests/WebRequest.inc b/requests/WebRequest.inc
new file mode 100644
index 00000000..2e0f19b9
--- /dev/null
+++ b/requests/WebRequest.inc
@@ -0,0 +1,401 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\requests;
+
+
+ /**
+ * Representation of a web-request.
+ *
+ * @author coderkun
+ */
+ class WebRequest extends \nre\core\Request
+ {
+ /**
+ * Passed GET-parameters
+ *
+ * @var array
+ */
+ private $getParams = array();
+ /**
+ * Passed POST-parameters
+ *
+ * @var array
+ */
+ private $postParams = array();
+ /**
+ * Stored routes
+ *
+ * @var array
+ */
+ private $routes = array();
+ /**
+ * Stored reverse-routes
+ *
+ * @var array
+ */
+ private $reverseRoutes = array();
+
+
+
+
+ /**
+ * Construct a new web-request.
+ */
+ public function __construct()
+ {
+ // Detect current request
+ $this->detectRequest();
+
+ // Load GET-parameters
+ $this->loadGetParams();
+
+ // Load POST-parameters
+ $this->loadPostParams();
+
+ // Detect AJAX
+ $this->detectAJAX();
+ }
+
+
+
+
+ /**
+ * Get a parameter.
+ *
+ * @param int $index Index of parameter
+ * @param string $defaultIndex Index of default configuration value for this parameter
+ * @return string Value of parameter
+ */
+ public function getParam($index, $defaultIndex=null)
+ {
+ if($index == 0) {
+ return $this->getGetParam('layout', $defaultIndex);
+ }
+ else {
+ return parent::getParam($index-1, $defaultIndex);
+ }
+ }
+
+
+ /**
+ * Get all parameters from index on.
+ *
+ * @param int $offset Offset-index
+ * @return array Parameters
+ */
+ public function getParams($offset=0)
+ {
+ if($offset == 0)
+ {
+ return array_merge(
+ array(
+ $this->getGetParam('layout', 'toplevel')
+ ),
+ parent::getParams()
+ );
+ }
+
+
+ return array_slice($this->params, $offset-1);
+ }
+
+
+ /**
+ * Get a GET-parameter.
+ *
+ * @param string $key Key of GET-parameter
+ * @param string $defaultIndex Index of default configuration value for this parameter
+ * @return string Value of GET-parameter
+ */
+ public function getGetParam($key, $defaultIndex=null)
+ {
+ // Check key
+ if(array_key_exists($key, $this->getParams))
+ {
+ // Return value
+ return $this->getParams[$key];
+ }
+
+
+ // Return default value
+ return \nre\core\Config::getDefault($defaultIndex);
+ }
+
+
+ /**
+ * Get all GET-parameters.
+ *
+ * @return array GET-Parameters
+ */
+ public function getGetParams()
+ {
+ return $this->getParams;
+ }
+
+
+ /**
+ * Get a POST-parameter.
+ *
+ * @param string $key Key of POST-parameter
+ * @param string $defaultValue Default value for this parameter
+ * @return string Value of POST-parameter
+ */
+ public function getPostParam($key, $defaultValue=null)
+ {
+ // Check key
+ if(array_key_exists($key, $this->postParams))
+ {
+ // Return value
+ return $this->postParams[$key];
+ }
+
+
+ // Return default value
+ return $defaultValue;
+ }
+
+
+ /**
+ * Get all POST-parameters.
+ *
+ * @return array POST-parameters
+ */
+ public function getPostParams()
+ {
+ return $this->postParams;
+ }
+
+
+ /**
+ * Get the method of the current request.
+ *
+ * @return string Current request method
+ */
+ public function getRequestMethod()
+ {
+ return $_SERVER['REQUEST_METHOD'];
+ }
+
+
+ /**
+ * Add a URL-route.
+ *
+ * @param string $pattern Regex-Pattern that defines the routing
+ * @param string $replacement Regex-Pattern for replacement
+ * @param bool $isLast Stop after that rule
+ */
+ public function addRoute($pattern, $replacement, $isLast=false)
+ {
+ // Store route
+ $this->routes[] = $this->newRoute($pattern, $replacement, $isLast);
+ }
+
+
+ /**
+ * Add a reverse URL-route.
+ *
+ * @param string $pattern Regex-Pattern that defines the reverse routing
+ * @param string $replacement Regex-Pattern for replacement
+ * @param bool $isLast Stop after that rule
+ */
+ public function addReverseRoute($pattern, $replacement, $isLast=false)
+ {
+ // Store reverse route
+ $this->reverseRoutes[] = $this->newRoute($pattern, $replacement, $isLast);
+ }
+
+
+ /**
+ * Apply stored reverse-routes to an URL
+ *
+ * @param string $url URL to apply reverse-routes to
+ * @return string Reverse-routed URL
+ */
+ public function applyReverseRoutes($url)
+ {
+ return $this->applyRoutes($url, $this->reverseRoutes);
+ }
+
+
+ /**
+ * Revalidate the current request
+ */
+ public function revalidate()
+ {
+ $this->detectRequest();
+ }
+
+
+ /**
+ * Get a SERVER-parameter.
+ *
+ * @param string $key Key of SERVER-parameter
+ * @return string Value of SERVER-parameter
+ */
+ public function getServerParam($key)
+ {
+ if(array_key_exists($key, $_SERVER)) {
+ return $_SERVER[$key];
+ }
+
+
+ return null;
+ }
+
+
+
+
+ /**
+ * Detect the current HTTP-request.
+ */
+ private function detectRequest()
+ {
+ // URL ermitteln
+ $url = isset($_GET) && array_key_exists('url', $_GET) ? $_GET['url'] : '';
+ $url = trim($url, '/');
+
+ // Routes anwenden
+ $url = $this->applyRoutes($url, $this->routes);
+
+ // URL splitten
+ $params = explode('/', $url);
+ if(empty($params[0])) {
+ $params = array();
+ }
+
+
+ // Parameter speichern
+ $this->params = $params;
+ }
+
+
+ /**
+ * Determine parameters passed by GET.
+ */
+ private function loadGetParams()
+ {
+ if(isset($_GET)) {
+ $this->getParams = $_GET;
+ }
+ }
+
+
+ /**
+ * Determine parameters passed by POST.
+ */
+ private function loadPostParams()
+ {
+ if(isset($_POST)) {
+ $this->postParams = $_POST;
+ }
+ }
+
+
+ /**
+ * Detect an AJAX-request by checking the X-Requested-With
+ * header and set the layout to 'ajax' in this case.
+ */
+ private function detectAjax()
+ {
+ // Get request headers
+ $headers = apache_request_headers();
+
+ // Check X-Requested-With header and set layout
+ if(array_key_exists('X-Requested-With', $headers) && $headers['X-Requested-With'] == 'XMLHttpRequest') {
+ if(!array_key_exists('layout', $this->getParams)) {
+ $this->getParams['layout'] = 'ajax';
+ }
+ }
+ }
+
+
+ /**
+ * Create a new URL-route.
+ *
+ * @param string $pattern Regex-Pattern that defines the reverse routing
+ * @param string $replacement Regex-Pattern for replacement
+ * @param bool $isLast Stop after that rule
+ * @return array New URL-route
+ */
+ private function newRoute($pattern, $replacement, $isLast=false)
+ {
+ return array(
+ 'pattern' => $pattern,
+ 'replacement' => $replacement,
+ 'isLast' => $isLast
+ );
+ }
+
+
+ /**
+ * Apply given routes to an URL
+ *
+ * @param string $url URL to apply routes to
+ * @param array $routes Routes to apply
+ * @return string Routed URL
+ */
+ private function applyRoutes($url, $routes)
+ {
+ // Traverse given routes
+ foreach($routes as &$route)
+ {
+ // Create and apply Regex
+ $urlR = preg_replace(
+ '>'.$route['pattern'].'>i',
+ $route['replacement'],
+ $url
+ );
+
+ // Split URL
+ $get = '';
+ if(($gpos = strrpos($urlR, '?')) !== false) {
+ $get = substr($urlR, $gpos+1);
+ $urlR = substr($urlR, 0, $gpos);
+ }
+
+ // Has current route changed anything?
+ if($urlR != $url || !empty($get))
+ {
+ // Extract GET-parameters
+ if(strlen($get) > 0)
+ {
+ $gets = explode('&', $get);
+ foreach($gets as $get)
+ {
+ $get = explode('=', $get);
+ if(!array_key_exists($get[0], $this->getParams)) {
+ $this->getParams[$get[0]] = $get[1];
+ }
+ }
+ }
+
+
+ // Stop when route “isLast”
+ if($route['isLast']) {
+ $url = $urlR;
+ break;
+ }
+ }
+
+
+ // Set new URL
+ $url = $urlR;
+ }
+
+
+ // Return routed URL
+ return $url;
+ }
+
+ }
+
+?>
diff --git a/responses/WebResponse.inc b/responses/WebResponse.inc
new file mode 100644
index 00000000..2ca25079
--- /dev/null
+++ b/responses/WebResponse.inc
@@ -0,0 +1,250 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\responses;
+
+
+ /**
+ * Representation of a web-response.
+ *
+ * @author coderkun
+ */
+ class WebResponse extends \nre\core\Response
+ {
+ /**
+ * Applied GET-parameters
+ *
+ * @var array
+ */
+ private $getParams = array();
+ /**
+ * Changed header lines
+ *
+ * @var array
+ */
+ private $headers = array();
+
+
+
+
+ /**
+ * Add a parameter.
+ *
+ * @param mixed $value Value of parameter
+ */
+ public function addParam($value)
+ {
+ if(array_key_exists('layout', $this->getParams)) {
+ parent::addParam($value);
+ }
+ else {
+ $this->addGetParam('layout', $value);
+ }
+ }
+
+
+ /**
+ * Add multiple parameters.
+ *
+ * @param mixed $value1 Value of first parameter
+ * @param mixed … Values of further parameters
+ */
+ public function addParams($value1)
+ {
+ $this->addParam($value1);
+
+ $this->params = array_merge(
+ $this->params,
+ array_slice(
+ func_get_args(),
+ 1
+ )
+ );
+ }
+
+
+ /**
+ * Delete all stored parameters (from offset on).
+ *
+ * @param int $offset Offset-index
+ */
+ public function clearParams($offset=0)
+ {
+ if($offset == 0) {
+ unset($this->getParams['layout']);
+ }
+
+ parent::clearParams(max(0, $offset-1));
+ }
+
+
+ /**
+ * Get a parameter.
+ *
+ * @param int $index Index of parameter
+ * @param string $defaultIndex Index of default configuration value for this parameter
+ * @return string Value of parameter
+ */
+ public function getParam($index, $defaultIndex=null)
+ {
+ if($index == 0) {
+ return $this->getGetParam('layout', $defaultIndex);
+ }
+ else {
+ return parent::getParam($index-1, $defaultIndex);
+ }
+ }
+
+
+ /**
+ * Get all parameters from index on.
+ *
+ * @param int $offset Offset-index
+ * @return array Parameter values
+ */
+ public function getParams($offset=0)
+ {
+ if($offset == 0)
+ {
+ if(!array_key_exists('layout', $this->getParams)) {
+ return array();
+ }
+
+ return array_merge(
+ array(
+ $this->getParams['layout']
+ ),
+ $this->params
+ );
+ }
+
+
+ return array_slice($this->params, $offset-1);
+ }
+
+
+ /**
+ * Add a GET-parameter.
+ *
+ * @param string $key Key of GET-parameter
+ * @param mixed $value Value of GET-parameter
+ */
+ public function addGetParam($key, $value)
+ {
+ $this->getParams[$key] = $value;
+ }
+
+
+ /**
+ * Add multiple GET-parameters.
+ *
+ * @param array $params Associative arary with key-value GET-parameters
+ */
+ public function addGetParams($params)
+ {
+ $this->getParams = array_merge(
+ $this->getParams,
+ $params
+ );
+ }
+
+
+ /**
+ * Get a GET-parameter.
+ *
+ * @param int $index Index of GET-parameter
+ * @param string $defaultIndex Index of default configuration value for this parameter
+ * @return string Value of GET-parameter
+ */
+ public function getGetParam($key, $defaultIndex=null)
+ {
+ // Check key
+ if(array_key_exists($key, $this->getParams))
+ {
+ // Return value
+ return $this->getParams[$key];
+ }
+
+ // Return default value
+ return \nre\core\Config::getDefault($defaultIndex);
+ }
+
+
+ /**
+ * Get all GET-parameters.
+ *
+ * @return array All GET-parameters
+ */
+ public function getGetParams()
+ {
+ return $this->getParams;
+ }
+
+
+ /**
+ * Add a line to the response header.
+ *
+ * @param string $headerLine Header line
+ * @param bool $replace Replace existing header line
+ * @param int $http_response_code HTTP-response code
+ */
+ public function addHeader($headerLine, $replace=true, $http_response_code=null)
+ {
+ $this->headers[] = $this->newHeader($headerLine, $replace, $http_response_code);
+ }
+
+
+ /**
+ * Clear all stored headers.
+ */
+ public function clearHeaders()
+ {
+ $this->headers = array();
+ }
+
+
+ /**
+ * Send stored headers.
+ */
+ public function header()
+ {
+ foreach($this->headers as $header)
+ {
+ header(
+ $header['string'],
+ $header['replace'],
+ $header['responseCode']
+ );
+ }
+ }
+
+
+
+
+ /**
+ * Create a new header line.
+ *
+ * @param string $headerLine Header line
+ * @param bool $replace Replace existing header line
+ * @param int $http_response_code HTTP-response code
+ */
+ private function newHeader($headerLine, $replace=true, $http_response_code=null)
+ {
+ return array(
+ 'string' => $headerLine,
+ 'replace' => $replace,
+ 'responseCode' => $http_response_code
+ );
+ }
+
+ }
+
+?>
diff --git a/seminarymedia/empty b/seminarymedia/empty
new file mode 100644
index 00000000..e69de29b
diff --git a/seminaryuploads/empty b/seminaryuploads/empty
new file mode 100644
index 00000000..e69de29b
diff --git a/tmp/empty b/tmp/empty
new file mode 100644
index 00000000..e69de29b
diff --git a/uploads/empty b/uploads/empty
new file mode 100644
index 00000000..e69de29b
diff --git a/views/binary/binary.tpl b/views/binary/binary.tpl
new file mode 100644
index 00000000..43a06a51
--- /dev/null
+++ b/views/binary/binary.tpl
@@ -0,0 +1 @@
+=$intermediate?>
diff --git a/views/binary/error/index.tpl b/views/binary/error/index.tpl
new file mode 100644
index 00000000..2d8440b4
--- /dev/null
+++ b/views/binary/error/index.tpl
@@ -0,0 +1 @@
+=_('Error')?>: =$code?>: =$string?>
diff --git a/views/binary/media/achievement.tpl b/views/binary/media/achievement.tpl
new file mode 100644
index 00000000..0d6fb0df
--- /dev/null
+++ b/views/binary/media/achievement.tpl
@@ -0,0 +1 @@
+=$file?>
diff --git a/views/binary/media/avatar.tpl b/views/binary/media/avatar.tpl
new file mode 100644
index 00000000..0d6fb0df
--- /dev/null
+++ b/views/binary/media/avatar.tpl
@@ -0,0 +1 @@
+=$file?>
diff --git a/views/binary/media/index.tpl b/views/binary/media/index.tpl
new file mode 100644
index 00000000..0d6fb0df
--- /dev/null
+++ b/views/binary/media/index.tpl
@@ -0,0 +1 @@
+=$file?>
diff --git a/views/binary/media/seminary.tpl b/views/binary/media/seminary.tpl
new file mode 100644
index 00000000..0d6fb0df
--- /dev/null
+++ b/views/binary/media/seminary.tpl
@@ -0,0 +1 @@
+=$file?>
diff --git a/views/binary/media/seminaryheader.tpl b/views/binary/media/seminaryheader.tpl
new file mode 100644
index 00000000..0d6fb0df
--- /dev/null
+++ b/views/binary/media/seminaryheader.tpl
@@ -0,0 +1 @@
+=$file?>
diff --git a/views/binary/uploads/seminary.tpl b/views/binary/uploads/seminary.tpl
new file mode 100644
index 00000000..0d6fb0df
--- /dev/null
+++ b/views/binary/uploads/seminary.tpl
@@ -0,0 +1 @@
+=$file?>
diff --git a/views/error.tpl b/views/error.tpl
new file mode 100644
index 00000000..f45b01a0
--- /dev/null
+++ b/views/error.tpl
@@ -0,0 +1,13 @@
+
+
+
+
+
+ Service Unavailable
+
+
+
+ Die Anwendung steht zur Zeit leider nicht zur Verfügung.
+
+
+
diff --git a/views/fault/error/index.tpl b/views/fault/error/index.tpl
new file mode 100644
index 00000000..f7e42500
--- /dev/null
+++ b/views/fault/error/index.tpl
@@ -0,0 +1,2 @@
+Fehler
+=$code?>: =$string?>
diff --git a/views/fault/fault.tpl b/views/fault/fault.tpl
new file mode 100644
index 00000000..a6459f96
--- /dev/null
+++ b/views/fault/fault.tpl
@@ -0,0 +1,16 @@
+
+
+
+
+
+ Questlab
+
+
+
+ Questlab
+
+ =$intermediate?>
+
+
+
+
diff --git a/views/html/achievements/index.tpl b/views/html/achievements/index.tpl
new file mode 100644
index 00000000..2d63d0e2
--- /dev/null
+++ b/views/html/achievements/index.tpl
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+ =_('Achievements')?>
+=_('Achievement description')?>
+
+
+ =_('Seldom Achievements')?>
+
+
+
+
+
+
+
+
+
+
+
+
+ =$achievement['title']?>
+ =sprintf(_('Achievement has been achieved only %d times'), $achievement['c'])?>
+
+
+
+
+
+ =_('Most successful collectors')?>
+
+
+
+
+ =$successfulCharacter['name']?>
+ =sprintf(_('Character has achieved %d Achievements'), $successfulCharacter['c'])?>
+
+
+
+
+
+=_('Personal Achievements')?>
+
+
=sprintf(_('Own progress: %d %%'), round(count($achievedAchievements) / (count($achievedAchievements)+count($unachievedAchievements)) * 100))?>
+
+
+
+
+=$character['rank']?>. =_('Rank')?>: =sprintf(_('You achieved %d of %d Achievements so far'), count($achievedAchievements), count($achievedAchievements)+count($unachievedAchievements))?>.
+
+
+
+
+
+
+ =$achievement['title']?>=sprintf(_('achieved at: %s'), $dateFormatter->format(new \DateTime($achievement['created'])))?>
+ =\hhu\z\Utils::t($achievement['description'])?>
+
+
+
+
+
+
+
+ =(!$achievement['hidden']) ? $achievement['title'] : _('Secret Achievement')?>
+
+ =\hhu\z\Utils::t($achievement['description'])?>
+
+ =_('Continue playing to unlock this secret Achievement')?>
+
+
+
+
+
+
+
diff --git a/views/html/charactergroups/group.tpl b/views/html/charactergroups/group.tpl
new file mode 100644
index 00000000..5b5e1f4c
--- /dev/null
+++ b/views/html/charactergroups/group.tpl
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+ 0) : ?>
+
+ =_('Manage')?>
+
+
+
+
+
+
=$group['name']?>
+
"=$group['motto']?>"
+
+
+ =$group['rank']?>. =_('Rank')?>
+ =$group['xps']?> XP
+ =count($group['characters'])?> =(count($group['characters']) > 1) ? _('Members') : _('Member')?>
+
+
+
+
+
diff --git a/views/html/charactergroups/groupsgroup.tpl b/views/html/charactergroups/groupsgroup.tpl
new file mode 100644
index 00000000..3d337cf3
--- /dev/null
+++ b/views/html/charactergroups/groupsgroup.tpl
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+=$groupsgroup['name']?>
+
+
+
+ =$group['name']?> =$group['xps']?> XP
+
+
+
+
+=_('Character Groups Quests')?>
+
diff --git a/views/html/charactergroups/index.tpl b/views/html/charactergroups/index.tpl
new file mode 100644
index 00000000..d88d25eb
--- /dev/null
+++ b/views/html/charactergroups/index.tpl
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+=_('Character Groups')?>
+
diff --git a/views/html/charactergroups/managegroup.tpl b/views/html/charactergroups/managegroup.tpl
new file mode 100644
index 00000000..53e1f19a
--- /dev/null
+++ b/views/html/charactergroups/managegroup.tpl
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
=$group['name']?>
+
"=$group['motto']?>"
+
+
+ =$group['rank']?>. =_('Rank')?>
+ =$group['xps']?> XP
+ =count($group['characters'])?> =(count($group['characters']) > 1) ? _('Members') : _('Member')?>
+
+
+
+
+
+
+
+
+
+
diff --git a/views/html/charactergroupsquests/quest.tpl b/views/html/charactergroupsquests/quest.tpl
new file mode 100644
index 00000000..6c696737
--- /dev/null
+++ b/views/html/charactergroupsquests/quest.tpl
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+=$quest['title']?>
+Maximale Belohnung: =$quest['xps']?> XP
+
+
+ =_('Description')?>
+
+
+
+
+ =\hhu\z\Utils::t($quest['description'])?>
+
+
+ =_('Rules')?>
+ =\hhu\z\Utils::t($quest['rules'])?>
+
+
+
+
+
+ =_('Won Quest')?>
+ =\hhu\z\Utils::t($quest['won_text'])?>
+
+
+
+
+ =_('Lost Quest')?>
+ =\hhu\z\Utils::t($quest['lost_text'])?>
+
+
+
+
+ =$groupsgroup['name']?>
+
+
+
+ =$dateFormatter->format(new \DateTime($group['created']))?>
+ =$group['name']?>
+ =$group['xps']?> XP
+
+
+
+
diff --git a/views/html/characters/character.tpl b/views/html/characters/character.tpl
new file mode 100644
index 00000000..ebd9d69e
--- /dev/null
+++ b/views/html/characters/character.tpl
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+ =$character['name']?>
+
+
+
+
+
+
+
=_('Total progress')?>: =round($character['quest_xps']*100/$seminary['achievable_xps'])?> %
+
+
+
=$character['xplevel']['level']?>
+
=_('Level')?>
+
+
+
=$character['xps']?>
+
XP
+
+
+
=$character['rank']?>.
+
=_('Rank')?>
+
+
=_('Milestones')?>
+
+
+
+
+
+
+
+
+
+ =_('Ranking')?>
+
+ &$rankCharacter) : ?>
+
+
+ =$character['rank']-count($ranking['superior'])+$index?>. =$rankCharacter['name']?>
+ =_('Level')?> =$rankCharacter['xplevel']?> (=sprintf(_('%d XPs'), $rankCharacter['xps'])?>)
+
+
+
+
+ =$character['rank']?>. =$character['name']?>
+ =_('Level')?> =$character['xplevel']['level']?> (=sprintf(_('%d XPs'), $character['xps'])?>)
+
+ &$rankCharacter) : ?>
+
+
+ =$character['rank']+$index+1?>. =$rankCharacter['name']?>
+ =_('Level')?> =$rankCharacter['xplevel']?> (=sprintf(_('%d XPs'), $rankCharacter['xps'])?>)
+
+
+
+
+
+ =_('Character Groups')?>
+
+
+
+
+
+
+
+
+
+ =_('Topic progress')?>
+
+
+
diff --git a/views/html/characters/index.tpl b/views/html/characters/index.tpl
new file mode 100644
index 00000000..171d09d7
--- /dev/null
+++ b/views/html/characters/index.tpl
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+=_('Characters')?>
+
+ 0) : ?>
+
+ =_('Manage')?>
+
+
+
+
diff --git a/views/html/characters/manage.tpl b/views/html/characters/manage.tpl
new file mode 100644
index 00000000..15b16aa2
--- /dev/null
+++ b/views/html/characters/manage.tpl
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+=_('Manage')?>
+
+
+
+ =_('Selection')?>:
+
+
+
+ checked="checked" disabled="disabled"/>
+
+
+ =$character['name']?>
+ =$character['xps']?> XP
+ =_('Admin')?>
+ =_('Moderator')?>
+ =_('User')?>
+
+
+
+
+
+
+ =_('Add role')?>
+
+
+
+
+
+
+
+ =_('Remove role')?>
+
+
+
+
+
+
+
diff --git a/views/html/characters/register.tpl b/views/html/characters/register.tpl
new file mode 100644
index 00000000..970dd25b
--- /dev/null
+++ b/views/html/characters/register.tpl
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+=_('Create Character')?>
+
+
+
+ &$settings) : ?>
+
+
+
+
+
+
+
+ =_('Character properties')?>
+ =_('Character name')?>:
+
+
+
+
+
+
+ &$settings) : ?>
+ =sprintf(_('The Seminary field “%s” is invalid'), $field)?>
+
+
+
+
+ =_('Seminary fields')?>
+
+ =$field['title']?>:
+
+ value="=$field['uservalue']?>" required="required"/>
+
+ =(array_key_exists('uservalue', $field) ? $field['uservalue'] : null)?>
+
+
+
+ selected="selected">=mb_eregi_replace('\\\\','',$option)?>
+
+
+
+
+
+
+
+
diff --git a/views/html/error/index.tpl b/views/html/error/index.tpl
new file mode 100644
index 00000000..a0b02462
--- /dev/null
+++ b/views/html/error/index.tpl
@@ -0,0 +1,17 @@
+=_('Error')?>
+=$code?>: =$string?>
+
+
+=_('Login')?>
+
+
+ =_('Username')?>:
+
+ =_('Password')?>:
+
+
+
+
+ =_('or')?> =_('register yourself')?>
+
+
diff --git a/views/html/html.tpl b/views/html/html.tpl
new file mode 100644
index 00000000..b121e295
--- /dev/null
+++ b/views/html/html.tpl
@@ -0,0 +1,63 @@
+
+
+
+
+
+ =\nre\configs\AppConfig::$app['name']?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ =$intermediate?>
+
+
+ 0) : ?>
+ =$seminarybar?>
+
+
+
+
+
diff --git a/views/html/introduction/index.tpl b/views/html/introduction/index.tpl
new file mode 100644
index 00000000..42979a96
--- /dev/null
+++ b/views/html/introduction/index.tpl
@@ -0,0 +1,50 @@
+=_('Introduction')?>
+
+=_('Login')?>
+
+
+ =_('Username')?>:
+
+ =_('Password')?>:
+
+
+
+ =_('or')?> =_('register yourself')?>
+
+
+
+Bereits im Sommersemester 2013 wurde das Projekt „Die Legende von Zyren“ unterstützt durch den Lehrförderungsfond der Heinrich-Heine-Universität Düsseldorf ins Leben gerufen, um die Inhalte der Vorlesung „Wissensrepräsentation“ den Studierenden des Faches Informationswissenschaft mit Hilfe von Spielelementen und -modellen zu vermitteln. Die innovative Lernumgebung besteht aus einem virtuellen Textadventure, dass über eine webbasierte Plattform zugänglich ist und realen Spielen in einer Präsenzveranstaltung, in denen die Studierenden unmittelbar in das Abenteuer eintauchen und in Teams spielerisch gegeneinander antreten.
+Auf der Plattform spielt sich jeder der Studierenden mit seinem virtuellen Avatar durch das Reich von Zyren und erlernt und vertieft auf spielerische Weise die Inhalte der Vorlesung „Wissensrepräsentation“, die in Form von Herausforderungen oder Rätseln in das Abenteuer eingebunden wurden und über den Fortlauf und den Erfolg des Spiels entscheiden.
+In der zusätzlichen Präsenzveranstaltung tauchen die Studierenden direkt in das Abenteuer ein und die vertiefen die Inhalte spielerisch. Hier schließen sich die Studierenden in Teams (Gilden) zusammen, müssen eigenverantwortlich Lerninhalte erarbeiten, Probleme lösen und in speziellen Gildenaufgaben gegen andere Teams antreten, um ihr kollaborativ erarbeitetes Wissen auf die Probe zu stellen.
+Für jede erfolgreiche absolvierte Herausforderung auf der Plattform oder in der Übung erhalten die Studierenden Erfahrungspunkte und Belohnungen.
+Um das Konzept auch anderen Fachbereichen und Lehrveranstaltungen zugänglich zu machen, wurde im Frühjahr 2014 das Projekt Questlab (Arbeitstitel „The Legend of Z“) gestartet um das Konzept zu generalisieren. Lehrende können die Plattform nun nutzen um eigene Aufgaben (Quests) zu kreieren und hochzuladen und sie optional in eine Geschichte einzubinden, die sie selbst gestalten können. Zudem wurde das Responsive Design überarbeitet und bietet nun optimalen Zugriff auf die Plattform über alle mobilen Endgeräte.
+
+Die Legende von Zyren in der Presse
+
+
+Das Team
+Projektleitung:
+
+Entwicklung und Evaluation des Prototypens:
+
+ Lisa Orszullok
+ Simone Soubusta
+ Julia Göretz
+ Anja Wintermeyer
+
+Entwicklung „The Legend of Z“:
+
+ Oliver Hanraths
+ Daniel Miskovic
+
diff --git a/views/html/library/index.tpl b/views/html/library/index.tpl
new file mode 100644
index 00000000..37b423ae
--- /dev/null
+++ b/views/html/library/index.tpl
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+ =_('Library')?>
+=sprintf(_('Library description, %s, %s'), $seminary['course'], $seminary['title'])?>
+
+
=sprintf(_('Total progress: %d %%'), ($totalQuestcount > 0) ? $numberFormatter->format(round($totalCharacterQuestcount/$totalQuestcount*100)) : 0) ?>
+
+
+
+
+
diff --git a/views/html/library/topic.tpl b/views/html/library/topic.tpl
new file mode 100644
index 00000000..8f1888df
--- /dev/null
+++ b/views/html/library/topic.tpl
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+ =$questtopic['title']?>
+
+
Themenfortschritt: =$questtopic['characterQuestcount']?> / =$questtopic['questcount']?>
+
+
+
+
+
+Quests zu diesem Thema:
+
diff --git a/views/html/menu/index.tpl b/views/html/menu/index.tpl
new file mode 100644
index 00000000..64627e74
--- /dev/null
+++ b/views/html/menu/index.tpl
@@ -0,0 +1,9 @@
+ =\nre\configs\AppConfig::$app['name']?>
+ 0) : ?> =_('Users')?>
+ =_('Seminaries')?>
+ 0) : ?>=$seminarymenu?>
+
+ =_('Login')?>
+
+ =_('Logout')?>
+
diff --git a/views/html/questgroups/create.tpl b/views/html/questgroups/create.tpl
new file mode 100644
index 00000000..17692935
--- /dev/null
+++ b/views/html/questgroups/create.tpl
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+=_('Create Questgroup')?>
+
+
+
+ =_('Title')?>:
+
+
+
+
diff --git a/views/html/questgroups/questgroup.tpl b/views/html/questgroups/questgroup.tpl
new file mode 100644
index 00000000..c0f2218e
--- /dev/null
+++ b/views/html/questgroups/questgroup.tpl
@@ -0,0 +1,70 @@
+
+
+
+
+
+=$questgroupshierarchypath?>
+
+
+=$questgroup['hierarchy']['title_singular']?> =$questgroup['hierarchy']['questgroup_pos']?>: =$questgroup['title']?>
+
+=$questgroup['title']?>
+
+ 0): ?>
+
+
+
=\hhu\z\Utils::t($text['text'])?>
+
+
+
+
+
+
+ 0) : ?>
+=$hierarchy['title_plural']?>
+
+
+
+
+
+
+
+
+
+
+
+
+
Fortschritt:
+
+
+
+
=$group['character_xps']?> / =$group['xps']?> XP
+
+
+
+
+
+
+
+
+
+ 0) : ?>
+=_('Quests')?>
+
+
diff --git a/views/html/questgroupshierarchypath/index.tpl b/views/html/questgroupshierarchypath/index.tpl
new file mode 100644
index 00000000..1f68e73c
--- /dev/null
+++ b/views/html/questgroupshierarchypath/index.tpl
@@ -0,0 +1,16 @@
+
diff --git a/views/html/quests/create.tpl b/views/html/quests/create.tpl
new file mode 100644
index 00000000..68f8b4b8
--- /dev/null
+++ b/views/html/quests/create.tpl
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+=_('Create Quest')?>
+
+
+
+ =_('Name')?>:
+
+ =_('Questgroup')?>:
+
+
+ =$questgroup['title']?>
+
+
+ =('Questtype')?>:
+
+
+ =$questtype['title']?>
+
+
+ =_('XPs')?>:
+
+
+
+
+ Prolog:
+
+ =('Entry text')?>:
+
+ =('Wrong text')?>:
+
+ =_('Task')?>:
+
+
+
+
diff --git a/views/html/quests/createmedia.tpl b/views/html/quests/createmedia.tpl
new file mode 100644
index 00000000..be46b2b8
--- /dev/null
+++ b/views/html/quests/createmedia.tpl
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+Create Questsmedia
+
+
+New mediaId: =var_dump($mediaid)?>
+
+
+Error: =$error?>
+
+
+
+
+
+
diff --git a/views/html/quests/index.tpl b/views/html/quests/index.tpl
new file mode 100644
index 00000000..9ea9b272
--- /dev/null
+++ b/views/html/quests/index.tpl
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+=_('Quests')?>
+
+
+
+ Filter
+ =_('Questgroup')?>:
+
+ alle
+
+ selected="selected">=$filter['title']?>
+
+
+ =_('Questtype')?>:
+
+ alle
+
+ selected="selected">=$filter['title']?>
+
+
+
+
+
+
+
+
diff --git a/views/html/quests/quest.tpl b/views/html/quests/quest.tpl
new file mode 100644
index 00000000..3408da26
--- /dev/null
+++ b/views/html/quests/quest.tpl
@@ -0,0 +1,115 @@
+
+
+
+
+
+=$questgroupshierarchypath?>
+=$quest['title']?>
+
+ 0) : ?>
+
+ =$questtexttype['type']?>
+
+
+
+
+
+
+
+
+
+
+ =\hhu\z\Utils::t($questtext['text'])?>
+
+ 0 || !empty($questtext['abort_text'])) : ?>
+
+
+
+
+
+
+
+
+
+
+ =_('solved')?>
+=sprintf(_('Quest completed. You have earned %d XPs.'), $quest['xps'])?>
+
+
+ =_('unsolved')?>
+=\hhu\z\Utils::t($quest['wrong_text'])?>
+
+
+
+
+
+
+
+ =_('Task')?>
+ =$t->t($quest['task'])?>
+ =$task?>
+
+
+
+
+
+ =_('continue')?>
+
+
+
+
+
+
+
diff --git a/views/html/quests/submission.tpl b/views/html/quests/submission.tpl
new file mode 100644
index 00000000..b8add296
--- /dev/null
+++ b/views/html/quests/submission.tpl
@@ -0,0 +1,12 @@
+
+
+
+
+
+=$questgroupshierarchypath?>
+=$quest['title']?>
+
+=sprintf(_('Submission of %s'),$character['name'])?>
+
diff --git a/views/html/quests/submissions.tpl b/views/html/quests/submissions.tpl
new file mode 100644
index 00000000..a5b390fe
--- /dev/null
+++ b/views/html/quests/submissions.tpl
@@ -0,0 +1,36 @@
+
+
+
+
+
+=$questgroupshierarchypath?>
+=$quest['title']?>
+
+
+ =_('submitted')?>
+
+
+ =_('unsolved')?>
+
+
+ =_('solved')?>
+
+
diff --git a/views/html/seminaries/create.tpl b/views/html/seminaries/create.tpl
new file mode 100644
index 00000000..22a9acaa
--- /dev/null
+++ b/views/html/seminaries/create.tpl
@@ -0,0 +1,15 @@
+
+
+
+
+
+=_('Seminaries')?>
+=_('New seminary')?>
+
+
+
+ =_('Title')?>:
+
+
+
+
diff --git a/views/html/seminaries/delete.tpl b/views/html/seminaries/delete.tpl
new file mode 100644
index 00000000..8e53fdb5
--- /dev/null
+++ b/views/html/seminaries/delete.tpl
@@ -0,0 +1,13 @@
+
+
+
+
+
+=_('Seminaries')?>
+=_('Delete seminary')?>
+
+=sprintf(_('Should the seminary “%s” really be deleted?'), $seminary['title'])?>
+
+
+
+
diff --git a/views/html/seminaries/edit.tpl b/views/html/seminaries/edit.tpl
new file mode 100644
index 00000000..27974c1b
--- /dev/null
+++ b/views/html/seminaries/edit.tpl
@@ -0,0 +1,15 @@
+
+
+
+
+
+=_('Seminaries')?>
+=_('Edit seminary')?>
+
+
+
+ =_('Title')?>:
+
+
+
+
diff --git a/views/html/seminaries/index.tpl b/views/html/seminaries/index.tpl
new file mode 100644
index 00000000..922a6e03
--- /dev/null
+++ b/views/html/seminaries/index.tpl
@@ -0,0 +1,31 @@
+=_('Seminaries')?>
+ 0) : ?>
+
+ =_('Create new seminary')?>
+
+
+
+
+
+
+
+
+
+
+ 0) : ?>
+ =$seminary['title']?>
+
+ =$seminary['title']?>
+
+
+ =sprintf(_('created by %s on %s'), $seminary['creator']['username'], $dateFormatter->format(new \DateTime($seminary['created'])))?>
+ =\hhu\z\Utils::t($seminary['description'])?>
+
+ =_('Create a Character')?>
+
+ =sprintf(_('Your Character “%s” has not been activated yet'), $seminary['usercharacter']['name'])?>
+
+
+
+
+
diff --git a/views/html/seminaries/seminary.tpl b/views/html/seminaries/seminary.tpl
new file mode 100644
index 00000000..7aa0ce44
--- /dev/null
+++ b/views/html/seminaries/seminary.tpl
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+ 0) : ?>
+
+ =_('Edit seminary')?>
+ =_('Delete seminary')?>
+ 0) : ?>=_('Show Quests')?>
+
+
+=\hhu\z\Utils::t($seminary['description'])?>
+
+=$hierarchy['title_plural']?>
+
+
+
+
+
+
+
+ =$hierarchy['title_singular']?> =$group['pos']?>:
+ =$group['title']?>
+
+
+
+
+
=$group['character_xps']?> / =$group['xps']?> XP
+
+
+ =$group['text']?>
+
+ Auf ins Abenteuer!
+
+
+
+
+
diff --git a/views/html/seminarybar/index.tpl b/views/html/seminarybar/index.tpl
new file mode 100644
index 00000000..ec094e70
--- /dev/null
+++ b/views/html/seminarybar/index.tpl
@@ -0,0 +1,48 @@
+
+ =$character['name']?>
+
+
+ =('Level')?> =$character['xplevel']['level']?>
+ =sprintf(_('%d XPs'), $character['xps'])?>
+ =$character['rank']?>. =_('Rank')?>
+ Zum Profil
+
+
+
+
+
+
+
+
+
+ =_('Last Achievement')?>
+
+
+
+
+
+ =$lastAchievement['title']?>
+ =sprintf(_('achieved at: %s'), $dateFormatter->format(new \DateTime($lastAchievement['created'])))?>
+
+
+
+
+
+
diff --git a/views/html/seminarymenu/index.tpl b/views/html/seminarymenu/index.tpl
new file mode 100644
index 00000000..56b9307b
--- /dev/null
+++ b/views/html/seminarymenu/index.tpl
@@ -0,0 +1,5 @@
+ =$loggedSeminary['title']?>
+ 0) : ?> =_('Characters')?>
+ =_('Character Groups')?>
+ =_('Achievements')?>
+ =_('Library')?>
diff --git a/views/html/userroles/user.tpl b/views/html/userroles/user.tpl
new file mode 100644
index 00000000..df8b7a66
--- /dev/null
+++ b/views/html/userroles/user.tpl
@@ -0,0 +1,5 @@
+
diff --git a/views/html/users/create.tpl b/views/html/users/create.tpl
new file mode 100644
index 00000000..2d5484dc
--- /dev/null
+++ b/views/html/users/create.tpl
@@ -0,0 +1,18 @@
+=_('Users')?>
+=_('New user')?>
+
+
+
+ =_('Username')?>:
+
+ =_('Prename')?>:
+
+ =_('Surname')?>:
+
+ =_('E‑mail address')?>:
+
+ =_('Password')?>:
+
+
+
+
diff --git a/views/html/users/delete.tpl b/views/html/users/delete.tpl
new file mode 100644
index 00000000..10f280ab
--- /dev/null
+++ b/views/html/users/delete.tpl
@@ -0,0 +1,8 @@
+=_('Users')?>
+=_('Delete user')?>
+
+=sprintf(_('Should the user “%s” (%s) really be deleted?'), $user['username'], $user['email'])?>
+
+
+
+
diff --git a/views/html/users/edit.tpl b/views/html/users/edit.tpl
new file mode 100644
index 00000000..510aaed3
--- /dev/null
+++ b/views/html/users/edit.tpl
@@ -0,0 +1,18 @@
+=_('Users')?>
+=_('Edit user')?>
+
+
+
+ =_('Username')?>:
+
+ =_('Prename')?>:
+
+ =_('Surname')?>:
+
+ =_('E‑mail address')?>:
+
+ =_('Password')?>:
+
+
+
+
diff --git a/views/html/users/index.tpl b/views/html/users/index.tpl
new file mode 100644
index 00000000..9f36049c
--- /dev/null
+++ b/views/html/users/index.tpl
@@ -0,0 +1,9 @@
+=_('Users')?>
+
+ =_('Create new user')?>
+
+
+
+ =$user['username']?> =sprintf(_('registered on %s'), $dateFormatter->format(new \DateTime($user['created'])))?>
+
+
diff --git a/views/html/users/login.tpl b/views/html/users/login.tpl
new file mode 100644
index 00000000..d51423f0
--- /dev/null
+++ b/views/html/users/login.tpl
@@ -0,0 +1,18 @@
+=_('Users')?>
+
+=_('Login')?>
+
+=_('Login failed')?>.
+
+
+
+ =_('Username')?>:
+
+ =_('Password')?>:
+
+
+
+
+
+
+
diff --git a/views/html/users/logout.tpl b/views/html/users/logout.tpl
new file mode 100644
index 00000000..e69de29b
diff --git a/views/html/users/register.tpl b/views/html/users/register.tpl
new file mode 100644
index 00000000..29843569
--- /dev/null
+++ b/views/html/users/register.tpl
@@ -0,0 +1,90 @@
+=_('Users')?>
+
+=_('Registration')?>
+
+
+ &$settings) : ?>
+
+
+ $value) : ?>
+
+ getMessage();
+ break;
+ } ?>
+
+
+
+
+
+
+
+
+
+ =_('Username')?>:
+ />
+ =_('Prename')?>:
+ />
+ =_('Surname')?>:
+ />
+ =_('E‑mail address')?>:
+ />
+ =_('Password')?>:
+ />
+
+
+
diff --git a/views/html/users/user.tpl b/views/html/users/user.tpl
new file mode 100644
index 00000000..4be14ae5
--- /dev/null
+++ b/views/html/users/user.tpl
@@ -0,0 +1,35 @@
+=_('Users')?>
+ 0) : ?>
+
+ =_('Edit user')?>
+ =_('Delete user')?>
+
+
+=$user['username']?>
+
+ =sprintf(_('registered on %s'), $dateFormatter->format(new \DateTime($user['created'])))?>
+ =_('Name')?>: =$user['prename']?> =$user['surname']?>
+ =_('E‑mail address')?>: =$user['email']?>
+
+
+=_('Characters')?>
+
+
+=_('Roles')?>
+=$userroles?>
diff --git a/views/inlineerror.tpl b/views/inlineerror.tpl
new file mode 100644
index 00000000..87a2b222
--- /dev/null
+++ b/views/inlineerror.tpl
@@ -0,0 +1 @@
+Dieser Teil der Anwendung steht zur Zeit leider nicht zur Verfügung.
diff --git a/www/.htaccess b/www/.htaccess
new file mode 100644
index 00000000..63163a80
--- /dev/null
+++ b/www/.htaccess
@@ -0,0 +1,8 @@
+
+ RewriteEngine On
+
+ RewriteBase /
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteRule ^(.*)$ index.php?url=$1 [QSA,L,NE]
+
diff --git a/www/css/desktop.css b/www/css/desktop.css
new file mode 100644
index 00000000..c6d2371d
--- /dev/null
+++ b/www/css/desktop.css
@@ -0,0 +1,359 @@
+@charset "UTF-8";
+
+/** CSS-Reset **/
+
+html{font-family:'Open Sans',sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}
+body{margin:0;background:#f7f5f2;color:#5e5c58;font:100%/1.6 'Open Sans',sans-serif;-webkit-animation:bugfix infinite 1s}
+a:focus{outline:thin dotted}
+a:active,a:hover{outline:0}
+b,strong,.fwb{font-weight:bold}
+dfn,.fsi{font-style:italic}
+img{border:0}
+h1,h2,h3{color:#103a3e}
+h2{font-size:120%;margin-top:25px}
+h3{font-size:100%}
+ul,ol,nav{padding:0;list-style-type:none}
+p{margin:0 0 16px;padding:0}
+audio,canvas,video{display:inline-block}
+audio:not([controls]){display:none;height:0}
+[hidden]{display:none}
+abbr[title]{border-bottom:1px dotted}
+hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}
+mark{background:#ff0;color:#000}
+code,kbd,pre,samp{font-family:monospace,serif;font-size:1em}
+pre{white-space:pre-wrap}
+q{quotes:"\201C" "\201D" "\2018" "\2019"}
+small{font-size:87.5%}
+sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
+sup{top:-.5em}
+sub{bottom:-.25em}
+svg:not(:root){overflow:hidden}
+figure{margin:0}
+fieldset,legend{border:0;margin:0 0 10px;padding:0}
+button,input,select,textarea{font-family:inherit;font-size:100%;margin:0}
+button,input{line-height:normal}
+button,select{text-transform:none}
+button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}
+button[disabled],html input[disabled]{cursor:default}
+input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}
+input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}
+input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}
+button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}
+textarea{overflow:auto;vertical-align:top}
+table{border-collapse:collapse;border-spacing:0}
+
+
+/** Initial **/
+
+@-webkit-keyframes bugfix{from{padding:0}to{padding:0}}
+
+.cf:before,.cf:after{display:table;content:""}
+.cf:after{clear:both}
+.cf{zoom:1}
+.cb{clear:both}
+
+.wrap{margin:0 5%;max-height:999999px;min-height:1px}
+
+a{color:#50a4ab;text-decoration:none}
+a:hover{color:#62c5cd}
+.fa{padding:0 10px 0 0}
+.fwb{font-weight:700}
+
+header{background:#0f373c;margin-bottom:19px;position:fixed;width:100%;z-index:99}
+header nav{z-index:99;margin:0 5%}
+
+header .fa{color:#5e9499}
+header a:hover .fa{color:#7fb0b4}
+
+menu{display:none;opacity:0;position:absolute;margin:0;padding:0 0 15px;background:#0f373c;right:0;left:0}
+menu li{display:block;margin:0 5%}
+
+menu a,#profile{display:block;padding:15px 0;color:#d9e5e7}
+menu a:hover,#profile:hover{color:#fff}
+
+menu .active{color:#fff}
+menu .active .fa,.crewards .unlocked .fa{color:#bcd75e}
+
+menu .smnry{font-size:0.875em;padding:8px 0 8px 12px;background:#0c2e32;border-bottom:2px solid #0f373c;border-radius:3px}
+
+#profile{float:right}
+.circlenote{margin-left:15px;background:#bcd75e;border-radius:0.8em;display:inline-block;color:#0f373c;font-weight:bold;line-height:1.6em;text-align:center;width:1.6em;font-size:.8em}
+
+#toggle,.toggle{display:none}
+.toggle{display:inline-block;padding:15px 15px 15px 0;position:relative;cursor:pointer;-webkit-touch-callout:none;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;user-select:none}
+#toggle:checked ~ menu{display:block;opacity:1}
+#navicon{display:block;color:#d9e5e7}
+
+article{padding:70px 0 30px}
+aside{display:none}
+
+.moodpic{margin:-15px -5.5% 0 -5.5%;overflow:hidden}
+.moodpic img{width:100vw}
+
+.breadcrumbs li{display:block;font-size:.875em}
+.breadcrumbs .fa{padding-right:5px;font-size:.75em;color:#989693}
+
+.questgroups li{margin:0 0 25px 0;border-radius:3px;overflow:hidden}
+.questgroups img{display:block;width:100%;}
+.questgroups section{padding:20px;background:#fff}
+
+.qg li{margin-left:10px;padding:0 0 25px 30px;border:2px solid #dbd9d5;border-width:0 0 0 2px}
+.qgicon{margin:17px 0 0 -39px;background:#f7f5f2;font-size:1.25em;float:left}
+.qgicon.locked{margin-top:-2px}
+.qgtitle a{background:#5cb6bd;border-radius:3px 3px 0 0;display:block;padding:20px;color:#fff;font-weight:700}
+.qgtitle a:hover{background:#62c5cd}
+.qgprogress,.qghidden{margin-bottom:4px;padding:20px 20px 4px 20px;background:#fff;border-radius:0 0 3px 3px}
+
+.qglist li{margin: 0 0 5px 0}
+.qglist .qgtitle a{padding:14px;border-radius:3px}
+.qglist .qgtitle .solved{background:#fff;color:#50a4ab}
+.qglist .qgtitle .solved:hover{color:#62c5cd}
+.qglist .qgtitle .solved .fa{color:#bcd75e}
+.qglist .qgtitle .bonus{background:#f5821f}
+.qglist .qgtitle .bonus:hover{background:#f68e34}
+
+#qtextbox{font-size:.875em;border-radius:5px;background:#fff;border:15px solid #fff;height:200px;overflow:hidden}
+.qtext{padding-right:15px}
+.qtext img,.grpqimg{float:right;margin-left:15px;max-width:30%;border-radius:3px}
+
+#imagelightbox{cursor:pointer;position:fixed;z-index:10000;-ms-touch-action:none;touch-action:none;-webkit-box-shadow:0 0 3.125em rgba( 0, 0, 0, .75 ); -moz-box-shadow:0 0 3.125em rgba( 0, 0, 0, .75 );box-shadow:0 0 3.125em rgba( 0, 0, 0, .75 )}
+
+.xpinfo{display:none}
+.xpbar{width:60%;float:left;height:10px;position:relative;background:#eee;border-radius:25px;margin:8px 0 16px}
+.xpbar span{display:block;height:100%;border-radius:20px;background:#bcd75e;position:relative;overflow:hidden}
+.xpnumeric{float:right;color:#869845}
+
+.cta,input[type="submit"]{display:inline-block;margin-top:10px;color:#fff;font-weight:700;padding:7px 20px;border-radius:4px}
+.orange,input[type="submit"]{text-shadow:1px 2px #d4701a;background:#f5821f;border:solid #d4701a;border-width:0 0 3px 0}
+.orange:hover,input[type="submit"]:hover{color:#fff;background:#f09140}
+
+input[type="submit"][disabled]{text-shadow:1px 2px #d48c4e;background:#f9ac69;border-color:#d48c4e}
+
+.admin li{margin:0 0 15px 0;display:inline-block}
+.admin li a{padding:4px 15px;background:#5cb6bd;border-bottom:2px solid #50a0a6;color:#fff;border-radius:3px}
+.admin li a:hover{background:#62c5cd}
+
+
+/** Login & Registration **/
+
+.logreg{width:auto;display:inline-block;padding:15px 20px;background:#eae8e4;border-radius:3px}
+.logreg label{display:block;font-size:.875em}
+.logreg input{margin:5px 0 15px}
+.logreg .cta{display:block}
+
+.avatar li{display:inline-block;margin-right:5px;text-align:center}
+.avatar img{border-radius:3px}
+
+/** Character Profile **/
+
+.cportrait img{width:100%}
+
+.cdata{display:inline-block;background:#fff;border-radius:3px;padding:12px 20px 0 20px;margin-bottom:5px}
+.cdata.square{text-align:center;width:10%;padding-top:0}
+.cdata.square.blue{background:#5cb6bd;color:#fff}
+.cdata .xpbar,.ctopics .xpbar{width:100%}
+.cdata .value{font-size:1.5em;margin:0;padding-top:8px}
+
+.crewards li{background:#fff;margin-bottom:5px;padding:15px 15px 10px 15px;border-radius:3px;font-size:.875em}
+.crewards li:last-child{margin-bottom:0}
+.crewards li .fa{font-size:1.25em}
+.crewards p{margin-bottom:5px}
+.crewards .passed .fa{color:#850000}
+
+.cgroups img{float:left;border-radius:3px;margin-right:5px;height:35px}
+.cgroups p{float:left;0;padding:0 12px;line-height:35px;background:#fff;border-radius:0 3px 3px 0}
+.cgroups a{float:left;padding:0 12px;line-height:35px;background:#50a4ab;color:#fff;background:#50a4ab;border-radius:3px 0 0 3px}
+
+.cranks li{clear:both;padding:8px 0 8px 8px;border-radius:3px}
+.cranks li:nth-child(odd){background:#fff}
+.cranks img{float:left;margin-right:15px;width:50px;height:50px;border-radius:25px}
+.cranks p,.ctopics p{margin:0;padding:0}
+
+.ctopics .xpbar,.ltopics .xpbar{background:#e4e1dd}
+.ctopics .xpbar span{background:#50a4ab}
+
+
+/** Charactergroup List **/
+
+.cglist li{list-style-type:decimal;background:#fff;margin:0 0 6px 25px;padding:5px 15px;border-radius:3px}
+.cglist .xp{float:right}
+.cgqlist li{list-style-type:square;margin-left:25px;padding:2px 15px}
+
+
+/** Charactergroup Profile **/
+
+.gbanner img{margin-top:10px;border-radius:3px}
+.gbanner h1{margin:10px 0 5px 0}
+
+.gdata{margin:20px 0 40px 0}
+.gdata li{float:left;background:#fff;padding:5px 15px;margin:0 5px 5px 0;border-radius:3px}
+
+.gchars li{background:#fff;padding:15px 0;border-radius:3px;display:inline-block;margin-bottom:5px;width:49%;text-align:center;vertical-align:top}
+.gchars img{width:50px;height:50px;border-radius:25px}
+.gchars p{margin:0;padding:0}
+.gchars .fa{position:absolute;margin:-10px 0 0}
+
+.gquests li{padding:12px 15px 0 15px;border-radius:3px}
+.gquests li:nth-child(odd){background:#fff}
+.gquests .date{color:#aca8a1;display:block}
+.gquests .xp{display:block}
+
+
+/** Charactergroup Quest **/
+
+.grpqlist li{background:#fff;margin-bottom:6px;padding:5px 15px 5px 0;font-size:.875em;border-radius:3px}
+.grpqlist .date{padding:0 15px;border-right:1px solid #dad8d5}
+.grpqlist .group{padding:0 15px}
+.grpqlist .xp{float:right}
+
+
+/** Achievements **/
+
+.rare ol{margin-bottom:40px}
+.rare li{margin-bottom:15px}
+.rare img{float:left;width:45px;height:45px;margin:4px 10px 0 0;border-radius:3px}
+.rare p{margin:0}
+
+.achmnts li{background:#fff;margin-bottom:10px;padding:15px;border-radius:3px}
+.achmnts img{width:55px;height:55px;float:left;margin-right:15px;border-radius:3px}
+.achmnts p,.achmnts h3{margin:0 0 4px;font-size:1em}
+.achmnts .unlcked{margin-top:3px;font-size:.875em;font-weight:normal;display:block}
+.achmnts .desc{font-size:.875em;padding-top:10px}
+.achmnts .prgrss{margin-top:15px}
+.achmnts .xpbar{margin:8px 0 0 0;width:80%}
+.achmnts .xpnumeric{margin:0}
+
+
+/** Library **/
+
+.libindxpr{margin-top:20px}
+.libindxpr p{font-weight:bold;margin:0}
+.libindxpr .xpbar{float:none;margin:9px 0 20px 0;background:#e4e1dd;width:100%}
+.libindxpr .xpbar span{background:#50a4ab}
+
+.libindx li{text-align:center;background:#fff;border-radius:3px;margin:0 0 15px 0;padding:15px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}
+.libindx i{margin-bottom:15px;font-size:1.25em;padding:0}
+.libindx .ltopc{overflow:hidden;text-overflow:ellipsis}
+.libindx .xpbar{width:100%;margin:0}
+
+.libtop li{background:#fff;padding:5px 5px 5px 10px;border-radius:3px;margin-bottom:6px}
+.libtop p{margin-bottom:2px}
+.libtop .addon li{display:inline;font-size:.875em;padding:0}
+.libtop .addon li:after{content:", "}
+.libtop .addon li:last-child:after{content: ""}
+
+
+/** Quest Types **/
+
+.success,.error{margin-top:15px;padding:5px 15px;border:1px solid #4F8A10;background:#DFF2BF;color:#4F8A10;border-radius:3px}
+.error{border:1px solid #850000;background:#ebd3d3;color:#850000}
+.success p,.error p{margin-bottom:5px}
+
+.solvdmsg{margin-top:20px}
+
+.mchoice{list-style-type:lower-alpha;list-style-position:inside}
+.mchoice li{margin:0 0 10px 0}
+.mchoice input[type=checkbox]{display:inline-block;margin:-19px 10px 0 24px;vertical-align:top}
+.mchoice label{width:90%;display:inline-block;margin:-25px 0 0 0;vertical-align:top}
+
+.submit{padding:15px;background:#eae8e4;border-radius:3px;margin-bottom:15px}
+.submit p{margin:15px 0 0 0;font-weight:bold}
+.submit ul{list-style-type:square;margin-left:20px}
+
+.textinput input[type=text]{height:16px;font-size:.875em}
+
+.crossword table{width:100%;max-width:800px;border-spacing:2px;border-collapse:separate}
+.crossword td{background:#d7d4cf;padding:1px}
+.crossword input[type=text]{text-align:center;height:26px;width:100%;min-width:8px;max-width:40px;margin:0;padding:0;border:none;text-transform:uppercase}
+.crossword ol{list-style-type:decimal;margin-left:25px}
+.crossword li{margin-top:20px}
+.crossword .index{position:absolute;font-size:0.625em;margin:-2px 0 0 2px}
+
+.opponent{width:50%;float:left}
+.opponent .portrait{height:250px;overflow:hidden;margin-bottom:15px}
+.opponent .hero{background:#fff;max-width:130px}
+.opponent .boss{max-width:100%}
+.opponent p{text-align:center}
+.opponent .fa{font-size:1.25em;color:#c7135b;padding-right:0}
+.bossfight .option{background:#fff;margin-bottom:10px;padding:15px 15px 5px;border-radius:3px}
+
+#dragZone{background:#eae8e4;box-sizing:border-box;padding:5px 5px 0 5px}
+.drop{background:#eae8e4}
+
+/** Media Queries **/
+
+@media only screen and (min-width:480px){
+.questgroups li,.ctopics li,.fll48{width:48%;display:inline-block;margin-right:2%}
+.questgroups li:nth-child(even),.ctopics li:nth-child(even),.flr48{width:48%;display:inline-block;margin-right:0;vertical-align:top}
+.xpinfo{display:inline-block;float:left;padding-right:20px}
+.xpbar{width:50%}
+
+.cinfo{float:left;width:70%}
+.cportrait{float:right;width:25%}
+
+.gbanner img{float:left;margin:20px 20px 0 0}
+.gbanner h1{margin:25px 0 2px}
+
+.gchars li{width:32%}
+
+.achmnts .xpbar{width:89%}
+
+.libindx li{float:left;width:49%}
+.libindx li:nth-child(2n){float:right}
+.libindx .ltopc{height:80px}
+
+.opponent .hero{max-width:200px}
+}
+
+@media only screen and (min-width:768px){
+.xpbar{width:70%}
+
+.gchars li{width:19%}
+
+.gquests .date{display:inline;margin-right:15px}
+.gquests .xp{float:right}
+
+.rare,.hunter{float:left;width:49%}
+.hunter{float:right}
+.achmnts .desc{padding-top:0}
+.achmnts .unlcked{font-size:.75em;float:right;margin:-6px -4px 0 0;color:#878787}
+
+.libindx li{float:left;width:32%;margin-right:2%}
+.libindx li:nth-child(2n){float:left}
+.libindx li:nth-child(3n){margin-right:0}
+
+.bossfight li{float:left;width:44%}
+.bossfight li:nth-child(even){float:right}
+.bossfight input,.bossfight p{text-align:center}
+}
+
+@media only screen and (min-width:1024px){
+header{position:fixed;left:0;top:0;bottom:0;width:300px;margin:0}
+header nav{margin:30px 10% 0}
+menu{display:block;opacity:1;position:relative}
+menu a{padding:10px 0}
+#profile{float:none;margin:0 0 15px 12px}
+#toggle,.toggle{display:none}
+.wrap{margin:0 0 0 300px}
+article{padding:20px 40px 80px 40px}
+.moodpic{margin:-20px -40px 0 -40px}
+.breadcrumbs li{display:inline;padding-right:5px}
+}
+
+@media only screen and (min-width:1366px){
+html{overflow-y:scroll;height:100%}
+body{background:#eae8e4;height:100%}
+.wrap{width:800px;max-width:800px;height:auto !important;min-height:100%;height:100%;position:relative;overflow:hidden}
+article{background:#f7f5f2;float:left}
+.moodpic{width:880px;height:230px}
+.moodpic img{width:100%}
+}
+
+@media only screen and (min-width:1600px){
+aside{display:block;float:left;width:350px;min-width:350px;margin:0 0 0 40px}
+aside section{margin:20px 0 40px 0}
+aside .char{width:120px;float:left;margin-right:10px}
+aside .charstats{background:#f7f5f2;border-radius:3px;padding:10px 0;margin-top:50px}
+aside .charstats li{font-size:.875em;padding:2px 0}
+aside .cranks li:nth-child(odd){background:#f7f5f2}
+}
\ No newline at end of file
diff --git a/www/error403.html b/www/error403.html
new file mode 100644
index 00000000..b1a0c1ea
--- /dev/null
+++ b/www/error403.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ Questlab
+
+
+
+ Questlab
+ Access denied.
+
+
+
diff --git a/www/error404.html b/www/error404.html
new file mode 100644
index 00000000..748077aa
--- /dev/null
+++ b/www/error404.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ Questlab
+
+
+
+ Questlab
+ Not found.
+
+
+
diff --git a/www/error500.html b/www/error500.html
new file mode 100644
index 00000000..5528716b
--- /dev/null
+++ b/www/error500.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ Questlab
+
+
+
+ Questlab
+ Internal server error.
+
+
+
diff --git a/www/index.php b/www/index.php
new file mode 100644
index 00000000..46301a31
--- /dev/null
+++ b/www/index.php
@@ -0,0 +1,45 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ /**
+ * Define constants
+ */
+ // Directory separator
+ if(!defined('DS')) {
+ define('DS', DIRECTORY_SEPARATOR);
+ }
+ // Root directory
+ if(!defined('ROOT')) {
+ define('ROOT', dirname(dirname(__FILE__)));
+ }
+
+
+ /**
+ * De-/Activate error messages
+ */
+ if($_SERVER['SERVER_ADDR'] == '127.0.0.1' || $_SERVER['SERVER_ADDR'] == '::1') {
+ error_reporting(E_ALL);
+ ini_set('display_errors', 1);
+ ini_set('log_errors', 0);
+ }
+ else {
+ error_reporting(E_ALL);
+ ini_set('display_errors', 0);
+ ini_set('log_errors', 1);
+ }
+
+
+ /**
+ * Run application
+ */
+ require ROOT.DS.'bootstrap.inc';
+
+?>
diff --git a/www/js/dnd.js b/www/js/dnd.js
new file mode 100644
index 00000000..ff1340c7
--- /dev/null
+++ b/www/js/dnd.js
@@ -0,0 +1,54 @@
+/**
+ * Drag&Drop-functions
+ */
+
+function onDragStart(event)
+{
+ jQuery(event.currentTarget).addClass("drag");
+ event.dataTransfer.setData("Text", event.target.id);
+}
+
+function onDragEnter(event)
+{
+ if(event.target.nodeName == "DIV" && !(event.target.parentNode.id == "dropZone" && event.target.hasChildNodes())) {
+ jQuery(event.target).addClass('drop');
+ }
+}
+
+function onDragOver(event)
+{
+ if(event.target.nodeName == "DIV" && !(event.target.parentNode.id == "dropZone" && event.target.hasChildNodes())) {
+ event.preventDefault();
+ }
+}
+
+function onDragLeave(event)
+{
+ jQuery(event.target).removeClass('drop');
+}
+
+function onDragEnd(event)
+{
+ jQuery(event.currentTarget).removeClass("drag");
+}
+
+function onDrop(event, setId)
+{
+ setId = (typeof setId == 'undefined') ? true : setId;
+ jQuery(event.currentTarget).removeClass('drag');
+ jQuery(event.target).removeClass('drop');
+ event.preventDefault();
+
+ var data = event.dataTransfer.getData("Text");
+ var dataElement = $('#'+data);
+ if(dataElement.parent() && $('#dnd_'+dataElement.parent().attr('id'))) {
+ $('#dnd_'+dataElement.parent().attr('id')).attr('value', "null");
+ }
+ jQuery(event.target).append(dataElement);
+
+ if(setId) {
+ console.log(data);
+ $('#dnd_' + jQuery(event.target).attr('id')).attr('value', data);
+ }
+
+}
diff --git a/www/js/imagelightbox.min.js b/www/js/imagelightbox.min.js
new file mode 100644
index 00000000..666b2b7f
--- /dev/null
+++ b/www/js/imagelightbox.min.js
@@ -0,0 +1,6 @@
+/*
+ By Osvaldas Valutis, www.osvaldas.info
+ Available for use under the MIT License
+*/
+
+;(function(e,t,n,r){"use strict";var i=function(){var e=n.body||n.documentElement,e=e.style;if(e.WebkitTransition=="")return"-webkit-";if(e.MozTransition=="")return"-moz-";if(e.OTransition=="")return"-o-";if(e.transition=="")return"";return false},s=i()===false?false:true,o=function(e,t,n){var r={},s=i();r[s+"transform"]="translateX("+t+")";r[s+"transition"]=s+"transform "+n+"s linear";e.css(r)},u="ontouchstart"in t,a=t.navigator.pointerEnabled||t.navigator.msPointerEnabled,f=function(e){if(u)return true;if(!a||typeof e==="undefined"||typeof e.pointerType==="undefined")return false;if(typeof e.MSPOINTER_TYPE_MOUSE!=="undefined"){if(e.MSPOINTER_TYPE_MOUSE!=e.pointerType)return true}else if(e.pointerType!="mouse")return true;return false};e.fn.imageLightbox=function(r){var r=e.extend({selector:'id="imagelightbox"',allowedTypes:"png|jpg|jpeg|gif",animationSpeed:250,preloadNext:true,enableKeyboard:true,quitOnEnd:false,quitOnImgClick:false,quitOnDocClick:true,onStart:false,onEnd:false,onLoadStart:false,onLoadEnd:false},r),i=e([]),l=e(),c=e(),h=0,p=0,d=0,v=false,m=function(t){return e(t).prop("tagName").toLowerCase()=="a"&&(new RegExp(".("+r.allowedTypes+")$","i")).test(e(t).attr("href"))},g=function(){if(!c.length)return false;var n=e(t).width()*.8,r=e(t).height()*.9,i=new Image;i.src=c.attr("src");i.onload=function(){h=i.width;p=i.height;if(h>n||p>r){var s=h/p>n/r?h/n:p/r;h/=s;p/=s}c.css({width:h+"px",height:p+"px",top:(e(t).height()-p)/2+"px",left:(e(t).width()-h)/2+"px"})}},y=function(t){if(v)return false;t=typeof t==="undefined"?false:t=="left"?1:-1;if(c.length){if(t!==false&&(i.length<2||r.quitOnEnd===true&&(t===-1&&i.index(l)==0||t===1&&i.index(l)==i.length-1))){w();return false}var n={opacity:0};if(s)o(c,100*t-d+"px",r.animationSpeed/1e3);else n.left=parseInt(c.css("left"))+100*t+"px";c.animate(n,r.animationSpeed,function(){b()});d=0}v=true;if(r.onLoadStart!==false)r.onLoadStart();setTimeout(function(){c=e(" ").attr("src",l.attr("href")).load(function(){c.appendTo("body");g();var n={opacity:1};c.css("opacity",0);if(s){o(c,-100*t+"px",0);setTimeout(function(){o(c,0+"px",r.animationSpeed/1e3)},50)}else{var u=parseInt(c.css("left"));n.left=u+"px";c.css("left",u-100*t+"px")}c.animate(n,r.animationSpeed,function(){v=false;if(r.onLoadEnd!==false)r.onLoadEnd()});if(r.preloadNext){var a=i.eq(i.index(l)+1);if(!a.length)a=i.eq(0);e(" ").attr("src",a.attr("href")).load()}}).error(function(){if(r.onLoadEnd!==false)r.onLoadEnd()});var n=0,u=0,p=0;c.on(a?"pointerup MSPointerUp":"click",function(e){e.preventDefault();if(r.quitOnImgClick){w();return false}if(f(e.originalEvent))return true;var t=(e.pageX||e.originalEvent.pageX)-e.target.offsetLeft;l=i.eq(i.index(l)-(h/2>t?1:-1));if(!l.length)l=i.eq(h/2>t?i.length:0);y(h/2>t?"left":"right")}).on("touchstart pointerdown MSPointerDown",function(e){if(!f(e.originalEvent)||r.quitOnImgClick)return true;if(s)p=parseInt(c.css("left"));n=e.originalEvent.pageX||e.originalEvent.touches[0].pageX}).on("touchmove pointermove MSPointerMove",function(e){if(!f(e.originalEvent)||r.quitOnImgClick)return true;e.preventDefault();u=e.originalEvent.pageX||e.originalEvent.touches[0].pageX;d=n-u;if(s)o(c,-d+"px",0);else c.css("left",p-d+"px")}).on("touchend touchcancel pointerup pointercancel MSPointerUp MSPointerCancel",function(e){if(!f(e.originalEvent)||r.quitOnImgClick)return true;if(Math.abs(d)>50){l=i.eq(i.index(l)-(d<0?1:-1));if(!l.length)l=i.eq(d<0?i.length:0);y(d>0?"right":"left")}else{if(s)o(c,0+"px",r.animationSpeed/1e3);else c.animate({left:p+"px"},r.animationSpeed/2)}})},r.animationSpeed+100)},b=function(){if(!c.length)return false;c.remove();c=e()},w=function(){if(!c.length)return false;c.animate({opacity:0},r.animationSpeed,function(){b();v=false;if(r.onEnd!==false)r.onEnd()})};e(t).on("resize",g);if(r.quitOnDocClick){e(n).on(u?"touchend":"click",function(t){if(c.length&&!e(t.target).is(c))w()})}if(r.enableKeyboard){e(n).on("keyup",function(e){if(!c.length)return true;e.preventDefault();if(e.keyCode==27)w();if(e.keyCode==37||e.keyCode==39){l=i.eq(i.index(l)-(e.keyCode==37?1:-1));if(!l.length)l=i.eq(e.keyCode==37?i.length:0);y(e.keyCode==37?"left":"right")}})}e(n).on("click",this.selector,function(t){if(!m(this))return true;t.preventDefault();if(v)return false;v=false;if(r.onStart!==false)r.onStart();l=e(this);y()});this.each(function(){if(!m(this))return true;i=i.add(e(this))});this.switchImageLightbox=function(e){var t=i.eq(e);if(t.length){var n=i.index(l);l=t;y(ef){if(b.getScrollTop()>=b.page.maxh)return!0}else if(0>=b.getScrollTop())return!0;b.scrollmom&&b.scrollmom.stop();
+b.lastdeltay+=f;b.debounced("mousewheely",function(){var d=b.lastdeltay;b.lastdeltay=0;b.rail.drag||b.doScrollBy(d)},120)}d.stopImmediatePropagation();return d.preventDefault()}var b=this;this.version="3.4.0";this.name="nicescroll";this.me=c;this.opt={doc:e("body"),win:!1};e.extend(this.opt,F);this.opt.snapbackspeed=80;if(k)for(var q in b.opt)"undefined"!=typeof k[q]&&(b.opt[q]=k[q]);this.iddoc=(this.doc=b.opt.doc)&&this.doc[0]?this.doc[0].id||"":"";this.ispage=/BODY|HTML/.test(b.opt.win?b.opt.win[0].nodeName:
+this.doc[0].nodeName);this.haswrapper=!1!==b.opt.win;this.win=b.opt.win||(this.ispage?e(window):this.doc);this.docscroll=this.ispage&&!this.haswrapper?e(window):this.win;this.body=e("body");this.iframe=this.isfixed=this.viewport=!1;this.isiframe="IFRAME"==this.doc[0].nodeName&&"IFRAME"==this.win[0].nodeName;this.istextarea="TEXTAREA"==this.win[0].nodeName;this.forcescreen=!1;this.canshowonmouseevent="scroll"!=b.opt.autohidemode;this.page=this.view=this.onzoomout=this.onzoomin=this.onscrollcancel=
+this.onscrollend=this.onscrollstart=this.onclick=this.ongesturezoom=this.onkeypress=this.onmousewheel=this.onmousemove=this.onmouseup=this.onmousedown=!1;this.scroll={x:0,y:0};this.scrollratio={x:0,y:0};this.cursorheight=20;this.scrollvaluemax=0;this.observerremover=this.observer=this.scrollmom=this.scrollrunning=this.checkrtlmode=!1;do this.id="ascrail"+K++;while(document.getElementById(this.id));this.hasmousefocus=this.hasfocus=this.zoomactive=this.zoom=this.selectiondrag=this.cursorfreezed=this.cursor=
+this.rail=!1;this.visibility=!0;this.hidden=this.locked=!1;this.cursoractive=!0;this.overflowx=b.opt.overflowx;this.overflowy=b.opt.overflowy;this.nativescrollingarea=!1;this.checkarea=0;this.events=[];this.saved={};this.delaylist={};this.synclist={};this.lastdeltay=this.lastdeltax=0;this.detected=M();var f=e.extend({},this.detected);this.ishwscroll=(this.canhwscroll=f.hastransform&&b.opt.hwacceleration)&&b.haswrapper;this.istouchcapable=!1;f.cantouch&&(f.ischrome&&!f.isios&&!f.isandroid)&&(this.istouchcapable=
+!0,f.cantouch=!1);f.cantouch&&(f.ismozilla&&!f.isios)&&(this.istouchcapable=!0,f.cantouch=!1);b.opt.enablemouselockapi||(f.hasmousecapture=!1,f.haspointerlock=!1);this.delayed=function(d,c,g,e){var f=b.delaylist[d],h=(new Date).getTime();if(!e&&f&&f.tt)return!1;f&&f.tt&&clearTimeout(f.tt);if(f&&f.last+g>h&&!f.tt)b.delaylist[d]={last:h+g,tt:setTimeout(function(){b.delaylist[d].tt=0;c.call()},g)};else if(!f||!f.tt)b.delaylist[d]={last:h,tt:0},setTimeout(function(){c.call()},0)};this.debounced=function(d,
+c,g){var f=b.delaylist[d];(new Date).getTime();b.delaylist[d]=c;f||setTimeout(function(){var c=b.delaylist[d];b.delaylist[d]=!1;c.call()},g)};this.synched=function(d,c){b.synclist[d]=c;(function(){b.onsync||(v(function(){b.onsync=!1;for(d in b.synclist){var c=b.synclist[d];c&&c.call(b);b.synclist[d]=!1}}),b.onsync=!0)})();return d};this.unsynched=function(d){b.synclist[d]&&(b.synclist[d]=!1)};this.css=function(d,c){for(var g in c)b.saved.css.push([d,g,d.css(g)]),d.css(g,c[g])};this.scrollTop=function(d){return"undefined"==
+typeof d?b.getScrollTop():b.setScrollTop(d)};this.scrollLeft=function(d){return"undefined"==typeof d?b.getScrollLeft():b.setScrollLeft(d)};BezierClass=function(b,c,g,f,e,h,l){this.st=b;this.ed=c;this.spd=g;this.p1=f||0;this.p2=e||1;this.p3=h||0;this.p4=l||1;this.ts=(new Date).getTime();this.df=this.ed-this.st};BezierClass.prototype={B2:function(b){return 3*b*b*(1-b)},B3:function(b){return 3*b*(1-b)*(1-b)},B4:function(b){return(1-b)*(1-b)*(1-b)},getNow:function(){var b=1-((new Date).getTime()-this.ts)/
+this.spd,c=this.B2(b)+this.B3(b)+this.B4(b);return 0>b?this.ed:this.st+Math.round(this.df*c)},update:function(b,c){this.st=this.getNow();this.ed=b;this.spd=c;this.ts=(new Date).getTime();this.df=this.ed-this.st;return this}};if(this.ishwscroll){this.doc.translate={x:0,y:0,tx:"0px",ty:"0px"};f.hastranslate3d&&f.isios&&this.doc.css("-webkit-backface-visibility","hidden");var r=function(){var d=b.doc.css(f.trstyle);return d&&"matrix"==d.substr(0,6)?d.replace(/^.*\((.*)\)$/g,"$1").replace(/px/g,"").split(/, +/):
+!1};this.getScrollTop=function(d){if(!d){if(d=r())return 16==d.length?-d[13]:-d[5];if(b.timerscroll&&b.timerscroll.bz)return b.timerscroll.bz.getNow()}return b.doc.translate.y};this.getScrollLeft=function(d){if(!d){if(d=r())return 16==d.length?-d[12]:-d[4];if(b.timerscroll&&b.timerscroll.bh)return b.timerscroll.bh.getNow()}return b.doc.translate.x};this.notifyScrollEvent=document.createEvent?function(b){var c=document.createEvent("UIEvents");c.initUIEvent("scroll",!1,!0,window,1);b.dispatchEvent(c)}:
+document.fireEvent?function(b){var c=document.createEventObject();b.fireEvent("onscroll");c.cancelBubble=!0}:function(b,c){};f.hastranslate3d&&b.opt.enabletranslate3d?(this.setScrollTop=function(d,c){b.doc.translate.y=d;b.doc.translate.ty=-1*d+"px";b.doc.css(f.trstyle,"translate3d("+b.doc.translate.tx+","+b.doc.translate.ty+",0px)");c||b.notifyScrollEvent(b.win[0])},this.setScrollLeft=function(d,c){b.doc.translate.x=d;b.doc.translate.tx=-1*d+"px";b.doc.css(f.trstyle,"translate3d("+b.doc.translate.tx+
+","+b.doc.translate.ty+",0px)");c||b.notifyScrollEvent(b.win[0])}):(this.setScrollTop=function(d,c){b.doc.translate.y=d;b.doc.translate.ty=-1*d+"px";b.doc.css(f.trstyle,"translate("+b.doc.translate.tx+","+b.doc.translate.ty+")");c||b.notifyScrollEvent(b.win[0])},this.setScrollLeft=function(d,c){b.doc.translate.x=d;b.doc.translate.tx=-1*d+"px";b.doc.css(f.trstyle,"translate("+b.doc.translate.tx+","+b.doc.translate.ty+")");c||b.notifyScrollEvent(b.win[0])})}else this.getScrollTop=function(){return b.docscroll.scrollTop()},
+this.setScrollTop=function(d){return b.docscroll.scrollTop(d)},this.getScrollLeft=function(){return b.docscroll.scrollLeft()},this.setScrollLeft=function(d){return b.docscroll.scrollLeft(d)};this.getTarget=function(b){return!b?!1:b.target?b.target:b.srcElement?b.srcElement:!1};this.hasParent=function(b,c){if(!b)return!1;for(var g=b.target||b.srcElement||b||!1;g&&g.id!=c;)g=g.parentNode||!1;return!1!==g};var u={thin:1,medium:3,thick:5};this.getOffset=function(){if(b.isfixed)return{top:parseFloat(b.win.css("top")),
+left:parseFloat(b.win.css("left"))};if(!b.viewport)return b.win.offset();var d=b.win.offset(),c=b.viewport.offset();return{top:d.top-c.top+b.viewport.scrollTop(),left:d.left-c.left+b.viewport.scrollLeft()}};this.updateScrollBar=function(d){if(b.ishwscroll)b.rail.css({height:b.win.innerHeight()}),b.railh&&b.railh.css({width:b.win.innerWidth()});else{var c=b.getOffset(),g=c.top,f=c.left,g=g+l(b.win,"border-top-width",!0);b.win.outerWidth();b.win.innerWidth();var f=f+(b.rail.align?b.win.outerWidth()-
+l(b.win,"border-right-width")-b.rail.width:l(b.win,"border-left-width")),e=b.opt.railoffset;e&&(e.top&&(g+=e.top),b.rail.align&&e.left&&(f+=e.left));b.locked||b.rail.css({top:g,left:f,height:d?d.h:b.win.innerHeight()});b.zoom&&b.zoom.css({top:g+1,left:1==b.rail.align?f-20:f+b.rail.width+4});b.railh&&!b.locked&&(g=c.top,f=c.left,d=b.railh.align?g+l(b.win,"border-top-width",!0)+b.win.innerHeight()-b.railh.height:g+l(b.win,"border-top-width",!0),f+=l(b.win,"border-left-width"),b.railh.css({top:d,left:f,
+width:b.railh.width}))}};this.doRailClick=function(d,c,g){var f;b.locked||(b.cancelEvent(d),c?(c=g?b.doScrollLeft:b.doScrollTop,f=g?(d.pageX-b.railh.offset().left-b.cursorwidth/2)*b.scrollratio.x:(d.pageY-b.rail.offset().top-b.cursorheight/2)*b.scrollratio.y,c(f)):(c=g?b.doScrollLeftBy:b.doScrollBy,f=g?b.scroll.x:b.scroll.y,d=g?d.pageX-b.railh.offset().left:d.pageY-b.rail.offset().top,g=g?b.view.w:b.view.h,f>=d?c(g):c(-g)))};b.hasanimationframe=v;b.hascancelanimationframe=w;b.hasanimationframe?b.hascancelanimationframe||
+(w=function(){b.cancelAnimationFrame=!0}):(v=function(b){return setTimeout(b,15-Math.floor(+new Date/1E3)%16)},w=clearInterval);this.init=function(){b.saved.css=[];if(f.isie7mobile)return!0;f.hasmstouch&&b.css(b.ispage?e("html"):b.win,{"-ms-touch-action":"none"});b.zindex="auto";b.zindex=!b.ispage&&"auto"==b.opt.zindex?h()||"auto":b.opt.zindex;!b.ispage&&"auto"!=b.zindex&&b.zindex>x&&(x=b.zindex);b.isie&&(0==b.zindex&&"auto"==b.opt.zindex)&&(b.zindex="auto");if(!b.ispage||!f.cantouch&&!f.isieold&&
+!f.isie9mobile){var d=b.docscroll;b.ispage&&(d=b.haswrapper?b.win:b.doc);f.isie9mobile||b.css(d,{"overflow-y":"hidden"});b.ispage&&f.isie7&&("BODY"==b.doc[0].nodeName?b.css(e("html"),{"overflow-y":"hidden"}):"HTML"==b.doc[0].nodeName&&b.css(e("body"),{"overflow-y":"hidden"}));f.isios&&(!b.ispage&&!b.haswrapper)&&b.css(e("body"),{"-webkit-overflow-scrolling":"touch"});var c=e(document.createElement("div"));c.css({position:"relative",top:0,"float":"right",width:b.opt.cursorwidth,height:"0px","background-color":b.opt.cursorcolor,
+border:b.opt.cursorborder,"background-clip":"padding-box","-webkit-border-radius":b.opt.cursorborderradius,"-moz-border-radius":b.opt.cursorborderradius,"border-radius":b.opt.cursorborderradius});c.hborder=parseFloat(c.outerHeight()-c.innerHeight());b.cursor=c;var g=e(document.createElement("div"));g.attr("id",b.id);g.addClass("nicescroll-rails");var l,k,n=["left","right"],G;for(G in n)k=n[G],(l=b.opt.railpadding[k])?g.css("padding-"+k,l+"px"):b.opt.railpadding[k]=0;g.append(c);g.width=Math.max(parseFloat(b.opt.cursorwidth),
+c.outerWidth())+b.opt.railpadding.left+b.opt.railpadding.right;g.css({width:g.width+"px",zIndex:b.zindex,background:b.opt.background,cursor:"default"});g.visibility=!0;g.scrollable=!0;g.align="left"==b.opt.railalign?0:1;b.rail=g;c=b.rail.drag=!1;b.opt.boxzoom&&(!b.ispage&&!f.isieold)&&(c=document.createElement("div"),b.bind(c,"click",b.doZoom),b.zoom=e(c),b.zoom.css({cursor:"pointer","z-index":b.zindex,backgroundImage:"url("+L+"zoomico.png)",height:18,width:18,backgroundPosition:"0px 0px"}),b.opt.dblclickzoom&&
+b.bind(b.win,"dblclick",b.doZoom),f.cantouch&&b.opt.gesturezoom&&(b.ongesturezoom=function(d){1.5d.scale&&b.doZoomOut(d);return b.cancelEvent(d)},b.bind(b.win,"gestureend",b.ongesturezoom)));b.railh=!1;if(b.opt.horizrailenabled){b.css(d,{"overflow-x":"hidden"});c=e(document.createElement("div"));c.css({position:"relative",top:0,height:b.opt.cursorwidth,width:"0px","background-color":b.opt.cursorcolor,border:b.opt.cursorborder,"background-clip":"padding-box","-webkit-border-radius":b.opt.cursorborderradius,
+"-moz-border-radius":b.opt.cursorborderradius,"border-radius":b.opt.cursorborderradius});c.wborder=parseFloat(c.outerWidth()-c.innerWidth());b.cursorh=c;var m=e(document.createElement("div"));m.attr("id",b.id+"-hr");m.addClass("nicescroll-rails");m.height=Math.max(parseFloat(b.opt.cursorwidth),c.outerHeight());m.css({height:m.height+"px",zIndex:b.zindex,background:b.opt.background});m.append(c);m.visibility=!0;m.scrollable=!0;m.align="top"==b.opt.railvalign?0:1;b.railh=m;b.railh.drag=!1}b.ispage?
+(g.css({position:"fixed",top:"0px",height:"100%"}),g.align?g.css({right:"0px"}):g.css({left:"0px"}),b.body.append(g),b.railh&&(m.css({position:"fixed",left:"0px",width:"100%"}),m.align?m.css({bottom:"0px"}):m.css({top:"0px"}),b.body.append(m))):(b.ishwscroll?("static"==b.win.css("position")&&b.css(b.win,{position:"relative"}),d="HTML"==b.win[0].nodeName?b.body:b.win,b.zoom&&(b.zoom.css({position:"absolute",top:1,right:0,"margin-right":g.width+4}),d.append(b.zoom)),g.css({position:"absolute",top:0}),
+g.align?g.css({right:0}):g.css({left:0}),d.append(g),m&&(m.css({position:"absolute",left:0,bottom:0}),m.align?m.css({bottom:0}):m.css({top:0}),d.append(m))):(b.isfixed="fixed"==b.win.css("position"),d=b.isfixed?"fixed":"absolute",b.isfixed||(b.viewport=b.getViewport(b.win[0])),b.viewport&&(b.body=b.viewport,!1==/relative|absolute/.test(b.viewport.css("position"))&&b.css(b.viewport,{position:"relative"})),g.css({position:d}),b.zoom&&b.zoom.css({position:d}),b.updateScrollBar(),b.body.append(g),b.zoom&&
+b.body.append(b.zoom),b.railh&&(m.css({position:d}),b.body.append(m))),f.isios&&b.css(b.win,{"-webkit-tap-highlight-color":"rgba(0,0,0,0)","-webkit-touch-callout":"none"}),f.isie&&b.opt.disableoutline&&b.win.attr("hideFocus","true"),f.iswebkit&&b.opt.disableoutline&&b.win.css({outline:"none"}));!1===b.opt.autohidemode?(b.autohidedom=!1,b.rail.css({opacity:b.opt.cursoropacitymax}),b.railh&&b.railh.css({opacity:b.opt.cursoropacitymax})):!0===b.opt.autohidemode?(b.autohidedom=e().add(b.rail),f.isie8&&
+(b.autohidedom=b.autohidedom.add(b.cursor)),b.railh&&(b.autohidedom=b.autohidedom.add(b.railh)),b.railh&&f.isie8&&(b.autohidedom=b.autohidedom.add(b.cursorh))):"scroll"==b.opt.autohidemode?(b.autohidedom=e().add(b.rail),b.railh&&(b.autohidedom=b.autohidedom.add(b.railh))):"cursor"==b.opt.autohidemode?(b.autohidedom=e().add(b.cursor),b.railh&&(b.autohidedom=b.autohidedom.add(b.cursorh))):"hidden"==b.opt.autohidemode&&(b.autohidedom=!1,b.hide(),b.locked=!1);if(f.isie9mobile)b.scrollmom=new H(b),b.onmangotouch=
+function(d){d=b.getScrollTop();var c=b.getScrollLeft();if(d==b.scrollmom.lastscrolly&&c==b.scrollmom.lastscrollx)return!0;var g=d-b.mangotouch.sy,f=c-b.mangotouch.sx;if(0!=Math.round(Math.sqrt(Math.pow(f,2)+Math.pow(g,2)))){var p=0>g?-1:1,e=0>f?-1:1,h=+new Date;b.mangotouch.lazy&&clearTimeout(b.mangotouch.lazy);80s?s=Math.round(s/2):s>b.page.maxh&&(s=b.page.maxh+Math.round((s-b.page.maxh)/2)):(0>s&&(h=s=0),s>b.page.maxh&&(s=b.page.maxh,h=0));if(b.railh&&b.railh.scrollable){var m=b.rail.drag.sl-k;b.ishwscroll&&b.opt.bouncescroll?0>m?m=Math.round(m/2):m>b.page.maxw&&(m=b.page.maxw+
+Math.round((m-b.page.maxw)/2)):(0>m&&(l=m=0),m>b.page.maxw&&(m=b.page.maxw,l=0))}g=!1;if(b.rail.drag.dl)g=!0,"v"==b.rail.drag.dl?m=b.rail.drag.sl:"h"==b.rail.drag.dl&&(s=b.rail.drag.st);else{var p=Math.abs(p),k=Math.abs(k),n=b.opt.directionlockdeadzone;if("v"==b.rail.drag.ck){if(p>n&&k<=0.3*p)return b.rail.drag=!1,!0;k>n&&(b.rail.drag.dl="f",e("body").scrollTop(e("body").scrollTop()))}else if("h"==b.rail.drag.ck){if(k>n&&p<=0.3*az)return b.rail.drag=!1,!0;p>n&&(b.rail.drag.dl="f",e("body").scrollLeft(e("body").scrollLeft()))}}b.synched("touchmove",
+function(){b.rail.drag&&2==b.rail.drag.pt&&(b.prepareTransition&&b.prepareTransition(0),b.rail.scrollable&&b.setScrollTop(s),b.scrollmom.update(l,h),b.railh&&b.railh.scrollable?(b.setScrollLeft(m),b.showCursor(s,m)):b.showCursor(s),f.isie10&&document.selection.clear())});f.ischrome&&b.istouchcapable&&(g=!1);if(g)return b.cancelEvent(d)}}}b.onmousedown=function(d,c){if(!(b.rail.drag&&1!=b.rail.drag.pt)){if(b.locked)return b.cancelEvent(d);b.cancelScroll();b.rail.drag={x:d.clientX,y:d.clientY,sx:b.scroll.x,
+sy:b.scroll.y,pt:1,hr:!!c};var g=b.getTarget(d);!b.ispage&&f.hasmousecapture&&g.setCapture();b.isiframe&&!f.hasmousecapture&&(b.saved.csspointerevents=b.doc.css("pointer-events"),b.css(b.doc,{"pointer-events":"none"}));return b.cancelEvent(d)}};b.onmouseup=function(d){if(b.rail.drag&&(f.hasmousecapture&&document.releaseCapture(),b.isiframe&&!f.hasmousecapture&&b.doc.css("pointer-events",b.saved.csspointerevents),1==b.rail.drag.pt))return b.rail.drag=!1,b.cancelEvent(d)};b.onmousemove=function(d){if(b.rail.drag&&
+1==b.rail.drag.pt){if(f.ischrome&&0==d.which)return b.onmouseup(d);b.cursorfreezed=!0;if(b.rail.drag.hr){b.scroll.x=b.rail.drag.sx+(d.clientX-b.rail.drag.x);0>b.scroll.x&&(b.scroll.x=0);var c=b.scrollvaluemaxw;b.scroll.x>c&&(b.scroll.x=c)}else b.scroll.y=b.rail.drag.sy+(d.clientY-b.rail.drag.y),0>b.scroll.y&&(b.scroll.y=0),c=b.scrollvaluemax,b.scroll.y>c&&(b.scroll.y=c);b.synched("mousemove",function(){b.rail.drag&&1==b.rail.drag.pt&&(b.showCursor(),b.rail.drag.hr?b.doScrollLeft(Math.round(b.scroll.x*
+b.scrollratio.x),b.opt.cursordragspeed):b.doScrollTop(Math.round(b.scroll.y*b.scrollratio.y),b.opt.cursordragspeed))});return b.cancelEvent(d)}};if(f.cantouch||b.opt.touchbehavior)b.onpreventclick=function(d){if(b.preventclick)return b.preventclick.tg.onclick=b.preventclick.click,b.preventclick=!1,b.cancelEvent(d)},b.bind(b.win,"mousedown",b.ontouchstart),b.onclick=f.isios?!1:function(d){return b.lastmouseup?(b.lastmouseup=!1,b.cancelEvent(d)):!0},b.opt.grabcursorenabled&&f.cursorgrabvalue&&(b.css(b.ispage?
+b.doc:b.win,{cursor:f.cursorgrabvalue}),b.css(b.rail,{cursor:f.cursorgrabvalue}));else{var r=function(d){if(b.selectiondrag){if(d){var c=b.win.outerHeight();d=d.pageY-b.selectiondrag.top;0=c&&(d-=c);b.selectiondrag.df=d}0!=b.selectiondrag.df&&(b.doScrollBy(2*-Math.floor(b.selectiondrag.df/6)),b.debounced("doselectionscroll",function(){r()},50))}};b.hasTextSelected="getSelection"in document?function(){return 0b.page.maxh?b.doScrollTop(b.page.maxh):(b.scroll.y=Math.round(b.getScrollTop()*(1/b.scrollratio.y)),
+b.scroll.x=Math.round(b.getScrollLeft()*(1/b.scrollratio.x)),b.cursoractive&&b.noticeCursor());b.scroll.y&&0==b.getScrollTop()&&b.doScrollTo(Math.floor(b.scroll.y*b.scrollratio.y));return b};this.resize=b.onResize;this.lazyResize=function(d){d=isNaN(d)?30:d;b.delayed("resize",b.resize,d);return b};this._bind=function(d,c,g,f){b.events.push({e:d,n:c,f:g,b:f,q:!1});d.addEventListener?d.addEventListener(c,g,f||!1):d.attachEvent?d.attachEvent("on"+c,g):d["on"+c]=g};this.jqbind=function(d,c,g){b.events.push({e:d,
+n:c,f:g,q:!0});e(d).bind(c,g)};this.bind=function(d,c,g,e){var h="jquery"in d?d[0]:d;"mousewheel"==c?"onwheel"in b.win?b._bind(h,"wheel",g,e||!1):(d="undefined"!=typeof document.onmousewheel?"mousewheel":"DOMMouseScroll",n(h,d,g,e||!1),"DOMMouseScroll"==d&&n(h,"MozMousePixelScroll",g,e||!1)):h.addEventListener?(f.cantouch&&/mouseup|mousedown|mousemove/.test(c)&&b._bind(h,"mousedown"==c?"touchstart":"mouseup"==c?"touchend":"touchmove",function(b){if(b.touches){if(2>b.touches.length){var d=b.touches.length?
+b.touches[0]:b;d.original=b;g.call(this,d)}}else b.changedTouches&&(d=b.changedTouches[0],d.original=b,g.call(this,d))},e||!1),b._bind(h,c,g,e||!1),f.cantouch&&"mouseup"==c&&b._bind(h,"touchcancel",g,e||!1)):b._bind(h,c,function(d){if((d=d||window.event||!1)&&d.srcElement)d.target=d.srcElement;"pageY"in d||(d.pageX=d.clientX+document.documentElement.scrollLeft,d.pageY=d.clientY+document.documentElement.scrollTop);return!1===g.call(h,d)||!1===e?b.cancelEvent(d):!0})};this._unbind=function(b,c,g,f){b.removeEventListener?
+b.removeEventListener(c,g,f):b.detachEvent?b.detachEvent("on"+c,g):b["on"+c]=!1};this.unbindAll=function(){for(var d=0;d
+(b.newscrolly-h)*(e-h)||0>(b.newscrollx-l)*(c-l))&&b.cancelScroll();!1==b.opt.bouncescroll&&(0>e?e=0:e>b.page.maxh&&(e=b.page.maxh),0>c?c=0:c>b.page.maxw&&(c=b.page.maxw));if(b.scrollrunning&&c==b.newscrollx&&e==b.newscrolly)return!1;b.newscrolly=e;b.newscrollx=c;b.newscrollspeed=g||!1;if(b.timer)return!1;b.timer=setTimeout(function(){var g=b.getScrollTop(),h=b.getScrollLeft(),l,k;l=c-h;k=e-g;l=Math.round(Math.sqrt(Math.pow(l,2)+Math.pow(k,2)));l=b.newscrollspeed&&1=b.newscrollspeed&&(l*=b.newscrollspeed);b.prepareTransition(l,!0);b.timerscroll&&b.timerscroll.tm&&clearInterval(b.timerscroll.tm);0c?c=0:c>b.page.maxh&&(c=b.page.maxh);0>e?e=0:e>b.page.maxw&&(e=b.page.maxw);if(c!=b.newscrolly||e!=b.newscrollx)return b.doScrollPos(e,c,b.opt.snapbackspeed);b.onscrollend&&b.scrollrunning&&b.onscrollend.call(b,{type:"scrollend",current:{x:e,y:c},end:{x:b.newscrollx,y:b.newscrolly}});b.scrollrunning=
+!1}):(this.doScrollLeft=function(c,f){var g=b.scrollrunning?b.newscrolly:b.getScrollTop();b.doScrollPos(c,g,f)},this.doScrollTop=function(c,f){var g=b.scrollrunning?b.newscrollx:b.getScrollLeft();b.doScrollPos(g,c,f)},this.doScrollPos=function(c,f,g){function e(){if(b.cancelAnimationFrame)return!0;b.scrollrunning=!0;if(r=1-r)return b.timer=v(e)||1;var c=0,d=sy=b.getScrollTop();if(b.dst.ay){var d=b.bzscroll?b.dst.py+b.bzscroll.getNow()*b.dst.ay:b.newscrolly,g=d-sy;if(0>g&&db.newscrolly)d=
+b.newscrolly;b.setScrollTop(d);d==b.newscrolly&&(c=1)}else c=1;var f=sx=b.getScrollLeft();if(b.dst.ax){f=b.bzscroll?b.dst.px+b.bzscroll.getNow()*b.dst.ax:b.newscrollx;g=f-sx;if(0>g&&fb.newscrollx)f=b.newscrollx;b.setScrollLeft(f);f==b.newscrollx&&(c+=1)}else c+=1;2==c?(b.timer=0,b.cursorfreezed=!1,b.bzscroll=!1,b.scrollrunning=!1,0>d?d=0:d>b.page.maxh&&(d=b.page.maxh),0>f?f=0:f>b.page.maxw&&(f=b.page.maxw),f!=b.newscrollx||d!=b.newscrolly?b.doScrollPos(f,d):b.onscrollend&&b.onscrollend.call(b,
+{type:"scrollend",current:{x:sx,y:sy},end:{x:b.newscrollx,y:b.newscrolly}})):b.timer=v(e)||1}f="undefined"==typeof f||!1===f?b.getScrollTop(!0):f;if(b.timer&&b.newscrolly==f&&b.newscrollx==c)return!0;b.timer&&w(b.timer);b.timer=0;var h=b.getScrollTop(),l=b.getScrollLeft();(0>(b.newscrolly-h)*(f-h)||0>(b.newscrollx-l)*(c-l))&&b.cancelScroll();b.newscrolly=f;b.newscrollx=c;if(!b.bouncescroll||!b.rail.visibility)0>b.newscrolly?b.newscrolly=0:b.newscrolly>b.page.maxh&&(b.newscrolly=b.page.maxh);if(!b.bouncescroll||
+!b.railh.visibility)0>b.newscrollx?b.newscrollx=0:b.newscrollx>b.page.maxw&&(b.newscrollx=b.page.maxw);b.dst={};b.dst.x=c-l;b.dst.y=f-h;b.dst.px=l;b.dst.py=h;var k=Math.round(Math.sqrt(Math.pow(b.dst.x,2)+Math.pow(b.dst.y,2)));b.dst.ax=b.dst.x/k;b.dst.ay=b.dst.y/k;var n=0,q=k;0==b.dst.x?(n=h,q=f,b.dst.ay=1,b.dst.py=0):0==b.dst.y&&(n=l,q=c,b.dst.ax=1,b.dst.px=0);k=b.getTransitionSpeed(k);g&&1>=g&&(k*=g);b.bzscroll=0=b.page.maxh||l==b.page.maxw&&c>=b.page.maxw)&&b.checkContentSize();var r=1;b.cancelAnimationFrame=!1;b.timer=1;b.onscrollstart&&!b.scrollrunning&&b.onscrollstart.call(b,{type:"scrollstart",current:{x:l,y:h},request:{x:c,y:f},end:{x:b.newscrollx,y:b.newscrolly},speed:k});e();(h==b.page.maxh&&f>=h||l==b.page.maxw&&c>=l)&&b.checkContentSize();b.noticeCursor()}},this.cancelScroll=function(){b.timer&&w(b.timer);b.timer=0;b.bzscroll=!1;b.scrollrunning=!1;return b}):(this.doScrollLeft=function(c,
+f){var g=b.getScrollTop();b.doScrollPos(c,g,f)},this.doScrollTop=function(c,f){var g=b.getScrollLeft();b.doScrollPos(g,c,f)},this.doScrollPos=function(c,f,g){var e=c>b.page.maxw?b.page.maxw:c;0>e&&(e=0);var h=f>b.page.maxh?b.page.maxh:f;0>h&&(h=0);b.synched("scroll",function(){b.setScrollTop(h);b.setScrollLeft(e)})},this.cancelScroll=function(){});this.doScrollBy=function(c,f){var g=0,g=f?Math.floor((b.scroll.y-c)*b.scrollratio.y):(b.timer?b.newscrolly:b.getScrollTop(!0))-c;if(b.bouncescroll){var e=
+Math.round(b.view.h/2);g<-e?g=-e:g>b.page.maxh+e&&(g=b.page.maxh+e)}b.cursorfreezed=!1;py=b.getScrollTop(!0);if(0>g&&0>=py)return b.noticeCursor();if(g>b.page.maxh&&py>=b.page.maxh)return b.checkContentSize(),b.noticeCursor();b.doScrollTop(g)};this.doScrollLeftBy=function(c,f){var g=0,g=f?Math.floor((b.scroll.x-c)*b.scrollratio.x):(b.timer?b.newscrollx:b.getScrollLeft(!0))-c;if(b.bouncescroll){var e=Math.round(b.view.w/2);g<-e?g=-e:g>b.page.maxw+e&&(g=b.page.maxw+e)}b.cursorfreezed=!1;px=b.getScrollLeft(!0);
+if(0>g&&0>=px||g>b.page.maxw&&px>=b.page.maxw)return b.noticeCursor();b.doScrollLeft(g)};this.doScrollTo=function(c,f){f&&Math.round(c*b.scrollratio.y);b.cursorfreezed=!1;b.doScrollTop(c)};this.checkContentSize=function(){var c=b.getContentSize();(c.h!=b.page.h||c.w!=b.page.w)&&b.resize(!1,c)};b.onscroll=function(c){b.rail.drag||b.cursorfreezed||b.synched("scroll",function(){b.scroll.y=Math.round(b.getScrollTop()*(1/b.scrollratio.y));b.railh&&(b.scroll.x=Math.round(b.getScrollLeft()*(1/b.scrollratio.x)));
+b.noticeCursor()})};b.bind(b.docscroll,"scroll",b.onscroll);this.doZoomIn=function(c){if(!b.zoomactive){b.zoomactive=!0;b.zoomrestore={style:{}};var h="position top left zIndex backgroundColor marginTop marginBottom marginLeft marginRight".split(" "),g=b.win[0].style,l;for(l in h){var k=h[l];b.zoomrestore.style[k]="undefined"!=typeof g[k]?g[k]:""}b.zoomrestore.style.width=b.win.css("width");b.zoomrestore.style.height=b.win.css("height");b.zoomrestore.padding={w:b.win.outerWidth()-b.win.width(),h:b.win.outerHeight()-
+b.win.height()};f.isios4&&(b.zoomrestore.scrollTop=e(window).scrollTop(),e(window).scrollTop(0));b.win.css({position:f.isios4?"absolute":"fixed",top:0,left:0,"z-index":x+100,margin:"0px"});h=b.win.css("backgroundColor");(""==h||/transparent|rgba\(0, 0, 0, 0\)|rgba\(0,0,0,0\)/.test(h))&&b.win.css("backgroundColor","#fff");b.rail.css({"z-index":x+101});b.zoom.css({"z-index":x+102});b.zoom.css("backgroundPosition","0px -18px");b.resizeZoom();b.onzoomin&&b.onzoomin.call(b);return b.cancelEvent(c)}};this.doZoomOut=
+function(c){if(b.zoomactive)return b.zoomactive=!1,b.win.css("margin",""),b.win.css(b.zoomrestore.style),f.isios4&&e(window).scrollTop(b.zoomrestore.scrollTop),b.rail.css({"z-index":b.zindex}),b.zoom.css({"z-index":b.zindex}),b.zoomrestore=!1,b.zoom.css("backgroundPosition","0px 0px"),b.onResize(),b.onzoomout&&b.onzoomout.call(b),b.cancelEvent(c)};this.doZoom=function(c){return b.zoomactive?b.doZoomOut(c):b.doZoomIn(c)};this.resizeZoom=function(){if(b.zoomactive){var c=b.getScrollTop();b.win.css({width:e(window).width()-
+b.zoomrestore.padding.w+"px",height:e(window).height()-b.zoomrestore.padding.h+"px"});b.onResize();b.setScrollTop(Math.min(b.page.maxh,c))}};this.init();e.nicescroll.push(this)},H=function(e){var c=this;this.nc=e;this.steptime=this.lasttime=this.speedy=this.speedx=this.lasty=this.lastx=0;this.snapy=this.snapx=!1;this.demuly=this.demulx=0;this.lastscrolly=this.lastscrollx=-1;this.timer=this.chky=this.chkx=0;this.time=function(){return+new Date};this.reset=function(e,l){c.stop();var k=c.time();c.steptime=
+0;c.lasttime=k;c.speedx=0;c.speedy=0;c.lastx=e;c.lasty=l;c.lastscrollx=-1;c.lastscrolly=-1};this.update=function(e,l){var k=c.time();c.steptime=k-c.lasttime;c.lasttime=k;var k=l-c.lasty,t=e-c.lastx,b=c.nc.getScrollTop(),q=c.nc.getScrollLeft(),b=b+k,q=q+t;c.snapx=0>q||q>c.nc.page.maxw;c.snapy=0>b||b>c.nc.page.maxh;c.speedx=t;c.speedy=k;c.lastx=e;c.lasty=l};this.stop=function(){c.nc.unsynched("domomentum2d");c.timer&&clearTimeout(c.timer);c.timer=0;c.lastscrollx=-1;c.lastscrolly=-1};this.doSnapy=function(e,
+l){var k=!1;0>l?(l=0,k=!0):l>c.nc.page.maxh&&(l=c.nc.page.maxh,k=!0);0>e?(e=0,k=!0):e>c.nc.page.maxw&&(e=c.nc.page.maxw,k=!0);k&&c.nc.doScrollPos(e,l,c.nc.opt.snapbackspeed)};this.doMomentum=function(e){var l=c.time(),k=e?l+e:c.lasttime;e=c.nc.getScrollLeft();var t=c.nc.getScrollTop(),b=c.nc.page.maxh,q=c.nc.page.maxw;c.speedx=0=l-k;if(0>t||t>b||0>e||e>q)k=!1;e=c.speedx&&k?c.speedx:!1;if(c.speedy&&k&&c.speedy||e){var f=Math.max(16,
+c.steptime);50r||r>q))e=0.1;if(c.speedy&&(u=Math.floor(c.lastscrolly-c.speedy*(1-c.demulxy)),c.lastscrolly=u,0>u||u>b))e=0.1;c.demulxy=Math.min(1,c.demulxy+e);c.nc.synched("domomentum2d",
+function(){c.speedx&&(c.nc.getScrollLeft()!=c.chkx&&c.stop(),c.chkx=r,c.nc.setScrollLeft(r));c.speedy&&(c.nc.getScrollTop()!=c.chky&&c.stop(),c.chky=u,c.nc.setScrollTop(u));c.timer||(c.nc.hideCursor(),c.doSnapy(r,u))});1>c.demulxy?c.timer=setTimeout(d,f):(c.stop(),c.nc.hideCursor(),c.doSnapy(r,u))};d()}else c.doSnapy(c.nc.getScrollLeft(),c.nc.getScrollTop())}},A=e.fn.scrollTop;e.cssHooks.pageYOffset={get:function(k,c,h){return(c=e.data(k,"__nicescroll")||!1)&&c.ishwscroll?c.getScrollTop():A.call(k)},
+set:function(k,c){var h=e.data(k,"__nicescroll")||!1;h&&h.ishwscroll?h.setScrollTop(parseInt(c)):A.call(k,c);return this}};e.fn.scrollTop=function(k){if("undefined"==typeof k){var c=this[0]?e.data(this[0],"__nicescroll")||!1:!1;return c&&c.ishwscroll?c.getScrollTop():A.call(this)}return this.each(function(){var c=e.data(this,"__nicescroll")||!1;c&&c.ishwscroll?c.setScrollTop(parseInt(k)):A.call(e(this),k)})};var B=e.fn.scrollLeft;e.cssHooks.pageXOffset={get:function(k,c,h){return(c=e.data(k,"__nicescroll")||
+!1)&&c.ishwscroll?c.getScrollLeft():B.call(k)},set:function(k,c){var h=e.data(k,"__nicescroll")||!1;h&&h.ishwscroll?h.setScrollLeft(parseInt(c)):B.call(k,c);return this}};e.fn.scrollLeft=function(k){if("undefined"==typeof k){var c=this[0]?e.data(this[0],"__nicescroll")||!1:!1;return c&&c.ishwscroll?c.getScrollLeft():B.call(this)}return this.each(function(){var c=e.data(this,"__nicescroll")||!1;c&&c.ishwscroll?c.setScrollLeft(parseInt(k)):B.call(e(this),k)})};var C=function(k){var c=this;this.length=
+0;this.name="nicescrollarray";this.each=function(e){for(var h=0;h