PageRenderTime 62ms CodeModel.GetById 12ms RepoModel.GetById 1ms app.codeStats 0ms

/app/models/character.php

https://github.com/bheiskell/Riiga
PHP | 524 lines | 340 code | 47 blank | 137 comment | 21 complexity | 5b27ef687ed4b4ddcd50199c4659417a MD5 | raw file
  1. <?php
  2. class Character extends AppModel {
  3. var $name = 'Character';
  4. var $order = array('UPPER(Character.name)' => 'ASC');
  5. var $hasAndBelongsToMany = array(
  6. 'Story' => array('with' => 'CharactersStory')
  7. );
  8. var $actsAs = array(
  9. 'Pending',
  10. 'Sluggable' => array(
  11. 'label' => 'name',
  12. 'separator' => '_',
  13. 'overwrite' => true,
  14. 'ignore' => array(),
  15. ),
  16. );
  17. var $belongsTo = array(
  18. 'Faction',
  19. 'FactionRank',
  20. 'Location',
  21. 'Race',
  22. 'Rank',
  23. 'Subrace',
  24. 'User',
  25. );
  26. var $validate = array(
  27. 'user_rank' => array(
  28. 'required' => false,
  29. 'allowEmpty' => false,
  30. 'rule' => array('checkLimit'),
  31. 'message' => 'Authors are allocated one character per rank.',
  32. ),
  33. 'user_id' => array(
  34. 'required' => false,
  35. 'allowEmpty' => false,
  36. 'rule' => array('comparison', '>=', 0),
  37. 'message' => 'Invalid Member'
  38. ),
  39. 'name' => array(
  40. array(
  41. 'required' => true,
  42. 'allowEmpty' => false,
  43. 'rule' => array('isUnique'),
  44. 'message' => 'This name is already taken; Try including a surname.',
  45. ),
  46. array(
  47. 'required' => true,
  48. 'allowEmpty' => false,
  49. 'rule' => array('maxlength', 256),
  50. 'message' => 'Name must be between one and 256 characters.',
  51. ),
  52. ),
  53. 'rank_id' => array(
  54. 'required' => true,
  55. 'allowEmpty' => false,
  56. 'rule' => array('checkRank'),
  57. ),
  58. 'race_id' => array(
  59. 'limitByRank' => array('rule' => array('limitByRank', 'Race')),
  60. 'numeric' => array (
  61. 'required' => true,
  62. 'allowEmpty' => false,
  63. 'rule' => array('numeric'),
  64. 'message' => "Specify the character's race."
  65. ),
  66. ),
  67. 'subrace_id' => array(
  68. 'checkSubrace' => array (
  69. 'rule' => array('checkSubrace'),
  70. 'message' => 'Only when human must a subrace be specified',
  71. ),
  72. 'numeric' => array(
  73. //'required' => true,
  74. 'allowEmpty' => true,
  75. 'rule' => array('numeric'),
  76. ),
  77. ),
  78. 'location_id' => array(
  79. 'limitByRank' => array('rule' => array('limitByRank', 'Location')),
  80. 'numeric' => array (
  81. 'required' => true,
  82. 'allowEmpty' => false,
  83. 'rule' => array('numeric'),
  84. 'message' => "Specify the character's residency.",
  85. ),
  86. ),
  87. 'faction_id' => array(
  88. 'checkFaction' => array('rule' => array('checkFaction')),
  89. 'numeric' => array (
  90. // The required validator checks using isset, not array_key_exists. I'm
  91. // not sure why, but cakephp's search must null out empty columns. Thus
  92. // my pending behavior pulls down non-isset compatible data. This is
  93. // probably fixable via checking each array value for === null.
  94. //'required' => true,
  95. 'allowEmpty' => true,
  96. 'rule' => array('numeric'),
  97. 'message' => "Faction ID must be numeric.",
  98. ),
  99. ),
  100. 'faction_rank_id' => array(
  101. 'checkFactionRank' => array(
  102. 'rule' => array('checkFactionRank'),
  103. 'message' => 'The faction rank does not belong to the selected faction',
  104. ),
  105. 'checkFactionRankAge' => array(
  106. 'rule' => array('checkFactionRankAge'),
  107. 'message' => 'The character is not old enough for that faction rank.',
  108. ),
  109. 'checkFactionRankLevel' => array(
  110. 'rule' => array('checkFactionRankLevel'),
  111. 'message' => 'The character is not a high enough level for that faction rank.',
  112. ),
  113. 'numeric' => array (
  114. //'required' => true,
  115. 'allowEmpty' => true,
  116. 'rule' => array('numeric'),
  117. 'message' => "Faction Rank ID must be numeric.",
  118. ),
  119. ),
  120. 'description' => array(
  121. 'required' => true,
  122. 'allowEmpty' => false,
  123. 'rule' => array('maxlength', 4096),
  124. 'message' => 'Description must be between one and 4096 characters.'
  125. ),
  126. 'age' => array(
  127. 'required' => true,
  128. 'allowEmpty' => false,
  129. 'rule' => array('maxlength', 256),
  130. 'message' => 'Age must be between one and 256 characters.'
  131. ),
  132. 'profession' => array(
  133. 'required' => true,
  134. 'allowEmpty' => false,
  135. 'rule' => array('maxlength', 256),
  136. 'message' => 'Profession must be between one and 256 characters.'
  137. ),
  138. 'history' => array(
  139. 'required' => true,
  140. 'allowEmpty' => false,
  141. 'rule' => array('maxlength', 4096),
  142. 'message' => 'History must be between one and 4096 characters.'
  143. ),
  144. 'avatar' => array(
  145. 'maxlength' => array(
  146. 'rule' => array('maxlength', 1024),
  147. 'message' => 'Avatar must be less than 1024 characters.'
  148. ),
  149. 'url' => array(
  150. 'rule' => 'url',
  151. 'allowEmpty' => true,
  152. 'message' => 'Avatar must be a valid URL.'
  153. )
  154. ),
  155. );
  156. /**
  157. * isOwner
  158. *
  159. * Confirm the user owns the specified character
  160. *
  161. * @param mixed $character_id
  162. * @param mixed $user_id
  163. * @access public
  164. * @return boolean True if ownership
  165. */
  166. public function isOwner($id, $user_id) {
  167. return 1 == $this->find('count', array(
  168. 'conditions' => compact('id', 'user_id')
  169. ));
  170. }
  171. /**
  172. * getUserIdById
  173. *
  174. * Obtain the user_id of a given character
  175. *
  176. * @param mixed $id
  177. * @access public
  178. * @return int Id of the user
  179. */
  180. public function getUserIdById($id) {
  181. return $this->field('user_id', compact('id'));
  182. }
  183. /**
  184. * checkLimit
  185. *
  186. * Ensure a user hasn't created more characters than they're allowed. Users
  187. * are allocated one character per rank until they reach level 7.
  188. *
  189. * @access protected
  190. * @return boolean True on successful validate.
  191. */
  192. protected function checkLimit() {
  193. if ($this->data['Character']['id']) { return true; }
  194. $user_id = $this->data['Character']['user_id'];
  195. $rank = $this->User->getRank($user_id);
  196. // Rank zero is a special case for new characters. Seven is max rank.
  197. if (0 === $rank) { $rank = 1; }
  198. if (7 === $rank) { return true; }
  199. return $rank > $this->find('count', array(
  200. 'conditions' => array('user_id' => $user_id)
  201. ));
  202. }
  203. /**
  204. * checkRank
  205. *
  206. * The character rank cannot exceed the user's rank.
  207. *
  208. * @param mixed $check array('key' => 'value')
  209. * @access protected
  210. * @return boolean Always true, because on failure a custom invalidate is
  211. * called with a dynamic message explaining the failure.
  212. */
  213. protected function checkRank($check) {
  214. $key = array_shift(array_keys($check));
  215. $value = array_shift(array_values($check));
  216. $userRank = $this->User->getRank($this->data['Character']['user_id']);
  217. // New users need to be able to register a character
  218. if (0 === $userRank) $userRank = 1;
  219. if ($userRank < $value) {
  220. $this->invalidate($key, sprintf(
  221. __('The character level cannot exceed your rank (%d)', true), $userRank
  222. ));
  223. }
  224. return true;
  225. }
  226. /**
  227. * limitByRank
  228. *
  229. * Check that the field doesn't exceed the character's rank.
  230. *
  231. * @param mixed $check array('key' => 'value')
  232. * @access protected
  233. * @return boolean Always true, because on failure a custom invalidate is
  234. * called with a dynamic message explaining the failure.
  235. */
  236. protected function limitByRank($check, $model) {
  237. $key = array_shift(array_keys($check));
  238. $value = array_shift(array_values($check));
  239. switch ($model) {
  240. case 'Location':
  241. $this->{$model}->CharacterLocation->contain('Rank');
  242. $data = $this->{$model}->CharacterLocation->findByLocationId($value);
  243. break;
  244. case 'Race':
  245. $this->{$model}->contain('Rank');
  246. $data = $this->{$model}->findById($value);
  247. break;
  248. default: return false;
  249. }
  250. if (empty($data) || !isset($data['Rank']['id'])) { return false; }
  251. $rank = $data['Rank']['id'];
  252. if ($rank > $this->data['Character']['rank_id']) {
  253. $this->invalidate($key, sprintf(__(
  254. 'Your character is not a high enough level (%d) for this option', true
  255. ), $rank));
  256. }
  257. return true;
  258. }
  259. /**
  260. * checkSubrace
  261. *
  262. * @param mixed $check
  263. * @access protected
  264. * @return boolean True on verification
  265. */
  266. protected function checkSubrace($check) {
  267. // I don't want to waste a db lookup to check the race_id is human.
  268. return (
  269. 1 == $this->data['Character']['race_id']
  270. && !empty($this->data['Character']['subrace_id'])
  271. ) || (
  272. 1 != $this->data['Character']['race_id']
  273. && empty($this->data['Character']['subrace_id'])
  274. );
  275. }
  276. /**
  277. * checkFaction
  278. *
  279. * Verify the numerous rules for factions. This whole block feels hack.
  280. *
  281. * @param mixed $check array('key' => 'value')
  282. * @access protected
  283. * @return boolean Always true, because on failure a custom invalidate is
  284. * called with a dynamic message explaining the failure.
  285. */
  286. protected function checkFaction($check) {
  287. $key = array_shift(array_keys($check));
  288. $value = array_shift(array_values($check));
  289. if (!$value) { return true; }
  290. $this->Faction->contain('Race');
  291. $data = $this->Faction->findById($value);
  292. $path = sprintf('/Race[id=%d]', $this->data['Character']['race_id']);
  293. $data = Set::extract($path, $data);
  294. if (empty($data)) {
  295. $this->invalidate($key, __(
  296. 'This faction does not accept members of the specified race.', true
  297. ));
  298. return true;
  299. }
  300. $data = $this->Faction->FactionRank->find('all', array(
  301. 'conditions' => array(
  302. 'faction_id =' => $this->data['Character']['faction_id'],
  303. 'rank_id <=' => $this->data['Character']['rank_id'],
  304. )
  305. ));
  306. if (empty($data)) {
  307. $this->invalidate($key, __(
  308. 'Your character is not a high enough rank to join this faction.', true
  309. ));
  310. return true;
  311. }
  312. if (is_numeric($this->data['Character']['age'])) {
  313. $path = sprintf('/FactionRank[age<=%d]', $this->data['Character']['age']);
  314. $data = Set::extract($path, $data);
  315. if (empty($data)) {
  316. $this->invalidate($key, __(
  317. 'Your character is not old enough to join this faction.', true
  318. ));
  319. return true;
  320. }
  321. }
  322. return true;
  323. }
  324. /**
  325. * checkFactionRank
  326. *
  327. * Check the faction rank agains the faction
  328. *
  329. * @param mixed $check
  330. * @access protected
  331. * @return boolean True when this check is fine
  332. */
  333. protected function checkFactionRank() {
  334. if (empty($this->data['Character']['faction_id'])) {
  335. return empty($this->data['Character']['faction_rank_id']);
  336. }
  337. return $this->FactionRank->isFactionRankInFaction(
  338. $this->data['Character']['faction_id'],
  339. $this->data['Character']['faction_rank_id']
  340. );
  341. }
  342. /**
  343. * checkFactionRankAge
  344. *
  345. * Check the faction rank against the age field. This is soft as the age
  346. * field doesn't have to be a number.
  347. *
  348. * @access protected
  349. * @return boolean True on valid faction rank
  350. */
  351. protected function checkFactionRankAge() {
  352. return $this->FactionRank->checkFactionRankAge(
  353. $this->data['Character']['faction_rank_id'],
  354. $this->data['Character']['age']
  355. );
  356. }
  357. /**
  358. * checkFactionRankLevel
  359. *
  360. * Compare the faction rank against the characters rank.
  361. *
  362. * @access protected
  363. * @return boolean True on valid faction rank
  364. */
  365. protected function checkFactionRankLevel() {
  366. return $this->FactionRank->checkFactionRankLevel(
  367. $this->data['Character']['faction_rank_id'],
  368. $this->data['Character']['rank_id']
  369. );
  370. }
  371. /**
  372. * __findAvailable
  373. *
  374. * Get a list of characters that are not currently in a story.
  375. *
  376. * @param mixed $user_id Character's owner
  377. * @access protected
  378. * @return array Results in the find('list') format
  379. */
  380. protected function __findAvailable($user_id) {
  381. $this->bindModel(array('hasOne' => array('CharactersStory')));
  382. $this->contain('CharactersStory');
  383. $characters = Set::combine(
  384. $this->findAllByUserId($user_id),
  385. '{n}.Character.id',
  386. '{n}.Character.name'
  387. );
  388. $unavailable = Set::extract(
  389. '/CharactersStory/character_id',
  390. $this->CharactersStory->find('all', array(
  391. 'conditions' => array(
  392. 'character_id' => array_keys($characters)
  393. )
  394. ))
  395. );
  396. foreach ($unavailable as $key) {
  397. unset($characters[$key]);
  398. }
  399. return $characters;
  400. }
  401. /**
  402. * __findListByStoryAndUser
  403. *
  404. * Get a character list by story and user.
  405. *
  406. * @param mixed $options Array keyed by story_id and user_id
  407. * @access protected
  408. * @return array Array of character names keyed my character id
  409. */
  410. protected function __findListByStoryAndUser($options) {
  411. $options =
  412. array_merge(array('story_id' => false, 'user_id' => false), $options);
  413. $character_ids = Set::extract(
  414. '/CharactersStory/character_id',
  415. $this->CharactersStory->findAllByStoryId($options['story_id'])
  416. );
  417. return Set::combine(
  418. $this->find('all', array(
  419. 'conditions' => array(
  420. 'id' => $character_ids,
  421. 'user_id' => $options['user_id'],
  422. )
  423. )),
  424. '{n}.Character.id',
  425. '{n}.Character.name'
  426. );
  427. }
  428. /**
  429. * __findAllByStoryId
  430. *
  431. * Find all characters by their story id. Be sure to include the
  432. * CharactersStory data that specifies whether the character has been removed
  433. * from the story.
  434. *
  435. * @param mixed $story_id
  436. * @access protected
  437. * @return array
  438. */
  439. protected function __findAllByStoryId($story_id) {
  440. $characters = $this->CharactersStory->find('all', array(
  441. 'conditions' => array('story_id' => $story_id),
  442. 'contain' => array('Character'),
  443. 'deactivated' => true,
  444. ));
  445. return $characters;
  446. }
  447. /**
  448. * bindCurrentStory
  449. *
  450. * Bind a has one relationship with the current story.
  451. *
  452. * @access public
  453. * @return void
  454. */
  455. public function bindCurrentStory() {
  456. $this->User->Character->bindModel(array(
  457. 'hasOne' => array(
  458. 'CharactersStory' => array(
  459. 'className' => 'CharactersStory',
  460. 'foreignKey' => false,
  461. 'conditions' => array(
  462. 'CharactersStory.is_deactivated' => false,
  463. 'CharactersStory.character_id = Character.id',
  464. ),
  465. ),
  466. 'CurrentStory' => array(
  467. 'className' => 'Story',
  468. 'foreignKey' => false,
  469. 'conditions' => array(
  470. 'CharactersStory.story_id = CurrentStory.id',
  471. )
  472. ),
  473. )
  474. ), true);
  475. }
  476. }
  477. ?>