001package squidpony.squidai;
002
003import squidpony.squidgrid.FOV;
004import squidpony.squidgrid.Measurement;
005import squidpony.squidgrid.Radius;
006import squidpony.squidgrid.mapping.DungeonUtility;
007import squidpony.squidmath.Coord;
008import squidpony.squidmath.NumberTools;
009import squidpony.squidmath.OrderedMap;
010import squidpony.squidmath.OrderedSet;
011
012import java.io.Serializable;
013import java.util.ArrayList;
014import java.util.Arrays;
015import java.util.Collection;
016
017/**
018 * An AOE type that has an origin, a radius, an angle, and a span; it will blast from the origin to a length equal to
019 * radius along the angle (in degrees), moving somewhat around corners/obstacles, and also spread a total of span
020 * degrees around the angle (a span of 90 will affect a full quadrant, centered on angle). You can specify the
021 * RadiusType to Radius.DIAMOND for Manhattan distance, RADIUS.SQUARE for Chebyshev, or RADIUS.CIRCLE for Euclidean.
022 *
023 * RADIUS.CIRCLE (Euclidean measurement) will produce the most real-looking cones. This will produce doubles for its
024 * {@link #findArea()} method which are greater than 0.0 and less than or equal to 1.0.
025 *
026 * This class uses {@link FOV} to create its area of effect.
027 * Created by Tommy Ettinger on 7/13/2015.
028 */
029public class ConeAOE implements AOE, Serializable {
030    private static final long serialVersionUID = 2L;
031    private FOV fov;
032    private Coord origin;
033    private double radius, angle, span;
034    private double[][] map;
035    private char[][] dungeon;
036    private Radius radiusType;
037    private Reach reach = new Reach(1, 1, Radius.SQUARE, AimLimit.FREE);
038
039    public ConeAOE(Coord origin, Coord endCenter, double span, Radius radiusType)
040    {
041        fov = new FOV(FOV.RIPPLE_LOOSE);
042        this.origin = origin;
043        radius = radiusType.radius(origin.x, origin.y, endCenter.x, endCenter.y);
044
045        angle = NumberTools.atan2_(endCenter.y - origin.y, endCenter.x - origin.x) * 360.0;
046//        this.startAngle = Math.abs((angle - span / 2.0) % 360.0);
047//        this.endAngle = Math.abs((angle + span / 2.0) % 360.0);
048        this.span = span;
049        this.radiusType = radiusType;
050    }
051    public ConeAOE(Coord origin, int radius, double angle, double span, Radius radiusType)
052    {
053        fov = new FOV(FOV.RIPPLE_LOOSE);
054        this.origin = origin;
055        this.radius = radius;
056//        this.startAngle = Math.abs((angle - span / 2.0) % 360.0);
057//        this.endAngle = Math.abs((angle + span / 2.0) % 360.0);
058        this.angle = angle;
059        this.span = span;
060        this.radiusType = radiusType;
061    }
062
063    @Override
064    public Coord getOrigin() {
065        return origin;
066    }
067
068    @Override
069    public void setOrigin(Coord origin) {
070        this.origin = origin;
071    }
072
073    @Override
074    public AimLimit getLimitType() {
075        return reach.limit;
076    }
077
078    @Override
079    public int getMinRange() {
080        return reach.minDistance;
081    }
082
083    @Override
084    public int getMaxRange() {
085        return reach.maxDistance;
086    }
087
088    @Override
089    public Radius getMetric() {
090        return reach.metric;
091    }
092
093    /**
094     * Gets the same values returned by getLimitType(), getMinRange(), getMaxRange(), and getMetric() bundled into one
095     * Reach object.
096     *
097     * @return a non-null Reach object.
098     */
099    @Override
100    public Reach getReach() {
101        return reach;
102    }
103
104    @Override
105    public void setLimitType(AimLimit limitType) {
106        reach.limit = limitType;
107
108    }
109
110    @Override
111    public void setMinRange(int minRange) {
112        reach.minDistance = minRange;
113    }
114
115    @Override
116    public void setMaxRange(int maxRange) {
117        reach.maxDistance = maxRange;
118
119    }
120
121    @Override
122    public void setMetric(Radius metric) {
123        reach.metric = metric;
124    }
125
126    /**
127     * Sets the same values as setLimitType(), setMinRange(), setMaxRange(), and setMetric() using one Reach object.
128     *
129     * @param reach a non-null Reach object.
130     */
131    @Override
132    public void setReach(Reach reach) {
133        if(reach != null)
134            this.reach = reach;
135    }
136
137    public double getRadius() {
138        return radius;
139    }
140
141    public void setRadius(double radius) {
142        this.radius = radius;
143    }
144
145    public double getAngle() {
146        return angle;
147    }
148
149    public void setAngle(double angle) {
150        if (reach.limit == null || reach.limit == AimLimit.FREE ||
151                (reach.limit == AimLimit.EIGHT_WAY && (int) angle % 45 == 0) ||
152                (reach.limit == AimLimit.DIAGONAL && (int) angle % 90 == 45) ||
153                (reach.limit == AimLimit.ORTHOGONAL && (int) angle % 90 == 0)) {
154            this.angle = angle;
155//            this.startAngle = Math.abs((angle - span / 2.0) % 360.0);
156//            this.endAngle = Math.abs((angle + span / 2.0) % 360.0);
157        }
158    }
159
160    public void setEndCenter(Coord endCenter) {
161//        radius = radiusType.radius(origin.x, origin.y, endCenter.x, endCenter.y);
162        if (AreaUtils.verifyLimit(reach.limit, origin, endCenter)) {
163            angle = NumberTools.atan2_(endCenter.y - origin.y, endCenter.x - origin.x) * 360.0;
164//            startAngle = Math.abs((angle - span / 2.0) % 360.0);
165//            endAngle = Math.abs((angle + span / 2.0) % 360.0);
166        }
167    }
168
169    public double getSpan() {
170        return span;
171    }
172
173    public void setSpan(double span) {
174        this.span = span;
175//        this.startAngle = Math.abs((angle - span / 2.0) % 360.0);
176//        this.endAngle = Math.abs((angle + span / 2.0) % 360.0);
177    }
178
179    public Radius getRadiusType() {
180        return radiusType;
181    }
182
183    public void setRadiusType(Radius radiusType) {
184        this.radiusType = radiusType;
185    }
186
187    @Override
188    public void shift(Coord aim) {
189        setEndCenter(aim);
190    }
191
192    @Override
193    public boolean mayContainTarget(Collection<Coord> targets) {
194        for (Coord p : targets) {
195            if (radiusType.radius(origin.x, origin.y, p.x, p.y) <= radius) {
196                double d = (angle - NumberTools.atan2_(p.y - origin.y, p.x - origin.x) * 360.0);
197                if(d > 180)
198                    d = 360 - d;
199                if(d < span * 0.5)
200                    return true;
201            }
202        }
203        return false;
204    }
205
206    @Override
207    public OrderedMap<Coord, ArrayList<Coord>> idealLocations(Collection<Coord> targets, Collection<Coord> requiredExclusions) {
208        if(targets == null)
209            return new OrderedMap<>();
210        if(requiredExclusions == null) requiredExclusions = new OrderedSet<>();
211
212        //requiredExclusions.remove(origin);
213        int totalTargets = targets.size();
214        OrderedMap<Coord, ArrayList<Coord>> bestPoints = new OrderedMap<>(totalTargets * 8);
215
216        if(totalTargets == 0)
217            return bestPoints;
218
219        Coord[] ts = targets.toArray(new Coord[0]);
220        Coord[] exs = requiredExclusions.toArray(new Coord[0]);
221        Coord t;
222
223        double[][][] compositeMap = new double[totalTargets][dungeon.length][dungeon[0].length];
224        double tAngle;
225
226
227        char[][] dungeonCopy = new char[dungeon.length][dungeon[0].length];
228        double[][] tmpfov = new double[dungeon.length][dungeon[0].length];
229        Coord tempPt;
230
231        for (int i = 0; i < exs.length; i++) {
232            t = exs[i];
233            tAngle = NumberTools.atan2Degrees360(t.y - origin.y, t.x - origin.x);
234            FOV.reuseRippleFOV(map, tmpfov, 2, origin.x, origin.y, radius, radiusType, tAngle, span);
235            for (int x = 0; x < dungeon.length; x++) {
236                for (int y = 0; y < dungeon[x].length; y++) {
237                    tempPt = Coord.get(x, y);
238                    dungeonCopy[x][y] = !(origin.x == x && origin.y == y) && (tmpfov[x][y] > 0.0 || !AreaUtils.verifyLimit(reach.limit, origin, tempPt)) ? '!' : dungeon[x][y];
239                }
240            }
241        }
242        
243        DijkstraMap dm = new DijkstraMap(dungeon, radiusType.matchingMeasurement());
244        for (int i = 0; i < ts.length; ++i) {
245            dm.initialize(dungeon);
246            t = ts[i];
247            tAngle = NumberTools.atan2Degrees360(t.y - origin.y, t.x - origin.x);
248
249            tmpfov = fov.calculateFOV(map, origin.x, origin.y, radius, radiusType, tAngle, span);
250
251
252            for (int x = 0; x < dungeon.length; x++) {
253                for (int y = 0; y < dungeon[x].length; y++) {
254                    if (tmpfov[x][y] > 0.0)
255                    {
256                        compositeMap[i][x][y] = dm.physicalMap[x][y];
257                    }
258                    else compositeMap[i][x][y] = DijkstraMap.WALL;
259                }
260            }
261            if(compositeMap[i][t.x][t.y] > DijkstraMap.FLOOR)
262            {
263                for (int x = 0; x < dungeon.length; x++) {
264                    Arrays.fill(compositeMap[i][x], 99999.0);
265                }
266                continue;
267            }
268
269
270            dm.initialize(compositeMap[i]);
271            dm.setGoal(t);
272            dm.scan(null, null, true);
273            for (int x = 0; x < dungeon.length; x++) { 
274                for (int y = 0; y < dungeon[0].length; y++) { 
275                    if(dm.gradientMap[x][y] < DijkstraMap.FLOOR && dungeonCopy[x][y] != '!')
276                        compositeMap[i][x][y] = dm.gradientMap[x][y];
277                    else
278                        compositeMap[i][x][y] = 99999.0;
279                }
280            }
281        }
282        double bestQuality = 99999 * ts.length;
283        double[][] qualityMap = new double[dungeon.length][dungeon[0].length];
284        for (int x = 0; x < qualityMap.length; x++) {
285            for (int y = 0; y < qualityMap[x].length; y++) {
286                qualityMap[x][y] = 0.0;
287                long bits = 0;
288                for (int i = 0; i < ts.length; ++i) {
289                    qualityMap[x][y] += compositeMap[i][x][y];
290                    if(compositeMap[i][x][y] < 99999.0 && i < 63)
291                        bits |= 1 << i;
292                }
293                if(qualityMap[x][y] < bestQuality)
294                {
295                    ArrayList<Coord> ap = new ArrayList<>();
296
297                    for (int i = 0; i < ts.length && i < 63; ++i) {
298                        if((bits & (1 << i)) != 0)
299                            ap.add(ts[i]);
300                    }
301                    if(ap.size() > 0) {
302                        bestQuality = qualityMap[x][y];
303                        bestPoints.clear();
304                        bestPoints.put(Coord.get(x, y), ap);
305                    }
306                }
307                else if(qualityMap[x][y] == bestQuality) {
308                    ArrayList<Coord> ap = new ArrayList<>();
309
310                    for (int i = 0; i < ts.length && i < 63; ++i) {
311                        if ((bits & (1 << i)) != 0)
312                            ap.add(ts[i]);
313                    }
314                    if (ap.size() > 0) {
315                        bestPoints.put(Coord.get(x, y), ap);
316                    }
317                }
318
319            }
320        }
321
322        return bestPoints;
323    }
324
325    @Override
326    public OrderedMap<Coord, ArrayList<Coord>> idealLocations(Collection<Coord> priorityTargets, Collection<Coord> lesserTargets, Collection<Coord> requiredExclusions) {
327        if(priorityTargets == null)
328            return idealLocations(lesserTargets, requiredExclusions);
329        if(requiredExclusions == null) requiredExclusions = new OrderedSet<>();
330
331        //requiredExclusions.remove(origin);
332        int totalTargets = priorityTargets.size() + lesserTargets.size();
333        OrderedMap<Coord, ArrayList<Coord>> bestPoints = new OrderedMap<>(totalTargets * 8);
334
335        if(totalTargets == 0)
336            return bestPoints;
337
338        Coord[] pts = priorityTargets.toArray(new Coord[0]);
339        Coord[] lts = lesserTargets.toArray(new Coord[0]);
340        Coord[] exs = requiredExclusions.toArray(new Coord[0]);
341        Coord t;
342
343        double[][][] compositeMap = new double[totalTargets][dungeon.length][dungeon[0].length];
344        double tAngle; //, tStartAngle, tEndAngle;
345
346        char[][] dungeonCopy = new char[dungeon.length][dungeon[0].length],
347                dungeonPriorities = new char[dungeon.length][dungeon[0].length];
348        for (int i = 0; i < dungeon.length; i++) {
349            System.arraycopy(dungeon[i], 0, dungeonCopy[i], 0, dungeon[i].length);
350            Arrays.fill(dungeonPriorities[i], '#');
351        }
352        double[][] tmpfov;
353        Coord tempPt;
354        for (int i = 0; i < exs.length; ++i) {
355            t = exs[i];
356
357
358            tAngle = NumberTools.atan2_(t.y - origin.y, t.x - origin.x) * 360.0;
359            //            tStartAngle = Math.abs((tAngle - span / 2.0) % 360.0);
360//            tEndAngle = Math.abs((tAngle + span / 2.0) % 360.0);
361            tmpfov = fov.calculateFOV(map, origin.x, origin.y, radius, radiusType, tAngle, span);
362            for (int x = 0; x < dungeon.length; x++) {
363                for (int y = 0; y < dungeon[x].length; y++) {
364                    tempPt = Coord.get(x, y);
365                    dungeonCopy[x][y] = (tmpfov[x][y] > 0.0 || !AreaUtils.verifyLimit(reach.limit, origin, tempPt)) ? '!' : dungeonCopy[x][y];
366                }
367            }
368        }
369
370        Measurement dmm = Measurement.MANHATTAN;
371        if(radiusType == Radius.SQUARE || radiusType == Radius.CUBE) dmm = Measurement.CHEBYSHEV;
372        else if(radiusType == Radius.CIRCLE || radiusType == Radius.SPHERE) dmm = Measurement.EUCLIDEAN;
373
374        for (int i = 0; i < pts.length; ++i) {
375            DijkstraMap dm = new DijkstraMap(dungeon, dmm);
376            t = pts[i];
377            tAngle = NumberTools.atan2_(t.y - origin.y, t.x - origin.x) * 360.0;
378//            tStartAngle = Math.abs((tAngle - span / 2.0) % 360.0);
379//            tEndAngle = Math.abs((tAngle + span / 2.0) % 360.0);
380
381            tmpfov = fov.calculateFOV(map, origin.x, origin.y, radius, radiusType, tAngle, span);
382
383            for (int x = 0; x < dungeon.length; x++) {
384                for (int y = 0; y < dungeon[x].length; y++) {
385                    if (tmpfov[x][y] > 0.0){
386                        compositeMap[i][x][y] = dm.physicalMap[x][y];
387                        dungeonPriorities[x][y] = dungeon[x][y];
388                    }
389                    else compositeMap[i][x][y] = DijkstraMap.WALL;
390                }
391            }
392            if(compositeMap[i][pts[i].x][pts[i].y] > DijkstraMap.FLOOR)
393            {
394                for (int x = 0; x < dungeon.length; x++) {
395                    Arrays.fill(compositeMap[i][x], 399999.0);
396                }
397                continue;
398            }
399
400
401
402            dm.initialize(compositeMap[i]);
403            dm.setGoal(t);
404            dm.scan(null, null, true);
405            for (int x = 0; x < dungeon.length; x++) {
406                for (int y = 0; y < dungeon[x].length; y++) {
407                    compositeMap[i][x][y] = (dm.gradientMap[x][y] < DijkstraMap.FLOOR  && dungeonCopy[x][y] != '!') ? dm.gradientMap[x][y] : 399999.0;
408                }
409            }
410        }
411
412        for (int i = pts.length; i < totalTargets; ++i) {
413            DijkstraMap dm = new DijkstraMap(dungeon, dmm);
414            t = lts[i - pts.length];
415
416            tAngle = NumberTools.atan2_(t.y - origin.y, t.x - origin.x) * 360.0;
417            //            tStartAngle = Math.abs((tAngle - span / 2.0) % 360.0);
418//            tEndAngle = Math.abs((tAngle + span / 2.0) % 360.0);
419
420            tmpfov = fov.calculateFOV(map, origin.x, origin.y, radius, radiusType, tAngle, span);
421
422            for (int x = 0; x < dungeon.length; x++) {
423                for (int y = 0; y < dungeon[x].length; y++) {
424                    if (tmpfov[x][y] > 0.0){
425                        compositeMap[i][x][y] = dm.physicalMap[x][y];
426                    }
427                    else compositeMap[i][x][y] = DijkstraMap.WALL;
428                }
429            }
430            if(compositeMap[i][lts[i - pts.length].x][lts[i - pts.length].y] > DijkstraMap.FLOOR)
431            {
432                for (int x = 0; x < dungeon.length; x++)
433                {
434                    Arrays.fill(compositeMap[i][x], 99999.0);
435                }
436                continue;
437            }
438
439
440            dm.initialize(compositeMap[i]);
441            dm.setGoal(t);
442            dm.scan(null, null, true);
443            for (int x = 0; x < dungeon.length; x++) {
444                for (int y = 0; y < dungeon[x].length; y++) {
445                    compositeMap[i][x][y] = (dm.gradientMap[x][y] < DijkstraMap.FLOOR && dungeonCopy[x][y] != '!' && dungeonPriorities[x][y] != '#') ? dm.gradientMap[x][y] : 99999.0;
446                }
447            }
448        }
449        double bestQuality = 99999 * lts.length + 399999 * pts.length;
450        double[][] qualityMap = new double[dungeon.length][dungeon[0].length];
451        for (int x = 0; x < qualityMap.length; x++) {
452            for (int y = 0; y < qualityMap[x].length; y++) {
453                qualityMap[x][y] = 0.0;
454                long pbits = 0, lbits = 0;
455                for (int i = 0; i < pts.length; ++i) {
456                    qualityMap[x][y] += compositeMap[i][x][y];
457                    if(compositeMap[i][x][y] < 399999.0 && i < 63)
458                        pbits |= 1 << i;
459                }
460                for (int i = pts.length; i < totalTargets; ++i) {
461                    qualityMap[x][y] += compositeMap[i][x][y];
462                    if(compositeMap[i][x][y] < 99999.0 && i < 63)
463                        lbits |= 1 << i;
464                }
465                if(qualityMap[x][y] < bestQuality)
466                {
467                    ArrayList<Coord> ap = new ArrayList<>();
468
469                    for (int i = 0; i < pts.length && i < 63; ++i) {
470                        if((pbits & (1 << i)) != 0)
471                            ap.add(pts[i]);
472                    }
473                    for (int i = pts.length; i < totalTargets && i < 63; ++i) {
474                        if((lbits & (1 << i)) != 0)
475                            ap.add(lts[i - pts.length]);
476                    }
477
478                    if(ap.size() > 0) {
479                        bestQuality = qualityMap[x][y];
480                        bestPoints.clear();
481                        bestPoints.put(Coord.get(x, y), ap);
482                    }
483
484                }
485                else if(qualityMap[x][y] == bestQuality) {
486                    ArrayList<Coord> ap = new ArrayList<>();
487
488                    for (int i = 0; i < pts.length && i < 63; ++i) {
489                        if ((pbits & (1 << i)) != 0) {
490                            ap.add(pts[i]);
491                            ap.add(pts[i]);
492                            ap.add(pts[i]);
493                            ap.add(pts[i]);
494                        }
495                    }
496                    for (int i = pts.length; i < totalTargets && i < 63; ++i) {
497                        if ((lbits & (1 << i)) != 0)
498                            ap.add(lts[i - pts.length]);
499                    }
500                    if (ap.size() > 0) {
501                        bestPoints.put(Coord.get(x, y), ap);
502                    }
503                }
504            }
505        }
506
507        return bestPoints;
508    }
509
510    /*
511    @Override
512    public ArrayList<ArrayList<Coord>> idealLocations(Set<Coord> targets, Set<Coord> requiredExclusions) {
513        int totalTargets = targets.size() + 1;
514        int maxEffect = (int)(radiusType.volume2D(radius) * Math.max(5, span) / 360.0);
515        double allowed = Math.toRadians(span / 2.0);
516        ArrayList<ArrayList<Coord>> locs = new ArrayList<ArrayList<Coord>>(totalTargets);
517
518        for(int i = 0; i < totalTargets; i++)
519        {
520            locs.add(new ArrayList<Coord>(maxEffect));
521        }
522        if(totalTargets == 1)
523            return locs;
524
525        int ctr = 0;
526        if(radius < 1)
527        {
528            locs.get(totalTargets - 2).addAll(targets);
529            return locs;
530        }
531
532        double tmpAngle, ang;
533        boolean[][] tested = new boolean[dungeon.length][dungeon[0].length];
534        for (int x = 1; x < dungeon.length - 1; x += radius) {
535            BY_POINT:
536            for (int y = 1; y < dungeon[x].length - 1; y += radius) {
537                ang = Math.atan2(y - origin.y, x - origin.x); // between -pi and pi
538
539                for(Coord ex : requiredExclusions) {
540                    if (radiusType.radius(x, y, ex.x, ex.y) <= radius) {
541                        tmpAngle = Math.abs(ang - Math.atan2(ex.y - origin.y, ex.x - origin.x));
542                        if(tmpAngle > Math.PI) tmpAngle = PI2 - tmpAngle;
543                        if(tmpAngle < allowed)
544                            continue BY_POINT;
545                    }
546                }
547                ctr = 0;
548                for(Coord tgt : targets) {
549                    if (radiusType.radius(x, y, tgt.x, tgt.y) <= radius) {
550                        tmpAngle = Math.abs(ang - Math.atan2(tgt.y - origin.y, tgt.x - origin.x));
551                        if(tmpAngle > Math.PI) tmpAngle = PI2 - tmpAngle;
552                        if(tmpAngle < allowed)
553                            ctr++;
554                    }
555                }
556                if(ctr > 0)
557                    locs.get(totalTargets - ctr).add(Coord.get(x, y));
558            }
559        }
560        Coord it;
561        for(int t = 0; t < totalTargets - 1; t++)
562        {
563            if(locs.get(t).size() > 0) {
564                int numPoints = locs.get(t).size();
565                for (int i = 0; i < numPoints; i++) {
566                    it = locs.get(t).get(i);
567                    for (int x = Math.max(1, it.x - (int)(radius) / 2); x < it.x + (radius + 1) / 2 && x < dungeon.length - 1; x++) {
568                        BY_POINT:
569                        for (int y = Math.max(1, it.y - (int)(radius) / 2); y <= it.y + (radius - 1) / 2 && y < dungeon[0].length - 1; y++)
570                        {
571                            if(tested[x][y])
572                                continue;
573                            tested[x][y] = true;
574                            ang = Math.atan2(y - origin.y, x - origin.x); // between -pi and pi
575                            for(Coord ex : requiredExclusions) {
576                                if (radiusType.radius(x, y, ex.x, ex.y) <= radius) {
577                                    tmpAngle = Math.abs(ang - Math.atan2(ex.y - origin.y, ex.x - origin.x));
578                                    if(tmpAngle > Math.PI) tmpAngle = PI2 - tmpAngle;
579                                    if(tmpAngle < allowed)
580                                        continue BY_POINT;
581                                }
582                            }
583
584                            ctr = 0;
585                            for(Coord tgt : targets) {
586                                if (radiusType.radius(x, y, tgt.x, tgt.y) <= radius) {
587                                    tmpAngle = Math.abs(ang - Math.atan2(tgt.y - origin.y, tgt.x - origin.x));
588                                    if(tmpAngle > Math.PI) tmpAngle = PI2 - tmpAngle;
589                                    if(tmpAngle < allowed)
590                                        ctr++;
591                                }
592                            }
593                            if(ctr > 0)
594                                locs.get(totalTargets - ctr).add(Coord.get(x, y));
595                        }
596                    }
597                }
598            }
599        }
600        return locs;
601    }
602*/
603    @Override
604    public void setMap(char[][] map) {
605        this.map = DungeonUtility.generateResistances(map);
606        dungeon = map;
607    }
608
609    @Override
610    public OrderedMap<Coord, Double> findArea() {
611        OrderedMap<Coord, Double> r = AreaUtils.arrayToHashMap(fov.calculateFOV(map, origin.x, origin.y, radius,
612                radiusType, angle, span));
613        r.remove(origin);
614        return r;
615    }
616
617}