ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/deliantra/server/ext/Jeweler.pm
Revision: 1.36
Committed: Tue May 4 22:49:21 2010 UTC (14 years ago) by root
Branch: MAIN
Changes since 1.35: +7 -4 lines
Log Message:
more common sense

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