Making The Escapists Crafting Guide App
Dec 17, 2020
4 minute read

Escapists Craft App

This is a part of a series of my journey into mobile application development. I am learning Flutter and I enjoy using it to make apps. The widgets are just amazing. Right off the bat, one can easily focus on creating apps, and making it beautiful, with very minimum learning curve.

Revisiting Steam games I bought but played only a little, I know I’m not the only one, I scroll through my library and found The Escapists 2. TE2 is a surprisingly fun game. You play as a prisoner making plans to escape. You also do daily routine as an inmate, jobs, and favors included. And of course, find the right tools to help you escape. Some items can be bought from shops while others can be looted, and crafted to make more sophisticated tools.

In game, you can view the crafting menu under your profile. Most of the times, I list down which tools I need to craft and the take note of the intelligence level required. I found an opportunity to write an app for it so it’s easily accessible to me while I go around looting. It will save me time to not drop off items only to realize I need it at a later time, or found it on a desk and ignore it. Eventually, I would memorize all the components. Naah, seriously I just want to create the app also while learning Flutter.

Things the app should do:

  1. List all tools that can be crafted

  2. Searching by entering some keyword matching the name

  3. View details of the tool

That should be fine for now. Remember, I am just saving some papers.

1. All tools

I won’t be building the whole library but will build on top of Gamepedia’s The Escapists Wiki on Crafting. A python script scrapes this page for the info found on the table and crawls on the individual tool’s pages to scrape more information. The output is a JSON file which our app will be reading for data.

Wiki page as input and JSON file as output

What the script will do is to save information about the tool/item, and the requirements, and the intelligence required. The tables are not at all uniform but more info is good. For each tool, the scraper will also fetch some more information on the individual pages.

The script will improve as I get more information or fix issues.

#!/usr/bin/env python3
import requests
from pyquery import PyQuery as pq
from collections import namedtuple
from pathlib import Path
import json
SITE_URL = "https://theescapists.gamepedia.com/Crafting"
BASE_URL = "https://theescapists.gamepedia.com/"
__item_fields__ = [
"name",
"requirements",
"intelligence_1",
"intelligence_2",
"intelligence",
"extra_info",
"url",
"icon_url",
"description",
"defence",
"stats_in_2",
"confiscated",
"image_urls",
"category",
"damage_in_1",
]
Item = namedtuple(
"Item",
__item_fields__,
defaults=(None,) * len(__item_fields__)
)
Requirement = namedtuple(
'Requirement',
[
"name",
"url",
"icon",
]
)
items_store_as_list = []
column_name_mapping = {
'Item': 'name',
'Requirements': 'requirements',
'Intelligence Required in TE1': 'intelligence_1',
'Intelligence Required in TE2': 'intelligence_2',
'Intelligence': 'intelligence',
'Intelligence Required': 'intelligence',
'Extra Info': 'extra_info',
'Defence Given': 'defence',
'Stats in TE2': 'stats_in_2',
'Damage in TE1': 'damage_in_1',
}
def get_details(detail_url):
r = requests.get(detail_url)
page = pq(r.text)
page.make_links_absolute(base_url=BASE_URL)
description = page('#mw-content-text .mw-parser-output > p:first-of-type').text()
if not description.strip():
description = page('#mw-content-text .mw-parser-output > p').text()
confiscated = page('table.infoboxtable tr').filter(lambda i: 'confiscated' in pq(this).text().lower()).eq(0).text()
image_urls = page('table.infoboxtable a.image img').map(lambda i: pq(this).attr('src'))
return {
'description': description,
'confiscated': confiscated,
'image_urls': image_urls,
}
def get_items_from_base_url():
s = requests.Session()
r = s.get(url=SITE_URL)
page = pq(r.text)
page.make_links_absolute(base_url=BASE_URL)
tables = page('.wikitable')
categories = page('.mw-headline').map(lambda: pq(this).text())
del categories[categories.index('The Escapists 1 DLC Prisons')]
for tbl_idx, _ in enumerate(tables):
table = tables.eq(tbl_idx)
column_names = table.find("th").map(lambda i, e: pq(e).text())
try:
category = categories.pop(0)
except IndexError:
category = ''
rows = table.find('tr')
for row_idx, _ in enumerate(table.find('tr')):
if row_idx == 0: # HEADER
continue
row = rows.eq(row_idx)
data = row.find('td')
data_to_save = {}
data_to_save['category'] = category
for col_id, col_name in enumerate(column_names):
col_data = data.eq(col_id)
normalized_column_name = column_name_mapping.get(col_name, col_name)
# Save information from name
if normalized_column_name == 'name':
name = col_data.text().strip()
data_to_save['name'] = name
print(f'Saving "{name}"')
data_to_save['icon_url'] = col_data.find('img').attr('src')
data_to_save['url'] = col_data.find('a').attr('href')
# Save requirements
elif normalized_column_name == 'requirements':
requirements = []
reqs_links = col_data.find('a')
idxs_of_requirements = [
_id
for _id, el in enumerate(reqs_links.map(lambda i, e: pq(e).text()))
if el
]
reqs_images = reqs_links.map(lambda i, e: pq(e).find('img').attr('src'))
for idx in idxs_of_requirements:
try:
req_image = reqs_images.pop(0)
except IndexError:
req_image = None
requirements.append(
Requirement(
name=reqs_links.eq(idx).text(),
url=reqs_links.eq(idx).attr('href'),
icon=req_image,
)._asdict()
)
data_to_save['requirements'] = requirements
# Save intelligence
elif normalized_column_name in ('intelligence_1', 'intelligence_2', 'intelligence'):
data_to_save[normalized_column_name] = col_data.text()
# Save extra info
elif normalized_column_name == 'extra_info':
data_to_save['extra_info'] = col_data.text()
# Save defence
elif normalized_column_name == 'defence':
data_to_save['defence'] = col_data.text()
# Save stats
elif normalized_column_name == 'stats_in_2':
data_to_save['stats_in_2'] = col_data.text()
# Save damage
elif normalized_column_name == 'damage_in_1':
data_to_save['damage_in_1'] = col_data.text()
# Check for details
extra_details = get_details(data_to_save['url'])
data_to_save.update(extra_details)
# Save item to store
items_store_as_list.append(Item(**data_to_save)._asdict()) # As list
if __name__ == '__main__':
get_items_from_base_url()
with open(Path(__file__).parent / 'data.json', 'w+') as f:
f.write(json.dumps(items_store_as_list, indent=2))
view raw script.py hosted with ❤ by GitHub

I can now use this as a data asset.

Using our scraped data in the assets

For this functionality, I will be using two widgets. A ListView and a TextField with a TextEditingController.

The ListView will display the list of items. This is suitable for small as well as long lists as the widget uses lazy loading.

ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
var data = items[index];
return ListTile(
title: Text(data.name),
leading: ConstrainedBox(
constraints: styledBoxConstraints,
child: CachedNetworkImage(imageUrl: data.icon),
)
);
},
);
view raw listview.dart hosted with ❤ by GitHub

Now I need a text field for search, and update the list while the text is being typed.

final searchTextController = TextEditingController();
// A simple search function
void searchItems() {
var name = searchTextController.text;
if (name.isNotEmpty) {
setState(() {
items = allItems
.where((o) => o.name.toLowerCase().contains(name.toLowerCase()))
.toList();
});
} else {
setState(() {
items = List.from(allItems);
});
}
}
// On a state or widget we register a listener function to the controller
// When the controller notifies for changes, this function is invoked
searchTextController.addListener(searchItems);
TextField(
controller: searchTextController,
decoration: InputDecoration(
hintText: "Search",
prefixIcon: Icon(Icons.search),
),
)
// Clean up the controller when the widget is removed from the widget tree
@override
void dispose() {
searchTextController.dispose();
super.dispose();
}

Here’s what the list view looks like.

List view

3. Details

Now the app needs to add a detail screen of the tool where we display the information.

First, update the ListView to navigate to the detail screen when tapped.

ListView.builder(
...,
itemBuilder: (context, index) {
...
return ListTile(
...,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailPage(item: data)
)
);
}
);
}
)
view raw listview_2.dart hosted with ❤ by GitHub

Then make the actual detail page. It’s just a simple SingleChildScrollView with many text field.

class _DetailPageState extends State<DetailPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(name),
),
body: SingleChildScrollView( // Allow vertical overflow and scroll
child: Column(
children: [
ConstrainedBox(
constraints: constraints,
child: CachedNetworkImage(imageUrl: iconUrl),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Render the required components
ListView.builder(
itemCount: requirements.length,
scrollDirection: Axis.horizontal,
shrinkWrap: true,
itemBuilder: (context, index) {
// RequirementItem is just a styled box with image and text
return RequirementItem(data: item.requirements[index]);
}
),
]
),
Text(intelligence),
Text(description),
...,
Text(confiscated),
Text(category),
Text(stats),
],
),
),
}
}
view raw detail.dart hosted with ❤ by GitHub

Above is just a sample and not styled yet. Here’s what the app looks like with some styling.

Detail view

Conclusion

The app is far from complete. Improvements I am considering:

  • A separator between categories on the list view

  • Improve the scraper to parse and get more of the details correctly

  • Improve the data structure to include relationships between components

  • Ability to view a component tree on the detail page using the relationships from the aforementioned improvement

  • Tapping the image of a component from the detail page to also navigate to it’s own detail page

  • Use Firebase and fetch updates to data over the network

  • Transitions, animations, and theming!

On the next part, I will be sharing what I learned while making improvements to The Escapists Craft app. ¡Hasta la próxima!

References


🐋 hello there! If you enjoy this, a "Thank you" is enough.

Or you can also ...

Buy me a teaBuy me a tea