ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/deliantra/server/ext/Jeweler.pm
Revision: 1.27
Committed: Sun Jul 20 17:01:29 2008 UTC (15 years, 10 months ago) by elmex
Branch: MAIN
CVS Tags: rel-2_7, rel-2_72, rel-2_71, rel-2_61
Changes since 1.26: +12 -19 lines
Log Message:
fixed bug in jeweler skill

File Contents

# Content
1 =head1 NAME
2
3 Jeweler
4
5 =head1 DESCRIPTION
6
7 The Jeweler skill helper module.
8
9 =cut
10
11 package Jeweler;
12
13 use strict;
14 use YAML;
15
16 =over 4
17
18 =item @RESISTS
19
20 List of all resistancies that can occur on rings and amulets.
21
22 =cut
23
24 our $CFG;
25
26 sub read_config {
27 my ($filename) = @_;
28
29 if (my $meta = $cf::RESOURCE{$filename}) {
30 $CFG = cf::decode_json $meta->{data};
31 } else {
32 warn "$filename doesn't exist! no config for jeweler skill loaded!\n";
33 $CFG = {};
34 }
35 }
36
37 sub getcfg {
38 my ($sect, $key) = @_;
39 return $CFG->{$sect} unless defined $key;
40
41 my $cfg = $CFG->{$sect}->{$key}
42 or die "Couldn't find $sect/$key in configuration!";
43
44 $cfg
45 }
46
47 our @RESISTS = (
48 cf::ATNR_PHYSICAL,
49 cf::ATNR_MAGIC,
50 cf::ATNR_FIRE,
51 cf::ATNR_ELECTRICITY,
52 cf::ATNR_COLD,
53 cf::ATNR_CONFUSION,
54
55 cf::ATNR_ACID,
56 cf::ATNR_DRAIN,
57 cf::ATNR_GHOSTHIT,
58 cf::ATNR_POISON,
59 cf::ATNR_SLOW,
60 cf::ATNR_PARALYZE,
61
62 cf::ATNR_TURN_UNDEAD,
63 cf::ATNR_FEAR,
64 cf::ATNR_DEPLETE,
65 cf::ATNR_DEATH,
66 cf::ATNR_HOLYWORD,
67 cf::ATNR_LIFE_STEALING,
68
69 cf::ATNR_BLIND,
70 cf::ATNR_DISEASE,
71 );
72
73 =item @EFFECT_RESISTS
74
75 List of all effect resistancies that occur on rings and amulets.
76 The difference is made because effect resistancies are less effective at lower levels.
77
78 =back
79
80 =cut
81
82 our @EFFECT_RESISTS = (
83 cf::ATNR_CONFUSION,
84 cf::ATNR_DRAIN,
85 cf::ATNR_POISON,
86 cf::ATNR_SLOW,
87 cf::ATNR_PARALYZE,
88 cf::ATNR_TURN_UNDEAD,
89 cf::ATNR_FEAR,
90 cf::ATNR_DEPLETE,
91 cf::ATNR_DEATH,
92 cf::ATNR_BLIND,
93 cf::ATNR_DISEASE,
94 );
95
96 our %RESMAP = (
97 cf::ATNR_PHYSICAL => "PHYSICAL",
98 cf::ATNR_MAGIC => "MAGIC",
99 cf::ATNR_FIRE => "FIRE",
100 cf::ATNR_ELECTRICITY => "ELECTRICITY",
101 cf::ATNR_COLD => "COLD",
102 cf::ATNR_CONFUSION => "CONFUSION",
103 cf::ATNR_ACID => "ACID",
104
105 cf::ATNR_DRAIN => "DRAIN",
106 cf::ATNR_GHOSTHIT => "GHOSTHIT",
107 cf::ATNR_POISON => "POISON",
108 cf::ATNR_SLOW => "SLOW",
109 cf::ATNR_PARALYZE => "PARALYZE",
110 cf::ATNR_TURN_UNDEAD => "TURN_UNDEAD",
111
112 cf::ATNR_FEAR => "FEAR",
113 cf::ATNR_DEPLETE => "DEPLETE",
114 cf::ATNR_DEATH => "DEATH",
115 cf::ATNR_HOLYWORD => "HOLYWORD",
116 cf::ATNR_LIFE_STEALING => "LIFE_STEALING",
117 cf::ATNR_BLIND => "BLIND",
118 cf::ATNR_DISEASE => "DISEASE",
119 );
120
121 our %REV_RESMAP = map { $RESMAP{$_} => $_ } keys %RESMAP;
122
123 our %LVL_DIFF_CHANCES = (
124 +5 => 100,
125 +4 => 95,
126 +3 => 85,
127 +2 => 75,
128 +1 => 65,
129 0 => 50,
130 -1 => 45,
131 -2 => 35,
132 -3 => 25,
133 -4 => 10,
134 -5 => 0
135 );
136
137 our %LVL_DIFF_MSG = (
138 -5 => 'Way above your skill',
139 -4 => 'Very low',
140 -3 => 'Slight chance',
141 -2 => 'Low',
142 -1 => 'Nearly 50:50',
143 0 => '50:50',
144 1 => 'Slightly above 50:50',
145 2 => 'Good',
146 3 => 'High',
147 4 => 'Nearly confident',
148 5 => '100%',
149 );
150
151 sub level_diff_to_str {
152 my ($delta) = @_;
153 $delta = -5 if $delta < -5;
154 $delta = 5 if $delta > 5;
155 return $LVL_DIFF_MSG{$delta}
156 }
157
158 sub level_diff_to_chance_perc {
159 my ($delta) = @_;
160 $delta = -5 if $delta < -5;
161 $delta = 5 if $delta > 5;
162 return $LVL_DIFF_CHANCES{$delta}
163 }
164
165 sub analyze {
166 my ($sk, $chdl, $pl, $input_level) = @_;
167
168 my $hadunid = 0;
169 my $found = 0;
170 for ($chdl->grep_by_type (cf::RING, cf::AMULET)) {
171 if (!$_->flag (cf::FLAG_IDENTIFIED) && $_->need_identify) {
172 $hadunid = 1;
173 next;
174 }
175 $found = 1;
176 my $r = Jeweler::Object->new (object => $_);
177 my $msg = $r->analyze ($sk, $pl, $input_level);
178 $pl->message ($r->to_string . ": " . $msg);
179 if ($pl->flag (cf::FLAG_WIZ)) {
180 $r->wiz_analyze ($pl);
181 }
182 }
183 $pl->message ("You couldn't identify the other rings and not analyze them!")
184 if $hadunid;
185 $pl->message ("You couldn't find anything in the bench to analyse!")
186 unless $found;
187 }
188
189 # this function converts metals/minerals into a raw ring (of adornment)
190 sub simple_converter {
191 my ($pl, $ingred, $chdl, $conv) = @_;
192
193 $conv = lc $conv;
194 my $cnvs = $CFG->{conversions};
195
196 return unless $cnvs->{$conv};
197
198 my %ingred_groups;
199
200 my @conv_cfg = @{$cnvs->{$conv}};
201 my $outarch = $conv;
202 my ($ingr_grp, $outarchvalfact, $srcarchname, $xp_gain) = @conv_cfg;
203
204 unless (@conv_cfg <= 4) {
205 warn "ERROR: Conversion for '$outarch' has only " . (@conv_cfg) . " arguments!";
206 return;
207 }
208
209 unless ($xp_gain > 0) {
210 warn "WARNING: xp gain isn't > 0 in conversion '$outarch'\n";
211 return;
212 }
213
214 unless ($outarchvalfact) {
215 warn "ERROR: source-arch-value-multiplier == 0 in conversion '$outarch'\n";
216 return;
217 }
218
219 unless ($outarchvalfact >= 1) {
220 warn "WARNING: source-arch-value-multiplier < 1 in conversion '$outarch', results in more valuable output!\n";
221 }
222
223 my $archvalsum = $ingred->value ($ingr_grp, $srcarchname);
224 $ingred->remove ($ingr_grp, $srcarchname);
225
226 my $outarchval = cf::arch::find ($outarch)->value;
227
228 my $nrof = int $archvalsum / (($outarchval || 1000) * $outarchvalfact);
229 if ($nrof) {
230 # XXX: yes, I know what I'm doing here, I don't set nrof, but it didn't work somehow (pls. check sometimes)
231 for (1 .. $nrof) {
232 $chdl->put (my $ob = cf::object::new $outarch);
233 $ob->set_animation (cf::rndm $ob->num_animations)
234 if ($ob->type == cf::RING);
235 $ob->flag (cf::FLAG_IDENTIFIED, 1);
236 }
237
238 my $xp_sum = $xp_gain * $nrof;
239
240 if ($xp_sum) {
241 $pl->ob->message ("You got $xp_sum xp by making $nrof ${outarch}s");
242 $pl->ob->change_exp ($xp_sum, "jeweler", cf::SK_EXP_SKILL_ONLY);
243 }
244 } else {
245 $pl->ob->message ("You fail to make something, probably you used not enough source material?");
246 }
247 }
248
249
250 package Jeweler::CauldronHandler;
251
252 use strict;
253
254 =head2 CauldronHandler
255
256 The Jeweler::CauldronHandler package, that helps you with handling the
257 cauldron stuff. Can also be used for other skills.
258
259 =cut
260
261 sub new {
262 my ($class, %arg) = @_;
263
264 my $self = bless {
265 %arg,
266 }, $class;
267
268 $self;
269 }
270
271 =over 4
272
273 =item find_cauldron ($arch_name, @map_stack)
274
275 This finds the cauldron with C<$arch_name> on the C<@map_stack> and initalises the CauldronHandler.
276 It takes the topmost cauldron that is found. Returns undef if no cauldron was found.
277 Returns the cauldron object if it was found.
278
279 =cut
280
281 sub find_cauldron {
282 my ($self, $arch_name, @map_stack) = @_;
283
284 my @c =
285 grep {
286 $_->flag (cf::FLAG_IS_CAULDRON)
287 and $_->arch->archname eq $arch_name
288 } @map_stack;
289
290 $self->{cauldron} = $c[0];
291 }
292
293 =item grep_by_type (@types)
294
295 Finds all objects in the cauldron that have the type of one of C<@types>.
296
297 =cut
298
299 sub grep_by_type {
300 my ($self, @types) = @_;
301
302 return () unless $self->{cauldron};
303
304 my @res = grep {
305 my $ob = $_;
306 (grep { $ob->type == $_ } @types) > 0
307 } $self->{cauldron}->inv;
308
309 return @res
310 }
311
312 =item extract_jeweler_ingredients
313
314 Extracts the ingredients that matter for the Jeweler skill
315 and returns a Jeweler::Ingredients object.
316
317 =cut
318
319 sub extract_jeweler_ingredients {
320 my ($self) = @_;
321
322 return () unless $self->{cauldron};
323
324 my $ingreds = {};
325
326 my %type_to_key = (
327 cf::RING => 'rings',
328 cf::AMULET => 'ammys',
329 cf::INORGANIC => 'mets_and_mins',
330 cf::GEM => 'gems',
331 cf::POTION => 'potions',
332 cf::SCROLL => 'scrolls',
333 );
334
335 for ($self->{cauldron}->inv) {
336 if (!$_->flag (cf::FLAG_IDENTIFIED) && $_->need_identify) {
337 die "unidentified";
338 } elsif ($_->flag (cf::FLAG_CURSED) || $_->flag (cf::FLAG_DAMNED)) {
339 die "cursed";
340 }
341
342 if (my $k = $type_to_key{$_->type}) {
343 push @{$ingreds->{$k}}, $_;
344 } else {
345 push @{$ingreds->{other}}, $_;
346 }
347 }
348
349 return Jeweler::Ingredients->new (ingredients => $ingreds, cauldron_helper => $self)
350 }
351
352 =item put ($object)
353
354 Just puts the C<$object> into the cauldron.
355
356 =cut
357
358 sub put {
359 my ($self, $obj) = @_;
360
361 return undef unless $self->{cauldron};
362 $self->{cauldron}->insert ($obj);
363 }
364
365 =back
366
367 =cut
368
369 package Jeweler::Ingredients;
370 use Storable qw/dclone/;
371 use strict;
372
373 =head2 Ingredients
374
375 This class handles the ingredients.
376
377 =over 4
378
379 =item new (ingredients => $ingred_hash)
380
381 This is called from the CauldronHandler that gives you the ingredients.
382
383 =cut
384
385 sub new {
386 my ($class, %arg) = @_;
387
388 my $self = bless {
389 %arg,
390 }, $class;
391
392 $self;
393 }
394
395 =item value ($group, $archname)
396
397 Returns the value of the ingredients in C<$group> with the archetypename C<$archname>.
398
399 =cut
400
401 sub value {
402 my ($self, $group, $archname) = @_;
403
404 my @objs = grep {
405 $_->arch->archname eq $archname
406 } @{$self->{ingredients}->{$group} || []};
407
408 my $sum = 0;
409 for (@objs) {
410 $sum += ($_->nrof || 1) * $_->value;
411 }
412
413 return $sum;
414 }
415
416 =item remove ($group, $archname)
417
418 Removes the ingredients in C<$group> with archname C<$archname>.
419 It removes all in C<$group> if archname is undef.
420
421 =cut
422
423 sub remove {
424 my ($self, $group, $archname) = @_;
425
426 my $ingred = $self->{ingredients};
427
428 my @out;
429
430 for (@{$ingred->{$group}}) {
431 if (defined $archname) {
432 if ($_->arch->archname eq $archname) {
433 Jeweler::Util::remove ($_);
434 } else {
435 push @out, $_;
436 }
437 } else {
438 Jeweler::Util::remove ($_);
439 }
440 }
441
442 @{$ingred->{$group}} = @out;
443 }
444
445 sub get_plan {
446 my ($self) = @_;
447
448 my $ingred = $self->{ingredients};
449
450 for my $plan (keys %{$Jeweler::CFG->{plans}}) {
451 my $plg = $Jeweler::CFG->{plans}->{$plan};
452 my @plga = ();
453 unless (ref $plg eq 'ARRAY') {
454 push @plga, $plg;
455 } else {
456 @plga = @$plg;
457 }
458 next unless @plga > 0;
459 if (Jeweler::Util::grep_for_match ($ingred, @plga)) {
460 return $plan;
461 }
462 }
463 }
464
465 sub get_ring {
466 my ($self) = @_;
467 return (
468 @{$self->{ingredients}->{ammys} || []},
469 @{$self->{ingredients}->{rings} || []}
470 );
471 }
472
473 sub improve_max {
474 my ($stat, $impro) = @_;
475 if ($stat >= 0) {
476 $stat = $impro > $stat ? $impro : $stat;
477 }
478 $stat
479 }
480
481 sub improve_ring_by_plan {
482 my ($self, $plan, $ring) = @_;
483
484 $ring = do { my $guard = Coro::Storable::guard; dclone $ring };
485
486 my $ingred = $self->{ingredients};
487 my $impr = {};
488
489 if ($plan =~ m/^stat_(\S+)$/) {
490 my $statname = $1;
491 my $plingred = Jeweler::getcfg (plans => $plan)
492 or die "ingredients for plan '$plan' not defined!";
493
494 my $cnt = 0;
495 if (my $pot = Jeweler::Util::grep_for_match ($ingred, @$plingred)) {
496 $cnt += $pot->nrof;
497 }
498
499 my $maxstat = Jeweler::getcfg (maximprovements => 'stats');
500 for my $x (1..$maxstat) {
501 my $y = Jeweler::Object::fx ($x, 'stat_items');
502
503 if ($cnt <= $y->[0]) {
504 $ring->{hash}->{stat}->{$statname} =
505 improve_max $ring->{hash}->{stat}->{$statname}, $x;
506 last;
507 }
508 }
509
510 } elsif ($plan =~ m/^spec_(\S+)$/) {
511 my $specname = $1;
512 my $plingred = Jeweler::getcfg (plans => $plan)
513 or die "ingredients for plan '$plan' not defined!";
514
515 my $cnt = 0;
516 if (my $pot = Jeweler::Util::grep_for_match ($ingred, @$plingred)) {
517 $cnt += $pot->nrof;
518 }
519
520 my $maxspec = Jeweler::getcfg (maximprovements => 'specials');
521 for my $x (1..$maxspec) {
522 my $y = Jeweler::Object::fx ($x, 'spec_items');
523
524 if ($cnt <= $y->[0]) {
525 $ring->{hash}->{spec}->{$specname} =
526 improve_max $ring->{hash}->{spec}->{$specname}, $x;
527 last;
528 }
529 }
530
531 } elsif ($plan =~ m/^resist_(\S+)$/) {
532 my $resname = $1;
533 my $resnum = $REV_RESMAP{$resname};
534 my $plingred = Jeweler::getcfg (plans => $plan)
535 or die "ingredients for plan '$plan' not defined!";
536
537 my $cnt = 0;
538 if (my $it = Jeweler::Util::grep_for_match ($ingred, @$plingred)) {
539 $cnt += $it->nrof;
540 }
541 my $resist_item_nr = 0;
542 $self->do_grep (sub { $resist_item_nr += ($_[0]->nrof || 1); 0 }, @$plingred);
543
544 my $maximprovname = (grep { $resnum eq $_ } @Jeweler::EFFECT_RESISTS)
545 ? 'effect_resistances'
546 : 'attack_resistances';
547
548 my $maxres = Jeweler::getcfg (maximprovements => $maximprovname);
549 $resist_item_nr = $maxres if ($resist_item_nr > $maxres);
550 $ring->{hash}->{resist}->{$resnum} =
551 improve_max $ring->{hash}->{resist}->{$resnum}, $resist_item_nr;
552 }
553
554 return $ring;
555 }
556
557 sub do_grep {
558 my ($self, $cb, $cat, @grepar) = @_;
559
560 my $ingred = $self->{ingredients};
561
562 my @rem;
563 for my $ing (@{$ingred->{$cat}}) {
564 if (Jeweler::Util::check_for_match ($ing, @grepar)) {
565 unless ($cb->($ing)) {
566 push @rem, $ing;
567 }
568 } else {
569 push @rem, $ing;
570 }
571 }
572 @{$ingred->{$cat}} = @rem;
573 }
574
575 sub check_costs {
576 my ($self, $costs, $do_remove) = @_;
577
578 my $costs = do { my $guard = Coro::Storable::guard; dclone $costs };
579
580 for my $key (keys %$costs) {
581 my @grepar;
582 if ($key =~ m/^(resist_|spec_|stat_)/) { # check the special items
583 eval { @grepar = @{Jeweler::getcfg (plans => $key) || []} };
584 next if $@;
585 } else { # check the gems
586 @grepar = ('gems', undef, undef, $key);
587 }
588
589 if ($do_remove) {
590 my $rem = $costs->{$key};
591 $self->do_grep (sub {
592 if ($rem) { $rem = Jeweler::Util::remove ($_[0], $rem); }
593 1
594 }, @grepar);
595 if ($rem > 0) {
596 warn "JEWELER BUG: removed ingredients ($key) $rem > 0 after removing!";
597 }
598
599 } else {
600 my $nr;
601 $self->do_grep (sub { $nr += ($_[0]->nrof || 1); 0 }, @grepar);
602 $costs->{$key} -= $nr;
603 }
604
605 }
606
607 return $costs;
608 }
609
610 =back
611
612 =cut
613
614 sub put_to_bench {
615 my ($self, $bench) = @_;
616
617 my $ingred = $self->{ingredients};
618
619 for my $ik (keys %$ingred) {
620 for (@{$ingred->{$ik} || []}) {
621 $bench->put ($_);
622 }
623 }
624 }
625
626 package Jeweler::Object;
627 use strict;
628 use POSIX;
629 use List::Util qw/max min sum/;
630
631 sub new {
632 my ($class, %arg) = @_;
633
634 my $self = bless { }, $class;
635
636 $self->ring_or_ammy_to_hash ($arg{object});
637
638 $self;
639 }
640
641 sub has_resist {
642 my ($self, $resistnam, $resistval) = @_;
643 my $resnum = $REV_RESMAP{uc $resistnam};
644 if (defined ($resistval)) {
645 return 1 if $self->{hash}->{resist}->{$resnum} == $resistval;
646 } else {
647 return 1 if $self->{hash}->{resist}->{$resnum};
648 }
649 return undef;
650 }
651
652 sub projected_exp {
653 my ($self, $input_level) = @_;
654
655 my $lvl = max ($self->power_to_level, 1);
656 my $exp =
657 (cf::level_to_min_exp ($lvl) - cf::level_to_min_exp ($lvl - 1))
658 / (10 + max ($lvl - 1, 0)); # 10 + level times making such a ring
659 # should get you to the rings level at least.
660
661 if (defined $input_level) {
662 my $subexp =
663 (cf::level_to_min_exp ($input_level)
664 - cf::level_to_min_exp ($input_level - 1))
665 / (10 + max ($input_level - 1, 0)); # see above for comment
666
667 $exp -= $subexp;
668 $exp = max ($exp, 0);
669
670 } else {
671 # the experience bonus here is to make level 1 rings give you at least
672 # 200 exp points when making them. This also makes leveling in the
673 # first few levels a bit easier. (probably until around level 5-6).
674 my $expbonus = cf::level_to_min_exp (2) / 5;
675 # this bonus should also only be given for _new_ rings and not for merged
676 # ones - to prevent infinite exp making.
677 $exp += $expbonus;
678 }
679
680 $exp
681 }
682
683 sub analyze {
684 my ($self, $sk, $pl, $input_level) = @_;
685 my $costs = $self->calc_costs;
686
687 unless (defined $costs) {
688 return "This ring has a resistancy above 99%, you can't make that.";
689 }
690
691 my $sklvl = cf::exp_to_level ($sk->stats->exp);
692 my $ringlvl = $self->power_to_level;
693
694 my $tmpl;
695 if ($pl->flag (cf::FLAG_WIZ)) {
696 $tmpl = Jeweler::level_diff_to_chance_perc ($sklvl - $ringlvl);
697 } else {
698 $tmpl = Jeweler::level_diff_to_str ($sklvl - $ringlvl);
699 }
700 my $msg = sprintf "Projected success rate: %s, you would get %d exp for this.", $tmpl, $self->projected_exp ($input_level);
701 return $msg;
702 }
703
704 sub calc_value_from_cost {
705 my ($self, $costs) = @_;
706 my $emarch = cf::arch::find 'emerald';
707 my $saarch = cf::arch::find 'sapphire';
708 my $pearch = cf::arch::find 'pearl';
709 my $ruarch = cf::arch::find 'ruby';
710 my $diarch = cf::arch::find 'gem';
711 my $value = $emarch->value * $costs->{emerald}
712 + $saarch->value * $costs->{sapphire}
713 + $pearch->value * $costs->{pearl}
714 + $ruarch->value * $costs->{ruby}
715 + $diarch->value * $costs->{gem};
716
717 $value
718 }
719
720 sub wiz_analyze {
721 my ($self, $pl) = @_;
722 my $costs = $self->calc_costs;
723 if (defined $costs) {
724 my $desc = "";
725 my $lvl = $self->power_to_level (\$desc);
726 my $scosts = $self->calc_value_from_cost ($costs);
727
728 $pl->message ("costs: "
729 . (join ', ', map { "$_: " . sprintf "%.2f", $costs->{$_} } keys %$costs)
730 . " ("
731 . ($scosts / "platinacoin"->cf::arch::find->value)
732 . " platinum)");
733 $pl->message ("level: $desc");
734 } else {
735 $pl->message ("level: impossible to make, due to impossible resistancy configuration");
736 }
737 }
738
739 sub get_chance_perc {
740 my ($self, $sk) = @_;
741 my $sklvl = cf::exp_to_level ($sk->stats->exp);
742 my $ringlvl = $self->power_to_level;
743 return Jeweler::level_diff_to_chance_perc ($sklvl - $ringlvl);
744 }
745
746 sub fx {
747 my ($res, $cfg) = @_;
748 my $or = $res;
749 my $ar = $Jeweler::CFG->{functions}->{$cfg};
750
751 if (ref $ar && ref $ar->[0] eq 'ARRAY') {
752 $res = $res - 1;
753 return $ar->[max (min ($res, @$ar - 1), 0)];
754
755 } else {
756 # +0.1 is for a jump to the next index when $res / 5 is exactly 1, 2, 3...
757 # old code:
758 #my $idx = ceil (($res / 5) + 0.1) - 1;
759 #my $a = $ar->[max (min ($idx, @$ar - 1), 0)];
760 #my $b = $ar->[max (min ($idx + 1, @$ar - 1), 0)];
761 #my $diff = $b - $a; # use the difference of the cost to the next cost
762 #my $o_cost = $a + ($diff / 5) * ($res % 5); # and do some linear interpolation
763 #return $o_cost;
764 return 0 if $res <= 0;
765 return ($ar / (1 - ($res * 0.01)) - $ar)
766 }
767 }
768
769 sub improve_by_ring {
770 my ($self, @rings) = @_;
771 my $ring = $self;
772 for my $iring (@rings) {
773 for my $cat (qw/stat spec resist/) {
774 for my $k (keys %{$iring->{hash}->{$cat}}) {
775 if ($ring->{hash}->{$cat}->{$k} < $iring->{hash}->{$cat}->{$k}) {
776 $ring->{hash}->{$cat}->{$k} = $iring->{hash}->{$cat}->{$k};
777 }
778 }
779 }
780 }
781 }
782
783 sub negate {
784 my ($self) = @_;
785 for my $cat (qw/stat spec resist/) {
786 for my $k (keys %{$self->{hash}->{$cat}}) {
787 if ($self->{hash}->{$cat}->{$k} > 0) {
788 $self->{hash}->{$cat}->{$k} *= -1;
789 }
790 }
791 }
792 $self->{hash}{value} = 0;
793 }
794
795 sub to_string {
796 my ($self) = @_;
797 my $r = $self->{hash};
798 return
799 $r->{arch} . " " .
800 join ("",
801 grep { $_ ne "" }
802 join ("",
803 (map {
804 my $rv = $r->{resist}->{$_};
805 "(resist " . (lc $Jeweler::RESMAP{$_}) . " " . ($rv > 0 ? '+' : '') . $rv . ")"
806 } grep { $r->{resist}->{$_} } @Jeweler::RESISTS),
807 (map {
808 my $rv = $r->{stat}->{$_};
809 "(" . (ucfirst lc $_) . ($rv > 0 ? '+' : '') . $rv . ")"
810 } grep { $r->{stat}->{$_} } keys %{$r->{stat}}),
811 (map {
812 my $rv = $r->{spec}->{$_};
813 "(" . (lc $_) . ($rv > 0 ? '+' : '') . $rv . ")"
814 } grep { $r->{spec}->{$_} } keys %{$r->{spec}})))
815 }
816
817 sub ring_or_ammy_to_hash {
818 my ($self, $thing) = @_;
819
820 my $obj = {};
821
822 for (@Jeweler::RESISTS) {
823 $obj->{resist}->{$_} = $thing->resist ($_);
824 }
825
826 my $stats = $thing->stats;
827
828 for (qw/Str Dex Con Wis Cha Int Pow/) {
829 $obj->{stat}->{lc $_} = $stats->$_;
830 }
831
832 $obj->{spec}{regen} = $stats->hp;
833 $obj->{spec}{magic} = $stats->sp;
834 $obj->{spec}{wc} = $stats->wc;
835 $obj->{spec}{dam} = $stats->dam;
836 $obj->{spec}{ac} = $stats->ac;
837 $obj->{spec}{speed} = $stats->exp;
838 $obj->{spec}{food} = $stats->food;
839
840 $obj->{name} = $thing->name;
841 $obj->{arch} = $thing->arch->archname;
842 $obj->{face} = $thing->face;
843
844 $obj->{value} = $thing->value;
845
846 $self->{hash} = $obj
847 }
848
849 sub to_object {
850 my ($self) = @_;
851
852 my $obj = cf::object::new $self->{hash}->{arch};
853
854 $obj->item_power (floor ($self->power_to_level / 3)); # there have to be strings attached!
855
856 $obj->face ($self->{hash}{face});
857
858 my $stats = $obj->stats;
859
860 $stats->hp ($self->{hash}{spec}{regen});
861 $stats->sp ($self->{hash}{spec}{magic});
862 $stats->wc ($self->{hash}{spec}{wc});
863 $stats->dam ($self->{hash}{spec}{dam});
864 $stats->ac ($self->{hash}{spec}{ac});
865 $stats->exp ($self->{hash}{spec}{speed});
866 $stats->food ($self->{hash}{spec}{food});
867
868 $stats->$_ ($self->{hash}{stat}{lc $_})
869 for qw/Str Dex Con Wis Cha Int Pow/;
870
871 for (@Jeweler::RESISTS) {
872 $obj->resist ($_, $self->{hash}->{resist}->{$_});
873 }
874
875 $obj->flag (cf::FLAG_IDENTIFIED, 1);
876
877 $obj->value ($self->{hash}{value});
878
879 return $obj;
880 }
881
882 sub set_value { $_[0]->{hash}{value} = $_[1] }
883
884 sub is_better_than {
885 my ($self, $other) = @_;
886
887 for my $type (qw/spec stat resist/) {
888 for my $stat (keys %{$self->{hash}->{$type}}) {
889 if ($self->{hash}->{$type}->{$stat} > $other->{hash}->{$type}->{$stat}) {
890 return 1;
891 }
892 }
893 }
894
895 return 0;
896 }
897
898 sub stat_level {
899 my ($self) = @_;
900 my $stats = $self->{hash}->{stat} || {};
901
902 my $maxlevel = Jeweler::getcfg (maxlevels => 'stat_level');
903 my $maxstat = Jeweler::getcfg (maximprovements => 'stats');
904
905 my $stat_cnt = scalar (grep { $_ > 0 } values %$stats);
906 my $stat_sum = sum (values %$stats); # also count the negative stats!
907 my $level = int (($maxlevel / $maxstat) * $stat_sum);
908
909 ($level, $stat_cnt)
910 }
911
912 sub resist_level {
913 my ($self) = @_;
914
915 my $resists = $self->{hash}->{resist} || {};
916
917 my $att_res_lvl = Jeweler::getcfg (maxlevels => 'resist_level');
918 my $efc_res_lvl = Jeweler::getcfg (maxlevels => 'effect_resist_level');
919 my $max_att_res = Jeweler::getcfg (maximprovements => 'attack_resistances');
920 my $max_efc_res = Jeweler::getcfg (maximprovements => 'effect_resistances');
921 my $max_ovr_res = Jeweler::getcfg (maximprovements => 'resistances');
922
923 my $ressum = 0;
924 my $rescnt = 0;
925 my @reslevels;
926
927 for my $resnam (keys %$resists) {
928 my $res = $resists->{$resnam};
929
930 $rescnt++
931 if $res > 0; # negative resistancies are not an improvement
932
933 $ressum += $res; # note: negative resistancies lower the sum
934
935 next unless $res > 0;
936
937 my $level = 0;
938 if (grep { $resnam eq $_ } @Jeweler::EFFECT_RESISTS) {
939 $level = ceil (($efc_res_lvl / $max_efc_res) * $res);
940 } else {
941 $level = ceil (($att_res_lvl / $max_att_res) * $res);
942 }
943 push @reslevels, $level;
944 }
945
946 my $overall_lvl = ($att_res_lvl / $max_ovr_res) * $ressum;
947
948 (max (@reslevels, $overall_lvl), $rescnt);
949 }
950
951 sub special_level {
952 my ($self) = @_;
953
954 my $specials = $self->{hash}->{spec} || {};
955
956 my $max_spc_lvl = Jeweler::getcfg (maxlevels => 'spec_level');
957 my $max_specials = Jeweler::getcfg (maximprovements => 'specials');
958
959 my @speclvls;
960 my $specsum = 0;
961 my $imprs = 0;
962
963 for my $spcnam (keys %$specials) {
964 my $spc = $specials->{$spcnam};
965 next unless $spc > 0;
966
967 $specsum += $spc;
968 $imprs++;
969
970 my $max_spc = Jeweler::getcfg (maxspecial => $spcnam);
971
972 my $lvl = ($max_spc_lvl / $max_spc) * $spc;
973 push @speclvls, $lvl;
974 }
975
976 my $sumlvl = ($max_spc_lvl / $max_specials) * $specsum;
977
978 (max (@speclvls, $sumlvl), $imprs)
979 }
980
981
982 # this function calculated the 'level' of an amulet or a ring
983 sub power_to_level {
984 my ($self, $lvldescr) = @_;
985
986 my $max_imprs = Jeweler::getcfg (maximprovements => 'improvements');
987 my $max_impr_lvl = Jeweler::getcfg (maxlevels => 'improve_level');
988
989 my ($stat_lvl, $stat_imprs) = $self->stat_level;
990 my ($resist_lvl, $res_imprs) = $self->resist_level;
991 my ($spec_lvl, $spec_imprs) = $self->special_level;
992
993 my $impr_sum = $stat_imprs + $res_imprs + $spec_imprs;
994
995 my $impr_lvl = ceil (($max_impr_lvl / ($max_imprs + 1)) * ($impr_sum - 1)); # 1 improvemnt bonus
996
997 my $levl = int max ($stat_lvl, $resist_lvl, $impr_lvl, $spec_lvl, 0);
998
999 if ($lvldescr) {
1000 $$lvldescr =
1001 sprintf "%3d: %s\n", $levl,
1002 "stat: $stat_lvl, resist: $resist_lvl, improve: $impr_lvl, "
1003 ."spec: $spec_lvl (num impr $stat_imprs + $res_imprs + $spec_imprs - 1)";
1004 }
1005
1006 $levl
1007 }
1008
1009 sub add_stat_costs {
1010 my ($self, $cost) = @_;
1011
1012 my $stats = $self->{hash}->{stat};
1013
1014 for my $stat (keys %$stats) {
1015 my $sum = $stats->{$stat};
1016
1017 next unless $sum > 0;
1018
1019 my $statfx = fx ($sum, 'stat_items');
1020 $cost->{"stat_$stat"} += $statfx->[0];
1021 split_diamonds ($cost, $statfx->[1], 'stat_' . $stat);
1022 }
1023 }
1024
1025 sub add_special_costs {
1026 my ($self, $cost) = @_;
1027
1028 my $specials = $self->{hash}->{spec};
1029
1030 for my $spec (keys %$specials) {
1031 my $sum = $specials->{$spec};
1032
1033 next unless $sum > 0;
1034
1035 my $specfx = fx ($sum, 'spec_items');
1036 $cost->{"spec_$spec"} += $specfx->[0];
1037 split_diamonds ($cost, $specfx->[1], 'spec_' . $spec);
1038 }
1039 }
1040
1041 sub calc_costs {
1042 my ($self) = @_;
1043
1044 my $costs = {};
1045
1046 my $ring = $self->{hash};
1047
1048 for my $resnum (keys %{$ring->{resist} || {}}) {
1049
1050 my $res = $ring->{resist}->{$resnum};
1051
1052 next unless $res > 0;
1053
1054 return undef if $res == 100;
1055
1056 $costs->{"resist_" . $Jeweler::RESMAP{$resnum}} += $res;
1057
1058 my $diamonds;
1059 if (grep { $resnum eq $_ } @Jeweler::EFFECT_RESISTS) {
1060 $diamonds += fx ($res, 'effect_resist_diamonds_x');
1061 } else {
1062 $diamonds += fx ($res, 'attack_resist_diamonds_x');
1063 }
1064
1065 split_diamonds ($costs, $diamonds, 'resist_' . $Jeweler::RESMAP{$resnum});
1066 }
1067
1068 $self->add_stat_costs ($costs);
1069 $self->add_special_costs ($costs);
1070
1071 return $costs;
1072 }
1073
1074 sub split_diamonds {
1075 my ($cost, $diamonds, $category) = @_;
1076
1077 my $stat_split = Jeweler::getcfg (diamond_split => $category);
1078
1079 my $sum = sum (@$stat_split);
1080
1081 my $emarch = cf::arch::find 'emerald';
1082 my $saarch = cf::arch::find 'sapphire';
1083 my $pearch = cf::arch::find 'pearl';
1084 my $ruarch = cf::arch::find 'ruby';
1085 my $diarch = cf::arch::find 'gem';
1086
1087 my $sumvalue = $diarch->value * $diamonds;
1088
1089 $cost->{emerald} += ceil $sumvalue * $stat_split->[0] / max 1, $emarch->value;
1090 $cost->{sapphire} += ceil $sumvalue * $stat_split->[1] / max 1, $saarch->value;
1091 $cost->{pearl} += ceil $sumvalue * $stat_split->[2] / max 1, $pearch->value;
1092 $cost->{ruby} += ceil $sumvalue * $stat_split->[3] / max 1, $ruarch->value;
1093 $cost->{gem} += ceil $sumvalue * $stat_split->[4] / max 1, $diarch->value;
1094 }
1095
1096 package Jeweler::Util;
1097
1098 use strict;
1099
1100 =head2 Util
1101
1102 Some utility functions for the Jeweler skill.
1103
1104 =over 4
1105
1106 =item remove ($object[, $nrof])
1107
1108 Removes the C<$object>. If C<$nrof> is given, remove only C<$nrof> objects.
1109 The return value is the number of 'single' objects that couldn't be removed.
1110
1111 =cut
1112
1113 sub remove {
1114 my ($obj, $nrof) = @_;
1115
1116 my $c = $obj->nrof || 1;
1117 my $r = $c > $nrof ? 0 : $nrof - $c;
1118 $obj->decrease (defined ($nrof) ? $nrof : ($obj->nrof || 1));
1119
1120 $r
1121 }
1122
1123 sub check_for_match {
1124 my ($thing, @matchar) = @_;
1125
1126 my $i = 0;
1127 my $check_cnts = 0;
1128 my $check_true = 0;
1129 for my $match (@matchar) {
1130 if ($i % 3 == 0) {
1131 return 1 if $check_true && $check_cnts == $check_true;
1132 $check_cnts = 0;
1133 $check_true = 0;
1134 }
1135
1136 if ($match =~ m/^\s*$/) {
1137 $i++;
1138 next;
1139 }
1140
1141 $check_cnts++;
1142 if ($i % 3 == 0) {
1143 $thing->name eq $match
1144 and $check_true++;
1145 } elsif ($i % 3 == 1) {
1146 $thing->title eq $match
1147 and $check_true++;
1148 } else { # $i % 3 == 2
1149 $thing->arch->archname eq $match
1150 and $check_true++;
1151 }
1152 $i++;
1153 }
1154 #d# warn "CHECK $check_true | $check_cnts | [".(join ',', @matchar).":".join (",", ($thing->name, $thing->title, $thing->arch->archname))."\n";
1155 return 1 if $check_true && $check_cnts == $check_true;
1156 return 0;
1157 }
1158
1159 sub grep_for_match {
1160 my ($ingred, $group, @matchar) = @_;
1161
1162 for my $thing (@{$ingred->{$group} || []}) {
1163 #d# warn sprintf "DEB:(%s,%s,%s)<->%s\n", $thing->name, $thing->title, $thing->arch->archname, "@matchar"; #d#
1164 if (check_for_match ($thing, @matchar)) {
1165 return $thing;
1166 }
1167 }
1168 return undef;
1169 }
1170
1171 =back
1172
1173 1