/[drupal]/contributions/modules/fsgame/fsgame.module
ViewVC logotype

Contents of /contributions/modules/fsgame/fsgame.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.55 - (show annotations) (download) (as text)
Tue Sep 30 02:42:30 2008 UTC (13 months, 4 weeks ago) by aaron
Branch: MAIN
CVS Tags: HEAD
Changes since 1.54: +4 -2 lines
File MIME type: text/x-php
  * Add debug messages to contests (aaronwinborn).
  * Fix determination of defense multipliers (aaronwinborn).
1 <?php
2 // $Id: fsgame.module,v 1.54 2008/09/28 14:16:36 aaron Exp $
3
4 /**
5 * @file
6 * Enables the 5 Second Game (inspired by rock-paper-scissors).
7 *
8 * @todo implement some sort of cluetip thingy for the classes.
9 */
10
11 /**
12 * Implementation of hook_help().
13 */
14 function fsgame_help($path, $arg) {
15 switch ($path) {
16 case 'admin/settings/fsgame':
17 return '<p>'. t('Configure 5 Second Game with these settings.') .'</p>';
18 }
19 }
20
21 /**
22 * Implementation of hook_menu().
23 */
24 function fsgame_menu() {
25 $items = array();
26
27 $items['admin/settings/fsgame'] = array(
28 'access arguments' => array('administer 5 Second Game'),
29 'description' => 'Configure 5 Second Game with these settings.',
30 'file' => 'fsgame.admin.inc',
31 'page callback' => 'drupal_get_form',
32 'page arguments' => array('fsgame_settings'),
33 'title' => '5 Second Game',
34 );
35 $items['fsgame/contest'] = array(
36 'access arguments' => array('create 5 Second Game character'),
37 'description' => 'Competition between two 5 Second Game characters.',
38 'file' => 'fsgame.pages.inc',
39 'page callback' => 'drupal_get_form',
40 'page arguments' => array('fsgame_contest_form'),
41 'title' => '5 Second Game Contest',
42 );
43 $items['fsgame/autocomplete'] = array(
44 'access arguments' => array('access content'),
45 'file' => 'fsgame.pages.inc',
46 'page callback' => 'fsgame_autocomplete',
47 'title' => '5 Second Game autocomplete',
48 'type' => MENU_CALLBACK,
49 );
50 $items['fsgame/autocomplete-exclude'] = array(
51 'access arguments' => array('access content'),
52 'file' => 'fsgame.pages.inc',
53 'page callback' => 'fsgame_autocomplete_exclude',
54 'title' => '5 Second Game autocomplete (exclude)',
55 'type' => MENU_CALLBACK,
56 );
57 $items['fsgame/claim/%node'] = array(
58 'access' => 'fsgame_claim_access',
59 'access arguments' => array(2),
60 'file' => 'fsgame.pages.inc',
61 'page callback' => 'fsgame_character_claim',
62 'page arguments' => array(2),
63 'title' => 'Claim 5 Second Game character',
64 'type' => MENU_CALLBACK,
65 );
66 $items['fsgame/js/form'] = array(
67 'page callback' => 'fsgame_form_update',
68 'access arguments' => array('access content'),
69 'type' => MENU_CALLBACK,
70 );
71
72 return $items;
73 }
74
75 function fsgame_form_update() {
76 $class = $_POST['fsgame']['character']['class'];
77 if ($form = form_get_cache($_POST['form_build_id'], $form_state)) {
78 // Validate the bid.
79 if (isset($form['fsgame']['character']['classes']['class']['#options'][$class])) {
80 // Get the new options and update the cache.
81 $form['fsgame']['character']['skill-wrapper']['skills'] = _fsgame_skill_select($class);
82 form_set_cache($_POST['form_build_id'], $form, $form_state);
83
84 // Build and render the new select element, then return it in JSON format.
85 $form_state = array();
86 $form['#post'] = array();
87 $form = form_builder($form['form_id']['#value'] , $form, $form_state);
88 $output = drupal_render($form['fsgame']['character']['skill-wrapper']['skills']);
89 drupal_json(array('status' => TRUE, 'data' => $output));
90 }
91 else {
92 drupal_json(array('status' => FALSE, 'data' => ''));
93 }
94 }
95 else {
96 drupal_json(array('status' => FALSE, 'data' => ''));
97 }
98 exit();
99 }
100
101 function _fsgame_skill_select($class = '') {
102 $class = fsgame_world_load('classes', $class);
103 if (!$class) {
104 return;
105 }
106
107 if (!is_array($class['preferred modes'])) {
108 $class['preferred modes'] = isset($class['preferred modes']) ? (array) $class['preferred modes'] : array();
109 }
110 if (!is_array($class['preferred skills'])) {
111 $class['preferred skills'] = isset($class['preferred skills']) ? (array) $class['preferred skills'] : array();
112 }
113
114 // Also allow a skill selection.
115 $skill_options = array();
116 foreach (fsgame_world_load('skills') as $skill) {
117 if ($skill['contest'] == 'attack' && (in_array($skill['skill mode'], $class['preferred modes']) || in_array($skill['id'], $class['preferred skills']))) {
118 $skill_options[$skill['id']] = $skill['title'];
119 }
120 }
121
122 $form = array(
123 '#options' => $skill_options,
124 // when the form is submitted, make it a key of 'character'.
125 '#parents' => array('fsgame', 'character', 'skill'),
126 '#required' => TRUE,
127 '#title' => t('Beginning attack skill'),
128 '#type' => 'select',
129 );
130 return $form;
131 }
132
133
134 /**
135 * Implementation of hook_perm().
136 */
137 function fsgame_perm() {
138 return array(
139 'administer 5 Second Game',
140 'claim 5 Second Game character',
141 'create 5 Second Game character',
142 'edit own 5 Second Game character',
143 'edit any 5 Second Game character',
144 'delete any 5 Second Game character',
145 'delete own 5 Second Game character',
146 );
147 }
148
149 function fsgame_claim_access($node) {
150 global $user;
151 return $user->uid && user_access('claim 5 Second Game character') && ($node->type == 'fsgame_character') && (!$node->uid) && ($_SESSION['fsgame_contest_anonymous_character'] == $node->nid);
152 }
153
154 /**
155 * Implementation of hook_theme().
156 */
157 function fsgame_theme($existing, $type, $theme, $path) {
158 return array(
159 'fsgame_character_class' => array(
160 'arguments' => array('node' => NULL),
161 'file' => 'fsgame.theme.inc',
162 ),
163 'fsgame_character_attributes' => array(
164 'arguments' => array('node' => NULL),
165 'file' => 'fsgame.theme.inc',
166 ),
167 'fsgame_contest_selectors_character' => array(
168 'arguments' => array('character_nid' => 'latest', 'opponent_nid' => 'latest'),
169 'file' => 'fsgame.theme.inc',
170 ),
171 'fsgame_contest_selectors_opponent' => array(
172 'arguments' => array('character_nid' => 'latest', 'opponent_nid' => 'latest'),
173 'file' => 'fsgame.theme.inc',
174 ),
175 'test' => array(
176 'arguments' => array('file' => NULL),
177 'file' => 'fsgame.theme.inc',
178 ),
179 );
180 }
181
182 function phptemplate_preprocess_test($variables) {
183 $variables['file'] = 'override';
184 }
185
186 /**
187 * Implementation of hook_node_info().
188 */
189 function fsgame_node_info() {
190 $types = array();
191
192 $types['fsgame_character'] = array(
193 'name' => t('Character'),
194 'module' => 'fsgame_character',
195 'description' => t('A <em>character</em> defines who you play as in the 5 Second Game.'),
196 'title_label' => t('Name'),
197 'body_label' => t('Description'),
198 'locked' => TRUE,
199 );
200
201 return $types;
202 }
203
204 /**
205 * Implementation of hook_access for characters.
206 */
207 function fsgame_character_access($op, $node, $account) {
208 if ($op == 'create') {
209 return user_access('create 5 Second Game character', $account);
210 }
211
212 if ($op == 'update') {
213 if (user_access('edit any 5 Second Game character', $account) ||
214 (user_access('edit own 5 Second Game character', $account) && ($account->uid == $node->uid))) {
215 return TRUE;
216 }
217 }
218
219 if ($op == 'delete') {
220 if (user_access('delete any 5 Second Game character', $account) ||
221 (user_access('delete own 5 Second Game Character', $account) && ($account->uid == $node->uid))) {
222 return TRUE;
223 }
224 }
225 }
226
227 /**
228 * Implementation of hook_form() for characters.
229 */
230 function fsgame_character_form(&$node, &$param) {
231 drupal_add_css(drupal_get_path('module', 'fsgame') .'/fsgame.css');
232 $type = node_get_types('type', $node);
233
234 $form['title'] = array(
235 '#default_value' => isset($node->title) ? $node->title : '',
236 '#title' => check_plain($type->title_label),
237 '#type' => 'textfield',
238 '#required' => TRUE,
239 );
240
241 $form['fsgame'] = array('#tree' => TRUE);
242 $form['fsgame']['character']['attributes'] = array(
243 '#collapsible' => TRUE,
244 '#description' => t("Your character's attributes affect how well the character performs in contests against other characters."),
245 '#title' => t('Attributes'),
246 '#type' => 'fieldset',
247 );
248 foreach (fsgame_base_attributes() as $attribute) {
249 $form['fsgame']['character']['attributes'][$attribute['id']] = array(
250 '#default_value' => isset($node->fsgame['character'][$attribute['id']]) ? $node->fsgame['character'][$attribute['id']] : $attribute['default_value'],
251 '#description' => $attribute['description'],
252 // when the form is submitted, make them keys of 'character'.
253 '#parents' => array('fsgame', 'character', $attribute['id']),
254 '#title' => $attribute['title'],
255 '#type' => 'textfield',
256 );
257
258 // disable attribute alteration if non-admin.
259 if (!user_access('administer 5 Second Game')) {
260 // #input has to be true so that these items get sent via a POST.
261 // this saves us the effort of reloading the defaults in the 'insert'.
262 $form['fsgame']['character']['attributes'][$attribute['id']]['#input'] = TRUE;
263 $form['fsgame']['character']['attributes'][$attribute['id']]['#type'] = 'item';
264 $form['fsgame']['character']['attributes'][$attribute['id']]['#value'] = $form['fsgame']['character']['attributes'][$attribute['id']]['#default_value'];
265 }
266 }
267
268 // CSS wizardry to allow "centering" and larger sizes.
269 $form['fsgame']['character']['attributes']['rock']['#prefix'] = '<div class="fsgame-character-attributes">';
270 $form['fsgame']['character']['attributes']['scissors']['#suffix'] = '</div>';
271
272 $form['fsgame']['character']['classes'] = array(
273 '#collapsible' => TRUE,
274 '#title' => t('Character class'),
275 '#type' => 'fieldset',
276 );
277
278 // $form['fsgame']['character']['skills'] = array(
279 // '#collapsible' => TRUE,
280 // '#title' => t('Character skills'),
281 // '#type' => 'fieldset',
282 // );
283
284 // Wrapper for fieldset contents (used by ahah.js).
285 $form['fsgame']['character']['skill-wrapper'] = array(
286 '#type' => 'item',
287 '#prefix' => '<div id="fsgame-character-skill-wrapper">',
288 '#suffix' => '</div>',
289 );
290
291 // $form['fsgame']['character']['skills'] = _fsgame_skill_select();
292
293 // Need this for AJAX.
294 $form['#cache'] = TRUE;
295 drupal_add_js("if (Drupal.jsEnabled) { $(document).ready(function() { $('#edit-fsgame-character-pick-class').css('display', 'none'); }); }", 'inline');
296
297 $form['fsgame']['character']['pick-class'] = array(
298 '#type' => 'submit',
299 '#value' => t('Change class (update list of skills)'),
300 // Submit the node form so the parent select options get updated.
301 // This is typically only used when JS is disabled. Since the parent options
302 // won't be changed via AJAX, a button is provided in the node form to submit
303 // the form and generate options in the parent select corresponding to the
304 // selected book. This is similar to what happens during a node preview.
305 '#submit' => array('node_form_submit_build_node'),
306 '#weight' => 20,
307 );
308
309
310 // new character, so pick a class, bub.
311 if (!$node->nid) {
312 $class_checkboxes = array();
313 foreach (fsgame_world_load('classes') as $class) {
314 $class_checkboxes[$class['id']] = fsgame_attributes_modifiers_string($class);
315 }
316
317 if (count($class_checkboxes) == 0) {
318 drupal_set_message(t("No character classes found; a site administrator needs to enable a 5 Second Game world."), 'error');
319 return; // if the user hasn't enabled any game worlds, we need to exit immediately. classes are compulsory.
320 }
321
322 $form['fsgame']['character']['classes']['class'] = array(
323 '#options' => $class_checkboxes,
324 // when the form is submitted, make it a key of 'character'.
325 '#parents' => array('fsgame', 'character', 'class'),
326 '#required' => TRUE,
327 '#title' => t('Character class'),
328 '#type' => 'radios',
329 '#ahah' => array(
330 'path' => 'fsgame/js/form',
331 'wrapper' => 'fsgame-character-skill-wrapper',
332 'effect' => 'slide',
333 ),
334 );
335 }
336 else { // existing character so stare at your stats, bub.
337 $class = fsgame_world_load('classes', $node->fsgame['character']['class']);
338
339 if (!$class) {
340 drupal_set_message("This character's class was not found; please contact a site administrator.", 'error');
341 return; // if this character no longer is plausible, complain loudly to the site admin cos they broke something.
342 }
343
344 $form['fsgame']['character']['classes']['class'] = array(
345 '#type' => 'item',
346 '#value' => t('Level @level !class_and_modifiers.',
347 array(
348 '@level' => $node->fsgame['character']['level'],
349 '!class_and_modifiers' => fsgame_attributes_modifiers_string($class),
350 )
351 ),
352 );
353
354 foreach ($node->fsgame['character']['skills'] as $type => $skill) {
355 }
356 // $form['fsgame']['character']['skills']['skill'] = array(
357 // '#type' => 'item',
358 // '#value' => t('Level @level !class_and_modifiers.',
359 // array(
360 // '@level' => $node->fsgame['character']['level'],
361 // '!class_and_modifiers' => fsgame_attributes_modifiers_string($class),
362 // )
363 // ),
364 // );
365 }
366
367 $form['body'] = array(
368 '#default_value' => isset($node->body) ? $node->body : '',
369 '#title' => check_plain($type->body_label),
370 '#type' => 'textarea',
371 '#required' => FALSE,
372 '#rows' => 10,
373 );
374
375 $form['#validate'][] = 'fsgame_character_node_form_validate';
376
377 return $form;
378 }
379
380 /**
381 * FormAPI #validate for fsgame_character_form().
382 */
383 function fsgame_character_node_form_validate($form, &$form_state) {
384 foreach (fsgame_base_attributes() as $attribute) {
385 if (!is_numeric($form_state['values']['fsgame']['character'][$attribute['id']])) {
386 form_set_error('fsgame][character]['. $attribute['id'], t('The %attribute attribute must be a number.', array('%attribute' => fsgame_base_attributes($attribute['id'], 'title'))));
387 }
388 }
389 }
390
391 /**
392 * Implementation of hook_load() for characters.
393 */
394 function fsgame_character_load($node) {
395 $character = db_fetch_array(db_query("SELECT * FROM {fsgame_character} WHERE nid = %d", $node->nid));
396 return array('fsgame' => array('character' => $character));
397 }
398
399 /**
400 * Implementation of hook_view() for characters.
401 */
402 function fsgame_character_view($node, $teaser = FALSE, $page = FALSE) {
403 drupal_add_css(drupal_get_path('module', 'fsgame') .'/fsgame.css');
404
405 $node = node_prepare($node, $teaser);
406 $node->content['fsgame_class'] = array(
407 '#value' => theme('fsgame_character_class', $node),
408 );
409
410 $node->content['fsgame_attributes'] = array(
411 '#value' => theme('fsgame_character_attributes', $node),
412 );
413
414 return $node;
415 }
416
417 /**
418 * Implementation of hook_insert() for characters.
419 */
420 function fsgame_character_insert($node) {
421 global $user;
422
423 $node->fsgame['character']['nid'] = $node->nid;
424
425 // modify the attributes permanently based on the user's class.
426 $class = fsgame_world_load('classes', $node->fsgame['character']['class']);
427 if (isset($class['permanent modifiers'])) { // presuming, of course, they exist.
428 foreach ($class['permanent modifiers'] as $attribute_id => $modifier) {
429 $node->fsgame['character'][$attribute_id] += $modifier;
430 }
431 }
432
433 drupal_write_record('fsgame_character', $node->fsgame['character']);
434
435 $node->fsgame['character']['skills'][$node->fsgame['character']['skill']] = 1;
436 $skill = array();
437 $skill['skill'] = $node->fsgame['character']['skill'];
438 $skill['level'] = 1;
439 $skill['nid'] = $node->nid;
440 unset($node->fsgame['character']['skill']);
441
442 drupal_write_record('fsgame_character_skills', $skill);
443
444 // If this is an anonymous user, then set the session to this character.
445 if (!$user->uid) {
446 $_SESSION['fsgame_contest_anonymous_character'] = $node->nid;
447 }
448 else {
449 // Otherwise, this now counts as our latest character.
450 fsgame_user_set_latest_character($user->uid, $node->nid);
451 }
452 }
453
454 /**
455 * Implementation of hook_update() for characters.
456 */
457 function fsgame_character_update($node) {
458 $node->fsgame['character']['nid'] = $node->nid;
459 drupal_write_record('fsgame_character', $node->fsgame['character'], 'nid');
460 }
461
462 /**
463 * Implementation of hook_delete() for characters.
464 */
465 function fsgame_character_delete($node) {
466 db_query('DELETE FROM {fsgame_character} WHERE nid = %d', $node->nid);
467 }
468
469 /**
470 * Return the base definitions of our attributes.
471 *
472 * @param $attribute
473 * Defaults to NULL; one of 'rock', 'paper', or 'scissors'.
474 * @param $key
475 * Defaults to NULL; one of 'title', 'description', 'default_value', 'maximum' or 'minimum'.
476 * @return $array_or_string
477 * With no params, an array of 'rock', 'paper', and 'scissors', each containing:
478 * 'title' => The title assigned by the administrator to the attribute.
479 * 'description' => The description assigned by the administrator (default: none).
480 * 'default_value' => The value the attribute will default to for a new character.
481 * 'minimum' => The minimum value allowed. Normally the same as default_value.
482 * 'maximum' => The maximum value allowed. 0 means there is no maximum.
483 * 'trumps' => The internal attribute ID that this attribute beats in a tie.
484 * 'id' => The internal ID of the attribute.
485 *
486 * With an $attribute passed in, returns just the attribute specific keys above.
487 * With an $attribute and $key passed, the specific value of that key.
488 */
489 function fsgame_base_attributes($attribute = NULL, $key = NULL) {
490 $internal_ids = array('rock', 'paper', 'scissors');
491 $trumps = array('scissors', 'rock', 'paper');
492 static $attributes = array();
493
494 if (!$attributes) {
495 foreach ($internal_ids as $internal_key => $internal_id) {
496 $attributes[$internal_id] = array(
497 'title' => variable_get('fsgame_attributes_'. $internal_id .'_title', t(drupal_ucfirst($internal_id))),
498 'description' => variable_get('fsgame_attributes_'. $internal_id .'_description', ''),
499 'default_value' => variable_get('fsgame_attributes_'. $internal_id .'_default', 1),
500 'minimum' => variable_get('fsgame_attributes_'. $internal_id .'_minimum', 1),
501 'maximum' => variable_get('fsgame_attributes_'. $internal_id .'_maximum', 0),
502 'trumps' => variable_get('fsgame_attributes_'. $attribute['id'] .'_trumps', $trumps[$internal_key]),
503 'id' => $internal_id, // make it explicit for easier array looping.
504 );
505 }
506 }
507
508 return $attribute ? ($key ? $attributes[$attribute][$key] : $attributes[$attribute]) : $attributes;
509 }
510
511 /**
512 * Return a list of attributes, suitable for use as FAPI #options.
513 */
514 function fsgame_base_attributes_fapi() {
515 $options = array();
516 foreach (fsgame_base_attributes() as $attribute) {
517 $options[$attribute['id']] = t(drupal_ucfirst($attribute['title']));
518 } // this is not magick. we're just slightly lazy.
519 return $options;
520 }
521
522 /**
523 * Turn world data modifier into a human-readable string.
524 *
525 * The goal is ideally a string like: (Rock) (+2 Rock; -1 Paper).
526 * where the first "(Rock)" is the primary attribute of this item
527 * (or, in the case of classes, "(Rock/Paper/Paper)"), and then
528 * any modifiers (either permanent or temporary) semi-colon spliced.
529 *
530 * @param $world_data
531 * The array containing information about the world data item.
532 * @param $type
533 * What kind of human-readable data to return: defaults to "all".
534 * Can also be "permanent", "active", or "attributes". If you choose
535 * "all", the name of the world_data is also returned in the string.
536 * @return $string
537 * A string suitable for printing.
538 */
539 function fsgame_attributes_modifiers_string($world_data = array(), $type = 'all') {
540 $attributes = array();
541 if (is_array($world_data['attributes'])) {
542 foreach ($world_data['attributes'] as $attribute_id) {
543 $attributes[] = fsgame_base_attributes($attribute_id, 'title');
544 } // build this item's attribute list (single or tri).
545 }
546
547 // permanent modifiers can never be removed once they're appended to the character.
548 // active modifiers only apply when the world data is in use (an equipped item, etc.).
549 foreach (array('permanent', 'active') as $modifier_type) {
550 if (!isset($world_data[$modifier_type .' modifiers'])) { continue; }
551 foreach ($world_data[$modifier_type .' modifiers'] as $attribute_id => $attribute_modifier) {
552 if ($attribute_modifier > 0) { $attribute_modifier = '+'. $attribute_modifier; } // math symbolism goOOOd.
553 $modifiers[$modifier_type][] = t('@attribute @modifier', array('@attribute' => fsgame_base_attributes($attribute_id, 'title'), '@modifier' => $attribute_modifier));
554 } // if we've got some modifiers, we'll bundle them all together into a happy string, overwriting the interim array we've created. they then get inserted into the final string we return below.
555 $modifiers[$modifier_type] = count($modifiers[$modifier_type]) > 0 ? t('<strong>@modifier_type</strong>: @modifiers', array('@modifier_type' => drupal_ucfirst($modifier_type), '@modifiers' => implode(', ', $modifiers[$modifier_type]))) : '';
556 }
557
558 switch ($type) {
559 case 'all':
560 return t('@title (@attributes!permanent_modifiers!active_modifiers)',
561 array(
562 '@title' => $world_data['title'],
563 '@attributes' => implode('/', $attributes),
564 '!permanent_modifiers' => $modifiers['permanent'] ? '; ' . $modifiers['permanent'] : '',
565 '!active_modifiers' => $modifiers['active'] ? '; ' . $modifiers['active'] : '',
566 )
567 );
568 case 'attributes':
569 return t('@attributes', array('@attributes' => implode('/', $attributes)));
570 case 'permanent':
571 return t('!permanent_modifiers', array('!permanent_modifiers' => $modifiers['permanent']));
572 case 'active':
573 return t('!active_modifiers', array('!active_modifiers' => $modifiers['active']));
574 }
575 }
576
577 /**
578 * This is the basic fight between two characters.
579 *
580 * When running the contest, all stats are determined
581 *
582 * @param &$contest
583 * This array will contain at a minimum the following elements. Modules implementing hook_fsgame_contest_alter may add to and alter
584 * this array at various stages in the contest.
585 *
586 * At a minimum, the array must contain the following elements:
587 * 'character' => A valid fsgame_character object owned by the current user.
588 * 'opponent' => A valid fsgame_character object not owned by the current user.
589 * 'character attack',
590 * 'character defense',
591 * 'opponent attack',
592 * 'opponent defense' => Arrays containing the selected attacks and defenses of the characters.
593 * These will each contain at least the following:
594 * 'skill' => the machine-name of the skill being used
595 * 'attribute' => the stat the skill uses
596 * 'title' => the human readable title of the skill being used
597 * 'modifiers' => an array with modifiers to the specific attack or defense.
598 * these will be created by modules invoking hook_fsgame_contest_alter, and each key of the 'mods' sub-array must
599 * at least contain the following:
600 * 'unique_modifier_id' => array('title' => A string describing the modifier, 'level' => The amount by which to modify the skill)
601 *
602 * Throughout the contest, as hook_fsgame_contest is invoked, the current stage of the contest will be sent as an additional parameter.
603 *
604 * After resolving the contest, a 'results' key in $contest will contain an array with information concerning the results.
605 * If that key is empty, then the contest was not resolved. Otherwise, it will contain at least the following elements:
606 * 'character' => the final result of the character's attack
607 * 'opponent' => the final result of the opponent's attack
608 * 'win' => either the character object, the opponent object, or the string 'draw'
609 */
610 function fsgame_contest(&$contest) {
611 $contest['results'] = array();
612
613 if (!fsgame_contest_allow($contest)) {
614 // Set the win to disallowed. Additionally, a disallowing module may set an explanation string in $contest['results']['disallow'].
615 $contest['results']['win'] = 'disallowed';
616 return FALSE;
617 }
618
619 _fsgame_contest_select_opponent_modes($contest);
620
621 // modules may add modifiers here
622 drupal_alter('fsgame_contest', $contest, 'before');
623
624 // tally the modifiers
625 foreach (array('character', 'opponent') as $which) {
626 foreach (array('attack', 'defense') as $element) {
627 _fsgame_contest_tally_mid_results($contest, $which, $element);
628 }
629 }
630
631 // modules may affect the initial results here
632 drupal_alter('fsgame_contest', $contest, 'during');
633
634 // tally final results, deducting valid defense from attacks and comparing final results
635 foreach (array('character' => 'opponent', 'opponent' => 'character') as $defense => $attack) {
636 if ($contest["$defense defense"]['attribute'] == $contest["$attack attack"]['attribute']) {
637 $contest["$attack attack"]['result'] -= $contest["$defense defense"]['result'];
638 drupal_set_message("defense against {$contest["$defense defense"]['attribute']}: $attack attack modified to {$contest["$attack attack"]['result']}.");
639 }
640 }
641
642 _fsgame_contest_determine_winner($contest);
643
644 // modules may act on the final results here
645 drupal_alter('fsgame_contest', $contest, 'after');
646
647 fsgame_character_set_latest_opponent($contest['character']->nid, $contest['opponent']->nid);
648 }
649
650 /**
651 * Implement hook_fsgame_contest.
652 */
653 function fsgame_fsgame_contest(&$contest, $stage) {
654 switch ($stage) {
655 case 'allow':
656 global $user;
657 // Unless the user is an admin or the game otherwise allows, fail if the characters belong to the same user.
658 if (!user_access('administer 5 Second Game') && !variable_get('fsgame_contest_allow_same_user', FALSE) && ($contest['character']->uid == $contest['opponent']->uid)) {
659 $contest['results']['disallow'] = variable_get('fsgame_contest_disallow_same_user_message', t('You may not fight another character that you own.'));
660 return FALSE;
661 }
662 return TRUE;
663 case 'during':
664 // TODO: add admin-determined dice rolls to results
665 }
666 }
667
668 /**
669 * Determine the opponent's contest modes.
670 */
671 function _fsgame_contest_select_opponent_modes(&$contest) {
672 $attributes = fsgame_base_attributes();
673 foreach ($attributes as $key => $attribute) {
674 $attacks[$key] = $attribute['title'];
675 $defenses[$key] = $attribute['title'];
676 }
677 // Allow modules implementing hook_fsgame_available_modes_alter to add/remove their own skills to/from the selectors.
678 drupal_alter('fsgame_available_modes', $attacks, 'opponent', 'attack', $contest['character'], $contest['opponent']);
679 drupal_alter('fsgame_available_modes', $defenses, 'opponent', 'defense', $contest['character'], $contest['opponent']);
680
681 // select a random attack/defense from the remaining options
682 $contest['opponent attack'] = fsgame_contest_select_mode('attack', array_rand($attacks), $contest['opponent'], $contest['character']);
683 $contest['opponent defense'] = fsgame_contest_select_mode('defense', array_rand($defenses), $contest['opponent'], $contest['character']);
684 }
685
686 /**
687 * This will populate the appropriate array with the skill & modifiers for a contest.
688 * @param $mode
689 * 'attack' or 'defense'
690 * @param $skill
691 * A string with the key to a skill.
692 * @param $character
693 * The character node whose skill this is. Note that this may refer to a contest's character or opponent.
694 * @param $opponent
695 * The character node opposing the character.
696 */
697 function fsgame_contest_select_mode($mode, $skill, $character, $opponent) {
698 return array(
699 'skill' => $skill,
700 'attribute' => $skill,
701 'title' => 'The skill title',
702 'modifiers' => array(
703 'base' => array(
704 'title' => 'A random modifier',
705 'level' => fsgame_dice('1d6'),
706 ),
707 ),
708 );
709 }
710
711 /**
712 * Allow modules to disallow a contest between characters.
713 * If a module implements hook_fsgame_allow_contest, then that hook will be invoked.
714 * If any hook returns FALSE, then the contest will be disallowed, halting further processing.
715 * @param $contest
716 * A contest array, as outlined in fsgame_contest.
717 */
718 function fsgame_contest_allow(&$contest) {
719 // don't allow nodes that are not fsgame_character type to continue, or if they are the same character.
720 if ($contest['character']->type != 'fsgame_character' || $contest['opponent']->type != 'fsgame_character') {
721 $contest['results']['disallow'] = variable_get('fsgame_contest_disallow_illegal_type_message', t('Illegal character types.'));
722 return FALSE;
723 }
724 if ($contest['character']->nid == $contest['opponent']->nid) {
725 $contest['results']['disallow'] = variable_get('fsgame_contest_disallow_self_contest_message', t('You may not fight yourself.'));
726 return FALSE;
727 }
728 foreach (module_implements('fsgame_contest') as $module) {
729 if (!module_invoke($module, 'fsgame_contest', $contest, 'allow')) {
730 // If a module disallows a contest, it may send an explanation to $contest['results']['disallow'].
731 return FALSE;
732 }
733 }
734 return TRUE;
735 }
736
737 function _fsgame_contest_determine_winner(&$contest) {
738 foreach (array('character' => 'opponent', 'opponent' => 'character') as $attacker => $defender) {
739 $contest['results'][$attacker] = $contest["$attacker attack"]['result'];
740 if (fsgame_base_attributes($contest["$attacker attack"]['attribute'], 'trumps') == $contest["$defender attack"]['attribute']) {
741 $contest['results'][$attacker] = intval($contest['results'][$attacker] * variable_get('fsgame_trump_multiplier', 1.5));
742 drupal_set_message("$attacker {$contest["$attacker attack"]['attribute']} trumps {$contest["$defender attack"]['attribute']}: X 1.5 = {$contest['results'][$attacker]}.");
743 }
744 }
745 if ($contest['results']['character'] > $contest['results']['opponent']) {
746 $contest['results']['win'] = $contest['character'];
747 }
748 else if ($contest['results']['opponent'] > $contest['results']['character']) {
749 $contest['results']['win'] = $contest['opponent'];
750 }
751 else if (variable_get('fsgame_allow_draws', TRUE)) {
752 $contest['results']['win'] = 'draw';
753 }
754 else {
755 $contest['results']['win'] = $contest['opponent'];
756 }
757 }
758
759 function _fsgame_contest_tally_mid_results(&$contest, $which, $element) {
760 // $value = isset($contest[$which .' '. $element]['attribute']) ? $contest[$which]->{$contest[$which .' '. $element]['attribute']} : 0;
761 $contest[$which .' '. $element]['result'] = intval($value) + _fsgame_contest_apply_modifiers($contest[$which .' '. $element]);
762 drupal_set_message("$which $element: {$contest[$which .' '. $element]['result']} ({$contest[$which .' '. $element]['attribute']})");
763 }
764
765 function _fsgame_contest_apply_modifiers($mode) {
766 $level = 0;
767 if (is_array($mode['modifiers'])) {
768 foreach ($mode['modifiers'] as $modifier) {
769 $level += intval($modifier['level']);
770 }
771 }
772 return $level;
773 }
774
775 /**
776 * Load and cache our various world data.
777 *
778 * This is a generic world loading function so that new modules
779 * can benefit from this API, and the cache that it creates.
780 *
781 * @param $type
782 * The type of data to load (classes, items, arenas, etc.).
783 * @param $id
784 * If passed, the ID of $type that should be returned.
785 * @param $refresh
786 * Forces a refresh of the cached $type data.
787 * @return $items
788 * If $type not passed, all world data currently loaded.
789 * If $type is passed, an array of all known world data of that type.
790 * If $type and $id passed, the specific data of $type to return.
791 */
792 function fsgame_world_load($type = NULL, $id = NULL, $refresh = FALSE) {
793 static $world = array();
794
795 if (!$world[$type] || $refresh) {
796 if (!$refresh && $cache = cache_get('fsgame_world_'. $type)) {
797 $world[$type] = unserialize($cache->data);
798 }
799 else {
800 $world[$type] = module_invoke_all('fsgame_world_'. $type);
801 foreach ($world[$type] as $type_id => $type_info) {
802 // store the ID inside for easier reference.
803 $world[$type][$type_id]['id'] = $type_id;
804 } // cache this world type data for one hour.
805 arsort($world[$type]); // alphabetical order please.
806 cache_set('fsgame_world_'. $type, serialize($world[$type]), 'cache', time() + 3600);
807 }
808 }
809
810 return $type ? ($id ? $world[$type][$id] : $world[$type]) : $world;
811 }
812
813 /**
814 * Roll dice and return the sum. Simple enough, eh?
815 *
816 * The only parameter is a string in traditional RPG dice notation. Examples
817 * include 4d6, 3d8, 3^5d6 (weighted, taking the highest 3 of 5 dice rolled),
818 * 1d100+50, 6d6-6, 1d50+50*4-1, and so on and so forth.
819 */
820 function fsgame_dice($notation) {
821
822 // remove whitespace and turn "x" to "*".
823 $notation = str_replace('x', '*', $notation);
824 $notation = preg_replace('/\s/', '', $notation);
825
826 // if we're keeping high rolls, find out how many and store in $keep_highest.
827 if (strstr($notation, '^')) { list($keep_highest, $notation) = explode('^', $notation); }
828
829 // split our notation on the "d". nothing magical.
830 list($num_rolls, $remainder) = explode('d', $notation);
831
832 // if there are operations to be done after the dice roll,
833 // split at the first one so we can get the number of sides,
834 // and save the rest into $modifiers for eval()ing later on.
835 if (preg_match('/^\d*([\+\-\*\/])/', $remainder)) {
836 $matches = preg_split('/([\+\-\*\/])/', $remainder, -1, PREG_SPLIT_DELIM_CAPTURE);
837 $num_sides = array_shift($matches); $modifiers = implode('', $matches);
838 } else { $num_sides = $remainder; $modifiers = NULL; }
839
840 // roll however many times necessary and $keep_highest as needed.
841 for ($ctr = 0; $ctr < $num_rolls; $ctr++) { $virgins[] = rand(1, $num_sides); }
842 if (isset($keep_highest)) { rsort($virgins); $virgins = array_splice($virgins, 0, $keep_highest); }
843 $rolling_result = array_sum($virgins);
844
845 // eval is simply easiest here so if we have to run one,
846 // we'll remove anything not a number or math-related.
847 if (isset($modifiers)) {
848 // TODO: i think the "d" is misleading -- makes it seem like it processes multiple dice events,
849 // when it's actually ignored and fails everything after. -winborn
850 $modifiers = preg_replace('/[^(\d|\+|\-|\*|\/|\(|\))]/', '', $modifiers);
851 eval("\$final_result = $rolling_result $modifiers;"); // ssshShHhh.
852 } else { $final_result = $rolling_result; }
853
854 return $final_result;
855 }
856
857 /**
858 * Implement hook_user.
859 * @TODO: This doesn't seem to work properly.
860 */
861 function fsgame_user($op, &$edit, &$account, $category = NULL) {
862 switch ($op) {
863 case 'login':
864 if ($_SESSION['fsgame_contest_anonymous_character']) {
865 $node = node_load($_SESSION['fsgame_contest_anonymous_character']);
866 if (fsgame_claim_access($node)) {
867 drupal_set_message(t("The character %title was created anonymously from this computer. You may not use the character again unless you !claim. If you do not claim that character before logging off, then it may not be used again.", array('%title' => $node->title, '!claim' => l(t('claim the character'), 'fsgame/claim/'. $node->nid))));
868 }
869 }
870 break;
871 case 'logout':
872 unset($_SESSION['fsgame_contest_anonymous_character']);
873 break;
874 case 'load':
875 $account->fsgame_latest_character = fsgame_user_get_latest_character($account->uid);
876 break;
877 case 'save':
878 fsgame_user_set_latest_character($account->uid, $account->fsgame_latest_character);
879 break;
880 case 'delete':
881 db_query('DELETE FROM {fsgame_user} WHERE uid = %d', $account->uid);
882 break;
883 }
884 }
885
886 /**
887 * Set the most recently accessed character of a user's account.
888 * @param $uid
889 * The account uid to set the latest character.
890 * If it equals $global $user->uid, then we also set $user->fsgame_latest_character to that character.
891 * @param $nid
892 * The nid of the character node.
893 * @return
894 * FALSE if unsuccessful. Otherwise SAVED_NEW or SAVED_UPDATED.
895 */
896 function fsgame_user_set_latest_character($uid, $nid) {
897 if ($uid && is_numeric($uid) && $nid && is_numeric($nid)) {
898 global $user;
899 if ($uid == $user->uid) {
900 $user->fsgame_latest_character = $nid;
901 }
902 if (db_result(db_query("SELECT uid FROM {fsgame_user} WHERE uid=%d", $uid))) {
903 return drupal_write_record('fsgame_user', $user, 'fsgame_latest_character');
904 // db_query("UPDATE {fsgame_user} SET latest_character = %d WHERE uid = %d", $nid, $uid);
905 }
906 else {
907 return drupal_write_record('fsgame_user', $user);
908 // db_query("INSERT INTO {fsgame_user} (uid, latest_character) VALUES (%d, %d)", $uid, $nid);
909 }
910 }
911 return FALSE;
912 }
913
914 /**
915 * Return the most recently accessed character of an account.
916 * @param $uid
917 * The account to check for the latest character.
918 * @return
919 * The nid of the account's latest character node.
920 */
921 function fsgame_user_get_latest_character($uid) {
922 return db_result(db_query("SELECT fsgame_latest_character FROM {fsgame_user} WHERE uid=%d", $uid));
923 }
924
925 /**
926 * Return the most recently fought opponent of a character.
927 * @param $nid
928 * The character to check for the latest opponent.
929 * @return
930 * The nid of the character's latest opponent node.
931 */
932 function fsgame_character_get_latest_opponent($nid) {
933 return db_result(db_query("SELECT latest_opponent FROM {fsgame_character} WHERE nid=%d", $nid));
934 }
935
936 /**
937 * Set the most recently fought opponent of a character.
938 * @param $character_nid
939 * The nid of the character node.
940 * @param $opponent_nid
941 * The nid of the opponent node.
942 */
943 function fsgame_character_set_latest_opponent($character_nid, $opponent_nid) {
944 if ($character_nid && is_numeric($character_nid) && $opponent_nid && is_numeric($opponent_nid)) {
945 db_query("UPDATE {fsgame_character} SET latest_opponent = %d WHERE nid = %d", $opponent_nid, $character_nid);
946 }
947 }

  ViewVC Help
Powered by ViewVC 1.1.2